diff --git a/docs/IRIS_EXTRACTION_HANDOFF.md b/docs/IRIS_EXTRACTION_HANDOFF.md index 1c90370..b9516b0 100644 --- a/docs/IRIS_EXTRACTION_HANDOFF.md +++ b/docs/IRIS_EXTRACTION_HANDOFF.md @@ -44,35 +44,72 @@ resolve(slot)`, so anything that rewrites a region owes that slot a zero -- 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 question on #18, and the numbers for it +### The plan after #18, agreed with the owner on 2026-09-14 -The owner proposed, and I agree with, **opt-in chaining that `Scroll` would -choose** plus **recalculating rather than repositioning** when a region cannot -be moved. Neither is implemented. What is settled and what is not: +Three changes, done together as one slice on top of #18, because each decides +the shape of the others. -- **Recalculating rather than repositioning is in**, as the `Remap` - retirement above. It costs the per-axis carry: a box that changed length on - one axis and not the other is now redrawn rather than remapped. Six of - `tabs`'s fourteen relocations and five of `text`'s sixteen, and one extra - redraw per frame on `replace_cost` -- 354,310,889 instructions against - 354,272,387, noise. -- **Opt-in chaining is not**, and the design question is *who may opt in*. A - widget can only move its subtree if its descendants chain through it, so - opting in has to be done by whoever performs the move. My recommendation on - the PR: any container that re-places a child after drawing it (`Span`, - `Aligned`, `Scroll` all do), not only `Scroll`. That keeps the chain 2-4 - deep instead of full tree depth, and keeps the slot write for ordinary - re-placement. -- Giving **every** widget a slot, which is what #18 does and what I read §2 to - say, is the thing to change: it puts a primitive's walk at full tree depth - for no benefit, since almost every slot is zero. +- **Slots are opt-in, created by the container that re-places children.** A + draw call asks for one; a widget that did not asks nothing and composes into + its nearest ancestor's slot. #18 gives every widget a slot, which puts a + primitive's walk at full tree depth for no benefit, since almost every slot + is zero. `Span`, `Scroll` and `Aligned` re-place children after drawing + them and are the ones that ask. That keeps the chain 2-4 deep, which + `chain_cost` measured as free. +- **A stretch is a rewalk from the widget whose box changed, not a flat pass.** + #18's `stretch` takes the top widget's old and new window boxes and rewrites + every descendant against that one pair, which works only while a composite + fraction of the top box is recoverable from a window rel -- and a + fixed-length box destroys that for everything beneath it, whatever is + stored. Instead: keep the parent-relative region -- the argument + `widget_within`, `primitive_within` and `set_mask` already receive -- on + each widget, primitive and mask, and recompose it against the new box. For + each child, compute its new window box from its local; the same length on + both axes is one slot write where it has a slot and a recomposition where it + does not, and a changed length asks that child's `on_resize` and recurses or + redraws. Nothing is inverted, so `stretchable`, `UiScalar::stretch`'s + division and its debug assertion all go. It also fixes two things #18 gets + away with only because nothing but leaves says `Scale`: `try_reuse` asks + `on_resize` of the top widget alone, and `stretch` never rewrites + `Mask::region`. +- **Containers say `Scale`.** `Span` on both axes: it hands every child + `UiSpan::FULL` on the orthogonal axis, and its aligned-axis placement is + written in fractions and offsets of its own box, so the children's local + regions do not change when the box does. A child's *length* can, if one + without a hint reports differently after a redraw, and that reaches the + span through `size_deps` as it does today. The general rule: a widget is + `Scale` on an axis unless its draw reads the pixel length of its box on that + axis, which is text. The default stays `Redraw`. + +**Open, and the owner's to decide: a slot that carries a box instead of a +translation.** Everything under a slot would store its region relative to +that box, and the shader would compose the chain with `within` instead of +adding deltas. A move and a stretch of a `Scale` subtree are then both one +slot write: no rewalk over primitives, no stored locals, and `Translate` comes +free, since a glyph at an abs offset from its row's start stays there when +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. + +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 +length on one axis and not the other is redrawn rather than remapped. Six of +`tabs`'s fourteen relocations and five of `text`'s sixteen, all single- +primitive leaves, and one extra redraw per frame on `replace_cost` -- +354,310,889 instructions against 354,272,387, noise. The rewalk restores that +carry, and generalises it to a subtree. Measured, so the next attempt is compared rather than argued: | rig | what it says | | --- | --- | -| `tests/chain_cost.rs` | GPU pass time by chain depth at 200k instances: free to depth 8 (+5%), then ~3 us per level -- +42.6% at 16, +221% at 64. Each step is a storage load addressed by the previous one, so it is the chaining that costs, not the arithmetic at a level; a slot carrying a whole region would measure the same. | -| `tests/replace_cost.rs` | Instructions per frame re-placing 200 rows: 1.98M writing each row's slot, 2.38M rewriting its regions, 7.13M redrawing it. A load for `perf`, not a check. | +| `tests/chain_cost.rs` | GPU pass time by chain depth at 200k instances: free to depth 8 (+5%), then ~3 us per level -- +42.6% at 16, +221% at 64. Each step is a storage load addressed by the previous one, so it is the chaining that costs, not the arithmetic at a level; a slot carrying a whole region should measure the same, but has not been. | +| `tests/replace_cost.rs` | Instructions per frame re-placing 200 rows: 1.98M writing each row's slot, 2.38M rewriting its regions, 7.13M redrawing it. A load for `perf`, not a check. Five primitives per row; the regime that decides whether the chain is worth it is a transcript row of a few hundred glyphs, so re-run it with 200 characters of text per row before concluding. | | `tests/draw_cost.rs` | Pre-existing: what recording a frame costs on the CPU by layer count. | Irrelevant at an example's couple of hundred primitives; a transcript's glyphs @@ -151,10 +188,11 @@ Other standing instructions from the owner: ## The next slice -**Nothing, until the owner answers the opt-in question on #18** -- the shape of -the slot set decides what `set_child_offset` even is, so building it first -risks writing it twice. After that: `set_child_offset` and `LazySpan` to finish -LAYOUT.md §2, then built-in alignment. +**The plan above, as one slice on #18**: opt-in slots, the rewalk stretch +over stored locals, and `Span` saying `Scale` -- after the owner has answered +whether a slot carries a box, since that decides whether the locals are stored +beside window regions or *are* the regions. Then `set_child_offset` and +`LazySpan` to finish LAYOUT.md §2, then built-in alignment. The archive is not a patch here: it writes `Widget::draw` against `painter.set_size`, which #16 replaced with a returned `Size`, and it writes @@ -163,9 +201,9 @@ Recreate on today's `Len` and let the dp slice follow. Still in the target, roughly in dependency order: -- **The rest of the position chain** (LAYOUT.md §2), on top of #18: - `set_child_offset` for a container that moves its children as a group, and - `LazySpan`. +- **The rest of the position chain** (LAYOUT.md §2), on top of the slice + above: `set_child_offset` for a container that moves its children as a + group, and `LazySpan`. - **Built-in alignment, and probably size**, after the chain rather than before it: the owner reordered the two on 2026-09-14. Reproduced in the harness -- `.width(rel(0.5))` inside a `Dir::DOWN` span reports 200 of 400