diff --git a/docs-astro/src/data/commands.json b/docs-astro/src/data/commands.json index 6bad518..c615bb5 100644 --- a/docs-astro/src/data/commands.json +++ b/docs-astro/src/data/commands.json @@ -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]" } ] }, diff --git a/scripts/gen-commands.py b/scripts/gen-commands.py index 6eb3135..76112dd 100644 --- a/scripts/gen-commands.py +++ b/scripts/gen-commands.py @@ -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", diff --git a/src/rdc/_skills/references/commands-quick-ref.md b/src/rdc/_skills/references/commands-quick-ref.md index 68df33f..9298e85 100644 --- a/src/rdc/_skills/references/commands-quick-ref.md +++ b/src/rdc/_skills/references/commands-quick-ref.md @@ -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. diff --git a/src/rdc/cli.py b/src/rdc/cli.py index fcab73d..3a47b9a 100644 --- a/src/rdc/cli.py +++ b/src/rdc/cli.py @@ -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 @@ -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") diff --git a/src/rdc/commands/descriptors.py b/src/rdc/commands/descriptors.py new file mode 100644 index 0000000..769af9f --- /dev/null +++ b/src/rdc/commands/descriptors.py @@ -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, + ) diff --git a/src/rdc/commands/resources.py b/src/rdc/commands/resources.py index 94abebb..9e4ea04 100644 --- a/src/rdc/commands/resources.py +++ b/src/rdc/commands/resources.py @@ -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, diff --git a/src/rdc/commands/vfs.py b/src/rdc/commands/vfs.py index 9e2f74e..95527ec 100644 --- a/src/rdc/commands/vfs.py +++ b/src/rdc/commands/vfs.py @@ -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", []) ) ), diff --git a/src/rdc/handlers/descriptor.py b/src/rdc/handlers/descriptor.py index 5d866f3..63df598 100644 --- a/src/rdc/handlers/descriptor.py +++ b/src/rdc/handlers/descriptor.py @@ -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]: @@ -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: diff --git a/src/rdc/handlers/query.py b/src/rdc/handlers/query.py index f19cebc..fa7e676 100644 --- a/src/rdc/handlers/query.py +++ b/src/rdc/handlers/query.py @@ -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]: @@ -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 @@ -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( diff --git a/tests/unit/test_descriptors_commands.py b/tests/unit/test_descriptors_commands.py new file mode 100644 index 0000000..1fbcc50 --- /dev/null +++ b/tests/unit/test_descriptors_commands.py @@ -0,0 +1,71 @@ +"""Tests for the descriptors CLI command.""" + +from __future__ import annotations + +from typing import Any + +from click.testing import CliRunner + +from rdc.cli import main + + +def _result() -> dict[str, Any]: + return { + "eid": 16, + "descriptors": [ + { + "eid": 16, + "stage": "Pixel", + "binding": "g_textures", + "type": "Image", + "set": 0, + "array_element": 46, + "resource_id": 371, + "resource_name": "2D Image 371", + "format": "BC1_SRGB", + "width": 512, + "height": 512, + } + ], + } + + +def test_descriptors_table(monkeypatch) -> None: + monkeypatch.setattr("rdc.commands.descriptors.call", lambda m, p: _result()) + res = CliRunner().invoke(main, ["descriptors", "16"]) + assert res.exit_code == 0 + assert "BINDING" in res.output + assert "g_textures" in res.output + assert "BC1_SRGB" in res.output + assert "512" in res.output + + +def test_descriptors_binding_filter_forwarded(monkeypatch) -> None: + captured: dict[str, Any] = {} + + def fake_call(method: str, params: dict[str, Any]) -> dict[str, Any]: + captured.update(params) + return _result() + + monkeypatch.setattr("rdc.commands.descriptors.call", fake_call) + res = CliRunner().invoke( + main, ["descriptors", "16", "--binding", "g_textures", "--stage", "ps"] + ) + assert res.exit_code == 0 + assert captured["binding"] == "g_textures" + assert captured["stage"] == "ps" + assert captured["eid"] == 16 + + +def test_descriptors_quiet_emits_resource_ids(monkeypatch) -> None: + monkeypatch.setattr("rdc.commands.descriptors.call", lambda m, p: _result()) + res = CliRunner().invoke(main, ["descriptors", "16", "-q"]) + assert res.exit_code == 0 + assert res.output.strip() == "371" + + +def test_descriptors_json(monkeypatch) -> None: + monkeypatch.setattr("rdc.commands.descriptors.call", lambda m, p: _result()) + res = CliRunner().invoke(main, ["descriptors", "16", "--json"]) + assert res.exit_code == 0 + assert '"binding": "g_textures"' in res.output diff --git a/tests/unit/test_descriptors_daemon.py b/tests/unit/test_descriptors_daemon.py index 806bef4..e90a411 100644 --- a/tests/unit/test_descriptors_daemon.py +++ b/tests/unit/test_descriptors_daemon.py @@ -15,6 +15,8 @@ MockPipeState, ResourceId, SamplerDescriptor, + ShaderReflection, + ShaderResource, ShaderStage, UsedDescriptor, ) @@ -191,3 +193,139 @@ def test_descriptors_eid_out_of_range() -> None: state = _make_state_with_pipe(pipe, max_eid=10) resp = _call(state, "descriptors", eid=999) assert resp["error"]["code"] == -32002 + + +def _ps_with_textures_binding() -> MockPipeState: + pipe = MockPipeState() + pipe._reflections[ShaderStage.Pixel] = ShaderReflection( + readOnlyResources=[ + ShaderResource(name="g_textures", fixedBindNumber=0, fixedBindSetOrSpace=0) + ], + ) + pipe._used_descriptors = [ + UsedDescriptor( + access=DescriptorAccess( + stage=ShaderStage.Pixel, + type=DescriptorType.Image, + index=0, + arrayElement=46, + ), + descriptor=Descriptor(resource=ResourceId(371), byteSize=0), + ), + ] + return pipe + + +def test_descriptors_binding_name_correlation() -> None: + """A used image element is correlated to its reflection binding name and resource.""" + pipe = _ps_with_textures_binding() + state = _make_state_with_pipe(pipe) + state.res_names = {371: "2D Image 371"} + state.tex_map = { + 371: SimpleNamespace( + width=512, height=512, format=SimpleNamespace(Name=lambda: "BC1_SRGB"), byteSize=174776 + ) + } + d = _call(state, "descriptors", eid=16)["result"]["descriptors"][0] + assert d["binding"] == "g_textures" + assert d["set"] == 0 + assert d["array_element"] == 46 + assert d["resource_id"] == 371 + assert d["resource_name"] == "2D Image 371" + assert d["width"] == 512 + assert d["height"] == 512 + + +def test_descriptors_binding_empty_without_reflection() -> None: + """With no shader reflection the binding is empty but the row is intact.""" + pipe = MockPipeState() + pipe._used_descriptors = [ + UsedDescriptor( + access=DescriptorAccess( + stage=ShaderStage.Pixel, type=DescriptorType.Image, index=0, arrayElement=46 + ), + descriptor=Descriptor(resource=ResourceId(371)), + ), + ] + state = _make_state_with_pipe(pipe) + d = _call(state, "descriptors", eid=16)["result"]["descriptors"][0] + assert d["binding"] == "" + assert d["set"] == "-" + assert d["resource_id"] == 371 + + +def test_descriptors_no_cross_set_collision() -> None: + """Bindings sharing a bind number across sets resolve to distinct names. + + Correlation is by reflection index (DescriptorAccess.index), so set=1/binding=0 and + set=2/binding=0 never collapse onto one another. + """ + pipe = MockPipeState() + pipe._reflections[ShaderStage.Pixel] = ShaderReflection( + readOnlyResources=[ + ShaderResource(name="g_textures", fixedBindNumber=0, fixedBindSetOrSpace=1), + ShaderResource(name="materialTex", fixedBindNumber=0, fixedBindSetOrSpace=2), + ], + ) + pipe._used_descriptors = [ + UsedDescriptor( + access=DescriptorAccess( + stage=ShaderStage.Pixel, type=DescriptorType.Image, index=0, arrayElement=46 + ), + descriptor=Descriptor(resource=ResourceId(371)), + ), + UsedDescriptor( + access=DescriptorAccess( + stage=ShaderStage.Pixel, type=DescriptorType.Image, index=1, arrayElement=0 + ), + descriptor=Descriptor(resource=ResourceId(500)), + ), + ] + state = _make_state_with_pipe(pipe) + rows = _call(state, "descriptors", eid=0)["result"]["descriptors"] + by_res = {d["resource_id"]: (d["binding"], d["set"]) for d in rows} + assert by_res[371] == ("g_textures", 1) + assert by_res[500] == ("materialTex", 2) + + +def test_descriptors_filters() -> None: + """stage/type/binding filters narrow the returned descriptors.""" + pipe = MockPipeState() + pipe._reflections[ShaderStage.Pixel] = ShaderReflection( + readOnlyResources=[ShaderResource(name="g_textures", fixedBindNumber=0)], + ) + pipe._used_descriptors = [ + UsedDescriptor( + access=DescriptorAccess( + stage=ShaderStage.Vertex, type=DescriptorType.ConstantBuffer, index=0 + ), + descriptor=Descriptor(resource=ResourceId(1)), + ), + UsedDescriptor( + access=DescriptorAccess( + stage=ShaderStage.Pixel, type=DescriptorType.Image, index=0, arrayElement=46 + ), + descriptor=Descriptor(resource=ResourceId(371)), + ), + UsedDescriptor( + access=DescriptorAccess( + stage=ShaderStage.Pixel, type=DescriptorType.Image, index=0, arrayElement=47 + ), + descriptor=Descriptor(resource=ResourceId(379)), + ), + ] + state = _make_state_with_pipe(pipe) + + by_stage = _call(state, "descriptors", eid=0, stage="ps")["result"]["descriptors"] + assert len(by_stage) == 2 + assert all(d["stage"] == "Pixel" for d in by_stage) + + by_type = _call(state, "descriptors", eid=0, type="image")["result"]["descriptors"] + assert len(by_type) == 2 + assert all(d["type"] == "Image" for d in by_type) + + by_name = _call(state, "descriptors", eid=0, binding="g_textures")["result"]["descriptors"] + assert len(by_name) == 2 + + by_number = _call(state, "descriptors", eid=0, binding="0")["result"]["descriptors"] + assert len(by_number) == 2 diff --git a/tests/unit/test_formatters.py b/tests/unit/test_formatters.py index 64fa570..4bb8b1f 100644 --- a/tests/unit/test_formatters.py +++ b/tests/unit/test_formatters.py @@ -302,7 +302,11 @@ def test_resources_table(self, monkeypatch) -> None: _patch(monkeypatch, "resources", _RESOURCES) result = CliRunner().invoke(main, ["resources"]) assert result.exit_code == 0 - assert result.output == "ID\tTYPE\tNAME\n10\tTexture\talbedo\n20\tBuffer\tverts\n" + assert result.output == ( + "ID\tTYPE\tNAME\tWIDTH\tHEIGHT\tFORMAT\tSIZE\n" + "10\tTexture\talbedo\t-\t-\t-\t-\n" + "20\tBuffer\tverts\t-\t-\t-\t-\n" + ) def test_counters_quiet(self, monkeypatch) -> None: _patch(monkeypatch, "counters", _COUNTERS) diff --git a/tests/unit/test_resources_commands.py b/tests/unit/test_resources_commands.py index 4c24c1b..cb77cf9 100644 --- a/tests/unit/test_resources_commands.py +++ b/tests/unit/test_resources_commands.py @@ -24,6 +24,30 @@ def test_resources_tsv(monkeypatch) -> None: assert "Texture" in result.output +def test_resources_enriched_columns(monkeypatch) -> None: + patch_cli_session( + monkeypatch, + { + "rows": [ + { + "id": 371, + "type": "Texture", + "name": "2D Image 371", + "width": 512, + "height": 512, + "format": "BC1_SRGB", + "size": 174776, + } + ] + }, + ) + result = CliRunner().invoke(resources_cmd, []) + assert result.exit_code == 0 + assert "ID\tTYPE\tNAME\tWIDTH\tHEIGHT\tFORMAT\tSIZE" in result.output + assert "512" in result.output + assert "BC1_SRGB" in result.output + + def test_resources_json(monkeypatch) -> None: patch_cli_session( monkeypatch, diff --git a/tests/unit/test_resources_filter.py b/tests/unit/test_resources_filter.py index 3df8e61..ded848e 100644 --- a/tests/unit/test_resources_filter.py +++ b/tests/unit/test_resources_filter.py @@ -246,15 +246,12 @@ class TestResourcesCLI: def _patch(self, monkeypatch: pytest.MonkeyPatch, rows: list[dict[str, Any]]) -> None: _patch_resources(monkeypatch, {"rows": rows}) - def test_no_options_three_column_header(self, monkeypatch: pytest.MonkeyPatch) -> None: + def test_no_options_header(self, monkeypatch: pytest.MonkeyPatch) -> None: self._patch(monkeypatch, self._BASE_ROWS) result = CliRunner().invoke(resources_cmd, []) assert result.exit_code == 0 - assert "ID" in result.output - assert "TYPE" in result.output - assert "NAME" in result.output - for ghost in ("WIDTH", "HEIGHT", "DEPTH", "FORMAT"): - assert ghost not in result.output + for col in ("ID", "TYPE", "NAME", "WIDTH", "HEIGHT", "FORMAT", "SIZE"): + assert col in result.output def test_type_option_forwarded(self, monkeypatch: pytest.MonkeyPatch) -> None: import rdc.commands._helpers as mod @@ -347,3 +344,41 @@ def test_no_session_exits_nonzero(self, monkeypatch: pytest.MonkeyPatch) -> None monkeypatch.setattr(mod, "load_session", lambda: None) result = CliRunner().invoke(resources_cmd, []) assert result.exit_code != 0 + + +# --------------------------------------------------------------------------- +# TestResourceEnrichment — dimensions/format/size enrichment +# --------------------------------------------------------------------------- + + +def _tex(width: int, height: int, fmt: str, size: int) -> Any: + return SimpleNamespace( + width=width, height=height, format=SimpleNamespace(Name=lambda: fmt), byteSize=size + ) + + +class TestResourceEnrichment: + def test_resource_detail_enriched_with_dims(self) -> None: + state = _state_with_resources([_make_res(371, "2D Image 371", "Texture")]) + state.tex_map = {371: _tex(512, 512, "BC1_SRGB", 174776)} + resp, _ = _handle_request(rpc_request("resource", {"id": 371}), state) + r = resp["result"]["resource"] + assert r["width"] == 512 + assert r["height"] == 512 + assert r["format"] == "BC1_SRGB" + assert r["size"] == 174776 + + def test_resources_list_enriched_with_dims(self) -> None: + state = _state_with_resources([_make_res(371, "2D Image 371", "Texture")]) + state.tex_map = {371: _tex(512, 512, "BC1_SRGB", 174776)} + resp, _ = _handle_request(rpc_request("resources"), state) + row = resp["result"]["rows"][0] + assert row["width"] == 512 + assert row["format"] == "BC1_SRGB" + assert row["size"] == 174776 + + def test_buffer_resource_gets_size(self) -> None: + state = _state_with_resources([_make_res(99, "vtx buffer", "Buffer")]) + state.buf_map = {99: SimpleNamespace(length=4096)} + resp, _ = _handle_request(rpc_request("resource", {"id": 99}), state) + assert resp["result"]["resource"]["size"] == 4096 diff --git a/tests/unit/test_vfs_commands.py b/tests/unit/test_vfs_commands.py index 6a09a6a..a40415e 100644 --- a/tests/unit/test_vfs_commands.py +++ b/tests/unit/test_vfs_commands.py @@ -360,21 +360,29 @@ def test_cat_descriptors(monkeypatch) -> None: "descriptors": [ { "stage": "Vertex", + "binding": "g_push", "type": "ConstantBuffer", + "set": 0, "index": 0, "array_element": 0, "resource_id": 42, + "resource_name": "", "format": "", "byte_size": 256, }, { "stage": "Pixel", - "type": "ConstantBuffer", + "binding": "g_textures", + "type": "Image", + "set": 0, "index": 0, - "array_element": 0, - "resource_id": 43, - "format": "", - "byte_size": 128, + "array_element": 46, + "resource_id": 371, + "resource_name": "2D Image 371", + "format": "BC1_SRGB", + "byte_size": 0, + "width": 512, + "height": 512, }, ], }, @@ -388,9 +396,14 @@ def test_cat_descriptors(monkeypatch) -> None: result = CliRunner().invoke(cat_cmd, ["/draws/5/descriptors"]) assert result.exit_code == 0 lines = result.output.strip().split("\n") - assert lines[0] == "STAGE\tTYPE\tINDEX\tARRAY_EL\tRESOURCE\tFORMAT\tBYTE_SIZE" + # Original columns kept in place (cut -f compatible); new columns appended on the right. + assert lines[0] == ( + "STAGE\tTYPE\tINDEX\tARRAY_EL\tRESOURCE\tFORMAT\tBYTE_SIZE\tBINDING\tSET\tRES_NAME\tWIDTH\tHEIGHT" + ) assert len(lines) == 3 - assert "Vertex\tConstantBuffer\t0\t0\t42\t\t256" in lines[1] + assert lines[1].startswith("Vertex\tConstantBuffer\t0\t0\t42\t\t256\tg_push") + assert "g_textures" in lines[2] + assert "512" in lines[2] def test_cat_descriptors_empty(monkeypatch) -> None: @@ -409,7 +422,9 @@ def test_cat_descriptors_empty(monkeypatch) -> None: result = CliRunner().invoke(cat_cmd, ["/draws/0/descriptors"]) assert result.exit_code == 0 lines = result.output.strip().split("\n") - assert lines[0] == "STAGE\tTYPE\tINDEX\tARRAY_EL\tRESOURCE\tFORMAT\tBYTE_SIZE" + assert lines[0] == ( + "STAGE\tTYPE\tINDEX\tARRAY_EL\tRESOURCE\tFORMAT\tBYTE_SIZE\tBINDING\tSET\tRES_NAME\tWIDTH\tHEIGHT" + ) assert len(lines) == 1