1use crate::{PixelSize, ValidationError};
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
47pub struct Budget {
50 pub limits: RasterLimits,
52 live_bytes: u64,
53 peak_bytes: u64,
54 work: u64,
55}
56
57impl Budget {
58 pub fn new(size: PixelSize, limits: RasterLimits) -> Result<Self, ValidationError> {
60 for (field, value) in [
61 ("max_pixels", limits.max_pixels),
62 ("max_bytes", limits.max_bytes),
63 ("max_work", limits.max_work),
64 ] {
65 if value == 0 {
66 return Err(ValidationError::invalid(
67 "prepared",
68 field,
69 "Raster limits must be positive",
70 ));
71 }
72 }
73 if u64::from(size.width) * u64::from(size.height) > limits.max_pixels {
74 return Err(ValidationError::limit(
75 "prepared",
76 "max_pixels",
77 "Output dimensions exceed the pixel budget",
78 ));
79 }
80 Ok(Self {
81 limits,
82 live_bytes: 0,
83 peak_bytes: 0,
84 work: 0,
85 })
86 }
87
88 pub fn resume(limits: RasterLimits, live_bytes: u64, peak_bytes: u64, work: u64) -> Self {
93 Self {
94 limits,
95 live_bytes,
96 peak_bytes: peak_bytes.max(live_bytes),
97 work,
98 }
99 }
100
101 pub fn live_bytes(&self) -> u64 {
103 self.live_bytes
104 }
105
106 pub fn peak_bytes(&self) -> u64 {
108 self.peak_bytes
109 }
110
111 pub fn work_done(&self) -> u64 {
113 self.work
114 }
115
116 pub fn release(&mut self, amount: u64) {
120 self.live_bytes = self.live_bytes.saturating_sub(amount);
121 }
122
123 pub fn reset_peak(&mut self) -> u64 {
128 std::mem::replace(&mut self.peak_bytes, self.live_bytes)
129 }
130
131 pub fn merge_peak(&mut self, peak: u64) {
133 self.peak_bytes = self.peak_bytes.max(peak);
134 }
135
136 pub fn set_live_bytes(&mut self, live_bytes: u64) {
138 self.live_bytes = live_bytes;
139 self.peak_bytes = self.peak_bytes.max(live_bytes);
140 }
141
142 pub fn bytes(&mut self, amount: u64, path: &str) -> Result<(), ValidationError> {
144 let next = self
145 .live_bytes
146 .checked_add(amount)
147 .filter(|value| *value <= self.limits.max_bytes)
148 .ok_or_else(|| {
149 ValidationError::limit(path, "max_bytes", "storage exceeds the logical byte budget")
150 })?;
151 self.live_bytes = next;
152 self.peak_bytes = self.peak_bytes.max(next);
153 Ok(())
154 }
155
156 pub fn work(&mut self, amount: u64, path: &str) -> Result<(), ValidationError> {
158 let next = self
159 .work
160 .checked_add(amount)
161 .filter(|value| *value <= self.limits.max_work)
162 .ok_or_else(|| ValidationError::limit(path, "max_work", "work exceeds the budget"))?;
163 self.work = next;
164 Ok(())
165 }
166
167 pub fn push<Value>(
170 &mut self,
171 values: &mut Vec<Value>,
172 value: Value,
173 path: &str,
174 ) -> Result<(), ValidationError> {
175 self.bytes(size_of::<Value>() as u64, path)?;
176 values.try_reserve(1).map_err(|_| {
177 ValidationError::limit(path, "max_bytes", "Could not reserve metadata storage")
178 })?;
179 values.push(value);
180 Ok(())
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn authoring_admits_large_work_but_retains_storage_and_pixel_limits() {
190 let limits = RasterLimits::default();
191 let mut budget = Budget::new(PixelSize::new(1920, 1080), limits).unwrap();
192 budget.work(4_000_000_000, "particles").unwrap();
193 assert_eq!(budget.work, 4_000_000_000);
194 budget.bytes(limits.max_bytes, "pixels").unwrap();
195 assert!(budget.bytes(1, "pixels").is_err());
196 assert_eq!(budget.live_bytes, limits.max_bytes);
197 assert!(limits.validate_size(PixelSize::new(8192, 8192)).is_err());
198 }
199
200 #[test]
201 fn sandbox_and_explicit_work_caps_reject_without_mutating_counters() {
202 for limits in [
203 RasterLimits::sandbox(),
204 RasterLimits {
205 max_work: 17,
206 ..RasterLimits::default()
207 },
208 ] {
209 let mut budget = Budget::new(PixelSize::new(1, 1), limits).unwrap();
210 budget.work(limits.max_work, "particles").unwrap();
211 assert!(budget.work(1, "particles").is_err());
212 assert_eq!(budget.work, limits.max_work);
213 }
214 }
215}