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 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-16 21:50:22 -04:00
1 parent da2f93254e
commit 9f825ed6fb
1 file changed
+71 -18
+71 -18
View File
@@ -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