Browse Source

Add Memory::caches for caching things from one frame to the next

pull/775/head
Emil Ernerfeldt 3 years ago
parent
commit
a1bf5aff47
  1. 1
      CHANGELOG.md
  2. 2
      Cargo.lock
  3. 4
      egui/Cargo.toml
  4. 34
      egui/src/memory.rs
  5. 169
      egui/src/util/cache.rs
  6. 41
      egui/src/util/fixed_cache.rs
  7. 6
      egui/src/util/mod.rs
  8. 10
      egui/src/widgets/color_picker.rs
  9. 2
      epaint/src/text/fonts.rs

1
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 feature `"serialize"` separatedly from `"persistence"`.
* Add `egui::widgets::global_dark_light_mode_buttons` to easily add buttons for switching the egui theme. * 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. * `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 🔧 ### Changed 🔧
* Label text will now be centered, right-aligned and/or justified based on the layout. * Label text will now be centered, right-aligned and/or justified based on the layout.

2
Cargo.lock

@ -831,7 +831,9 @@ dependencies = [
name = "egui" name = "egui"
version = "0.14.2" version = "0.14.2"
dependencies = [ dependencies = [
"ahash",
"epaint", "epaint",
"nohash-hasher",
"ron", "ron",
"serde", "serde",
"serde_json", "serde_json",

4
egui/Cargo.toml

@ -24,8 +24,10 @@ all-features = true
[dependencies] [dependencies]
epaint = { version = "0.14.0", path = "../epaint", default-features = false } 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 } ron = { version = "0.6.4", optional = true }
serde = { version = "1", features = ["derive", "rc"], optional = true }
[features] [features]
default = ["default_fonts", "single_threaded"] default = ["default_fonts", "single_threaded"]

34
egui/src/memory.rs

@ -19,6 +19,7 @@ use crate::{any, area, window, Id, InputState, LayerId, Pos2, Rect, Style};
pub struct Memory { pub struct Memory {
pub options: Options, pub options: Options,
// ------------------------------------------
/// This map stores current states for widgets that don't require `Id`. /// 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. /// This will be saved between different program runs if you use the `persistence` feature.
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
@ -30,7 +31,7 @@ pub struct Memory {
pub data: any::TypeMap, pub data: any::TypeMap,
/// Same as `data`, but this data will not be saved between runs. /// 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, pub data_temp: any::TypeMap,
/// This map stores current states for all widgets with custom `Id`s. /// This map stores current states for all widgets with custom `Id`s.
@ -44,9 +45,37 @@ pub struct Memory {
pub id_data: any::AnyMap<Id>, pub id_data: any::AnyMap<Id>,
/// Same as `id_data`, but this data will not be saved between runs. /// 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<Id>, pub id_data_temp: any::AnyMap<Id>,
// ------------------------------------------
/// 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<usize, CharCounter>;
///
/// # let mut ctx = egui::CtxRef::default();
/// let mut memory = ctx.memory();
/// let cache = memory.caches.cache::<CharCountCache<'_>>();
/// 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 /// new scale that will be applied at the start of the next frame
pub(crate) new_pixels_per_point: Option<f32>, pub(crate) new_pixels_per_point: Option<f32>,
@ -286,6 +315,7 @@ impl Memory {
input: &InputState, input: &InputState,
used_ids: &epaint::ahash::AHashMap<Id, Rect>, used_ids: &epaint::ahash::AHashMap<Id, Rect>,
) { ) {
self.caches.update();
self.areas.end_frame(); self.areas.end_frame();
self.interaction.focus.end_frame(used_ids); self.interaction.focus.end_frame(used_ids);
self.drag_value.end_frame(input); self.drag_value.end_frame(input);

169
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<Key, Value>: 'static + Send + Sync {
fn compute(&mut self, key: Key) -> Value;
}
/// Very stupid/simple key-value cache. TODO: improve /// Caches the results of a computation for one frame.
#[derive(Clone)] /// If it is still used next frame, it is not recomputed.
pub struct Cache<K, V>([Option<(K, V)>; SIZE]); /// If it is not used next frame, it is evicted from the cache to save memory.
pub struct FrameCache<Value, Computer> {
generation: u32,
computer: Computer,
cache: nohash_hasher::IntMap<u64, (u32, Value)>,
}
impl<K, V> Default for Cache<K, V> impl<Value, Computer> Default for FrameCache<Value, Computer>
where where
K: Copy, Computer: Default,
V: Copy,
{ {
fn default() -> Self { fn default() -> Self {
Self([None; SIZE]) Self::new(Computer::default())
} }
} }
impl<K, V> std::fmt::Debug for Cache<K, V> { impl<Value, Computer> FrameCache<Value, Computer> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { pub fn new(computer: Computer) -> Self {
write!(f, "Cache") Self {
generation: 0,
computer,
cache: Default::default(),
}
}
/// 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<K, V> Cache<K, V> impl<Value, Computer> FrameCache<Value, Computer> {
where /// Get from cache (if the same key was used last frame)
K: std::hash::Hash + PartialEq, /// or recompute and store in the cache.
pub fn get<Key>(&mut self, key: Key) -> Value
where
Key: Copy + std::hash::Hash,
Value: Clone,
Computer: ComputerMut<Key, Value>,
{
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<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait
for FrameCache<Value, Computer>
{ {
pub fn get(&self, key: &K) -> Option<&V> { fn update(&mut self) {
let bucket = (hash(key) % (SIZE as u64)) as usize; self.evice_cache()
match &self.0[bucket] { }
Some((k, v)) if k == key => Some(v),
_ => None, 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<usize, CharCounter>;
///
/// # let mut cache_storage = CacheStorage::default();
/// let mut cache = cache_storage.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5);
/// ```
#[derive(Default)]
pub struct CacheStorage {
caches: ahash::AHashMap<std::any::TypeId, Box<dyn CacheTrait>>,
}
impl CacheStorage {
pub fn cache<FrameCache: CacheTrait + Default>(&mut self) -> &mut FrameCache {
self.caches
.entry(std::any::TypeId::of::<FrameCache>())
.or_insert_with(|| Box::new(FrameCache::default()))
.as_any_mut()
.downcast_mut::<FrameCache>()
.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()
}
}
pub fn set(&mut self, key: K, value: V) { impl std::fmt::Debug for CacheStorage {
let bucket = (hash(&key) % (SIZE as u64)) as usize; fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0[bucket] = Some((key, value)); write!(
f,
"FrameCacheStorage[{} caches with {} elements]",
self.caches.len(),
self.num_values()
)
} }
} }

