1use crate::{
2 fonts::{FontId, GlyphId},
3 geometry::{
4 rect::RectI,
5 vector::{vec2f, vec2i, Vector2F, Vector2I},
6 },
7 platform,
8};
9use etagere::BucketedAtlasAllocator;
10use metal::{MTLPixelFormat, TextureDescriptor};
11use ordered_float::OrderedFloat;
12use std::{collections::HashMap, sync::Arc};
13
14#[derive(Hash, Eq, PartialEq)]
15struct GlyphDescriptor {
16 font_id: FontId,
17 font_size: OrderedFloat<f32>,
18 glyph_id: GlyphId,
19 subpixel_variant: (u8, u8),
20}
21
22#[derive(Clone)]
23pub struct GlyphSprite {
24 pub atlas_id: usize,
25 pub atlas_origin: Vector2I,
26 pub offset: Vector2F,
27 pub size: Vector2I,
28}
29
30pub struct SpriteCache {
31 device: metal::Device,
32 atlas_size: Vector2I,
33 fonts: Arc<dyn platform::FontSystem>,
34 atlasses: Vec<Atlas>,
35 glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
36}
37
38impl SpriteCache {
39 pub fn new(
40 device: metal::Device,
41 size: Vector2I,
42 fonts: Arc<dyn platform::FontSystem>,
43 ) -> Self {
44 let atlasses = vec![Atlas::new(&device, size)];
45 Self {
46 device,
47 atlas_size: size,
48 fonts,
49 atlasses,
50 glyphs: Default::default(),
51 }
52 }
53
54 pub fn atlas_size(&self) -> Vector2I {
55 self.atlas_size
56 }
57
58 pub fn render_glyph(
59 &mut self,
60 font_id: FontId,
61 font_size: f32,
62 glyph_id: GlyphId,
63 target_position: Vector2F,
64 scale_factor: f32,
65 ) -> Option<GlyphSprite> {
66 const SUBPIXEL_VARIANTS: u8 = 4;
67
68 let target_position = target_position * scale_factor;
69 let fonts = &self.fonts;
70 let atlasses = &mut self.atlasses;
71 let atlas_size = self.atlas_size;
72 let device = &self.device;
73 let subpixel_variant = (
74 (target_position.x().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
75 % SUBPIXEL_VARIANTS,
76 (target_position.y().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
77 % SUBPIXEL_VARIANTS,
78 );
79 self.glyphs
80 .entry(GlyphDescriptor {
81 font_id,
82 font_size: OrderedFloat(font_size),
83 glyph_id,
84 subpixel_variant,
85 })
86 .or_insert_with(|| {
87 let subpixel_shift = vec2f(
88 subpixel_variant.0 as f32 / SUBPIXEL_VARIANTS as f32,
89 subpixel_variant.1 as f32 / SUBPIXEL_VARIANTS as f32,
90 );
91 let (glyph_bounds, mask) = fonts.rasterize_glyph(
92 font_id,
93 font_size,
94 glyph_id,
95 subpixel_shift,
96 scale_factor,
97 )?;
98 assert!(glyph_bounds.width() < atlas_size.x());
99 assert!(glyph_bounds.height() < atlas_size.y());
100
101 let atlas_bounds = atlasses
102 .last_mut()
103 .unwrap()
104 .try_insert(glyph_bounds.size(), &mask)
105 .unwrap_or_else(|| {
106 let mut atlas = Atlas::new(device, atlas_size);
107 let bounds = atlas.try_insert(glyph_bounds.size(), &mask).unwrap();
108 atlasses.push(atlas);
109 bounds
110 });
111
112 // Snap sprite to pixel grid.
113 let offset = glyph_bounds.origin().to_f32()
114 - vec2f(target_position.x().fract(), target_position.y().fract());
115
116 Some(GlyphSprite {
117 atlas_id: atlasses.len() - 1,
118 atlas_origin: atlas_bounds.origin(),
119 offset,
120 size: glyph_bounds.size(),
121 })
122 })
123 .clone()
124 }
125
126 pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
127 self.atlasses.get(atlas_id).map(|a| a.texture.as_ref())
128 }
129}
130
131struct Atlas {
132 allocator: BucketedAtlasAllocator,
133 texture: metal::Texture,
134}
135
136impl Atlas {
137 fn new(device: &metal::DeviceRef, size: Vector2I) -> Self {
138 let descriptor = TextureDescriptor::new();
139 descriptor.set_pixel_format(MTLPixelFormat::A8Unorm);
140 descriptor.set_width(size.x() as u64);
141 descriptor.set_height(size.y() as u64);
142
143 Self {
144 allocator: BucketedAtlasAllocator::new(etagere::Size::new(size.x(), size.y())),
145 texture: device.new_texture(&descriptor),
146 }
147 }
148
149 fn try_insert(&mut self, size: Vector2I, mask: &[u8]) -> Option<RectI> {
150 let allocation = self
151 .allocator
152 .allocate(etagere::size2(size.x() + 1, size.y() + 1))?;
153
154 let bounds = allocation.rectangle;
155 let region = metal::MTLRegion::new_2d(
156 bounds.min.x as u64,
157 bounds.min.y as u64,
158 size.x() as u64,
159 size.y() as u64,
160 );
161 self.texture
162 .replace_region(region, 0, mask.as_ptr() as *const _, size.x() as u64);
163 Some(RectI::from_points(
164 vec2i(bounds.min.x, bounds.min.y),
165 vec2i(bounds.max.x, bounds.max.y),
166 ))
167 }
168}