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;
|
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.
|
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
|
Three changes, done together as one slice on top of #18, because each decides
|
||||||
choose** plus **recalculating rather than repositioning** when a region cannot
|
the shape of the others.
|
||||||
be moved. Neither is implemented. What is settled and what is not:
|
|
||||||
|
|
||||||
- **Recalculating rather than repositioning is in**, as the `Remap`
|
- **Slots are opt-in, created by the container that re-places children.** A
|
||||||
retirement above. It costs the per-axis carry: a box that changed length on
|
draw call asks for one; a widget that did not asks nothing and composes into
|
||||||
one axis and not the other is now redrawn rather than remapped. Six of
|
its nearest ancestor's slot. #18 gives every widget a slot, which puts a
|
||||||
`tabs`'s fourteen relocations and five of `text`'s sixteen, and one extra
|
primitive's walk at full tree depth for no benefit, since almost every slot
|
||||||
redraw per frame on `replace_cost` -- 354,310,889 instructions against
|
is zero. `Span`, `Scroll` and `Aligned` re-place children after drawing
|
||||||
354,272,387, noise.
|
them and are the ones that ask. That keeps the chain 2-4 deep, which
|
||||||
- **Opt-in chaining is not**, and the design question is *who may opt in*. A
|
`chain_cost` measured as free.
|
||||||
widget can only move its subtree if its descendants chain through it, so
|
- **A stretch is a rewalk from the widget whose box changed, not a flat pass.**
|
||||||
opting in has to be done by whoever performs the move. My recommendation on
|
#18's `stretch` takes the top widget's old and new window boxes and rewrites
|
||||||
the PR: any container that re-places a child after drawing it (`Span`,
|
every descendant against that one pair, which works only while a composite
|
||||||
`Aligned`, `Scroll` all do), not only `Scroll`. That keeps the chain 2-4
|
fraction of the top box is recoverable from a window rel -- and a
|
||||||
deep instead of full tree depth, and keeps the slot write for ordinary
|
fixed-length box destroys that for everything beneath it, whatever is
|
||||||
re-placement.
|
stored. Instead: keep the parent-relative region -- the argument
|
||||||
- Giving **every** widget a slot, which is what #18 does and what I read §2 to
|
`widget_within`, `primitive_within` and `set_mask` already receive -- on
|
||||||
say, is the thing to change: it puts a primitive's walk at full tree depth
|
each widget, primitive and mask, and recompose it against the new box. For
|
||||||
for no benefit, since almost every slot is zero.
|
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:
|
Measured, so the next attempt is compared rather than argued:
|
||||||
|
|
||||||
| rig | what it says |
|
| 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/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. |
|
| `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. |
|
| `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
|
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
|
## The next slice
|
||||||
|
|
||||||
**Nothing, until the owner answers the opt-in question on #18** -- the shape of
|
**The plan above, as one slice on #18**: opt-in slots, the rewalk stretch
|
||||||
the slot set decides what `set_child_offset` even is, so building it first
|
over stored locals, and `Span` saying `Scale` -- after the owner has answered
|
||||||
risks writing it twice. After that: `set_child_offset` and `LazySpan` to finish
|
whether a slot carries a box, since that decides whether the locals are stored
|
||||||
LAYOUT.md §2, then built-in alignment.
|
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
|
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
|
`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:
|
Still in the target, roughly in dependency order:
|
||||||
|
|
||||||
- **The rest of the position chain** (LAYOUT.md §2), on top of #18:
|
- **The rest of the position chain** (LAYOUT.md §2), on top of the slice
|
||||||
`set_child_offset` for a container that moves its children as a group, and
|
above: `set_child_offset` for a container that moves its children as a
|
||||||
`LazySpan`.
|
group, and `LazySpan`.
|
||||||
- **Built-in alignment, and probably size**, after the chain rather than
|
- **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
|
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
|
harness -- `.width(rel(0.5))` inside a `Dir::DOWN` span reports 200 of 400
|
||||||
|
|||||||
Reference in new issue
Block a user