From: Joshua Simmons Date: Fri, 3 Mar 2023 22:28:03 +0000 (+0100) Subject: Improve font cache X-Git-Url: https://git.nega.tv//gitweb.cgi?a=commitdiff_plain;h=35fe7e6d6385722201324bcc07cf3805234ae0aa;p=josh%2Fnarcissus Improve font cache Don't do so much work on kerning. Clean and simplify up cache API. Improve comments a little. --- diff --git a/bins/narcissus/src/main.rs b/bins/narcissus/src/main.rs index 7485aef..3cfc546 100644 --- a/bins/narcissus/src/main.rs +++ b/bins/narcissus/src/main.rs @@ -6,9 +6,9 @@ use crate::{ }; use helpers::{create_buffer_with_data, create_image_with_data, load_image, load_obj}; use mapped_buffer::MappedBuffer; -use narcissus_app::{create_app, Event, Key, WindowDesc}; -use narcissus_core::{default, rand::Pcg64}; -use narcissus_font::{FontCollection, GlyphCache, TouchedGlyph, TouchedGlyphInfo}; +use narcissus_app::{create_app, Event, Key, PressedState, WindowDesc}; +use narcissus_core::{default, rand::Pcg64, slice::array_windows}; +use narcissus_font::{FontCollection, GlyphCache, HorizontalMetrics, TouchedGlyph}; use narcissus_gpu::{ create_device, Access, BufferImageCopy, BufferUsageFlags, ClearValue, Extent2d, Extent3d, ImageAspectFlags, ImageBarrier, ImageDesc, ImageDimension, ImageFormat, ImageLayout, @@ -26,12 +26,12 @@ mod pipelines; const MAX_SHARKS: usize = 262_144; const NUM_SHARKS: usize = 50; -const GLYPH_CACHE_WIDTH: usize = 1024; -const GLYPH_CACHE_HEIGHT: usize = 512; +const GLYPH_CACHE_SIZE: usize = 1024; const MAX_GLYPH_INSTANCES: usize = 262_144; const MAX_GLYPHS: usize = 8192; -/// Marker trait indicates it's safe to convert a given type directly to an array of bytes. +/// Marker trait indicates it's safe to convert a given type directly to an +/// array of bytes. /// /// # Safety /// @@ -58,7 +58,7 @@ pub fn main() { let text_pipeline = TextPipeline::new(device.as_ref()); let fonts = Fonts::new(); - let mut glyph_cache = GlyphCache::new(&fonts, GLYPH_CACHE_WIDTH, GLYPH_CACHE_HEIGHT, 1); + let mut glyph_cache = GlyphCache::new(&fonts, GLYPH_CACHE_SIZE, GLYPH_CACHE_SIZE, 1); let blåhaj_image = load_image("bins/narcissus/data/blåhaj.png"); let (blåhaj_vertices, blåhaj_indices) = load_obj("bins/narcissus/data/blåhaj.obj"); @@ -163,6 +163,11 @@ pub fn main() { } } + let mut glyph_instances = Vec::new(); + + let mut align_v = false; + let mut kerning = true; + let start_time = Instant::now(); 'main: loop { let frame = device.begin_frame(); @@ -173,12 +178,20 @@ pub fn main() { KeyPress { window_id: _, key, - pressed: _, + pressed, modifiers: _, } => { if key == Key::Escape { break 'main; } + if key == Key::Space && pressed == PressedState::Pressed { + align_v = !align_v; + println!("align: {align_v}"); + } + if key == Key::K && pressed == PressedState::Pressed { + kerning = !kerning; + println!("kerning: {kerning}"); + } } Quit => { break 'main; @@ -261,53 +274,89 @@ pub fn main() { basic_uniform_buffer.write(BasicUniforms { clip_from_model }); // Do some Font Shit.' - let line0 = "Snarfe, Blåhaj! And the Quick Brown Fox jumped Over the Lazy doge. ½½½½ Snarfe, Blåhaj! And the Quick Brown Fox jumped Over the Lazy doge. ½½½½ Snarfe, Blåhaj! And the Quick Brown Fox jumped Over the Lazy doge. ½½½½"; - let line1 = "加盟国は、国際連合と協力して 加盟国は、国際連合と協力して 加盟国は、国際連合と協力して 加盟国は、国際連合と協力して 加盟国は、国際連合と協力して 加盟国は、国際連合と協力して"; - - let mut glyph_instances = Vec::new(); + let line0 = "Snarfe, Blåhaj! And the Quick Brown Fox jumped Over the Lazy doge."; + let line1 = "加盟国は、国際連合と協力して"; let mut x; let mut y = 0.0; let mut rng = Pcg64::new(); - for line in 0..34 { + glyph_instances.clear(); + + let mut line_glyph_indices = Vec::new(); + let mut line_kern_advances = Vec::new(); + + for line in 0.. { let (font_family, font_size_px, text) = if line & 1 == 0 { - (FontFamily::RobotoRegular, 10.0, line0) + (FontFamily::RobotoRegular, 14.0, line0) } else { - (FontFamily::NotoSansJapanese, 20.0, line1) + (FontFamily::NotoSansJapanese, 14.0, line1) }; - let font_size_px = font_size_px + (line / 2) as f32 * 2.0; - let font = fonts.font(font_family); let scale = font.scale_for_size_px(font_size_px); x = 0.0; y += (font.ascent() - font.descent() + font.line_gap()) * scale; - y = y.trunc(); - - let mut prev_glyph_index = None; - for c in text.chars() { - let TouchedGlyphInfo { - touched_glyph_index, - glyph_index, - advance_width, - } = glyph_cache.touch_glyph(font_family, c, font_size_px); - - if let Some(prev_glyph_index) = prev_glyph_index.replace(glyph_index) { - x += font.kerning_advance(prev_glyph_index, glyph_index) * scale; - } + if align_v { + y = y.trunc(); + } + + if y > height as f32 { + break; + } - const COLOR_SERIES: &[u32; 4] = &[0xfffac228, 0xfff57d15, 0xffd44842, 0xff9f2a63]; - glyph_instances.push(GlyphInstance { - x, - y, - touched_glyph_index, - color: *rng.select(COLOR_SERIES).unwrap(), - }); + let font_size_str = format!("{font_size_px}: "); - x += advance_width * scale; + line_glyph_indices.clear(); + line_glyph_indices.extend(font_size_str.chars().chain(text.chars()).map(|c| { + font.glyph_index(c) + .unwrap_or_else(|| font.glyph_index('□').unwrap()) + })); + + line_kern_advances.clear(); + line_kern_advances.push(0.0); + line_kern_advances.extend( + array_windows(line_glyph_indices.as_slice()) + .map(|&[prev_index, next_index]| font.kerning_advance(prev_index, next_index)), + ); + + 'repeat_str: for _ in 0.. { + for (glyph_index, advance) in line_glyph_indices + .iter() + .copied() + .zip(line_kern_advances.iter().copied()) + { + if x >= width as f32 { + break 'repeat_str; + } + + let touched_glyph_index = + glyph_cache.touch_glyph(font_family, glyph_index, font_size_px); + + let HorizontalMetrics { + advance_width, + left_side_bearing: _, + } = font.horizontal_metrics(glyph_index); + + if kerning { + x += advance * scale; + } + + let color = *rng + .select(&[0xfffac228, 0xfff57d15, 0xffd44842, 0xff9f2a63]) + .unwrap(); + + glyph_instances.push(GlyphInstance { + x, + y, + touched_glyph_index, + color, + }); + + x += advance_width * scale; + } } } @@ -391,9 +440,7 @@ pub fn main() { height, color_attachments: &[RenderingAttachment { image: swapchain_image, - load_op: LoadOp::Clear(ClearValue::ColorF32([ - 0.392157, 0.584314, 0.929412, 1.0, - ])), + load_op: LoadOp::Clear(ClearValue::ColorF32([1.0, 1.0, 1.0, 1.0])), store_op: StoreOp::Store, }], depth_attachment: Some(RenderingAttachment { diff --git a/libs/narcissus-font/src/cache.rs b/libs/narcissus-font/src/cache.rs index e11a4e1..4347f0c 100644 --- a/libs/narcissus-font/src/cache.rs +++ b/libs/narcissus-font/src/cache.rs @@ -1,21 +1,17 @@ use std::collections::hash_map::Entry; -use crate::{ - font::{GlyphBitmapBox, HorizontalMetrics}, - FontCollection, GlyphIndex, Oversample, Packer, -}; +use crate::{font::GlyphBitmapBox, FontCollection, GlyphIndex, Oversample, Packer}; use narcissus_core::default; +pub use narcissus_core::FiniteF32; use rustc_hash::FxHashMap; use stb_truetype_sys::rectpack::Rect; -pub use narcissus_core::FiniteF32; - /// A key that uniquely identifies a given glyph within the glyph cache. #[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] struct GlyphKey { - family: Family, - c: char, + glyph_index: GlyphIndex, size_px: FiniteF32, + family: Family, } /// An index into the CachedGlyph slice. @@ -40,17 +36,8 @@ pub struct TouchedGlyph { pub offset_y1: f32, } -/// Information stored for each touched glyph to avoid re-computing for each instance of that glyph. -#[derive(Clone, Copy)] -pub struct TouchedGlyphInfo { - pub touched_glyph_index: TouchedGlyphIndex, - pub glyph_index: GlyphIndex, - pub advance_width: f32, -} - struct CachedGlyph { glyph_key: GlyphKey, - glyph_index: GlyphIndex, x0: i32, y0: i32, offset_x0: f32, @@ -71,7 +58,7 @@ where texture: Box<[u8]>, next_touched_glyph_index: u32, - touched_glyph_lookup: FxHashMap, TouchedGlyphInfo>, + touched_glyph_lookup: FxHashMap, TouchedGlyphIndex>, touched_glyphs: Vec, cached_glyph_indices_sorted: Vec, @@ -116,24 +103,32 @@ where /// Calculate oversampling factor for a given font size in pixels. /// - /// Oversampling renders the glyphs pre-filtered at a higher resolution, so rendering can use bilinear filtering to - /// avoid blurriness on small fonts that aren't placed at pixel boundaries. Since it scales the size of the rendered - /// glyph by some fixed multipler, it's very costly in terms of atlas space for larger fonts. At the same time, - /// larger fonts don't benefit significantly from the filtering. + /// Oversampling renders the glyphs pre-filtered so rendering can use + /// bilinear filtering to avoid blurriness when glyphs are not placed on + /// exact pixel boundaries. Since it scales the size of the rendered + /// glyph by a fixed multipler, it can be very costly in terms of atlas + /// space for larger font sizes. Additionally the positive impact of + /// oversampling is less pronounced at large font sizes. /// - /// This function chooses an arbitrary threshold above which to disable oversampling to avoid wasting atlas space. + /// This function chooses an arbitrary threshold above which to disable + /// oversampling in an attempt to balance atlas space usage and quality. fn oversample_for_size(size_px: f32) -> Oversample { if size_px <= 25.0 { - Oversample::X2 + Oversample::X4 } else { - Oversample::None + Oversample::X2 } } - pub fn touch_glyph(&mut self, family: F::Family, c: char, size_px: f32) -> TouchedGlyphInfo { + pub fn touch_glyph( + &mut self, + family: F::Family, + glyph_index: GlyphIndex, + size_px: f32, + ) -> TouchedGlyphIndex { let key = GlyphKey { family, - c, + glyph_index, size_px: FiniteF32::new(size_px).unwrap(), }; @@ -142,28 +137,14 @@ where Entry::Vacant(entry) => { let touched_glyph_index = TouchedGlyphIndex(self.next_touched_glyph_index); self.next_touched_glyph_index += 1; - - let font = self.fonts.font(family); - let glyph_index = font - .glyph_index(c) - .unwrap_or_else(|| font.glyph_index('□').unwrap()); - - let HorizontalMetrics { - advance_width, - left_side_bearing: _, - } = font.horizontal_metrics(glyph_index); - - *entry.insert(TouchedGlyphInfo { - touched_glyph_index, - glyph_index, - advance_width, - }) + *entry.insert(touched_glyph_index) } } } pub fn update_atlas(&mut self) -> (&[TouchedGlyph], Option<&[u8]>) { - // We might have touched more, or fewer, glyphs this iteration, so update the touched glyphs array. + // We might have touched more, or fewer, glyphs this iteration, so + // update the touched glyphs array. self.touched_glyphs .resize(self.touched_glyph_lookup.len(), TouchedGlyph::default()); @@ -172,23 +153,22 @@ where let updated_atlas = 'emergency_repack: loop { let cached_glyphs_len = self.cached_glyphs.len(); - let sorted_indices = &self.cached_glyph_indices_sorted; + let sorted_indices = self.cached_glyph_indices_sorted.as_slice(); // For each touched glyph, try and find it in our cached glyph list. - for (glyph_key, touched_glyph_info) in self.touched_glyph_lookup.iter() { - let touched_glyph_index = touched_glyph_info.touched_glyph_index.0; - let glyph_index = touched_glyph_info.glyph_index; - + for (glyph_key, touched_glyph_index) in self.touched_glyph_lookup.iter() { match sorted_indices .binary_search_by_key(glyph_key, |&index| self.cached_glyphs[index].glyph_key) { - // We've already cached this glyph. So we just need to write into `touched_glyphs`. + // We've already cached this glyph. So we just need to write + // into `touched_glyphs`. Ok(index) => { let cached_glyph_index = sorted_indices[index]; let cached_glyph = &self.cached_glyphs[cached_glyph_index]; let rect = &self.rects[cached_glyph_index]; - let touched_glyph = &mut self.touched_glyphs[touched_glyph_index as usize]; + let touched_glyph = + &mut self.touched_glyphs[touched_glyph_index.0 as usize]; touched_glyph.x0 = rect.x; touched_glyph.x1 = rect.x + rect.w; @@ -200,7 +180,8 @@ where touched_glyph.offset_x1 = cached_glyph.offset_x1; touched_glyph.offset_y1 = cached_glyph.offset_y1; } - // This glyph isn't cached, so we must prepare to pack and render it. + // This glyph isn't cached, so we must prepare to pack and + // render it. Err(_) => { let font = self.fonts.font(glyph_key.family); let size_px = glyph_key.size_px.get(); @@ -208,17 +189,18 @@ where let scale = font.scale_for_size_px(size_px) * oversample.as_f32(); let GlyphBitmapBox { x0, x1, y0, y1 } = - font.glyph_bitmap_box(glyph_index, scale, scale, 0.0, 0.0); + font.glyph_bitmap_box(glyph_key.glyph_index, scale, scale, 0.0, 0.0); let w = x1 - x0 + self.padding as i32 + oversample.as_i32() - 1; let h = y1 - y0 + self.padding as i32 + oversample.as_i32() - 1; self.cached_glyphs.push(CachedGlyph { glyph_key: *glyph_key, - glyph_index, x0, y0, - offset_x0: 0.0, // These zeroed fields will be filled out in the render step. + // These zeroed fields will be filled out in the + // render step. + offset_x0: 0.0, offset_y0: 0.0, offset_x1: 0.0, offset_y1: 0.0, @@ -241,19 +223,23 @@ where break false; } - // New glyphs are now stored in the range `cached_glyphs_len..` in both the `rects` and `cached_glyphs` - // structures. + // New glyphs are now stored in the range `cached_glyphs_len..` in + // both the `rects` and `cached_glyphs` structures. let new_rects = &mut self.rects[cached_glyphs_len..]; let new_cached_glyphs = &mut self.cached_glyphs[cached_glyphs_len..]; - // First we must pack the new glyph rects so we know where to render them. + // We add the new rects to the existing packer state. This can be + // less than optimal, but allows us to avoid invalidating previous + // entries in the cache. if !self.packer.pack(new_rects) { assert!( !is_emergency_repack, "emergency repack failed, texture atlas exhausted" ); - // If packing fails, wipe the cache and try again with a full repack and render of touched_glyphs. + // If packing fails, wipe the cache and try again with a full + // repack, dropping any glyphs that aren't required in this + // update. self.cached_glyph_indices_sorted.clear(); self.cached_glyphs.clear(); self.rects.clear(); @@ -291,7 +277,7 @@ where 0.0, oversample, oversample, - cached_glyph.glyph_index, + cached_glyph.glyph_key.glyph_index, ); let offset_x0 = cached_glyph.x0 as f32 / oversample.as_f32() + sub_x; @@ -317,8 +303,10 @@ where touched_glyph.offset_y1 = offset_y1; } - // Instead of sorting the `cached_glyphs` and `rects` arrays directly, we sort an indirection array. Since - // we've changed the cached_glyphs array, this needs to be updated now. + // The `cached_glyphs` and `rects` arrays need to be sorted for the + // lookup binary search, but instead of sorting them directly, we + // sort a small indirection table since that's a bit simpler to + // execute. self.cached_glyph_indices_sorted.clear(); self.cached_glyph_indices_sorted .extend(0..self.cached_glyphs.len()); @@ -328,8 +316,9 @@ where break true; }; - // Each update gets new touched glyphs, so we need to clear the hashmap. However this cannot happen until the - // function exit as the touched glyphs are needed for the emergency repack. + // Each update gets new touched glyphs, so we need to clear the hashmap. + // However this cannot happen until the function exit as the touched + // glyphs are needed for the emergency repack. self.touched_glyph_lookup.clear(); self.next_touched_glyph_index = 0; diff --git a/libs/narcissus-font/src/font.rs b/libs/narcissus-font/src/font.rs index bb350a2..608ad58 100644 --- a/libs/narcissus-font/src/font.rs +++ b/libs/narcissus-font/src/font.rs @@ -29,7 +29,8 @@ impl Oversample { /// Font vertical metrics in unscaled coordinates. /// -/// You should advance the vertical position by `ascent * scale - descent * scale + line_gap * scale` +/// You should advance the vertical position by +/// `(ascent - descent + line_gap) * scale` #[derive(Clone, Copy, Debug)] pub struct VerticalMetrics { /// Coordinate above the baseline the font extends. @@ -45,9 +46,11 @@ pub struct VerticalMetrics { /// You should advance the horizontal position by `advance_width * scale` #[derive(Clone, Copy, Debug)] pub struct HorizontalMetrics { - /// The offset from the current horizontal position to the next horizontal position. + /// The offset from the current horizontal position to the next horizontal + /// position. pub advance_width: f32, - /// The offset from the current horizontal position to the left edge of the character. + /// The offset from the current horizontal position to the left edge of the + /// character. pub left_side_bearing: f32, } @@ -55,6 +58,12 @@ pub struct HorizontalMetrics { #[repr(C)] pub struct GlyphIndex(NonZeroI32); +impl GlyphIndex { + pub fn as_u32(self) -> u32 { + self.0.get() as u32 + } +} + /// Coordinates: /// +x right /// +y down @@ -82,6 +91,12 @@ pub struct Font<'a> { } impl<'a> Font<'a> { + /// Create a new `Font` from ttf data. + /// + /// # Safety + /// + /// Must be a valid ttf font from a trusted source. Invalid data is not + /// safely handled. pub unsafe fn from_bytes(data: &'a [u8]) -> Self { let info = unsafe { let mut info = MaybeUninit::uninit(); @@ -114,9 +129,11 @@ impl<'a> Font<'a> { } } - /// Returns a scale factor to produce a font whose "height" is `size_px` pixels tall. + /// Returns a scale factor to produce a font whose "height" is `size_px` + /// pixels tall. /// - /// Height is measured as the distance from the highest ascender to the lowest descender. + /// Height is measured as the distance from the highest ascender to the + /// lowest descender. pub fn scale_for_size_px(&self, size_px: f32) -> f32 { size_px / (self.vertical_metrics.ascent - self.vertical_metrics.descent) } @@ -136,10 +153,11 @@ impl<'a> Font<'a> { self.vertical_metrics.line_gap } - /// Return the `GlyphIndex` for the character, or `None` if the font has no matching glyph. + /// Return the `GlyphIndex` for the character, or `None` if the font has no + /// matching glyph. pub fn glyph_index(&self, c: char) -> Option { let glyph_index = unsafe { stbtt_FindGlyphIndex(&self.info, c as i32) }; - NonZeroI32::new(glyph_index).map(|glyph_index| GlyphIndex(glyph_index)) + NonZeroI32::new(glyph_index).map(GlyphIndex) } pub fn glyph_bitmap_box( diff --git a/libs/narcissus-font/src/lib.rs b/libs/narcissus-font/src/lib.rs index 75eb07e..eb8447f 100644 --- a/libs/narcissus-font/src/lib.rs +++ b/libs/narcissus-font/src/lib.rs @@ -2,6 +2,6 @@ mod cache; mod font; mod packer; -pub use cache::{GlyphCache, TouchedGlyph, TouchedGlyphIndex, TouchedGlyphInfo}; -pub use font::{Font, FontCollection, GlyphIndex, Oversample}; +pub use cache::{GlyphCache, TouchedGlyph, TouchedGlyphIndex}; +pub use font::{Font, FontCollection, GlyphIndex, HorizontalMetrics, Oversample}; pub use packer::{Packer, Rect};