Compare commits

...
Author SHA1 Message Date
iris-aiandClaude Fable 5.1 62a16b5608 Re-ask a dirty widget at its offer locally instead of deferring to its parent
A local redraw refused any widget whose given box was not as long as its
offer and marked its parent instead. Under the frame/extent protocol that
is nearly every widget beneath a self-sized container: a span hands its
children its own placement across itself, which is `FULL` while the span
is measured and its answer once it is placed, so the children's offer and
given frames differ on every such axis. A `many` frame at seed 13, depth
8 escalated 43 marks along chains up to seven levels and redrew 508 of
583 active widgets where e44dea3 redraws 159.

Retain the offer's frame beside the given one and ask the offer question
locally: the offer frame composed where the given one is, at the offer's
lengths and placement, then place at the given box where the two differ.
Seed 13 `many` goes from 4.73 ms to 1.28 ms against e44dea3's 0.90, and
294 distinct widgets a frame; size, scroll and repaint are unchanged.

Not sound yet: the suite, the debug oracle and the shrinker at 400 trees
of depth 5 pass, but the oracle at 1000 seeds of depth 6 diverges on seed
532 under reorder and seed 398 under every-size. Both reduce to a
self-sized container whose answer changes under a local redraw; the
reduced plans are in docs/HANDOFF.md of ai-app-2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 20:51:36 -04:00
3 changed files with 46 additions and 14 deletions

No files matched your search

