Compare commits

...
4 Commits
Author SHA1 Message Date
iris-ai 750217631d Drop three things nothing reads, and say what a span makes scalable
`Axis::pair` and `RegionAlign::NEAR` arrived on this branch with no caller
and never got one. `Holds::contains` took `&self` where its five siblings on
the same `Copy` pair of pixels take `self`.

The comment beside a span's cross-axis accumulator said a scalable child
"makes Children scalable too"; `Children` names nothing here, and what it
makes scalable is the span.
2026-09-20 01:02:25 -04:00
iris-ai 9b4cc329ce Say the window when the window is why a drawing was refused
`AxisHolds` is four contracts, and `diag::outside` counted three: a refusal
because this window is outside the range the drawing was made for bumped
"reuse outside: a rel base". The two are different questions -- a window
range is pixels, a rel base pin is a window-unit length that an unchanged
window can still change -- so the rig answered "why did that redraw?" with
the wrong one for every resize.
2026-09-20 01:00:13 -04:00
iris-ai 02048eab77 Rebuild a suboptimal swapchain after presenting, not before
`Surface::configure` panics while a texture the surface handed out is still
alive, which wgpu says at both `configure` and `get_current_texture`. The
`Suboptimal` arm configured with the texture it was about to draw with in
hand, so the first suboptimal frame -- a resize or a display change on some
drivers -- takes the app down instead of rebuilding the swapchain.

The texture is good for this frame, so it is drawn with and presented, and
the rebuild happens once `present` has consumed it.
2026-09-20 01:00:05 -04:00
iris-ai d8d51221ee Keep a contract only where it still holds for this widget
`redraw` keeps the narrower of an old and a fresh contract so that widening
and narrowing back do not churn the parent that reads it. The drawing's half
asked whether the old range still covers this window and box before keeping
it; the answer's half did not, so a widget whose answer contract widened in
a frame that also resized the window kept a range the new window is outside.

The parent's next ask then refuses that answer and draws the whole subtree
again -- throwing away the drawing the widget had just made. Cost, not
geometry: the size kept is the size just reported.

`a_contract_this_window_is_outside_is_not_kept` draws the leaf twice before
the change and once after.
2026-09-20 01:00:01 -04:00
8 changed files with 73 additions and 41 deletions

No files matched your search

