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
56            .current_pixels
57            .saturating_sub(width as u64 * height as u64);
58        self.current_targets = self.current_targets.saturating_sub(1);
59    }
60
61    /// Reset the budget (e.g., on frame boundary).
62    pub fn reset(&mut self) {
63        self.current_pixels = 0;
64        self.current_targets = 0;
65    }
66
67    /// Returns true if the budget is exhausted.
68    pub fn is_exhausted(&self) -> bool {
69        self.current_targets >= self.max_targets
70    }
71}
72
73#[cfg(test)]
74mod p1_27_offscreen_budget_tests {
75    use super::OffscreenBudget;
76
77    #[test]
78    fn default_budget_allows_allocation() {
79        let budget = OffscreenBudget::default();
80        assert!(budget.can_allocate(1920, 1080));
81    }
82
83    #[test]
84    fn mobile_budget_has_lower_limits() {
85        let budget = OffscreenBudget::mobile();
86        assert!(budget.can_allocate(1280, 720));
87        assert!(!budget.can_allocate(3840, 2160)); // 4K exceeds mobile budget
88    }
89
90    #[test]
91    fn budget_tracks_registration() {
92        let mut budget = OffscreenBudget::default();
93        budget.register(1920, 1080);
94        assert_eq!(budget.current_targets, 1);
95        assert_eq!(budget.current_pixels, 1920u64 * 1080);
96    }
97
98    #[test]
99    fn budget_enforces_max_targets() {
100        let mut budget = OffscreenBudget {
101            max_targets: 2,
102            max_total_pixels: u64::MAX,
103            current_pixels: 0,
104            current_targets: 0,
105        };
106        budget.register(100, 100);
107        budget.register(100, 100);
108        assert!(!budget.can_allocate(100, 100)); // 3rd target exceeds max
109        assert!(budget.is_exhausted());
110    }
111
112    #[test]
113    fn budget_enforces_pixel_limit() {
114        let mut budget = OffscreenBudget {
115            max_targets: 100,
116            max_total_pixels: 1000,
117            current_pixels: 0,
118            current_targets: 0,
119        };
120        assert!(budget.can_allocate(10, 10)); // 100 pixels
121        budget.register(10, 10);
122        assert!(!budget.can_allocate(100, 10)); // 1000 pixels would exceed
123    }
124
125    #[test]
126    fn release_frees_budget() {
127        let mut budget = OffscreenBudget::default();
128        budget.register(1920, 1080);
129        budget.release(1920, 1080);
130        assert_eq!(budget.current_targets, 0);
131        assert_eq!(budget.current_pixels, 0);
132    }
133
134    #[test]
135    fn reset_clears_all() {
136        let mut budget = OffscreenBudget::default();
137        budget.register(1920, 1080);
138        budget.register(1280, 720);
139        budget.reset();
140        assert_eq!(budget.current_targets, 0);
141        assert_eq!(budget.current_pixels, 0);
142    }
143}