Move LazySpan rows through one retained offset
This commit is contained in:
1 parent
0aa03cf621
commit
fffed42f9e
13 files changed
+503
-104
No files matched your search
@@ -12,35 +12,6 @@ and six phone-report sections went on 2026-09-08 for that reason.
|
||||
|
||||
## Fix
|
||||
|
||||
- [ ] **A row moving because the list grew should be one `move_offsets`
|
||||
write, and today it is a redraw.** Found 2026-09-09 by
|
||||
`scripts/rigs/ui-profile`'s `arena_churn` and left for whoever picks
|
||||
this up next; the upload half of it is done and this is the layout
|
||||
half.
|
||||
|
||||
The measurement. Over the bench fixture's 401 streamed deltas, the
|
||||
instance arena uploads **71.9%** of itself per frame against a
|
||||
**71.8%** floor -- those entries genuinely differ, so no amount of
|
||||
better dirty-tracking touches it. The control that says it is wrong is
|
||||
the fling phase on the same screen and the same content: it moves the
|
||||
same primitives every frame and uploads **3.3%**, because a scroll
|
||||
reaches `UiRenderState::mov` and writes one `move_offsets` delta for
|
||||
the subtree (LAYOUT.md section 2) instead of rewriting every
|
||||
primitive's absolute region.
|
||||
|
||||
The list is pinned to the newest end, so a growing reply pushes every
|
||||
earlier row up. `draw_inner` now treats sub-pixel size differences as a
|
||||
move and the span refactor removed nearly all repeated draws, but the
|
||||
instance floor remains 71.8%. The remaining question is why those
|
||||
translations still rewrite primitive regions instead of stopping at
|
||||
the rows' move slots.
|
||||
|
||||
Done looks like: `arena_churn`'s `what_a_streamed_reply_uploads` shows
|
||||
stream instances in the same range as the fling's, and its `whole`
|
||||
column stops being the interesting one. The rig prints floor,
|
||||
uploaded and whole per array precisely so this is checkable rather
|
||||
than argued.
|
||||
|
||||
- [ ] **Where the scroll *pin* lives.** The rest of "scrolling moves out
|
||||
of the list" landed on 2026-09-08 -- `List` is `LazySpan`, the physics
|
||||
and the gesture live in one `ScrollController`, `.scrollable()` is the
|
||||
|
||||
@@ -94,6 +94,15 @@ resolved in the vertex shader.**
|
||||
- `mov(id, delta)` becomes: look up `id`'s slot, write
|
||||
`move_offsets[slot].delta += delta`. One write — no primitive touched, no
|
||||
recursion, since descendants already reference this slot transitively.
|
||||
- A container may also retain one optional **child-coordinate slot** between
|
||||
its own slot and every direct child's slot. `Painter::set_child_offset`
|
||||
creates that boundary before the first child is drawn and can update it
|
||||
after measuring a child on later redraws. The container's own primitives,
|
||||
hit region and mask stay fixed; its whole child subtree moves through one
|
||||
write and every existing GPU, hit-test and accessibility chain sees the
|
||||
same result. `LazySpan` uses this while still walking visible rows for
|
||||
virtualisation: row boxes stay in stable local coordinates and the shared
|
||||
boundary carries the changing screen translation.
|
||||
- `shader.wgsl`'s vertex stage, after computing `top_left`/`bot_right` in
|
||||
pixels (after `:106`, before the clip-space divide at `:113`), walks
|
||||
`move_idx → move_offsets[i].parent` for a bounded number of steps (a
|
||||
@@ -120,6 +129,12 @@ for a resize that changes a region's `rel` component (a genuine reflow,
|
||||
§3) and for a size-independent widget's resize (§3), where the content's
|
||||
shape doesn't change and one field write already suffices.
|
||||
|
||||
Provisional layout can still write an instance at an intermediate position
|
||||
and restore it before upload. `Primitives::set_instance` remembers the value
|
||||
at the first write in a frame and clears the dirty bit when the final bytes
|
||||
match it. The GPU therefore observes final layout state, not CPU-only
|
||||
measurement work.
|
||||
|
||||
### 2b. Two more readers of "where is this widget," and masks
|
||||
|
||||
Moving the offset into the vertex shader means `ActiveData.region` is no
|
||||
|
||||
+14
-8
@@ -670,8 +670,10 @@ The trap that only the rig could have caught: writing an entry is not the
|
||||
same as changing it. Recycling rewrote every glyph of every moved row with
|
||||
identical bytes, marking 73% of the glyph array against 0.6% genuinely
|
||||
changed. `PrimitiveVec::set` and `Primitives::set_instance` compare before
|
||||
marking, and `arena_churn` prints both numbers so the gap cannot reopen
|
||||
unnoticed.
|
||||
marking. Layout can also write a provisional instance and restore it within
|
||||
one frame; `Primitives` remembers the pre-frame bytes and cancels that dirty
|
||||
bit when the GPU-visible result is unchanged. `arena_churn` prints both
|
||||
numbers so either gap cannot reopen unnoticed.
|
||||
|
||||
**Layout has no measurement mode.** A widget is drawn provisionally only
|
||||
when its size cannot be known yet, and that retained drawing is moved into
|
||||
@@ -685,12 +687,16 @@ Measured over the fixture's 401 streamed events: the busiest frame makes
|
||||
Streamed-frame CPU p50 is 0.35ms, from 1.18ms before this layout change.
|
||||
Arena size and upload floors are unchanged.
|
||||
|
||||
**What is left, and it is a layout question rather than an upload one.**
|
||||
Stream instances upload 71.9%, against a 71.8% floor: the list is pinned to
|
||||
the newest end, so a growing reply moves every row, and a row's instances
|
||||
carry an absolute region. Moving a subtree is supposed to be one
|
||||
`move_offsets` write (LAYOUT.md section 2); something on this path is
|
||||
redrawing instead.
|
||||
Pinned growth now uses the same subtree translation as scrolling. A
|
||||
container can retain a child-coordinate move slot through
|
||||
`Painter::set_child_offset`; `LazySpan` keeps retained rows in stable local
|
||||
boxes and changes that one slot when its anchor moves. It still walks the
|
||||
visible run to virtualise it, but unchanged rows no longer acquire new
|
||||
absolute primitive regions. Over the fixture's 401 streamed events, instance
|
||||
upload is **2.9% against a 2.9% floor**, from 71.9% against 71.8%; median
|
||||
instance bytes per frame are **1,728**, from 176,496. This is framework
|
||||
layout/rendering behaviour and the transcript screen contains no special
|
||||
case for it.
|
||||
|
||||
### The Android release profile is `opt-level = 3`, not `"s"` (2026-09-09)
|
||||
|
||||
|
||||
+12
-9
@@ -272,18 +272,21 @@ cannot pan; there is a `debug_assert` in `drag` naming that.
|
||||
|
||||
## Measurements worth not re-taking
|
||||
|
||||
- A settled scroll tick of a `LazySpan` with 31 rows on screen:
|
||||
**1 real draw and 31 move-slot writes**, no primitive rewrites and no
|
||||
text reshaped. An idle frame is `(0, 0, 0, 0)` — `draw_inner` does not
|
||||
even enter the widget. This is the number any "store the edges and only
|
||||
recompute what changed" optimisation would have to beat, and it is why
|
||||
the walk was left alone.
|
||||
- A settled scroll tick of a `LazySpan`, for 20, 200 or 2,000 total rows:
|
||||
**1 real draw and 1 child-coordinate move-slot write**, no primitive
|
||||
rewrites and no text reshaped. The visible-row walk remains: it is what
|
||||
admits and retires rows at the viewport boundary, and a newly admitted row
|
||||
has real initial-placement work of its own. An idle frame is `(0, 0, 0,
|
||||
0)` — `draw_inner` does not even enter the widget.
|
||||
- A fully hinted `Span` draws each child once. Unknown fixed children draw
|
||||
provisionally and move; region-dependent children redraw if their final
|
||||
box has a different size.
|
||||
- The one design that would collapse those 31 moves into a single delta
|
||||
write is moving the content as a unit, which needs a content length —
|
||||
which a lazy layout cannot supply.
|
||||
- Moving the currently retained run as a unit does **not** require the lazy
|
||||
span's unknowable total content length. Its anchor supplies the relation
|
||||
between stable local row boxes and their desired screen boxes; one retained
|
||||
child-coordinate slot carries that translation. The offset is occasionally
|
||||
rebased after 65,536 pixels to preserve `f32` precision, a rare O(visible)
|
||||
move-slot pass rather than steady-state work.
|
||||
|
||||
## Tests that pin the behaviour
|
||||
|
||||
|
||||
Reference in new issue
Block a user