b0ade34d
Add GPU render testing design spec
a73x 2026-04-17 10:39
Commit message
docs/superpowers/specs/2026-04-17-gpu-render-testing-design.md
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,159 @@ | |||
| 1 | # GPU Render Testing Design | ||
| 2 | |||
| 3 | Automated visual regression and performance regression testing for the Vulkan rendering pipeline. | ||
| 4 | |||
| 5 | ## Problem | ||
| 6 | |||
| 7 | There is no automated way to verify that waystty's GPU output is correct, or to detect rendering performance regressions. The frame timing instrumentation measures *how fast* frames render but not *what* they render. Smoke tests validate subsystem init but not visual output. | ||
| 8 | |||
| 9 | ## Goals | ||
| 10 | |||
| 11 | - Detect visual rendering regressions automatically (wrong colors, missing glyphs, shifted cells, broken attributes) | ||
| 12 | - Detect performance regressions in the rendering pipeline | ||
| 13 | - Keep the test infrastructure local-only, zero external dependencies beyond Zig | ||
| 14 | - Produce actionable failure output (diff images, timing comparisons) | ||
| 15 | |||
| 16 | ## Non-Goals | ||
| 17 | |||
| 18 | - CI/headless testing (requires real GPU + compositor) | ||
| 19 | - Perceptual/SSIM diffing (RMSE is sufficient for terminal content) | ||
| 20 | - External compositor screenshots (captures happen in-process via Vulkan readback) | ||
| 21 | - Multi-scale testing (tests run at current display scale; add scale variants later if needed) | ||
| 22 | |||
| 23 | ## Design | ||
| 24 | |||
| 25 | ### 1. Capture Mode (`--capture <script> <output.png>`) | ||
| 26 | |||
| 27 | A new CLI mode that renders a VT script and captures the final frame as a PNG. | ||
| 28 | |||
| 29 | **Flow:** | ||
| 30 | |||
| 31 | 1. waystty starts normally (Wayland surface, Vulkan pipeline, PTY) | ||
| 32 | 2. The child shell is replaced by `cat <script>` piped into the PTY | ||
| 33 | 3. After PTY input is exhausted (child exits), one final frame is rendered to flush all dirty rows | ||
| 34 | 4. `vkCmdCopyImageToBuffer` copies the swapchain image to a host-visible staging buffer after fence-wait on the last submit | ||
| 35 | 5. Raw BGRA pixel data is written as PNG to `<output.png>` | ||
| 36 | 6. Frame timing stats are dumped to stderr (reusing existing benchmark output) | ||
| 37 | 7. waystty exits cleanly | ||
| 38 | |||
| 39 | **Key detail:** The readback happens after the GPU has finished the final present. This captures exactly what would have been composited. | ||
| 40 | |||
| 41 | **Code locations:** | ||
| 42 | - Arg parsing + quiescence detection + exit-after-capture: `src/main.zig` | ||
| 43 | - Vulkan readback + PNG write: `src/renderer.zig` | ||
| 44 | |||
| 45 | ### 2. Golden Image Comparison | ||
| 46 | |||
| 47 | A standalone Zig tool that compares two PNG images and reports pass/fail. | ||
| 48 | |||
| 49 | **Comparison logic:** | ||
| 50 | - Load actual and reference PNGs | ||
| 51 | - Hard fail if dimensions differ (means grid size or scale changed) | ||
| 52 | - Per-pixel RGBA distance, averaged across all pixels to compute RMSE | ||
| 53 | - Pass threshold: RMSE < 0.3% (configurable via `WAYSTTY_TEST_THRESHOLD`) | ||
| 54 | - On failure: write a diff image (red overlay on dimmed actual) and report the RMSE value | ||
| 55 | |||
| 56 | **Failure output:** | ||
| 57 | ``` | ||
| 58 | FAIL: tests/golden/reference/bold_colors.png | ||
| 59 | RMSE: 1.2% (threshold: 0.3%) | ||
| 60 | Diff: tests/golden/output/bold_colors.diff.png | ||
| 61 | Actual: tests/golden/output/bold_colors.png | ||
| 62 | ``` | ||
| 63 | |||
| 64 | **Why Zig, not ImageMagick/Python:** | ||
| 65 | - Zero external deps — project is pure Zig | ||
| 66 | - PNG encode/decode is straightforward (use zigimg or minimal vendored decoder) | ||
| 67 | - Same build system — `zig build test-render` builds and runs everything | ||
| 68 | |||
| 69 | **Code location:** `src/tools/imgdiff.zig` — standalone build target. | ||
| 70 | |||
| 71 | ### 3. VT Test Scripts | ||
| 72 | |||
| 73 | Curated escape sequences, one per rendering feature: | ||
| 74 | |||
| 75 | | Script | Exercises | | ||
| 76 | |--------|-----------| | ||
| 77 | | `basic_ascii.vt` | Full printable ASCII range, default colors | | ||
| 78 | | `bold_colors.vt` | Bold, dim, italic, underline + 16 ANSI colors | | ||
| 79 | | `256_colors.vt` | 256-color palette grid | | ||
| 80 | | `truecolor.vt` | 24-bit RGB gradients | | ||
| 81 | | `box_drawing.vt` | Box-drawing and block element characters | | ||
| 82 | | `cursor_movement.vt` | Cursor positioning, clear, scroll regions | | ||
| 83 | | `reverse_video.vt` | Reverse video, hidden, strikethrough attributes | | ||
| 84 | |||
| 85 | Start with 3-4 scripts initially, add more as features land. | ||
| 86 | |||
| 87 | **Directory layout:** | ||
| 88 | ``` | ||
| 89 | tests/golden/ | ||
| 90 | ├── scripts/ # VT input files (raw escape sequences) | ||
| 91 | │ ├── basic_ascii.vt | ||
| 92 | │ ├── bold_colors.vt | ||
| 93 | │ └── ... | ||
| 94 | ├── reference/ # Approved golden PNGs (checked into git) | ||
| 95 | │ ├── basic_ascii.png | ||
| 96 | │ ├── bold_colors.png | ||
| 97 | │ └── ... | ||
| 98 | └── output/ # Generated by test run (gitignored) | ||
| 99 | ├── basic_ascii.png | ||
| 100 | ├── basic_ascii.diff.png | ||
| 101 | └── ... | ||
| 102 | ``` | ||
| 103 | |||
| 104 | ### 4. Performance Regression Detection | ||
| 105 | |||
| 106 | Leverages the existing FrameTimingRing. The `--capture` mode gets timing data for free. | ||
| 107 | |||
| 108 | **Baseline file comparison:** | ||
| 109 | - `make bench-baseline` runs the benchmark workload, writes frame timing stats (min/avg/p99/max per section) to `tests/bench/baseline.json` | ||
| 110 | - `make bench-check` runs the same workload, compares each metric against the baseline | ||
| 111 | |||
| 112 | **Regression threshold:** p99 must not increase by more than 20% versus baseline. Configurable via `WAYSTTY_BENCH_REGRESSION_PCT`. | ||
| 113 | |||
| 114 | **Why p99:** Averages hide spikes. A change adding occasional 5ms stalls to `gpu_submit` won't move the average but will tank perceived smoothness. p99 catches these. | ||
| 115 | |||
| 116 | **Output:** | ||
| 117 | ``` | ||
| 118 | bench: snapshot p99 21us (baseline 19us) +10.5% OK | ||
| 119 | bench: row_rebuild p99 603us (baseline 580us) +3.9% OK | ||
| 120 | bench: gpu_submit p99 290us (baseline 90us) +222.2% REGRESSION | ||
| 121 | ``` | ||
| 122 | |||
| 123 | **Baseline management:** | ||
| 124 | - `baseline.json` is checked into git — the "known good" perf profile for this machine | ||
| 125 | - After intentional perf changes, `make bench-baseline` to update | ||
| 126 | - Machine-specific by nature — local-only tool, so this is fine | ||
| 127 | |||
| 128 | ### 5. Makefile Targets | ||
| 129 | |||
| 130 | | Target | Action | | ||
| 131 | |--------|--------| | ||
| 132 | | `make test-render` | For each `.vt` script: run `--capture`, compare against golden, report pass/fail. Non-zero exit on failure. | | ||
| 133 | | `make golden-update` | Run all captures, copy output to `reference/`. Used after visual verification of intentional changes. | | ||
| 134 | | `make bench-baseline` | Save current perf profile to `tests/bench/baseline.json` | | ||
| 135 | | `make bench-check` | Run benchmark workload, compare against baseline, flag regressions | | ||
| 136 | |||
| 137 | ### 6. Dependencies | ||
| 138 | |||
| 139 | - `zigimg` (or minimal PNG encoder/decoder) added to `build.zig.zon` | ||
| 140 | - No other external dependencies | ||
| 141 | |||
| 142 | ## Workflow | ||
| 143 | |||
| 144 | 1. Make a rendering change | ||
| 145 | 2. `make test-render` — see if anything visually regressed | ||
| 146 | 3. If a test fails, inspect the diff image in `tests/golden/output/` | ||
| 147 | 4. If the change is intentional, `make golden-update` to approve new baselines | ||
| 148 | 5. `make bench-check` — see if anything got slower | ||
| 149 | 6. If perf changed intentionally, `make bench-baseline` to update | ||
| 150 | |||
| 151 | ## Implementation Order | ||
| 152 | |||
| 153 | 1. Vulkan readback + PNG write in renderer.zig | ||
| 154 | 2. `--capture` mode in main.zig | ||
| 155 | 3. Image comparison tool (imgdiff.zig) | ||
| 156 | 4. Initial VT test scripts (basic_ascii, bold_colors, box_drawing) | ||
| 157 | 5. Makefile targets (test-render, golden-update) | ||
| 158 | 6. Generate and commit initial golden reference images | ||
| 159 | 7. Benchmark baseline infrastructure (bench-baseline, bench-check) | ||