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.
Scrollmust 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 --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo test --workspace: 74 passed, 8 ignoredcargo 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-cmpatca2b4b2:tabs,view,minimal, andtextat 1920x1200;tabscold 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
UiRegionin the coordinates of the slot it names.UiRegion::FULLis the identity. Slots are opt-in:Painter::placegives a child a slot;widgetandwidget_withinshare the nearest ancestor's. - A widget's
ActiveData::regionis where it was placed in its parent's slot. A placed widget draws inFULL; 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
Holdsinterval 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. Painterrecords 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_underprevents 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-
leftoverlengths 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
TextViewreports that interval throughPainter::holds. Span's decision to distributeleftoveris a pixel question. Its validity interval is split at the length where fixed parts fill the box; pureleftoverchildren are undrawn on the no-space side.Scrollreports 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:
UiRenderStatebehindRc<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.