iris: a measurement is a mode on the painter, not a discarded draw
Painter::draw_twice(child, first, |used| second) becomes Painter::measure plus an ordinary draw. Iris's objection was the shape it forced on the caller rather than the two draws themselves: the arithmetic that picks the real region had to happen inside a closure, and anything it wanted to keep came back out through a captured &mut. LazySpan::place was the only caller, and it now reads as the three statements it is. DrawMode::Measure is that draw with everything it writes switched off -- no arena slot, no mask, no move slot, nothing left in `active`, nothing marked dirty. Only the returned Size survives, and the widget is left exactly as it was, so the real draw that follows is an ordinary first draw or redraw. That last part is load-bearing: a measurement that left an ActiveData behind would let the following draw hit draw_inner's "already at this region" fast path and return having drawn nothing. A measurement also does not consume a redraw mark, since it is not the redraw the mark asked for, and it takes none of the fast paths, since "already drawn here" cannot report a size. Every Painter method that writes now returns early on the mode -- a widget's own draw never checks, which is the point. A debug_assert at the end of draw_inner catches one that forgot, because the failure otherwise is a single leaked primitive per measured widget per frame, which a screen redrawn every frame turns into an arena that grows without bound. What this is worth, and what it is not. The amplification it applies to, measured on a streamed frame: 1,083 Widget::draw calls over 113 distinct widgets, with the worst drawn 11 times at nesting depth 7-8 -- it is not two draws but two to the power of how many measuring ancestors a widget has. Only the writes go away; the walk and the region arithmetic still happen 11 times, and removing those needs a size answerable without a draw, which LAYOUT.md section 5 rules out. Streamed frame p50 1.39ms -> 1.22ms, p99 4.75ms -> 3.58ms. The upload numbers do not move, because slot recycling had already made the discarded writes free in arena terms. Also extracts move_slot_for from draw_inner, since measuring must not allocate one and the reuse-in-place rule wanted saying once. Verified: run-tests.sh, iris's suite, clippy and rustfmt clean, and the headless phone render is byte-identical to the previous commit's on the real GPU (Venus, RX 7900 XT -- checked, not llvmpipe).
This commit is contained in:
1 parent
4fb369fdd0
commit
2540f6517c
4 files changed
+204
-75
No files matched your search
@@ -119,7 +119,7 @@
|
||||
//! placement of that row -- not merely an optimisation: see `place`'s doc
|
||||
//! for why a row that fills whatever it is offered (a `.background(rect
|
||||
//! (...))`) needs this to ever be placed at the right size at all, and why
|
||||
//! reusing `draw_twice` every frame instead would defeat `draw_inner`'s own
|
||||
//! re-measuring every frame instead would defeat `draw_inner`'s own
|
||||
//! skip-or-move caching. Only a row's first-ever appearance pays the
|
||||
//! two-draw measurement; nothing here estimates a height for an off-screen
|
||||
//! row that has never been measured, so this stays independent of how many
|
||||
@@ -1014,7 +1014,7 @@ impl LazySpan {
|
||||
/// region. A row seen for the first time has no cached height to place
|
||||
/// it *at*, so it is measured first (an oversized, fixed-size region)
|
||||
/// and then drawn a *second* time at the tight box that measurement
|
||||
/// implies, via `Painter::draw_twice` -- not `reposition` (a pure
|
||||
/// implies, via `Painter::measure` -- not `reposition` (a pure
|
||||
/// translation, no resize). This distinction is required, not just an
|
||||
/// optimisation: a row is not always plain wrapped text --
|
||||
/// `.background(rect(tint))` is an ordinary way to style one, and
|
||||
@@ -1023,7 +1023,7 @@ impl LazySpan {
|
||||
/// Measuring such a row at the oversized box has it paint an oversized
|
||||
/// rect there; `reposition` only ever writes an offset, never a size,
|
||||
/// so an every-frame reposition-only scheme would leave that primitive
|
||||
/// oversized forever. Using `draw_twice` for *every* frame would fix
|
||||
/// oversized forever. Measuring on *every* frame would fix
|
||||
/// that but break the opposite property: its two calls use two
|
||||
/// different regions, so whichever one `ActiveData.region` ends up
|
||||
/// holding always disagrees with the *next* frame's first call,
|
||||
@@ -1116,8 +1116,9 @@ impl LazySpan {
|
||||
}
|
||||
// Never measured, so there is no height to place it at: it is
|
||||
// measured at an oversized region first and drawn again at the
|
||||
// box that measurement implies (`draw_twice`, not
|
||||
// `reposition`, which writes an offset and never a size).
|
||||
// box that measurement implies (`Painter::measure` then a
|
||||
// real draw, not `reposition`, which writes an offset and
|
||||
// never a size).
|
||||
//
|
||||
// A bottom-known row measures at a *zero-anchored* region
|
||||
// rather than at its own box: using the real box would make
|
||||
@@ -1132,12 +1133,9 @@ impl LazySpan {
|
||||
Placement::Trailing(_) => 0.0,
|
||||
};
|
||||
let first = Self::abs_region(dir, measure_from, measure_from + GENEROUS_PADDING);
|
||||
let mut height = 0.0;
|
||||
painter.draw_twice(widget, first, |used| {
|
||||
height = resolve(used);
|
||||
let (lead, trail) = placement.edges(height);
|
||||
Self::abs_region(dir, lead, trail)
|
||||
});
|
||||
let height = resolve(painter.measure(widget, first));
|
||||
let (lead, trail) = placement.edges(height);
|
||||
painter.widget_within(widget, Self::abs_region(dir, lead, trail));
|
||||
height
|
||||
}
|
||||
};
|
||||
@@ -2102,7 +2100,7 @@ mod tests {
|
||||
/// `ReplaceLast` case drives up to 400 times during a streamed reply
|
||||
/// (`bench_client.rs`'s stream phase): the last slot's widget is
|
||||
/// swapped for a brand-new one, same key, and (since a fresh widget
|
||||
/// has no cached height) placed via `place`'s `draw_twice` path every
|
||||
/// has no cached height) placed via `place`'s measure-then-draw path every
|
||||
/// time -- the provisional-then-real two-draw sequence LAYOUT.md
|
||||
/// documents as the one place in this crate that deliberately draws a
|
||||
/// widget twice. If `draw_inner`'s old-children diffing or
|
||||
@@ -2131,7 +2129,7 @@ mod tests {
|
||||
|
||||
let before = render.active_widgets();
|
||||
for i in 0..400u32 {
|
||||
// A varying height keeps every replace on the `draw_twice`
|
||||
// A varying height keeps every replace on the measure-then-draw
|
||||
// (cache-miss) path rather than settling into the O(1)
|
||||
// same-size `mov` fast path once the height happens to repeat.
|
||||
let (_bg_id, new_row) = background_styled_row(&mut rsc, 20.0 + (i % 3) as f32);
|
||||
@@ -2171,7 +2169,7 @@ mod tests {
|
||||
///
|
||||
/// Two rows, two shapes of the same fault: row 2 has a cached height
|
||||
/// (one `widget_within`), row 4 is replaced so it has none (`place`'s
|
||||
/// `draw_twice`, which reaches `draw_inner` twice for one id in one
|
||||
/// measure-then-draw, which reaches `draw_inner` twice for one id in one
|
||||
/// frame and so orphans a copy even with no ancestor involved).
|
||||
#[test]
|
||||
fn an_ancestor_redrawing_a_dirty_row_leaves_no_stale_copy() {
|
||||
|
||||
Reference in new issue
Block a user