a73x

7a1e22d2

Input-latency bench: share grid-lock across all bench modes

a73x   2026-04-18 06:55

Retrofit existing WAYSTTY_BENCH to use the same grid-lock mechanism
as the new input-latency bench. Output-bench numbers today are
silently compositor-dependent (accepts tiling resize); after this
change, any bench mode either runs at the configured grid size
(default 80x24, overridable via WAYSTTY_BENCH_COLS/ROWS) or aborts
with a diagnostic pointing at tiling-compositor resize as the cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

diff --git a/docs/superpowers/specs/2026-04-18-input-latency-bench-design.md b/docs/superpowers/specs/2026-04-18-input-latency-bench-design.md
index d3e318f..648f3cd 100644
--- a/docs/superpowers/specs/2026-04-18-input-latency-bench-design.md
+++ b/docs/superpowers/specs/2026-04-18-input-latency-bench-design.md
@@ -20,9 +20,11 @@ The output is two headline metrics plus a per-stage breakdown:

New env var `WAYSTTY_INPUT_BENCH={cold|hot|both}` activates input-bench mode.

When active, waystty:
Grid-lock (below) is shared infrastructure — applied whenever *any* bench mode is active (`WAYSTTY_BENCH` or `WAYSTTY_INPUT_BENCH`). This retrofits the existing output-only bench so both benches have the same reproducibility guarantees.

1. **Locks the grid to a known size** before spawning any child. Default 80×24, overridable via `WAYSTTY_INPUT_BENCH_COLS` / `WAYSTTY_INPUT_BENCH_ROWS`. This removes font/DPI/monitor variance so numbers are reproducible across machines and so hot-mode sentinel lifetime is a computed guarantee, not a hope.
When input-bench mode is active, waystty:

1. **Locks the grid to a known size** (see Grid-lock module below). Default 80×24, overridable via `WAYSTTY_BENCH_COLS` / `WAYSTTY_BENCH_ROWS`. This removes font/DPI/monitor variance so numbers are reproducible across machines and so hot-mode sentinel lifetime is a computed guarantee, not a hope.
2. Spawns a minimal PTY child instead of `$SHELL`:
   - `cold` → `cat > /dev/null`. Reads and discards stdin so the PTY slave's input buffer stays drained; kernel PTY line discipline echoes each byte (ECHO flag on the slave). Cat's canonical-mode buffering is irrelevant — we measure kernel echo, not cat's reads.
   - `hot` → `sh -c 'yes "$(printf "x%.0s" {1..500})" | pv -qL 24K'`. Produces 500-char lines rate-limited to 24 KB/s, ≈ 46 lines/sec. On an 80×24 grid this gives a sentinel lifetime of ≈30 frames at 60Hz — enough time for any reasonable presentation to land — while still exercising the render pipeline (100KB/s of parsing, atlas churn on long lines, per-frame row rebuild).
@@ -34,6 +36,20 @@ When active, waystty:

MAILBOX present mode is preserved throughout. The driver handles `wp_presentation_feedback.discarded` by keeping feedback listeners live on subsequent frames, since a discarded frame's sentinel is still in the grid on the next frame.

## Module 0: Grid-lock (shared across all bench modes)

Applied whenever `WAYSTTY_BENCH` or `WAYSTTY_INPUT_BENCH` is set.

1. Size the initial window to `cols × cell_w` / `rows × cell_h` as today.
2. Advertise `xdg_toplevel.set_min_size(w, h)` and `set_max_size(w, h)` to signal that the window should not be resized. Compositors that honor these hints (most floating compositors) will leave the window alone.
3. In the main-loop resize observer (`src/main.zig:409`), if bench mode is active and the compositor forces a different size, **abort** the bench with a diagnostic on stderr:

   > `waystty bench: compositor sized window to WxH, expected CxR grid. Run in a floating window or a non-tiling compositor for reproducible benchmarks.`

4. Print the achieved grid size as the first line of any bench stats output, so it's always visible alongside the numbers.

This retrofits the existing `WAYSTTY_BENCH` mode — today it starts at 80×24 but silently accepts compositor resize, so numbers are already compositor-dependent. After this change, existing output-bench numbers are guaranteed reproducible or it fails loudly.

## Module 1: Sentinel allocation

Each sample uses a unique codepoint from the Unicode Private Use Area U+E000…U+EFFF (4096 distinct sentinels). PUA is chosen because it never appears in normal output from `cat`, `yes`, or `pv`, so a grid scan for a specific codepoint cannot collide with unrelated output. Only one sample is in flight at a time, so wraparound at 4096 is safe.
@@ -144,7 +160,7 @@ Mirrors the existing `bench` / `profile` targets: `OPT` defaults to `ReleaseFast

- `src/bench_input.zig` — new. `BenchDriver` struct, sentinel allocator, in-flight sample state, pending-feedback map, stats printer, WSI fallback trigger.
- `src/bench_stats.zig` — add `frame_counter: u64` to `FrameTiming`; increment on each rendered frame.
- `src/main.zig` — env parsing, grid-size lock, driver init/tick hookup, PTY child switcheroo, termios verification, scenario sequencer with teardown, sentinel scan in the post-frame hook, bench-keyboard `.key`-suppression gate.
- `src/main.zig` — env parsing, grid-size lock (shared across bench modes, retrofits existing `WAYSTTY_BENCH`), `xdg_toplevel` min/max size hints, resize-observer abort on mismatch, driver init/tick hookup, PTY child switcheroo, termios verification, scenario sequencer with teardown, sentinel scan in the post-frame hook, bench-keyboard `.key`-suppression gate.
- `src/wayland.zig` — bind `wp_presentation` global, plumb `wp_presentation_feedback` creation and event callbacks.
- `src/renderer.zig` — expose swapchain present call site so `wp_presentation.feedback(surface)` can be called immediately before `vkQueuePresentKHR`.
- `Makefile` — `bench-input` target.
@@ -155,7 +171,8 @@ Mirrors the existing `bench` / `profile` targets: `OPT` defaults to `ReleaseFast
- **Unit:** sentinel allocator produces 4096 distinct codepoints before wrapping.
- **Unit:** pair-on-arrival state machine completes when events arrive in either order; advances past `discarded`.
- **Unit:** WSI-fallback trigger fires after >10% of first 50 samples time out and not before.
- **Manual:** grid is 80×24 at bench start regardless of window size; verify via logging.
- **Manual (input bench):** grid is 80×24 at bench start regardless of window size; verify via logging.
- **Manual (existing output bench):** `make bench` still passes after grid-lock retrofit; grid-size line appears in stats output; verify on both floating and tiling compositors (tiling should abort with the diagnostic).
- **Manual:** termios `ECHO` is set on the PTY slave after spawn; verify with a short run of `stty -a` in debug mode.
- **Manual:** `make bench-input` produces cold and hot numbers with cold < hot. Sanity: cold p50 on the order of one refresh interval (wait-to-vsync plus compositor latch) is expected; hot p99 noticeably larger than cold p99 confirms contention is being observed.
- **Manual:** run `WAYSTTY_INPUT_BENCH=cold` against a debug build; verify via logs that each injected sentinel round-trips and matches within a few frames.