Answer "have I asked this child?" in one read

A container's draw asked it once per child by searching the list of children
it had added so far, and four other per-child steps searched a list too, so
one draw cost the square of its children: 70% of a 1,600-child redraw was
those searches. A draw takes a DrawId and leaves it on every widget it asks
about; one note per widget is enough because the handle a container holds a
child by cannot be cloned. tests/children_cost.rs is the rig that shows it,
and it is the only one here that varies width: 3.680 ms to 0.811 ms at 1,600
children, and flat per child.

Beside it, the rest of the fourteenth sweep of #19: a mask's rectangle
resolved once per fragment instead of once per instance, which takes the
storage buffers out of the fragment stage and is 8.8x on a screenful of
deeply nested clips; TextBuffer::shape copying its attrs before the check
that would not need them, which allocated once per named-family text per
frame; a should_panic test on a debug assertion that made cargo test
--release fail; Fixed::div, reached only by its own test; Moves::remove
re-uploading an array it cannot have changed; and two comments the branch
itself falsified.

docs/LAYOUT_LOG.md has all eight with their measurements, the five things
looked at and left, and what was verified.
This commit is contained in:
iris-ai committed 2026-09-20 23:56:12 -04:00
1 parent cbccfb600a
commit 97fca76108
17 files changed
+588 -149

No files matched your search

+37 -5
View File
@@ -1,9 +1,9 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::{
ActiveData, Answer, Axis, Bounds, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len,
MaskIdx, MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Size, StrongWidget, UiRegion,
UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
ActiveData, Answer, Axis, Bounds, Declared, DrawId, DrawLayers, IdLike, LayoutHolds, LayoutLen,
Len, MaskIdx, MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Size, StrongWidget,
UiRegion, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
ui::painter::Ask,
util::{HashMap, Vec2},
};
@@ -97,6 +97,10 @@ pub struct UiRenderState {
pending: std::collections::BinaryHeap<(usize, WidgetId)>,
pub(super) requests: crate::RequestArena,
changed: Vec<WidgetId>,
/// The last draw id handed out. Each draw takes a fresh one and leaves it
/// on every widget it asks about, which is how it knows in one read
/// whether it has asked already.
last_draw: DrawId,
request_readers: HashMap<WidgetId, crate::util::HashSet<WidgetId>>,
pub moves: Moves,
}
@@ -113,6 +117,7 @@ impl UiRenderState {
pending: Default::default(),
requests: Default::default(),
changed: Vec::new(),
last_draw: DrawId::NONE,
request_readers: Default::default(),
moves: Default::default(),
resized: false,
@@ -322,6 +327,14 @@ impl UiRenderState {
old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
) -> Answer {
let draw = self.next_draw();
// Whoever asked about this widget wrote this, and a draw of the widget
// itself is not that: the record is rebuilt below, so it is carried
// across rather than reset.
let asked_by = old
.as_ref()
.or_else(|| self.active.get(&id))
.map_or(DrawId::NONE, |active| active.asked_by);
let rel_base = info.rel_base;
let (move_idx, region, retired_move) = match info.region_node {
// A node entry is only a translation. Its local box keeps the
@@ -383,6 +396,7 @@ impl UiRenderState {
answer_under: LayoutHolds::ANY,
depth: info.depth,
move_idx,
draw,
rsc,
};
@@ -419,6 +433,7 @@ impl UiRenderState {
layer,
own_layer: _,
depth: _,
draw: _,
id,
} = painter;
@@ -507,7 +522,7 @@ impl UiRenderState {
region.to_px(window),
);
for c in &old_children {
if !children.contains(c) {
if !self.asked_in(*c, draw) {
self.undraw_rec(*c, rsc);
}
}
@@ -531,7 +546,7 @@ impl UiRenderState {
// and a change there has to reach it. Asking answered whatever mark
// it had: a hint is read live, and a drawing is not kept past one.
for &dep in &size_deps {
if !children.contains(&dep) {
if !self.asked_in(dep, draw) {
self.asked(
dep,
DrawInfo {
@@ -574,6 +589,7 @@ impl UiRenderState {
region,
// Whoever asked writes the answer.
answer: None,
asked_by,
re_asked: info.re_asked,
size,
holds,
@@ -603,6 +619,21 @@ impl UiRenderState {
}
}
fn next_draw(&mut self) -> DrawId {
self.last_draw = self.last_draw.next();
self.last_draw
}
/// Whether `draw` has asked about this widget, which is what it left on
/// the widget's own record when it did. One widget is asked about by one
/// container, since the handle a container holds a child by cannot be
/// cloned, so one note per widget is enough to answer this.
pub(super) fn asked_in(&self, id: WidgetId, draw: DrawId) -> bool {
self.active
.get(&id)
.is_some_and(|active| active.asked_by == draw)
}
/// Keeps a region node's entry across redraws because descendants retain
/// its index.
fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx {
@@ -956,6 +987,7 @@ impl UiRenderState {
asked: PlaceDesc::WHOLE,
region: UiRegion::FULL,
answer: None,
asked_by: DrawId::NONE,
re_asked: false,
size,
holds: LayoutHolds::ANY,