Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-astro/src/data/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@
"name": "rt",
"id": "rt",
"help": "Export render target as PNG.",
"usage": "rdc rt [EID] [-o PATH] [--target INTEGER] [--raw] [--overlay CHOICE] [--width INTEGER] [--height INTEGER]"
"usage": "rdc rt [EID] [-o PATH] [--target INTEGER] [--depth] [--raw] [--overlay CHOICE] [--width INTEGER] [--height INTEGER]"
},
{
"name": "mesh",
Expand Down
81 changes: 81 additions & 0 deletions openspec/changes/2026-06-09-rt-depth-flag/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# OpenSpec: 2026-06-09-rt-depth-flag

## Summary

Add `--depth` flag to `rdc rt` so `rdc rt <EID> --depth -o depth.png` exports the raw
depth attachment via the existing `rt_depth` VFS route.

## Context and Motivation

Issue #236's verify section literally used `rdc rt --eid 500 --depth` as the canonical
test command. The backend was shipped in PR #237: VFS route
`/draws/<eid>/targets/depth.png` → handler `rt_depth` is registered in `vfs/router.py`
line 152 and works in both local and remote (pid==0) modes. The CLI plumbing was never
added, leaving users with no ergonomic path to the depth attachment.

## Design

### New flag: `--depth`

```
rdc rt <EID> [--depth] [-o output.png] [--raw]
```

When `--depth` is passed, `rt_cmd` routes to
`/draws/{eid}/targets/depth.png` instead of `/draws/{eid}/targets/color{target}.png`.

### `--target` sentinel change

`--target` currently defaults to `0`. Click cannot distinguish "user passed `--target 0`"
from "user did not pass `--target`" when the default is `0`. To detect mutual exclusion:

- Change `--target` default in the Click decorator to `None` (sentinel).
- In the help text, document the default as `0` explicitly.
- In `rt_cmd` logic: if `--depth` is set and `--target` is not `None`, raise
`click.UsageError("--depth and --target are mutually exclusive")`.
- If `--depth` is not set and `--target is None`, use `0` as the effective target.

This is a breaking change to the `--target` parameter type annotation (from `int` to
`int | None`) but not to user-visible defaults.

### Interaction with `--overlay`

`--overlay` takes priority: if `--overlay` is provided, the existing overlay path runs
unchanged and `--depth` is silently ignored (no interaction needed; depth overlay is a
separate concept). Document in help text.

### Distinction: `--overlay depth` vs `--depth`

These are fundamentally different operations:

- `--overlay depth` calls `rt_overlay` with `overlay="depth"`, which asks RenderDoc to
render the depth *overlay visualization* on top of the colour buffer. Output is a
colour-mapped image rendered by RenderDoc's overlay engine.
- `--depth` exports the raw depth/stencil attachment texture at the draw's framebuffer
bind point, via the `rt_depth` handler. Output is the raw depth buffer as PNG.

The `rdc rt` help text must call this distinction out explicitly.

### Shell completion: `_complete_rt_target`

`_complete_rt_target` filters `color\d+\.png` filenames and therefore already excludes
`depth.png`. No change needed; `--depth` is a boolean flag without a completeable value.

### Docs surface

`scripts/gen-commands.py` introspects Click command objects to produce
`docs-astro/src/data/commands.json`, which feeds the Astro-based command reference.
`scripts/gen-skill-ref.py` generates `src/rdc/_skills/references/commands-quick-ref.md`.
Both are regenerated via `pixi run gen-commands` and `pixi run gen-skill-ref`. The new
flag will appear automatically once regenerated; no manual doc edits required.

## Risks

- **CLI surface change**: `--target` default changes from `int(0)` to `None` internally.
Any caller that inspects `rt_cmd`'s Click parameter objects programmatically (e.g.
test introspection) may need updating.
- **Shell completion**: no impact; `_complete_rt_target` is not invoked for `--depth`.
- **Docs regen**: both `check-commands` and `check-skill-ref` CI checks will fail if
regenerated artifacts are not committed alongside the implementation.
- **Remote mode**: `_export_vfs_path` already handles `pid==0` (remote daemon) via
`_deliver_binary`; no special casing needed for `--depth`.
38 changes: 38 additions & 0 deletions openspec/changes/2026-06-09-rt-depth-flag/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Tasks: 2026-06-09-rt-depth-flag

## T1: Implementation — `rt_cmd` CLI plumbing

