1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

Add measurement cache

This commit is contained in:
lucasmerlin
2026-06-05 13:15:35 +02:00
parent 3a85b165aa
commit ca2131824d
4 changed files with 61 additions and 3 deletions

View File

@@ -1,3 +1,4 @@
use super::MeasureCache;
use crate::{ use crate::{
AtomKind, AtomLayout, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui, AtomKind, AtomLayout, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui,
}; };
@@ -124,6 +125,7 @@ impl<'a> Atom<'a> {
mut available_size: Vec2, mut available_size: Vec2,
mut wrap_mode: Option<TextWrapMode>, mut wrap_mode: Option<TextWrapMode>,
fallback_font: FontSelection, fallback_font: FontSelection,
cache: &mut MeasureCache<'a>,
) -> SizedAtom<'a> { ) -> SizedAtom<'a> {
if !self.shrink && self.max_size.x.is_infinite() { if !self.shrink && self.max_size.x.is_infinite() {
wrap_mode = Some(TextWrapMode::Extend); wrap_mode = Some(TextWrapMode::Extend);
@@ -149,6 +151,7 @@ impl<'a> Atom<'a> {
wrap_mode, wrap_mode,
fallback_font, fallback_font,
}, },
cache,
); );
let size = self let size = self

View File

@@ -102,6 +102,7 @@ impl<'a> AtomKind<'a> {
wrap_mode, wrap_mode,
fallback_font, fallback_font,
}: IntoSizedArgs, }: IntoSizedArgs,
cache: &mut super::MeasureCache<'a>,
) -> IntoSizedResult<'a> { ) -> IntoSizedResult<'a> {
match self { match self {
AtomKind::Text(text) => { AtomKind::Text(text) => {
@@ -131,8 +132,10 @@ impl<'a> AtomKind<'a> {
AtomKind::Layout(layout) => { AtomKind::Layout(layout) => {
// Measure at the natural size for the parent's sizing, but keep a shared handle to // Measure at the natural size for the parent's sizing, but keep a shared handle to
// the original layout so a grown atom can be re-measured at its painted size in // the original layout so a grown atom can be re-measured at its painted size in
// `paint_at` (cheap `Arc` clone, no deep copy). // `paint_at` (cheap `Rc` clone, no deep copy). `measure_rc` shares the `cache`
let sized = layout.measure(ui, available_size); // (keyed by the `Rc`'s identity) so a deep tree of `grow` layouts doesn't
// re-measure its descendants exponentially.
let sized = AtomLayout::measure_rc(layout, ui, available_size, cache);
IntoSizedResult { IntoSizedResult {
intrinsic_size: sized.intrinsic_size, intrinsic_size: sized.intrinsic_size,
sized: SizedAtomKind::Layout { sized: SizedAtomKind::Layout {

View File

@@ -6,9 +6,19 @@ use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
use epaint::text::TextWrapMode; use epaint::text::TextWrapMode;
use epaint::{Color32, Galley}; use epaint::{Color32, Galley};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
/// Frame-pass-local memoization cache for [`AtomLayout::measure_rc`].
///
/// Keyed by an [`Rc::as_ptr`] identity plus the available-size bits. Both are stable within a
/// single top-level measure pass (nested layouts are held alive via `Rc`), so repeatedly measuring
/// the same nested layout at the same size — which a deep tree of `grow` layouts does `O(2^depth)`
/// times — becomes a cache hit instead of a full re-measure.
pub(crate) type MeasureCache<'a> = HashMap<(usize, u64), SizedAtomLayout<'a>>;
/// The `(main, cross)` axis indices for `direction`, for indexing a [`Vec2`] (0 = x, 1 = y). /// The `(main, cross)` axis indices for `direction`, for indexing a [`Vec2`] (0 = x, 1 = y).
#[inline] #[inline]
fn main_cross_axis(direction: Direction) -> (usize, usize) { fn main_cross_axis(direction: Direction) -> (usize, usize) {
@@ -341,6 +351,44 @@ impl<'a> AtomLayout<'a> {
/// clamped by `max_size`/`min_size`, exactly like [`Self::allocate`] does with /// clamped by `max_size`/`min_size`, exactly like [`Self::allocate`] does with
/// [`Ui::available_size`]. /// [`Ui::available_size`].
pub fn measure(&self, ui: &Ui, available_size: Vec2) -> SizedAtomLayout<'a> { pub fn measure(&self, ui: &Ui, available_size: Vec2) -> SizedAtomLayout<'a> {
self.measure_impl(ui, available_size, &mut MeasureCache::default())
}
/// Measure a nested layout held by an [`Rc`], memoizing the result in `cache`.
///
/// A grown nested `Layout` atom is re-measured (the cross-after-main reflow) at its grown
/// size, recursively. Without memoization a deep tree of `grow` layouts re-measures its
/// descendants `O(2^depth)` times. Keyed by the layout's [`Rc::as_ptr`] identity and the
/// available size — both stable within a pass — repeated `(layout, size)` measures become
/// cache hits. The `Rc` is held by the caller (the `Layout` atom / reflow source), which is
/// why the identity lives here rather than in [`Self::measure_impl`].
pub(crate) fn measure_rc(
layout: &Rc<Self>,
ui: &Ui,
available_size: Vec2,
cache: &mut MeasureCache<'a>,
) -> SizedAtomLayout<'a> {
let key = (
Rc::as_ptr(layout) as usize,
(u64::from(available_size.x.to_bits()) << 32) | u64::from(available_size.y.to_bits()),
);
if let Some(cached) = cache.get(&key) {
return cached.clone();
}
let result = layout.measure_impl(ui, available_size, cache);
cache.insert(key, result.clone());
result
}
/// The measure body. Threads `cache` so nested [`Rc`] layouts are memoized via
/// [`Self::measure_rc`]; it does not memoize its own result (a top-level layout is measured
/// once, and a nested one is keyed by its `Rc` at the call site).
pub(crate) fn measure_impl(
&self,
ui: &Ui,
available_size: Vec2,
cache: &mut MeasureCache<'a>,
) -> SizedAtomLayout<'a> {
let atoms = &self.atoms; let atoms = &self.atoms;
let frame = self.frame; let frame = self.frame;
let sense = self.sense; let sense = self.sense;
@@ -444,6 +492,7 @@ impl<'a> AtomLayout<'a> {
available_inner_size, available_inner_size,
Some(wrap_mode), Some(wrap_mode),
fallback_font.clone(), fallback_font.clone(),
cache,
); );
let size = sized.size; let size = sized.size;
@@ -473,6 +522,7 @@ impl<'a> AtomLayout<'a> {
available_size_for_shrink_item, available_size_for_shrink_item,
Some(wrap_mode), Some(wrap_mode),
fallback_font, fallback_font,
cache,
) )
} else { } else {
let mut item = item.clone(); let mut item = item.clone();
@@ -482,6 +532,7 @@ impl<'a> AtomLayout<'a> {
available_size_for_shrink_item, available_size_for_shrink_item,
Some(wrap_mode), Some(wrap_mode),
fallback_font, fallback_font,
cache,
) )
}; };
let size = sized.size; let size = sized.size;
@@ -553,7 +604,7 @@ impl<'a> AtomLayout<'a> {
sized.size[main_axis] + grow_main, sized.size[main_axis] + grow_main,
available_inner_size[cross_axis], available_inner_size[cross_axis],
); );
let remeasured = source.measure(ui, grown); let remeasured = AtomLayout::measure_rc(source, ui, grown, cache);
sized.size[cross_axis] = remeasured.outer_size[cross_axis]; sized.size[cross_axis] = remeasured.outer_size[cross_axis];
**inner = remeasured; **inner = remeasured;
} }

View File

@@ -594,6 +594,7 @@ impl TextEdit<'_> {
Vec2::new(available_inner_width, f32::INFINITY), Vec2::new(available_inner_width, f32::INFINITY),
Some(TextWrapMode::Extend), Some(TextWrapMode::Extend),
FontSelection::default(), FontSelection::default(),
&mut Default::default(),
) )
.size .size
.x .x