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 +4 -0
- rdc/adapter.py +61 -0
- rdc/cli.py +65 -0
- rdc/commands/capture.py +62 -0
- rdc/commands/completion.py +49 -0
- rdc/commands/counters.py +50 -0
- rdc/commands/doctor.py +98 -0
- rdc/commands/events.py +140 -0
- rdc/commands/export.py +57 -0
- rdc/commands/info.py +125 -0
- rdc/commands/pipeline.py +330 -0
- rdc/commands/resources.py +126 -0
- rdc/commands/search.py +67 -0
- rdc/commands/session.py +57 -0
- rdc/commands/unix_helpers.py +79 -0
- rdc/commands/usage.py +57 -0
- rdc/commands/vfs.py +206 -0
- rdc/daemon_client.py +32 -0
- rdc/daemon_server.py +2079 -0
- rdc/discover.py +84 -0
- rdc/formatters/json_fmt.py +20 -0
- rdc/formatters/tsv.py +60 -0
- rdc/protocol.py +61 -0
- rdc/services/__init__.py +1 -0
- rdc/services/query_service.py +512 -0
- rdc/services/session_service.py +174 -0
- rdc/session_state.py +86 -0
- rdc/vfs/__init__.py +0 -0
- rdc/vfs/formatter.py +62 -0
- rdc/vfs/router.py +191 -0
- rdc/vfs/tree_cache.py +308 -0
- rdc_cli-0.2.0.dist-info/METADATA +130 -0
- rdc_cli-0.2.0.dist-info/RECORD +37 -0
- rdc_cli-0.2.0.dist-info/WHEEL +5 -0
- rdc_cli-0.2.0.dist-info/entry_points.txt +2 -0
- rdc_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- rdc_cli-0.2.0.dist-info/top_level.txt +1 -0
rdc/__init__.py
ADDED
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()
|
rdc/commands/capture.py
ADDED
|
@@ -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))
|
rdc/commands/counters.py
ADDED
|
@@ -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)
|