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.rs836
1 files changed, 583 insertions, 253 deletions
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index 6fa1922d..05db5f80 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -1,296 +1,389 @@
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};
+use crate::graphics::text::cache::{self as text_cache, 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;
+use std::collections::hash_map;
+use std::rc::{self, Rc};
+use std::sync::atomic::{self, AtomicU64};
use std::sync::Arc;
-#[allow(missing_debug_implementations)]
-pub struct Pipeline {
- renderers: Vec<glyphon::TextRenderer>,
- atlas: glyphon::TextAtlas,
- prepare_layer: usize,
- cache: RefCell<Cache>,
+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,
+ },
}
-impl Pipeline {
- pub fn new(
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- format: wgpu::TextureFormat,
- ) -> Self {
- Pipeline {
- 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: RefCell::new(Cache::new()),
+#[derive(Debug, Clone)]
+pub struct Cache {
+ id: Id,
+ group: cache::Group,
+ text: Rc<[Text]>,
+ version: usize,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id(u64);
+
+impl Cache {
+ pub fn new(group: cache::Group, 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)),
+ group,
+ text: Rc::from(text),
+ version: 0,
+ })
+ }
+
+ pub fn update(&mut self, text: Vec<Text>) {
+ if self.text.is_empty() && text.is_empty() {
+ return;
}
+
+ self.text = Rc::from(text);
+ self.version += 1;
+ }
+}
+
+struct Upload {
+ renderer: glyphon::TextRenderer,
+ buffer_cache: BufferCache,
+ transformation: Transformation,
+ version: usize,
+ group_version: usize,
+ text: rc::Weak<[Text]>,
+ _atlas: rc::Weak<()>,
+}
+
+#[derive(Default)]
+pub struct Storage {
+ groups: FxHashMap<cache::Group, Group>,
+ uploads: FxHashMap<Id, Upload>,
+}
+
+struct Group {
+ atlas: glyphon::TextAtlas,
+ version: usize,
+ should_trim: bool,
+ handle: Rc<()>, // Keeps track of active uploads
+}
+
+impl Storage {
+ pub fn new() -> Self {
+ Self::default()
}
- pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
- font_system()
- .write()
- .expect("Write font system")
- .load_font(bytes);
+ fn get(&self, cache: &Cache) -> Option<(&glyphon::TextAtlas, &Upload)> {
+ if cache.text.is_empty() {
+ return None;
+ }
- self.cache = RefCell::new(Cache::new());
+ self.groups
+ .get(&cache.group)
+ .map(|group| &group.atlas)
+ .zip(self.uploads.get(&cache.id))
}
- pub fn prepare(
+ fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
- sections: &[Text<'_>],
- layer_bounds: Rectangle,
- scale_factor: f32,
- target_size: Size<u32>,
+ viewport: &glyphon::Viewport,
+ encoder: &mut wgpu::CommandEncoder,
+ format: wgpu::TextureFormat,
+ state: &glyphon::Cache,
+ cache: &Cache,
+ new_transformation: Transformation,
+ bounds: Rectangle,
) {
- 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 group_count = self.groups.len();
+
+ let group = self.groups.entry(cache.group).or_insert_with(|| {
+ log::debug!(
+ "New text atlas: {:?} (total: {})",
+ cache.group,
+ group_count + 1
+ );
+
+ Group {
+ atlas: glyphon::TextAtlas::with_color_mode(
+ device, queue, state, format, COLOR_MODE,
+ ),
+ version: 0,
+ should_trim: false,
+ handle: Rc::new(()),
+ }
+ });
+
+ match self.uploads.entry(cache.id) {
+ hash_map::Entry::Occupied(entry) => {
+ let upload = entry.into_mut();
+
+ if upload.version != cache.version
+ || upload.group_version != group.version
+ || upload.transformation != new_transformation
+ {
+ if !cache.text.is_empty() {
+ let _ = prepare(
+ device,
+ queue,
+ viewport,
+ encoder,
+ &mut upload.renderer,
+ &mut group.atlas,
+ &mut upload.buffer_cache,
+ &cache.text,
+ bounds,
+ new_transformation,
+ );
+ }
- let renderer = &mut self.renderers[self.prepare_layer];
- let cache = self.cache.get_mut();
+ // Only trim if glyphs have changed
+ group.should_trim =
+ group.should_trim || upload.version != cache.version;
- enum Allocation {
- Paragraph(Paragraph),
- Editor(Editor),
- Cache(cache::KeyHash),
- Raw(Arc<glyphon::Buffer>),
- }
+ upload.text = Rc::downgrade(&cache.text);
+ upload.version = cache.version;
+ upload.group_version = group.version;
+ upload.transformation = new_transformation;
- let allocations: Vec<_> = sections
- .iter()
- .map(|section| match section {
- Text::Paragraph { paragraph, .. } => {
- paragraph.upgrade().map(Allocation::Paragraph)
+ upload.buffer_cache.trim();
}
- 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,
- },
+ }
+ hash_map::Entry::Vacant(entry) => {
+ let mut renderer = glyphon::TextRenderer::new(
+ &mut group.atlas,
+ device,
+ wgpu::MultisampleState::default(),
+ None,
+ );
+
+ let mut buffer_cache = BufferCache::new();
+
+ if !cache.text.is_empty() {
+ let _ = prepare(
+ device,
+ queue,
+ viewport,
+ encoder,
+ &mut renderer,
+ &mut group.atlas,
+ &mut buffer_cache,
+ &cache.text,
+ bounds,
+ new_transformation,
);
-
- Some(Allocation::Cache(key))
- }
- Text::Raw { raw, .. } => {
- raw.buffer.upgrade().map(Allocation::Raw)
}
- })
- .collect();
- let layer_bounds = layer_bounds * scale_factor;
+ let _ = entry.insert(Upload {
+ renderer,
+ buffer_cache,
+ transformation: new_transformation,
+ version: 0,
+ group_version: group.version,
+ text: Rc::downgrade(&cache.text),
+ _atlas: Rc::downgrade(&group.handle),
+ });
+
+ group.should_trim = cache.group.is_singleton();
+
+ log::debug!(
+ "New text upload: {} (total: {})",
+ cache.id.0,
+ self.uploads.len()
+ );
+ }
+ }
+ }
- 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,
- )
- }
- };
+ pub fn trim(&mut self) {
+ self.uploads
+ .retain(|_id, upload| upload.text.strong_count() > 0);
- let bounds = bounds * transformation * scale_factor;
+ self.groups.retain(|id, group| {
+ let active_uploads = Rc::weak_count(&group.handle);
- 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,
- };
+ if active_uploads == 0 {
+ log::debug!("Dropping text atlas: {id:?}");
- 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),
- })
- },
- );
+ return false;
+ }
+
+ if group.should_trim {
+ log::trace!("Trimming text atlas: {id:?}");
+
+ group.atlas.trim();
+ 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() {
+ log::debug!(
+ "Invalidating text atlas: {id:?} \
+ (uploads: {active_uploads})"
+ );
+
+ group.version += 1;
+ }
+ }
+
+ true
+ });
+ }
+}
+
+pub struct Viewport(glyphon::Viewport);
- let result = renderer.prepare(
- device,
+impl Viewport {
+ pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size<u32>) {
+ self.0.update(
queue,
- font_system,
- &mut self.atlas,
glyphon::Resolution {
- width: target_size.width,
- height: target_size.height,
+ width: resolution.width,
+ height: resolution.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...
+#[allow(missing_debug_implementations)]
+pub struct Pipeline {
+ state: glyphon::Cache,
+ 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 {
+ let state = glyphon::Cache::new(device);
+ let atlas = glyphon::TextAtlas::with_color_mode(
+ device, queue, &state, format, COLOR_MODE,
+ );
+
+ Pipeline {
+ state,
+ format,
+ renderers: Vec::new(),
+ atlas,
+ prepare_layer: 0,
+ cache: BufferCache::new(),
+ }
+ }
+
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ viewport: &Viewport,
+ encoder: &mut wgpu::CommandEncoder,
+ storage: &mut Storage,
+ batch: &Batch,
+ layer_bounds: Rectangle,
+ layer_transformation: Transformation,
+ ) {
+ 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,
+ &viewport.0,
+ encoder,
+ renderer,
+ &mut self.atlas,
+ &mut self.cache,
+ text,
+ layer_bounds * layer_transformation,
+ layer_transformation * *transformation,
+ );
+
+ 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,
+ &viewport.0,
+ encoder,
+ self.format,
+ &self.state,
+ cache,
+ layer_transformation * *transformation,
+ layer_bounds * layer_transformation,
+ );
+ }
}
}
}
pub fn render<'a>(
&'a self,
- layer: usize,
+ viewport: &'a Viewport,
+ 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,
@@ -299,15 +392,252 @@ 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, &viewport.0, render_pass)
+ .expect("Render text");
+
+ layer_count += 1;
+ }
+ Item::Cached { cache, .. } => {
+ if let Some((atlas, upload)) = storage.get(cache) {
+ upload
+ .renderer
+ .render(atlas, &viewport.0, render_pass)
+ .expect("Render cached text");
+ }
+ }
+ }
+ }
+
+ layer_count
+ }
+
+ pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport {
+ Viewport(glyphon::Viewport::new(device, &self.state))
}
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,
+ viewport: &glyphon::Viewport,
+ encoder: &mut wgpu::CommandEncoder,
+ renderer: &mut glyphon::TextRenderer,
+ atlas: &mut glyphon::TextAtlas,
+ buffer_cache: &mut BufferCache,
+ sections: &[Text],
+ layer_bounds: Rectangle,
+ layer_transformation: Transformation,
+) -> 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(text_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,
+ text_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 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,
+ viewport,
+ text_areas,
+ &mut glyphon::SwashCache::new(),
+ )
+}