- [ ] In `src/rdc/commands/export.py`, change `--target` default from `0` to `None`;
update help text to read `"Color target index (default 0)"` (no user-visible change).
- [ ] Add `--depth` boolean flag (`is_flag=True`) to the `@click.command("rt")` decorator.
- [ ] In `rt_cmd` body (after the `--overlay` early-return, before `_export_vfs_path`):
- Raise `click.UsageError("--depth and --target are mutually exclusive")` if
`depth` is `True` and `target is not None`.
- If `depth` is `True`: call
`_export_vfs_path(f"/draws/{eid}/targets/depth.png", output, raw)`.
- Otherwise: use `target if target is not None else 0` as the color index.

## T2: Unit tests

- [ ] Extend `TestRtCmd` in `tests/unit/test_export_commands.py` with four cases from
test-plan.md: `test_rt_depth_routes_to_depth_png`,
`test_rt_depth_with_explicit_target_raises_usage_error`,
`test_rt_depth_target_none_defaults_to_color0`,
`test_rt_depth_remote_pid0_writes_output`,
`test_rt_depth_with_overlay_ignores_depth`.
- [ ] Run `pixi run test` — all pass.

## T3: Docs + completion regen

- [ ] Run `pixi run gen-commands` to regenerate `docs-astro/src/data/commands.json`.
- [ ] Run `pixi run gen-skill-ref` to regenerate
`src/rdc/_skills/references/commands-quick-ref.md`.
- [ ] Verify `pixi run check-commands` and `pixi run check-skill-ref` pass (CI gate).
- [ ] Commit regenerated artifacts alongside the implementation.

## T4: Manual real-GPU verify

- [ ] Run the four manual steps from test-plan.md against a vkcube capture:
local depth export, remote-proxy depth export, mutual-exclusion error, overlay
depth regression.
- [ ] Confirm output PNG is non-empty and visually plausible depth greyscale.
109 changes: 109 additions & 0 deletions openspec/changes/2026-06-09-rt-depth-flag/test-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Test Plan: 2026-06-09-rt-depth-flag

## Scope

### In scope
- `--depth` routes to `/draws/{eid}/targets/depth.png`
- `--depth` + explicit `--target` raises `UsageError`
- `--depth` + `-o` writes output file
- `--depth` without `--overlay` reaches `_export_vfs_path`
- Remote daemon path (pid==0) with `--depth`

