Measure a child on the layer it draws on, not twice on two

Fixed point cost 3x in layout: `many` went from 0.179 ms a frame to 0.544,
and `scroll` from 0.011 to 0.030. The counters said why -- eight more "placed
by redrawing" a frame -- and the reason was mine rather than the grid's. A
retained drawing belongs to the layer it was made on, which `4e28f10` started
enforcing, and `Stack` measures the child that sizes it by drawing it on its
own layer and then draws it again on the child layer. So every stacked child
redrew twice a frame, forever.

`Painter::child_layer_at` addresses a child's layer rather than walking to
it, and `Stack` measures on the layer that child ends up on. The second ask
is then a reuse. Its glyphs are written once rather than once under the
background and once over it.

Measured on the same fixture: `scroll` 0.031 ms to 0.020, `many` 0.570 to
0.283, and the scroll phase's counters are back to what they were before
fixed point -- 4 widget draws and 12 draw requests a frame, exactly. What is
left above that baseline is not this.

`ReuseOutcome` could not say "another layer" or "the region-node choice
changed"; both returned without a counter, which is why the first look at
this said nothing. They have counters now.

Checked: fmt, clippy, 105 tests, five shrinker cases at 300 seeds, 100
generated seeds, and the examples byte-identical but for 36 pixels of
`random` at one level -- edges that were being drawn twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-16 04:04:31 -04:00
1 parent 95fb4f962c
commit 97cc8b32ed
5 files changed
+73 -20

No files matched your search

+37 -13
View File
@@ -560,13 +560,43 @@ fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
assert_corners!(h, fixed, (200, 0), (280, 200));
}
/// `Stack` measures the child that sizes it by drawing it, then draws it
/// again above the background it stacks over. The second ask is for the same
/// box, so nothing geometric says the answer has gone stale, and reusing it
/// leaves the drawing under the background. The same tree got away with it
/// while the two asks differed by a rounding.
/// A retained drawing belongs to the layer it was made on: asked for again
/// on another one it has to be drawn there, since nothing about its geometry
/// says it is in a list that paints at a different moment.
#[test]
fn a_widget_asked_again_on_another_layer_is_drawn_there() {
/// Draws its child on its own layer, then again one layer in -- which is
/// what a container measuring a child by drawing it used to do.
struct Twice(StrongWidget);
impl Widget for Twice {
fn draw(&mut self, painter: &mut Painter) -> Size {
let size = painter.widget(&self.0).size();
painter.child_layer();
painter.widget(&self.0);
size
}
}
let mut h = Harness::new((400, 200));
let (front, draws) = counted(&mut h, Size::from((100, 50)), false);
let outer = Twice(front.add_strong(&mut h.rsc)).add(&mut h.rsc);
h.set_root(outer);
h.frame();
assert_ne!(
h.render.active[&front.id()].layer,
h.render.active[&outer.id()].layer,
"the first drawing was kept, on the layer it was measured on"
);
assert_eq!(draws.get(), 2, "the second ask could not reuse the first");
}
/// Which is why `Stack` measures the child that sizes it on the layer that
/// child draws on: one drawing, above the background it stacks over, rather
/// than one on each layer and the wrong one kept.
#[test]
fn a_stacks_sizing_child_is_drawn_once_where_it_belongs() {
let mut h = Harness::new((400, 200));
let background = rect(Color::RED).add(&mut h.rsc);
let (front, draws) = counted(&mut h, Size::from((100, 50)), false);
@@ -582,13 +612,7 @@ fn a_widget_asked_again_on_another_layer_is_drawn_there() {
h.frame();
let layer = |id| h.render.active[&id].layer;
assert_ne!(
layer(front.id()),
layer(stack.id()),
"the measured drawing was left on the stack's own layer"
);
assert_ne!(layer(front.id()), layer(stack.id()));
assert_ne!(layer(front.id()), layer(background.id()));
// Measured once for the size and once where it goes, which is what the
// stack costs and not something this test is asserting a number for.
assert_eq!(draws.get(), 2);
assert_eq!(draws.get(), 1);
}