diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index a5fe256..7c77f74 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -8,7 +8,7 @@ log. 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 **`45a7176`**, eighty-one commits, pushed. Built-in alignment is +head is **`5f16617`**, eighty-two commits, pushed. Built-in alignment is complete there; see "Built-in alignment" below for the retained-layout details. No PR reviews were present when checked on 2026-09-15. @@ -167,7 +167,7 @@ in, and it is compiled out of the release runs above. ## Verification -**At `45a7176`**, the current head: +**At `5f16617`**, the current head: - `cargo fmt --all --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`: sixteen targets green, 83 suite @@ -768,20 +768,8 @@ pixels and `Holds`. Decided with Bryan on 2026-09-15. **One half step further is arithmetically available and deliberately not taken.** The `Holds` assertion is quiet at one, and the whole-of-a-box case becomes an exact identity. What stops it is shrinker seed 220 on - `reorder`, which then lays out differently warm than cold: too narrow a - range is supposed to cost a redraw and nothing else, and there it - re-breaks a wrapping text, whose reported width moves a `Branch` onto - its other subtree. **That is the next thing to chase** -- it is the - unsettled-text family rather than a rounding question, it reduces to ten - widgets, and closing it is what lets this go lower: - - ```sh - SHRINK_SEED=220 SHRINK_DEPTH=5 SHRINK_CASE=reorder \ - cargo test --release --test shrink -- --ignored --nocapture - ``` - - The tree is `Scroll(X) { Pad as a region node { Span(X) { Branch, - Span(X){ wrapped text, one-line text } } } }`. + `reorder` -- diagnosed on 2026-09-16 and written up under "The offer + chain breaks at a region node" below. It is not a rounding question. - **The multiply on the way in** gets one more step at the top of the range and nothing at the bottom, since truncation only ever drops. The whole of a box has no multiply in it, however many pixels are added to it, and @@ -800,6 +788,65 @@ pixels and `Holds`. Decided with Bryan on 2026-09-15. as floats and are put on the grid where they arrive. `Vec2` stays what the GPU and the platform speak; `PxVec2` is what layout decides in. +## The offer chain breaks at a region node + +**The open defect at `5f16617`**, and the reason `Holds::through` keeps a +half step it does not need. It is the fourth thing this session to come down +to the offer against the placed box, after `aea878d`, `d8ae9c3` and the +`Stack`/`Pad` overrides in `2bc6bdf`. + +`offered_region` composes a widget's offer through its ancestors' offers, +which is the invariant. Where the parent is a **region node** it does not: +it falls back to `UiRegion::FULL`, and `redraw` then resolves that against +the node's slot entry -- which holds the node's **placed** box. So a widget +under a region node is re-asked in the box its parent *placed* that node in, +not the box its parent *offered* it. + +That is only wrong where the two differ, and `Scroll` is where they do: it +offers its content the viewport and places it in a box as long as the +content. So the offer becomes a function of the answer being re-derived, and +the old answer confirms itself. + +Reproduced from the shrinker's ten-widget reduction: + + Scroll(X) { Pad as a region node { Span(X) { + Branch(threshold 213), Span(X){ wrapped text, one-line text } } } } + +```sh +SHRINK_SEED=220 SHRINK_DEPTH=5 SHRINK_CASE=reorder \ + cargo test --release --test shrink -- --ignored --nocapture +``` + +Traced, at a 900x1200 window, with `reorder` rotating both spans: + +- Each order **does** settle, and to a different answer: `[0,1]` gives the + pad a 2080.18px box and `[1,0]` gives it 1789.90. So this is not an + unsettled tree; both are fixed points. +- Cold, rotated: the texts are asked at 900 and 594.06, shape, and report + 305.94 and 583.97, so their span reports 889.90. +- Warm, rotated: the outer span is re-asked in **2080.18**, the pre-rotation + content box. Its texts are asked at 305.94 and 977.42, both reuse retained + answers without reshaping, and the span reports 1283.36 -- the same as + before the rotation, so no answer changed and nothing propagated up to the + `Scroll`. Widget 0 lands 290.27px out and the `Branch` picks its other + subtree. +- Marking every widget dirty converges warm onto cold, which is what says + the retained path and not the arithmetic is at fault. + +**What makes the fix awkward** is coordinate frames, and it is worth knowing +before starting. `ActiveData::offer` is in the parent *widget's* coordinates +and `ActiveData::region` is in the parent *node's*, so "was this node placed +where it was offered" is not a comparison either field can answer alone. Two +shapes look plausible: + +- Compose offers through a region node by expressing the node's offer as a + part of its placed box -- an inverse composition, with a rounding of its + own. +- Let `offered_region` say it cannot answer, and have `redraw` escalate to + the parent the way `parent_must_place` does. Correct and small, but it + costs region nodes their point for partial repaint unless the "placed + where offered" case is detected, which is the frame problem again. + ## The leftover boundary Fixed in `5ed9e87`, which closed the shrinker's `reorder` case. Neither of its @@ -1011,8 +1058,14 @@ Queued from this work, in order: allowance went from three half steps to two, and the whole of a box now maps back to one step rather than one step per level of nesting. - **And it was free.** 1,880M instructions and 755M cycles against 1,908M - and 760M, medians of 25 with all twenty-five work counters identical. + **And it made layout faster.** 1,840M instructions and 735M cycles against + 1,908M and 760M -- 3.6% and 3.3% -- medians of 25 with all twenty-five + work counters identical. `5f16617` is where the win is: a draw already + descends past every move entry on its way in, so `DrawInfo` carries what + the slot composes to and a widget's own box is a select rather than a walk + back up a mean of 2.8 levels, eight hundred times a frame. Composing on + the fine grid alone cost 1,880M and 755M, so the widening was paid for + twice over by not doing the walk. Two things paid for the widening and are the reason to keep them: a *length* composes on its own in two multiplies a level rather than the four both ends cost, since where the parent sits falls out of the