From 6e11c0c94544f769dc2835ef2881cde9b7e98570 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Thu, 10 Jun 2021 12:38:24 +0100 Subject: [PATCH 1/5] Text rastering: allow up to 16 sub-pixel steps, access to scaled font info, cleaner code --- Cargo.toml | 2 +- kas-theme/src/flat_theme.rs | 4 +-- kas-wgpu/src/draw/text_pipe.rs | 54 ++++++++++++++++++++-------------- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 331c8003b..6c01d91e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,4 +89,4 @@ features = ["serde"] members = ["kas-macros", "kas-theme", "kas-wgpu"] [patch.crates-io] -kas-text = { git = "https://github.com/kas-gui/kas-text.git", rev = "05b50366a0a2fe7d1e611d93acbb2d9f83c52f0a" } +kas-text = { git = "https://github.com/kas-gui/kas-text.git", rev = "448d07fc3290f011440d83af6d97901ebce9c72e" } diff --git a/kas-theme/src/flat_theme.rs b/kas-theme/src/flat_theme.rs index 7f3cd7aea..2ac243085 100644 --- a/kas-theme/src/flat_theme.rs +++ b/kas-theme/src/flat_theme.rs @@ -114,10 +114,10 @@ where } fn init(&mut self, _draw: &mut D) { - if let Err(e) = kas::text::fonts::fonts().select_default() { + let fonts = fonts::fonts(); + if let Err(e) = fonts.select_default() { panic!("Error loading font: {}", e); } - let fonts = fonts::fonts(); self.fonts = Some(Rc::new( self.config .iter_fonts() diff --git a/kas-wgpu/src/draw/text_pipe.rs b/kas-wgpu/src/draw/text_pipe.rs index bfc2d872f..2e8f23c44 100644 --- a/kas-wgpu/src/draw/text_pipe.rs +++ b/kas-wgpu/src/draw/text_pipe.rs @@ -10,8 +10,9 @@ use ab_glyph::{Font, FontRef}; use kas::cast::*; use kas::draw::{color::Rgba, Pass}; use kas::geom::{Quad, Vec2}; -use kas::text::fonts::{fonts, FaceId}; -use kas::text::{Effect, Glyph, TextDisplay}; +use kas::text::conv::DPU; +use kas::text::fonts::{fonts, FaceId, ScaledFaceRef}; +use kas::text::{Effect, Glyph, GlyphId, TextDisplay}; use std::collections::hash_map::{Entry, HashMap}; use std::mem::size_of; use std::num::NonZeroU32; @@ -37,19 +38,21 @@ struct SpriteDescriptor(u64); impl SpriteDescriptor { /// Choose a sub-pixel precision multiplier based on the height /// - /// Must return an integer between 1 and 15. + /// Must return an integer between 1 and 16. fn sub_pixel_from_height(height: f32) -> f32 { // Due to rounding sub-pixel precision is disabled for height > 20 - (30.0 / height).round().clamp(1.0, 15.0) + (30.0 / height).round().clamp(1.0, 16.0) } fn new(face: FaceId, glyph: Glyph, height: f32) -> Self { let face: u16 = face.get().cast(); let glyph_id: u16 = glyph.id.0; let mult = Self::sub_pixel_from_height(height); + let mult2 = 0.5 * mult; + let steps = u8::conv_nearest(mult); let height: u32 = (height * SCALE_MULT).cast_nearest(); - let x_off: u8 = (glyph.position.0.fract() * mult).cast_nearest(); - let y_off: u8 = (glyph.position.1.fract() * mult).cast_nearest(); + let x_off = u8::conv_floor(glyph.position.0.fract() * mult + mult2) % steps; + let y_off = u8::conv_floor(glyph.position.1.fract() * mult + mult2) % steps; assert!(height & 0xFF00_0000 == 0 && x_off & 0xF0 == 0 && y_off & 0xF0 == 0); let packed = face as u64 | ((glyph_id as u64) << 16) @@ -59,6 +62,7 @@ impl SpriteDescriptor { SpriteDescriptor(packed) } + #[allow(unused)] fn face(self) -> usize { (self.0 & 0x0000_0000_0000_FFFF) as usize } @@ -98,22 +102,27 @@ struct Sprite { impl atlases::Pipeline { fn rasterize( &mut self, + sf: ScaledFaceRef, face: &FontRef<'static>, desc: SpriteDescriptor, ) -> Option<(Sprite, (u32, u32), (u32, u32), Vec)> { - let fract_pos = desc.fractional_position(); + let id = GlyphId(desc.glyph()); + let (x, y) = desc.fractional_position(); + let glyph_off = Vec2(x.round(), y.round()); + let glyph = ab_glyph::Glyph { - id: ab_glyph::GlyphId(desc.glyph()), + id: ab_glyph::GlyphId(id.0), scale: desc.height().into(), - position: fract_pos.into(), + position: ab_glyph::point(x, y), }; let outline = face.outline_glyph(glyph)?; let bounds = outline.px_bounds(); let size = to_vec2(bounds.max - bounds.min); - let offset = to_vec2(bounds.min) - Vec2(fract_pos.0.round(), fract_pos.1.round()); + let offset = to_vec2(bounds.min) - glyph_off; let size_u32 = (u32::conv_trunc(size.0), u32::conv_trunc(size.1)); if size_u32.0 == 0 || size_u32.1 == 0 { + log::warn!("Zero-sized glyph: {}", desc.glyph()); return None; // nothing to draw } @@ -273,18 +282,22 @@ impl Pipeline { /// /// This returns `None` if there's nothing to render. It may also return /// `None` (with a warning) on error. - fn get_glyph(&mut self, desc: SpriteDescriptor) -> Option { + fn get_glyph(&mut self, face: FaceId, dpu: DPU, height: f32, glyph: Glyph) -> Option { + let desc = SpriteDescriptor::new(face, glyph, height); match self.glyphs.entry(desc) { Entry::Occupied(entry) => entry.get().clone(), Entry::Vacant(entry) => { // NOTE: we only need the allocation and coordinates now; the // rendering could be offloaded. - let face = &self.faces[desc.face()]; - let result = self.atlas_pipe.rasterize(face, desc); + let sf = fonts().get_face(face).scale_by_dpu(dpu); + let face = &self.faces[usize::conv(face.0)]; + let result = self.atlas_pipe.rasterize(sf, face, desc); let sprite = if let Some((sprite, origin, size, data)) = result { self.prepare.push((sprite.atlas, origin, size, data)); Some(sprite) } else { + log::warn!("Failed to rasterize glyph: {:?}", glyph); + None }; entry.insert(sprite.clone()); @@ -329,9 +342,8 @@ impl Window { ) { let time = std::time::Instant::now(); - let for_glyph = |face: FaceId, _, height: f32, glyph: Glyph| { - let desc = SpriteDescriptor::new(face, glyph, height); - if let Some(sprite) = pipe.get_glyph(desc) { + let for_glyph = |face: FaceId, dpu: DPU, height: f32, glyph: Glyph| { + if let Some(sprite) = pipe.get_glyph(face, dpu, height, glyph) { let pos = pos + Vec2::from(glyph.position); let a = pos + sprite.offset; let b = a + sprite.size; @@ -369,9 +381,8 @@ impl Window { let time = std::time::Instant::now(); let mut rects = vec![]; - let mut for_glyph = |face: FaceId, _, height: f32, glyph: Glyph, _: usize, _: ()| { - let desc = SpriteDescriptor::new(face, glyph, height); - if let Some(sprite) = pipe.get_glyph(desc) { + let mut for_glyph = |face: FaceId, dpu: DPU, height: f32, glyph: Glyph, _: usize, _: ()| { + if let Some(sprite) = pipe.get_glyph(face, dpu, height, glyph) { let pos = pos + Vec2::from(glyph.position); let a = pos + sprite.offset; let b = a + sprite.size; @@ -430,9 +441,8 @@ impl Window { let time = std::time::Instant::now(); let mut rects = vec![]; - let for_glyph = |face: FaceId, _, height: f32, glyph: Glyph, _, col: Rgba| { - let desc = SpriteDescriptor::new(face, glyph, height); - if let Some(sprite) = pipe.get_glyph(desc) { + let for_glyph = |face: FaceId, dpu: DPU, height: f32, glyph: Glyph, _, col: Rgba| { + if let Some(sprite) = pipe.get_glyph(face, dpu, height, glyph) { let pos = pos + Vec2::from(glyph.position); let a = pos + sprite.offset; let b = a + sprite.size; From 9b762c3491777a6330b452bde261815d9d009ec7 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Thu, 10 Jun 2021 15:25:15 +0100 Subject: [PATCH 2/5] Add fontdue renderer feature --- kas-wgpu/Cargo.toml | 4 ++ kas-wgpu/README.md | 3 + kas-wgpu/src/draw/text_pipe.rs | 106 ++++++++++++++++++++++----------- 3 files changed, 77 insertions(+), 36 deletions(-) diff --git a/kas-wgpu/Cargo.toml b/kas-wgpu/Cargo.toml index f4a3c35a3..e7418cc2c 100644 --- a/kas-wgpu/Cargo.toml +++ b/kas-wgpu/Cargo.toml @@ -33,6 +33,9 @@ stack_dst = ["kas-theme/stack_dst"] # Use kas-theme's unsize feature (nightly-only) unsize = ["kas-theme/unsize"] +# Also: +# fontdue: use this for font rasterisation + [dependencies] kas = { path = "..", version = "0.7.0", features = ["config", "winit"] } kas-theme = { path = "../kas-theme", features = ["config"], version = "0.7.0" } @@ -47,6 +50,7 @@ thiserror = "1.0.23" window_clipboard = { version = "0.2.0", optional = true } image = "0.23.14" guillotiere = "0.6.0" +fontdue = { version = "0.5.2", optional = true } [dev-dependencies] chrono = "0.4" diff --git a/kas-wgpu/README.md b/kas-wgpu/README.md index e4cb09718..6d7adec23 100644 --- a/kas-wgpu/README.md +++ b/kas-wgpu/README.md @@ -31,12 +31,15 @@ Optional features This crate has the following feature flags: - `clipboard` (enabled by default): clipboard integration +- `fontdue`: use [fontdue] library for font rasterisation (otherwise, `ab_glyph` is used) - `stack_dst` (enabled by default): enables `kas-theme::MultiTheme` - `gat`: enables usage of the Generic Associated Types feature (nightly only and currently unstable), allowing some usages of `unsafe` to be avoided. (The plan is to enable this by default once the feature is mature.) - `unsize`: forwards this feature flag to `kas-theme` +[fontdue]: https://github.com/mooman219/fontdue + Copyright and Licence ------- diff --git a/kas-wgpu/src/draw/text_pipe.rs b/kas-wgpu/src/draw/text_pipe.rs index 2e8f23c44..afc0dee85 100644 --- a/kas-wgpu/src/draw/text_pipe.rs +++ b/kas-wgpu/src/draw/text_pipe.rs @@ -6,17 +6,24 @@ //! Text drawing pipeline use super::{atlases, ShaderManager}; -use ab_glyph::{Font, FontRef}; +#[cfg(not(feature = "fontdue"))] +use ab_glyph::Font as _; use kas::cast::*; use kas::draw::{color::Rgba, Pass}; use kas::geom::{Quad, Vec2}; use kas::text::conv::DPU; use kas::text::fonts::{fonts, FaceId, ScaledFaceRef}; -use kas::text::{Effect, Glyph, GlyphId, TextDisplay}; +use kas::text::{Effect, Glyph, TextDisplay}; use std::collections::hash_map::{Entry, HashMap}; use std::mem::size_of; use std::num::NonZeroU32; +#[cfg(not(feature = "fontdue"))] +type FontFace = ab_glyph::FontRef<'static>; +#[cfg(feature = "fontdue")] +use fontdue::Font as FontFace; + +#[cfg(not(feature = "fontdue"))] fn to_vec2(p: ab_glyph::Point) -> Vec2 { Vec2(p.x, p.y) } @@ -71,11 +78,13 @@ impl SpriteDescriptor { ((self.0 & 0x0000_0000_FFFF_0000) >> 16) as u16 } + #[allow(unused)] fn height(self) -> f32 { let height = ((self.0 & 0x00FF_FFFF_0000_0000) >> 32) as u32; f32::conv(height) / SCALE_MULT } + #[allow(unused)] fn fractional_position(self) -> (f32, f32) { let mult = 1.0 / Self::sub_pixel_from_height(self.height()); let x = ((self.0 & 0x0F00_0000_0000_0000) >> 56) as u8; @@ -100,13 +109,14 @@ struct Sprite { } impl atlases::Pipeline { + #[cfg(not(feature = "fontdue"))] fn rasterize( &mut self, sf: ScaledFaceRef, - face: &FontRef<'static>, + face: &FontFace, desc: SpriteDescriptor, - ) -> Option<(Sprite, (u32, u32), (u32, u32), Vec)> { - let id = GlyphId(desc.glyph()); + ) -> Option<(Vec2, (u32, u32), Vec)> { + let id = kas::text::GlyphId(desc.glyph()); let (x, y) = desc.fractional_position(); let glyph_off = Vec2(x.round(), y.round()); @@ -118,40 +128,44 @@ impl atlases::Pipeline { let outline = face.outline_glyph(glyph)?; let bounds = outline.px_bounds(); - let size = to_vec2(bounds.max - bounds.min); let offset = to_vec2(bounds.min) - glyph_off; - let size_u32 = (u32::conv_trunc(size.0), u32::conv_trunc(size.1)); - if size_u32.0 == 0 || size_u32.1 == 0 { + let size = bounds.max - bounds.min; + let size = (u32::conv_trunc(size.x), u32::conv_trunc(size.y)); + if size.0 == 0 || size.1 == 0 { log::warn!("Zero-sized glyph: {}", desc.glyph()); return None; // nothing to draw } - let (atlas, _, origin, tex_quad) = match self.allocate(size_u32) { - Ok(result) => result, - Err(_) => { - log::warn!( - "text_pipe: failed to allocate glyph with size {:?}", - size_u32 - ); - return None; - } - }; - let mut data = Vec::new(); - data.resize(usize::conv(size_u32.0 * size_u32.1), 0u8); + data.resize(usize::conv(size.0 * size.1), 0u8); outline.draw(|x, y, c| { // Convert to u8 with saturating conversion, rounding down: - data[usize::conv((y * size_u32.0) + x)] = (c * 256.0) as u8; + data[usize::conv((y * size.0) + x)] = (c * 256.0) as u8; }); - let sprite = Sprite { - atlas, - size, - offset, - tex_quad, - }; + Some((offset, size, data)) + } + + #[cfg(feature = "fontdue")] + fn rasterize( + &mut self, + sf: ScaledFaceRef, + face: &FontFace, + desc: SpriteDescriptor, + ) -> Option<(Vec2, (u32, u32), Vec)> { + // Ironically fontdue uses DPU internally, but doesn't let us input that. + let px_per_em = sf.dpu().0 * face.units_per_em(); + let (metrics, data) = face.rasterize_indexed(desc.glyph() as usize, px_per_em); + + let size = (u32::conv(metrics.width), u32::conv(metrics.height)); + let h_off = -metrics.ymin - i32::conv(metrics.height); + let offset = Vec2(metrics.xmin.cast(), h_off.cast()); + if size.0 == 0 || size.1 == 0 { + log::warn!("Zero-sized glyph: {}", desc.glyph()); + return None; // nothing to draw + } - Some((sprite, origin, size_u32, data)) + Some((offset, size, data)) } } @@ -171,7 +185,7 @@ unsafe impl bytemuck::Pod for Instance {} /// A pipeline for rendering text pub struct Pipeline { atlas_pipe: atlases::Pipeline, - faces: Vec>, + faces: Vec, glyphs: HashMap>, prepare: Vec<(u32, (u32, u32), (u32, u32), Vec)>, } @@ -224,7 +238,15 @@ impl Pipeline { let face_data = fonts.face_data(); for i in n1..n2 { let (data, index) = face_data.get_data(i); - let face = FontRef::try_from_slice_and_index(data, index).unwrap(); + #[cfg(not(feature = "fontdue"))] + let face = ab_glyph::FontRef::try_from_slice_and_index(data, index).unwrap(); + #[cfg(feature = "fontdue")] + let settings = fontdue::FontSettings { + collection_index: index, + scale: 40.0, // TODO: max expected font size in dpem + }; + #[cfg(feature = "fontdue")] + let face = FontFace::from_bytes(data, settings).unwrap(); self.faces.push(face); } } @@ -291,14 +313,26 @@ impl Pipeline { // rendering could be offloaded. let sf = fonts().get_face(face).scale_by_dpu(dpu); let face = &self.faces[usize::conv(face.0)]; - let result = self.atlas_pipe.rasterize(sf, face, desc); - let sprite = if let Some((sprite, origin, size, data)) = result { - self.prepare.push((sprite.atlas, origin, size, data)); - Some(sprite) + let mut sprite = None; + if let Some((offset, size, data)) = self.atlas_pipe.rasterize(sf, face, desc) { + match self.atlas_pipe.allocate(size) { + Ok((atlas, _, origin, tex_quad)) => { + let s = Sprite { + atlas, + size: Vec2(size.0.cast(), size.1.cast()), + offset, + tex_quad, + }; + + self.prepare.push((s.atlas, origin, size, data)); + sprite = Some(s); + } + Err(_) => { + log::warn!("text_pipe: failed to allocate glyph with size {:?}", size); + } + }; } else { log::warn!("Failed to rasterize glyph: {:?}", glyph); - - None }; entry.insert(sprite.clone()); sprite From 237310d33fac1c5155f61fc63badcde0239a6c21 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 11 Jun 2021 07:56:25 +0100 Subject: [PATCH 3/5] Enable font raster configuration --- example-config/theme.yaml | 4 + kas-theme/src/config.rs | 75 ++++++++++++++++ kas-theme/src/lib.rs | 2 +- kas-theme/src/traits.rs | 3 + kas-wgpu/Cargo.toml | 7 +- kas-wgpu/src/draw/draw_pipe.rs | 3 +- kas-wgpu/src/draw/text_pipe.rs | 153 ++++++++++++++++++++++++--------- kas-wgpu/src/shared.rs | 4 +- 8 files changed, 202 insertions(+), 49 deletions(-) diff --git a/example-config/theme.yaml b/example-config/theme.yaml index f1075e918..dbf9b51dd 100644 --- a/example-config/theme.yaml +++ b/example-config/theme.yaml @@ -54,3 +54,7 @@ fonts: families: - sans-serif weight: 600 +raster: + mode: 0 + subpixel_threshold: 0 + subpixel_steps: 1 diff --git a/kas-theme/src/config.rs b/kas-theme/src/config.rs index 1f0d69ab7..e6475a2e3 100644 --- a/kas-theme/src/config.rs +++ b/kas-theme/src/config.rs @@ -39,6 +39,10 @@ pub struct Config { /// Standard fonts #[cfg_attr(feature = "config", serde(default))] fonts: BTreeMap>, + + /// Text glyph rastering settings + #[cfg_attr(feature = "config", serde(default))] + raster: RasterConfig, } impl Default for Config { @@ -50,6 +54,61 @@ impl Default for Config { color_schemes: defaults::color_schemes(), font_aliases: Default::default(), fonts: defaults::fonts(), + raster: Default::default(), + } + } +} + +/// Font raster settings +/// +/// These are not used by the theme, but passed through to the rendering +/// backend. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "config", derive(serde::Serialize, serde::Deserialize))] +pub struct RasterConfig { + //// Raster mode/engine (backend dependent) + #[cfg_attr(feature = "config", serde(default))] + pub mode: u8, + /// Scale multiplier for fixed-precision + /// + /// This should be an integer `n >= 1`, e.g. `n = 4` provides four sub-pixel + /// steps of precision. It is also required that `n * h < (1 << 24)` where + /// `h` is the text height in pixels. + #[cfg_attr(feature = "config", serde(default = "defaults::scale_steps"))] + pub scale_steps: u8, + /// Subpixel positioning threshold + /// + /// Text with height `h` less than this threshold will use sub-pixel + /// positioning, which should make letter spacing more accurate for small + /// fonts (though exact behaviour depends on the font; it may be worse). + /// This may make rendering worse by breaking pixel alignment. + /// + /// Note: this feature may not be available, depending on the backend and + /// the mode. + /// + /// See also sub-pixel positioning steps. + #[cfg_attr(feature = "config", serde(default = "defaults::subpixel_threshold"))] + pub subpixel_threshold: u8, + /// Subpixel steps + /// + /// The number of sub-pixel positioning steps to use. 1 is the minimum and + /// equivalent to no sub-pixel positioning. 16 is the maximum. + /// + /// Note that since this applies to horizontal and vertical positioning, the + /// maximum number of rastered glyphs is multiplied by the square of this + /// value, though this maxmimum may not be reached in practice. Since this + /// feature is usually only used for small fonts this likely acceptable. + #[cfg_attr(feature = "config", serde(default = "defaults::subpixel_steps"))] + pub subpixel_steps: u8, +} + +impl Default for RasterConfig { + fn default() -> Self { + RasterConfig { + mode: 0, + scale_steps: defaults::scale_steps(), + subpixel_threshold: defaults::subpixel_threshold(), + subpixel_steps: defaults::subpixel_steps(), } } } @@ -152,6 +211,12 @@ impl ThemeConfig for Config { }); } } + + /// Get raster config + #[inline] + fn raster(&self) -> &RasterConfig { + &self.raster + } } #[derive(Clone, Debug, PartialEq)] @@ -185,4 +250,14 @@ mod defaults { ]; list.iter().cloned().collect() } + + pub fn scale_steps() -> u8 { + 4 + } + pub fn subpixel_threshold() -> u8 { + 0 + } + pub fn subpixel_steps() -> u8 { + 5 + } } diff --git a/kas-theme/src/lib.rs b/kas-theme/src/lib.rs index 9c914e63e..5848de55a 100644 --- a/kas-theme/src/lib.rs +++ b/kas-theme/src/lib.rs @@ -32,7 +32,7 @@ mod traits; pub use kas; pub use colors::{Colors, ColorsLinear, ColorsSrgb}; -pub use config::Config; +pub use config::{Config, RasterConfig}; pub use dim::{Dimensions, DimensionsParams, DimensionsWindow}; pub use flat_theme::FlatTheme; #[cfg(feature = "stack_dst")] diff --git a/kas-theme/src/traits.rs b/kas-theme/src/traits.rs index a330871db..312f25af2 100644 --- a/kas-theme/src/traits.rs +++ b/kas-theme/src/traits.rs @@ -27,6 +27,9 @@ pub trait ThemeConfig: /// Apply startup effects fn apply_startup(&self); + + /// Get raster config + fn raster(&self) -> &crate::RasterConfig; } /// A *theme* provides widget sizing and drawing implementations. diff --git a/kas-wgpu/Cargo.toml b/kas-wgpu/Cargo.toml index e7418cc2c..1ed009eaa 100644 --- a/kas-wgpu/Cargo.toml +++ b/kas-wgpu/Cargo.toml @@ -18,7 +18,7 @@ no-default-features = true features = ["stack_dst"] [features] -default = ["clipboard", "stack_dst"] +default = ["ab_glyph", "clipboard", "stack_dst"] nightly = ["unsize", "kas/nightly", "kas-theme/nightly"] # Use Generic Associated Types (this is too unstable to include in nightly!) @@ -34,7 +34,7 @@ stack_dst = ["kas-theme/stack_dst"] unsize = ["kas-theme/unsize"] # Also: -# fontdue: use this for font rasterisation +# ab_glyph, fontdue: enable at least one of these for text rasterisation [dependencies] kas = { path = "..", version = "0.7.0", features = ["config", "winit"] } @@ -44,13 +44,14 @@ futures = "0.3" log = "0.4" smallvec = "1.6.1" wgpu = "0.8.0" -ab_glyph = "0.2.10" +ab_glyph = { version = "0.2.10", optional = true } winit = "0.25" thiserror = "1.0.23" window_clipboard = { version = "0.2.0", optional = true } image = "0.23.14" guillotiere = "0.6.0" fontdue = { version = "0.5.2", optional = true } +cfg-if = "1.0.0" [dev-dependencies] chrono = "0.4" diff --git a/kas-wgpu/src/draw/draw_pipe.rs b/kas-wgpu/src/draw/draw_pipe.rs index d25333939..26859b4de 100644 --- a/kas-wgpu/src/draw/draw_pipe.rs +++ b/kas-wgpu/src/draw/draw_pipe.rs @@ -24,6 +24,7 @@ impl DrawPipe { mut custom: CB, device: &wgpu::Device, shaders: &ShaderManager, + raster_config: &kas_theme::RasterConfig, ) -> Self { // Create staging belt and a local pool let staging_belt = wgpu::util::StagingBelt::new(1024); @@ -60,7 +61,7 @@ impl DrawPipe { let shaded_round = shaded_round::Pipeline::new(device, shaders, &bgl_common); let flat_round = flat_round::Pipeline::new(device, shaders, &bgl_common); let custom = custom.build(&device, &bgl_common, RENDER_TEX_FORMAT); - let text = text_pipe::Pipeline::new(device, shaders, &bgl_common); + let text = text_pipe::Pipeline::new(device, shaders, &bgl_common, raster_config); DrawPipe { local_pool, diff --git a/kas-wgpu/src/draw/text_pipe.rs b/kas-wgpu/src/draw/text_pipe.rs index afc0dee85..c9c641107 100644 --- a/kas-wgpu/src/draw/text_pipe.rs +++ b/kas-wgpu/src/draw/text_pipe.rs @@ -6,34 +6,62 @@ //! Text drawing pipeline use super::{atlases, ShaderManager}; -#[cfg(not(feature = "fontdue"))] -use ab_glyph::Font as _; use kas::cast::*; use kas::draw::{color::Rgba, Pass}; use kas::geom::{Quad, Vec2}; use kas::text::conv::DPU; use kas::text::fonts::{fonts, FaceId, ScaledFaceRef}; use kas::text::{Effect, Glyph, TextDisplay}; +use kas_theme::RasterConfig; use std::collections::hash_map::{Entry, HashMap}; use std::mem::size_of; use std::num::NonZeroU32; -#[cfg(not(feature = "fontdue"))] -type FontFace = ab_glyph::FontRef<'static>; -#[cfg(feature = "fontdue")] -use fontdue::Font as FontFace; +cfg_if::cfg_if! { + if #[cfg(feature = "ab_glyph")] { + type FaceAb = ab_glyph::FontRef<'static>; + } else { + type FaceAb = (); + } +} +#[cfg(feature = "fontdue")] +type FaceFontdue = fontdue::Font; #[cfg(not(feature = "fontdue"))] +type FaceFontdue = (); + +type FontFace = (FaceAb, FaceFontdue); + +#[cfg(feature = "ab_glyph")] fn to_vec2(p: ab_glyph::Point) -> Vec2 { Vec2(p.x, p.y) } -/// Scale multiplier for fixed-precision -/// -/// This should be an integer `n >= 1`, e.g. `n = 4` provides four sub-pixel -/// steps of precision. It is also required that `n * h < (1 << 24)` where -/// `h` is the text height in pixels. -const SCALE_MULT: f32 = 4.0; +struct ConfigCache { + #[allow(unused)] + sb_align: bool, + #[allow(unused)] + fontdue: bool, + scale_steps: f32, + subpixel_threshold: f32, + subpixel_steps: f32, +} + +impl From<&RasterConfig> for ConfigCache { + fn from(c: &RasterConfig) -> Self { + assert!( + c.mode < 3, + "supported raster modes: 0=ab_glyph, 1=ab_glyph with side-bearing alignment, 2=fontdue" + ); + ConfigCache { + sb_align: c.mode == 1, + fontdue: c.mode == 2, + scale_steps: c.scale_steps.cast(), + subpixel_threshold: c.subpixel_threshold.cast(), + subpixel_steps: c.subpixel_steps.cast(), + } + } +} /// A Sprite descriptor /// @@ -46,18 +74,21 @@ impl SpriteDescriptor { /// Choose a sub-pixel precision multiplier based on the height /// /// Must return an integer between 1 and 16. - fn sub_pixel_from_height(height: f32) -> f32 { - // Due to rounding sub-pixel precision is disabled for height > 20 - (30.0 / height).round().clamp(1.0, 16.0) + fn sub_pixel_from_height(config: &ConfigCache, height: f32) -> f32 { + if height < config.subpixel_threshold { + config.subpixel_steps + } else { + 1.0 + } } - fn new(face: FaceId, glyph: Glyph, height: f32) -> Self { + fn new(config: &ConfigCache, face: FaceId, glyph: Glyph, height: f32) -> Self { let face: u16 = face.get().cast(); let glyph_id: u16 = glyph.id.0; - let mult = Self::sub_pixel_from_height(height); + let mult = Self::sub_pixel_from_height(config, height); let mult2 = 0.5 * mult; let steps = u8::conv_nearest(mult); - let height: u32 = (height * SCALE_MULT).cast_nearest(); + let height: u32 = (height * config.scale_steps).cast_nearest(); let x_off = u8::conv_floor(glyph.position.0.fract() * mult + mult2) % steps; let y_off = u8::conv_floor(glyph.position.1.fract() * mult + mult2) % steps; assert!(height & 0xFF00_0000 == 0 && x_off & 0xF0 == 0 && y_off & 0xF0 == 0); @@ -79,14 +110,14 @@ impl SpriteDescriptor { } #[allow(unused)] - fn height(self) -> f32 { + fn height(self, config: &ConfigCache) -> f32 { let height = ((self.0 & 0x00FF_FFFF_0000_0000) >> 32) as u32; - f32::conv(height) / SCALE_MULT + f32::conv(height) / config.scale_steps } #[allow(unused)] - fn fractional_position(self) -> (f32, f32) { - let mult = 1.0 / Self::sub_pixel_from_height(self.height()); + fn fractional_position(self, config: &ConfigCache) -> (f32, f32) { + let mult = 1.0 / Self::sub_pixel_from_height(config, self.height(config)); let x = ((self.0 & 0x0F00_0000_0000_0000) >> 56) as u8; let y = ((self.0 & 0xF000_0000_0000_0000) >> 60) as u8; let x = f32::conv(x) * mult; @@ -109,20 +140,26 @@ struct Sprite { } impl atlases::Pipeline { - #[cfg(not(feature = "fontdue"))] - fn rasterize( + #[cfg(feature = "ab_glyph")] + fn raster_ab( &mut self, + config: &ConfigCache, sf: ScaledFaceRef, - face: &FontFace, + face: &FaceAb, desc: SpriteDescriptor, ) -> Option<(Vec2, (u32, u32), Vec)> { + use ab_glyph::Font; + let id = kas::text::GlyphId(desc.glyph()); - let (x, y) = desc.fractional_position(); + let (mut x, y) = desc.fractional_position(config); let glyph_off = Vec2(x.round(), y.round()); + if config.sb_align && desc.height(config) >= config.subpixel_threshold { + x -= sf.h_side_bearing(id); + } let glyph = ab_glyph::Glyph { id: ab_glyph::GlyphId(id.0), - scale: desc.height().into(), + scale: desc.height(config).into(), position: ab_glyph::point(x, y), }; let outline = face.outline_glyph(glyph)?; @@ -147,10 +184,10 @@ impl atlases::Pipeline { } #[cfg(feature = "fontdue")] - fn rasterize( + fn raster_fontdue( &mut self, sf: ScaledFaceRef, - face: &FontFace, + face: &FaceFontdue, desc: SpriteDescriptor, ) -> Option<(Vec2, (u32, u32), Vec)> { // Ironically fontdue uses DPU internally, but doesn't let us input that. @@ -184,6 +221,7 @@ unsafe impl bytemuck::Pod for Instance {} /// A pipeline for rendering text pub struct Pipeline { + config: ConfigCache, atlas_pipe: atlases::Pipeline, faces: Vec, glyphs: HashMap>, @@ -195,6 +233,7 @@ impl Pipeline { device: &wgpu::Device, shaders: &ShaderManager, bgl_common: &wgpu::BindGroupLayout, + config: &RasterConfig, ) -> Self { let atlas_pipe = atlases::Pipeline::new( device, @@ -219,6 +258,7 @@ impl Pipeline { &shaders.frag_glyph, ); Pipeline { + config: config.into(), atlas_pipe, faces: Default::default(), glyphs: Default::default(), @@ -238,16 +278,27 @@ impl Pipeline { let face_data = fonts.face_data(); for i in n1..n2 { let (data, index) = face_data.get_data(i); - #[cfg(not(feature = "fontdue"))] - let face = ab_glyph::FontRef::try_from_slice_and_index(data, index).unwrap(); - #[cfg(feature = "fontdue")] - let settings = fontdue::FontSettings { - collection_index: index, - scale: 40.0, // TODO: max expected font size in dpem - }; - #[cfg(feature = "fontdue")] - let face = FontFace::from_bytes(data, settings).unwrap(); - self.faces.push(face); + + cfg_if::cfg_if! { + if #[cfg(feature = "ab_glyph")] { + let face_ab = ab_glyph::FontRef::try_from_slice_and_index(data, index).unwrap(); + } else { + let face_ab = (); + } + } + cfg_if::cfg_if! { + if #[cfg(feature = "fontdue")] { + let settings = fontdue::FontSettings { + collection_index: index, + scale: 40.0, // TODO: max expected font size in dpem + }; + let face_fontdue = FaceFontdue::from_bytes(data, settings).unwrap(); + } else { + let face_fontdue = (); + } + } + + self.faces.push((face_ab, face_fontdue)); } } } @@ -305,7 +356,7 @@ impl Pipeline { /// This returns `None` if there's nothing to render. It may also return /// `None` (with a warning) on error. fn get_glyph(&mut self, face: FaceId, dpu: DPU, height: f32, glyph: Glyph) -> Option { - let desc = SpriteDescriptor::new(face, glyph, height); + let desc = SpriteDescriptor::new(&self.config, face, glyph, height); match self.glyphs.entry(desc) { Entry::Occupied(entry) => entry.get().clone(), Entry::Vacant(entry) => { @@ -314,7 +365,24 @@ impl Pipeline { let sf = fonts().get_face(face).scale_by_dpu(dpu); let face = &self.faces[usize::conv(face.0)]; let mut sprite = None; - if let Some((offset, size, data)) = self.atlas_pipe.rasterize(sf, face, desc) { + + cfg_if::cfg_if! { + if #[cfg(all(not(feature = "fontdue"), not(feature = "ab_glyph")))] { + std::compile_error!("require at least one of these features: ab_glyph, fontdue"); + } else if #[cfg(all(feature = "fontdue", feature = "ab_glyph"))] { + let result = if self.config.fontdue { + self.atlas_pipe.raster_fontdue(sf, &face.1, desc) + } else { + self.atlas_pipe.raster_ab(&self.config, sf, &face.0, desc) + }; + } else if #[cfg(feature = "ab_glyph")] { + let result = self.atlas_pipe.raster_ab(&self.config, sf, &face.0, desc); + } else { + let result = self.atlas_pipe.raster_fontdue(sf, &face.1, desc); + } + } + + if let Some((offset, size, data)) = result { match self.atlas_pipe.allocate(size) { Ok((atlas, _, origin, tex_quad)) => { let s = Sprite { @@ -332,7 +400,8 @@ impl Pipeline { } }; } else { - log::warn!("Failed to rasterize glyph: {:?}", glyph); + // This comes up a lot and is usually harmless + log::debug!("Failed to rasterize glyph: {:?}", glyph); }; entry.insert(sprite.clone()); sprite diff --git a/kas-wgpu/src/shared.rs b/kas-wgpu/src/shared.rs index 8dc7efe11..9ed0fb7aa 100644 --- a/kas-wgpu/src/shared.rs +++ b/kas-wgpu/src/shared.rs @@ -16,7 +16,7 @@ use crate::{warn_about_error, Error, Options, WindowId}; use kas::event::UpdateHandle; use kas::updatable::Updatable; use kas::TkAction; -use kas_theme::Theme; +use kas_theme::{Theme, ThemeConfig}; #[cfg(feature = "clipboard")] use window_clipboard::Clipboard; @@ -67,7 +67,7 @@ where let (device, queue) = futures::executor::block_on(req)?; let shaders = ShaderManager::new(&device); - let mut draw = DrawPipe::new(custom, &device, &shaders); + let mut draw = DrawPipe::new(custom, &device, &shaders, theme.config().raster()); theme.init(&mut draw); From 2e988eb835c7710c251be9c03548e3b7b0c1e3f4 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 11 Jun 2021 07:59:38 +0100 Subject: [PATCH 4/5] Update kas-text version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6c01d91e7..f17f536e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,4 +89,4 @@ features = ["serde"] members = ["kas-macros", "kas-theme", "kas-wgpu"] [patch.crates-io] -kas-text = { git = "https://github.com/kas-gui/kas-text.git", rev = "448d07fc3290f011440d83af6d97901ebce9c72e" } +kas-text = { git = "https://github.com/kas-gui/kas-text.git", rev = "8e85e49df78f7908662d1356cacdf0b796274530" } From 12841128738eae2eacd2e1a6bacb75676738a108 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 11 Jun 2021 08:07:49 +0100 Subject: [PATCH 5/5] Fix --- .github/workflows/test.yml | 4 ++-- kas-theme/src/traits.rs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 81869a046..13c6be4f2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: run: cargo test --manifest-path kas-theme/Cargo.toml --all-features - name: test (kas-wgpu) run: | - cargo test --manifest-path kas-wgpu/Cargo.toml --no-default-features + cargo test --manifest-path kas-wgpu/Cargo.toml --no-default-features --features ab_glyph cargo test --manifest-path kas-wgpu/Cargo.toml --all-features test: @@ -74,4 +74,4 @@ jobs: - name: test (kas-theme) run: cargo test --manifest-path kas-theme/Cargo.toml - name: test (kas-wgpu) - run: cargo test --manifest-path kas-wgpu/Cargo.toml --features clipboard + run: cargo test --manifest-path kas-wgpu/Cargo.toml diff --git a/kas-theme/src/traits.rs b/kas-theme/src/traits.rs index 312f25af2..843b2ab7e 100644 --- a/kas-theme/src/traits.rs +++ b/kas-theme/src/traits.rs @@ -15,6 +15,9 @@ use std::ops::{Deref, DerefMut}; pub trait ThemeConfig: Clone + std::fmt::Debug + 'static { /// Apply startup effects fn apply_startup(&self); + + /// Get raster config + fn raster(&self) -> &crate::RasterConfig; } /// Requirements on theme config (with `config` feature)