xc3_lib/
mibl.rs

1//! Textures in `.witx` or `.witex` files or embedded in `.wismt` files and other formats.
2//!
3//! # Overview
4//! [Mibl] image textures consists of an image data section containing all array layers and mipmaps
5//! and a footer describing the surface dimensions and format.
6//! The image data is ordered by layer and mipmap like
7//! "Layer0 Mip 0 Layer 0 Mip 1 ... Layer L-1 Mip M-1" for L layers and M mipmaps.
8//! This is the same ordering expected by DDS and modern graphics APIs.
9//!
10//! The image data uses a "swizzled" memory layout optimized for the Tegra X1
11//! and must be decoded to a standard row-major layout using [Mibl::deswizzled_image_data]
12//! for use on other hardware.
13//!
14//! All of the image formats used in game are supported by DDS, enabling cheap conversions using [Mibl::to_dds]
15//! and [Mibl::from_dds]. For converting to and from uncompressed formats like PNG or TIFF,
16//! use the encoding and decoding provided by [image_dds].
17//!
18//! # File Paths
19//! Xenoblade 3 `.wismt` [Mibl] are in [Xbc1](crate::xbc1::Xbc1) archives.
20//!
21//! | Game | File Patterns |
22//! | --- | --- |
23//! | Xenoblade 1 DE | `monolib/shader/*.{witex,witx}` |
24//! | Xenoblade 2 | `monolib/shader/*.{witex,witx}` |
25//! | Xenoblade 3 | `chr/tex/nx/{h,m}/*.wismt`, `monolib/shader/*.{witex,witx}` |
26use std::{borrow::Cow, io::SeekFrom};
27
28use binrw::{binrw, BinRead, BinWrite};
29use image_dds::{ddsfile::Dds, Surface};
30use tegra_swizzle::surface::BlockDim;
31use xc3_write::Xc3Write;
32
33pub use tegra_swizzle::SwizzleError;
34
35use crate::{error::CreateMiblError, xc3_write_binwrite_impl};
36
37/// A swizzled image texture surface.
38#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
39#[derive(Debug, PartialEq, Eq, Clone)]
40pub struct Mibl {
41    /// The combined swizzled image surface data.
42    /// Ordered as `Layer 0 Mip 0, Layer 0 Mip 1, ... Layer L-1 Mip M-1`
43    /// for L layers and M mipmaps similar to DDS files.
44    pub image_data: Vec<u8>,
45    /// A description of the surface in [image_data](#structfield.image_data).
46    pub footer: MiblFooter,
47}
48
49const MIBL_FOOTER_SIZE: u64 = 40;
50
51/// A description of the image surface.
52#[binrw]
53#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
54#[derive(Debug, PartialEq, Eq, Clone)]
55pub struct MiblFooter {
56    /// The size of [image_data](struct.Mibl.html#structfield.image_data)
57    /// aligned to the page size of 4096 (0x1000) bytes.
58    /// This may include the bytes for the footer for some files
59    /// and will not always equal the file size.
60    pub image_size: u32,
61    pub unk: u32, // TODO: is this actually 0x1000 for swizzled like with nutexb?
62    /// The width of the base mip level in pixels.
63    pub width: u32,
64    /// The height of the base mip level in pixels.
65    pub height: u32,
66    /// The depth of the base mip level in pixels.
67    pub depth: u32,
68    pub view_dimension: ViewDimension,
69    pub image_format: ImageFormat,
70    /// The number of mip levels or 1 if there are no mipmaps.
71    pub mipmap_count: u32,
72    pub version: u32, // 10001?
73
74    #[brw(magic(b"LBIM"))]
75    #[br(temp)]
76    #[bw(ignore)]
77    _magic: (),
78}
79
80#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
81#[derive(BinRead, BinWrite, Debug, Clone, Copy, PartialEq, Eq)]
82#[brw(repr(u32))]
83pub enum ViewDimension {
84    D2 = 1,
85    D3 = 2,
86    Cube = 8,
87}
88
89/// nvn image format types used by Xenoblade 1 DE, Xenoblade 2, and Xenoblade 3.
90#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
91#[derive(BinRead, BinWrite, Debug, Clone, Copy, PartialEq, Eq)]
92#[brw(repr(u32))]
93pub enum ImageFormat {
94    R8Unorm = 1,
95    R8G8B8A8Unorm = 37,
96    R16G16B16A16Float = 41,
97    R4G4B4A4Unorm = 57,
98    BC1Unorm = 66,
99    BC2Unorm = 67,
100    BC3Unorm = 68,
101    BC4Unorm = 73,
102    BC5Unorm = 75,
103    BC7Unorm = 77,
104    BC6UFloat = 80,
105    B8G8R8A8Unorm = 109,
106}
107
108impl ImageFormat {
109    pub fn block_dim(&self) -> BlockDim {
110        match self {
111            ImageFormat::R8Unorm => BlockDim::uncompressed(),
112            ImageFormat::R8G8B8A8Unorm => BlockDim::uncompressed(),
113            ImageFormat::R16G16B16A16Float => BlockDim::uncompressed(),
114            ImageFormat::R4G4B4A4Unorm => BlockDim::uncompressed(),
115            ImageFormat::BC1Unorm => BlockDim::block_4x4(),
116            ImageFormat::BC2Unorm => BlockDim::block_4x4(),
117            ImageFormat::BC3Unorm => BlockDim::block_4x4(),
118            ImageFormat::BC4Unorm => BlockDim::block_4x4(),
119            ImageFormat::BC5Unorm => BlockDim::block_4x4(),
120            ImageFormat::BC7Unorm => BlockDim::block_4x4(),
121            ImageFormat::BC6UFloat => BlockDim::block_4x4(),
122            ImageFormat::B8G8R8A8Unorm => BlockDim::uncompressed(),
123        }
124    }
125
126    pub fn bytes_per_pixel(&self) -> u32 {
127        match self {
128            ImageFormat::R8Unorm => 1,
129            ImageFormat::R8G8B8A8Unorm => 4,
130            ImageFormat::R16G16B16A16Float => 8,
131            ImageFormat::R4G4B4A4Unorm => 2,
132            ImageFormat::BC1Unorm => 8,
133            ImageFormat::BC2Unorm => 16,
134            ImageFormat::BC3Unorm => 16,
135            ImageFormat::BC4Unorm => 8,
136            ImageFormat::BC5Unorm => 16,
137            ImageFormat::BC7Unorm => 16,
138            ImageFormat::BC6UFloat => 16,
139            ImageFormat::B8G8R8A8Unorm => 4,
140        }
141    }
142}
143
144impl BinRead for Mibl {
145    type Args<'a> = ();
146
147    fn read_options<R: std::io::Read + std::io::Seek>(
148        reader: &mut R,
149        endian: binrw::Endian,
150        args: Self::Args<'_>,
151    ) -> binrw::BinResult<Self> {
152        let saved_pos = reader.stream_position()?;
153
154        // Assume the MIBL is the last item in the reader.
155        reader.seek(SeekFrom::End(-(MIBL_FOOTER_SIZE as i64)))?;
156        let footer = MiblFooter::read_options(reader, endian, args)?;
157
158        reader.seek(SeekFrom::Start(saved_pos))?;
159
160        // Avoid potentially storing the footer in the image data.
161        // Alignment will be applied when writing.
162        let unaligned_size = footer.swizzled_surface_size();
163        let mut image_data = vec![0u8; unaligned_size];
164        reader.read_exact(&mut image_data)?;
165
166        Ok(Mibl { image_data, footer })
167    }
168}
169
170impl BinWrite for Mibl {
171    type Args<'a> = ();
172
173    fn write_options<W: std::io::Write + std::io::Seek>(
174        &self,
175        writer: &mut W,
176        endian: binrw::Endian,
177        _args: Self::Args<'_>,
178    ) -> binrw::BinResult<()> {
179        // Assume the image data isn't aligned to the page size.
180        let unaligned_size = self.image_data.len() as u64;
181        let aligned_size = unaligned_size.next_multiple_of(4096);
182
183        self.image_data.write_options(writer, endian, ())?;
184
185        // Fit the footer within the padding if possible.
186        // Otherwise, create another 4096 bytes for the footer.
187        let padding_size = aligned_size - unaligned_size;
188        writer.write_all(&vec![0u8; padding_size as usize])?;
189
190        if padding_size < MIBL_FOOTER_SIZE {
191            writer.write_all(&[0u8; 4096])?;
192        }
193
194        writer.seek(SeekFrom::End(-(MIBL_FOOTER_SIZE as i64)))?;
195        self.footer.write_options(writer, endian, ())?;
196
197        Ok(())
198    }
199}
200
201xc3_write_binwrite_impl!(Mibl);
202
203impl Mibl {
204    /// Deswizzles all layers and mipmaps to a standard row-major memory layout.
205    pub fn deswizzled_image_data(&self) -> Result<Vec<u8>, SwizzleError> {
206        if self
207            .footer
208            .width
209            .checked_mul(self.footer.height)
210            .and_then(|i| i.checked_mul(self.footer.depth))
211            .is_none()
212        {
213            return Err(SwizzleError::NotEnoughData {
214                expected_size: usize::MAX,
215                actual_size: self.image_data.len(),
216            });
217        }
218        tegra_swizzle::surface::deswizzle_surface(
219            self.footer.width,
220            self.footer.height,
221            self.footer.depth,
222            &self.image_data,
223            self.footer.image_format.block_dim(),
224            None,
225            self.footer.image_format.bytes_per_pixel(),
226            self.footer.mipmap_count,
227            if self.footer.view_dimension == ViewDimension::Cube {
228                6
229            } else {
230                1
231            },
232        )
233    }
234
235    /// Similar to [Self::to_surface] but adds the swizzled `base_mip_level` with the existing mipmaps.
236    /// The base mip should have twice current width and height.
237    pub fn to_surface_with_base_mip(
238        &self,
239        base_mip_level: &[u8],
240    ) -> Result<Surface<Vec<u8>>, SwizzleError> {
241        let mid = self.to_surface()?;
242
243        let width = mid.width.saturating_mul(2);
244        let height = mid.height.saturating_mul(2);
245
246        // Combine deswizzled data so we don't have to worry about alignment.
247        // TODO: How does this work for 3D or array layers?
248        // TODO: validate dimensions.
249        let mut data = tegra_swizzle::surface::deswizzle_surface(
250            width,
251            height,
252            self.footer.depth,
253            base_mip_level,
254            self.footer.image_format.block_dim(),
255            None,
256            self.footer.image_format.bytes_per_pixel(),
257            1,
258            if self.footer.view_dimension == ViewDimension::Cube {
259                6
260            } else {
261                1
262            },
263        )?;
264
265        if self.footer.image_format == ImageFormat::R4G4B4A4Unorm {
266            // image_dds only supports bgra4.
267            swap_red_blue_unorm4(&mut data);
268        }
269
270        data.extend_from_slice(&mid.data);
271
272        // TODO: Error if invalid dimensions?
273        // TODO: Mipmaps shouldn't be more than 32 for 32-bit dimensions.
274        Ok(Surface {
275            width,
276            height,
277            depth: mid.depth,
278            layers: mid.layers,
279            mipmaps: mid.mipmaps.saturating_add(1),
280            image_format: mid.image_format,
281            data,
282        })
283    }
284
285    /// Split the texture into a texture with half resolution and a separate base mip level.
286    /// The inverse operation of [Self::to_surface_with_base_mip].
287    pub fn split_base_mip(&self) -> (Self, Vec<u8>) {
288        // TODO: Does this correctly handle alignment?
289        let base_mip_size = self.footer.swizzled_base_mip_size();
290        let (base_mip, image_data) = self.image_data.split_at(base_mip_size);
291
292        (
293            Self {
294                image_data: image_data.to_vec(),
295                footer: MiblFooter {
296                    image_size: image_data.len().next_multiple_of(4096) as u32,
297                    width: self.footer.width / 2,
298                    height: self.footer.height / 2,
299                    mipmap_count: self.footer.mipmap_count - 1,
300                    ..self.footer
301                },
302            },
303            base_mip.to_vec(),
304        )
305    }
306
307    /// Deswizzles all layers and mipmaps to a compatible surface for easier conversions.
308    pub fn to_surface(&self) -> Result<Surface<Vec<u8>>, SwizzleError> {
309        let mut data = self.deswizzled_image_data()?;
310        if self.footer.image_format == ImageFormat::R4G4B4A4Unorm {
311            // image_dds only supports bgra4.
312            swap_red_blue_unorm4(&mut data);
313        }
314        Ok(Surface {
315            width: self.footer.width,
316            height: self.footer.height,
317            depth: self.footer.depth,
318            layers: if self.footer.view_dimension == ViewDimension::Cube {
319                6
320            } else {
321                1
322            },
323            mipmaps: self.footer.mipmap_count,
324            image_format: self.footer.image_format.into(),
325            data,
326        })
327    }
328
329    /// Swizzles all layers and mipmaps in `surface` to an equivalent [Mibl].
330    ///
331    /// Returns an error if the conversion fails or the image format is not supported.
332    pub fn from_surface<T: AsRef<[u8]>>(surface: Surface<T>) -> Result<Self, CreateMiblError> {
333        let Surface {
334            width,
335            height,
336            depth,
337            layers,
338            mipmaps,
339            image_format,
340            data,
341        } = surface;
342        let image_format = ImageFormat::try_from(image_format)?;
343
344        let data = if image_format == ImageFormat::R4G4B4A4Unorm {
345            // image_dds only supports bgra4.
346            let mut data = data.as_ref().to_vec();
347            swap_red_blue_unorm4(&mut data);
348            Cow::Owned(data)
349        } else {
350            Cow::Borrowed(data.as_ref())
351        };
352
353        let image_data = tegra_swizzle::surface::swizzle_surface(
354            width,
355            height,
356            depth,
357            &data,
358            image_format.block_dim(),
359            None,
360            image_format.bytes_per_pixel(),
361            mipmaps,
362            layers,
363        )?;
364
365        let image_size = image_data.len().next_multiple_of(4096) as u32;
366
367        Ok(Self {
368            image_data,
369            footer: MiblFooter {
370                image_size,
371                unk: 4096,
372                width,
373                height,
374                depth,
375                view_dimension: if depth > 1 {
376                    ViewDimension::D3
377                } else if layers == 6 {
378                    ViewDimension::Cube
379                } else {
380                    ViewDimension::D2
381                },
382                image_format,
383                mipmap_count: mipmaps,
384                version: 10001,
385            },
386        })
387    }
388
389    /// Deswizzles all layers and mipmaps to a Direct Draw Surface (DDS).
390    pub fn to_dds(&self) -> Result<Dds, crate::dds::CreateDdsError> {
391        self.to_surface()?.to_dds().map_err(Into::into)
392    }
393
394    /// Swizzles all layers and mipmaps in `dds` to an equivalent [Mibl].
395    ///
396    /// Returns an error if the conversion fails or the image format is not supported.
397    pub fn from_dds(dds: &Dds) -> Result<Self, CreateMiblError> {
398        let surface = image_dds::Surface::from_dds(dds)?;
399        Self::from_surface(surface)
400    }
401}
402
403impl MiblFooter {
404    fn swizzled_surface_size(&self) -> usize {
405        tegra_swizzle::surface::swizzled_surface_size(
406            self.width,
407            self.height,
408            self.depth,
409            self.image_format.block_dim(),
410            None,
411            self.image_format.bytes_per_pixel(),
412            self.mipmap_count,
413            if self.view_dimension == ViewDimension::Cube {
414                6
415            } else {
416                1
417            },
418        )
419    }
420
421    pub(crate) fn deswizzled_surface_size(&self) -> usize {
422        tegra_swizzle::surface::deswizzled_surface_size(
423            self.width,
424            self.height,
425            self.depth,
426            self.image_format.block_dim(),
427            self.image_format.bytes_per_pixel(),
428            self.mipmap_count,
429            if self.view_dimension == ViewDimension::Cube {
430                6
431            } else {
432                1
433            },
434        )
435    }
436
437    fn swizzled_base_mip_size(&self) -> usize {
438        tegra_swizzle::surface::swizzled_surface_size(
439            self.width,
440            self.height,
441            self.depth,
442            self.image_format.block_dim(),
443            None,
444            self.image_format.bytes_per_pixel(),
445            1,
446            if self.view_dimension == ViewDimension::Cube {
447                6
448            } else {
449                1
450            },
451        )
452    }
453}
454
455impl From<ImageFormat> for image_dds::ImageFormat {
456    fn from(value: ImageFormat) -> Self {
457        match value {
458            ImageFormat::R8Unorm => image_dds::ImageFormat::R8Unorm,
459            ImageFormat::R8G8B8A8Unorm => image_dds::ImageFormat::Rgba8Unorm,
460            ImageFormat::R16G16B16A16Float => image_dds::ImageFormat::Rgba16Float,
461            ImageFormat::R4G4B4A4Unorm => image_dds::ImageFormat::Bgra4Unorm, // requires channel swap
462            ImageFormat::BC1Unorm => image_dds::ImageFormat::BC1RgbaUnorm,
463            ImageFormat::BC2Unorm => image_dds::ImageFormat::BC2RgbaUnorm,
464            ImageFormat::BC3Unorm => image_dds::ImageFormat::BC3RgbaUnorm,
465            ImageFormat::BC4Unorm => image_dds::ImageFormat::BC4RUnorm,
466            ImageFormat::BC5Unorm => image_dds::ImageFormat::BC5RgUnorm,
467            ImageFormat::BC7Unorm => image_dds::ImageFormat::BC7RgbaUnorm,
468            ImageFormat::BC6UFloat => image_dds::ImageFormat::BC6hRgbUfloat,
469            ImageFormat::B8G8R8A8Unorm => image_dds::ImageFormat::Bgra8Unorm,
470        }
471    }
472}
473
474impl TryFrom<image_dds::ImageFormat> for ImageFormat {
475    type Error = CreateMiblError;
476
477    fn try_from(value: image_dds::ImageFormat) -> Result<Self, Self::Error> {
478        match value {
479            image_dds::ImageFormat::R8Unorm => Ok(ImageFormat::R8Unorm),
480            image_dds::ImageFormat::Rgba8Unorm => Ok(ImageFormat::R8G8B8A8Unorm),
481            image_dds::ImageFormat::Rgba16Float => Ok(ImageFormat::R16G16B16A16Float),
482            image_dds::ImageFormat::Bgra8Unorm => Ok(ImageFormat::B8G8R8A8Unorm),
483            image_dds::ImageFormat::BC1RgbaUnorm => Ok(ImageFormat::BC1Unorm),
484            image_dds::ImageFormat::BC2RgbaUnorm => Ok(ImageFormat::BC2Unorm),
485            image_dds::ImageFormat::BC3RgbaUnorm => Ok(ImageFormat::BC3Unorm),
486            image_dds::ImageFormat::BC4RUnorm => Ok(ImageFormat::BC4Unorm),
487            image_dds::ImageFormat::BC5RgUnorm => Ok(ImageFormat::BC5Unorm),
488            image_dds::ImageFormat::BC6hRgbUfloat => Ok(ImageFormat::BC6UFloat),
489            image_dds::ImageFormat::BC7RgbaUnorm => Ok(ImageFormat::BC7Unorm),
490            image_dds::ImageFormat::Bgra4Unorm => Ok(ImageFormat::R4G4B4A4Unorm),
491            _ => Err(CreateMiblError::UnsupportedImageFormat(value)),
492        }
493    }
494}
495
496fn swap_red_blue_unorm4(data: &mut [u8]) {
497    data.chunks_exact_mut(2).for_each(|c| {
498        // Swap 4-bit red and blue channels.
499        let r = c[1] & 0xF;
500        let b = c[0] & 0xF;
501        c[0] = c[0] & 0xF0 | r;
502        c[1] = c[1] & 0xF0 | b;
503    });
504}