use std::sync::Mutex; use std::time::{Duration, Instant}; use crate::{ ActiveData, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers, RegionAlign, Size, StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets, render::{ Drawn, MoveOffset, NOT_DRAWN, Primitive, PrimitiveHandle, PrimitiveInst, Primitives, RectPrimitive, rounded_rect_coverage, }, util::{HashMap, HashSet, Id, Vec2}, }; /// What [`UiRenderState::update`] did on its last call -- read back by the /// `iris::frame` diagnostic (`iris::diagnostics::log_frame` in the `iris` /// crate) so a report can tell a full relayout from a frame that only /// redrew a handful of dirty widgets from one that drew nothing at all. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RedrawKind { /// Neither the root nor any widget changed -- `update` did nothing. None, /// [`UiRenderState::redraw_all`]: a new root, or a resize. All, /// [`UiRenderState::redraw_updates`]: only the widgets `needs_redraw` /// named. Updates, } pub struct UiRenderState { pub active: HashMap, /// Every primitive in the tree, in one arena -- see [`Primitives`] for /// why it is not per layer. pub primitives: Primitives, /// What each layer draws, in order: slots into `primitives`. pub layers: PrimitiveLayers, pub(super) output_size: Vec2, /// Physical pixels per `dp` -- see `Len::dp`'s field doc. `1.0` (an /// unscaled display) until a backend that knows its own density calls /// `set_density` (Android's `content_scale`, read at `surface_changed` /// time); the winit backend has no analogous per-monitor value wired up /// yet and stays at the default. pub(super) density: f32, old_root: Option, resized: bool, /// The widgets whose `Widget::draw` is on the stack right now -- so /// [`Self::redraw`] can tell "this widget needs drawing again" from /// "an ancestor is drawing it at this very moment", where a second /// draw would leave the first one's primitives behind with nothing /// owning them. An id is inserted immediately before `draw` is called /// and removed the moment it returns (both in `draw_inner`), so this /// is empty between frames -- asserted at the end of `update`. /// /// It used to only ever be inserted into, and `redraw` removed the id /// *before* testing for it, which made the test constant `false`: the /// guard could never fire and the set grew by one entry per widget /// ever drawn and was never emptied. draw_started: HashSet, /// `Widget::draw` calls and `Primitives::region_mut` rewrites since the /// last `take_counters`. LAYOUT.md section 8's pass conditions are /// stated in terms of these two: an unchanged frame must cost 0 of /// each, and moving one widget must cost 0 draws and 0 rewrites /// regardless of how many primitives are in its subtree. draw_count: u64, region_mut_count: u64, mov_count: u64, /// Text layouts actually computed -- bumped by `Painter::render_text`, /// which `TextView::render` only reaches on a cache miss. pub(super) shape_count: u64, /// `Instant::now()` at construction -- the zero every `iris::frame` line /// dates itself from, so a report's `now=` is comparable to a harness's /// own `t_ms` (`Harness::new` builds its `base` the same way, in the /// same constructor call) without either side needing the wall clock. epoch: Instant, /// How many times [`Self::update`] has run -- the `iris::frame` line's /// frame number. Counts every call, including one that found nothing to /// redraw, so a gap in the sequence in a report is a frame this state /// was never asked to run at all (a stalled event loop), not one that /// ran and did nothing. frame_no: u64, /// How long the redraw phase of the last [`Self::update`] took -- /// [`Self::redraw_all`] or [`Self::redraw_updates`], whichever ran, or /// zero if neither did. Read back by `iris::diagnostics::log_frame`. last_layout: Duration, last_redraw_kind: RedrawKind, /// When the sensor dispatch (`SensorUi::run_sensors`, in the `iris` /// crate) last saw an input sample, dated by the sample's own clock /// (`CursorState::time`) rather than when the dispatch ran -- same /// reasoning as that field's own doc. A `Mutex` because `run_sensors` /// takes `&self` and this is the one render state both backends /// already share across frames. last_input_at: Mutex>, } /// Whether a draw is producing what goes on screen, or only asking a /// widget how big it would be. /// /// **There is no size query without a draw** (LAYOUT.md section 5): /// `Widget::draw` reports the size it used, and nothing else can answer /// it. A container that cannot choose what to offer a child without /// already knowing the child's size therefore has to draw it -- so /// [`Self::Measure`] is that draw with everything it *writes* switched /// off. It allocates no arena slot, no mask, no move slot, leaves nothing /// in `active` and marks nothing dirty; the widget is walked and its text /// is shaped (which is memoized, and is the expensive half anyway), and /// only the returned `Size` survives. /// /// Because it leaves no trace, the real draw that follows is an ordinary /// first draw or redraw and cannot be short-circuited by the measurement /// having "already drawn" the widget at that region -- which is the trap /// the discarded-draw approach it replaced had to work around. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum DrawMode { /// Write primitives, keep the result in `active`. Draw, /// Report a size and write nothing. Measure, } impl DrawMode { fn measuring(self) -> bool { self == Self::Measure } } /// What a widget being redrawn keeps from the draw it is replacing. /// /// These four always travel together -- they are read off one /// `ActiveData` that was just taken out of `active` and handed straight /// to the draw that replaces it -- and they were four positional /// parameters of [`UiRenderState::draw_inner`] until 2026-09-09, next to /// six others. [`Default`] is the "nothing to keep" case: a widget drawn /// for the first time, and the root of a full relayout. pub(crate) struct Retained { /// So children this draw does not draw again can be retired. pub children: Vec, /// Reused in place with its delta reset, never reallocated: a /// descendant that is not itself redrawn still points at it. See /// LAYOUT.md section 2. pub move_slot: Option, pub own_mask: MaskIdx, /// Slots the draw may write into instead of allocating -- see /// `Painter::take_recycled`. Anything it does not claim is freed when /// the draw ends. pub primitives: Vec, } impl Default for Retained { /// Nothing kept: no children to retire, no move slot to reuse, no /// mask of its own yet, nothing to recycle. Hand-written because /// `MaskIdx`'s zero is a real slot rather than "none". fn default() -> Self { Self { children: Vec::new(), move_slot: None, own_mask: MaskIdx::NONE, primitives: Vec::new(), } } } /// The bound on the parent walk -- see `resolve_move` in shader.wgsl, /// which walks the identical chain and must be kept in step with this /// constant. It exists so a cyclic `parent` link cannot hang either walk, /// not as a statement about how deep a real tree gets: it was 16, and the /// transcript screen's composer field turned out to sit **17** slots below /// the root (measured 2026-09-07 on this checkout's emulator, by tapping /// the composer in a debug build -- the assert in `resolve_move_chain` /// prints the chain). A chain past the bound is not reported anywhere at /// run time; both walks just stop summing, so the widget is drawn and hit /// tested short by whatever the outer slots held. /// /// Named for the walk rather than for one of its two subjects: it bounds /// the move-offset chain *and* the mask chain (`Mask::parent`, walked in /// the fragment stage), and `MOVE_CHAIN_LIMIT` said only the first /// (review, 2026-09-07). pub const PARENT_CHAIN_LIMIT: usize = 64; impl UiRenderState { pub fn new() -> Self { Self { active: Default::default(), primitives: Default::default(), layers: Default::default(), output_size: Vec2::ZERO, density: 1.0, old_root: None, resized: false, draw_started: Default::default(), draw_count: 0, region_mut_count: 0, mov_count: 0, shape_count: 0, epoch: Instant::now(), frame_no: 0, last_layout: Duration::ZERO, last_redraw_kind: RedrawKind::None, last_input_at: Mutex::new(None), } } /// Reads and zeroes the (draws, region_mut rewrites, move_offsets /// writes, text shapes) counters -- call once per frame before /// `update()` to measure exactly that frame, per LAYOUT.md section 8. /// /// The fourth is the one a draw count cannot stand in for: a widget /// can be redrawn without re-shaping (`TextView::render` memoizes by /// width) and re-shaped without any extra draw, and it is re-shaping /// that the per-block transcript row exists to avoid -- see /// `transcript_ui`'s `a_delta_into_a_long_reply_shapes_one_block`. pub fn take_counters(&mut self) -> (u64, u64, u64, u64) { ( std::mem::take(&mut self.draw_count), std::mem::take(&mut self.region_mut_count), std::mem::take(&mut self.mov_count), std::mem::take(&mut self.shape_count), ) } /// Writes a primitive into the arena and, unless it is /// [`Drawn::No`], into `layer`'s draw order. pub(super) fn write_primitive( &mut self, layer: usize, drawn: Drawn, inst: PrimitiveInst

