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}