Carry a box in a move slot, not a translation

A slot now holds the box its contents are placed within, 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, so every caller passes
`UiRegion::FULL.offset(delta)` and nothing changes on screen yet: 42 tests
pass and `tabs` at 1920x1200 is byte-identical.

`Moves::resolve` takes the region to compose rather than returning a sum, so
the CPU walk is the same operation the shader performs.

Measured against the translate slot on the same binary with
`tests/chain_cost.rs`, 200k instances: +0.6% at depth 1, +0.5% at 2, +0.8% at
4, then +9.6% at 8 and +32.2% at 64. Free at the depth opt-in slots produce,
which is the next commit; the per-level cost was always the dependent load
rather than the arithmetic.

The identity is `UiRegion::FULL` rather than zero, which `MoveOffset`'s
comment says beside the `Zeroable` that `Pod` requires: a zeroed entry is a
box of no extent and collapses its subtree to a point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-14 12:14:24 -04:00
1 parent db1751fdfd
commit 1f9dc48b80
4 files changed
+69 -48

No files matched your search

+15 -14
View File
@@ -1,7 +1,7 @@
use crate::{
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId,
Widgets,
util::{Arena, Id, TrackedArena, Vec2},
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget,
WidgetId, Widgets,
util::{Arena, Id, TrackedArena},
};
/// How far the shader will walk a move chain. It bounds a malformed cycle
@@ -46,26 +46,27 @@ impl Moves {
self.arena.remove(Id::preset(idx.idx() as u32));
}
/// Sets a slot's translation, in physical pixels, relative to its parent.
pub fn set(&mut self, idx: MoveIdx, delta: Vec2) {
/// Sets the box a slot's contents are placed within, itself given in the
/// coordinates of its parent slot.
pub fn set(&mut self, idx: MoveIdx, region: UiRegion) {
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
if entry.delta != delta {
entry.delta = delta;
if entry.region != region {
entry.region = region;
self.changed = true;
}
}
/// The translation a primitive in `idx` has accumulated, which is the
/// same walk the vertex shader does.
pub fn resolve(&self, idx: MoveIdx) -> Vec2 {
let mut total = Vec2::ZERO;
/// Composes a region held in `idx`'s coordinates down the chain, which is
/// the same walk the vertex shader does.
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
let mut region = local;
let mut at = idx;
for _ in 0..CHAIN_LIMIT {
if at == MoveIdx::NONE {
return total;
return region;
}
let entry = self.arena[at.idx()];
total += entry.delta;
region = region.within(&entry.region);
at = entry.parent;
}
debug_assert!(
@@ -73,7 +74,7 @@ impl Moves {
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
and the shader stops at the same depth"
);
total
region
}
pub fn entries(&self) -> &[MoveOffset] {
+5 -9
View File
@@ -110,7 +110,7 @@ impl UiRenderState {
// draw widget
let move_idx = self.move_slot(id, parent);
self.moves.set(move_idx, Vec2::ZERO);
self.moves.set(move_idx, UiRegion::FULL);
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.insert(id);
@@ -211,7 +211,7 @@ impl UiRenderState {
// coordinates its parent drew, so the chain above applies alike.
let moved =
region.to_px(self.output_size).top_left - old.to_px(self.output_size).top_left;
self.moves.set(slot, moved);
self.moves.set(slot, UiRegion::FULL.offset(moved));
return Some(size);
}
if !self.reusable(id, region, rsc) || !old.stretchable() {
@@ -269,7 +269,7 @@ impl UiRenderState {
*region = region.stretch(&from, &to);
}
active.region = active.region.stretch(&from, &to);
self.moves.set(active.move_idx, Vec2::ZERO);
self.moves.set(active.move_idx, UiRegion::FULL);
// SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) };
for child in children {
@@ -367,12 +367,8 @@ impl UiRenderState {
/// chain above has moved since, which is the walk the vertex shader does.
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let active = self.active.get(&id.id())?;
let moved = self.moves.resolve(active.move_idx);
let region = active.region.to_px(self.output_size);
Some(PixelRegion {
top_left: region.top_left + moved,
bot_right: region.bot_right + moved,
})
let region = self.moves.resolve(active.move_idx, active.region);
Some(region.to_px(self.output_size))
}
/// redraws a widget that's currently active (drawn)