Skip to main content

scenix_texture/
format.rs

1use scenix_core::ValidationError;
2
3/// CPU-side texture format metadata.
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub enum TextureFormat {
7    /// Four 8-bit normalized linear RGBA channels.
8    Rgba8Unorm,
9    /// Four 8-bit normalized sRGB RGBA channels.
10    Rgba8UnormSrgb,
11    /// Four 16-bit floating-point RGBA channels.
12    Rgba16Float,
13    /// 32-bit floating-point depth.
14    Depth32Float,
15    /// BC7 compressed RGBA, 4x4 blocks, 16 bytes per block.
16    Bc7RgbaUnorm,
17    /// ASTC 4x4 compressed RGBA, 16 bytes per block.
18    Astc4x4RgbaUnorm,
19    /// ETC2 RGBA8 compressed data, 4x4 blocks, 16 bytes per block.
20    Etc2Rgba8Unorm,
21}
22
23impl TextureFormat {
24    /// Returns whether this is a block-compressed format.
25    #[inline]
26    pub const fn is_compressed(self) -> bool {
27        matches!(
28            self,
29            Self::Bc7RgbaUnorm | Self::Astc4x4RgbaUnorm | Self::Etc2Rgba8Unorm
30        )
31    }
32
33    /// Returns bytes per texel for uncompressed formats.
34    #[inline]
35    pub const fn bytes_per_pixel(self) -> Option<usize> {
36        match self {
37            Self::Rgba8Unorm | Self::Rgba8UnormSrgb | Self::Depth32Float => Some(4),
38            Self::Rgba16Float => Some(8),
39            Self::Bc7RgbaUnorm | Self::Astc4x4RgbaUnorm | Self::Etc2Rgba8Unorm => None,
40        }
41    }
42
43    /// Returns `(width, height)` for compressed blocks.
44    #[inline]
45    pub const fn block_dimensions(self) -> Option<(u32, u32)> {
46        if self.is_compressed() {
47            Some((4, 4))
48        } else {
49            None
50        }
51    }
52
53    /// Returns bytes per compressed block.
54    #[inline]
55    pub const fn bytes_per_block(self) -> Option<usize> {
56        if self.is_compressed() { Some(16) } else { None }
57    }
58
59    /// Returns the dimensions of a mip level.
60    #[inline]
61    pub const fn mip_dimensions(width: u32, height: u32, level: u32) -> (u32, u32) {
62        let w = shr_clamped(width, level);
63        let h = shr_clamped(height, level);
64        (if w == 0 { 1 } else { w }, if h == 0 { 1 } else { h })
65    }
66
67    /// Returns the expected byte length for a 2D texture level.
68    #[inline]
69    pub fn expected_2d_len(self, width: u32, height: u32) -> Result<usize, ValidationError> {
70        self.expected_3d_len(width, height, 1)
71    }
72
73    /// Returns the expected byte length for a 3D texture level.
74    pub fn expected_3d_len(
75        self,
76        width: u32,
77        height: u32,
78        depth: u32,
79    ) -> Result<usize, ValidationError> {
80        if width == 0 || height == 0 || depth == 0 {
81            return Err(ValidationError::OutOfRange);
82        }
83
84        if let Some(bytes_per_pixel) = self.bytes_per_pixel() {
85            checked_area(width, height, depth)?
86                .checked_mul(bytes_per_pixel)
87                .ok_or(ValidationError::OutOfRange)
88        } else {
89            let (block_w, block_h) = self.block_dimensions().unwrap_or((4, 4));
90            let blocks_x = width.div_ceil(block_w);
91            let blocks_y = height.div_ceil(block_h);
92            checked_area(blocks_x, blocks_y, depth)?
93                .checked_mul(self.bytes_per_block().unwrap_or(16))
94                .ok_or(ValidationError::OutOfRange)
95        }
96    }
97}
98
99#[inline]
100const fn shr_clamped(value: u32, shift: u32) -> u32 {
101    if shift >= u32::BITS {
102        0
103    } else {
104        value >> shift
105    }
106}
107
108#[inline]
109fn checked_area(width: u32, height: u32, depth: u32) -> Result<usize, ValidationError> {
110    (width as usize)
111        .checked_mul(height as usize)
112        .and_then(|value| value.checked_mul(depth as usize))
113        .ok_or(ValidationError::OutOfRange)
114}