41
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<K, V>([Option<(K, V)>; FIXED_CACHE_SIZE]);
impl<K, V> Default for FixedCache<K, V>
where
K: Copy,
V: Copy,
{
fn default() -> Self {
Self([None; FIXED_CACHE_SIZE])
}
}
impl<K, V> std::fmt::Debug for FixedCache<K, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Cache")
}
}
impl<K, V> FixedCache<K, V>
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));
}
}

6
egui/src/util/mod.rs

@ -1,8 +1,10 @@
//! Miscellaneous tools used by the rest of egui. //! Miscellaneous tools used by the rest of egui.
pub(crate) mod cache; pub mod cache;
pub(crate) mod fixed_cache;
mod history; mod history;
pub mod undoer; pub mod undoer;
pub(crate) use cache::Cache;
pub use history::History; pub use history::History;
pub use epaint::util::{hash, hash_with};

10
egui/src/widgets/color_picker.rs

@ -1,6 +1,6 @@
//! Color picker widgets. //! Color picker widgets.
use crate::util::Cache; use crate::util::fixed_cache::FixedCache;
use crate::*; use crate::*;
use epaint::{color::*, *}; use epaint::{color::*, *};
@ -322,7 +322,7 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b
.ctx() .ctx()
.memory() .memory()
.data_temp .data_temp
.get_or_default::<Cache<Color32, Hsva>>() .get_or_default::<FixedCache<Color32, Hsva>>()
.get(srgba) .get(srgba)
.cloned() .cloned()
.unwrap_or_else(|| Hsva::from(*srgba)); .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() ui.ctx()
.memory() .memory()
.data_temp .data_temp
.get_mut_or_default::<Cache<Color32, Hsva>>() .get_mut_or_default::<FixedCache<Color32, Hsva>>()
.set(*srgba, hsva); .set(*srgba, hsva);
response response
@ -386,7 +386,7 @@ pub fn color_edit_button_srgba(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -
.ctx() .ctx()
.memory() .memory()
.data_temp .data_temp
.get_or_default::<Cache<Color32, Hsva>>() .get_or_default::<FixedCache<Color32, Hsva>>()
.get(srgba) .get(srgba)
.cloned() .cloned()
.unwrap_or_else(|| Hsva::from(*srgba)); .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() ui.ctx()
.memory() .memory()
.data_temp .data_temp
.get_mut_or_default::<Cache<Color32, Hsva>>() .get_mut_or_default::<FixedCache<Color32, Hsva>>()
.set(*srgba, hsva); .set(*srgba, hsva);
response response

2
epaint/src/text/fonts.rs

@ -405,7 +405,7 @@ struct GalleyCache {
impl GalleyCache { impl GalleyCache {
fn layout(&mut self, fonts: &Fonts, job: LayoutJob) -> Arc<Galley> { fn layout(&mut self, fonts: &Fonts, job: LayoutJob) -> Arc<Galley> {
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) { match self.cache.entry(hash) {
std::collections::hash_map::Entry::Occupied(entry) => { std::collections::hash_map::Entry::Occupied(entry) => {

Loading…
Cancel
Save