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