Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs-astro/src/data/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@
"id": "bindings",
"help": "Show bound resources per shader stage.",
"usage": "rdc bindings [EID] [--binding INTEGER] [--set INTEGER] [--no-header] [--json] [--jsonl] [-q]"
},
{
"name": "descriptors",
"id": "descriptors",
"help": "Show the descriptors a draw actually used, resolved to resources.",
"usage": "rdc descriptors [EID] [--stage CHOICE] [--type TEXT] [--binding TEXT] [--no-header] [--json] [--jsonl] [-q]"
}
]
},
Expand Down
2 changes: 1 addition & 1 deletion scripts/gen-commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"info", "stats", "events", "draws", "event", "draw", "log", "counters",
]),
("Draw Analysis", "draw-analysis", None, [
"pipeline", "bindings",
"pipeline", "bindings", "descriptors",
]),
("Resources", "resources-section", None, [
"resources", "resource", "usage", "shader", "shaders", "shader-map",
Expand Down
22 changes: 22 additions & 0 deletions src/rdc/_skills/references/commands-quick-ref.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,28 @@ Debug vertex shader for vertex VTX_ID at event EID.
| `--json` | JSON output | flag | |
| `--no-header` | Suppress TSV header row | flag | |

## `rdc descriptors`

Show the descriptors a draw actually used, resolved to resources.

**Arguments:**

| Name | Type | Required |
|------|------|----------|
| `eid` | integer | no |

**Options:**

| Flag | Help | Type | Default |
|------|------|------|---------|
| `--stage` | Filter by shader stage. | choice | |
| `--type` | Filter by descriptor type (case-insensitive). | text | |
| `--binding` | Filter by binding name or number. | text | |
| `--no-header` | Omit TSV header | flag | |
| `--json` | JSON output | flag | |
| `--jsonl` | JSONL output | flag | |
| `-q, --quiet` | Print primary key column only | flag | |

## `rdc diff`

Compare two RenderDoc captures side-by-side.
Expand Down
2 changes: 2 additions & 0 deletions src/rdc/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from rdc.commands.completion import completion_cmd
from rdc.commands.counters import counters_cmd
from rdc.commands.debug import debug_group
from rdc.commands.descriptors import descriptors_cmd
from rdc.commands.diff import diff_cmd
from rdc.commands.doctor import doctor_cmd
from rdc.commands.events import draw_cmd, draws_cmd, event_cmd, events_cmd
Expand Down Expand Up @@ -123,6 +124,7 @@ def entry() -> None:
main.add_command(shader_map_cmd, name="shader-map")
main.add_command(pipeline_cmd, name="pipeline")
main.add_command(bindings_cmd, name="bindings")
main.add_command(descriptors_cmd, name="descriptors")
main.add_command(shader_cmd, name="shader")
main.add_command(shaders_cmd, name="shaders")
main.add_command(resources_cmd, name="resources")
Expand Down
99 changes: 99 additions & 0 deletions src/rdc/commands/descriptors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Used-descriptor inspection command."""

from __future__ import annotations

from typing import Any

import click

from rdc.commands._helpers import call, complete_eid
from rdc.formatters.options import list_output_options, render_list
from rdc.formatters.tsv import write_tsv

_STAGE_CHOICES = ["vs", "hs", "ds", "gs", "ps", "cs"]


@click.command("descriptors")
@click.argument("eid", required=False, type=int, shell_complete=complete_eid)
@click.option(
"--stage",
"stage_filter",
type=click.Choice(_STAGE_CHOICES, case_sensitive=False),
help="Filter by shader stage.",
)
@click.option("--type", "type_filter", help="Filter by descriptor type (case-insensitive).")
@click.option("--binding", "binding_filter", help="Filter by binding name or number.")
@list_output_options
def descriptors_cmd( # noqa: PLR0913
eid: int | None,
stage_filter: str | None,
type_filter: str | None,
binding_filter: str | None,
no_header: bool,
use_json: bool,
use_jsonl: bool,
quiet: bool,
) -> None:
"""Show the descriptors a draw actually used, resolved to resources.

Each array element a shader read is listed with its resolved binding name
(e.g. g_textures), resource, and texture dimensions -- the per-draw view
that maps a bindless index to its concrete texture.
"""
params: dict[str, Any] = {}
if eid is not None:
params["eid"] = eid
if stage_filter is not None:
params["stage"] = stage_filter
if type_filter is not None:
params["type"] = type_filter
if binding_filter is not None:
params["binding"] = binding_filter

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

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.

16 changes: 14 additions & 2 deletions src/rdc/commands/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,20 @@ def resources_cmd( # noqa: PLR0913
rows: list[dict[str, Any]] = result.get("rows", [])

def _table() -> None:
tsv_rows = [[r.get("id", "-"), r.get("type", "-"), r.get("name", "-")] for r in rows]
write_tsv(tsv_rows, header=["ID", "TYPE", "NAME"], no_header=no_header)
tsv_rows = [
[
r.get("id", "-"),
r.get("type", "-"),
r.get("name", "-"),
r.get("width", "-"),
r.get("height", "-"),
r.get("format", "-"),
r.get("size", "-"),
]
for r in rows
]
header = ["ID", "TYPE", "NAME", "WIDTH", "HEIGHT", "FORMAT", "SIZE"]
write_tsv(tsv_rows, header=header, no_header=no_header)

render_list(
rows,
Expand Down
6 changes: 4 additions & 2 deletions src/rdc/commands/vfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,11 @@ def _fmt_pixel_mod(m: dict[str, Any]) -> str:
)
),
"descriptors": lambda r: (
"STAGE\tTYPE\tINDEX\tARRAY_EL\tRESOURCE\tFORMAT\tBYTE_SIZE\n"
"STAGE\tTYPE\tINDEX\tARRAY_EL\tRESOURCE\tFORMAT\tBYTE_SIZE\tBINDING\tSET\tRES_NAME\tWIDTH\tHEIGHT\n"
+ "\n".join(
f"{d['stage']}\t{d['type']}\t{d['index']}\t{d['array_element']}\t{d['resource_id']}\t{d['format']}\t{d['byte_size']}"
f"{d['stage']}\t{d['type']}\t{d['index']}\t{d['array_element']}\t{d['resource_id']}\t"
f"{d['format']}\t{d['byte_size']}\t{d.get('binding', '')}\t{d.get('set', '')}\t"
f"{d.get('resource_name', '')}\t{d.get('width', '')}\t{d.get('height', '')}"
for d in r.get("descriptors", [])
)
),
Expand Down
85 changes: 84 additions & 1 deletion src/rdc/handlers/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,68 @@
from rdc.daemon_server import DaemonState


def _reflection_resources(pipe_state: Any) -> dict[int, dict[str, list[Any]]]:
"""Per-stage reflection resources by category, keyed by stage value.

Correlation uses ``DescriptorAccess.index``, which renderdoc documents as the index into
the shader's reflection list for that descriptor type. It is therefore per (set, binding)
and never collides across descriptor sets/spaces, unlike a ``fixedBindNumber`` join.
"""
from rdc.services.query_service import STAGE_MAP

out: dict[int, dict[str, list[Any]]] = {}
for stage_val in STAGE_MAP.values():
refl = pipe_state.GetShaderReflection(stage_val)
if refl is None:
continue
out[stage_val] = {
"ro": list(getattr(refl, "readOnlyResources", [])),
"rw": list(getattr(refl, "readWriteResources", [])),
"cb": list(getattr(refl, "constantBlocks", [])),
"sampler": list(getattr(refl, "samplers", [])),
}
return out


def _refl_category(type_name: str) -> str:
if type_name == "ConstantBuffer":
return "cb"
if type_name.startswith("ReadWrite"):
return "rw"
if type_name == "Sampler":
return "sampler"
return "ro"


def _resolve_binding(
refl_map: dict[int, dict[str, list[Any]]], acc: Any, type_name: str
) -> tuple[str, int | None, int | None]:
"""Resolve (name, set, bind_number) for a used descriptor via its reflection index."""
arr = refl_map.get(int(acc.stage), {}).get(_refl_category(type_name), [])
idx = acc.index
if 0 <= idx < len(arr):
r = arr[idx]
return (
getattr(r, "name", ""),
getattr(r, "fixedBindSetOrSpace", None),
getattr(r, "fixedBindNumber", None),
)
return "", None, None


def _descriptor_filters(params: dict[str, Any]) -> tuple[int | None, str | None, str | None]:
"""Parse stage/type/binding filter params (stage as a STAGE_MAP value)."""
from rdc.services.query_service import STAGE_MAP

stage = params.get("stage")
want_stage = STAGE_MAP.get(str(stage).lower()) if stage is not None else None
type_filter = params.get("type")
type_lower = str(type_filter).lower() if type_filter is not None else None
binding = params.get("binding")
bind_lower = str(binding).lower() if binding is not None else None
return want_stage, type_lower, bind_lower


def _handle_descriptors(
request_id: int, params: dict[str, Any], state: DaemonState
) -> tuple[dict[str, Any], bool]:
Expand All @@ -27,23 +89,44 @@ def _handle_descriptors(
if not hasattr(pipe_state, "GetAllUsedDescriptors"):
return _error_response(request_id, -32002, "GetAllUsedDescriptors not available"), True
used = pipe_state.GetAllUsedDescriptors(True)
refl_map = _reflection_resources(pipe_state)
want_stage, type_lower, bind_lower = _descriptor_filters(params)
desc_rows: list[dict[str, Any]] = []
for ud in used:
acc = ud.access
if want_stage is not None and int(acc.stage) != want_stage:
continue
desc = ud.descriptor
stage_name = _enum_name(acc.stage)
type_name = _enum_name(acc.type)
if type_lower is not None and type_name.lower() != type_lower:
continue
fmt = getattr(desc, "format", None)
fmt_name = fmt.Name() if fmt and hasattr(fmt, "Name") else str(fmt) if fmt else ""
res_id = int(desc.resource)
d_row: dict[str, Any] = {
"stage": stage_name,
"type": type_name,
"index": acc.index,
"array_element": acc.arrayElement,
"resource_id": int(desc.resource),
"resource_id": res_id,
"format": fmt_name,
"byte_size": getattr(desc, "byteSize", 0),
}
name, bset, bnum = _resolve_binding(refl_map, acc, type_name)
d_row["binding"] = name
d_row["set"] = bset if bset is not None else "-"
if bind_lower is not None:
candidates = {name.lower()}
if bnum is not None:
candidates.add(str(bnum).lower())
if bind_lower not in candidates:
continue
d_row["resource_name"] = state.res_names.get(res_id, "")
tex = state.tex_map.get(res_id)
if tex is not None:
d_row["width"] = tex.width
d_row["height"] = tex.height
if type_name in ("Sampler", "ImageSampler"):
s = getattr(ud, "sampler", None)
if s is not None:
Expand Down
20 changes: 19 additions & 1 deletion src/rdc/handlers/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,23 @@ def _handle_shaders(
return _result_response(request_id, {"rows": rows}), True


def enrich_resource_row(row: dict[str, Any], state: DaemonState) -> dict[str, Any]:
"""Merge texture/buffer dimensions and size into a base resource row."""
rid = int(row.get("id", 0))
tex = state.tex_map.get(rid)
if tex is not None:
fmt = getattr(tex, "format", None)
row["width"] = tex.width
row["height"] = tex.height
row["format"] = fmt.Name() if fmt is not None and hasattr(fmt, "Name") else ""
row["size"] = getattr(tex, "byteSize", 0)
return row
buf = state.buf_map.get(rid)
if buf is not None:
row["size"] = getattr(buf, "length", getattr(buf, "byteSize", 0))
return row


def _handle_resources(
request_id: int, params: dict[str, Any], state: DaemonState
) -> tuple[dict[str, Any], bool]:
Expand All @@ -175,6 +192,7 @@ def _handle_resources(
rows = [r for r in rows if name_lower in r["name"].lower()]
if sort_by in ("name", "type"):
rows.sort(key=lambda r: r[sort_by].lower())
rows = [enrich_resource_row(r, state) for r in rows]
return _result_response(request_id, {"rows": rows}), True


Expand All @@ -188,7 +206,7 @@ def _handle_resource(
detail = get_resource_detail(state.adapter, resid)
if detail is None:
return _error_response(request_id, -32001, "resource not found"), True
return _result_response(request_id, {"resource": detail}), True
return _result_response(request_id, {"resource": enrich_resource_row(detail, state)}), True


def _handle_passes(
Expand Down
Loading
Loading