Record the pixel path closing, and the mask a redraw inherits
A box in pixels is threaded down the draw now rather than composed back up the move chain, which closes the offer chain breaking at a region node, makes `Holds::through` an exact preimage, and retires `redraw`'s third ask. The section that diagnosed the defect becomes the one that states the rule, with what the plan got wrong recorded beside it: `px` cannot be stored on `ActiveData`, because a resize that every `Holds` admits redraws nothing and would leave it stale everywhere. `AGREE_STEPS` stays 2, measured rather than assumed: one step passes the 100-seed oracle and fails the 400-seed shrinker on `resize-size` by 0.002 px, so the second step belongs to the resize path re-expressing a part as a fraction of a box that changed length. The review of that change found an older defect of its own -- a `Masked` widget settled on its own panicked, since `redraw` handed it its own mask as the one it inherited -- and the invariant that closes it is here too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9a26862bce
commit
4e75019b9a
1 file changed
+247
-187
+247
-187
@@ -7,8 +7,8 @@ log.
|
|||||||
## Where things stand
|
## Where things stand
|
||||||
|
|
||||||
Canonical Iris `main` is **`ca2b4b2`** (#17, the headless rig). **#18
|
Canonical Iris `main` is **`ca2b4b2`** (#17, the headless rig). **#18
|
||||||
`split/18-position-chain`** is open in `/home/bob/repos/iris-pr18`; its local
|
`split/18-position-chain`** is open in `/home/bob/repos/iris-pr18`; its head
|
||||||
head is **`5f16617`**, eighty-two commits, pushed. Built-in alignment is
|
is **`ea6dbae`**, eighty-five commits, pushed. Built-in alignment is
|
||||||
complete there; see "Built-in alignment" below for the retained-layout
|
complete there; see "Built-in alignment" below for the retained-layout
|
||||||
details. No PR reviews were present when checked on 2026-09-15.
|
details. No PR reviews were present when checked on 2026-09-15.
|
||||||
|
|
||||||
@@ -18,6 +18,22 @@ widget that asked in *that widget's* frame, and the window is no longer a
|
|||||||
move entry -- see "A report is a fraction of the box it was given" and the
|
move entry -- see "A report is a fraction of the box it was given" and the
|
||||||
root item under "Fixed point". `Holds::through` is untouched by either.
|
root item under "Fixed point". `Holds::through` is untouched by either.
|
||||||
|
|
||||||
|
**And then the open defect closed**, in `32542d0` (2026-09-17). A box in
|
||||||
|
pixels is no longer composed back up the move chain at all: it is threaded
|
||||||
|
down the draw one multiply a level, and `UiRenderState::asked_px` walks the
|
||||||
|
same steps back up where a local redraw starts part-way down the tree. That
|
||||||
|
deletes `wide.rs`, `Moves::compose`, `Moves::size_of`, `px_of`, `px_region`,
|
||||||
|
`offered_region`, `slot_wide` and `redraw`'s third ask, makes
|
||||||
|
`Holds::through` the exact preimage of the map it inverts, and fixes the
|
||||||
|
offer chain under a region node -- 252 lines of `core/` net, free on cold
|
||||||
|
layout and 9-13% off the retained paths. It is its own section: "A box in
|
||||||
|
pixels is one multiply from its parent's".
|
||||||
|
|
||||||
|
**`ea6dbae` is a defect the review of that one found**, unrelated to it and
|
||||||
|
older: `redraw` handed a widget its own mask back as the mask it inherited,
|
||||||
|
so a `Masked` widget settled on its own panicked. See the mask item under
|
||||||
|
"Retained-layout invariants".
|
||||||
|
|
||||||
The current head completes LAYOUT.md §2's position chain and the requested
|
The current head completes LAYOUT.md §2's position chain and the requested
|
||||||
`leftover` behavior. A child whose length is only `leftover` is not drawn when
|
`leftover` behavior. A child whose length is only `leftover` is not drawn when
|
||||||
nothing is left. A child that also asks for pixels or a relative fraction keeps
|
nothing is left. A child that also asks for pixels or a relative fraction keeps
|
||||||
@@ -109,17 +125,23 @@ The tree, five widgets, now running in `tests/cases/unsettled.rs` as
|
|||||||
|
|
||||||
Scroll(Y) { Span(DOWN) { Scroll(X){ text } as a region node, rect 87x24 } }
|
Scroll(Y) { Span(DOWN) { Scroll(X){ text } as a region node, rect 87x24 } }
|
||||||
|
|
||||||
`UiRenderState::redraw` asks a dirty widget in the box its parent asked it in
|
`UiRenderState::redraw` asked a dirty widget in the box its parent had
|
||||||
and then again in the box its parent chose from that answer, and it skipped
|
*offered* it and then again in the box its parent chose from that answer, and
|
||||||
that second ask whenever the two were the same **length**. They are not the
|
it skipped that second ask whenever the two were the same **length**. They
|
||||||
same box: the outer scroll offers its whole viewport and places the span 24px
|
are not the same box: the outer scroll offers its whole viewport and places
|
||||||
above it, snapping to the end of a content 24px longer than its box, so the
|
the span 24px above it, snapping to the end of a content 24px longer than its
|
||||||
offer was as long as the final box and 24px below it. A region node writes
|
box, so the offer was as long as the final box and 24px below it. A region
|
||||||
the box it drew in into its own move entry, so the inner scroll stayed where
|
node writes the box it drew in into its own move entry, so the inner scroll
|
||||||
the offer put it. `d3b0ebf` had already compared whole boxes for
|
stayed where the offer put it. `d3b0ebf` had already compared whole boxes for
|
||||||
`parent_must_place` and left this one a length comparison -- one decision with
|
`parent_must_place` and left this one a length comparison -- one decision with
|
||||||
its two halves disagreeing.
|
its two halves disagreeing.
|
||||||
|
|
||||||
|
**The pair of asks it was a disagreement between is gone** (2026-09-17): a
|
||||||
|
local redraw asks in the box its parent *gave*, which is where the widget
|
||||||
|
already sits, and hands anything else back to the parent. See "A box in
|
||||||
|
pixels is one multiply from its parent's". The test stays, and the lesson
|
||||||
|
above it does.
|
||||||
|
|
||||||
**The rigs.** `98d4e98` splits `iris::random::grow` into `plan(seed, depth,
|
**The rigs.** `98d4e98` splits `iris::random::grow` into `plan(seed, depth,
|
||||||
&edits)` and `build(rsc, &plan)`. A seed cannot be made smaller, which is why
|
&edits)` and `build(rsc, &plan)`. A seed cannot be made smaller, which is why
|
||||||
a failure the oracle found could never be handed to the shrinker; a `Plan`
|
a failure the oracle found could never be handed to the shrinker; a `Plan`
|
||||||
@@ -141,7 +163,9 @@ the box its parent chose from the widget's answer, handed that box over as if
|
|||||||
it were an offer, so `draw_inner` ran `placed_box` on an already-placed box
|
it were an offer, so `draw_inner` ran `placed_box` on an already-placed box
|
||||||
and applied the widget's own alignment to it twice. That only showed where
|
and applied the widget's own alignment to it twice. That only showed where
|
||||||
the alignment was the widget's own to apply, which is why it stayed hidden
|
the alignment was the widget's own to apply, which is why it stayed hidden
|
||||||
while `Stack`, `Pad` and `Scroll` overrode every child.
|
while `Stack`, `Pad` and `Scroll` overrode every child. (That ask no longer
|
||||||
|
exists at all, which is the same defect made unwritable rather than fixed
|
||||||
|
twice.)
|
||||||
|
|
||||||
Closed by it: shrinker seed 288 on `region-node` (11 widgets from 42, a
|
Closed by it: shrinker seed 288 on `region-node` (11 widgets from 42, a
|
||||||
`Text` inset 8.8px at each end) and oracle seed 326 at depth 6 (43 from 205,
|
`Text` inset 8.8px at each end) and oracle seed 326 at depth 6 (43 from 205,
|
||||||
@@ -173,7 +197,37 @@ in, and it is compiled out of the release runs above.
|
|||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
**At `5f16617`**, the current head:
|
**At `32542d0`**, a box in pixels threaded down the draw (2026-09-17):
|
||||||
|
|
||||||
|
- `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --
|
||||||
|
-D warnings`, `cargo test --workspace`: green, 86 suite tests, 18 core
|
||||||
|
unit tests, 11 generated cases.
|
||||||
|
- The release oracle at 100 seeds in 14.4 s, and **120 seeds in debug** in
|
||||||
|
59 s -- the debug run is the one that exercises the `Holds` assertion in
|
||||||
|
`draw_at`, and the point of an exact range is that a too-narrow one fires
|
||||||
|
it.
|
||||||
|
- All fifteen shrinker cases at 400 seeds of depth 5 in 58 s, and at 1000
|
||||||
|
seeds of depth 6 in 147 s.
|
||||||
|
- Shrinker seed 220 on `reorder`, the defect this closes, and it is a test
|
||||||
|
now:
|
||||||
|
`unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`
|
||||||
|
fails without the fix and passes with it. **The old offer chain with the
|
||||||
|
old allowance also passes seed 220** -- it is the exact `Holds::through`
|
||||||
|
that exposes it -- so the test was checked against that pair rather than
|
||||||
|
against the old head alone.
|
||||||
|
- `view`, `minimal`, `random`, `tabs` and `text` render byte-identical at
|
||||||
|
1920x1200 against `5b78002`, and so does the `tabs` touch replay, before
|
||||||
|
and after the gesture. `random` live-resized from 1920x1200 to 1280x800 is
|
||||||
|
byte-identical both to the old head's resize and to a cold 1280x800 render
|
||||||
|
(`5a11a464...`).
|
||||||
|
- Twenty-five rig work counters identical on the `cold` and `resize` phases,
|
||||||
|
which is what makes those two rows of the table under "Performance" a
|
||||||
|
measurement rather than a bound.
|
||||||
|
**At `ea6dbae`**, the mask fix on top of it: the suite again at 87 tests, the
|
||||||
|
core unit tests, the release oracle at 100 seeds, the fifteen shrinker cases
|
||||||
|
at 400 seeds of depth 5, and `tabs` byte-identical at 1920x1200.
|
||||||
|
|
||||||
|
**At `5f16617`**:
|
||||||
|
|
||||||
- `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --
|
- `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --
|
||||||
-D warnings`, `cargo test --workspace`: sixteen targets green, 83 suite
|
-D warnings`, `cargo test --workspace`: sixteen targets green, 83 suite
|
||||||
@@ -459,6 +513,35 @@ What it costs, all of it measured rather than asserted:
|
|||||||
truncation either side is measurably too narrow. See the item under
|
truncation either side is measurably too narrow. See the item under
|
||||||
"Retained-layout invariants".
|
"Retained-layout invariants".
|
||||||
|
|
||||||
|
**Threading a box in pixels down the draw is free on cold layout and 9-13%
|
||||||
|
off the retained paths** (2026-09-17). Instructions:u, medians of 21 runs of
|
||||||
|
binaries built in one worktree, seed 1 at depth 8, against `5b78002`:
|
||||||
|
|
||||||
|
| phase | before | after | |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| `cold`, 200 frames | 313.1M | 312.9M | -0.04% |
|
||||||
|
| `resize` | 408.1M | 405.6M | -0.61% |
|
||||||
|
| `many` | 1,924M | 1,756M | -8.75% |
|
||||||
|
| `scroll` | 357.3M | 323.4M | -9.49% |
|
||||||
|
| `repaint` | 363.3M | 315.4M | -13.18% |
|
||||||
|
|
||||||
|
**`cold` and `resize` are the pair that compares directly**: all twenty-five
|
||||||
|
work counters are identical on both, so those two rows say the draw path is
|
||||||
|
no more expensive threaded than composed. The other three do *less* work
|
||||||
|
rather than the same work faster, and the counters say how much a frame:
|
||||||
|
`repaint` goes from 23 draw requests and 13 widget draws to **1 and 1**,
|
||||||
|
`scroll` from 20 and 11 to 8 and 2, `many` from 273 and 186 to 207 and 157.
|
||||||
|
Primitive writes are identical in every phase, and so are all five reference
|
||||||
|
renders, which together are what say the output did not move.
|
||||||
|
|
||||||
|
Two things account for it: `redraw` composes nothing any more
|
||||||
|
(`offered_region`, `px_of`, `px_region` and `Moves::compose` are gone), and a
|
||||||
|
widget whose box moved without changing length settles itself instead of
|
||||||
|
escalating to its parent -- which is what `repaint`'s other twelve draws
|
||||||
|
were. Cycles are not quoted because this VM's are not worth quoting: across
|
||||||
|
these runs one unchanged binary's `cycles:u` ranged from 136M to 2,236M,
|
||||||
|
with the garbage readings described below.
|
||||||
|
|
||||||
Earlier, against #18's own history: the retained rewrite took `many` from
|
Earlier, against #18's own history: the retained rewrite took `many` from
|
||||||
25.17M instructions a frame at `691e3eb` to 6.14M at `29c7881`, and `resize`
|
25.17M instructions a frame at `691e3eb` to 6.14M at `29c7881`, and `resize`
|
||||||
from 16.08M to 8.39M. That fixture has since changed twice; do not compare
|
from 16.08M to 8.39M. That fixture has since changed twice; do not compare
|
||||||
@@ -483,6 +566,15 @@ across it.
|
|||||||
report its whole content length, which is what the `Scroll` above it
|
report its whole content length, which is what the `Scroll` above it
|
||||||
measures. One that set its own mask would have to report its box and hand
|
measures. One that set its own mask would have to report its box and hand
|
||||||
the content length up another way.
|
the content length up another way.
|
||||||
|
- **A widget's own mask is not the one it inherited**, and `ActiveData` keeps
|
||||||
|
both: `mask` is what its drawing is clipped to and `parent_mask` what
|
||||||
|
whoever drew it handed down. They differ exactly where the widget called
|
||||||
|
`set_mask`, which is what says whose mask a move rewrites -- and a local
|
||||||
|
redraw inherits the second, because handing back the first hands a widget
|
||||||
|
its own mask to set again, which `set_mask` asserts against. Found by
|
||||||
|
review on 2026-09-17: every `Masked` widget redrawn on its own panicked,
|
||||||
|
for as long as there has been a local-redraw path. Pinned by
|
||||||
|
`retained::a_masked_widget_redrawn_on_its_own_sets_its_mask_again`.
|
||||||
- A span is as long across itself as its longest child, unless a rule beside
|
- A span is as long across itself as its longest child, unless a rule beside
|
||||||
it says how long it is -- and then it does not read its children there at
|
it says how long it is -- and then it does not read its children there at
|
||||||
all, since the answer is not wanted and reading one is what makes its size
|
all, since the answer is not wanted and reading one is what makes its size
|
||||||
@@ -504,14 +596,16 @@ across it.
|
|||||||
widget has one parent. `.scrollable()` sets it once; raw `Scroll::new` does
|
widget has one parent. `.scrollable()` sets it once; raw `Scroll::new` does
|
||||||
not, and `Scroll` never reasserts it while drawing.
|
not, and `Scroll` never reasserts it while drawing.
|
||||||
- The first box a parent asks about is the offer. A later box chosen from the
|
- The first box a parent asks about is the offer. A later box chosen from the
|
||||||
child's answer is the final box, not another independent answer. Dirty
|
child's answer is the final box, not another independent answer. An offer
|
||||||
widgets are re-asked at the offer and only then drawn in the final box. An
|
composes through its ancestors' offers -- as *lengths* of them, which is a
|
||||||
offer composes through its ancestors' offers, not through their current
|
chain of fractions with no coordinate frame in it, so a region node between
|
||||||
placed boxes.
|
two widgets changes nothing about it. A dirty widget is re-asked in the box
|
||||||
|
its parent gave it, and only where that box is as long as the offer;
|
||||||
|
anything else is its parent's question to ask again.
|
||||||
- An answer is reusable only where both its measurement and the drawing made
|
- An answer is reusable only where both its measurement and the drawing made
|
||||||
in its final placed box remain valid. The final drawing's `Holds` interval
|
in its final placed box remain valid. The final drawing's `Holds` interval
|
||||||
is translated back into lengths of the offered box and intersected with the
|
is translated back into lengths of the box the widget was asked in and
|
||||||
answer's interval.
|
intersected with the answer's interval.
|
||||||
- **A container's near-edge override is owed only where the box really is the
|
- **A container's near-edge override is owed only where the box really is the
|
||||||
child's own answer.** `Stack` gives every child the box `box_of` derives
|
child's own answer.** `Stack` gives every child the box `box_of` derives
|
||||||
from its sizing child, so that one child has no room in it and would be
|
from its sizing child, so that one child has no room in it and would be
|
||||||
@@ -523,16 +617,23 @@ across it.
|
|||||||
box it hands its content along the scrolling axis is the content's own
|
box it hands its content along the scrolling axis is the content's own
|
||||||
length, and where shorter content sits across the viewport is `Scroll`'s own
|
length, and where shorter content sits across the viewport is `Scroll`'s own
|
||||||
alignment by design.
|
alignment by design.
|
||||||
- **A box a parent has already placed is asked about at the near edge.** The
|
- **A box a parent has already placed is asked about at the near edge**, and
|
||||||
second ask in `redraw` is in the final box, not at an offer, so applying the
|
the ask that does it is `draw_inner`'s own second one, in the box the
|
||||||
widget's alignment to it again places its content twice (`d8ae9c3`).
|
answer chose out of the box the parent gave. `redraw` had a third ask for
|
||||||
- Equal box lengths do not imply equal placement. An ordinary widget whose
|
this, in `ActiveData::region` with `decided` forced on, and it is gone:
|
||||||
offered and current boxes differ in position must involve its parent again;
|
applying the widget's alignment to an already-placed box places its
|
||||||
a region node can settle itself only when its own alignment, rather than a
|
content twice (`d8ae9c3`), and not re-placing it is simpler than undoing
|
||||||
container override, determines the final box. **Both halves of that
|
the placement.
|
||||||
decision compare whole boxes** -- whether the parent must place it, and
|
- **What a drawing depends on is the lengths of its box**, so the same
|
||||||
whether the offer already was the final box. Comparing lengths for the
|
lengths somewhere else is the same question and a local redraw of a widget
|
||||||
second left a region node drawn at its offer (`aea878d`).
|
whose box has moved but not changed length asks it. The box it asks in is
|
||||||
|
the one its parent gave, which is where its parent put it, so nothing has
|
||||||
|
to reproduce a position. That replaces the whole-box comparison and the
|
||||||
|
region-node exception beside it: a region node drawn at its offer
|
||||||
|
(`aea878d`) and a widget placed twice (`d8ae9c3`) were both that pair
|
||||||
|
disagreeing about which box a redraw is in. **A box in pixels is not
|
||||||
|
compared with a composed one anywhere any more** -- see the section of
|
||||||
|
that name.
|
||||||
- A retained drawing can be reused only when its `Holds` interval contains the
|
- A retained drawing can be reused only when its `Holds` interval contains the
|
||||||
new pixel box on both axes, its parent node is unchanged, its region-node
|
new pixel box on both axes, its parent node is unchanged, its region-node
|
||||||
choice matches the retained structure, and the widget is clean. A valid
|
choice matches the retained structure, and the widget is clean. A valid
|
||||||
@@ -777,11 +878,15 @@ pixels and `Holds`. Decided with Bryan on 2026-09-15.
|
|||||||
rounding, so it scales with the step: at `1/64` that was 0.016 px, enough
|
rounding, so it scales with the step: at `1/64` that was 0.016 px, enough
|
||||||
to move a box, and at `1/1024` it is a thousandth of a pixel. Range is
|
to move a box, and at `1/1024` it is a thousandth of a pixel. Range is
|
||||||
+/-2.1M px and conversion to `f32` is exact to 16,384 px.
|
+/-2.1M px and conversion to `f32` is exact to 16,384 px.
|
||||||
- **What the fuzzers ask for is a step per level of nesting**, which is two
|
- **What the fuzzers ask for is two steps, and they are both positions
|
||||||
for these trees. Truncating made each of those steps a whole one rather
|
now.** It was a step per level of nesting -- two for these trees -- and
|
||||||
than half of one; it did not add a level. Traced on 2026-09-16 to the same
|
threading a box in pixels down the draw did not reduce the count: one step
|
||||||
box reached two ways, each rounding where the other does not -- not
|
fails the 400-seed shrinker on `resize-size`, seeds 384 and 162, by
|
||||||
accumulation, and not one place. Two of them are fixed in `bdab558`:
|
0.002 px, while passing the 100-seed oracle (tried 2026-09-17). Truncating
|
||||||
|
made each step a whole one rather than half of one; it did not add a level.
|
||||||
|
Traced on 2026-09-16 to the same box reached two ways, each rounding where
|
||||||
|
the other does not -- not accumulation, and not one place. Two of them are
|
||||||
|
fixed in `bdab558`:
|
||||||
- `Scroll` wrote a box it had been given back out as its own length in
|
- `Scroll` wrote a box it had been given back out as its own length in
|
||||||
pixels. Centring a part in `rel 1` lands a step from centring it in
|
pixels. Centring a part in `rel 1` lands a step from centring it in
|
||||||
`px 900`, because `a(x - y)` and `ax - ay` do not round alike. Content
|
`px 900`, because `a(x - y)` and `ax - ay` do not round alike. Content
|
||||||
@@ -791,41 +896,34 @@ pixels and `Holds`. Decided with Bryan on 2026-09-15.
|
|||||||
share's rounding along the row. A position is now the fixed parts before
|
share's rounding along the row. A position is now the fixed parts before
|
||||||
it, exact, plus one rounded share. Two hundred equal shares of a 1000 px
|
it, exact, plus one rounded share. Two hundred equal shares of a 1000 px
|
||||||
row ended at 999.999 and now end at 1000.
|
row ended at 999.999 and now end at 1000.
|
||||||
What is left is a box centred in a fraction of its parent against the same
|
What is left is two positions, and `AGREE_STEPS` stays **2** for them. One
|
||||||
box centred in its own pixels, one step per level between them. Closing it
|
is a box centred in a fraction of its parent against the same box centred
|
||||||
means alignment resolved in pixels everywhere -- which costs the retained
|
in its own pixels, which is what shows at zero tolerance -- 0.001 px on a
|
||||||
resize path, since it is the fractional form that re-centres a subtree
|
handful of seeds. Closing it means alignment resolved in pixels
|
||||||
without redrawing it. Not worth it at a thousandth of a pixel.
|
everywhere, which costs the retained resize path, since it is the
|
||||||
- `Holds::through` inverts `px + rel * box`, which drops a step, so the answer
|
fractional form that re-centres a subtree without redrawing it. The other
|
||||||
is an interval even for a single length: inverting the length alone gives a
|
is `AxisRemap::Scale` re-expressing a part as a fraction of a box that
|
||||||
point that need not contain the box the part was drawn in. What it allows
|
changed length, which is the one the `resize-size` seeds above need.
|
||||||
for has two parts and they are not the same shape.
|
Neither is worth a thousandth of a pixel.
|
||||||
- **The two routes to a length** -- composed down the chain against
|
- **`Holds::through` is the exact preimage of `px + floor(rel * box)`**, as
|
||||||
measured against the window -- get two half steps either side, since
|
of `32542d0`. `floor(rel * B) >= lo - px` is `rel * B >= (lo - px) << REL`
|
||||||
`45a7176` made the walk round once instead of once a level. It was three
|
and `floor(rel * B) <= hi - px` is `rel * B < (hi - px + 1) << REL`, which
|
||||||
while composing rounded per level, and four before that, which was
|
is two `div_toward`s once the sign of `rel` has said which bound comes from
|
||||||
overshoot. Tightening it has never moved one of the rig's twenty-five
|
which end. No `ROUTES`, no `way_in`, and the whole of a box -- with or
|
||||||
work counters, so slack here buys no reuse.
|
without pixels taken off it -- maps back to itself.
|
||||||
|
|
||||||
**One half step further is arithmetically available and deliberately not
|
The answer is still an interval where this range is a single length,
|
||||||
taken.** The `Holds` assertion is quiet at one, and the whole-of-a-box
|
because a floor is not invertible: many boxes give one length. What is gone
|
||||||
case becomes an exact identity. What stops it is shrinker seed 220 on
|
is the *allowance*, which was there because the box arrived by a second
|
||||||
`reorder` -- diagnosed on 2026-09-16 and written up under "The offer
|
route, composed down the move chain against threaded down the draw. There
|
||||||
chain breaks at a region node" below. It is not a rounding question.
|
is one route now; see "A box in pixels is one multiply from its parent's".
|
||||||
- **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
|
|
||||||
allowing for one there compounded a step per level down a chain of
|
|
||||||
widgets each taking the whole of its parent (`60367d8`).
|
|
||||||
|
|
||||||
**It cannot go to zero while a length reached two ways is two numbers.**
|
**The range has to contain the box a drawing was made in**, which the
|
||||||
The range has to contain the box a drawing was made in, which the `Holds`
|
`Holds` assertion in `draw_at` checks, and it must not contain a box the
|
||||||
assertion in `draw_at` checks, and it must not contain a box the drawing
|
drawing does not hold for, which the warm-against-cold oracle checks.
|
||||||
does not hold for, which the warm-against-cold oracle checks. Those two
|
Being the preimage rather than a margin is what makes those two one
|
||||||
only coincide where composition is exact -- the `i64` item under "Next" --
|
statement instead of a trade-off between them. **Run the generated cases
|
||||||
and shrinking this allowance is the test of whether that worked. Too wide
|
in debug**, since the assertion is compiled out of every release run.
|
||||||
is the unsound side of it. **Run the generated cases in debug**, since the
|
|
||||||
assertion is compiled out of every release run.
|
|
||||||
- **The window is not a move entry** (`5b78002`). It was one, holding the
|
- **The window is not a move entry** (`5b78002`). It was one, holding the
|
||||||
output in pixels, so composing through it zeroed every `rel` below and the
|
output in pixels, so composing through it zeroed every `rel` below and the
|
||||||
shader's `rel * window.dim` and the CPU's final multiply by the output
|
shader's `rel * window.dim` and the CPU's final multiply by the output
|
||||||
@@ -840,87 +938,74 @@ 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
|
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.
|
GPU and the platform speak; `PxVec2` is what layout decides in.
|
||||||
|
|
||||||
## The offer chain breaks at a region node
|
## A box in pixels is one multiply from its parent's
|
||||||
|
|
||||||
**The open defect at `5f16617`**, and the reason `Holds::through` keeps a
|
The rule the offer chain broke, and the one layout now rests on. `ActiveData`
|
||||||
half step it does not need. It is the fourth thing this session to come down
|
keeps a widget's box as *lengths of its parent's box* -- `given_len`, and
|
||||||
to the offer against the placed box, after `aea878d`, `d8ae9c3` and the
|
`offer_len` for the box it was first asked about -- `DrawInfo` carries the
|
||||||
`Stack`/`Pad` overrides in `2bc6bdf`.
|
pixel lengths themselves (`px`, `offered_px`), and a draw threads them down
|
||||||
|
one `Len::to_px` at a time. Two steps a level: the box its parent gave it,
|
||||||
|
and then the part of that box its own answer placed its drawing in, which
|
||||||
|
`placed_lens` states once for both `placed_box` and the walk.
|
||||||
|
`Painter::px_size` and `px_len` read that value rather than composing
|
||||||
|
anything, and `UiRenderState::asked_px` takes the same steps back up the
|
||||||
|
parent chain when a local redraw starts part-way down the tree. Neither
|
||||||
|
chain has a coordinate frame in it, which is what makes both immune to a
|
||||||
|
region node; warm and cold reach every length by the same expression, so
|
||||||
|
they agree by construction rather than by allowance.
|
||||||
|
|
||||||
`offered_region` composes a widget's offer through its ancestors' offers,
|
Three things follow, and all three are why it was worth doing:
|
||||||
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
|
- **`Holds::through` is the exact preimage of `px + floor(rel * box)`** --
|
||||||
offers its content the viewport and places it in a box as long as the
|
two integer divisions, no `ROUTES`, no `way_in`, and the whole of a box
|
||||||
content. So the offer becomes a function of the answer being re-derived, and
|
mapping back to itself. It can be exact because there is one route to a
|
||||||
the old answer confirms itself.
|
length rather than two.
|
||||||
|
- **A local redraw asks the widget in the box its parent gave it**, and only
|
||||||
|
where that box is as long as the one it was offered; any other box is a
|
||||||
|
different question and its parent's to ask, with the mark left on. The
|
||||||
|
`decided`/region-node exception and the third ask in `redraw` went with
|
||||||
|
that: `draw_inner` places the answer inside the given box itself, which is
|
||||||
|
the ask that leaves the widget where its parent put it.
|
||||||
|
- **Symbolic regions are for the GPU, hit testing and remaps alone.**
|
||||||
|
`Moves::resolve` is the only walk left and it is the one the vertex shader
|
||||||
|
does. `wide.rs`, `Moves::compose`, `Moves::size_of`, `px_of`, `px_region`,
|
||||||
|
`offered_region` and `slot_wide` are all gone, 252 lines of `core/` net.
|
||||||
|
What their rounding is worth is cosmetic below a step, and nothing layout
|
||||||
|
decides turns on it.
|
||||||
|
|
||||||
Reproduced from the shrinker's ten-widget reduction:
|
**`px` is not stored on `ActiveData`, deliberately.** The plan for this said
|
||||||
|
to keep it there; it cannot be kept. A resize every widget's `Holds` admits
|
||||||
|
redraws nothing, which is the point of `Holds`, so a stored pixel length
|
||||||
|
would then be stale on every widget in the tree with nothing on it to say
|
||||||
|
so -- and the next local redraw of any of them would lay out for the old
|
||||||
|
window. Refreshing it costs a walk down every reused subtree on the resize
|
||||||
|
path; `asked_px` costs a walk up the parent chain only where a widget is
|
||||||
|
already being redrawn, and the mean chain here is 2.8 levels.
|
||||||
|
|
||||||
Scroll(X) { Pad as a region node { Span(X) {
|
What it was, kept because it is the shape of the mistake. `offered_region`
|
||||||
Branch(threshold 213), Span(X){ wrapped text, one-line text } } } }
|
composed a widget's offer through its ancestors' offers, which is the
|
||||||
|
invariant, except where the parent was a **region node**: there it fell back
|
||||||
|
to `UiRegion::FULL`, and `redraw` resolved that against the node's slot
|
||||||
|
entry, which holds 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 everything under such a node was re-asked at
|
||||||
|
the content's width, the texts kept the widths that width produced, the
|
||||||
|
content stayed the length those widths make, and the old answer confirmed
|
||||||
|
itself. On the shrinker's reduction of seed 220 at 900x1200, `reorder` left
|
||||||
|
widget 0 290.27px out; marking every widget dirty converged warm onto cold,
|
||||||
|
which is what said the retained path and not the arithmetic was at fault.
|
||||||
|
It is pinned as
|
||||||
|
`unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`,
|
||||||
|
the shrinker's ten widgets transcribed: with no padding between the node and
|
||||||
|
the span inside it, nothing distinguished the placed box from the offer and
|
||||||
|
the span settled itself. **Both halves had to be in place at once** -- the
|
||||||
|
old chain with the old allowance passes seed 220, and the old chain with the
|
||||||
|
exact `through` is what fails it.
|
||||||
|
|
||||||
```sh
|
**Do not widen the grid instead**: every failure in this document is one
|
||||||
SHRINK_SEED=220 SHRINK_DEPTH=5 SHRINK_CASE=reorder \
|
value reached by two expressions, sitting exactly on a boundary because the
|
||||||
cargo test --release --test shrink -- --ignored --nocapture
|
boundary was defined by the same value coming back the other way, and no
|
||||||
```
|
precision shrinks a residue that is the whole distance.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
**The fix is to stop recomputing the offer at all** (agreed with Bryan,
|
|
||||||
2026-09-16). `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 --
|
|
||||||
and neither answer is needed. By the retained contract a drawing and its
|
|
||||||
answer depend on the box's pixel *lengths* only; checked against every
|
|
||||||
widget, the pixel reads are `px_len` and `px_size` and nothing reads a
|
|
||||||
position. The offer's symbolic position was only ever an input to its pixel
|
|
||||||
size, and that the parent already knew when it asked. So:
|
|
||||||
|
|
||||||
- Thread the placed box's pixel size down beside `offered_px` -- one
|
|
||||||
expression per level, `local.size().to_px(parent_px)` -- and keep both on
|
|
||||||
`ActiveData`. Cold layout computes them the same way, so warm and cold
|
|
||||||
agree by construction rather than by allowance.
|
|
||||||
- `at_offer` becomes `px == offered_px`. Equal lengths at a different
|
|
||||||
position no longer escalate: the widget draws at `region`, which is the
|
|
||||||
`aea878d` fix without the second draw. A region node whose lengths differ
|
|
||||||
still draws twice, and the first box only needs the right length.
|
|
||||||
- `redraw` reads the stored `offered_px`. `offered_region`, `px_of`,
|
|
||||||
`px_region`, `Moves::compose`/`size_of`, `slot_wide` and `wide.rs` go; a
|
|
||||||
narrow `resolve` stays for `window_region`, which is hit testing and not a
|
|
||||||
layout decision.
|
|
||||||
- `remap_subtree`'s `Translate` keeps lengths, so stored sizes stay valid
|
|
||||||
at no cost on the hot path; `Scale` is the resize path and recomputes
|
|
||||||
them down the subtree from the same expression.
|
|
||||||
- `Holds::through` becomes the exact preimage of the truncating map
|
|
||||||
`px + floor(rel * B)`: two integer divisions, no `ROUTES`, no `way_in`,
|
|
||||||
and the whole-of-a-box case is the identity. The allowance existed only
|
|
||||||
because the box arrived by a second route.
|
|
||||||
|
|
||||||
Symbolic regions then exist for the GPU and for remaps alone, and their
|
|
||||||
rounding is cosmetic below a step. That is the separation the fixed-point
|
|
||||||
work kept reaching for. **Do not widen the grid instead**: every failure in
|
|
||||||
this document is one value reached by two expressions, sitting exactly on a
|
|
||||||
boundary because the boundary was defined by the same value coming back the
|
|
||||||
other way, and no precision shrinks a residue that is the whole distance.
|
|
||||||
|
|
||||||
## The leftover boundary
|
## The leftover boundary
|
||||||
|
|
||||||
@@ -1097,23 +1182,17 @@ Queued from this work, in order:
|
|||||||
child draws in, or only what the parent reports for it.
|
child draws in, or only what the parent reports for it.
|
||||||
|
|
||||||
- ~~**Compose in `i64` and narrow only when storing**~~ -- **done in
|
- ~~**Compose in `i64` and narrow only when storing**~~ -- **done in
|
||||||
`45a7176`**, and what it bought is below. The rest of this item is kept
|
`45a7176`, and then deleted with the composition it was for.** There is
|
||||||
because the measurement in it is what stopped the work being aimed at the
|
nothing left to compose: a box in pixels is threaded down the draw one
|
||||||
wrong thing.
|
multiply a level, so `wide.rs` went with `Moves::compose` and
|
||||||
|
`Moves::size_of`. What the widening bought while it was there --
|
||||||
|
`Holds::through`'s allowance from three half steps to two, and the whole
|
||||||
|
of a box mapping back to one step rather than one a level -- is what
|
||||||
|
exactness now gives outright. The rest of this item is kept because the
|
||||||
|
measurement in it is what stopped the work being aimed at the wrong thing.
|
||||||
|
|
||||||
The range inside a single operation is already protected -- `Fixed::mul`
|
**Widening the grid cannot make equal shares the same number of screen
|
||||||
widens to `i64` for the product, `Holds::through` does all of its
|
pixels, because the CPU's grid is not what puts them on the screen.**
|
||||||
arithmetic in `i64` and narrows once at the end, and `div_toward` takes
|
|
||||||
`i64`. What is not protected is *precision across a chain*:
|
|
||||||
`UiSpan::within` composes a box through its parent with four multiplies and
|
|
||||||
every one comes back to the `i32` grid before the next starts, which is the
|
|
||||||
"step per level of nesting" in "Fixed point" and what forces
|
|
||||||
`Holds::through`'s two-route allowance. A `Wide<SHIFT>` held through a
|
|
||||||
composition and narrowed when it is written to `ActiveData` or a primitive
|
|
||||||
would round once instead of once a level, at no cost in stored size.
|
|
||||||
|
|
||||||
**But it cannot make equal shares the same number of screen pixels,
|
|
||||||
because the CPU's grid is not what puts them on the screen.**
|
|
||||||
`prelude.wgsl` decodes the raw `rel`/`px` counts into `f32`, walks the move
|
`prelude.wgsl` decodes the raw `rel`/`px` counts into `f32`, walks the move
|
||||||
chain in floats -- with the comment saying so, "what has to hold is that
|
chain in floats -- with the comment saying so, "what has to hold is that
|
||||||
this agrees with itself frame to frame, not that it matches the CPU to the
|
this agrees with itself frame to frame, not that it matches the CPU to the
|
||||||
@@ -1126,39 +1205,20 @@ Queued from this work, in order:
|
|||||||
gaps are exactly ten pixels on both sides of the truncation change. So the
|
gaps are exactly ten pixels on both sides of the truncation change. So the
|
||||||
screen invariant is already as good as integers allow: a length in pixels
|
screen invariant is already as good as integers allow: a length in pixels
|
||||||
is that many pixels, and equal shares differ by at most one whole pixel
|
is that many pixels, and equal shares differ by at most one whole pixel
|
||||||
because three equal integers cannot sum to 1000. Layout's own one-or-two
|
because three equal integers cannot sum to 1000. Layout's own one-step
|
||||||
step spread is below what the shader can express, and shows only where it
|
spread is below what the shader can express, and shows only where it
|
||||||
pushes a value across the `floor` -- which is what moved `tabs`'s corner
|
pushes a value across the `floor` -- which is what moved `tabs`'s corner
|
||||||
arcs by a pixel at `08c9d5a`.
|
arcs by a pixel at `08c9d5a`.
|
||||||
|
|
||||||
What it bought is **layout's own decisions**: `Holds::through`'s two-route
|
**And composing it down the draw rather than back up the chain made layout
|
||||||
allowance went from three half steps to two, and the whole of a box now
|
faster**, which is the part that survives: `5f16617` took 1,840M
|
||||||
maps back to one step rather than one step per level of nesting.
|
instructions and 735M cycles against 1,908M and 760M -- 3.6% and 3.3%,
|
||||||
|
medians of 25 with all twenty-five work counters identical -- because a
|
||||||
**And it made layout faster.** 1,840M instructions and 735M cycles against
|
draw already descends past every move entry on its way in, so a widget's
|
||||||
1,908M and 760M -- 3.6% and 3.3% -- medians of 25 with all twenty-five
|
own box was a select rather than a walk back up a mean of 2.8 levels,
|
||||||
work counters identical. `5f16617` is where the win is: a draw already
|
eight hundred times a frame. Threading the pixels themselves took the
|
||||||
descends past every move entry on its way in, so `DrawInfo` carries what
|
select out too, at no cost to cold layout and 9-13% off the retained
|
||||||
the slot composes to and a widget's own box is a select rather than a walk
|
paths; the numbers are under "Performance".
|
||||||
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.
|
|
||||||
|
|
||||||
**One narrow step is left in that path.** `Painter` still composes a
|
|
||||||
child's region into its own on the grid before asking for it in pixels.
|
|
||||||
Taking it out needs the child's region as its parent stated it, which
|
|
||||||
`draw_inner` is not handed -- it receives that box already composed into
|
|
||||||
the slot's frame, because that is the form `ActiveData` and the GPU want.
|
|
||||||
Worth doing after the region-node offer defect below, which is what pins
|
|
||||||
`Holds::through` at the moment.
|
|
||||||
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
|
|
||||||
difference; and the pixel term takes its fraction on the ordinary grid, so
|
|
||||||
only the fraction itself -- the part that compounds multiplicatively --
|
|
||||||
needs the room. A third, skipping a parent that spans its own box, cost
|
|
||||||
18M instructions and is not there, the same verdict `UiSpan::within`'s
|
|
||||||
short-circuits got.
|
|
||||||
|
|
||||||
Other queued work, in dependency order:
|
Other queued work, in dependency order:
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user