Move LazySpan rows through one retained offset

This commit is contained in:
iris committed 2026-09-09 16:15:09 -04:00
1 parent 992482414f
commit f95835593f
8 files changed
+453 -50

No files matched your search

+71 -2
View File
@@ -143,6 +143,13 @@ macro_rules! primitives {
/// compacted, so a `Mask` can hold a slot across frames.
pub struct Primitives {
instances: Vec<PrimitiveInstance>,
/// The value a slot held before its first rewrite since the last
/// upload. Layout may place a widget provisionally and restore it in
/// the same frame; remembering the pre-frame value lets `set_instance`
/// clear that dirty bit instead of uploading a change the GPU never
/// needs to observe. Entries are overwritten on the next clean-to-dirty
/// transition, so no separate end-of-frame sweep is needed.
original_instances: Vec<Option<PrimitiveInstance>>,
assoc: Vec<WidgetId>,
/// Where each slot's [`PrimitiveHandle`] sits in its owner's
/// `ActiveData::primitives` -- the index that makes
@@ -179,6 +186,7 @@ impl Default for Primitives {
fn default() -> Self {
Self {
instances: Default::default(),
original_instances: Default::default(),
assoc: Default::default(),
handle_idx: Default::default(),
freed: Vec::new(),
@@ -251,11 +259,13 @@ impl Primitives {
fn push(&mut self, inst: PrimitiveInstance, id: WidgetId) -> u32 {
let slot = if let Some(i) = self.reusable.pop() {
self.instances[i] = inst;
self.original_instances[i] = None;
self.assoc[i] = id;
self.handle_idx[i] = Self::NO_HANDLE;
i
} else {
self.instances.push(inst);
self.original_instances.push(None);
self.assoc.push(id);
self.handle_idx.push(Self::NO_HANDLE);
self.instances.len() - 1
@@ -342,8 +352,18 @@ impl Primitives {
if bytemuck::bytes_of(&self.instances[slot]) == bytemuck::bytes_of(&inst) {
return;
}
if !self.dirty.contains(slot) {
self.original_instances[slot] = Some(self.instances[slot]);
}
self.instances[slot] = inst;
self.dirty.mark(slot);
if self.original_instances[slot]
.is_some_and(|original| bytemuck::bytes_of(&original) == bytemuck::bytes_of(&inst))
{
self.original_instances[slot] = None;
self.dirty.unmark(slot);
} else {
self.dirty.mark(slot);
}
}
/// Retires a slot, answering the mask it was drawn under so the caller
@@ -392,6 +412,7 @@ impl Primitives {
pub fn clear(&mut self) {
self.dirty.mark_all();
self.instances.clear();
self.original_instances.clear();
self.assoc.clear();
self.handle_idx.clear();
self.freed.clear();
@@ -464,7 +485,13 @@ impl Primitives {
}
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
self.dirty.mark(h.slot as usize);
let slot = h.slot as usize;
// The caller receives unrestricted mutable access, so this path
// cannot tell whether a later write restored the prior bytes. Leave
// the entry dirty rather than letting a stale `set_instance` baseline
// cancel it.
self.original_instances[slot] = None;
self.dirty.mark(slot);
&mut self.instances[h.slot as usize].region
}
}
@@ -785,3 +812,45 @@ impl<T> Deref for PrimitiveVec<T> {
&self.vec
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{UiVec2, Widgets, util::Id};
#[test]
fn an_instance_restored_before_upload_is_clean() {
let mut primitives = Primitives::default();
let mut widgets = Widgets::new();
let widget = widgets.add_strong(());
let owner = widget.id();
let original = PrimitiveInstance {
region: UiRegion::FULL,
binding: RectPrimitive::BINDING,
idx: 0,
mask_idx: MaskIdx::NONE,
move_idx: Id::preset(0),
};
primitives.push(original, owner);
primitives.dirty.clear();
let mut moved = original;
moved.region = moved.region.offset(UiVec2::abs((0.0, 20.0)));
primitives.set_instance(0, moved, owner);
assert!(!primitives.dirty.is_clean());
primitives.set_instance(0, original, owner);
assert!(
primitives.dirty.is_clean(),
"the GPU never observes the provisional position"
);
// A subsequent frame takes its baseline from the value currently in
// the arena, rather than reusing the now-stale original above.
primitives.set_instance(0, moved, owner);
assert!(!primitives.dirty.is_clean());
primitives.dirty.clear();
primitives.set_instance(0, original, owner);
assert!(!primitives.dirty.is_clean());
}
}
+4
View File
@@ -20,6 +20,10 @@ pub struct ActiveData {
pub size: Size,
/// Retained so descendants' parent links stay valid across redraws.
pub move_slot: MoveIdx,
/// The optional coordinate boundary between this widget and its direct
/// children. Descendants retain links to it across redraws, just as they
/// do to `move_slot`.
pub child_move_slot: Option<MoveIdx>,
/// The part of this widget's move delta already folded into `region`.
pub move_applied: Vec2,
}
+6 -5
View File
@@ -18,11 +18,12 @@ pub struct UiData {
pub textures: Textures,
pub text: TextData,
pub masks: TrackedArena<Mask, u32>,
/// One entry per widget ever drawn, forming the parent-linked chain
/// `resolve_move` walks in both shader stages. Allocated once on a
/// widget's first draw and reused for every later redraw of the same
/// id (never reallocated), so a retained descendant's `parent` index
/// never goes stale -- see LAYOUT.md section 2.
/// One entry per widget ever drawn, plus optional child-coordinate
/// boundaries owned by containers. Together they form the parent-linked
/// chain `resolve_move` walks in both shader stages. A widget's ordinary
/// entry is allocated once on its first draw and reused for every later
/// redraw of the same id, so a retained descendant's `parent` index never
/// goes stale -- see LAYOUT.md section 2.
pub move_offsets: TrackedArena<MoveOffset, u32>,
/// Every widget whose [`crate::Widget::tick`] should run before the
/// next frame -- today, a `LazySpan` coasting through a fling. Added by
+49 -4
View File
@@ -1,6 +1,7 @@
use crate::{
Axis, Color, Len, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer,
TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
Axis, Color, Len, MoveOffset, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs,
TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
WidgetId,
render::{
Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive,
PrimitiveHandle, PrimitiveInst, RectPrimitive,
@@ -16,6 +17,7 @@ pub struct Painter<'a> {
pub(super) region: UiRegion,
pub(super) mask: MaskIdx,
pub(super) move_slot: MoveIdx,
pub(super) child_move_slot: Option<MoveIdx>,
/// This widget's retained mask slot.
pub(super) own_mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>,
@@ -217,6 +219,47 @@ impl<'a> Painter<'a> {
self.widget_at(id, region.within(&self.region))
}
/// Translate this widget's children as one retained subtree, in output
/// pixels. The first call must happen before drawing a child, because the
/// slot becomes the parent of every direct child's ordinary move slot.
/// Once retained, it may be updated later in a redraw (for example after
/// measuring a changed child). All deeper descendants inherit it and the
/// CPU hit-test walk resolves the same translation as the shader.
///
/// This offsets the child coordinate space, not this widget: its own
/// primitives and hit region remain fixed. Once allocated, the boundary
/// stays in the chain across redraws; set it to zero to return children to
/// their unshifted positions.
pub fn set_child_offset(&mut self, offset: Vec2) {
let slot = match self.child_move_slot {
Some(slot) => slot,
None => {
assert!(
self.children.is_empty(),
"a child offset must be created before drawing a child"
);
let parent = self.move_slot.idx() as u32;
let slot = self
.rsc
.ui_mut()
.move_offsets
.push(MoveOffset::new([offset.x, offset.y], parent));
// One ref for this widget's ownership and one on the
// up-link. Direct children take their own refs when their
// move slots are allocated.
self.rsc.ui_mut().move_offsets.push_ref(slot);
self.rsc.ui_mut().move_offsets.push_ref(self.move_slot);
self.child_move_slot = Some(slot);
return;
}
};
let next = [offset.x, offset.y];
if self.rsc.ui().move_offsets[slot.idx()].delta != next {
self.rsc.ui_mut().move_offsets.get_mut(slot).delta = next;
self.state.note_move();
}
}
pub fn known_len<W: ?Sized>(&self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
if let Some(len) = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) {
return Some(len.fold_dp(self.density()));
@@ -236,12 +279,13 @@ impl<'a> Painter<'a> {
// call -- would always find nothing. `self.move_slot` is this
// widget's own slot, already known, and always correct regardless
// of insertion order. See `UiRenderState::move_parent_of`.
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
self.state.draw_inner(
self.layer,
id.id(),
region,
Some(self.id),
self.move_slot.idx() as u32,
parent_move_slot.idx() as u32,
self.mask,
Retained::default(),
self.rsc,
@@ -261,12 +305,13 @@ impl<'a> Painter<'a> {
} else if let Some((layer, mask)) = retained {
self.children.push(id.id());
self.rsc.widgets_mut().needs_redraw.insert(id.id());
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
self.state.draw_inner(
layer,
id.id(),
region,
Some(self.id),
self.move_slot.idx() as u32,
parent_move_slot.idx() as u32,
mask,
Retained::default(),
self.rsc,
+25
View File
@@ -99,6 +99,7 @@ pub(crate) struct Retained {
pub region: Option<UiRegion>,
pub children: Vec<WidgetId>,
pub move_slot: Option<MoveIdx>,
pub child_move_slot: Option<MoveIdx>,
pub own_mask: MaskIdx,
pub primitives: Vec<PrimitiveHandle>,
}
@@ -109,6 +110,7 @@ impl Default for Retained {
region: None,
children: Vec::new(),
move_slot: None,
child_move_slot: None,
own_mask: MaskIdx::NONE,
primitives: Vec::new(),
}
@@ -173,6 +175,10 @@ impl UiRenderState {
)
}
pub(super) fn note_move(&mut self) {
self.mov_count += 1;
}
/// Writes a primitive into the arena and, unless it is
/// [`Drawn::No`], into `layer`'s draw order.
pub(super) fn write_primitive<P: Primitive>(
@@ -446,6 +452,7 @@ impl UiRenderState {
region: mut old_region,
children: mut old_children,
move_slot: mut old_move_slot,
mut child_move_slot,
mut own_mask,
primitives: mut recycle,
} = retained;
@@ -498,6 +505,7 @@ impl UiRenderState {
old_region = Some(active.region);
old_children = active.children;
old_move_slot = Some(active.move_slot);
child_move_slot = active.child_move_slot;
own_mask = active.own_mask;
recycle = active.primitives;
} else if self.active.contains_key(&id) {
@@ -512,6 +520,7 @@ impl UiRenderState {
old_region = Some(active.region);
old_children = active.children;
old_move_slot = Some(active.move_slot);
child_move_slot = active.child_move_slot;
own_mask = active.own_mask;
recycle = active.primitives;
}
@@ -533,6 +542,7 @@ impl UiRenderState {
region,
mask,
move_slot,
child_move_slot,
own_mask,
layer,
id,
@@ -577,6 +587,7 @@ impl UiRenderState {
region,
mask: _,
move_slot,
child_move_slot,
own_mask,
textures,
primitives,
@@ -608,6 +619,7 @@ impl UiRenderState {
layer,
size,
move_slot,
child_move_slot,
own_mask,
move_applied: Vec2::ZERO,
};
@@ -804,6 +816,18 @@ impl UiRenderState {
rsc.ui_mut().masks.remove(outer);
}
}
// The self-ownership ref held by this widget's optional
// child-coordinate slot, plus that slot's link to this
// widget's ordinary move slot. Direct children may still
// hold the slot alive until `remove_rec` reaches them, the
// same lifetime shape as `move_slot` immediately below.
if let Some(slot) = active.child_move_slot {
let parent_slot = rsc.ui().move_offsets[slot.idx()].parent;
rsc.ui_mut().move_offsets.remove(slot);
if parent_slot != MoveOffset::NONE_PARENT {
rsc.ui_mut().move_offsets.remove(Id::preset(parent_slot));
}
}
let parent_slot = rsc.ui_mut().move_offsets[active.move_slot.idx()].parent;
rsc.ui_mut().move_offsets.remove(active.move_slot);
if parent_slot != MoveOffset::NONE_PARENT {
@@ -1213,6 +1237,7 @@ impl UiRenderState {
region: Some(active.region),
children: active.children,
move_slot: Some(active.move_slot),
child_move_slot: active.child_move_slot,
own_mask: active.own_mask,
primitives: active.primitives,
},
+31
View File
@@ -45,6 +45,26 @@ impl Dirty {
self.words[word] |= 1 << (i % 64);
}
pub fn contains(&self, i: usize) -> bool {
self.all
|| self
.words
.get(i / 64)
.is_some_and(|word| word & (1 << (i % 64)) != 0)
}
/// Clear one entry that was restored to the value already on the GPU.
/// `all` has no per-entry representation and is used only when every
/// byte must be uploaded regardless of later writes, so it stays set.
pub fn unmark(&mut self, i: usize) {
if self.all {
return;
}
if let Some(word) = self.words.get_mut(i / 64) {
*word &= !(1 << (i % 64));
}
}
/// Everything must be written: the buffer was reallocated (its
/// contents are undefined), or the array was cleared.
pub fn mark_all(&mut self) {
@@ -114,6 +134,17 @@ mod tests {
assert_eq!(marked(&[3, 4, 5], 64, 0), vec![3..6]);
}
#[test]
fn a_restored_entry_can_be_unmarked() {
let mut dirty = Dirty::default();
dirty.mark(3);
dirty.mark(5);
assert!(dirty.contains(3));
dirty.unmark(3);
assert!(!dirty.contains(3));
assert_eq!(dirty.ranges(8, 0), vec![5..6]);
}
#[test]
fn a_run_that_crosses_a_word_boundary_is_one_range() {
assert_eq!(marked(&[62, 63, 64, 65], 128, 0), vec![62..66]);