spvdb is a source-level debugger and interpreter for SPIR-V shader modules.
It executes shaders on the CPU, surfacing breakpoints, single-stepping,
local-variable inspection, and call-stack reconstruction from
NonSemantic.Shader.DebugInfo.100 debug information.
Supported execution models: GLCompute, Vertex, Fragment.
- Source-level breakpoints by file and line number, or by SPIR-V result-id
- Single-step by source line (step into / step over / step out) or by instruction
- Inspect local variables, output variables, and descriptor buffer contents
- Reconstruct inlined call stacks via
DebugInlinedAt - Bind descriptor-set buffers as JSON arrays, images from PNG/BMP/JPEG/TGA/HDR
- Set built-in values (
GlobalInvocationId,FragCoord, …) and specialization constants - Interactive REPL (via replxx) and a scriptable batch front-end for use with test frameworks
| Tool | Minimum version |
|---|---|
| CMake | 3.20 |
| C++ compiler with C++20 support | GCC 11 / Clang 14 / MSVC 19.29 |
spirv-as (for assembling .spvasm test fixtures) |
any recent SPIRV-Tools |
git clone --recurse-submodules https://github.com/zangold-nv/spvdb.git
cd spvdbIf you already cloned without --recurse-submodules:
git submodule update --init --recursivecmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build buildThis produces:
build/cli/spvdb— interactive debuggerbuild/tools/spvdb-test/spvdb-test— scriptable test front-endbuild/tests/— Catch2 unit and integration tests
cd build
ctest --output-on-failureAdd spvdb as a subdirectory; the libspvdb static library target is the only
thing exposed to the parent:
add_subdirectory(third_party/spvdb)
target_link_libraries(my_target PRIVATE spvdb)When included this way the CLI, test, and tool targets are suppressed and
SPVDB_SKIP_STB_IMAGE_IMPL is set automatically to avoid duplicate symbol
errors with the parent's own stb_image build.
spvdb [<path.spv> [<entry_point>]]
| Command | Description |
|---|---|
file <path.spv> |
Load a SPIR-V module |
info entries |
List available entry points |
entry <name> |
Select an entry point |
set input <set> <binding> <json> |
Bind a descriptor-set buffer (JSON array) |
set input loc <location> <json> |
Bind a vertex/fragment input by Location decoration |
set builtin <name> <value> |
Set a built-in value (e.g. GlobalInvocationId, FragCoord) |
set specconst <id> <value> |
Override a specialization constant by SpecId |
| Command | Aliases | Description |
|---|---|---|
run |
r |
Start execution from the entry point |
continue |
c |
Continue until the next breakpoint or exit |
step |
s |
Step one source line (steps into calls) |
next |
n |
Step one source line (steps over calls) |
finish |
Run until the current function returns | |
stepi |
si |
Step one SPIR-V instruction |
| Command | Description |
|---|---|
break <file>:<line> |
Set a source breakpoint |
break %<id> |
Set a result-id breakpoint |
info breakpoints |
List all breakpoints |
delete <bp-id> |
Remove a breakpoint |
File paths are matched as suffixes, so break main.slang:42 matches
/any/path/to/main.slang:42.
| Command | Aliases | Description |
|---|---|---|
info locals |
Print local variables at the current location | |
info outputs |
Print output and storage-buffer variables | |
print <name> |
p |
Evaluate and print a single variable |
backtrace |
bt, where |
Print the current call stack |
list [line] |
l |
Show source lines around the current location |
disassemble |
dis |
Show SPIR-V instructions near the current PC |
$ spvdb shader.spv
(spvdb) entry main
(spvdb) set input 0 0 [1, 2, 3, 4]
(spvdb) break shader.slang:27
Breakpoint 1 set at shader.slang:27
(spvdb) run
stopped: Breakpoint 1 at shader.slang:27
(spvdb) info locals
local: a = 7
local: b = 5
(spvdb) next
stopped: Step at shader.slang:28
(spvdb) continue
stopped: EntryFinished
(spvdb) info outputs
output: result = [12]
(spvdb) quit
Add lib/api/spvdb.h (and optionally lib/api/spvdb_session.h for the
session type) to your include path and link against libspvdb.
#include "api/spvdb.h"
// Load a SPIR-V module from a file.
auto mod = spvdb::load_module_from_file("shader.spv");
if (!mod) { /* handle error */ }
// Create a debuggable session for the "main" entry point.
auto sess = spvdb::create_session(*mod, "main", {});
if (!sess) { /* handle error */ }
// Bind a storage-buffer descriptor: set=0, binding=0, data=[7, 5, 0].
spvdb::set_descriptor_json(*sess, 0, 0, "[7, 5, 0]");
// Set a breakpoint and run.
spvdb::set_breakpoint(*sess, "shader.slang", 27);
auto reason = spvdb::run(*sess); // StopReason::Breakpoint
// Inspect locals.
for (auto& [name, val] : spvdb::local_variables(*sess))
std::cout << name << " = " << spvdb::value_to_string(val) << "\n";
// Continue to completion.
reason = spvdb::run(*sess); // StopReason::EntryFinished
// Read back the output buffer.
auto result = spvdb::read_descriptor(*sess, 0, 2);| Type | Description |
|---|---|
spvdb::Module |
Loaded SPIR-V module (immutable, shareable) |
spvdb::Session |
Mutable execution state for one invocation |
spvdb::SessionOptions |
Memory initialisation (init_pattern, init_value) |
spvdb::Value |
Tagged union: Bool, Int32/64, UInt32/64, Float32/64, Composite, Pointer |
spvdb::StopReason |
Breakpoint, Step, EntryReached, EntryFinished, Panic |
spvdb::SourceLocation |
file, line, column |
spvdb::Result<T> |
Success/error wrapper; check with if (r) or r.error() |
// Module loading
Result<Module> load_module(std::span<const uint32_t> words);
Result<Module> load_module_from_file(const std::string& path);
std::vector<EntryPoint> list_entry_points(const Module&);
// Session creation & setup
Result<Session> create_session(const Module&, const std::string& entry,
const SessionOptions& = {});
void set_spec_constant(Session&, uint32_t spec_id, uint32_t value);
void set_descriptor(Session&, uint32_t set, uint32_t binding,
std::vector<uint32_t> data);
void set_descriptor_json(Session&, uint32_t set, uint32_t binding,
const std::string& json);
void set_image(Session&, uint32_t set, uint32_t binding,
const std::string& image_path);
void set_input_location(Session&, uint32_t location, Value value);
void set_builtin(Session&, uint32_t builtin_id, Value value);
// Breakpoints
uint32_t set_breakpoint(Session&, const std::string& file, uint32_t line);
uint32_t set_breakpoint_at_id(Session&, uint32_t result_id);
void remove_breakpoint(Session&, uint32_t bp_id);
// Execution
StopReason run(Session&);
StopReason step(Session&);
StopReason step_over(Session&);
StopReason step_out(Session&);
StopReason step_instruction(Session&);
std::string panic_message(const Session&);
// Inspection
SourceLocation current_location(const Session&);
std::vector<std::pair<std::string,Value>> local_variables(const Session&);
std::vector<std::pair<std::string,Value>> output_variables(const Session&);
Result<Value> evaluate_variable(const Session&, const std::string& name);
Result<Value> read_descriptor(const Session&, uint32_t set, uint32_t binding);
std::vector<StackFrame> backtrace(const Session&);spvdb/
├── lib/
│ ├── api/ Public C++ API (spvdb.h, spvdb_session.h)
│ ├── core/ Value and Result types
│ ├── parser/ SPIR-V binary parser
│ ├── module/ Module and instruction representation
│ └── interpreter/ Execution engine + GLSL.std.450 + image sampler
├── cli/ Interactive REPL (replxx-based)
├── tests/
│ ├── unit/ Catch2 unit tests
│ ├── spirv/ Hand-written SPIR-V assembly test fixtures
│ └── integration/ Integration tests
└── third_party/
├── SPIRV-Headers/ (submodule) SPIR-V opcode headers
├── Catch2/ (submodule) unit test framework
├── nlohmann_json/ (submodule) JSON parsing for descriptors
├── replxx/ (submodule) readline-style REPL
└── stb/ stb_image for image descriptor loading
See LICENSE for details.