nutexb/
lib.rs

1//! # nutexb
2//! Nutexb is an image texture format used in Super Smash Bros Ultimate and some other games.
3//! The extension ".nutexb" may stand for "Namco Universal Texture Binary".
4//!
5//! Image data is stored in a contiguous region of memory with metadata stored in the
6//! [layer_mipmaps](struct.NutexbFile.html#structfield.layer_mipmaps) and [footer](struct.NutexbFile.html#structfield.footer).
7//! The supported image formats in [NutexbFormat] use standard compressed and uncompressed formats used for DDS files.
8//! The arrays and mipmaps for the image data are stored in a memory layout optimized for the Tegra X1 in a process known as swizzling.
9//! This library provides tools for reading and writing nutexb files as well as working with the swizzled image data.
10//!
11//! ## Reading
12//! Read a [NutexbFile] with [NutexbFile::read] or [NutexbFile::read_from_file].
13//! The image data needs to be deswizzled first with [NutexbFile::deswizzled_data]
14//! to use with applications that expect a standard row-major memory layout.
15/*!
16```rust no_run
17# fn main() -> Result<(), Box<dyn std::error::Error>> {
18use nutexb::NutexbFile;
19
20let nutexb = NutexbFile::read_from_file("col_001.nutexb")?;
21let surface_data = nutexb.deswizzled_data();
22# Ok(()) }
23```
24 */
25//!
26//! ## Writing
27//! The easiest way to create a [NutexbFile] is by calling [NutexbFile::from_dds] and
28//! [NutexbFile::from_image] when using the `"ddsfile"` and `"image"` features, respectively.
29//! For manually specifying the surface dimensions and data, use [NutexbFile::from_surface].
30/*!
31```rust no_run
32# fn main() -> Result<(), Box<dyn std::error::Error>> {
33use nutexb::NutexbFile;
34
35let image = image::open("col_001.png")?;
36
37let nutexb = NutexbFile::from_image(&image.to_rgba8(), "col_001")?;
38nutexb.write_to_file("col_001.nutexb")?;
39# Ok(()) }
40```
41
42```rust no_run
43# fn main() -> Result<(), Box<dyn std::error::Error>> {
44use nutexb::NutexbFile;
45
46let mut reader = std::io::BufReader::new(std::fs::File::open("cube.dds")?);
47let dds = ddsfile::Dds::read(&mut reader)?;
48
49let nutexb = NutexbFile::from_dds(&dds, "cube")?;
50nutexb.write_to_file("col_001.nutexb")?;
51# Ok(()) }
52```
53 */
54use binrw::{binrw, prelude::*, NullString, Endian, VecArgs};
55use convert::{create_nutexb, create_nutexb_unswizzled};
56use std::{
57    io::{Cursor, Read, Seek, SeekFrom, Write},
58    num::NonZeroUsize,
59    path::Path,
60};
61use tegra_swizzle::surface::{deswizzled_surface_size, swizzled_surface_size, BlockDim};
62
63#[cfg(feature = "ddsfile")]
64pub use ddsfile;
65
66#[cfg(feature = "ddsfile")]
67pub use dds::ReadDdsError;
68
69#[cfg(feature = "ddsfile")]
70mod dds;
71
72#[cfg(feature = "image")]
73pub use image;
74
75mod convert;
76pub use convert::Surface;
77
78const FOOTER_SIZE: usize = 112;
79const LAYER_MIPMAPS_SIZE: usize = 64;
80
81/// The data stored in a nutexb file like `"def_001_col.nutexb"`.
82#[derive(Debug, Clone, BinWrite)]
83pub struct NutexbFile {
84    /// Combined image data for all array layer and mipmap levels.
85    pub data: Vec<u8>,
86
87    /// The size of the mipmaps for each array layer.
88    ///
89    /// Most nutexb files use swizzled image data,
90    /// so these sizes won't add up to the length of [data](struct.NutexbFile.html#structfield.data).
91    pub layer_mipmaps: Vec<LayerMipmaps>,
92
93    /// Information about the image stored in [data](#structfield.data).
94    pub footer: NutexbFooter,
95}
96
97// Use a custom parser since we don't know the data size until finding the footer.
98impl BinRead for NutexbFile {
99    type Args<'arg> = ();
100
101    fn read_options<R: Read + Seek>(
102        reader: &mut R,
103        _endian: Endian,
104        _args: Self::Args<'_>,
105    ) -> BinResult<Self> {
106        // We need the footer to know the size of the layer mipmaps.
107        reader.seek(SeekFrom::End(-(FOOTER_SIZE as i64)))?;
108        let footer: NutexbFooter = reader.read_le()?;
109
110        // We need the layer mipmaps to know the size of the data section.
111        reader.seek(SeekFrom::Current(
112            -(FOOTER_SIZE as i64 + LAYER_MIPMAPS_SIZE as i64 * footer.layer_count as i64),
113        ))?;
114
115        // The image data takes up the remaining space.
116        let data_size = reader.stream_position()?;
117
118        let layer_mipmaps: Vec<LayerMipmaps> = reader.read_le_args(VecArgs {
119            count: footer.layer_count as usize,
120            inner: (footer.mipmap_count,),
121        })?;
122
123        reader.seek(SeekFrom::Start(0))?;
124
125        let mut data = vec![0u8; data_size as usize];
126        reader.read_exact(&mut data)?;
127
128        Ok(Self {
129            data,
130            layer_mipmaps,
131            footer,
132        })
133    }
134}
135
136impl NutexbFile {
137    /// Reads the [NutexbFile] from the specified `reader`.
138    pub fn read<R: Read + Seek>(reader: &mut R) -> BinResult<Self> {
139        reader.read_le::<NutexbFile>()
140    }
141
142    /// Reads the [NutexbFile] from the specified `path`.
143    /// The entire file is buffered to improve performance.
144    pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<NutexbFile, binrw::Error> {
145        let mut file = Cursor::new(std::fs::read(path)?);
146        let nutexb = file.read_le::<NutexbFile>()?;
147        Ok(nutexb)
148    }
149
150    /// Writes the [NutexbFile] to the specified `writer`.
151    pub fn write<W: Write + Seek>(&self, writer: &mut W) -> Result<(), binrw::Error> {
152        self.write_le(writer).map_err(Into::into)
153    }
154
155    /// Writes the [NutexbFile] to the specified `path`.
156    /// The entire file is buffered to improve performance.
157    pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), binrw::Error> {
158        let mut writer = Cursor::new(Vec::new());
159        self.write(&mut writer)?;
160        std::fs::write(path, writer.into_inner()).map_err(Into::into)
161    }
162
163    /// Deswizzles all the layers and mipmaps in [data](#structfield.data).
164    pub fn deswizzled_data(&self) -> Result<Vec<u8>, tegra_swizzle::SwizzleError> {
165        tegra_swizzle::surface::deswizzle_surface(
166            self.footer.width as usize,
167            self.footer.height as usize,
168            self.footer.depth as usize,
169            &self.data,
170            self.footer.image_format.block_dim(),
171            None,
172            self.footer.image_format.bytes_per_pixel() as usize,
173            self.footer.mipmap_count as usize,
174            self.footer.layer_count as usize,
175        )
176    }
177
178    /// Creates a [NutexbFile] from `image` with the nutexb string set to `name`.
179    /// The data in `image` is swizzled according to the specified dimensions and format.
180    pub fn from_surface<T: AsRef<[u8]>, S: Into<String>>(
181        image: Surface<T>,
182        name: S,
183    ) -> Result<Self, tegra_swizzle::SwizzleError> {
184        create_nutexb(image, name)
185    }
186
187    /// Creates a [NutexbFile] from `surface` with the nutexb string set to `name` without any swizzling.
188    /// This assumes no layers or mipmaps for `image`.
189    /// Prefer [NutexbFile::from_surface] for better memory access performance in most cases.
190    ///
191    /// Textures created with [NutexbFile::from_surface] use a memory layout optimized for the Tegra X1 with better access performance in the general case.
192    /// This function exists for the rare case where swizzling the image data is not desired for performance or compatibility reasons.
193    pub fn from_surface_unswizzled<T: AsRef<[u8]>, S: Into<String>>(
194        surface: &Surface<T>,
195        name: S,
196    ) -> Self {
197        create_nutexb_unswizzled(surface, name)
198    }
199
200    #[cfg(feature = "ddsfile")]
201    /// Creates a swizzled [NutexbFile] from `dds` with the Nutexb string set to `name`.
202    ///
203    /// DDS supports all Nutexb image formats as well as array layers, mipmaps, cube maps, and 3D volume textures.
204    pub fn from_dds<S: Into<String>>(dds: &ddsfile::Dds, name: S) -> Result<Self, ReadDdsError> {
205        let surface = dds::create_surface(dds)?;
206        Self::from_surface(surface, name).map_err(Into::into)
207    }
208
209    /// Deswizzle the surface data to DDS while preserving the layers, mipmaps, and image format.
210    #[cfg(feature = "ddsfile")]
211    pub fn to_dds(&self) -> Result<ddsfile::Dds, tegra_swizzle::SwizzleError> {
212        dds::create_dds(&self)
213    }
214
215    #[cfg(feature = "image")]
216    /// Creates a swizzled 2D [NutexbFile] from `image` with the Nutexb string set to `name` and without mipmaps.
217    pub fn from_image<S: Into<String>>(
218        image: &image::RgbaImage,
219        name: S,
220    ) -> Result<Self, tegra_swizzle::SwizzleError> {
221        let surface = Surface {
222            width: image.width(),
223            height: image.height(),
224            depth: 1, // No depth for a 2d image
225            image_data: image.as_raw(),
226            mipmap_count: 1,
227            layer_count: 1,
228            image_format: NutexbFormat::R8G8B8A8Srgb,
229        };
230        Self::from_surface(surface, name)
231    }
232
233    /// Resizes the image data to the expected size based on the [footer](#structfield.footer) information by truncating or padding with zeros.
234    ///
235    /// Calling this method is unnecessary for nutexbs created with [NutexbFile::from_surface] or [NutexbFile::from_surface_unswizzled].
236    /// These methods already calculate the appropriate image data size.
237    pub fn optimize_size(&mut self) {
238        let new_len = if self.footer.unk3 == 0x1000 {
239            swizzled_surface_size(
240                self.footer.width as usize,
241                self.footer.height as usize,
242                self.footer.depth as usize,
243                self.footer.image_format.block_dim(),
244                None,
245                self.footer.image_format.bytes_per_pixel() as usize,
246                self.footer.mipmap_count as usize,
247                self.footer.layer_count as usize,
248            )
249        } else {
250            // Not all nutexbs store swizzled surfaces.
251            deswizzled_surface_size(
252                self.footer.width as usize,
253                self.footer.height as usize,
254                self.footer.depth as usize,
255                self.footer.image_format.block_dim(),
256                self.footer.image_format.bytes_per_pixel() as usize,
257                self.footer.mipmap_count as usize,
258                self.footer.layer_count as usize,
259            )
260        };
261
262        // Remove padding and align the surface to the appropriate size.
263        self.data.resize(new_len, 0);
264        self.footer.data_size = self.data.len() as u32;
265    }
266}
267
268/// Information about the image data.
269#[binrw]
270#[derive(Debug, Clone, PartialEq)]
271#[brw(magic = b" XNT")]
272pub struct NutexbFooter {
273    // TODO: Make this field "name: String"
274    // TODO: Names can be at most 63 characters + 1 null byte?
275    /// The name of the texture, which usually matches the file name without its extension like `"def_001_col"`.
276    #[brw(pad_size_to = 0x40)]
277    pub string: NullString,
278    /// The width of the texture in pixels.
279    pub width: u32,
280    /// The height of the texture in pixels.
281    pub height: u32,
282    /// The depth of the texture in pixels or 1 for 2D textures.
283    pub depth: u32,
284    /// The format of [data](struct.NutexbFile.html#structfield.data).
285    pub image_format: NutexbFormat,
286    pub unk2: u32, // TODO: Some kind of flags?
287    /// The number of mipmaps in [data](struct.NutexbFile.html#structfield.data) or 1 for no mipmapping.
288    pub mipmap_count: u32,
289    /// `0x1000` for nutexbs with swizzling and `0` otherwise
290    pub unk3: u32,
291    /// The number of texture layers in [data](struct.NutexbFile.html#structfield.data). This is 6 for cubemaps and 1 otherwise.
292    pub layer_count: u32,
293    /// The size in bytes of [data](struct.NutexbFile.html#structfield.data).
294    pub data_size: u32,
295    #[brw(magic = b" XET")]
296    pub version: (u16, u16),
297}
298
299/// The mipmap sizes for each array layer.
300#[binrw]
301#[derive(Debug, Clone)]
302#[br(import(mipmap_count: u32))]
303pub struct LayerMipmaps {
304    /// The size in bytes of the deswizzled data for each mipmap.
305    #[brw(pad_size_to = 0x40)]
306    #[br(count = mipmap_count)]
307    pub mipmap_sizes: Vec<u32>,
308}
309
310/// Supported image data formats.
311///
312/// These formats have a corresponding format in modern versions of graphics APIs like OpenGL, Vulkan, etc.
313/// All known [NutexbFormat] are supported by DDS DXGI formats.
314///
315/// In some contexts, "Unorm" is called "linear" or expanded to "unsigned normalized".
316/// "U" and "S" prefixes refer to "unsigned" and "signed" data, respectively.
317/// "Srgb", "Unorm", and "Snorm" variants use the same data format but use different conversions to floating point when accessed by a GPU shader.
318// TODO: It's possible this is some sort of flags.
319// ex: num channels, format, type (srgb, unorm, etc)?
320#[derive(Debug, Clone, Copy, PartialEq, Eq, BinRead, BinWrite)]
321#[brw(repr(u32))]
322pub enum NutexbFormat {
323    R8Unorm = 0x0100,
324    R8G8B8A8Unorm = 0x0400,
325    R8G8B8A8Srgb = 0x0405,
326    R32G32B32A32Float = 0x0434,
327    B8G8R8A8Unorm = 0x0450,
328    B8G8R8A8Srgb = 0x0455,
329    BC1Unorm = 0x0480,
330    BC1Srgb = 0x0485,
331    BC2Unorm = 0x0490,
332    BC2Srgb = 0x0495,
333    BC3Unorm = 0x04a0,
334    BC3Srgb = 0x04a5,
335    BC4Unorm = 0x0180,
336    BC4Snorm = 0x0185,
337    BC5Unorm = 0x0280,
338    BC5Snorm = 0x0285,
339    BC6Ufloat = 0x04d7,
340    BC6Sfloat = 0x04d8,
341    BC7Unorm = 0x04e0,
342    BC7Srgb = 0x04e5,
343}
344
345impl NutexbFormat {
346    /// The number of bytes per pixel.
347    /// For block compressed formats like [NutexbFormat::BC7Srgb], this is the size in bytes of a single block.
348    /// # Examples
349    /**
350    ```rust
351    # use nutexb::NutexbFormat;
352    assert_eq!(1, NutexbFormat::R8Unorm.bytes_per_pixel());
353    assert_eq!(4, NutexbFormat::R8G8B8A8Unorm.bytes_per_pixel());
354    assert_eq!(8, NutexbFormat::BC1Unorm.bytes_per_pixel());
355    assert_eq!(16, NutexbFormat::BC7Srgb.bytes_per_pixel());
356    assert_eq!(16, NutexbFormat::R32G32B32A32Float.bytes_per_pixel());
357    ```
358    */
359    pub fn bytes_per_pixel(&self) -> u32 {
360        match &self {
361            NutexbFormat::R8G8B8A8Unorm
362            | NutexbFormat::R8G8B8A8Srgb
363            | NutexbFormat::B8G8R8A8Unorm
364            | NutexbFormat::B8G8R8A8Srgb => 4,
365            NutexbFormat::R32G32B32A32Float => 16,
366            NutexbFormat::BC1Unorm | NutexbFormat::BC1Srgb => 8,
367            NutexbFormat::BC2Unorm | NutexbFormat::BC2Srgb => 16,
368            NutexbFormat::BC3Unorm | NutexbFormat::BC3Srgb => 16,
369            NutexbFormat::BC4Unorm | NutexbFormat::BC4Snorm => 8,
370            NutexbFormat::BC5Unorm | NutexbFormat::BC5Snorm => 16,
371            NutexbFormat::BC6Ufloat | NutexbFormat::BC6Sfloat => 16,
372            NutexbFormat::BC7Unorm | NutexbFormat::BC7Srgb => 16,
373            NutexbFormat::R8Unorm => 1,
374        }
375    }
376
377    /// The width in pixels for a compressed block or `1` for uncompressed formats.
378    ///
379    /// # Examples
380    /**
381    ```rust
382    # use nutexb::NutexbFormat;
383    assert_eq!(1, NutexbFormat::R8Unorm.block_width());
384    assert_eq!(1, NutexbFormat::R8G8B8A8Unorm.block_width());
385    assert_eq!(4, NutexbFormat::BC1Unorm.block_width());
386    assert_eq!(4, NutexbFormat::BC7Srgb.block_width());
387    ```
388    */
389    pub fn block_width(&self) -> u32 {
390        match &self {
391            NutexbFormat::R8Unorm
392            | NutexbFormat::R8G8B8A8Unorm
393            | NutexbFormat::R8G8B8A8Srgb
394            | NutexbFormat::R32G32B32A32Float
395            | NutexbFormat::B8G8R8A8Unorm
396            | NutexbFormat::B8G8R8A8Srgb => 1,
397            _ => 4,
398        }
399    }
400
401    /// The height in pixels for a compressed block or `1` for uncompressed formats.
402    ///
403    /// # Examples
404    /**
405    ```rust
406    # use nutexb::NutexbFormat;
407    assert_eq!(1, NutexbFormat::R8Unorm.block_height());
408    assert_eq!(1, NutexbFormat::R8G8B8A8Unorm.block_height());
409    assert_eq!(4, NutexbFormat::BC1Unorm.block_height());
410    assert_eq!(4, NutexbFormat::BC7Srgb.block_height());
411    ```
412    */
413    pub fn block_height(&self) -> u32 {
414        // All known nutexb formats use square blocks.
415        self.block_width()
416    }
417
418    /// The depth in pixels for a compressed block or `1` for uncompressed formats.
419    ///
420    /// # Examples
421    /**
422    ```rust
423    # use nutexb::NutexbFormat;
424    assert_eq!(1, NutexbFormat::R8Unorm.block_depth());
425    assert_eq!(1, NutexbFormat::R8G8B8A8Unorm.block_depth());
426    assert_eq!(1, NutexbFormat::BC1Unorm.block_depth());
427    assert_eq!(1, NutexbFormat::BC7Srgb.block_depth());
428    ```
429    */
430    pub fn block_depth(&self) -> u32 {
431        // All known nutexb formats use 2D blocks.
432        1
433    }
434
435    pub(crate) fn block_dim(&self) -> BlockDim {
436        BlockDim {
437            width: NonZeroUsize::new(self.block_width() as usize).unwrap(),
438            height: NonZeroUsize::new(self.block_height() as usize).unwrap(),
439            depth: NonZeroUsize::new(self.block_depth() as usize).unwrap(),
440        }
441    }
442}