From b6ec254dc97c50d09e6886422c4623e6c9c864de Mon Sep 17 00:00:00 2001 From: Joshua Simmons Date: Sun, 26 Feb 2023 19:21:45 +0100 Subject: [PATCH] First pass on narcissus-font library --- Cargo.lock | 16 ++ Cargo.toml | 1 + libs/narcissus-font/Cargo.toml | 11 ++ libs/narcissus-font/src/cache.rs | 236 ++++++++++++++++++++++++++++++ libs/narcissus-font/src/font.rs | 218 +++++++++++++++++++++++++++ libs/narcissus-font/src/lib.rs | 13 ++ libs/narcissus-font/src/packer.rs | 75 ++++++++++ 7 files changed, 570 insertions(+) create mode 100644 libs/narcissus-font/Cargo.toml create mode 100644 libs/narcissus-font/src/cache.rs create mode 100644 libs/narcissus-font/src/font.rs create mode 100644 libs/narcissus-font/src/lib.rs create mode 100644 libs/narcissus-font/src/packer.rs diff --git a/Cargo.lock b/Cargo.lock index 103f7b8..cca78d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 934a857..1527ed0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 index 0000000..abe74b3 --- /dev/null +++ b/libs/narcissus-font/Cargo.toml @@ -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 index 0000000..bc2d4ab --- /dev/null +++ b/libs/narcissus-font/src/cache.rs @@ -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, + 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, CachedGlyphIndex>, + + cached_glyphs: Vec, + + 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 { + 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::>(); + + 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::>(); + + 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 index 0000000..a7638d3 --- /dev/null +++ b/libs/narcissus-font/src/font.rs @@ -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 { + 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 index 0000000..ffe1cbd --- /dev/null +++ b/libs/narcissus-font/src/lib.rs @@ -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 index 0000000..cb78791 --- /dev/null +++ b/libs/narcissus-font/src/packer.rs @@ -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, + 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 + } +} -- 2.49.0