Record the agreed plan after #18: opt-in slots, a rewalk stretch, Span saying Scale
And the open question beside it: a slot that carries a box rather than a translation, which would make a move and a stretch of a Scale subtree one write each. To be measured with chain_cost before it is chosen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
c683e62e4f
commit
5959d58e44
1 file changed
+67
-29
@@ -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
|
||||
|
||||
Reference in new issue
Block a user