diff --git a/docs/IRIS_EXTRACTION_HANDOFF.md b/docs/IRIS_EXTRACTION_HANDOFF.md index 34906bb..662e627 100644 --- a/docs/IRIS_EXTRACTION_HANDOFF.md +++ b/docs/IRIS_EXTRACTION_HANDOFF.md @@ -9,15 +9,20 @@ 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 `3f7cd82`, twenty-four commits, +worktree `/home/bob/repos/iris-pr18`, head `f61e893`, thirty-four commits, workspace tests passing, fmt and clippy clean. It is LAYOUT.md ยง2's position -chain, generalised to boxes. The last six are a performance pass that changes -no layout: the two halves of the text fix (`e5f8b6b`, `2525637`), three -retained-layout details that claimed something they did not do (`f1a47e9`), -the many-widgets-dirty cases (`77bb75e`), what the settle order is holding up -(`bf94380`), and carrying a widget's depth rather than walking for it -(`3f7cd82`). Every one of them keeps the five reference renders and the resize -render byte-identical, and the hundred-seed sweep passes. `/home/bob/repos/ai-app-2` is on `rustify`, worktree clean. +chain, generalised to boxes. After the performance pass (`e5f8b6b` to +`3f7cd82`) came the fuzzers and what they found: the shrinking fuzzer +(`b7caab3`, `386a0d1`), `SetSize` measuring in its declared length +(`c596bf1`), text re-breaking across the interval a break holds for +(`9913194`), and a dirty widget measured where its parent asked rather than +in a box its own answer decided (`02ff8c7`). `f61e893` restores `abs()` in +the rect shader, which `7c50a3e`'s rename had taken with it so that every +window failed shader validation while `cargo test` stayed green, and adds +the device-free shader validation test that would have caught it. The five +reference renders and the resize render are byte-identical across all of +it, and the hundred-seed sweep passes. `/home/bob/repos/ai-app-2` is on +`rustify`, worktree clean. **The one thing waiting on the owner.** `tabs` at 1920x1200 is no longer byte-identical to `upstream/main`: 1,283 pixels of 2.3M (0.06%), two @@ -70,7 +75,7 @@ Invariants, not history. Everything in `core/src/ui` rests on them. tree depth, which is the difference between free and +42.6%. - **A widget's region is held in the coordinates of the slot it draws in**, so a placed widget draws against `UiRegion::FULL` and its box lives in its slot. - `ActiveData::region` is the box it was *offered*, in its parent's slot + `ActiveData::region` is the box it was *placed in*, in its parent's slot coordinates, and `ActiveData::parent_move` is the slot that is in; `window_region` composes the one through the other, which is the walk the shader does. @@ -273,10 +278,14 @@ aligned size-changing chain. 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. + pixel size **and that box is a constraint rather than its own answer**. 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. A box decided from the + widget's own size, on an axis that size reads, takes it too: measuring there + can only repeat the answer, so the reader that decided it draws instead -- + see "a box that is its own answer" below. - 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. Output resize is the distinct invariant: all pixel-dependent leaves and @@ -537,9 +546,9 @@ have taken another word, so at any width down to the longest of them the same break holds. `TextBuffer::shape` answers across that interval, with a sub-pixel tolerance for the edge. -**What is left is not a stale drawing -- the layout has two answers.** A span -measures its children in its own box, and its own box is what its parent gave -it, from the size it reported, from those children. Four widgets: +**A box that is its own answer.** A span measures its children in its own +box, and its own box is what its parent gave it, from the size it reported, +from those children. Four widgets: Aligned(mid, -, Span[ Text(wrap), OneLine ]) @@ -550,12 +559,42 @@ the span is offered the window, the text is asked for 334.06 and answers 318.45, and the span settles at 624.38. **Both are stable.** Which one you get depends on what the tree was before. -So no rule about when to keep a drawing can fix it, and several were tried. -`Painter::settle` -- place a child into its own reported size without measuring -it there -- passes all four cases in `tests/unsettled.rs` and fails two in -`generated.rs`, in `Aligned` alone, in `Span` alone, with the child force-drawn -first or not. The fix wants the constraint a container measures under to be -something it is *given* rather than something it *ends up with*. +No rule about when to keep a drawing can fix that, and several were tried +(`Painter::settle` -- place a child into its own reported size without +measuring it there -- passed the hand-written cases and failed two generated +ones). The defect is in *where a dirty widget is measured*: a local redraw +draws it in the box it was placed in, and when a reader decided that box from +the widget's answer, the old answer is a fixed point of measuring there +whatever the content now says. So `ActiveData::offered_px` keeps the pixel +size of the box the parent *first* asked about the child in -- `known_len` or +the first `place` of a draw -- beside `px`, the box it drew against. A dirty +widget whose size reads an axis on which some reader up its chain gave what +it read a box other than the one it asked in is not drawn locally: the chain +is marked and the parent of the highest such placement draws, since above it +every box is a constraint. The walk has to go up the whole reader chain rather +than one edge, because a pass-through hands a derived box down unchanged: the +second shrunk case is a `Scroll` placing a `SetSize` in a box the content +decided, and the span under it placed once, in that box. + +That made `Scroll` show a second thing: it read its box's pixel length for +the clamp through `px_len`, which records the reported size as depending on +the box, and it does not -- a `Scroll`'s size is its content's. Every scroll +tick was then a size question asked in a derived box, at 34x the +instructions. `Painter::px_len_for_draw` is the read that records nothing, +for a draw whose reported size does not follow from it. Instructions per +frame on the depth-8 rig, 1000 frames, against the head before this: + +| | before | after | +| --- | --- | --- | +| `many`, 32 dirty | 0.66M | 0.74M | +| `many`, 130 dirty | 27.7M | 26.5M | +| `resize` | 15.8M | 15.0M | +| `scroll`, `repaint`, `size` | 0.36M | 0.36M | + +The small load pays 12% for the escalations that are now required; the +larger ones get cheaper because a `Scroll`'s retained size no longer goes +invalid when its box changes. The reorder fuzzer passes 200 trees at depth 7 +in every case, and the sweep passes. `OrthoSize::{Fill, Children}` -- a span choosing whether to fill across its axis or report its longest child -- is written and parked for the same reason: