Skip to content

Commit 61a3ac2

Browse files
authored
fix(handlers): align GetConstantBlock/push constants with real RenderDoc API (B29-B31) (#129)
- GetConstantBlock returns UsedDescriptor, not bare Descriptor (B30) - Unwrap .descriptor in shader_constants, cbuffer_decode handlers - Rewrite pipe_push_constants: iterate constantBlocks with bufferBacked=False instead of non-existent pushConstantRangeByteOffset/Size (B29) - Expose raw push constant bytes as hex-encoded raw_bytes field (B31) - Move _flatten_shader_var to _helpers.py for reuse across handlers
1 parent 48bf0cf commit 61a3ac2

6 files changed

Lines changed: 170 additions & 81 deletions

File tree

src/rdc/handlers/_helpers.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,44 @@ def get_default_disasm_target(controller: Any) -> str:
272272
return str(targets[0]) if targets else "SPIR-V"
273273

274274

275+
def _flatten_shader_var(var: Any) -> dict[str, Any]:
276+
"""Recursively convert a ShaderVariable to a dict."""
277+
members = getattr(var, "members", [])
278+
if members:
279+
return {
280+
"name": var.name,
281+
"type": str(getattr(var, "type", "")),
282+
"rows": getattr(var, "rows", 0),
283+
"columns": getattr(var, "columns", 0),
284+
"value": None,
285+
"members": [_flatten_shader_var(m) for m in members],
286+
}
287+
288+
rows = getattr(var, "rows", 0)
289+
columns = getattr(var, "columns", 0)
290+
count = max(rows * columns, 1)
291+
292+
val = getattr(var, "value", None)
293+
if val is None:
294+
values: list[Any] = []
295+
else:
296+
type_str = str(getattr(var, "type", "")).lower()
297+
if "uint" in type_str:
298+
values = list(getattr(val, "u32v", [0.0] * 16)[:count])
299+
elif "int" in type_str or "sint" in type_str:
300+
values = list(getattr(val, "s32v", [0] * 16)[:count])
301+
else:
302+
values = list(getattr(val, "f32v", [0.0] * 16)[:count])
303+
304+
return {
305+
"name": var.name,
306+
"type": str(getattr(var, "type", "")),
307+
"rows": rows,
308+
"columns": columns,
309+
"value": values,
310+
}
311+
312+
275313
def _resolve_vfs_path(path: str, state: DaemonState) -> tuple[str, str | None]:
276314
"""Normalize VFS path: strip trailing slash, resolve /current alias."""
277315
path = path.rstrip("/") or "/"

src/rdc/handlers/buffer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ def _handle_cbuffer_decode( # noqa: PLR0912
140140
shader = pipe_state.GetShader(stage_val)
141141
entry = pipe_state.GetShaderEntryPoint(stage_val)
142142
if hasattr(pipe_state, "GetConstantBlock"):
143-
cb_desc = pipe_state.GetConstantBlock(stage_val, target_idx, 0)
143+
cb_used = pipe_state.GetConstantBlock(stage_val, target_idx, 0)
144+
cb_desc = cb_used.descriptor
144145
cb_resource = cb_desc.resource
145146
cb_offset = getattr(cb_desc, "byteOffset", 0)
146147
cb_size = getattr(cb_desc, "byteSize", 0)

src/rdc/handlers/pipe_state.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
STAGE_MAP,
1010
_enum_name,
1111
_error_response,
12+
_flatten_shader_var,
1213
_result_response,
1314
_sanitize_size,
1415
_set_frame_event,
16+
get_pipeline_for_stage,
1517
)
1618
from rdc.handlers._types import Handler
1719

@@ -252,18 +254,46 @@ def _handle_pipe_push_constants(
252254
if err:
253255
return _error_response(request_id, -32002, err), True
254256
pipe_state = state.adapter.get_pipeline_state()
255-
ranges: list[dict[str, Any]] = []
257+
controller = state.adapter.controller
258+
push_constants: list[dict[str, Any]] = []
256259
for stage_val, stage_name in _STAGE_NAMES.items():
257-
if int(pipe_state.GetShader(stage_val)) == 0:
260+
shader_id = pipe_state.GetShader(stage_val)
261+
if int(shader_id) == 0:
258262
continue
259263
refl = pipe_state.GetShaderReflection(stage_val)
260264
if refl is None:
261265
continue
262-
offset = getattr(refl, "pushConstantRangeByteOffset", 0)
263-
size = getattr(refl, "pushConstantRangeByteSize", 0)
264-
if size > 0:
265-
ranges.append({"stage": stage_name, "offset": offset, "size": size})
266-
return _result_response(request_id, {"eid": eid, "push_constants": ranges}), True
266+
pipe = get_pipeline_for_stage(pipe_state, stage_val)
267+
entry = pipe_state.GetShaderEntryPoint(stage_val)
268+
for idx, cb in enumerate(getattr(refl, "constantBlocks", [])):
269+
if getattr(cb, "bufferBacked", True):
270+
continue
271+
bound = pipe_state.GetConstantBlock(stage_val, idx, 0)
272+
desc = bound.descriptor
273+
cbuffer_vars = controller.GetCBufferVariableContents(
274+
pipe,
275+
shader_id,
276+
stage_val,
277+
entry,
278+
idx,
279+
desc.resource,
280+
desc.byteOffset,
281+
desc.byteSize,
282+
)
283+
variables = [_flatten_shader_var(v) for v in cbuffer_vars]
284+
push_constants.append(
285+
{
286+
"stage": stage_name,
287+
"name": cb.name,
288+
"size": getattr(cb, "byteSize", 0),
289+
"variables": variables,
290+
}
291+
)
292+
raw = getattr(pipe_state, "pushconsts", b"")
293+
return _result_response(
294+
request_id,
295+
{"eid": eid, "push_constants": push_constants, "raw_bytes": raw.hex()},
296+
), True
267297

268298

269299
def _handle_pipe_rasterizer(

src/rdc/handlers/shader.py

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
STAGE_MAP,
99
_build_shader_cache,
1010
_error_response,
11+
_flatten_shader_var,
1112
_result_response,
1213
_set_frame_event,
1314
get_default_disasm_target,
@@ -99,44 +100,6 @@ def _handle_shader_reflect(
99100
), True
100101

101102

102-
def _flatten_shader_var(var: Any) -> dict[str, Any]:
103-
"""Recursively convert a ShaderVariable to a dict."""
104-
members = getattr(var, "members", [])
105-
if members:
106-
return {
107-
"name": var.name,
108-
"type": str(getattr(var, "type", "")),
109-
"rows": getattr(var, "rows", 0),
110-
"columns": getattr(var, "columns", 0),
111-
"value": None,
112-
"members": [_flatten_shader_var(m) for m in members],
113-
}
114-
115-
rows = getattr(var, "rows", 0)
116-
columns = getattr(var, "columns", 0)
117-
count = max(rows * columns, 1)
118-
119-
val = getattr(var, "value", None)
120-
if val is None:
121-
values: list[Any] = []
122-
else:
123-
type_str = str(getattr(var, "type", "")).lower()
124-
if "uint" in type_str:
125-
values = list(getattr(val, "u32v", [0.0] * 16)[:count])
126-
elif "int" in type_str or "sint" in type_str:
127-
values = list(getattr(val, "s32v", [0] * 16)[:count])
128-
else:
129-
values = list(getattr(val, "f32v", [0.0] * 16)[:count])
130-
131-
return {
132-
"name": var.name,
133-
"type": str(getattr(var, "type", "")),
134-
"rows": rows,
135-
"columns": columns,
136-
"value": values,
137-
}
138-
139-
140103
def _handle_shader_constants(
141104
request_id: int, params: dict[str, Any], state: DaemonState
142105
) -> tuple[dict[str, Any], bool]:
@@ -165,15 +128,16 @@ def _handle_shader_constants(
165128
for idx, cb_def in enumerate(getattr(refl, "constantBlocks", [])):
166129
bind_point = getattr(cb_def, "fixedBindNumber", getattr(cb_def, "bindPoint", 0))
167130
bound = pipe_state.GetConstantBlock(stage_val, idx, 0)
131+
desc = bound.descriptor
168132
cbuffer_vars = controller.GetCBufferVariableContents(
169133
pipe,
170134
shader_id,
171135
stage_val,
172136
entry,
173137
idx,
174-
bound.resource,
175-
bound.byteOffset,
176-
bound.byteSize,
138+
desc.resource,
139+
desc.byteOffset,
140+
desc.byteSize,
177141
)
178142
variables = [_flatten_shader_var(v) for v in cbuffer_vars]
179143
constants.append(

tests/mocks/mock_renderdoc.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,7 @@ def __init__(
12861286
self.rasterizer: RasterizerState | None = None
12871287
self.depthStencil: DepthStencilState | None = None
12881288
self.multisample: MultisampleState = MultisampleState()
1289+
self.pushconsts: bytes = b""
12891290

12901291
def GetShader(self, stage: ShaderStage) -> ResourceId:
12911292
return self._shaders.get(stage, ResourceId.Null())
@@ -1340,9 +1341,10 @@ def GetConstantBlock(
13401341
stage: int,
13411342
slot: int,
13421343
array_idx: int,
1343-
) -> Descriptor:
1344-
"""Mock GetConstantBlock — returns descriptor with cbuffer resource."""
1345-
return self._cbuffer_descriptors.get((stage, slot), Descriptor())
1344+
) -> UsedDescriptor:
1345+
"""Mock GetConstantBlock — returns UsedDescriptor with cbuffer resource."""
1346+
desc = self._cbuffer_descriptors.get((stage, slot), Descriptor())
1347+
return UsedDescriptor(descriptor=desc)
13461348

13471349
def GetAllUsedDescriptors(self, only_used: bool = True) -> list[UsedDescriptor]:
13481350
"""Mock GetAllUsedDescriptors — returns configured used descriptors."""

tests/unit/test_daemon_pipeline_extended.py

Lines changed: 83 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@
1313
from mock_renderdoc import (
1414
ActionDescription,
1515
ActionFlags,
16+
ConstantBlock,
1617
DepthStencilState,
18+
Descriptor,
1719
MockPipeState,
1820
MultisampleState,
1921
RasterizerState,
2022
ResourceDescription,
2123
ResourceId,
2224
ShaderReflection,
2325
ShaderStage,
26+
ShaderValue,
27+
ShaderVariable,
2428
)
2529

2630
from rdc.adapter import RenderDocAdapter
@@ -46,6 +50,20 @@ def _req(method: str, **params: Any) -> dict[str, Any]:
4650
def _make_state(tmp_path: Path, pipe: MockPipeState) -> DaemonState:
4751
actions = _build_actions()
4852
resources = _build_resources()
53+
cbvars: dict[tuple[int, int], list[Any]] = {}
54+
55+
def _get_cbuf(
56+
_pipe: Any,
57+
_sh: Any,
58+
stage: Any,
59+
_e: str,
60+
idx: int,
61+
_r: Any,
62+
_o: int,
63+
_s: int,
64+
) -> list[Any]:
65+
return cbvars.get((int(stage), idx), [])
66+
4967
controller = SimpleNamespace(
5068
GetRootActions=lambda: actions,
5169
GetResources=lambda: resources,
@@ -57,6 +75,8 @@ def _make_state(tmp_path: Path, pipe: MockPipeState) -> DaemonState:
5775
GetBuffers=lambda: [],
5876
GetDebugMessages=lambda: [],
5977
Shutdown=lambda: None,
78+
_cbuffer_variables=cbvars,
79+
GetCBufferVariableContents=_get_cbuf,
6080
)
6181
s = DaemonState(capture="test.rdc", current_eid=0, token="abcdef1234567890")
6282
s.adapter = RenderDocAdapter(controller=controller, version=(1, 41))
@@ -144,57 +164,91 @@ def test_empty_when_no_shaders(self, tmp_path: Path) -> None:
144164
resp, _ = _handle_request(_req("pipe_push_constants", eid=10), s)
145165
assert "error" not in resp
146166
assert resp["result"]["push_constants"] == []
147-
assert resp["result"]["eid"] == 10
167+
assert resp["result"]["raw_bytes"] == ""
148168

149-
def test_empty_when_size_is_zero(self, tmp_path: Path) -> None:
169+
def test_empty_when_all_buffer_backed(self, tmp_path: Path) -> None:
150170
pipe = MockPipeState()
151171
refl = ShaderReflection(
152172
resourceId=ResourceId(5),
153-
pushConstantRangeByteOffset=0,
154-
pushConstantRangeByteSize=0,
173+
constantBlocks=[ConstantBlock(name="ubo", bufferBacked=True, byteSize=64)],
155174
)
156175
pipe._shaders[ShaderStage.Vertex] = ResourceId(5)
157176
pipe._reflections[ShaderStage.Vertex] = refl
158177
s = _make_state(tmp_path, pipe)
159178
resp, _ = _handle_request(_req("pipe_push_constants", eid=10), s)
160179
assert resp["result"]["push_constants"] == []
161180

162-
def test_returns_range_when_size_nonzero(self, tmp_path: Path) -> None:
181+
def test_single_stage_push_constants(self, tmp_path: Path) -> None:
163182
pipe = MockPipeState()
164-
refl = ShaderReflection(
165-
resourceId=ResourceId(5),
166-
pushConstantRangeByteOffset=16,
167-
pushConstantRangeByteSize=64,
168-
)
183+
cb = ConstantBlock(name="push_block", bufferBacked=False, byteSize=16)
184+
refl = ShaderReflection(resourceId=ResourceId(5), constantBlocks=[cb])
169185
pipe._shaders[ShaderStage.Vertex] = ResourceId(5)
170186
pipe._reflections[ShaderStage.Vertex] = refl
187+
pipe._cbuffer_descriptors[(ShaderStage.Vertex, 0)] = Descriptor(
188+
resource=ResourceId(100),
189+
byteSize=16,
190+
)
191+
val = ShaderValue()
192+
val.f32v = [1.0, 2.0, 3.0, 4.0] + [0.0] * 12
193+
var = ShaderVariable(name="color", type="vec4", rows=1, columns=4, value=val)
171194
s = _make_state(tmp_path, pipe)
195+
s.adapter.controller._cbuffer_variables[(ShaderStage.Vertex, 0)] = [var]
172196
resp, _ = _handle_request(_req("pipe_push_constants", eid=10), s)
173-
ranges = resp["result"]["push_constants"]
174-
assert len(ranges) == 1
175-
assert ranges[0]["stage"] == "vs"
176-
assert ranges[0]["offset"] == 16
177-
assert ranges[0]["size"] == 64
197+
result = resp["result"]
198+
assert len(result["push_constants"]) == 1
199+
pc = result["push_constants"][0]
200+
assert pc["stage"] == "vs"
201+
assert pc["name"] == "push_block"
202+
assert pc["size"] == 16
203+
assert len(pc["variables"]) == 1
204+
assert pc["variables"][0]["name"] == "color"
205+
assert pc["variables"][0]["value"] == [1.0, 2.0, 3.0, 4.0]
178206

179207
def test_multiple_stages(self, tmp_path: Path) -> None:
180208
pipe = MockPipeState()
181-
for stage, stage_id in (
182-
(ShaderStage.Vertex, 5),
183-
(ShaderStage.Pixel, 6),
184-
):
185-
refl = ShaderReflection(
186-
resourceId=ResourceId(stage_id),
187-
pushConstantRangeByteOffset=0,
188-
pushConstantRangeByteSize=128,
189-
)
190-
pipe._shaders[stage] = ResourceId(stage_id)
209+
for stage, sid in ((ShaderStage.Vertex, 5), (ShaderStage.Pixel, 6)):
210+
cb = ConstantBlock(name="pc", bufferBacked=False, byteSize=8)
211+
refl = ShaderReflection(resourceId=ResourceId(sid), constantBlocks=[cb])
212+
pipe._shaders[stage] = ResourceId(sid)
191213
pipe._reflections[stage] = refl
192214
s = _make_state(tmp_path, pipe)
193215
resp, _ = _handle_request(_req("pipe_push_constants", eid=10), s)
194-
ranges = resp["result"]["push_constants"]
195-
stage_names = {r["stage"] for r in ranges}
196-
assert "vs" in stage_names
197-
assert "ps" in stage_names
216+
stages = {pc["stage"] for pc in resp["result"]["push_constants"]}
217+
assert "vs" in stages
218+
assert "ps" in stages
219+
220+
def test_raw_bytes_present(self, tmp_path: Path) -> None:
221+
pipe = MockPipeState()
222+
pipe.pushconsts = b"\x01\x02\xab"
223+
s = _make_state(tmp_path, pipe)
224+
resp, _ = _handle_request(_req("pipe_push_constants", eid=10), s)
225+
assert resp["result"]["raw_bytes"] == "0102ab"
226+
227+
def test_raw_bytes_empty_when_no_attr(self, tmp_path: Path) -> None:
228+
pipe = MockPipeState()
229+
delattr(pipe, "pushconsts")
230+
s = _make_state(tmp_path, pipe)
231+
resp, _ = _handle_request(_req("pipe_push_constants", eid=10), s)
232+
assert resp["result"]["raw_bytes"] == ""
233+
234+
def test_mixed_buffer_backed_and_push(self, tmp_path: Path) -> None:
235+
pipe = MockPipeState()
236+
blocks = [
237+
ConstantBlock(name="ubo", bufferBacked=True, byteSize=64),
238+
ConstantBlock(name="pc", bufferBacked=False, byteSize=16),
239+
]
240+
refl = ShaderReflection(resourceId=ResourceId(5), constantBlocks=blocks)
241+
pipe._shaders[ShaderStage.Vertex] = ResourceId(5)
242+
pipe._reflections[ShaderStage.Vertex] = refl
243+
pipe._cbuffer_descriptors[(ShaderStage.Vertex, 1)] = Descriptor(
244+
resource=ResourceId(200),
245+
byteSize=16,
246+
)
247+
s = _make_state(tmp_path, pipe)
248+
resp, _ = _handle_request(_req("pipe_push_constants", eid=10), s)
249+
pcs = resp["result"]["push_constants"]
250+
assert len(pcs) == 1
251+
assert pcs[0]["name"] == "pc"
198252

199253

200254
# ── pipe_rasterizer ───────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)