diff --git a/CHANGELOG.md b/CHANGELOG.md index 0238a659..e01cf5ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md), [`eg * Add feature `"serialize"` separatedly from `"persistence"`. * Add `egui::widgets::global_dark_light_mode_buttons` to easily add buttons for switching the egui theme. * `TextEdit` can now be used to show text which can be selectedd and copied, but not edited. +* Add `Memory::caches` for caching things from one frame to the next. ### Changed 🔧 * Label text will now be centered, right-aligned and/or justified based on the layout. diff --git a/Cargo.lock b/Cargo.lock index 512cdefa..6167fcaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,7 +831,9 @@ dependencies = [ name = "egui" version = "0.14.2" dependencies = [ + "ahash", "epaint", + "nohash-hasher", "ron", "serde", "serde_json", diff --git a/egui/Cargo.toml b/egui/Cargo.toml index 499f51de..24f0811d 100644 --- a/egui/Cargo.toml +++ b/egui/Cargo.toml @@ -24,8 +24,10 @@ all-features = true [dependencies] epaint = { version = "0.14.0", path = "../epaint", default-features = false } -serde = { version = "1", features = ["derive", "rc"], optional = true } +ahash = "0.7" +nohash-hasher = "0.2" ron = { version = "0.6.4", optional = true } +serde = { version = "1", features = ["derive", "rc"], optional = true } [features] default = ["default_fonts", "single_threaded"] diff --git a/egui/src/memory.rs b/egui/src/memory.rs index ea0052c3..198a7bc9 100644 --- a/egui/src/memory.rs +++ b/egui/src/memory.rs @@ -19,6 +19,7 @@ use crate::{any, area, window, Id, InputState, LayerId, Pos2, Rect, Style}; pub struct Memory { pub options: Options, + // ------------------------------------------ /// This map stores current states for widgets that don't require `Id`. /// This will be saved between different program runs if you use the `persistence` feature. #[cfg(feature = "persistence")] @@ -30,7 +31,7 @@ pub struct Memory { pub data: any::TypeMap, /// Same as `data`, but this data will not be saved between runs. - #[cfg_attr(feature = "persistence", serde(skip))] + #[cfg_attr(feature = "serde", serde(skip))] pub data_temp: any::TypeMap, /// This map stores current states for all widgets with custom `Id`s. @@ -44,9 +45,37 @@ pub struct Memory { pub id_data: any::AnyMap, /// Same as `id_data`, but this data will not be saved between runs. - #[cfg_attr(feature = "persistence", serde(skip))] + #[cfg_attr(feature = "serde", serde(skip))] pub id_data_temp: any::AnyMap, + // ------------------------------------------ + /// Can be used to cache computations from one frame to another. + /// + /// This is for saving CPU when you have something that may take 1-100ms to compute. + /// Things that are very slow (>100ms) should instead be done async (i.e. in another thread) + /// so as not to lock the UI thread. + /// + /// ``` + /// use egui::util::cache::{ComputerMut, FrameCache}; + /// + /// #[derive(Default)] + /// struct CharCounter {} + /// impl ComputerMut<&str, usize> for CharCounter { + /// fn compute(&mut self, s: &str) -> usize { + /// s.chars().count() // you probably want to cache something more expensive than this + /// } + /// } + /// type CharCountCache<'a> = FrameCache; + /// + /// # let mut ctx = egui::CtxRef::default(); + /// let mut memory = ctx.memory(); + /// let cache = memory.caches.cache::>(); + /// assert_eq!(cache.get("hello"), 5); + /// ``` + #[cfg_attr(feature = "serde", serde(skip))] + pub caches: crate::util::cache::CacheStorage, + + // ------------------------------------------ /// new scale that will be applied at the start of the next frame pub(crate) new_pixels_per_point: Option, @@ -286,6 +315,7 @@ impl Memory { input: &InputState, used_ids: &epaint::ahash::AHashMap, ) { + self.caches.update(); self.areas.end_frame(); self.interaction.focus.end_frame(used_ids); self.drag_value.end_frame(input); diff --git a/egui/src/util/cache.rs b/egui/src/util/cache.rs index 88a7a4dc..f9f64d03 100644 --- a/egui/src/util/cache.rs +++ b/egui/src/util/cache.rs @@ -1,41 +1,164 @@ -use epaint::util::hash; +//! Computing the same thing each frame can be expensive, +//! so often you want to save the result from the previous frame and reuse it. +//! +//! Enter [`FrameCache`]: it caches the results of a computation for one frame. +//! If it is still used next frame, it is not recomputed. +//! If it is not used next frame, it is evicted from the cache to save memory. -const SIZE: usize = 1024; // must be small for web/WASM build (for unknown reason) +/// Something that does an expensive computation that we want to cache +/// to save us from recomputing it each frame. +pub trait ComputerMut: 'static + Send + Sync { + fn compute(&mut self, key: Key) -> Value; +} -/// Very stupid/simple key-value cache. TODO: improve -#[derive(Clone)] -pub struct Cache([Option<(K, V)>; SIZE]); +/// Caches the results of a computation for one frame. +/// If it is still used next frame, it is not recomputed. +/// If it is not used next frame, it is evicted from the cache to save memory. +pub struct FrameCache { + generation: u32, + computer: Computer, + cache: nohash_hasher::IntMap, +} -impl Default for Cache +impl Default for FrameCache where - K: Copy, - V: Copy, + Computer: Default, { fn default() -> Self { - Self([None; SIZE]) + Self::new(Computer::default()) } } -impl std::fmt::Debug for Cache { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Cache") - } -} - -impl Cache -where - K: std::hash::Hash + PartialEq, -{ - pub fn get(&self, key: &K) -> Option<&V> { - let bucket = (hash(key) % (SIZE as u64)) as usize; - match &self.0[bucket] { - Some((k, v)) if k == key => Some(v), - _ => None, +impl FrameCache { + pub fn new(computer: Computer) -> Self { + Self { + generation: 0, + computer, + cache: Default::default(), } } - pub fn set(&mut self, key: K, value: V) { - let bucket = (hash(&key) % (SIZE as u64)) as usize; - self.0[bucket] = Some((key, value)); + /// Must be called once per frame to clear the [`Galley`] cache. + pub fn evice_cache(&mut self) { + let current_generation = self.generation; + self.cache.retain(|_key, cached| { + cached.0 == current_generation // only keep those that were used this frame + }); + self.generation = self.generation.wrapping_add(1); + } +} + +impl FrameCache { + /// Get from cache (if the same key was used last frame) + /// or recompute and store in the cache. + pub fn get(&mut self, key: Key) -> Value + where + Key: Copy + std::hash::Hash, + Value: Clone, + Computer: ComputerMut, + { + let hash = crate::util::hash(key); + + match self.cache.entry(hash) { + std::collections::hash_map::Entry::Occupied(entry) => { + let cached = entry.into_mut(); + cached.0 = self.generation; + cached.1.clone() + } + std::collections::hash_map::Entry::Vacant(entry) => { + let value = self.computer.compute(key); + entry.insert((self.generation, value.clone())); + value + } + } + } +} + +#[allow(clippy::len_without_is_empty)] +pub trait CacheTrait: 'static + Send + Sync { + /// Call once per frame to evict cache. + fn update(&mut self); + + /// Number of values currently in the cache. + fn len(&self) -> usize; + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any; +} + +impl CacheTrait + for FrameCache +{ + fn update(&mut self) { + self.evice_cache() + } + + fn len(&self) -> usize { + self.cache.len() + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } +} + +/// ``` +/// use egui::util::cache::{CacheStorage, ComputerMut, FrameCache}; +/// +/// #[derive(Default)] +/// struct CharCounter {} +/// impl ComputerMut<&str, usize> for CharCounter { +/// fn compute(&mut self, s: &str) -> usize { +/// s.chars().count() +/// } +/// } +/// type CharCountCache<'a> = FrameCache; +/// +/// # let mut cache_storage = CacheStorage::default(); +/// let mut cache = cache_storage.cache::>(); +/// assert_eq!(cache.get("hello"), 5); +/// ``` +#[derive(Default)] +pub struct CacheStorage { + caches: ahash::AHashMap>, +} + +impl CacheStorage { + pub fn cache(&mut self) -> &mut FrameCache { + self.caches + .entry(std::any::TypeId::of::()) + .or_insert_with(|| Box::new(FrameCache::default())) + .as_any_mut() + .downcast_mut::() + .unwrap() + } + + /// Total number of cached values + fn num_values(&self) -> usize { + self.caches.values().map(|cache| cache.len()).sum() + } + + /// Call once per frame to evict cache. + pub fn update(&mut self) { + for cache in self.caches.values_mut() { + cache.update(); + } + } +} + +impl Clone for CacheStorage { + fn clone(&self) -> Self { + // We return an empty cache that can be filled in again. + Self::default() + } +} + +impl std::fmt::Debug for CacheStorage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "FrameCacheStorage[{} caches with {} elements]", + self.caches.len(), + self.num_values() + ) } } diff --git a/egui/src/util/fixed_cache.rs b/egui/src/util/fixed_cache.rs new file mode 100644 index 00000000..78882f3d --- /dev/null +++ b/egui/src/util/fixed_cache.rs @@ -0,0 +1,41 @@ +use epaint::util::hash; + +const FIXED_CACHE_SIZE: usize = 1024; // must be small for web/WASM build (for unknown reason) + +/// Very stupid/simple key-value cache. TODO: improve +#[derive(Clone)] +pub(crate) struct FixedCache([Option<(K, V)>; FIXED_CACHE_SIZE]); + +impl Default for FixedCache +where + K: Copy, + V: Copy, +{ + fn default() -> Self { + Self([None; FIXED_CACHE_SIZE]) + } +} + +impl std::fmt::Debug for FixedCache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Cache") + } +} + +impl FixedCache +where + K: std::hash::Hash + PartialEq, +{ + pub fn get(&self, key: &K) -> Option<&V> { + let bucket = (hash(key) % (FIXED_CACHE_SIZE as u64)) as usize; + match &self.0[bucket] { + Some((k, v)) if k == key => Some(v), + _ => None, + } + } + + pub fn set(&mut self, key: K, value: V) { + let bucket = (hash(&key) % (FIXED_CACHE_SIZE as u64)) as usize; + self.0[bucket] = Some((key, value)); + } +} diff --git a/egui/src/util/mod.rs b/egui/src/util/mod.rs index ea785458..c5b96a32 100644 --- a/egui/src/util/mod.rs +++ b/egui/src/util/mod.rs @@ -1,8 +1,10 @@ //! Miscellaneous tools used by the rest of egui. -pub(crate) mod cache; +pub mod cache; +pub(crate) mod fixed_cache; mod history; pub mod undoer; -pub(crate) use cache::Cache; pub use history::History; + +pub use epaint::util::{hash, hash_with}; diff --git a/egui/src/widgets/color_picker.rs b/egui/src/widgets/color_picker.rs index 658d3e43..79471350 100644 --- a/egui/src/widgets/color_picker.rs +++ b/egui/src/widgets/color_picker.rs @@ -1,6 +1,6 @@ //! Color picker widgets. -use crate::util::Cache; +use crate::util::fixed_cache::FixedCache; use crate::*; use epaint::{color::*, *}; @@ -322,7 +322,7 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b .ctx() .memory() .data_temp - .get_or_default::>() + .get_or_default::>() .get(srgba) .cloned() .unwrap_or_else(|| Hsva::from(*srgba)); @@ -334,7 +334,7 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b ui.ctx() .memory() .data_temp - .get_mut_or_default::>() + .get_mut_or_default::>() .set(*srgba, hsva); response @@ -386,7 +386,7 @@ pub fn color_edit_button_srgba(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) - .ctx() .memory() .data_temp - .get_or_default::>() + .get_or_default::>() .get(srgba) .cloned() .unwrap_or_else(|| Hsva::from(*srgba)); @@ -398,7 +398,7 @@ pub fn color_edit_button_srgba(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) - ui.ctx() .memory() .data_temp - .get_mut_or_default::>() + .get_mut_or_default::>() .set(*srgba, hsva); response diff --git a/epaint/src/text/fonts.rs b/epaint/src/text/fonts.rs index 73d95f9f..c78b98f7 100644 --- a/epaint/src/text/fonts.rs +++ b/epaint/src/text/fonts.rs @@ -405,7 +405,7 @@ struct GalleyCache { impl GalleyCache { fn layout(&mut self, fonts: &Fonts, job: LayoutJob) -> Arc { - let hash = crate::util::hash_with(&job, ahash::AHasher::new_with_keys(123, 456)); // TODO: even faster hasher? + let hash = crate::util::hash(&job); // TODO: even faster hasher? match self.cache.entry(hash) { std::collections::hash_map::Entry::Occupied(entry) => {