summaryrefslogtreecommitdiffstats
path: root/wgpu/src/text.rs
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-04-03 21:07:54 +0200
committerLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-04-03 21:07:54 +0200
commitb05e61f5c8ae61c9f3c7cc08cded53901ebbccfd (patch)
tree3d35a011d94d4936f09b5a9be4031358a09c60da /wgpu/src/text.rs
parent99a904112ca111f2ab0e60e30b6c369741b1653b (diff)
downloadiced-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.rs628
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(),
+ )
+}