Record fixed-point layout, and what it left open
Four commits on #18 put layout on a grid: `Fixed<SHIFT>`, then positions, lengths, and the last of the pixels with `Holds`. The invariants that were about float rounding are rewritten rather than annotated -- the 0.05 px comparison is equality now, a move is a translation rather than a re-expression, and the span boundary needs no margin. What is left open is written down where the next session will look for it: the remaining step of imprecision is one rounding between two ways of asking how long a box is, and `OrthoSize` is paused on a decision rather than started. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1a73aba1cd
commit
ddf746d8a4
1 file changed
+82
-33
+82
-33
@@ -8,7 +8,7 @@ log.
|
|||||||
|
|
||||||
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 local
|
||||||
head is **`5ed9e87`**, fifty-one commits. Built-in alignment is complete there;
|
head is **`cb955f1`**, fifty-five commits. Built-in alignment is complete there;
|
||||||
see "Built-in alignment" below for the retained-layout details. No PR reviews
|
see "Built-in alignment" below for the retained-layout details. No PR reviews
|
||||||
were present when checked on 2026-09-15.
|
were present when checked on 2026-09-15.
|
||||||
|
|
||||||
@@ -65,25 +65,27 @@ rewrite:
|
|||||||
- `Span`'s leftover/no-leftover split is a strict layout decision, not a
|
- `Span`'s leftover/no-leftover split is a strict layout decision, not a
|
||||||
rounding tolerance. Its `Holds` range must use the same exact divided
|
rounding tolerance. Its `Holds` range must use the same exact divided
|
||||||
boundary as drawing; a tolerant endpoint retained zero-height children at
|
boundary as drawing; a tolerant endpoint retained zero-height children at
|
||||||
the boundary in generated seed 16. `5ed9e87` moves that boundary rather
|
the boundary in generated seed 16. `5ed9e87` moved that boundary rather
|
||||||
than softening it -- see "The leftover boundary" below.
|
than softening it, and `39e4ca2` removed the move: on the grid the box and
|
||||||
|
the sum are the same count. See "Fixed point" below.
|
||||||
|
|
||||||
`core/src/ui/holds.rs`, the retained tests, and the generated cold-layout oracle
|
`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.
|
pin those rules. Seeds 10 and 86 are now in the ordinary generated set.
|
||||||
|
|
||||||
## Verification at `5ed9e87`
|
## Verification at `cb955f1`
|
||||||
|
|
||||||
- `cargo fmt --all --check`
|
- `cargo fmt --all --check`
|
||||||
- `cargo build --workspace --all-features`
|
- `cargo build --workspace --all-features`
|
||||||
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
|
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
|
||||||
- `cargo test --workspace --all-features`: 89 passed, 10 ignored
|
- `cargo test --workspace --all-features`: 102 passed, 10 ignored
|
||||||
- The release generated cold-layout oracle passed 100 seeds in 68.5 s.
|
- The release generated cold-layout oracle passed 100 seeds in 75 s.
|
||||||
- The release shrinker passed **all five** cases -- `resize`, `repaint`,
|
- The release shrinker passed **all five** cases -- `resize`, `repaint`,
|
||||||
`resize-repaint`, `reorder`, `size-change` -- at 1000 seeds of depth 6:
|
`resize-repaint`, `reorder`, `size-change` -- at 300 seeds of depth 5:
|
||||||
159,024 widgets per case, largest tree 587 widgets. At the ordinary 300
|
26,001 widgets per case, largest tree 331. It passed 1000 seeds of depth 6
|
||||||
seeds of depth 5 it is 26,001 widgets per case, largest tree 331.
|
(159,024 widgets per case, largest tree 587) at `5ed9e87`, before fixed
|
||||||
|
point; re-run that before quoting it again.
|
||||||
- `tabs`, `view`, `minimal`, `text` and `random` render byte-identical at
|
- `tabs`, `view`, `minimal`, `text` and `random` render byte-identical at
|
||||||
1920x1200 against the same worktree without `5ed9e87`.
|
1920x1200 across the whole fixed-point sequence.
|
||||||
- `tests/drift.rs` passed its 20,000-move exactness check in release mode.
|
- `tests/drift.rs` passed its 20,000-move exactness check in release mode.
|
||||||
- The seeded `random` example, live-resized from 1920x1200 to 1280x800, is
|
- The seeded `random` example, live-resized from 1920x1200 to 1280x800, is
|
||||||
byte-identical to a cold 1280x800 render. Both PNGs hash to
|
byte-identical to a cold 1280x800 render. Both PNGs hash to
|
||||||
@@ -175,33 +177,31 @@ tighter figures.
|
|||||||
validity mechanism.
|
validity mechanism.
|
||||||
- Declared non-`leftover` lengths are resolved by the widget's parent where the
|
- Declared non-`leftover` lengths are resolved by the widget's parent where the
|
||||||
widget is drawn. A declared-length change therefore redraws the parent.
|
widget is drawn. A declared-length change therefore redraws the parent.
|
||||||
- Pixel comparison uses a 0.05 physical-pixel tolerance, against the last
|
- A pixel comparison is equality: lengths are whole counts of `1/1024` px,
|
||||||
actual layout. Repeated subpixel changes accumulate and eventually redraw.
|
so a change too small to reach the next step is not a change and one that
|
||||||
|
reaches it is, however little of a pixel it is worth.
|
||||||
- Text shaping is retained separately from line breaking. A greedy line break
|
- Text shaping is retained separately from line breaking. A greedy line break
|
||||||
remains valid from its longest produced line through the width at which it
|
remains valid from its longest produced line through the width at which it
|
||||||
was made, and `TextView` reports that interval through `Painter::holds`.
|
was made, and `TextView` reports that interval through `Painter::holds`.
|
||||||
- `Span`'s decision to distribute `leftover` is a pixel question. The room to
|
- `Span`'s decision to distribute `leftover` is a pixel question. The room to
|
||||||
divide is `len * fixed - total.px`, and under `HOLDS_EPSILON_PX` of it is
|
divide is `len * fixed - total.px`; pure `leftover` children are undrawn
|
||||||
none; pure `leftover` children are undrawn on the no-room side. The validity
|
where there is none. The box a parent hands back and the sum of what the
|
||||||
interval is split exactly at that moved boundary. Exact/open ranges are for
|
children asked for are counts of the same step, so the boundary needs no
|
||||||
hard widget decisions; the ordinary tolerant ranges remain for accumulated
|
margin and the validity interval is split exactly at it.
|
||||||
coordinate rounding.
|
|
||||||
- `Scroll` reports its content's first measured size. Its drawing can survive
|
- `Scroll` reports its content's first measured size. Its drawing can survive
|
||||||
container-length changes only over the interval in which clamping and its
|
container-length changes only over the interval in which clamping and its
|
||||||
current offset do not change.
|
current offset do not change.
|
||||||
- **A move must re-express each part as the same fraction of its new box, not
|
- **A move that keeps a box's length is a translation, and an offset is
|
||||||
add an offset to the last answer.** A subtree's stored regions are the only
|
exact on the grid.** A box that also changed length has to re-express each
|
||||||
record of where it is, so an offset integrates its own rounding and nothing
|
part as a fraction of the new one, and that division and multiplication
|
||||||
recomputes it. Measured 2026-09-15 on `tests/drift.rs`: offsetting both ends
|
round: `39e4ca2` translates where `from.len() == to.len()` and scales only
|
||||||
of a span shortens that fixture's row by 0.071 over 20,000 moves and 0.712
|
where it must, which is what made the shrinker's `resize` case agree
|
||||||
over 200,000, growing with the count; placing the far end from the near one
|
exactly. This inverts the float-era rule, and the measurements behind that
|
||||||
leaves 0.069, because the length is re-derived from the endpoints either
|
rule are why `tests/drift.rs` exists: in floats, offsetting both ends of a
|
||||||
way. 20,000 moves is five minutes of scrolling at 60Hz, which is when it
|
span shortened that fixture's row by 0.071 px over 20,000 moves and 0.712
|
||||||
passes the 0.05 physical pixels layout treats as the same place. The
|
over 200,000, while re-expressing fractions stayed exact. 20,000 moves is
|
||||||
fraction path is exact at 200,000. So `RegionRemap` is load-bearing for
|
five minutes of scrolling at 60Hz. On the grid the drift is gone either
|
||||||
accuracy rather than for generality, and its multiplies are not what is
|
way, and `tests/drift.rs` pins that it stays gone.
|
||||||
being paid for. The generated oracle compares within 0.05 and `unsettled.rs`
|
|
||||||
runs too few frames to reach it, which is why `tests/drift.rs` exists.
|
|
||||||
|
|
||||||
## Built-in alignment
|
## Built-in alignment
|
||||||
|
|
||||||
@@ -292,6 +292,43 @@ length the box takes and the drawing holds for that length alone.
|
|||||||
A `debug_assert` that a reported non-leftover size does not exceed the box it
|
A `debug_assert` that a reported non-leftover size does not exceed the box it
|
||||||
drew in would have caught both immediately, and is still worth adding.
|
drew in would have caught both immediately, and is still worth adding.
|
||||||
|
|
||||||
|
## Fixed point
|
||||||
|
|
||||||
|
Layout decides on a grid rather than in floats, in four commits: `7548139`
|
||||||
|
the number, `4e28f10` positions, `bd6de71` lengths, `39e4ca2` the last of the
|
||||||
|
pixels and `Holds`. Decided with Bryan on 2026-09-15.
|
||||||
|
|
||||||
|
- **`Fixed<SHIFT>` is an `i32` counting `1 / 2^SHIFT`.** Adding and
|
||||||
|
subtracting are exact; a multiply or a conversion rounds once, back onto
|
||||||
|
the same steps. Two routes to one place that come within half a step land
|
||||||
|
on the same number, so everything downstream compares for equality.
|
||||||
|
- **`Px` is `1/1024` px, `Rel` is `1/2^24` of a box, `Weight` is `1/65536`
|
||||||
|
of a share.** `PX_SHIFT` and `REL_SHIFT` are the only statement of the
|
||||||
|
first two, and the shader's copy is prepended from them by
|
||||||
|
`render::module_source` rather than written again in WGSL.
|
||||||
|
- A weight is not a fraction: a list divides its room by the total of its
|
||||||
|
weights, so `Weight` trades precision for the range to hold a whole list,
|
||||||
|
and `Rel::ratio` turns two weights into a share on the finer grid.
|
||||||
|
- **Arithmetic saturates rather than wrapping**, because a clamped coordinate
|
||||||
|
keeps the ordering a wrapped one inverts.
|
||||||
|
- `Px` was `1/64` first. The residue of a length reached two ways is one
|
||||||
|
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.
|
||||||
|
- `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
|
||||||
|
point that need not contain the box the part was drawn in.
|
||||||
|
- A pointer, a wheel notch, a shaped glyph advance and a window size arrive
|
||||||
|
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.
|
||||||
|
|
||||||
## The leftover boundary
|
## The leftover boundary
|
||||||
|
|
||||||
Fixed in `5ed9e87`, which closed the shrinker's `reorder` case. Neither of its
|
Fixed in `5ed9e87`, which closed the shrinker's `reorder` case. Neither of its
|
||||||
@@ -332,6 +369,13 @@ cargo clippy --workspace --all-targets -- -D warnings
|
|||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`cb955f1` put the ordinary tests in `tests/cases/`, as modules of one
|
||||||
|
`tests/suite.rs` target -- eleven links became one, and with
|
||||||
|
`profile.test`'s `debug = "line-tables-only"` a rebuild of `iris`'s test
|
||||||
|
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.
|
||||||
|
|
||||||
Run the long generated oracle only after ordinary tests pass:
|
Run the long generated oracle only after ordinary tests pass:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -393,6 +437,11 @@ Queued from this work, in order:
|
|||||||
read its own rule on the orthogonal axis and, where that is fixed, skip
|
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
|
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.
|
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,
|
- `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.
|
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`
|
- Restore `max_width`/`max_height` as `SizeRule::{Min, Max, Clamp}`. `8220a78`
|
||||||
@@ -401,9 +450,9 @@ Queued from this work, in order:
|
|||||||
`px_size`, which pinned its interval to one exact box on both axes and
|
`px_size`, which pinned its interval to one exact box on both axes and
|
||||||
redrew its whole subtree on any resize. The clamp boundary is a hard layout
|
redrew its whole subtree on any resize. The clamp boundary is a hard layout
|
||||||
decision: its `Holds` range must be exact and split at the crossover, the
|
decision: its `Holds` range must be exact and split at the crossover, the
|
||||||
way generated seed 16 taught for `Span` -- and the crossover itself has to
|
way generated seed 16 taught for `Span`. On the grid the crossover no
|
||||||
sit off the length a box structurally lands on, the way `5ed9e87` taught.
|
longer needs moving off where boxes land -- see "Fixed point" -- but it
|
||||||
See "The leftover boundary".
|
does need both sides of the comparison to be `Px`.
|
||||||
|
|
||||||
Other queued work, in dependency order:
|
Other queued work, in dependency order:
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user