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 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
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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(),
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(&mut self, key: K, value: V) {
|
/// Must be called once per frame to clear the [`Galley`] cache.
|
||||||
let bucket = (hash(&key) % (SIZE as u64)) as usize;
|
pub fn evice_cache(&mut self) {
|
||||||
self.0[bucket] = Some((key, value));
|
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.
|
//! 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};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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…
Reference in a new issue