+3
View File
@@ -18,6 +18,9 @@ pub struct ActiveData {
/// The original frame in its parent widget's coordinates. Recomposition /// The original frame in its parent widget's coordinates. Recomposition
/// and pixel-length evaluation both follow this chain. /// and pixel-length evaluation both follow this chain.
pub given_region: UiRegion, pub given_region: UiRegion,
/// The frame it was first asked in, in the same coordinates: the offer's
/// frame, which its parent's placing draw may since have narrowed.
pub offer_region: UiRegion,
/// The lengths of the box its parent first asked about it in, as /// 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 /// 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 /// was given was decided knowing its answer, so this is the question
+10
View File
@@ -271,6 +271,14 @@ impl<'a> Painter<'a> {
.get(&id.id()) .get(&id.id())
.map_or(given_len, |a| a.offer_len), .map_or(given_len, |a| a.offer_len),
}; };
let offer_region = match first_ask {
true => local,
false => self
.state
.active
.get(&id.id())
.map_or(local, |a| a.offer_region),
};
let offer_placement = if first_ask { let offer_placement = if first_ask {
placement placement
} else { } else {
@@ -295,6 +303,7 @@ impl<'a> Painter<'a> {
region_node, region_node,
mask: self.mask, mask: self.mask,
given_region: local, given_region: local,
offer_region,
offer_len, offer_len,
offer_placement, offer_placement,
px, px,
@@ -427,6 +436,7 @@ impl<'a> Painter<'a> {
self.offered.push(child.id()); self.offered.push(child.id());
let active = self.state.active.get_mut(&child.id()).unwrap(); let active = self.state.active.get_mut(&child.id()).unwrap();
active.offer_len = local.size(); active.offer_len = local.size();
active.offer_region = local;
active.offer_placement = placement; active.offer_placement = placement;
} }
let placement = UiRegion { let placement = UiRegion {
+33 -14
View File
@@ -24,6 +24,8 @@ pub(super) struct DrawInfo {
pub given_region: UiRegion, pub given_region: UiRegion,
/// The original offer's lengths relative to the parent's own offer. /// The original offer's lengths relative to the parent's own offer.
pub offer_len: UiVec2, pub offer_len: UiVec2,
/// The offer's frame in the parent widget's coordinates.
pub offer_region: UiRegion,
pub offer_placement: [Option<UiSpan>; 2], pub offer_placement: [Option<UiSpan>; 2],
/// This ask's box in pixels, and the offer's: one multiply from the /// This ask's box in pixels, and the offer's: one multiply from the
/// parent's own, which is where every pixel length in layout comes from. /// parent's own, which is where every pixel length in layout comes from.
@@ -134,6 +136,7 @@ impl UiRenderState {
region_node: false, region_node: false,
mask: MaskIdx::NONE, mask: MaskIdx::NONE,
given_region: region, given_region: region,
offer_region: region,
offer_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2], offer_placement: [None; 2],
px, px,
@@ -274,6 +277,7 @@ impl UiRenderState {
// same question again from these. // same question again from these.
active.region = region; active.region = region;
active.given_region = info.given_region; active.given_region = info.given_region;
active.offer_region = info.offer_region;
active.offer_len = info.offer_len; active.offer_len = info.offer_len;
if info.placement == info.offer_placement && info.px == info.offered_px { if info.placement == info.offer_placement && info.px == info.offered_px {
active.answer = Some(answer); active.answer = Some(answer);
@@ -478,6 +482,7 @@ impl UiRenderState {
region_node: false, region_node: false,
mask, mask,
given_region: UiRegion::FULL, given_region: UiRegion::FULL,
offer_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2], offer_placement: [None; 2],
px, px,
@@ -495,6 +500,7 @@ impl UiRenderState {
region, region,
placement, placement,
given_region: info.given_region, given_region: info.given_region,
offer_region: info.offer_region,
offer_len: info.offer_len, offer_len: info.offer_len,
offer_placement: info.offer_placement, offer_placement: info.offer_placement,
// Whoever asked writes the answer, if this was the asking. // Whoever asked writes the answer, if this was the asking.
@@ -724,6 +730,7 @@ impl UiRenderState {
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.region = region; active.region = region;
active.given_region = info.given_region; active.given_region = info.given_region;
active.offer_region = info.offer_region;
active.offer_len = info.offer_len; active.offer_len = info.offer_len;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
@@ -795,6 +802,7 @@ impl UiRenderState {
region_node: active.move_idx != active.parent_move, region_node: active.move_idx != active.parent_move,
mask, mask,
given_region: child_local, given_region: child_local,
offer_region: active.offer_region,
offer_len: active.offer_len, offer_len: active.offer_len,
offer_placement: active.offer_placement, offer_placement: active.offer_placement,
px: child_local.size().to_px(info.px), px: child_local.size().to_px(info.px),
@@ -934,6 +942,7 @@ impl UiRenderState {
region: UiRegion::FULL, region: UiRegion::FULL,
placement: UiRegion::FULL, placement: UiRegion::FULL,
given_region: UiRegion::FULL, given_region: UiRegion::FULL,
offer_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2], offer_placement: [None; 2],
answer: None, answer: None,
@@ -1170,16 +1179,6 @@ impl UiRenderState {
return true; return true;
}; };
let (given_px, offered_px) = self.asked_px(id); let (given_px, offered_px) = self.asked_px(id);
// Asked again in the box its parent gave it, which is the question
// its parent asked only while that box is as long as the offer. Any
// other box is a different question, so the parent asks it, with the
// mark left on. Lengths and not whole boxes: what a drawing depends
// on is its lengths, so the same lengths elsewhere is one question.
if given_px != offered_px {
self.mark(id, rsc.widgets_mut());
self.mark(parent, rsc.widgets_mut());
return false;
}
let info = DrawInfo { let info = DrawInfo {
layer: active.layer, layer: active.layer,
parent: active.parent, parent: active.parent,
@@ -1188,6 +1187,7 @@ impl UiRenderState {
region_node: rsc.widgets().is_region_node(id), region_node: rsc.widgets().is_region_node(id),
mask: active.parent_mask, mask: active.parent_mask,
given_region: active.given_region, given_region: active.given_region,
offer_region: active.offer_region,
offer_len: active.offer_len, offer_len: active.offer_len,
offer_placement: active.offer_placement, offer_placement: active.offer_placement,
px: given_px, px: given_px,
@@ -1202,14 +1202,33 @@ impl UiRenderState {
diag::bump(Counter::LocalRedraws); diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc); let old = self.remove(id, false, rsc);
// Refresh the original measurement before restoring the assigned slot. // Asked again where its parent asked: the offer's frame, composed
// Its lengths may differ even though the fraction reference is unchanged. // where the given one is, at the offer's lengths and placement. That
// is the question its answer came from, whatever box the parent then
// chose from the answer -- which is often a different frame, since a
// span hands its children its own placement across itself. The
// parent draws in its own frame, or in `FULL` where it is a region
// node.
let parent_frame = match self.active.get(&parent) {
Some(p) if p.move_idx == p.parent_move => p.region,
_ => UiRegion::FULL,
};
let offer_frame = match info.offer_region == UiRegion::FULL {
true => parent_frame,
false => info.offer_region.within(&parent_frame),
};
let offered = DrawInfo { let offered = DrawInfo {
placement: info.offer_placement, placement: info.offer_placement,
given_region: info.offer_region,
px: offered_px,
..info ..info
}; };
let answer = self.draw_inner(id, given, offered, old, false, rsc); // Where the given differs from the offer, the first draw is only the
if info.placement != offered.placement { // measurement and the second puts the drawing where the parent did.
let placed_apart =
info.placement != offered.placement || info.px != offered.px || given != offer_frame;
let answer = self.draw_inner(id, offer_frame, offered, old, placed_apart, rsc);
if placed_apart {
self.draw_inner(id, given, info, None, false, rsc); self.draw_inner(id, given, info, None, false, rsc);
} }
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();