Skip to content

Commit 1e5ffaa

Browse files
committed
Merge branch 'master' of github.com:BANANASJIM/rdc-cli
2 parents 9516917 + 2095e5d commit 1e5ffaa

30 files changed

Lines changed: 1449 additions & 162 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ jobs:
5656
enable-cache: true
5757
- run: uv python install 3.12
5858
- run: uv sync --extra dev
59-
- run: uv run --with pip-audit pip-audit
59+
# CVE-2026-4539: no fixed pygments release yet (last_affected=2.19.2); ignore until upstream ships a patch
60+
- run: uv run --with pip-audit pip-audit --ignore-vuln CVE-2026-4539
6061

6162
validate-aur:
6263
name: Validate AUR PKGBUILD

docs-astro/src/data/commands.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"name": "open",
1010
"id": "open",
1111
"help": "Create local default session and start daemon skeleton.",
12-
"usage": "rdc open [CAPTURE] [--preload] [--proxy HOST[:PORT]|adb://SERIAL] [--android] [--serial TEXT] [--remote HOST[:PORT]] [--listen [ADDR]:PORT] [--connect HOST:PORT] [--token TEXT]"
12+
"usage": "rdc open [CAPTURE] [--preload] [--proxy HOST[:PORT]|adb://SERIAL] [--android] [--serial TEXT] [--remote HOST[:PORT]] [--listen [ADDR]:PORT] [--connect HOST:PORT] [--token TEXT] [--timeout FLOAT]"
1313
},
1414
{
1515
"name": "close",
@@ -473,6 +473,24 @@
473473
"help": "Capture on a remote host and transfer to local.",
474474
"usage": "rdc remote capture <APP> -o <PATH> [--url TEXT] [--args TEXT] [--workdir TEXT] [--frame INTEGER] [--timeout FLOAT] [--api-validation] [--callstacks] [--hook-children] [--ref-all-resources] [--soft-memory-limit INTEGER] [--keep-remote] [--json]"
475475
},
476+
{
477+
"name": "remote setup",
478+
"id": "remote-setup",
479+
"help": "Verify a remote server is reachable, handshake, and save state.",
480+
"usage": "rdc remote setup <URL> [--timeout FLOAT] [--json]"
481+
},
482+
{
483+
"name": "remote status",
484+
"id": "remote-status",
485+
"help": "Show the currently saved remote server state.",
486+
"usage": "rdc remote status [--json]"
487+
},
488+
{
489+
"name": "remote disconnect",
490+
"id": "remote-disconnect",
491+
"help": "Delete the saved remote server state (local only).",
492+
"usage": "rdc remote disconnect [--json]"
493+
},
476494
{
477495
"name": "android setup",
478496
"id": "android-setup",

pixi.lock

Lines changed: 214 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pixi.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ cmake = ">=3.26,<4"
3131
[target.linux-64.dependencies]
3232
cmake = ">=3.26,<4"
3333
ninja = ">=1.11,<2"
34+
libxml2 = ">=2.12,<3"
3435

3536
[feature.vulkan.target.osx-64.dependencies]
3637
libvulkan-headers = ">=1.3"

scripts/gen-commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"Remote", "remote",
7070
"Connect to a remote RenderDoc server for remote capture and replay.",
7171
["serve", "remote connect", "remote list", "remote capture",
72+
"remote setup", "remote status", "remote disconnect",
7273
"android setup", "android stop", "android capture"],
7374
),
7475
("Utilities", "utilities", None, [

src/rdc/_build_renderdoc.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ def _safe_extractall(zf: zipfile.ZipFile, dest: Path) -> None:
295295
sys.stderr.write(f"ERROR: zip-slip attempt detected: {member.filename}\n")
296296
raise SystemExit(1)
297297
zf.extract(member, dest)
298+
# Restore Unix execute bits that zipfile.extract() drops.
299+
unix_mode = member.external_attr >> 16
300+
if unix_mode and (unix_mode & 0o111):
301+
target.chmod(target.stat().st_mode | (unix_mode & 0o111))
298302

299303

300304
def download_swig(build_dir: Path) -> None:
@@ -377,6 +381,13 @@ def configure_build(
377381
cmd += CMAKE_COMMON_FLAGS
378382
cmd.append(f"-DRENDERDOC_SWIG_PACKAGE={swig_dir}")
379383

384+
# Use base_prefix Python so cmake finds python3.XX-config (venv lacks it)
385+
base_python = Path(sys.base_prefix) / "bin" / Path(sys.executable).name
386+
if base_python.exists():
387+
cmd.append(f"-DPython3_EXECUTABLE={base_python}")
388+
else:
389+
_log(f"WARNING: base Python not found at {base_python}, cmake will auto-detect")
390+
380391
env = dict(os.environ)
381392
if plat == "linux":
382393
_log("stripping LTO flags")
@@ -749,8 +760,10 @@ def main(argv: list[str] | None = None) -> None:
749760
args = parser.parse_args(argv)
750761

751762
plat = _platform()
752-
install_dir = Path(args.install_dir) if args.install_dir else default_install_dir()
753-
build_dir = Path(args.build_dir) if args.build_dir else install_dir.parent / "renderdoc-build"
763+
install_dir = Path(args.install_dir).resolve() if args.install_dir else default_install_dir()
764+
build_dir = (
765+
Path(args.build_dir).resolve() if args.build_dir else install_dir.parent / "renderdoc-build"
766+
)
754767

755768
if _artifacts_present(install_dir, plat):
756769
_log(f"renderdoc already exists at {install_dir}/")

src/rdc/_progress.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Progress callback factory for RenderDoc long-running operations."""
2+
3+
from __future__ import annotations
4+
5+
import math
6+
import time
7+
from collections.abc import Callable
8+
9+
import click
10+
11+
12+
def make_progress_cb(label: str, min_interval: float = 1.0) -> Callable[[float], None]:
13+
"""Return a callback suitable for RenderDoc ProgressCallback.
14+
15+
Writes to stderr. On tty: '\\r{label}: {pct:.0%}' in place, '\\n' at
16+
completion. Non-tty: one line per call, throttled to min_interval seconds.
17+
First call (progress=0.0001) and final call (progress>=1.0) always emit.
18+
Clamps NaN/negative/inf to [0, 1].
19+
"""
20+
last_echo: list[float] = [0.0]
21+
first_seen: list[bool] = [False]
22+
23+
def _cb(progress: float) -> None:
24+
if math.isnan(progress) or progress < 0:
25+
progress = 0.0
26+
elif math.isinf(progress) or progress > 1.0:
27+
progress = 1.0
28+
29+
is_first = not first_seen[0]
30+
is_done = progress >= 1.0
31+
first_seen[0] = True
32+
33+
stderr = click.get_text_stream("stderr")
34+
on_tty = stderr.isatty()
35+
36+
if on_tty:
37+
end = "\n" if is_done else "\r"
38+
click.echo(f"{label}: {progress:.0%}", nl=False, err=True)
39+
click.echo(end, nl=False, err=True)
40+
else:
41+
now = time.monotonic()
42+
if is_first or is_done or (now - last_echo[0] >= min_interval):
43+
click.echo(f"{label}: {progress:.0%}", err=True)
44+
last_echo[0] = now
45+
46+
return _cb

src/rdc/_skills/SKILL.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ Check setup: `rdc doctor`.
2020

2121
Follow this session lifecycle for any capture analysis task:
2222

23-
1. **Open** a capture: `rdc open path/to/capture.rdc`
23+
1. **Open** a capture:
24+
- Local: `rdc open path/to/capture.rdc`
25+
- Remote replay (Proxy): `rdc open capture.rdc --proxy host:port`
26+
- Split thin-client: `rdc open --connect host:port --token TOKEN`
27+
- Android device: `rdc open capture.rdc --android [--serial SERIAL]`
2428
2. **Inspect** metadata: `rdc info`, `rdc stats`, `rdc events`
2529
3. **Navigate** the VFS: `rdc ls /`, `rdc ls /textures`, `rdc cat /pipelines/0`
2630
4. **Analyze** specifics: `rdc shaders`, `rdc pipeline`, `rdc resources`, `rdc bindings`
@@ -196,6 +200,54 @@ rdc shader-restore EID # revert single shader
196200
rdc shader-restore-all # revert all modifications
197201
```
198202

203+
## Remote Capture Workflow
204+
205+
rdc-cli wraps `renderdoccmd remoteserver` to support PC-to-PC remote captures.
206+
207+
- `rdc serve [--port PORT] [--allow-ips CIDR] [--no-exec] [--daemon]` — launch remoteserver on the target machine
208+
- `rdc remote connect <host:port>` — save remote connection state
209+
- `rdc remote list` — enumerate capturable apps on the remote
210+
- `rdc remote capture <app> -o frame.rdc [--args ...] [--frame N] [--keep-remote]` — inject, capture, and transfer back. `--keep-remote` skips the transfer and prints the remote path; replay it with `rdc open <path> --proxy host:port`. (The CLI's own `next:` hint currently still references the deprecated `--remote` alias for `--proxy`.)
211+
- `rdc open frame.rdc --proxy host:port` — remote-backed replay (daemon local, GPU remote)
212+
213+
`remote_state.py` persists the last connected host so subsequent `rdc remote list` can omit `--url`.
214+
215+
## Split Mode (thin client)
216+
217+
Split mode decouples CLI and daemon — run the daemon where the GPU is and connect from a machine that doesn't need the renderdoc module. Useful when the analyst's laptop is macOS/Windows and the GPU is on a Linux server.
218+
219+
- Server side: `rdc open capture.rdc --listen [ADDR[:PORT]]`
220+
- Prints these four labeled lines to stdout (among other status output): `host: ADDR`, `port: PORT`, `token: TOKEN`, `connect with: rdc open --connect ADDR:PORT --token TOKEN`
221+
- Client side: `rdc open --connect HOST:PORT --token TOKEN`
222+
223+
SSH tunnel tip (use the port from `--listen`, or `rdc serve`'s default `39920`): `ssh -L 39920:localhost:39920 user@server`, then connect to `localhost:39920`.
224+
225+
Every normal command (`rdc draws`, `rdc rt`, ...) works transparently in Split mode. Binary exports use `file_read` RPC with raw binary frames — no base64 overhead.
226+
227+
## Android Workflow
228+
229+
- Prerequisite: the RenderDoc APK must already be installed on the host via `rdc setup-renderdoc --android` (upstream) or `--android --arm` (ARM PS fork for Mali). `rdc android setup` does not push the APK itself.
230+
- `rdc android setup [--serial SERIAL]` — starts remoteserver on the device via RenderDoc's Device Protocol API (`StartRemoteServer`), sets adb forward, saves remote state.
231+
- `rdc android capture <activity> [--serial SERIAL] [--timeout N] [--port PORT] [-o out.rdc]` — GPU debug layers based capture (works around EMUI/Mali injection limitations).
232+
- `rdc android stop [--serial SERIAL]` — stops the remoteserver and cleans state.
233+
- For remote replay: `rdc open frame.rdc --android [--serial SERIAL]` — this is the only form that rewrites the saved `adb://SERIAL` to the forwarded `localhost:PORT`. Passing `--proxy adb://SERIAL` directly bypasses the rewrite and is known to crash the daemon (see `session.py:_resolve_android_url`).
234+
235+
Hardware matrix: Adreno is the happy path; Mali may need the ARM Performance Studio fork (see `rdc setup-renderdoc --android --arm`).
236+
237+
## Troubleshooting
238+
239+
Always run `rdc doctor` first. It reports status for renderdoc module, renderdoccmd, adb, Android APK, and platform-specific toolchains. Only the missing-renderdoc-module case emits a dedicated build-hint block; other checks surface inline hints in the detail column, so read each failing line rather than relying on a uniform next-step list.
240+
241+
Common failure categories (conceptual, not literal error strings — map from the text the tool actually emits):
242+
243+
- **network / connect failed** — remote host unreachable, firewall, wrong port. Verify `rdc serve` is running on the target.
244+
- **version mismatch** — host and target RenderDoc versions differ. Re-run `rdc setup-renderdoc` or `rdc setup-renderdoc --android` to align.
245+
- **inject failed / ident=0** — injection blocked (Android EMUI, macOS SIP, Windows privilege). Run `rdc doctor` and check the platform-specific detail.
246+
- **OpenCapture unsupported** — local GPU can't replay the capture's API surface; switch to `--proxy` or `--android` remote replay.
247+
- **not loaded / no session** — forgot `rdc open`; use `rdc status` to inspect.
248+
249+
For long operations (large capture transfers, remote replay init), the CLI has limited progress feedback — this is a known UX gap, not a hang. Wait up to the `--timeout` value before concluding failure.
250+
199251
## Command Reference
200252

201253
For the complete list of all commands with their arguments, options, types, and defaults, see [references/commands-quick-ref.md](references/commands-quick-ref.md).

src/rdc/_skills/references/commands-quick-ref.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,7 @@ Create local default session and start daemon skeleton.
622622
| `--listen` | Listen on [ADDR]:PORT. Use :0 for auto-port on all interfaces. | text | |
623623
| `--connect` | Connect to an already-running external daemon. | text | |
624624
| `--token` | Authentication token (required with --connect). | text | |
625+
| `--timeout` | Daemon startup timeout in seconds. | float | |
625626

626627
## `rdc pass`
627628

@@ -759,6 +760,16 @@ Connect to a remote RenderDoc server.
759760
|------|------|------|---------|
760761
| `--json` | Output as JSON. | flag | |
761762

763+
## `rdc remote disconnect`
764+
765+
Delete the saved remote server state (local only).
766+
767+
**Options:**
768+
769+
| Flag | Help | Type | Default |
770+
|------|------|------|---------|
771+
| `--json` | Output as JSON. | flag | |
772+
762773
## `rdc remote list`
763774

764775
List capturable applications on a remote host.
@@ -770,6 +781,33 @@ List capturable applications on a remote host.
770781
| `--url` | Override saved remote (host:port). | text | |
771782
| `--json` | Output as JSON. | flag | |
772783

784+
## `rdc remote setup`
785+
786+
Verify a remote server is reachable, handshake, and save state.
787+
788+
**Arguments:**
789+
790+
| Name | Type | Required |
791+
|------|------|----------|
792+
| `url` | text | yes |
793+
794+
**Options:**
795+
796+
| Flag | Help | Type | Default |
797+
|------|------|------|---------|
798+
| `--timeout` | TCP probe timeout in seconds. | float | 10.0 |
799+
| `--json` | Output as JSON. | flag | |
800+
801+
## `rdc remote status`
802+
803+
Show the currently saved remote server state.
804+
805+
**Options:**
806+
807+
| Flag | Help | Type | Default |
808+
|------|------|------|---------|
809+
| `--json` | Output as JSON. | flag | |
810+
773811
## `rdc resource`
774812

775813
Show details of a specific resource.

src/rdc/capture_core.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from pathlib import Path
1111
from typing import Any
1212

13+
import click
14+
1315
from rdc import _platform
1416
from rdc.discover import find_renderdoc
1517

@@ -220,7 +222,9 @@ def run_target_control_loop(
220222
else:
221223
tc.TriggerCapture(1)
222224

223-
deadline = time.monotonic() + timeout
225+
start = time.monotonic()
226+
deadline = start + timeout
227+
last_echo = start
224228
while time.monotonic() < deadline:
225229
if not tc.Connected():
226230
return CaptureResult(error="target disconnected")
@@ -239,6 +243,10 @@ def run_target_control_loop(
239243
)
240244
if msg_type == 1:
241245
return CaptureResult(error="target disconnected")
246+
now = time.monotonic()
247+
if now - last_echo >= 5.0:
248+
click.echo(f"waiting for capture... ({int(now - start):d}s)", err=True)
249+
last_echo = now
242250
time.sleep(0.01)
243251

244252
return CaptureResult(error="timeout waiting for capture")

0 commit comments

Comments
 (0)