Let OnResize answer for the window too, and delete the second rule
A resize had its own mechanism: `reads_output` recorded that a widget had looked at the output, `update` scanned every active widget for one whose `output_px` had moved, marked it and its whole reader chain, and `resize_marks` kept those marks from counting as content dirtiness -- while `on_resize` answered the same question for every other box. Two answers to "does this drawing survive its box changing length", and the one that applied to the window ignored what the widget had declared. With the output held as the root of the chain there is one question. A resize offers the root widget its box again, `try_reuse` answers per axis from `on_resize`, and `redraws_under` prices the subtree. Gone with it: `reads_output`, `resized`, `resize_marks`, the scan, the eager reader marking, and the shallowest-first branch in `redraw_updates`, which only existed because resize marking worked differently -- the settle loop now has one order. Two things this needed. An unslotted widget may be reused when only its parent's box changed length: it has nothing of its own to write, and what it drew is a fraction of that box, so the slot above it already carries the change. And `root_readers` holds the widgets whose size came from the output rather than their own box -- `MaxSize` -- since no box of theirs need have changed; they are marked per axis, from a set kept as they draw rather than by scanning. `a_resize_does_not_redraw_what_the_shader_can_move` now says `Scale`, which is what it was always describing, and `a_resize_redraws_what_does _not_scale` is its other half. `ReadsWidth` declares `Scale` across the axis it does not read, so per-axis precision comes from the widget rather than from which output axis it happened to touch. Resize phase, seed 1 depth 8: 6.45M instructions per frame to 5.84M. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9f4311774b
commit
ef815dadfd
4 files changed
+105
-81
No files matched your search
@@ -31,8 +31,6 @@ pub struct ActiveData {
|
||||
pub size_output_inputs: [bool; 2],
|
||||
/// The output dimensions against which those dependencies were observed.
|
||||
pub output_px: Vec2,
|
||||
/// Output axes it read directly or while resolving its offered box.
|
||||
pub reads_output: [bool; 2],
|
||||
/// The slot its primitives are positioned through: its own if its parent
|
||||
/// placed it, otherwise the nearest ancestor that has one.
|
||||
pub move_idx: MoveIdx,
|
||||
|
||||
@@ -26,7 +26,6 @@ pub struct Painter<'a> {
|
||||
/// Offered pixel axes which can affect the size this draw reports.
|
||||
pub(super) size_box_inputs: [bool; 2],
|
||||
pub(super) size_output_inputs: [bool; 2],
|
||||
pub(super) reads_output: [bool; 2],
|
||||
/// The slot this widget's primitives are positioned through: its own if
|
||||
/// its parent placed it, otherwise the nearest ancestor that has one.
|
||||
pub(super) move_idx: MoveIdx,
|
||||
@@ -286,7 +285,6 @@ impl<'a> Painter<'a> {
|
||||
/// The output's size in pixels. A widget that reads it draws again when
|
||||
/// the output changes, since nothing else can put that right.
|
||||
pub fn output_size(&mut self) -> Vec2 {
|
||||
self.reads_output = [true; 2];
|
||||
self.size_output_inputs = [true; 2];
|
||||
self.state.output_size
|
||||
}
|
||||
@@ -294,7 +292,6 @@ impl<'a> Painter<'a> {
|
||||
/// One axis of the output in pixels. Prefer this to [`Self::output_size`]
|
||||
/// when the other axis cannot affect the size this widget reports.
|
||||
pub fn output_len(&mut self, axis: Axis) -> f32 {
|
||||
self.reads_output[axis as usize] = true;
|
||||
self.size_output_inputs[axis as usize] = true;
|
||||
self.state.output_size.axis(axis)
|
||||
}
|
||||
@@ -303,7 +300,6 @@ impl<'a> Painter<'a> {
|
||||
/// the boxes it sits within, so a widget that reads it draws again when
|
||||
/// the output changes.
|
||||
pub fn px_size(&mut self) -> Vec2 {
|
||||
self.reads_output = [true; 2];
|
||||
self.size_box_inputs = [true; 2];
|
||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
||||
region.size().to_px(self.state.output_size)
|
||||
@@ -312,7 +308,6 @@ impl<'a> Painter<'a> {
|
||||
/// One axis of this widget's box in pixels. Prefer this to
|
||||
/// [`Self::px_size`] when the other axis cannot affect the reported size.
|
||||
pub fn px_len(&mut self, axis: Axis) -> f32 {
|
||||
self.reads_output[axis as usize] = true;
|
||||
self.size_box_inputs[axis as usize] = true;
|
||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
||||
region
|
||||
|
||||
+77
-72
@@ -21,14 +21,12 @@ pub struct UiRenderState {
|
||||
old_root: Option<WidgetId>,
|
||||
/// The slot every chain bottoms out in, holding the output as a box.
|
||||
root_move: MoveIdx,
|
||||
resized: [bool; 2],
|
||||
/// Widgets whose reported size depends on the root box rather than on
|
||||
/// their own, so nothing below them changing length can reach them.
|
||||
root_readers: HashSet<WidgetId>,
|
||||
/// Content/state dirtiness whose retained size cannot answer a layout
|
||||
/// question until that widget has drawn again.
|
||||
invalid_sizes: HashSet<WidgetId>,
|
||||
/// Marks introduced only to traverse resize dependency paths. Unlike
|
||||
/// content dirtiness, these may retain an answer whose observed pixel
|
||||
/// axes did not change.
|
||||
resize_marks: HashSet<WidgetId>,
|
||||
/// What has already been drawn during the pass under way, so a widget
|
||||
/// reached by redrawing an ancestor is not drawn again on its own
|
||||
/// account. Emptied when the pass ends.
|
||||
@@ -46,13 +44,12 @@ impl UiRenderState {
|
||||
layers: Default::default(),
|
||||
output_size: Vec2::ZERO,
|
||||
old_root: None,
|
||||
resized: [false; 2],
|
||||
invalid_sizes: Default::default(),
|
||||
resize_marks: Default::default(),
|
||||
draw_started: Default::default(),
|
||||
slots: Default::default(),
|
||||
moves: Default::default(),
|
||||
root_move: MoveIdx::NONE,
|
||||
root_readers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,14 +69,25 @@ impl UiRenderState {
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||
let size = size.into();
|
||||
for (axis, resized) in AXES.into_iter().zip(self.resized.iter_mut()) {
|
||||
*resized |= size.axis(axis) != self.output_size.axis(axis);
|
||||
}
|
||||
self.output_size = size;
|
||||
self.output_size = size.into();
|
||||
self.write_root();
|
||||
}
|
||||
|
||||
/// Which axes of the root widget's box are no longer the ones the root
|
||||
/// slot holds, which is all a resize now is: one slot written, found by
|
||||
/// the same comparison every other box change is found by.
|
||||
fn root_axes_changed(&self) -> [bool; 2] {
|
||||
let Some(active) = self.old_root.and_then(|root| self.active.get(&root)) else {
|
||||
return [false; 2];
|
||||
};
|
||||
let px = self.px_of(active.parent_move, active.region);
|
||||
let mut changed = [false; 2];
|
||||
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
|
||||
*c = pixel_len_changed(active.px.axis(axis), px.axis(axis));
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
pub fn output_size(&self) -> Vec2 {
|
||||
self.output_size
|
||||
}
|
||||
@@ -92,7 +100,6 @@ impl UiRenderState {
|
||||
self.invalid_sizes.clear();
|
||||
self.invalid_sizes
|
||||
.extend(rsc.widgets().needs_redraw.iter().copied());
|
||||
self.resize_marks.clear();
|
||||
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
||||
// decide whether to panic or not
|
||||
if !rsc.widgets().waiting.is_empty() {
|
||||
@@ -113,53 +120,52 @@ impl UiRenderState {
|
||||
if self.root_changed(root) {
|
||||
self.redraw_all(root, rsc);
|
||||
self.old_root = root.map(|r| r.id());
|
||||
} else if self.resized.iter().any(|&resized| resized) {
|
||||
// A region is a fraction of the output plus an offset, resolved
|
||||
// against the window in the shader, so a resize moves the whole
|
||||
// drawing on its own. Only a widget that read pixels can be wrong.
|
||||
{
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _marking = diag::timer(TimerKind::ResizeMarking);
|
||||
let dependents: Vec<_> = self
|
||||
} else if self.root_axes_changed().iter().any(|&c| c) {
|
||||
// Every box is a part of the root box, so writing it is a box
|
||||
// that changed length like any other. Offering the root widget
|
||||
// its box again puts that through `try_reuse`, which answers per
|
||||
// axis and lets `redraws_under` price the subtree -- rather than
|
||||
// marking it, which would redraw it whichever axis moved. What
|
||||
// that cannot reach is a widget whose size came from the root box
|
||||
// instead of its own, since its own box need not have changed.
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _marking = diag::timer(TimerKind::ResizeMarking);
|
||||
let changed = self.root_axes_changed();
|
||||
for id in self.root_readers.clone() {
|
||||
let reads = self
|
||||
.active
|
||||
.iter()
|
||||
.filter_map(|(&id, active)| {
|
||||
AXES.into_iter()
|
||||
.zip(self.resized)
|
||||
.any(|(axis, changed)| {
|
||||
changed
|
||||
&& active.reads_output[axis as usize]
|
||||
&& pixel_len_changed(
|
||||
active.output_px.axis(axis),
|
||||
self.output_size.axis(axis),
|
||||
)
|
||||
})
|
||||
.then_some(id)
|
||||
})
|
||||
.collect();
|
||||
for id in dependents {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ResizeDependents);
|
||||
rsc.widgets_mut().needs_redraw.insert(id);
|
||||
if let Some(top) = self.mark_readers(id, rsc) {
|
||||
rsc.widgets_mut().needs_redraw.insert(top);
|
||||
}
|
||||
.get(&id)
|
||||
.map_or([false; 2], |active| active.size_output_inputs);
|
||||
if !AXES
|
||||
.into_iter()
|
||||
.zip(changed)
|
||||
.any(|(axis, c)| c && reads[axis as usize])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
self.resize_marks.extend(
|
||||
rsc.widgets()
|
||||
.needs_redraw
|
||||
.iter()
|
||||
.filter(|id| !self.invalid_sizes.contains(id))
|
||||
.copied(),
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ResizeDependents);
|
||||
rsc.widgets_mut().needs_redraw.insert(id);
|
||||
}
|
||||
if let Some(root) = root {
|
||||
self.draw_inner(
|
||||
0,
|
||||
root.id(),
|
||||
UiRegion::FULL,
|
||||
None,
|
||||
1,
|
||||
self.root_move,
|
||||
false,
|
||||
MaskIdx::NONE,
|
||||
None,
|
||||
rsc,
|
||||
);
|
||||
}
|
||||
}
|
||||
if rsc.widgets().has_updates() {
|
||||
self.redraw_updates(rsc);
|
||||
}
|
||||
self.resized = [false; 2];
|
||||
self.invalid_sizes.clear();
|
||||
self.resize_marks.clear();
|
||||
self.draw_started.clear();
|
||||
}
|
||||
|
||||
@@ -242,7 +248,6 @@ impl UiRenderState {
|
||||
depth,
|
||||
size_box_inputs: [false; 2],
|
||||
size_output_inputs: [false; 2],
|
||||
reads_output: [false; 2],
|
||||
move_idx,
|
||||
rsc,
|
||||
};
|
||||
@@ -269,7 +274,6 @@ impl UiRenderState {
|
||||
size_deps,
|
||||
size_box_inputs,
|
||||
size_output_inputs,
|
||||
reads_output,
|
||||
move_idx,
|
||||
layer,
|
||||
depth: _,
|
||||
@@ -297,7 +301,6 @@ impl UiRenderState {
|
||||
size_box_inputs,
|
||||
size_output_inputs,
|
||||
output_px: self.output_size,
|
||||
reads_output,
|
||||
move_idx,
|
||||
parent_move,
|
||||
mask,
|
||||
@@ -310,10 +313,13 @@ impl UiRenderState {
|
||||
}
|
||||
}
|
||||
|
||||
match active.size_output_inputs.iter().any(|&reads| reads) {
|
||||
true => self.root_readers.insert(id),
|
||||
false => self.root_readers.remove(&id),
|
||||
};
|
||||
rsc.on_draw(&active);
|
||||
self.active.insert(id, active);
|
||||
self.invalid_sizes.remove(&id);
|
||||
self.resize_marks.remove(&id);
|
||||
size
|
||||
}
|
||||
|
||||
@@ -390,8 +396,7 @@ impl UiRenderState {
|
||||
}
|
||||
|
||||
fn size_is_invalid(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
||||
self.invalid_sizes.contains(&id)
|
||||
|| (widgets.needs_redraw.contains(&id) && !self.resize_marks.contains(&id))
|
||||
self.invalid_sizes.contains(&id) || widgets.needs_redraw.contains(&id)
|
||||
}
|
||||
|
||||
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
||||
@@ -458,10 +463,14 @@ impl UiRenderState {
|
||||
self.keep_depth(id, depth);
|
||||
return Some(size);
|
||||
}
|
||||
// Only a placed widget can be given a different box without drawing
|
||||
// again: everything it drew is a fraction of its slot's box, so one
|
||||
// entry says where all of it went.
|
||||
if slot == parent_move {
|
||||
// Only a placed widget can be given a different *region* without
|
||||
// drawing again: it has an entry of its own to say where it went,
|
||||
// where an unslotted one shares its parent's and has nothing to
|
||||
// write. Its parent's box changing length is not that -- everything
|
||||
// it drew is a fraction of that box, so the slot already above it
|
||||
// carries the change and `on_resize` below decides whether the
|
||||
// drawing survives it.
|
||||
if slot == parent_move && old_region != region {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
{
|
||||
diag::bump(Counter::ReuseUnslotted);
|
||||
@@ -495,7 +504,9 @@ impl UiRenderState {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
self.moves.set(slot, region);
|
||||
if slot != parent_move {
|
||||
self.moves.set(slot, region);
|
||||
}
|
||||
self.keep_depth(id, depth);
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
active.region = region;
|
||||
@@ -612,7 +623,6 @@ impl UiRenderState {
|
||||
self.root_move = MoveIdx::NONE;
|
||||
self.layers.clear();
|
||||
self.invalid_sizes.clear();
|
||||
self.resize_marks.clear();
|
||||
self.draw_started.clear();
|
||||
rsc.widgets_mut().needs_redraw.clear();
|
||||
rsc.free();
|
||||
@@ -631,10 +641,7 @@ impl UiRenderState {
|
||||
// reader and gives each changing box its final constraints first.
|
||||
while let Some(id) = {
|
||||
let dirty = rsc.widgets().needs_redraw.iter().copied();
|
||||
match self.resized.iter().any(|&resized| resized) {
|
||||
true => dirty.min_by_key(|&id| self.depth(id)),
|
||||
false => dirty.max_by_key(|&id| self.depth(id)),
|
||||
}
|
||||
dirty.max_by_key(|&id| self.depth(id))
|
||||
} {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::QueuePops);
|
||||
@@ -685,7 +692,7 @@ impl UiRenderState {
|
||||
widgets: &Widgets,
|
||||
) -> bool {
|
||||
self.root_changed(root)
|
||||
|| self.resized.iter().any(|&resized| resized)
|
||||
|| self.root_axes_changed().iter().any(|&c| c)
|
||||
|| widgets.has_updates()
|
||||
}
|
||||
|
||||
@@ -723,7 +730,7 @@ impl UiRenderState {
|
||||
/// redraws a widget that's currently active (drawn)
|
||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
||||
self.draw_started.remove(&id);
|
||||
if rsc.widgets().needs_redraw.contains(&id) && !self.resize_marks.contains(&id) {
|
||||
if rsc.widgets().needs_redraw.contains(&id) {
|
||||
self.invalid_sizes.insert(id);
|
||||
}
|
||||
// A widget can only answer whether its size changed by drawing in the
|
||||
@@ -736,9 +743,7 @@ impl UiRenderState {
|
||||
AXES.into_iter()
|
||||
.any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis)))
|
||||
});
|
||||
if (self.resized.iter().any(|&resized| resized) || box_changed)
|
||||
&& let Some(top) = self.mark_readers(id, rsc)
|
||||
{
|
||||
if box_changed && let Some(top) = self.mark_readers(id, rsc) {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::EagerReaderRedraws);
|
||||
self.redraw(top, rsc);
|
||||
|
||||
Reference in new issue
Block a user