brewery
notashelf /
23ad00348d20a22a810453be592b185e80f2372b

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git

Commit 23ad00348d20

tarball

NotAShelf <raf@notashelf.dev> · 2026-06-26 07:21 UTC

unverified3 files changed+124-14
font: bound the glyph cache; synthesize missing bold/italic

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I39a00a2847022f684060e94c816df7786a6a6964
diff --git a/Cargo.lock b/Cargo.lockindex f51ab08..028b865 100644--- a/Cargo.lock+++ b/Cargo.lock@@ -12,6 +12,12 @@ dependencies = [ ]  [[package]]+name = "allocator-api2"+version = "0.2.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"++[[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -32,6 +38,7 @@ dependencies = [  "calloop-wayland-source",  "fontconfig",  "freetype-rs",+ "lru",  "pound",  "rustix",  "smithay-client-toolkit",@@ -147,6 +154,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"  [[package]]+name = "equivalent"+version = "1.0.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"++[[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -163,6 +176,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"  [[package]]+name = "foldhash"+version = "0.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"++[[package]] name = "fontconfig" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -194,6 +213,17 @@ dependencies = [ ]  [[package]]+name = "hashbrown"+version = "0.17.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"+dependencies = [+ "allocator-api2",+ "equivalent",+ "foldhash",+]++[[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -246,6 +276,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"  [[package]]+name = "lru"+version = "0.18.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"+dependencies = [+ "hashbrown",+]++[[package]] name = "matchers" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index"diff --git a/Cargo.toml b/Cargo.tomlindex 2496395..43a6dbb 100644--- a/Cargo.toml+++ b/Cargo.toml@@ -13,6 +13,7 @@ calloop = "0.14.4" calloop-wayland-source = "0.4.1" fontconfig = "0.11.0" freetype-rs = "0.38.0"+lru = "0.18.0" pound = "0.1.6" rustix = { version = "1.1.4", features = [   "pty",diff --git a/src/font.rs b/src/font.rsindex 3ac11c8..bebc571 100644--- a/src/font.rs+++ b/src/font.rs@@ -4,19 +4,25 @@ //! FreeType rasterizes each glyph to an 8-bit coverage mask or, for colour //! fonts, a pre-multiplied BGRA bitmap. Layout is fixed-cell, so a glyph's own //! advance is never consulted - only the [`CellMetrics`] taken from the primary-//! face. All C interop is handled by the `freetype` and `fontconfig` safe-//! wrappers, so this module contains no `unsafe`.+//! face. C interop goes through the `freetype`/`fontconfig` safe wrappers; the+//! sole `unsafe` is reading a face's fixed-strike array (see `nearest_strike`).  use std::collections::HashMap; use std::fmt;+use std::num::NonZeroUsize; use std::path::PathBuf;  use fontconfig::{CharSet, Fontconfig, Pattern}; use freetype::bitmap::PixelMode;-use freetype::face::LoadFlag;-use freetype::{Face, Library};+use freetype::face::{LoadFlag, StyleFlag};+use freetype::{Face, Library, Matrix, Vector};+use lru::LruCache; use thiserror::Error; +/// Upper bound on cached glyphs; the working set of a terminal is far smaller,+/// but this caps memory under adversarial all-of-Unicode output.+const GLYPH_CACHE_CAP: usize = 4096;+ #[derive(Debug, Error)] pub enum FontError {     #[error("FreeType: {0}")]@@ -99,7 +105,7 @@ pub struct Fonts {     styled: [Option<usize>; 4],     /// Fallback faces resolved by coverage, deduplicated by file path.     fallbacks: HashMap<PathBuf, usize>,-    cache: HashMap<(char, usize), Glyph>,+    cache: LruCache<(char, usize), Glyph>, }  impl fmt::Debug for Fonts {@@ -132,7 +138,7 @@ impl Fonts {             faces: vec![regular],             styled: [Some(0), None, None, None],             fallbacks: HashMap::new(),-            cache: HashMap::new(),+            cache: LruCache::new(NonZeroUsize::new(GLYPH_CACHE_CAP).expect("cap is nonzero")),         })     } @@ -144,12 +150,18 @@ impl Fonts {     /// it on first use.     pub fn glyph(&mut self, c: char, style: Style) -> Result<&Glyph, FontError> {         let key = (c, style.index());-        if !self.cache.contains_key(&key) {-            let face = self.face_for(c, style)?;-            let glyph = rasterize(&self.faces[face], c)?;-            self.cache.insert(key, glyph);+        if self.cache.get(&key).is_none() {+            let idx = self.face_for(c, style)?;+            let face = &self.faces[idx];+            // Synthesize bold/italic only when the resolved face lacks the real+            // variant (most monospace families ship both).+            let flags = face.style_flags();+            let synth_bold = style.bold && !flags.contains(StyleFlag::BOLD);+            let synth_italic = style.italic && !flags.contains(StyleFlag::ITALIC);+            let glyph = rasterize(face, c, synth_bold, synth_italic)?;+            self.cache.put(key, glyph);         }-        Ok(&self.cache[&key])+        Ok(self.cache.get(&key).expect("glyph was just inserted"))     }      /// Pick the face that should render `c`: the requested style if it has the@@ -294,8 +306,23 @@ fn cell_metrics(face: &Face, family: &str) -> Result<CellMetrics, FontError> {     }) } -fn rasterize(face: &Face, c: char) -> Result<Glyph, FontError> {-    face.load_char(c as usize, LoadFlag::RENDER | LoadFlag::COLOR)?;+fn rasterize(+    face: &Face,+    c: char,+    synth_bold: bool,+    synth_italic: bool,+) -> Result<Glyph, FontError> {+    // A shear transform fakes italics on a face that has no real oblique. It is+    // applied to the outline at load time, so reset it immediately after.+    if synth_italic {+        face.set_transform(&mut shear_matrix(), &mut Vector { x: 0, y: 0 });+    }+    let result = face.load_char(c as usize, LoadFlag::RENDER | LoadFlag::COLOR);+    if synth_italic {+        face.set_transform(&mut identity_matrix(), &mut Vector { x: 0, y: 0 });+    }+    result?;+     let slot = face.glyph();     let bitmap = slot.bitmap();     let width = bitmap.width().max(0) as usize;@@ -303,12 +330,17 @@ fn rasterize(face: &Face, c: char) -> Result<Glyph, FontError> {     let pitch = bitmap.pitch();     let src = bitmap.buffer(); -    let data = match bitmap.pixel_mode()? {+    let mut data = match bitmap.pixel_mode()? {         PixelMode::Gray => GlyphData::Mask(pack_rows(src, width, pitch, height)),         PixelMode::Bgra => GlyphData::Color(pack_rows(src, width * 4, pitch, height)),         PixelMode::Mono => GlyphData::Mask(expand_mono(src, width, pitch, height)),         _ => GlyphData::Mask(vec![0; width * height]),     };+    // Fake bold by widening coverage one pixel to the right (colour glyphs are+    // left alone - there is no such thing as a bold emoji).+    if synth_bold && let GlyphData::Mask(mask) = &mut data {+        embolden(mask, width, height);+    }      Ok(Glyph {         left: slot.bitmap_left(),@@ -319,6 +351,35 @@ fn rasterize(face: &Face, c: char) -> Result<Glyph, FontError> {     }) } +fn shear_matrix() -> Matrix {+    // ~0.2 horizontal shear in 16.16 fixed point.+    Matrix {+        xx: 0x1_0000,+        xy: 0x3333,+        yx: 0,+        yy: 0x1_0000,+    }+}++fn identity_matrix() -> Matrix {+    Matrix {+        xx: 0x1_0000,+        xy: 0,+        yx: 0,+        yy: 0x1_0000,+    }+}++/// Widen each row's coverage by one pixel (synthetic bold).+fn embolden(mask: &mut [u8], width: usize, height: usize) {+    for y in 0..height {+        let row = &mut mask[y * width..y * width + width];+        for x in (1..width).rev() {+            row[x] = row[x].max(row[x - 1]);+        }+    }+}+ /// Copy `height` rows of `row_bytes` each out of FreeType's padded buffer, /// honouring pitch sign (positive = top-down). fn pack_rows(src: &[u8], row_bytes: usize, pitch: i32, height: usize) -> Vec<u8> {@@ -386,6 +447,15 @@ mod tests {     }      #[test]+    fn embolden_widens_coverage() {+        // 3x2 mask, one lit pixel per row at x=1.+        let mut mask = vec![0, 255, 0, 0, 200, 0];+        embolden(&mut mask, 3, 2);+        // Each lit pixel bleeds one column to the right; the left edge is unchanged.+        assert_eq!(mask, vec![0, 255, 255, 0, 200, 200]);+    }++    #[test]     fn glyphs_are_cached() {         let mut f = fonts();         f.glyph('a', Style::default()).unwrap();