A C++ library and command-line tool for decoding GFXReconstruct .gfxr Vulkan capture files into SQLite databases.
The resulting database exposes Vulkan API events, command buffers, descriptors, resources, and per-frame state as queryable SQL tables, making it convenient to analyze and visualize Vulkan workloads.
| Target | Type | Purpose |
|---|---|---|
gfxr-sqlite-library |
static lib | Core decoder. Embeds SQLite. Links against gfxreconstruct's decode/format/util. |
gfxr-sqlite |
executable | CLI wrapper that converts a single .gfxr file to a .sqlite3 file on disk. |
gfxr-sqlite-test |
executable | Native C++ test for the library. |
- CMake 3.15+
- A C++20 compiler (MSVC 2019+, Clang 12+, or GCC 11+)
- Python 3.8+ (used by gfxreconstruct's build scripts and the SQLite-consumer code generators)
- Platform libraries: zlib, lz4 (zstd optional)
- Linux:
apt install zlib1g-dev liblz4-dev libzstd-dev - macOS: bundled in
external/gfxreconstruct/external/precompiled/macos - Windows: bundled in
external/gfxreconstruct/external/precompiled/win64
- Linux:
The repo ships with a top-level build script that handles submodule init, builds gfxreconstruct, and builds gfxr-sqlite end-to-end:
git clone https://github.com/sarc-acl/gfxr-sqlite.git
cd gfxr-sqlite
python3 scripts/build.pyThat's it — the script:
- Initializes the nested
external/gfxreconstructsubmodule (overriding theupdate = noneset in.gitmodules). - Builds gfxreconstruct via scripts/build_dependencies.py with the same flags Sokatoa uses (no OpenXR, no D3D12, hidden symbol visibility).
- Configures and builds the gfxr-sqlite CMake project.
Options:
python3 scripts/build.py debug # debug build (default is release)
python3 scripts/build.py --clean # clean rebuild (deps + project)
python3 scripts/build.py --skip-deps # skip the gfxreconstruct rebuild
python3 scripts/build.py -j 8 # limit parallel compile jobs to 8
# (default 0 = all cores)If you only need to (re-)build the gfxreconstruct dependency:
python3 scripts/build_dependencies.py [release|debug] [--clean] [-j N]Artifacts land under out/build/ (or out/dbuild/ for debug):
gfxr-sqlite(orRelease/gfxr-sqlite.exeon MSVC) — the CLIlibgfxr-sqlite-library.a(orgfxr-sqlite-library.lib) — the static librarygfxr-sqlite-test— the native test executable
If you'd rather drive each step yourself (or you already have a built gfxreconstruct elsewhere on disk), the manual flow is:
# 1. Init the nested gfxreconstruct submodule (the `-c` flag overrides update=none).
git -c submodule.external/gfxreconstruct.update=checkout \
submodule update --init --recursive external/gfxreconstruct
# 2. Build gfxreconstruct.
( cd external/gfxreconstruct && \
python3 scripts/build.py -c release \
--skip-check-code-style --skip-tests --skip-d3d12-support \
--cmake-extra=GFXRECON_ENABLE_OPENXR=OFF \
--cmake-extra=CMAKE_CXX_VISIBILITY_PRESET=hidden )
# 3. Configure + build gfxr-sqlite.
cmake -S . -B out/build -DCMAKE_BUILD_TYPE=Release
cmake --build out/build --config Release --parallelTo reuse an existing gfxreconstruct checkout (instead of the nested submodule), pass -DGFXR_SQLITE_GFXR_PATH=/path/to/gfxreconstruct to the cmake configure step and skip step 1+2.
The CLI accepts either a .gfxr trace or a .apidump file in place of the capture argument; the output database is the same either way.
gfxr-sqlite path/to/capture.gfxr
# produces path/to/capture.gfxr.sqlite3
gfxr-sqlite -o my_capture.sqlite3 path/to/capture.gfxr
# writes to my_capture.sqlite3The output .sqlite3 file can be opened with any SQLite client. Schema documentation lives in the headers under src/gfxr-db/includes/.
Alongside .gfxr traces, gfxr-sqlite can build the same database from the JSON output of the LunarG VulkanTools api_dump layer. This records a readable JSON transcript of the Vulkan calls rather than a replayable binary trace, and stands in for gfxreconstruct whenever that layer is not in use.
Because it leans on behaviour the stock layer does not provide — passing the output file name down as a layer setting and some additional settings used to dump setup information such as Vulkan object creation/destruction and command buffer recording prior to the start of capture — this path needs the api_dump layer from the fork at https://github.com/sarc-acl/VulkanTools. The consumer expects the file to carry the .apidump extension. The layer itself writes .json, so implementing applications should rename its output to .apidump before conversion; a file named .json is still accepted, since detection falls back to a content sniff when the extension is not .apidump.
The database is only faithful when the layer runs with these options set at capture time:
| Option | Value | Why |
|---|---|---|
output_format |
json |
The consumer parses the JSON spelling. |
file |
true |
Write to a file rather than stdout. |
show_types |
true |
Argument and member types, which the reader resolves. |
show_shader |
true |
Expands the SPIR-V contents of shader modules. |
detailed |
true |
Expands struct members; without it calls carry no arguments. |
show_thread_and_frame |
true |
Records the frame and thread the reader walks. |
indent_size |
0 |
Cosmetic only — the reader is whitespace-insensitive — but keeps the file small. |
show_enum_value |
true |
Emits the numeric value with the name, so an enumerant newer than the reader's headers still resolves. |
float_precision |
9 |
std::numeric_limits<float>::max_digits10, so floats round trip. |
always_dump_setup |
true |
Dumps the setup calls objects depend on, not just the capture window. |
Implementing applications will need to configure the output file name by passing the path down as a layer setting because the path exceeds Android's property length limit. A capture started and stopped by hand (manual capture mode) is bounded by capture_trigger, which puts the layer into triggered mode and makes it ignore any frame range.
The .apidump file is passed in exactly the slot a .gfxr would occupy:
gfxr-sqlite path/to/capture.apidump
# produces path/to/capture.apidump.sqlite3
gfxr-sqlite -o my_capture.sqlite3 path/to/capture.apidump
# writes to my_capture.sqlite3The database goes through the identical VulkanSqliteConsumer, so the schema and the example queries are unchanged. What differs is what a .gfxr carries in its own binary structure that a JSON dump does not, which ApiDumpProcessor synthesises in its place:
- Block indices. A
.gfxrnumbers every record with a block index, stored asapiEvents.id. The dump has none, so calls are numbered in document order instead. - Frame boundaries. A
.gfxrends each frame with a frame-marker block. Here they are inserted as synthesisedFrameMarkermetadata commands, one per frame object. The dump counts frames from zero while the schema counts from one (reconciled internally); frame numbers the dump skipped are filled in and out-of-order frames are dropped. - Handle ids. The consumer assigns handle ids in a globally incrementing manner, exactly as gfxr does, mapping each runtime address (keyed by object type) onto the next free id. An address recycled after a destroy starts a fresh id, and a handle returned by a getter resolves to the id it was first seen with.
Because a dump only contains what the layer expanded, some data a .gfxr would carry may be absent:
- Pointers and arrays the layer did not expand are recorded as present-but-omitted, matching gfxreconstruct's own handling of that case, rather than as contents.
- Commands gfxreconstruct does not decode are skipped, and unrecognised enum names, structure types or scalars are counted. The conversion prints a one-line summary of any such gaps.
- There is no admin data: no timestamps, no driver or application metadata packets, and no D3D12/OpenXR detection — an api dump is always treated as Vulkan.
- A capture cut short by the application being killed is the common case. Everything parsed before the truncation is kept, and the early end is reported as a warning rather than discarding the database.
The schema captures Vulkan API events under apiEvents, with each event's Vulkan function name resolved through the functionNames lookup table. Higher-level structure tables (frames, queueSubmits, commandBufferRecordings, commandBufferCommands, etc.) reference apiEvents by id. The patterns below illustrate the most common joins. More advanced documentation on the schema and example queries can be found at https://sarc-acl.github.io/gfxr-sqlite/
SELECT
id AS frameId,
beginApiEventId,
endApiEventId
FROM frames
ORDER BY id;queueSubmits.apiEventId points to the vkQueueSubmit (or vkQueueSubmit2) call itself; join apiEvents + functionNames to recover the function name.
SELECT
qs.id AS queueSubmitId,
qs.frameId,
qs.queueId,
q.familyIndex AS queueFamily,
fn.name AS submitFunction
FROM queueSubmits qs
JOIN queues q ON q.id = qs.queueId
JOIN apiEvents ae ON ae.id = qs.apiEventId
JOIN functionNames fn ON fn.id = ae.functionNameId
WHERE qs.frameId = 42
ORDER BY qs.id;A queue submit fans out through queueSubmitBatches → queueSubmitBuffers → commandBufferRecordings. Each recording belongs to one commandBuffers row (the long-lived handle).
SELECT
qs.id AS queueSubmitId,
qsb.idx AS batchIdx,
qsbuf.idx AS bufferIdx,
cbr.id AS recordingId,
cb.id AS commandBufferId,
printf('0x%x', cb.handle) AS commandBufferHandle
FROM queueSubmits qs
JOIN queueSubmitBatches qsb ON qsb.queueSubmitId = qs.id
JOIN queueSubmitBuffers qsbuf ON qsbuf.queueSubmitBatchId = qsb.id
JOIN commandBufferRecordings cbr ON cbr.id = qsbuf.commandBufferRecordingId
JOIN commandBuffers cb ON cb.id = cbr.commandBufferId
WHERE qs.frameId = 42
ORDER BY qs.id, qsb.idx, qsbuf.idx;Every vkCmd* call inside a recording lives in commandBufferCommands, pointing at the underlying apiEvents row. Join through functionNames to get readable names. Commands are returned in API-event order (which matches recording order).
SELECT
cbc.apiEventId,
fn.name AS functionName
FROM commandBufferCommands cbc
JOIN apiEvents ae ON ae.id = cbc.apiEventId
JOIN functionNames fn ON fn.id = ae.functionNameId
WHERE cbc.commandBufferRecordingId = 7
ORDER BY cbc.apiEventId;The full hierarchy in one query. Useful for sanity-checking the structure of a particular frame.
SELECT
qs.frameId,
qs.id AS queueSubmitId,
cbr.id AS recordingId,
cbc.apiEventId,
fn.name AS functionName
FROM queueSubmits qs
JOIN queueSubmitBatches qsb ON qsb.queueSubmitId = qs.id
JOIN queueSubmitBuffers qsbuf ON qsbuf.queueSubmitBatchId = qsb.id
JOIN commandBufferRecordings cbr ON cbr.id = qsbuf.commandBufferRecordingId
JOIN commandBufferCommands cbc ON cbc.commandBufferRecordingId = cbr.id
JOIN apiEvents ae ON ae.id = cbc.apiEventId
JOIN functionNames fn ON fn.id = ae.functionNameId
WHERE qs.frameId = 42
ORDER BY qs.id, cbr.id, cbc.apiEventId;apiEventArguments holds the argument list (one row per arg, with idx for order) for every recorded call.
SELECT idx, name, type, value
FROM apiEventArguments
WHERE apiEventId = 12345
ORDER BY idx;gfxr-sqlite's CMake supports being consumed via add_subdirectory(). The relevant cache variables:
| Variable | Default | Purpose |
|---|---|---|
GFXR_SQLITE_GFXR_PATH |
<this-dir>/external/gfxreconstruct |
Path to a gfxreconstruct source tree (root). Override to reuse a parent project's copy. |
GFXR_SQLITE_BUILD_EXECUTABLE |
ON standalone / OFF as subdirectory |
Build the gfxr-sqlite CLI executable. |
GFXR_SQLITE_BUILD_TEST |
ON standalone / OFF as subdirectory |
Build the gfxr-sqlite-test test executable. |
# In your parent CMakeLists.txt, before add_subdirectory:
set(GFXR_SQLITE_GFXR_PATH "${CMAKE_SOURCE_DIR}/third_party/gfxreconstruct"
CACHE PATH "Path to gfxreconstruct sources" FORCE)
set(GFXR_SQLITE_BUILD_EXECUTABLE OFF CACHE BOOL "" FORCE)
set(GFXR_SQLITE_BUILD_TEST OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/gfxr-sqlite)
add_library(my_consumer ...)
target_link_libraries(my_consumer PRIVATE gfxr-sqlite-library)gfxr-sqlite-library exports its public include directories (the gfxr-db/includes headers, plus gfxreconstruct framework + Vulkan-Headers paths). Consumers get them automatically by linking.
The headers you'll typically include:
#include "create_db_helper.h" // gfxrSqlite::CreateDBHelper — drives the decode
#include "gfxr_db_manager.h" // wraps an open SQLite connection
#include "memdb_ext.h" // optional in-memory VFS extension
#include "sqlite3.h" // embedded SQLite amalgamationA minimal usage pattern (see src/tools/gfxr_sqlite_main.cpp for a complete example):
#include "create_db_helper.h"
#include "memdb_ext.h"
#include "sqlite3.h"
sqlite3MemdbExtInit();
auto errorHandler = [](const std::string& err) { std::cerr << err << '\n'; };
gfxrSqlite::CreateDBHelper helper{
/*gfxrPath=*/"capture.gfxr",
/*dbPath=*/"file:/memdb0?vfs=memdb_ext",
/*logFilename=*/std::nullopt,
/*enforceForeignKeys=*/false,
errorHandler};
helper.createDatabase();
sqlite3* db = helper.getDB();
// ... query db ...A subset of the decoder is generated from gfxreconstruct's Vulkan registry. The generators live under src/gfxr-db/internal/generated/. To regenerate (only needed after a Vulkan registry update or generator change):
python3 src/gfxr-db/internal/generated/generate_vulkan.py
# uses external/gfxreconstruct by default; override with --gfxr-dir <path>The same script regenerates the api dump ingest path alongside the gfxr consumer: the enum_from_string table used to resolve enum and flag names, and the struct_from_apidump, pnext_from_apidump and apidump_dispatch generators that turn a dump's JSON into the parameter buffer. Like the gfxr tables, these are generated so they track Vulkan-Headers, and only the api dump dispatch and enum tables are specific to this path.
Apache-2.0. See individual source files for copyright headers.