Skip to content

feat(descriptors): resolve bindless array index to texture from the CLI#264

Merged
BANANASJIM merged 9 commits into
BANANASJIM:masterfrom
K1ngst0m:feat/bindless-descriptor-cli
Jul 1, 2026
Merged

feat(descriptors): resolve bindless array index to texture from the CLI#264
BANANASJIM merged 9 commits into
BANANASJIM:masterfrom
K1ngst0m:feat/bindless-descriptor-cli

Conversation

@K1ngst0m

@K1ngst0m K1ngst0m commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

What

Add rdc descriptors and enrich rdc resource/rdc resources so 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 undocumented cat /draws/N/descriptors path, without binding-name correlation.

How

  • rdc descriptors [EID] (new command): promotes the per-draw used-descriptor view to a discoverable command with --stage / --type / --binding filters. Each used array element is correlated to its symbolic binding name via ReplayController.GetDescriptorLocations (the resolver the GUI uses) joined to shader reflection, and resolved to its resource name + texture dimensions. descriptors --binding g_textures is the one-shot albedoIdx → texture view.
  • resource / resources: enriched with width/height/format/size as additive columns, sourced from the daemon's already-cached tex_map / buf_map (no extra replay). Existing columns and quiet/-q behaviour are unchanged.
  • VFS descriptors leaf: columns aligned with the new command.
  • Regenerated commands.json and the skill quick-ref to keep the check-commands / check-skill-ref gates 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 every ud.access, so identity is unstable and id-keying mis-assigns binding names. Verified against a real bindless Vulkan capture.

Test plan

  • pixi run check passes (lint + mypy strict + tests, 93% coverage)
  • Added/updated unit tests (binding-name correlation, positional-alignment regression, --stage/--type/--binding filters, older-module fallback, resource/resources enrichment, command output, VFS leaf)
  • Manual testing on a real bindless Vulkan capture: rdc descriptors 16 --binding g_textures resolves each used element to its texture + dimensions; rdc resource 371 now reports 512x512 BC1_SRGB

Summary by CodeRabbit

  • New Features

    • Added a new descriptors command to inspect draw-call descriptors, with filters for stage, type, and binding.
    • Expanded command reference and generated listings to include the new command.
  • Bug Fixes

    • Descriptor and resource tables now show richer details like binding, set, resource name, dimensions, format, and size.
    • TSV output headers and empty-state output were updated to match the new columns consistently.
  • Tests

    • Added and updated coverage for the new command and the enriched resource/descriptor output.

K1ngst0m added 7 commits June 30, 2026 05:17
…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.
@K1ngst0m
K1ngst0m requested a review from BANANASJIM as a code owner June 30, 2026 11:46
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7df82d79-758e-4792-b014-7d36d735e492

📥 Commits

Reviewing files that changed from the base of the PR and between 6d6e680 and 61f6c5c.

📒 Files selected for processing (4)
  • src/rdc/commands/vfs.py
  • src/rdc/handlers/descriptor.py
  • tests/unit/test_descriptors_daemon.py
  • tests/unit/test_vfs_commands.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/test_vfs_commands.py

📝 Walkthrough

Walkthrough

Adds a new rdc descriptors CLI command for inspecting used draw-call descriptors, backed by a reworked handler that resolves reflection-based binding names, set indices, and applies stage/type/binding filters. Resource rows are also enriched with width/height/format/size from texture and buffer maps. VFS descriptor schema, docs, and commands.json are updated accordingly.

New descriptors command and resource enrichment