### Out of scope
- `rt_depth` daemon handler correctness (shipped in PR #237, covered by prior tests)
- `--overlay` behaviour (no change)

## Test Matrix

| Layer | Test Type | File |
|-------|-----------|------|
| Unit | `--depth` flag routing | `tests/unit/test_export_commands.py` (extend `TestRtCmd`) |
| Unit | `--depth` + `--target` mutual exclusion | `tests/unit/test_export_commands.py` |
| Unit | `--depth` + `-o` output file written | `tests/unit/test_export_commands.py` |
| Unit | remote pid==0 path with `--depth` | `tests/unit/test_export_commands.py` |
| Unit | `--depth` + `--overlay` ignores depth (no error) | `tests/unit/test_export_commands.py` |
| Manual | real GPU: local + remote | see below |

## Unit Cases (extend `TestRtCmd` in `test_export_commands.py`)

### `test_rt_depth_routes_to_depth_png`

Monkeypatch `call` and `_deliver_binary`. Invoke `rt_cmd` with `["100", "--depth", "-o",
str(out_file)]`. Assert exit code 0. Assert `vfs_ls` was called with path
`/draws/100/targets/depth.png`.

### `test_rt_depth_with_explicit_target_raises_usage_error`

Invoke `rt_cmd` with `["100", "--depth", "--target", "1", "-o", str(out_file)]`. Assert
exit code non-zero. Assert output/error contains `"mutually exclusive"`.

### `test_rt_depth_target_none_defaults_to_color0`

Invoke `rt_cmd` with `["100", "-o", str(out_file)]` (no `--depth`, no `--target`). Assert
`vfs_ls` was called with path `/draws/100/targets/color0.png`. Confirms sentinel default
behaviour is preserved.

### `test_rt_depth_remote_pid0_writes_output`

Monkeypatch `rdc.commands.vfs._load_session` to return a session with `pid=0`,
`rdc.commands.vfs.fetch_remote_file` to return PNG bytes, and
`rdc.commands.vfs._stdout_is_tty` to `lambda: False`. Invoke `rt_cmd` with
`["100", "--depth", "-o", str(out_file)]`.

NOTE (corrected against actual code): `_export_vfs_path` → `_deliver_binary` does NOT
emit any "--output required" error for the VFS path. For `pid==0` it calls
`fetch_remote_file(temp_path)` and, when `output` is set, writes the bytes to the output
file (vfs.py lines 234-244). There is no remote "--output is required" guard on this
path; that guard exists only in the `--overlay` branch of `rt_cmd`. So:

- Assert exit code 0.
- Assert `vfs_ls` was called with `/draws/100/targets/depth.png`.
- Assert `out_file` was written with the mocked PNG bytes.

(If the remote no-`-o` behaviour is also worth covering: under `CliRunner`,
`_stdout_is_tty()` is False, so `--depth` with no `-o` and `pid==0` writes bytes to
stdout and exits 0 — it does NOT error. Per the settled main-agent ruling, `--depth`
without `-o` behaves exactly like color export, i.e. no special TTY/remote guard.)

### `test_rt_depth_with_overlay_ignores_depth`

Per the design, `--overlay` takes priority and `--depth` is silently ignored (the
`--overlay` branch early-returns before the depth/target logic, so no mutual-exclusion
error is raised for `--depth + --overlay`). Monkeypatch `call` so `rt_overlay` returns a
`path` and `fetch_remote_file` returns bytes. Invoke `rt_cmd` with
`["100", "--overlay", "depth", "--depth", "-o", str(out_file)]`. Assert exit code 0,
assert `rt_overlay` was called (not `vfs_ls` for `depth.png`), and `vfs_ls` was NOT
called with `/draws/100/targets/depth.png`. Confirms `--depth` is inert under `--overlay`
and does not trip the mutual-exclusion guard.

## Manual Real-GPU Verify

Prerequisite: vkcube capture with a known draw EID that has a depth attachment (any 3D
draw should qualify).

1. **Local mode**:
```
rdc open capture.rdc
rdc rt <EID> --depth -o /tmp/depth_local.png
```
Assert exit code 0, `/tmp/depth_local.png` is a valid PNG, non-empty.

2. **Remote proxy mode**:
```
rdc open capture.rdc --proxy host:port
rdc rt <EID> --depth -o /tmp/depth_remote.png
```
Assert exit code 0, file written, pixel values plausible (non-constant, depth values
visible as greyscale gradient).

3. **Mutual exclusion error**:
```
rdc rt <EID> --depth --target 0 -o /tmp/d.png
```
Assert non-zero exit with `"mutually exclusive"` in output.

4. **`--overlay depth` still works** (regression):
```
rdc rt <EID> --overlay depth -o /tmp/depth_overlay.png
```
Assert exit code 0. File is a colour-mapped overlay PNG (distinct from raw depth).
3 changes: 2 additions & 1 deletion src/rdc/_skills/references/commands-quick-ref.md
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,8 @@ Export render target as PNG.
| Flag | Help | Type | Default |
|------|------|------|---------|
| `-o, --output` | Write to file | path | |
| `--target` | Color target index (default 0) | integer | 0 |
| `--target` | Color target index (default 0); mutually exclusive with --depth | integer | |
| `--depth` | Export the raw depth attachment texture (/draws/<eid>/targets/depth.png); distinct from --overlay depth, which renders RenderDoc's depth overlay visualization. Ignored when --overlay is set. | flag | |
| `--raw` | Force raw output even on TTY | flag | |
| `--overlay` | Render with debug overlay | choice | |
| `--width` | Overlay render width | integer | 256 |
Expand Down
26 changes: 22 additions & 4 deletions src/rdc/commands/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,19 @@ def texture_cmd(id: int, output: str | None, mip: int, raw: bool) -> None:
@click.option("-o", "--output", type=click.Path(), default=None, help="Write to file")
@click.option(
"--target",
default=0,
default=None,
type=int,
shell_complete=_complete_rt_target,
help="Color target index (default 0)",
help="Color target index (default 0); mutually exclusive with --depth",
)
@click.option(
"--depth",
is_flag=True,
help=(
"Export the raw depth attachment texture (/draws/<eid>/targets/depth.png); "
"distinct from --overlay depth, which renders RenderDoc's depth overlay "
"visualization. Ignored when --overlay is set."
),
)
@click.option("--raw", is_flag=True, help="Force raw output even on TTY")
@click.option(
Expand All @@ -160,7 +169,8 @@ def texture_cmd(id: int, output: str | None, mip: int, raw: bool) -> None:
def rt_cmd(
eid: int | None,
output: str | None,
target: int,
target: int | None,
depth: bool,
raw: bool,
overlay: str | None,
width: int,
Expand Down Expand Up @@ -194,7 +204,15 @@ def rt_cmd(

if eid is None:
raise click.UsageError("EID is required when --overlay is not used")
_export_vfs_path(f"/draws/{eid}/targets/color{target}.png", output, raw)

if depth:
if target is not None:
raise click.UsageError("--depth and --target are mutually exclusive")
_export_vfs_path(f"/draws/{eid}/targets/depth.png", output, raw)
return

color = target if target is not None else 0
_export_vfs_path(f"/draws/{eid}/targets/color{color}.png", output, raw)


@click.command("buffer")
Expand Down
Loading
Loading