#![warn(missing_docs)]
use std::collections::HashMap;
use cosmic_text::fontdb;
use ttf_parser::Face;
use crate::text::msdf::{MsdfGlyph, build_glyph_msdf, glyph_advance};
use crate::text::msdf_snapshot::{
SnapshotError, SnapshotGlyph, SnapshotHeader, SnapshotSection, decode_snapshot, encode_snapshot,
};
pub const DEFAULT_BASE_EM: u32 = 48;
pub const DEFAULT_SPREAD: f64 = 6.0;
const PAGE_SIZE: u32 = 1024;
const PAGE_BUDGET: usize = 8;
const GLYPH_PADDING: u32 = 2;
pub const MSDF_BYTES_PER_PIXEL: u32 = 4;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct MsdfGlyphKey {
pub font: fontdb::ID,
pub glyph_id: u16,
pub weight: u16,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct MsdfRect {
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
}
impl MsdfRect {
pub fn right(&self) -> u32 {
self.x + self.w
}
pub fn bottom(&self) -> u32 {
self.y + self.h
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct MsdfSlot {
pub page: u32,
pub rect: MsdfRect,
pub bearing_x: f32,
pub bearing_y: f32,
pub advance: f32,
pub spread: f32,
}
#[derive(Copy, Clone)]
struct Shelf {
y_top: u32,
height: u32,
cursor: u32,
}
pub struct MsdfAtlasPage {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
dirty: Option<MsdfRect>,
shelves: Vec<Shelf>,
last_used: u64,
}
impl MsdfAtlasPage {
fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
pixels: vec![0; (width * height * MSDF_BYTES_PER_PIXEL) as usize],
dirty: None,
shelves: Vec::new(),
last_used: 0,
}
}
fn allocate(&mut self, w: u32, h: u32) -> Option<MsdfRect> {
if w > self.width || h > self.height {
return None;
}
let mut best: Option<usize> = None;
for (i, shelf) in self.shelves.iter().enumerate() {
if shelf.cursor + w > self.width || shelf.height < h {
continue;
}
let waste = shelf.height - h;
if best
.map(|b| waste < self.shelves[b].height - h)
.unwrap_or(true)
{
best = Some(i);
}
}
if let Some(i) = best {
let shelf = &mut self.shelves[i];
let rect = MsdfRect {
x: shelf.cursor,
y: shelf.y_top,
w,
h,
};
shelf.cursor += w + GLYPH_PADDING;
return Some(rect);
}
let next_y = self
.shelves
.last()
.map(|s| s.y_top + s.height + GLYPH_PADDING)
.unwrap_or(0);
if next_y + h > self.height {
return None;
}
self.shelves.push(Shelf {
y_top: next_y,
height: h,
cursor: w + GLYPH_PADDING,
});
Some(MsdfRect {
x: 0,
y: next_y,
w,
h,
})
}
}
pub struct MsdfAtlas {
pages: Vec<MsdfAtlasPage>,
map: HashMap<MsdfGlyphKey, MsdfEntry>,
base_em: u32,
spread: f64,
frame: u64,
page_budget: usize,
lru_protection_window: u64,
error_correction: bool,
}
#[derive(Copy, Clone, Debug, PartialEq)]
enum MsdfEntry {
Slot(MsdfSlot),
Empty { advance: f32 },
}
impl Default for MsdfAtlas {
fn default() -> Self {
Self::new(DEFAULT_BASE_EM, DEFAULT_SPREAD)
}
}
impl MsdfAtlas {
pub fn new(base_em: u32, spread: f64) -> Self {
Self {
pages: vec![MsdfAtlasPage::new(PAGE_SIZE, PAGE_SIZE)],
map: HashMap::new(),
base_em,
spread,
frame: 0,
page_budget: PAGE_BUDGET,
lru_protection_window: 1,
error_correction: false,
}
}
pub fn set_error_correction(&mut self, on: bool) {
self.error_correction = on;
}
pub fn set_lru_protection_window(&mut self, n: u32) {
self.lru_protection_window = u64::from(n.max(1));
}
pub fn base_em(&self) -> u32 {
self.base_em
}
pub fn spread(&self) -> f64 {
self.spread
}
pub fn pages(&self) -> &[MsdfAtlasPage] {
&self.pages
}
pub fn page(&self, index: u32) -> Option<&MsdfAtlasPage> {
self.pages.get(index as usize)
}
pub fn slot(&self, key: MsdfGlyphKey) -> Option<MsdfSlot> {
match self.map.get(&key)? {
MsdfEntry::Slot(s) => Some(*s),
MsdfEntry::Empty { .. } => None,
}
}
pub fn touch(&mut self, key: MsdfGlyphKey) -> Option<MsdfSlot> {
let slot = self.slot(key)?;
self.pages[slot.page as usize].last_used = self.frame;
Some(slot)
}
pub fn advance(&self, key: MsdfGlyphKey) -> Option<f32> {
Some(match self.map.get(&key)? {
MsdfEntry::Slot(s) => s.advance,
MsdfEntry::Empty { advance } => *advance,
})
}
pub fn take_dirty(&mut self) -> Vec<(usize, MsdfRect)> {
self.frame += 1;
let mut out = Vec::new();
for (i, page) in self.pages.iter_mut().enumerate() {
if let Some(rect) = page.dirty.take() {
out.push((i, rect));
}
}
out
}
pub fn ensure(&mut self, key: MsdfGlyphKey, face: &Face<'_>) -> Option<MsdfSlot> {
if let Some(entry) = self.map.get(&key) {
return match entry {
MsdfEntry::Slot(s) => {
let s = *s;
self.pages[s.page as usize].last_used = self.frame;
Some(s)
}
MsdfEntry::Empty { .. } => None,
};
}
match build_glyph_msdf(
face,
key.glyph_id,
self.base_em,
self.spread,
self.error_correction,
) {
Some(glyph) => {
let slot = self.pack(glyph);
self.map.insert(key, MsdfEntry::Slot(slot));
Some(slot)
}
None => {
let advance = glyph_advance(face, key.glyph_id, self.base_em);
self.map.insert(key, MsdfEntry::Empty { advance });
None
}
}
}
pub fn ensure_many(&mut self, keys: &[MsdfGlyphKey], face: &Face<'_>) {
let mut seen = std::collections::HashSet::new();
let misses: Vec<MsdfGlyphKey> = keys
.iter()
.copied()
.filter(|k| !self.map.contains_key(k) && seen.insert(*k))
.collect();
if misses.is_empty() {
return;
}
let (base_em, spread, correct) = (self.base_em, self.spread, self.error_correction);
let build = |k: &MsdfGlyphKey| {
(
*k,
build_glyph_msdf(face, k.glyph_id, base_em, spread, correct),
)
};
#[cfg(feature = "parallel-raster")]
let built: Vec<(MsdfGlyphKey, Option<MsdfGlyph>)> = {
use rayon::prelude::*;
misses.par_iter().map(build).collect()
};
#[cfg(not(feature = "parallel-raster"))]
let built: Vec<(MsdfGlyphKey, Option<MsdfGlyph>)> = misses.iter().map(build).collect();
for (key, glyph) in built {
match glyph {
Some(glyph) => {
let slot = self.pack(glyph);
self.map.insert(key, MsdfEntry::Slot(slot));
}
None => {
let advance = glyph_advance(face, key.glyph_id, self.base_em);
self.map.insert(key, MsdfEntry::Empty { advance });
}
}
}
}
fn snapshot_header(&self) -> SnapshotHeader {
SnapshotHeader {
base_em: self.base_em,
spread: self.spread,
error_correction: self.error_correction,
}
}
pub fn export_snapshot(&self, token_of: impl Fn(fontdb::ID) -> Option<u64>) -> Vec<u8> {
let mut by_token: HashMap<u64, Vec<(u16, u16, SnapshotGlyph)>> = HashMap::new();
for (key, entry) in &self.map {
let Some(token) = token_of(key.font) else {
continue;
};
let g = match entry {
MsdfEntry::Slot(s) => SnapshotGlyph::Outline(self.read_slot_glyph(s)),
MsdfEntry::Empty { advance } => SnapshotGlyph::Empty { advance: *advance },
};
by_token
.entry(token)
.or_default()
.push((key.glyph_id, key.weight, g));
}
let sections: Vec<SnapshotSection> = by_token.into_iter().collect();
encode_snapshot(self.snapshot_header(), §ions)
}
pub fn import_snapshot(
&mut self,
bytes: &[u8],
id_of: impl Fn(u64) -> Option<fontdb::ID>,
) -> Result<usize, SnapshotError> {
let sections = decode_snapshot(bytes, self.snapshot_header())?;
let mut packed = 0;
for (token, glyphs) in sections {
let Some(font) = id_of(token) else {
continue;
};
for (glyph_id, weight, g) in glyphs {
let key = MsdfGlyphKey {
font,
glyph_id,
weight,
};
if self.map.contains_key(&key) {
continue;
}
match g {
SnapshotGlyph::Outline(glyph) => {
let slot = self.pack(glyph);
self.map.insert(key, MsdfEntry::Slot(slot));
packed += 1;
}
SnapshotGlyph::Empty { advance } => {
self.map.insert(key, MsdfEntry::Empty { advance });
}
}
}
}
Ok(packed)
}
fn read_slot_glyph(&self, s: &MsdfSlot) -> MsdfGlyph {
let page = &self.pages[s.page as usize];
let rgba = copy_rgba_out(&page.pixels, page.width, &s.rect);
MsdfGlyph {
rgba,
width: s.rect.w,
height: s.rect.h,
bearing_x: s.bearing_x,
bearing_y: s.bearing_y,
advance: s.advance,
spread: s.spread,
}
}
fn pack(&mut self, glyph: MsdfGlyph) -> MsdfSlot {
let MsdfGlyph {
rgba,
width,
height,
bearing_x,
bearing_y,
advance,
spread,
} = glyph;
let (page_idx, rect) = self.allocate(width, height);
let page = &mut self.pages[page_idx];
copy_rgba_into_rgba(&mut page.pixels, page.width, &rect, &rgba);
merge_dirty(&mut page.dirty, rect);
MsdfSlot {
page: page_idx as u32,
rect,
bearing_x,
bearing_y,
advance,
spread,
}
}
fn allocate(&mut self, w: u32, h: u32) -> (usize, MsdfRect) {
for (i, page) in self.pages.iter_mut().enumerate() {
if let Some(rect) = page.allocate(w, h) {
page.last_used = self.frame;
return (i, rect);
}
}
if self.pages.len() >= self.page_budget
&& let Some(i) = self
.pages
.iter()
.enumerate()
.filter(|(_, p)| {
p.last_used + self.lru_protection_window <= self.frame
&& p.width >= w
&& p.height >= h
})
.min_by_key(|(_, p)| p.last_used)
.map(|(i, _)| i)
{
self.recycle_page(i);
let page = &mut self.pages[i];
let rect = page.allocate(w, h).expect("recycled page fits the glyph");
page.last_used = self.frame;
return (i, rect);
}
let new_w = PAGE_SIZE.max(w.next_power_of_two());
let new_h = PAGE_SIZE.max(h.next_power_of_two());
let mut page = MsdfAtlasPage::new(new_w, new_h);
page.last_used = self.frame;
let rect = page
.allocate(w, h)
.expect("freshly-sized page must fit a glyph");
self.pages.push(page);
(self.pages.len() - 1, rect)
}
fn recycle_page(&mut self, index: usize) {
let page_idx = index as u32;
self.map
.retain(|_, e| !matches!(e, MsdfEntry::Slot(s) if s.page == page_idx));
let page = &mut self.pages[index];
page.pixels.fill(0);
page.shelves.clear();
page.dirty = Some(MsdfRect {
x: 0,
y: 0,
w: page.width,
h: page.height,
});
}
#[cfg(test)]
fn set_page_budget_for_tests(&mut self, budget: usize) {
self.page_budget = budget;
}
}
fn copy_rgba_into_rgba(dst: &mut [u8], stride_pixels: u32, rect: &MsdfRect, src_rgba: &[u8]) {
let dst_row_bytes = stride_pixels as usize * MSDF_BYTES_PER_PIXEL as usize;
let src_row_bytes = rect.w as usize * 4;
for row in 0..rect.h as usize {
let dst_off = (rect.y as usize + row) * dst_row_bytes
+ rect.x as usize * MSDF_BYTES_PER_PIXEL as usize;
let src_off = row * src_row_bytes;
let row_bytes = rect.w as usize * 4;
dst[dst_off..dst_off + row_bytes].copy_from_slice(&src_rgba[src_off..src_off + row_bytes]);
}
}
fn copy_rgba_out(src: &[u8], stride_pixels: u32, rect: &MsdfRect) -> Vec<u8> {
let src_row_bytes = stride_pixels as usize * MSDF_BYTES_PER_PIXEL as usize;
let row_bytes = rect.w as usize * 4;
let mut out = Vec::with_capacity(row_bytes * rect.h as usize);
for row in 0..rect.h as usize {
let off = (rect.y as usize + row) * src_row_bytes
+ rect.x as usize * MSDF_BYTES_PER_PIXEL as usize;
out.extend_from_slice(&src[off..off + row_bytes]);
}
out
}
fn merge_dirty(dirty: &mut Option<MsdfRect>, rect: MsdfRect) {
*dirty = Some(match *dirty {
None => rect,
Some(prev) => {
let x = prev.x.min(rect.x);
let y = prev.y.min(rect.y);
let r = prev.right().max(rect.right());
let b = prev.bottom().max(rect.bottom());
MsdfRect {
x,
y,
w: r - x,
h: b - y,
}
}
});
}
#[cfg(all(test, feature = "inter"))]
mod tests {
use super::*;
fn test_face() -> ttf_parser::Face<'static> {
ttf_parser::Face::parse(damascene_fonts::INTER_VARIABLE, 0).unwrap()
}
fn fake_font_id(seed: u32) -> fontdb::ID {
let mut db = fontdb::Database::new();
db.load_font_data(damascene_fonts::INTER_VARIABLE.to_vec());
let id = db.faces().next().expect("test fontdb has Inter").id;
let _ = seed;
id
}
fn key(face: &Face<'_>, ch: char) -> MsdfGlyphKey {
MsdfGlyphKey {
font: fake_font_id(0),
glyph_id: face.glyph_index(ch).unwrap().0,
weight: 0,
}
}
#[test]
fn ensure_inserts_glyph_and_marks_dirty() {
let face = test_face();
let mut atlas = MsdfAtlas::default();
let slot = atlas.ensure(key(&face, 'A'), &face).expect("slot");
assert_eq!(slot.page, 0);
assert!(slot.rect.w > 0 && slot.rect.h > 0);
let dirty = atlas.take_dirty();
assert_eq!(dirty.len(), 1);
assert!(atlas.take_dirty().is_empty());
}
#[test]
fn ensure_many_matches_per_glyph_ensure() {
let face = test_face();
let chars = ['A', 'B', 'g', 'A', ' ', '@'];
let keys: Vec<MsdfGlyphKey> = chars.iter().map(|&c| key(&face, c)).collect();
let mut batched = MsdfAtlas::default();
batched.ensure_many(&keys, &face);
let mut serial = MsdfAtlas::default();
for &k in &keys {
serial.ensure(k, &face);
}
for &k in &keys {
match (batched.slot(k), serial.slot(k)) {
(Some(b), Some(s)) => {
assert_eq!((b.rect.w, b.rect.h), (s.rect.w, s.rect.h));
assert_eq!(b.advance, s.advance);
}
(None, None) => assert_eq!(batched.advance(k), serial.advance(k)),
(b, s) => panic!("residency mismatch for {k:?}: {b:?} vs {s:?}"),
}
}
}
#[test]
fn snapshot_round_trip_reproduces_residency() {
let face = test_face();
let font = fake_font_id(0);
let chars = ['A', 'g', '@', ' '];
let mut src = MsdfAtlas::default();
for &c in &chars {
src.ensure(key(&face, c), &face);
}
let bytes = src.export_snapshot(|id| (id == font).then_some(42));
let mut dst = MsdfAtlas::default();
let packed = dst
.import_snapshot(&bytes, |t| (t == 42).then_some(font))
.expect("imports");
assert_eq!(packed, 3, "A/g/@ are outlines; space is empty");
for &c in &chars {
let k = key(&face, c);
match (src.slot(k), dst.slot(k)) {
(Some(a), Some(b)) => {
assert_eq!((a.rect.w, a.rect.h), (b.rect.w, b.rect.h));
assert_eq!(
(a.bearing_x, a.bearing_y, a.advance, a.spread),
(b.bearing_x, b.bearing_y, b.advance, b.spread)
);
let pa = copy_rgba_out(
&src.pages[a.page as usize].pixels,
src.pages[a.page as usize].width,
&a.rect,
);
let pb = copy_rgba_out(
&dst.pages[b.page as usize].pixels,
dst.pages[b.page as usize].width,
&b.rect,
);
assert_eq!(pa, pb, "MTSDF bytes differ after round trip for {c:?}");
}
(None, None) => assert_eq!(src.advance(k), dst.advance(k)),
(a, b) => panic!("residency mismatch for {c:?}: {a:?} vs {b:?}"),
}
}
}
#[test]
fn snapshot_import_rejects_param_mismatch() {
let face = test_face();
let font = fake_font_id(0);
let mut src = MsdfAtlas::default(); src.ensure(key(&face, 'A'), &face);
let bytes = src.export_snapshot(|_| Some(1));
let mut dst = MsdfAtlas::new(32, 4.0); assert!(
dst.import_snapshot(&bytes, |_| Some(font)).is_err(),
"a 48-em snapshot must not load into a 32-em atlas"
);
assert!(
dst.slot(key(&face, 'A')).is_none(),
"nothing imported on reject"
);
}
#[test]
fn ensure_many_skips_resident_keys() {
let face = test_face();
let mut atlas = MsdfAtlas::default();
let a = key(&face, 'A');
atlas.ensure(a, &face).expect("slot");
atlas.take_dirty();
atlas.ensure_many(&[a, key(&face, 'B')], &face);
let dirty = atlas.take_dirty();
assert_eq!(dirty.len(), 1, "only the new glyph should dirty a page");
}
#[test]
fn ensure_is_idempotent() {
let face = test_face();
let mut atlas = MsdfAtlas::default();
let s1 = atlas.ensure(key(&face, 'A'), &face).unwrap();
atlas.take_dirty();
let s2 = atlas.ensure(key(&face, 'A'), &face).unwrap();
assert_eq!(s1, s2);
assert!(atlas.take_dirty().is_empty());
}
#[test]
fn whitespace_returns_none_but_caches_advance() {
let face = test_face();
let mut atlas = MsdfAtlas::default();
let space_key = key(&face, ' ');
assert!(atlas.ensure(space_key, &face).is_none());
let advance = atlas.advance(space_key).expect("space advance cached");
assert!(advance > 0.0);
}
#[test]
fn distinct_weights_get_distinct_slots_and_bitmaps() {
use crate::text::msdf::apply_weight_variation;
let font = fake_font_id(0);
let glyph_id = test_face().glyph_index('H').unwrap().0;
let mut atlas = MsdfAtlas::default();
let mut slots = Vec::new();
for weight in [400u16, 700u16] {
let mut face = test_face();
let applied = apply_weight_variation(&mut face, weight);
assert_eq!(applied, weight, "Inter Variable has a wght axis");
let key = MsdfGlyphKey {
font,
glyph_id,
weight: applied,
};
slots.push(atlas.ensure(key, &face).expect("slot"));
}
assert_ne!(slots[0].rect, slots[1].rect, "separate atlas slots");
let px_of = |s: &MsdfSlot| {
copy_rgba_out(
&atlas.pages[s.page as usize].pixels,
atlas.pages[s.page as usize].width,
&s.rect,
)
};
assert!(
slots[0].rect.w != slots[1].rect.w || px_of(&slots[0]) != px_of(&slots[1]),
"regular and bold instances must rasterize different outlines"
);
}
#[test]
fn static_weight_normalizes_to_default_bucket() {
use crate::text::msdf::apply_weight_variation;
let mut face = test_face();
assert_eq!(apply_weight_variation(&mut face, 0), 0);
}
#[test]
fn distinct_glyphs_get_distinct_slots() {
let face = test_face();
let mut atlas = MsdfAtlas::default();
let a = atlas.ensure(key(&face, 'A'), &face).unwrap();
let b = atlas.ensure(key(&face, 'B'), &face).unwrap();
assert_ne!(a.rect, b.rect);
}
#[test]
fn allocation_recycles_lru_page_at_budget() {
let face = test_face();
let mut atlas = MsdfAtlas::default();
atlas.set_page_budget_for_tests(1);
let key_a = key(&face, 'A');
atlas.ensure(key_a, &face).expect("slot");
let (grown, _) = atlas.allocate(PAGE_SIZE, PAGE_SIZE);
assert_eq!(grown, 1);
assert_eq!(atlas.pages().len(), 2);
assert!(atlas.slot(key_a).is_some());
atlas.take_dirty();
let (recycled, rect) = atlas.allocate(PAGE_SIZE, PAGE_SIZE);
assert_eq!(recycled, 0);
assert_eq!(atlas.pages().len(), 2);
assert_eq!((rect.x, rect.y), (0, 0));
assert!(atlas.slot(key_a).is_none());
let dirty = atlas.take_dirty();
assert!(
dirty
.iter()
.any(|(i, r)| *i == 0 && r.w == PAGE_SIZE && r.h == PAGE_SIZE),
"expected a full-page dirty rect on page 0, got {dirty:?}"
);
}
#[test]
fn touch_protects_pages_from_recycling() {
let face = test_face();
let mut atlas = MsdfAtlas::default();
atlas.set_page_budget_for_tests(1);
let key_a = key(&face, 'A');
atlas.ensure(key_a, &face).expect("slot");
atlas.take_dirty();
assert!(atlas.touch(key_a).is_some());
let (idx, _) = atlas.allocate(PAGE_SIZE, PAGE_SIZE);
assert_eq!(idx, 1);
assert!(atlas.slot(key_a).is_some());
}
#[test]
fn lru_protection_window_spans_multiple_flush_ticks() {
let face = test_face();
let mut atlas = MsdfAtlas::default();
atlas.set_page_budget_for_tests(1);
atlas.set_lru_protection_window(2);
let key_a = key(&face, 'A');
atlas.ensure(key_a, &face).expect("slot");
atlas.take_dirty();
let (idx, _) = atlas.allocate(PAGE_SIZE, PAGE_SIZE);
assert_eq!(idx, 1, "page must be protected for a second tick");
assert!(atlas.slot(key_a).is_some());
atlas.take_dirty();
atlas.take_dirty();
let (idx, _) = atlas.allocate(PAGE_SIZE, PAGE_SIZE);
assert!(idx <= 1, "stale page recycles in place");
assert_eq!(atlas.pages().len(), 3 - 1, "no further growth");
}
#[test]
fn recycling_preserves_empty_advance_entries() {
let face = test_face();
let mut atlas = MsdfAtlas::default();
atlas.set_page_budget_for_tests(1);
let space = key(&face, ' ');
let key_a = key(&face, 'A');
atlas.ensure(space, &face);
atlas.ensure(key_a, &face).expect("slot");
atlas.take_dirty();
let (idx, _) = atlas.allocate(PAGE_SIZE, PAGE_SIZE);
assert_eq!(idx, 0);
assert!(atlas.slot(key_a).is_none());
assert!(atlas.advance(space).is_some());
}
#[test]
fn shelf_packer_fits_a_typical_run_in_one_page() {
let face = test_face();
let mut atlas = MsdfAtlas::default();
let font = fake_font_id(0);
for ch in "The quick brown fox jumps over the lazy dog 0123456789".chars() {
atlas.ensure(
MsdfGlyphKey {
font,
glyph_id: face.glyph_index(ch).map(|g| g.0).unwrap_or(0),
weight: 0,
},
&face,
);
}
assert_eq!(atlas.pages().len(), 1);
}
}