Record the text-width defect, and that the depth-4 seeds are fixed
The two seeds a wider scan found were one defect, older than the branch: a text reported a width rounded to the step below the line it measured, so the box that came back to it broke differently. Two tolerances were holding that together and both are gone.
This commit is contained in:
1 parent
f64d6a8d4d
commit
faa4e98311
1 file changed
+66
-37
+66
-37
@@ -10,21 +10,19 @@ 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
|
||||||
**`ffd79f3`**, pushed. It holds LAYOUT.md §2's position chain, `leftover`,
|
**`c8beca5`**, 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
|
||||||
present when checked on 2026-09-15.
|
present when checked on 2026-09-15.
|
||||||
|
|
||||||
**The branch has a warm/cold divergence nothing on it explains yet**, found
|
**Widen one fuzzer axis at a time, and record which.** Seeds **1121** and
|
||||||
2026-09-17 by widening the fuzzers rather than by any change: seeds **1121**
|
**1839** at **depth 4** failed on `ea6dbae` and on every commit before it,
|
||||||
and **1839** at **depth 4** fail `shuffle-swap-for-three` and
|
and nothing in the routine verification reached them: the fast oracle takes
|
||||||
`shuffle-all-but-first` respectively, and they fail the same way on
|
ten seeds, the shrinker 400 at depth 5 and the long oracle 1000 at depth 6,
|
||||||
`ea6dbae`, before either of the day's commits. Nothing in the routine
|
so a defect past seed 400 at depth 4 had nowhere to show. Both are fixed by
|
||||||
verification reaches them -- the fast oracle takes ten seeds, the shrinker
|
`4bd8607`. The scan that found them, worth running again after any change
|
||||||
400 at depth 5 and the long oracle 1000 at depth 6, and these are past 400
|
to layout:
|
||||||
at a depth neither long run uses. Chasing them is first in "Next". The scan
|
|
||||||
that found them, which is worth keeping as a pattern:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// tests/scan.rs, deleted once it had done its job
|
// tests/scan.rs, deleted once it had done its job
|
||||||
@@ -38,9 +36,10 @@ over_seeds((1..=2000).collect(), |seed| {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
262 s for 2000 seeds at depth 4 over all fifteen cases. `Rng::new` is
|
261 s for 2000 seeds at depth 4 over all fifteen cases, and clean at
|
||||||
`seed | 1`, so an even seed and the odd one above it are one tree: 1120 and
|
`4bd8607`. `Rng::new` is `seed | 1`, so an even seed and the odd one above
|
||||||
1121 are the same counterexample, as are 1838 and 1839.
|
it are one tree: 1120 and 1121 are the same counterexample, as are 1838 and
|
||||||
|
1839.
|
||||||
|
|
||||||
**Two ideas outrank everything else in this document** (Bryan, 2026-09-17).
|
**Two ideas outrank everything else in this document** (Bryan, 2026-09-17).
|
||||||
First, a changed tree lays out exactly as if it had been drawn that way from
|
First, a changed tree lays out exactly as if it had been drawn that way from
|
||||||
@@ -142,6 +141,38 @@ divergence needs a reader drawing while a size dependency two levels under
|
|||||||
it is dirty, which no hand-built tree has reproduced yet, and the seeds at
|
it is dirty, which no hand-built tree has reproduced yet, and the seeds at
|
||||||
depth 4 above fail for some other reason.
|
depth 4 above fail for some other reason.
|
||||||
|
|
||||||
|
### A text is handed back a box its own line fits in (landed, `4bd8607`)
|
||||||
|
|
||||||
|
A wrapping text reported the width it used through `Px::from_f32`, which
|
||||||
|
takes the nearest step and is under the line the shaper measured half the
|
||||||
|
time. A parent that sizes itself from that report -- a stack taking a span's
|
||||||
|
width, the span taking its widest child's -- then hands the text back a box
|
||||||
|
its own longest line does not fit in, and a greedy break there is one line
|
||||||
|
longer. Warm kept the break it had; cold made the narrower one.
|
||||||
|
|
||||||
|
Two tolerances were holding that together and both are gone:
|
||||||
|
|
||||||
|
- `TextBuffer::shape` answered any width within `BREAK_EPSILON_PX = 0.05` of
|
||||||
|
the longest line from the break in hand. Fifty steps of the grid, and a
|
||||||
|
structural decision taken on a hair's breadth -- the thing the `Span`
|
||||||
|
boundary invariant below already forbids. It is `want >= layout.width()`
|
||||||
|
now, exactly.
|
||||||
|
- The `Holds` range the text declares started at the nearest step to its
|
||||||
|
longest line, so it admitted boxes that line does not fit in. It starts at
|
||||||
|
`Px::ceil_from_f32` of it now.
|
||||||
|
|
||||||
|
Neither was the fix. **The fix is the report**: `Size::from_px(
|
||||||
|
PxVec2::ceil_from_f32(tex.size))`, the step at or above what was measured,
|
||||||
|
so the box that comes back fits. With it in place either tolerance could
|
||||||
|
have stayed and the case passes; both are wrong on their own terms, so both
|
||||||
|
went. `Fixed::ceil_from_f32` is new and is the only rounding on the grid
|
||||||
|
that is not to the nearest step.
|
||||||
|
|
||||||
|
The general shape, and the third time this branch has hit it: **a value that
|
||||||
|
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.
|
||||||
|
|
||||||
### `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
|
||||||
@@ -446,19 +477,24 @@ above for what closing the rest would cost.
|
|||||||
|
|
||||||
## Verification at the current head
|
## Verification at the current head
|
||||||
|
|
||||||
At `ffd79f3`:
|
At `c8beca5`:
|
||||||
|
|
||||||
- `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --
|
- `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --
|
||||||
-D warnings`, `cargo test --workspace`: green, 89 suite tests, 18 core
|
-D warnings`, `cargo test --workspace`: green, 90 suite tests, 19 core
|
||||||
unit tests, 11 generated cases. Only the long runs and the profiling rigs
|
unit tests, 11 generated cases. Only the long runs and the profiling rigs
|
||||||
are ignored; no known defect is.
|
are ignored; no known defect is.
|
||||||
- The release oracle at 100 seeds in 14.3 s, and **120 seeds in debug** in
|
- The release oracle at 100 seeds in 14.3 s, and **120 seeds in debug** in
|
||||||
59 s -- the debug run exercises the `Holds` assertion in `draw_at`.
|
59 s -- the debug run exercises the `Holds` assertion in `draw_at`.
|
||||||
- All fifteen shrinker cases at 400 seeds of depth 5 in 56 s, and the
|
- All fifteen shrinker cases at 400 seeds of depth 5 in 56 s, the oracle at
|
||||||
oracle at 1000 seeds of depth 6 in 142 s.
|
1000 seeds of depth 6 in 142 s, and **2000 seeds at depth 4 over all
|
||||||
- `view`, `minimal`, `random`, `tabs` and `text` byte-identical at
|
fifteen cases** in 261 s.
|
||||||
1920x1200 against `ea6dbae`. `random` live-resized from 1920x1200 to
|
- `view`, `minimal`, `random` and `tabs` byte-identical at 1920x1200
|
||||||
1280x800 is byte-identical to a cold 1280x800 render.
|
against `ea6dbae`. `random` live-resized from 1920x1200 to 1280x800 is
|
||||||
|
byte-identical to a cold 1280x800 render. **`text` is a new picture**:
|
||||||
|
`4bd8607` moved its lower paragraph one pixel, the box being a step wider
|
||||||
|
and its left edge crossing the shader's snap, and `c8beca5` rewrote the
|
||||||
|
alignment panel, which had all three labels in the middle of a box the
|
||||||
|
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.
|
||||||
@@ -632,22 +668,15 @@ 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. **Seeds 1121 and 1839 at depth 4**, which fail on this head and on
|
1. **A fast test for `0e0d4af`**, the stale-answer guard, which went in
|
||||||
`ea6dbae` alike -- a warm/cold divergence older than either of the
|
|
||||||
day's commits and the only known correctness defect on the branch.
|
|
||||||
Reduce each with `SHRINK_SEED`/`SHRINK_DEPTH=4`/`SHRINK_CASE`, turn it
|
|
||||||
into a fast test, and add the seed to `generated.rs`'s `SEEDS` the way
|
|
||||||
86 and 20 are there. Then re-scan 2000 seeds at depth 4 before calling
|
|
||||||
it done.
|
|
||||||
2. **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.
|
||||||
3. Write `ActiveData::answer` in one place.
|
2. Write `ActiveData::answer` in one place.
|
||||||
4. Keep the `DrawInfo` on `ActiveData`; delete the copied fields and the
|
3. Keep the `DrawInfo` on `ActiveData`; delete the copied fields and the
|
||||||
reconstruction in `redraw`.
|
reconstruction in `redraw`.
|
||||||
5. `Span`'s leftover boundary through `Holds::through`.
|
4. `Span`'s leftover boundary through `Holds::through`.
|
||||||
6. **Round on the CPU and snap to the nearest pixel in the shader**, as
|
5. **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
|
||||||
@@ -663,20 +692,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.
|
||||||
7. The smaller items: the stale `f32` comment, the gap of an undrawn child,
|
6. 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.
|
||||||
8. `LazySpan`, the next LAYOUT.md §2 item. Region nodes cover the movable
|
7. `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.
|
||||||
9. `SizeRule::{Min, Max, Clamp}`, restoring the `max_width`/`max_height`
|
8. `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.
|
||||||
10. `Scroll` taking a direction rather than one axis.
|
9. `Scroll` taking a direction rather than one axis.
|
||||||
11. The measure/draw split, once the above is in. It deletes the two-ask
|
10. 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