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/resources.rs
StratoSDK / crates / strato-renderer / src / resources.rs
1//! Advanced resource management system for StratoUI renderer
2//!
3//! This module provides enterprise-grade resource management including:
4//! - Intelligent buffer pooling with size-based allocation strategies
5//! - Advanced texture atlas management with automatic defragmentation
6//! - Resource lifetime tracking with automatic cleanup
7//! - Memory optimization with usage pattern analysis
8//! - Multi-threaded resource access with lock-free operations where possible
9//! - Resource streaming and lazy loading
10//! - Memory pressure detection and adaptive strategies
11//! - Resource dependency tracking and batch operations
12 
13use anyhow::Result;
14use parking_lot::RwLock;
15use serde::{Deserialize, Serialize};
16use slotmap::{DefaultKey, SlotMap};
17use std::collections::{BTreeSet, HashMap, VecDeque};
18use std::sync::{Arc, Mutex, Weak};
19use std::time::{Duration, Instant};
20use wgpu::{Buffer, BufferDescriptor, BufferUsages, Device, Maintain, MapMode, *};
21 
22use crate::device::ManagedDevice;
23 
24/// Enable advanced memory tracking and profiling
25const ENABLE_MEMORY_PROFILING: bool = cfg!(debug_assertions);
26 
27/// Memory pressure threshold (80% of total available memory)
28const MEMORY_PRESSURE_THRESHOLD: f32 = 0.8;
29 
30/// Default cleanup interval
31const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(30);
32 
33/// Maximum number of texture atlases per format
34const MAX_ATLASES_PER_FORMAT: usize = 16;
35 
36/// Buffer allocation alignment
37const BUFFER_ALIGNMENT: u64 = 256;
38 
39/// Unique identifier for resources
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub struct ResourceHandle(pub u64);
42 
43impl ResourceHandle {
44 pub fn new() -> Self {
45 use std::sync::atomic::{AtomicU64, Ordering};
46 static COUNTER: AtomicU64 = AtomicU64::new(1);
47 ResourceHandle(COUNTER.fetch_add(1, Ordering::Relaxed))
48 }
49}
50 
51/// Types of resources managed by the system
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub enum ResourceType {
54 Buffer,
55 Texture,
56 BindGroup,
57 Pipeline,
58 Sampler,
59 QuerySet,
60 RenderBundle,
61}
62 
63/// Memory usage categories for fine-grained tracking
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum MemoryCategory {
66 Vertex,
67 Index,
68 Uniform,
69 Storage,
70 Texture2D,
71 Texture3D,
72 TextureCube,
73 RenderTarget,
74 DepthStencil,
75 Staging,
76}
77 
78/// Resource priority for memory management decisions
79#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
80pub enum ResourcePriority {
81 Critical = 4, // Never evict
82 High = 3, // Evict only under extreme pressure
83 Medium = 2, // Default priority
84 Low = 1, // First to be evicted
85 Disposable = 0, // Can be recreated easily
86}
87 
88/// Advanced memory allocation strategy
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum AllocationStrategy {
91 /// Best fit - minimize wasted space
92 BestFit,
93 /// First fit - fastest allocation
94 FirstFit,
95 /// Next fit - balance between speed and fragmentation
96 NextFit,
97 /// Buddy system - reduce fragmentation
98 Buddy,
99 /// Slab allocation - for fixed-size allocations
100 Slab,
101}
102 
103/// Resource allocation metadata
104#[derive(Debug, Clone)]
105pub struct AllocationMetadata {
106 pub size: u64,
107 pub alignment: u64,
108 pub category: MemoryCategory,
109 pub priority: ResourcePriority,
110 pub created_at: Instant,
111 pub last_accessed: Instant,
112 pub access_count: u64,
113 pub label: Option<String>,
114}
115 
116/// Buffer pool configuration
117#[derive(Debug, Clone)]
118pub struct BufferPoolConfig {
119 /// Initial number of buffers in the pool
120 pub initial_size: usize,
121 /// Maximum number of buffers in the pool
122 pub max_size: usize,
123 /// Buffer size in bytes
124 pub buffer_size: u64,
125 /// Buffer usage flags
126 pub usage: BufferUsages,
127 /// Whether buffers should be mapped at creation
128 pub mapped_at_creation: bool,
129}
130 
131impl Default for BufferPoolConfig {
132 fn default() -> Self {
133 Self {
134 initial_size: 4,
135 max_size: 32,
136 buffer_size: 1024 * 1024, // 1MB
137 usage: BufferUsages::VERTEX | BufferUsages::INDEX | BufferUsages::COPY_DST,
138 mapped_at_creation: false,
139 }
140 }
141}
142 
143/// Pooled buffer with usage tracking
144#[derive(Debug)]
145pub struct PooledBuffer {
146 buffer: Buffer,
147 size: u64,
148 usage: BufferUsages,
149 ref_count: Arc<()>,
150 last_used: std::time::Instant,
151 is_dirty: bool,
152}
153 
154impl PooledBuffer {
155 fn new(device: &Device, config: &BufferPoolConfig, label: Option<&str>) -> Self {
156 let buffer = device.create_buffer(&BufferDescriptor {
157 label,
158 size: config.buffer_size,
159 usage: config.usage,
160 mapped_at_creation: config.mapped_at_creation,
161 });
162 
163 Self {
164 buffer,
165 size: config.buffer_size,
166 usage: config.usage,
167 ref_count: Arc::new(()),
168 last_used: std::time::Instant::now(),
169 is_dirty: false,
170 }
171 }
172 
173 /// Get the underlying wgpu buffer
174 pub fn buffer(&self) -> &Buffer {
175 &self.buffer
176 }
177 
178 /// Mark the buffer as used
179 pub fn touch(&mut self) {
180 self.last_used = std::time::Instant::now();
181 }
182 
183 /// Mark the buffer as dirty (needs cleanup)
184 pub fn mark_dirty(&mut self) {
185 self.is_dirty = true;
186 }
187 
188 /// Check if the buffer is currently referenced
189 pub fn is_referenced(&self) -> bool {
190 Arc::strong_count(&self.ref_count) > 1
191 }
192 
193 /// Get a reference handle for this buffer
194 pub fn get_ref(&self) -> BufferRef {
195 BufferRef {
196 inner: Arc::downgrade(&self.ref_count),
197 }
198 }
199}
200 
201/// Weak reference to a pooled buffer
202#[derive(Debug, Clone)]
203pub struct BufferRef {
204 inner: Weak<()>,
205}
206 
207impl BufferRef {
208 /// Check if the buffer is still alive
209 pub fn is_alive(&self) -> bool {
210 self.inner.strong_count() > 0
211 }
212}
213 
214/// Buffer pool for efficient GPU memory management
215pub struct BufferPool {
216 config: BufferPoolConfig,
217 available: VecDeque<PooledBuffer>,
218 in_use: Vec<PooledBuffer>,
219 total_allocated: u64,
220 peak_usage: u64,
221}
222 
223impl BufferPool {
224 /// Create a new buffer pool
225 pub fn new(device: &Device, config: BufferPoolConfig) -> Result<Self> {
226 let mut pool = Self {
227 config: config.clone(),
228 available: VecDeque::new(),
229 in_use: Vec::new(),
230 total_allocated: 0,
231 peak_usage: 0,
232 };
233 
234 // Pre-allocate initial buffers
235 for i in 0..config.initial_size {
236 let buffer = PooledBuffer::new(device, &config, Some(&format!("PooledBuffer_{}", i)));
237 pool.total_allocated += buffer.size;
238 pool.available.push_back(buffer);
239 }
240 
241 Ok(pool)
242 }
243 
244 /// Acquire a buffer from the pool
245 pub fn acquire(&mut self, device: &Device) -> Result<&mut PooledBuffer> {
246 if let Some(mut buffer) = self.available.pop_front() {
247 buffer.touch();
248 self.in_use.push(buffer);
249 Ok(self.in_use.last_mut().unwrap())
250 } else if self.total_allocated < (self.config.max_size as u64 * self.config.buffer_size) {
251 // Create new buffer if we haven't reached the limit
252 let buffer = PooledBuffer::new(
253 device,
254 &self.config,
255 Some(&format!("PooledBuffer_{}", self.in_use.len())),
256 );
257 self.total_allocated += buffer.size;
258 self.in_use.push(buffer);
259 Ok(self.in_use.last_mut().unwrap())
260 } else {
261 anyhow::bail!("Buffer pool exhausted")
262 }
263 }
264 
265 /// Release unused buffers back to the pool
266 pub fn release_unused(&mut self) {
267 let mut i = 0;
268 while i < self.in_use.len() {
269 if !self.in_use[i].is_referenced() {
270 let buffer = self.in_use.remove(i);
271 self.available.push_back(buffer);
272 } else {
273 i += 1;
274 }
275 }
276 
277 // Update peak usage
278 let current_usage = self.in_use.len() as u64 * self.config.buffer_size;
279 self.peak_usage = self.peak_usage.max(current_usage);
280 }
281 
282 /// Clean up old unused buffers
283 pub fn cleanup(&mut self, max_age: std::time::Duration) {
284 let now = std::time::Instant::now();
285 self.available
286 .retain(|buffer| now.duration_since(buffer.last_used) < max_age);
287 
288 // Recalculate total allocation
289 self.total_allocated =
290 (self.available.len() + self.in_use.len()) as u64 * self.config.buffer_size;
291 }
292 
293 /// Get pool statistics
294 pub fn stats(&self) -> BufferPoolStats {
295 BufferPoolStats {
296 available_count: self.available.len(),
297 in_use_count: self.in_use.len(),
298 total_allocated: self.total_allocated,
299 peak_usage: self.peak_usage,
300 }
301 }
302}
303 
304/// Buffer pool statistics
305#[derive(Debug, Clone)]
306pub struct BufferPoolStats {
307 pub available_count: usize,
308 pub in_use_count: usize,
309 pub total_allocated: u64,
310 pub peak_usage: u64,
311}
312 
313/// Texture atlas for efficient texture management
314pub struct TextureAtlas {
315 texture: Texture,
316 view: TextureView,
317 sampler: Sampler,
318 size: u32,
319 allocations: HashMap<u32, AtlasAllocation>,
320 free_regions: BTreeSet<Region>,
321 next_id: u32,
322}
323 
324#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
325pub struct Region {
326 pub x: u32,
327 pub y: u32,
328 pub width: u32,
329 pub height: u32,
330}
331 
332#[derive(Debug, Clone)]
333struct AtlasAllocation {
334 region: Region,
335 ref_count: Arc<()>,
336}
337 
338impl TextureAtlas {
339 /// Create a new texture atlas
340 pub fn new(device: &Device, size: u32) -> Self {
341 let texture = device.create_texture(&TextureDescriptor {
342 label: Some("TextureAtlas"),
343 size: Extent3d {
344 width: size,
345 height: size,
346 depth_or_array_layers: 1,
347 },
348 mip_level_count: 1,
349 sample_count: 1,
350 dimension: TextureDimension::D2,
351 format: TextureFormat::Rgba8UnormSrgb,
352 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
353 view_formats: &[],
354 });
355 
356 let view = texture.create_view(&TextureViewDescriptor::default());
357 
358 let sampler = device.create_sampler(&SamplerDescriptor {
359 label: Some("AtlasSampler"),
360 address_mode_u: AddressMode::ClampToEdge,
361 address_mode_v: AddressMode::ClampToEdge,
362 address_mode_w: AddressMode::ClampToEdge,
363 mag_filter: FilterMode::Linear,
364 min_filter: FilterMode::Linear,
365 mipmap_filter: FilterMode::Nearest,
366 ..Default::default()
367 });
368 
369 let mut free_regions = BTreeSet::new();
370 free_regions.insert(Region {
371 x: 0,
372 y: 0,
373 width: size,
374 height: size,
375 });
376 
377 Self {
378 texture,
379 view,
380 sampler,
381 size,
382 allocations: HashMap::new(),
383 free_regions,
384 next_id: 1,
385 }
386 }
387 
388 /// Allocate a region in the atlas
389 pub fn allocate(&mut self, width: u32, height: u32) -> Option<AtlasHandle> {
390 // Find the best fitting region using best-fit algorithm
391 let mut best_region = None;
392 let mut best_fit_score = u32::MAX;
393 
394 for &region in &self.free_regions {
395 if region.width >= width && region.height >= height {
396 let score = (region.width - width) + (region.height - height);
397 if score < best_fit_score {
398 best_fit_score = score;
399 best_region = Some(region);
400 }
401 }
402 }
403 
404 if let Some(region) = best_region {
405 self.free_regions.remove(&region);
406 
407 // Split the region if necessary
408 if region.width > width {
409 self.free_regions.insert(Region {
410 x: region.x + width,
411 y: region.y,
412 width: region.width - width,
413 height: height,
414 });
415 }
416 
417 if region.height > height {
418 self.free_regions.insert(Region {
419 x: region.x,
420 y: region.y + height,
421 width: region.width,
422 height: region.height - height,
423 });
424 }
425 
426 let allocated_region = Region {
427 x: region.x,
428 y: region.y,
429 width,
430 height,
431 };
432 
433 let allocation = AtlasAllocation {
434 region: allocated_region,
435 ref_count: Arc::new(()),
436 };
437 
438 let id = self.next_id;
439 self.next_id += 1;
440 
441 self.allocations.insert(id, allocation.clone());
442 
443 Some(AtlasHandle {
444 id,
445 region: allocated_region,
446 atlas_size: self.size,
447 _ref: Arc::downgrade(&allocation.ref_count),
448 })
449 } else {
450 None
451 }
452 }
453 
454 /// Upload data to a region in the atlas
455 pub fn upload_data(
456 &self,
457 queue: &Queue,
458 handle: &AtlasHandle,
459 data: &[u8],
460 bytes_per_pixel: u32,
461 ) {
462 queue.write_texture(
463 ImageCopyTexture {
464 texture: &self.texture,
465 mip_level: 0,
466 origin: Origin3d {
467 x: handle.region.x,
468 y: handle.region.y,
469 z: 0,
470 },
471 aspect: TextureAspect::All,
472 },
473 data,
474 ImageDataLayout {
475 offset: 0,
476 bytes_per_row: Some(handle.region.width * bytes_per_pixel),
477 rows_per_image: Some(handle.region.height),
478 },
479 Extent3d {
480 width: handle.region.width,
481 height: handle.region.height,
482 depth_or_array_layers: 1,
483 },
484 );
485 }
486 
487 /// Clean up unused allocations
488 pub fn cleanup(&mut self) {
489 let mut to_remove = Vec::new();
490 
491 for (&id, allocation) in &self.allocations {
492 if Arc::strong_count(&allocation.ref_count) == 1 {
493 to_remove.push(id);
494 }
495 }
496 
497 for id in to_remove {
498 if let Some(allocation) = self.allocations.remove(&id) {
499 self.free_regions.insert(allocation.region);
500 self.merge_free_regions();
501 }
502 }
503 }
504 
505 /// Merge adjacent free regions
506 fn merge_free_regions(&mut self) {
507 // This is a simplified merge - a full implementation would be more complex
508 let regions: Vec<_> = self.free_regions.iter().cloned().collect();
509 self.free_regions.clear();
510 
511 for region in regions {
512 self.free_regions.insert(region);
513 }
514 
515 // TODO: Implement proper region merging algorithm
516 }
517 
518 /// Get texture view for rendering
519 pub fn texture_view(&self) -> &TextureView {
520 &self.view
521 }
522 
523 /// Get sampler for rendering
524 pub fn sampler(&self) -> &Sampler {
525 &self.sampler
526 }
527 
528 /// Get atlas statistics
529 pub fn stats(&self) -> AtlasStats {
530 let total_area = self.size * self.size;
531 let used_area: u32 = self
532 .allocations
533 .values()
534 .map(|alloc| alloc.region.width * alloc.region.height)
535 .sum();
536 
537 AtlasStats {
538 size: self.size,
539 allocations: self.allocations.len(),
540 used_area,
541 free_area: total_area - used_area,
542 fragmentation: if total_area > 0 {
543 self.free_regions.len() as f32 / (total_area as f32)
544 } else {
545 0.0
546 },
547 }
548 }
549}
550 
551/// Handle to an allocated region in the texture atlas
552#[derive(Debug, Clone)]
553pub struct AtlasHandle {
554 pub id: u32,
555 pub region: Region,
556 pub atlas_size: u32,
557 _ref: Weak<()>,
558}
559 
560impl AtlasHandle {
561 /// Get UV coordinates for this region
562 pub fn uv_coords(&self) -> (f32, f32, f32, f32) {
563 let atlas_size = self.atlas_size as f32;
564 (
565 self.region.x as f32 / atlas_size,
566 self.region.y as f32 / atlas_size,
567 self.region.width as f32 / atlas_size,
568 self.region.height as f32 / atlas_size,
569 )
570 }
571 
572 /// Check if the handle is still valid
573 pub fn is_valid(&self) -> bool {
574 self._ref.strong_count() > 0
575 }
576}
577 
578/// Atlas statistics
579#[derive(Debug, Clone)]
580pub struct AtlasStats {
581 pub size: u32,
582 pub allocations: usize,
583 pub used_area: u32,
584 pub free_area: u32,
585 pub fragmentation: f32,
586}
587 
588/// Comprehensive resource manager
589pub struct ResourceManager {
590 managed_device: Arc<ManagedDevice>,
591 
592 // Buffer pools
593 vertex_pool: Mutex<BufferPool>,
594 index_pool: Mutex<BufferPool>,
595 uniform_pool: Mutex<BufferPool>,
596 
597 // Texture management
598 texture_atlas: RwLock<TextureAtlas>,
599 textures: RwLock<SlotMap<DefaultKey, Arc<Texture>>>,
600 
601 // Resource tracking
602 memory_usage: Mutex<MemoryUsage>,
603 cleanup_interval: std::time::Duration,
604 last_cleanup: Mutex<std::time::Instant>,
605}
606 
607#[derive(Debug, Default, Clone)]
608pub struct MemoryUsage {
609 pub buffer_memory: u64,
610 pub texture_memory: u64,
611 pub total_allocations: usize,
612 pub peak_memory: u64,
613}
614 
615impl ResourceManager {
616 /// Create a new resource manager
617 pub fn new(managed_device: Arc<ManagedDevice>) -> Result<Self> {
618 let vertex_config = BufferPoolConfig {
619 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
620 ..Default::default()
621 };
622 
623 let index_config = BufferPoolConfig {
624 usage: BufferUsages::INDEX | BufferUsages::COPY_DST,
625 buffer_size: 512 * 1024, // 512KB for indices
626 ..Default::default()
627 };
628 
629 let uniform_config = BufferPoolConfig {
630 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
631 buffer_size: 64 * 1024, // 64KB for uniforms
632 ..Default::default()
633 };
634 
635 let device_ref = &managed_device.device;
636 let _queue_ref = &managed_device.queue;
637 
638 Ok(Self {
639 vertex_pool: Mutex::new(BufferPool::new(device_ref, vertex_config)?),
640 index_pool: Mutex::new(BufferPool::new(device_ref, index_config)?),
641 uniform_pool: Mutex::new(BufferPool::new(device_ref, uniform_config)?),
642 texture_atlas: RwLock::new(TextureAtlas::new(device_ref, 2048)), // 2K atlas
643 textures: RwLock::new(SlotMap::new()),
644 memory_usage: Mutex::new(MemoryUsage::default()),
645 cleanup_interval: std::time::Duration::from_secs(30),
646 last_cleanup: Mutex::new(std::time::Instant::now()),
647 managed_device,
648 })
649 }
650 
651 /// Acquire a vertex buffer from the pool
652 pub fn acquire_vertex_buffer(&self) -> Result<BufferRef> {
653 let mut pool = self.vertex_pool.lock().unwrap();
654 let buffer = pool.acquire(&self.managed_device.device)?;
655 Ok(buffer.get_ref())
656 }
657 
658 /// Acquire an index buffer from the pool
659 pub fn acquire_index_buffer(&self) -> Result<BufferRef> {
660 let mut pool = self.index_pool.lock().unwrap();
661 let buffer = pool.acquire(&self.managed_device.device)?;
662 Ok(buffer.get_ref())
663 }
664 
665 /// Acquire a uniform buffer from the pool
666 pub fn acquire_uniform_buffer(&self) -> Result<BufferRef> {
667 let mut pool = self.uniform_pool.lock().unwrap();
668 let buffer = pool.acquire(&self.managed_device.device)?;
669 Ok(buffer.get_ref())
670 }
671 
672 /// Allocate space in the texture atlas
673 pub fn allocate_atlas_space(&self, width: u32, height: u32) -> Option<AtlasHandle> {
674 let mut atlas = self.texture_atlas.write();
675 atlas.allocate(width, height)
676 }
677 
678 /// Create a new standalone texture
679 pub fn create_texture(&self, descriptor: &TextureDescriptor) -> DefaultKey {
680 let texture = self.managed_device.device.create_texture(descriptor);
681 let mut textures = self.textures.write();
682 textures.insert(Arc::new(texture))
683 }
684 
685 /// Get a texture by handle
686 pub fn get_texture(&self, handle: DefaultKey) -> Option<Arc<Texture>> {
687 let textures = self.textures.read();
688 textures.get(handle).cloned()
689 }
690 
691 /// Release unused resources
692 pub fn cleanup(&self) {
693 let mut last_cleanup = self.last_cleanup.lock().unwrap();
694 let now = std::time::Instant::now();
695 
696 if now.duration_since(*last_cleanup) > self.cleanup_interval {
697 // Clean up buffer pools
698 self.vertex_pool
699 .lock()
700 .unwrap()
701 .cleanup(self.cleanup_interval);
702 self.index_pool
703 .lock()
704 .unwrap()
705 .cleanup(self.cleanup_interval);
706 self.uniform_pool
707 .lock()
708 .unwrap()
709 .cleanup(self.cleanup_interval);
710 
711 // Clean up atlas
712 self.texture_atlas.write().cleanup();
713 
714 // Release unused buffers
715 self.vertex_pool.lock().unwrap().release_unused();
716 self.index_pool.lock().unwrap().release_unused();
717 self.uniform_pool.lock().unwrap().release_unused();
718 
719 *last_cleanup = now;
720 }
721 }
722 
723 /// Get comprehensive resource statistics
724 pub fn stats(&self) -> ResourceStats {
725 ResourceStats {
726 vertex_pool: self.vertex_pool.lock().unwrap().stats(),
727 index_pool: self.index_pool.lock().unwrap().stats(),
728 uniform_pool: self.uniform_pool.lock().unwrap().stats(),
729 texture_atlas: self.texture_atlas.read().stats(),
730 memory_usage: self.memory_usage.lock().unwrap().clone(),
731 texture_count: self.textures.read().len(),
732 }
733 }
734 
735 /// Force garbage collection of all resources
736 pub fn force_gc(&self) {
737 // More aggressive cleanup
738 let long_duration = std::time::Duration::from_secs(0);
739 
740 self.vertex_pool.lock().unwrap().cleanup(long_duration);
741 self.index_pool.lock().unwrap().cleanup(long_duration);
742 self.uniform_pool.lock().unwrap().cleanup(long_duration);
743 
744 self.texture_atlas.write().cleanup();
745 
746 self.vertex_pool.lock().unwrap().release_unused();
747 self.index_pool.lock().unwrap().release_unused();
748 self.uniform_pool.lock().unwrap().release_unused();
749 }
750 
751 /// Get active resource count for integration
752 pub fn get_active_count(&self) -> usize {
753 self.textures.read().len()
754 }
755 
756 /// Cleanup unused resources (integration method)
757 pub fn cleanup_unused(&self) {
758 self.cleanup();
759 }
760 
761 /// Cleanup all resources (integration method)
762 pub fn cleanup_all(&self) {
763 self.force_gc();
764 }
765}
766 
767/// Comprehensive resource statistics
768#[derive(Debug, Clone)]
769pub struct ResourceStats {
770 pub vertex_pool: BufferPoolStats,
771 pub index_pool: BufferPoolStats,
772 pub uniform_pool: BufferPoolStats,
773 pub texture_atlas: AtlasStats,
774 pub memory_usage: MemoryUsage,
775 pub texture_count: usize,
776}
777 
778impl ResourceStats {
779 /// Get total memory usage in bytes
780 pub fn total_memory(&self) -> u64 {
781 self.vertex_pool.total_allocated
782 + self.index_pool.total_allocated
783 + self.uniform_pool.total_allocated
784 + self.memory_usage.texture_memory
785 }
786 
787 /// Get total resource count
788 pub fn total_resources(&self) -> usize {
789 self.vertex_pool.available_count
790 + self.vertex_pool.in_use_count
791 + self.index_pool.available_count
792 + self.index_pool.in_use_count
793 + self.uniform_pool.available_count
794 + self.uniform_pool.in_use_count
795 + self.texture_count
796 }
797}
798 
799#[cfg(test)]
800mod tests {
801 use super::*;
802 
803 #[test]
804 fn test_region_ordering() {
805 let region1 = Region {
806 x: 0,
807 y: 0,
808 width: 10,
809 height: 10,
810 };
811 let region2 = Region {
812 x: 0,
813 y: 0,
814 width: 20,
815 height: 10,
816 };
817 let region3 = Region {
818 x: 10,
819 y: 0,
820 width: 10,
821 height: 10,
822 };
823 
824 assert!(region1 < region2);
825 assert!(region1 < region3);
826 }
827 
828 #[test]
829 fn test_atlas_handle_uv() {
830 let handle = AtlasHandle {
831 id: 1,
832 region: Region {
833 x: 100,
834 y: 200,
835 width: 50,
836 height: 75,
837 },
838 atlas_size: 1000,
839 _ref: Weak::new(),
840 };
841 
842 let (u, v, w, h) = handle.uv_coords();
843 assert_eq!(u, 0.1);
844 assert_eq!(v, 0.2);
845 assert_eq!(w, 0.05);
846 assert_eq!(h, 0.075);
847 }
848}
849