dcc-mcp-renderdoc 0.3.2__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dcc_mcp_renderdoc-0.4.0/.release-please-manifest.json +2 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/CHANGELOG.md +26 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/PKG-INFO +25 -13
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/README.md +24 -12
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/pyproject.toml +1 -1
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/release-please-config.json +1 -1
- dcc_mcp_renderdoc-0.4.0/src/dcc_mcp_renderdoc/__version__.py +3 -0
- dcc_mcp_renderdoc-0.4.0/src/dcc_mcp_renderdoc/_target_control.py +185 -0
- dcc_mcp_renderdoc-0.4.0/src/dcc_mcp_renderdoc/downloader.py +88 -0
- dcc_mcp_renderdoc-0.4.0/src/dcc_mcp_renderdoc/runtime.py +887 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/SKILL.md +3 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/tools.yaml +1 -1
- dcc_mcp_renderdoc-0.4.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +45 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/tools.yaml +2 -2
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/tests/live_renderdoc_smoke.py +18 -4
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/tests/renderdoc_smoke.c +11 -10
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/tests/test_package.py +1 -0
- dcc_mcp_renderdoc-0.4.0/tests/test_runtime.py +1833 -0
- dcc_mcp_renderdoc-0.3.2/.release-please-manifest.json +0 -2
- dcc_mcp_renderdoc-0.3.2/src/dcc_mcp_renderdoc/__version__.py +0 -3
- dcc_mcp_renderdoc-0.3.2/src/dcc_mcp_renderdoc/runtime.py +0 -596
- dcc_mcp_renderdoc-0.3.2/src/dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +0 -32
- dcc_mcp_renderdoc-0.3.2/tests/test_runtime.py +0 -429
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/.gitignore +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/LICENSE +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/__init__.py +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/server.py +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/metadata/depends.md +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/metadata/depends.md +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_process.py +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/get_version.py +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/tests/test_download.py +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/tools/__init__.py +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/tools/download_latest_renderdoc.py +0 -0
- {dcc_mcp_renderdoc-0.3.2 → dcc_mcp_renderdoc-0.4.0}/tools/lint_skills.py +0 -0
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.3...v0.4.0) (2026-07-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* auto-download RenderDoc runtime ([e380b95](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/e380b95c9cd7f86a031e4634b39740111af507ec))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* classify render work by API operation ([f5cf5a6](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/f5cf5a69b31cd4066d47d28c6701262d3c3225ac))
|
|
14
|
+
* reject captures without rendering work ([5799193](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/57991932e74c57e2826ffba10cbb3f433e55dca7))
|
|
15
|
+
|
|
16
|
+
## [0.3.3](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.2...v0.3.3) (2026-07-22)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* accept POSIX target ident exit status ([e35d14e](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/e35d14ed7dfdd429f5356ac8a5d5a98411a9dc28))
|
|
22
|
+
* harden target control lifecycle ([1a1eca8](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/1a1eca82fb453ec40b11a33791160d6074e70a6f))
|
|
23
|
+
* isolate target control sidecar ([2e16b2b](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/2e16b2b009c76136a5ad2889c4943c458401923a))
|
|
24
|
+
* normalize target names across platforms ([44602a0](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/44602a0fca3fd2749688f56f169280d18ddcb989))
|
|
25
|
+
* trigger captures through RenderDoc Target Control ([68d93aa](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/68d93aa9c0629b61cac75275ca53a55a65f29729))
|
|
26
|
+
* trigger captures through target control ([f0a11c8](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/f0a11c844a5597c0ae6b022a8ec07c32d7a15806))
|
|
27
|
+
* verify target before capture trigger ([a392e9f](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/a392e9fc974152d5d2dc183c1ed8b60ed15c2195))
|
|
28
|
+
|
|
3
29
|
## [0.3.2](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.1...v0.3.2) (2026-07-22)
|
|
4
30
|
|
|
5
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dcc-mcp-renderdoc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: RenderDoc capture and replay automation skills for DCC-MCP
|
|
5
5
|
Project-URL: Homepage, https://github.com/dcc-mcp/dcc-mcp-renderdoc
|
|
6
6
|
Project-URL: Repository, https://github.com/dcc-mcp/dcc-mcp-renderdoc
|
|
@@ -78,8 +78,8 @@ attach a stable session id with `--meta-json`, query `dcc-mcp-cli stats --range
|
|
|
78
78
|
RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
|
|
79
79
|
|
|
80
80
|
The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
|
|
81
|
-
conversion
|
|
82
|
-
|
|
81
|
+
conversion. Delayed capture uses RenderDoc's official Target Control API through the sibling
|
|
82
|
+
`qrenderdoc` bundled Python runtime, without foreground focus or synthetic keyboard input.
|
|
83
83
|
|
|
84
84
|
## Install
|
|
85
85
|
|
|
@@ -103,25 +103,37 @@ through the stable gateway at `http://127.0.0.1:9765/mcp`; set
|
|
|
103
103
|
## Agent workflows
|
|
104
104
|
|
|
105
105
|
- Launch a game or test executable under RenderDoc and wait for a typed `.rdc` capture.
|
|
106
|
-
- Trigger
|
|
107
|
-
- Inject into a
|
|
108
|
-
-
|
|
106
|
+
- Trigger a capture through official Target Control after a configurable delay.
|
|
107
|
+
- Inject into a process that had to be launched by a platform client, then trigger and collect a capture.
|
|
108
|
+
- Reject no-work captures with actionable diagnostics while preserving the `.rdc` artifact.
|
|
109
|
+
- Inspect capture driver, machine identity, chunk version, frame-work and Present counts, and representative calls.
|
|
109
110
|
- Export a capture thumbnail for visual review.
|
|
110
111
|
- Export Chrome trace JSON for timeline tooling.
|
|
111
112
|
|
|
112
113
|
The capture tool launches only the explicit executable and arguments supplied by the caller. It
|
|
113
114
|
never invokes a shell. Analysis tools are read-only with respect to the `.rdc` input.
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
Pass `trigger_after_secs` to `capture_program` for a Target Control trigger. This requires
|
|
117
|
+
`qrenderdoc` beside `renderdoccmd`. The official RenderDoc runtime supports Windows and Linux;
|
|
118
|
+
macOS is covered only by this project's Python unit tests. Linux Target Control requires an X or
|
|
119
|
+
Wayland display, so headless hosts must run under Xvfb (or explicitly configure a working Qt
|
|
120
|
+
platform). The official Linux archive does not bundle Qt's `offscreen` platform plugin. Each
|
|
121
|
+
sidecar uses an isolated Qt data profile with RenderDoc analytics explicitly opted out, preventing
|
|
122
|
+
the first-run consent dialog without reading or changing the user's qrenderdoc configuration.
|
|
123
|
+
|
|
124
|
+
When a launcher creates the rendered child process, set `hook_children=true` and pass
|
|
125
|
+
`trigger_process_name`. The adapter first checks the launched target itself; if its name does not
|
|
126
|
+
match, it follows only that target's official `NewChild` messages to find a unique named child. A
|
|
127
|
+
child name without child hooking fails before launch. Use `capture_process` only when the target is
|
|
128
|
+
already running; late injection may not capture graphics devices created before RenderDoc was
|
|
129
|
+
attached.
|
|
119
130
|
|
|
120
131
|
## Real CI
|
|
121
132
|
|
|
122
|
-
CI discovers the current stable RenderDoc build from the official downloads page. It
|
|
123
|
-
small OpenGL program,
|
|
124
|
-
|
|
133
|
+
CI discovers the current stable Linux RenderDoc build from the official downloads page. It
|
|
134
|
+
compiles a small OpenGL program, requests a real frame through Target Control under Xvfb using
|
|
135
|
+
Qt's bundled `xcb` platform, asserts the structured trigger mode, calls the MCP analysis tool
|
|
136
|
+
against the resulting `.rdc`, and verifies thumbnail and timeline exports.
|
|
125
137
|
|
|
126
138
|
## Development
|
|
127
139
|
|
|
@@ -50,8 +50,8 @@ attach a stable session id with `--meta-json`, query `dcc-mcp-cli stats --range
|
|
|
50
50
|
RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
|
|
51
51
|
|
|
52
52
|
The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
|
|
53
|
-
conversion
|
|
54
|
-
|
|
53
|
+
conversion. Delayed capture uses RenderDoc's official Target Control API through the sibling
|
|
54
|
+
`qrenderdoc` bundled Python runtime, without foreground focus or synthetic keyboard input.
|
|
55
55
|
|
|
56
56
|
## Install
|
|
57
57
|
|
|
@@ -75,25 +75,37 @@ through the stable gateway at `http://127.0.0.1:9765/mcp`; set
|
|
|
75
75
|
## Agent workflows
|
|
76
76
|
|
|
77
77
|
- Launch a game or test executable under RenderDoc and wait for a typed `.rdc` capture.
|
|
78
|
-
- Trigger
|
|
79
|
-
- Inject into a
|
|
80
|
-
-
|
|
78
|
+
- Trigger a capture through official Target Control after a configurable delay.
|
|
79
|
+
- Inject into a process that had to be launched by a platform client, then trigger and collect a capture.
|
|
80
|
+
- Reject no-work captures with actionable diagnostics while preserving the `.rdc` artifact.
|
|
81
|
+
- Inspect capture driver, machine identity, chunk version, frame-work and Present counts, and representative calls.
|
|
81
82
|
- Export a capture thumbnail for visual review.
|
|
82
83
|
- Export Chrome trace JSON for timeline tooling.
|
|
83
84
|
|
|
84
85
|
The capture tool launches only the explicit executable and arguments supplied by the caller. It
|
|
85
86
|
never invokes a shell. Analysis tools are read-only with respect to the `.rdc` input.
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
Pass `trigger_after_secs` to `capture_program` for a Target Control trigger. This requires
|
|
89
|
+
`qrenderdoc` beside `renderdoccmd`. The official RenderDoc runtime supports Windows and Linux;
|
|
90
|
+
macOS is covered only by this project's Python unit tests. Linux Target Control requires an X or
|
|
91
|
+
Wayland display, so headless hosts must run under Xvfb (or explicitly configure a working Qt
|
|
92
|
+
platform). The official Linux archive does not bundle Qt's `offscreen` platform plugin. Each
|
|
93
|
+
sidecar uses an isolated Qt data profile with RenderDoc analytics explicitly opted out, preventing
|
|
94
|
+
the first-run consent dialog without reading or changing the user's qrenderdoc configuration.
|
|
95
|
+
|
|
96
|
+
When a launcher creates the rendered child process, set `hook_children=true` and pass
|
|
97
|
+
`trigger_process_name`. The adapter first checks the launched target itself; if its name does not
|
|
98
|
+
match, it follows only that target's official `NewChild` messages to find a unique named child. A
|
|
99
|
+
child name without child hooking fails before launch. Use `capture_process` only when the target is
|
|
100
|
+
already running; late injection may not capture graphics devices created before RenderDoc was
|
|
101
|
+
attached.
|
|
91
102
|
|
|
92
103
|
## Real CI
|
|
93
104
|
|
|
94
|
-
CI discovers the current stable RenderDoc build from the official downloads page. It
|
|
95
|
-
small OpenGL program,
|
|
96
|
-
|
|
105
|
+
CI discovers the current stable Linux RenderDoc build from the official downloads page. It
|
|
106
|
+
compiles a small OpenGL program, requests a real frame through Target Control under Xvfb using
|
|
107
|
+
Qt's bundled `xcb` platform, asserts the structured trigger mode, calls the MCP analysis tool
|
|
108
|
+
against the resulting `.rdc`, and verifies thumbnail and timeline exports.
|
|
97
109
|
|
|
98
110
|
## Development
|
|
99
111
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"packages":{".":{"release-type":"python","package-name":"dcc-mcp-renderdoc","changelog-path":"CHANGELOG.md","include-component-in-tag":false,"extra-files":[{"type":"toml","path":"pyproject.toml","jsonpath":"$.project.version"},{"type":"generic","path":"src/dcc_mcp_renderdoc/__version__.py"},{"type":"
|
|
1
|
+
{"packages":{".":{"release-type":"python","package-name":"dcc-mcp-renderdoc","changelog-path":"CHANGELOG.md","include-component-in-tag":false,"extra-files":[{"type":"toml","path":"pyproject.toml","jsonpath":"$.project.version"},{"type":"generic","path":"src/dcc_mcp_renderdoc/__version__.py"},{"type":"generic","path":"uv.lock"}]}}}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""RenderDoc 1.45 bundled-Python bridge for headless Target Control capture."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _positive_int(name):
|
|
9
|
+
value = int(os.environ[name])
|
|
10
|
+
if value <= 0:
|
|
11
|
+
raise ValueError("{} must be positive".format(name))
|
|
12
|
+
return value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _nonnegative_float(name):
|
|
16
|
+
value = float(os.environ[name])
|
|
17
|
+
if value < 0:
|
|
18
|
+
raise ValueError("{} must not be negative".format(name))
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _open_target(rd, ident):
|
|
23
|
+
target = rd.CreateTargetControl("", ident, "dcc-mcp-renderdoc", False)
|
|
24
|
+
if target is None:
|
|
25
|
+
raise RuntimeError("RenderDoc target ident {} is busy or unavailable".format(ident))
|
|
26
|
+
if not target.Connected():
|
|
27
|
+
busy_client = target.GetBusyClient()
|
|
28
|
+
detail = " (busy client: {})".format(busy_client) if busy_client else ""
|
|
29
|
+
target.Shutdown()
|
|
30
|
+
raise RuntimeError("could not connect to RenderDoc target ident {}{}".format(ident, detail))
|
|
31
|
+
return target
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _target_name_matches(actual, expected):
|
|
35
|
+
actual = str(actual).replace("\\", "/").rsplit("/", 1)[-1].casefold()
|
|
36
|
+
expected = str(expected).replace("\\", "/").rsplit("/", 1)[-1].casefold()
|
|
37
|
+
return (
|
|
38
|
+
actual == expected
|
|
39
|
+
or (actual.endswith(".exe") and actual[:-4] == expected)
|
|
40
|
+
or (expected.endswith(".exe") and expected[:-4] == actual)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _wait_for_child_target(rd, parent, target_name, trigger_at, deadline):
|
|
45
|
+
matches = []
|
|
46
|
+
quiet_deadline = None
|
|
47
|
+
try:
|
|
48
|
+
while time.monotonic() < deadline:
|
|
49
|
+
now = time.monotonic()
|
|
50
|
+
if (
|
|
51
|
+
len(matches) == 1
|
|
52
|
+
and quiet_deadline is not None
|
|
53
|
+
and now >= trigger_at
|
|
54
|
+
and now >= quiet_deadline
|
|
55
|
+
):
|
|
56
|
+
return matches.pop()
|
|
57
|
+
message = parent.ReceiveMessage(None)
|
|
58
|
+
if message is not None and message.type == rd.TargetControlMessageType.NewChild:
|
|
59
|
+
child_ident = int(message.newChild.ident)
|
|
60
|
+
child_pid = int(message.newChild.processId)
|
|
61
|
+
child = _open_target(rd, child_ident)
|
|
62
|
+
try:
|
|
63
|
+
if int(child.GetPID()) != child_pid:
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
"RenderDoc child target PID did not match NewChild message"
|
|
66
|
+
)
|
|
67
|
+
if _target_name_matches(child.GetTarget(), target_name):
|
|
68
|
+
matches.append(child)
|
|
69
|
+
child = None
|
|
70
|
+
finally:
|
|
71
|
+
if child is not None:
|
|
72
|
+
child.Shutdown()
|
|
73
|
+
if matches:
|
|
74
|
+
if len(matches) > 1:
|
|
75
|
+
raise RuntimeError("multiple child targets matched {}".format(target_name))
|
|
76
|
+
quiet_deadline = time.monotonic() + 0.25
|
|
77
|
+
if quiet_deadline > deadline:
|
|
78
|
+
raise RuntimeError("insufficient time to select a unique child target")
|
|
79
|
+
continue
|
|
80
|
+
if message is not None and message.type == rd.TargetControlMessageType.Disconnected:
|
|
81
|
+
if len(matches) == 1:
|
|
82
|
+
return matches.pop()
|
|
83
|
+
raise RuntimeError("RenderDoc parent Target Control disconnected before child")
|
|
84
|
+
for match in matches:
|
|
85
|
+
child_message = match.ReceiveMessage(None)
|
|
86
|
+
if (
|
|
87
|
+
child_message is not None
|
|
88
|
+
and child_message.type == rd.TargetControlMessageType.Disconnected
|
|
89
|
+
):
|
|
90
|
+
raise RuntimeError("RenderDoc child Target Control disconnected before trigger")
|
|
91
|
+
time.sleep(0.05)
|
|
92
|
+
if len(matches) == 1:
|
|
93
|
+
raise RuntimeError("insufficient time to select a unique child target")
|
|
94
|
+
raise RuntimeError("no child target matched {}".format(target_name))
|
|
95
|
+
finally:
|
|
96
|
+
for match in matches:
|
|
97
|
+
match.Shutdown()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _pump_until(rd, target, deadline):
|
|
101
|
+
while time.monotonic() < deadline:
|
|
102
|
+
message = target.ReceiveMessage(None)
|
|
103
|
+
if message is not None and message.type == rd.TargetControlMessageType.Disconnected:
|
|
104
|
+
raise RuntimeError("RenderDoc Target Control disconnected before trigger")
|
|
105
|
+
time.sleep(0.05)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main():
|
|
109
|
+
status_path = os.environ.get("DCC_MCP_RENDERDOC_TARGET_STATUS")
|
|
110
|
+
status = {
|
|
111
|
+
"schema_version": 1,
|
|
112
|
+
"connected": False,
|
|
113
|
+
"triggered": False,
|
|
114
|
+
"shutdown": False,
|
|
115
|
+
"timed_out": False,
|
|
116
|
+
"target_pid": None,
|
|
117
|
+
"capture_path": None,
|
|
118
|
+
"error": None,
|
|
119
|
+
}
|
|
120
|
+
target = None
|
|
121
|
+
try:
|
|
122
|
+
import renderdoc as rd
|
|
123
|
+
|
|
124
|
+
ident = _positive_int("DCC_MCP_RENDERDOC_TARGET_IDENT")
|
|
125
|
+
capture_wait_secs = _positive_int("DCC_MCP_RENDERDOC_TARGET_TIMEOUT_SECS")
|
|
126
|
+
trigger_after_secs = _nonnegative_float("DCC_MCP_RENDERDOC_TRIGGER_AFTER_SECS")
|
|
127
|
+
target_name = os.environ.get("DCC_MCP_RENDERDOC_TARGET_NAME", "").strip()
|
|
128
|
+
expected_pid_value = os.environ.get("DCC_MCP_RENDERDOC_EXPECTED_PID", "").strip()
|
|
129
|
+
expected_pid = int(expected_pid_value) if expected_pid_value else None
|
|
130
|
+
if expected_pid is not None and expected_pid <= 0:
|
|
131
|
+
raise ValueError("DCC_MCP_RENDERDOC_EXPECTED_PID must be positive")
|
|
132
|
+
trigger_at = time.monotonic() + trigger_after_secs
|
|
133
|
+
capture_deadline = trigger_at + capture_wait_secs
|
|
134
|
+
target = _open_target(rd, ident)
|
|
135
|
+
if target_name and not _target_name_matches(target.GetTarget(), target_name):
|
|
136
|
+
child = _wait_for_child_target(rd, target, target_name, trigger_at, capture_deadline)
|
|
137
|
+
try:
|
|
138
|
+
target.Shutdown()
|
|
139
|
+
except BaseException:
|
|
140
|
+
child.Shutdown()
|
|
141
|
+
raise
|
|
142
|
+
target = child
|
|
143
|
+
status["connected"] = True
|
|
144
|
+
status["target_pid"] = int(target.GetPID())
|
|
145
|
+
if expected_pid is not None and status["target_pid"] != expected_pid:
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
"RenderDoc target PID {} did not match expected PID {}".format(
|
|
148
|
+
status["target_pid"], expected_pid
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
_pump_until(rd, target, trigger_at)
|
|
152
|
+
target.TriggerCapture(1)
|
|
153
|
+
status["triggered"] = True
|
|
154
|
+
while time.monotonic() < capture_deadline:
|
|
155
|
+
message = target.ReceiveMessage(None)
|
|
156
|
+
if message is not None and message.type == rd.TargetControlMessageType.NewCapture:
|
|
157
|
+
status["capture_path"] = str(message.newCapture.path)
|
|
158
|
+
break
|
|
159
|
+
if message is not None and message.type == rd.TargetControlMessageType.Disconnected:
|
|
160
|
+
raise RuntimeError("RenderDoc Target Control disconnected before capture")
|
|
161
|
+
time.sleep(0.05)
|
|
162
|
+
if status["capture_path"] is None:
|
|
163
|
+
status["timed_out"] = True
|
|
164
|
+
raise RuntimeError("timed out waiting for RenderDoc NewCapture")
|
|
165
|
+
except BaseException as exc:
|
|
166
|
+
status["error"] = str(exc)
|
|
167
|
+
finally:
|
|
168
|
+
if target is not None:
|
|
169
|
+
try:
|
|
170
|
+
target.Shutdown()
|
|
171
|
+
status["shutdown"] = True
|
|
172
|
+
target = None
|
|
173
|
+
except BaseException as exc:
|
|
174
|
+
status["error"] = status["error"] or ("Target Control shutdown failed: " + str(exc))
|
|
175
|
+
if status_path:
|
|
176
|
+
with open(status_path, "w") as status_file:
|
|
177
|
+
json.dump(status, status_file)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
try:
|
|
182
|
+
main()
|
|
183
|
+
except BaseException:
|
|
184
|
+
pass
|
|
185
|
+
raise SystemExit(0)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Download and cache the official RenderDoc command-line bundle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import urllib.request
|
|
11
|
+
import zipfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _cache_root() -> Path:
|
|
16
|
+
configured = os.environ.get("DCC_MCP_RUNTIME_CACHE")
|
|
17
|
+
if configured:
|
|
18
|
+
return Path(configured).expanduser().resolve() / "renderdoc"
|
|
19
|
+
if os.name == "nt":
|
|
20
|
+
root = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData/Local"))
|
|
21
|
+
return root / "dcc-mcp/renderdoc"
|
|
22
|
+
return Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "dcc-mcp/renderdoc"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _safe_destination(root: Path, member_name: str) -> Path:
|
|
26
|
+
destination = (root / member_name).resolve()
|
|
27
|
+
try:
|
|
28
|
+
destination.relative_to(root.resolve())
|
|
29
|
+
except ValueError as exc:
|
|
30
|
+
raise RuntimeError(f"Archive member escapes destination: {member_name}") from exc
|
|
31
|
+
return destination
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract(archive: Path, destination: Path) -> None:
|
|
35
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
if zipfile.is_zipfile(archive):
|
|
37
|
+
with zipfile.ZipFile(archive) as bundle:
|
|
38
|
+
for member in bundle.infolist():
|
|
39
|
+
_safe_destination(destination, member.filename)
|
|
40
|
+
bundle.extractall(destination)
|
|
41
|
+
return
|
|
42
|
+
with tarfile.open(archive, "r:gz") as bundle:
|
|
43
|
+
for member in bundle.getmembers():
|
|
44
|
+
_safe_destination(destination, member.name)
|
|
45
|
+
if member.issym() or member.islnk():
|
|
46
|
+
raise RuntimeError(f"Archive links are not accepted: {member.name}")
|
|
47
|
+
bundle.extractall(destination)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def download_latest() -> Path:
|
|
51
|
+
request = urllib.request.Request(
|
|
52
|
+
"https://renderdoc.org/builds", headers={"User-Agent": "dcc-mcp-renderdoc"}
|
|
53
|
+
)
|
|
54
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
55
|
+
html = response.read().decode("utf-8")
|
|
56
|
+
versions = set(re.findall(r"/stable/([0-9]+\.[0-9]+)/", html))
|
|
57
|
+
if not versions:
|
|
58
|
+
raise RuntimeError("No stable RenderDoc versions found on the official builds page")
|
|
59
|
+
version = max(versions, key=lambda value: tuple(int(part) for part in value.split(".")))
|
|
60
|
+
if sys.platform == "win32":
|
|
61
|
+
archive_name, command_name = f"RenderDoc_{version}_64.zip", "renderdoccmd.exe"
|
|
62
|
+
elif sys.platform.startswith("linux"):
|
|
63
|
+
archive_name, command_name = f"renderdoc_{version}.tar.gz", "renderdoccmd"
|
|
64
|
+
else:
|
|
65
|
+
raise RuntimeError(f"RenderDoc has no supported desktop bundle for {sys.platform}")
|
|
66
|
+
destination = _cache_root() / version
|
|
67
|
+
existing = next(destination.rglob(command_name), None) if destination.exists() else None
|
|
68
|
+
if existing:
|
|
69
|
+
return existing.resolve()
|
|
70
|
+
archive = _cache_root() / archive_name
|
|
71
|
+
_cache_root().mkdir(parents=True, exist_ok=True)
|
|
72
|
+
try:
|
|
73
|
+
with (
|
|
74
|
+
urllib.request.urlopen(
|
|
75
|
+
f"https://renderdoc.org/stable/{version}/{archive_name}", timeout=120
|
|
76
|
+
) as response,
|
|
77
|
+
archive.open("wb") as stream,
|
|
78
|
+
):
|
|
79
|
+
shutil.copyfileobj(response, stream)
|
|
80
|
+
_extract(archive, destination)
|
|
81
|
+
finally:
|
|
82
|
+
archive.unlink(missing_ok=True)
|
|
83
|
+
command = next(destination.rglob(command_name), None)
|
|
84
|
+
if command is None:
|
|
85
|
+
raise RuntimeError(f"Downloaded bundle did not contain {command_name}")
|
|
86
|
+
if sys.platform.startswith("linux"):
|
|
87
|
+
command.chmod(command.stat().st_mode | 0o111)
|
|
88
|
+
return command.resolve()
|