Don't do so much work on kerning.
Clean and simplify up cache API.
Improve comments a little.
};
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,
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
///
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");
}
}
+ 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();
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;
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;
+ }
}
}
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 {
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.
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,
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>,
/// 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(),
};
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());
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;
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();
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,
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();
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;
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());
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;
/// 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.
/// 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,
}
#[repr(C)]
pub struct GlyphIndex(NonZeroI32);
+impl GlyphIndex {
+ pub fn as_u32(self) -> u32 {
+ self.0.get() as u32
+ }
+}
+
/// Coordinates:
/// +x right
/// +y down
}
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();
}
}
- /// 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)
}
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(
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};