Skip to main content

gpui/
image_texture.rs

1//! Validated immutable upload data for opt-in source-sized image textures.
2use anyhow::{Result, ensure};
3use std::sync::Arc;
4
5/// Storage and color contract for source-sized image textures.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum ImageTextureFormat {
8    /// Straight alpha, encoded sRGB.
9    Rgba8Srgb,
10    /// Premultiplied, bounded linear Display P3 / D65, little-endian binary16.
11    LinearP3Rgba16Float,
12}
13
14/// Full mip chain with a validated storage, color and alpha contract.
15/// This is an upload description, not a device resource. Backends retain normal
16/// scene ordering and coverage when sampling it at a different display size.
17#[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    /// Maximum combined mip bytes for one source (64 MiB).
27    pub const MAX_BYTES: usize = 64 * 1024 * 1024;
28    /// Validate a complete mip chain, including odd and one-dimensional sizes.
29    /// Every level halves each dimension, rounding down and clamping to one.
30    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    /// Validate a complete bounded premultiplied linear-P3 RGBA16F mip chain.
34    /// Rejects non-finite, negative, out-of-range and invalid associated pixels.
35    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    /// A single immutable mip page; the canvas selects the pyramid level.
45    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    /// Validated source storage/color contract.
102    pub fn format(&self) -> ImageTextureFormat {
103        self.format
104    }
105    /// Unpadded bytes per source pixel.
106    pub fn bytes_per_pixel(&self) -> usize {
107        if self.format == ImageTextureFormat::Rgba8Srgb {
108            4
109        } else {
110            8
111        }
112    }
113    /// Source width in physical pixels.
114    pub fn width(&self) -> u32 {
115        self.width
116    }
117    /// Source height in physical pixels.
118    pub fn height(&self) -> u32 {
119        self.height
120    }
121    /// Complete immutable mip chain, largest level first.
122    pub fn levels(&self) -> &[Vec<u8>] {
123        &self.levels
124    }
125    /// Bytes in a 256-byte-row-aligned staging buffer for the complete chain.
126    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    /// Combined unpadded upload and GPU payload size.
137    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/// Shared admission counters for one scene preparation. Old custom producers
155/// ignore this optional context; image producers can share loader-specific limits.
156#[derive(Default)]
157pub struct PreparationFrame {
158    uploads: std::sync::Mutex<std::collections::HashMap<u64, (usize, usize)>>,
159}
160impl PreparationFrame {
161    /// Charge one source handoff. The key identifies a retained policy owner.
162    /// A rejected reservation changes no counters. Already-retained outputs do
163    /// not need a reservation; limits apply only to newly recorded image copies.
164    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/// Resource-only cache statistics. All allocated resources, including idle and
183/// GPU-borrowed ones, count against capacity until evicted or the pool is dropped.
184#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
185pub struct ImageStagingPoolStats {
186    /// Capacity currently owned by the pool and its leases.
187    pub allocated_bytes: usize,
188    /// Bytes currently idle and eligible for reuse.
189    pub cached_bytes: usize,
190    /// Successful backend resource allocations.
191    pub allocations: usize,
192    /// Successful checkouts of a previously idle resource.
193    pub reuses: usize,
194}
195struct PoolState<T> {
196    idle: Vec<(u64, usize, T)>,
197    stats: ImageStagingPoolStats,
198}
199/// Backend SPI for bounded reusable staging resources. A lease may return to
200/// this cache only after the host's completion guard has released its last Arc.
201/// Backends own resource creation, mapping, synchronization and device identity.
202pub struct ImageStagingPool<T> {
203    capacity: usize,
204    state: std::sync::Mutex<PoolState<T>>,
205}
206/// Exclusive staging ownership; sharing this lease delays reuse until all
207/// completion guards and staging snapshots have released it.
208pub 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    /// Create an empty cache with a hard resource-byte capacity.
229    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    /// Allocate outside the pool lock. Idle mismatches are evicted to make room;
239    /// borrowed resources are never evicted or reused. Errors roll back capacity.
240    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    /// Read allocation and idle-cache counters.
299    pub fn stats(&self) -> ImageStagingPoolStats {
300        self.state.lock().unwrap().stats
301    }
302}
303impl<T> ImageStagingLease<T> {
304    /// Whether this checkout reuses a resource instead of allocating one.
305    pub fn was_reused(&self) -> bool {
306        self.reused
307    }
308    /// Access the backend resource while this exclusive lease is held.
309    pub fn resource(&self) -> &T {
310        self.resource.as_ref().unwrap()
311    }
312    /// Discard a mapping/driver failure instead of returning a resource whose
313    /// state is no longer suitable for reuse.
314    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/// Opt-in linear-sRGB image sampling parameters. Only bounded encoded-sRGB
379/// sources are supported here; this is not a wide/HDR texture descriptor.
380/// RGB rows multiply decoded [R,G,B,1], then vignette darkens, then RGB clips
381/// and encodes to SDR sRGB. Alpha is preserved before native coverage/opacity.
382#[derive(Clone, Copy, Debug, PartialEq)]
383pub struct LinearImagePaint {
384    rows: [[f32; 4]; 3],
385    vignette: [f32; 4],
386    p3: bool,
387}
388impl LinearImagePaint {
389    /// Vignette = intensity, inner radius, positive feather, roundness. Radius
390    /// units refer to the shortest source edge; roundness 0 follows its ellipse.
391    /// Reject coefficients too large for finite f32 sampling arithmetic.
392    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    /// Select linear Display P3 working primaries.
414    pub fn with_p3(mut self, p3: bool) -> Self {
415        self.p3 = p3;
416        self
417    }
418    /// Whether rows operate in linear Display P3.
419    pub fn is_p3(self) -> bool {
420        self.p3
421    }
422    /// Affine rows evaluated in the selected linear working primaries.
423    pub fn rows(self) -> [[f32; 4]; 3] {
424        self.rows
425    }
426    /// Intensity, inner radius, feather and roundness in source-image units.
427    pub fn vignette(self) -> [f32; 4] {
428        self.vignette
429    }
430}
431
432/// Source interpretation and requested presentation. Wide requires an accepted
433/// native wide-SDR window; browser/wgpu hosts must reject it.
434#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
435pub enum ImageColorMode {
436    #[default]
437    /// Straight encoded-sRGB input.
438    EncodedSrgb,
439    /// Associated linear-P3 input mapped to clipped sRGB.
440    LinearP3Preview,
441    /// Associated linear-P3 input presented by the native wide-SDR compositor.
442    LinearP3Wide,
443}
444impl ImageColorMode {
445    /// Stable renderer uniform value.
446    pub fn shader_value(self) -> f32 {
447        match self {
448            Self::EncodedSrgb => 0.,
449            Self::LinearP3Preview => 1.,
450            Self::LinearP3Wide => 2.,
451        }
452    }
453}