goud_engine/rendering/text/
atlas_cache.rs1use std::collections::hash_map::Entry;
7use std::collections::HashMap;
8
9use crate::assets::{loaders::FontAsset, AssetHandle};
10use crate::libs::graphics::backend::render_backend::RenderBackend;
11use crate::libs::graphics::backend::types::TextureHandle;
12
13use super::glyph_atlas::GlyphAtlas;
14
15#[derive(Debug)]
21pub struct GlyphAtlasCache {
22 cache: HashMap<(AssetHandle<FontAsset>, u32), GlyphAtlas>,
23 pending_destroy: Vec<TextureHandle>,
25}
26
27impl GlyphAtlasCache {
28 pub fn new() -> Self {
30 Self {
31 cache: HashMap::new(),
32 pending_destroy: Vec::new(),
33 }
34 }
35
36 pub fn get_or_create(
48 &mut self,
49 font: &FontAsset,
50 font_handle: AssetHandle<FontAsset>,
51 size_px: f32,
52 ) -> Result<&GlyphAtlas, String> {
53 let size_key = size_px.round() as u32;
54 let key = (font_handle, size_key);
55
56 if let Entry::Vacant(e) = self.cache.entry(key) {
57 let parsed_font = font.parse()?;
58 let atlas = GlyphAtlas::generate(&parsed_font, size_px)?;
59 e.insert(atlas);
60 }
61
62 self.cache
64 .get(&key)
65 .ok_or_else(|| "internal error: cache entry missing after insertion".to_string())
66 }
67
68 pub fn invalidate_font(&mut self, font_handle: AssetHandle<FontAsset>) {
74 self.cache.retain(|&(h, _), atlas| {
75 if h == font_handle {
76 if let Some(tex) = atlas.take_gpu_texture() {
77 self.pending_destroy.push(tex);
78 }
79 false
80 } else {
81 true
82 }
83 });
84 }
85
86 pub fn process_reloads(&mut self, reloaded: &[AssetHandle<FontAsset>]) -> usize {
91 let before = self.cache.len();
92 for &handle in reloaded {
93 self.invalidate_font(handle);
94 }
95 before - self.cache.len()
96 }
97
98 pub fn destroy_gpu_textures(&mut self, backend: &mut dyn RenderBackend) {
102 for handle in self.pending_destroy.drain(..) {
103 backend.destroy_texture(handle);
104 }
105 }
106
107 pub fn pending_destroy_count(&self) -> usize {
109 self.pending_destroy.len()
110 }
111
112 pub fn clear(&mut self) {
114 for atlas in self.cache.values_mut() {
115 if let Some(tex) = atlas.take_gpu_texture() {
116 self.pending_destroy.push(tex);
117 }
118 }
119 self.cache.clear();
120 }
121
122 #[cfg(test)]
124 fn len(&self) -> usize {
125 self.cache.len()
126 }
127}
128
129impl Default for GlyphAtlasCache {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::assets::loaders::font::format::FontFormat;
139 use crate::assets::loaders::FontAsset;
140 use crate::assets::loaders::FontStyle;
141
142 fn test_font_asset() -> FontAsset {
144 let bytes = include_bytes!("../../../test_assets/fonts/test_font.ttf").to_vec();
145 let font = fontdue::Font::from_bytes(bytes.as_slice(), fontdue::FontSettings::default())
146 .expect("parse");
147
148 FontAsset::new(
149 bytes,
150 "TestFont".to_string(),
151 FontStyle::Regular,
152 FontFormat::Ttf,
153 1000,
154 font.glyph_count() as u16,
155 0,
156 )
157 }
158
159 fn handle_a() -> AssetHandle<FontAsset> {
160 AssetHandle::new(0, 1)
161 }
162
163 fn handle_b() -> AssetHandle<FontAsset> {
164 AssetHandle::new(1, 1)
165 }
166
167 #[test]
168 fn test_cache_get_or_create_returns_atlas() {
169 let mut cache = GlyphAtlasCache::new();
170 let font = test_font_asset();
171
172 let result = cache.get_or_create(&font, handle_a(), 16.0);
173 assert!(result.is_ok());
174 assert_eq!(cache.len(), 1);
175 }
176
177 #[test]
178 fn test_cache_hit_returns_same_atlas() {
179 let mut cache = GlyphAtlasCache::new();
180 let font = test_font_asset();
181
182 let _ = cache.get_or_create(&font, handle_a(), 16.0).unwrap();
183 let _ = cache.get_or_create(&font, handle_a(), 16.0).unwrap();
184
185 assert_eq!(cache.len(), 1);
187
188 let atlas = cache.get_or_create(&font, handle_a(), 16.0).unwrap();
190 assert!(atlas.glyph_info('A').is_some());
191 }
192
193 #[test]
194 fn test_cache_different_sizes_get_different_entries() {
195 let mut cache = GlyphAtlasCache::new();
196 let font = test_font_asset();
197
198 let _ = cache.get_or_create(&font, handle_a(), 16.0).unwrap();
199 let _ = cache.get_or_create(&font, handle_a(), 32.0).unwrap();
200
201 assert_eq!(cache.len(), 2);
202 }
203
204 #[test]
205 fn test_cache_different_handles_get_different_entries() {
206 let mut cache = GlyphAtlasCache::new();
207 let font = test_font_asset();
208
209 let _ = cache.get_or_create(&font, handle_a(), 16.0).unwrap();
210 let _ = cache.get_or_create(&font, handle_b(), 16.0).unwrap();
211
212 assert_eq!(cache.len(), 2);
213 }
214
215 #[test]
216 fn test_invalidate_font_removes_all_sizes() {
217 let mut cache = GlyphAtlasCache::new();
218 let font = test_font_asset();
219
220 let _ = cache.get_or_create(&font, handle_a(), 16.0).unwrap();
221 let _ = cache.get_or_create(&font, handle_a(), 32.0).unwrap();
222 let _ = cache.get_or_create(&font, handle_b(), 16.0).unwrap();
223
224 cache.invalidate_font(handle_a());
225
226 assert_eq!(cache.len(), 1);
228 }
229
230 #[test]
231 fn test_clear_removes_all_entries() {
232 let mut cache = GlyphAtlasCache::new();
233 let font = test_font_asset();
234
235 let _ = cache.get_or_create(&font, handle_a(), 16.0).unwrap();
236 let _ = cache.get_or_create(&font, handle_b(), 24.0).unwrap();
237
238 cache.clear();
239 assert_eq!(cache.len(), 0);
240 }
241
242 #[test]
243 fn test_default_creates_empty_cache() {
244 let cache = GlyphAtlasCache::default();
245 assert_eq!(cache.len(), 0);
246 }
247
248 #[test]
249 fn test_process_reloads_invalidates_multiple_fonts() {
250 let mut cache = GlyphAtlasCache::new();
251 let font = test_font_asset();
252
253 let _ = cache.get_or_create(&font, handle_a(), 16.0).unwrap();
255 let _ = cache.get_or_create(&font, handle_a(), 32.0).unwrap();
256 let _ = cache.get_or_create(&font, handle_b(), 16.0).unwrap();
257
258 assert_eq!(cache.len(), 3);
259
260 let invalidated = cache.process_reloads(&[handle_a(), handle_b()]);
262
263 assert_eq!(invalidated, 3, "all three atlases should be invalidated");
264 assert_eq!(cache.len(), 0);
265 }
266
267 #[test]
268 fn test_process_reloads_returns_zero_for_unknown_fonts() {
269 let mut cache = GlyphAtlasCache::new();
270 let font = test_font_asset();
271
272 let _ = cache.get_or_create(&font, handle_a(), 16.0).unwrap();
273
274 let invalidated = cache.process_reloads(&[handle_b()]);
276
277 assert_eq!(invalidated, 0);
278 assert_eq!(cache.len(), 1, "existing entry should remain");
279 }
280
281 #[test]
282 fn test_process_reloads_partial_invalidation() {
283 let mut cache = GlyphAtlasCache::new();
284 let font = test_font_asset();
285
286 let _ = cache.get_or_create(&font, handle_a(), 16.0).unwrap();
287 let _ = cache.get_or_create(&font, handle_b(), 16.0).unwrap();
288
289 let invalidated = cache.process_reloads(&[handle_a()]);
290
291 assert_eq!(invalidated, 1);
292 assert_eq!(cache.len(), 1, "handle_b entry should remain");
293 }
294}