Skip to main content

cvkg_render_gpu/types/
budget.rs

1/// Budget for offscreen render targets.
2/// Prevents OOM on mobile GPUs by enforcing a maximum number of concurrent
3/// offscreen targets and a maximum total pixel count.
4#[derive(Clone, Debug)]
5pub struct OffscreenBudget {
6    /// Maximum number of concurrent offscreen targets.
7    pub max_targets: usize,
8    /// Maximum total pixel count across all offscreen targets.
9    pub max_total_pixels: u64,
10    /// Current total pixel count.
11    pub current_pixels: u64,
12    /// Current number of allocated targets.
13    pub current_targets: usize,
14}
15
16impl Default for OffscreenBudget {
17    fn default() -> Self {
18        Self {
19            max_targets: 8,
20            // 4x 1080p frames = ~8.3M pixels
21            max_total_pixels: 1920u64 * 1080 * 4,
22            current_pixels: 0,
23            current_targets: 0,
24        }
25    }
26}
27
28impl OffscreenBudget {
29    /// Create a budget with mobile-friendly defaults (lower limits).
30    pub fn mobile() -> Self {
31        Self {
32            max_targets: 4,
33            // 2x 720p frames = ~1.8M pixels
34            max_total_pixels: 1280u64 * 720 * 2,
35            current_pixels: 0,
36            current_targets: 0,
37        }
38    }
39
40    /// Check if a new target of the given size can be allocated.
41    pub fn can_allocate(&self, width: u32, height: u32) -> bool {
42        let pixels = width as u64 * height as u64;
43        self.current_targets < self.max_targets
44            && self.current_pixels + pixels <= self.max_total_pixels
45    }
46
47    /// Register a new offscreen target.
48    pub fn register(&mut self, width: u32, height: u32) {
49        self.current_pixels += width as u64 * height as u64;
50        self.current_targets += 1;
51    }
52
53    /// Release an offscreen target.
54    pub fn release(&mut self, width: u32, height: u32) {
55        self.current_pixels = self.current_pixels.saturating_sub(width as u64 * height as u64);
56        self.current_targets = self.current_targets.saturating_sub(1);
57    }
58
59    /// Reset the budget (e.g., on frame boundary).
60    pub fn reset(&mut self) {
61        self.current_pixels = 0;
62        self.current_targets = 0;
63    }
64
65    /// Returns true if the budget is exhausted.
66    pub fn is_exhausted(&self) -> bool {
67        self.current_targets >= self.max_targets
68    }
69}
70
71#[cfg(test)]
72mod p1_27_offscreen_budget_tests {
73    use super::OffscreenBudget;
74
75    #[test]
76    fn default_budget_allows_allocation() {
77        let budget = OffscreenBudget::default();
78        assert!(budget.can_allocate(1920, 1080));
79    }
80
81    #[test]
82    fn mobile_budget_has_lower_limits() {
83        let budget = OffscreenBudget::mobile();
84        assert!(budget.can_allocate(1280, 720));
85        assert!(!budget.can_allocate(3840, 2160)); // 4K exceeds mobile budget
86    }
87
88    #[test]
89    fn budget_tracks_registration() {
90        let mut budget = OffscreenBudget::default();
91        budget.register(1920, 1080);
92        assert_eq!(budget.current_targets, 1);
93        assert_eq!(budget.current_pixels, 1920u64 * 1080);
94    }
95
96    #[test]
97    fn budget_enforces_max_targets() {
98        let mut budget = OffscreenBudget {
99            max_targets: 2,
100            max_total_pixels: u64::MAX,
101            current_pixels: 0,
102            current_targets: 0,
103        };
104        budget.register(100, 100);
105        budget.register(100, 100);
106        assert!(!budget.can_allocate(100, 100)); // 3rd target exceeds max
107        assert!(budget.is_exhausted());
108    }
109
110    #[test]
111    fn budget_enforces_pixel_limit() {
112        let mut budget = OffscreenBudget {
113            max_targets: 100,
114            max_total_pixels: 1000,
115            current_pixels: 0,
116            current_targets: 0,
117        };
118        assert!(budget.can_allocate(10, 10)); // 100 pixels
119        budget.register(10, 10);
120        assert!(!budget.can_allocate(100, 10)); // 1000 pixels would exceed
121    }
122
123    #[test]
124    fn release_frees_budget() {
125        let mut budget = OffscreenBudget::default();
126        budget.register(1920, 1080);
127        budget.release(1920, 1080);
128        assert_eq!(budget.current_targets, 0);
129        assert_eq!(budget.current_pixels, 0);
130    }
131
132    #[test]
133    fn reset_clears_all() {
134        let mut budget = OffscreenBudget::default();
135        budget.register(1920, 1080);
136        budget.register(1280, 720);
137        budget.reset();
138        assert_eq!(budget.current_targets, 0);
139        assert_eq!(budget.current_pixels, 0);
140    }
141}