dcc-mcp-renderdoc 0.2.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.2.0/.gitignore +12 -0
- dcc_mcp_renderdoc-0.2.0/.release-please-manifest.json +2 -0
- dcc_mcp_renderdoc-0.2.0/CHANGELOG.md +21 -0
- dcc_mcp_renderdoc-0.2.0/LICENSE +22 -0
- dcc_mcp_renderdoc-0.2.0/PKG-INFO +79 -0
- dcc_mcp_renderdoc-0.2.0/README.md +51 -0
- dcc_mcp_renderdoc-0.2.0/pyproject.toml +68 -0
- dcc_mcp_renderdoc-0.2.0/release-please-config.json +2 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/__init__.py +6 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/__version__.py +3 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/runtime.py +255 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/server.py +78 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/SKILL.md +25 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/metadata/depends.md +2 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py +15 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py +15 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py +15 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/tools.yaml +63 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +25 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/metadata/depends.md +2 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py +34 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/get_version.py +14 -0
- dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/tools.yaml +45 -0
- dcc_mcp_renderdoc-0.2.0/tests/live_renderdoc_smoke.py +142 -0
- dcc_mcp_renderdoc-0.2.0/tests/renderdoc_smoke.c +71 -0
- dcc_mcp_renderdoc-0.2.0/tests/test_download.py +15 -0
- dcc_mcp_renderdoc-0.2.0/tests/test_package.py +22 -0
- dcc_mcp_renderdoc-0.2.0/tests/test_runtime.py +57 -0
- dcc_mcp_renderdoc-0.2.0/tools/__init__.py +1 -0
- dcc_mcp_renderdoc-0.2.0/tools/download_latest_renderdoc.py +89 -0
- dcc_mcp_renderdoc-0.2.0/tools/lint_skills.py +8 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.1.0...v0.2.0) (2026-07-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add RenderDoc MCP adapter ([a324344](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/a324344d5dcab758282c77598f4591076526f44e))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* capture a supported OpenGL core context ([7817a6f](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/7817a6fd3a7e683a37348ed4a0582bfe456024b6))
|
|
14
|
+
* include C boolean definitions in smoke target ([a9471ed](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/a9471ed7fef6f663a74ed9c9207ae8231bc5dafc))
|
|
15
|
+
* initialize OpenGL before frame capture ([af3c93c](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/af3c93c2b7fc794769187d7e237596d5c37de97b))
|
|
16
|
+
* target explicit RenderDoc frame capture ([99f1568](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/99f1568e0367c156c6c01106109ea4333e0788f5))
|
|
17
|
+
* trigger a presented frame in RenderDoc smoke ([2d939cb](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/2d939cb3432ff6280ac44e56b6f785d1ff98d2ee))
|
|
18
|
+
|
|
19
|
+
## Changelog
|
|
20
|
+
|
|
21
|
+
All notable changes to this project will be documented in this file.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 loonghao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dcc-mcp-renderdoc
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: RenderDoc capture and replay automation skills for DCC-MCP
|
|
5
|
+
Project-URL: Homepage, https://github.com/dcc-mcp/dcc-mcp-renderdoc
|
|
6
|
+
Project-URL: Repository, https://github.com/dcc-mcp/dcc-mcp-renderdoc
|
|
7
|
+
Project-URL: Issues, https://github.com/dcc-mcp/dcc-mcp-renderdoc/issues
|
|
8
|
+
Author-email: loonghao <hal.long@outlook.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: automation,game-development,graphics-debugging,mcp,renderdoc
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Games/Entertainment
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: dcc-mcp-core<1.0.0,>=0.19.38
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
26
|
+
Requires-Dist: twine>=6; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# dcc-mcp-renderdoc
|
|
30
|
+
|
|
31
|
+
RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
|
|
32
|
+
|
|
33
|
+
The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
|
|
34
|
+
conversion, so agents can automate graphics regression triage without keeping the RenderDoc GUI
|
|
35
|
+
open or installing a second bridge.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install dcc-mcp-renderdoc
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Install RenderDoc separately, then expose its command line tool with either `PATH` or:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export DCC_MCP_RENDERDOC_CMD=/opt/renderdoc/bin/renderdoccmd
|
|
47
|
+
dcc-mcp-renderdoc
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
On Windows, set the variable to `renderdoccmd.exe`.
|
|
51
|
+
|
|
52
|
+
## Agent workflows
|
|
53
|
+
|
|
54
|
+
- Launch a game or test executable under RenderDoc and wait for a typed `.rdc` capture.
|
|
55
|
+
- Inspect capture driver, machine identity, chunk version, API-call counts, and representative calls.
|
|
56
|
+
- Export a capture thumbnail for visual review.
|
|
57
|
+
- Export Chrome trace JSON for timeline tooling.
|
|
58
|
+
|
|
59
|
+
The capture tool launches only the explicit executable and arguments supplied by the caller. It
|
|
60
|
+
never invokes a shell. Analysis tools are read-only with respect to the `.rdc` input.
|
|
61
|
+
|
|
62
|
+
## Real CI
|
|
63
|
+
|
|
64
|
+
CI discovers the current stable RenderDoc build from the official downloads page. It compiles a
|
|
65
|
+
small OpenGL program, captures a real frame under Xvfb, calls the MCP analysis tool against the
|
|
66
|
+
resulting `.rdc`, and verifies thumbnail and timeline exports.
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv sync --extra dev
|
|
72
|
+
uv run python -m pytest
|
|
73
|
+
uv run ruff check src tests tools
|
|
74
|
+
uv run python tools/lint_skills.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
RenderDoc is an MIT-licensed graphics debugger maintained independently at
|
|
78
|
+
[renderdoc.org](https://renderdoc.org/). This adapter is not affiliated with the RenderDoc project.
|
|
79
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# dcc-mcp-renderdoc
|
|
2
|
+
|
|
3
|
+
RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
|
|
4
|
+
|
|
5
|
+
The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
|
|
6
|
+
conversion, so agents can automate graphics regression triage without keeping the RenderDoc GUI
|
|
7
|
+
open or installing a second bridge.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install dcc-mcp-renderdoc
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install RenderDoc separately, then expose its command line tool with either `PATH` or:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
export DCC_MCP_RENDERDOC_CMD=/opt/renderdoc/bin/renderdoccmd
|
|
19
|
+
dcc-mcp-renderdoc
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
On Windows, set the variable to `renderdoccmd.exe`.
|
|
23
|
+
|
|
24
|
+
## Agent workflows
|
|
25
|
+
|
|
26
|
+
- Launch a game or test executable under RenderDoc and wait for a typed `.rdc` capture.
|
|
27
|
+
- Inspect capture driver, machine identity, chunk version, API-call counts, and representative calls.
|
|
28
|
+
- Export a capture thumbnail for visual review.
|
|
29
|
+
- Export Chrome trace JSON for timeline tooling.
|
|
30
|
+
|
|
31
|
+
The capture tool launches only the explicit executable and arguments supplied by the caller. It
|
|
32
|
+
never invokes a shell. Analysis tools are read-only with respect to the `.rdc` input.
|
|
33
|
+
|
|
34
|
+
## Real CI
|
|
35
|
+
|
|
36
|
+
CI discovers the current stable RenderDoc build from the official downloads page. It compiles a
|
|
37
|
+
small OpenGL program, captures a real frame under Xvfb, calls the MCP analysis tool against the
|
|
38
|
+
resulting `.rdc`, and verifies thumbnail and timeline exports.
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv sync --extra dev
|
|
44
|
+
uv run python -m pytest
|
|
45
|
+
uv run ruff check src tests tools
|
|
46
|
+
uv run python tools/lint_skills.py
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
RenderDoc is an MIT-licensed graphics debugger maintained independently at
|
|
50
|
+
[renderdoc.org](https://renderdoc.org/). This adapter is not affiliated with the RenderDoc project.
|
|
51
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dcc-mcp-renderdoc"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "RenderDoc capture and replay automation skills for DCC-MCP"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "loonghao", email = "hal.long@outlook.com" }]
|
|
13
|
+
keywords = ["renderdoc", "mcp", "graphics-debugging", "game-development", "automation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Games/Entertainment",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["dcc-mcp-core>=0.19.38,<1.0.0"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/dcc-mcp/dcc-mcp-renderdoc"
|
|
28
|
+
Repository = "https://github.com/dcc-mcp/dcc-mcp-renderdoc"
|
|
29
|
+
Issues = "https://github.com/dcc-mcp/dcc-mcp-renderdoc/issues"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["build>=1.2", "pytest>=8", "ruff>=0.8", "twine>=6"]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
dcc-mcp-renderdoc = "dcc_mcp_renderdoc.server:main"
|
|
36
|
+
|
|
37
|
+
[project.entry-points."dcc_mcp.adapters"]
|
|
38
|
+
renderdoc = "dcc_mcp_renderdoc:RenderDocMcpServer"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/dcc_mcp_renderdoc"]
|
|
42
|
+
artifacts = ["src/dcc_mcp_renderdoc/skills/**"]
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.sdist]
|
|
45
|
+
include = [
|
|
46
|
+
"src/dcc_mcp_renderdoc",
|
|
47
|
+
"tests",
|
|
48
|
+
"tools",
|
|
49
|
+
"README.md",
|
|
50
|
+
"CHANGELOG.md",
|
|
51
|
+
"LICENSE",
|
|
52
|
+
"release-please-config.json",
|
|
53
|
+
".release-please-manifest.json",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
pythonpath = ["src", "."]
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
target-version = "py39"
|
|
62
|
+
line-length = 100
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E", "F", "I", "B"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint.per-file-ignores]
|
|
68
|
+
"src/dcc_mcp_renderdoc/skills/**/scripts/*.py" = ["E501"]
|
|
@@ -0,0 +1,2 @@
|
|
|
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"}]}}}
|
|
2
|
+
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Small, typed wrapper around RenderDoc's official command-line client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import xml.etree.ElementTree as ET
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Iterable, Optional, Sequence
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RenderDocError(RuntimeError):
|
|
16
|
+
"""Raised when a RenderDoc operation cannot satisfy its contract."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve_renderdoccmd(explicit: Optional[str] = None) -> Path:
|
|
20
|
+
"""Resolve an executable RenderDoc CLI without invoking a shell."""
|
|
21
|
+
candidate = explicit or os.environ.get("DCC_MCP_RENDERDOC_CMD")
|
|
22
|
+
if candidate:
|
|
23
|
+
path = Path(candidate).expanduser().resolve()
|
|
24
|
+
if path.is_file():
|
|
25
|
+
return path
|
|
26
|
+
raise RenderDocError(f"RenderDoc command does not exist: {path}")
|
|
27
|
+
|
|
28
|
+
for name in ("renderdoccmd.exe", "renderdoccmd"):
|
|
29
|
+
found = shutil.which(name)
|
|
30
|
+
if found:
|
|
31
|
+
return Path(found).resolve()
|
|
32
|
+
raise RenderDocError(
|
|
33
|
+
"renderdoccmd was not found; install RenderDoc or set DCC_MCP_RENDERDOC_CMD"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _run(
|
|
38
|
+
arguments: Sequence[str],
|
|
39
|
+
*,
|
|
40
|
+
timeout_secs: int,
|
|
41
|
+
command: Optional[str] = None,
|
|
42
|
+
) -> subprocess.CompletedProcess[str]:
|
|
43
|
+
executable = resolve_renderdoccmd(command)
|
|
44
|
+
try:
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
[str(executable), *arguments],
|
|
47
|
+
check=False,
|
|
48
|
+
capture_output=True,
|
|
49
|
+
text=True,
|
|
50
|
+
timeout=timeout_secs,
|
|
51
|
+
shell=False,
|
|
52
|
+
)
|
|
53
|
+
except subprocess.TimeoutExpired as exc:
|
|
54
|
+
raise RenderDocError(f"RenderDoc command timed out after {timeout_secs}s") from exc
|
|
55
|
+
except OSError as exc:
|
|
56
|
+
raise RenderDocError(f"Could not start RenderDoc: {exc}") from exc
|
|
57
|
+
if result.returncode != 0:
|
|
58
|
+
detail = (result.stderr or result.stdout or "unknown error").strip()[-2000:]
|
|
59
|
+
raise RenderDocError(f"RenderDoc exited with code {result.returncode}: {detail}")
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_version(*, command: Optional[str] = None) -> dict[str, Any]:
|
|
64
|
+
result = _run(["version"], timeout_secs=30, command=command)
|
|
65
|
+
output = "\n".join(part.strip() for part in (result.stdout, result.stderr) if part.strip())
|
|
66
|
+
return {"command": str(resolve_renderdoccmd(command)), "version_output": output}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def capture_program(
|
|
70
|
+
executable: str,
|
|
71
|
+
output_template: str,
|
|
72
|
+
*,
|
|
73
|
+
arguments: Optional[Sequence[str]] = None,
|
|
74
|
+
working_directory: Optional[str] = None,
|
|
75
|
+
wait_for_exit: bool = True,
|
|
76
|
+
api_validation: bool = False,
|
|
77
|
+
hook_children: bool = False,
|
|
78
|
+
timeout_secs: int = 300,
|
|
79
|
+
command: Optional[str] = None,
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
|
+
"""Launch one explicit executable under RenderDoc and return new captures."""
|
|
82
|
+
target = Path(executable).expanduser().resolve()
|
|
83
|
+
if not target.is_file():
|
|
84
|
+
raise RenderDocError(f"Target executable does not exist: {target}")
|
|
85
|
+
output = Path(output_template).expanduser().resolve()
|
|
86
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
cwd = Path(working_directory).expanduser().resolve() if working_directory else target.parent
|
|
88
|
+
if not cwd.is_dir():
|
|
89
|
+
raise RenderDocError(f"Working directory does not exist: {cwd}")
|
|
90
|
+
|
|
91
|
+
before = {path.resolve() for path in output.parent.glob("*.rdc")}
|
|
92
|
+
cli_args = ["capture", "--working-dir", str(cwd), "--capture-file", str(output)]
|
|
93
|
+
if wait_for_exit:
|
|
94
|
+
cli_args.append("--wait-for-exit")
|
|
95
|
+
if api_validation:
|
|
96
|
+
cli_args.append("--opt-api-validation")
|
|
97
|
+
if hook_children:
|
|
98
|
+
cli_args.append("--opt-hook-children")
|
|
99
|
+
cli_args.extend([str(target), *(str(value) for value in arguments or [])])
|
|
100
|
+
result = _run(cli_args, timeout_secs=timeout_secs, command=command)
|
|
101
|
+
|
|
102
|
+
captures = sorted(
|
|
103
|
+
(path.resolve() for path in output.parent.glob("*.rdc") if path.resolve() not in before),
|
|
104
|
+
key=lambda path: path.stat().st_mtime_ns,
|
|
105
|
+
)
|
|
106
|
+
if not captures:
|
|
107
|
+
output_detail = "\n".join(
|
|
108
|
+
part.strip() for part in (result.stdout, result.stderr) if part.strip()
|
|
109
|
+
)[-2000:]
|
|
110
|
+
raise RenderDocError(
|
|
111
|
+
"The target exited without creating a new .rdc capture; ensure it presents a frame "
|
|
112
|
+
f"or uses RenderDoc's in-application capture API. RenderDoc output: {output_detail}"
|
|
113
|
+
)
|
|
114
|
+
return {
|
|
115
|
+
"target": str(target),
|
|
116
|
+
"captures": [str(path) for path in captures],
|
|
117
|
+
"stdout": result.stdout.strip(),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _require_capture(capture_file: str) -> Path:
|
|
122
|
+
path = Path(capture_file).expanduser().resolve()
|
|
123
|
+
if not path.is_file() or path.suffix.lower() != ".rdc":
|
|
124
|
+
raise RenderDocError(f"RenderDoc capture does not exist or is not .rdc: {path}")
|
|
125
|
+
return path
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _child_text(parent: ET.Element, name: str) -> Optional[str]:
|
|
129
|
+
child = parent.find(name)
|
|
130
|
+
return child.text.strip() if child is not None and child.text else None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_capture_xml(xml_file: str, *, representative_limit: int = 20) -> dict[str, Any]:
|
|
134
|
+
"""Parse the stable high-level fields emitted by RenderDoc's XML converter."""
|
|
135
|
+
path = Path(xml_file)
|
|
136
|
+
try:
|
|
137
|
+
root = ET.parse(path).getroot()
|
|
138
|
+
except (ET.ParseError, OSError) as exc:
|
|
139
|
+
raise RenderDocError(f"Could not parse RenderDoc XML: {exc}") from exc
|
|
140
|
+
if root.tag != "rdc":
|
|
141
|
+
raise RenderDocError(f"Unexpected RenderDoc XML root: {root.tag}")
|
|
142
|
+
header = root.find("header")
|
|
143
|
+
chunks = root.find("chunks")
|
|
144
|
+
if header is None or chunks is None:
|
|
145
|
+
raise RenderDocError("RenderDoc XML is missing header or chunks")
|
|
146
|
+
|
|
147
|
+
driver = header.find("driver")
|
|
148
|
+
thumbnail = header.find("thumbnail")
|
|
149
|
+
names = [chunk.get("name") or "unnamed" for chunk in chunks.findall("chunk")]
|
|
150
|
+
counts = Counter(names)
|
|
151
|
+
return {
|
|
152
|
+
"driver": {
|
|
153
|
+
"id": driver.get("id") if driver is not None else None,
|
|
154
|
+
"name": driver.text.strip() if driver is not None and driver.text else None,
|
|
155
|
+
},
|
|
156
|
+
"machine_ident": _child_text(header, "machineIdent"),
|
|
157
|
+
"thumbnail": {
|
|
158
|
+
"width": int(thumbnail.get("width", "0")) if thumbnail is not None else 0,
|
|
159
|
+
"height": int(thumbnail.get("height", "0")) if thumbnail is not None else 0,
|
|
160
|
+
},
|
|
161
|
+
"chunk_version": chunks.get("version"),
|
|
162
|
+
"chunk_count": len(names),
|
|
163
|
+
"chunk_frequencies": [
|
|
164
|
+
{"name": name, "count": count} for name, count in counts.most_common(20)
|
|
165
|
+
],
|
|
166
|
+
"representative_chunks": names[:representative_limit],
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def inspect_capture(
|
|
171
|
+
capture_file: str,
|
|
172
|
+
*,
|
|
173
|
+
representative_limit: int = 20,
|
|
174
|
+
command: Optional[str] = None,
|
|
175
|
+
) -> dict[str, Any]:
|
|
176
|
+
capture = _require_capture(capture_file)
|
|
177
|
+
with tempfile.TemporaryDirectory(prefix="dcc-mcp-renderdoc-") as directory:
|
|
178
|
+
xml_file = Path(directory) / "capture.xml"
|
|
179
|
+
_run(
|
|
180
|
+
[
|
|
181
|
+
"convert",
|
|
182
|
+
"--filename",
|
|
183
|
+
str(capture),
|
|
184
|
+
"--output",
|
|
185
|
+
str(xml_file),
|
|
186
|
+
"--convert-format",
|
|
187
|
+
"xml",
|
|
188
|
+
],
|
|
189
|
+
timeout_secs=180,
|
|
190
|
+
command=command,
|
|
191
|
+
)
|
|
192
|
+
details = parse_capture_xml(str(xml_file), representative_limit=representative_limit)
|
|
193
|
+
return {"capture_file": str(capture), "size_bytes": capture.stat().st_size, **details}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _prepare_output(output_file: str, expected_suffixes: Iterable[str]) -> Path:
|
|
197
|
+
path = Path(output_file).expanduser().resolve()
|
|
198
|
+
if path.suffix.lower() not in set(expected_suffixes):
|
|
199
|
+
choices = ", ".join(sorted(expected_suffixes))
|
|
200
|
+
raise RenderDocError(f"Output must use one of these extensions: {choices}")
|
|
201
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
return path
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def export_thumbnail(
|
|
206
|
+
capture_file: str,
|
|
207
|
+
output_file: str,
|
|
208
|
+
*,
|
|
209
|
+
max_size: int = 0,
|
|
210
|
+
command: Optional[str] = None,
|
|
211
|
+
) -> dict[str, Any]:
|
|
212
|
+
capture = _require_capture(capture_file)
|
|
213
|
+
output = _prepare_output(output_file, {".bmp", ".jpg", ".png", ".tga"})
|
|
214
|
+
_run(
|
|
215
|
+
["thumb", "--out", str(output), "--max-size", str(max_size), str(capture)],
|
|
216
|
+
timeout_secs=120,
|
|
217
|
+
command=command,
|
|
218
|
+
)
|
|
219
|
+
if not output.is_file():
|
|
220
|
+
raise RenderDocError("RenderDoc reported success but did not create the thumbnail")
|
|
221
|
+
return {
|
|
222
|
+
"capture_file": str(capture),
|
|
223
|
+
"output_file": str(output),
|
|
224
|
+
"size_bytes": output.stat().st_size,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def export_timeline(
|
|
229
|
+
capture_file: str,
|
|
230
|
+
output_file: str,
|
|
231
|
+
*,
|
|
232
|
+
command: Optional[str] = None,
|
|
233
|
+
) -> dict[str, Any]:
|
|
234
|
+
capture = _require_capture(capture_file)
|
|
235
|
+
output = _prepare_output(output_file, {".json"})
|
|
236
|
+
_run(
|
|
237
|
+
[
|
|
238
|
+
"convert",
|
|
239
|
+
"--filename",
|
|
240
|
+
str(capture),
|
|
241
|
+
"--output",
|
|
242
|
+
str(output),
|
|
243
|
+
"--convert-format",
|
|
244
|
+
"chrome.json",
|
|
245
|
+
],
|
|
246
|
+
timeout_secs=180,
|
|
247
|
+
command=command,
|
|
248
|
+
)
|
|
249
|
+
if not output.is_file():
|
|
250
|
+
raise RenderDocError("RenderDoc reported success but did not create the timeline")
|
|
251
|
+
return {
|
|
252
|
+
"capture_file": str(capture),
|
|
253
|
+
"output_file": str(output),
|
|
254
|
+
"size_bytes": output.stat().st_size,
|
|
255
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Standalone RenderDoc MCP server lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from dcc_mcp_core import DccServerOptions
|
|
13
|
+
from dcc_mcp_core.server_base import DccServerBase
|
|
14
|
+
|
|
15
|
+
from .__version__ import __version__
|
|
16
|
+
from .runtime import RenderDocError, get_version
|
|
17
|
+
|
|
18
|
+
DEFAULT_PORT = 8765
|
|
19
|
+
_server: Optional["RenderDocMcpServer"] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RenderDocMcpServer(DccServerBase):
|
|
23
|
+
"""Headless DCC-MCP adapter backed by renderdoccmd."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, port: int = DEFAULT_PORT) -> None:
|
|
26
|
+
os.environ.setdefault("DCC_MCP_PYTHON_EXECUTABLE", sys.executable)
|
|
27
|
+
options = DccServerOptions.from_env(
|
|
28
|
+
"renderdoc",
|
|
29
|
+
Path(__file__).resolve().parent / "skills",
|
|
30
|
+
port=port,
|
|
31
|
+
server_name="dcc-mcp-renderdoc",
|
|
32
|
+
server_version=__version__,
|
|
33
|
+
)
|
|
34
|
+
super().__init__(options=options)
|
|
35
|
+
|
|
36
|
+
def _version_string(self) -> str:
|
|
37
|
+
try:
|
|
38
|
+
return get_version()["version_output"]
|
|
39
|
+
except RenderDocError:
|
|
40
|
+
return "RenderDoc CLI unavailable"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def start_server(port: Optional[int] = None) -> RenderDocMcpServer:
|
|
44
|
+
global _server
|
|
45
|
+
if _server is None or not _server.is_running:
|
|
46
|
+
selected_port = (
|
|
47
|
+
port
|
|
48
|
+
if port is not None
|
|
49
|
+
else int(os.environ.get("DCC_MCP_RENDERDOC_PORT", DEFAULT_PORT))
|
|
50
|
+
)
|
|
51
|
+
_server = RenderDocMcpServer(selected_port)
|
|
52
|
+
_server.register_builtin_actions()
|
|
53
|
+
_server.start()
|
|
54
|
+
return _server
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def stop_server() -> None:
|
|
58
|
+
global _server
|
|
59
|
+
if _server is not None:
|
|
60
|
+
_server.stop()
|
|
61
|
+
_server = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> None:
|
|
65
|
+
"""Run until interrupted."""
|
|
66
|
+
stopped = threading.Event()
|
|
67
|
+
signal.signal(signal.SIGINT, lambda *_: stopped.set())
|
|
68
|
+
if hasattr(signal, "SIGTERM"):
|
|
69
|
+
signal.signal(signal.SIGTERM, lambda *_: stopped.set())
|
|
70
|
+
start_server()
|
|
71
|
+
try:
|
|
72
|
+
stopped.wait()
|
|
73
|
+
finally:
|
|
74
|
+
stop_server()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: renderdoc-analysis
|
|
3
|
+
description: >-
|
|
4
|
+
Domain skill — Inspect an existing RenderDoc capture and export its embedded thumbnail or Chrome
|
|
5
|
+
trace. Use for offline graphics triage and automation artifacts. Not for launching a capture —
|
|
6
|
+
use renderdoc-capture.
|
|
7
|
+
license: MIT
|
|
8
|
+
compatibility: "RenderDoc 1.45+; dcc-mcp-core 0.19+"
|
|
9
|
+
allowed-tools: "python"
|
|
10
|
+
metadata:
|
|
11
|
+
dcc-mcp:
|
|
12
|
+
dcc: renderdoc
|
|
13
|
+
layer: domain
|
|
14
|
+
version: "0.1.0"
|
|
15
|
+
search-hint: "RenderDoc inspect rdc chunks thumbnail Chrome trace graphics analysis"
|
|
16
|
+
tags: "renderdoc,analysis,thumbnail,timeline,graphics-debugging"
|
|
17
|
+
tools: tools.yaml
|
|
18
|
+
depends: "dcc-diagnostics"
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# RenderDoc Analysis
|
|
22
|
+
|
|
23
|
+
Inspect before exporting. These tools never modify the input `.rdc`; exports require an explicit
|
|
24
|
+
destination path and create its parent directory when needed.
|
|
25
|
+
|
dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from dcc_mcp_core.skill import skill_entry, skill_success
|
|
2
|
+
|
|
3
|
+
from dcc_mcp_renderdoc.runtime import export_thumbnail
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@skill_entry
|
|
7
|
+
def main(capture_file: str, output_file: str, max_size: int = 0, **_kwargs):
|
|
8
|
+
result = export_thumbnail(capture_file, output_file, max_size=max_size)
|
|
9
|
+
return skill_success("RenderDoc thumbnail exported.", **result)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
from dcc_mcp_core.skill import run_main
|
|
14
|
+
|
|
15
|
+
run_main(main)
|
dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from dcc_mcp_core.skill import skill_entry, skill_success
|
|
2
|
+
|
|
3
|
+
from dcc_mcp_renderdoc.runtime import export_timeline
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@skill_entry
|
|
7
|
+
def main(capture_file: str, output_file: str, **_kwargs):
|
|
8
|
+
result = export_timeline(capture_file, output_file)
|
|
9
|
+
return skill_success("RenderDoc Chrome timeline exported.", **result)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
from dcc_mcp_core.skill import run_main
|
|
14
|
+
|
|
15
|
+
run_main(main)
|
dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from dcc_mcp_core.skill import skill_entry, skill_success
|
|
2
|
+
|
|
3
|
+
from dcc_mcp_renderdoc.runtime import inspect_capture
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@skill_entry
|
|
7
|
+
def main(capture_file: str, representative_limit: int = 20, **_kwargs):
|
|
8
|
+
result = inspect_capture(capture_file, representative_limit=representative_limit)
|
|
9
|
+
return skill_success("RenderDoc capture inspected.", **result)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
from dcc_mcp_core.skill import run_main
|
|
14
|
+
|
|
15
|
+
run_main(main)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
tools:
|
|
2
|
+
- name: inspect_capture
|
|
3
|
+
description: Convert one .rdc capture to temporary XML and summarize stable header and chunk metadata. Use for offline triage. Not for GPU performance conclusions or high-level drawcall reconstruction.
|
|
4
|
+
input_schema:
|
|
5
|
+
type: object
|
|
6
|
+
required: [capture_file]
|
|
7
|
+
properties:
|
|
8
|
+
capture_file: {type: string, minLength: 1, maxLength: 1000}
|
|
9
|
+
representative_limit: {type: integer, minimum: 0, maximum: 100, default: 20}
|
|
10
|
+
additionalProperties: false
|
|
11
|
+
output_schema: {type: object}
|
|
12
|
+
read_only: true
|
|
13
|
+
destructive: false
|
|
14
|
+
idempotent: true
|
|
15
|
+
execution: async
|
|
16
|
+
affinity: any
|
|
17
|
+
timeout_hint_secs: 180
|
|
18
|
+
source_file: scripts/inspect_capture.py
|
|
19
|
+
next-tools:
|
|
20
|
+
on-success: [renderdoc_analysis__export_thumbnail, renderdoc_analysis__export_timeline]
|
|
21
|
+
on-failure: [renderdoc_capture__get_version, dcc_diagnostics__audit_log]
|
|
22
|
+
- name: export_thumbnail
|
|
23
|
+
description: Export the embedded capture thumbnail as PNG, JPG, BMP, or TGA. Use for visual review artifacts. Not for replaying or rendering arbitrary capture resources.
|
|
24
|
+
input_schema:
|
|
25
|
+
type: object
|
|
26
|
+
required: [capture_file, output_file]
|
|
27
|
+
properties:
|
|
28
|
+
capture_file: {type: string, minLength: 1, maxLength: 1000}
|
|
29
|
+
output_file: {type: string, minLength: 1, maxLength: 1000}
|
|
30
|
+
max_size: {type: integer, minimum: 0, maximum: 16384, default: 0}
|
|
31
|
+
additionalProperties: false
|
|
32
|
+
output_schema: {type: object}
|
|
33
|
+
read_only: false
|
|
34
|
+
destructive: false
|
|
35
|
+
idempotent: true
|
|
36
|
+
execution: async
|
|
37
|
+
affinity: any
|
|
38
|
+
timeout_hint_secs: 120
|
|
39
|
+
source_file: scripts/export_thumbnail.py
|
|
40
|
+
next-tools:
|
|
41
|
+
on-success: [renderdoc_analysis__inspect_capture]
|
|
42
|
+
on-failure: [renderdoc_capture__get_version, dcc_diagnostics__audit_log]
|
|
43
|
+
- name: export_timeline
|
|
44
|
+
description: Export capture events as Chrome profiler JSON. Use for timeline tooling and CI artifacts. Not for authoritative GPU duration measurement.
|
|
45
|
+
input_schema:
|
|
46
|
+
type: object
|
|
47
|
+
required: [capture_file, output_file]
|
|
48
|
+
properties:
|
|
49
|
+
capture_file: {type: string, minLength: 1, maxLength: 1000}
|
|
50
|
+
output_file: {type: string, minLength: 1, maxLength: 1000}
|
|
51
|
+
additionalProperties: false
|
|
52
|
+
output_schema: {type: object}
|
|
53
|
+
read_only: false
|
|
54
|
+
destructive: false
|
|
55
|
+
idempotent: true
|
|
56
|
+
execution: async
|
|
57
|
+
affinity: any
|
|
58
|
+
timeout_hint_secs: 180
|
|
59
|
+
source_file: scripts/export_timeline.py
|
|
60
|
+
next-tools:
|
|
61
|
+
on-success: [renderdoc_analysis__inspect_capture]
|
|
62
|
+
on-failure: [renderdoc_capture__get_version, dcc_diagnostics__audit_log]
|
|
63
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: renderdoc-capture
|
|
3
|
+
description: >-
|
|
4
|
+
Domain skill — Discover RenderDoc and launch an explicit game or graphics executable under
|
|
5
|
+
capture. Use for repeatable local or CI frame capture. Not for reading an existing capture —
|
|
6
|
+
use renderdoc-analysis.
|
|
7
|
+
license: MIT
|
|
8
|
+
compatibility: "RenderDoc 1.45+; dcc-mcp-core 0.19+"
|
|
9
|
+
allowed-tools: "python"
|
|
10
|
+
metadata:
|
|
11
|
+
dcc-mcp:
|
|
12
|
+
dcc: renderdoc
|
|
13
|
+
layer: domain
|
|
14
|
+
version: "0.1.0"
|
|
15
|
+
search-hint: "RenderDoc capture launch executable game graphics frame rdc"
|
|
16
|
+
tags: "renderdoc,capture,graphics-debugging,game-development"
|
|
17
|
+
tools: tools.yaml
|
|
18
|
+
depends: "dcc-diagnostics"
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# RenderDoc Capture
|
|
22
|
+
|
|
23
|
+
Call `get_version` before capture. `capture_program` starts only the explicit executable and
|
|
24
|
+
arguments, never a shell. The target must trigger a RenderDoc frame capture.
|
|
25
|
+
|
dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dcc_mcp_core.skill import skill_entry, skill_success
|
|
2
|
+
|
|
3
|
+
from dcc_mcp_renderdoc.runtime import capture_program
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@skill_entry
|
|
7
|
+
def main(
|
|
8
|
+
executable: str,
|
|
9
|
+
output_template: str,
|
|
10
|
+
arguments=None,
|
|
11
|
+
working_directory=None,
|
|
12
|
+
wait_for_exit: bool = True,
|
|
13
|
+
api_validation: bool = False,
|
|
14
|
+
hook_children: bool = False,
|
|
15
|
+
timeout_secs: int = 300,
|
|
16
|
+
**_kwargs,
|
|
17
|
+
):
|
|
18
|
+
result = capture_program(
|
|
19
|
+
executable,
|
|
20
|
+
output_template,
|
|
21
|
+
arguments=arguments,
|
|
22
|
+
working_directory=working_directory,
|
|
23
|
+
wait_for_exit=wait_for_exit,
|
|
24
|
+
api_validation=api_validation,
|
|
25
|
+
hook_children=hook_children,
|
|
26
|
+
timeout_secs=timeout_secs,
|
|
27
|
+
)
|
|
28
|
+
return skill_success(f"Created {len(result['captures'])} RenderDoc capture(s).", **result)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
from dcc_mcp_core.skill import run_main
|
|
33
|
+
|
|
34
|
+
run_main(main)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dcc_mcp_core.skill import skill_entry, skill_success
|
|
2
|
+
|
|
3
|
+
from dcc_mcp_renderdoc.runtime import get_version
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@skill_entry
|
|
7
|
+
def main(**_kwargs):
|
|
8
|
+
return skill_success("RenderDoc CLI is available.", **get_version())
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
from dcc_mcp_core.skill import run_main
|
|
13
|
+
|
|
14
|
+
run_main(main)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
tools:
|
|
2
|
+
- name: get_version
|
|
3
|
+
description: Resolve renderdoccmd and return its version output. Use before capture or analysis to verify the runtime. Not for capture-file inspection.
|
|
4
|
+
input_schema: {type: object, properties: {}, additionalProperties: false}
|
|
5
|
+
output_schema: {type: object}
|
|
6
|
+
read_only: true
|
|
7
|
+
destructive: false
|
|
8
|
+
idempotent: true
|
|
9
|
+
execution: sync
|
|
10
|
+
affinity: any
|
|
11
|
+
timeout_hint_secs: 30
|
|
12
|
+
source_file: scripts/get_version.py
|
|
13
|
+
next-tools:
|
|
14
|
+
on-success: [renderdoc_capture__capture_program]
|
|
15
|
+
on-failure: [dcc_diagnostics__audit_log]
|
|
16
|
+
- name: capture_program
|
|
17
|
+
description: Launch one explicit executable under RenderDoc and return newly created .rdc files. Use for controlled game or graphics test capture. Not for shell commands or attaching to an existing process.
|
|
18
|
+
input_schema:
|
|
19
|
+
type: object
|
|
20
|
+
required: [executable, output_template]
|
|
21
|
+
properties:
|
|
22
|
+
executable: {type: string, minLength: 1, maxLength: 1000}
|
|
23
|
+
output_template: {type: string, minLength: 1, maxLength: 1000}
|
|
24
|
+
arguments:
|
|
25
|
+
type: array
|
|
26
|
+
maxItems: 100
|
|
27
|
+
items: {type: string, maxLength: 2000}
|
|
28
|
+
working_directory: {type: string, maxLength: 1000}
|
|
29
|
+
wait_for_exit: {type: boolean, default: true}
|
|
30
|
+
api_validation: {type: boolean, default: false}
|
|
31
|
+
hook_children: {type: boolean, default: false}
|
|
32
|
+
timeout_secs: {type: integer, minimum: 1, maximum: 3600, default: 300}
|
|
33
|
+
additionalProperties: false
|
|
34
|
+
output_schema: {type: object}
|
|
35
|
+
read_only: false
|
|
36
|
+
destructive: false
|
|
37
|
+
idempotent: false
|
|
38
|
+
execution: async
|
|
39
|
+
affinity: any
|
|
40
|
+
timeout_hint_secs: 3600
|
|
41
|
+
source_file: scripts/capture_program.py
|
|
42
|
+
next-tools:
|
|
43
|
+
on-success: [renderdoc_analysis__inspect_capture, renderdoc_analysis__export_thumbnail]
|
|
44
|
+
on-failure: [dcc_diagnostics__audit_log]
|
|
45
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Capture and inspect one real OpenGL frame through the MCP endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import urllib.request
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from dcc_mcp_renderdoc.server import RenderDocMcpServer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _post(url: str, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
17
|
+
payload: dict[str, Any] = {"jsonrpc": "2.0", "id": 1, "method": method}
|
|
18
|
+
if params is not None:
|
|
19
|
+
payload["params"] = params
|
|
20
|
+
request = urllib.request.Request(
|
|
21
|
+
url,
|
|
22
|
+
data=json.dumps(payload).encode(),
|
|
23
|
+
headers={
|
|
24
|
+
"Accept": "application/json, text/event-stream",
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
with urllib.request.urlopen(request, timeout=60) as response:
|
|
29
|
+
return json.loads(response.read())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _content(response: dict[str, Any]) -> dict[str, Any]:
|
|
33
|
+
if response.get("error") or response.get("result", {}).get("isError"):
|
|
34
|
+
raise RuntimeError(json.dumps(response, indent=2))
|
|
35
|
+
result = response.get("result", {})
|
|
36
|
+
structured = result.get("structuredContent")
|
|
37
|
+
if isinstance(structured, dict):
|
|
38
|
+
return structured
|
|
39
|
+
if result.get("content"):
|
|
40
|
+
return json.loads(result["content"][0]["text"])
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _call(url: str, name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
45
|
+
return _content(_post(url, "tools/call", {"name": name, "arguments": arguments or {}}))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _wait(url: str, envelope: dict[str, Any]) -> dict[str, Any]:
|
|
49
|
+
job_id = envelope.get("job_id")
|
|
50
|
+
if not job_id:
|
|
51
|
+
return envelope
|
|
52
|
+
deadline = time.monotonic() + 300
|
|
53
|
+
while time.monotonic() < deadline:
|
|
54
|
+
status = _call(url, "jobs_get_status", {"job_id": job_id, "include_result": True})
|
|
55
|
+
if status.get("status") == "completed":
|
|
56
|
+
return status
|
|
57
|
+
if status.get("status") in {"failed", "cancelled", "interrupted"}:
|
|
58
|
+
raise RuntimeError(json.dumps(status, indent=2))
|
|
59
|
+
time.sleep(0.1)
|
|
60
|
+
raise TimeoutError(f"RenderDoc MCP job {job_id} did not finish")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _tool(url: str, suffix: str) -> str:
|
|
64
|
+
names: list[str] = []
|
|
65
|
+
cursor = None
|
|
66
|
+
for _ in range(20):
|
|
67
|
+
response = _post(url, "tools/list", {"cursor": cursor} if cursor else None)
|
|
68
|
+
result = response.get("result", {})
|
|
69
|
+
names.extend(tool["name"] for tool in result.get("tools", []))
|
|
70
|
+
cursor = result.get("nextCursor")
|
|
71
|
+
if not cursor:
|
|
72
|
+
break
|
|
73
|
+
matches = [name for name in names if name == suffix or name.endswith(f"__{suffix}")]
|
|
74
|
+
if len(matches) != 1:
|
|
75
|
+
raise RuntimeError(f"Expected one tool ending in {suffix!r}, found {matches!r}")
|
|
76
|
+
return matches[0]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main() -> None:
|
|
80
|
+
parser = argparse.ArgumentParser()
|
|
81
|
+
parser.add_argument("--renderdoc", required=True)
|
|
82
|
+
parser.add_argument("--program", required=True)
|
|
83
|
+
parser.add_argument("--artifacts", type=Path, default=Path("artifacts"))
|
|
84
|
+
args = parser.parse_args()
|
|
85
|
+
|
|
86
|
+
artifacts = args.artifacts.resolve()
|
|
87
|
+
artifacts.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
os.environ["DCC_MCP_RENDERDOC_CMD"] = str(Path(args.renderdoc).resolve())
|
|
89
|
+
program = str(Path(args.program).resolve())
|
|
90
|
+
|
|
91
|
+
server = RenderDocMcpServer(port=0)
|
|
92
|
+
try:
|
|
93
|
+
server.register_builtin_actions()
|
|
94
|
+
server.start(install_atexit_hook=False)
|
|
95
|
+
url = server.mcp_url
|
|
96
|
+
_call(url, "load_skill", {"skill_name": "renderdoc-capture"})
|
|
97
|
+
_call(url, "load_skill", {"skill_name": "renderdoc-analysis"})
|
|
98
|
+
version = _wait(url, _call(url, _tool(url, "get_version")))
|
|
99
|
+
capture_job = _call(
|
|
100
|
+
url,
|
|
101
|
+
_tool(url, "capture_program"),
|
|
102
|
+
{
|
|
103
|
+
"executable": program,
|
|
104
|
+
"output_template": str(artifacts / "smoke"),
|
|
105
|
+
"arguments": [str(artifacts / "smoke")],
|
|
106
|
+
"working_directory": str(Path(program).parent),
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
_wait(url, capture_job)
|
|
110
|
+
captures = sorted(artifacts.glob("*.rdc"))
|
|
111
|
+
if not captures:
|
|
112
|
+
raise RuntimeError("MCP capture tool did not create an .rdc file")
|
|
113
|
+
capture = str(captures[-1])
|
|
114
|
+
inspection = _wait(
|
|
115
|
+
url,
|
|
116
|
+
_call(url, _tool(url, "inspect_capture"), {"capture_file": capture}),
|
|
117
|
+
)
|
|
118
|
+
_wait(
|
|
119
|
+
url,
|
|
120
|
+
_call(
|
|
121
|
+
url,
|
|
122
|
+
_tool(url, "export_thumbnail"),
|
|
123
|
+
{"capture_file": capture, "output_file": str(artifacts / "smoke.png")},
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
_wait(
|
|
127
|
+
url,
|
|
128
|
+
_call(
|
|
129
|
+
url,
|
|
130
|
+
_tool(url, "export_timeline"),
|
|
131
|
+
{"capture_file": capture, "output_file": str(artifacts / "smoke.json")},
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
assert (artifacts / "smoke.png").stat().st_size > 0
|
|
135
|
+
assert (artifacts / "smoke.json").stat().st_size > 0
|
|
136
|
+
print(json.dumps({"version": version, "inspection_job": inspection}))
|
|
137
|
+
finally:
|
|
138
|
+
server.stop()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#include <GL/gl.h>
|
|
2
|
+
#include <GL/glx.h>
|
|
3
|
+
#include <X11/Xlib.h>
|
|
4
|
+
#include <dlfcn.h>
|
|
5
|
+
#include <stdbool.h>
|
|
6
|
+
#include <stdio.h>
|
|
7
|
+
#include <unistd.h>
|
|
8
|
+
|
|
9
|
+
#include "renderdoc_app.h"
|
|
10
|
+
|
|
11
|
+
#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091
|
|
12
|
+
#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092
|
|
13
|
+
#define GLX_CONTEXT_PROFILE_MASK_ARB 0x9126
|
|
14
|
+
#define GLX_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
|
|
15
|
+
|
|
16
|
+
typedef GLXContext (*CreateContextAttribs)(Display *, GLXFBConfig, GLXContext, Bool, const int *);
|
|
17
|
+
|
|
18
|
+
int main(int argc, char **argv) {
|
|
19
|
+
Display *display = XOpenDisplay(NULL);
|
|
20
|
+
int count = 0;
|
|
21
|
+
int attributes[] = {GLX_X_RENDERABLE, True, GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
|
|
22
|
+
GLX_RENDER_TYPE, GLX_RGBA_BIT, GLX_DOUBLEBUFFER, True, None};
|
|
23
|
+
GLXFBConfig *configs = display ? glXChooseFBConfig(display, DefaultScreen(display), attributes, &count) : NULL;
|
|
24
|
+
XVisualInfo *visual = count > 0 ? glXGetVisualFromFBConfig(display, configs[0]) : NULL;
|
|
25
|
+
CreateContextAttribs create_context =
|
|
26
|
+
(CreateContextAttribs)glXGetProcAddressARB((const GLubyte *)"glXCreateContextAttribsARB");
|
|
27
|
+
if (!display || !visual || !create_context) return 2;
|
|
28
|
+
|
|
29
|
+
Window root = RootWindow(display, visual->screen);
|
|
30
|
+
Colormap color_map = XCreateColormap(display, root, visual->visual, AllocNone);
|
|
31
|
+
XSetWindowAttributes window_attributes = {0};
|
|
32
|
+
window_attributes.colormap = color_map;
|
|
33
|
+
window_attributes.event_mask = ExposureMask;
|
|
34
|
+
Window window = XCreateWindow(display, root, 0, 0, 320, 200, 0, visual->depth, InputOutput,
|
|
35
|
+
visual->visual, CWColormap | CWEventMask, &window_attributes);
|
|
36
|
+
int context_attributes[] = {GLX_CONTEXT_MAJOR_VERSION_ARB, 3, GLX_CONTEXT_MINOR_VERSION_ARB, 2,
|
|
37
|
+
GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB, None};
|
|
38
|
+
GLXContext context = create_context(display, configs[0], NULL, True, context_attributes);
|
|
39
|
+
XFree(configs);
|
|
40
|
+
if (!context) return 2;
|
|
41
|
+
XMapWindow(display, window);
|
|
42
|
+
glXMakeCurrent(display, window, context);
|
|
43
|
+
|
|
44
|
+
pRENDERDOC_GetAPI get_api = (pRENDERDOC_GetAPI)dlsym(RTLD_DEFAULT, "RENDERDOC_GetAPI");
|
|
45
|
+
RENDERDOC_API_1_1_2 *api = NULL;
|
|
46
|
+
if (!get_api || !get_api(eRENDERDOC_API_Version_1_1_2, (void **)&api)) return 3;
|
|
47
|
+
if (argc > 1) api->SetCaptureFilePathTemplate(argv[1]);
|
|
48
|
+
|
|
49
|
+
glViewport(0, 0, 320, 200);
|
|
50
|
+
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
|
51
|
+
glClear(GL_COLOR_BUFFER_BIT);
|
|
52
|
+
glXSwapBuffers(display, window);
|
|
53
|
+
glFinish();
|
|
54
|
+
|
|
55
|
+
api->StartFrameCapture(NULL, NULL);
|
|
56
|
+
if (!api->IsFrameCapturing()) return 4;
|
|
57
|
+
glViewport(0, 0, 320, 200);
|
|
58
|
+
glClearColor(0.1f, 0.4f, 0.8f, 1.0f);
|
|
59
|
+
glClear(GL_COLOR_BUFFER_BIT);
|
|
60
|
+
glXSwapBuffers(display, window);
|
|
61
|
+
glFinish();
|
|
62
|
+
if (!api->EndFrameCapture(NULL, NULL)) return 5;
|
|
63
|
+
usleep(200000);
|
|
64
|
+
|
|
65
|
+
glXMakeCurrent(display, None, NULL);
|
|
66
|
+
glXDestroyContext(display, context);
|
|
67
|
+
XDestroyWindow(display, window);
|
|
68
|
+
XCloseDisplay(display);
|
|
69
|
+
printf("captured=%u\n", api->GetNumCaptures());
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from tools.download_latest_renderdoc import _safe_destination, latest_version
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_latest_version_uses_numeric_ordering():
|
|
9
|
+
html = "/stable/1.9/renderdoc_1.9.tar.gz /stable/1.45/renderdoc_1.45.tar.gz"
|
|
10
|
+
assert latest_version(html) == "1.45"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_archive_member_must_remain_below_destination(tmp_path: Path):
|
|
14
|
+
with pytest.raises(RuntimeError, match="escapes destination"):
|
|
15
|
+
_safe_destination(tmp_path, "../outside")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from dcc_mcp_renderdoc.server import RenderDocMcpServer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_server_constructs_with_headless_contract():
|
|
7
|
+
server = RenderDocMcpServer(port=0)
|
|
8
|
+
try:
|
|
9
|
+
assert server._options.server_name == "dcc-mcp-renderdoc"
|
|
10
|
+
assert server._options.dcc_name == "renderdoc"
|
|
11
|
+
finally:
|
|
12
|
+
server.stop()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_bundled_skills_and_release_workflow_exist():
|
|
16
|
+
root = Path(__file__).parents[1]
|
|
17
|
+
skills = root / "src" / "dcc_mcp_renderdoc" / "skills"
|
|
18
|
+
assert {path.name for path in skills.iterdir() if path.is_dir()} == {
|
|
19
|
+
"renderdoc-analysis",
|
|
20
|
+
"renderdoc-capture",
|
|
21
|
+
}
|
|
22
|
+
assert (root / ".github" / "workflows" / "release.yml").is_file()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from subprocess import CompletedProcess
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from dcc_mcp_renderdoc import runtime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_parse_capture_xml_summarizes_stable_fields(tmp_path: Path):
|
|
10
|
+
xml_file = tmp_path / "capture.xml"
|
|
11
|
+
xml_file.write_text(
|
|
12
|
+
"""<rdc><header><driver id="2">Vulkan</driver><machineIdent>123</machineIdent>
|
|
13
|
+
<thumbnail width="640" height="360" /></header><chunks version="17">
|
|
14
|
+
<chunk name="vkCmdDraw"/><chunk name="vkCmdDraw"/><chunk name="Present"/>
|
|
15
|
+
</chunks></rdc>""",
|
|
16
|
+
encoding="utf-8",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
result = runtime.parse_capture_xml(str(xml_file), representative_limit=2)
|
|
20
|
+
|
|
21
|
+
assert result["driver"] == {"id": "2", "name": "Vulkan"}
|
|
22
|
+
assert result["thumbnail"] == {"width": 640, "height": 360}
|
|
23
|
+
assert result["chunk_count"] == 3
|
|
24
|
+
assert result["chunk_frequencies"][0] == {"name": "vkCmdDraw", "count": 2}
|
|
25
|
+
assert result["representative_chunks"] == ["vkCmdDraw", "vkCmdDraw"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_resolve_renderdoccmd_prefers_explicit_file(tmp_path: Path):
|
|
29
|
+
executable = tmp_path / "renderdoccmd"
|
|
30
|
+
executable.touch()
|
|
31
|
+
assert runtime.resolve_renderdoccmd(str(executable)) == executable.resolve()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_capture_program_uses_argument_vector_and_reports_new_capture(tmp_path, monkeypatch):
|
|
35
|
+
target = tmp_path / "game.exe"
|
|
36
|
+
target.touch()
|
|
37
|
+
observed = {}
|
|
38
|
+
|
|
39
|
+
def fake_run(arguments, **_kwargs):
|
|
40
|
+
observed["arguments"] = arguments
|
|
41
|
+
(tmp_path / "capture_frame1.rdc").touch()
|
|
42
|
+
return CompletedProcess(arguments, 0, "captured", "")
|
|
43
|
+
|
|
44
|
+
monkeypatch.setattr(runtime, "_run", fake_run)
|
|
45
|
+
result = runtime.capture_program(
|
|
46
|
+
str(target),
|
|
47
|
+
str(tmp_path / "capture"),
|
|
48
|
+
arguments=["--scene", "demo"],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
assert observed["arguments"][-3:] == [str(target.resolve()), "--scene", "demo"]
|
|
52
|
+
assert result["captures"] == [str((tmp_path / "capture_frame1.rdc").resolve())]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_invalid_capture_is_rejected(tmp_path: Path):
|
|
56
|
+
with pytest.raises(runtime.RenderDocError, match="not .rdc"):
|
|
57
|
+
runtime.inspect_capture(str(tmp_path / "missing.rdc"))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Repository tooling helpers."""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Download the latest stable RenderDoc bundle from the official builds page."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
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
|
+
BUILDS_URL = "https://renderdoc.org/builds"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def latest_version(html: str) -> str:
|
|
18
|
+
versions = set(re.findall(r"/stable/([0-9]+\.[0-9]+)/", html))
|
|
19
|
+
if not versions:
|
|
20
|
+
raise RuntimeError("No stable RenderDoc versions found on the official builds page")
|
|
21
|
+
return max(versions, key=lambda value: tuple(int(part) for part in value.split(".")))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _safe_destination(root: Path, member_name: str) -> Path:
|
|
25
|
+
destination = (root / member_name).resolve()
|
|
26
|
+
try:
|
|
27
|
+
destination.relative_to(root.resolve())
|
|
28
|
+
except ValueError as exc:
|
|
29
|
+
raise RuntimeError(f"Archive member escapes destination: {member_name}") from exc
|
|
30
|
+
return destination
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_archive(archive: Path, destination: Path) -> None:
|
|
34
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
if zipfile.is_zipfile(archive):
|
|
36
|
+
with zipfile.ZipFile(archive) as bundle:
|
|
37
|
+
for member in bundle.infolist():
|
|
38
|
+
_safe_destination(destination, member.filename)
|
|
39
|
+
bundle.extractall(destination)
|
|
40
|
+
return
|
|
41
|
+
with tarfile.open(archive, "r:gz") as bundle:
|
|
42
|
+
for member in bundle.getmembers():
|
|
43
|
+
_safe_destination(destination, member.name)
|
|
44
|
+
if member.issym() or member.islnk():
|
|
45
|
+
raise RuntimeError(f"Archive links are not accepted: {member.name}")
|
|
46
|
+
bundle.extractall(destination)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def download_latest(output: Path, platform: str = sys.platform) -> Path:
|
|
50
|
+
request = urllib.request.Request(BUILDS_URL, headers={"User-Agent": "dcc-mcp-renderdoc"})
|
|
51
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
52
|
+
version = latest_version(response.read().decode("utf-8"))
|
|
53
|
+
if platform == "win32":
|
|
54
|
+
archive_name = f"RenderDoc_{version}_64.zip"
|
|
55
|
+
elif platform.startswith("linux"):
|
|
56
|
+
archive_name = f"renderdoc_{version}.tar.gz"
|
|
57
|
+
else:
|
|
58
|
+
raise RuntimeError(f"RenderDoc has no supported desktop bundle for {platform}")
|
|
59
|
+
|
|
60
|
+
version_dir = output.resolve() / version
|
|
61
|
+
command_name = "renderdoccmd.exe" if platform == "win32" else "renderdoccmd"
|
|
62
|
+
existing = next(version_dir.rglob(command_name), None) if version_dir.exists() else None
|
|
63
|
+
if existing:
|
|
64
|
+
return existing.resolve()
|
|
65
|
+
|
|
66
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
archive = output / archive_name
|
|
68
|
+
url = f"https://renderdoc.org/stable/{version}/{archive_name}"
|
|
69
|
+
with urllib.request.urlopen(url, timeout=120) as response, archive.open("wb") as stream:
|
|
70
|
+
shutil.copyfileobj(response, stream)
|
|
71
|
+
extract_archive(archive, version_dir)
|
|
72
|
+
archive.unlink()
|
|
73
|
+
command = next(version_dir.rglob(command_name), None)
|
|
74
|
+
if command is None:
|
|
75
|
+
raise RuntimeError(f"Downloaded bundle did not contain {command_name}")
|
|
76
|
+
if platform.startswith("linux"):
|
|
77
|
+
command.chmod(command.stat().st_mode | 0o111)
|
|
78
|
+
return command.resolve()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main() -> None:
|
|
82
|
+
parser = argparse.ArgumentParser()
|
|
83
|
+
parser.add_argument("--output", type=Path, default=Path(".renderdoc-bin"))
|
|
84
|
+
args = parser.parse_args()
|
|
85
|
+
print(download_latest(args.output))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from dcc_mcp_core import validate_skill
|
|
4
|
+
|
|
5
|
+
skills_root = Path(__file__).parents[1] / "src" / "dcc_mcp_renderdoc" / "skills"
|
|
6
|
+
reports = [validate_skill(str(path)) for path in skills_root.iterdir() if path.is_dir()]
|
|
7
|
+
assert all(report.is_clean for report in reports), [report.issues for report in reports]
|
|
8
|
+
print(f"validated {len(reports)} bundled skills")
|