Layer / File(s) Summary
Resource row enrichment
src/rdc/handlers/query.py, src/rdc/commands/resources.py
Adds enrich_resource_row to populate width, height, format, and size from tex_map/buf_map, applies it in both resource list and detail handlers, and expands the resources TSV renderer with those extra columns.
Descriptor handler: reflection binding resolution and filters
src/rdc/handlers/descriptor.py
Adds _reflection_resources, _refl_category, _resolve_binding, and _descriptor_filters helpers. Rewrites _handle_descriptors to precompute reflection maps, apply stage/type/binding filters during iteration, and populate binding, set, resource_name, width, and height per row.
descriptors CLI command and VFS schema
src/rdc/commands/descriptors.py, src/rdc/cli.py, src/rdc/commands/vfs.py
Implements descriptors_cmd with Click wiring for eid, --stage, --type, --binding, and output-mode options (TSV/JSON/JSONL/quiet). Registers it on the main group. Updates the VFS descriptors extractor to the expanded column schema.
Docs, commands.json, gen-commands
src/rdc/_skills/references/commands-quick-ref.md, docs-astro/src/data/commands.json, scripts/gen-commands.py
Adds rdc descriptors quick-reference docs, inserts the command entry into commands.json under Draw Analysis, and adds descriptors to the gen-commands category list.
Tests
tests/unit/test_descriptors_daemon.py, tests/unit/test_descriptors_commands.py, tests/unit/test_resources_filter.py, tests/unit/test_resources_commands.py, tests/unit/test_vfs_commands.py, tests/unit/test_formatters.py
Adds daemon tests for binding correlation, fallback without reflection, cross-set collision, and filter narrowing. Adds CLI tests for table/filter/quiet/JSON output. Adds resource enrichment tests and updates VFS/formatter golden assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • BANANASJIM/rdc-cli#22: This PR implements the descriptors per-draw descriptor listing handler, CLI command, TSV/JSON rendering, filtering, and documentation that the issue tracks.

Suggested labels

Review effort 3/5

🐇 A new command hops into view,
rdc descriptors — binding names too!
Reflection maps shimmer, stage filters align,
Width, height, and format all perfectly fine.
With TSV or JSON, the data flows free —
A hop for the CLI, a hop for me! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main CLI change: resolving descriptor array indices to concrete textures.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
tests/unit/test_descriptors_daemon.py (1)

367-368: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the type=image result 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 the type filter.

🤖 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 win

Use distinct dimensions and assert size here.

With both dimensions set to 512 and no size assertion, this test can't tell whether both columns are rendered correctly or one was duplicated/omitted. Using asymmetric dimensions and checking 174776 would 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 win

Cover --type forwarding in the CLI test.

This only proves --binding and --stage make it into the RPC params. --type is 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 win

Cover list-path height and buffer size too.

resource and resources are separate handlers, but the list test only checks texture width/format/size, and the buffer check only exercises resource. A regression that drops height or buffer size from resources would 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0a4daab and 6d6e680.

📒 Files selected for processing (16)
  • docs-astro/src/data/commands.json
  • scripts/gen-commands.py
  • src/rdc/_skills/references/commands-quick-ref.md
  • src/rdc/cli.py
  • src/rdc/commands/descriptors.py
  • src/rdc/commands/resources.py
  • src/rdc/commands/vfs.py
  • src/rdc/handlers/descriptor.py
  • src/rdc/handlers/query.py
  • tests/mocks/mock_renderdoc.py
  • tests/unit/test_descriptors_commands.py
  • tests/unit/test_descriptors_daemon.py
  • tests/unit/test_formatters.py
  • tests/unit/test_resources_commands.py
  • tests/unit/test_resources_filter.py
  • tests/unit/test_vfs_commands.py

Comment on lines +53 to +99
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,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment thread src/rdc/handlers/descriptor.py Outdated
Comment thread src/rdc/handlers/descriptor.py Outdated
Comment on lines +126 to +139
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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 BANANASJIM left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Silent failure looks like "no bindings":60-63: except Exception: continue swallows any GetDescriptorLocations error 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.

  2. The regression test doesn't guard the fix it namestests/unit/test_descriptors_daemon.py::test_descriptors_locations_aligned_per_descriptor passes against the previous id()-keyed implementation too, because the mock's UsedDescriptor.access is a stable object and can't reproduce the "fresh proxy per access" behavior the commit message cites. A fixture where .access returns 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.

  3. VFS leaf column compatibilitysrc/rdc/commands/vfs.py reshapes the already-shipped /draws/<eid>/descriptors leaf TSV (drops INDEX and BYTE_SIZE, reorders columns), which breaks cut -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.

K1ngst0m added 2 commits June 30, 2026 08:05
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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@K1ngst0m
K1ngst0m requested a review from BANANASJIM June 30, 2026 14:44
@BANANASJIM
BANANASJIM dismissed their stale review July 1, 2026 00:36

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.

@BANANASJIM
BANANASJIM merged commit 5329530 into BANANASJIM:master Jul 1, 2026
31 of 32 checks passed
@K1ngst0m
K1ngst0m deleted the feat/bindless-descriptor-cli branch July 1, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants