use crate::compat::{HashMap, Rc};
use super::glyph_source::{paint_active, Cell, InkKind};
use super::raster::rasterize_face_at_weight;
use super::shaping::face_for_family;
pub const ATLAS_GC_FRAMES: u64 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GlyphKey {
pub face: &'static str,
pub ch: char,
pub size_px: u8,
pub cell_w: u32,
pub cell_h: u32,
pub weight: u16,
pub italic: bool,
}
impl GlyphKey {
pub const fn new(face: &'static str, ch: char, size_px: u8, weight: u16) -> Self {
Self {
face,
ch,
size_px,
cell_w: size_px as u32,
cell_h: size_px as u32,
weight,
italic: false,
}
}
pub const fn with_cell(mut self, width: u32, height: u32) -> Self {
self.cell_w = width;
self.cell_h = height;
self
}
pub const fn with_italic(mut self, italic: bool) -> Self {
self.italic = italic;
self
}
pub const fn cell(self) -> Cell {
Cell::new(self.cell_w, self.cell_h)
}
}
#[derive(Debug, Clone)]
pub struct AtlasSlot {
width: u32,
height: u32,
bitmap: Rc<[u8]>,
source: &'static str,
ink: InkKind,
hits: u64,
last_epoch: u64,
}
impl AtlasSlot {
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn bitmap(&self) -> &[u8] {
&self.bitmap
}
pub fn source(&self) -> &'static str {
self.source
}
pub fn ink(&self) -> InkKind {
self.ink
}
pub fn hits(&self) -> u64 {
self.hits
}
}
#[derive(Debug)]
pub struct GlyphAtlas {
slots: HashMap<GlyphKey, AtlasSlot>,
epoch: u64,
#[cfg(test)]
rasterisations: u64,
}
impl GlyphAtlas {
pub fn new() -> Self {
Self {
slots: HashMap::new(),
epoch: 0,
#[cfg(test)]
rasterisations: 0,
}
}
pub fn epoch(&self) -> u64 {
self.epoch
}
pub fn slot_count(&self) -> usize {
self.slots.len()
}
pub fn contains(&self, key: &GlyphKey) -> bool {
self.slots.contains_key(key)
}
pub fn get_or_raster(&mut self, key: GlyphKey) -> AtlasSlot {
if let Some(slot) = self.slots.get_mut(&key) {
slot.hits = slot.hits.saturating_add(1);
slot.last_epoch = self.epoch;
return slot.clone();
}
#[cfg(test)]
{
self.rasterisations = self.rasterisations.saturating_add(1);
}
let slot = rasterise(key, self.epoch);
self.slots.insert(key, slot.clone());
slot
}
pub fn begin_frame(&mut self) {
self.epoch = self.epoch.wrapping_add(1);
let epoch = self.epoch;
self.slots.retain(|_, slot| epoch.saturating_sub(slot.last_epoch) <= ATLAS_GC_FRAMES);
}
#[cfg(test)]
fn rasterisations(&self) -> u64 {
self.rasterisations
}
}
impl Default for GlyphAtlas {
fn default() -> Self {
Self::new()
}
}
fn rasterise(key: GlyphKey, epoch: u64) -> AtlasSlot {
let cell = key.cell();
if let Some(face) = face_for_family(key.face) {
if let Some(bitmap) = rasterize_face_at_weight(face, key.ch, cell, key.weight) {
let bitmap = oblique_if_requested_shared(bitmap, cell, key.italic);
return AtlasSlot {
width: cell.width,
height: cell.height,
bitmap,
source: face.name,
ink: InkKind::Coverage,
hits: 0,
last_epoch: epoch,
};
}
}
let mut out = crate::compat::vec![0u8; cell.area()];
let painted = paint_active(key.ch, cell, &mut out);
let (source, ink) = match painted {
Some(painted) => (painted.source, painted.ink),
None => (key.face, InkKind::Coverage),
};
AtlasSlot {
width: cell.width,
height: cell.height,
bitmap: Rc::from(oblique_if_requested(out, cell, key.italic)),
source,
ink,
hits: 0,
last_epoch: epoch,
}
}
fn oblique_if_requested_shared(bitmap: Rc<[u8]>, cell: Cell, italic: bool) -> Rc<[u8]> {
if !italic {
return bitmap;
}
Rc::from(oblique_if_requested(bitmap.to_vec(), cell, true))
}
fn oblique_if_requested(bitmap: Vec<u8>, cell: Cell, italic: bool) -> Vec<u8> {
if !italic || cell.is_empty() || bitmap.len() < cell.area() {
return bitmap;
}
let width = cell.width as i32;
let height = cell.height as i32;
let mut slanted = crate::compat::vec![0u8; bitmap.len()];
for py in 0..height {
let shift = (((height as f32 * 0.5) - py as f32) * 0.2).round() as i32;
for px in 0..width {
let source_px = px - shift;
if source_px < 0 || source_px >= width {
continue;
}
let source = (py * width + source_px) as usize;
let destination = (py * width + px) as usize;
slanted[destination] = bitmap[source];
}
}
slanted
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::{vec, Rc};
use crate::render::text::{paint_active, Cell};
#[test]
fn italic_is_part_of_the_key_and_slants_the_bitmap() {
let upright = GlyphKey::new("face", 'A', 8, 400);
let italic = upright.with_italic(true);
assert_ne!(upright, italic, "italic must not alias the upright cache entry");
let cell = Cell::new(8, 8);
let mut source = vec![0u8; cell.area()];
source[1] = 255;
let slanted = oblique_if_requested(source.clone(), cell, true);
assert_eq!(slanted[1], 0, "the top row moved horizontally");
assert!(slanted.contains(&255), "the ink is still present");
assert_eq!(
oblique_if_requested(source.clone(), cell, false),
source,
"an upright request must not alter the bytes"
);
}
#[test]
fn a_non_square_cell_is_a_distinct_key_and_matches_the_uncached_path() {
let square = GlyphKey::new("face", 'A', 16, 400);
let tall = square.with_cell(8, 16);
assert_ne!(square, tall, "a non-square cell must not alias the square entry");
let cell = tall.cell();
assert_eq!((cell.width, cell.height), (8, 16), "the explicit cell is the rasterised box");
let mut expected = vec![0u8; cell.area()];
let _ = paint_active('A', cell, &mut expected);
let mut atlas = GlyphAtlas::new();
let slot = atlas.get_or_raster(tall);
assert_eq!(slot.bitmap(), &expected[..], "atlas bytes differ for a non-square cell");
}
fn key_for(ch: char, size_px: u8) -> GlyphKey {
let cell = Cell::new(u32::from(size_px), u32::from(size_px));
let mut scratch = vec![0u8; cell.area()];
let face =
paint_active(ch, cell, &mut scratch).map(|painted| painted.source).unwrap_or("font8x8");
GlyphKey::new(face, ch, size_px, 400)
}
#[test]
fn a_repeated_lookup_returns_the_same_bitmap_and_counts_the_hit() {
let mut atlas = GlyphAtlas::new();
let key = key_for('A', 16);
let first = atlas.get_or_raster(key);
let second = atlas.get_or_raster(key);
assert_eq!(first.bitmap(), second.bitmap(), "a hit must return the cached bytes");
assert!(
Rc::ptr_eq(&first.bitmap, &second.bitmap),
"a hit must share the buffer rather than copy it"
);
assert_eq!(first.hits(), 0, "the first request rasterises; it is not a hit");
assert_eq!(second.hits(), 1, "the second request is one hit");
assert_eq!(atlas.rasterisations(), 1, "only the first request reaches the rasteriser");
}
#[test]
fn a_second_frame_of_the_same_text_rasterises_nothing() {
let mut atlas = GlyphAtlas::new();
let keys: crate::compat::Vec<GlyphKey> =
"Hello 世界".chars().map(|ch| key_for(ch, 16)).collect();
for key in keys.iter().copied() {
atlas.get_or_raster(key);
}
let first_frame = atlas.rasterisations();
assert_eq!(
first_frame as usize,
atlas.slot_count(),
"one rasterisation per distinct glyph"
);
atlas.begin_frame();
for key in keys.iter().copied() {
atlas.get_or_raster(key);
}
assert_eq!(
atlas.rasterisations(),
first_frame,
"the second frame must be served entirely from the atlas"
);
}
#[test]
fn garbage_collection_keeps_a_slot_hit_in_the_current_frame() {
let mut atlas = GlyphAtlas::new();
let key = key_for('A', 16);
atlas.begin_frame();
atlas.get_or_raster(key);
assert!(atlas.contains(&key), "the request should have cached the slot");
atlas.begin_frame();
assert!(atlas.contains(&key), "a slot hit in the current frame must not be collected");
for _ in 0..=ATLAS_GC_FRAMES {
atlas.begin_frame();
}
assert!(!atlas.contains(&key), "a slot untouched past the window must be recycled");
}
#[test]
fn the_atlas_matches_the_uncached_path_for_mixed_text() {
let mut atlas = GlyphAtlas::new();
for ch in "Aé 中あ".chars() {
let key = key_for(ch, 16);
let cell = key.cell();
let mut expected = vec![0u8; cell.area()];
let painted = paint_active(ch, cell, &mut expected);
let slot = atlas.get_or_raster(key);
assert_eq!(
slot.bitmap(),
&expected[..],
"atlas bytes differ from the uncached path for U+{:04X} ({ch:?})",
ch as u32
);
assert_eq!(
Some(slot.source()),
painted.map(|painted| painted.source),
"atlas and uncached path disagree about the source for U+{:04X}",
ch as u32
);
}
}
}