feat(descriptors): resolve bindless array index to texture from the CLI#264
Conversation
…ce name and dims - add reflection binding maps + GetDescriptorLocations join (batched per store) with graceful fallback when the API or reflection is unavailable - attach resolved resource_name and texture width/height per used descriptor
…scriptors Promotes the used-descriptor view (previously only via cat /draws/N/descriptors) to a discoverable command with --stage/--type/--binding filters, surfacing the resolved binding name, resource and texture dimensions. Regenerate commands.json and commands-quick-ref.md to keep the check-commands and check-skill-ref gates green.
Align the cat /draws/N/descriptors columns with the new 'rdc descriptors' command (binding, set, resource name, width, height).
…d size Merge texture (width/height/format/byteSize) and buffer (length) data from the daemon's cached tex_map/buf_map into resource rows, so a resolved resource id can be inspected without a second lookup.
…urces' Surfaces the enriched dimension/format/size fields in the table view; existing ID/TYPE/NAME columns and quiet-key behaviour are unchanged. Update the golden parity and header tests for the widened table.
The renderdoc bindings return a fresh proxy on every ud.access, so keying the GetDescriptorLocations result by id(access) collided and mis-assigned binding names (verified on a real bindless Vulkan capture). Align locations to the used list by position and add a regression test that distinguishes locations by type.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a new New descriptors command and resource enrichment
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
tests/unit/test_descriptors_daemon.py (1)
367-368: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the
type=imageresult count too.
all(...)passes on an empty list, so this would not catch a filter implementation that drops every descriptor. Please also assert the expected row count for thetypefilter.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_descriptors_daemon.py` around lines 367 - 368, The `type=image` test only checks `all(d["type"] == "Image" for d in by_type)`, which can still pass if the filter returns an empty list. Update the descriptor test around `_call(..., type="image")` to also assert the expected number of returned descriptors, using the existing `by_type` variable so the `descriptors` filtering behavior is validated for both content and count.tests/unit/test_resources_commands.py (1)
27-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse distinct dimensions and assert
sizehere.With both dimensions set to
512and nosizeassertion, this test can't tell whether both columns are rendered correctly or one was duplicated/omitted. Using asymmetric dimensions and checking174776would lock down the new table contract.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_resources_commands.py` around lines 27 - 49, The resources table test is too weak because both dimensions use the same value, so it can’t prove WIDTH and HEIGHT are rendered independently. Update test_resources_enriched_columns to use distinct values for the dimensions in the patch_cli_session row, and add an assertion for the size value so the new table contract is fully verified through resources_cmd.tests/unit/test_descriptors_commands.py (1)
43-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover
--typeforwarding in the CLI test.This only proves
--bindingand--stagemake it into the RPC params.--typeis part of the new command surface and can regress independently at the Click/RPC boundary.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_descriptors_commands.py` around lines 43 - 57, The CLI test for descriptors only verifies forwarding of --binding and --stage, so add coverage for --type as well in test_descriptors_binding_filter_forwarded. Update the same CliRunner invocation and RPC capture setup used around main and rdc.commands.descriptors.call to pass a --type value, then assert that the captured params include the expected type alongside binding, stage, and eid.tests/unit/test_resources_filter.py (1)
371-384: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover list-path
heightand buffersizetoo.
resourceandresourcesare separate handlers, but the list test only checks texturewidth/format/size, and the buffer check only exercisesresource. A regression that dropsheightor buffersizefromresourceswould still pass.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_resources_filter.py` around lines 371 - 384, The current tests only verify texture width/format/size in the resources list and buffer size in the single-resource path, so they miss regressions in the list handler. Update the test coverage in test_resources_filter.py to assert that resources also includes height for texture entries and size for buffer entries, using the existing _handle_request, rpc_request("resources"), rpc_request("resource"), and the resource/rows fields to validate both handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/rdc/commands/descriptors.py`:
- Around line 53-99: The descriptors command is dropping the resolved EID in
machine-readable output because render_list() only receives rows while the table
path injects row_eid separately. Update the descriptors handling around the
result from call("descriptors", params) so each item passed to render_list()
also carries the EID, or otherwise ensure render_list() emits it for
--json/--jsonl just like _table() does; keep the change localized to the
descriptors command and its render_list() call.
In `@src/rdc/handlers/descriptor.py`:
- Around line 20-24: The reflection map in _bind_bucket currently keys only on
fixedBindNumber, so descriptors from different fixedBindSetOrSpace values can
overwrite each other and later lookups in the descriptor resolution path pick
the wrong binding. Update _bind_bucket to build a composite key using both
fixedBindSetOrSpace and fixedBindNumber, then adjust the consumers in the
descriptor handling flow that rely on this map so they look up by that same pair
instead of a single bind number.
- Around line 126-139: The fallback binding logic in GetDescriptorLocations
handling is mixing the numeric binding lookup with the set column, which causes
--binding matches to fail when bind_num is unavailable. Keep d_row["set"] using
acc.index as before, but in the bind_lower comparison path for descriptor
filtering, also add the descriptor slot/index as a numeric candidate when
bind_num is None so numeric bindings still match; update the logic around
descriptor binding selection in descriptor.py to preserve the separate meaning
of binding, set, and numeric fallback.
---
Nitpick comments:
In `@tests/unit/test_descriptors_commands.py`:
- Around line 43-57: The CLI test for descriptors only verifies forwarding of
--binding and --stage, so add coverage for --type as well in
test_descriptors_binding_filter_forwarded. Update the same CliRunner invocation
and RPC capture setup used around main and rdc.commands.descriptors.call to pass
a --type value, then assert that the captured params include the expected type
alongside binding, stage, and eid.
In `@tests/unit/test_descriptors_daemon.py`:
- Around line 367-368: The `type=image` test only checks `all(d["type"] ==
"Image" for d in by_type)`, which can still pass if the filter returns an empty
list. Update the descriptor test around `_call(..., type="image")` to also
assert the expected number of returned descriptors, using the existing `by_type`
variable so the `descriptors` filtering behavior is validated for both content
and count.
In `@tests/unit/test_resources_commands.py`:
- Around line 27-49: The resources table test is too weak because both
dimensions use the same value, so it can’t prove WIDTH and HEIGHT are rendered
independently. Update test_resources_enriched_columns to use distinct values for
the dimensions in the patch_cli_session row, and add an assertion for the size
value so the new table contract is fully verified through resources_cmd.
In `@tests/unit/test_resources_filter.py`:
- Around line 371-384: The current tests only verify texture width/format/size
in the resources list and buffer size in the single-resource path, so they miss
regressions in the list handler. Update the test coverage in
test_resources_filter.py to assert that resources also includes height for
texture entries and size for buffer entries, using the existing _handle_request,
rpc_request("resources"), rpc_request("resource"), and the resource/rows fields
to validate both handlers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5f9a33e3-98f5-4b9e-be07-7078f8fc6c47
📒 Files selected for processing (16)
docs-astro/src/data/commands.jsonscripts/gen-commands.pysrc/rdc/_skills/references/commands-quick-ref.mdsrc/rdc/cli.pysrc/rdc/commands/descriptors.pysrc/rdc/commands/resources.pysrc/rdc/commands/vfs.pysrc/rdc/handlers/descriptor.pysrc/rdc/handlers/query.pytests/mocks/mock_renderdoc.pytests/unit/test_descriptors_commands.pytests/unit/test_descriptors_daemon.pytests/unit/test_formatters.pytests/unit/test_resources_commands.pytests/unit/test_resources_filter.pytests/unit/test_vfs_commands.py
| result = call("descriptors", params) | ||
| rows: list[dict[str, Any]] = result.get("descriptors", []) | ||
| row_eid = result.get("eid", "-") | ||
|
|
||
| def _table() -> None: | ||
| tsv_rows = [ | ||
| [ | ||
| row_eid, | ||
| r.get("stage", "-"), | ||
| r.get("binding", "-"), | ||
| r.get("type", "-"), | ||
| r.get("set", "-"), | ||
| r.get("array_element", "-"), | ||
| r.get("resource_id", "-"), | ||
| r.get("resource_name", "-"), | ||
| r.get("format", "-"), | ||
| r.get("width", "-"), | ||
| r.get("height", "-"), | ||
| ] | ||
| for r in rows | ||
| ] | ||
| write_tsv( | ||
| tsv_rows, | ||
| header=[ | ||
| "EID", | ||
| "STAGE", | ||
| "BINDING", | ||
| "TYPE", | ||
| "SET", | ||
| "ARRAY_EL", | ||
| "RESOURCE", | ||
| "RES_NAME", | ||
| "FORMAT", | ||
| "WIDTH", | ||
| "HEIGHT", | ||
| ], | ||
| no_header=no_header, | ||
| ) | ||
|
|
||
| render_list( | ||
| rows, | ||
| use_json=use_json, | ||
| use_jsonl=use_jsonl, | ||
| quiet=quiet, | ||
| quiet_key="resource_id", | ||
| table=_table, | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Preserve eid in JSON/JSONL output too.
The table path includes row_eid, but render_list() only sees rows, so --json/--jsonl discard the resolved event id. If the caller omits the positional EID, machine-readable output no longer tells you which draw these descriptors came from.
Suggested fix
result = call("descriptors", params)
- rows: list[dict[str, Any]] = result.get("descriptors", [])
row_eid = result.get("eid", "-")
+ rows: list[dict[str, Any]] = [
+ {"eid": row_eid, **r} for r in result.get("descriptors", [])
+ ]
...
- row_eid,
+ r.get("eid", "-"),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| result = call("descriptors", params) | |
| rows: list[dict[str, Any]] = result.get("descriptors", []) | |
| row_eid = result.get("eid", "-") | |
| def _table() -> None: | |
| tsv_rows = [ | |
| [ | |
| row_eid, | |
| r.get("stage", "-"), | |
| r.get("binding", "-"), | |
| r.get("type", "-"), | |
| r.get("set", "-"), | |
| r.get("array_element", "-"), | |
| r.get("resource_id", "-"), | |
| r.get("resource_name", "-"), | |
| r.get("format", "-"), | |
| r.get("width", "-"), | |
| r.get("height", "-"), | |
| ] | |
| for r in rows | |
| ] | |
| write_tsv( | |
| tsv_rows, | |
| header=[ | |
| "EID", | |
| "STAGE", | |
| "BINDING", | |
| "TYPE", | |
| "SET", | |
| "ARRAY_EL", | |
| "RESOURCE", | |
| "RES_NAME", | |
| "FORMAT", | |
| "WIDTH", | |
| "HEIGHT", | |
| ], | |
| no_header=no_header, | |
| ) | |
| render_list( | |
| rows, | |
| use_json=use_json, | |
| use_jsonl=use_jsonl, | |
| quiet=quiet, | |
| quiet_key="resource_id", | |
| table=_table, | |
| ) | |
| result = call("descriptors", params) | |
| row_eid = result.get("eid", "-") | |
| rows: list[dict[str, Any]] = [ | |
| {"eid": row_eid, **r} for r in result.get("descriptors", []) | |
| ] | |
| def _table() -> None: | |
| tsv_rows = [ | |
| [ | |
| r.get("eid", "-"), | |
| r.get("stage", "-"), | |
| r.get("binding", "-"), | |
| r.get("type", "-"), | |
| r.get("set", "-"), | |
| r.get("array_element", "-"), | |
| r.get("resource_id", "-"), | |
| r.get("resource_name", "-"), | |
| r.get("format", "-"), | |
| r.get("width", "-"), | |
| r.get("height", "-"), | |
| ] | |
| for r in rows | |
| ] | |
| write_tsv( | |
| tsv_rows, | |
| header=[ | |
| "EID", | |
| "STAGE", | |
| "BINDING", | |
| "TYPE", | |
| "SET", | |
| "ARRAY_EL", | |
| "RESOURCE", | |
| "RES_NAME", | |
| "FORMAT", | |
| "WIDTH", | |
| "HEIGHT", | |
| ], | |
| no_header=no_header, | |
| ) | |
| render_list( | |
| rows, | |
| use_json=use_json, | |
| use_jsonl=use_jsonl, | |
| quiet=quiet, | |
| quiet_key="resource_id", | |
| table=_table, | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/rdc/commands/descriptors.py` around lines 53 - 99, The descriptors
command is dropping the resolved EID in machine-readable output because
render_list() only receives rows while the table path injects row_eid
separately. Update the descriptors handling around the result from
call("descriptors", params) so each item passed to render_list() also carries
the EID, or otherwise ensure render_list() emits it for --json/--jsonl just like
_table() does; keep the change localized to the descriptors command and its
render_list() call.
| loc = loc_list[i] | ||
| logical = getattr(loc, "logicalBindName", "") if loc is not None else "" | ||
| bind_num = getattr(loc, "fixedBindNumber", None) if loc is not None else None | ||
| name, bset = "", None | ||
| stage_buckets = bind_maps.get(int(acc.stage)) | ||
| if stage_buckets is not None and bind_num is not None: | ||
| name, bset = stage_buckets[_refl_bucket(type_name)].get(bind_num, ("", None)) | ||
| d_row["binding"] = name or logical | ||
| d_row["set"] = bset if bset is not None else acc.index | ||
| if bind_lower is not None: | ||
| candidates = {str(d_row["binding"]).lower()} | ||
| if bind_num is not None: | ||
| candidates.add(str(bind_num).lower()) | ||
| if bind_lower not in candidates: |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Keep numeric binding fallback separate from the set column.
When GetDescriptorLocations is unavailable or throws, bind_num stays None. In that path, Line 134 puts acc.index into set, but Lines 135-139 never use acc.index as a numeric binding candidate, so --binding 5 stops matching entirely even though the descriptor slot is known.
Suggested fix
- d_row["binding"] = name or logical
- d_row["set"] = bset if bset is not None else acc.index
+ d_row["binding"] = name or logical or str(acc.index)
+ d_row["set"] = bset if bset is not None else ""
if bind_lower is not None:
candidates = {str(d_row["binding"]).lower()}
+ candidates.add(str(acc.index).lower())
if bind_num is not None:
candidates.add(str(bind_num).lower())
if bind_lower not in candidates:
continue📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| loc = loc_list[i] | |
| logical = getattr(loc, "logicalBindName", "") if loc is not None else "" | |
| bind_num = getattr(loc, "fixedBindNumber", None) if loc is not None else None | |
| name, bset = "", None | |
| stage_buckets = bind_maps.get(int(acc.stage)) | |
| if stage_buckets is not None and bind_num is not None: | |
| name, bset = stage_buckets[_refl_bucket(type_name)].get(bind_num, ("", None)) | |
| d_row["binding"] = name or logical | |
| d_row["set"] = bset if bset is not None else acc.index | |
| if bind_lower is not None: | |
| candidates = {str(d_row["binding"]).lower()} | |
| if bind_num is not None: | |
| candidates.add(str(bind_num).lower()) | |
| if bind_lower not in candidates: | |
| loc = loc_list[i] | |
| logical = getattr(loc, "logicalBindName", "") if loc is not None else "" | |
| bind_num = getattr(loc, "fixedBindNumber", None) if loc is not None else None | |
| name, bset = "", None | |
| stage_buckets = bind_maps.get(int(acc.stage)) | |
| if stage_buckets is not None and bind_num is not None: | |
| name, bset = stage_buckets[_refl_bucket(type_name)].get(bind_num, ("", None)) | |
| d_row["binding"] = name or logical or str(acc.index) | |
| d_row["set"] = bset if bset is not None else "" | |
| if bind_lower is not None: | |
| candidates = {str(d_row["binding"]).lower()} | |
| candidates.add(str(acc.index).lower()) | |
| if bind_num is not None: | |
| candidates.add(str(bind_num).lower()) | |
| if bind_lower not in candidates: |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/rdc/handlers/descriptor.py` around lines 126 - 139, The fallback binding
logic in GetDescriptorLocations handling is mixing the numeric binding lookup
with the set column, which causes --binding matches to fail when bind_num is
unavailable. Keep d_row["set"] using acc.index as before, but in the bind_lower
comparison path for descriptor filtering, also add the descriptor slot/index as
a numeric candidate when bind_num is None so numeric bindings still match;
update the logic around descriptor binding selection in descriptor.py to
preserve the separate meaning of binding, set, and numeric fallback.
BANANASJIM
left a comment
There was a problem hiding this comment.
Thanks for this — bindless array index → concrete texture is a real gap in the CLI, the command is well shaped, the regenerated commands.json / quick-ref signatures are byte-exact, and CI is green. I ran a detailed review and there's one correctness issue worth fixing before merge, plus a few follow-ups.
Blocking: binding names collide across descriptor sets/spaces
src/rdc/handlers/descriptor.py:20-24 keys the reflection bucket on fixedBindNumber alone:
def _bind_bucket(items):
return {getattr(r, "fixedBindNumber", 0): (r.name, getattr(r, "fixedBindSetOrSpace", 0)) for r in items}and the lookup at :130-132 joins on bind_num only. But fixedBindNumber is per-set/per-space, not globally unique — renderdoc's shader_types.h documents it as "API-specific ... purely for informational purposes" and recommends sorting by fixedBindSetOrSpace before fixedBindNumber.
Concrete failure: a Vulkan shader with (name=materialTex, set=2, binding=0) and (name=g_textures, set=1, binding=0) collapses to a single bucket entry for key 0; one resource silently overwrites the other, so both used descriptors resolve to the same (wrong) name and set. This is precisely the multi-set bindless layout the feature targets, and the current tests all use a single set, so it isn't caught.
Suggested direction: key the bucket on (fixedBindSetOrSpace, fixedBindNumber) and disambiguate the lookup with the descriptor's set/space. See the open question below about whether the location's fixedBindNumber is the right join value to begin with.
Should-fix
-
Silent failure looks like "no bindings" —
:60-63:except Exception: continueswallows anyGetDescriptorLocationserror per store, leaving every binding empty. A total correlation failure then renders identically to a draw that legitimately has no named bindings. A logged warning would keep the two distinguishable. -
The regression test doesn't guard the fix it names —
tests/unit/test_descriptors_daemon.py::test_descriptors_locations_aligned_per_descriptorpasses against the previousid()-keyed implementation too, because the mock'sUsedDescriptor.accessis a stable object and can't reproduce the "fresh proxy per access" behavior the commit message cites. A fixture where.accessreturns a fresh object on each read would make it a true red-first guard; a multi-store case would also exercise the by-store regrouping in_descriptor_locations, which is currently untested. -
VFS leaf column compatibility —
src/rdc/commands/vfs.pyreshapes the already-shipped/draws/<eid>/descriptorsleaf TSV (dropsINDEXandBYTE_SIZE, reorders columns), which breakscut -f-style consumers of the prior layout. Appending the new columns on the right instead would preserve it; the JSON output already keeps the old fields.
Open question (real-hardware)
The name join routes through DescriptorLogicalLocation.fixedBindNumber, but renderdoc's descriptors_bindings docs describe the logical location as "API-specific ... not used for any lookups or processing, only for user display," and point to the per-stage getters (GetReadOnlyResources / GetReadWriteResources / GetConstantBlocks / GetSamplers) which return a direct reflection reference. Since you validated on real captures: does the location's fixedBindNumber reliably match the reflection register on both Vulkan and D3D12 (root params / register spaces)? If not, the per-stage getters may be the more robust correlation path.
Happy to re-review once the multi-set keying is sorted. Thanks again for the contribution.
GetAllUsedDescriptors' access.index is documented as the index into the shader's reflection list for that descriptor type, so resolving names through it is per (set, binding) and cannot collide across descriptor sets/spaces. The previous fixedBindNumber-keyed join collapsed e.g. set=1/binding=0 and set=2/binding=0 onto a single entry, mislabelling one of them. - replace the fixedBindNumber bucket + GetDescriptorLocations join with a direct reflection-index lookup over the read-only/read-write/constant-block/sampler arrays (also yields real names for push constants and samplers) - drop the descriptor-location machinery and its now-unused mock types; this removes the per-store except/continue that rendered a total correlation failure identically to a draw with no named bindings - add a multi-set regression test the old keying could not pass
The /draws/<eid>/descriptors leaf shipped with STAGE TYPE INDEX ARRAY_EL RESOURCE FORMAT BYTE_SIZE. Restore that column order and append BINDING/SET/RES_NAME/WIDTH/HEIGHT on the right so existing cut -f consumers of the prior layout keep working; the JSON output already kept the original fields.
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
Merging as-is per maintainer decision; the confirmed multi-set binding-name collision and the other findings are addressed immediately in a fix-forward PR. Findings retained above for record.
What
Add
rdc descriptorsand enrichrdc resource/rdc resourcesso a bindless array index (e.g.albedoIdx) can be mapped to its concrete texture from the CLI.Why
For bindless rendering, a single array binding (
g_textures[]) is indexed per-draw. The CLI could only show the declared array binding (rdc bindings) and a resource's id/name/type — there was no way to see which array element a draw actually used, the texture it resolved to, or its dimensions. RenderDoc captures all of this; the gap was purely in the CLI surface. The per-draw used-descriptor data already existed in the daemon but was only reachable via the undocumentedcat /draws/N/descriptorspath, without binding-name correlation.How
rdc descriptors [EID](new command): promotes the per-draw used-descriptor view to a discoverable command with--stage/--type/--bindingfilters. Each used array element is correlated to its symbolic binding name viaReplayController.GetDescriptorLocations(the resolver the GUI uses) joined to shader reflection, and resolved to its resource name + texture dimensions.descriptors --binding g_texturesis the one-shotalbedoIdx → textureview.resource/resources: enriched with width/height/format/size as additive columns, sourced from the daemon's already-cachedtex_map/buf_map(no extra replay). Existing columns andquiet/-qbehaviour are unchanged.descriptorsleaf: columns aligned with the new command.commands.jsonand the skill quick-ref to keep thecheck-commands/check-skill-refgates green.Non-obvious: descriptor locations are mapped to the used-descriptor list by position, not by
id(access)— the renderdoc bindings hand back a fresh proxy on everyud.access, so identity is unstable and id-keying mis-assigns binding names. Verified against a real bindless Vulkan capture.Test plan
pixi run checkpasses (lint + mypy strict + tests, 93% coverage)--stage/--type/--bindingfilters, older-module fallback, resource/resources enrichment, command output, VFS leaf)rdc descriptors 16 --binding g_texturesresolves each used element to its texture + dimensions;rdc resource 371now reports512x512 BC1_SRGBSummary by CodeRabbit
New Features
descriptorscommand to inspect draw-call descriptors, with filters for stage, type, and binding.Bug Fixes
Tests