Recompose retained frames exactly and preserve text width validity

Keep each widget's original local frame and replay the same composition
order on reuse. Remove inverse region remapping, including its fixed-frame
fallback that forced otherwise valid subtrees to draw again.

Require exact pixel-region equality in the shared generated oracle. Check
primitive and mask geometry as well as draw reuse when fixed frames resize.
Publish text's retained line-break range, with no upper bound when there
are no soft breaks, and cover widening, explicit newlines, and empty text.

Compared with efb416b, the depth-8 diagnostic rig performs 7-9% fewer widget
evaluations in the affected phases. Uninstrumented release runs use 3.5%
fewer instructions for size changes and 5.0% fewer for resize. Repaint and
scroll use 0.7% and 0.6% more instructions. Container updates remain substantially more expensive than the e44dea3 baseline;
this is still an experimental continuation, not a production replacement.
This commit is contained in:
iris-ai committed 2026-09-17 15:11:03 -04:00
1 parent efb416bbc3
commit 2ed5503717
7 files changed
+162 -203

No files matched your search

+17
View File
@@ -176,6 +176,23 @@ impl TextBuffer {
self.layout_key.as_ref()?.max_width
}
/// Widths covered by the current line breaks, including a wider shaping
/// retained when a later draw requested a narrower box.
pub fn width_holds(&self) -> crate::Holds {
let Some(width) = self.wrap_width() else {
return crate::Holds::ANY;
};
let width = Px::from_f32(width);
let soft_wrapped = self.layout.lines().any(|line| {
matches!(
line.break_reason(),
parley::layout::BreakReason::Regular | parley::layout::BreakReason::Emergency
)
});
let upper = if soft_wrapped { width } else { Px::MAX };
crate::Holds::from(Px::ceil_from_f32(self.layout.width()).min(width)..=upper)
}
pub fn size(&self) -> Vec2 {
Vec2::new(self.layout.width(), self.layout.height())
}
+3 -4
View File
@@ -15,10 +15,9 @@ pub struct ActiveData {
pub region: UiRegion,
/// Where its drawing sits inside that box, in the box's own coordinates.
pub placement: UiRegion,
/// The same box as lengths of its parent's box, which is the one route
/// to a box in pixels: a draw threads these down a level at a time, and
/// [`crate::UiRenderState::redraw`] takes the same steps back up.
pub given_len: UiVec2,
/// The original frame in its parent widget's coordinates. Recomposition
/// and pixel-length evaluation both follow this chain.
pub given_region: UiRegion,
/// The lengths of the box its parent first asked about it in, as
/// lengths of the box the parent was itself offered. Any later box it
/// was given was decided knowing its answer, so this is the question
+1 -1
View File
@@ -258,7 +258,7 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx,
region_node,
mask: self.mask,
given_len,
given_region: local,
offer_len,
offer_placement,
px,
+30 -155
View File
@@ -2,9 +2,9 @@
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{ask_box, declared_lens, placed_box, placed_lens};
use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx,
Moves, Painter, PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan,
UiVec2, Weight, WidgetId, Widgets,
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, Moves,
Painter, PixelRegion, PxVec2, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight,
WidgetId, Widgets,
util::{HashMap, Vec2},
};
@@ -20,11 +20,9 @@ pub(super) struct DrawInfo {
pub parent_move: MoveIdx,
pub region_node: bool,
pub mask: MaskIdx,
/// The box its parent gave it, as lengths of the parent's own box, and
/// the lengths of the box it was first asked about in the same form.
/// Both describe the box the *parent* stated, so the second, placing ask
/// carries them unchanged while its own region is the placement inside.
pub given_len: UiVec2,
/// The frame in the parent widget's coordinates, before composition.
pub given_region: UiRegion,
/// The original offer's lengths relative to the parent's own offer.
pub offer_len: UiVec2,
pub offer_placement: [Option<UiSpan>; 2],
/// This ask's box in pixels, and the offer's: one multiply from the
@@ -121,7 +119,7 @@ impl UiRenderState {
let stands = self
.active
.get(&root)
.is_some_and(|active| active.answers_at(active.given_len.to_px(size)));
.is_some_and(|active| active.answers_at(active.given_region.size().to_px(size)));
if !stands {
widgets.needs_redraw.insert(root);
}
@@ -141,7 +139,7 @@ impl UiRenderState {
parent_move: MoveIdx::NONE,
region_node: false,
mask: MaskIdx::NONE,
given_len: region.size(),
given_region: region,
offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2],
px,
@@ -278,7 +276,7 @@ impl UiRenderState {
// what of that box the answer then took. A local redraw asks the
// same question again from these.
active.region = region;
active.given_len = info.given_len;
active.given_region = info.given_region;
active.offer_len = info.offer_len;
if info.placement == info.offer_placement && info.px == info.offered_px {
active.answer = Some(settled);
@@ -483,7 +481,7 @@ impl UiRenderState {
parent_move: move_idx,
region_node: false,
mask,
given_len: UiVec2::FULL_SIZE,
given_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2],
px,
@@ -500,7 +498,7 @@ impl UiRenderState {
id,
region,
placement,
given_len: info.given_len,
given_region: info.given_region,
offer_len: info.offer_len,
offer_placement: info.offer_placement,
// Whoever asked writes the answer, if this was the asking.
@@ -616,7 +614,7 @@ impl UiRenderState {
Some(parent) => self.asked_px(parent.id),
None => (self.output_size, self.output_size),
};
let px = active.given_len.to_px(parent_px);
let px = active.given_region.size().to_px(parent_px);
let mut offered = active.offer_len.to_px(parent_offer);
for axis in AXES {
// A declared length is resolved by whoever drew the widget, in
@@ -697,14 +695,12 @@ impl UiRenderState {
}
let extent_moved = active.placement != placement;
let moved = active.region != region;
let (answer, old_region, slot) =
((active.size, active.holds), active.region, active.move_idx);
let (answer, slot) = ((active.size, active.holds), active.move_idx);
if moved {
if has_region_node {
self.moves.set(slot, region);
} else {
let remap = RegionRemap::new(old_region, region)?;
self.remap_subtree(id, &remap, info.parent_move, rsc);
self.recompose_subtree(id, region, info.parent_move, rsc);
}
}
if extent_moved {
@@ -713,7 +709,7 @@ impl UiRenderState {
self.redepth(id, info.depth);
let active = self.active.get_mut(&id).unwrap();
active.region = region;
active.given_len = info.given_len;
active.given_region = info.given_region;
active.offer_len = info.offer_len;
#[cfg(feature = "layout-diagnostics")]
{
@@ -785,7 +781,7 @@ impl UiRenderState {
parent_move,
region_node: active.move_idx != active.parent_move,
mask,
given_len: child_local.size(),
given_region: child_local,
offer_len: active.offer_len,
offer_placement: active.offer_placement,
px: child_local.size().to_px(info.px),
@@ -820,44 +816,35 @@ impl UiRenderState {
}
}
/// Re-expresses an ordinary retained subtree in a new parent region.
/// An independently movable descendant needs only its own region changed;
/// its contents stay in that region's coordinate space.
fn remap_subtree(
/// Replays the original local compositions, including their rounding order.
/// A region node terminates the walk because its contents name its slot.
fn recompose_subtree(
&mut self,
id: WidgetId,
remap: &RegionRemap,
region: UiRegion,
parent_move: MoveIdx,
rsc: &mut dyn UiRsc,
) {
let active = self.active.get_mut(&id).unwrap();
active.region = region;
if active.move_idx != parent_move {
let region = remap.apply(active.region);
active.region = region;
self.moves.set(active.move_idx, region);
return;
}
active.region = remap.apply(active.region);
for primitive in &active.primitives {
let handle = &primitive.handle;
*self.layers[handle.layer].region_mut(handle) =
primitive.region.resolve(active.region, active.placement);
primitive.region.resolve(region, active.placement);
}
if let Some(local) = active.mask_region {
rsc.ui_mut().masks.get_mut(active.mask).region =
local.resolve(region, active.placement);
}
let own_mask = (active.mask != active.parent_mask).then_some(active.mask);
let children = active.children.len();
// A mask the widget set itself moves with it; one it inherited
// belongs to the widget that set it, and moves there or not at all.
if let Some(idx) = own_mask {
let mask = rsc.ui_mut().masks.get_mut(idx);
debug_assert_eq!(mask.move_idx, parent_move);
mask.region = active
.mask_region
.unwrap()
.resolve(active.region, active.placement);
}
for index in 0..children {
let child = self.active[&id].children[index];
self.remap_subtree(child, remap, parent_move, rsc);
let local = self.active[&child].given_region;
self.recompose_subtree(child, local.within(&region), parent_move, rsc);
}
}
@@ -933,7 +920,7 @@ impl UiRenderState {
id,
region: UiRegion::FULL,
placement: UiRegion::FULL,
given_len: UiVec2::FULL_SIZE,
given_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2],
answer: None,
@@ -1171,7 +1158,7 @@ impl UiRenderState {
parent_move: active.parent_move,
region_node: rsc.widgets().is_region_node(id),
mask: active.parent_mask,
given_len: active.given_len,
given_region: active.given_region,
offer_len: active.offer_len,
offer_placement: active.offer_placement,
px: given_px,
@@ -1219,118 +1206,6 @@ fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool {
len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len
}
/// A retained region rewritten from one parent box into another. A fixed
/// source extent can be translated but cannot recover fractions for a resize.
#[derive(Clone, Copy)]
struct RegionRemap {
axes: [AxisRemap; 2],
}
/// Moving one axis of a box into another, worked out once for the whole
/// subtree that moves with it. Every part of that subtree is divided by the
/// same extent and placed between the same two ends, so the ends and the
/// divisor belong here rather than in each part's arithmetic.
#[derive(Clone, Copy)]
enum AxisRemap {
/// A box that kept its length carries its parts by moving them, which is
/// exact. Dividing to find the fraction each sits at and multiplying to
/// place it again are two roundings, and they land a step from where
/// growing the tree that way does.
Translate(Len),
/// A box that changed length has to re-express each part as a fraction of
/// the new one, which is what a part of a box means.
Scale(AxisScale),
}
#[derive(Clone, Copy)]
struct AxisScale {
/// What the fraction is measured from, and what divides it. `whole` is
/// the common case of a box spanning the whole of its parent's, where
/// dividing by one is the expensive way to write a subtraction.
start_rel: Rel,
extent: Rel,
whole: bool,
/// `lerp` is `a + (b - a) * fraction`, and both ends are the same for
/// every part, so each is kept as its near end and its span.
from_px: Px,
from_px_span: Px,
to_rel: Rel,
to_rel_span: Rel,
to_px: Px,
to_px_span: Px,
}
impl RegionRemap {
fn new(from: UiRegion, to: UiRegion) -> Option<Self> {
Some(Self {
axes: [AxisRemap::new(from.x, to.x)?, AxisRemap::new(from.y, to.y)?],
})
}
fn apply(&self, region: UiRegion) -> UiRegion {
// A box that only moved carries every part of itself by the same two
// amounts, and that is the common move. Asking it once for the whole
// region is what lets it be eight adds in a row rather than four
// sequences with a branch each -- measured, it is where the time in a
// move goes.
if let [AxisRemap::Translate(x), AxisRemap::Translate(y)] = self.axes {
return region.translated(x, y);
}
UiRegion {
x: self.axes[0].apply_span(region.x),
y: self.axes[1].apply_span(region.y),
}
}
}
impl AxisRemap {
fn new(from: UiSpan, to: UiSpan) -> Option<Self> {
if from.len() == to.len() {
return Some(Self::Translate(to.start - from.start));
}
let extent = from.end.rel - from.start.rel;
// Without a relative extent there is no fraction to re-express: a box
// of fixed length cannot say where its parts sit in a different one.
if extent == Rel::ZERO {
return None;
}
Some(Self::Scale(AxisScale {
start_rel: from.start.rel,
extent,
whole: extent == Rel::ONE,
from_px: from.start.px,
from_px_span: from.end.px - from.start.px,
to_rel: to.start.rel,
to_rel_span: to.end.rel - to.start.rel,
to_px: to.start.px,
to_px_span: to.end.px - to.start.px,
}))
}
fn apply_span(&self, span: UiSpan) -> UiSpan {
UiSpan {
start: self.apply_scalar(span.start),
end: self.apply_scalar(span.end),
}
}
fn apply_scalar(&self, scalar: Len) -> Len {
let scale = match self {
Self::Translate(by) => return scalar + *by,
Self::Scale(scale) => scale,
};
let offset = scalar.rel - scale.start_rel;
let fraction = match scale.whole {
true => offset,
false => offset / scale.extent,
};
let from_px = scale.from_px + scale.from_px_span.mul(fraction);
let to_rel = scale.to_rel + scale.to_rel_span.mul(fraction);
let to_px = scale.to_px + scale.to_px_span.mul(fraction);
Len::from_parts(to_rel, scalar.px - from_px + to_px)
}
}
impl Default for UiRenderState {
fn default() -> Self {
Self::new()