Move LazySpan rows through one retained offset
This commit is contained in:
1 parent
992482414f
commit
f95835593f
8 files changed
+453
-50
No files matched your search
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in new issue
Block a user