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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ where = ["src"]
[tool.pytest.ini_options]
addopts = "-q"
testpaths = ["tests"]
pythonpath = ["src", "tests/mocks", "scripts"]
pythonpath = ["src", "tests/mocks", "tests/e2e", "scripts"]
markers = [
"gpu: requires real renderdoc module and GPU",
"vulkan_samples: requires vulkan-samples binary for live capture testing",
Expand Down
2 changes: 2 additions & 0 deletions src/rdc/commands/vfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ def _fmt_pixel_mod(m: dict[str, Any]) -> str:
"EID\tFRAG\tDEPTH\tPASSED\tFLAGS\n"
+ "\n".join(_fmt_pixel_mod(m) for m in r.get("modifications", []))
),
"pass_attachment": lambda r: format_kv(r),
"shader_used_by": lambda r: "\n".join(str(e) for e in r.get("eids", [])),
}


Expand Down
32 changes: 32 additions & 0 deletions src/rdc/handlers/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(self, response: dict[str, Any]) -> None:
_VALID_LOG_LEVELS: set[str] = {*_LOG_SEVERITY_MAP.values(), "UNKNOWN"}

_SHADER_PATH_RE = re.compile(r"^/draws/(\d+)/(?:shader|targets|bindings|cbuffer)(?:/|$)")
_PASS_ATTACH_RE = re.compile(r"^/passes/([^/]+)/attachments(?:/|$)")


