1use anyhow::{Result, ensure};
3use std::sync::Arc;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum ImageTextureFormat {
8 Rgba8Srgb,
10 LinearP3Rgba16Float,
12 LinearRec2020Rgba16Float,
14}
15
16#[derive(Clone, Debug)]
20pub struct ImageTextureData {
21 width: u32,
22 height: u32,
23 levels: Arc<[Vec<u8>]>,
24 byte_len: usize,
25 format: ImageTextureFormat,
26}
27impl ImageTextureData {
28 pub const MAX_BYTES: usize = 64 * 1024 * 1024;
30 pub fn new(width: u32, height: u32, levels: Vec<Vec<u8>>) -> Result<Self> {
33 Self::with_format(width, height, levels, ImageTextureFormat::Rgba8Srgb, false)
34 }
35 pub fn new_linear_p3_rgba16f(width: u32, height: u32, levels: Vec<Vec<u8>>) -> Result<Self> {
38 Self::with_format(
39 width,
40 height,
41 levels,
42 ImageTextureFormat::LinearP3Rgba16Float,
43 false,
44 )
45 }
46 pub fn new_linear_rec2020_rgba16f(
49 width: u32,
50 height: u32,
51 levels: Vec<Vec<u8>>,
52 ) -> Result<Self> {
53 Self::with_format(
54 width,
55 height,
56 levels,
57 ImageTextureFormat::LinearRec2020Rgba16Float,
58 false,
59 )
60 }
61 pub fn page(
63 width: u32,
64 height: u32,
65 bytes: Vec<u8>,
66 format: ImageTextureFormat,
67 ) -> Result<Self> {
68 Self::with_format(width, height, vec![bytes], format, true)
69 }
70 fn with_format(
71 width: u32,
72 height: u32,
73 levels: Vec<Vec<u8>>,
74 format: ImageTextureFormat,
75 page: bool,
76 ) -> Result<Self> {
77 let bpp = if format == ImageTextureFormat::Rgba8Srgb {
78 4
79 } else {
80 8
81 };
82 ensure!(
83 width > 0 && height > 0 && width <= 16384 && height <= 16384,
84 "invalid image texture dimensions"
85 );
86 let count = (32 - width.max(height).leading_zeros()) as usize;
87 ensure!(
88 levels.len() == if page { 1 } else { count },
89 "image requires a complete mip chain or an explicit single-level page"
90 );
91 let mut byte_len = 0usize;
92 for (level, bytes) in levels.iter().enumerate() {
93 let w = (width >> level).max(1) as usize;
94 let h = (height >> level).max(1) as usize;
95 ensure!(bytes.len() == w * h * bpp, "invalid image mip byte count");
96 if bpp == 8 {
97 for pixel in bytes.chunks_exact(8) {
98 let c: [u16; 4] = std::array::from_fn(|i| {
99 u16::from_le_bytes([pixel[i * 2], pixel[i * 2 + 1]])
100 });
101 ensure!(
102 c[3] <= 0x3c00 && c[..3].iter().all(|v| *v <= c[3]),
103 "RGBA16F requires bounded premultiplied linear RGB"
104 );
105 }
106 }
107 byte_len += bytes.len();
108 ensure!(byte_len <= Self::MAX_BYTES, "image texture exceeds 64 MiB");
109 }
110 Ok(Self {
111 width,
112 height,
113 levels: levels.into(),
114 byte_len,
115 format,
116 })
117 }
118 pub fn format(&self) -> ImageTextureFormat {
120 self.format
121 }
122 pub fn bytes_per_pixel(&self) -> usize {
124 if self.format == ImageTextureFormat::Rgba8Srgb {
125 4
126 } else {
127 8
128 }
129 }
130 pub fn width(&self) -> u32 {
132 self.width
133 }
134 pub fn height(&self) -> u32 {
136 self.height
137 }
138 pub fn levels(&self) -> &[Vec<u8>] {
140 &self.levels
141 }
142 pub fn staging_byte_len(&self) -> usize {
144 (0..self.levels.len())
145 .map(|level| {
146 let row = (((self.width >> level).max(1) as usize) * self.bytes_per_pixel())
147 .div_ceil(256)
148 * 256;
149 row * (self.height >> level).max(1) as usize
150 })
151 .sum()
152 }
153 pub fn byte_len(&self) -> usize {
155 self.byte_len
156 }
157}
158#[cfg(test)]
159mod tests {
160 use super::*;
161 #[test]
162 fn validates_full_odd_and_thin_chains() {
163 assert!(ImageTextureData::new(3, 1, vec![vec![0; 12], vec![0; 4]]).is_ok());
164 assert!(ImageTextureData::new(1, 4, vec![vec![0; 16], vec![0; 8], vec![0; 4]]).is_ok());
165 assert!(ImageTextureData::new(3, 1, vec![vec![0; 12]]).is_err());
166 assert!(ImageTextureData::new(0, 1, vec![]).is_err());
167 assert!(ImageTextureData::new(1, 1, vec![vec![0; 3]]).is_err());
168 }
169}
170
171#[derive(Default)]
174pub struct PreparationFrame {
175 uploads: std::sync::Mutex<std::collections::HashMap<u64, (usize, usize)>>,
176}
177impl PreparationFrame {
178 pub fn try_reserve_upload(
182 &self,
183 key: u64,
184 bytes: usize,
185 max_bytes: usize,
186 max_uploads: usize,
187 ) -> bool {
188 let mut uploads = self.uploads.lock().unwrap();
189 let usage = uploads.entry(key).or_default();
190 if usage.1 >= max_uploads || bytes > max_bytes.saturating_sub(usage.0) {
191 return false;
192 }
193 usage.0 += bytes;
194 usage.1 += 1;
195 true
196 }
197}
198
199#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
202pub struct ImageStagingPoolStats {
203 pub allocated_bytes: usize,
205 pub cached_bytes: usize,
207 pub allocations: usize,
209 pub reuses: usize,
211}
212struct PoolState<T> {
213 idle: Vec<(u64, usize, T)>,
214 stats: ImageStagingPoolStats,
215}
216pub struct ImageStagingPool<T> {
220 capacity: usize,
221 state: std::sync::Mutex<PoolState<T>>,
222}
223pub struct ImageStagingLease<T> {
226 resource: Option<T>,
227 reused: bool,
228 pool: std::sync::Weak<ImageStagingPool<T>>,
229 key: u64,
230 bytes: usize,
231}
232struct AllocationReservation<'a, T> {
233 pool: &'a ImageStagingPool<T>,
234 bytes: usize,
235 committed: bool,
236}
237impl<T> Drop for AllocationReservation<'_, T> {
238 fn drop(&mut self) {
239 if !self.committed {
240 self.pool.state.lock().unwrap().stats.allocated_bytes -= self.bytes;
241 }
242 }
243}
244impl<T> ImageStagingPool<T> {
245 pub fn new(capacity: usize) -> Arc<Self> {
247 Arc::new(Self {
248 capacity,
249 state: std::sync::Mutex::new(PoolState {
250 idle: vec![],
251 stats: ImageStagingPoolStats::default(),
252 }),
253 })
254 }
255 pub fn checkout(
258 self: &Arc<Self>,
259 key: u64,
260 bytes: usize,
261 create: impl FnOnce() -> Result<T>,
262 ) -> Result<ImageStagingLease<T>> {
263 ensure!(
264 bytes > 0 && bytes <= self.capacity,
265 "invalid staging resource size"
266 );
267 let mut state = self.state.lock().unwrap();
268 if let Some(index) = state
269 .idle
270 .iter()
271 .position(|(k, b, _)| *k == key && *b == bytes)
272 {
273 let (_, _, resource) = state.idle.swap_remove(index);
274 state.stats.cached_bytes -= bytes;
275 state.stats.reuses += 1;
276 return Ok(ImageStagingLease {
277 resource: Some(resource),
278 reused: true,
279 pool: Arc::downgrade(self),
280 key,
281 bytes,
282 });
283 }
284 ensure!(
285 bytes <= self.capacity,
286 "staging resource exceeds pool capacity"
287 );
288 while bytes > self.capacity.saturating_sub(state.stats.allocated_bytes) {
289 let Some((_, evicted, resource)) = state.idle.pop() else {
290 anyhow::bail!("staging pool is busy");
291 };
292 state.stats.allocated_bytes -= evicted;
293 state.stats.cached_bytes -= evicted;
294 drop(resource);
295 }
296 state.stats.allocated_bytes += bytes;
297 drop(state);
298 let mut reservation = AllocationReservation {
299 pool: self,
300 bytes,
301 committed: false,
302 };
303 let resource = create()?;
304 self.state.lock().unwrap().stats.allocations += 1;
305 reservation.committed = true;
306 Ok(ImageStagingLease {
307 resource: Some(resource),
308 reused: false,
309 pool: Arc::downgrade(self),
310 key,
311 bytes,
312 })
313 }
314
315 pub fn stats(&self) -> ImageStagingPoolStats {
317 self.state.lock().unwrap().stats
318 }
319}
320impl<T> ImageStagingLease<T> {
321 pub fn was_reused(&self) -> bool {
323 self.reused
324 }
325 pub fn resource(&self) -> &T {
327 self.resource.as_ref().unwrap()
328 }
329 pub fn discard(mut self) {
332 self.resource.take();
333 if let Some(pool) = self.pool.upgrade() {
334 pool.state.lock().unwrap().stats.allocated_bytes -= self.bytes;
335 }
336 }
337}
338impl<T> Drop for ImageStagingLease<T> {
339 fn drop(&mut self) {
340 if let Some(resource) = self.resource.take()
341 && let Some(pool) = self.pool.upgrade()
342 {
343 let mut state = pool.state.lock().unwrap();
344 state.stats.cached_bytes += self.bytes;
345 state.idle.push((self.key, self.bytes, resource));
346 }
347 }
348}
349
350#[cfg(test)]
351mod resource_tests {
352 use super::*;
353 #[test]
354 fn frame_admission_shares_counts_and_bytes_without_charging_rejection() {
355 let frame = PreparationFrame::default();
356 assert!(frame.try_reserve_upload(1, 6, 10, 2));
357 assert!(!frame.try_reserve_upload(1, 5, 10, 2));
358 assert!(frame.try_reserve_upload(1, 4, 10, 2));
359 assert!(!frame.try_reserve_upload(1, 0, 10, 2));
360 assert!(frame.try_reserve_upload(2, 10, 10, 1));
361 assert!(PreparationFrame::default().try_reserve_upload(1, 10, 10, 1));
362 }
363 #[test]
364 fn pool_honors_outstanding_leases_eviction_and_failed_allocations() -> Result<()> {
365 let pool = ImageStagingPool::new(16);
366 let lease = Arc::new(pool.checkout(1, 16, || Ok(vec![7; 16]))?);
367 let guard = lease.clone();
368 drop(lease);
369 assert!(pool.checkout(1, 16, || Ok(vec![0; 16])).is_err());
370 drop(guard);
371 let reused = pool.checkout(1, 16, || panic!("must reuse"))?;
372 assert!(reused.was_reused());
373 assert_eq!(reused.resource(), &vec![7; 16]);
374 drop(reused);
375 let failure: Result<ImageStagingLease<Vec<u8>>> =
376 pool.checkout(2, 8, || anyhow::bail!("allocation failed"));
377 assert!(failure.is_err());
378 assert_eq!(pool.stats().allocated_bytes, 0);
379 let discarded = pool.checkout(3, 8, || Ok(vec![3; 8]))?;
380 discarded.discard();
381 assert_eq!(pool.stats().allocated_bytes, 0);
382 assert_eq!(pool.stats().cached_bytes, 0);
383 assert_eq!(pool.stats().reuses, 1);
384 let panic = std::panic::catch_unwind(|| {
385 let _ = pool.checkout(4, 8, || -> Result<Vec<u8>> {
386 panic!("driver allocation panic")
387 });
388 });
389 assert!(panic.is_err());
390 assert_eq!(pool.stats().allocated_bytes, 0);
391 Ok(())
392 }
393}
394
395#[derive(Clone, Copy, Debug, PartialEq)]
400pub struct LinearImagePaint {
401 rows: [[f32; 4]; 3],
402 vignette: [f32; 4],
403 p3: bool,
404}
405impl LinearImagePaint {
406 pub fn new(rows: [[f32; 4]; 3], vignette: [f32; 4]) -> Result<Self> {
410 ensure!(
411 rows.iter().all(|row| row.iter().all(|v| v.is_finite())
412 && row.iter().map(|v| f64::from(v.abs())).sum::<f64>() <= f64::from(f32::MAX) / 4.),
413 "image matrix exceeds f32 sampling range"
414 );
415 let [intensity, radius, feather, roundness] = vignette;
416 ensure!(
417 vignette.iter().all(|v| v.is_finite())
418 && (0.0..=1.0).contains(&intensity)
419 && radius >= 0.
420 && feather >= f32::MIN_POSITIVE
421 && (0.0..=1.0).contains(&roundness),
422 "invalid image vignette"
423 );
424 Ok(Self {
425 rows,
426 vignette,
427 p3: false,
428 })
429 }
430 pub fn with_p3(mut self, p3: bool) -> Self {
432 self.p3 = p3;
433 self
434 }
435 pub fn is_p3(self) -> bool {
437 self.p3
438 }
439 pub fn rows(self) -> [[f32; 4]; 3] {
441 self.rows
442 }
443 pub fn vignette(self) -> [f32; 4] {
445 self.vignette
446 }
447}
448
449#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
452pub enum ImageColorMode {
453 #[default]
454 EncodedSrgb,
456 LinearP3Preview,
458 LinearP3Wide,
460 LinearRec2020Preview,
462 LinearRec2020Wide,
464}
465impl ImageColorMode {
466 pub fn shader_value(self) -> f32 {
468 match self {
469 Self::EncodedSrgb => 0.,
470 Self::LinearP3Preview => 1.,
471 Self::LinearP3Wide => 2.,
472 Self::LinearRec2020Preview => 3.,
473 Self::LinearRec2020Wide => 4.,
474 }
475 }
476}