]> git.nega.tv - josh/narcissus/commitdiff
Improve font cache
authorJoshua Simmons <josh@nega.tv>
Fri, 3 Mar 2023 22:28:03 +0000 (23:28 +0100)
committerJoshua Simmons <josh@nega.tv>
Fri, 3 Mar 2023 22:28:03 +0000 (23:28 +0100)
Don't do so much work on kerning.
Clean and simplify up cache API.
Improve comments a little.

bins/narcissus/src/main.rs
libs/narcissus-font/src/cache.rs
libs/narcissus-font/src/font.rs
libs/narcissus-font/src/lib.rs

index 7485aefc185ded09640d643f3caa225cac0361ef..3cfc546b8a8f615b77ec5c13cd7961b883f55373 100644 (file)
@@ -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 {
index e11a4e123e95d8ea8191e2001ca3c66390aa434b..4347f0c55f24b6c817b6ad541cfda6a3aacd7483 100644 (file)
@@ -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: 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<F> {
     glyph_key: GlyphKey<F>,
-    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<GlyphKey<F::Family>, TouchedGlyphInfo>,
+    touched_glyph_lookup: FxHashMap<GlyphKey<F::Family>, TouchedGlyphIndex>,
     touched_glyphs: Vec<TouchedGlyph>,
 
     cached_glyph_indices_sorted: Vec<usize>,
@@ -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;
 
index bb350a20e7da7e1ac5f807c04a090f4fa408cb19..608ad5832ad90f857bc39199edabfcd73c85a7f6 100644 (file)
@@ -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<GlyphIndex> {
         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(
index 75eb07e61504c2feee8c27d4baa189ee73e15ab2..eb8447fe7e0a100b175305ee44c903f397b3e5be 100644 (file)
@@ -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};