summaryrefslogtreecommitdiffstats
path: root/wgpu/src/text.rs
diff options
context:
space:
mode:
Diffstat (limited to 'wgpu/src/text.rs')
-rw-r--r--wgpu/src/text.rs733
1 files changed, 482 insertions, 251 deletions
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index 97ff77f5..1b21bb1c 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -1,20 +1,193 @@
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 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;
+
+const COLOR_MODE: glyphon::ColorMode = if color::GAMMA_CORRECTION {
+ glyphon::ColorMode::Accurate
+} else {
+ glyphon::ColorMode::Web
+};
+
+pub type Batch = Vec<Item>;
+
+#[derive(Debug)]
+pub enum Item {
+ Group {
+ transformation: Transformation,
+ 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 new(text: Vec<Text>) -> Option<Self> {
+ static NEXT_ID: AtomicU64 = AtomicU64::new(0);
+
+ if text.is_empty() {
+ return None;
+ }
+
+ Some(Self {
+ id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
+ text: Rc::from(text),
+ version: 0,
+ })
+ }
+
+ pub fn update(&mut self, text: Vec<Text>) {
+ self.text = Rc::from(text);
+ self.version += 1;
+ }
+}
+
+struct Upload {
+ renderer: glyphon::TextRenderer,
+ atlas: glyphon::TextAtlas,
+ buffer_cache: BufferCache,
+ transformation: Transformation,
+ version: usize,
+}
+
+#[derive(Default)]
+pub struct Storage {
+ uploads: FxHashMap<Id, Upload>,
+ recently_used: FxHashSet<Id>,
+}
+
+impl Storage {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ fn get(&self, cache: &Cache) -> Option<&Upload> {
+ if cache.text.is_empty() {
+ return None;
+ }
+
+ self.uploads.get(&cache.id)
+ }
+
+ fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ encoder: &mut wgpu::CommandEncoder,
+ format: wgpu::TextureFormat,
+ cache: &Cache,
+ new_transformation: Transformation,
+ bounds: Rectangle,
+ target_size: Size<u32>,
+ ) {
+ match self.uploads.entry(cache.id) {
+ hash_map::Entry::Occupied(entry) => {
+ let upload = entry.into_mut();
+
+ if !cache.text.is_empty()
+ && (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,
+ );
+
+ upload.version = cache.version;
+ 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,
+ device,
+ wgpu::MultisampleState::default(),
+ None,
+ );
+
+ let mut buffer_cache = BufferCache::new();
+
+ let _ = prepare(
+ device,
+ queue,
+ encoder,
+ &mut renderer,
+ &mut atlas,
+ &mut buffer_cache,
+ &cache.text,
+ bounds,
+ new_transformation,
+ target_size,
+ );
+
+ let _ = entry.insert(Upload {
+ renderer,
+ atlas,
+ buffer_cache,
+ transformation: new_transformation,
+ version: 0,
+ });
+
+ log::info!(
+ "New text upload: {} (total: {})",
+ cache.id.0,
+ self.uploads.len()
+ );
+ }
+ }
+
+ 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 {
- renderers: Vec<glyphon::TextRenderer>,
+ format: wgpu::TextureFormat,
atlas: glyphon::TextAtlas,
+ renderers: Vec<glyphon::TextRenderer>,
prepare_layer: usize,
- cache: RefCell<Cache>,
+ cache: BufferCache,
}
impl Pipeline {
@@ -24,275 +197,95 @@ impl Pipeline {
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
- },
+ device, queue, format, COLOR_MODE,
),
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(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
- sections: &[Text<'_>],
+ storage: &mut Storage,
+ batch: &Batch,
layer_bounds: Rectangle,
- scale_factor: f32,
+ layer_transformation: Transformation,
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,
- ));
- }
-
- 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,
+ for item in batch {
+ match item {
+ Item::Group {
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,
- )
+ text,
+ } => {
+ if self.renderers.len() <= self.prepare_layer {
+ self.renderers.push(glyphon::TextRenderer::new(
+ &mut self.atlas,
+ device,
+ wgpu::MultisampleState::default(),
+ None,
+ ));
}
- };
- 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 renderer = &mut self.renderers[self.prepare_layer];
+ let result = prepare(
+ device,
+ queue,
+ encoder,
+ renderer,
+ &mut self.atlas,
+ &mut self.cache,
+ text,
+ layer_bounds,
+ layer_transformation * *transformation,
+ target_size,
+ );
- let top = match vertical_alignment {
- alignment::Vertical::Top => bounds.y,
- alignment::Vertical::Center => {
- bounds.y - bounds.height / 2.0
+ 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...
+ }
}
- 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(
- device,
- queue,
- encoder,
- font_system,
- &mut self.atlas,
- glyphon::Resolution {
- width: target_size.width,
- height: target_size.height,
- },
- text_areas,
- &mut glyphon::SwashCache::new(),
- );
-
- 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<'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,
@@ -301,15 +294,253 @@ 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];
+
+ renderer
+ .render(&self.atlas, render_pass)
+ .expect("Render text");
+
+ layer_count += 1;
+ }
+ Item::Cached { cache, .. } => {
+ if let Some(upload) = storage.get(cache) {
+ upload
+ .renderer
+ .render(&upload.atlas, render_pass)
+ .expect("Render cached text");
+ }
+ }
+ }
+ }
+
+ layer_count
}
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: &[Text],
+ layer_bounds: Rectangle,
+ layer_transformation: Transformation,
+ 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: f32::from(*size),
+ line_height: f32::from(*line_height),
+ 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 * layer_transformation;
+
+ 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 * layer_transformation;
+
+ 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 * layer_transformation),
+ )?;
+
+ Some(glyphon::TextArea {
+ buffer,
+ left,
+ top,
+ scale: transformation.scale_factor()
+ * layer_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(),
+ )
+}