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
|
||||
`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
|
||||
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
|
||||
@@ -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`)
|
||||
|
||||
**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
|
||||
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
|
||||
@@ -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;
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
At `c8beca5`:
|
||||
At `a92c6ac`:
|
||||
|
||||
- `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --
|
||||
-D warnings`, `cargo test --workspace`: green, 90 suite tests, 19 core
|
||||
@@ -497,7 +581,9 @@ At `c8beca5`:
|
||||
width of the widest of them.
|
||||
- Twenty-five rig work counters identical on `cold`, `repaint`, `scroll`,
|
||||
`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
|
||||
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):
|
||||
|
||||
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
|
||||
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.
|
||||
2. Write `ActiveData::answer` in one place.
|
||||
3. Keep the `DrawInfo` on `ActiveData`; delete the copied fields and the
|
||||
Check at the same time whether `a92c6ac`'s ordering has made the guard
|
||||
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`.
|
||||
4. `Span`'s leftover boundary through `Holds::through`.
|
||||
5. **Round on the CPU and snap to the nearest pixel in the shader**, as
|
||||
6. `Span`'s leftover boundary through `Holds::through`.
|
||||
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
|
||||
(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
|
||||
@@ -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`
|
||||
still inlines; expect a couple of percent of instructions and re-run the
|
||||
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
|
||||
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
|
||||
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.
|
||||
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
|
||||
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
|
||||
only what the parent reports for it.
|
||||
9. `Scroll` taking a direction rather than one axis.
|
||||
10. The measure/draw split, once the above is in. It deletes the two-ask
|
||||
11. `Scroll` taking a direction rather than one axis.
|
||||
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.
|
||||
|
||||
`docs/LAYOUT.md` §4, §5 and the density section are stale: they name
|
||||
|
||||
Reference in new issue
Block a user