docs: the test rig's layers 1 and 2, with their commands and their limits

RUST.md's "Three test layers" section rewritten in place with what was
built: the `cargo test -p transcript-fixture` command and the five
assertions with the mutation that fails each, the `run-headless.sh
--phone [--replay …]` commands and the 15s/18s they take, and a
paragraph on what still cannot be answered below layer 3 (anything about
pixels, any frame time, anything JNI). Also the two traps that cost time
-- `swaymsg seat - cursor` reaching nothing on a compositor with no
input devices, and a leftover window tiling beside the new one so a
screenshot looks like a duplicated-primitive bug.

IRIS.md gains the public surface: `iris::harness`, `TouchScript`,
`List::fling_velocity`, the fling's clock, and the desktop backend's
move to physical-pixel layout with `content_scale`/`IRIS_SCALE`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-07 12:39:53 -04:00
1 parent 1121d7cc83
commit 038f6a3832
2 files changed
+95 -33

No files matched your search

+14
View File
@@ -283,6 +283,20 @@ Each exists because something was invisible without it.
checkout's own emulator, taps "Run benchmark" by label, and prints the checkout's own emulator, taps "Run benchmark" by label, and prints the
report -- written so the P0 build/install/tap/read-report cycle stops report -- written so the P0 build/install/tap/read-report cycle stops
being retyped by hand each time (docs/RUST.md's P0 box). being retyped by hand each time (docs/RUST.md's P0 box).
- **iris's three test layers** (docs/RUST.md's "Three test layers" has
the commands and what each cannot answer): test at the cheapest one
that can answer the question. `cargo test -p transcript-fixture` runs
the real transcript screen over the bench fixture with **no window, no
compositor and no GPU** (`iris::harness`), on a clock the test owns and
a gesture replayed from a `t_ms action x y` file under
`iris/transcript-fixture/touch/` -- which is how the batched 120Hz
flick a finger actually makes is testable at all, since a `ui-trace`
swipe is many evenly-spaced events. `iris/run-headless.sh phone --phone
--shot …` opens the same screen in a window at the phone's own size and
density for looking at, and `--replay FILE` drives the same recording
into it. The emulator is for JNI, the IME, insets, the surface
lifecycle and one verification run before a build goes to the phone --
not for iterating on layout.
### Driving the UI ### Driving the UI
+81 -33
View File
@@ -48,7 +48,7 @@ closes it.
In order; two builders at a time. Each is ticked here by the agent that In order; two builders at a time. Each is ticked here by the agent that
closes it. closes it.
- [ ] Test rig, layers 1 and 2 ("Three test layers" below). Running. - [x] Test rig, layers 1 and 2 ("Three test layers" below), landed 2026-09-07.
- [ ] Fling parity with Compose, and the phone's keyboard push-up, with - [ ] Fling parity with Compose, and the phone's keyboard push-up, with
insets shown in the diagnostics overlay. Running, in a worktree. insets shown in the diagnostics overlay. Running, in a worktree.
- [ ] Rows at the transcript's top edge: culled too early in one state, - [ ] Rows at the transcript's top edge: culled too early in one state,
@@ -93,7 +93,7 @@ The bench client (`android-app/src/bench_client.rs`, ~1000 lines) is
the first thing to look at moving, since a desktop bench on the same the first thing to look at moving, since a desktop bench on the same
fixture is layer 2 of the test rig below. fixture is layer 2 of the test rig below.
### Three test layers, cheapest first (decided 2026-09-07, rig not yet built) ### Three test layers, cheapest first (decided 2026-09-07; layers 1 and 2 built the same day)
Iris's suggestion, adopted and layered: test at the cheapest layer that Iris's suggestion, adopted and layered: test at the cheapest layer that
can answer the question, and go up only when it cannot. The emulator can answer the question, and go up only when it cannot. The emulator
@@ -101,43 +101,91 @@ costs minutes a cycle; the desktop window seconds; the headless harness
runs inside `cargo test`. runs inside `cargo test`.
1. **Headless, in-process, no compositor and no GPU -- the default.** 1. **Headless, in-process, no compositor and no GPU -- the default.**
`iris/src/layout_tests.rs` already builds a tree over `UiRenderState` `iris::harness` (`iris/src/harness.rs`), plus the fixture crate it
with no window, `sense.rs`'s gesture tests feed fabricated opens. `Harness::new(size, density)` builds an `Rsc`, a
`CursorState`s with their own times, and `List::tick_fling` is `UiRenderState` and a state whose `FocusHost`/`OpenUrl` *record* what
driven by hand. Extend that into one harness that opens the platform was asked for; `frame(t_ms)`/`frames_until(..)` run
`transcript-ui`'s screen on `app/bench-fixture` (as `bench_client.rs` frames on a clock the test owns, and `replay(&TouchScript)` feeds a
does on Android, no server), at the phone's logical size and recorded gesture one sample at a time exactly as
`content_scale` from `docs/bench/iris-phone-v2-2026-09-06.md` (2.55, `IrisViewPeer::on_touch_event` replays Android's historical samples.
120Hz -- read, never typed from memory), ticks frames, and feeds a The recordings are plain `t_ms action x y` files under
**replayed touch stream** from a trivial file of `(t_ms, action, x, `iris/transcript-fixture/touch/`, and `flick-120hz.touch` is the
y)` lines with `cursor.time` taken from the file. That is what the phone's own shape: DOWN, four samples 4ms apart, UP, 20ms in total.
emulator cannot do at all: the batched 120Hz flick from the phone
report becomes a deterministic test asserting on scroll offset and on cd iris && cargo test -p transcript-fixture
the `iris drag release:` velocity. Anything about layout, scroll
position, selection, focus or fold state is answered here, with runs in about a second and asserts (a) the flick releases with a real
assertions rather than eyes. Nothing renders; a widget's placed velocity (`List::fling_velocity`, which only `Released(Some(v))`
rectangle is the evidence. fills), (b) the list travels and settles inside the AOSP spline's own
`FlingCalculator::duration`, (c) a tap moves nothing and opens no
link, (d) a long-press-then-drag leaves selected text and does not
pan, and (e) the composer clears a simulated 1000px IME inset
(`Composer::set_bottom_inset`). Each was confirmed to fail without
its subject rather than assumed: dropping `animate(id)` from
`Selection::drag` -- the phone's own "fling does nothing" defect --
and starting the fling curve at the wall clock each fail only the
flick test; flinging on `Tapped` fails only the tap test; a 5s
`LONG_PRESS` fails only the selection test; a `set_bottom_inset` that
ignores its argument fails only the composer test.
**What still cannot be answered below layer 3**: nothing renders
here, so anything about pixels -- glyph rasterisation, the atlas,
stale or duplicated primitives, colour, the surface lifecycle, the
renderer rebuild -- is invisible to layer 1 and only *looked at* in
layer 2. Frame *times* are not measurable at either: layer 1 does no
GPU work at all and layer 2 runs a debug build on this VM's virtio
GPU, so a number from either is not the phone's. Anything JNI (the
IME, real insets, the clipboard, battery) is layer 3 by construction:
layer 1 records that the platform was asked and layer 2 has no
Android platform to ask.
2. **A phone-shaped desktop window under headless sway -- for looking.** 2. **A phone-shaped desktop window under headless sway -- for looking.**
`iris/run-headless.sh` already runs a winit binary under a private
sway and screenshots it with `grim`. Add a `--phone` mode (output and cd iris && ./run-headless.sh phone --phone --shot /tmp/p.png -- -p transcript-fixture
window at the phone's size and scale, with the scale reaching iris
the way Android's does so dp layout runs at that density) and About 15 seconds warm. `--phone` sets the private sway output to
touch-shaped mouse input: a left-button drag pans and flings through 1080x2424@120Hz and exports `IRIS_SCALE=2.55`, which reaches iris the
`DragArbiter`, long-press selects, no hover. One input path, not a way `DisplayMetrics.density` does on Android
parallel one (`CODE_RULES`). Colour, spacing, text and anything a (`iris::default::content_scale`) -- the desktop backend now lays out
person has to see is answered here. `swaymsg seat - cursor in physical pixels with a density instead of dividing into a separate
move/press/release` drives it when a gesture is needed on screen. logical space, so both platforms run one path. `transcript-fixture`'s
`phone` example opens the same screen from the same bytes as layer 1
and the Android bench.
A gesture on screen uses the *same recordings*:
./run-headless.sh phone --phone --replay transcript-fixture/touch/flick-120hz.touch \
--shot /tmp/p.png -- -p transcript-fixture
writes `/tmp/p-before.png` and `/tmp/p.png` either side of the flick;
looked at 2026-09-07, the list moved back about seven turns of the
fixture and settled.
**`swaymsg seat - cursor` cannot drive it, and that cost an hour.**
This compositor runs the headless backend with no input devices
(`WLR_LIBINPUT_NO_DEVICES=1`, `LIBSEAT_BACKEND=noop`): the cursor
commands all report `success` and nothing whatever reaches the
client, with `swaymsg -t get_seats` showing `capabilities: 0` as the
only sign. wlroots 0.19 dropped `WLR_HEADLESS_INPUTS`, and ydotool's
uinput device would be ignored by a compositor that is not reading
libinput. `iris/rig-input`'s `replay-touch` uses the
**virtual-pointer protocol** instead, which is a client protocol and
needs neither devices nor root, and it parses `iris::harness`'s own
`TouchScript`. Two traps inside it, both found by printing winit's
events: a button sent in the same frame as the motion that first puts
the pointer over the window is dropped (the client sees the enter,
the moves and the *release*, never the press), so the pointer is
positioned and left to settle 200ms first; and a leftover window from
an earlier manual run **tiles beside the new one**, halving the width
and producing a screenshot that looks exactly like a duplicated-
primitive rendering bug -- `swaymsg -t get_tree` and `pgrep -af
examples/phone` are the check.
3. **The Android emulator -- platform plumbing and the final pass.** 3. **The Android emulator -- platform plumbing and the final pass.**
JNI, IME, insets, surface lifecycle, the renderer rebuild, and one JNI, IME, insets, surface lifecycle, the renderer rebuild, and one
verification run before a build goes to the phone. Not for iterating verification run before a build goes to the phone. Not for iterating
on layout. on layout.
Pass condition for the rig: `cargo test` runs a fixture-backed headless
transcript screen with a replayed flick and asserts a nonzero release
velocity and a moved scroll offset; one command opens the same screen in
a phone-shaped window and screenshots it. Record the commands here when
it lands.
### The 22:16 phone report, worked 2026-09-06/07 ### The 22:16 phone report, worked 2026-09-06/07
Iris's four items are listed in docs/IRIS_TODO.md's "From the phone, Iris's four items are listed in docs/IRIS_TODO.md's "From the phone,