summaryrefslogtreecommitdiffstats
path: root/wgpu/src/text.rs
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-04-05 23:59:21 +0200
committerLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-04-05 23:59:21 +0200
commit6d3e1d835e1688fbc58622a03a784ed25ed3f0e1 (patch)
treeb1a14b0ec7b2da4368d5c98850fe9e9eebc5490a /wgpu/src/text.rs
parent4a356cfc16f3b45d64826732009d9feeac016b28 (diff)
downloadiced-6d3e1d835e1688fbc58622a03a784ed25ed3f0e1.tar.gz
iced-6d3e1d835e1688fbc58622a03a784ed25ed3f0e1.tar.bz2
iced-6d3e1d835e1688fbc58622a03a784ed25ed3f0e1.zip
Decouple caching from layering and simplify everything
Diffstat (limited to 'wgpu/src/text.rs')
-rw-r--r--wgpu/src/text.rs427
1 files changed, 225 insertions, 202 deletions
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index a7695b74..e84e675d 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -4,245 +4,273 @@ use crate::graphics::color;
use crate::graphics::text::cache::{self, Cache as BufferCache};
use crate::graphics::text::{font_system, to_color, Editor, Paragraph};
+use rustc_hash::{FxHashMap, FxHashSet};
+use std::collections::hash_map;
+use std::rc::Rc;
+use std::sync::atomic::{self, AtomicU64};
use std::sync::Arc;
pub use crate::graphics::Text;
-pub type Batch = Vec<Text>;
+const COLOR_MODE: glyphon::ColorMode = if color::GAMMA_CORRECTION {
+ glyphon::ColorMode::Accurate
+} else {
+ glyphon::ColorMode::Web
+};
-#[allow(missing_debug_implementations)]
-pub struct Pipeline {
- format: wgpu::TextureFormat,
- atlas: glyphon::TextAtlas,
- renderers: Vec<glyphon::TextRenderer>,
- prepare_layer: usize,
- cache: BufferCache,
-}
+pub type Batch = Vec<Item>;
-pub enum Cache {
- Staged(Batch),
- Uploaded {
- batch: Batch,
- renderer: glyphon::TextRenderer,
- atlas: Option<glyphon::TextAtlas>,
- buffer_cache: Option<BufferCache>,
+#[derive(Debug)]
+pub enum Item {
+ Group {
transformation: Transformation,
- target_size: Size<u32>,
- needs_reupload: bool,
+ text: Vec<Text>,
},
+ Cached {
+ transformation: Transformation,
+ cache: Cache,
+ },
+}
+
+#[derive(Debug, Clone)]
+pub struct Cache {
+ id: Id,
+ text: Rc<[Text]>,
+ version: usize,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id(u64);
+
impl Cache {
- pub fn is_empty(&self) -> bool {
- match self {
- Cache::Staged(batch) | Cache::Uploaded { batch, .. } => {
- batch.is_empty()
- }
+ pub fn new(text: Vec<Text>) -> Self {
+ static NEXT_ID: AtomicU64 = AtomicU64::new(0);
+
+ Self {
+ id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
+ text: Rc::from(text),
+ version: 0,
}
}
- pub fn update(&mut self, new_batch: Batch) {
- match self {
- Self::Staged(batch) => {
- *batch = new_batch;
- }
- Self::Uploaded {
- batch,
- needs_reupload,
- ..
- } => {
- *batch = new_batch;
- *needs_reupload = true;
- }
- }
+ pub fn update(&mut self, text: Vec<Text>) {
+ self.text = Rc::from(text);
+ self.version += 1;
}
}
-impl Default for Cache {
- fn default() -> Self {
- Self::Staged(Batch::default())
- }
+struct Upload {
+ renderer: glyphon::TextRenderer,
+ atlas: glyphon::TextAtlas,
+ buffer_cache: BufferCache,
+ transformation: Transformation,
+ version: usize,
}
-impl Pipeline {
- pub fn new(
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- format: wgpu::TextureFormat,
- ) -> Self {
- Pipeline {
- format,
- renderers: Vec::new(),
- atlas: glyphon::TextAtlas::with_color_mode(
- device,
- queue,
- format,
- if color::GAMMA_CORRECTION {
- glyphon::ColorMode::Accurate
- } else {
- glyphon::ColorMode::Web
- },
- ),
- prepare_layer: 0,
- cache: BufferCache::new(),
- }
+#[derive(Default)]
+pub struct Storage {
+ uploads: FxHashMap<Id, Upload>,
+ recently_used: FxHashSet<Id>,
+}
+
+impl Storage {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ fn get(&self, id: Id) -> Option<&Upload> {
+ self.uploads.get(&id)
}
- pub fn prepare_batch(
+ fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
- sections: &Batch,
- layer_bounds: Rectangle,
- layer_transformation: Transformation,
+ format: wgpu::TextureFormat,
+ cache: &Cache,
+ new_transformation: Transformation,
+ bounds: Rectangle,
target_size: Size<u32>,
) {
- if self.renderers.len() <= self.prepare_layer {
- self.renderers.push(glyphon::TextRenderer::new(
- &mut self.atlas,
- device,
- wgpu::MultisampleState::default(),
- None,
- ));
- }
+ match self.uploads.entry(cache.id) {
+ hash_map::Entry::Occupied(entry) => {
+ let upload = entry.into_mut();
- let renderer = &mut self.renderers[self.prepare_layer];
- let result = prepare(
- device,
- queue,
- encoder,
- renderer,
- &mut self.atlas,
- &mut self.cache,
- sections,
- layer_bounds,
- layer_transformation,
- target_size,
- );
+ if upload.version != cache.version
+ || upload.transformation != new_transformation
+ {
+ let _ = prepare(
+ device,
+ queue,
+ encoder,
+ &mut upload.renderer,
+ &mut upload.atlas,
+ &mut upload.buffer_cache,
+ &cache.text,
+ bounds,
+ new_transformation,
+ target_size,
+ );
- match result {
- Ok(()) => {
- self.prepare_layer += 1;
- }
- Err(glyphon::PrepareError::AtlasFull) => {
- // If the atlas cannot grow, then all bets are off.
- // Instead of panicking, we will just pray that the result
- // will be somewhat readable...
- }
- }
- }
+ upload.version = cache.version;
+ upload.transformation = new_transformation;
- pub fn prepare_cache(
- &mut self,
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- encoder: &mut wgpu::CommandEncoder,
- cache: &mut Cache,
- layer_bounds: Rectangle,
- new_transformation: Transformation,
- new_target_size: Size<u32>,
- ) {
- match cache {
- Cache::Staged(_) => {
- let Cache::Staged(batch) =
- std::mem::replace(cache, Cache::Staged(Batch::default()))
- else {
- unreachable!()
- };
-
- // TODO: Find a better heuristic (?)
- let (mut atlas, mut buffer_cache) = if batch.len() > 10 {
- (
- Some(glyphon::TextAtlas::with_color_mode(
- device,
- queue,
- self.format,
- if color::GAMMA_CORRECTION {
- glyphon::ColorMode::Accurate
- } else {
- glyphon::ColorMode::Web
- },
- )),
- Some(BufferCache::new()),
- )
- } else {
- (None, None)
- };
+ 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(
- atlas.as_mut().unwrap_or(&mut self.atlas),
+ &mut atlas,
device,
wgpu::MultisampleState::default(),
None,
);
+ let mut buffer_cache = BufferCache::new();
+
let _ = prepare(
device,
queue,
encoder,
&mut renderer,
- atlas.as_mut().unwrap_or(&mut self.atlas),
- buffer_cache.as_mut().unwrap_or(&mut self.cache),
- &batch,
- layer_bounds,
+ &mut atlas,
+ &mut buffer_cache,
+ &cache.text,
+ bounds,
new_transformation,
- new_target_size,
+ target_size,
);
- *cache = Cache::Uploaded {
- batch,
- needs_reupload: false,
+ let _ = entry.insert(Upload {
renderer,
atlas,
buffer_cache,
transformation: new_transformation,
- target_size: new_target_size,
- }
+ version: 0,
+ });
}
- Cache::Uploaded {
- batch,
- needs_reupload,
- renderer,
- atlas,
- buffer_cache,
- transformation,
- target_size,
- } => {
- if *needs_reupload
- || atlas.is_none()
- || buffer_cache.is_none()
- || new_transformation != *transformation
- || new_target_size != *target_size
- {
- let _ = prepare(
+ }
+
+ let _ = self.recently_used.insert(cache.id);
+ }
+
+ pub fn trim(&mut self) {
+ self.uploads.retain(|id, _| self.recently_used.contains(id));
+ self.recently_used.clear();
+ }
+}
+
+#[allow(missing_debug_implementations)]
+pub struct Pipeline {
+ format: wgpu::TextureFormat,
+ atlas: glyphon::TextAtlas,
+ renderers: Vec<glyphon::TextRenderer>,
+ prepare_layer: usize,
+ cache: BufferCache,
+}
+
+impl Pipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ format: wgpu::TextureFormat,
+ ) -> Self {
+ Pipeline {
+ format,
+ renderers: Vec::new(),
+ atlas: glyphon::TextAtlas::with_color_mode(
+ device, queue, format, COLOR_MODE,
+ ),
+ prepare_layer: 0,
+ cache: BufferCache::new(),
+ }
+ }
+
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ encoder: &mut wgpu::CommandEncoder,
+ storage: &mut Storage,
+ batch: &Batch,
+ layer_bounds: Rectangle,
+ layer_transformation: Transformation,
+ target_size: Size<u32>,
+ ) {
+ for item in batch {
+ match item {
+ Item::Group {
+ transformation,
+ text,
+ } => {
+ if self.renderers.len() <= self.prepare_layer {
+ self.renderers.push(glyphon::TextRenderer::new(
+ &mut self.atlas,
+ device,
+ wgpu::MultisampleState::default(),
+ None,
+ ));
+ }
+
+ let renderer = &mut self.renderers[self.prepare_layer];
+ let result = prepare(
device,
queue,
encoder,
renderer,
- atlas.as_mut().unwrap_or(&mut self.atlas),
- buffer_cache.as_mut().unwrap_or(&mut self.cache),
- batch,
+ &mut self.atlas,
+ &mut self.cache,
+ text,
layer_bounds,
- new_transformation,
- new_target_size,
+ layer_transformation * *transformation,
+ target_size,
);
- *transformation = new_transformation;
- *target_size = new_target_size;
- *needs_reupload = false;
+ match result {
+ Ok(()) => {
+ self.prepare_layer += 1;
+ }
+ Err(glyphon::PrepareError::AtlasFull) => {
+ // If the atlas cannot grow, then all bets are off.
+ // Instead of panicking, we will just pray that the result
+ // will be somewhat readable...
+ }
+ }
+ }
+ Item::Cached {
+ transformation,
+ cache,
+ } => {
+ storage.prepare(
+ device,
+ queue,
+ encoder,
+ self.format,
+ cache,
+ layer_transformation * *transformation,
+ layer_bounds,
+ target_size,
+ );
}
}
}
}
- pub fn render_batch<'a>(
+ pub fn render<'a>(
&'a self,
- layer: usize,
+ storage: &'a Storage,
+ start: usize,
+ batch: &'a Batch,
bounds: Rectangle<u32>,
render_pass: &mut wgpu::RenderPass<'a>,
- ) {
- let renderer = &self.renderers[layer];
+ ) -> usize {
+ let mut layer_count = 0;
render_pass.set_scissor_rect(
bounds.x,
@@ -251,34 +279,29 @@ impl Pipeline {
bounds.height,
);
- renderer
- .render(&self.atlas, render_pass)
- .expect("Render text");
- }
+ for item in batch {
+ match item {
+ Item::Group { .. } => {
+ let renderer = &self.renderers[start + layer_count];
- pub fn render_cache<'a>(
- &'a self,
- cache: &'a Cache,
- bounds: Rectangle<u32>,
- render_pass: &mut wgpu::RenderPass<'a>,
- ) {
- let Cache::Uploaded {
- renderer, atlas, ..
- } = cache
- else {
- return;
- };
+ renderer
+ .render(&self.atlas, render_pass)
+ .expect("Render text");
- render_pass.set_scissor_rect(
- bounds.x,
- bounds.y,
- bounds.width,
- bounds.height,
- );
+ layer_count += 1;
+ }
+ Item::Cached { cache, .. } => {
+ if let Some(upload) = storage.get(cache.id) {
+ upload
+ .renderer
+ .render(&upload.atlas, render_pass)
+ .expect("Render cached text");
+ }
+ }
+ }
+ }
- renderer
- .render(atlas.as_ref().unwrap_or(&self.atlas), render_pass)
- .expect("Render text");
+ layer_count
}
pub fn end_frame(&mut self) {
@@ -296,7 +319,7 @@ fn prepare(
renderer: &mut glyphon::TextRenderer,
atlas: &mut glyphon::TextAtlas,
buffer_cache: &mut BufferCache,
- sections: &Batch,
+ sections: &[Text],
layer_bounds: Rectangle,
layer_transformation: Transformation,
target_size: Size<u32>,
@@ -333,8 +356,8 @@ fn prepare(
font_system,
cache::Key {
content,
- size: (*size).into(),
- line_height: f32::from(line_height.to_absolute(*size)),
+ size: f32::from(*size),
+ line_height: f32::from(*line_height),
font: *font,
bounds: Size {
width: bounds.width,