From a4959bbe7b871ddca4da3437fdae19e82837f9f4 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 12 Oct 2022 13:42:30 -0600 Subject: [PATCH] Add caching of swash rendering, significantly improves layout speed --- examples/text/src/font/cache.rs | 136 ++++++++++++++++++++++++++++++ examples/text/src/font/layout.rs | 112 ++++++++++++++++-------- examples/text/src/font/matches.rs | 5 +- examples/text/src/font/mod.rs | 20 +++++ examples/text/src/font/shape.rs | 62 +++----------- 5 files changed, 243 insertions(+), 92 deletions(-) create mode 100644 examples/text/src/font/cache.rs diff --git a/examples/text/src/font/cache.rs b/examples/text/src/font/cache.rs new file mode 100644 index 00000000..505e48ea --- /dev/null +++ b/examples/text/src/font/cache.rs @@ -0,0 +1,136 @@ +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct CacheKey { + pub x_bin: SubpixelBin, + pub y_bin: SubpixelBin, + pub font_size: i32, + pub glyph_id: u16, +} + +impl CacheKey { + pub fn new(pos: (f32, f32), font_size: i32, glyph_id: u16) -> (i32, i32, Self) { + let (x, x_bin) = SubpixelBin::new(pos.0); + let (y, y_bin) = SubpixelBin::new(pos.1); + ( + x, + y, + Self { + x_bin, + y_bin, + font_size, + glyph_id, + } + ) + } +} + +#[cfg(not(feature = "swash"))] +pub type CacheItem = (); + +#[cfg(feature = "swash")] +pub type CacheItem = Option; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum SubpixelBin { + Zero, + One, + Two, + Three, +} + +impl SubpixelBin { + pub fn new(pos: f32) -> (i32, Self) { + let trunc = pos.trunc() as i32; + let fract = pos.fract(); + if pos.is_sign_negative() { + if fract > -0.125 { + (trunc, Self::Zero) + } else if fract > -0.375 { + (trunc - 1, Self::Three) + } else if fract > -0.625 { + (trunc - 1, Self::Two) + } else if fract > -0.875 { + (trunc - 1, Self::One) + } else { + (trunc - 1, Self::Zero) + } + } else { + if fract < 0.125 { + (trunc, Self::Zero) + } else if fract < 0.375 { + (trunc, Self::One) + } else if fract < 0.625 { + (trunc, Self::Two) + } else if fract < 0.875 { + (trunc, Self::Three) + } else { + (trunc + 1, Self::Zero) + } + } + } + + pub fn as_float(&self) -> f32 { + match self { + Self::Zero => 0.0, + Self::One => 0.25, + Self::Two => 0.5, + Self::Three => 0.75, + } + } +} + +#[test] +fn test_subpixel_bins() { + // POSITIVE TESTS + + // Maps to 0.0 + assert_eq!(SubpixelBin::new(0.0), (0, SubpixelBin::Zero)); + assert_eq!(SubpixelBin::new(0.124), (0, SubpixelBin::Zero)); + + // Maps to 0.25 + assert_eq!(SubpixelBin::new(0.125), (0, SubpixelBin::One)); + assert_eq!(SubpixelBin::new(0.25), (0, SubpixelBin::One)); + assert_eq!(SubpixelBin::new(0.374), (0, SubpixelBin::One)); + + // Maps to 0.5 + assert_eq!(SubpixelBin::new(0.375), (0, SubpixelBin::Two)); + assert_eq!(SubpixelBin::new(0.5), (0, SubpixelBin::Two)); + assert_eq!(SubpixelBin::new(0.624), (0, SubpixelBin::Two)); + + // Maps to 0.75 + assert_eq!(SubpixelBin::new(0.625), (0, SubpixelBin::Three)); + assert_eq!(SubpixelBin::new(0.75), (0, SubpixelBin::Three)); + assert_eq!(SubpixelBin::new(0.874), (0, SubpixelBin::Three)); + + // Maps to 1.0 + assert_eq!(SubpixelBin::new(0.875), (1, SubpixelBin::Zero)); + assert_eq!(SubpixelBin::new(0.999), (1, SubpixelBin::Zero)); + assert_eq!(SubpixelBin::new(1.0), (1, SubpixelBin::Zero)); + assert_eq!(SubpixelBin::new(1.124), (1, SubpixelBin::Zero)); + + // NEGATIVE TESTS + + // Maps to 0.0 + assert_eq!(SubpixelBin::new(-0.0), (0, SubpixelBin::Zero)); + assert_eq!(SubpixelBin::new(-0.124), (0, SubpixelBin::Zero)); + + // Maps to 0.25 + assert_eq!(SubpixelBin::new(-0.125), (-1, SubpixelBin::Three)); + assert_eq!(SubpixelBin::new(-0.25), (-1, SubpixelBin::Three)); + assert_eq!(SubpixelBin::new(-0.374), (-1, SubpixelBin::Three)); + + // Maps to 0.5 + assert_eq!(SubpixelBin::new(-0.375), (-1, SubpixelBin::Two)); + assert_eq!(SubpixelBin::new(-0.5), (-1, SubpixelBin::Two)); + assert_eq!(SubpixelBin::new(-0.624), (-1, SubpixelBin::Two)); + + // Maps to 0.75 + assert_eq!(SubpixelBin::new(-0.625), (-1, SubpixelBin::One)); + assert_eq!(SubpixelBin::new(-0.75), (-1, SubpixelBin::One)); + assert_eq!(SubpixelBin::new(-0.874), (-1, SubpixelBin::One)); + + // Maps to 1.0 + assert_eq!(SubpixelBin::new(-0.875), (-1, SubpixelBin::Zero)); + assert_eq!(SubpixelBin::new(-0.999), (-1, SubpixelBin::Zero)); + assert_eq!(SubpixelBin::new(-1.0), (-1, SubpixelBin::Zero)); + assert_eq!(SubpixelBin::new(-1.124), (-1, SubpixelBin::Zero)); +} diff --git a/examples/text/src/font/layout.rs b/examples/text/src/font/layout.rs index 5c86cd06..231a30b7 100644 --- a/examples/text/src/font/layout.rs +++ b/examples/text/src/font/layout.rs @@ -1,24 +1,22 @@ -use core::marker::PhantomData; +use super::{CacheKey, Font, FontLineIndex}; -use super::FontLineIndex; - -pub struct FontLayoutGlyph<'a, T: 'a> { +pub struct FontLayoutGlyph<'a> { pub start: usize, pub end: usize, pub x: f32, pub w: f32, + pub font: &'a Font<'a>, #[cfg(feature = "ab_glyph")] pub inner: Option, #[cfg(feature = "rusttype")] pub inner: rusttype::PositionedGlyph<'a>, #[cfg(feature = "swash")] - pub inner: (i32, i32, Option), - pub phantom: PhantomData<&'a T>, + pub inner: (i32, i32, CacheKey), } pub struct FontLayoutLine<'a> { pub line_i: FontLineIndex, - pub glyphs: Vec>, + pub glyphs: Vec>, } impl<'a> FontLayoutLine<'a> { @@ -52,41 +50,81 @@ impl<'a> FontLayoutLine<'a> { } #[cfg(feature = "swash")] - if let Some(ref image) = glyph.inner.2 { - use swash::scale::image::Content; + { + use swash::scale::{Render, Source, StrikeWith}; + use swash::zeno::{Format, Vector}; - let x = glyph.inner.0 + image.placement.left; - let y = glyph.inner.1 - image.placement.top; + let mut cache = glyph.font.cache.lock().unwrap(); - match image.content { - Content::Mask => { - let mut i = 0; - for off_y in 0..image.placement.height as i32 { - for off_x in 0..image.placement.width as i32 { - let color = - (image.data[i] as u32) << 24 | - base & 0xFFFFFF; - f(x + off_x, y + off_y, color); - i += 1; + let (x_int, y_int, cache_key) = glyph.inner; + + let image_opt = cache.entry(cache_key).or_insert_with(|| { + let mut scale_context = glyph.font.scale_context.lock().unwrap(); + + // Build the scaler + let mut scaler = scale_context + .builder(glyph.font.swash) + .size(cache_key.font_size as f32) + .hint(true) + .build(); + + // Compute the fractional offset-- you'll likely want to quantize this + // in a real renderer + let offset = Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float()); + + // Select our source order + Render::new(&[ + // Color outline with the first palette + Source::ColorOutline(0), + // Color bitmap with best fit selection mode + Source::ColorBitmap(StrikeWith::BestFit), + // Standard scalable outline + Source::Outline, + ]) + // Select a subpixel format + .format(Format::Alpha) + // Apply the fractional offset + .offset(offset) + // Render the image + .render(&mut scaler, cache_key.glyph_id) + }); + + if let Some(ref image) = image_opt { + use swash::scale::image::Content; + + let x = x_int + image.placement.left; + let y = y_int - image.placement.top; + + match image.content { + Content::Mask => { + let mut i = 0; + for off_y in 0..image.placement.height as i32 { + for off_x in 0..image.placement.width as i32 { + let color = + (image.data[i] as u32) << 24 | + base & 0xFFFFFF; + f(x + off_x, y + off_y, color); + i += 1; + } } - } - }, - Content::Color => { - let mut i = 0; - for off_y in 0..image.placement.height as i32 { - for off_x in 0..image.placement.width as i32 { - let color = - (image.data[i + 3] as u32) << 24 | - (image.data[i] as u32) << 16 | - (image.data[i + 1] as u32) << 8 | - (image.data[i + 2] as u32); - f(x + off_x, y + off_y, color); - i += 4; + }, + Content::Color => { + let mut i = 0; + for off_y in 0..image.placement.height as i32 { + for off_x in 0..image.placement.width as i32 { + let color = + (image.data[i + 3] as u32) << 24 | + (image.data[i] as u32) << 16 | + (image.data[i + 1] as u32) << 8 | + (image.data[i + 2] as u32); + f(x + off_x, y + off_y, color); + i += 4; + } } + }, + Content::SubpixelMask => { + println!("TODO: SubpixelMask"); } - }, - Content::SubpixelMask => { - println!("TODO: SubpixelMask"); } } } diff --git a/examples/text/src/font/matches.rs b/examples/text/src/font/matches.rs index 3cf88eaf..3a4b385e 100644 --- a/examples/text/src/font/matches.rs +++ b/examples/text/src/font/matches.rs @@ -68,10 +68,7 @@ impl<'a> FontMatches<'a> { y_advance, x_offset, y_offset, - #[cfg(feature = "ab_glyph")] - font: &self.fonts[font_i].ab_glyph, - #[cfg(feature = "swash")] - font: &self.fonts[font_i].swash, + font: &self.fonts[font_i], inner, }); } diff --git a/examples/text/src/font/mod.rs b/examples/text/src/font/mod.rs index 61bf8468..846a0001 100644 --- a/examples/text/src/font/mod.rs +++ b/examples/text/src/font/mod.rs @@ -1,3 +1,11 @@ +use std::{ + collections::HashMap, + sync::Mutex, +}; + +pub use self::cache::*; +mod cache; + pub use self::layout::*; mod layout; @@ -10,6 +18,12 @@ mod shape; pub use self::system::*; mod system; +#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct FontCacheKey { + glyph_id: u16, + +} + #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] pub struct FontLineIndex(usize); @@ -32,6 +46,9 @@ pub struct Font<'a> { pub rusttype: rusttype::Font<'a>, #[cfg(feature = "swash")] pub swash: swash::FontRef<'a>, + #[cfg(feature = "swash")] + pub scale_context: Mutex, + pub cache: Mutex>, } impl<'a> Font<'a> { @@ -45,6 +62,9 @@ impl<'a> Font<'a> { rusttype: rusttype::Font::try_from_bytes_and_index(data, index)?, #[cfg(feature = "swash")] swash: swash::FontRef::from_index(data, index as usize)?, + #[cfg(feature = "swash")] + scale_context: Mutex::new(swash::scale::ScaleContext::new()), + cache: Mutex::new(HashMap::new()), }) } } diff --git a/examples/text/src/font/shape.rs b/examples/text/src/font/shape.rs index dccd9a87..0f66865d 100644 --- a/examples/text/src/font/shape.rs +++ b/examples/text/src/font/shape.rs @@ -1,8 +1,7 @@ #[cfg(feature = "ab_glyph")] -use ab_glyph::Font; -use core::marker::PhantomData; +use ab_glyph::Font as _; -use super::{FontLayoutGlyph, FontLayoutLine, FontLineIndex}; +use super::{CacheKey, Font, FontLayoutGlyph, FontLayoutLine, FontLineIndex}; pub struct FontShapeGlyph<'a> { pub start: usize, @@ -11,26 +10,23 @@ pub struct FontShapeGlyph<'a> { pub y_advance: f32, pub x_offset: f32, pub y_offset: f32, - #[cfg(feature = "ab_glyph")] - pub font: &'a ab_glyph::FontRef<'a>, + pub font: &'a Font<'a>, #[cfg(feature = "ab_glyph")] pub inner: ab_glyph::GlyphId, #[cfg(feature = "rusttype")] pub inner: rusttype::Glyph<'a>, #[cfg(feature = "swash")] - pub font: &'a swash::FontRef<'a>, - #[cfg(feature = "swash")] pub inner: swash::GlyphId, } impl<'a> FontShapeGlyph<'a> { - fn layout(&self, font_size: i32, x: f32, y: f32) -> FontLayoutGlyph<'a, ()> { + fn layout(&self, font_size: i32, x: f32, y: f32) -> FontLayoutGlyph<'a> { let x_offset = font_size as f32 * self.x_offset; let y_offset = font_size as f32 * self.y_offset; let x_advance = font_size as f32 * self.x_advance; #[cfg(feature = "ab_glyph")] - let inner = self.font.outline_glyph( + let inner = self.font.ab_glyph.outline_glyph( self.inner.with_scale_and_position( font_size as f32, ab_glyph::point( @@ -49,55 +45,19 @@ impl<'a> FontShapeGlyph<'a> { )); #[cfg(feature = "swash")] - let inner = { - use swash::scale::{Render, ScaleContext, Source, StrikeWith}; - use swash::zeno::{Format, Vector}; - - //TODO: store somewhere else - static mut CONTEXT: Option = None; - - unsafe { - if CONTEXT.is_none() { - CONTEXT = Some(ScaleContext::new()); - } - } - - // Build the scaler - let mut scaler = unsafe { CONTEXT.as_mut().unwrap() } - .builder(*self.font) - .size(font_size as f32) - .hint(true) - .build(); - - // Compute the fractional offset-- you'll likely want to quantize this - // in a real renderer - let offset = Vector::new((x + x_offset).fract(), (y - y_offset).fract()); - - // Select our source order - let image_opt = Render::new(&[ - // Color outline with the first palette - Source::ColorOutline(0), - // Color bitmap with best fit selection mode - Source::ColorBitmap(StrikeWith::BestFit), - // Standard scalable outline - Source::Outline, - ]) - // Select a subpixel format - .format(Format::Alpha) - // Apply the fractional offset - .offset(offset) - // Render the image - .render(&mut scaler, self.inner); - ((x + x_offset).trunc() as i32, (y - y_offset).trunc() as i32, image_opt) - }; + let inner = CacheKey::new( + (x + x_offset, y - y_offset), + font_size, + self.inner + ); FontLayoutGlyph { start: self.start, end: self.end, x: x, w: x_advance, + font: self.font, inner, - phantom: PhantomData, } } }