From fe10bf89bd391041d78fa670c731e24151b414cd Mon Sep 17 00:00:00 2001 From: BANANASJIM Date: Tue, 7 Jul 2026 01:53:51 -0700 Subject: [PATCH] fix(shader): decode reflected value lanes --- .gitignore | 2 + src/rdc/handlers/_helpers.py | 61 +++++++++++-- src/rdc/handlers/buffer.py | 22 ++--- src/rdc/handlers/debug.py | 18 ++-- tests/mocks/mock_renderdoc.py | 10 ++- tests/unit/test_buffer_decode.py | 108 +++++++++++++++++++++++ tests/unit/test_daemon_shader_api_fix.py | 57 ++++++++++++ tests/unit/test_debug_handlers.py | 53 ++++++++++- 8 files changed, 296 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index db62593f..f49577f1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ tests/fixtures/vulkan_samples .claude/* !.claude/skills/ _site/ +docs-astro/.astro/ docs-astro/dist/ +docs-astro/node_modules/ diff --git a/src/rdc/handlers/_helpers.py b/src/rdc/handlers/_helpers.py index 9a5a1f6d..80bfa32e 100644 --- a/src/rdc/handlers/_helpers.py +++ b/src/rdc/handlers/_helpers.py @@ -508,6 +508,58 @@ def get_default_disasm_target(controller: Any) -> str: return str(targets[0]) if targets else "SPIR-V" +def _shader_value_lane_name(var_type: Any) -> str: + """Return the ShaderValue lane name for a reflected variable type.""" + if isinstance(var_type, int): + return { + 0: "f32v", + 1: "f64v", + 2: "f16v", + 3: "s32v", + 4: "u32v", + 5: "s16v", + 6: "u16v", + 7: "s64v", + 8: "u64v", + 9: "s8v", + 10: "u8v", + 11: "u32v", + }.get(var_type, "f32v") + + type_name = getattr(var_type, "name", var_type) + type_str = str(type_name).lower() + if "double" in type_str or type_str in {"f64", "float64"}: + return "f64v" + if "half" in type_str or type_str in {"f16", "float16"}: + return "f16v" + if "uint64" in type_str or "ulong" in type_str or type_str == "u64": + return "u64v" + if "uint16" in type_str or "ushort" in type_str or type_str == "u16": + return "u16v" + if "uint8" in type_str or "ubyte" in type_str or type_str == "u8": + return "u8v" + if "uint" in type_str or type_str in {"u32", "uint32"}: + return "u32v" + if "int64" in type_str or "slong" in type_str or type_str in {"s64", "long"}: + return "s64v" + if "int16" in type_str or "sshort" in type_str or type_str in {"s16", "short"}: + return "s16v" + if "int8" in type_str or "sbyte" in type_str or type_str == "s8": + return "s8v" + if "sint" in type_str or "int" in type_str or type_str in {"s32", "int32"}: + return "s32v" + if "bool" in type_str: + return "u32v" + return "f32v" + + +def _shader_value_lane_fallback(lane_name: str) -> list[float | int]: + """Return a type-stable fallback for a missing ShaderValue lane.""" + if lane_name.startswith(("u", "s")): + return [0] * 16 + return [0.0] * 16 + + def _flatten_shader_var(var: Any) -> dict[str, Any]: """Recursively convert a ShaderVariable to a dict.""" members = getattr(var, "members", []) @@ -529,13 +581,8 @@ def _flatten_shader_var(var: Any) -> dict[str, Any]: if val is None: values: list[Any] = [] else: - type_str = str(getattr(var, "type", "")).lower() - if "uint" in type_str: - values = list(getattr(val, "u32v", [0.0] * 16)[:count]) - elif "int" in type_str or "sint" in type_str: - values = list(getattr(val, "s32v", [0] * 16)[:count]) - else: - values = list(getattr(val, "f32v", [0.0] * 16)[:count]) + lane_name = _shader_value_lane_name(getattr(var, "type", "")) + values = list(getattr(val, lane_name, _shader_value_lane_fallback(lane_name))[:count]) return { "name": var.name, diff --git a/src/rdc/handlers/buffer.py b/src/rdc/handlers/buffer.py index 594b32c5..3132f6d7 100644 --- a/src/rdc/handlers/buffer.py +++ b/src/rdc/handlers/buffer.py @@ -12,6 +12,8 @@ _error_response, _result_response, _set_frame_event, + _shader_value_lane_fallback, + _shader_value_lane_name, get_pipeline_for_stage, require_pipe, ) @@ -46,20 +48,7 @@ def _decode_index_buffer(data: bytes, stride: int) -> list[int]: def _shader_variable_value_kind(var_type: Any) -> str: """Return the ShaderValue lane name for a reflected variable type.""" - if isinstance(var_type, int): - if var_type == 4: - return "u32v" - if var_type == 5: - return "s32v" - return "f32v" - - type_name = getattr(var_type, "name", var_type) - type_str = str(type_name).lower() - if "uint" in type_str or type_str in {"u32", "uint32"}: - return "u32v" - if "sint" in type_str or "int" in type_str or type_str in {"s32", "int32"}: - return "s32v" - return "f32v" + return _shader_value_lane_name(var_type) def _shader_variable_values(var: Any) -> Any: @@ -69,9 +58,8 @@ def _shader_variable_values(var: Any) -> Any: return None r = getattr(var, "rows", 1) or 1 c = getattr(var, "columns", 1) or 1 - lane = getattr(val, _shader_variable_value_kind(getattr(var, "type", "")), None) - if lane is None: - return val + lane_name = _shader_variable_value_kind(getattr(var, "type", "")) + lane = getattr(val, lane_name, _shader_value_lane_fallback(lane_name)) values = [lane[ri * c + ci] for ri in range(r) for ci in range(c)] return values if len(values) > 1 else values[0] diff --git a/src/rdc/handlers/debug.py b/src/rdc/handlers/debug.py index 12f163c1..5fdd3b04 100644 --- a/src/rdc/handlers/debug.py +++ b/src/rdc/handlers/debug.py @@ -11,6 +11,8 @@ _get_flat_actions, _result_response, _set_frame_event, + _shader_value_lane_fallback, + _shader_value_lane_name, ) from rdc.handlers._types import Handler @@ -29,21 +31,19 @@ def _format_var_value(var: Any) -> list[float | int]: val = var.value if val is None: return [0.0] * count - var_type = str(getattr(var, "type", "float")).lower() - if "uint" in var_type or "u32" in var_type: - return list(val.u32v[:count]) - if "int" in var_type or "s32" in var_type or "sint" in var_type: - return list(val.s32v[:count]) - return list(val.f32v[:count]) + lane_name = _shader_value_lane_name(getattr(var, "type", "float")) + return list(getattr(val, lane_name, _shader_value_lane_fallback(lane_name))[:count]) def _format_var_type(var: Any) -> str: """Return a human-readable type string for a ShaderVariable.""" - t = str(getattr(var, "type", "float")).lower() - if "uint" in t or "u32" in t: + lane_name = _shader_value_lane_name(getattr(var, "type", "float")) + if lane_name.startswith("u"): return "uint" - if "int" in t or "s32" in t or "sint" in t: + if lane_name.startswith("s"): return "int" + if lane_name == "f64v": + return "double" return "float" diff --git a/tests/mocks/mock_renderdoc.py b/tests/mocks/mock_renderdoc.py index e78d4638..55ef43c7 100644 --- a/tests/mocks/mock_renderdoc.py +++ b/tests/mocks/mock_renderdoc.py @@ -1000,11 +1000,19 @@ class MeshFormat: @dataclass class ShaderValue: - """Mock for ShaderValue union (real API has f32v, u32v, s32v, f64v).""" + """Mock for ShaderValue union lanes used by RenderDoc.""" + f16v: list[float] = field(default_factory=lambda: [0.0] * 16) f32v: list[float] = field(default_factory=lambda: [0.0] * 16) + f64v: list[float] = field(default_factory=lambda: [0.0] * 16) + u8v: list[int] = field(default_factory=lambda: [0] * 16) + s8v: list[int] = field(default_factory=lambda: [0] * 16) + u16v: list[int] = field(default_factory=lambda: [0] * 16) + s16v: list[int] = field(default_factory=lambda: [0] * 16) u32v: list[int] = field(default_factory=lambda: [0] * 16) s32v: list[int] = field(default_factory=lambda: [0] * 16) + u64v: list[int] = field(default_factory=lambda: [0] * 16) + s64v: list[int] = field(default_factory=lambda: [0] * 16) @dataclass diff --git a/tests/unit/test_buffer_decode.py b/tests/unit/test_buffer_decode.py index 28c31bdc..0bcbb2f0 100644 --- a/tests/unit/test_buffer_decode.py +++ b/tests/unit/test_buffer_decode.py @@ -321,6 +321,114 @@ def test_uint_numeric_type_extracts_u32v(self, state: DaemonState) -> None: assert r["variables"][0]["type"] == "4" assert r["variables"][0]["value"] == [13, 17, 19, 23] + def test_uint_missing_lane_uses_int_fallback(self, state: DaemonState) -> None: + """Missing integer lanes return integer zeros, not raw value objects.""" + state.adapter.controller.GetCBufferVariableContents = lambda *args: [ + ShaderVariable( + name="missingUint", + type=4, + rows=1, + columns=2, + value=SimpleNamespace(), + ) + ] + resp, _ = _handle_request( + rpc_request( + "cbuffer_decode", {"eid": 10, "set": 0, "binding": 0}, token="abcdef1234567890" + ), + state, + ) + value = resp["result"]["variables"][0]["value"] + assert value == [0, 0] + assert all(type(item) is int for item in value) + + def test_64_bit_integer_numeric_types_use_64_bit_lanes(self, state: DaemonState) -> None: + """RenderDoc exposes signed/unsigned 64-bit ints as numeric types 7 and 8.""" + state.adapter.controller.GetCBufferVariableContents = lambda *args: [ + ShaderVariable( + name="signedLong", + type=7, + rows=1, + columns=1, + value=ShaderValue(s64v=[-9_000_000_000] + [0] * 15), + ), + ShaderVariable( + name="unsignedLong", + type=8, + rows=1, + columns=1, + value=ShaderValue(u64v=[18_000_000_000] + [0] * 15), + ), + ] + resp, _ = _handle_request( + rpc_request( + "cbuffer_decode", {"eid": 10, "set": 0, "binding": 0}, token="abcdef1234567890" + ), + state, + ) + variables = resp["result"]["variables"] + assert variables[0]["value"] == -9_000_000_000 + assert variables[1]["value"] == 18_000_000_000 + + @pytest.mark.parametrize( + ("var_type", "lane_name"), + [ + (9, "s8v"), + (10, "u8v"), + ("sbyte", "s8v"), + ("ubyte", "u8v"), + ], + ) + def test_byte_integer_types_use_byte_lanes(self, var_type: object, lane_name: str) -> None: + """Byte-sized reflected variables map to byte ShaderValue lanes.""" + from rdc.handlers._helpers import _shader_value_lane_name + + assert _shader_value_lane_name(var_type) == lane_name + + def test_double_numeric_type_extracts_f64v(self, state: DaemonState) -> None: + """RenderDoc exposes double cbuffer variables as numeric type 1.""" + double_val = ShaderValue(f32v=[0.0] * 16, f64v=[1.25, 2.5] + [0.0] * 14) + state.adapter.controller.GetCBufferVariableContents = lambda *args: [ + ShaderVariable( + name="clipRange", + type=1, + rows=1, + columns=2, + value=double_val, + ) + ] + resp, _ = _handle_request( + rpc_request( + "cbuffer_decode", {"eid": 10, "set": 0, "binding": 0}, token="abcdef1234567890" + ), + state, + ) + r = resp["result"] + assert r["variables"][0]["type"] == "1" + assert r["variables"][0]["value"] == [1.25, 2.5] + + def test_double_named_type_extracts_f64v(self, state: DaemonState) -> None: + """Mocks and adapters may expose reflected double types by name.""" + double_val = ShaderValue(f32v=[0.0] * 16, f64v=[3.5] + [0.0] * 15) + state.adapter.controller.GetCBufferVariableContents = lambda *args: [ + ShaderVariable( + name="exposure", + type="double", + rows=1, + columns=1, + value=double_val, + ) + ] + resp, _ = _handle_request( + rpc_request( + "cbuffer_decode", {"eid": 10, "set": 0, "binding": 0}, token="abcdef1234567890" + ), + state, + ) + r = resp["result"] + assert r["variables"][0]["type"] == "double" + assert r["variables"][0]["value"] == 3.5 + class TestCbufferRaw: def test_stageful_raw_data_node_visible_to_vfs_ls(self, state: DaemonState) -> None: diff --git a/tests/unit/test_daemon_shader_api_fix.py b/tests/unit/test_daemon_shader_api_fix.py index 63e25c13..6694c7b7 100644 --- a/tests/unit/test_daemon_shader_api_fix.py +++ b/tests/unit/test_daemon_shader_api_fix.py @@ -2,6 +2,8 @@ from __future__ import annotations +from types import SimpleNamespace + import mock_renderdoc as rd from conftest import make_daemon_state, rpc_request @@ -236,6 +238,61 @@ def test_shader_constants_returns_structured_variables() -> None: assert "data" not in r["constants"][0] +def test_shader_constants_double_variable_uses_f64v() -> None: + """Double ShaderVariable values are flattened from f64v.""" + ctrl = rd.MockReplayController() + ps_id = rd.ResourceId(101) + ctrl._pipe_state._shaders[rd.ShaderStage.Pixel] = ps_id + ctrl._pipe_state._entry_points[rd.ShaderStage.Pixel] = "main_ps" + ctrl._pipe_state._reflections[rd.ShaderStage.Pixel] = rd.ShaderReflection( + resourceId=ps_id, + entryPoint="main_ps", + constantBlocks=[rd.ConstantBlock(name="Globals", fixedBindNumber=0)], + ) + ctrl._pipe_state._cbuffer_descriptors[(rd.ShaderStage.Pixel, 0)] = rd.Descriptor( + resource=rd.ResourceId(500), + ) + ctrl._cbuffer_variables[(rd.ShaderStage.Pixel, 0)] = [ + rd.ShaderVariable( + name="g_Exposure", + type=1, + rows=1, + columns=1, + value=rd.ShaderValue(f32v=[0.0] * 16, f64v=[4.25] + [0.0] * 15), + ), + ] + ctrl._actions = [rd.ActionDescription(eventId=10, flags=rd.ActionFlags.Drawcall)] + + state = DaemonState(capture="x.rdc", current_eid=0, token="tok") + state.adapter = RenderDocAdapter(controller=ctrl, version=(1, 41)) + state.max_eid = 100 + + resp, _ = _handle_request(rpc_request("shader_constants", {"eid": 10, "stage": "ps"}), state) + r = resp["result"] + v = r["constants"][0]["variables"][0] + assert v["name"] == "g_Exposure" + assert v["type"] == "1" + assert v["value"] == [4.25] + + +def test_flatten_shader_var_integer_missing_lane_uses_int_fallback() -> None: + """Missing integer lanes keep integer zero values in flattened output.""" + from rdc.handlers._helpers import _flatten_shader_var + + var = SimpleNamespace( + name="missing_uint", + type=4, + rows=1, + columns=2, + value=SimpleNamespace(), + ) + + result = _flatten_shader_var(var) + + assert result["value"] == [0, 0] + assert all(type(value) is int for value in result["value"]) + + def test_shader_constants_struct_variable_recurses() -> None: """Struct ShaderVariable with members recurses into children.""" ctrl = rd.MockReplayController() diff --git a/tests/unit/test_debug_handlers.py b/tests/unit/test_debug_handlers.py index cf6e054f..3313fb9b 100644 --- a/tests/unit/test_debug_handlers.py +++ b/tests/unit/test_debug_handlers.py @@ -2,6 +2,8 @@ from __future__ import annotations +from types import SimpleNamespace + import mock_renderdoc as rd from conftest import make_daemon_state, rpc_request @@ -10,15 +12,19 @@ def _make_var( name: str = "x", - var_type: str = "float", + var_type: object = "float", rows: int = 1, columns: int = 4, f32v: list[float] | None = None, + f64v: list[float] | None = None, + u64v: list[int] | None = None, u32v: list[int] | None = None, s32v: list[int] | None = None, ) -> rd.ShaderVariable: val = rd.ShaderValue( f32v=(f32v or [0.0] * 16), + f64v=(f64v or [0.0] * 16), + u64v=(u64v or [0] * 16), u32v=(u32v or [0] * 16), s32v=(s32v or [0] * 16), ) @@ -323,6 +329,51 @@ def test_format_var_value_sint() -> None: assert result == [-7] +def test_format_var_value_double() -> None: + """Double scalar extracts from f64v.""" + from rdc.handlers.debug import _format_var_type, _format_var_value + + var = _make_var("d", "double", rows=1, columns=1, f64v=[6.25] + [0.0] * 15) + assert _format_var_value(var) == [6.25] + assert _format_var_type(var) == "double" + + +def test_format_var_value_numeric_double() -> None: + """Numeric RenderDoc double type extracts from f64v.""" + from rdc.handlers.debug import _format_var_type, _format_var_value + + var = _make_var("d", 1, rows=1, columns=1, f64v=[7.5] + [0.0] * 15) + assert _format_var_value(var) == [7.5] + assert _format_var_type(var) == "double" + + +def test_format_var_value_numeric_u64() -> None: + """Numeric RenderDoc unsigned 64-bit type extracts from u64v.""" + from rdc.handlers.debug import _format_var_type, _format_var_value + + var = _make_var("u64", 8, rows=1, columns=1, u64v=[18_000_000_000] + [0] * 15) + assert _format_var_value(var) == [18_000_000_000] + assert _format_var_type(var) == "uint" + + +def test_format_var_value_missing_integer_lane_uses_int_fallback() -> None: + """Missing integer lanes keep integer zero values in debug output.""" + from rdc.handlers.debug import _format_var_value + + var = rd.ShaderVariable( + name="u", + type=4, + rows=1, + columns=2, + value=SimpleNamespace(), + ) + + result = _format_var_value(var) + + assert result == [0, 0] + assert all(type(value) is int for value in result) + + def test_format_var_value_none() -> None: """None value returns zeros.""" from rdc.handlers.debug import _format_var_value