diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 94ce4fe..e9661e9 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -376,19 +376,12 @@ boundaries through `through`, or make the grid floor everywhere. ### Where the residual comes from, and the snap -Both remaining steps are a symbolic region re-expressed by division rather -than recomputed the way a cold draw computes it: `AxisRemap::Scale` divides -to find a part's fraction of the old box, and `placed_box` scales a mixed -`Len` by the alignment. Neither touches the threaded pixel chain, so every -layout decision already agrees warm against cold; what differs is the -composed position the shader and hit testing see, by up to 0.002 px. - -Closing `Scale` exactly is possible: keep each primitive's region in its -widget's own coordinates and recompose on a move with `within`, eight -multiplies and no division against the current four divisions and twelve -multiplies, exact by construction, sixteen bytes more per primitive. Closing -the alignment one means resolving alignment in pixels, which costs the -retained resize path. Neither is worth a thousandth of a pixel on its own. +The `e44dea3` baseline's two-step allowance came from inverse remapping and +alignment composed by different routes. The frame/extent continuation below +retains local widget frames as well as local primitive coordinates, recomposes +in the same order as a cold draw, and removes inverse remapping. Its oracle +requires exact pixel-region equality. That experiment is not yet the pinned +framework; the baseline's snap decision below still applies there. Where it does matter is `snap_floor` in `prelude.wgsl`, which adds half a layout step before flooring: that absorbs float error and not a layout @@ -429,8 +422,8 @@ back. The original proposal was implemented by Claude as `5fcace1` on `wip/region-and-placement` in `/home/bob/repos/iris-pr18`. It kept the fraction reference stable but failed five layout cases and three draw-count cases. -The correction is saved as `efb416b`, pushed to origin's -`wip/region-and-placement`, in the isolated checkout +The first correction is `efb416b`; the exact-recomposition continuation is +`2ed5503`, pushed to origin's `wip/region-and-placement`, in the isolated checkout `/home/bob/repos/iris-layout-experiment`; **it is not on PR #18**. The original checkout is unchanged. Keep the experiment's build directory separate: Cargo can accept a different worktree's artifacts as fresh when a target directory @@ -475,34 +468,62 @@ placeholder zero answer became reusable once it was drawn during placement. and `adding_text_to_a_reverse_row_keeps_its_shared_height` reproduces the zero-height failure with a small tree. -Ordinary verification currently passes: formatting, workspace clippy with -`layout-diagnostics`, all workspace tests (96 suite tests), and the fast -generated cases. New regressions also inspect primitive coordinates for mixed -frame/extent drawings, including a region node, and a content change after -moving an inherited extent. A `tabs` resize from 1920x1200 to 900x1200 matches -a cold 900x1200 screenshot with zero differing pixels. The release oracle -passes 2000 seeds at depth 4 and 1000 seeds at depth 6 over all fifteen cases. -The shrinker passes 400 seeds at depth 5, and the debug oracle passes 120 -seeds at depth 4 with assertions enabled. +**Exact recomposition and reuse.** The continuation replaces `given_len` with +`given_region`: each widget retains its original frame in its parent's +coordinates. Moving a retained subtree replays those same local compositions, +and stops at region nodes. `RegionRemap`, `AxisRemap`, and inverse division +are gone. Fixed-pixel frames can now change size without forcing otherwise +valid descendants to draw again. This adds 16 bytes per active widget; local +primitive coordinates were already retained by `efb416b`. + +The shared oracle now compares pixel regions with ordinary equality; there +is no `AGREE_STEPS` allowance. Text publishes the range of its retained line +breaks rather than narrowing that range to every later +requested width. A layout with only explicit line breaks or no breaks holds +at arbitrarily wider widths. New tests compare actual primitive and mask +geometry and assert reuse for fixed-frame resizing and widening unwrapped +text (including explicit newlines and empty content). + +Ordinary verification passes: formatting, workspace clippy with +`layout-diagnostics`, workspace tests, and the fast generated cases. The +suite contains 98 tests. A `tabs` resize from 1920x1200 +to 900x1200 matches a cold 900x1200 screenshot with zero differing pixels. +With exact equality, the final release oracle passes 2000 seeds at depth 4 +and 1000 seeds at depth 6, the shrinker passes 400 seeds at depth 5, and the +debug oracle passes 120 seeds at depth 4 with assertions enabled. Every scan +runs all fifteen scenarios. The fixed-frame reuse regression was also run +against `efb416b`: it fails there with two child draws instead of one. **Do not land this as a performance improvement.** The old stretching and wrapper reuse tests pass, and one numeric-size test improves from three draws -to two. Cold work also improves against the broken `5fcace1` prototype: 604 to -535 widget draws and 15,296 to 10,384 primitive writes on seed 1, depth 8. -But against the last working `e44dea3`, the same rig does substantially more -nested layout work: +to two. The continuation reduces widget draws from `efb416b` by 6.9% cold, +9.0% with many dirty widgets, 7.6% for a size change, and 8.7% for resize. +Against the last working `e44dea3`, it still does substantially more nested +layout work (seed 1, depth 8): | phase | widget draws before | prototype | primitive writes before | prototype | | --- | ---: | ---: | ---: | ---: | -| cold | 369 | 535 | 9635 | 10384 | +| cold | 369 | 498 | 9635 | 9704 | | repaint | 1 | 1 | 1 | 1 | -| many | 157 | 310 | 4721 | 6970 | -| size | 16 | 275 | 106 | 5980 | +| many | 157 | 282 | 4721 | 6398 | +| size | 16 | 254 | 106 | 5725 | | scroll | 2 | 2 | 0 | 0 | -| resize | 13 | 328.5 | 0 | 6721.5 | +| resize | 13 | 300 | 0 | 6255 | These are instrumented work counts, not elapsed-time speedups (50 frames per phase except cold, which is one; 578 generated widgets, 191 finally active). +Uninstrumented release binaries, run directly with `perf stat -r 3 -e +instructions:u`, also improve over `efb416b`: 2000 size-change frames fall +from 14.3923B to 13.8914B instructions (3.5%); 2000 resize frames fall from +16.9756B to 16.1331B (5.0%). These whole-process totals include fixture setup; +reported run-to-run variation is at most 0.24%. The many-dirty workload falls +from 16.7962B to 16.0030B instructions (4.7%, 2000 frames). Repaint and scroll +keep their draw counts but incur small CPU increases: 0.8849B to 0.8913B +(+0.7%) and 2.4332B to 2.4479B (+0.6%) over 100,000 frames each. These are +CPU-work measurements, not phone frame times. Keeping measurement validity separate from final drawing validity +and deferring to an already dirty parent were also tried; neither improved +this rig's draw counts, so those changes were removed. + Raw placement reads in containers and repeated offer/placement evaluations remain conservative. Retaining local coordinates also costs memory per primitive. The experiment demonstrates selective reuse with one `Widget::draw` @@ -591,17 +612,13 @@ cold reach every length by the same expression. ### What the fuzzers tolerate -`AGREE_STEPS` in `tests/scenario/mod.rs` is **2**, and both steps are -positions. One is a box centred in a fraction of its parent against the -same box centred in its own pixels, 0.001 px on a handful of seeds. The -other is `AxisRemap::Scale` re-expressing a part as a fraction of a box that -changed length; one step fails the 400-seed shrinker on `resize-size`, -seeds 384 and 162, by 0.002 px while passing the 100-seed oracle. Two of the -earlier sources were fixed rather than tolerated (`bdab558`): `Scroll` wrote -a box it had been given back out as its own length in pixels, and `Span` -placed each child a step from where the last ended rather than as the fixed -parts before it plus one rounded share. See "Where the residual comes from" -above for what closing the rest would cost. +The frame/extent continuation removes `AGREE_STEPS`: warm and cold pixel +regions must compare exactly. This is distinct from the equal-share test: +when a row's grid-step count is not divisible by the number of children, +individual share widths can differ while every rerun of that layout must +still agree exactly. The PR #18 baseline still allows two position steps; +its earlier failure at one step (`resize-size`, seeds 384 and 162 at depth 5) +is a useful regression target for the experimental recomposition. ## Retained-layout invariants diff --git a/docs/PLAN.md b/docs/PLAN.md index e768645..6d78b38 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -871,7 +871,10 @@ replace that reference. A measurement also records the available extent, so reusing it checks both inputs. Retained primitive and mask coordinates name whether they follow the frame or the extent. Numeric extent reads constrain layout validity; merely drawing in an extent does not require another widget -evaluation when it moves. `Widget::draw` remains the only layout body. The +evaluation when it moves. Widgets also retain their original local frames, +so reuse composes positions in the same order as a fresh draw instead of +recovering fractions by division. The experimental oracle requires exact +pixel-region equality. `Widget::draw` remains the only layout body. The prototype has significant container-update costs and is not yet adopted into the pinned framework.