]> git.nega.tv - josh/narcissus/commitdiff
First pass on narcissus-font library
authorJoshua Simmons <josh@nega.tv>
Sun, 26 Feb 2023 18:21:45 +0000 (19:21 +0100)
committerJoshua Simmons <josh@nega.tv>
Sun, 26 Feb 2023 18:22:32 +0000 (19:22 +0100)
Cargo.lock
Cargo.toml
libs/narcissus-font/Cargo.toml [new file with mode: 0644]
libs/narcissus-font/src/cache.rs [new file with mode: 0644]
libs/narcissus-font/src/font.rs [new file with mode: 0644]
libs/narcissus-font/src/lib.rs [new file with mode: 0644]
libs/narcissus-font/src/packer.rs [new file with mode: 0644]

index 103f7b8b27e9b3f51535f235dac0d5b168262260..cca78d2b66fe753986c44895a12ab3a58d0209e7 100644 (file)
@@ -36,6 +36,7 @@ version = "0.1.0"
 dependencies = [
  "narcissus-app",
  "narcissus-core",
+ "narcissus-font",
  "narcissus-gpu",
  "narcissus-image",
  "narcissus-maths",
@@ -57,6 +58,15 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "narcissus-font"
+version = "0.1.0"
+dependencies = [
+ "narcissus-core",
+ "rustc-hash",
+ "stb_truetype-sys",
+]
+
 [[package]]
 name = "narcissus-gpu"
 version = "0.1.0"
@@ -90,6 +100,12 @@ dependencies = [
 name = "renderdoc-sys"
 version = "0.1.0"
 
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
 [[package]]
 name = "sdl2-sys"
 version = "0.1.0"
index 934a8572157059bd0dc8bbe2cc479ba9023febdc..1527ed0830b03cbc1092cc31a3ef849c12e7ae41 100644 (file)
@@ -9,6 +9,7 @@ members = [
     "libs/ffi/vulkan-sys",
     "libs/narcissus-app",
     "libs/narcissus-core",
+    "libs/narcissus-font",
     "libs/narcissus-gpu",
     "libs/narcissus-image",
     "libs/narcissus-maths",
diff --git a/libs/narcissus-font/Cargo.toml b/libs/narcissus-font/Cargo.toml
new file mode 100644 (file)
index 0000000..abe74b3
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name = "narcissus-font"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+stb_truetype-sys = { path = "../ffi/stb_truetype-sys" }
+narcissus-core = { path = "../narcissus-core" }
+rustc-hash = "1.1.0"
\ No newline at end of file
diff --git a/libs/narcissus-font/src/cache.rs b/libs/narcissus-font/src/cache.rs
new file mode 100644 (file)
index 0000000..bc2d4ab
--- /dev/null
@@ -0,0 +1,236 @@
+use rustc_hash::FxHashMap;
+use stb_truetype_sys::rectpack::Rect;
+
+use crate::{font::GlyphBitmapBox, FontCollection, GlyphIndex, Oversample, Packer};
+
+pub use narcissus_core::FiniteF32;
+
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
+#[repr(transparent)]
+pub struct CachedGlyphIndex(u32);
+
+#[derive(Clone, Copy, Default)]
+#[repr(C)]
+pub struct CachedGlyph {
+    // Bitmap coordinates in texture atlas.
+    pub x0: i32,
+    pub x1: i32,
+    pub y0: i32,
+    pub y1: i32,
+
+    // Glyph bounding box relative to glyph origin.
+    pub offset_x0: f32,
+    pub offset_x1: f32,
+    pub offset_y0: f32,
+    pub offset_y1: f32,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash)]
+struct GlyphKey<Family> {
+    family: Family,
+    glyph_index: GlyphIndex,
+    scale: FiniteF32,
+}
+
+pub struct GlyphCache<'a, F>
+where
+    F: FontCollection<'a>,
+{
+    fonts: &'a F,
+
+    oversample_h: Oversample,
+    oversample_v: Oversample,
+
+    next_cached_glyph_index: u32,
+    glyph_lookup: FxHashMap<GlyphKey<F::Family>, CachedGlyphIndex>,
+
+    cached_glyphs: Vec<CachedGlyph>,
+
+    width: usize,
+    height: usize,
+    texture: Box<[u8]>,
+}
+
+const GLYPH_CACHE_PADDING: usize = 1;
+
+impl<'a, F> GlyphCache<'a, F>
+where
+    F: FontCollection<'a>,
+{
+    pub fn new(
+        fonts: &'a F,
+        width: usize,
+        height: usize,
+        oversample_h: Oversample,
+        oversample_v: Oversample,
+    ) -> Self {
+        Self {
+            fonts,
+            oversample_h,
+            oversample_v,
+
+            next_cached_glyph_index: 0,
+            glyph_lookup: Default::default(),
+
+            cached_glyphs: Vec::new(),
+
+            width,
+            height,
+            texture: vec![0; width * height].into_boxed_slice(),
+        }
+    }
+
+    pub fn width(&self) -> usize {
+        self.width
+    }
+
+    pub fn height(&self) -> usize {
+        self.height
+    }
+
+    pub fn cache_glyph(
+        &mut self,
+        family: F::Family,
+        glyph_index: GlyphIndex,
+        scale: f32,
+    ) -> CachedGlyphIndex {
+        let key = GlyphKey {
+            family,
+            glyph_index,
+            scale: FiniteF32::new(scale).unwrap(),
+        };
+
+        *self.glyph_lookup.entry(key).or_insert_with(|| {
+            let cached_glyph_index = CachedGlyphIndex(self.next_cached_glyph_index);
+            self.next_cached_glyph_index += 1;
+            cached_glyph_index
+        })
+    }
+
+    pub fn update_atlas(&mut self) -> (&[CachedGlyph], &[u8]) {
+        self.next_cached_glyph_index = 0;
+
+        #[derive(PartialEq, Eq, PartialOrd, Ord)]
+        struct GlyphToRender<F> {
+            family: F,
+            glyph_index: GlyphIndex,
+            scale: FiniteF32,
+            cached_glyph_index: CachedGlyphIndex,
+        }
+
+        let mut glyphs_to_render = self
+            .glyph_lookup
+            .iter()
+            .map(|(glyph_key, &cached_glyph_index)| GlyphToRender {
+                family: glyph_key.family,
+                glyph_index: glyph_key.glyph_index,
+                scale: glyph_key.scale,
+                cached_glyph_index,
+            })
+            .collect::<Vec<_>>();
+
+        glyphs_to_render.sort_unstable();
+
+        let padding = GLYPH_CACHE_PADDING as i32;
+        let oversample_h = self.oversample_h.as_i32();
+        let oversample_v = self.oversample_v.as_i32();
+
+        let mut rects = glyphs_to_render
+            .iter()
+            .map(|glyph| {
+                let scale = glyph.scale.get();
+
+                let bitmap_box = self.fonts.font(glyph.family).glyph_bitmap_box(
+                    glyph.glyph_index,
+                    scale * oversample_h as f32,
+                    scale * oversample_v as f32,
+                    0.0,
+                    0.0,
+                );
+
+                let w = bitmap_box.x1 - bitmap_box.x0 + padding + oversample_h - 1;
+                let h = bitmap_box.y1 - bitmap_box.y0 + padding + oversample_v - 1;
+
+                Rect {
+                    id: glyph.cached_glyph_index.0 as i32,
+                    w,
+                    h,
+                    x: 0,
+                    y: 0,
+                    was_packed: 0,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        let mut packer = Packer::new(
+            self.width - GLYPH_CACHE_PADDING,
+            self.height - GLYPH_CACHE_PADDING,
+        );
+
+        packer.pack(rects.as_mut_slice());
+
+        self.texture.fill(0);
+        self.cached_glyphs
+            .resize(glyphs_to_render.len(), CachedGlyph::default());
+
+        let oversample_h = oversample_h as f32;
+        let oversample_v = oversample_v as f32;
+
+        for (glyph, rect) in glyphs_to_render.iter().zip(rects.iter_mut()) {
+            let font = self.fonts.font(glyph.family);
+
+            // Pad on left and top.
+            rect.x += padding;
+            rect.y += padding;
+            rect.w -= padding;
+            rect.h -= padding;
+
+            let scale = glyph.scale.get();
+            let scale_x = scale * oversample_h;
+            let scale_y = scale * oversample_v;
+
+            let (sub_x, sub_y) = font.render_glyph_bitmap(
+                &mut self.texture,
+                rect.x,
+                rect.y,
+                rect.w,
+                rect.h,
+                self.width as i32,
+                scale_x,
+                scale_y,
+                0.0,
+                0.0,
+                self.oversample_h,
+                self.oversample_v,
+                glyph.glyph_index,
+            );
+
+            let cached_glyph = &mut self.cached_glyphs[rect.id as usize];
+
+            cached_glyph.x0 = rect.x;
+            cached_glyph.x1 = rect.x + rect.w;
+            cached_glyph.y0 = rect.y;
+            cached_glyph.y1 = rect.y + rect.h;
+
+            let GlyphBitmapBox {
+                x0,
+                x1: _,
+                y0,
+                y1: _,
+            } = font.glyph_bitmap_box(
+                glyph.glyph_index,
+                scale * oversample_h,
+                scale * oversample_v,
+                0.0,
+                0.0,
+            );
+
+            cached_glyph.offset_x0 = x0 as f32 / oversample_h + sub_x;
+            cached_glyph.offset_y0 = y0 as f32 / oversample_v + sub_y;
+            cached_glyph.offset_x1 = (x0 + rect.w) as f32 / oversample_h + sub_x;
+            cached_glyph.offset_y1 = (y0 + rect.h) as f32 / oversample_v + sub_y;
+        }
+
+        (&self.cached_glyphs, &self.texture)
+    }
+}
diff --git a/libs/narcissus-font/src/font.rs b/libs/narcissus-font/src/font.rs
new file mode 100644 (file)
index 0000000..a7638d3
--- /dev/null
@@ -0,0 +1,218 @@
+use std::{marker::PhantomData, mem::MaybeUninit, num::NonZeroI32};
+
+use stb_truetype_sys::{
+    stbtt_FindGlyphIndex, stbtt_GetFontOffsetForIndex, stbtt_GetFontVMetrics,
+    stbtt_GetGlyphBitmapBoxSubpixel, stbtt_GetGlyphHMetrics, stbtt_GetGlyphKernAdvance,
+    stbtt_InitFont, stbtt_MakeGlyphBitmapSubpixelPrefilter, stbtt_ScaleForPixelHeight, truetype,
+};
+
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Oversample {
+    X1,
+    X2,
+    X3,
+    X4,
+    X5,
+    X6,
+    X7,
+    X8,
+}
+
+impl Oversample {
+    pub fn as_i32(self) -> i32 {
+        self as i32 + 1
+    }
+}
+
+/// Font vertical metrics in unscaled coordinates.
+///
+/// You should advance the vertical position by `ascent * scale - descent * scale + line_gap * scale`
+#[derive(Clone, Copy, Debug)]
+pub struct VerticalMetrics {
+    /// Coordinate above the baseline the font extends.
+    pub ascent: f32,
+    /// Coordinate below the baseline the font extends.
+    pub descent: f32,
+    /// The spacing between one row's descent and the next row's ascent.
+    pub line_gap: f32,
+}
+
+/// Glyph horizontal metrics in unscaled coordinates.
+///
+/// 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.
+    pub advance_width: f32,
+    /// The offset from the current horizontal position to the left edge of the character.
+    pub left_side_bearing: f32,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
+#[repr(C)]
+pub struct GlyphIndex(NonZeroI32);
+
+/// Coordinates:
+///  +x right
+///  +y down
+///
+/// ```text
+/// (x0,y0)
+///        +-----+
+///        |     |
+///        |     |
+///        |     |
+///        +-----+
+///               (x1,y1)
+/// ```
+pub struct GlyphBitmapBox {
+    pub x0: i32,
+    pub x1: i32,
+    pub y0: i32,
+    pub y1: i32,
+}
+
+pub struct Font<'a> {
+    info: truetype::FontInfo,
+    phantom: PhantomData<&'a [u8]>,
+}
+
+impl<'a> Font<'a> {
+    pub unsafe fn from_bytes(data: &'a [u8]) -> Self {
+        let info = unsafe {
+            let mut info = MaybeUninit::uninit();
+            let ret = stbtt_InitFont(
+                info.as_mut_ptr(),
+                data.as_ptr(),
+                stbtt_GetFontOffsetForIndex(data.as_ptr(), 0),
+            );
+            assert!(ret != 0, "failed to load ttf font");
+            info.assume_init()
+        };
+
+        Self {
+            info,
+            phantom: PhantomData,
+        }
+    }
+
+    pub fn scale_for_pixel_height(&self, height: f32) -> f32 {
+        unsafe { stbtt_ScaleForPixelHeight(&self.info, height) }
+    }
+
+    pub fn vertical_metrics(&self) -> VerticalMetrics {
+        let mut ascent = 0;
+        let mut descent = 0;
+        let mut line_gap = 0;
+        unsafe {
+            stbtt_GetFontVMetrics(&self.info, &mut ascent, &mut descent, &mut line_gap);
+        }
+        VerticalMetrics {
+            ascent: ascent as f32,
+            descent: descent as f32,
+            line_gap: line_gap as f32,
+        }
+    }
+
+    pub fn glyph_id(&self, c: char) -> Option<GlyphIndex> {
+        let glyph_id = unsafe { stbtt_FindGlyphIndex(&self.info, c as i32) };
+        NonZeroI32::new(glyph_id).map(|glyph_id| GlyphIndex(glyph_id))
+    }
+
+    pub fn glyph_bitmap_box(
+        &self,
+        glyph: GlyphIndex,
+        scale_x: f32,
+        scale_y: f32,
+        shift_x: f32,
+        shift_y: f32,
+    ) -> GlyphBitmapBox {
+        let mut x0 = 0;
+        let mut x1 = 0;
+        let mut y0 = 0;
+        let mut y1 = 0;
+        unsafe {
+            stbtt_GetGlyphBitmapBoxSubpixel(
+                &self.info,
+                glyph.0.get(),
+                scale_x,
+                scale_y,
+                shift_x,
+                shift_y,
+                &mut x0,
+                &mut y0,
+                &mut x1,
+                &mut y1,
+            );
+        }
+        GlyphBitmapBox { x0, x1, y0, y1 }
+    }
+
+    pub fn render_glyph_bitmap(
+        &self,
+        out: &mut [u8],
+        out_x: i32,
+        out_y: i32,
+        out_w: i32,
+        out_h: i32,
+        out_stride: i32,
+        scale_x: f32,
+        scale_y: f32,
+        shift_x: f32,
+        shift_y: f32,
+        oversample_h: Oversample,
+        oversample_v: Oversample,
+        glyph: GlyphIndex,
+    ) -> (f32, f32) {
+        let mut sub_x = 0.0;
+        let mut sub_y = 0.0;
+
+        unsafe {
+            stbtt_MakeGlyphBitmapSubpixelPrefilter(
+                &self.info,
+                out.as_mut_ptr()
+                    .offset(out_y as isize * out_stride as isize + out_x as isize),
+                out_w,
+                out_h,
+                out_stride,
+                scale_x,
+                scale_y,
+                shift_x,
+                shift_y,
+                oversample_h.as_i32(),
+                oversample_v.as_i32(),
+                &mut sub_x,
+                &mut sub_y,
+                glyph.0.get(),
+            );
+        }
+
+        (sub_x, sub_y)
+    }
+
+    pub fn horizontal_metrics(&self, glyph: GlyphIndex) -> HorizontalMetrics {
+        let mut advance_width = 0;
+        let mut left_side_bearing = 0;
+        unsafe {
+            stbtt_GetGlyphHMetrics(
+                &self.info,
+                glyph.0.get(),
+                &mut advance_width,
+                &mut left_side_bearing,
+            )
+        };
+        HorizontalMetrics {
+            advance_width: advance_width as f32,
+            left_side_bearing: left_side_bearing as f32,
+        }
+    }
+
+    pub fn kerning_advance(&self, glyph_1: GlyphIndex, glyph_2: GlyphIndex) -> f32 {
+        unsafe { stbtt_GetGlyphKernAdvance(&self.info, glyph_1.0.get(), glyph_2.0.get()) as f32 }
+    }
+}
+
+pub trait FontCollection<'a> {
+    type Family: Copy + Eq + Ord + std::hash::Hash;
+    fn font(&self, font_family: Self::Family) -> &Font<'a>;
+}
diff --git a/libs/narcissus-font/src/lib.rs b/libs/narcissus-font/src/lib.rs
new file mode 100644 (file)
index 0000000..ffe1cbd
--- /dev/null
@@ -0,0 +1,13 @@
+mod cache;
+mod font;
+mod packer;
+
+pub use cache::CachedGlyph;
+pub use cache::CachedGlyphIndex;
+pub use cache::GlyphCache;
+pub use font::Font;
+pub use font::FontCollection;
+pub use font::GlyphIndex;
+pub use font::Oversample;
+pub use packer::Packer;
+pub use packer::Rect;
diff --git a/libs/narcissus-font/src/packer.rs b/libs/narcissus-font/src/packer.rs
new file mode 100644 (file)
index 0000000..cb78791
--- /dev/null
@@ -0,0 +1,75 @@
+use narcissus_core::{box_assume_init, uninit_box};
+use stb_truetype_sys::{rectpack, stbrp_init_target, stbrp_pack_rects};
+
+pub use rectpack::Rect;
+
+pub struct Packer {
+    context: Box<rectpack::Context>,
+    nodes: Box<[rectpack::Node]>,
+    width: i32,
+    height: i32,
+}
+
+impl Packer {
+    /// Create a new rectangle packer.
+    ///
+    /// # Panics
+    /// Panics if width or height exceed i32::MAX.
+    pub fn new(width: usize, height: usize) -> Self {
+        assert!(width < i32::MAX as usize && height < i32::MAX as usize);
+
+        let mut nodes = vec![rectpack::Node::default(); width].into_boxed_slice();
+
+        let width = width as i32;
+        let height = height as i32;
+
+        // Safety: `nodes` must not be deleted while context lives, and `context` must not be
+        //         relocated.
+        let context = unsafe {
+            let mut context = uninit_box();
+            stbrp_init_target(
+                context.as_mut_ptr(),
+                width,
+                height,
+                nodes.as_mut_ptr(),
+                width, // Matches node count.
+            );
+            box_assume_init(context)
+        };
+
+        Self {
+            context,
+            nodes,
+            width,
+            height,
+        }
+    }
+
+    /// Clear all previously packed rectangle state.
+    pub fn clear(&mut self) {
+        // Safety: `context` and `nodes` are always valid while packer exists, and width always
+        //         matches node count.
+        unsafe {
+            stbrp_init_target(
+                self.context.as_mut(),
+                self.width,
+                self.height,
+                self.nodes.as_mut_ptr(),
+                self.width,
+            )
+        }
+    }
+
+    /// Pack the provided rectangles into the rectangle given when the packer was created.
+    ///
+    /// Calling this function multiple times to incrementally pack a collection of rectangles may
+    /// be less effective than packing the entire collection all at once.
+    ///
+    /// Returns true if all rectangles were successfully packed.
+    pub fn pack(&mut self, rects: &mut [rectpack::Rect]) -> bool {
+        let num_rects = rects.len().try_into().expect("too many rects to pack");
+        // Safety: `context` and `nodes` are always valid while packer exists.
+        let ret = unsafe { stbrp_pack_rects(self.context.as_mut(), rects.as_mut_ptr(), num_rects) };
+        ret == 1
+    }
+}