From 9f825ed6fbd7c85b7147b2f8a1724e676c839cc1 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Wed, 16 Sep 2026 21:50:22 -0400 Subject: [PATCH] Diagnose seed 220: the offer chain breaks at a region node `5f16617` carries the composed box down the draw instead of walking back up for it, which made exact composition a 3.6%/3.3% speedup rather than a cost. The numbers are in "Performance" and the i64 item. And seed 220 is diagnosed rather than left as a seed. `offered_region` composes a widget's offer through its ancestors' offers except where the parent is a region node, where it falls back to the node's **placed** box. `Scroll` is where placed and offered differ -- 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. It is the fourth defect this session to come down to the offer against the placed box. The write-up has the trace both ways, the fact that each child order settles to its own fixed point so the tree is not unsettled, that marking everything dirty converges warm onto cold, and the coordinate-frame problem that makes the two plausible fixes awkward -- `offer` is in the parent widget's frame and `region` in the parent node's, so neither answers "was this placed where it was offered" alone. Co-Authored-By: Claude Opus 5 --- docs/HANDOFF.md | 89 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 18 deletions(-) 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