Add caching of swash rendering, significantly improves layout speed

This commit is contained in:
Jeremy Soller 2022-10-12 13:42:30 -06:00
parent 0b5050e601
commit a4959bbe7b
No known key found for this signature in database
GPG key ID: 87F211AF2BE4C2FE
5 changed files with 243 additions and 92 deletions

View file

@ -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<swash::scale::image::Image>;
#[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));
}

View file

@ -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<ab_glyph::OutlinedGlyph>,
#[cfg(feature = "rusttype")]
pub inner: rusttype::PositionedGlyph<'a>,
#[cfg(feature = "swash")]
pub inner: (i32, i32, Option<swash::scale::image::Image>),
pub phantom: PhantomData<&'a T>,
pub inner: (i32, i32, CacheKey),
}
pub struct FontLayoutLine<'a> {
pub line_i: FontLineIndex,
pub glyphs: Vec<FontLayoutGlyph<'a, ()>>,
pub glyphs: Vec<FontLayoutGlyph<'a>>,
}
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");
}
}
}

View file

@ -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,
});
}

View file

@ -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<swash::scale::ScaleContext>,
pub cache: Mutex<HashMap<CacheKey, CacheItem>>,
}
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()),
})
}
}

View file

@ -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<ScaleContext> = 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,
}
}
}