Files
ai-app/docs/HANDOFF.md
T

8.7 KiB

Handoff

Where the work in flight stands for a session picking it up cold. Keep current invariants, measurements, and failed hypotheses here; this is not a decisions log.

Where things stand

Canonical Iris main is ca2b4b2 (#17, the headless rig). #18 split/18-position-chain is open in /home/bob/repos/iris-pr18; its local head is 29c7881, forty-five commits, pushed to the fork. No PR reviews were present when checked on 2026-09-15; the verification summary is posted on the PR.

The current head completes LAYOUT.md §2's position chain and the requested leftover behavior. A child whose length is only leftover is not drawn when nothing is left. A child that also asks for pixels or a relative fraction keeps that part and overflows as before.

The last commit replaces the parallel resize rules with one retained-layout contract, Holds: the interval of box lengths for which a widget's drawing and reported size stay valid. Reading Painter::px_len or px_size narrows the interval to the length read; Painter::holds lets the widget widen it. Parent validity is the intersection of the ranges its children induce. This contract is trusted. A widget that declares an incorrect range is a defective widget; Iris does not add defensive work to recover optimizations from a false declaration.

The implementation also fixes three counterexamples found while finishing the rewrite:

  • An asked-but-undrawn size dependency must name the widget that asked as its parent. Using the asker's parent skipped a reader and made generated seed 10 settle differently warm and cold.
  • Scroll must return the answer from the first box it asked about, whether that answer came from a retained length or a fresh measurement. Returning the final placed answer only on the retained path advanced one fixed-point iteration and broke seed 86.
  • A widget retains the layer it was entered on, not the last child layer its painter visited. The old value drifted on local redraw and put a redrawn tab background above its retained text.

core/src/ui/holds.rs, the retained tests, and the generated cold-layout oracle pin those rules. Seeds 10 and 86 are now in the ordinary generated set.

Verification at 29c7881

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace: 74 passed, 8 ignored
  • cargo test --release --test generated -- --ignored a_long_run_of_seeds_agrees: 100 seeds passed in 65.63 s
  • Reference renders against /home/bob/repos/iris-main-cmp at ca2b4b2: tabs, view, minimal, and text at 1920x1200; tabs cold at 900x1200; live resize from 1920x1200 to 900x1200; and the tab interaction before and after replay. Every comparison had zero differing pixels. The live-resize image is also byte-identical to the cold 900x1200 image.

The final pre-submit review was run in four passes. It caught the retained layer defect above, corrected validity-range inversion for negative relative extents, and removed an impossible-state unwrap from Scroll.

Performance

The retained rewrite was compared with #18's previous head 691e3eb using the same release layout_diagnostics fixture, seed 1, depth 8, 500 frames, 130 dirty widgets:

phase 691e3eb 29c7881
many 25.17M instructions/frame 6.14M
resize 16.08M 8.39M
scroll 0.720M 0.691M
repaint 0.720M 0.689M
size 0.730M 0.698M

The final 29c7881 measurements were 3,070,693,965 instructions for 500 many frames and 4,192,804,214 for 500 resize frames. Re-run before quoting tighter figures.

Retained-layout invariants

  • A slot holds a whole UiRegion in the coordinates of the slot it names. UiRegion::FULL is the identity. Slots are opt-in: Painter::place gives a child a slot; widget and widget_within share the nearest ancestor's.
  • A widget's ActiveData::region is where it was placed in its parent's slot. A placed widget draws in FULL; its box lives in its slot. This keeps chains at positioning-container depth rather than full widget-tree depth.
  • The first box a parent asks about is the offer. A later box chosen from the child's answer is a placement, not another independent answer. Dirty widgets are re-asked at the offer and only then placed again.
  • A retained drawing can be reused only when its Holds interval contains the new pixel box on both axes, its parent slot is unchanged, and the widget is clean. A moved unslotted widget must draw again because it has no slot to rewrite.
  • Painter records size-dependency edges only when a parent reads a child's size or hint. An undrawn measured child remains recorded so a later change reaches the parent that decided whether to draw it.
  • Dirty widgets settle deepest-first. dirty_size_under prevents a reader from taking a retained answer while something below that answer is still dirty; the walk is an optimization against laying out twice, not a second validity mechanism.
  • Declared non-leftover lengths are resolved by the widget's parent where the widget is drawn. A declared-length change therefore redraws the parent.
  • Pixel comparison uses a 0.05 physical-pixel tolerance, against the last actual layout. Repeated subpixel changes accumulate and eventually redraw.
  • Text shaping is retained separately from line breaking. A greedy line break remains valid from its longest produced line through the width at which it was made, and TextView reports that interval through Painter::holds.
  • Span's decision to distribute leftover is a pixel question. Its validity interval is split at the length where fixed parts fill the box; pure leftover children are undrawn on the no-space side.
  • Scroll reports its content's first measured size. Its drawing can survive container-length changes only over the interval in which clamping and its current offset do not change.

Rigs and reproduction

Ordinary framework verification:

cd /home/bob/repos/iris-pr18
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace

Run the long generated oracle only after ordinary tests pass:

cargo test --release --test generated -- --ignored a_long_run_of_seeds_agrees

tests/generated.rs compares a warm incremental tree with a cold tree of the same state. IRIS_GENERATED_SEED, IRIS_GENERATED_SEEDS, and IRIS_GENERATED_DEPTH select failures. tests/shrink.rs reduces a failing tree; use it to turn a seed into a readable regression rather than leaving the seed as the only record.

The headless reference set must be run one process at a time because the rig reuses one compositor. Comparison worktrees need separate target directories. Useful commands:

./scripts/run-headless.sh tabs --mode 1920x1200@60Hz --shot /tmp/tabs.png
./scripts/run-headless.sh tabs --mode 900x1200@60Hz --shot /tmp/cold.png
./scripts/run-headless.sh tabs --mode 1920x1200@60Hz \
  --resize 900x1200@60Hz --shot /tmp/resized.png
./scripts/run-headless.sh tabs --mode 1920x1200@60Hz \
  --replay /tmp/tabs.touch --shot /tmp/replay.png

The replay used for the final check was:

0 down 1728 24
80 up 1728 24
400 down 1836 1116
480 up 1836 1116
800 down 1836 1116
880 up 1836 1116

tests/layout_diagnostics.rs is the retained CPU rig. Select cold, many, repaint, size, scroll, or resize with IRIS_PHASE; use the feature for explanatory counters and an uninstrumented release binary under perf for instruction totals.

Next

Let PR #18 review. The next small LAYOUT.md §2 item is LazySpan; set_child_offset is no longer separate, because placing a child is Painter::place.

After #18 lands, the next structural design is built-in alignment and size. Today a declared size and the Size returned by draw are two sources of truth, and alignment can disappear behind a wrapper. Agree their ownership before implementing it. Do not restore OnResize::Translate; retained translation is now expressed by the same Holds contract and box chain.

Other queued work, in dependency order:

  • UiRenderState behind Rc<RefCell<_>>.
  • Split Len/layout length and add density-independent pixels.
  • Input restructuring: pointer capture, drag slop and axis, cancellation, mask-aware hit testing, and timestamps.
  • Retained paints, selection, overlays, and shared runtime state.
  • Generic desktop/Android hosts and reusable example/APK tooling.
  • Application-owned fonts and replaceable glyph-atlas buckets.
  • Positioned text overflow and cluster-safe ellipsis.

The archive is a reference, not a patch: it predates returned Size, the current box chain, and the current length types. Recreate changes on current types and keep app/session concepts out of Iris.