dependencies = [
"narcissus-app",
"narcissus-core",
+ "narcissus-font",
"narcissus-gpu",
"narcissus-image",
"narcissus-maths",
"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"
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"
"libs/ffi/vulkan-sys",
"libs/narcissus-app",
"libs/narcissus-core",
+ "libs/narcissus-font",
"libs/narcissus-gpu",
"libs/narcissus-image",
"libs/narcissus-maths",
--- /dev/null
+[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
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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>;
+}
--- /dev/null
+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;
--- /dev/null
+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
+ }
+}