dcc-mcp-renderdoc 0.3.3__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.3 → dcc_mcp_renderdoc-0.4.0}/CHANGELOG.md +13 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/PKG-INFO +3 -2
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/README.md +2 -1
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/pyproject.toml +1 -1
- {dcc_mcp_renderdoc-0.3.3 → 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/downloader.py +88 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/runtime.py +136 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/SKILL.md +3 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/tools.yaml +1 -1
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +5 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/tools.yaml +2 -2
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/tests/test_runtime.py +154 -7
- dcc_mcp_renderdoc-0.3.3/.release-please-manifest.json +0 -2
- dcc_mcp_renderdoc-0.3.3/src/dcc_mcp_renderdoc/__version__.py +0 -3
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/.gitignore +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/LICENSE +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/__init__.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/_target_control.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/server.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/metadata/depends.md +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/metadata/depends.md +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_process.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/get_version.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/tests/live_renderdoc_smoke.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/tests/renderdoc_smoke.c +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/tests/test_download.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/tests/test_package.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/tools/__init__.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/tools/download_latest_renderdoc.py +0 -0
- {dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/tools/lint_skills.py +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
3
16
|
## [0.3.3](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.2...v0.3.3) (2026-07-22)
|
|
4
17
|
|
|
5
18
|
|
|
@@ -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
|
|
@@ -105,7 +105,8 @@ through the stable gateway at `http://127.0.0.1:9765/mcp`; set
|
|
|
105
105
|
- Launch a game or test executable under RenderDoc and wait for a typed `.rdc` capture.
|
|
106
106
|
- Trigger a capture through official Target Control after a configurable delay.
|
|
107
107
|
- Inject into a process that had to be launched by a platform client, then trigger and collect a capture.
|
|
108
|
-
-
|
|
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
|
|
|
@@ -77,7 +77,8 @@ through the stable gateway at `http://127.0.0.1:9765/mcp`; set
|
|
|
77
77
|
- Launch a game or test executable under RenderDoc and wait for a typed `.rdc` capture.
|
|
78
78
|
- Trigger a capture through official Target Control after a configurable delay.
|
|
79
79
|
- Inject into a process that had to be launched by a platform client, then trigger and collect a capture.
|
|
80
|
-
-
|
|
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
|
|
|
@@ -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,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()
|
|
@@ -16,6 +16,8 @@ from collections import Counter
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any, Iterable, Optional, Sequence
|
|
18
18
|
|
|
19
|
+
from .downloader import download_latest
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class RenderDocError(RuntimeError):
|
|
21
23
|
"""Raised when a RenderDoc operation cannot satisfy its contract."""
|
|
@@ -120,6 +122,17 @@ def resolve_renderdoccmd(explicit: Optional[str] = None) -> Path:
|
|
|
120
122
|
found = shutil.which(name)
|
|
121
123
|
if found:
|
|
122
124
|
return Path(found).resolve()
|
|
125
|
+
if os.environ.get("DCC_MCP_RENDERDOC_AUTO_DOWNLOAD", "1").lower() not in {
|
|
126
|
+
"0",
|
|
127
|
+
"false",
|
|
128
|
+
"no",
|
|
129
|
+
}:
|
|
130
|
+
try:
|
|
131
|
+
return download_latest()
|
|
132
|
+
except (OSError, RuntimeError) as exc:
|
|
133
|
+
raise RenderDocError(
|
|
134
|
+
f"RenderDoc was not found and automatic download failed: {exc}"
|
|
135
|
+
) from exc
|
|
123
136
|
raise RenderDocError(
|
|
124
137
|
"renderdoccmd was not found; install RenderDoc or set DCC_MCP_RENDERDOC_CMD"
|
|
125
138
|
)
|
|
@@ -472,9 +485,11 @@ def capture_program(
|
|
|
472
485
|
f"before creating its graphics device. {diagnostics}. "
|
|
473
486
|
f"RenderDoc output: {output_detail}"
|
|
474
487
|
)
|
|
488
|
+
capture_validations = _validate_frame_captures(captures, command=command)
|
|
475
489
|
return {
|
|
476
490
|
"target": str(target),
|
|
477
491
|
"captures": [str(path) for path in captures],
|
|
492
|
+
"capture_validations": capture_validations,
|
|
478
493
|
"focused_target_window": False,
|
|
479
494
|
"trigger_mode": trigger_mode,
|
|
480
495
|
"stdout": stdout.strip(),
|
|
@@ -542,9 +557,11 @@ def capture_process(
|
|
|
542
557
|
f"capture_program. {diagnostics}. "
|
|
543
558
|
f"RenderDoc output: {output_detail}"
|
|
544
559
|
)
|
|
560
|
+
capture_validations = _validate_frame_captures(captures, command=command)
|
|
545
561
|
return {
|
|
546
562
|
"process_id": process_id,
|
|
547
563
|
"captures": [str(path) for path in captures],
|
|
564
|
+
"capture_validations": capture_validations,
|
|
548
565
|
"focused_target_window": False,
|
|
549
566
|
"trigger_mode": "target_control",
|
|
550
567
|
"stdout": stdout.strip(),
|
|
@@ -626,6 +643,75 @@ def _child_text(parent: ET.Element, name: str) -> Optional[str]:
|
|
|
626
643
|
return child.text.strip() if child is not None and child.text else None
|
|
627
644
|
|
|
628
645
|
|
|
646
|
+
def _chunk_operation(name: str) -> str:
|
|
647
|
+
if name.casefold().startswith("internal::"):
|
|
648
|
+
return ""
|
|
649
|
+
return name.rsplit("::", 1)[-1].casefold()
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _is_draw_or_dispatch_chunk(name: str) -> bool:
|
|
653
|
+
operation = _chunk_operation(name)
|
|
654
|
+
if operation.startswith("gldrawbuffer"):
|
|
655
|
+
return False
|
|
656
|
+
return operation.startswith(
|
|
657
|
+
(
|
|
658
|
+
"draw",
|
|
659
|
+
"dispatch",
|
|
660
|
+
"tracerays",
|
|
661
|
+
"executeindirect",
|
|
662
|
+
"vkcmddraw",
|
|
663
|
+
"vkcmddispatch",
|
|
664
|
+
"vkcmdtracerays",
|
|
665
|
+
"gldraw",
|
|
666
|
+
"glmultidraw",
|
|
667
|
+
"gldispatch",
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _is_frame_work_chunk(name: str) -> bool:
|
|
673
|
+
if _is_draw_or_dispatch_chunk(name):
|
|
674
|
+
return True
|
|
675
|
+
operation = _chunk_operation(name)
|
|
676
|
+
if operation.startswith("gl"):
|
|
677
|
+
clear = operation == "glclear" or operation.startswith(
|
|
678
|
+
("glclearbuffer", "glclearnamedframebuffer", "glcleartex")
|
|
679
|
+
)
|
|
680
|
+
else:
|
|
681
|
+
clear = operation != "clearstate" and operation.startswith(("clear", "vkcmdclear"))
|
|
682
|
+
return clear or operation.startswith(
|
|
683
|
+
(
|
|
684
|
+
"copy",
|
|
685
|
+
"resolve",
|
|
686
|
+
"blit",
|
|
687
|
+
"vkcmdcopy",
|
|
688
|
+
"vkcmdresolve",
|
|
689
|
+
"vkcmdblit",
|
|
690
|
+
"glcopy",
|
|
691
|
+
"glblit",
|
|
692
|
+
"vkcmdbeginrenderpass",
|
|
693
|
+
"vkcmdbeginrendering",
|
|
694
|
+
"vkcmdexecutecommands",
|
|
695
|
+
"executecommandlist",
|
|
696
|
+
)
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _is_present_chunk(name: str) -> bool:
|
|
701
|
+
operation = _chunk_operation(name)
|
|
702
|
+
return operation.startswith(
|
|
703
|
+
(
|
|
704
|
+
"present",
|
|
705
|
+
"vkqueuepresent",
|
|
706
|
+
"swapbuffers",
|
|
707
|
+
"glxswapbuffers",
|
|
708
|
+
"eglswapbuffers",
|
|
709
|
+
"wglswapbuffers",
|
|
710
|
+
"wglswaplayerbuffers",
|
|
711
|
+
)
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
|
|
629
715
|
def parse_capture_xml(xml_file: str, *, representative_limit: int = 20) -> dict[str, Any]:
|
|
630
716
|
"""Parse the stable high-level fields emitted by RenderDoc's XML converter."""
|
|
631
717
|
path = Path(xml_file)
|
|
@@ -644,6 +730,9 @@ def parse_capture_xml(xml_file: str, *, representative_limit: int = 20) -> dict[
|
|
|
644
730
|
thumbnail = header.find("thumbnail")
|
|
645
731
|
names = [chunk.get("name") or "unnamed" for chunk in chunks.findall("chunk")]
|
|
646
732
|
counts = Counter(names)
|
|
733
|
+
draw_dispatch_count = sum(_is_draw_or_dispatch_chunk(name) for name in names)
|
|
734
|
+
frame_work_count = sum(_is_frame_work_chunk(name) for name in names)
|
|
735
|
+
present_count = sum(_is_present_chunk(name) for name in names)
|
|
647
736
|
return {
|
|
648
737
|
"driver": {
|
|
649
738
|
"id": driver.get("id") if driver is not None else None,
|
|
@@ -656,6 +745,12 @@ def parse_capture_xml(xml_file: str, *, representative_limit: int = 20) -> dict[
|
|
|
656
745
|
},
|
|
657
746
|
"chunk_version": chunks.get("version"),
|
|
658
747
|
"chunk_count": len(names),
|
|
748
|
+
"draw_dispatch_count": draw_dispatch_count,
|
|
749
|
+
"frame_work_count": frame_work_count,
|
|
750
|
+
"present_count": present_count,
|
|
751
|
+
"frame_content_status": (
|
|
752
|
+
"rendering_commands_present" if frame_work_count else "no_rendering_work"
|
|
753
|
+
),
|
|
659
754
|
"chunk_frequencies": [
|
|
660
755
|
{"name": name, "count": count} for name, count in counts.most_common(20)
|
|
661
756
|
],
|
|
@@ -689,6 +784,47 @@ def inspect_capture(
|
|
|
689
784
|
return {"capture_file": str(capture), "size_bytes": capture.stat().st_size, **details}
|
|
690
785
|
|
|
691
786
|
|
|
787
|
+
def _validate_frame_captures(
|
|
788
|
+
captures: Sequence[Path], *, command: Optional[str]
|
|
789
|
+
) -> list[dict[str, Any]]:
|
|
790
|
+
validations = []
|
|
791
|
+
for capture in captures:
|
|
792
|
+
details = inspect_capture(str(capture), command=command)
|
|
793
|
+
if details["frame_work_count"] == 0:
|
|
794
|
+
diagnostics = {
|
|
795
|
+
"capture_file": details["capture_file"],
|
|
796
|
+
"driver": details["driver"],
|
|
797
|
+
"chunk_count": details["chunk_count"],
|
|
798
|
+
"draw_dispatch_count": details["draw_dispatch_count"],
|
|
799
|
+
"present_count": details["present_count"],
|
|
800
|
+
"representative_chunks": details["representative_chunks"],
|
|
801
|
+
}
|
|
802
|
+
raise RenderDocError(
|
|
803
|
+
"RenderDoc capture contains no rendering work (Draw, Dispatch, Clear, Copy, "
|
|
804
|
+
"Resolve, Blit, render-pass work, or command-list execution) and is not a usable "
|
|
805
|
+
"frame capture. The .rdc was preserved; retry while the target is actively "
|
|
806
|
+
"rendering and verify the selected process or child target. "
|
|
807
|
+
f"diagnostics={json.dumps(diagnostics, sort_keys=True)}"
|
|
808
|
+
)
|
|
809
|
+
validations.append(
|
|
810
|
+
{
|
|
811
|
+
key: details[key]
|
|
812
|
+
for key in (
|
|
813
|
+
"capture_file",
|
|
814
|
+
"size_bytes",
|
|
815
|
+
"driver",
|
|
816
|
+
"thumbnail",
|
|
817
|
+
"chunk_count",
|
|
818
|
+
"draw_dispatch_count",
|
|
819
|
+
"frame_work_count",
|
|
820
|
+
"present_count",
|
|
821
|
+
"frame_content_status",
|
|
822
|
+
)
|
|
823
|
+
}
|
|
824
|
+
)
|
|
825
|
+
return validations
|
|
826
|
+
|
|
827
|
+
|
|
692
828
|
def _prepare_output(output_file: str, expected_suffixes: Iterable[str]) -> Path:
|
|
693
829
|
path = Path(output_file).expanduser().resolve()
|
|
694
830
|
if path.suffix.lower() not in set(expected_suffixes):
|
|
@@ -23,3 +23,6 @@ metadata:
|
|
|
23
23
|
Inspect before exporting. These tools never modify the input `.rdc`; exports require an explicit
|
|
24
24
|
destination path and create its parent directory when needed.
|
|
25
25
|
|
|
26
|
+
Inspection reports `draw_dispatch_count`, `frame_work_count`, `present_count`, and
|
|
27
|
+
`frame_content_status` so a structurally readable capture with no rendering work is not mistaken
|
|
28
|
+
for a usable frame.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
tools:
|
|
2
2
|
- name: inspect_capture
|
|
3
|
-
description: Convert one .rdc capture to temporary XML and summarize stable header and
|
|
3
|
+
description: Convert one .rdc capture to temporary XML and summarize stable header, chunk metadata, Draw/Dispatch count, frame-work count, Present count, and frame-content status. Use for offline triage and identifying readable but unusable no-work captures. Not for GPU performance conclusions or high-level drawcall reconstruction.
|
|
4
4
|
input_schema:
|
|
5
5
|
type: object
|
|
6
6
|
required: [capture_file]
|
|
@@ -32,6 +32,11 @@ The official RenderDoc runtime supports Windows and Linux, not macOS (where this
|
|
|
32
32
|
Python unit tests only). On headless Linux, run the adapter under Xvfb or provide another working
|
|
33
33
|
X/Wayland display; the official archive does not include Qt's `offscreen` platform plugin.
|
|
34
34
|
|
|
35
|
+
Capture tools inspect each new `.rdc` before reporting success. A capture with no frame work such
|
|
36
|
+
as Draw, Dispatch, Clear, Copy, Resolve, Blit, render-pass work, or command-list execution is
|
|
37
|
+
rejected with chunk diagnostics while the `.rdc` remains at the requested output path; retry while
|
|
38
|
+
the intended target is actively rendering or correct the selected child process.
|
|
39
|
+
|
|
35
40
|
Use `capture_process` when Steam or another platform client must launch the target first. It
|
|
36
41
|
injects into the explicit PID, connects to the returned Target Control ident, requests one capture
|
|
37
42
|
after the configured delay, and waits for the resulting file. It never takes foreground focus or
|
|
@@ -14,7 +14,7 @@ tools:
|
|
|
14
14
|
on-success: [renderdoc_capture__capture_program]
|
|
15
15
|
on-failure: [dcc_diagnostics__audit_log]
|
|
16
16
|
- name: capture_program
|
|
17
|
-
description: Launch one explicit executable under RenderDoc and return newly created .rdc files. With trigger_after_secs, request the frame through official local Target Control without foreground or keyboard input. With hook_children enabled, trigger_process_name first checks the launched target, then follows only its official NewChild messages to select a unique matching child. Use for controlled game or graphics test capture. Not for shell commands or attaching to an existing process.
|
|
17
|
+
description: Launch one explicit executable under RenderDoc and return newly created .rdc files only when they contain frame work such as Draw, Dispatch, Clear, Copy, Resolve, Blit, render-pass work, or command-list execution. A no-work capture is preserved and reported as a failure with chunk diagnostics. With trigger_after_secs, request the frame through official local Target Control without foreground or keyboard input. With hook_children enabled, trigger_process_name first checks the launched target, then follows only its official NewChild messages to select a unique matching child. Use for controlled game or graphics test capture. Not for shell commands or attaching to an existing process.
|
|
18
18
|
input_schema:
|
|
19
19
|
type: object
|
|
20
20
|
required: [executable, output_template]
|
|
@@ -46,7 +46,7 @@ tools:
|
|
|
46
46
|
on-success: [renderdoc_analysis__inspect_capture, renderdoc_analysis__export_thumbnail]
|
|
47
47
|
on-failure: [dcc_diagnostics__audit_log]
|
|
48
48
|
- name: capture_process
|
|
49
|
-
description: Inject RenderDoc into one process before it creates its graphics device, request a capture through official local Target Control after a delay, and return the new .rdc file
|
|
49
|
+
description: Inject RenderDoc into one process before it creates its graphics device, request a capture through official local Target Control after a delay, and return the new .rdc file only when it contains frame work such as Draw, Dispatch, Clear, Copy, Resolve, Blit, render-pass work, or command-list execution. A no-work capture is preserved and reported as a failure with chunk diagnostics. Use when a game must be launched by its platform client. Prefer capture_program for an already initialized game; not for process termination.
|
|
50
50
|
input_schema:
|
|
51
51
|
type: object
|
|
52
52
|
required: [process_id, output_template]
|
|
@@ -32,6 +32,23 @@ def _default_target_control_delay(monkeypatch):
|
|
|
32
32
|
monkeypatch.setattr(runtime.sys, "platform", "win32")
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def valid_frame_captures(monkeypatch):
|
|
37
|
+
monkeypatch.setattr(
|
|
38
|
+
runtime,
|
|
39
|
+
"_validate_frame_captures",
|
|
40
|
+
lambda captures, **_kwargs: [
|
|
41
|
+
{
|
|
42
|
+
"capture_file": str(capture),
|
|
43
|
+
"draw_dispatch_count": 1,
|
|
44
|
+
"frame_work_count": 1,
|
|
45
|
+
"frame_content_status": "rendering_commands_present",
|
|
46
|
+
}
|
|
47
|
+
for capture in captures
|
|
48
|
+
],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
35
52
|
def test_parse_capture_xml_summarizes_stable_fields(tmp_path: Path):
|
|
36
53
|
xml_file = tmp_path / "capture.xml"
|
|
37
54
|
xml_file.write_text(
|
|
@@ -47,10 +64,90 @@ def test_parse_capture_xml_summarizes_stable_fields(tmp_path: Path):
|
|
|
47
64
|
assert result["driver"] == {"id": "2", "name": "Vulkan"}
|
|
48
65
|
assert result["thumbnail"] == {"width": 640, "height": 360}
|
|
49
66
|
assert result["chunk_count"] == 3
|
|
67
|
+
assert result["draw_dispatch_count"] == 2
|
|
68
|
+
assert result["frame_work_count"] == 2
|
|
69
|
+
assert result["present_count"] == 1
|
|
70
|
+
assert result["frame_content_status"] == "rendering_commands_present"
|
|
50
71
|
assert result["chunk_frequencies"][0] == {"name": "vkCmdDraw", "count": 2}
|
|
51
72
|
assert result["representative_chunks"] == ["vkCmdDraw", "vkCmdDraw"]
|
|
52
73
|
|
|
53
74
|
|
|
75
|
+
def test_parse_capture_xml_accepts_clear_and_copy_only_frame_work(tmp_path: Path):
|
|
76
|
+
xml_file = tmp_path / "capture.xml"
|
|
77
|
+
xml_file.write_text(
|
|
78
|
+
"""<rdc><header><driver id="1">D3D11</driver></header><chunks version="1">
|
|
79
|
+
<chunk name="ID3D11DeviceContext::ClearRenderTargetView"/>
|
|
80
|
+
<chunk name="ID3D11DeviceContext::CopyResource"/>
|
|
81
|
+
<chunk name="IDXGISwapChain::Present"/>
|
|
82
|
+
</chunks></rdc>""",
|
|
83
|
+
encoding="utf-8",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
result = runtime.parse_capture_xml(str(xml_file))
|
|
87
|
+
|
|
88
|
+
assert result["draw_dispatch_count"] == 0
|
|
89
|
+
assert result["frame_work_count"] == 2
|
|
90
|
+
assert result["frame_content_status"] == "rendering_commands_present"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_parse_capture_xml_rejects_opengl_state_only_frame(tmp_path: Path):
|
|
94
|
+
xml_file = tmp_path / "capture.xml"
|
|
95
|
+
xml_file.write_text(
|
|
96
|
+
"""<rdc><header><driver id="3">OpenGL</driver></header><chunks version="1">
|
|
97
|
+
<chunk name="glDrawBuffer"/><chunk name="glDrawBuffers"/>
|
|
98
|
+
<chunk name="glNamedFramebufferDrawBuffer"/>
|
|
99
|
+
<chunk name="glNamedFramebufferDrawBuffers"/>
|
|
100
|
+
<chunk name="glClearColor"/><chunk name="glClearDepth"/>
|
|
101
|
+
<chunk name="glClearStencil"/><chunk name="SwapBuffers"/>
|
|
102
|
+
</chunks></rdc>""",
|
|
103
|
+
encoding="utf-8",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
result = runtime.parse_capture_xml(str(xml_file))
|
|
107
|
+
|
|
108
|
+
assert result["draw_dispatch_count"] == 0
|
|
109
|
+
assert result["frame_work_count"] == 0
|
|
110
|
+
assert result["present_count"] == 1
|
|
111
|
+
assert result["frame_content_status"] == "no_rendering_work"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_parse_capture_xml_accepts_vulkan_render_pass_work(tmp_path: Path):
|
|
115
|
+
xml_file = tmp_path / "capture.xml"
|
|
116
|
+
xml_file.write_text(
|
|
117
|
+
"""<rdc><header><driver id="2">Vulkan</driver></header><chunks version="1">
|
|
118
|
+
<chunk name="vkCmdBeginRenderPass"/><chunk name="vkCmdBeginRendering"/>
|
|
119
|
+
<chunk name="vkQueuePresentKHR"/>
|
|
120
|
+
</chunks></rdc>""",
|
|
121
|
+
encoding="utf-8",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
result = runtime.parse_capture_xml(str(xml_file))
|
|
125
|
+
|
|
126
|
+
assert result["draw_dispatch_count"] == 0
|
|
127
|
+
assert result["frame_work_count"] == 2
|
|
128
|
+
assert result["present_count"] == 1
|
|
129
|
+
assert result["frame_content_status"] == "rendering_commands_present"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_parse_capture_xml_accepts_explicit_command_list_execution(tmp_path: Path):
|
|
133
|
+
xml_file = tmp_path / "capture.xml"
|
|
134
|
+
xml_file.write_text(
|
|
135
|
+
"""<rdc><header><driver id="2">Vulkan</driver></header><chunks version="1">
|
|
136
|
+
<chunk name="vkCmdExecuteCommands"/>
|
|
137
|
+
<chunk name="ID3D11DeviceContext::ExecuteCommandList"/>
|
|
138
|
+
<chunk name="ID3D12CommandQueue::ExecuteCommandLists"/>
|
|
139
|
+
<chunk name="vkQueuePresentKHR"/>
|
|
140
|
+
</chunks></rdc>""",
|
|
141
|
+
encoding="utf-8",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
result = runtime.parse_capture_xml(str(xml_file))
|
|
145
|
+
|
|
146
|
+
assert result["draw_dispatch_count"] == 0
|
|
147
|
+
assert result["frame_work_count"] == 3
|
|
148
|
+
assert result["present_count"] == 1
|
|
149
|
+
|
|
150
|
+
|
|
54
151
|
def test_resolve_renderdoccmd_prefers_explicit_file(tmp_path: Path):
|
|
55
152
|
executable = tmp_path / "renderdoccmd"
|
|
56
153
|
executable.touch()
|
|
@@ -63,8 +160,17 @@ def test_capture_program_uses_argument_vector_and_reports_new_capture(tmp_path,
|
|
|
63
160
|
observed = {}
|
|
64
161
|
|
|
65
162
|
def fake_run(arguments, **_kwargs):
|
|
66
|
-
|
|
67
|
-
|
|
163
|
+
if arguments[0] == "capture":
|
|
164
|
+
observed["arguments"] = arguments
|
|
165
|
+
(tmp_path / "capture_frame1.rdc").write_bytes(b"rdc")
|
|
166
|
+
elif arguments[0] == "convert":
|
|
167
|
+
xml_file = Path(arguments[arguments.index("--output") + 1])
|
|
168
|
+
xml_file.write_text(
|
|
169
|
+
"""<rdc><header><driver id="2">Vulkan</driver></header><chunks version="1">
|
|
170
|
+
<chunk name="vkCmdDrawIndexed"/><chunk name="Present"/>
|
|
171
|
+
</chunks></rdc>""",
|
|
172
|
+
encoding="utf-8",
|
|
173
|
+
)
|
|
68
174
|
return CompletedProcess(arguments, 0, "captured", "")
|
|
69
175
|
|
|
70
176
|
monkeypatch.setattr(runtime, "_run", fake_run)
|
|
@@ -76,6 +182,36 @@ def test_capture_program_uses_argument_vector_and_reports_new_capture(tmp_path,
|
|
|
76
182
|
|
|
77
183
|
assert observed["arguments"][-3:] == [str(target.resolve()), "--scene", "demo"]
|
|
78
184
|
assert result["captures"] == [str((tmp_path / "capture_frame1.rdc").resolve())]
|
|
185
|
+
assert result["capture_validations"][0]["draw_dispatch_count"] == 1
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_capture_program_rejects_present_only_capture_and_preserves_rdc(tmp_path, monkeypatch):
|
|
189
|
+
target = tmp_path / "game.exe"
|
|
190
|
+
target.touch()
|
|
191
|
+
capture = tmp_path / "capture_frame1.rdc"
|
|
192
|
+
|
|
193
|
+
def fake_run(arguments, **_kwargs):
|
|
194
|
+
if arguments[0] == "capture":
|
|
195
|
+
capture.write_bytes(b"rdc")
|
|
196
|
+
elif arguments[0] == "convert":
|
|
197
|
+
xml_file = Path(arguments[arguments.index("--output") + 1])
|
|
198
|
+
xml_file.write_text(
|
|
199
|
+
"""<rdc><header><driver id="1">D3D11</driver></header><chunks version="1">
|
|
200
|
+
<chunk name="Internal::Drawcall Metadata"/>
|
|
201
|
+
<chunk name="ID3D11DeviceContext::RSSetViewports"/>
|
|
202
|
+
<chunk name="ID3D11DeviceContext::ClearState"/>
|
|
203
|
+
<chunk name="IDXGISwapChain::Present"/>
|
|
204
|
+
</chunks></rdc>""",
|
|
205
|
+
encoding="utf-8",
|
|
206
|
+
)
|
|
207
|
+
return CompletedProcess(arguments, 0, "", "")
|
|
208
|
+
|
|
209
|
+
monkeypatch.setattr(runtime, "_run", fake_run)
|
|
210
|
+
|
|
211
|
+
with pytest.raises(runtime.RenderDocError, match="no rendering work"):
|
|
212
|
+
runtime.capture_program(str(target), str(tmp_path / "capture"))
|
|
213
|
+
|
|
214
|
+
assert capture.is_file()
|
|
79
215
|
|
|
80
216
|
|
|
81
217
|
def test_non_waiting_launch_accepts_renderdoc_target_id(monkeypatch, tmp_path):
|
|
@@ -1395,7 +1531,7 @@ def test_bundled_target_control_is_python36_compatible():
|
|
|
1395
1531
|
ast.parse(source, feature_version=(3, 6))
|
|
1396
1532
|
|
|
1397
1533
|
|
|
1398
|
-
def test_capture_program_reuses_target_control_trigger(tmp_path, monkeypatch):
|
|
1534
|
+
def test_capture_program_reuses_target_control_trigger(tmp_path, monkeypatch, valid_frame_captures):
|
|
1399
1535
|
target = tmp_path / "game.exe"
|
|
1400
1536
|
target.touch()
|
|
1401
1537
|
capture = tmp_path / "capture_frame1.rdc"
|
|
@@ -1453,7 +1589,7 @@ def test_capture_program_rejects_child_name_without_child_hook(tmp_path):
|
|
|
1453
1589
|
)
|
|
1454
1590
|
|
|
1455
1591
|
|
|
1456
|
-
def test_capture_process_reuses_target_control_trigger(tmp_path, monkeypatch):
|
|
1592
|
+
def test_capture_process_reuses_target_control_trigger(tmp_path, monkeypatch, valid_frame_captures):
|
|
1457
1593
|
capture = tmp_path / "capture_frame1.rdc"
|
|
1458
1594
|
observed = {}
|
|
1459
1595
|
monkeypatch.setattr(
|
|
@@ -1494,7 +1630,9 @@ def test_capture_process_reuses_target_control_trigger(tmp_path, monkeypatch):
|
|
|
1494
1630
|
assert result["trigger_mode"] == "target_control"
|
|
1495
1631
|
|
|
1496
1632
|
|
|
1497
|
-
def test_capture_process_injects_triggers_and_reports_capture(
|
|
1633
|
+
def test_capture_process_injects_triggers_and_reports_capture(
|
|
1634
|
+
tmp_path, monkeypatch, valid_frame_captures
|
|
1635
|
+
):
|
|
1498
1636
|
observed = {}
|
|
1499
1637
|
capture = tmp_path / "capture_frame1.rdc"
|
|
1500
1638
|
|
|
@@ -1522,7 +1660,7 @@ def test_capture_process_injects_triggers_and_reports_capture(tmp_path, monkeypa
|
|
|
1522
1660
|
|
|
1523
1661
|
|
|
1524
1662
|
def test_capture_process_triggers_while_injector_is_running_and_only_stops_injector(
|
|
1525
|
-
tmp_path, monkeypatch
|
|
1663
|
+
tmp_path, monkeypatch, valid_frame_captures
|
|
1526
1664
|
):
|
|
1527
1665
|
started = {}
|
|
1528
1666
|
target = {"running": True}
|
|
@@ -1628,7 +1766,9 @@ def test_capture_process_readiness_timeout_force_stops_only_injector(tmp_path, m
|
|
|
1628
1766
|
assert target["running"] is True
|
|
1629
1767
|
|
|
1630
1768
|
|
|
1631
|
-
def test_triggered_program_capture_keeps_controller_alive_until_capture(
|
|
1769
|
+
def test_triggered_program_capture_keeps_controller_alive_until_capture(
|
|
1770
|
+
tmp_path, monkeypatch, valid_frame_captures
|
|
1771
|
+
):
|
|
1632
1772
|
target = tmp_path / "game.exe"
|
|
1633
1773
|
target.touch()
|
|
1634
1774
|
command = tmp_path / "renderdoccmd.exe"
|
|
@@ -1684,3 +1824,10 @@ def test_triggered_program_capture_keeps_controller_alive_until_capture(tmp_path
|
|
|
1684
1824
|
def test_invalid_capture_is_rejected(tmp_path: Path):
|
|
1685
1825
|
with pytest.raises(runtime.RenderDocError, match="not .rdc"):
|
|
1686
1826
|
runtime.inspect_capture(str(tmp_path / "missing.rdc"))
|
|
1827
|
+
|
|
1828
|
+
|
|
1829
|
+
def test_missing_runtime_can_disable_auto_download(monkeypatch, tmp_path: Path):
|
|
1830
|
+
monkeypatch.setenv("DCC_MCP_RENDERDOC_CMD", str(tmp_path / "missing"))
|
|
1831
|
+
monkeypatch.setenv("DCC_MCP_RENDERDOC_AUTO_DOWNLOAD", "0")
|
|
1832
|
+
with pytest.raises(runtime.RenderDocError, match="does not exist"):
|
|
1833
|
+
runtime.resolve_renderdoccmd()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dcc_mcp_renderdoc-0.3.3 → dcc_mcp_renderdoc-0.4.0}/src/dcc_mcp_renderdoc/_target_control.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|