rdc-cli 0.2.0__py3-none-any.whl

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.
rdc/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """rdc package."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.2.0"
rdc/adapter.py ADDED
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+
8
+ def parse_version_tuple(value: str) -> tuple[int, int]:
9
+ """Parse RenderDoc version string into (major, minor)."""
10
+ match = re.search(r"(\d+)\.(\d+)", value)
11
+ if not match:
12
+ return (0, 0)
13
+ return int(match.group(1)), int(match.group(2))
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class RenderDocAdapter:
18
+ """Compatibility adapter for RenderDoc API changes across versions."""
19
+
20
+ controller: Any
21
+ version: tuple[int, int]
22
+
23
+ def get_root_actions(self) -> Any:
24
+ """Return root actions with compatibility handling."""
25
+ if self.version >= (1, 32) and hasattr(self.controller, "GetRootActions"):
26
+ return self.controller.GetRootActions()
27
+ if hasattr(self.controller, "GetDrawcalls"):
28
+ return self.controller.GetDrawcalls()
29
+ raise AttributeError("controller has neither GetRootActions nor GetDrawcalls")
30
+
31
+ def get_api_properties(self) -> Any:
32
+ """Return API properties from the controller."""
33
+ return self.controller.GetAPIProperties()
34
+
35
+ def get_resources(self) -> Any:
36
+ """Return all resources from the controller."""
37
+ return self.controller.GetResources()
38
+
39
+ def get_pipeline_state(self) -> Any:
40
+ """Return current pipeline state."""
41
+ return self.controller.GetPipelineState()
42
+
43
+ def get_structured_file(self) -> Any:
44
+ """Return structured file from the controller."""
45
+ return self.controller.GetStructuredFile()
46
+
47
+ def set_frame_event(self, eid: int, force: bool = True) -> None:
48
+ """Move the replay to the given event ID."""
49
+ self.controller.SetFrameEvent(eid, force)
50
+
51
+ def get_textures(self) -> Any:
52
+ """Return all texture descriptions from the controller."""
53
+ return self.controller.GetTextures()
54
+
55
+ def get_buffers(self) -> Any:
56
+ """Return all buffer descriptions from the controller."""
57
+ return self.controller.GetBuffers()
58
+
59
+ def shutdown(self) -> None:
60
+ """Shutdown the replay controller."""
61
+ self.controller.Shutdown()
rdc/cli.py ADDED
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+
5
+ from rdc import __version__
6
+ from rdc.commands.capture import capture_cmd
7
+ from rdc.commands.completion import completion_cmd
8
+ from rdc.commands.counters import counters_cmd
9
+ from rdc.commands.doctor import doctor_cmd
10
+ from rdc.commands.events import draw_cmd, draws_cmd, event_cmd, events_cmd
11
+ from rdc.commands.export import buffer_cmd, rt_cmd, texture_cmd
12
+ from rdc.commands.info import info_cmd, log_cmd, stats_cmd
13
+ from rdc.commands.pipeline import bindings_cmd, pipeline_cmd, shader_cmd, shaders_cmd
14
+ from rdc.commands.resources import pass_cmd, passes_cmd, resource_cmd, resources_cmd
15
+ from rdc.commands.search import search_cmd
16
+ from rdc.commands.session import close_cmd, goto_cmd, open_cmd, status_cmd
17
+ from rdc.commands.unix_helpers import count_cmd, shader_map_cmd
18
+ from rdc.commands.usage import usage_cmd
19
+ from rdc.commands.vfs import cat_cmd, complete_cmd, ls_cmd, tree_cmd
20
+
21
+
22
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
23
+ @click.version_option(version=__version__, prog_name="rdc")
24
+ def main() -> None:
25
+ """rdc: Unix-friendly CLI for RenderDoc captures."""
26
+
27
+
28
+ main.add_command(doctor_cmd, name="doctor")
29
+ main.add_command(capture_cmd, name="capture")
30
+ main.add_command(open_cmd, name="open")
31
+ main.add_command(close_cmd, name="close")
32
+ main.add_command(status_cmd, name="status")
33
+ main.add_command(goto_cmd, name="goto")
34
+ main.add_command(info_cmd, name="info")
35
+ main.add_command(stats_cmd, name="stats")
36
+ main.add_command(events_cmd, name="events")
37
+ main.add_command(draws_cmd, name="draws")
38
+ main.add_command(event_cmd, name="event")
39
+ main.add_command(draw_cmd, name="draw")
40
+ main.add_command(count_cmd, name="count")
41
+ main.add_command(shader_map_cmd, name="shader-map")
42
+ main.add_command(pipeline_cmd, name="pipeline")
43
+ main.add_command(bindings_cmd, name="bindings")
44
+ main.add_command(shader_cmd, name="shader")
45
+ main.add_command(shaders_cmd, name="shaders")
46
+ main.add_command(resources_cmd, name="resources")
47
+ main.add_command(resource_cmd, name="resource")
48
+ main.add_command(passes_cmd, name="passes")
49
+ main.add_command(pass_cmd, name="pass")
50
+ main.add_command(log_cmd, name="log")
51
+ main.add_command(ls_cmd, name="ls")
52
+ main.add_command(cat_cmd, name="cat")
53
+ main.add_command(tree_cmd, name="tree")
54
+ main.add_command(complete_cmd, name="_complete")
55
+ main.add_command(texture_cmd, name="texture")
56
+ main.add_command(rt_cmd, name="rt")
57
+ main.add_command(buffer_cmd, name="buffer")
58
+ main.add_command(search_cmd, name="search")
59
+ main.add_command(usage_cmd, name="usage")
60
+ main.add_command(completion_cmd, name="completion")
61
+ main.add_command(counters_cmd, name="counters")
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+
10
+ def _find_renderdoccmd() -> str | None:
11
+ in_path = shutil.which("renderdoccmd")
12
+ if in_path:
13
+ return in_path
14
+
15
+ common_paths = [
16
+ Path("/opt/renderdoc/bin/renderdoccmd"),
17
+ Path("/usr/local/bin/renderdoccmd"),
18
+ ]
19
+ for path in common_paths:
20
+ if path.exists() and path.is_file():
21
+ return str(path)
22
+ return None
23
+
24
+
25
+ @click.command(
26
+ "capture",
27
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
28
+ )
29
+ @click.option("--api", "api_name", type=str, help="Capture API (maps to --opt-api).")
30
+ @click.option("-o", "--output", type=click.Path(path_type=Path), help="Output capture file path.")
31
+ @click.option("--list-apis", is_flag=True, help="List capture APIs via renderdoccmd and exit.")
32
+ @click.pass_context
33
+ def capture_cmd(
34
+ ctx: click.Context,
35
+ api_name: str | None,
36
+ output: Path | None,
37
+ list_apis: bool,
38
+ ) -> None:
39
+ """Thin wrapper around renderdoccmd capture."""
40
+ bin_path = _find_renderdoccmd()
41
+ if not bin_path:
42
+ click.echo("error: renderdoccmd not found in PATH", err=True)
43
+ raise SystemExit(1)
44
+
45
+ argv: list[str] = [bin_path, "capture"]
46
+
47
+ if list_apis:
48
+ argv.append("--list-apis")
49
+ else:
50
+ if api_name:
51
+ argv.extend(["--opt-api", api_name])
52
+ if output:
53
+ argv.extend(["--capture-file", str(output)])
54
+ argv.extend(ctx.args)
55
+
56
+ result = subprocess.run(argv, check=False)
57
+ if result.returncode != 0:
58
+ raise SystemExit(result.returncode)
59
+
60
+ if output and not list_apis:
61
+ click.echo(f"capture saved: {output}", err=True)
62
+ click.echo(f"next: rdc open {output}", err=True)
@@ -0,0 +1,49 @@
1
+ """Shell completion script generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ _SUPPORTED_SHELLS = ("bash", "zsh", "fish")
11
+
12
+
13
+ def _detect_shell() -> str:
14
+ """Detect current shell from $SHELL."""
15
+ name = Path(os.environ.get("SHELL", "bash")).name
16
+ return name if name in _SUPPORTED_SHELLS else "bash"
17
+
18
+
19
+ def _generate(shell: str) -> str:
20
+ """Generate completion script via Click's built-in mechanism."""
21
+ from click.shell_completion import get_completion_class
22
+
23
+ from rdc.cli import main # deferred: rdc.cli imports this module
24
+
25
+ cls = get_completion_class(shell)
26
+ if cls is None:
27
+ raise click.ClickException(f"Unsupported shell: {shell}")
28
+ comp = cls(cli=main, ctx_args={}, prog_name="rdc", complete_var="_RDC_COMPLETE")
29
+ return comp.source()
30
+
31
+
32
+ @click.command("completion")
33
+ @click.argument("shell", required=False, type=click.Choice(_SUPPORTED_SHELLS))
34
+ def completion_cmd(shell: str | None) -> None:
35
+ """Generate shell completion script.
36
+
37
+ Prints the completion script to stdout. Redirect or eval as needed.
38
+
39
+ \b
40
+ Examples:
41
+ rdc completion bash > ~/.local/share/bash-completion/completions/rdc
42
+ rdc completion zsh > ~/.zfunc/_rdc
43
+ eval "$(rdc completion bash)"
44
+ """
45
+ if shell is None:
46
+ shell = _detect_shell()
47
+ click.echo(f"# Detected shell: {shell}", err=True)
48
+
49
+ click.echo(_generate(shell))
@@ -0,0 +1,50 @@
1
+ """rdc counters command — GPU performance counters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import click
8
+
9
+ from rdc.commands.info import _daemon_call
10
+ from rdc.formatters.json_fmt import write_json
11
+
12
+
13
+ @click.command("counters")
14
+ @click.option("--list", "show_list", is_flag=True, help="List available counters.")
15
+ @click.option("--eid", type=int, default=None, help="Filter to specific event ID.")
16
+ @click.option("--name", "name_filter", default=None, help="Filter counters by name substring.")
17
+ @click.option("--json", "use_json", is_flag=True, help="JSON output.")
18
+ def counters_cmd(
19
+ show_list: bool,
20
+ eid: int | None,
21
+ name_filter: str | None,
22
+ use_json: bool,
23
+ ) -> None:
24
+ """Query GPU performance counters.
25
+
26
+ Use --list to enumerate available counters, or run without --list
27
+ to fetch counter values for all draw events.
28
+ """
29
+ if show_list:
30
+ result = _daemon_call("counter_list")
31
+ if use_json:
32
+ write_json(result)
33
+ return
34
+ click.echo("ID\tNAME\tUNIT\tTYPE\tCATEGORY")
35
+ for c in result.get("counters", []):
36
+ click.echo(f"{c['id']}\t{c['name']}\t{c['unit']}\t{c['type']}\t{c['category']}")
37
+ return
38
+
39
+ params: dict[str, Any] = {}
40
+ if eid is not None:
41
+ params["eid"] = eid
42
+ if name_filter is not None:
43
+ params["name"] = name_filter
44
+ result = _daemon_call("counter_fetch", params)
45
+ if use_json:
46
+ write_json(result)
47
+ return
48
+ click.echo("EID\tCOUNTER\tVALUE\tUNIT")
49
+ for r in result.get("rows", []):
50
+ click.echo(f"{r['eid']}\t{r['counter']}\t{r['value']}\t{r['unit']}")
rdc/commands/doctor.py ADDED
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ import shutil
5
+ import sys
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ import click
10
+
11
+ from rdc.discover import find_renderdoc
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class CheckResult:
16
+ name: str
17
+ ok: bool
18
+ detail: str
19
+
20
+
21
+ def _check_python() -> CheckResult:
22
+ return CheckResult("python", True, sys.version.split()[0])
23
+
24
+
25
+ def _check_platform() -> CheckResult:
26
+ system = platform.system().lower()
27
+ if system == "linux":
28
+ return CheckResult("platform", True, "linux")
29
+ if system == "darwin":
30
+ return CheckResult("platform", True, "darwin (dev-host only for replay)")
31
+ return CheckResult("platform", False, f"unsupported system: {system}")
32
+
33
+
34
+ _RENDERDOC_BUILD_HINT = """\
35
+ To build the renderdoc Python module:
36
+ git clone --depth 1 https://github.com/baldurk/renderdoc.git
37
+ cd renderdoc
38
+ cmake -B build -DENABLE_PYRENDERDOC=ON -DENABLE_QRENDERDOC=OFF
39
+ cmake --build build -j$(nproc)
40
+ export RENDERDOC_PYTHON_PATH=$PWD/build/lib"""
41
+
42
+
43
+ def _import_renderdoc() -> tuple[Any | None, CheckResult]:
44
+ module = find_renderdoc()
45
+ if module is None:
46
+ return None, CheckResult("renderdoc-module", False, "not found in search paths")
47
+
48
+ version = getattr(module, "GetVersionString", lambda: "unknown")()
49
+ return module, CheckResult("renderdoc-module", True, f"version={version}")
50
+
51
+
52
+ def _check_replay_support(module: Any | None) -> CheckResult:
53
+ if module is None:
54
+ return CheckResult("replay-support", False, "renderdoc module unavailable")
55
+
56
+ has_init = hasattr(module, "InitialiseReplay")
57
+ has_shutdown = hasattr(module, "ShutdownReplay")
58
+ has_global_env = hasattr(module, "GlobalEnvironment")
59
+
60
+ if has_init and has_shutdown and has_global_env:
61
+ return CheckResult("replay-support", True, "renderdoc replay API surface found")
62
+ return CheckResult("replay-support", False, "missing replay API surface")
63
+
64
+
65
+ def _check_renderdoccmd() -> CheckResult:
66
+ path = shutil.which("renderdoccmd")
67
+ if path:
68
+ return CheckResult("renderdoccmd", True, path)
69
+ return CheckResult("renderdoccmd", False, "not found in PATH")
70
+
71
+
72
+ def run_doctor() -> list[CheckResult]:
73
+ module, renderdoc_check = _import_renderdoc()
74
+ return [
75
+ _check_python(),
76
+ _check_platform(),
77
+ renderdoc_check,
78
+ _check_replay_support(module),
79
+ _check_renderdoccmd(),
80
+ ]
81
+
82
+
83
+ @click.command("doctor")
84
+ def doctor_cmd() -> None:
85
+ """Run environment checks for rdc-cli."""
86
+ results = run_doctor()
87
+ has_error = False
88
+
89
+ for result in results:
90
+ icon = "✅" if result.ok else "❌"
91
+ click.echo(f"{icon} {result.name}: {result.detail}")
92
+ if not result.ok:
93
+ has_error = True
94
+ if result.name == "renderdoc-module":
95
+ click.echo(_RENDERDOC_BUILD_HINT, err=True)
96
+
97
+ if has_error:
98
+ raise SystemExit(1)
rdc/commands/events.py ADDED
@@ -0,0 +1,140 @@
1
+ """Commands: rdc events, rdc draws, rdc event, rdc draw."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import Any
7
+
8
+ import click
9
+
10
+ from rdc.commands.info import _daemon_call, _format_kv
11
+ from rdc.formatters.json_fmt import write_json, write_jsonl
12
+ from rdc.formatters.tsv import write_footer, write_tsv
13
+
14
+
15
+ @click.command("events")
16
+ @click.option("--type", "event_type", default=None, help="Filter by type")
17
+ @click.option("--filter", "pattern", default=None, help="Filter by name glob")
18
+ @click.option("--limit", type=int, default=None, help="Max rows")
19
+ @click.option("--range", "eid_range", default=None, help="EID range N:M")
20
+ @click.option("--no-header", is_flag=True, help="Omit TSV header")
21
+ @click.option("--json", "use_json", is_flag=True, help="JSON output")
22
+ @click.option("--jsonl", "use_jsonl", is_flag=True, help="JSONL output")
23
+ @click.option("-q", "--quiet", is_flag=True, help="Only EID column")
24
+ def events_cmd(
25
+ event_type: str | None,
26
+ pattern: str | None,
27
+ limit: int | None,
28
+ eid_range: str | None,
29
+ no_header: bool,
30
+ use_json: bool,
31
+ use_jsonl: bool,
32
+ quiet: bool,
33
+ ) -> None:
34
+ """List all events."""
35
+ params: dict[str, Any] = {}
36
+ if event_type:
37
+ params["type"] = event_type
38
+ if pattern:
39
+ params["filter"] = pattern
40
+ if limit is not None:
41
+ params["limit"] = limit
42
+ if eid_range:
43
+ params["range"] = eid_range
44
+ result = _daemon_call("events", params)
45
+ rows_data = result.get("events", [])
46
+ if use_json:
47
+ write_json(rows_data)
48
+ return
49
+ if use_jsonl:
50
+ write_jsonl(rows_data)
51
+ return
52
+ if quiet:
53
+ for r in rows_data:
54
+ sys.stdout.write(str(r["eid"]) + chr(10))
55
+ return
56
+ header = ["EID", "TYPE", "NAME"]
57
+ rows = [[r["eid"], r["type"], r["name"]] for r in rows_data]
58
+ write_tsv(rows, header=header, no_header=no_header)
59
+
60
+
61
+ @click.command("draws")
62
+ @click.option("--pass", "pass_name", default=None, help="Filter by pass name")
63
+ @click.option("--sort", "sort_field", default=None, help="Sort field")
64
+ @click.option("--limit", type=int, default=None, help="Max rows")
65
+ @click.option("--no-header", is_flag=True, help="Omit TSV header")
66
+ @click.option("--json", "use_json", is_flag=True, help="JSON output")
67
+ @click.option("--jsonl", "use_jsonl", is_flag=True, help="JSONL output")
68
+ @click.option("-q", "--quiet", is_flag=True, help="Only EID column")
69
+ def draws_cmd(
70
+ pass_name: str | None,
71
+ sort_field: str | None,
72
+ limit: int | None,
73
+ no_header: bool,
74
+ use_json: bool,
75
+ use_jsonl: bool,
76
+ quiet: bool,
77
+ ) -> None:
78
+ """List draw calls."""
79
+ params: dict[str, Any] = {}
80
+ if pass_name:
81
+ params["pass"] = pass_name
82
+ if sort_field:
83
+ params["sort"] = sort_field
84
+ if limit is not None:
85
+ params["limit"] = limit
86
+ result = _daemon_call("draws", params)
87
+ rows_data = result.get("draws", [])
88
+ summary = result.get("summary", "")
89
+ if use_json:
90
+ write_json(rows_data)
91
+ return
92
+ if use_jsonl:
93
+ write_jsonl(rows_data)
94
+ return
95
+ if quiet:
96
+ for r in rows_data:
97
+ sys.stdout.write(str(r["eid"]) + chr(10))
98
+ return
99
+ header = ["EID", "TYPE", "TRIANGLES", "INSTANCES", "PASS", "MARKER"]
100
+ rows = [
101
+ [
102
+ r["eid"],
103
+ r["type"],
104
+ r["triangles"],
105
+ r["instances"],
106
+ r.get("pass", "-"),
107
+ r.get("marker", "-"),
108
+ ]
109
+ for r in rows_data
110
+ ]
111
+ write_tsv(rows, header=header, no_header=no_header)
112
+ if summary:
113
+ write_footer(summary)
114
+
115
+
116
+ @click.command("event")
117
+ @click.argument("eid", type=int)
118
+ @click.option("--json", "use_json", is_flag=True, help="JSON output")
119
+ def event_cmd(eid: int, use_json: bool) -> None:
120
+ """Show single API call detail."""
121
+ result = _daemon_call("event", {"eid": eid})
122
+ if use_json:
123
+ write_json(result)
124
+ return
125
+ _format_kv(result)
126
+
127
+
128
+ @click.command("draw")
129
+ @click.argument("eid", type=int, required=False, default=None)
130
+ @click.option("--json", "use_json", is_flag=True, help="JSON output")
131
+ def draw_cmd(eid: int | None, use_json: bool) -> None:
132
+ """Show draw call detail."""
133
+ params: dict[str, Any] = {}
134
+ if eid is not None:
135
+ params["eid"] = eid
136
+ result = _daemon_call("draw", params)
137
+ if use_json:
138
+ write_json(result)
139
+ return
140
+ _format_kv(result)
rdc/commands/export.py ADDED
@@ -0,0 +1,57 @@
1
+ """Export convenience commands: texture, rt, buffer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from rdc.commands.info import _daemon_call
8
+ from rdc.commands.vfs import _deliver_binary
9
+ from rdc.vfs.router import resolve_path
10
+
11
+
12
+ def _export_vfs_path(vfs_path: str, output: str | None, raw: bool) -> None:
13
+ """Resolve a VFS path and deliver binary content."""
14
+ result = _daemon_call("vfs_ls", {"path": vfs_path})
15
+ kind = result.get("kind")
16
+
17
+ if kind != "leaf_bin":
18
+ click.echo(f"error: {vfs_path}: not a binary node", err=True)
19
+ raise SystemExit(1)
20
+
21
+ resolved = result.get("path", vfs_path)
22
+ match = resolve_path(resolved)
23
+ if match is None or match.handler is None:
24
+ click.echo(f"error: {vfs_path}: no content handler", err=True)
25
+ raise SystemExit(1)
26
+
27
+ _deliver_binary(vfs_path, match, raw, output)
28
+
29
+
30
+ @click.command("texture")
31
+ @click.argument("id", type=int)
32
+ @click.option("-o", "--output", type=click.Path(), default=None, help="Write to file")
33
+ @click.option("--mip", default=0, type=int, help="Mip level (default 0)")
34
+ @click.option("--raw", is_flag=True, help="Force raw output even on TTY")
35
+ def texture_cmd(id: int, output: str | None, mip: int, raw: bool) -> None:
36
+ """Export texture as PNG."""
37
+ vfs_path = f"/textures/{id}/mips/{mip}.png" if mip > 0 else f"/textures/{id}/image.png"
38
+ _export_vfs_path(vfs_path, output, raw)
39
+
40
+
41
+ @click.command("rt")
42
+ @click.argument("eid", type=int)
43
+ @click.option("-o", "--output", type=click.Path(), default=None, help="Write to file")
44
+ @click.option("--target", default=0, type=int, help="Color target index (default 0)")
45
+ @click.option("--raw", is_flag=True, help="Force raw output even on TTY")
46
+ def rt_cmd(eid: int, output: str | None, target: int, raw: bool) -> None:
47
+ """Export render target as PNG."""
48
+ _export_vfs_path(f"/draws/{eid}/targets/color{target}.png", output, raw)
49
+
50
+
51
+ @click.command("buffer")
52
+ @click.argument("id", type=int)
53
+ @click.option("-o", "--output", type=click.Path(), default=None, help="Write to file")
54
+ @click.option("--raw", is_flag=True, help="Force raw output even on TTY")
55
+ def buffer_cmd(id: int, output: str | None, raw: bool) -> None:
56
+ """Export buffer raw data."""
57
+ _export_vfs_path(f"/buffers/{id}/data", output, raw)