Record what the last of the fixed-point imprecision is

Traced rather than guessed: the same box reached two ways, each rounding
where the other does not. Two of the three are fixed, the third is priced --
it costs the retained resize path, at a thousandth of a pixel.

`OrthoSize` is deleted rather than paused; the decision it was waiting on was
that a span asks whether a rule already answers for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-16 03:42:56 -04:00
1 parent ddf746d8a4
commit 617331f913
1 file changed
+35 -19
+35 -19
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 **`cb955f1`**, fifty-five commits. Built-in alignment is complete there;
head is **`bdab558`**, fifty-seven commits. 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.
@@ -72,13 +72,14 @@ rewrite:
`core/src/ui/holds.rs`, the retained tests, and the generated cold-layout oracle
pin those rules. Seeds 10 and 86 are now in the ordinary generated set.
## Verification at `cb955f1`
## Verification at `bdab558`
- `cargo fmt --all --check`
- `cargo build --workspace --all-features`
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
- `cargo test --workspace --all-features`: 102 passed, 10 ignored
- The release generated cold-layout oracle passed 100 seeds in 75 s.
- `cargo test --workspace --all-features`: 103 passed, 10 ignored
- The release generated cold-layout oracle passed 100 seeds in 9.4 s, and a
shrinker case at 300 seeds in 3.5 s: both run a thread per core but one.
- The release shrinker passed **all five** cases -- `resize`, `repaint`,
`resize-repaint`, `reorder`, `size-change` -- at 300 seeds of depth 5:
26,001 widgets per case, largest tree 331. It passed 1000 seeds of depth 6
@@ -137,6 +138,13 @@ tighter figures.
## Retained-layout invariants
- 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
all, since the answer is not wanted and reading one is what makes its size
depend on it. `OrthoSize` was that second case written twice and is gone
(`9d8415d`); `Painter::ruled` is how a container asks which it is in, and
the only thing a widget may learn about a rule over it.
- A region node holds a whole `UiRegion` in its parent node's coordinates.
`UiRegion::FULL` is the identity. Widgets opt in with `.region_node()` or
`Widgets::set_region_node`; ordinary widgets share the nearest ancestor
@@ -315,12 +323,24 @@ 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
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.
- **What the fuzzers ask for is a step per operation.** The shrinker's five
cases agree within one (`resize` exactly), the oracle's two-operation cases
within two. Closing that needs one way of asking how long a box is: today a
chain composed down through `within` and a length measured against the
window are two, and they round separately. That is the next real step in
precision and it is bigger than any of these four commits.
- **What the fuzzers ask for is a step per level of nesting**, which is two
for these trees. 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
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
that fills the viewport unscrolled is handed back as it came, and the
`repaint` and `resize-repaint` cases became exact.
- `Span` placed each child a step from where the last ended, carrying every
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
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
box centred in its own pixels, one step per level between them. Closing it
means alignment resolved in pixels everywhere -- which costs the retained
resize path, since it is the fractional form that re-centres a subtree
without redrawing it. Not worth it at a thousandth of a pixel.
- `Holds::through` inverts `px + rel * box`, which rounds, so the answer is
an interval even for a single length. It maps the half step either side,
plus one more for the two ways above; inverting the length alone gives a
@@ -376,6 +396,11 @@ targets went from 14.3 s to 7.7 s and `target/` from 45 GB to 13 GB. Pick a
module out with `cargo test --test suite layout::`. The fuzzers and the
`*_cost` measurements are still their own targets.
The fuzzers take a thread per core but one (`9d8415d`), since a seed grows,
lays out and drops its tree alone: the oracle's hundred seeds went from 68 s
to 9.4 s and a shrinker case at 300 seeds from 18 s to 3.5 s. A failing seed
still shrinks and panics on its own thread.
Run the long generated oracle only after ordinary tests pass:
```sh
@@ -433,15 +458,6 @@ the same `Holds` contract and box chain.
Queued from this work, in order:
- Delete `OrthoSize`. It is redundant now that size is built in: a span should
read its own rule on the orthogonal axis and, where that is fixed, skip
reading its children's orthogonal sizes entirely, which is what `Full` does
today. That also removes the span's own instance of the compounding above.
**Paused 2026-09-16 for a decision**: a widget deliberately cannot see its
own rule ("the widget under a rule never learns of it"), so either that
changes or the span reports something else on the orthogonal axis -- which
is what every span reports, and a column sized to its widest item is what
`Children` is for.
- `Scroll` should take a direction rather than one axis: vertical, horizontal,
or both. Reporting `LEFTOVER` on both axes is already the right shape for it.
- Restore `max_width`/`max_height` as `SizeRule::{Min, Max, Clamp}`. `8220a78`