From cb1cfff0bf7625c54fd9fa00b776fa4436b247fe Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Mon, 14 Sep 2026 15:58:10 -0400 Subject: [PATCH] Record retained layout optimization results --- docs/IRIS_EXTRACTION_HANDOFF.md | 81 +++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/docs/IRIS_EXTRACTION_HANDOFF.md b/docs/IRIS_EXTRACTION_HANDOFF.md index 2fd6c68..13ae22f 100644 --- a/docs/IRIS_EXTRACTION_HANDOFF.md +++ b/docs/IRIS_EXTRACTION_HANDOFF.md @@ -9,8 +9,8 @@ Canonical `main` is **`ca2b4b2`** (#17, the headless rig). Sixteen slices are in. **#18 `split/18-position-chain`** is open and finished apart from one decision: -worktree `/home/bob/repos/iris-pr18`, head `cdec293`, thirteen commits, 50 -tests passing, fmt and clippy clean. It is LAYOUT.md §2's position chain, generalised +worktree `/home/bob/repos/iris-pr18`, head `84f589e`, fifteen commits, +workspace tests passing, fmt and clippy clean. It is LAYOUT.md §2's position chain, generalised to boxes. `/home/bob/repos/ai-app-2` is on `rustify`, worktree clean. **The one thing waiting on the owner.** `tabs` at 1920x1200 is no longer @@ -137,30 +137,51 @@ not in the repository, because the counters it reads are patched into **The GPU is not the subject.** The pass is a tenth of a millisecond at every revision and every phase; all of this is the CPU laying out. -Three separate costs, in the order they are worth fixing: +`a640c6c` and `84f589e` remove most of the CPU work without weakening the +retained-layout rules. Against the same seed-1/depth-7 load, widget draws are +now 2,117 cold, 1 for a leaf repaint, 11 for a scroll, and 3,139 for a resize +(from 2,755, 1,313, 1,313, and 5,512 respectively). Workspace tests pass; the +five reference renders, resize render, and image-tab replay are byte-identical +to the prior #18 head. + +The generated tree now includes `Aligned` with every meaningful per-axis +alignment. That exposed two retained-layout ordering bugs which `84f589e` +fixes. The regular cold-layout equivalence suite passes. The ignored 100-seed +sweep passed every transition through seed 59 and now gets past the former +scroll failure at seed 52; at seed 60 it reaches the already-documented +iterative wrapping-text defect, differing by 0.00003 px after `SwapForThree`. + +The branches are invariants rather than widget exceptions: + +- A dirty widget draws locally first only while its retained box has the same + pixel size. If its returned `Size` is unchanged, no size reader can observe + the repaint and no ancestor draws. If the size changed, the dependent path + lays out. A changed pixel box takes the conservative path first, which is the + condition the discarded `/tmp/escalate.patch` missed on resize. +- Dirty widgets settle deepest-first. A changed size queues only its immediate + reader; propagation stops as soon as a reader's own answer stays unchanged. + During an output resize, the resize condition stays live until these updates + finish, so an `Aligned` ancestor chooses the new child box before a + pixel-dependent descendant draws in it. +- A span measures an unknown child in the part of its axis still available, + rather than giving every child the whole container and immediately taking + most of it away. This is the archive's faster shape, recreated on the current + types; it often makes the measurement box the final box without caching an + answer under different constraints. + +A proposed `Scroll` shortcut that reused its direct child's retained size was +discarded. “Direct child is clean” is not strong enough while a dirty +descendant's structural change is still propagating; seed 52 demonstrated the +stale-size failure. Re-measuring the scroll subtree costs 11 draws rather than +1, but remains two orders of magnitude below the old 1,313 and follows the +actual dependency invariant. + +The remaining costs are: -- **A repaint escalates to the top size reader and redraws its whole - subtree.** `redraw` asks `mark_readers` first, so a widget whose drawing - changed but whose *size* did not still redraws everything from the highest - widget that ever read it. At depth 8 a one-rectangle repaint costs 4825 - draws and 24 ms, which is more than building the tree from nothing. The - measured alternative is to draw the widget in its own box first, compare - the `Size` it returns with the one it had, and escalate only when it - differs: repaint falls to 1 draw and 0.000 ms, a scroll to 11 draws, and a - resize to 532 draws and 2.3 ms -- below the pre-#16 numbers on every line. - **It is not correct as written**: the resize case in `tests/generated.rs` - fails at seed 1 with four widgets misplaced, because drawing the widget - clears the mark that was the only thing stopping a later parent draw from - reusing it into a different box. The patch is kept at - `/tmp/escalate.patch`. This is the owner's call, since it changes when a - widget is drawn. - **A container measures a child by drawing it in a box it will not keep.** - `Span::draw` places each child without an exact `size_hint` at - `UiRegion::FULL` to read its length, then places it again at the box it - worked out, and the second place redraws whatever the first drew. It - compounds with nesting: draws per drawn widget are 1.3x at depth 4, 3.3x at - 7 and 5.2x at 8. Wrapping text is shaped at the trial width and then again - at the real one -- 332 shapes for 152 text widgets on a cold frame. + The remaining-region trial removes many mismatches, but an unknown child's + measured length can still make its final box differ, and nested containers + compound those redraws. This is now the largest CPU layout cost. - **Redundant draws rewrite primitives.** A cold frame writes 48,050 instances for a tree that holds 10,872. What reaches the GPU is 10,872, since a layer uploads whole, so this is CPU cost only -- but a one-leaf @@ -169,8 +190,8 @@ Three separate costs, in the order they are worth fixing: ## The random trees `iris::random` grows a seeded tree -- spans in every direction holding two to -four children, stacks, scrolls on either axis, padding with each of its four -sides its own number, +four children, stacks, scrolls on either axis, alignment on either axis, +padding with each of its four sides its own number, rects with varying opacity, text both wrapping and overflowing, a declared size over half of it, stopping at a depth. `examples/random.rs` draws one (`IRIS_SEED`, `IRIS_DEPTH`). `tests/generated.rs` grows each seed twice -- once @@ -211,7 +232,7 @@ cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace ``` -49 tests pass on #18's head. `--workspace` matters: `rig-input` is a crate of +50 tests pass on #18's head. `--workspace` matters: `rig-input` is a crate of its own. Render checks are the last pass, not the iteration loop -- the owner asked for @@ -329,7 +350,11 @@ Then, roughly in dependency order: centred content, which otherwise has to say `Redraw` because only its own draw knows where the middle was. Size is the harder half: a declared size beside the one `draw` returns is two sources of truth for one thing, so - settle what each means before building it. + settle what each means before building it. This is independent of #18's + retained-update fix now that `Aligned` is in its generated coverage; land + #18 first, then design built-in alignment and size together as the next + structural slice rather than mixing that representation change into this + performance correction. - **`OnResize::Translate`, which still does nothing.** The chain removed half its obstacle: a placed widget's slot holds the box it was offered while its drawing is a set of fractions of that box, so the two are no longer one