1use crate::{PixelSize, ValidationError, ValidationLimits};
3use std::mem::size_of;
4
5#[derive(Clone, Copy, Debug)]
7pub struct RasterLimits {
8 pub max_pixels: u64,
10 pub max_bytes: u64,
12 pub max_work: u64,
14}
15
16impl Default for RasterLimits {
17 fn default() -> Self {
18 Self::authoring()
19 }
20}
21
22impl RasterLimits {
23 pub const fn authoring() -> Self {
25 Self {
26 max_pixels: 16_777_216,
27 max_bytes: 256 * 1024 * 1024,
28 max_work: u64::MAX,
29 }
30 }
31
32 pub const fn sandbox() -> Self {
34 Self {
35 max_work: 200_000_000,
36 ..Self::authoring()
37 }
38 }
39
40 pub fn validate_size(self, size: PixelSize) -> Result<(), ValidationError> {
43 Budget::new(size, self).map(|_| ())
44 }
45}
46
47#[derive(Clone, Copy, Debug, Default)]
49pub struct SceneLimits {
50 pub structure: ValidationLimits,
52 pub raster: RasterLimits,
54}
55
56pub struct Budget {
59 pub limits: RasterLimits,
61 live_bytes: u64,
62 peak_bytes: u64,
63 work: u64,
64}
65
66impl Budget {
67 pub fn new(size: PixelSize, limits: RasterLimits) -> Result<Self, ValidationError> {
69 for (field, value) in [
70 ("max_pixels", limits.max_pixels),
71 ("max_bytes", limits.max_bytes),
72 ("max_work", limits.max_work),
73 ] {
74 if value == 0 {
75 return Err(ValidationError::invalid(
76 "prepared",
77 field,
78 "Raster limits must be positive",
79 ));
80 }
81 }
82 if u64::from(size.width) * u64::from(size.height) > limits.max_pixels {
83 return Err(ValidationError::limit(
84 "prepared",
85 "max_pixels",
86 "Output dimensions exceed the pixel budget",
87 ));
88 }
89 Ok(Self {
90 limits,
91 live_bytes: 0,
92 peak_bytes: 0,
93 work: 0,
94 })
95 }
96
97 pub fn resume(limits: RasterLimits, live_bytes: u64, peak_bytes: u64, work: u64) -> Self {
102 Self {
103 limits,
104 live_bytes,
105 peak_bytes: peak_bytes.max(live_bytes),
106 work,
107 }
108 }
109
110 pub fn live_bytes(&self) -> u64 {
112 self.live_bytes
113 }
114
115 pub fn peak_bytes(&self) -> u64 {
117 self.peak_bytes
118 }
119
120 pub fn work_done(&self) -> u64 {
122 self.work
123 }
124
125 pub fn release(&mut self, amount: u64) {
129 self.live_bytes = self.live_bytes.saturating_sub(amount);
130 }
131
132 pub fn reset_peak(&mut self) -> u64 {
137 std::mem::replace(&mut self.peak_bytes, self.live_bytes)
138 }
139
140 pub fn merge_peak(&mut self, peak: u64) {
142 self.peak_bytes = self.peak_bytes.max(peak);
143 }
144
145 pub fn set_live_bytes(&mut self, live_bytes: u64) {
147 self.live_bytes = live_bytes;
148 self.peak_bytes = self.peak_bytes.max(live_bytes);
149 }
150
151 pub fn bytes(&mut self, amount: u64, path: &str) -> Result<(), ValidationError> {
153 let next = self
154 .live_bytes
155 .checked_add(amount)
156 .filter(|value| *value <= self.limits.max_bytes)
157 .ok_or_else(|| {
158 ValidationError::limit(path, "max_bytes", "storage exceeds the logical byte budget")
159 })?;
160 self.live_bytes = next;
161 self.peak_bytes = self.peak_bytes.max(next);
162 Ok(())
163 }
164
165 pub fn work(&mut self, amount: u64, path: &str) -> Result<(), ValidationError> {
167 let next = self
168 .work
169 .checked_add(amount)
170 .filter(|value| *value <= self.limits.max_work)
171 .ok_or_else(|| ValidationError::limit(path, "max_work", "work exceeds the budget"))?;
172 self.work = next;
173 Ok(())
174 }
175
176 pub fn push<Value>(
179 &mut self,
180 values: &mut Vec<Value>,
181 value: Value,
182 path: &str,
183 ) -> Result<(), ValidationError> {
184 self.bytes(size_of::<Value>() as u64, path)?;
185 values.try_reserve(1).map_err(|_| {
186 ValidationError::limit(path, "max_bytes", "Could not reserve metadata storage")
187 })?;
188 values.push(value);
189 Ok(())
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn authoring_admits_large_work_but_retains_storage_and_pixel_limits() {
199 let limits = RasterLimits::default();
200 let mut budget = Budget::new(PixelSize::new(1920, 1080), limits).unwrap();
201 budget.work(4_000_000_000, "particles").unwrap();
202 assert_eq!(budget.work, 4_000_000_000);
203 budget.bytes(limits.max_bytes, "pixels").unwrap();
204 assert!(budget.bytes(1, "pixels").is_err());
205 assert_eq!(budget.live_bytes, limits.max_bytes);
206 assert!(limits.validate_size(PixelSize::new(8192, 8192)).is_err());
207 }
208
209 #[test]
210 fn sandbox_and_explicit_work_caps_reject_without_mutating_counters() {
211 for limits in [
212 RasterLimits::sandbox(),
213 RasterLimits {
214 max_work: 17,
215 ..RasterLimits::default()
216 },
217 ] {
218 let mut budget = Budget::new(PixelSize::new(1, 1), limits).unwrap();
219 budget.work(limits.max_work, "particles").unwrap();
220 assert!(budget.work(1, "particles").is_err());
221 assert_eq!(budget.work, limits.max_work);
222 }
223 }
224}