diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index d4b59b7..c3ab9f0 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -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 **`bdab558`**, fifty-seven commits. Built-in alignment is complete there; +head is **`f11f5f4`**, sixty 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,12 +72,12 @@ 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 `bdab558` +## Verification at `f11f5f4` - `cargo fmt --all --check` - `cargo build --workspace --all-features` - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- `cargo test --workspace --all-features`: 103 passed, 10 ignored +- `cargo test --workspace --all-features`: 105 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`, @@ -120,24 +120,51 @@ choice and the incompatible visual effect of making `Full` the default. ### Performance -The retained rewrite was compared with #18's previous head `691e3eb` using the -same release `layout_diagnostics` fixture, seed 1, depth 8, 500 frames, 130 -dirty widgets: +Measured 2026-09-16 on the release `layout_diagnostics` fixture, seed 1, +depth 8, 500 frames, as median milliseconds a frame. `5ed9e87` is the commit +before fixed point; `39e4ca2` is fixed point complete. -| phase | `691e3eb` | `29c7881` | -| --- | ---: | ---: | -| many | 25.17M instructions/frame | 6.14M | -| resize | 16.08M | 8.39M | -| scroll | 0.720M | 0.691M | -| repaint | 0.720M | 0.689M | -| size | 0.730M | 0.698M | +| phase | `5ed9e87` | `39e4ca2` | `f11f5f4` | +| --- | ---: | ---: | ---: | +| many | 0.179 ms | 0.544 | 0.278 | +| resize | 0.020 | 0.035 | 0.041 | +| scroll | 0.011 | 0.030 | 0.020 | +| repaint | 0.012 | 0.031 | 0.028 | -The final `29c7881` measurements were 3,070,693,965 instructions for 500 -`many` frames and 4,192,804,214 for 500 `resize` frames. Re-run before quoting -tighter figures. +**Fixed point cost 3x, and two thirds of that was not the grid.** The +counters said eight more "placed by redrawing" a frame in `scroll`, all of +them "reuse: another layer": a retained drawing belongs to the layer it was +made on, and `Stack` measured the child that sizes it on its own layer before +drawing it again on the child layer. `97cc8b3` measures on the layer the +child ends up on, which puts the scroll phase's counters back exactly where +they were -- 4 widget draws, 12 draw requests. + +What is left is per-operation cost, not more work: at `f11f5f4` the `many` +phase does 125 widget draws against 121 before, and takes 1.6x as long. The +arithmetic is the difference -- an `i64` multiply and a rounding branch where +there was an `f32` multiply. The profile's shape, in order: `InstanceList:: +push`, `Painter::glyphs`, `RegionRemap::apply_span`, `UiRegion::within`. The +one obvious thing left is `glyphs`, which converts four `f32`s per glyph: +a `GlyphEntry` holding `Px` would convert once when the glyph is rasterised +instead of on every frame that draws it. + +Earlier, against #18's own history: the retained rewrite took `many` from +25.17M instructions a frame at `691e3eb` to 6.14M at `29c7881`, and `resize` +from 16.08M to 8.39M. That fixture has since changed; do not compare across +it. ## Retained-layout invariants +- **A retained drawing belongs to the layer it was made on.** Asked for again + on another layer it is redrawn, since nothing about its geometry says it is + in a list that paints at a different moment. A container that measures a + child by drawing it therefore measures on the layer that child will draw on + -- `Painter::child_layer_at` addresses one -- or it pays two draws a frame + forever and keeps whichever the second ask left. +- A widget that clips its contents to its box reports its box: `Scroll` and + `Masked` both report `LEFTOVER`, and a `debug_assert` in `draw_at` holds + any widget that set a mask to it. Overflowing is otherwise ordinary and a + text too tall for its box says so. - 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 @@ -458,6 +485,11 @@ the same `Holds` contract and box chain. Queued from this work, in order: +- `Painter::glyphs` converts four `f32`s to `Px` per glyph, every frame that + draws it. A `GlyphEntry` holding `Px` would convert once, when the glyph is + rasterised. It is the largest single thing left in the layout profile after + `InstanceList::push`. + - `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`