diff options
author | 2024-04-03 21:07:54 +0200 | |
---|---|---|
committer | 2024-04-03 21:07:54 +0200 | |
commit | b05e61f5c8ae61c9f3c7cc08cded53901ebbccfd (patch) | |
tree | 3d35a011d94d4936f09b5a9be4031358a09c60da /wgpu/src/text.rs | |
parent | 99a904112ca111f2ab0e60e30b6c369741b1653b (diff) | |
download | iced-b05e61f5c8ae61c9f3c7cc08cded53901ebbccfd.tar.gz iced-b05e61f5c8ae61c9f3c7cc08cded53901ebbccfd.tar.bz2 iced-b05e61f5c8ae61c9f3c7cc08cded53901ebbccfd.zip |
Redesign `iced_wgpu` layering architecture
Diffstat (limited to 'wgpu/src/text.rs')
-rw-r--r-- | wgpu/src/text.rs | 628 |
1 files changed, 409 insertions, 219 deletions
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 97ff77f5..016ac92a 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,20 +1,67 @@ use crate::core::alignment; use crate::core::{Rectangle, Size, Transformation}; use crate::graphics::color; -use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::cache::{self, Cache as BufferCache}; use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; -use crate::layer::Text; -use std::borrow::Cow; -use std::cell::RefCell; use std::sync::Arc; +pub use crate::graphics::Text; + +pub type Batch = Vec<Text>; + #[allow(missing_debug_implementations)] pub struct Pipeline { - renderers: Vec<glyphon::TextRenderer>, + format: wgpu::TextureFormat, atlas: glyphon::TextAtlas, + renderers: Vec<glyphon::TextRenderer>, prepare_layer: usize, - cache: RefCell<Cache>, + cache: BufferCache, +} + +pub enum Cache { + Staged(Batch), + Uploaded { + batch: Batch, + renderer: glyphon::TextRenderer, + atlas: Option<glyphon::TextAtlas>, + buffer_cache: Option<BufferCache>, + scale_factor: f32, + target_size: Size<u32>, + needs_reupload: bool, + }, +} + +impl Cache { + pub fn is_empty(&self) -> bool { + match self { + Cache::Staged(batch) | Cache::Uploaded { batch, .. } => { + batch.is_empty() + } + } + } + + 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; + } + } + } +} + +impl Default for Cache { + fn default() -> Self { + Self::Staged(Batch::default()) + } } impl Pipeline { @@ -24,6 +71,7 @@ impl Pipeline { format: wgpu::TextureFormat, ) -> Self { Pipeline { + format, renderers: Vec::new(), atlas: glyphon::TextAtlas::with_color_mode( device, @@ -36,25 +84,16 @@ impl Pipeline { }, ), prepare_layer: 0, - cache: RefCell::new(Cache::new()), + cache: BufferCache::new(), } } - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - font_system() - .write() - .expect("Write font system") - .load_font(bytes); - - self.cache = RefCell::new(Cache::new()); - } - - pub fn prepare( + pub fn prepare_batch( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, - sections: &[Text<'_>], + sections: &Batch, layer_bounds: Rectangle, scale_factor: f32, target_size: Size<u32>, @@ -68,210 +107,18 @@ impl Pipeline { )); } - let mut font_system = font_system().write().expect("Write font system"); - let font_system = font_system.raw(); - let renderer = &mut self.renderers[self.prepare_layer]; - let cache = self.cache.get_mut(); - - enum Allocation { - Paragraph(Paragraph), - Editor(Editor), - Cache(cache::KeyHash), - Raw(Arc<glyphon::Buffer>), - } - - let allocations: Vec<_> = sections - .iter() - .map(|section| match section { - Text::Paragraph { paragraph, .. } => { - paragraph.upgrade().map(Allocation::Paragraph) - } - Text::Editor { editor, .. } => { - editor.upgrade().map(Allocation::Editor) - } - Text::Cached(text) => { - let (key, _) = cache.allocate( - font_system, - cache::Key { - content: text.content, - size: text.size.into(), - line_height: f32::from( - text.line_height.to_absolute(text.size), - ), - font: text.font, - bounds: Size { - width: text.bounds.width, - height: text.bounds.height, - }, - shaping: text.shaping, - }, - ); - - Some(Allocation::Cache(key)) - } - Text::Raw { raw, .. } => { - raw.buffer.upgrade().map(Allocation::Raw) - } - }) - .collect(); - - let layer_bounds = layer_bounds * scale_factor; - - let text_areas = sections.iter().zip(allocations.iter()).filter_map( - |(section, allocation)| { - let ( - buffer, - bounds, - horizontal_alignment, - vertical_alignment, - color, - clip_bounds, - transformation, - ) = match section { - Text::Paragraph { - position, - color, - clip_bounds, - transformation, - .. - } => { - use crate::core::text::Paragraph as _; - - let Some(Allocation::Paragraph(paragraph)) = allocation - else { - return None; - }; - - ( - paragraph.buffer(), - Rectangle::new(*position, paragraph.min_bounds()), - paragraph.horizontal_alignment(), - paragraph.vertical_alignment(), - *color, - *clip_bounds, - *transformation, - ) - } - Text::Editor { - position, - color, - clip_bounds, - transformation, - .. - } => { - use crate::core::text::Editor as _; - - let Some(Allocation::Editor(editor)) = allocation - else { - return None; - }; - - ( - editor.buffer(), - Rectangle::new(*position, editor.bounds()), - alignment::Horizontal::Left, - alignment::Vertical::Top, - *color, - *clip_bounds, - *transformation, - ) - } - Text::Cached(text) => { - let Some(Allocation::Cache(key)) = allocation else { - return None; - }; - - let entry = cache.get(key).expect("Get cached buffer"); - - ( - &entry.buffer, - Rectangle::new( - text.bounds.position(), - entry.min_bounds, - ), - text.horizontal_alignment, - text.vertical_alignment, - text.color, - text.clip_bounds, - Transformation::IDENTITY, - ) - } - Text::Raw { - raw, - transformation, - } => { - let Some(Allocation::Raw(buffer)) = allocation else { - return None; - }; - - let (width, height) = buffer.size(); - - ( - buffer.as_ref(), - Rectangle::new( - raw.position, - Size::new(width, height), - ), - alignment::Horizontal::Left, - alignment::Vertical::Top, - raw.color, - raw.clip_bounds, - *transformation, - ) - } - }; - - let bounds = bounds * transformation * scale_factor; - - let left = match horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => { - bounds.x - bounds.width / 2.0 - } - alignment::Horizontal::Right => bounds.x - bounds.width, - }; - - let top = match vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => { - bounds.y - bounds.height / 2.0 - } - alignment::Vertical::Bottom => bounds.y - bounds.height, - }; - - let clip_bounds = layer_bounds.intersection( - &(clip_bounds * transformation * scale_factor), - )?; - - Some(glyphon::TextArea { - buffer, - left, - top, - scale: scale_factor * transformation.scale_factor(), - bounds: glyphon::TextBounds { - left: clip_bounds.x as i32, - top: clip_bounds.y as i32, - right: (clip_bounds.x + clip_bounds.width) as i32, - bottom: (clip_bounds.y + clip_bounds.height) as i32, - }, - default_color: to_color(color), - }) - }, - ); - - let result = renderer.prepare( + let result = prepare( device, queue, encoder, - font_system, + renderer, &mut self.atlas, - glyphon::Resolution { - width: target_size.width, - height: target_size.height, - }, - text_areas, - &mut glyphon::SwashCache::new(), + &mut self.cache, + sections, + layer_bounds, + scale_factor, + target_size, ); match result { @@ -286,7 +133,109 @@ impl Pipeline { } } - pub fn render<'a>( + pub fn prepare_cache( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + cache: &mut Cache, + layer_bounds: Rectangle, + new_scale_factor: f32, + 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) + }; + + let mut renderer = glyphon::TextRenderer::new( + atlas.as_mut().unwrap_or(&mut self.atlas), + device, + wgpu::MultisampleState::default(), + None, + ); + + 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, + new_scale_factor, + new_target_size, + ); + + *cache = Cache::Uploaded { + batch, + needs_reupload: false, + renderer, + atlas, + buffer_cache, + scale_factor: new_scale_factor, + target_size: new_target_size, + } + } + Cache::Uploaded { + batch, + needs_reupload, + renderer, + atlas, + buffer_cache, + scale_factor, + target_size, + } => { + if *needs_reupload + || atlas.is_none() + || buffer_cache.is_none() + || new_scale_factor != *scale_factor + || new_target_size != *target_size + { + let _ = prepare( + device, + queue, + encoder, + renderer, + atlas.as_mut().unwrap_or(&mut self.atlas), + buffer_cache.as_mut().unwrap_or(&mut self.cache), + batch, + layer_bounds, + *scale_factor, + *target_size, + ); + + *scale_factor = new_scale_factor; + *target_size = new_target_size; + } + } + } + } + + pub fn render_batch<'a>( &'a self, layer: usize, bounds: Rectangle<u32>, @@ -306,10 +255,251 @@ impl Pipeline { .expect("Render text"); } + 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; + }; + + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + renderer + .render(atlas.as_ref().unwrap_or(&self.atlas), render_pass) + .expect("Render text"); + } + pub fn end_frame(&mut self) { self.atlas.trim(); - self.cache.get_mut().trim(); + self.cache.trim(); self.prepare_layer = 0; } } + +fn prepare( + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut glyphon::TextRenderer, + atlas: &mut glyphon::TextAtlas, + buffer_cache: &mut BufferCache, + sections: &Batch, + layer_bounds: Rectangle, + scale_factor: f32, + target_size: Size<u32>, +) -> Result<(), glyphon::PrepareError> { + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + + enum Allocation { + Paragraph(Paragraph), + Editor(Editor), + Cache(cache::KeyHash), + Raw(Arc<glyphon::Buffer>), + } + + let allocations: Vec<_> = sections + .iter() + .map(|section| match section { + Text::Paragraph { paragraph, .. } => { + paragraph.upgrade().map(Allocation::Paragraph) + } + Text::Editor { editor, .. } => { + editor.upgrade().map(Allocation::Editor) + } + Text::Cached { + content, + bounds, + size, + line_height, + font, + shaping, + .. + } => { + let (key, _) = buffer_cache.allocate( + font_system, + cache::Key { + content, + size: (*size).into(), + line_height: f32::from(line_height.to_absolute(*size)), + font: *font, + bounds: Size { + width: bounds.width, + height: bounds.height, + }, + shaping: *shaping, + }, + ); + + Some(Allocation::Cache(key)) + } + Text::Raw { raw, .. } => raw.buffer.upgrade().map(Allocation::Raw), + }) + .collect(); + + let layer_bounds = layer_bounds * scale_factor; + + let text_areas = sections.iter().zip(allocations.iter()).filter_map( + |(section, allocation)| { + let ( + buffer, + bounds, + horizontal_alignment, + vertical_alignment, + color, + clip_bounds, + transformation, + ) = match section { + Text::Paragraph { + position, + color, + clip_bounds, + transformation, + .. + } => { + use crate::core::text::Paragraph as _; + + let Some(Allocation::Paragraph(paragraph)) = allocation + else { + return None; + }; + + ( + paragraph.buffer(), + Rectangle::new(*position, paragraph.min_bounds()), + paragraph.horizontal_alignment(), + paragraph.vertical_alignment(), + *color, + *clip_bounds, + *transformation, + ) + } + Text::Editor { + position, + color, + clip_bounds, + transformation, + .. + } => { + use crate::core::text::Editor as _; + + let Some(Allocation::Editor(editor)) = allocation else { + return None; + }; + + ( + editor.buffer(), + Rectangle::new(*position, editor.bounds()), + alignment::Horizontal::Left, + alignment::Vertical::Top, + *color, + *clip_bounds, + *transformation, + ) + } + Text::Cached { + bounds, + horizontal_alignment, + vertical_alignment, + color, + clip_bounds, + .. + } => { + let Some(Allocation::Cache(key)) = allocation else { + return None; + }; + + let entry = + buffer_cache.get(key).expect("Get cached buffer"); + + ( + &entry.buffer, + Rectangle::new(bounds.position(), entry.min_bounds), + *horizontal_alignment, + *vertical_alignment, + *color, + *clip_bounds, + Transformation::IDENTITY, + ) + } + Text::Raw { + raw, + transformation, + } => { + let Some(Allocation::Raw(buffer)) = allocation else { + return None; + }; + + let (width, height) = buffer.size(); + + ( + buffer.as_ref(), + Rectangle::new(raw.position, Size::new(width, height)), + alignment::Horizontal::Left, + alignment::Vertical::Top, + raw.color, + raw.clip_bounds, + *transformation, + ) + } + }; + + let bounds = bounds * transformation * scale_factor; + + let left = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.x - bounds.width / 2.0, + alignment::Horizontal::Right => bounds.x - bounds.width, + }; + + let top = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.y - bounds.height / 2.0, + alignment::Vertical::Bottom => bounds.y - bounds.height, + }; + + let clip_bounds = layer_bounds + .intersection(&(clip_bounds * transformation * scale_factor))?; + + Some(glyphon::TextArea { + buffer, + left, + top, + scale: scale_factor * transformation.scale_factor(), + bounds: glyphon::TextBounds { + left: clip_bounds.x as i32, + top: clip_bounds.y as i32, + right: (clip_bounds.x + clip_bounds.width) as i32, + bottom: (clip_bounds.y + clip_bounds.height) as i32, + }, + default_color: to_color(color), + }) + }, + ); + + renderer.prepare( + device, + queue, + encoder, + font_system, + atlas, + glyphon::Resolution { + width: target_size.width, + height: target_size.height, + }, + text_areas, + &mut glyphon::SwashCache::new(), + ) +} |