Skip to main content

scenix_texture/
texture.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3use core::ops::Range;
4
5use scenix_core::ValidationError;
6
7use crate::TextureFormat;
8
9/// CPU-side 2D texture bytes.
10#[derive(Clone, Debug, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct Texture2D {
13    /// Width in texels.
14    pub width: u32,
15    /// Height in texels.
16    pub height: u32,
17    /// Texture format.
18    pub format: TextureFormat,
19    /// Contiguous texture bytes.
20    pub data: Vec<u8>,
21    /// Number of mip levels. `0` means base data with auto generation later.
22    pub mip_levels: u32,
23    /// Optional debug label.
24    pub label: Option<String>,
25}
26
27/// CPU-side cube texture bytes.
28#[derive(Clone, Debug, PartialEq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct TextureCube {
31    /// Face width and height in texels.
32    pub size: u32,
33    /// Texture format.
34    pub format: TextureFormat,
35    /// Six faces in positive X, negative X, positive Y, negative Y, positive Z, negative Z order.
36    pub faces: [Vec<u8>; 6],
37    /// Number of mip levels per face.
38    pub mip_levels: u32,
39    /// Optional debug label.
40    pub label: Option<String>,
41}
42
43/// CPU-side 3D texture bytes.
44#[derive(Clone, Debug, PartialEq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct Texture3D {
47    /// Width in texels.
48    pub width: u32,
49    /// Height in texels.
50    pub height: u32,
51    /// Depth in texels.
52    pub depth: u32,
53    /// Texture format.
54    pub format: TextureFormat,
55    /// Contiguous texture bytes.
56    pub data: Vec<u8>,
57    /// Number of mip levels. `0` means base data with auto generation later.
58    pub mip_levels: u32,
59    /// Optional debug label.
60    pub label: Option<String>,
61}
62
63impl Texture2D {
64    /// Creates a base-level texture and validates byte size.
65    #[inline]
66    pub fn new(
67        width: u32,
68        height: u32,
69        format: TextureFormat,
70        data: Vec<u8>,
71    ) -> Result<Self, ValidationError> {
72        Self::with_mip_levels(width, height, format, data, 1)
73    }
74
75    /// Creates a texture with explicit mip-level count and validates byte size.
76    pub fn with_mip_levels(
77        width: u32,
78        height: u32,
79        format: TextureFormat,
80        data: Vec<u8>,
81        mip_levels: u32,
82    ) -> Result<Self, ValidationError> {
83        let texture = Self {
84            width,
85            height,
86            format,
87            data,
88            mip_levels,
89            label: None,
90        };
91        texture.validate()?;
92        Ok(texture)
93    }
94
95    /// Creates a texture from explicit mip levels and flattens the data.
96    pub fn from_mips(
97        width: u32,
98        height: u32,
99        format: TextureFormat,
100        mips: Vec<Vec<u8>>,
101    ) -> Result<Self, ValidationError> {
102        if mips.is_empty() {
103            return Err(ValidationError::OutOfRange);
104        }
105        validate_2d_mips(format, width, height, &mips)?;
106
107        let mip_levels = mips.len() as u32;
108        let total_len = mips.iter().try_fold(0_usize, |total, mip| {
109            total
110                .checked_add(mip.len())
111                .ok_or(ValidationError::OutOfRange)
112        })?;
113        let mut data = Vec::with_capacity(total_len);
114        for mip in mips {
115            data.extend_from_slice(&mip);
116        }
117
118        Ok(Self {
119            width,
120            height,
121            format,
122            data,
123            mip_levels,
124            label: None,
125        })
126    }
127
128    /// Returns this texture with a label.
129    #[inline]
130    pub fn labeled(mut self, label: impl Into<String>) -> Self {
131        self.label = Some(label.into());
132        self
133    }
134
135    /// Returns the expected byte length of the base mip level.
136    #[inline]
137    pub fn base_level_len(&self) -> Result<usize, ValidationError> {
138        self.format.expected_2d_len(self.width, self.height)
139    }
140
141    /// Returns the byte range occupied by one mip level in `data`.
142    pub fn mip_level_range(&self, level: u32) -> Result<Range<usize>, ValidationError> {
143        mip_level_range_2d(self.format, self.width, self.height, self.mip_levels, level)
144    }
145
146    /// Validates dimensions and byte length.
147    pub fn validate(&self) -> Result<(), ValidationError> {
148        let expected =
149            expected_2d_len_for_mip_count(self.format, self.width, self.height, self.mip_levels)?;
150        if self.data.len() == expected {
151            Ok(())
152        } else {
153            Err(ValidationError::OutOfRange)
154        }
155    }
156}
157
158impl TextureCube {
159    /// Creates a cube texture with six faces and validates each face.
160    pub fn new(
161        size: u32,
162        format: TextureFormat,
163        faces: [Vec<u8>; 6],
164    ) -> Result<Self, ValidationError> {
165        let texture = Self {
166            size,
167            format,
168            faces,
169            mip_levels: 1,
170            label: None,
171        };
172        texture.validate()?;
173        Ok(texture)
174    }
175
176    /// Returns this cube texture with a label.
177    #[inline]
178    pub fn labeled(mut self, label: impl Into<String>) -> Self {
179        self.label = Some(label.into());
180        self
181    }
182
183    /// Validates dimensions and every face byte length.
184    pub fn validate(&self) -> Result<(), ValidationError> {
185        let expected =
186            expected_2d_len_for_mip_count(self.format, self.size, self.size, self.mip_levels)?;
187        if self.faces.iter().all(|face| face.len() == expected) {
188            Ok(())
189        } else {
190            Err(ValidationError::OutOfRange)
191        }
192    }
193
194    /// Returns the byte range occupied by one mip level inside each face.
195    pub fn mip_level_range(&self, level: u32) -> Result<Range<usize>, ValidationError> {
196        mip_level_range_2d(self.format, self.size, self.size, self.mip_levels, level)
197    }
198}
199
200impl Texture3D {
201    /// Creates a base-level 3D texture and validates byte size.
202    #[inline]
203    pub fn new(
204        width: u32,
205        height: u32,
206        depth: u32,
207        format: TextureFormat,
208        data: Vec<u8>,
209    ) -> Result<Self, ValidationError> {
210        Self::with_mip_levels(width, height, depth, format, data, 1)
211    }
212
213    /// Creates a 3D texture with explicit mip-level count and validates byte size.
214    pub fn with_mip_levels(
215        width: u32,
216        height: u32,
217        depth: u32,
218        format: TextureFormat,
219        data: Vec<u8>,
220        mip_levels: u32,
221    ) -> Result<Self, ValidationError> {
222        let texture = Self {
223            width,
224            height,
225            depth,
226            format,
227            data,
228            mip_levels,
229            label: None,
230        };
231        texture.validate()?;
232        Ok(texture)
233    }
234
235    /// Returns this texture with a label.
236    #[inline]
237    pub fn labeled(mut self, label: impl Into<String>) -> Self {
238        self.label = Some(label.into());
239        self
240    }
241
242    /// Validates dimensions and byte length.
243    pub fn validate(&self) -> Result<(), ValidationError> {
244        let levels = self.mip_levels.max(1);
245        if levels > max_mip_levels_3d(self.width, self.height, self.depth)? {
246            return Err(ValidationError::OutOfRange);
247        }
248        let mut expected = 0_usize;
249        for level in 0..levels {
250            let (width, height) = TextureFormat::mip_dimensions(self.width, self.height, level);
251            let depth = mip_dimension(self.depth, level);
252            expected = expected
253                .checked_add(self.format.expected_3d_len(width, height, depth)?)
254                .ok_or(ValidationError::OutOfRange)?;
255        }
256        if self.data.len() == expected {
257            Ok(())
258        } else {
259            Err(ValidationError::OutOfRange)
260        }
261    }
262
263    /// Returns the byte range occupied by one mip level in `data`.
264    pub fn mip_level_range(&self, level: u32) -> Result<Range<usize>, ValidationError> {
265        let levels = self.mip_levels.max(1);
266        if level >= levels || levels > max_mip_levels_3d(self.width, self.height, self.depth)? {
267            return Err(ValidationError::OutOfRange);
268        }
269        let mut offset = 0_usize;
270        for current in 0..level {
271            let (width, height) = TextureFormat::mip_dimensions(self.width, self.height, current);
272            let depth = mip_dimension(self.depth, current);
273            offset = offset
274                .checked_add(self.format.expected_3d_len(width, height, depth)?)
275                .ok_or(ValidationError::OutOfRange)?;
276        }
277        let (width, height) = TextureFormat::mip_dimensions(self.width, self.height, level);
278        let depth = mip_dimension(self.depth, level);
279        let len = self.format.expected_3d_len(width, height, depth)?;
280        Ok(offset..offset + len)
281    }
282}
283
284fn mip_level_range_2d(
285    format: TextureFormat,
286    width: u32,
287    height: u32,
288    mip_levels: u32,
289    level: u32,
290) -> Result<Range<usize>, ValidationError> {
291    let levels = mip_levels.max(1);
292    if level >= levels || levels > max_mip_levels_2d(width, height)? {
293        return Err(ValidationError::OutOfRange);
294    }
295    let mut offset = 0_usize;
296    for current in 0..level {
297        let (w, h) = TextureFormat::mip_dimensions(width, height, current);
298        offset = offset
299            .checked_add(format.expected_2d_len(w, h)?)
300            .ok_or(ValidationError::OutOfRange)?;
301    }
302    let (w, h) = TextureFormat::mip_dimensions(width, height, level);
303    let len = format.expected_2d_len(w, h)?;
304    Ok(offset..offset + len)
305}
306
307fn validate_2d_mips(
308    format: TextureFormat,
309    width: u32,
310    height: u32,
311    mips: &[Vec<u8>],
312) -> Result<(), ValidationError> {
313    if mips.len() > max_mip_levels_2d(width, height)? as usize {
314        return Err(ValidationError::OutOfRange);
315    }
316    for (level, mip) in mips.iter().enumerate() {
317        let (w, h) = TextureFormat::mip_dimensions(width, height, level as u32);
318        if mip.len() != format.expected_2d_len(w, h)? {
319            return Err(ValidationError::OutOfRange);
320        }
321    }
322    Ok(())
323}
324
325fn expected_2d_len_for_mip_count(
326    format: TextureFormat,
327    width: u32,
328    height: u32,
329    mip_levels: u32,
330) -> Result<usize, ValidationError> {
331    let levels = mip_levels.max(1);
332    if levels > max_mip_levels_2d(width, height)? {
333        return Err(ValidationError::OutOfRange);
334    }
335    let mut expected = 0_usize;
336    for level in 0..levels {
337        let (w, h) = TextureFormat::mip_dimensions(width, height, level);
338        expected = expected
339            .checked_add(format.expected_2d_len(w, h)?)
340            .ok_or(ValidationError::OutOfRange)?;
341    }
342    Ok(expected)
343}
344
345fn mip_dimension(value: u32, level: u32) -> u32 {
346    if level >= u32::BITS {
347        1
348    } else {
349        (value >> level).max(1)
350    }
351}
352
353fn max_mip_levels_2d(width: u32, height: u32) -> Result<u32, ValidationError> {
354    let max_dimension = width.max(height);
355    if max_dimension == 0 {
356        Err(ValidationError::OutOfRange)
357    } else {
358        Ok(u32::BITS - max_dimension.leading_zeros())
359    }
360}
361
362fn max_mip_levels_3d(width: u32, height: u32, depth: u32) -> Result<u32, ValidationError> {
363    let max_dimension = width.max(height).max(depth);
364    if max_dimension == 0 {
365        Err(ValidationError::OutOfRange)
366    } else {
367        Ok(u32::BITS - max_dimension.leading_zeros())
368    }
369}