Record the bottom-up settle, and park two real defects on branches
Bryan's ordering replaces the stale-answer check with something the shape of the frame makes impossible: nothing shallower is drawn while anything deeper is dirty, so a parent never reads an answer that is about to move. The other two are written up rather than landed. A stack takes its sizing child's fraction twice, which no oracle can see because warm and cold are wrong alike; the fix is three grid steps short of the tolerance. And padding is to go outside what it pads with an `Inset` beside it, which one unaccounted halving is holding up. Both are pushed as `wip/` branches.
This commit is contained in:
1 parent
faa4e98311
commit
be895375ec
1 file changed
+107
-13
+107
-13
@@ -10,7 +10,7 @@ stayed.
|
|||||||
|
|
||||||
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`, head
|
`split/18-position-chain`** is open in `/home/bob/repos/iris-pr18`, head
|
||||||
**`c8beca5`**, pushed. It holds LAYOUT.md §2's position chain, `leftover`,
|
**`a92c6ac`**, pushed. It holds LAYOUT.md §2's position chain, `leftover`,
|
||||||
the `Holds` retained-layout contract, region nodes, built-in alignment and
|
the `Holds` retained-layout contract, region nodes, built-in alignment and
|
||||||
size rules, fixed-point layout, a box in pixels threaded down the draw, and
|
size rules, fixed-point layout, a box in pixels threaded down the draw, and
|
||||||
a report read as a fraction of the containing widget. No PR review was
|
a report read as a fraction of the containing widget. No PR review was
|
||||||
@@ -84,6 +84,14 @@ grid width fixes it; where it becomes visible is the shader's snap.
|
|||||||
|
|
||||||
### A report is a fraction of the containing widget (landed, `ffd79f3`)
|
### A report is a fraction of the containing widget (landed, `ffd79f3`)
|
||||||
|
|
||||||
|
**Superseded in part.** Bryan refined the rule twice on 2026-09-17: a
|
||||||
|
fraction means whatever the *parent* says it means, and it need not even
|
||||||
|
mean the same thing for two siblings -- "Main point is truly that rel is
|
||||||
|
decided by parent". The span conclusion below is unchanged and the reason
|
||||||
|
for it stands (a span's offer differs per child and the row does not), but
|
||||||
|
`reports_of` is the wrong shape for saying it; see the parked padding
|
||||||
|
branch.
|
||||||
|
|
||||||
`rel(0.5)` is half the span whatever else is in it and wherever the child
|
`rel(0.5)` is half the span whatever else is in it and wherever the child
|
||||||
sits. A report used to come back composed through the box it was offered,
|
sits. A report used to come back composed through the box it was offered,
|
||||||
and a span offers each child the room from its cursor, so a nested span
|
and a span offers each child the room from its cursor, so a nested span
|
||||||
@@ -173,6 +181,82 @@ comes back as a box has to be rounded away from the measurement, not to the
|
|||||||
nearest step.** Rounding to nearest is right for a value being carried;
|
nearest step.** Rounding to nearest is right for a value being carried;
|
||||||
it is wrong for a bound.
|
it is wrong for a bound.
|
||||||
|
|
||||||
|
### A frame settles strictly bottom-up (landed, `a92c6ac`)
|
||||||
|
|
||||||
|
The queue was already deepest-first, but a widget that could not settle
|
||||||
|
where it was called `redraw` on its parent from inside itself, which drew a
|
||||||
|
shallow widget while dirty widgets deeper in other subtrees were still
|
||||||
|
pending. A parent drawing over a subtree that has not settled reads answers
|
||||||
|
about to move, and the one that settles does so inside the parent's draw --
|
||||||
|
where its mark comes off and nothing compares what it now answers.
|
||||||
|
|
||||||
|
A widget that cannot settle defers instead: it marks its parent, stays
|
||||||
|
marked, and waits in `deferred` until the walk reaches the parent's depth,
|
||||||
|
which cannot happen before everything deeper has settled.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
loop {
|
||||||
|
let next = rsc.widgets().needs_redraw.iter().copied()
|
||||||
|
.filter(|id| !self.deferred.contains(id))
|
||||||
|
.max_by_key(|&id| self.depth(id));
|
||||||
|
let Some(id) = next else { break };
|
||||||
|
if !self.redraw(id, rsc) {
|
||||||
|
self.deferred.insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bryan's, 2026-09-17, and the right answer where `0e0d4af` was a check:
|
||||||
|
"then that entire category of issue can't even occur". `dirty_size_under`
|
||||||
|
stays in `draw_inner` anyway, because `update` draws the root for a resize
|
||||||
|
*before* `redraw_updates` runs at all and the ordering does not cover that
|
||||||
|
entry. Whether it is still load-bearing is untested -- the experiment that
|
||||||
|
said it was not had a failed edit in it and measured nothing.
|
||||||
|
|
||||||
|
Drawn widgets, widget draws and primitive writes are unchanged on every rig
|
||||||
|
phase; `many` pays 51 queue pops for 27 and 1059 depth reads for 410.
|
||||||
|
|
||||||
|
### Two branches parked, both real, neither ready
|
||||||
|
|
||||||
|
**`wip/stack-fraction-twice`.** A stack sized by a child that reports a
|
||||||
|
*fraction* applies that fraction twice: its parent places the stack at the
|
||||||
|
reported length, and `box_of(size)` then takes the same fraction of that
|
||||||
|
box. Half a row becomes a quarter. Pixels are idempotent under a second
|
||||||
|
application, so only a share ever shrank -- and warm and cold shrink alike,
|
||||||
|
so **no oracle can see it**. Bryan: "I believe this exact thing has come up
|
||||||
|
multiple times for some reason."
|
||||||
|
|
||||||
|
The fix is that every child gets the whole box, plus `widget_at` not
|
||||||
|
resolving a rule into a box already chosen from it. `Painter::box_of` is
|
||||||
|
deleted rather than guarded: a first attempt made it answer the whole box on
|
||||||
|
a `decided` axis, which fails because `place` can reuse the stack's
|
||||||
|
*measuring* drawing by remapping it, so `Stack::draw` never re-runs. **A
|
||||||
|
drawing has to be a function of its box alone** -- if `decided` is in it, a
|
||||||
|
reused drawing is wrong.
|
||||||
|
|
||||||
|
What stops it landing: seed 1091 at depth 4, `shuffle-swap-for-three`,
|
||||||
|
disagrees by three steps where the oracle tolerates two (warm 1053.9971
|
||||||
|
against cold 1054). `box_of` was also making placement *exact*, by handing a
|
||||||
|
child a box of exactly the length it asked for, and the whole box puts a
|
||||||
|
rounding back at each nesting level. Find that composition; do not widen
|
||||||
|
`AGREE_STEPS`.
|
||||||
|
|
||||||
|
**`wip/padding-outset-and-inset`.** Padding goes outside what it pads and
|
||||||
|
never insets the child (Bryan, 2026-09-17): otherwise a child's `rel` and
|
||||||
|
`leftover` would mean the inner box while its `px` meant the outer one.
|
||||||
|
`Padding::region` moves the child's box in rather than shrinking it, a new
|
||||||
|
`Inset` widget with `.inset()` is the old behaviour, and `Pad` is to be
|
||||||
|
renamed **`Outset`** with `.outset()` to match. `in_parent_frame`'s
|
||||||
|
composition and the `reports_of` argument go with it -- a report comes up
|
||||||
|
raw and the parent says what it is a fraction of, which `Inset` does for
|
||||||
|
itself.
|
||||||
|
|
||||||
|
What stops it landing: a child declaring `rel(0.5)` under an `Inset` comes
|
||||||
|
out 47.5 px wide of the 190 inside rather than 95, and the second halving is
|
||||||
|
unaccounted for. The `Pad` half is green on its own; three tests moved to
|
||||||
|
`.inset()` because they were using padding as scaffolding rather than
|
||||||
|
testing it.
|
||||||
|
|
||||||
### `Span`'s leftover boundary is a third expression for the room
|
### `Span`'s leftover boundary is a third expression for the room
|
||||||
|
|
||||||
The decision uses a rounded division, `total.px.div(fixed)`, while the room
|
The decision uses a rounded division, `total.px.div(fixed)`, while the room
|
||||||
@@ -477,7 +561,7 @@ above for what closing the rest would cost.
|
|||||||
|
|
||||||
## Verification at the current head
|
## Verification at the current head
|
||||||
|
|
||||||
At `c8beca5`:
|
At `a92c6ac`:
|
||||||
|
|
||||||
- `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --
|
- `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --
|
||||||
-D warnings`, `cargo test --workspace`: green, 90 suite tests, 19 core
|
-D warnings`, `cargo test --workspace`: green, 90 suite tests, 19 core
|
||||||
@@ -497,7 +581,9 @@ At `c8beca5`:
|
|||||||
width of the widest of them.
|
width of the widest of them.
|
||||||
- Twenty-five rig work counters identical on `cold`, `repaint`, `scroll`,
|
- Twenty-five rig work counters identical on `cold`, `repaint`, `scroll`,
|
||||||
`resize` and `size` across `0e0d4af`; `many` differs only in reuse
|
`resize` and `size` across `0e0d4af`; `many` differs only in reuse
|
||||||
attempts.
|
attempts. Across `a92c6ac` drawn widgets, widget draws and primitive
|
||||||
|
writes are identical on every phase, and only the scheduling counters
|
||||||
|
move.
|
||||||
|
|
||||||
**None of that reaches seeds 1121 and 1839 at depth 4**, which fail on this
|
**None of that reaches seeds 1121 and 1839 at depth 4**, which fail on this
|
||||||
head and on `ea6dbae` alike. Everything below is verification of what was
|
head and on `ea6dbae` alike. Everything below is verification of what was
|
||||||
@@ -668,15 +754,23 @@ The replay used for the reference check:
|
|||||||
|
|
||||||
In order, from the review above and Bryan's steer (2026-09-17):
|
In order, from the review above and Bryan's steer (2026-09-17):
|
||||||
|
|
||||||
1. **A fast test for `0e0d4af`**, the stale-answer guard, which went in
|
1. **`wip/stack-fraction-twice`**: find the composition that went from
|
||||||
|
exact to rounded when `box_of` stopped narrowing, and land it. This is
|
||||||
|
the one outright wrong layout known on the branch.
|
||||||
|
2. **`wip/padding-outset-and-inset`**: account for the second halving under
|
||||||
|
`Inset`, rename `Pad` to `Outset` and `.pad()` to `.outset()`, and audit
|
||||||
|
every `.pad()` in the examples for which of the two it meant.
|
||||||
|
3. **A fast test for `0e0d4af`**, the stale-answer guard, which went in
|
||||||
with only a fuzz seed behind it. The shape wanted is a widget drawing
|
with only a fuzz seed behind it. The shape wanted is a widget drawing
|
||||||
while a size dependency two levels under it is dirty, where its
|
while a size dependency two levels under it is dirty, where its
|
||||||
drawing is reusable at the measuring box and not at the placing one.
|
drawing is reusable at the measuring box and not at the placing one.
|
||||||
2. Write `ActiveData::answer` in one place.
|
Check at the same time whether `a92c6ac`'s ordering has made the guard
|
||||||
3. Keep the `DrawInfo` on `ActiveData`; delete the copied fields and the
|
dead outside the resize path.
|
||||||
|
4. Write `ActiveData::answer` in one place.
|
||||||
|
5. Keep the `DrawInfo` on `ActiveData`; delete the copied fields and the
|
||||||
reconstruction in `redraw`.
|
reconstruction in `redraw`.
|
||||||
4. `Span`'s leftover boundary through `Holds::through`.
|
6. `Span`'s leftover boundary through `Holds::through`.
|
||||||
5. **Round on the CPU and snap to the nearest pixel in the shader**, as
|
7. **Round on the CPU and snap to the nearest pixel in the shader**, as
|
||||||
one change with one verification. Bryan approved the snap on 2026-09-17
|
one change with one verification. Bryan approved the snap on 2026-09-17
|
||||||
(rendering may change wherever it brings the screen closer to what the
|
(rendering may change wherever it brings the screen closer to what the
|
||||||
user's code says: three equal sections of 1000 px need one of them
|
user's code says: three equal sections of 1000 px need one of them
|
||||||
@@ -692,20 +786,20 @@ In order, from the review above and Bryan's steer (2026-09-17):
|
|||||||
bounds move by half a `Rel` step); check with `nm` that `UiSpan::within`
|
bounds move by half a `Rel` step); check with `nm` that `UiSpan::within`
|
||||||
still inlines; expect a couple of percent of instructions and re-run the
|
still inlines; expect a couple of percent of instructions and re-run the
|
||||||
long fuzzers and the render set once for both.
|
long fuzzers and the render set once for both.
|
||||||
6. The smaller items: the stale `f32` comment, the gap of an undrawn child,
|
8. The smaller items: the stale `f32` comment, the gap of an undrawn child,
|
||||||
confirm nested `leftover` weights, one zero-divisor fallback. Add to
|
confirm nested `leftover` weights, one zero-divisor fallback. Add to
|
||||||
them: a span that overflows itself hands a child a box of negative
|
them: a span that overflows itself hands a child a box of negative
|
||||||
length, which is ordinary now rather than a corner, and nothing states
|
length, which is ordinary now rather than a corner, and nothing states
|
||||||
what a widget may assume about one.
|
what a widget may assume about one.
|
||||||
7. `LazySpan`, the next LAYOUT.md §2 item. Region nodes cover the movable
|
9. `LazySpan`, the next LAYOUT.md §2 item. Region nodes cover the movable
|
||||||
subtree case; do not restore a separate child-placement API.
|
subtree case; do not restore a separate child-placement API.
|
||||||
8. `SizeRule::{Min, Max, Clamp}`, restoring the `max_width`/`max_height`
|
10. `SizeRule::{Min, Max, Clamp}`, restoring the `max_width`/`max_height`
|
||||||
builders `8220a78` deleted. The clamp boundary is a hard layout decision
|
builders `8220a78` deleted. The clamp boundary is a hard layout decision
|
||||||
with an exact `Holds` split at the crossover, both sides in `Px`. Still
|
with an exact `Holds` split at the crossover, both sides in `Px`. Still
|
||||||
awaiting Bryan: whether a `Max` narrows the box the child draws in, or
|
awaiting Bryan: whether a `Max` narrows the box the child draws in, or
|
||||||
only what the parent reports for it.
|
only what the parent reports for it.
|
||||||
9. `Scroll` taking a direction rather than one axis.
|
11. `Scroll` taking a direction rather than one axis.
|
||||||
10. The measure/draw split, once the above is in. It deletes the two-ask
|
12. The measure/draw split, once the above is in. It deletes the two-ask
|
||||||
protocol, which is what `0e0d4af` had to put a guard around.
|
protocol, which is what `0e0d4af` had to put a guard around.
|
||||||
|
|
||||||
`docs/LAYOUT.md` §4, §5 and the density section are stale: they name
|
`docs/LAYOUT.md` §4, §5 and the density section are stale: they name
|
||||||
|
|||||||
Reference in new issue
Block a user