Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-renderer/src/glyph_atlas.rs
StratoSDK / crates / strato-renderer / src / glyph_atlas.rs
1//! Glyph atlas system for efficient text rendering
2//!
3//! This module provides a texture atlas system that packs glyph bitmaps into larger textures
4//! for efficient GPU rendering. It uses cosmic-text for glyph rasterization and manages
5//! texture space allocation using a simple bin-packing algorithm.
6 
7use crate::font_config::create_safe_font_system;
8use crate::text::Font;
9use cosmic_text::{CacheKey, FontSystem, SwashCache};
10use std::collections::HashMap;
11use std::sync::{Arc, Mutex};
12use strato_core::types::Color;
13 
14/// Represents a glyph in the atlas
15#[derive(Debug, Clone, Copy)]
16pub struct GlyphInfo {
17 /// UV coordinates in the atlas texture (normalized 0.0-1.0)
18 pub uv_rect: (f32, f32, f32, f32), // (u_min, v_min, u_max, v_max)
19 /// Size of the glyph in pixels
20 pub size: (u32, u32),
21 /// Offset for positioning the glyph
22 pub bearing: (i32, i32),
23 /// How much to advance after this glyph
24 pub advance: f32,
25}
26 
27/// A texture atlas that contains multiple glyphs
28pub struct GlyphAtlas {
29 /// The atlas texture data (grayscale)
30 texture_data: Vec<u8>,
31 /// Width and height of the atlas texture
32 dimensions: (u32, u32),
33 /// Current allocation position (simple top-to-bottom, left-to-right packing)
34 current_row_y: u32,
35 current_x: u32,
36 current_row_height: u32,
37 /// Map from CacheKey to glyph info
38 glyph_map: HashMap<CacheKey, GlyphInfo>,
39 /// Whether the atlas has been updated and needs to be uploaded to GPU
40 dirty: bool,
41}
42 
43impl GlyphAtlas {
44 /// Create a new glyph atlas
45 pub fn new(width: u32, height: u32) -> Self {
46 Self {
47 texture_data: vec![0; (width * height) as usize],
48 dimensions: (width, height),
49 current_row_y: 0,
50 current_x: 0,
51 current_row_height: 0,
52 glyph_map: HashMap::new(),
53 dirty: false,
54 }
55 }
56 
57 /// Add a glyph to the atlas
58 pub fn add_glyph(
59 &mut self,
60 cache_key: CacheKey,
61 glyph_bitmap: &[u8],
62 size: (u32, u32),
63 bearing: (i32, i32),
64 advance: f32,
65 ) -> Option<GlyphInfo> {
66 let (glyph_width, glyph_height) = size;
67 
68 // Check if glyph is already in atlas
69 if let Some(info) = self.glyph_map.get(&cache_key) {
70 return Some(*info);
71 }
72 
73 // Check if we have space in current row
74 if self.current_x + glyph_width > self.dimensions.0 {
75 // Move to next row
76 self.current_row_y += self.current_row_height;
77 self.current_x = 0;
78 self.current_row_height = 0;
79 }
80 
81 // Check if we have vertical space
82 if self.current_row_y + glyph_height > self.dimensions.1 {
83 // Atlas is full
84 return None;
85 }
86 
87 // Copy glyph data to atlas
88 let atlas_x = self.current_x;
89 let atlas_y = self.current_row_y;
90 
91 for y in 0..glyph_height {
92 for x in 0..glyph_width {
93 let src_idx = (y * glyph_width + x) as usize;
94 let dst_idx = ((atlas_y + y) * self.dimensions.0 + (atlas_x + x)) as usize;
95 if src_idx < glyph_bitmap.len() && dst_idx < self.texture_data.len() {
96 self.texture_data[dst_idx] = glyph_bitmap[src_idx];
97 }
98 }
99 }
100 
101 // Calculate UV coordinates
102 let u_min = atlas_x as f32 / self.dimensions.0 as f32;
103 let v_min = atlas_y as f32 / self.dimensions.1 as f32;
104 let u_max = (atlas_x + glyph_width) as f32 / self.dimensions.0 as f32;
105 let v_max = (atlas_y + glyph_height) as f32 / self.dimensions.1 as f32;
106 
107 let glyph_info = GlyphInfo {
108 uv_rect: (u_min, v_min, u_max, v_max),
109 size,
110 bearing,
111 advance,
112 };
113 
114 // Update atlas state
115 self.current_x += glyph_width;
116 self.current_row_height = self.current_row_height.max(glyph_height);
117 self.glyph_map.insert(cache_key, glyph_info);
118 self.dirty = true;
119 
120 Some(glyph_info)
121 }
122 
123 /// Get glyph info if it exists in the atlas
124 pub fn get_glyph(&self, cache_key: CacheKey) -> Option<GlyphInfo> {
125 self.glyph_map.get(&cache_key).copied()
126 }
127 
128 /// Get the atlas texture data
129 pub fn texture_data(&self) -> &[u8] {
130 &self.texture_data
131 }
132 
133 /// Get atlas dimensions
134 pub fn dimensions(&self) -> (u32, u32) {
135 self.dimensions
136 }
137 
138 /// Check if atlas needs to be uploaded to GPU
139 pub fn is_dirty(&self) -> bool {
140 self.dirty
141 }
142 
143 /// Mark atlas as clean (after GPU upload)
144 pub fn mark_clean(&mut self) {
145 self.dirty = false;
146 }
147 
148 /// Clear the atlas
149 pub fn clear(&mut self) {
150 self.texture_data.fill(0);
151 self.current_row_y = 0;
152 self.current_x = 0;
153 self.current_row_height = 0;
154 self.glyph_map.clear();
155 self.dirty = true;
156 }
157 
158 /// Get usage statistics
159 pub fn get_usage_stats(&self) -> (f32, u32) {
160 let used_pixels = self.current_row_y * self.dimensions.0 + self.current_x;
161 let total_pixels = self.dimensions.0 * self.dimensions.1;
162 let usage_percentage = used_pixels as f32 / total_pixels as f32 * 100.0;
163 (usage_percentage, self.glyph_map.len() as u32)
164 }
165}
166 
167/// Manager for multiple glyph atlases
168pub struct GlyphAtlasManager {
169 atlases: Vec<GlyphAtlas>,
170 atlas_size: (u32, u32),
171}
172 
173impl GlyphAtlasManager {
174 /// Create a new glyph atlas manager
175 pub fn new(atlas_size: (u32, u32)) -> Self {
176 Self {
177 atlases: vec![GlyphAtlas::new(atlas_size.0, atlas_size.1)],
178 atlas_size,
179 }
180 }
181 
182 /// Get or create a glyph in an atlas
183 pub fn get_or_create_glyph(
184 &mut self,
185 font_system: &mut FontSystem,
186 swash_cache: &mut SwashCache,
187 cache_key: CacheKey,
188 ) -> Option<(usize, GlyphInfo)> {
189 // Check existing atlases first
190 for (atlas_idx, atlas) in self.atlases.iter().enumerate() {
191 if let Some(info) = atlas.get_glyph(cache_key) {
192 return Some((atlas_idx, info));
193 }
194 }
195 
196 // Need to rasterize the glyph
197 // Rasterize using swash
198 let image = swash_cache
199 .get_image(font_system, cache_key)
200 .as_ref()
201 .cloned()?;
202 
203 let glyph_width = image.placement.width;
204 let glyph_height = image.placement.height;
205 let bearing_x = image.placement.left;
206 let bearing_y = image.placement.top;
207 
208 // Convert content to alpha mask (if it's not already?)
209 // swash_cache.get_image returns image data. cosmic-text uses Format::Alpha usually?
210 // Let's check image.content.
211 
212 let glyph_bitmap = match image.content {
213 cosmic_text::SwashContent::Mask => image.data,
214 cosmic_text::SwashContent::SubpixelMask => {
215 // Convert subpixel to standard alpha? Or just use it?
216 // For now assume we handle it as alpha or it's handled by shader
217 // We'll take every 3rd byte or average?
218 // For simplicity, let's just take it as is, but it might be 3x wider?
219 // No, cosmic-text handles this.
220 image.data
221 }
222 cosmic_text::SwashContent::Color => {
223 // Color emoji etc. Not supported in our simple atlas yet (grayscale).
224 return None;
225 }
226 };
227 
228 // Try to add to existing atlases
229 for (atlas_idx, atlas) in self.atlases.iter_mut().enumerate() {
230 if let Some(info) = atlas.add_glyph(
231 cache_key,
232 &glyph_bitmap,
233 (glyph_width, glyph_height),
234 (bearing_x, bearing_y),
235 0.0, // Advance is handled by layout run, we store 0 or don't use it in vertex gen
236 ) {
237 return Some((atlas_idx, info));
238 }
239 }
240 
241 // Create new atlas if needed
242 let mut new_atlas = GlyphAtlas::new(self.atlas_size.0, self.atlas_size.1);
243 if let Some(info) = new_atlas.add_glyph(
244 cache_key,
245 &glyph_bitmap,
246 (glyph_width, glyph_height),
247 (bearing_x, bearing_y),
248 0.0,
249 ) {
250 let atlas_idx = self.atlases.len();
251 self.atlases.push(new_atlas);
252 return Some((atlas_idx, info));
253 }
254 
255 None
256 }
257 
258 /// Get an atlas by index
259 pub fn get_atlas(&self, index: usize) -> Option<&GlyphAtlas> {
260 self.atlases.get(index)
261 }
262 
263 /// Get a mutable atlas by index
264 pub fn get_atlas_mut(&mut self, index: usize) -> Option<&mut GlyphAtlas> {
265 self.atlases.get_mut(index)
266 }
267 
268 /// Get the number of atlases
269 pub fn atlas_count(&self) -> usize {
270 self.atlases.len()
271 }
272}
273 
274impl Default for GlyphAtlasManager {
275 fn default() -> Self {
276 Self::new((1024, 1024))
277 }
278}
279 
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use cosmic_text::{Attrs, Buffer, Metrics, Shaping};
284 
285 #[test]
286 fn test_atlas_creation() {
287 let atlas = GlyphAtlas::new(256, 256);
288 assert_eq!(atlas.dimensions(), (256, 256));
289 assert!(!atlas.is_dirty());
290 }
291 
292 #[test]
293 fn test_glyph_addition() {
294 let mut atlas = GlyphAtlas::new(256, 256);
295 
296 let mut font_system = FontSystem::new();
297 let mut swash_cache = SwashCache::new();
298 
299 let metrics = Metrics::new(16.0, 20.0);
300 let mut buffer = Buffer::new(&mut font_system, metrics);
301 buffer.set_text(&mut font_system, "A", Attrs::new(), Shaping::Advanced);
302 buffer.shape_until_scroll(&mut font_system, false);
303 
304 let mut cache_key_opt = None;
305 for run in buffer.layout_runs() {
306 for glyph in run.glyphs.iter() {
307 let physical = glyph.physical((0.0, 0.0), 1.0);
308 cache_key_opt = Some(physical.cache_key);
309 break;
310 }
311 if cache_key_opt.is_some() {
312 break;
313 }
314 }
315 
316 let cache_key = cache_key_opt.expect("Failed to obtain glyph cache key");
317 
318 let image = swash_cache
319 .get_image(&mut font_system, cache_key)
320 .as_ref()
321 .cloned()
322 .expect("Failed to rasterize glyph image");
323 
324 let glyph_width = image.placement.width;
325 let glyph_height = image.placement.height;
326 let bearing_x = image.placement.left;
327 let bearing_y = image.placement.top;
328 let glyph_bitmap = image.data;
329 
330 let info = atlas.add_glyph(
331 cache_key,
332 &glyph_bitmap,
333 (glyph_width, glyph_height),
334 (bearing_x, bearing_y),
335 0.0,
336 );
337 
338 assert!(info.is_some());
339 assert!(atlas.is_dirty());
340 
341 let info = info.unwrap();
342 assert_eq!(
343 info.uv_rect,
344 (
345 0.0,
346 0.0,
347 glyph_width as f32 / 256.0,
348 glyph_height as f32 / 256.0
349 )
350 );
351 }
352 
353 #[test]
354 fn test_atlas_manager() {
355 let manager = GlyphAtlasManager::new((256, 256));
356 assert_eq!(manager.atlas_count(), 1);
357 }
358}
359