diff --git a/docs/IRIS_EXTRACTION_HANDOFF.md b/docs/IRIS_EXTRACTION_HANDOFF.md index b9516b0..f00ba3d 100644 --- a/docs/IRIS_EXTRACTION_HANDOFF.md +++ b/docs/IRIS_EXTRACTION_HANDOFF.md @@ -9,40 +9,76 @@ Canonical `main` is **`ca2b4b2`** (#17, the headless rig). Sixteen slices are in; #16's size work and #17's rig both merged on 2026-09-14. **#18 `split/18-position-chain`** is open, worktree `/home/bob/repos/iris-pr18`, -head `db1751f`, five commits. LAYOUT.md §2's O(1) subtree movement, plus the -`Remap` retirement the owner asked for on top of it. +head `86a7e8d`, eight commits. LAYOUT.md §2's O(1) subtree movement, the +`Remap` retirement, and then the three changes the owner chose on 2026-09-14: +the slot carries a box, slots are opt-in, and a widget's region is held in the +coordinates of the slot it draws in. -- Every active widget owns a slot in `UiRenderState::moves` -- a translation in - physical pixels and the slot it is relative to. A primitive instance and a - mask each name one, and `prelude.wgsl` walks the chain and adds the - accumulated delta. A mask resolves its own chain rather than the drawn - primitive's, so a stationary viewport can clip content moving inside it. -- `try_reuse`'s translation case writes one slot instead of remapping a - subtree: 100 primitive region writes to 0 on a span of 20 rows five - primitives deep. -- `window_region` walks the same chain on the CPU, so hit testing and anyone - asking in window pixels agree with the shader. `Moves::resolve` stops at - `CHAIN_LIMIT` as the shader does, and debug-asserts that it reached the end. -- `Vec2` is `repr(align(8))`, WGSL's alignment for a `vec2`, so a GPU - struct holding one is laid out the way its shader reads it without saying so - itself. `GlyphPrimitive` no longer states its own alignment; both it and - `MoveOffset` keep a manual `unsafe impl Pod`, because the trailing padding - that alignment introduces is what `derive(Pod)` refuses. **No manual padding - fields** -- the owner rejected one on 2026-09-14. -- `Remap` is gone, with `UiScalar::outside`, `UiSpan::outside` and - `LerpUtil::lerp_inv`. A translation never needed an inversion: shifting a box - shifts everything composed into it by the same amount, since - `lerp(s + d, e + d, t) == lerp(s, e, t) + d` on both channels, whatever the - box's relative extent. Only a change of length needs each part's fraction - recovered, which is `UiRegion::stretch` behind `UiRegion::stretchable`. The - decision is made once before the walk and neither relocation method - branches, which is how the owner asked for it. +- **A slot holds a box, not a translation** (`1f9dc48`), given in the + coordinates of the slot it names, and `prelude.wgsl` composes the chain with + `within` instead of adding a delta. A translation is the special case where + the box has its parent's relative extent. Measured against the translate + slot on the same binary: +0.5-0.8% at depth 1-4, +9.6% at 8, +32.2% at 64. + Free where opt-in slots put the chain. The identity is `UiRegion::FULL`, not + zero -- a zeroed entry collapses its subtree to a point. +- **`Painter::place` is how a container asks for a slot** (`d989691`): it + draws a child it decides the box of and may decide again. `Span`, `Aligned` + and `Scroll` use it; everything else shares its nearest ancestor's slot, + which `tests/layout.rs` pins at one deep with four widgets in between. + Placing a child again is one entry to write, moved or resized alike. +- **Nothing is inverted any more.** `UiRegion::stretch`, `stretchable` and + `UiScalar::stretch` are gone, with the case they could not express: a + 40-tall row now stretches on its other axis, which `stretchable` refused. +- **`redraws_under` is the CPU walk that remains**, and it is a question + rather than a marking: before keeping a drawing, ask whether anything under + it would have to be drawn again for the new length, and give up the reuse if + so. Marking instead does not terminate -- the mark escalates to the + descendant's size reader, which re-places the child, which marks it again. + A part of a box with no relative extent holds its contents as offsets, and + composing into it leaves none either, so the walk stops where a length did + not change: an 80-wide child of a widened row is never asked. +- **`Span`, `Pad`, `Stack`, `Offset`, `Aligned`, `SetSize` and `LayerOffset` + say `Scale`.** Each places in fractions and offsets of its own box and none + reads its pixel length. `Scroll` and `MaxSize` do read pixels and stay + `Redraw`. Measured as not the cause of the divergence below: taking every + one of these claims back out leaves the failing set identical. +- **`OnResize::Scale` keeps its name.** The owner rejected `Stretch` on + 2026-09-14: stretch has an opposite and scale does not, and the answer is + per axis so the axis is already established where it is read. -**Two invariants everything here rests on.** `resolved = region + -resolve(slot)`, so anything that rewrites a region owes that slot a zero -- -`stretch` does it for the subtree it rewrites, `draw_inner` for the widget it -draws. And a stretch is only expressible out of a box with a relative extent; -a fixed length holds its parts as offsets from its start and keeps no fraction. +**The open defect, and it is older than this branch.** `tests/generated.rs` +grows a seeded random tree and checks that laying it out again lands where +growing it cold does. It does not always. Every case seen is a **wrapping text +on a span's own axis**: the span measures the child in the whole box, the +child shapes to that width and reports the width it used, the span places it +in exactly that width, which is a length change, so it shapes again and its +longest line is shorter than the box it was just given. Where it settles +depends on how many passes it has had, and a warm tree has had a different +number from a cold one -- so layout is not a function of the state, which it +has to be. A span whose axis is not the wrap axis is stable, which is every +real column of text, and why nothing had run into it. + + seeds 1-30, three checks each db1751f (before) 86a7e8d (now) + diverging 7 of 90 30 of 90 + +The same defect either way: a placed child reaches the second shaping more +often now. It wants fixing where the two draws meet, LAYOUT.md §4, rather than +anywhere in the chain -- and **it should be fixed before #18 merges**, since +tripling how often a tree lands somewhere unrepeatable is not something to +land even on a defect that was already there. +`a_wrapping_child_of_a_row_settles_somewhere_else_each_time` is the ignored +reproduction; the six seeds the live tests use are ones that agree. + +**Rigs added.** `iris::random` grows the tree (spans in every direction, +stacks, rects with varying opacity, text wrapping and overflowing, a declared +size over half of it, stopping at a depth), `examples/random.rs` draws one -- +`IRIS_SEED` and `IRIS_DEPTH` pick it -- and `tests/generated.rs` compares warm +against cold under a size change, a resize, and both. It found the +non-terminating marking above immediately. + +**Not verified yet:** the headless render shots and the live resize under +sway. The owner asked for those last, after the CPU side is right, and the CPU +side is not right until the divergence is. ### The plan after #18, agreed with the owner on 2026-09-14 @@ -91,10 +127,29 @@ the row grows. The CPU work on a resize becomes asking `on_resize` down the widgets and redrawing the ones that say so -- one ask for a transcript row against a few hundred glyph rewrites. It replaces #18's chain rather than extending it, and it contradicts LAYOUT.md §2's line that slots carry -translation only. Before choosing it, run `chain_cost` with a region per slot -in place of a `Vec2`: the per-level cost was measured as the dependent load -rather than the arithmetic, so the extra lerps should be free, but that is an -assumption until the rig says so. +translation only -- a line an agent wrote into the design on 2026-09-04, not +something the owner asked for. Her own statement of the requirement was "if it +needs to be moved then that can be done after the fact efficiently, or resized +just done after as well". + +**Measured, 2026-09-14.** `MoveOffset` widened from a `Vec2` to a `UiRegion` +(36 bytes) and `resolve_move` composing with `within` instead of adding +deltas, against the same binary's translate slots: depth 1 78.5 us against +78.0, depth 2 78.7 against 78.3, depth 4 79.6 against 79.0, then depth 8 89.8 +against 81.9, depth 16 128.6 against 111.3, depth 64 331.2 against 250.6. So a +box slot is free at the 2-4 depth opt-in slots produce (+0.5-0.8%) and costs +10-30% past depth 8, where the chain is already expensive. All 42 tests pass +against the composing shader and `tabs` at 1920x1200 is byte-identical, +because composing through a translation box is the same map as adding its +delta. The experiment is `stash@{0}` in `/home/bob/repos/iris-pr18`. + +The slot has to carry the whole box rather than a scale and an offset: a +pixel-space affine map scales everything under it, including a child that must +keep its pixel length, and the `rel`/`abs` pair is exactly what distinguishes +the two. What a box slot does not buy is a shorter CPU walk. Asking each +widget whether its drawing depends on the length that changed is the same walk +either way; what the box removes is the region rewrite and the upload at the +widgets that answer `Scale`, and the inversion at the ones that do not. What is settled either way: recalculating rather than repositioning is in, as the `Remap` retirement above. It costs the per-axis carry: a box that changed