citro3d/
texture.rs

1use std::mem::MaybeUninit;
2
3use citro3d_sys::C3D_TexCalcMaxLevel;
4pub use enums::*;
5
6mod enums;
7
8/// The maximum number of textures that can be bound at once
9pub const TEXTURE_COUNT: usize = 4;
10/// Minimum width and height of a texture
11pub const MIN_TEX_SIZE: u16 = 8;
12/// Maximum width and height of a texture
13pub const MAX_TEX_SIZE: u16 = 1024;
14
15/// Texture width and height must be between 8 and 1024 (inclusive)
16#[derive(Debug, Clone)]
17pub struct TextureParameters {
18    pub width: u16,
19    pub height: u16,
20    pub max_level: u8,
21    pub format: ColorFormat,
22    pub mode: Mode,
23    pub on_vram: bool,
24}
25
26/// Parameters used to initialize a `Texture`.
27/// Pass it into `Texture::new` to create a new texture.
28impl TextureParameters {
29    /// `TextureParameters` to initialize a new 2D `Texture` with no mipmapping.
30    pub const fn new_2d(width: u16, height: u16, format: ColorFormat) -> TextureParameters {
31        TextureParameters {
32            width,
33            height,
34            max_level: 0,
35            format,
36            mode: Mode::Tex2D,
37            on_vram: false,
38        }
39    }
40
41    /// `TextureParameters` to initialize a new 2D `Texture` with mipmapping.
42    pub fn new_2d_with_mipmap(width: u16, height: u16, format: ColorFormat) -> TextureParameters {
43        TextureParameters {
44            width,
45            height,
46            max_level: unsafe { C3D_TexCalcMaxLevel(width as u32, height as u32) as u8 },
47            format,
48            mode: Mode::Tex2D,
49            on_vram: false,
50        }
51    }
52
53    /// `TextureParameters` to initialize a new 2D `Texture` with no mipmapping that is stored in VRAM.
54    pub const fn new_2d_in_vram(width: u16, height: u16, format: ColorFormat) -> TextureParameters {
55        TextureParameters {
56            width,
57            height,
58            max_level: 0,
59            format,
60            mode: Mode::Tex2D,
61            on_vram: true,
62        }
63    }
64
65    /// `TextureParameters` to initialize a new 2D `Texture` for a shadow map.
66    pub const fn new_shadow(width: u16, height: u16) -> TextureParameters {
67        TextureParameters {
68            width,
69            height,
70            max_level: 0,
71            format: ColorFormat::Rgba8,
72            mode: Mode::Shadow2D,
73            on_vram: true,
74        }
75    }
76}
77
78impl From<TextureParameters> for citro3d_sys::C3D_TexInitParams {
79    fn from(value: TextureParameters) -> Self {
80        citro3d_sys::C3D_TexInitParams {
81            width: value.width,
82            height: value.height,
83            _bitfield_align_1: [],
84            _bitfield_1: citro3d_sys::C3D_TexInitParams::new_bitfield_1(
85                value.max_level,
86                value.format as u8,
87                value.mode as u8,
88                value.on_vram,
89            ),
90            __bindgen_padding_0: 0,
91        }
92    }
93}
94
95/// The Tex3DS representation of the struct, as the one generated from bindgen is not populated with the fields.
96#[allow(non_camel_case_types, non_snake_case)]
97#[repr(C)]
98struct Tex3DS_Texture_s {
99    numSubTextures: u16,
100    width: u16,
101    height: u16,
102    format: u8,
103    mipmapLevels: u8,
104    subTextures: [citro3d_sys::Tex3DS_SubTexture; 1],
105}
106
107pub struct Tex3DSTexture {
108    texture: Texture,
109    tex3ds: Box<Tex3DS_Texture_s>,
110}
111
112impl Tex3DSTexture {
113    pub fn texture(&self) -> &Texture {
114        &self.texture
115    }
116
117    pub fn into_texture(self) -> Texture {
118        self.texture
119    }
120
121    pub fn sub_textures(&self) -> &[citro3d_sys::Tex3DS_SubTexture] {
122        // SAFETY: Everything in self.tex3ds was allocated through some call to `Tex3DSi_ImportCommon`
123        // which allocated the struct as a variable size such that `self.tex3ds.subTextures`
124        // is self.tex3ds.numSubTextures long.
125        unsafe {
126            std::slice::from_raw_parts(
127                self.tex3ds.subTextures.as_ptr(),
128                self.tex3ds.numSubTextures as usize,
129            )
130        }
131    }
132
133    /// Import a texture from bytes generated by the `tex3ds` tool.
134    #[doc(alias = "Tex3DS_TextureImport")]
135    pub fn new(data: &[u8], use_vram: bool) -> Option<Tex3DSTexture> {
136        let mut texture: Texture = unsafe { std::mem::zeroed() };
137        let mut cube: Box<citro3d_sys::C3D_TexCube> = unsafe { Box::new(std::mem::zeroed()) };
138
139        // SAFETY: Transmuting the underlying pointer requires that type Tex3DS_Texture_s is correct
140        let t3d: *mut Tex3DS_Texture_s = unsafe {
141            citro3d_sys::Tex3DS_TextureImport(
142                data.as_ptr() as _,
143                data.len(),
144                texture.as_raw(),
145                cube.as_mut(),
146                use_vram,
147            ) as _
148        };
149
150        if t3d.is_null() {
151            return None;
152        }
153
154        let tex3ds = unsafe { Box::from_raw(t3d) };
155
156        texture.format = tex3ds.format.try_into().ok()?;
157        // TODO - check if mode is a cubemap and set it conditionally, then
158        // tex.cube being some/none could be used to check if it's a cubemap.
159        texture.cube = Some(cube);
160
161        Some(Tex3DSTexture { texture, tex3ds })
162    }
163}
164
165pub struct Texture {
166    pub(crate) tex: citro3d_sys::C3D_Tex,
167    pub(crate) format: ColorFormat,
168    pub(crate) in_vram: bool,
169    /// cube being Some does not necessarily mean it is a cube map, as cube is initialized
170    /// when importing via Tex3DS even if it isn't used.
171    pub(crate) cube: Option<Box<citro3d_sys::C3D_TexCube>>,
172}
173
174impl Texture {
175    /// Allocate a new texture with the given parameters.
176    /// Texture allocation can fail if the texture size specified by the parameters is too small or
177    /// large, or memory allocation fails.
178    #[doc(alias = "C3D_TexInit")]
179    pub fn new(params: TextureParameters) -> crate::error::Result<Self> {
180        if !check_texture_size(params.width) || !check_texture_size(params.height) {
181            return Err(crate::Error::InvalidSize);
182        }
183
184        let mut cube: Option<Box<citro3d_sys::C3D_TexCube>> = None;
185        if params.mode == Mode::CubeMap || params.mode == Mode::ShadowCube {
186            cube = unsafe { Some(Box::new(std::mem::zeroed())) };
187        }
188
189        let format = params.format;
190        let in_vram = params.on_vram;
191        let params: citro3d_sys::C3D_TexInitParams = params.into();
192
193        // SAFETY: C3D_Tex is only initialised here after citro3d_sys::C3d_TexInitWithParams returns success,
194        // and is properly cleaned up with citro3d::C3D_TexDelete on drop
195        unsafe {
196            let mut c3d_tex: MaybeUninit<citro3d_sys::C3D_Tex> = core::mem::zeroed();
197
198            let success = citro3d_sys::C3D_TexInitWithParams(
199                c3d_tex.as_mut_ptr(),
200                cube.as_mut().map(|p| p.as_mut() as _).unwrap_or_default(),
201                params,
202            );
203
204            if !success {
205                return Err(crate::Error::FailedToInitialize);
206            }
207
208            let mut tex = Texture {
209                tex: c3d_tex.assume_init(),
210                format,
211                in_vram,
212                cube,
213            };
214
215            // Set a default filter, as it won't render properly without one
216            tex.set_filter(Filter::Linear, Filter::Nearest);
217
218            Ok(tex)
219        }
220    }
221
222    /// Upload the provided data buffer to the texture, and to the given face if it's a cube
223    /// texture. For flat textures, the face argument is not considered so [`Face::default()`]
224    /// can be used.
225    #[doc(alias = "C3D_TexUpload")]
226    pub fn load_image(&mut self, data: &[u8], face: Face) -> crate::Result<()> {
227        self.load_image_at_mipmap_level(data, face, 0)
228    }
229
230    /// Upload the provided data buffer to the texture's specific mipmap level, and to the given
231    /// face if it's a cube texture. For flat textures, the face argument is not considered so
232    /// [`Face::default()`] can be used.
233    #[doc(alias = "C3D_TexLoadImage")]
234    pub fn load_image_at_mipmap_level(
235        &mut self,
236        data: &[u8],
237        face: Face,
238        mipmap_level: u8,
239    ) -> crate::Result<()> {
240        let size = unsafe {
241            if mipmap_level > 0 {
242                citro3d_sys::C3D_TexCalcLevelSize(
243                    self.format.bits_per_pixel() as u32,
244                    mipmap_level as i32,
245                )
246            } else {
247                citro3d_sys::C3D_TexCalcTotalSize(
248                    self.format.bits_per_pixel() as u32,
249                    self.max_level() as i32,
250                )
251            }
252        };
253
254        // Verify data buffer is long enough
255        if data.len() < size as usize {
256            return Err(crate::Error::InvalidSize);
257        }
258
259        // SAFETY: The `data` buffer has been verified to be long enough
260        unsafe {
261            citro3d_sys::C3D_TexLoadImage(
262                self.as_raw(),
263                data.as_ptr() as *const _,
264                face as u8,
265                mipmap_level as i32,
266            );
267        }
268
269        Ok(())
270    }
271
272    /// Binds this texture to the given texture unit of the GPU.
273    ///
274    /// SAFETY: This texture must stay alive as long as it's bound to the GPU (and a texenv is using that TexUnit?)
275    pub(crate) unsafe fn bind(&self, index: Index) {
276        unsafe { citro3d_sys::C3D_TexBind(index as _, &self.tex as *const _ as *mut _) };
277    }
278
279    /// Generate a mipmap for this texture, and this face if it's a cube texture.
280    /// For flat textures `Face::default()` or `Face::TEX2D` can be used.
281    pub fn generate_mipmap(&mut self, face: Face) {
282        unsafe {
283            citro3d_sys::C3D_TexGenerateMipmap(&mut self.tex as *mut _, face as u8);
284        }
285    }
286
287    pub fn set_filter(&mut self, mag_filter: Filter, min_filter: Filter) {
288        unsafe { citro3d_sys::C3D_TexSetFilter(self.as_raw(), mag_filter as u8, min_filter as u8) };
289    }
290
291    pub fn set_filter_mipmap(&mut self, filter: Filter) {
292        unsafe {
293            citro3d_sys::C3D_TexSetFilterMipmap(self.as_raw(), filter as u8);
294        }
295    }
296
297    pub fn set_wrap(&mut self, wrap_s: Wrap, wrap_t: Wrap) {
298        unsafe {
299            citro3d_sys::C3D_TexSetWrap(self.as_raw(), wrap_s as u8, wrap_t as u8);
300        }
301    }
302
303    pub fn set_lod_bias(&mut self, lod_bias: f32) {
304        unsafe {
305            citro3d_sys::C3D_TexSetLodBias(self.as_raw(), lod_bias);
306        }
307    }
308
309    pub fn width(&self) -> u16 {
310        unsafe { self.tex.__bindgen_anon_2.__bindgen_anon_1.width }
311    }
312
313    pub fn height(&self) -> u16 {
314        unsafe { self.tex.__bindgen_anon_2.__bindgen_anon_1.height }
315    }
316
317    pub fn param(&self) -> u32 {
318        self.tex.param
319    }
320
321    pub fn format(&self) -> ColorFormat {
322        self.format
323    }
324
325    pub fn lod_bias(&self) -> u16 {
326        unsafe { self.tex.__bindgen_anon_3.__bindgen_anon_1.lodBias }
327    }
328
329    pub fn max_level(&self) -> u8 {
330        unsafe { self.tex.__bindgen_anon_3.__bindgen_anon_1.maxLevel }
331    }
332
333    pub fn min_level(&self) -> u8 {
334        unsafe { self.tex.__bindgen_anon_3.__bindgen_anon_1.minLevel }
335    }
336
337    fn as_raw(&self) -> *mut citro3d_sys::C3D_Tex {
338        &self.tex as *const _ as *mut _
339    }
340}
341
342impl Drop for Texture {
343    fn drop(&mut self) {
344        // SAFETY: self.tex was initialised with C3D_TexInitWithParams
345        unsafe { citro3d_sys::C3D_TexDelete(self.as_raw()) }
346    }
347}
348
349fn check_texture_size(size: u16) -> bool {
350    if !(MIN_TEX_SIZE..=MAX_TEX_SIZE).contains(&size) {
351        return false;
352    }
353
354    if (size & (size - 1)) > 0 {
355        return false;
356    }
357
358    true
359}