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 withefb416b, 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 thee44dea3baseline; this is still an experimental continuation, not a production replacement.
This commit is contained in:
1 parent
efb416bbc3
commit
2ed5503717
7 files changed
+162
-203
No files matched your search
+30
-155
@@ -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(®ion), 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()
|
||||
|
||||
Reference in new issue
Block a user