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/gpu/texture_mgr.rs
StratoSDK / crates / strato-renderer / src / gpu / texture_mgr.rs
1//! Texture management for GPU rendering
2//!
3//! BLOCCO 8: Texture Management
4//! Handles texture atlas creation, glyph caching, and texture binding
5 
6use anyhow::Result;
7use std::collections::HashMap;
8use wgpu::{
9 AddressMode, Device, Extent3d, FilterMode, Queue, Sampler, SamplerDescriptor, Texture,
10 TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView,
11};
12 
13/// Glyph metrics for positioning and layout
14#[derive(Debug, Clone, Copy)]
15pub struct GlyphMetrics {
16 pub width: u32,
17 pub height: u32,
18 pub bearing_x: i32,
19 pub bearing_y: i32,
20 pub advance: f32,
21}
22 
23/// Cached glyph with atlas location and UV coordinates
24#[derive(Debug, Clone)]
25pub struct CachedGlyph {
26 pub metrics: GlyphMetrics,
27 pub uv_rect: (f32, f32, f32, f32), // (u0, v0, u1, v1)
28 pub atlas_region: (u32, u32, u32, u32), // (x, y, w, h)
29}
30 
31/// Cached image with atlas location and UV coordinates
32#[derive(Debug, Clone)]
33pub struct CachedImage {
34 pub uv_rect: (f32, f32, f32, f32), // (u0, v0, u1, v1)
35 pub atlas_region: (u32, u32, u32, u32), // (x, y, w, h)
36}
37 
38/// Key for glyph cache lookup
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub struct GlyphKey {
41 pub character: char,
42 pub font_size: u32, // Size in pixels
43}
44 
45/// Glyph cache for fast lookup
46pub struct GlyphCache {
47 glyphs: HashMap<GlyphKey, CachedGlyph>,
48}
49 
50impl GlyphCache {
51 pub fn new() -> Self {
52 Self {
53 glyphs: HashMap::new(),
54 }
55 }
56 
57 pub fn get(&self, key: &GlyphKey) -> Option<&CachedGlyph> {
58 self.glyphs.get(key)
59 }
60 
61 pub fn insert(&mut self, key: GlyphKey, glyph: CachedGlyph) {
62 self.glyphs.insert(key, glyph);
63 }
64 
65 pub fn len(&self) -> usize {
66 self.glyphs.len()
67 }
68 
69 pub fn is_empty(&self) -> bool {
70 self.glyphs.is_empty()
71 }
72}
73 
74/// Glyph rasterizer using fontdue
75pub struct GlyphRasterizer {
76 pub font: fontdue::Font,
77}
78 
79impl GlyphRasterizer {
80 /// Create new glyph rasterizer with embedded Segoe UI font
81 pub fn new() -> Result<Self> {
82 // Embed Segoe UI Italic font (path from crates/strato-renderer/src/gpu/ to root/font/)
83 const FONT_DATA: &[u8] = include_bytes!("../../../../font/segoeuithis.ttf");
84 
85 let font = fontdue::Font::from_bytes(FONT_DATA, fontdue::FontSettings::default())
86 .map_err(|e| anyhow::anyhow!("Failed to load font: {}", e))?;
87 
88 println!("=== GLYPH RASTERIZER INITIALIZED ===");
89 
90 Ok(Self { font })
91 }
92 
93 /// Rasterize a character at given size
94 pub fn rasterize(&self, character: char, size: f32) -> Option<(Vec<u8>, GlyphMetrics)> {
95 let (metrics, bitmap) = self.font.rasterize(character, size);
96 
97 if metrics.width == 0 || metrics.height == 0 {
98 return None;
99 }
100 
101 // Convert grayscale to RGBA
102 let rgba_data: Vec<u8> = bitmap
103 .iter()
104 .flat_map(|&alpha| [255u8, 255, 255, alpha])
105 .collect();
106 
107 let glyph_metrics = GlyphMetrics {
108 width: metrics.width as u32,
109 height: metrics.height as u32,
110 bearing_x: metrics.xmin,
111 bearing_y: metrics.ymin + metrics.height as i32,
112 advance: metrics.advance_width,
113 };
114 
115 Some((rgba_data, glyph_metrics))
116 }
117}
118 
119/// Texture atlas for efficient texture management
120pub struct TextureAtlas {
121 texture: Texture,
122 texture_view: TextureView,
123 sampler: Sampler,
124 width: u32,
125 height: u32,
126 format: TextureFormat,
127 // Allocation tracking
128 current_x: u32,
129 current_y: u32,
130 row_height: u32,
131}
132 
133impl TextureAtlas {
134 /// Create new texture atlas
135 ///
136 /// # Arguments
137 /// * `device` - GPU device
138 /// * `width` - Atlas width
139 /// * `height` - Atlas height
140 pub fn new(device: &Device, width: u32, height: u32) -> Self {
141 let size = Extent3d {
142 width,
143 height,
144 depth_or_array_layers: 1,
145 };
146 
147 let format = TextureFormat::Rgba8UnormSrgb;
148 
149 let texture = device.create_texture(&TextureDescriptor {
150 label: Some("Texture Atlas"),
151 size,
152 mip_level_count: 1,
153 sample_count: 1,
154 dimension: TextureDimension::D2,
155 format,
156 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
157 view_formats: &[],
158 });
159 
160 let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
161 
162 let sampler = device.create_sampler(&SamplerDescriptor {
163 label: Some("Texture Atlas Sampler"),
164 address_mode_u: AddressMode::ClampToEdge,
165 address_mode_v: AddressMode::ClampToEdge,
166 address_mode_w: AddressMode::ClampToEdge,
167 mag_filter: FilterMode::Linear,
168 min_filter: FilterMode::Linear,
169 mipmap_filter: FilterMode::Nearest,
170 ..Default::default()
171 });
172 
173 println!("=== TEXTURE ATLAS CREATED ===");
174 println!("Size: {}x{}", width, height);
175 println!("Format: {:?}", format);
176 println!("=============================");
177 
178 Self {
179 texture,
180 texture_view,
181 sampler,
182 width,
183 height,
184 format,
185 current_x: 0,
186 current_y: 0,
187 row_height: 0,
188 }
189 }
190 
191 /// Allocate region in atlas for a glyph (simple shelf-packing)
192 pub fn allocate_region(&mut self, width: u32, height: u32) -> Option<(u32, u32)> {
193 // Check if glyph fits in current row
194 if self.current_x + width > self.width {
195 // Move to next row
196 self.current_x = 0;
197 self.current_y += self.row_height;
198 self.row_height = 0;
199 }
200 
201 // Check if we have vertical space
202 if self.current_y + height > self.height {
203 return None; // Atlas full
204 }
205 
206 let x = self.current_x;
207 let y = self.current_y;
208 
209 // Update allocation state
210 self.current_x += width;
211 self.row_height = self.row_height.max(height);
212 
213 Some((x, y))
214 }
215 
216 /// Upload texture data to a region of the atlas
217 ///
218 /// # Arguments
219 /// * `queue` - GPU queue
220 /// * `data` - RGBA8 pixel data
221 /// * `x` - X offset in atlas
222 /// * `y` - Y offset in atlas
223 /// * `width` - Region width
224 /// * `height` - Region height
225 pub fn upload_region(
226 &self,
227 queue: &Queue,
228 data: &[u8],
229 x: u32,
230 y: u32,
231 width: u32,
232 height: u32,
233 ) -> Result<()> {
234 queue.write_texture(
235 wgpu::ImageCopyTexture {
236 texture: &self.texture,
237 mip_level: 0,
238 origin: wgpu::Origin3d { x, y, z: 0 },
239 aspect: wgpu::TextureAspect::All,
240 },
241 data,
242 wgpu::ImageDataLayout {
243 offset: 0,
244 bytes_per_row: Some(4 * width),
245 rows_per_image: Some(height),
246 },
247 Extent3d {
248 width,
249 height,
250 depth_or_array_layers: 1,
251 },
252 );
253 
254 Ok(())
255 }
256 
257 /// Reserve a 1x1 white pixel at (0,0) for solid color rendering
258 pub fn reserve_white_pixel(&mut self, queue: &Queue) {
259 let white_pixel = [255u8, 255, 255, 255];
260 self.upload_region(queue, &white_pixel, 0, 0, 1, 1)
261 .expect("Failed to upload white pixel");
262 
263 // Advance allocator to skip this pixel
264 // We'll just advance by a small amount to keep it simple, e.g., move to x=1
265 // effectively reserving the first pixel of the first row
266 self.current_x = 1;
267 self.row_height = 1;
268 }
269 
270 /// Create a default 1x1 white texture for solid color rendering
271 pub fn create_default_white(device: &Device, queue: &Queue) -> Self {
272 let mut atlas = Self::new(device, 1, 1);
273 atlas.reserve_white_pixel(queue);
274 atlas
275 }
276 
277 /// Get texture view
278 pub fn view(&self) -> &TextureView {
279 &self.texture_view
280 }
281 
282 /// Get sampler
283 pub fn sampler(&self) -> &Sampler {
284 &self.sampler
285 }
286 
287 /// Get atlas dimensions
288 pub fn size(&self) -> (u32, u32) {
289 (self.width, self.height)
290 }
291}
292 
293/// Texture manager with glyph caching
294pub struct TextureManager {
295 atlas: TextureAtlas,
296 glyph_cache: GlyphCache,
297 image_cache: HashMap<u64, CachedImage>,
298 rasterizer: GlyphRasterizer,
299}
300 
301impl TextureManager {
302 /// Create new texture manager with default white texture
303 pub fn new(device: &Device, queue: &Queue) -> Self {
304 Self {
305 atlas: TextureAtlas::create_default_white(device, queue),
306 glyph_cache: GlyphCache::new(),
307 image_cache: HashMap::new(),
308 rasterizer: GlyphRasterizer::new().expect("Failed to create glyph rasterizer"),
309 }
310 }
311 
312 /// Create texture manager with font support (512x512 atlas)
313 pub fn new_with_font(device: &Device, queue: &Queue) -> Self {
314 // Increase atlas size to 2048x2048 to support images
315 let mut atlas = TextureAtlas::new(device, 2048, 2048);
316 
317 // IMPORTANT: Reserve white pixel at (0,0) for solid color rendering
318 // The shader samples (0,0) when rendering non-textured shapes
319 atlas.reserve_white_pixel(queue);
320 
321 Self {
322 atlas,
323 glyph_cache: GlyphCache::new(),
324 image_cache: HashMap::new(),
325 rasterizer: GlyphRasterizer::new().expect("Failed to create glyph rasterizer"),
326 }
327 }
328 
329 /// Get or cache a glyph, rasterizing if needed
330 pub fn get_or_cache_glyph(
331 &mut self,
332 queue: &Queue,
333 character: char,
334 font_size: u32,
335 ) -> Option<&CachedGlyph> {
336 let key = GlyphKey {
337 character,
338 font_size,
339 };
340 
341 // Check cache first
342 if self.glyph_cache.get(&key).is_some() {
343 return self.glyph_cache.get(&key);
344 }
345 
346 // Rasterize and cache
347 if let Some((rgba_data, metrics)) = self.rasterizer.rasterize(character, font_size as f32) {
348 // Allocate space in atlas
349 if let Some((x, y)) = self.atlas.allocate_region(metrics.width, metrics.height) {
350 // Upload to GPU
351 if self
352 .atlas
353 .upload_region(queue, &rgba_data, x, y, metrics.width, metrics.height)
354 .is_ok()
355 {
356 // Calculate UV coordinates
357 let atlas_size = self.atlas.size();
358 let u0 = x as f32 / atlas_size.0 as f32;
359 let v0 = y as f32 / atlas_size.1 as f32;
360 let u1 = (x + metrics.width) as f32 / atlas_size.0 as f32;
361 let v1 = (y + metrics.height) as f32 / atlas_size.1 as f32;
362 
363 let cached_glyph = CachedGlyph {
364 metrics,
365 uv_rect: (u0, v0, u1, v1),
366 atlas_region: (x, y, metrics.width, metrics.height),
367 };
368 
369 self.glyph_cache.insert(key, cached_glyph);
370 return self.glyph_cache.get(&key);
371 }
372 }
373 }
374 
375 None
376 }
377 
378 /// Get or upload an image
379 pub fn get_or_upload_image(
380 &mut self,
381 queue: &Queue,
382 id: u64,
383 data: &[u8],
384 width: u32,
385 height: u32,
386 ) -> Option<&CachedImage> {
387 // Check cache first
388 if self.image_cache.contains_key(&id) {
389 return self.image_cache.get(&id);
390 }
391 
392 // Allocate space in atlas
393 if let Some((x, y)) = self.atlas.allocate_region(width, height) {
394 // Upload to GPU
395 if self
396 .atlas
397 .upload_region(queue, data, x, y, width, height)
398 .is_ok()
399 {
400 // Calculate UV coordinates
401 let atlas_size = self.atlas.size();
402 let u0 = x as f32 / atlas_size.0 as f32;
403 let v0 = y as f32 / atlas_size.1 as f32;
404 let u1 = (x + width) as f32 / atlas_size.0 as f32;
405 let v1 = (y + height) as f32 / atlas_size.1 as f32;
406 
407 let cached_image = CachedImage {
408 uv_rect: (u0, v0, u1, v1),
409 atlas_region: (x, y, width, height),
410 };
411 
412 self.image_cache.insert(id, cached_image);
413 return self.image_cache.get(&id);
414 } else {
415 println!("Failed to upload image region");
416 }
417 } else {
418 println!(
419 "Failed to allocate atlas region for image: {}x{}",
420 width, height
421 );
422 }
423 
424 None
425 }
426 
427 /// Get texture atlas
428 pub fn atlas(&self) -> &TextureAtlas {
429 &self.atlas
430 }
431 
432 /// Get glyph cache stats
433 pub fn cache_stats(&self) -> (usize, (u32, u32)) {
434 (self.glyph_cache.len(), self.atlas.size())
435 }
436 /// Get line metrics for a given font size
437 pub fn get_line_metrics(&self, size: f32) -> Option<fontdue::LineMetrics> {
438 self.rasterizer.font.horizontal_line_metrics(size)
439 }
440}
441 
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use crate::gpu::DeviceManager;
446 use wgpu::Backends;
447 
448 #[test]
449 fn test_glyph_key() {
450 let key1 = GlyphKey {
451 character: 'A',
452 font_size: 24,
453 };
454 let key2 = GlyphKey {
455 character: 'A',
456 font_size: 24,
457 };
458 let key3 = GlyphKey {
459 character: 'B',
460 font_size: 24,
461 };
462 
463 assert_eq!(key1, key2);
464 assert_ne!(key1, key3);
465 }
466 
467 #[test]
468 fn test_glyph_cache() {
469 let mut cache = GlyphCache::new();
470 
471 let key = GlyphKey {
472 character: 'A',
473 font_size: 24,
474 };
475 let glyph = CachedGlyph {
476 metrics: GlyphMetrics {
477 width: 10,
478 height: 12,
479 bearing_x: 1,
480 bearing_y: 11,
481 advance: 11.0,
482 },
483 uv_rect: (0.0, 0.0, 0.1, 0.1),
484 atlas_region: (0, 0, 10, 12),
485 };
486 
487 cache.insert(key, glyph);
488 assert!(cache.get(&key).is_some());
489 assert_eq!(cache.len(), 1);
490 }
491 
492 #[test]
493 fn test_glyph_rasterizer() {
494 let rasterizer = GlyphRasterizer::new().unwrap();
495 
496 let result = rasterizer.rasterize('A', 24.0);
497 assert!(result.is_some());
498 
499 let (data, metrics) = result.unwrap();
500 assert!(metrics.width > 0);
501 assert!(metrics.height > 0);
502 assert_eq!(data.len(), (metrics.width * metrics.height * 4) as usize);
503 }
504 
505 #[tokio::test]
506 #[ignore] // TODO: Fix shelf packing test expectations
507 async fn test_atlas_allocation() {
508 let dm = DeviceManager::new(Backends::all()).await.unwrap();
509 let mut atlas = TextureAtlas::new(dm.device(), 256, 256);
510 
511 // Test basic allocation
512 let region1 = atlas.allocate_region(10, 12);
513 assert!(region1.is_some());
514 assert_eq!(region1.unwrap(), (0, 0));
515 
516 // Test multiple allocations in same row
517 let region2 = atlas.allocate_region(8, 10);
518 assert!(region2.is_some());
519 
520 // Test allocation that should succeed (fits in atlas)
521 let region3 = atlas.allocate_region(100, 20);
522 assert!(region3.is_some());
523 
524 // Test allocation that's too wide for the atlas
525 let region_fail = atlas.allocate_region(300, 20);
526 assert_eq!(region_fail, None);
527 }
528 
529 #[tokio::test]
530 async fn test_texture_manager_glyph_caching() {
531 let dm = DeviceManager::new(Backends::all()).await.unwrap();
532 let mut tex_mgr = TextureManager::new_with_font(dm.device(), dm.queue());
533 
534 // Cache first glyph
535 let glyph1 = tex_mgr.get_or_cache_glyph(dm.queue(), 'A', 24);
536 assert!(glyph1.is_some());
537 
538 // Should retrieve from cache (not rasterize again)
539 let glyph2 = tex_mgr.get_or_cache_glyph(dm.queue(), 'A', 24);
540 assert!(glyph2.is_some());
541 
542 let (cache_size, _) = tex_mgr.cache_stats();
543 assert_eq!(cache_size, 1); // Only one glyph cached
544 }
545 
546 #[tokio::test]
547 async fn test_texture_atlas_creation() {
548 let dm = DeviceManager::new(Backends::all()).await.unwrap();
549 let atlas = TextureAtlas::new(dm.device(), 256, 256);
550 
551 assert_eq!(atlas.size(), (256, 256));
552 }
553 
554 #[tokio::test]
555 async fn test_default_white_texture() {
556 let dm = DeviceManager::new(Backends::all()).await.unwrap();
557 let atlas = TextureAtlas::create_default_white(dm.device(), dm.queue());
558 
559 assert_eq!(atlas.size(), (1, 1));
560 }
561}
562