From b5b78d505e22cafccb4ecbf57dc61f536ca558ca Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 30 Apr 2024 07:57:54 +0200 Subject: Introduce `canvas::Cache` grouping Caches with the same `Group` will share their text atlas! --- graphics/src/cache.rs | 158 +++++++++++++++++++++++++++++++++++++++++ graphics/src/cached.rs | 24 ------- graphics/src/geometry.rs | 2 +- graphics/src/geometry/cache.rs | 83 ++++++++++------------ graphics/src/lib.rs | 4 +- renderer/src/fallback.rs | 16 +++-- tiny_skia/src/geometry.rs | 5 +- wgpu/src/geometry.rs | 13 ++-- wgpu/src/text.rs | 74 ++++++++++++++----- widget/src/canvas.rs | 1 + 10 files changed, 279 insertions(+), 101 deletions(-) create mode 100644 graphics/src/cache.rs delete mode 100644 graphics/src/cached.rs diff --git a/graphics/src/cache.rs b/graphics/src/cache.rs new file mode 100644 index 00000000..7106c392 --- /dev/null +++ b/graphics/src/cache.rs @@ -0,0 +1,158 @@ +//! Cache computations and efficiently reuse them. +use std::cell::RefCell; +use std::fmt; +use std::sync::atomic::{self, AtomicU64}; + +/// A simple cache that stores generated values to avoid recomputation. +/// +/// Keeps track of the last generated value after clearing. +pub struct Cache { + group: Group, + state: RefCell>, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + group: Group::unique(), + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + Cache { + group, + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Returns the [`Group`] of the [`Cache`]. + pub fn group(&self) -> Group { + self.group + } + + /// Puts the given value in the [`Cache`]. + /// + /// Notice that, given this is a cache, a mutable reference is not + /// necessary to call this method. You can safely update the cache in + /// rendering code. + pub fn put(&self, value: T) { + *self.state.borrow_mut() = State::Filled { current: value }; + } + + /// Returns a reference cell to the internal [`State`] of the [`Cache`]. + pub fn state(&self) -> &RefCell> { + &self.state + } + + /// Clears the [`Cache`]. + pub fn clear(&self) + where + T: Clone, + { + use std::ops::Deref; + + let previous = match self.state.borrow().deref() { + State::Empty { previous } => previous.clone(), + State::Filled { current } => Some(current.clone()), + }; + + *self.state.borrow_mut() = State::Empty { previous }; + } +} + +/// A cache group. +/// +/// Caches that share the same group generally change together. +/// +/// A cache group can be used to implement certain performance +/// optimizations during rendering, like batching or sharing atlases. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Group(u64); + +impl Group { + /// Generates a new unique cache [`Group`]. + pub fn unique() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + + Self(NEXT.fetch_add(1, atomic::Ordering::Relaxed)) + } +} + +impl fmt::Debug for Cache +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::ops::Deref; + + let state = self.state.borrow(); + + match state.deref() { + State::Empty { previous } => { + write!(f, "Cache::Empty {{ previous: {previous:?} }}") + } + State::Filled { current } => { + write!(f, "Cache::Filled {{ current: {current:?} }}") + } + } + } +} + +impl Default for Cache { + fn default() -> Self { + Self::new() + } +} + +/// The state of a [`Cache`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum State { + /// The [`Cache`] is empty. + Empty { + /// The previous value of the [`Cache`]. + previous: Option, + }, + /// The [`Cache`] is filled. + Filled { + /// The current value of the [`Cache`] + current: T, + }, +} + +/// A piece of data that can be cached. +pub trait Cached: Sized { + /// The type of cache produced. + type Cache: Clone; + + /// Loads the [`Cache`] into a proper instance. + /// + /// [`Cache`]: Self::Cache + fn load(cache: &Self::Cache) -> Self; + + /// Caches this value, producing its corresponding [`Cache`]. + /// + /// [`Cache`]: Self::Cache + fn cache(self, group: Group, previous: Option) -> Self::Cache; +} + +#[cfg(debug_assertions)] +impl Cached for () { + type Cache = (); + + fn load(_cache: &Self::Cache) -> Self {} + + fn cache( + self, + _group: Group, + _previous: Option, + ) -> Self::Cache { + } +} diff --git a/graphics/src/cached.rs b/graphics/src/cached.rs deleted file mode 100644 index c0e4e029..00000000 --- a/graphics/src/cached.rs +++ /dev/null @@ -1,24 +0,0 @@ -/// A piece of data that can be cached. -pub trait Cached: Sized { - /// The type of cache produced. - type Cache: Clone; - - /// Loads the [`Cache`] into a proper instance. - /// - /// [`Cache`]: Self::Cache - fn load(cache: &Self::Cache) -> Self; - - /// Caches this value, producing its corresponding [`Cache`]. - /// - /// [`Cache`]: Self::Cache - fn cache(self, previous: Option) -> Self::Cache; -} - -#[cfg(debug_assertions)] -impl Cached for () { - type Cache = (); - - fn load(_cache: &Self::Cache) -> Self {} - - fn cache(self, _previous: Option) -> Self::Cache {} -} diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index dd07097f..ab4a7a36 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -18,8 +18,8 @@ pub use text::Text; pub use crate::gradient::{self, Gradient}; +use crate::cache::Cached; use crate::core::{self, Size}; -use crate::Cached; /// A renderer capable of drawing some [`Self::Geometry`]. pub trait Renderer: core::Renderer { diff --git a/graphics/src/geometry/cache.rs b/graphics/src/geometry/cache.rs index 665e996b..d70cee0b 100644 --- a/graphics/src/geometry/cache.rs +++ b/graphics/src/geometry/cache.rs @@ -1,8 +1,8 @@ +use crate::cache::{self, Cached}; use crate::core::Size; use crate::geometry::{self, Frame}; -use crate::Cached; -use std::cell::RefCell; +pub use cache::Group; /// A simple cache that stores generated geometry to avoid recomputation. /// @@ -12,7 +12,13 @@ pub struct Cache where Renderer: geometry::Renderer, { - state: RefCell>, + raw: crate::Cache::Cache>>, +} + +#[derive(Debug, Clone)] +struct Data { + bounds: Size, + geometry: T, } impl Cache @@ -22,20 +28,25 @@ where /// Creates a new empty [`Cache`]. pub fn new() -> Self { Cache { - state: RefCell::new(State::Empty { previous: None }), + raw: cache::Cache::new(), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + Cache { + raw: crate::Cache::with_group(group), } } /// Clears the [`Cache`], forcing a redraw the next time it is used. pub fn clear(&self) { - use std::ops::Deref; - - let previous = match self.state.borrow().deref() { - State::Empty { previous } => previous.clone(), - State::Filled { geometry, .. } => Some(geometry.clone()), - }; - - *self.state.borrow_mut() = State::Empty { previous }; + self.raw.clear(); } /// Draws geometry using the provided closure and stores it in the @@ -56,27 +67,30 @@ where ) -> Renderer::Geometry { use std::ops::Deref; - let previous = match self.state.borrow().deref() { - State::Empty { previous } => previous.clone(), - State::Filled { - bounds: cached_bounds, - geometry, - } => { - if *cached_bounds == bounds { - return Cached::load(geometry); + let state = self.raw.state(); + + let previous = match state.borrow().deref() { + cache::State::Empty { previous } => { + previous.as_ref().map(|data| data.geometry.clone()) + } + cache::State::Filled { current } => { + if current.bounds == bounds { + return Cached::load(¤t.geometry); } - Some(geometry.clone()) + Some(current.geometry.clone()) } }; let mut frame = Frame::new(renderer, bounds); draw_fn(&mut frame); - let geometry = frame.into_geometry().cache(previous); + let geometry = frame.into_geometry().cache(self.raw.group(), previous); let result = Cached::load(&geometry); - *self.state.borrow_mut() = State::Filled { bounds, geometry }; + *state.borrow_mut() = cache::State::Filled { + current: Data { bounds, geometry }, + }; result } @@ -85,16 +99,10 @@ where impl std::fmt::Debug for Cache where Renderer: geometry::Renderer, + ::Cache: std::fmt::Debug, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let state = self.state.borrow(); - - match *state { - State::Empty { .. } => write!(f, "Cache::Empty"), - State::Filled { bounds, .. } => { - write!(f, "Cache::Filled {{ bounds: {bounds:?} }}") - } - } + write!(f, "{:?}", &self.raw) } } @@ -106,16 +114,3 @@ where Self::new() } } - -enum State -where - Geometry: Cached, -{ - Empty { - previous: Option, - }, - Filled { - bounds: Size, - geometry: Geometry::Cache, - }, -} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 865ebd97..b5ef55e7 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -9,10 +9,10 @@ )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] mod antialiasing; -mod cached; mod settings; mod viewport; +pub mod cache; pub mod color; pub mod compositor; pub mod damage; @@ -27,7 +27,7 @@ pub mod text; pub mod geometry; pub use antialiasing::Antialiasing; -pub use cached::Cached; +pub use cache::Cache; pub use compositor::Compositor; pub use error::Error; pub use gradient::Gradient; diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index c932de00..5f69b420 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -428,8 +428,8 @@ where mod geometry { use super::Renderer; use crate::core::{Point, Radians, Rectangle, Size, Vector}; + use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::{self, Fill, Path, Stroke, Text}; - use crate::graphics::Cached; impl geometry::Renderer for Renderer where @@ -483,21 +483,25 @@ mod geometry { } } - fn cache(self, previous: Option) -> Self::Cache { + fn cache( + self, + group: cache::Group, + previous: Option, + ) -> Self::Cache { match (self, previous) { ( Self::Primary(geometry), Some(Geometry::Primary(previous)), - ) => Geometry::Primary(geometry.cache(Some(previous))), + ) => Geometry::Primary(geometry.cache(group, Some(previous))), (Self::Primary(geometry), None) => { - Geometry::Primary(geometry.cache(None)) + Geometry::Primary(geometry.cache(group, None)) } ( Self::Secondary(geometry), Some(Geometry::Secondary(previous)), - ) => Geometry::Secondary(geometry.cache(Some(previous))), + ) => Geometry::Secondary(geometry.cache(group, Some(previous))), (Self::Secondary(geometry), None) => { - Geometry::Secondary(geometry.cache(None)) + Geometry::Secondary(geometry.cache(group, None)) } _ => unreachable!(), } diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 117daf41..02b6e1b9 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,9 +1,10 @@ use crate::core::text::LineHeight; use crate::core::{Pixels, Point, Radians, Rectangle, Size, Vector}; +use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{self, Path, Style}; -use crate::graphics::{Cached, Gradient, Text}; +use crate::graphics::{Gradient, Text}; use crate::Primitive; use std::rc::Rc; @@ -32,7 +33,7 @@ impl Cached for Geometry { Self::Cache(cache.clone()) } - fn cache(self, _previous: Option) -> Cache { + fn cache(self, _group: cache::Group, _previous: Option) -> Cache { match self { Self::Live { primitives, diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 60967082..f6213e1d 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -3,6 +3,7 @@ use crate::core::text::LineHeight; use crate::core::{ Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, }; +use crate::graphics::cache::{self, Cached}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ @@ -10,7 +11,7 @@ use crate::graphics::geometry::{ }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::graphics::{self, Cached, Text}; +use crate::graphics::{self, Text}; use crate::text; use crate::triangle; @@ -38,7 +39,11 @@ impl Cached for Geometry { Geometry::Cached(cache.clone()) } - fn cache(self, previous: Option) -> Self::Cache { + fn cache( + self, + group: cache::Group, + previous: Option, + ) -> Self::Cache { match self { Self::Live { meshes, text } => { if let Some(mut previous) = previous { @@ -51,14 +56,14 @@ impl Cached for Geometry { if let Some(cache) = &mut previous.text { cache.update(text); } else { - previous.text = text::Cache::new(text); + previous.text = text::Cache::new(group, text); } previous } else { Cache { meshes: triangle::Cache::new(meshes), - text: text::Cache::new(text), + text: text::Cache::new(group, text), } } } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 38712660..5806863c 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,7 +1,8 @@ use crate::core::alignment; use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::cache; use crate::graphics::color; -use crate::graphics::text::cache::{self, Cache as BufferCache}; +use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache}; use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; use rustc_hash::FxHashMap; @@ -35,6 +36,7 @@ pub enum Item { #[derive(Debug, Clone)] pub struct Cache { id: Id, + group: cache::Group, text: Rc<[Text]>, version: usize, } @@ -43,7 +45,7 @@ pub struct Cache { pub struct Id(u64); impl Cache { - pub fn new(text: Vec) -> Option { + pub fn new(group: cache::Group, text: Vec) -> Option { static NEXT_ID: AtomicU64 = AtomicU64::new(0); if text.is_empty() { @@ -52,6 +54,7 @@ impl Cache { Some(Self { id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + group, text: Rc::from(text), version: 0, }) @@ -65,29 +68,39 @@ impl Cache { struct Upload { renderer: glyphon::TextRenderer, - atlas: glyphon::TextAtlas, buffer_cache: BufferCache, transformation: Transformation, version: usize, text: rc::Weak<[Text]>, + _atlas: rc::Weak<()>, } #[derive(Default)] pub struct Storage { + groups: FxHashMap, uploads: FxHashMap, } +struct Group { + atlas: glyphon::TextAtlas, + previous_uploads: usize, + handle: Rc<()>, +} + impl Storage { pub fn new() -> Self { Self::default() } - fn get(&self, cache: &Cache) -> Option<&Upload> { + fn get(&self, cache: &Cache) -> Option<(&glyphon::TextAtlas, &Upload)> { if cache.text.is_empty() { return None; } - self.uploads.get(&cache.id) + self.groups + .get(&cache.group) + .map(|group| &group.atlas) + .zip(self.uploads.get(&cache.id)) } fn prepare( @@ -101,6 +114,20 @@ impl Storage { bounds: Rectangle, target_size: Size, ) { + let group_count = self.groups.len(); + + let group = self.groups.entry(cache.group).or_insert_with(|| { + log::info!("New text atlas created (total: {})", group_count + 1); + + Group { + atlas: glyphon::TextAtlas::with_color_mode( + device, queue, format, COLOR_MODE, + ), + previous_uploads: 0, + handle: Rc::new(()), + } + }); + match self.uploads.entry(cache.id) { hash_map::Entry::Occupied(entry) => { let upload = entry.into_mut(); @@ -114,7 +141,7 @@ impl Storage { queue, encoder, &mut upload.renderer, - &mut upload.atlas, + &mut group.atlas, &mut upload.buffer_cache, &cache.text, bounds, @@ -127,16 +154,11 @@ impl Storage { upload.transformation = new_transformation; upload.buffer_cache.trim(); - upload.atlas.trim(); } } hash_map::Entry::Vacant(entry) => { - let mut atlas = glyphon::TextAtlas::with_color_mode( - device, queue, format, COLOR_MODE, - ); - let mut renderer = glyphon::TextRenderer::new( - &mut atlas, + &mut group.atlas, device, wgpu::MultisampleState::default(), None, @@ -149,7 +171,7 @@ impl Storage { queue, encoder, &mut renderer, - &mut atlas, + &mut group.atlas, &mut buffer_cache, &cache.text, bounds, @@ -159,11 +181,11 @@ impl Storage { let _ = entry.insert(Upload { renderer, - atlas, buffer_cache, transformation: new_transformation, version: 0, text: Rc::downgrade(&cache.text), + _atlas: Rc::downgrade(&group.handle), }); log::info!( @@ -178,6 +200,22 @@ impl Storage { pub fn trim(&mut self) { self.uploads .retain(|_id, upload| upload.text.strong_count() > 0); + + self.groups.retain(|_id, group| { + let uploads_alive = Rc::weak_count(&group.handle); + + if uploads_alive == 0 { + return false; + } + + if uploads_alive < group.previous_uploads { + group.atlas.trim(); + } + + group.previous_uploads = uploads_alive; + + true + }); } } @@ -306,10 +344,10 @@ impl Pipeline { layer_count += 1; } Item::Cached { cache, .. } => { - if let Some(upload) = storage.get(cache) { + if let Some((atlas, upload)) = storage.get(cache) { upload .renderer - .render(&upload.atlas, render_pass) + .render(atlas, render_pass) .expect("Render cached text"); } } @@ -345,7 +383,7 @@ fn prepare( enum Allocation { Paragraph(Paragraph), Editor(Editor), - Cache(cache::KeyHash), + Cache(text_cache::KeyHash), Raw(Arc), } @@ -369,7 +407,7 @@ fn prepare( } => { let (key, _) = buffer_cache.allocate( font_system, - cache::Key { + text_cache::Key { content, size: f32::from(*size), line_height: f32::from(*line_height), diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 42f92de0..be09f163 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -6,6 +6,7 @@ mod program; pub use event::Event; pub use program::Program; +pub use crate::graphics::cache::Group; pub use crate::graphics::geometry::{ fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, -- cgit From 3c7b43d031a06d59afbba83bc9d088517c096f20 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 30 Apr 2024 08:06:51 +0200 Subject: Use shared `Cache` group in `the_matrix` example --- examples/the_matrix/src/main.rs | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 55c9da4b..97ad31b9 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -1,12 +1,13 @@ use iced::mouse; use iced::time::{self, Instant}; use iced::widget::canvas; -use iced::widget::canvas::{Cache, Geometry}; use iced::{ Color, Element, Font, Length, Point, Rectangle, Renderer, Subscription, Theme, }; +use std::cell::RefCell; + pub fn main() -> iced::Result { iced::program("The Matrix - Iced", TheMatrix::update, TheMatrix::view) .subscription(TheMatrix::subscription) @@ -15,8 +16,7 @@ pub fn main() -> iced::Result { } struct TheMatrix { - ticks: usize, - backgrounds: Vec, + tick: usize, } #[derive(Debug, Clone, Copy)] @@ -28,7 +28,7 @@ impl TheMatrix { fn update(&mut self, message: Message) { match message { Message::Tick(_now) => { - self.ticks += 1; + self.tick += 1; } } } @@ -47,33 +47,35 @@ impl TheMatrix { impl Default for TheMatrix { fn default() -> Self { - let mut backgrounds = Vec::with_capacity(30); - backgrounds.resize_with(30, Cache::default); - - Self { - ticks: 0, - backgrounds, - } + Self { tick: 0 } } } impl canvas::Program for TheMatrix { - type State = (); + type State = RefCell>; fn draw( &self, - _state: &Self::State, + state: &Self::State, renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, - ) -> Vec { + ) -> Vec { use rand::distributions::Distribution; use rand::Rng; const CELL_SIZE: f32 = 10.0; - vec![self.backgrounds[self.ticks % self.backgrounds.len()].draw( + let mut caches = state.borrow_mut(); + + if caches.is_empty() { + let group = canvas::Group::unique(); + + caches.resize_with(30, || canvas::Cache::with_group(group)); + } + + vec![caches[self.tick % caches.len()].draw( renderer, bounds.size(), |frame| { -- cgit From cfe4ddb86620f9b21027c8a828e80df9a8887407 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 30 Apr 2024 08:10:22 +0200 Subject: Fix `clippy` lint in `the_matrix` example --- examples/the_matrix/src/main.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 97ad31b9..4ed1db32 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -15,6 +15,7 @@ pub fn main() -> iced::Result { .run() } +#[derive(Default)] struct TheMatrix { tick: usize, } @@ -45,12 +46,6 @@ impl TheMatrix { } } -impl Default for TheMatrix { - fn default() -> Self { - Self { tick: 0 } - } -} - impl canvas::Program for TheMatrix { type State = RefCell>; -- cgit From c51b85e7ab067f5e7411eccd10a5ae192e6ee0a8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 30 Apr 2024 21:59:46 +0200 Subject: Invalidate text uploads after atlas trimming --- examples/the_matrix/Cargo.toml | 1 + examples/the_matrix/src/main.rs | 2 ++ wgpu/src/text.rs | 36 +++++++++++++++++++++++++----------- wgpu/src/triangle.rs | 2 +- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/examples/the_matrix/Cargo.toml b/examples/the_matrix/Cargo.toml index 17cf443b..775e76e0 100644 --- a/examples/the_matrix/Cargo.toml +++ b/examples/the_matrix/Cargo.toml @@ -10,3 +10,4 @@ iced.workspace = true iced.features = ["canvas", "tokio", "debug"] rand = "0.8" +tracing-subscriber = "0.3" diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 4ed1db32..f3a67ac8 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -9,6 +9,8 @@ use iced::{ use std::cell::RefCell; pub fn main() -> iced::Result { + tracing_subscriber::fmt::init(); + iced::program("The Matrix - Iced", TheMatrix::update, TheMatrix::view) .subscription(TheMatrix::subscription) .antialiasing(true) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 5806863c..8ec1d56a 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -71,6 +71,7 @@ struct Upload { buffer_cache: BufferCache, transformation: Transformation, version: usize, + group_version: usize, text: rc::Weak<[Text]>, _atlas: rc::Weak<()>, } @@ -83,8 +84,9 @@ pub struct Storage { struct Group { atlas: glyphon::TextAtlas, - previous_uploads: usize, - handle: Rc<()>, + version: usize, + should_trim: bool, + handle: Rc<()>, // Keeps track of active uploads } impl Storage { @@ -117,13 +119,18 @@ impl Storage { let group_count = self.groups.len(); let group = self.groups.entry(cache.group).or_insert_with(|| { - log::info!("New text atlas created (total: {})", group_count + 1); + log::debug!( + "New text atlas: {:?} (total: {})", + cache.group, + group_count + 1 + ); Group { atlas: glyphon::TextAtlas::with_color_mode( device, queue, format, COLOR_MODE, ), - previous_uploads: 0, + version: 0, + should_trim: false, handle: Rc::new(()), } }); @@ -134,6 +141,7 @@ impl Storage { if !cache.text.is_empty() && (upload.version != cache.version + || upload.group_version != group.version || upload.transformation != new_transformation) { let _ = prepare( @@ -151,9 +159,11 @@ impl Storage { upload.text = Rc::downgrade(&cache.text); upload.version = cache.version; + upload.group_version = group.version; upload.transformation = new_transformation; upload.buffer_cache.trim(); + group.should_trim = true; } } hash_map::Entry::Vacant(entry) => { @@ -184,11 +194,12 @@ impl Storage { buffer_cache, transformation: new_transformation, version: 0, + group_version: group.version, text: Rc::downgrade(&cache.text), _atlas: Rc::downgrade(&group.handle), }); - log::info!( + log::debug!( "New text upload: {} (total: {})", cache.id.0, self.uploads.len() @@ -201,18 +212,21 @@ impl Storage { self.uploads .retain(|_id, upload| upload.text.strong_count() > 0); - self.groups.retain(|_id, group| { - let uploads_alive = Rc::weak_count(&group.handle); + self.groups.retain(|id, group| { + if Rc::weak_count(&group.handle) == 0 { + log::debug!("Dropping text atlas: {id:?}"); - if uploads_alive == 0 { return false; } - if uploads_alive < group.previous_uploads { + if group.should_trim { + log::debug!("Trimming text atlas: {id:?}"); + group.atlas.trim(); - } - group.previous_uploads = uploads_alive; + group.version += 1; + group.should_trim = false; + } true }); diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index ca36de82..b0551f55 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -138,7 +138,7 @@ impl Storage { batch: Rc::downgrade(&cache.batch), }); - log::info!( + log::debug!( "New mesh upload: {} (total: {})", cache.id.0, self.uploads.len() -- cgit From b276a603a17eda219b32f207aa53e2b6a1321a9f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 30 Apr 2024 23:15:04 +0200 Subject: Fix cache trimming loop in `iced_wgpu::text` --- graphics/src/cache.rs | 37 ++++++++++++++++++++++++++++++++++--- wgpu/src/text.rs | 24 ++++++++++++++++++------ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/graphics/src/cache.rs b/graphics/src/cache.rs index 7106c392..bbba79eb 100644 --- a/graphics/src/cache.rs +++ b/graphics/src/cache.rs @@ -15,7 +15,7 @@ impl Cache { /// Creates a new empty [`Cache`]. pub fn new() -> Self { Cache { - group: Group::unique(), + group: Group::singleton(), state: RefCell::new(State::Empty { previous: None }), } } @@ -27,6 +27,11 @@ impl Cache { /// You should generally group caches that are likely to change /// together. pub fn with_group(group: Group) -> Self { + assert!( + !group.is_singleton(), + "The group {group:?} cannot be shared!" + ); + Cache { group, state: RefCell::new(State::Empty { previous: None }), @@ -75,14 +80,40 @@ impl Cache { /// A cache group can be used to implement certain performance /// optimizations during rendering, like batching or sharing atlases. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Group(u64); +pub struct Group { + id: u64, + is_singleton: bool, +} impl Group { /// Generates a new unique cache [`Group`]. pub fn unique() -> Self { static NEXT: AtomicU64 = AtomicU64::new(0); - Self(NEXT.fetch_add(1, atomic::Ordering::Relaxed)) + Self { + id: NEXT.fetch_add(1, atomic::Ordering::Relaxed), + is_singleton: false, + } + } + + /// Returns `true` if the [`Group`] can only ever have a + /// single [`Cache`] in it. + /// + /// This is the default kind of [`Group`] assigned when using + /// [`Cache::new`]. + /// + /// Knowing that a [`Group`] will never be shared may be + /// useful for rendering backends to perform additional + /// optimizations. + pub fn is_singleton(self) -> bool { + self.is_singleton + } + + fn singleton() -> Self { + Self { + is_singleton: true, + ..Self::unique() + } } } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 8ec1d56a..2508906f 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -157,13 +157,16 @@ impl Storage { target_size, ); + // Only trim if glyphs have changed + group.should_trim = + group.should_trim || upload.version != cache.version; + upload.text = Rc::downgrade(&cache.text); upload.version = cache.version; upload.group_version = group.version; upload.transformation = new_transformation; upload.buffer_cache.trim(); - group.should_trim = true; } } hash_map::Entry::Vacant(entry) => { @@ -213,19 +216,28 @@ impl Storage { .retain(|_id, upload| upload.text.strong_count() > 0); self.groups.retain(|id, group| { - if Rc::weak_count(&group.handle) == 0 { + let active_uploads = Rc::weak_count(&group.handle); + + if active_uploads == 0 { log::debug!("Dropping text atlas: {id:?}"); return false; } - if group.should_trim { - log::debug!("Trimming text atlas: {id:?}"); + if id.is_singleton() || group.should_trim { + log::debug!( + "Trimming text atlas: {id:?} (uploads: {active_uploads})" + ); group.atlas.trim(); - - group.version += 1; group.should_trim = false; + + // We only need to worry about glyph fighting + // when the atlas may be shared by multiple + // uploads. + if !id.is_singleton() { + group.version += 1; + } } true -- cgit From 7e2d0dc931a4409e661851a1c7dffdcfd5b2f93a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 30 Apr 2024 23:51:00 +0200 Subject: Keep text atlases alive during temporary empty uploads --- wgpu/src/text.rs | 76 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 2508906f..7e683c77 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -61,6 +61,10 @@ impl Cache { } pub fn update(&mut self, text: Vec) { + if self.text.is_empty() && text.is_empty() { + return; + } + self.text = Rc::from(text); self.version += 1; } @@ -139,23 +143,24 @@ impl Storage { hash_map::Entry::Occupied(entry) => { let upload = entry.into_mut(); - if !cache.text.is_empty() - && (upload.version != cache.version - || upload.group_version != group.version - || upload.transformation != new_transformation) + if upload.version != cache.version + || upload.group_version != group.version + || upload.transformation != new_transformation { - let _ = prepare( - device, - queue, - encoder, - &mut upload.renderer, - &mut group.atlas, - &mut upload.buffer_cache, - &cache.text, - bounds, - new_transformation, - target_size, - ); + if !cache.text.is_empty() { + let _ = prepare( + device, + queue, + encoder, + &mut upload.renderer, + &mut group.atlas, + &mut upload.buffer_cache, + &cache.text, + bounds, + new_transformation, + target_size, + ); + } // Only trim if glyphs have changed group.should_trim = @@ -179,18 +184,20 @@ impl Storage { let mut buffer_cache = BufferCache::new(); - let _ = prepare( - device, - queue, - encoder, - &mut renderer, - &mut group.atlas, - &mut buffer_cache, - &cache.text, - bounds, - new_transformation, - target_size, - ); + if !cache.text.is_empty() { + let _ = prepare( + device, + queue, + encoder, + &mut renderer, + &mut group.atlas, + &mut buffer_cache, + &cache.text, + bounds, + new_transformation, + target_size, + ); + } let _ = entry.insert(Upload { renderer, @@ -202,6 +209,8 @@ impl Storage { _atlas: Rc::downgrade(&group.handle), }); + group.should_trim = cache.group.is_singleton(); + log::debug!( "New text upload: {} (total: {})", cache.id.0, @@ -224,10 +233,8 @@ impl Storage { return false; } - if id.is_singleton() || group.should_trim { - log::debug!( - "Trimming text atlas: {id:?} (uploads: {active_uploads})" - ); + if group.should_trim { + log::trace!("Trimming text atlas: {id:?}"); group.atlas.trim(); group.should_trim = false; @@ -236,6 +243,11 @@ impl Storage { // when the atlas may be shared by multiple // uploads. if !id.is_singleton() { + log::debug!( + "Invalidating text atlas: {id:?} \ + (uploads: {active_uploads})" + ); + group.version += 1; } } -- cgit From 62433a65e92c025cd9c36e81fc16bab77790bacb Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 30 Apr 2024 23:51:41 +0200 Subject: Enable logging in `clock` example --- examples/clock/Cargo.toml | 1 + examples/clock/src/main.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index 2d3d5908..dc2e5382 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -10,3 +10,4 @@ iced.workspace = true iced.features = ["canvas", "tokio", "debug"] time = { version = "0.3", features = ["local-offset"] } +tracing-subscriber = "0.3" diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 897f8f1b..d717db36 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -8,6 +8,8 @@ use iced::{ }; pub fn main() -> iced::Result { + tracing_subscriber::fmt::init(); + iced::program("Clock - Iced", Clock::update, Clock::view) .subscription(Clock::subscription) .theme(Clock::theme) -- cgit