def _result_response(request_id: int, result: dict[str, Any]) -> dict[str, Any]:
Expand Down Expand Up @@ -212,6 +213,7 @@ def _walk(actions: list[Any]) -> None:
"stages": shader_stages[sid],
"uses": len(shader_eids[sid]),
"first_eid": shader_eids[sid][0],
"eids": shader_eids[sid],
"entry": getattr(refl, "entryPoint", "main") if refl else "main",
"inputs": len(getattr(refl, "readOnlyResources", [])) if refl else 0,
"outputs": len(getattr(refl, "readWriteResources", [])) if refl else 0,
Expand Down Expand Up @@ -347,3 +349,33 @@ def _ensure_shader_populated(
assert state.vfs_tree is not None
populate_draw_subtree(state.vfs_tree, eid, pipe)
return None


def _ensure_pass_attachments_populated(
request_id: int, path: str, state: DaemonState
) -> dict[str, Any] | None:
"""Trigger dynamic pass attachment subtree population if needed."""
m = _PASS_ATTACH_RE.match(path)
if m is None or state.vfs_tree is None:
return None
pass_name = m.group(1)
attach_path = f"/passes/{pass_name}/attachments"
node = state.vfs_tree.static.get(attach_path)
if node is None or node.children:
return None

safe_map = state.vfs_tree.pass_name_map
orig_name = safe_map.get(pass_name, pass_name)
pass_info = next((p for p in state.vfs_tree.pass_list if p["name"] == orig_name), None)
if pass_info is None:
return None

begin_eid = pass_info.get("begin_eid", 0)
err = _seek_replay(state, begin_eid)
if err:
return _error_response(request_id, -32002, err)
pipe = state.adapter.get_pipeline_state() # type: ignore[union-attr]
from rdc.vfs.tree_cache import populate_pass_attachments

populate_pass_attachments(state.vfs_tree, pass_name, pipe)
return None
48 changes: 48 additions & 0 deletions src/rdc/handlers/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,53 @@ def _handle_pass_deps(
return _result_response(request_id, result), True


def _handle_pass_attachment(
request_id: int, params: dict[str, Any], state: DaemonState
) -> tuple[dict[str, Any], bool]:
"""Return attachment info for a specific pass attachment."""
if state.adapter is None:
return _error_response(request_id, -32002, "no replay loaded"), True
from rdc.services.query_service import get_pass_detail

name = str(params.get("name", ""))
attachment = str(params.get("attachment", ""))
if state.vfs_tree and name in state.vfs_tree.pass_name_map:
Comment on lines +596 to +598
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Return -32602 for missing name/attachment instead of not-found errors.

Defaulting to "" makes malformed requests look like lookup failures (pass not found / unknown attachment). This should be treated as invalid parameters for clearer client behavior.

💡 Proposed fix
-    name = str(params.get("name", ""))
-    attachment = str(params.get("attachment", ""))
+    if "name" not in params or not str(params["name"]).strip():
+        return _error_response(request_id, -32602, "missing name"), True
+    if "attachment" not in params or not str(params["attachment"]).strip():
+        return _error_response(request_id, -32602, "missing attachment"), True
+    name = str(params["name"]).strip()
+    attachment = str(params["attachment"]).strip()

Also applies to: 603-605, 632-632

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rdc/handlers/query.py` around lines 596 - 598, The code currently
defaults name and attachment to empty strings (e.g., name =
str(params.get("name", "")) and attachment = str(params.get("attachment", "")))
which masks malformed requests as not-found errors; change these reads to
validate presence and non-empty values and return a JSON-RPC invalid params
error (-32602) when missing/empty instead of proceeding to lookups (affecting
the initial block that checks state.vfs_tree/pass_name_map and the other
occurrences where params are defaulted). Specifically, replace the defaulting
pattern with explicit checks of params.get("name") and params.get("attachment")
(or check after casting) and if either is missing or empty, return the RPC error
-32602 with a clear message rather than performing the pass or attachment
lookup.

name = state.vfs_tree.pass_name_map[name]

actions = state.adapter.get_root_actions()
detail = get_pass_detail(actions, state.structured_file, name)
if detail is None:
return _error_response(request_id, -32001, f"pass not found: {name}"), True

err = _seek_replay(state, detail["begin_eid"])
if err:
return _error_response(request_id, -32002, err), True
pipe = state.adapter.get_pipeline_state()

if attachment == "depth":
depth_id = int(pipe.GetDepthTarget().resource)
if depth_id == 0:
return _error_response(request_id, -32001, "no depth target"), True
return _result_response(
request_id, {"pass": name, "attachment": "depth", "resource_id": depth_id}
), True

if attachment.startswith("color"):
try:
idx = int(attachment[5:])
except ValueError:
return _error_response(request_id, -32602, f"invalid attachment: {attachment}"), True
all_targets = pipe.GetOutputTargets()
if idx < 0 or idx >= len(all_targets) or int(all_targets[idx].resource) == 0:
return _error_response(request_id, -32001, f"color target {idx} not found"), True
return _result_response(
request_id,
{"pass": name, "attachment": attachment, "resource_id": int(all_targets[idx].resource)},
), True

return _error_response(request_id, -32001, f"unknown attachment: {attachment}"), True


def _handle_preload(
request_id: int, params: dict[str, Any], state: DaemonState
) -> tuple[dict[str, Any], bool]:
Expand All @@ -607,6 +654,7 @@ def _handle_preload(
"passes": _handle_passes,
"pass": _handle_pass,
"pass_deps": _handle_pass_deps,
"pass_attachment": _handle_pass_attachment,
"log": _handle_log,
"info": _handle_info,
"stats": _handle_stats,
Expand Down
13 changes: 13 additions & 0 deletions src/rdc/handlers/shader.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,18 @@ def _handle_shader_list_info(
return _result_response(request_id, {"id": sid, **info_meta}), True


def _handle_shader_used_by(
request_id: int, params: dict[str, Any], state: DaemonState
) -> tuple[dict[str, Any], bool]:
"""Return list of EIDs that use this shader."""
_build_shader_cache(state)
sid = int(params.get("id", 0))
meta = state.shader_meta.get(sid)
if meta is None:
return _error_response(request_id, -32001, f"shader {sid} not found"), True
return _result_response(request_id, {"id": sid, "eids": meta.get("eids", [])}), True


def _handle_shader_list_disasm(
request_id: int, params: dict[str, Any], state: DaemonState
) -> tuple[dict[str, Any], bool]:
Expand All @@ -279,4 +291,5 @@ def _handle_shader_list_disasm(
"shader_all": _handle_shader_all,
"shader_list_info": _handle_shader_list_info,
"shader_list_disasm": _handle_shader_list_disasm,
"shader_used_by": _handle_shader_used_by,
}
8 changes: 8 additions & 0 deletions src/rdc/handlers/vfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from rdc.handlers._helpers import (
_action_type_str,
_build_shader_cache,
_ensure_pass_attachments_populated,
_ensure_shader_populated,
_error_response,
_get_flat_actions,
Expand Down Expand Up @@ -280,6 +281,10 @@ def _handle_vfs_ls(
if pop_err:
return pop_err, True

pop_err2 = _ensure_pass_attachments_populated(request_id, path, state)
if pop_err2:
return pop_err2, True

node = state.vfs_tree.static.get(path)
if node is None:
return _error_response(request_id, -32001, f"not found: {path}"), True
Expand Down Expand Up @@ -341,6 +346,9 @@ def _subtree(p: str, d: int) -> dict[str, Any]:
err = _ensure_shader_populated(request_id, p, state)
if err is not None:
raise _VfsPopulateError(err)
err2 = _ensure_pass_attachments_populated(request_id, p, state)
if err2 is not None:
raise _VfsPopulateError(err2)
n = tree.static.get(p)
if n is None:
return {"name": p.rsplit("/", 1)[-1] or "/", "kind": "dir", "children": []}
Expand Down
4 changes: 4 additions & 0 deletions src/rdc/vfs/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ def _r(
_r(r"/draws/(?P<eid>\d+)/vbuffer", "leaf", "vbuffer_decode", [("eid", int)])
_r(r"/draws/(?P<eid>\d+)/ibuffer", "leaf", "ibuffer_decode", [("eid", int)])
_r(r"/draws/(?P<eid>\d+)/descriptors", "leaf", "descriptors", [("eid", int)])
_r(r"/draws/(?P<eid>\d+)/pixel", "dir", None, [("eid", int)])
_r(r"/draws/(?P<eid>\d+)/pixel/(?P<x>\d+)", "dir", None, [("eid", int), ("x", int)])
_r(
r"/draws/(?P<eid>\d+)/pixel/(?P<x>\d+)/(?P<y>\d+)",
"leaf",
Expand Down Expand Up @@ -149,6 +151,7 @@ def _r(
_r(r"/passes/(?P<name>[^/]+)/info", "leaf", "pass")
_r(r"/passes/(?P<name>[^/]+)/draws", "dir")
_r(r"/passes/(?P<name>[^/]+)/attachments", "dir")
_r(r"/passes/(?P<name>[^/]+)/attachments/(?P<attachment>[^/]+)", "leaf", "pass_attachment")

# resources
_r("/resources", "dir")
Expand All @@ -161,6 +164,7 @@ def _r(
_r(r"/shaders/(?P<id>\d+)", "dir", None, [("id", int)])
_r(r"/shaders/(?P<id>\d+)/info", "leaf", "shader_list_info", [("id", int)])
_r(r"/shaders/(?P<id>\d+)/disasm", "leaf", "shader_list_disasm", [("id", int)])
_r(r"/shaders/(?P<id>\d+)/used-by", "leaf", "shader_used_by", [("id", int)])
_r("/by-marker", "dir")
_r("/textures", "dir")
_r(r"/textures/(?P<id>\d+)", "dir", None, [("id", int)])
Expand Down
31 changes: 30 additions & 1 deletion src/rdc/vfs/tree_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"vbuffer",
"ibuffer",
"descriptors",
"pixel",
]
_PIPELINE_CHILDREN = [
"summary",
Expand All @@ -64,7 +65,7 @@
]
_PASS_CHILDREN = ["info", "draws", "attachments"]
_SHADER_STAGE_CHILDREN = ["disasm", "source", "reflect", "constants"]
_SHADER_CHILDREN = ["info", "disasm"]
_SHADER_CHILDREN = ["info", "disasm", "used-by"]


@dataclass
Expand Down Expand Up @@ -171,6 +172,7 @@ def build_vfs_skeleton(
tree.static[f"{prefix}/vbuffer"] = VfsNode("vbuffer", "leaf")
tree.static[f"{prefix}/ibuffer"] = VfsNode("ibuffer", "leaf")
tree.static[f"{prefix}/descriptors"] = VfsNode("descriptors", "leaf")
tree.static[f"{prefix}/pixel"] = VfsNode("pixel", "dir")

# /passes — sanitize names containing "/" to avoid path corruption
safe_pass_names = [n.replace("/", "_") for n in pass_names]
Expand Down Expand Up @@ -362,6 +364,32 @@ def populate_draw_subtree(
return subtree


def populate_pass_attachments(
tree: VfsTree,
pass_name: str,
pipe_state: Any,
) -> None:
"""Populate /passes/<name>/attachments/ with color and depth leaves."""
prefix = f"/passes/{pass_name}/attachments"
node = tree.static.get(prefix)
if node is None or node.children:
return

children: list[str] = []
for i, t in enumerate(pipe_state.GetOutputTargets()):
if int(t.resource) != 0:
name = f"color{i}"
children.append(name)
tree.static[f"{prefix}/{name}"] = VfsNode(name, "leaf")

depth = pipe_state.GetDepthTarget()
if int(depth.resource) != 0:
children.append("depth")
tree.static[f"{prefix}/depth"] = VfsNode("depth", "leaf")

node.children = list(children)


def populate_shaders_subtree(tree: VfsTree, shader_meta: dict[int, dict[str, Any]]) -> None:
"""Populate /shaders/<id>/ dir nodes from shader metadata.

Expand All @@ -376,3 +404,4 @@ def populate_shaders_subtree(tree: VfsTree, shader_meta: dict[int, dict[str, Any
tree.static[prefix] = VfsNode(str(sid), "dir", list(_SHADER_CHILDREN))
tree.static[f"{prefix}/info"] = VfsNode("info", "leaf")
tree.static[f"{prefix}/disasm"] = VfsNode("disasm", "leaf")
tree.static[f"{prefix}/used-by"] = VfsNode("used-by", "leaf")
Loading