You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add export support for shader constant buffers and VS input meshes #224
rdc-cli currently has useful capture inspection and mesh export commands, but two common debugging workflows still require custom RenderDoc Python scripts:
Exporting shader constant buffer contents for a specific event and stage.
For example, dumping VS cbuffer1 at a given EID, including reflected variables and raw bytes.
Exporting the VS input mesh / input assembler geometry.
The current rdc mesh command exports post-transform mesh data (vs-out). That is useful, but when debugging original model geometry, vertex buffer layout, skinning inputs, or index buffer ranges, users often need the mesh before the vertex shader runs.
In practice, exporting vs-out can look like a projected or flattened surface, while the expected debugging target is the original VS input mesh.
Proposed solution
Add first-class CLI support for both workflows.
Constant buffer export
Add a command such as:
rdc cbuffer 1084 --stage vs --binding 1 --json
rdc cbuffer 1084 --stage vs --binding 1 --raw -o cbuffer1.bin
### Alternatives considered
This can currently be done with custom rdc script Python code that calls RenderDoc APIs directly, reads the constant buffers, decodes vertex/index buffers, resolves indirect draw arguments, and writes an OBJ manually.
That works, but it is fragile and not convenient for repeated capture analysis.
Both have full unit + Vulkan integration test coverage and pass CI (ruff/mypy strict, ~92.7% coverage).
@Misaka-Mikoto-Tech — the dev environment is Linux, where RenderDoc replay is Vulkan-only, so I could not verify the D3D12 path (constant-buffer register spaces / root constants, D3D12 input-assembler layout) against a real capture. Could you test both against your D3D12 captures and report back?
Useful checks:
rdc cbuffer <EID> --stage vs --binding 1 --json — do the reflected variable names/values match what you expect for that cbuffer? --raw -o cb.bin — is the byte size/content correct?
rdc mesh <EID> --stage vs-in -o vsin.obj — is this the original (pre-VS) model geometry rather than the projected vs-out surface?
If cbuffer --raw errors on a buffer-backed cbuffer, please paste the error and, if possible, the output of rdc cbuffer <EID> --stage vs --binding 1 --json so I can check the reflection lookup against D3D12 register-space semantics. Thanks!
Update — a deeper self-review (cross-checked against RenderDoc's official decode_mesh.py reference) found real correctness bugs that the Vulkan-mocked tests could not catch. Both PRs got hardening follow-up commits:
baseVertex is now applied to indices (index + mesh.baseVertex, per RenderDoc reference) — without this, indexed D3D12 draws with a non-zero BaseVertexLocation produced silently scrambled triangles.
Position is now read at vertexByteOffset within the vertex stride (not offset 0) — fixes wrong positions when POSITION isn't the first IA element.
Non-triangle/patch topology now emits a clear stderr warning instead of a silently empty OBJ.
Known limitation (now documented): vs-in exports the position attribute + triangle connectivity only, not full per-attribute IA.
Null/unbound cbuffer descriptor → clean -32001 "cbuffer not bound at this draw" (was: silent 0-byte file reported as success).
Descriptor byteSize == 0 (common for D3D12 root CBVs) → falls back to the reflected block size; never passes size 0 to GetBufferData (which would dump the entire upload heap as if it were the cbuffer). Clean error if size is truly unknown.
--set/--binding help now states the D3D12 meaning (register space / shader register bN).
@Misaka-Mikoto-Tech — the highest-value checks on real D3D12 captures (a non-empty result is not sufficient — the prior failure modes were plausible-looking-but-wrong output):
vs-in: pick an indexed draw whose VS input is multi-attribute (e.g. POSITION+NORMAL+TEXCOORD, stride ≥ 32) and ideally has a non-zero BaseVertexLocation. rdc mesh <eid> --stage vs-in -o vsin.obj, open it next to RenderDoc's Mesh Viewer Input tab — do XYZ positions and triangle connectivity actually match?
cbuffer: pick a cbuffer at a known register(bN, spaceM) (ideally also a root CBV and a root-constants block). For each: rdc cbuffer <eid> --stage ps --set <M> --binding <N> --raw -o cb.bin and the decoded form (no --raw). Report: (a) does --set/--binding resolve the intended cbuffer? (b) cb.bin size vs the decoded variable footprint — equal, or 256-aligned/padded? (c) root CBV: is cb.bin the cbuffer or megabytes of heap? (d) root constants: clean -32602 not buffer-backed?
(b)/(c) and the vs-in position/connectivity check are the load-bearing answers I can't determine without real D3D12 hardware.
Both features are merged to master: rdc mesh --stage vs-in (squash 072cc49, #227) and rdc cbuffer (squash f23143a, #228). A deeper self-review added hardening for several silent-wrong-output cases (baseVertex/vertexByteOffset for vs-in; null-resource and byteSize==0 guards for cbuffer). @Misaka-Mikoto-Tech the D3D12-specific paths couldn't be verified on the Linux dev box — the verification asks above still stand (vs-in: compare positions+connectivity against RenderDoc's Mesh Viewer on a multi-attribute, non-zero-baseVertex draw; cbuffer: raw size vs decoded footprint, and root-CBV must not dump the whole heap). Please report back when you can; if anything's off we'll fix-forward. Leaving this issue open until you've had a chance to verify.
Thanks for implementing both workflows. I tested the current rdc 0.6.0 on Windows with real D3D12 and D3D11 captures. Short version: both features are partially implemented, but there are still correctness problems on Windows captures.
Environment
Windows
RenderDoc replay module: 1.41
rdc --version: 0.6.0
GPU: NVIDIA GeForce RTX 3090
Tested APIs: D3D12 and D3D11
1. rdc cbuffer
D3D12: decoded JSON resolves the intended buffer, but uint variables are decoded as floats
Test capture: D3D12
EID: 7459
Command:
rdc cbuffer 7459 --stage ps --binding 1 --json
The command resolves the intended ps cbuffer1, but the reflected variable has type: "4" and is still emitted from f32v.
So the decoded output is bit-identical but type-wrong. The implementation appears to always prefer f32v; it should choose u32v, s32v, f32v, etc. based on the reflected variable type.
ATTRIBUTE0 first vertex:
[590.5662841796875, -24.96360969543457, 341.7481384277344]
So on this D3D12 capture, rdc mesh --stage vs-in does not export the original input-assembler position geometry. It exports the same post-VS position data as vs-out.
D3D11: vs-in fails even though RenderDoc GUI Mesh Viewer Input tab has valid data
Test capture: D3D11
EID: 1130
Draw type:
DrawIndexedInstancedIndirect(<170412, 1>)
Command:
rdc mesh 1130 --stage vs-in --json
Result:
{"error": {"message": "no PostVS data at this event"}}
However, RenderDoc GUI Mesh Viewer Input tab shows valid IA data. Direct IA decode through GetVertexInputs() + GetVBuffers() also succeeds.
Attributes:
POSITION
NORMAL
COLOR
TEXCOORD0
TEXCOORD1
TEXCOORD2
TEXCOORD3
TANGENT
For mesh --stage vs-in, relying only on controller.GetPostVSData(..., MeshDataStage.VSIn) is not sufficient for these Windows captures.
A robust implementation probably needs a fallback that mirrors what RenderDoc GUI Mesh Viewer Input can do:
Read IA layout from pipe.GetVertexInputs().
Read bound vertex buffers from pipe.GetVBuffers().
Read index buffer from pipe.GetIBuffer().
Decode the POSITION input semantic, respecting:
vertexBuffer
byteOffset
vertex buffer byteOffset
stride
index buffer format/offset
base vertex
per-instance inputs where relevant
Use the decoded POSITION stream + decoded index buffer for the OBJ.
For cbuffer --json, choose the value vector according to reflected variable type, e.g. u32v for uint variables instead of always using f32v.
For cbuffer --raw, the raw export path should include stage, or otherwise resolve the exact stage-specific constant block before exporting. The current VFS path /draws/<eid>/cbuffer/<set>/<binding>/data is both missing from the populated VFS tree and ambiguous when multiple stages bind the same register.
I think there are two different use cases that should be separated:
Full VS-IN / IA attribute export
mesh --stage vs-in currently seems to export only one selected position-like attribute plus indices/faces. That is useful for OBJ-style geometry, but it is not equivalent to RenderDoc's Mesh Viewer Input tab.
VS input is not always a conventional position-backed mesh. For example, UI/sprite/procedural meshes may store only indices, tile IDs, corner IDs, or instance data in VS inputs, and reconstruct the real position in the vertex shader from constants/textures. In those cases there may be no POSITION attribute at all.
So a full VS-IN export should dump all input attributes per vertex/index and should not require any attribute to be named or inferred as POSITION.
OBJ export from VS-IN
For OBJ export, selecting a position attribute is still necessary because OBJ needs vertex positions. But the current name-based lookup is too fragile.
A better position selection strategy would be:
Prefer an explicit semantic if present: POSITION, SV_Position, or maybe pos*.
Otherwise ignore per-instance attributes and infer from vertex-rate attributes.
Prefer float/vector formats, especially float3 or float4.
Prefer slot 0 and lower byte offsets.
If there are multiple plausible candidates, either choose the highest-ranked candidate with a warning or require an explicit user override.
Provide an override such as --position-attribute ATTRIBUTE0, --position-slot 0 --position-offset 0, or --position-index 0.
Include the selected attribute in JSON/metadata output, e.g. position_attribute, position_source: semantic|heuristic|user.
For the D3D12 NTE capture I tested, the VS-IN attributes are ATTRIBUTE0 and ATTRIBUTE13; ATTRIBUTE0 is the actual position stream, and ATTRIBUTE13 is instance-rate uint data. A robust OBJ fallback should select ATTRIBUTE0 by heuristic or by user override, not fail just because no input semantic is named POSITION.
Unreal/D3D12 captures often expose inputs as ATTRIBUTE0, ATTRIBUTE1, etc., where the names are not semantically meaningful.
I reviewed the latest commits, especially 01527a8 and 73d1c65. The fixes look reasonable for the intended OBJ export workflow:
cbuffer --json now appears to handle typed values correctly instead of decoding uint data as floats.
cbuffer --raw now includes the shader stage in the VFS path, which should avoid the previous ambiguous/missing path issue.
mesh --stage vs-in now has a much better fallback path for OBJ export: semantic lookup first, then vertex-rate float/vector heuristic, with explicit overrides such as --position-attribute, --position-index, --position-slot, and --position-offset.
This should cover my D3D12/Unreal-style case where the VS input names are ATTRIBUTE0, ATTRIBUTE13, etc.; in that capture, ATTRIBUTE0 is the position stream and ATTRIBUTE13 is instance-rate uint data, so the new heuristic/override support should work.
One clarification: this implements VS-IN OBJ export, not a full RenderDoc Mesh Viewer Input-tab export of all IA attributes. That distinction is fine for this issue; if I need full per-attribute VS-IN dumps later, I can handle that with a custom RenderDoc Python script.
Thanks again for being responsive and for hardening the D3D paths.
Closing based on the latest maintainer/user feedback. The D3D cbuffer fixes and VS-IN OBJ export hardening are delivered via #265 and #266: typed cbuffer values, stage-qualified raw cbuffer paths, IA fallback for VS-IN OBJ export, robust position selection, explicit overrides, and position metadata.
Scope note: this closes the requested cbuffer + VS-IN OBJ export workflow. A full RenderDoc Mesh Viewer Input-tab / complete IA attribute-table export remains a separate feature if needed later, and should stay separate from OBJ export.
Activity
BANANASJIM commentedon May 15, 2026
Both workflows are implemented as two independent PRs:
rdc mesh --stage vs-in: exports the VS input (input-assembler) geometry — original vertex/index buffers before the vertex shader runs.rdc cbuffer <EID> --stage <s> --binding <n> [--json | --raw -o file.bin]: dumps a shader constant buffer, decoded (reflected variables) or raw bytes.Both have full unit + Vulkan integration test coverage and pass CI (ruff/mypy strict, ~92.7% coverage).
@Misaka-Mikoto-Tech — the dev environment is Linux, where RenderDoc replay is Vulkan-only, so I could not verify the D3D12 path (constant-buffer register spaces / root constants, D3D12 input-assembler layout) against a real capture. Could you test both against your D3D12 captures and report back?
Useful checks:
rdc cbuffer <EID> --stage vs --binding 1 --json— do the reflected variable names/values match what you expect for that cbuffer?--raw -o cb.bin— is the byte size/content correct?rdc mesh <EID> --stage vs-in -o vsin.obj— is this the original (pre-VS) model geometry rather than the projectedvs-outsurface?If
cbuffer --rawerrors on a buffer-backed cbuffer, please paste the error and, if possible, the output ofrdc cbuffer <EID> --stage vs --binding 1 --jsonso I can check the reflection lookup against D3D12 register-space semantics. Thanks!fix(mesh): apply baseVertex + position offset for vs-in; warn on non-…
fix(cbuffer): guard null resource and byteSize==0; clarify D3D12 bind…
BANANASJIM commentedon May 15, 2026
Update — a deeper self-review (cross-checked against RenderDoc's official
decode_mesh.pyreference) found real correctness bugs that the Vulkan-mocked tests could not catch. Both PRs got hardening follow-up commits:#227 (vs-in mesh) — fixed:
baseVertexis now applied to indices (index + mesh.baseVertex, per RenderDoc reference) — without this, indexed D3D12 draws with a non-zeroBaseVertexLocationproduced silently scrambled triangles.vertexByteOffsetwithin the vertex stride (not offset 0) — fixes wrong positions when POSITION isn't the first IA element.#228 (cbuffer) — fixed:
-32001 "cbuffer not bound at this draw"(was: silent 0-byte file reported as success).byteSize == 0(common for D3D12 root CBVs) → falls back to the reflected block size; never passes size 0 toGetBufferData(which would dump the entire upload heap as if it were the cbuffer). Clean error if size is truly unknown.--set/--bindinghelp now states the D3D12 meaning (register space / shader registerbN).@Misaka-Mikoto-Tech — the highest-value checks on real D3D12 captures (a non-empty result is not sufficient — the prior failure modes were plausible-looking-but-wrong output):
POSITION+NORMAL+TEXCOORD, stride ≥ 32) and ideally has a non-zeroBaseVertexLocation.rdc mesh <eid> --stage vs-in -o vsin.obj, open it next to RenderDoc's Mesh Viewer Input tab — do XYZ positions and triangle connectivity actually match?register(bN, spaceM)(ideally also a root CBV and a root-constants block). For each:rdc cbuffer <eid> --stage ps --set <M> --binding <N> --raw -o cb.binand the decoded form (no--raw). Report: (a) does--set/--bindingresolve the intended cbuffer? (b)cb.binsize vs the decoded variable footprint — equal, or 256-aligned/padded? (c) root CBV: iscb.binthe cbuffer or megabytes of heap? (d) root constants: clean-32602 not buffer-backed?(b)/(c) and the vs-in position/connectivity check are the load-bearing answers I can't determine without real D3D12 hardware.
feat(mesh): add vs-in stage for input-assembler mesh export (#224) (#227
feat(cbuffer): add rdc cbuffer command and cbuffer_raw export (#224) (#…
BANANASJIM commentedon May 16, 2026
Both features are merged to
master:rdc mesh --stage vs-in(squash072cc49, #227) andrdc cbuffer(squashf23143a, #228). A deeper self-review added hardening for several silent-wrong-output cases (baseVertex/vertexByteOffset for vs-in; null-resource and byteSize==0 guards for cbuffer). @Misaka-Mikoto-Tech the D3D12-specific paths couldn't be verified on the Linux dev box — the verification asks above still stand (vs-in: compare positions+connectivity against RenderDoc's Mesh Viewer on a multi-attribute, non-zero-baseVertex draw; cbuffer: raw size vs decoded footprint, and root-CBV must not dump the whole heap). Please report back when you can; if anything's off we'll fix-forward. Leaving this issue open until you've had a chance to verify.BANANASJIM commentedon Jun 23, 2026
Both requested workflows are implemented and merged:
rdc mesh <eid> --stage vs-inexports the input-assembler geometry before the vertex shader runs (feat(mesh): add vs-in stage for input-assembler mesh export (#224) #227).rdc cbuffer <eid> --stage <stage> --binding <n> [--json | --raw -o cbuffer.bin]dumps reflected variables and/or raw bytes (feat(cbuffer): add rdc cbuffer command and cbuffer_raw export (#224) #228).Both shipped against this issue and were archived in #229. Closing as delivered.
BANANASJIM commentedon Jun 23, 2026
Reopening — the close was premature on my part. The functionality is delivered (#227/#228), but leaving the close decision to the maintainer.
Misaka-Mikoto-Tech commentedon Jul 5, 2026
Thanks for implementing both workflows. I tested the current
rdc 0.6.0on Windows with real D3D12 and D3D11 captures. Short version: both features are partially implemented, but there are still correctness problems on Windows captures.Environment
rdc --version: 0.6.01.
rdc cbufferD3D12: decoded JSON resolves the intended buffer, but uint variables are decoded as floats
Test capture: D3D12
EID:
7459Command:
The command resolves the intended
ps cbuffer1, but the reflected variable hastype: "4"and is still emitted fromf32v.CLI output for first variable:
{ "name": "unknown.unknown[0]", "type": "4", "value": [1.0, 1.0, 1024.0, 1024.0] }Direct RenderDoc API inspection of the same variable shows it should be read from
value.u32v:{ "name": "unknown.unknown[0]", "type": 4, "value": [1065353216, 1065353216, 1149239296, 1149239296], "chosen_vector": "u32v" }So the decoded output is bit-identical but type-wrong. The implementation appears to always prefer
f32v; it should chooseu32v,s32v,f32v, etc. based on the reflected variable type.D3D12:
--raw -ofailsCommand:
Result:
Same failure for
--stage vs.I also noticed the raw VFS path does not include shader stage:
This is ambiguous on D3D12 when, for example,
vs b1andps b1both exist and point to different resources.For this draw, direct RenderDoc API extraction shows:
So stage is necessary to identify the intended cbuffer.
D3D11: decoded JSON works, but
--raw -ostill failsTest capture: D3D11
EID:
1130Commands:
These decoded outputs look correct and match direct RenderDoc API typed values.
Example:
But raw export still fails:
Result:
Direct RenderDoc API extraction works:
So
cbuffer --rawappears to be broken at the CLI/VFS path layer, not because RenderDoc cannot provide the data.2.
rdc mesh --stage vs-inD3D12:
vs-inexports the same data asvs-outTest capture: D3D12
EID:
7459Commands:
The two outputs have identical
vertices,indices, andfaces.vs-infirst vertex fromrdc mesh:But the actual IA input layout is:
Direct IA vertex-buffer decode gives:
So on this D3D12 capture,
rdc mesh --stage vs-indoes not export the original input-assembler position geometry. It exports the same post-VS position data asvs-out.D3D11:
vs-infails even though RenderDoc GUI Mesh Viewer Input tab has valid dataTest capture: D3D11
EID:
1130Draw type:
Command:
Result:
However, RenderDoc GUI Mesh Viewer Input tab shows valid IA data. Direct IA decode through
GetVertexInputs()+GetVBuffers()also succeeds.Attributes:
Example decoded IA data:
I also tested a normal D3D11
DrawIndexed()draw.EID:
1067Result:
But direct IA decode succeeds:
So this is not limited to indirect draws.
Suggested implementation direction
For
mesh --stage vs-in, relying only oncontroller.GetPostVSData(..., MeshDataStage.VSIn)is not sufficient for these Windows captures.A robust implementation probably needs a fallback that mirrors what RenderDoc GUI Mesh Viewer Input can do:
pipe.GetVertexInputs().pipe.GetVBuffers().pipe.GetIBuffer().vertexBufferbyteOffsetbyteOffsetFor
cbuffer --json, choose the value vector according to reflected variable type, e.g.u32vfor uint variables instead of always usingf32v.For
cbuffer --raw, the raw export path should include stage, or otherwise resolve the exact stage-specific constant block before exporting. The current VFS path/draws/<eid>/cbuffer/<set>/<binding>/datais both missing from the populated VFS tree and ambiguous when multiple stages bind the same register.fix(cbuffer): handle D3D raw export and vs-in fallback
Misaka-Mikoto-Tech commentedon Jul 6, 2026
I think there are two different use cases that should be separated:
mesh --stage vs-incurrently seems to export only one selected position-like attribute plus indices/faces. That is useful for OBJ-style geometry, but it is not equivalent to RenderDoc's Mesh Viewer Input tab.VS input is not always a conventional position-backed mesh. For example, UI/sprite/procedural meshes may store only indices, tile IDs, corner IDs, or instance data in VS inputs, and reconstruct the real position in the vertex shader from constants/textures. In those cases there may be no POSITION attribute at all.
So a full VS-IN export should dump all input attributes per vertex/index and should not require any attribute to be named or inferred as POSITION.
For OBJ export, selecting a position attribute is still necessary because OBJ needs vertex positions. But the current name-based lookup is too fragile.
A better position selection strategy would be:
POSITION,SV_Position, or maybepos*.--position-attribute ATTRIBUTE0,--position-slot 0 --position-offset 0, or--position-index 0.position_attribute,position_source: semantic|heuristic|user.For the D3D12 NTE capture I tested, the VS-IN attributes are
ATTRIBUTE0andATTRIBUTE13;ATTRIBUTE0is the actual position stream, andATTRIBUTE13is instance-rate uint data. A robust OBJ fallback should selectATTRIBUTE0by heuristic or by user override, not fail just because no input semantic is namedPOSITION.Unreal/D3D12 captures often expose inputs as
ATTRIBUTE0,ATTRIBUTE1, etc., where the names are not semantically meaningful.Originally posted by @Misaka-Mikoto-Tech in #265 (comment)
fix(mesh): select vs-in position inputs
Misaka-Mikoto-Tech commentedon Jul 6, 2026
Thanks for the quick fixes and follow-up work!
I reviewed the latest commits, especially
01527a8and73d1c65. The fixes look reasonable for the intended OBJ export workflow:cbuffer --jsonnow appears to handle typed values correctly instead of decoding uint data as floats.cbuffer --rawnow includes the shader stage in the VFS path, which should avoid the previous ambiguous/missing path issue.mesh --stage vs-innow has a much better fallback path for OBJ export: semantic lookup first, then vertex-rate float/vector heuristic, with explicit overrides such as--position-attribute,--position-index,--position-slot, and--position-offset.ATTRIBUTE0,ATTRIBUTE13, etc.; in that capture,ATTRIBUTE0is the position stream andATTRIBUTE13is instance-rate uint data, so the new heuristic/override support should work.One clarification: this implements VS-IN OBJ export, not a full RenderDoc Mesh Viewer Input-tab export of all IA attributes. That distinction is fine for this issue; if I need full per-attribute VS-IN dumps later, I can handle that with a custom RenderDoc Python script.
Thanks again for being responsive and for hardening the D3D paths.
BANANASJIM commentedon Jul 7, 2026
Closing based on the latest maintainer/user feedback. The D3D cbuffer fixes and VS-IN OBJ export hardening are delivered via #265 and #266: typed cbuffer values, stage-qualified raw cbuffer paths, IA fallback for VS-IN OBJ export, robust position selection, explicit overrides, and position metadata.
Scope note: this closes the requested cbuffer + VS-IN OBJ export workflow. A full RenderDoc Mesh Viewer Input-tab / complete IA attribute-table export remains a separate feature if needed later, and should stay separate from OBJ export.