Add Memory::caches for caching things from one frame to the next
This commit is contained in:
parent
613c0b29f6
commit
a1bf5aff47
9 changed files with 239 additions and 38 deletions
|
@ -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.
|
||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -831,7 +831,9 @@ dependencies = [
|
|||
name = "egui"
|
||||
version = "0.14.2"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"epaint",
|
||||
"nohash-hasher",
|
||||
"ron",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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<Id>,
|
||||
|
||||
/// 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>,
|
||||
|
||||
// ------------------------------------------
|
||||
/// 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
|
||||
pub(crate) new_pixels_per_point: Option<f32>,
|
||||
|
||||
|
@ -286,6 +315,7 @@ impl Memory {
|
|||
input: &InputState,
|
||||
used_ids: &epaint::ahash::AHashMap<Id, Rect>,
|
||||
) {
|
||||
self.caches.update();
|
||||
self.areas.end_frame();
|
||||
self.interaction.focus.end_frame(used_ids);
|
||||
self.drag_value.end_frame(input);
|
||||
|
|
|
@ -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
|
||||
#[derive(Clone)]
|
||||
pub struct Cache<K, V>([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<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
|
||||
K: Copy,
|
||||
V: Copy,
|
||||
Computer: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self([None; SIZE])
|
||||
Self::new(Computer::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> std::fmt::Debug for Cache<K, V> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Cache")
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Cache<K, V>
|
||||
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<Value, Computer> FrameCache<Value, Computer> {
|
||||
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<Value, Computer> FrameCache<Value, Computer> {
|
||||
/// Get from cache (if the same key was used last frame)
|
||||
/// 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>
|
||||
{
|
||||
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<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()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
41
egui/src/util/fixed_cache.rs
Normal file
41
egui/src/util/fixed_cache.rs
Normal file
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -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::<Cache<Color32, Hsva>>()
|
||||
.get_or_default::<FixedCache<Color32, Hsva>>()
|
||||
.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::<Cache<Color32, Hsva>>()
|
||||
.get_mut_or_default::<FixedCache<Color32, Hsva>>()
|
||||
.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::<Cache<Color32, Hsva>>()
|
||||
.get_or_default::<FixedCache<Color32, Hsva>>()
|
||||
.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::<Cache<Color32, Hsva>>()
|
||||
.get_mut_or_default::<FixedCache<Color32, Hsva>>()
|
||||
.set(*srgba, hsva);
|
||||
|
||||
response
|
||||
|
|
|
@ -405,7 +405,7 @@ struct GalleyCache {
|
|||
|
||||
impl GalleyCache {
|
||||
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) {
|
||||
std::collections::hash_map::Entry::Occupied(entry) => {
|
||||
|
|
Loading…
Reference in a new issue