StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 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 | |
| 7 | use crate::font_config::create_safe_font_system; |
| 8 | use crate::text::Font; |
| 9 | use cosmic_text::{CacheKey, FontSystem, SwashCache}; |
| 10 | use std::collections::HashMap; |
| 11 | use std::sync::{Arc, Mutex}; |
| 12 | use strato_core::types::Color; |
| 13 | |
| 14 | /// Represents a glyph in the atlas |
| 15 | #[derive(Debug, Clone, Copy)] |
| 16 | pub 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 |
| 28 | pub 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 | |
| 43 | impl 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 |
| 168 | pub struct GlyphAtlasManager { |
| 169 | atlases: Vec<GlyphAtlas>, |
| 170 | atlas_size: (u32, u32), |
| 171 | } |
| 172 | |
| 173 | impl 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 | |
| 274 | impl Default for GlyphAtlasManager { |
| 275 | fn default() -> Self { |
| 276 | Self::new((1024, 1024)) |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | #[cfg(test)] |
| 281 | mod 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 |