+8 -5
View File
@@ -72,6 +72,7 @@ labelled! {
TextBreaks = "text line breaks", TextBreaks = "text line breaks",
GlyphPlacements = "glyph placements", GlyphPlacements = "glyph placements",
OutsidePinnedLen = "reuse outside: the length it was pinned to", OutsidePinnedLen = "reuse outside: the length it was pinned to",
OutsideWindow = "reuse outside: this window",
OutsideRelBase = "reuse outside: a rel base", OutsideRelBase = "reuse outside: a rel base",
OutsideRegion = "reuse outside: a region length", OutsideRegion = "reuse outside: a region length",
} }
@@ -352,7 +353,7 @@ pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
} }
/// A drawing that cannot be reused because the box on offer is outside what /// A drawing that cannot be reused because the box on offer is outside what
/// it holds for, and which of the three contracts said so. They overlap: a /// it holds for, and which of the four contracts said so. They overlap: a
/// drawing can be outside two of them at once, and counting each is what /// drawing can be outside two of them at once, and counting each is what
/// says where a rel base redrawing more than it should is coming from. /// says where a rel base redrawing more than it should is coming from.
pub(crate) fn outside( pub(crate) fn outside(
@@ -369,10 +370,12 @@ pub(crate) fn outside(
if holds.region_len.is_some_and(|pinned| pinned != len) { if holds.region_len.is_some_and(|pinned| pinned != len) {
bump(Counter::OutsidePinnedLen); bump(Counter::OutsidePinnedLen);
} }
if !holds.window.contains(window) if !holds.window.contains(window) {
|| holds bump(Counter::OutsideWindow);
.rel_base }
.is_some_and(|pinned| pinned != rel_base[axis]) if holds
.rel_base
.is_some_and(|pinned| pinned != rel_base[axis])
{ {
bump(Counter::OutsideRelBase); bump(Counter::OutsideRelBase);
} }
-8
View File
@@ -84,14 +84,6 @@ pub struct RegionAlign {
pub y: AxisAlign, pub y: AxisAlign,
} }
impl RegionAlign {
/// Both axes at the near edge: the start of a box in its own orientation.
pub const NEAR: Self = Self {
x: AxisAlign::NEG,
y: AxisAlign::NEG,
};
}
impl RegionAlign { impl RegionAlign {
pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG); pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG);
pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG); pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG);
-9
View File
@@ -11,15 +11,6 @@ pub enum Axis {
impl Axis { impl Axis {
/// Both of them, for the layout code that asks the same question of each. /// Both of them, for the layout code that asks the same question of each.
pub const BOTH: [Self; 2] = [Self::X, Self::Y]; pub const BOTH: [Self; 2] = [Self::X, Self::Y];
/// A per-axis pair with `aligned` on this axis and `ortho` on the other,
/// which is what `from_axis` does for a vector.
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] {
match self {
Self::X => [aligned, ortho],
Self::Y => [ortho, aligned],
}
}
} }
impl std::ops::Not for Axis { impl std::ops::Not for Axis {
+1 -1
View File
@@ -29,7 +29,7 @@ impl Holds {
Self { lo: len, hi: len } Self { lo: len, hi: len }
} }
pub const fn contains(&self, len: Px) -> bool { pub const fn contains(self, len: Px) -> bool {
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw() len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
} }
+13 -10
View File
@@ -1108,20 +1108,23 @@ impl UiRenderState {
let old = self.remove(id, false, rsc); let old = self.remove(id, false, rsc);
let drawn = self.draw_inner(id, info, old, rsc); let drawn = self.draw_inner(id, info, old, rsc);
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
// A wider contract does not invalidate the guarantee the parent kept.
// Retain that guarantee so widening and narrowing back do not churn it.
if let Some(was) = was_answer
&& drawn.answer.size == was.size
&& drawn.answer.holds.covers(was.holds)
{
active.answer = was_answer;
}
// Against the box it was asked in, which is what both contracts are // Against the box it was asked in, which is what both contracts are
// about. Where the answer put the drawing is shorter than that // about. Where the answer put the drawing is shorter than that
// wherever the widget reported less than it was offered. // wherever the widget reported less than it was offered.
if active.holds.covers(was_holds) let (window, rel_base, region) = (self.output_size, active.rel_base, active.region);
&& was_holds.contains(self.output_size, active.rel_base, active.region) // A wider contract does not invalidate the guarantee the parent kept.
// Retain that guarantee so widening and narrowing back do not churn
// it -- but only where the narrower range still holds here: one this
// window is outside is refused by the parent's next ask, and refusing
// it throws away the drawing this one just made.
if let Some(was) = was_answer
&& drawn.answer.size == was.size
&& drawn.answer.holds.covers(was.holds)
&& was.holds.contains(window, rel_base, region)
{ {
active.answer = was_answer;
}
if active.holds.covers(was_holds) && was_holds.contains(window, rel_base, region) {
active.holds = was_holds; active.holds = was_holds;
} }
if active.answer != was_answer || active.holds != was_holds { if active.answer != was_answer || active.holds != was_holds {
+9 -6
View File
@@ -22,12 +22,12 @@ impl UiRenderer {
} }
pub fn draw(&mut self) { pub fn draw(&mut self) {
let output = match self.surface.get_current_texture() { let (output, suboptimal) = match self.surface.get_current_texture() {
CurrentSurfaceTexture::Success(texture) => texture, CurrentSurfaceTexture::Success(texture) => (texture, false),
CurrentSurfaceTexture::Suboptimal(texture) => { // Used for this frame, and the swapchain rebuilt after it has
self.surface.configure(&self.device, &self.config); // been presented: configuring the surface while a texture it
texture // handed out is still alive panics.
} CurrentSurfaceTexture::Suboptimal(texture) => (texture, true),
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => { CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
self.surface.configure(&self.device, &self.config); self.surface.configure(&self.device, &self.config);
return; return;
@@ -60,6 +60,9 @@ impl UiRenderer {
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
self.window.pre_present_notify(); self.window.pre_present_notify();
self.queue.present(output); self.queue.present(output);
if suboptimal {
self.surface.configure(&self.device, &self.config);
}
} }
pub fn resize(&mut self, size: &PhysicalSize<u32>) { pub fn resize(&mut self, size: &PhysicalSize<u32>) {
+1 -1
View File
@@ -124,7 +124,7 @@ impl Widget for Span {
if shrinks { if shrinks {
// Choosing between a fixed and a relative length from the // Choosing between a fixed and a relative length from the
// span's own eventual width admits multiple fixed points. // span's own eventual width admits multiple fixed points.
// A scalable child therefore makes Children scalable too; // A scalable child therefore makes the span scalable too;
// only fixed children are compared with one another. // only fixed children are compared with one another.
if !used.is_px() { if !used.is_px() {
ortho = LayoutLen::LEFTOVER; ortho = LayoutLen::LEFTOVER;
+41 -1
View File
@@ -1480,14 +1480,17 @@ fn a_redrawn_subtree_is_not_undrawn_by_the_parent_it_left() {
/// A leaf that reports less than the box it is given and states which lengths /// A leaf that reports less than the box it is given and states which lengths
/// of that box its drawing holds for, so a test can widen the contract /// of that box its drawing holds for, so a test can widen the contract
/// without changing the answer. /// without changing the answer. It counts its draws, since what a kept
/// contract costs is whether the parent has to make it draw again.
struct Contracted { struct Contracted {
holds: std::ops::RangeInclusive<Px>, holds: std::ops::RangeInclusive<Px>,
size: Size, size: Size,
draws: Rc<Cell<usize>>,
} }
impl Widget for Contracted { impl Widget for Contracted {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.holds(Axis::X, self.holds.clone()); painter.holds(Axis::X, self.holds.clone());
self.size self.size
} }
@@ -1511,6 +1514,7 @@ fn widening_what_a_drawing_holds_for_does_not_relay_out_the_parent() {
let child = Contracted { let child = Contracted {
holds: Px::from_int(300)..=Px::from_int(500), holds: Px::from_int(300)..=Px::from_int(500),
size: Size::from((100, 200)), size: Size::from((100, 200)),
draws: Rc::new(Cell::new(0)),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
@@ -1533,3 +1537,39 @@ fn widening_what_a_drawing_holds_for_does_not_relay_out_the_parent() {
"a wider contract for the same answer is not a change to lay out" "a wider contract for the same answer is not a change to lay out"
); );
} }
/// The other half of the rule above: a kept contract is the narrower one, so
/// it is only worth keeping where it still holds. A window the old range is
/// outside is not one its parent can be handed back, and keeping it there
/// throws away the drawing the widget just made.
#[test]
fn a_contract_this_window_is_outside_is_not_kept() {
let mut h = Harness::new((400, 200));
let leaf_draws = Rc::new(Cell::new(0));
let child = Contracted {
holds: Px::from_int(300)..=Px::from_int(500),
size: Size::from((100, 200)),
draws: leaf_draws.clone(),
}
.add(&mut h.rsc);
let root = CountedParent {
inner: child.upgrade(&mut h.rsc),
draws: Rc::new(Cell::new(0)),
}
.add(&mut h.rsc);
h.set_root(root);
// Wide enough that the old contract leaves the new box out, and the leaf
// is marked in the same frame -- so it settles itself first and its
// parent draws afterwards, asking about what it settled.
h.resize((600, 200));
h.rsc[child].holds = Px::from_int(200)..=Px::from_int(700);
let settled = leaf_draws.get();
h.frame();
assert_eq!(
leaf_draws.get(),
settled + 1,
"the leaf settled once and its parent kept what it settled"
);
}