1use std::{collections::HashMap, sync::Arc};
2
3use crate::{
4 fonts::{FontId, GlyphId},
5 geometry::{
6 rect::RectI,
7 vector::{vec2i, Vector2I},
8 },
9 FontCache,
10};
11use etagere::BucketedAtlasAllocator;
12use metal::{MTLPixelFormat, TextureDescriptor};
13use ordered_float::OrderedFloat;
14
15#[derive(Hash, Eq, PartialEq)]
16struct GlyphDescriptor {
17 font_id: FontId,
18 font_size: OrderedFloat<f32>,
19 glyph_id: GlyphId,
20}
21
22pub struct SpriteCache {
23 device: metal::Device,
24 atlas_size: Vector2I,
25 font_cache: Arc<FontCache>,
26 atlasses: Vec<Atlas>,
27 glyphs: HashMap<GlyphDescriptor, Option<(usize, RectI)>>,
28}
29
30impl SpriteCache {
31 pub fn new(device: metal::Device, size: Vector2I, font_cache: Arc<FontCache>) -> Self {
32 let atlasses = vec![Atlas::new(&device, size)];
33 Self {
34 device,
35 atlas_size: size,
36 font_cache,
37 atlasses,
38 glyphs: Default::default(),
39 }
40 }
41
42 pub fn render_glyph(
43 &mut self,
44 font_id: FontId,
45 font_size: f32,
46 glyph_id: GlyphId,
47 scale_factor: f32,
48 ) -> Option<(usize, RectI)> {
49 let font_cache = &self.font_cache;
50 let atlasses = &mut self.atlasses;
51 let atlas_size = self.atlas_size;
52 let device = &self.device;
53 self.glyphs
54 .entry(GlyphDescriptor {
55 font_id,
56 font_size: OrderedFloat(font_size),
57 glyph_id,
58 })
59 .or_insert_with(|| {
60 let (size, mask) =
61 font_cache.render_glyph(font_id, font_size, glyph_id, scale_factor)?;
62 assert!(size.x() < atlas_size.x());
63 assert!(size.y() < atlas_size.y());
64
65 let atlas = atlasses.last_mut().unwrap();
66 if let Some(bounds) = atlas.try_insert(size, &mask) {
67 Some((atlasses.len() - 1, bounds))
68 } else {
69 let mut atlas = Atlas::new(device, atlas_size);
70 let bounds = atlas.try_insert(size, &mask).unwrap();
71 atlasses.push(atlas);
72 Some((atlasses.len() - 1, bounds))
73 }
74 })
75 .clone()
76 }
77
78 pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
79 self.atlasses.get(atlas_id).map(|a| a.texture.as_ref())
80 }
81}
82
83struct Atlas {
84 allocator: BucketedAtlasAllocator,
85 texture: metal::Texture,
86}
87
88impl Atlas {
89 fn new(device: &metal::DeviceRef, size: Vector2I) -> Self {
90 let descriptor = TextureDescriptor::new();
91 descriptor.set_pixel_format(MTLPixelFormat::A8Unorm);
92 descriptor.set_width(size.x() as u64);
93 descriptor.set_height(size.y() as u64);
94
95 Self {
96 allocator: BucketedAtlasAllocator::new(etagere::Size::new(size.x(), size.y())),
97 texture: device.new_texture(&descriptor),
98 }
99 }
100
101 fn try_insert(&mut self, size: Vector2I, mask: &[u8]) -> Option<RectI> {
102 let allocation = self
103 .allocator
104 .allocate(etagere::size2(size.x(), size.y()))?;
105
106 let bounds = allocation.rectangle;
107 let region = metal::MTLRegion::new_2d(
108 bounds.min.x as u64,
109 bounds.min.y as u64,
110 bounds.width() as u64,
111 bounds.height() as u64,
112 );
113 self.texture
114 .replace_region(region, 0, mask.as_ptr() as *const _, size.x() as u64);
115 Some(RectI::from_points(
116 vec2i(bounds.min.x, bounds.min.y),
117 vec2i(bounds.max.x, bounds.max.y),
118 ))
119 }
120}