Skip to main content

proof_engine/rendergraph/
resources.rs

1//! Automatic resource management for the render graph.
2//!
3//! Provides resource descriptors, transient/imported resources, pooling with
4//! frame-based lifetime tracking, resource aliasing, versioning for
5//! read-after-write hazard detection, and memory budget estimation.
6
7use std::collections::HashMap;
8use std::fmt;
9
10// ---------------------------------------------------------------------------
11// Texture format & usage
12// ---------------------------------------------------------------------------
13
14/// Pixel / data format for textures and buffers.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum TextureFormat {
17    R8Unorm,
18    R16Float,
19    R32Float,
20    Rg8Unorm,
21    Rg16Float,
22    Rg32Float,
23    Rgba8Unorm,
24    Rgba8Srgb,
25    Rgba16Float,
26    Rgba32Float,
27    Bgra8Unorm,
28    Bgra8Srgb,
29    Depth16Unorm,
30    Depth24PlusStencil8,
31    Depth32Float,
32    R11G11B10Float,
33    Rgb10A2Unorm,
34    Bc1Unorm,
35    Bc3Unorm,
36    Bc5Unorm,
37    Bc7Unorm,
38}
39
40impl TextureFormat {
41    /// Bytes per pixel (approximate for block-compressed formats).
42    pub fn bytes_per_pixel(self) -> u32 {
43        match self {
44            Self::R8Unorm => 1,
45            Self::R16Float => 2,
46            Self::R32Float => 4,
47            Self::Rg8Unorm => 2,
48            Self::Rg16Float => 4,
49            Self::Rg32Float => 8,
50            Self::Rgba8Unorm | Self::Rgba8Srgb => 4,
51            Self::Rgba16Float => 8,
52            Self::Rgba32Float => 16,
53            Self::Bgra8Unorm | Self::Bgra8Srgb => 4,
54            Self::Depth16Unorm => 2,
55            Self::Depth24PlusStencil8 => 4,
56            Self::Depth32Float => 4,
57            Self::R11G11B10Float => 4,
58            Self::Rgb10A2Unorm => 4,
59            Self::Bc1Unorm => 1, // 0.5 bpp * 2 (rough)
60            Self::Bc3Unorm | Self::Bc5Unorm | Self::Bc7Unorm => 1,
61        }
62    }
63
64    /// Returns true for depth / depth-stencil formats.
65    pub fn is_depth(self) -> bool {
66        matches!(
67            self,
68            Self::Depth16Unorm | Self::Depth24PlusStencil8 | Self::Depth32Float
69        )
70    }
71}
72
73/// How a resource can be bound in a pass.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75pub enum UsageFlags {
76    RenderTarget,
77    DepthStencil,
78    ShaderRead,
79    ShaderWrite,
80    CopySource,
81    CopyDest,
82    StorageBuffer,
83    UniformBuffer,
84}
85
86/// Description of the dimensions / scale of a texture.
87#[derive(Debug, Clone, Copy, PartialEq)]
88pub enum SizePolicy {
89    /// Exact pixel dimensions.
90    Absolute { width: u32, height: u32 },
91    /// Fraction of the backbuffer resolution.
92    Relative { width_scale: f32, height_scale: f32 },
93}
94
95impl SizePolicy {
96    /// Resolve to concrete pixel dimensions given the backbuffer size.
97    pub fn resolve(self, backbuffer_width: u32, backbuffer_height: u32) -> (u32, u32) {
98        match self {
99            Self::Absolute { width, height } => (width, height),
100            Self::Relative {
101                width_scale,
102                height_scale,
103            } => {
104                let w = ((backbuffer_width as f32) * width_scale).max(1.0) as u32;
105                let h = ((backbuffer_height as f32) * height_scale).max(1.0) as u32;
106                (w, h)
107            }
108        }
109    }
110}
111
112// ---------------------------------------------------------------------------
113// Resource descriptor
114// ---------------------------------------------------------------------------
115
116/// Full description of a GPU resource.
117#[derive(Debug, Clone)]
118pub struct ResourceDescriptor {
119    pub name: String,
120    pub size: SizePolicy,
121    pub format: TextureFormat,
122    pub mip_levels: u32,
123    pub array_layers: u32,
124    pub sample_count: u32,
125    pub usages: Vec<UsageFlags>,
126}
127
128impl ResourceDescriptor {
129    pub fn new(name: &str, format: TextureFormat) -> Self {
130        Self {
131            name: name.to_string(),
132            size: SizePolicy::Relative {
133                width_scale: 1.0,
134                height_scale: 1.0,
135            },
136            format,
137            mip_levels: 1,
138            array_layers: 1,
139            sample_count: 1,
140            usages: vec![UsageFlags::RenderTarget, UsageFlags::ShaderRead],
141        }
142    }
143
144    pub fn with_size(mut self, size: SizePolicy) -> Self {
145        self.size = size;
146        self
147    }
148
149    pub fn with_mip_levels(mut self, levels: u32) -> Self {
150        self.mip_levels = levels;
151        self
152    }
153
154    pub fn with_array_layers(mut self, layers: u32) -> Self {
155        self.array_layers = layers;
156        self
157    }
158
159    pub fn with_sample_count(mut self, count: u32) -> Self {
160        self.sample_count = count;
161        self
162    }
163
164    pub fn with_usages(mut self, usages: Vec<UsageFlags>) -> Self {
165        self.usages = usages;
166        self
167    }
168
169    /// Estimated byte size when resolved against a given backbuffer.
170    pub fn estimated_bytes(&self, bb_w: u32, bb_h: u32) -> u64 {
171        let (w, h) = self.size.resolve(bb_w, bb_h);
172        let bpp = self.format.bytes_per_pixel() as u64;
173        let base = (w as u64) * (h as u64) * bpp * (self.array_layers as u64) * (self.sample_count as u64);
174        // Mip chain: sum of 1 + 1/4 + 1/16 + ... ~ 4/3 for full chain
175        if self.mip_levels > 1 {
176            let mut total: u64 = 0;
177            let mut mw = w as u64;
178            let mut mh = h as u64;
179            for _ in 0..self.mip_levels {
180                total += mw * mh * bpp * (self.array_layers as u64) * (self.sample_count as u64);
181                mw = (mw / 2).max(1);
182                mh = (mh / 2).max(1);
183            }
184            total
185        } else {
186            base
187        }
188    }
189
190    /// True if two descriptors are memory-compatible (same size, format, sample count).
191    pub fn is_compatible_with(&self, other: &ResourceDescriptor, bb_w: u32, bb_h: u32) -> bool {
192        let (sw, sh) = self.size.resolve(bb_w, bb_h);
193        let (ow, oh) = other.size.resolve(bb_w, bb_h);
194        sw == ow
195            && sh == oh
196            && self.format == other.format
197            && self.mip_levels == other.mip_levels
198            && self.array_layers == other.array_layers
199            && self.sample_count == other.sample_count
200    }
201}
202
203// ---------------------------------------------------------------------------
204// Resource handle / version
205// ---------------------------------------------------------------------------
206
207/// Opaque handle to a resource in the graph.
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
209pub struct ResourceHandle {
210    pub index: u32,
211    pub version: u32,
212}
213
214impl ResourceHandle {
215    pub fn new(index: u32, version: u32) -> Self {
216        Self { index, version }
217    }
218
219    pub fn next_version(self) -> Self {
220        Self {
221            index: self.index,
222            version: self.version + 1,
223        }
224    }
225}
226
227impl fmt::Display for ResourceHandle {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        write!(f, "Res({}v{})", self.index, self.version)
230    }
231}
232
233// ---------------------------------------------------------------------------
234// Resource slot (transient vs imported)
235// ---------------------------------------------------------------------------
236
237/// Whether a resource is managed by the graph or externally.
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum ResourceLifetime {
240    /// Created at first use and destroyed after last use each frame.
241    Transient,
242    /// Externally managed — never allocated/freed by the graph.
243    Imported,
244}
245
246/// Runtime state of a concrete GPU allocation.
247#[derive(Debug, Clone)]
248pub struct PhysicalResource {
249    pub id: u64,
250    pub descriptor: ResourceDescriptor,
251    pub lifetime: ResourceLifetime,
252    /// Frame index at which this was last used.
253    pub last_used_frame: u64,
254    /// First pass index (topo-sorted) that writes to this resource this frame.
255    pub first_write_pass: Option<usize>,
256    /// Last pass index (topo-sorted) that reads from this resource this frame.
257    pub last_read_pass: Option<usize>,
258}
259
260impl PhysicalResource {
261    pub fn new(id: u64, descriptor: ResourceDescriptor, lifetime: ResourceLifetime) -> Self {
262        Self {
263            id,
264            descriptor,
265            lifetime,
266            last_used_frame: 0,
267            first_write_pass: None,
268            last_read_pass: None,
269        }
270    }
271
272    /// True if this resource's lifetime spans the given pass index.
273    pub fn is_alive_at(&self, pass_idx: usize) -> bool {
274        let first = self.first_write_pass.unwrap_or(usize::MAX);
275        let last = self.last_read_pass.unwrap_or(0);
276        pass_idx >= first && pass_idx <= last
277    }
278}
279
280// ---------------------------------------------------------------------------
281// Resource version tracking
282// ---------------------------------------------------------------------------
283
284/// Tracks all versions of a single logical resource within one frame,
285/// enabling read-after-write hazard detection.
286#[derive(Debug, Clone)]
287pub struct ResourceVersionChain {
288    pub handle: ResourceHandle,
289    pub descriptor: ResourceDescriptor,
290    pub lifetime: ResourceLifetime,
291    /// Ordered list of (version, writer_pass_name).
292    pub versions: Vec<(u32, String)>,
293    /// Readers per version: version -> list of pass names.
294    pub readers: HashMap<u32, Vec<String>>,
295}
296
297impl ResourceVersionChain {
298    pub fn new(handle: ResourceHandle, descriptor: ResourceDescriptor, lifetime: ResourceLifetime) -> Self {
299        Self {
300            handle,
301            descriptor,
302            lifetime,
303            versions: Vec::new(),
304            readers: HashMap::new(),
305        }
306    }
307
308    /// Record a write, bumping version.
309    pub fn record_write(&mut self, pass_name: &str) -> u32 {
310        let ver = if let Some((last, _)) = self.versions.last() {
311            last + 1
312        } else {
313            0
314        };
315        self.versions.push((ver, pass_name.to_string()));
316        ver
317    }
318
319    /// Record a read at a specific version.
320    pub fn record_read(&mut self, version: u32, pass_name: &str) {
321        self.readers
322            .entry(version)
323            .or_default()
324            .push(pass_name.to_string());
325    }
326
327    /// The current (latest) version.
328    pub fn current_version(&self) -> u32 {
329        self.versions.last().map(|(v, _)| *v).unwrap_or(0)
330    }
331
332    /// Detect if a read-after-write hazard exists: a pass reads version N
333    /// while another pass writes version N+1.
334    pub fn has_raw_hazard(&self) -> bool {
335        for (ver, _writer) in &self.versions {
336            if *ver == 0 {
337                continue;
338            }
339            let prev = ver - 1;
340            if let Some(readers) = self.readers.get(&prev) {
341                if !readers.is_empty() {
342                    // There are readers of the previous version while a newer version exists.
343                    // This is a potential RAW hazard that requires a barrier.
344                    return true;
345                }
346            }
347        }
348        false
349    }
350}
351
352// ---------------------------------------------------------------------------
353// Resource pool
354// ---------------------------------------------------------------------------
355
356/// Manages physical resources with frame-based lifetime tracking.
357/// Reuses allocations between frames when descriptors match.
358pub struct ResourcePool {
359    resources: Vec<PhysicalResource>,
360    next_id: u64,
361    /// Map from resource name to pool index.
362    name_map: HashMap<String, usize>,
363    /// Resources that are free and can be reused.
364    free_list: Vec<usize>,
365    /// Current frame index.
366    current_frame: u64,
367    /// Maximum number of frames a resource can be unused before being freed.
368    pub max_idle_frames: u64,
369    /// Version chains for the current frame.
370    version_chains: HashMap<u32, ResourceVersionChain>,
371    /// Aliasing groups: sets of resource indices that share the same memory.
372    alias_groups: Vec<Vec<usize>>,
373}
374
375impl ResourcePool {
376    pub fn new() -> Self {
377        Self {
378            resources: Vec::new(),
379            next_id: 1,
380            name_map: HashMap::new(),
381            free_list: Vec::new(),
382            current_frame: 0,
383            max_idle_frames: 3,
384            version_chains: HashMap::new(),
385            alias_groups: Vec::new(),
386        }
387    }
388
389    /// Advance to the next frame. Frees resources that have been idle too long.
390    pub fn begin_frame(&mut self) {
391        self.current_frame += 1;
392        self.version_chains.clear();
393        self.alias_groups.clear();
394
395        // Return all transient resources to free list
396        let mut to_free = Vec::new();
397        for (i, r) in self.resources.iter().enumerate() {
398            if r.lifetime == ResourceLifetime::Transient {
399                to_free.push(i);
400            }
401        }
402        for idx in to_free {
403            if !self.free_list.contains(&idx) {
404                self.free_list.push(idx);
405            }
406        }
407
408        // Evict resources idle for too long
409        let max_idle = self.max_idle_frames;
410        let cur = self.current_frame;
411        let mut evicted = Vec::new();
412        for &idx in &self.free_list {
413            if cur.saturating_sub(self.resources[idx].last_used_frame) > max_idle {
414                evicted.push(idx);
415            }
416        }
417        for idx in &evicted {
418            self.free_list.retain(|&i| i != *idx);
419        }
420        // Mark evicted slots as available (we don't actually shrink the vec)
421        for idx in evicted {
422            let name = self.resources[idx].descriptor.name.clone();
423            self.name_map.remove(&name);
424        }
425
426        // Reset pass ranges on all living resources
427        for r in &mut self.resources {
428            r.first_write_pass = None;
429            r.last_read_pass = None;
430        }
431    }
432
433    /// End the current frame. Reports statistics.
434    pub fn end_frame(&self) -> PoolFrameStats {
435        let active = self.resources.len() - self.free_list.len();
436        let free = self.free_list.len();
437        let total_bytes: u64 = self
438            .resources
439            .iter()
440            .enumerate()
441            .filter(|(i, _)| !self.free_list.contains(i))
442            .map(|(_, r)| r.descriptor.estimated_bytes(1920, 1080))
443            .sum();
444        PoolFrameStats {
445            active_resources: active,
446            free_resources: free,
447            total_resources: self.resources.len(),
448            estimated_memory_bytes: total_bytes,
449            alias_groups: self.alias_groups.len(),
450        }
451    }
452
453    /// Acquire a resource matching the given descriptor. Reuses a free slot if possible.
454    pub fn acquire(
455        &mut self,
456        descriptor: ResourceDescriptor,
457        lifetime: ResourceLifetime,
458        bb_w: u32,
459        bb_h: u32,
460    ) -> ResourceHandle {
461        // Check for existing resource by name
462        if let Some(&idx) = self.name_map.get(&descriptor.name) {
463            self.resources[idx].last_used_frame = self.current_frame;
464            self.free_list.retain(|&i| i != idx);
465            return ResourceHandle::new(idx as u32, 0);
466        }
467
468        // Try to reuse a compatible free resource
469        let compatible = self.free_list.iter().position(|&idx| {
470            self.resources[idx]
471                .descriptor
472                .is_compatible_with(&descriptor, bb_w, bb_h)
473        });
474
475        if let Some(free_pos) = compatible {
476            let idx = self.free_list.remove(free_pos);
477            self.resources[idx].descriptor.name = descriptor.name.clone();
478            self.resources[idx].last_used_frame = self.current_frame;
479            self.name_map.insert(descriptor.name, idx);
480            return ResourceHandle::new(idx as u32, 0);
481        }
482
483        // Allocate new
484        let id = self.next_id;
485        self.next_id += 1;
486        let idx = self.resources.len();
487        let name = descriptor.name.clone();
488        self.resources
489            .push(PhysicalResource::new(id, descriptor, lifetime));
490        self.resources[idx].last_used_frame = self.current_frame;
491        self.name_map.insert(name, idx);
492        ResourceHandle::new(idx as u32, 0)
493    }
494
495    /// Release a resource back to the free list.
496    pub fn release(&mut self, handle: ResourceHandle) {
497        let idx = handle.index as usize;
498        if idx < self.resources.len() && !self.free_list.contains(&idx) {
499            self.free_list.push(idx);
500        }
501    }
502
503    /// Mark a pass as writing to a resource.
504    pub fn record_write(&mut self, handle: ResourceHandle, pass_idx: usize, pass_name: &str) {
505        let idx = handle.index as usize;
506        if idx < self.resources.len() {
507            let r = &mut self.resources[idx];
508            if r.first_write_pass.is_none() {
509                r.first_write_pass = Some(pass_idx);
510            }
511            r.last_read_pass = Some(r.last_read_pass.map_or(pass_idx, |v| v.max(pass_idx)));
512        }
513
514        let chain = self
515            .version_chains
516            .entry(handle.index)
517            .or_insert_with(|| {
518                let desc = self.resources[idx].descriptor.clone();
519                let lt = self.resources[idx].lifetime;
520                ResourceVersionChain::new(handle, desc, lt)
521            });
522        chain.record_write(pass_name);
523    }
524
525    /// Mark a pass as reading from a resource.
526    pub fn record_read(&mut self, handle: ResourceHandle, pass_idx: usize, _pass_name: &str) {
527        let idx = handle.index as usize;
528        if idx < self.resources.len() {
529            let r = &mut self.resources[idx];
530            r.last_read_pass = Some(r.last_read_pass.map_or(pass_idx, |v| v.max(pass_idx)));
531        }
532
533        if let Some(chain) = self.version_chains.get_mut(&handle.index) {
534            let ver = chain.current_version();
535            chain.record_read(ver, _pass_name);
536        }
537    }
538
539    /// Get the descriptor for a handle.
540    pub fn descriptor(&self, handle: ResourceHandle) -> Option<&ResourceDescriptor> {
541        self.resources
542            .get(handle.index as usize)
543            .map(|r| &r.descriptor)
544    }
545
546    /// Get a physical resource by handle.
547    pub fn physical(&self, handle: ResourceHandle) -> Option<&PhysicalResource> {
548        self.resources.get(handle.index as usize)
549    }
550
551    /// Compute aliasing groups: resources whose lifetimes don't overlap can share memory.
552    pub fn compute_aliasing(&mut self, total_passes: usize) -> &[Vec<usize>] {
553        self.alias_groups.clear();
554
555        let transient_indices: Vec<usize> = self
556            .resources
557            .iter()
558            .enumerate()
559            .filter(|(i, r)| r.lifetime == ResourceLifetime::Transient && !self.free_list.contains(i))
560            .map(|(i, _)| i)
561            .collect();
562
563        // Greedy interval colouring
564        let mut assigned: Vec<bool> = vec![false; transient_indices.len()];
565
566        for (i, &idx_a) in transient_indices.iter().enumerate() {
567            if assigned[i] {
568                continue;
569            }
570            let mut group = vec![idx_a];
571            assigned[i] = true;
572
573            for (j, &idx_b) in transient_indices.iter().enumerate().skip(i + 1) {
574                if assigned[j] {
575                    continue;
576                }
577                // Check that idx_b doesn't overlap with anything in the group
578                let overlaps = group.iter().any(|&g| {
579                    self.lifetimes_overlap(g, idx_b, total_passes)
580                });
581                if !overlaps {
582                    group.push(idx_b);
583                    assigned[j] = true;
584                }
585            }
586
587            if group.len() > 1 {
588                self.alias_groups.push(group);
589            }
590        }
591
592        &self.alias_groups
593    }
594
595    fn lifetimes_overlap(&self, a: usize, b: usize, _total: usize) -> bool {
596        let ra = &self.resources[a];
597        let rb = &self.resources[b];
598        let a_start = ra.first_write_pass.unwrap_or(0);
599        let a_end = ra.last_read_pass.unwrap_or(0);
600        let b_start = rb.first_write_pass.unwrap_or(0);
601        let b_end = rb.last_read_pass.unwrap_or(0);
602        a_start <= b_end && b_start <= a_end
603    }
604
605    /// Check all version chains for read-after-write hazards.
606    pub fn detect_raw_hazards(&self) -> Vec<(String, u32)> {
607        let mut hazards = Vec::new();
608        for (_, chain) in &self.version_chains {
609            if chain.has_raw_hazard() {
610                hazards.push((chain.descriptor.name.clone(), chain.handle.index));
611            }
612        }
613        hazards
614    }
615
616    /// Estimate total memory budget for all active resources.
617    pub fn estimate_memory_budget(&self, bb_w: u32, bb_h: u32) -> MemoryBudget {
618        let mut total = 0u64;
619        let mut transient = 0u64;
620        let mut imported = 0u64;
621        let mut peak = 0u64;
622
623        // Per-pass memory high-water mark
624        let max_pass = self
625            .resources
626            .iter()
627            .filter_map(|r| r.last_read_pass)
628            .max()
629            .unwrap_or(0);
630
631        for pass_idx in 0..=max_pass {
632            let mut frame_mem = 0u64;
633            for (i, r) in self.resources.iter().enumerate() {
634                if self.free_list.contains(&i) {
635                    continue;
636                }
637                if r.is_alive_at(pass_idx) {
638                    frame_mem += r.descriptor.estimated_bytes(bb_w, bb_h);
639                }
640            }
641            peak = peak.max(frame_mem);
642        }
643
644        for (i, r) in self.resources.iter().enumerate() {
645            if self.free_list.contains(&i) {
646                continue;
647            }
648            let bytes = r.descriptor.estimated_bytes(bb_w, bb_h);
649            total += bytes;
650            match r.lifetime {
651                ResourceLifetime::Transient => transient += bytes,
652                ResourceLifetime::Imported => imported += bytes,
653            }
654        }
655
656        MemoryBudget {
657            total_bytes: total,
658            transient_bytes: transient,
659            imported_bytes: imported,
660            peak_frame_bytes: peak,
661            resource_count: self.resources.len() - self.free_list.len(),
662        }
663    }
664
665    /// Total number of resources (including free slots).
666    pub fn total_slots(&self) -> usize {
667        self.resources.len()
668    }
669
670    /// Number of active (non-free) resources.
671    pub fn active_count(&self) -> usize {
672        self.resources.len() - self.free_list.len()
673    }
674
675    /// Get the version chain for a resource handle.
676    pub fn version_chain(&self, handle: ResourceHandle) -> Option<&ResourceVersionChain> {
677        self.version_chains.get(&handle.index)
678    }
679}
680
681impl Default for ResourcePool {
682    fn default() -> Self {
683        Self::new()
684    }
685}
686
687// ---------------------------------------------------------------------------
688// Imported resource wrapper
689// ---------------------------------------------------------------------------
690
691/// An externally-managed resource brought into the render graph.
692#[derive(Debug, Clone)]
693pub struct ImportedResource {
694    pub name: String,
695    pub descriptor: ResourceDescriptor,
696    /// External handle / ID for the actual GPU object.
697    pub external_id: u64,
698}
699
700impl ImportedResource {
701    pub fn new(name: &str, descriptor: ResourceDescriptor, external_id: u64) -> Self {
702        Self {
703            name: name.to_string(),
704            descriptor,
705            external_id,
706        }
707    }
708}
709
710// ---------------------------------------------------------------------------
711// Statistics
712// ---------------------------------------------------------------------------
713
714/// Per-frame resource pool statistics.
715#[derive(Debug, Clone)]
716pub struct PoolFrameStats {
717    pub active_resources: usize,
718    pub free_resources: usize,
719    pub total_resources: usize,
720    pub estimated_memory_bytes: u64,
721    pub alias_groups: usize,
722}
723
724impl fmt::Display for PoolFrameStats {
725    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
726        write!(
727            f,
728            "Pool: {} active, {} free, {} total, {:.2} MB, {} alias groups",
729            self.active_resources,
730            self.free_resources,
731            self.total_resources,
732            self.estimated_memory_bytes as f64 / (1024.0 * 1024.0),
733            self.alias_groups,
734        )
735    }
736}
737
738/// Memory budget estimate.
739#[derive(Debug, Clone)]
740pub struct MemoryBudget {
741    pub total_bytes: u64,
742    pub transient_bytes: u64,
743    pub imported_bytes: u64,
744    pub peak_frame_bytes: u64,
745    pub resource_count: usize,
746}
747
748impl MemoryBudget {
749    pub fn total_mb(&self) -> f64 {
750        self.total_bytes as f64 / (1024.0 * 1024.0)
751    }
752
753    pub fn peak_mb(&self) -> f64 {
754        self.peak_frame_bytes as f64 / (1024.0 * 1024.0)
755    }
756}
757
758impl fmt::Display for MemoryBudget {
759    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
760        write!(
761            f,
762            "Budget: {:.2} MB total ({:.2} MB transient, {:.2} MB imported), peak {:.2} MB, {} resources",
763            self.total_mb(),
764            self.transient_bytes as f64 / (1024.0 * 1024.0),
765            self.imported_bytes as f64 / (1024.0 * 1024.0),
766            self.peak_mb(),
767            self.resource_count,
768        )
769    }
770}
771
772// ---------------------------------------------------------------------------
773// Transient resource helper
774// ---------------------------------------------------------------------------
775
776/// A transient resource that is created at first use and destroyed after last
777/// use within a single frame. This is a convenience wrapper; the actual
778/// lifetime management is performed by [`ResourcePool`].
779#[derive(Debug, Clone)]
780pub struct TransientResource {
781    pub handle: ResourceHandle,
782    pub descriptor: ResourceDescriptor,
783}
784
785impl TransientResource {
786    pub fn new(handle: ResourceHandle, descriptor: ResourceDescriptor) -> Self {
787        Self { handle, descriptor }
788    }
789
790    pub fn name(&self) -> &str {
791        &self.descriptor.name
792    }
793
794    pub fn format(&self) -> TextureFormat {
795        self.descriptor.format
796    }
797
798    pub fn estimated_bytes(&self, bb_w: u32, bb_h: u32) -> u64 {
799        self.descriptor.estimated_bytes(bb_w, bb_h)
800    }
801}
802
803// ---------------------------------------------------------------------------
804// Resource table (used during graph building)
805// ---------------------------------------------------------------------------
806
807/// Bookkeeping structure used while constructing a render graph.
808/// Maps logical resource names to handles and descriptors.
809pub struct ResourceTable {
810    entries: Vec<ResourceTableEntry>,
811    name_to_index: HashMap<String, usize>,
812}
813
814#[derive(Debug, Clone)]
815pub struct ResourceTableEntry {
816    pub name: String,
817    pub descriptor: ResourceDescriptor,
818    pub handle: ResourceHandle,
819    pub lifetime: ResourceLifetime,
820    /// Which passes write to this resource.
821    pub writers: Vec<String>,
822    /// Which passes read from this resource.
823    pub readers: Vec<String>,
824}
825
826impl ResourceTable {
827    pub fn new() -> Self {
828        Self {
829            entries: Vec::new(),
830            name_to_index: HashMap::new(),
831        }
832    }
833
834    /// Declare a transient resource.
835    pub fn declare_transient(&mut self, descriptor: ResourceDescriptor) -> ResourceHandle {
836        let name = descriptor.name.clone();
837        if let Some(&idx) = self.name_to_index.get(&name) {
838            return self.entries[idx].handle;
839        }
840        let idx = self.entries.len();
841        let handle = ResourceHandle::new(idx as u32, 0);
842        self.entries.push(ResourceTableEntry {
843            name: name.clone(),
844            descriptor,
845            handle,
846            lifetime: ResourceLifetime::Transient,
847            writers: Vec::new(),
848            readers: Vec::new(),
849        });
850        self.name_to_index.insert(name, idx);
851        handle
852    }
853
854    /// Declare an imported resource.
855    pub fn declare_imported(&mut self, descriptor: ResourceDescriptor) -> ResourceHandle {
856        let name = descriptor.name.clone();
857        if let Some(&idx) = self.name_to_index.get(&name) {
858            return self.entries[idx].handle;
859        }
860        let idx = self.entries.len();
861        let handle = ResourceHandle::new(idx as u32, 0);
862        self.entries.push(ResourceTableEntry {
863            name: name.clone(),
864            descriptor,
865            handle,
866            lifetime: ResourceLifetime::Imported,
867            writers: Vec::new(),
868            readers: Vec::new(),
869        });
870        self.name_to_index.insert(name, idx);
871        handle
872    }
873
874    /// Record that a pass writes to a resource.
875    pub fn add_writer(&mut self, handle: ResourceHandle, pass_name: &str) {
876        if let Some(entry) = self.entries.get_mut(handle.index as usize) {
877            if !entry.writers.contains(&pass_name.to_string()) {
878                entry.writers.push(pass_name.to_string());
879            }
880        }
881    }
882
883    /// Record that a pass reads from a resource.
884    pub fn add_reader(&mut self, handle: ResourceHandle, pass_name: &str) {
885        if let Some(entry) = self.entries.get_mut(handle.index as usize) {
886            if !entry.readers.contains(&pass_name.to_string()) {
887                entry.readers.push(pass_name.to_string());
888            }
889        }
890    }
891
892    /// Look up a resource by name.
893    pub fn lookup(&self, name: &str) -> Option<ResourceHandle> {
894        self.name_to_index
895            .get(name)
896            .map(|&idx| self.entries[idx].handle)
897    }
898
899    /// Get entry by handle.
900    pub fn entry(&self, handle: ResourceHandle) -> Option<&ResourceTableEntry> {
901        self.entries.get(handle.index as usize)
902    }
903
904    /// Iterate all entries.
905    pub fn entries(&self) -> &[ResourceTableEntry] {
906        &self.entries
907    }
908
909    /// Find dangling resources: declared but never written or never read.
910    pub fn find_dangling(&self) -> Vec<DanglingResource> {
911        let mut result = Vec::new();
912        for entry in &self.entries {
913            if entry.writers.is_empty() && entry.lifetime == ResourceLifetime::Transient {
914                result.push(DanglingResource {
915                    name: entry.name.clone(),
916                    kind: DanglingKind::NeverWritten,
917                });
918            }
919            if entry.readers.is_empty() && entry.lifetime == ResourceLifetime::Transient {
920                result.push(DanglingResource {
921                    name: entry.name.clone(),
922                    kind: DanglingKind::NeverRead,
923                });
924            }
925        }
926        result
927    }
928
929    pub fn len(&self) -> usize {
930        self.entries.len()
931    }
932
933    pub fn is_empty(&self) -> bool {
934        self.entries.is_empty()
935    }
936}
937
938impl Default for ResourceTable {
939    fn default() -> Self {
940        Self::new()
941    }
942}
943
944/// A resource that is improperly connected.
945#[derive(Debug, Clone)]
946pub struct DanglingResource {
947    pub name: String,
948    pub kind: DanglingKind,
949}
950
951#[derive(Debug, Clone, Copy, PartialEq, Eq)]
952pub enum DanglingKind {
953    NeverWritten,
954    NeverRead,
955}
956
957impl fmt::Display for DanglingResource {
958    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
959        match self.kind {
960            DanglingKind::NeverWritten => write!(f, "'{}' is never written", self.name),
961            DanglingKind::NeverRead => write!(f, "'{}' is never read", self.name),
962        }
963    }
964}
965
966// ---------------------------------------------------------------------------
967// Tests
968// ---------------------------------------------------------------------------
969
970#[cfg(test)]
971mod tests {
972    use super::*;
973
974    fn make_desc(name: &str) -> ResourceDescriptor {
975        ResourceDescriptor::new(name, TextureFormat::Rgba16Float)
976    }
977
978    #[test]
979    fn test_pool_acquire_release() {
980        let mut pool = ResourcePool::new();
981        pool.begin_frame();
982        let h = pool.acquire(make_desc("color"), ResourceLifetime::Transient, 1920, 1080);
983        assert_eq!(h.index, 0);
984        assert_eq!(pool.active_count(), 1);
985        pool.release(h);
986        assert_eq!(pool.active_count(), 0);
987    }
988
989    #[test]
990    fn test_pool_reuse() {
991        let mut pool = ResourcePool::new();
992        pool.begin_frame();
993        let h1 = pool.acquire(make_desc("a"), ResourceLifetime::Transient, 1920, 1080);
994        pool.release(h1);
995        let h2 = pool.acquire(make_desc("b"), ResourceLifetime::Transient, 1920, 1080);
996        // Should reuse the same slot
997        assert_eq!(h1.index, h2.index);
998    }
999
1000    #[test]
1001    fn test_memory_budget() {
1002        let mut pool = ResourcePool::new();
1003        pool.begin_frame();
1004        let _h = pool.acquire(make_desc("color"), ResourceLifetime::Transient, 1920, 1080);
1005        let budget = pool.estimate_memory_budget(1920, 1080);
1006        assert!(budget.total_bytes > 0);
1007        assert!(budget.transient_bytes > 0);
1008        assert_eq!(budget.imported_bytes, 0);
1009    }
1010
1011    #[test]
1012    fn test_resource_table_dangling() {
1013        let mut table = ResourceTable::new();
1014        let h = table.declare_transient(make_desc("unused"));
1015        // Never written, never read
1016        let dangling = table.find_dangling();
1017        assert_eq!(dangling.len(), 2); // never written + never read
1018        // Now add a writer
1019        table.add_writer(h, "some_pass");
1020        let dangling = table.find_dangling();
1021        assert_eq!(dangling.len(), 1); // still never read
1022    }
1023
1024    #[test]
1025    fn test_version_chain_hazard() {
1026        let handle = ResourceHandle::new(0, 0);
1027        let desc = make_desc("test");
1028        let mut chain = ResourceVersionChain::new(handle, desc, ResourceLifetime::Transient);
1029        let v0 = chain.record_write("pass_a");
1030        chain.record_read(v0, "pass_b");
1031        let _v1 = chain.record_write("pass_c");
1032        // pass_b reads v0, pass_c writes v1 -> RAW hazard
1033        assert!(chain.has_raw_hazard());
1034    }
1035
1036    #[test]
1037    fn test_size_policy_resolve() {
1038        let abs = SizePolicy::Absolute {
1039            width: 512,
1040            height: 256,
1041        };
1042        assert_eq!(abs.resolve(1920, 1080), (512, 256));
1043
1044        let rel = SizePolicy::Relative {
1045            width_scale: 0.5,
1046            height_scale: 0.25,
1047        };
1048        assert_eq!(rel.resolve(1920, 1080), (960, 270));
1049    }
1050
1051    #[test]
1052    fn test_descriptor_estimated_bytes() {
1053        let desc = ResourceDescriptor::new("color", TextureFormat::Rgba16Float)
1054            .with_size(SizePolicy::Absolute {
1055                width: 1920,
1056                height: 1080,
1057            });
1058        let bytes = desc.estimated_bytes(1920, 1080);
1059        // 1920 * 1080 * 8 bytes = 16,588,800
1060        assert_eq!(bytes, 1920 * 1080 * 8);
1061    }
1062
1063    #[test]
1064    fn test_descriptor_mip_chain_bytes() {
1065        let desc = ResourceDescriptor::new("color", TextureFormat::Rgba8Unorm)
1066            .with_size(SizePolicy::Absolute {
1067                width: 256,
1068                height: 256,
1069            })
1070            .with_mip_levels(3);
1071        let bytes = desc.estimated_bytes(256, 256);
1072        // mip0: 256*256*4 = 262144, mip1: 128*128*4 = 65536, mip2: 64*64*4 = 16384
1073        assert_eq!(bytes, 262144 + 65536 + 16384);
1074    }
1075}