, ) -> PrimitiveHandle { let (slot, data_idx) = self.primitives.alloc(inst); let pos = match drawn { Drawn::Yes => self.layers[layer].push(slot, false), Drawn::No => NOT_DRAWN, }; PrimitiveHandle { layer, pos, slot, data_idx, binding: P::BINDING, } } /// A standalone image, which draws with its own bind group rather /// than sharing the layer's one instanced draw. pub(super) fn write_image( &mut self, layer: usize, id: WidgetId, texture_idx: u32, region: UiRegion, mask_idx: MaskIdx, move_idx: MoveIdx, ) -> PrimitiveHandle { let slot = self .primitives .alloc_image(id, texture_idx, region, mask_idx, move_idx); let pos = self.layers[layer].push(slot, true); PrimitiveHandle { layer, pos, slot, data_idx: 0, binding: crate::render::IMAGE_BINDING, } } /// Compacts every layer's draw order around the primitives freed /// this frame, corrects the handles that moved, and only then hands /// the arena slots back for reuse -- that order is the whole reason /// `Primitives::freed` exists. Once per frame, at the end of /// [`Self::update`], so the harness (which has no renderer) applies /// it exactly as a real backend does. fn apply_free(&mut self) { for (layer, order) in self.layers.iter_mut() { for change in order.apply_free() { // Straight to the handle, never a scan of everything the // owner drew: a widget freed and redrawn in one frame has // *every* one of its primitives renumbered here, so a scan // makes this pass quadratic in that widget's primitive // count -- 1.37s for one 51,200-glyph text block, against // 20ms to shape and rasterise the same text (measured // 2026-09-08). `Primitives::handle_index` is written where // the handle is taken, in `Painter::own`. let owner = self.primitives.owner(change.slot); let Some(idx) = self.primitives.handle_index(change.slot) else { continue; }; if let Some(active) = self.active.get_mut(&owner) && let Some(h) = active.primitives.get_mut(idx) { debug_assert!( h.layer == layer && h.slot == change.slot, "slot {} says it is handle {idx} of {owner:?}, which is slot {} in layer {}", change.slot, h.slot, h.layer, ); h.pos = change.pos; } } } self.primitives.release_freed(); } pub fn resize(&mut self, size: impl Into) { self.output_size = size.into(); self.resized = true; } /// Sets the physical-pixels-per-dp ratio every `Len::dp` in the tree /// resolves against from the next layout pass on -- see `density`'s /// field doc. Not folded into `resize` because the two change on /// different triggers (a surface resize on every rotation or keyboard /// open; a density change only if the app follows the display to a /// different screen, which Android surfaces separately). /// /// Marks the tree for a full redraw when the value actually changes: /// every `Len::dp` already resolved and every glyph already shaped /// (`Text::shape` keys its cache on `(attrs, width, density)`) belongs /// to the old one, and nothing else would ask for them again /// (review, 2026-09-07). pub fn set_density(&mut self, density: f32) { if density != self.density { self.resized = true; } self.density = density; } pub fn density(&self) -> f32 { self.density } pub fn update<'a>(&mut self, root: impl Into>, rsc: &mut dyn UiRsc) { // safety mechanism for memory leaks; might wanna return a result instead so user can // decide whether to panic or not if !rsc.widgets().waiting.is_empty() { let widgets = rsc.widgets(); let len = widgets.waiting.len(); let all: Vec<_> = widgets .waiting .iter() .map(|&w| format!("'{}' ({w:?})", widgets.label(w))) .collect(); panic!( "{len} widget(s) were never upgraded\n\ this is likely a memory leak; consider upgrading to strong if you plan on using it later\n\ weak widgets: {all:#?}" ); } let root = root.into(); debug_assert!( self.draw_started.is_empty(), "a previous frame left {} widget(s) marked as mid-draw", self.draw_started.len(), ); // Timed unconditionally -- an `Instant::now()` pair is cheap enough // not to move the `--phone` bench's frame time (checked when this // was added), and gating it behind the trace toggle would leave // `iris::frame` with nothing to report the one frame somebody just // turned tracing on to look at. let layout_start = Instant::now(); let kind = if self.needs_redraw_all(root) { self.redraw_all(root, rsc); self.old_root = root.map(|r| r.id()); self.resized = false; RedrawKind::All } else if rsc.widgets().has_updates() { self.redraw_updates(rsc); RedrawKind::Updates } else { RedrawKind::None }; self.last_layout = layout_start.elapsed(); self.last_redraw_kind = kind; self.frame_no += 1; // After the redraw and before anything reads the frame: every // slot freed above is still named by its layer's draw order until // this runs. self.apply_free(); #[cfg(debug_assertions)] debug_assert!(self.primitive_counts_agree(), "{}", self.orphan_report(rsc),); } /// `Instant::now()` at construction -- see the field's own doc. pub fn epoch(&self) -> Instant { self.epoch } /// How many times [`Self::update`] has run, counting from 1. pub fn frame_number(&self) -> u64 { self.frame_no } /// How long the last [`Self::update`]'s redraw phase took. pub fn last_layout_duration(&self) -> Duration { self.last_layout } /// What the last [`Self::update`] did -- see [`RedrawKind`]. pub fn last_redraw_kind(&self) -> RedrawKind { self.last_redraw_kind } /// Records that a real input sample was just dispatched, dated by the /// sample's own clock -- called once per sensor pass, so `iris::frame`'s /// `since_input` can answer "how stale was the input /// this frame drew" instead of a caller guessing from the frame /// interval. `&self` because `run_sensors` only ever has that -- see /// `last_input_at`'s field doc. pub fn note_input(&self, at: Instant) { if let Ok(mut guard) = self.last_input_at.lock() { *guard = Some(at); } } /// `now - ` the last input sample's own timestamp, or `None` if no /// input has ever reached this render state (a cold start, or a screen /// that only ever animates on its own). Saturates to zero rather than /// panicking if `now` is earlier than the input sample somehow was -- /// a diagnostic reading wrong is not worth a crash over. pub fn time_since_input(&self, now: Instant) -> Option { let at = *self.last_input_at.lock().ok()?; at.map(|at| now.saturating_duration_since(at)) } /// Primitive instances every currently-active widget owns, summed -- /// what `iris::frame`'s `primitives=` reports. Not a per-frame delta: /// `redraw_updates` only rewrites what changed, so this is "how much is /// on screen", which is what a report reads as "did this frame have /// more to draw than the last one", not "how much work did this frame /// do" (`take_counters` answers that). /// /// A mask's shape does not count: it is a [`Drawn::No`] primitive /// that is never rasterized, so including it would put one extra on /// the line for every masked widget and make a number Iris reads off /// a phone report disagree with what is drawn. pub fn active_primitive_count(&self) -> usize { self.active .values() .map(|a| a.primitives.iter().filter(|h| h.pos != NOT_DRAWN).count()) .sum() } fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) { self.clear(rsc); // free all resources & cache if let Some(id) = root { self.draw_inner( 0, id.id(), UiRegion::FULL, None, MoveOffset::NONE_PARENT, MaskIdx::NONE, DrawMode::Draw, Retained::default(), rsc, ); } } /// The slot an *already-active* widget's `move_offsets` entry chains /// to, read back from `self.active`. Only valid where the parent is /// guaranteed to already be in `self.active` -- true for `redraw()`, /// which targets a widget that was fully drawn on some earlier update, /// but **not** for a widget being drawn as part of its own parent's /// `Widget::draw` call: that parent's `ActiveData` is not inserted /// until its `draw` returns (below), so a child drawn partway through /// it would always read back "no parent" here. `Painter::widget_at` /// avoids that trap by passing its own already-known `move_slot` /// straight through instead of asking `self.active` to look it up. fn move_parent_of(&self, parent: Option) -> u32 { parent .and_then(|p| self.active.get(&p)) .map(|p| p.move_slot.idx() as u32) .unwrap_or(MoveOffset::NONE_PARENT) } // TODO: should prolly make a DrawInfo struct or smth for everything other than rsc #[allow(clippy::too_many_arguments)] pub(super) fn draw_inner( &mut self, layer: usize, id: WidgetId, region: UiRegion, parent: Option, parent_move_slot: u32, mask: MaskIdx, mode: DrawMode, retained: Retained, rsc: &mut dyn UiRsc, ) -> Size { let Retained { children: mut old_children, move_slot: mut old_move_slot, mut own_mask, primitives: mut recycle, } = retained; // Consumed here, not merely read: this call *is* the redraw the mark // asked for, and leaving the mark set is what stranded a widget's // primitives. A measure-then-draw reaches this twice for the same id // in one frame (`LazySpan::place`'s measurement pass), and on the second // call the still-set mark took the whole `if let` below -- including // the `remove` that frees the first draw's primitives -- out of play, // so `active.insert` at the end overwrote the only handles that could // ever have freed them. The result is a full second copy of the row, // drawn every frame from then on at the oversized measurement region // and, with `LazySpan` setting no mask, outside the list's own bounds: // the doubled `Compacted:` row in docs/bench/iris-phone-v2-2026-09-06.md. // The same shape reaches any dirty widget an ancestor redraws first. // A measurement consumes no redraw mark and takes none of the // fast paths: it is not the redraw the mark asked for, and // "already drawn at this region" would make it return without // reporting a size at all. let dirty = !mode.measuring() && rsc.widgets_mut().needs_redraw.remove(&id); if let Some(active) = self.active.get_mut(&id) && !dirty && !mode.measuring() { // check to see if we can skip drawing first if active.region == region { return active.size; } else if active.region.size() == region.size() { // TODO: epsilon? let from = active.region; let size = active.size; self.mov(id, from, region, rsc); return size; } else if rsc .widgets() .get_dyn(id) .map(|w| w.is_size_independent()) .unwrap_or(false) { // The offered region changed shape, but this widget's own // drawn output does not depend on it (a fixed-size leaf) -- // rewrite its own primitives' regions in place (O(primitives // owned directly by this widget, which for a leaf is O(1)) // instead of redrawing. See LAYOUT.md section 3. let from = active.region; for h in &active.primitives { let r = self.primitives.region_mut(h); *r = r.outside(&from).within(®ion); self.region_mut_count += 1; } // `move_applied` is deliberately **not** touched here, // unlike in `mov`: it counts the part of this widget's own // move-slot delta that `region` has already absorbed, and // this branch writes no delta at all -- the primitives were // moved directly. Counting one would make // `resolved_region` subtract a distance the chain never // held, putting the hit box short of the drawing by // exactly this step. See `ActiveData::move_applied`, and // `a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at`. active.region = region; return active.size; } // if not, then maintain resize and track old children to remove unneeded let active = self.remove(id, false, true, rsc).unwrap(); old_children = active.children; old_move_slot = Some(active.move_slot); own_mask = active.own_mask; recycle = active.primitives; } else if dirty && self.active.contains_key(&id) { debug_assert!(!mode.measuring()); // Dirty and already drawn: none of the fast paths above may be // taken (the widget's own content changed, so its old primitives // say nothing about its new ones), but they are also the only // thing that frees them. Same two lines, reached the other way. let active = self.remove(id, false, true, rsc).unwrap(); old_children = active.children; old_move_slot = Some(active.move_slot); own_mask = active.own_mask; recycle = active.primitives; } // draw widget let reentrant = !self.draw_started.insert(id); debug_assert!( !reentrant, "widget {id:?} is being drawn while its own draw is already on the stack; \ the second draw's primitives would orphan the first's" ); // A measurement writes no primitive, so nothing ever reads this // -- and allocating one would leak a slot per measured widget per // frame, since `move_offsets` only frees on a widget's removal. let move_slot = match mode { DrawMode::Measure => Id::preset(MoveOffset::NONE_PARENT), DrawMode::Draw => Self::move_slot_for(old_move_slot, parent_move_slot, rsc), }; // The mask this widget was drawn *under*, kept aside because // `Painter::set_mask` overwrites `painter.mask` with the widget's // own new one -- and `ActiveData::mask`'s only consumer is // `redraw`, which feeds it back in as the *inherited* mask. Storing // the set one instead handed a `Masked` its own mask on every // targeted redraw -- an abort the first time the composer's scroll // area was redrawn on the emulator, and now (masks nest) a mask // whose parent is itself, which `set_mask`'s own assert names. let inherited_mask = mask; let mut painter = Painter { state: self, region, mask, move_slot, own_mask, layer, id, textures: Vec::new(), primitives: Vec::new(), recycle: recycle.into_iter().peekable(), children: Vec::new(), mode, rsc, }; let mut widget = painter.rsc.widgets().get_dyn_dynamic(id); painter.state.draw_count += 1; let size = widget.draw(&mut painter); // A reported length is consumed by containers that read `abs`, // `rel` and `rest` straight off it (`Span`'s placement, `Pad`'s // addition), so an unresolved `dp` in one is silently worth zero // -- see `Len::fold_dp`, which is what a widget reporting a // caller-declared size has to put it through. debug_assert!( size.x.dp == 0.0 && size.y.dp == 0.0, "widget {id:?} reported an unresolved `dp` size ({size:?}); \ report `Len::fold_dp(painter.density())` instead" ); drop(widget); painter.state.draw_started.remove(&id); let Painter { state: _, rsc: _, region, mask: _, move_slot, own_mask, textures, primitives, recycle, children, layer, id, mode: _, } = painter; if mode.measuring() { // Nothing to unwind: a measurement allocates no slot, no // mask, no move offset and no `ActiveData`, so the size is // the whole of what it produced. Asserted rather than // assumed, because a `Painter` method that forgot to check // the mode would otherwise leak silently -- one primitive per // measured widget per frame, which a screen redrawn every // frame turns into an arena that grows without bound. debug_assert!( primitives.is_empty() && textures.is_empty(), "measuring {id:?} wrote {} primitive(s) and {} texture(s); \ every `Painter` write must check `Painter::measuring`", primitives.len(), textures.len(), ); return size; } // Whatever the draw did not claim is genuinely gone: this draw // wrote fewer primitives than the last one, or stopped matching // part way. Freeing it here rather than in `remove` is what lets // the draw in between reuse the slots -- see // `Primitives::recycle`. for h in recycle { self.free_primitive(&h); } // add to active let active = ActiveData { id, region, parent, textures, primitives, children, mask: inherited_mask, layer, size, move_slot, own_mask, move_applied: Vec2::ZERO, repositioned: Vec2::ZERO, }; // remove old children that weren't kept for c in &old_children { if !active.children.contains(c) { self.remove_rec(*c, rsc); } } rsc.on_draw(&active); self.active.insert(id, active); size } /// This widget's slot in `move_offsets`: the one it already had if it /// is being redrawn, or a fresh one linked to its parent's. /// /// A redraw **reuses the slot in place with its delta reset**, never /// reallocates: the geometry this draw is about to write is already /// at its correct absolute position, so a delta accumulated before it /// would double-offset it -- while the chain link (`parent`) is left /// alone, because the logical parent has not changed and a descendant /// that is not itself redrawn still points here. See LAYOUT.md /// section 2. fn move_slot_for(old: Option, parent_move_slot: u32, rsc: &mut dyn UiRsc) -> MoveIdx { match old { Some(slot) => { rsc.ui_mut().move_offsets.get_mut(slot).delta = [0.0, 0.0]; slot } None => { let slot = rsc .ui_mut() .move_offsets .push(MoveOffset::new([0.0, 0.0], parent_move_slot)); rsc.ui_mut().move_offsets.push_ref(slot); if parent_move_slot != MoveOffset::NONE_PARENT { rsc.ui_mut() .move_offsets .push_ref(Id::preset(parent_move_slot)); } slot } } } /// O(1): write the delta for this widget's own slot in /// `move_offsets`. No primitive is touched and there is no recursion -- /// every descendant's primitive references this slot transitively /// through the parent chain the shader walks (`resolve_move`), so it /// picks the new delta up for free. See LAYOUT.md section 2. fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion, rsc: &mut dyn UiRsc) { let Some(active) = self.active.get_mut(&id) else { return; }; let slot = active.move_slot; active.region = to; let from_px = from.top_left().to_abs(self.output_size); let to_px = to.top_left().to_abs(self.output_size); let delta = to_px - from_px; active.move_applied += delta; let entry = rsc.ui_mut().move_offsets.get_mut(slot); entry.delta[0] += delta.x; entry.delta[1] += delta.y; self.mov_count += 1; } /// Move an already-active widget to `to`. Used by `Painter::reposition`, /// for a parent that drew a child provisionally (at the whole region it /// was offered) and now knows where the child actually belongs. /// /// Unlike `mov` (called by `draw_inner`'s own dispatch, where the /// *offered* region really did move and `active.region` already tracks /// it), the child here was not offered a smaller region -- it was /// offered everything and chose, on its own, to occupy only /// `active.size` of it. By convention every widget in this crate that /// does that anchors its own content at the top-left of whatever it /// was given (`Rect`/`Image`/`Sized`/`MaxSize` -- see their `draw` /// bodies), so that is where this assumes the child was actually /// painted, not `active.region` itself (which is the *offered* box, /// usually bigger). A nested `Aligned` whose own child is not top-left /// anchored -- i.e. `Aligned` wrapping `Aligned` -- is the one shape /// this does not cover; none of iris's widgets or examples build that /// today. See LAYOUT.md's "Rejected, and why" / deviations for the /// full reasoning. /// /// The delta is overwritten, not accumulated like `mov`'s: `from` is /// recomputed fresh from `active.size`/`active.region` every call, so /// repeating the same `reposition` (e.g. an unrelated redraw elsewhere /// re-running this widget's parent without its own layout changing) /// must land on the same answer, not drift further each time. pub(super) fn reposition(&mut self, id: WidgetId, to: UiRegion, rsc: &mut dyn UiRsc) { let Some(active) = self.active.get(&id) else { return; }; let move_applied = active.move_applied; let repositioned = active.repositioned; let from = active .size .to_uivec2(self.density) .align(RegionAlign::TOP_LEFT) .within(&active.region); let slot = active.move_slot; let from_px = from.top_left().to_abs(self.output_size); let to_px = to.top_left().to_abs(self.output_size); let delta = to_px - from_px; // Not `delta` alone: a parent may have `mov`ed this widget to a // region that itself moved earlier in the same frame, and that // part of the slot is `move_applied`'s, not this call's. Writing // `delta` on its own dropped it and put the content back at the // pre-move position. `from` is computed against `active.region`, // which `mov` already updated, so `delta` is purely the placement // inside the region and the two summands never overlap. let entry = rsc.ui_mut().move_offsets.get_mut(slot); debug_assert_eq!( entry.delta, [ move_applied.x + repositioned.x, move_applied.y + repositioned.y ], "widget {id:?}'s move slot was written by something other than `mov`/`reposition`; \ the slot is theirs and means `move_applied + repositioned` -- see `ActiveData`" ); entry.delta = [move_applied.x + delta.x, move_applied.y + delta.y]; if let Some(active) = self.active.get_mut(&id) { active.repositioned = delta; } self.mov_count += 1; } /// Retires `id`'s primitives (unless `keep_primitives`, in which case /// they come back in the returned `ActiveData` for the redraw about to /// happen to recycle -- see `Painter::take_recycled`), drops the mask /// refs they held, and takes the widget out of `active`. /// /// The handles stay in the returned `ActiveData` either way, freed or /// not: `remask_shape_users` below reads them, and so does the /// caller. **A caller that passed `keep_primitives: false` must not /// free them again** -- they name slots that may already have been /// handed out. /// /// The mask refs are dropped either way: a recycled slot is rewritten /// with whatever mask the *new* draw is under, and that draw takes its /// own ref (`Painter::write_primitive`). /// /// NOTE: instance textures are cleared and self.textures freed fn remove( &mut self, id: WidgetId, undraw: bool, keep_primitives: bool, rsc: &mut dyn UiRsc, ) -> Option { let mut active = self.active.remove(&id); if let Some(active) = &mut active { for h in &active.primitives { let mask = self.primitives.instance(h.slot).mask_idx; if mask != MaskIdx::NONE { rsc.ui_mut().masks.remove(mask); } } if !keep_primitives { for h in &active.primitives { self.free_primitive(h); } } Self::remask_shape_users(&self.active, id, active.own_mask, &active.primitives, rsc); active.textures.clear(); rsc.ui_mut().textures.free(); if undraw { // A captured widget that goes away mid-gesture (LazySpan's // virtualisation retiring a row, a rebuild) must not leave // the pointer captured by an id nothing will ever draw // again. That path out is the sensor pass's, not this // one's: `iris::sense::SensorUi::run_sensors` releases a // capture whose widget no longer resolves to a region, // which covers this case and every other way an id can // stop being drawn. // Permanent removal: retire this widget's own move slot // (the self-ownership ref taken when it was allocated) and // the up-link ref it held on its parent's slot -- read from // the arena entry itself, not from `active.parent`, since // the parent's own `ActiveData` may already be gone by the // time a deep descendant is retired (see LAYOUT.md // section 2's lifecycle note). if active.own_mask != MaskIdx::NONE { // The self-ownership ref `Painter::set_mask` took when // it allocated this widget's own mask slot, and the // chain link's ref on the mask this one nests inside // -- read from the arena entry, for the same reason // the move slot's parent is. let outer = rsc.ui().masks[active.own_mask.idx()].parent; rsc.ui_mut().masks.remove(active.own_mask); if outer != MaskIdx::NONE { rsc.ui_mut().masks.remove(outer); } } let parent_slot = rsc.ui_mut().move_offsets[active.move_slot.idx()].parent; rsc.ui_mut().move_offsets.remove(active.move_slot); if parent_slot != MoveOffset::NONE_PARENT { rsc.ui_mut().move_offsets.remove(Id::preset(parent_slot)); } rsc.on_undraw(active); } } active } /// Retires one primitive: its arena slot and, if a layer's draw order /// names it, its position there. The two go together -- a slot handed /// out again while its old order entry still names it would be drawn /// twice -- which is why this is one function rather than two lines /// repeated at each call site. fn free_primitive(&mut self, h: &PrimitiveHandle) { self.primitives.free(h); if h.pos != NOT_DRAWN { self.layers[h.layer].free(h.pos, h.is_image()); } } /// A mask whose shape primitive was just freed clips to a slot that /// now holds something else, so the widget that owns it is marked for /// redraw -- its own `set_mask` is the only thing that resolves the /// slot, and it is the same mechanism a dirty widget already goes /// through. /// /// `own` is the mask belonging to the widget being removed and is /// skipped: this runs in the middle of that widget's own redraw, /// which sets its mask again on the way out, and a mark left on /// itself would redraw it every frame from then on. Skipping it is /// also what keeps the O(active) scan off the ordinary path -- a /// plain `.masked()` frees exactly its own shape, so `stale` is empty /// and this returns before touching `active`. /// /// Both `Vec`s start empty and stay unallocated in that case, and /// membership is a linear scan of two lists that are a handful long /// (a widget's own primitives, and the live masks): this runs once /// per widget removed, which is once per dirty widget per frame, and /// a set built there would be an allocation on the phone's frame /// path in exchange for nothing at these sizes. fn remask_shape_users( active: &HashMap, id: WidgetId, own: MaskIdx, freed: &[PrimitiveHandle], rsc: &mut dyn UiRsc, ) { let mut stale: Vec = Vec::new(); for (i, mask) in rsc.ui().masks.iter().enumerate() { let idx = Id::preset(i as u32); if idx != own && freed.iter().any(|h| h.slot == mask.primitive) { stale.push(idx); } } if stale.is_empty() { return; } let mut owners: Vec = Vec::new(); for (widget, data) in active { if *widget != id && stale.contains(&data.own_mask) { owners.push(*widget); } } for owner in owners { rsc.widgets_mut().needs_redraw.insert(owner); } } fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option { let inst = self.remove(id, true, false, rsc); if let Some(inst) = &inst { for c in &inst.children { self.remove_rec(*c, rsc); } } inst } fn clear(&mut self, rsc: &mut dyn UiRsc) { for (_, active) in self.active.drain() { rsc.on_undraw(&active); } self.layers.clear(); self.primitives.clear(); rsc.widgets_mut().needs_redraw.clear(); rsc.free(); } pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) { while let Some(&id) = rsc.widgets().needs_redraw.iter().next() { self.redraw(id, rsc); } rsc.free(); } pub fn root_changed<'a>(&self, root: impl Into>) -> bool { root.into().map(|r| r.id()) != self.old_root } /// What `update` will redraw everything for. Named and shared with /// `needs_redraw` rather than written out twice, because the two must /// agree: `needs_redraw` is what asks for the frame that `update` would /// draw, so a condition in one and not the other is a frame nobody /// requests and a stale window. `resized` was missing from `needs_redraw`, /// which is latent on Wayland only because winit asks for a redraw after a /// resize by itself -- a resize changes neither the root nor any widget, /// so nothing else here would have asked. fn needs_redraw_all<'a>(&self, root: impl Into>) -> bool { self.root_changed(root) || self.resized } pub fn needs_redraw<'a>( &self, root: impl Into>, widgets: &Widgets, ) -> bool { self.needs_redraw_all(root) || widgets.has_updates() } pub fn active_widgets(&self) -> usize { self.active.len() } /// Primitive instances still bound for the GPU whose owner is no /// longer in `active`, or whose owner's `ActiveData` no longer names /// them: a copy nothing can move, clip, resize or free, redrawn every /// frame at whatever position it last had. `(slot, owner)` each -- /// the arena knows which primitive, not which layer's draw order still /// names it. /// /// Asserted empty at the end of every [`Self::update`], because this /// is exactly the shape of the duplicated transcript row on Iris's /// phone (`docs/bench/iris-phone-v2-2026-09-06.md`): counting /// `active` alone cannot see it, since the orphan's owner is very /// much alive -- it is the *earlier* set of primitives that got /// stranded when the widget was drawn a second time without the first /// draw being freed. O(primitives), debug builds only. pub fn orphaned_primitives(&self) -> Vec<(u32, WidgetId)> { let mut orphans = Vec::new(); for (slot, owner, _) in self.primitives.live_instances() { let owned = self .active .get(&owner) .is_some_and(|a| a.primitives.iter().any(|h| h.slot == slot)); if !owned { orphans.push((slot, owner)); } } orphans } /// Whether every primitive still bound for the GPU is owned by a live /// widget, decided by counting rather than by walking: an orphan is a /// live instance no `ActiveData` names, so it can only ever make the /// live count exceed the owned one. O(active widgets) -- a few dozen -- /// against [`Self::orphaned_primitives`]'s O(primitives), which on a /// transcript is tens of thousands and made a debug build on a phone /// too slow to finish a benchmark run. fn primitive_counts_agree(&self) -> bool { let live: usize = self.primitives.live_count(); let owned: usize = self.active.values().map(|a| a.primitives.len()).sum(); live == owned } /// The message [`Self::update`]'s orphan assert prints -- built here /// rather than inline so the (allocating, O(primitives)) work only /// happens on the failing path. #[cfg(debug_assertions)] fn orphan_report(&self, rsc: &dyn UiRsc) -> String { let orphans = self.orphaned_primitives(); let mut lines: Vec = orphans .iter() .take(8) .map(|(slot, owner)| { let alive = self.active.contains_key(owner); format!( " instance {slot}: owner '{}' ({owner:?}), owner still active: {alive}", rsc.widgets().label(*owner), ) }) .collect(); if orphans.len() > lines.len() { lines.push(format!(" ... and {} more", orphans.len() - lines.len())); } format!( "{} primitive(s) are drawn but owned by nobody -- a stale copy \ nothing will ever move or free:\n{}", orphans.len(), lines.join("\n"), ) } pub fn debug(&self, widgets: &Widgets, label: &str) -> impl Iterator { self.active.iter().filter_map(move |(&id, inst)| { let l = widgets.label(id); if l == label { Some(inst) } else { None } }) } pub fn debug_layers(&self) { for ((idx, depth), order) in self.layers.iter_depth() { let indent = " ".repeat(depth * 2); let len = order.order().len(); print!("{indent}{idx}: {len} primitives"); if len >= 1 { print!(" ({})", self.primitives.instance(order.order()[0]).binding); } println!(); } } /// `active[id].region`, corrected by every `move_offsets` delta between /// `id` and the root -- the CPU-side twin of the vertex shader's chain /// walk, over the same arena, so the two cannot disagree about where a /// widget is. O(chain depth), not O(primitives). See LAYOUT.md /// section 2b. pub fn resolved_region(&self, id: &impl IdLike, rsc: &dyn UiRsc) -> Option { let active = self.active.get(&id.id())?; // The chain sum is what the shader adds to this widget's // *primitives*, which were written before any of those moves. // `region`, unlike them, has already been shifted by whatever // part of this widget's own slot `mov` put there -- see // `ActiveData::move_applied`, which is exactly that part. let delta = self.resolve_move_chain(active.move_slot, rsc) - active.move_applied; Some(active.region.offset(UiVec2::abs(delta))) } /// The plain-Rust twin of `resolve_move` in shader.wgsl: sums the /// pixel delta along the parent chain starting at `slot`. Both walks /// share `PARENT_CHAIN_LIMIT` as their bound so the two cannot disagree /// about where the chain ends. fn resolve_move_chain(&self, slot: MoveIdx, rsc: &dyn UiRsc) -> Vec2 { let offsets = &rsc.ui().move_offsets; let mut delta = Vec2::ZERO; let mut at = slot; for i in 0..PARENT_CHAIN_LIMIT { let entry = &offsets[at.idx()]; delta.x += entry.delta[0]; delta.y += entry.delta[1]; if entry.parent == MoveOffset::NONE_PARENT { return delta; } at = Id::preset(entry.parent); // The chain itself, not just the fact that it was too long: a // cycle and a tree genuinely nested deeper than the shader can // follow are different faults with different fixes, and the // slot numbers are the only thing that tells them apart. debug_assert!( i + 1 < PARENT_CHAIN_LIMIT, "move offset chain exceeded PARENT_CHAIN_LIMIT ({PARENT_CHAIN_LIMIT}): {chain} \ -- a \ repeated slot means a `parent` link is cyclic, all-distinct slots mean the tree \ nests deeper than shader.wgsl's own walk of the same bound", chain = Self::move_chain_debug(slot, offsets) ); } delta } /// The parent chain from `slot`, as `slot(dx, dy) -> ...`, walked twice /// `PARENT_CHAIN_LIMIT` so a cycle shows up as a repeated slot number /// rather than as a chain that merely stops. Only ever called from the /// failed assertion above. fn move_chain_debug(slot: MoveIdx, offsets: &[MoveOffset]) -> String { let mut parts = Vec::new(); let mut at = slot; for _ in 0..PARENT_CHAIN_LIMIT * 2 { let entry = &offsets[at.idx()]; parts.push(format!( "{}({}, {})", at.idx(), entry.delta[0], entry.delta[1] )); if entry.parent == MoveOffset::NONE_PARENT { break; } at = Id::preset(entry.parent); } parts.join(" -> ") } /// One primitive's corners in window pixels -- the transliteration of /// `shader.wgsl`'s `corners_of`, `floor` for `floor`. The rounding is /// the whole reason this is not `region.to_px()`: the shader floors /// each half separately before adding the move delta, and a hit test /// that skipped it would disagree with the pixels by up to one along /// each edge -- invisible in every test written against a whole-pixel /// layout and wrong on the phone, whose 2.55 density makes nothing /// land on a whole pixel. pub fn primitive_corners(&self, slot: u32, rsc: &dyn UiRsc) -> PixelRegion { let inst = self.primitives.instance(slot); let delta = self.resolve_move_chain(inst.move_idx, rsc); let size = self.output_size; let corner = |c: UiVec2| (c.get_rel() * size).floor() + c.get_abs().floor() + delta; PixelRegion { top_left: corner(inst.region.top_left()), bot_right: corner(inst.region.bot_right()), } } /// Where a mask's clip actually is on screen: the box of the /// primitive it references. Its *shape* within that box is /// [`Self::mask_coverage`]'s -- this is the bounding box, which is /// what a test asking "is the clip over the right part of the screen" /// wants and all a square-cornered mask has ever had. pub fn mask_region(&self, mask: MaskIdx, rsc: &dyn UiRsc) -> PixelRegion { self.primitive_corners(rsc.ui().masks[mask.idx()].primitive, rsc) } /// How much of the pixel at `pos` (window pixels) survives `mask` and /// every mask it nests inside: the referenced primitives' own /// coverage, multiplied along the chain. The CPU half of /// `shader.wgsl`'s `fs_main` mask loop -- same order, same bound, same /// `rounded_rect_coverage` -- so a corner that cannot be tapped and a /// corner that is not drawn are the same corner (LAYOUT.md's "Masks /// with a shape", point 4). /// /// A mask whose shape is not a rect covers everything, exactly as the /// shader's own `mask_coverage` does: `Painter::set_mask_to` rejects /// those by name, so this is the unreachable half of the same /// agreement rather than a second policy. pub fn mask_coverage(&self, mask: MaskIdx, pos: Vec2, rsc: &dyn UiRsc) -> f32 { let mut coverage = 1.0; let mut at = mask; for i in 0..PARENT_CHAIN_LIMIT { if at == MaskIdx::NONE { return coverage; } let m = rsc.ui().masks[at.idx()]; if let Some(rect) = self.primitives.primitive_data::(m.primitive) { let c = self.primitive_corners(m.primitive, rsc); coverage *= rounded_rect_coverage(pos, c.top_left, c.bot_right, rect.radius); } at = m.parent; debug_assert!( i + 1 < PARENT_CHAIN_LIMIT || at == MaskIdx::NONE, "mask chain exceeded PARENT_CHAIN_LIMIT ({PARENT_CHAIN_LIMIT}) from {mask:?} -- a \ repeated slot means a `parent` link is cyclic, all-distinct slots mean the tree \ nests deeper than shader.wgsl's own walk of the same bound", ); } coverage } /// Whether `pos` is inside `mask` at all -- more than half covered, /// which is where the drawn edge is (`rounded_rect_coverage`'s doc). /// What a hit test asks. pub fn mask_admits(&self, mask: MaskIdx, pos: Vec2, rsc: &dyn UiRsc) -> bool { self.mask_coverage(mask, pos, rsc) > 0.5 } /// The first primitive `id`'s subtree wrote this frame, depth first /// in draw order -- what a mask pointed at a widget clips to /// (`Painter::set_mask_to_widget`). A widget that draws more than one /// (a bordered rect is one primitive; a card with a stripe is two) /// gives its first; a widget that wants another names it. pub fn first_primitive(&self, id: WidgetId) -> Option { let active = self.active.get(&id)?; if let Some(h) = active.primitives.first() { return Some(h.slot); } active .children .iter() .find_map(|child| self.first_primitive(*child)) } pub fn window_region(&self, id: &impl IdLike, rsc: &dyn UiRsc) -> Option { let region = self.resolved_region(id, rsc)?; Some(region.to_px(self.output_size)) } /// redraws a widget that's currently active (drawn) pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { rsc.widgets_mut().needs_redraw.remove(&id); // An ancestor is drawing this widget right now, and that draw is // about to write fresh primitives for it. Drawing it a second time // here would leave one of the two copies on screen with nothing // owning it -- see `draw_started`'s own doc. if self.draw_started.contains(&id) { return; } let Some(active) = self.remove(id, false, true, rsc) else { return; }; let old_size = active.size; let parent = active.parent; // `old_move_slot` being `Some` below means the slot is reused in // place rather than freshly parented, so this is only reached for // logging/clarity's sake, never actually used to link a new slot. let parent_move_slot = self.move_parent_of(parent); self.draw_inner( active.layer, id, active.region, parent, parent_move_slot, active.mask, DrawMode::Draw, Retained { children: active.children, move_slot: Some(active.move_slot), own_mask: active.own_mask, primitives: active.primitives, }, rsc, ); // If this widget's own reported size changed, its parent's layout // (which placed it using the old size) is now stale and needs to // relay out too. Checked after the real draw, not before it -- // there is no query left that answers "what size would this be" // without actually drawing (LAYOUT.md section 5). if let Some(pid) = parent { let new_size = self.active.get(&id).map(|a| a.size); if new_size != Some(old_size) { self.redraw(pid, rsc); } } } } impl Default for UiRenderState { fn default() -> Self { Self::new() } }