From b9a9576207ddfc7afd89da30b7cfc7ca0d7e335c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 6 Jan 2023 23:29:38 +0100 Subject: Remove `iced_glow`, `glyph-brush`, and `wgpu_glyph` dependencies --- wgpu/src/text.rs | 264 ++++--------------------------------------------------- 1 file changed, 19 insertions(+), 245 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index e17b84c1..125e6be0 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,265 +1,39 @@ -use crate::Transformation; - -use iced_graphics::font; - -use std::{cell::RefCell, collections::HashMap}; -use wgpu_glyph::ab_glyph; - pub use iced_native::text::Hit; #[derive(Debug)] -pub struct Pipeline { - draw_brush: RefCell>, - draw_font_map: RefCell>, - measure_brush: RefCell>, -} +pub struct Pipeline; impl Pipeline { pub fn new( - device: &wgpu::Device, - format: wgpu::TextureFormat, - default_font: Option<&[u8]>, - multithreading: bool, + _device: &wgpu::Device, + _format: wgpu::TextureFormat, + _default_font: Option<&[u8]>, + _multithreading: bool, ) -> Self { - let default_font = default_font.map(|slice| slice.to_vec()); - - // TODO: Font customization - #[cfg(not(target_os = "ios"))] - #[cfg(feature = "default_system_font")] - let default_font = { - default_font.or_else(|| { - font::Source::new() - .load(&[font::Family::SansSerif, font::Family::Serif]) - .ok() - }) - }; - - let default_font = - default_font.unwrap_or_else(|| font::FALLBACK.to_vec()); - - let font = ab_glyph::FontArc::try_from_vec(default_font) - .unwrap_or_else(|_| { - log::warn!( - "System font failed to load. Falling back to \ - embedded font..." - ); - - ab_glyph::FontArc::try_from_slice(font::FALLBACK) - .expect("Load fallback font") - }); - - let draw_brush_builder = - wgpu_glyph::GlyphBrushBuilder::using_font(font.clone()) - .initial_cache_size((2048, 2048)) - .draw_cache_multithread(multithreading); - - #[cfg(target_arch = "wasm32")] - let draw_brush_builder = draw_brush_builder.draw_cache_align_4x4(true); - - let draw_brush = draw_brush_builder.build(device, format); - - let measure_brush = - glyph_brush::GlyphBrushBuilder::using_font(font).build(); - - Pipeline { - draw_brush: RefCell::new(draw_brush), - draw_font_map: RefCell::new(HashMap::new()), - measure_brush: RefCell::new(measure_brush), - } - } - - pub fn queue(&mut self, section: wgpu_glyph::Section<'_>) { - self.draw_brush.borrow_mut().queue(section); - } - - pub fn draw_queued( - &mut self, - device: &wgpu::Device, - staging_belt: &mut wgpu::util::StagingBelt, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - transformation: Transformation, - region: wgpu_glyph::Region, - ) { - self.draw_brush - .borrow_mut() - .draw_queued_with_transform_and_scissoring( - device, - staging_belt, - encoder, - target, - transformation.into(), - region, - ) - .expect("Draw text"); + Pipeline } pub fn measure( &self, - content: &str, - size: f32, - font: iced_native::Font, - bounds: iced_native::Size, + _content: &str, + _size: f32, + _font: iced_native::Font, + _bounds: iced_native::Size, ) -> (f32, f32) { - use wgpu_glyph::GlyphCruncher; - - let wgpu_glyph::FontId(font_id) = self.find_font(font); - - let section = wgpu_glyph::Section { - bounds: (bounds.width, bounds.height), - text: vec![wgpu_glyph::Text { - text: content, - scale: size.into(), - font_id: wgpu_glyph::FontId(font_id), - extra: wgpu_glyph::Extra::default(), - }], - ..Default::default() - }; - - if let Some(bounds) = - self.measure_brush.borrow_mut().glyph_bounds(section) - { - (bounds.width().ceil(), bounds.height().ceil()) - } else { - (0.0, 0.0) - } + (0.0, 0.0) } pub fn hit_test( &self, - content: &str, - size: f32, - font: iced_native::Font, - bounds: iced_native::Size, - point: iced_native::Point, - nearest_only: bool, + _content: &str, + _size: f32, + _font: iced_native::Font, + _bounds: iced_native::Size, + _point: iced_native::Point, + _nearest_only: bool, ) -> Option { - use wgpu_glyph::GlyphCruncher; - - let wgpu_glyph::FontId(font_id) = self.find_font(font); - - let section = wgpu_glyph::Section { - bounds: (bounds.width, bounds.height), - text: vec![wgpu_glyph::Text { - text: content, - scale: size.into(), - font_id: wgpu_glyph::FontId(font_id), - extra: wgpu_glyph::Extra::default(), - }], - ..Default::default() - }; - - let mut mb = self.measure_brush.borrow_mut(); - - // The underlying type is FontArc, so clones are cheap. - use wgpu_glyph::ab_glyph::{Font, ScaleFont}; - let font = mb.fonts()[font_id].clone().into_scaled(size); - - // Implements an iterator over the glyph bounding boxes. - let bounds = mb.glyphs(section).map( - |wgpu_glyph::SectionGlyph { - byte_index, glyph, .. - }| { - ( - *byte_index, - iced_native::Rectangle::new( - iced_native::Point::new( - glyph.position.x - font.h_side_bearing(glyph.id), - glyph.position.y - font.ascent(), - ), - iced_native::Size::new( - font.h_advance(glyph.id), - font.ascent() - font.descent(), - ), - ), - ) - }, - ); - - // Implements computation of the character index based on the byte index - // within the input string. - let char_index = |byte_index| { - let mut b_count = 0; - for (i, utf8_len) in - content.chars().map(|c| c.len_utf8()).enumerate() - { - if byte_index < (b_count + utf8_len) { - return i; - } - b_count += utf8_len; - } - - byte_index - }; - - if !nearest_only { - for (idx, bounds) in bounds.clone() { - if bounds.contains(point) { - return Some(Hit::CharOffset(char_index(idx))); - } - } - } - - let nearest = bounds - .map(|(index, bounds)| (index, bounds.center())) - .min_by(|(_, center_a), (_, center_b)| { - center_a - .distance(point) - .partial_cmp(¢er_b.distance(point)) - .unwrap_or(std::cmp::Ordering::Greater) - }); - - nearest.map(|(idx, center)| { - Hit::NearestCharOffset(char_index(idx), point - center) - }) + None } - pub fn trim_measurement_cache(&mut self) { - // TODO: We should probably use a `GlyphCalculator` for this. However, - // it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop. - // This makes stuff quite inconvenient. A manual method for trimming the - // cache would make our lives easier. - loop { - let action = self - .measure_brush - .borrow_mut() - .process_queued(|_, _| {}, |_| {}); - - match action { - Ok(_) => break, - Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => { - let (width, height) = suggested; - - self.measure_brush - .borrow_mut() - .resize_texture(width, height); - } - } - } - } - - pub fn find_font(&self, font: iced_native::Font) -> wgpu_glyph::FontId { - match font { - iced_native::Font::Default => wgpu_glyph::FontId(0), - iced_native::Font::External { name, bytes } => { - if let Some(font_id) = self.draw_font_map.borrow().get(name) { - return *font_id; - } - - let font = ab_glyph::FontArc::try_from_slice(bytes) - .expect("Load font"); - - let _ = self.measure_brush.borrow_mut().add_font(font.clone()); - - let font_id = self.draw_brush.borrow_mut().add_font(font); - - let _ = self - .draw_font_map - .borrow_mut() - .insert(String::from(name), font_id); - - font_id - } - } - } + pub fn trim_measurement_cache(&mut self) {} } -- cgit From baf51a8fcffc78e4ca20f7dcbba18ca3655f2840 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 31 Jan 2023 06:29:21 +0100 Subject: Draft `glyphon` implementation of text pipeline for `iced_wgpu` --- wgpu/src/text.rs | 160 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 150 insertions(+), 10 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 125e6be0..ccb627cd 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,26 +1,166 @@ pub use iced_native::text::Hit; -#[derive(Debug)] -pub struct Pipeline; +use iced_graphics::layer::Text; +use iced_native::{Font, Size}; + +#[allow(missing_debug_implementations)] +pub struct Pipeline { + renderer: glyphon::TextRenderer, + atlas: glyphon::TextAtlas, + cache: glyphon::SwashCache<'static>, +} + +// TODO: Share with `iced_graphics` +static FONT_SYSTEM: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(glyphon::FontSystem::new); impl Pipeline { pub fn new( - _device: &wgpu::Device, - _format: wgpu::TextureFormat, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, _default_font: Option<&[u8]>, _multithreading: bool, ) -> Self { - Pipeline + Pipeline { + renderer: glyphon::TextRenderer::new(device, queue), + atlas: glyphon::TextAtlas::new(device, queue, format), + cache: glyphon::SwashCache::new(&FONT_SYSTEM), + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + sections: &[Text<'_>], + scale_factor: f32, + target_size: Size, + ) { + let buffers: Vec<_> = sections + .iter() + .map(|section| { + let metrics = glyphon::Metrics::new( + (section.size * scale_factor) as i32, + (section.size * 1.2 * scale_factor) as i32, + ); + + let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics); + + buffer.set_size( + (section.bounds.width * scale_factor).ceil() as i32, + (section.bounds.height * scale_factor).ceil() as i32, + ); + + buffer.set_text( + section.content, + glyphon::Attrs::new() + .color({ + let [r, g, b, a] = section.color.into_rgba8(); + glyphon::Color::rgba(r, g, b, a) + }) + .family(match section.font { + Font::Default => glyphon::Family::SansSerif, + Font::External { name, .. } => { + glyphon::Family::Name(name) + } + }), + ); + + buffer.shape_until_scroll(); + + buffer + }) + .collect(); + + let text_areas: Vec<_> = sections + .iter() + .zip(buffers.iter()) + .map(|(section, buffer)| glyphon::TextArea { + buffer, + left: (section.bounds.x * scale_factor) as i32, + top: (section.bounds.y * scale_factor) as i32, + bounds: glyphon::TextBounds { + left: (section.bounds.x * scale_factor) as i32, + top: (section.bounds.y * scale_factor) as i32, + right: ((section.bounds.x + section.bounds.width) + * scale_factor) as i32, + bottom: ((section.bounds.y + section.bounds.height) + * scale_factor) as i32, + }, + }) + .collect(); + + self.renderer + .prepare( + device, + queue, + &mut self.atlas, + glyphon::Resolution { + width: target_size.width, + height: target_size.height, + }, + &text_areas, + glyphon::Color::rgb(0, 0, 0), + &mut self.cache, + ) + .expect("Prepare text sections"); + } + + pub fn render( + &mut self, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + ) { + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + })], + depth_stencil_attachment: None, + }); + + self.renderer + .render(&self.atlas, &mut render_pass) + .expect("Render text"); } pub fn measure( &self, - _content: &str, - _size: f32, - _font: iced_native::Font, - _bounds: iced_native::Size, + content: &str, + size: f32, + font: Font, + bounds: Size, ) -> (f32, f32) { - (0.0, 0.0) + let attrs = match font { + Font::Default => glyphon::Attrs::new(), + Font::External { name, .. } => glyphon::Attrs { + family: glyphon::Family::Name(name), + ..glyphon::Attrs::new() + }, + }; + + let mut paragraph = + glyphon::BufferLine::new(content, glyphon::AttrsList::new(attrs)); + + // TODO: Cache layout + let layout = paragraph.layout( + &FONT_SYSTEM, + size as i32, + bounds.width as i32, + glyphon::Wrap::Word, + ); + + ( + layout.iter().fold(0.0, |max, line| line.w.max(max)), + size * 1.2 * layout.len() as f32, + ) } pub fn hit_test( -- cgit From ba258f8fbcf72eaabefe193b6fbee6484b44e569 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Feb 2023 03:24:14 +0100 Subject: Implement support for multiple text layers in `iced_wgpu` --- wgpu/src/text.rs | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index ccb627cd..2b18299f 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,13 +1,14 @@ pub use iced_native::text::Hit; use iced_graphics::layer::Text; -use iced_native::{Font, Size}; +use iced_native::{Font, Rectangle, Size}; #[allow(missing_debug_implementations)] pub struct Pipeline { - renderer: glyphon::TextRenderer, + renderers: Vec, atlas: glyphon::TextAtlas, cache: glyphon::SwashCache<'static>, + layer: usize, } // TODO: Share with `iced_graphics` @@ -23,9 +24,10 @@ impl Pipeline { _multithreading: bool, ) -> Self { Pipeline { - renderer: glyphon::TextRenderer::new(device, queue), + renderers: Vec::new(), atlas: glyphon::TextAtlas::new(device, queue, format), cache: glyphon::SwashCache::new(&FONT_SYSTEM), + layer: 0, } } @@ -34,9 +36,17 @@ impl Pipeline { device: &wgpu::Device, queue: &wgpu::Queue, sections: &[Text<'_>], + bounds: Rectangle, scale_factor: f32, target_size: Size, ) { + if self.renderers.len() <= self.layer { + self.renderers + .push(glyphon::TextRenderer::new(device, queue)); + } + + let renderer = &mut self.renderers[self.layer]; + let buffers: Vec<_> = sections .iter() .map(|section| { @@ -73,6 +83,13 @@ impl Pipeline { }) .collect(); + let bounds = glyphon::TextBounds { + left: (bounds.x * scale_factor) as i32, + top: (bounds.y * scale_factor) as i32, + right: ((bounds.x + bounds.width) * scale_factor) as i32, + bottom: ((bounds.y + bounds.height) * scale_factor) as i32, + }; + let text_areas: Vec<_> = sections .iter() .zip(buffers.iter()) @@ -80,18 +97,11 @@ impl Pipeline { buffer, left: (section.bounds.x * scale_factor) as i32, top: (section.bounds.y * scale_factor) as i32, - bounds: glyphon::TextBounds { - left: (section.bounds.x * scale_factor) as i32, - top: (section.bounds.y * scale_factor) as i32, - right: ((section.bounds.x + section.bounds.width) - * scale_factor) as i32, - bottom: ((section.bounds.y + section.bounds.height) - * scale_factor) as i32, - }, + bounds, }) .collect(); - self.renderer + renderer .prepare( device, queue, @@ -126,9 +136,18 @@ impl Pipeline { depth_stencil_attachment: None, }); - self.renderer + let renderer = &mut self.renderers[self.layer]; + + renderer .render(&self.atlas, &mut render_pass) .expect("Render text"); + + self.layer += 1; + } + + pub fn end_frame(&mut self) { + self.renderers.truncate(self.layer); + self.layer = 0; } pub fn measure( -- cgit From 98a16fd670d501a0072fee13b023be686ff30cf8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Feb 2023 03:39:15 +0100 Subject: Implement proper text alignment support in `iced_wgpu` --- wgpu/src/text.rs | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 2b18299f..19d0782a 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,6 +1,7 @@ pub use iced_native::text::Hit; use iced_graphics::layer::Text; +use iced_native::alignment; use iced_native::{Font, Rectangle, Size}; #[allow(missing_debug_implementations)] @@ -93,11 +94,37 @@ impl Pipeline { let text_areas: Vec<_> = sections .iter() .zip(buffers.iter()) - .map(|(section, buffer)| glyphon::TextArea { - buffer, - left: (section.bounds.x * scale_factor) as i32, - top: (section.bounds.y * scale_factor) as i32, - bounds, + .map(|(section, buffer)| { + let x = section.bounds.x * scale_factor; + let y = section.bounds.y * scale_factor; + + let max_width = buffer + .layout_runs() + .fold(0.0f32, |max, run| max.max(run.line_w)); + + let total_height = buffer.visible_lines() as f32 + * section.size + * 1.2 + * scale_factor; + + let left = match section.horizontal_alignment { + alignment::Horizontal::Left => x, + alignment::Horizontal::Center => x - max_width / 2.0, + alignment::Horizontal::Right => x - max_width, + }; + + let top = match section.vertical_alignment { + alignment::Vertical::Top => y, + alignment::Vertical::Center => y - total_height / 2.0, + alignment::Vertical::Bottom => y - total_height, + }; + + glyphon::TextArea { + buffer, + left: left as i32, + top: top as i32, + bounds, + } }) .collect(); -- cgit From bb27982009d89a1cf4874697f7c17ff0af71d93d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Feb 2023 04:06:29 +0100 Subject: Convert sRGB to linear RGB for text in `iced_wgpu` --- wgpu/src/text.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 19d0782a..3f828da1 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -67,8 +67,14 @@ impl Pipeline { section.content, glyphon::Attrs::new() .color({ - let [r, g, b, a] = section.color.into_rgba8(); - glyphon::Color::rgba(r, g, b, a) + let [r, g, b, a] = section.color.into_linear(); + + glyphon::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) }) .family(match section.font { Font::Default => glyphon::Family::SansSerif, -- cgit From 1d0c44fb255f5fbf5a03e7c737e40ab66d39de9e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Feb 2023 01:24:27 +0100 Subject: Implement basic text caching in `iced_wgpu` --- wgpu/src/text.rs | 201 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 141 insertions(+), 60 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 3f828da1..a1d621d4 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,16 +2,107 @@ pub use iced_native::text::Hit; use iced_graphics::layer::Text; use iced_native::alignment; -use iced_native::{Font, Rectangle, Size}; +use iced_native::{Color, Font, Rectangle, Size}; + +use rustc_hash::{FxHashMap, FxHashSet}; +use std::cell::RefCell; +use std::hash::{BuildHasher, Hash, Hasher}; +use twox_hash::RandomXxHashBuilder64; #[allow(missing_debug_implementations)] pub struct Pipeline { renderers: Vec, atlas: glyphon::TextAtlas, cache: glyphon::SwashCache<'static>, + measurement_cache: RefCell, + render_cache: Cache, layer: usize, } +struct Cache { + entries: FxHashMap>, + recently_used: FxHashSet, + hasher: RandomXxHashBuilder64, +} + +impl Cache { + fn new() -> Self { + Self { + entries: FxHashMap::default(), + recently_used: FxHashSet::default(), + hasher: RandomXxHashBuilder64::default(), + } + } + + fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer<'static>> { + self.entries.get(key) + } + + fn allocate( + &mut self, + key: Key<'_>, + ) -> (KeyHash, &mut glyphon::Buffer<'static>) { + let hash = { + let mut hasher = self.hasher.build_hasher(); + + key.content.hash(&mut hasher); + (key.size as i32).hash(&mut hasher); + key.font.hash(&mut hasher); + (key.bounds.width as i32).hash(&mut hasher); + (key.bounds.height as i32).hash(&mut hasher); + key.color.into_rgba8().hash(&mut hasher); + + hasher.finish() + }; + + if !self.entries.contains_key(&hash) { + let metrics = + glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32); + + let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics); + + buffer.set_size(key.bounds.width as i32, key.bounds.height as i32); + buffer.set_text( + key.content, + glyphon::Attrs::new().family(to_family(key.font)).color({ + let [r, g, b, a] = key.color.into_linear(); + + glyphon::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) + }), + ); + + let _ = self.entries.insert(hash, buffer); + } + + let _ = self.recently_used.insert(hash); + + (hash, self.entries.get_mut(&hash).unwrap()) + } + + fn trim(&mut self) { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); + } +} + +#[derive(Debug, Clone, Copy)] +struct Key<'a> { + content: &'a str, + size: f32, + font: Font, + bounds: Size, + color: Color, +} + +type KeyHash = u64; + // TODO: Share with `iced_graphics` static FONT_SYSTEM: once_cell::sync::Lazy = once_cell::sync::Lazy::new(glyphon::FontSystem::new); @@ -28,6 +119,8 @@ impl Pipeline { renderers: Vec::new(), atlas: glyphon::TextAtlas::new(device, queue, format), cache: glyphon::SwashCache::new(&FONT_SYSTEM), + measurement_cache: RefCell::new(Cache::new()), + render_cache: Cache::new(), layer: 0, } } @@ -48,48 +141,29 @@ impl Pipeline { let renderer = &mut self.renderers[self.layer]; - let buffers: Vec<_> = sections + let keys: Vec<_> = sections .iter() .map(|section| { - let metrics = glyphon::Metrics::new( - (section.size * scale_factor) as i32, - (section.size * 1.2 * scale_factor) as i32, - ); - - let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics); - - buffer.set_size( - (section.bounds.width * scale_factor).ceil() as i32, - (section.bounds.height * scale_factor).ceil() as i32, - ); - - buffer.set_text( - section.content, - glyphon::Attrs::new() - .color({ - let [r, g, b, a] = section.color.into_linear(); - - glyphon::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) - }) - .family(match section.font { - Font::Default => glyphon::Family::SansSerif, - Font::External { name, .. } => { - glyphon::Family::Name(name) - } - }), - ); - - buffer.shape_until_scroll(); - - buffer + let (key, _) = self.render_cache.allocate(Key { + content: section.content, + size: section.size * scale_factor, + font: section.font, + bounds: Size { + width: section.bounds.width * scale_factor, + height: section.bounds.height * scale_factor, + }, + color: section.color, + }); + + key }) .collect(); + let buffers: Vec<_> = keys + .iter() + .map(|key| self.render_cache.get(key).expect("Get cached buffer")) + .collect(); + let bounds = glyphon::TextBounds { left: (bounds.x * scale_factor) as i32, top: (bounds.y * scale_factor) as i32, @@ -190,29 +264,27 @@ impl Pipeline { font: Font, bounds: Size, ) -> (f32, f32) { - let attrs = match font { - Font::Default => glyphon::Attrs::new(), - Font::External { name, .. } => glyphon::Attrs { - family: glyphon::Family::Name(name), - ..glyphon::Attrs::new() + let mut measurement_cache = self.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate(Key { + content, + size: size, + font, + bounds: Size { + width: bounds.width, + height: f32::INFINITY, }, - }; + color: Color::BLACK, + }); + + let (total_lines, max_width) = paragraph + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); - let mut paragraph = - glyphon::BufferLine::new(content, glyphon::AttrsList::new(attrs)); - - // TODO: Cache layout - let layout = paragraph.layout( - &FONT_SYSTEM, - size as i32, - bounds.width as i32, - glyphon::Wrap::Word, - ); - - ( - layout.iter().fold(0.0, |max, line| line.w.max(max)), - size * 1.2 * layout.len() as f32, - ) + (max_width, size * 1.2 * total_lines as f32) } pub fn hit_test( @@ -227,5 +299,14 @@ impl Pipeline { None } - pub fn trim_measurement_cache(&mut self) {} + pub fn trim_measurement_cache(&mut self) { + self.measurement_cache.borrow_mut().trim(); + } +} + +fn to_family(font: Font) -> glyphon::Family<'static> { + match font { + Font::Default => glyphon::Family::SansSerif, + Font::External { name, .. } => glyphon::Family::Name(name), + } } -- cgit From 02fc7e6e89e0a098a2a8cae8490f36d3bdf8126c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Feb 2023 01:30:49 +0100 Subject: Trim text `render_cache` after rendering in `iced_wgpu` --- wgpu/src/text.rs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index a1d621d4..cca00435 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -254,6 +254,8 @@ impl Pipeline { pub fn end_frame(&mut self) { self.renderers.truncate(self.layer); + self.render_cache.trim(); + self.layer = 0; } -- cgit From 6b707711469c7298bb363029a0c3d12a7834278c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Feb 2023 01:34:25 +0100 Subject: Avoid unnecessary `Vec` allocation in text pipeline --- wgpu/src/text.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index cca00435..1e2bf5c2 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -159,11 +159,6 @@ impl Pipeline { }) .collect(); - let buffers: Vec<_> = keys - .iter() - .map(|key| self.render_cache.get(key).expect("Get cached buffer")) - .collect(); - let bounds = glyphon::TextBounds { left: (bounds.x * scale_factor) as i32, top: (bounds.y * scale_factor) as i32, @@ -173,8 +168,11 @@ impl Pipeline { let text_areas: Vec<_> = sections .iter() - .zip(buffers.iter()) - .map(|(section, buffer)| { + .zip(keys.iter()) + .map(|(section, key)| { + let buffer = + self.render_cache.get(key).expect("Get cached buffer"); + let x = section.bounds.x * scale_factor; let y = section.bounds.y * scale_factor; -- cgit From c8e8b1a7ba47ab13cef2dc7f300fbfcefe1e8a48 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Feb 2023 01:51:08 +0100 Subject: Use `bounds` directly for `measure` in text pipeline --- wgpu/src/text.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 1e2bf5c2..026a6d27 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -270,10 +270,7 @@ impl Pipeline { content, size: size, font, - bounds: Size { - width: bounds.width, - height: f32::INFINITY, - }, + bounds, color: Color::BLACK, }); -- cgit From 0a324f0aebdee06c9cce8ef107b44b847dace05b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Feb 2023 02:02:41 +0100 Subject: Implement `hit_test` for `text::Pipeline` in `iced_wgpu` --- wgpu/src/text.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 026a6d27..41787136 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -286,14 +286,26 @@ impl Pipeline { pub fn hit_test( &self, - _content: &str, - _size: f32, - _font: iced_native::Font, - _bounds: iced_native::Size, - _point: iced_native::Point, + content: &str, + size: f32, + font: iced_native::Font, + bounds: iced_native::Size, + point: iced_native::Point, _nearest_only: bool, ) -> Option { - None + let mut measurement_cache = self.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate(Key { + content, + size: size, + font, + bounds, + color: Color::BLACK, + }); + + let cursor = paragraph.hit(point.x as i32, point.y as i32)?; + + Some(Hit::CharOffset(cursor.index)) } pub fn trim_measurement_cache(&mut self) { -- cgit From a7580e0696a1a0ba76a89db3f78bc99ba3fbb361 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 06:50:38 +0100 Subject: Count `layout_runs` instead of using `visible_lines` in `text::Pipeline::prepare` --- wgpu/src/text.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 41787136..fa7fd5df 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -176,14 +176,15 @@ impl Pipeline { let x = section.bounds.x * scale_factor; let y = section.bounds.y * scale_factor; - let max_width = buffer + let (total_lines, max_width) = buffer .layout_runs() - .fold(0.0f32, |max, run| max.max(run.line_w)); + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); - let total_height = buffer.visible_lines() as f32 - * section.size - * 1.2 - * scale_factor; + let total_height = + total_lines as f32 * section.size * 1.2 * scale_factor; let left = match section.horizontal_alignment { alignment::Horizontal::Left => x, -- cgit From b29de28d1f0f608f8029c93d154cfd1b0f8b8cbb Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 07:33:33 +0100 Subject: Overhaul `Font` type to allow font family selection --- wgpu/src/text.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index fa7fd5df..083d9d2b 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -112,8 +112,6 @@ impl Pipeline { device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat, - _default_font: Option<&[u8]>, - _multithreading: bool, ) -> Self { Pipeline { renderers: Vec::new(), @@ -316,7 +314,11 @@ impl Pipeline { fn to_family(font: Font) -> glyphon::Family<'static> { match font { - Font::Default => glyphon::Family::SansSerif, - Font::External { name, .. } => glyphon::Family::Name(name), + Font::Name(name) => glyphon::Family::Name(name), + Font::SansSerif => glyphon::Family::SansSerif, + Font::Serif => glyphon::Family::Serif, + Font::Cursive => glyphon::Family::Cursive, + Font::Fantasy => glyphon::Family::Fantasy, + Font::Monospace => glyphon::Family::Monospace, } } -- cgit From 238154af4ac8dda7f12dd90aa7be106e933bcb30 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 11:12:15 +0100 Subject: Implement `font::load` command in `iced_native` --- wgpu/src/text.rs | 470 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 266 insertions(+), 204 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 083d9d2b..cdfcd576 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -5,108 +5,37 @@ use iced_native::alignment; use iced_native::{Color, Font, Rectangle, Size}; use rustc_hash::{FxHashMap, FxHashSet}; +use std::borrow::Cow; use std::cell::RefCell; use std::hash::{BuildHasher, Hash, Hasher}; +use std::sync::Arc; use twox_hash::RandomXxHashBuilder64; #[allow(missing_debug_implementations)] pub struct Pipeline { + system: Option, renderers: Vec, atlas: glyphon::TextAtlas, - cache: glyphon::SwashCache<'static>, - measurement_cache: RefCell, - render_cache: Cache, layer: usize, } -struct Cache { - entries: FxHashMap>, - recently_used: FxHashSet, - hasher: RandomXxHashBuilder64, -} - -impl Cache { - fn new() -> Self { - Self { - entries: FxHashMap::default(), - recently_used: FxHashSet::default(), - hasher: RandomXxHashBuilder64::default(), - } - } - - fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer<'static>> { - self.entries.get(key) - } - - fn allocate( - &mut self, - key: Key<'_>, - ) -> (KeyHash, &mut glyphon::Buffer<'static>) { - let hash = { - let mut hasher = self.hasher.build_hasher(); - - key.content.hash(&mut hasher); - (key.size as i32).hash(&mut hasher); - key.font.hash(&mut hasher); - (key.bounds.width as i32).hash(&mut hasher); - (key.bounds.height as i32).hash(&mut hasher); - key.color.into_rgba8().hash(&mut hasher); - - hasher.finish() - }; +#[ouroboros::self_referencing] +struct System { + fonts: glyphon::FontSystem, - if !self.entries.contains_key(&hash) { - let metrics = - glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32); + #[borrows(fonts)] + #[not_covariant] + cache: glyphon::SwashCache<'this>, - let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics); + #[borrows(fonts)] + #[not_covariant] + measurement_cache: RefCell>, - buffer.set_size(key.bounds.width as i32, key.bounds.height as i32); - buffer.set_text( - key.content, - glyphon::Attrs::new().family(to_family(key.font)).color({ - let [r, g, b, a] = key.color.into_linear(); - - glyphon::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) - }), - ); - - let _ = self.entries.insert(hash, buffer); - } - - let _ = self.recently_used.insert(hash); - - (hash, self.entries.get_mut(&hash).unwrap()) - } - - fn trim(&mut self) { - self.entries - .retain(|key, _| self.recently_used.contains(key)); - - self.recently_used.clear(); - } + #[borrows(fonts)] + #[not_covariant] + render_cache: Cache<'this>, } -#[derive(Debug, Clone, Copy)] -struct Key<'a> { - content: &'a str, - size: f32, - font: Font, - bounds: Size, - color: Color, -} - -type KeyHash = u64; - -// TODO: Share with `iced_graphics` -static FONT_SYSTEM: once_cell::sync::Lazy = - once_cell::sync::Lazy::new(glyphon::FontSystem::new); - impl Pipeline { pub fn new( device: &wgpu::Device, @@ -114,15 +43,41 @@ impl Pipeline { format: wgpu::TextureFormat, ) -> Self { Pipeline { + system: Some( + SystemBuilder { + fonts: glyphon::FontSystem::new(), + cache_builder: |fonts| glyphon::SwashCache::new(fonts), + measurement_cache_builder: |_| RefCell::new(Cache::new()), + render_cache_builder: |_| Cache::new(), + } + .build(), + ), renderers: Vec::new(), atlas: glyphon::TextAtlas::new(device, queue, format), - cache: glyphon::SwashCache::new(&FONT_SYSTEM), - measurement_cache: RefCell::new(Cache::new()), - render_cache: Cache::new(), layer: 0, } } + pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + let heads = self.system.take().unwrap().into_heads(); + + let (locale, mut db) = heads.fonts.into_locale_and_db(); + + db.load_font_source(glyphon::fontdb::Source::Binary(Arc::new( + bytes.to_owned(), + ))); + + self.system = Some( + SystemBuilder { + fonts: glyphon::FontSystem::new_with_locale_and_db(locale, db), + cache_builder: |fonts| glyphon::SwashCache::new(fonts), + measurement_cache_builder: |_| RefCell::new(Cache::new()), + render_cache_builder: |_| Cache::new(), + } + .build(), + ); + } + pub fn prepare( &mut self, device: &wgpu::Device, @@ -132,93 +87,100 @@ impl Pipeline { scale_factor: f32, target_size: Size, ) { - if self.renderers.len() <= self.layer { - self.renderers - .push(glyphon::TextRenderer::new(device, queue)); - } - - let renderer = &mut self.renderers[self.layer]; - - let keys: Vec<_> = sections - .iter() - .map(|section| { - let (key, _) = self.render_cache.allocate(Key { - content: section.content, - size: section.size * scale_factor, - font: section.font, - bounds: Size { - width: section.bounds.width * scale_factor, - height: section.bounds.height * scale_factor, + self.system.as_mut().unwrap().with_mut(|fields| { + if self.renderers.len() <= self.layer { + self.renderers + .push(glyphon::TextRenderer::new(device, queue)); + } + + let renderer = &mut self.renderers[self.layer]; + + let keys: Vec<_> = sections + .iter() + .map(|section| { + let (key, _) = fields.render_cache.allocate( + fields.fonts, + Key { + content: section.content, + size: section.size * scale_factor, + font: section.font, + bounds: Size { + width: section.bounds.width * scale_factor, + height: section.bounds.height * scale_factor, + }, + color: section.color, + }, + ); + + key + }) + .collect(); + + let bounds = glyphon::TextBounds { + left: (bounds.x * scale_factor) as i32, + top: (bounds.y * scale_factor) as i32, + right: ((bounds.x + bounds.width) * scale_factor) as i32, + bottom: ((bounds.y + bounds.height) * scale_factor) as i32, + }; + + let text_areas: Vec<_> = sections + .iter() + .zip(keys.iter()) + .map(|(section, key)| { + let buffer = fields + .render_cache + .get(key) + .expect("Get cached buffer"); + + let x = section.bounds.x * scale_factor; + let y = section.bounds.y * scale_factor; + + let (total_lines, max_width) = buffer + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); + + let total_height = + total_lines as f32 * section.size * 1.2 * scale_factor; + + let left = match section.horizontal_alignment { + alignment::Horizontal::Left => x, + alignment::Horizontal::Center => x - max_width / 2.0, + alignment::Horizontal::Right => x - max_width, + }; + + let top = match section.vertical_alignment { + alignment::Vertical::Top => y, + alignment::Vertical::Center => y - total_height / 2.0, + alignment::Vertical::Bottom => y - total_height, + }; + + glyphon::TextArea { + buffer, + left: left as i32, + top: top as i32, + bounds, + } + }) + .collect(); + + renderer + .prepare( + device, + queue, + &mut self.atlas, + glyphon::Resolution { + width: target_size.width, + height: target_size.height, }, - color: section.color, - }); - - key - }) - .collect(); - - let bounds = glyphon::TextBounds { - left: (bounds.x * scale_factor) as i32, - top: (bounds.y * scale_factor) as i32, - right: ((bounds.x + bounds.width) * scale_factor) as i32, - bottom: ((bounds.y + bounds.height) * scale_factor) as i32, - }; - - let text_areas: Vec<_> = sections - .iter() - .zip(keys.iter()) - .map(|(section, key)| { - let buffer = - self.render_cache.get(key).expect("Get cached buffer"); - - let x = section.bounds.x * scale_factor; - let y = section.bounds.y * scale_factor; - - let (total_lines, max_width) = buffer - .layout_runs() - .enumerate() - .fold((0, 0.0), |(_, max), (i, buffer)| { - (i + 1, buffer.line_w.max(max)) - }); - - let total_height = - total_lines as f32 * section.size * 1.2 * scale_factor; - - let left = match section.horizontal_alignment { - alignment::Horizontal::Left => x, - alignment::Horizontal::Center => x - max_width / 2.0, - alignment::Horizontal::Right => x - max_width, - }; - - let top = match section.vertical_alignment { - alignment::Vertical::Top => y, - alignment::Vertical::Center => y - total_height / 2.0, - alignment::Vertical::Bottom => y - total_height, - }; - - glyphon::TextArea { - buffer, - left: left as i32, - top: top as i32, - bounds, - } - }) - .collect(); - - renderer - .prepare( - device, - queue, - &mut self.atlas, - glyphon::Resolution { - width: target_size.width, - height: target_size.height, - }, - &text_areas, - glyphon::Color::rgb(0, 0, 0), - &mut self.cache, - ) - .expect("Prepare text sections"); + &text_areas, + glyphon::Color::rgb(0, 0, 0), + fields.cache, + ) + .expect("Prepare text sections"); + }); } pub fn render( @@ -251,7 +213,10 @@ impl Pipeline { pub fn end_frame(&mut self) { self.renderers.truncate(self.layer); - self.render_cache.trim(); + self.system + .as_mut() + .unwrap() + .with_render_cache_mut(|cache| cache.trim()); self.layer = 0; } @@ -263,24 +228,29 @@ impl Pipeline { font: Font, bounds: Size, ) -> (f32, f32) { - let mut measurement_cache = self.measurement_cache.borrow_mut(); - - let (_, paragraph) = measurement_cache.allocate(Key { - content, - size: size, - font, - bounds, - color: Color::BLACK, - }); + self.system.as_ref().unwrap().with(|fields| { + let mut measurement_cache = fields.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate( + fields.fonts, + Key { + content, + size: size, + font, + bounds, + color: Color::BLACK, + }, + ); - let (total_lines, max_width) = paragraph - .layout_runs() - .enumerate() - .fold((0, 0.0), |(_, max), (i, buffer)| { - (i + 1, buffer.line_w.max(max)) - }); + let (total_lines, max_width) = paragraph + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); - (max_width, size * 1.2 * total_lines as f32) + (max_width, size * 1.2 * total_lines as f32) + }) } pub fn hit_test( @@ -292,23 +262,31 @@ impl Pipeline { point: iced_native::Point, _nearest_only: bool, ) -> Option { - let mut measurement_cache = self.measurement_cache.borrow_mut(); - - let (_, paragraph) = measurement_cache.allocate(Key { - content, - size: size, - font, - bounds, - color: Color::BLACK, - }); + self.system.as_ref().unwrap().with(|fields| { + let mut measurement_cache = fields.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate( + fields.fonts, + Key { + content, + size: size, + font, + bounds, + color: Color::BLACK, + }, + ); - let cursor = paragraph.hit(point.x as i32, point.y as i32)?; + let cursor = paragraph.hit(point.x as i32, point.y as i32)?; - Some(Hit::CharOffset(cursor.index)) + Some(Hit::CharOffset(cursor.index)) + }) } pub fn trim_measurement_cache(&mut self) { - self.measurement_cache.borrow_mut().trim(); + self.system + .as_mut() + .unwrap() + .with_measurement_cache_mut(|cache| cache.borrow_mut().trim()); } } @@ -322,3 +300,87 @@ fn to_family(font: Font) -> glyphon::Family<'static> { Font::Monospace => glyphon::Family::Monospace, } } + +struct Cache<'a> { + entries: FxHashMap>, + recently_used: FxHashSet, + hasher: RandomXxHashBuilder64, +} + +impl<'a> Cache<'a> { + fn new() -> Self { + Self { + entries: FxHashMap::default(), + recently_used: FxHashSet::default(), + hasher: RandomXxHashBuilder64::default(), + } + } + + fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer<'a>> { + self.entries.get(key) + } + + fn allocate( + &mut self, + fonts: &'a glyphon::FontSystem, + key: Key<'_>, + ) -> (KeyHash, &mut glyphon::Buffer<'a>) { + let hash = { + let mut hasher = self.hasher.build_hasher(); + + key.content.hash(&mut hasher); + (key.size as i32).hash(&mut hasher); + key.font.hash(&mut hasher); + (key.bounds.width as i32).hash(&mut hasher); + (key.bounds.height as i32).hash(&mut hasher); + key.color.into_rgba8().hash(&mut hasher); + + hasher.finish() + }; + + if !self.entries.contains_key(&hash) { + let metrics = + glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32); + let mut buffer = glyphon::Buffer::new(&fonts, metrics); + + buffer.set_size(key.bounds.width as i32, key.bounds.height as i32); + buffer.set_text( + key.content, + glyphon::Attrs::new().family(to_family(key.font)).color({ + let [r, g, b, a] = key.color.into_linear(); + + glyphon::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) + }), + ); + + let _ = self.entries.insert(hash, buffer); + } + + let _ = self.recently_used.insert(hash); + + (hash, self.entries.get_mut(&hash).unwrap()) + } + + fn trim(&mut self) { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); + } +} + +#[derive(Debug, Clone, Copy)] +struct Key<'a> { + content: &'a str, + size: f32, + font: Font, + bounds: Size, + color: Color, +} + +type KeyHash = u64; -- cgit From 5a82fc654e2933c4c93dac5393685861feb07b1f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 11:21:35 +0100 Subject: Use floating coordinates directly in `text::Pipeline` --- wgpu/src/text.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index cdfcd576..2ca3f0d8 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -276,7 +276,7 @@ impl Pipeline { }, ); - let cursor = paragraph.hit(point.x as i32, point.y as i32)?; + let cursor = paragraph.hit(point.x, point.y)?; Some(Hit::CharOffset(cursor.index)) }) @@ -329,21 +329,20 @@ impl<'a> Cache<'a> { let mut hasher = self.hasher.build_hasher(); key.content.hash(&mut hasher); - (key.size as i32).hash(&mut hasher); + key.size.to_bits().hash(&mut hasher); key.font.hash(&mut hasher); - (key.bounds.width as i32).hash(&mut hasher); - (key.bounds.height as i32).hash(&mut hasher); + key.bounds.width.to_bits().hash(&mut hasher); + key.bounds.height.to_bits().hash(&mut hasher); key.color.into_rgba8().hash(&mut hasher); hasher.finish() }; if !self.entries.contains_key(&hash) { - let metrics = - glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32); + let metrics = glyphon::Metrics::new(key.size, key.size * 1.2); let mut buffer = glyphon::Buffer::new(&fonts, metrics); - buffer.set_size(key.bounds.width as i32, key.bounds.height as i32); + buffer.set_size(key.bounds.width, key.bounds.height); buffer.set_text( key.content, glyphon::Attrs::new().family(to_family(key.font)).color({ -- cgit From d2825360a75600bb6b4097737c987e2d9e05da6a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 11:35:12 +0100 Subject: Load `Iced-Icons.ttf` font in `text::Pipeline::new` --- wgpu/src/text.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 2ca3f0d8..4af57af3 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -45,7 +45,13 @@ impl Pipeline { Pipeline { system: Some( SystemBuilder { - fonts: glyphon::FontSystem::new(), + fonts: glyphon::FontSystem::new_with_fonts( + [glyphon::fontdb::Source::Binary(Arc::new( + include_bytes!("../fonts/Iced-Icons.ttf") + .as_slice(), + ))] + .into_iter(), + ), cache_builder: |fonts| glyphon::SwashCache::new(fonts), measurement_cache_builder: |_| RefCell::new(Cache::new()), render_cache_builder: |_| Cache::new(), -- cgit From 17470bf7d36ee164311020b9d8c79662ac49c166 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 12:35:10 +0100 Subject: Fix `clippy` lints :tada: --- wgpu/src/text.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 4af57af3..967596f2 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -7,6 +7,7 @@ use iced_native::{Color, Font, Rectangle, Size}; use rustc_hash::{FxHashMap, FxHashSet}; use std::borrow::Cow; use std::cell::RefCell; +use std::collections::hash_map; use std::hash::{BuildHasher, Hash, Hasher}; use std::sync::Arc; use twox_hash::RandomXxHashBuilder64; @@ -70,7 +71,7 @@ impl Pipeline { let (locale, mut db) = heads.fonts.into_locale_and_db(); db.load_font_source(glyphon::fontdb::Source::Binary(Arc::new( - bytes.to_owned(), + bytes.into_owned(), ))); self.system = Some( @@ -241,7 +242,7 @@ impl Pipeline { fields.fonts, Key { content, - size: size, + size, font, bounds, color: Color::BLACK, @@ -275,7 +276,7 @@ impl Pipeline { fields.fonts, Key { content, - size: size, + size, font, bounds, color: Color::BLACK, @@ -344,9 +345,9 @@ impl<'a> Cache<'a> { hasher.finish() }; - if !self.entries.contains_key(&hash) { + if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { let metrics = glyphon::Metrics::new(key.size, key.size * 1.2); - let mut buffer = glyphon::Buffer::new(&fonts, metrics); + let mut buffer = glyphon::Buffer::new(fonts, metrics); buffer.set_size(key.bounds.width, key.bounds.height); buffer.set_text( @@ -363,7 +364,7 @@ impl<'a> Cache<'a> { }), ); - let _ = self.entries.insert(hash, buffer); + let _ = entry.insert(buffer); } let _ = self.recently_used.insert(hash); -- cgit From da4182099db703d59006ca72de4cb4d54c9d7855 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 12:42:02 +0100 Subject: Disable `std` feature for `twox-hash` to fix Wasm build --- wgpu/src/text.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 967596f2..6c88acc3 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -10,7 +10,6 @@ use std::cell::RefCell; use std::collections::hash_map; use std::hash::{BuildHasher, Hash, Hasher}; use std::sync::Arc; -use twox_hash::RandomXxHashBuilder64; #[allow(missing_debug_implementations)] pub struct Pipeline { @@ -311,15 +310,21 @@ fn to_family(font: Font) -> glyphon::Family<'static> { struct Cache<'a> { entries: FxHashMap>, recently_used: FxHashSet, - hasher: RandomXxHashBuilder64, + hasher: HashBuilder, } +#[cfg(not(target_arch = "wasm32"))] +type HashBuilder = twox_hash::RandomXxHashBuilder64; + +#[cfg(target_arch = "wasm32")] +type HashBuilder = std::hash::BuildHasherDefault; + impl<'a> Cache<'a> { fn new() -> Self { Self { entries: FxHashMap::default(), recently_used: FxHashSet::default(), - hasher: RandomXxHashBuilder64::default(), + hasher: HashBuilder::default(), } } -- cgit From 6cf4a10906e777645b6bf47faf0e7b8f3c36549e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:28:46 +0100 Subject: Stop reusing `SwashCache` in `text::Pipeline` `SwashCache` can't be easily trimmed and it's not really getting us anything since `glyphon` already caches using a glyph atlas anyways. --- wgpu/src/text.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 6c88acc3..59a840ca 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -23,10 +23,6 @@ pub struct Pipeline { struct System { fonts: glyphon::FontSystem, - #[borrows(fonts)] - #[not_covariant] - cache: glyphon::SwashCache<'this>, - #[borrows(fonts)] #[not_covariant] measurement_cache: RefCell>, @@ -52,7 +48,6 @@ impl Pipeline { ))] .into_iter(), ), - cache_builder: |fonts| glyphon::SwashCache::new(fonts), measurement_cache_builder: |_| RefCell::new(Cache::new()), render_cache_builder: |_| Cache::new(), } @@ -76,7 +71,6 @@ impl Pipeline { self.system = Some( SystemBuilder { fonts: glyphon::FontSystem::new_with_locale_and_db(locale, db), - cache_builder: |fonts| glyphon::SwashCache::new(fonts), measurement_cache_builder: |_| RefCell::new(Cache::new()), render_cache_builder: |_| Cache::new(), } @@ -183,7 +177,7 @@ impl Pipeline { }, &text_areas, glyphon::Color::rgb(0, 0, 0), - fields.cache, + &mut glyphon::SwashCache::new(fields.fonts), ) .expect("Prepare text sections"); }); -- cgit From f37b87fbabf09296ad7fea695baded25020d5fbc Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:30:11 +0100 Subject: Avoid allocating `text_areas` in `text::Pipeline` --- wgpu/src/text.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 59a840ca..e8c3e385 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -123,10 +123,8 @@ impl Pipeline { bottom: ((bounds.y + bounds.height) * scale_factor) as i32, }; - let text_areas: Vec<_> = sections - .iter() - .zip(keys.iter()) - .map(|(section, key)| { + let text_areas = + sections.iter().zip(keys.iter()).map(|(section, key)| { let buffer = fields .render_cache .get(key) @@ -163,8 +161,7 @@ impl Pipeline { top: top as i32, bounds, } - }) - .collect(); + }); renderer .prepare( @@ -175,7 +172,7 @@ impl Pipeline { width: target_size.width, height: target_size.height, }, - &text_areas, + text_areas, glyphon::Color::rgb(0, 0, 0), &mut glyphon::SwashCache::new(fields.fonts), ) -- cgit From 15d257a52a994f0110d775eb49ca7e6100b0e1a8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:30:32 +0100 Subject: Stop truncating the `renderers` in `text::Pipeline` We avoid recreating buffers this way, and the amount of layers should stay relatively low anyways. --- wgpu/src/text.rs | 1 - 1 file changed, 1 deletion(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index e8c3e385..16eea234 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -209,7 +209,6 @@ impl Pipeline { } pub fn end_frame(&mut self) { - self.renderers.truncate(self.layer); self.system .as_mut() .unwrap() -- cgit From dd80772da9ce89230d5a96c96923837f9887befa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:31:53 +0100 Subject: Set a minimum `height` for `Buffer` of `size * 1.2` This avoids text from misteriously disappearing, even if the user uses a `height` that isn't enough to fit the text. --- wgpu/src/text.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 16eea234..f1171b5f 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -344,7 +344,10 @@ impl<'a> Cache<'a> { let metrics = glyphon::Metrics::new(key.size, key.size * 1.2); let mut buffer = glyphon::Buffer::new(fonts, metrics); - buffer.set_size(key.bounds.width, key.bounds.height); + buffer.set_size( + key.bounds.width, + key.bounds.height.max(key.size * 1.2), + ); buffer.set_text( key.content, glyphon::Attrs::new().family(to_family(key.font)).color({ -- cgit From 7d4c63d4119c505801daf138be44e65599e748d0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:33:09 +0100 Subject: Set `Attrs::monospaced` if `Font::Monospace` is selected --- wgpu/src/text.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index f1171b5f..6e7e60d8 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -350,16 +350,19 @@ impl<'a> Cache<'a> { ); buffer.set_text( key.content, - glyphon::Attrs::new().family(to_family(key.font)).color({ - let [r, g, b, a] = key.color.into_linear(); - - glyphon::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) - }), + glyphon::Attrs::new() + .family(to_family(key.font)) + .color({ + let [r, g, b, a] = key.color.into_linear(); + + glyphon::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) + }) + .monospaced(matches!(key.font, Font::Monospace)), ); let _ = entry.insert(buffer); -- cgit From a970f34cb4968bfe913c1f77b5087f44f29aba8a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 7 Feb 2023 18:35:05 +0100 Subject: Apply `ceil` to text bounds when drawing --- wgpu/src/text.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 6e7e60d8..95994a4e 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -105,8 +105,10 @@ impl Pipeline { size: section.size * scale_factor, font: section.font, bounds: Size { - width: section.bounds.width * scale_factor, - height: section.bounds.height * scale_factor, + width: (section.bounds.width * scale_factor) + .ceil(), + height: (section.bounds.height * scale_factor) + .ceil(), }, color: section.color, }, -- cgit From 730d6a07564d014c470e02f233394ec98325d463 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 8 Feb 2023 00:47:16 +0100 Subject: Reuse a `RenderPass` as much as possible in `iced_wgpu` --- wgpu/src/text.rs | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 95994a4e..73708fd8 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -16,7 +16,7 @@ pub struct Pipeline { system: Option, renderers: Vec, atlas: glyphon::TextAtlas, - layer: usize, + prepare_layer: usize, } #[ouroboros::self_referencing] @@ -55,7 +55,7 @@ impl Pipeline { ), renderers: Vec::new(), atlas: glyphon::TextAtlas::new(device, queue, format), - layer: 0, + prepare_layer: 0, } } @@ -88,12 +88,12 @@ impl Pipeline { target_size: Size, ) { self.system.as_mut().unwrap().with_mut(|fields| { - if self.renderers.len() <= self.layer { + if self.renderers.len() <= self.prepare_layer { self.renderers .push(glyphon::TextRenderer::new(device, queue)); } - let renderer = &mut self.renderers[self.layer]; + let renderer = &mut self.renderers[self.prepare_layer]; let keys: Vec<_> = sections .iter() @@ -179,35 +179,21 @@ impl Pipeline { &mut glyphon::SwashCache::new(fields.fonts), ) .expect("Prepare text sections"); + + self.prepare_layer += 1; }); } - pub fn render( - &mut self, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, + pub fn render<'a>( + &'a self, + layer: usize, + render_pass: &mut wgpu::RenderPass<'a>, ) { - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - })], - depth_stencil_attachment: None, - }); - - let renderer = &mut self.renderers[self.layer]; + let renderer = &self.renderers[layer]; renderer - .render(&self.atlas, &mut render_pass) + .render(&self.atlas, render_pass) .expect("Render text"); - - self.layer += 1; } pub fn end_frame(&mut self) { @@ -216,7 +202,7 @@ impl Pipeline { .unwrap() .with_render_cache_mut(|cache| cache.trim()); - self.layer = 0; + self.prepare_layer = 0; } pub fn measure( -- cgit From ddbf93a82ff2ee0ca3265baf2f5b4442717b9101 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 8 Feb 2023 01:23:40 +0100 Subject: Set scissoring properly in `text::Pipeline` --- wgpu/src/text.rs | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 73708fd8..655ad987 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -187,10 +187,18 @@ impl Pipeline { pub fn render<'a>( &'a self, layer: usize, + bounds: Rectangle, render_pass: &mut wgpu::RenderPass<'a>, ) { let renderer = &self.renderers[layer]; + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + renderer .render(&self.atlas, render_pass) .expect("Render text"); -- cgit From 05c787c2efbd8c8bc11925e1605b8b09ba744268 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 8 Feb 2023 23:21:04 +0100 Subject: Grow atlas in `text::Pipeline` when necessary --- wgpu/src/text.rs | 54 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 18 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 655ad987..4406177a 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -86,7 +86,7 @@ impl Pipeline { bounds: Rectangle, scale_factor: f32, target_size: Size, - ) { + ) -> bool { self.system.as_mut().unwrap().with_mut(|fields| { if self.renderers.len() <= self.prepare_layer { self.renderers @@ -165,23 +165,39 @@ impl Pipeline { } }); - renderer - .prepare( - device, - queue, - &mut self.atlas, - glyphon::Resolution { - width: target_size.width, - height: target_size.height, - }, - text_areas, - glyphon::Color::rgb(0, 0, 0), - &mut glyphon::SwashCache::new(fields.fonts), - ) - .expect("Prepare text sections"); - - self.prepare_layer += 1; - }); + let result = renderer.prepare( + device, + queue, + &mut self.atlas, + glyphon::Resolution { + width: target_size.width, + height: target_size.height, + }, + text_areas, + glyphon::Color::rgb(0, 0, 0), + &mut glyphon::SwashCache::new(fields.fonts), + ); + + match result { + Ok(()) => { + self.prepare_layer += 1; + + true + } + Err(glyphon::PrepareError::AtlasFull(content_type)) => { + self.prepare_layer = 0; + + if self.atlas.grow(device, content_type) { + false + } else { + // If the atlas cannot grow, then all bets are off. + // Instead of panicking, we will just pray that the result + // will be somewhat readable... + true + } + } + } + }) } pub fn render<'a>( @@ -205,6 +221,8 @@ impl Pipeline { } pub fn end_frame(&mut self) { + self.atlas.trim(); + self.system .as_mut() .unwrap() -- cgit From 17a4d817c45da732e9fdc6559b6bbc7d5be94052 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 9 Feb 2023 07:00:21 +0100 Subject: Collapse conditional and please `clippy` --- wgpu/src/text.rs | 1 + 1 file changed, 1 insertion(+) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 4406177a..c0f49417 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -187,6 +187,7 @@ impl Pipeline { Err(glyphon::PrepareError::AtlasFull(content_type)) => { self.prepare_layer = 0; + #[allow(clippy::needless_bool)] if self.atlas.grow(device, content_type) { false } else { -- cgit From 51844c5d0c7f7c9b65c56330862b69f0baa0e3c1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 10 Feb 2023 18:52:35 +0100 Subject: Trim `Cache` every 300 frames in `text::Pipeline` --- wgpu/src/text.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) (limited to 'wgpu/src/text.rs') diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index c0f49417..dea6ab18 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -316,6 +316,7 @@ struct Cache<'a> { entries: FxHashMap>, recently_used: FxHashSet, hasher: HashBuilder, + trim_count: usize, } #[cfg(not(target_arch = "wasm32"))] @@ -325,11 +326,14 @@ type HashBuilder = twox_hash::RandomXxHashBuilder64; type HashBuilder = std::hash::BuildHasherDefault; impl<'a> Cache<'a> { + const TRIM_INTERVAL: usize = 300; + fn new() -> Self { Self { entries: FxHashMap::default(), recently_used: FxHashSet::default(), hasher: HashBuilder::default(), + trim_count: 0, } } @@ -389,10 +393,16 @@ impl<'a> Cache<'a> { } fn trim(&mut self) { - self.entries - .retain(|key, _| self.recently_used.contains(key)); + if self.trim_count >= Self::TRIM_INTERVAL { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); - self.recently_used.clear(); + self.trim_count = 0; + } else { + self.trim_count += 1; + } } } -- cgit