diff --git a/Cargo.lock b/Cargo.lock index 85e8e6a..8512d72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1123,6 +1123,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666" dependencies = [ "crc32fast", + "libz-sys", "miniz_oxide 0.8.0", ] @@ -2036,6 +2037,17 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libz-sys" +version = "1.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -3169,6 +3181,7 @@ dependencies = [ "color-eyre", "csv-async", "fastrand", + "flate2", "futures", "futures-util", "glob", diff --git a/Cargo.toml b/Cargo.toml index d5eee3b..3e8f0f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ druid = { version = "0.8", features = ["im", "serde", "image", "png", "jpeg", "b druid-widget-nursery = "0.1" dtmt-shared = { path = "lib/dtmt-shared" } fastrand = "2.1.0" +flate2 = { version = "1.0.30", features = ["zlib"] } futures = "0.3.25" futures-util = "0.3.24" glob = "0.3.0" diff --git a/crates/dtmt/src/cmd/bundle/extract.rs b/crates/dtmt/src/cmd/bundle/extract.rs index 5a297e7..fca20e1 100644 --- a/crates/dtmt/src/cmd/bundle/extract.rs +++ b/crates/dtmt/src/cmd/bundle/extract.rs @@ -275,7 +275,13 @@ struct ExtractOptions<'a> { #[tracing::instrument( skip(ctx, options), - fields(decompile = options.decompile, flatten = options.flatten, dry_run = options.dry_run) + fields( + bundle_name = tracing::field::Empty, + bundle_hash = tracing::field::Empty, + decompile = options.decompile, + flatten = options.flatten, + dry_run = options.dry_run, + ) )] async fn extract_bundle( ctx: Arc, @@ -318,6 +324,11 @@ where let bundle = { let data = fs::read(path.as_ref()).await?; let name = Bundle::get_name_from_path(&ctx, path.as_ref()); + { + let span = tracing::span::Span::current(); + span.record("bundle_hash", format!("{:X}", name)); + span.record("bundle_name", name.display().to_string()); + } Bundle::from_binary(&ctx, name, data)? }; diff --git a/lib/sdk/Cargo.toml b/lib/sdk/Cargo.toml index 0cd0e4e..9abbb23 100644 --- a/lib/sdk/Cargo.toml +++ b/lib/sdk/Cargo.toml @@ -10,6 +10,7 @@ byteorder = { workspace = true } color-eyre = { workspace = true } csv-async = { workspace = true } fastrand = { workspace = true } +flate2 = { workspace = true } futures = { workspace = true } futures-util = { workspace = true } glob = { workspace = true } diff --git a/lib/sdk/src/binary.rs b/lib/sdk/src/binary.rs index 83ccca0..40f9e9a 100644 --- a/lib/sdk/src/binary.rs +++ b/lib/sdk/src/binary.rs @@ -42,6 +42,26 @@ impl FromBinary for Vec { } } +pub fn flags_from_bits(bits: T::Bits) -> T +where + ::Bits: std::fmt::Binary, +{ + if let Some(flags) = T::from_bits(bits) { + flags + } else { + let unknown = bits & !T::all().bits(); + + tracing::warn!( + "Unknown bits found for '{}': known = {:0b}, unknown = {:0b}", + std::any::type_name::(), + T::all().bits(), + unknown + ); + + T::from_bits_truncate(bits) + } +} + pub mod sync { use std::ffi::CStr; use std::io::{self, Read, Seek, SeekFrom, Write}; diff --git a/lib/sdk/src/filetype/texture.rs b/lib/sdk/src/filetype/texture.rs index 5a60b28..9e309ee 100644 --- a/lib/sdk/src/filetype/texture.rs +++ b/lib/sdk/src/filetype/texture.rs @@ -5,16 +5,15 @@ use bitflags::bitflags; use color_eyre::eyre::Context; use color_eyre::{eyre, SectionExt}; use color_eyre::{Help, Result}; -use num_traits::ToPrimitive as _; +use flate2::read::ZlibDecoder; use oodle::{OodleLZ_CheckCRC, OodleLZ_FuzzSafe}; use serde::{Deserialize, Serialize}; use tokio::fs; use crate::binary::sync::{ReadExt, WriteExt}; use crate::bundle::file::UserFile; -use crate::filetype::texture::dds::{DXGIFormat, ImageType}; use crate::murmur::{HashGroup, IdString32, IdString64}; -use crate::{BundleFile, BundleFileType, BundleFileVariant}; +use crate::{binary, BundleFile, BundleFileType, BundleFileVariant}; mod dds; @@ -43,7 +42,7 @@ struct TextureDefinitionOutput { } bitflags! { - #[derive(Clone, Copy, Debug)] + #[derive(Clone, Copy, Debug, Default)] struct TextureFlags: u32 { const STREAMABLE = 0b0000_0001; const UNKNOWN = 1 << 1; @@ -57,7 +56,7 @@ struct TextureHeaderMipInfo { size: usize, } -#[derive(Clone, Debug)] +#[derive(Clone, Default)] struct TextureHeader { flags: TextureFlags, n_streamable_mipmaps: usize, @@ -67,13 +66,33 @@ struct TextureHeader { meta_size: usize, } +impl std::fmt::Debug for TextureHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TextureHeader") + .field("flags", &self.flags) + .field("n_streamable_mipmaps", &self.n_streamable_mipmaps) + .field("width", &self.width) + .field("height", &self.height) + .field("mip_infos", &{ + let mut s = self + .mip_infos + .iter() + .fold(String::from("["), |mut s, info| { + s.push_str(&format!("{}/{}, ", info.offset, info.size)); + s + }); + s.push(']'); + s + }) + .field("meta_size", &self.meta_size) + .finish() + } +} + impl TextureHeader { #[tracing::instrument(skip(r))] fn from_binary(mut r: impl ReadExt) -> Result { - let flags = r.read_u32().and_then(|bits| { - TextureFlags::from_bits(bits) - .ok_or_else(|| eyre::eyre!("Unknown bits set in TextureFlags: {:032b}", bits)) - })?; + let flags = r.read_u32().map(binary::flags_from_bits)?; let n_streamable_mipmaps = r.read_u32()? as usize; let width = r.read_u32()? as usize; let height = r.read_u32()? as usize; @@ -121,7 +140,7 @@ impl TextureHeader { } } -#[derive(Clone, Debug)] +#[derive(Clone)] struct Texture { header: TextureHeader, data: Vec, @@ -129,6 +148,37 @@ struct Texture { category: IdString32, } +impl std::fmt::Debug for Texture { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut out = f.debug_struct("Texture"); + out.field("header", &self.header); + + if self.data.len() <= 5 { + out.field("data", &format!("{:x?}", &self.data)); + } else { + out.field( + "data", + &format!("{:x?}.. ({} bytes)", &self.data[..5], &self.data.len()), + ); + } + + if let Some(stream) = self.stream.as_ref() { + if stream.len() <= 5 { + out.field("stream", &format!("{:x?}", &stream)); + } else { + out.field( + "stream", + &format!("{:x?}.. ({} bytes)", &stream[..5], &stream.len()), + ); + } + } else { + out.field("stream", &"None"); + } + + out.field("category", &self.category).finish() + } +} + impl Texture { #[tracing::instrument(skip(data, chunks))] fn decompress_stream_data(mut data: impl Read, chunks: impl AsRef<[usize]>) -> Result> { @@ -214,35 +264,83 @@ impl Texture { Ok(out) } - #[tracing::instrument(skip(ctx, r, stream_r))] + #[tracing::instrument( + "Texture::from_binary", + skip(ctx, r, stream_r), + fields( + compression_type = tracing::field::Empty, + compressed_size = tracing::field::Empty, + uncompressed_size = tracing::field::Empty, + ) + )] fn from_binary( ctx: &crate::Context, mut r: impl Read + Seek, mut stream_r: Option, ) -> Result { - // Looking at the executable in IDA, there is one other valid value: `2`. - // If this ever comes up in the game data, I'll have to reverse engineer the - // (de)compression algorithm through IDA. let compression_type = r.read_u32()?; - eyre::ensure!( - compression_type == 1, - "Unknown compression type for texture '{}'", - compression_type - ); - let compressed_size = r.read_u32()? as usize; let uncompressed_size = r.read_u32()? as usize; - let out_buf = { - let mut comp_buf = vec![0; compressed_size]; - r.read_exact(&mut comp_buf)?; + { + let span = tracing::Span::current(); + span.record("compression_type", compression_type); + span.record("compressed_size", compressed_size); + span.record("uncompressed_size", uncompressed_size); + } - oodle::decompress( + let mut comp_buf = vec![0; compressed_size]; + r.read_exact(&mut comp_buf)?; + + let out_buf = match compression_type { + // Uncompressed + // This one never seems to contain the additional `TextureHeader` metadata, + // so we return early in this branch. + 0 => { + eyre::ensure!( + compressed_size == 0 && uncompressed_size == 0, + "Cannot handle texture with compression_type == 0, but buffer sizes > 0" + ); + tracing::trace!("Found raw texture"); + + let pos = r.stream_position()?; + let end = { + r.seek(SeekFrom::End(0))?; + let end = r.stream_position()?; + r.seek(SeekFrom::Start(pos))?; + end + }; + + // Reads until the last u32. + let mut data = vec![0u8; (end - pos - 4) as usize]; + r.read_exact(&mut data)?; + + let category = r.read_u32().map(IdString32::from)?; + + return Ok(Self { + header: TextureHeader::default(), + data, + stream: None, + category, + }); + } + 1 => oodle::decompress( comp_buf, uncompressed_size, OodleLZ_FuzzSafe::No, OodleLZ_CheckCRC::No, - )? + )?, + 2 => { + let mut decoder = ZlibDecoder::new(comp_buf.as_slice()); + let mut buf = Vec::with_capacity(uncompressed_size); + + decoder.read_to_end(&mut buf)?; + buf + } + _ => eyre::bail!( + "Unknown compression type for texture '{}'", + compression_type + ), }; eyre::ensure!( @@ -339,6 +437,126 @@ impl Texture { serde_sjson::to_string(&texture).wrap_err("Failed to serialize texture definition") } + #[tracing::instrument(fields( + dds_header = tracing::field::Empty, + dx10_header = tracing::field::Empty, + image_format = tracing::field::Empty, + ))] + fn create_dds_user_file(&self, name: String) -> Result { + let mut data = Cursor::new(&self.data); + let mut dds_header = + dds::DDSHeader::from_binary(&mut data).wrap_err("Failed to read DDS header")?; + + { + let span = tracing::Span::current(); + span.record("dds_header", format!("{:?}", dds_header)); + } + + if !dds_header.pixel_format.flags.contains(dds::DDPF::FOURCC) { + tracing::debug!("Found DDS without FourCC. Dumping raw data"); + return Ok(UserFile::with_name(self.data.clone(), name)); + } + + // eyre::ensure!( + // dds_header.pixel_format.four_cc == dds::FourCC::DX10, + // "Only DX10 textures are currently supported. FourCC == {}", + // dds_header.pixel_format.four_cc, + // ); + + let dx10_header = + dds::Dx10Header::from_binary(&mut data).wrap_err("Failed to read DX10 header")?; + + { + let span = tracing::Span::current(); + span.record("dx10_header", format!("{:?}", dx10_header)); + } + + // match dx10_header.dxgi_format { + // DXGIFormat::BC1_UNORM + // | DXGIFormat::BC3_UNORM + // | DXGIFormat::BC4_UNORM + // | DXGIFormat::BC5_UNORM + // | DXGIFormat::BC6H_UF16 + // | DXGIFormat::BC7_UNORM => {} + // _ => { + // eyre::bail!( + // "Unsupported DXGI format: {} (0x{:0X})", + // dx10_header.dxgi_format, + // dx10_header.dxgi_format.to_u32().unwrap_or_default() + // ); + // } + // } + + let stingray_image_format = dds::stripped_format_from_header(&dds_header, &dx10_header)?; + { + let span = tracing::Span::current(); + span.record("image_format", format!("{:?}", stingray_image_format)); + } + + // eyre::ensure!( + // stingray_image_format.image_type == ImageType::Image2D, + // "Unsupported image type: {}", + // stingray_image_format.image_type, + // ); + + let block_size = 4 * dds_header.pitch_or_linear_size / dds_header.width; + let bits_per_block: usize = match block_size { + 8 => 128, + 16 => 64, + block_size => eyre::bail!("Unsupported block size {}", block_size), + }; + + let pitch = self.header.width / 4 * block_size; + let bytes_per_block = self.header.width / bits_per_block / 4; + + tracing::debug!( + "block_size = {} | pitch = {} | bits_per_block = {} | bytes_per_block = {}", + block_size, + pitch, + bits_per_block, + bytes_per_block + ); + + let mut out_data = Cursor::new(Vec::with_capacity(self.data.len())); + + // Currently, we only extract the largest mipmap, + // so we need to set the dimensions accordingly, and remove the + // flag. + dds_header.width = self.header.width; + dds_header.height = self.header.height; + dds_header.mipmap_count = 0; + dds_header.flags &= !dds::DDSD::MIPMAPCOUNT; + + dds_header + .to_binary(&mut out_data) + .wrap_err("Failed to write DDS header")?; + + dx10_header + .to_binary(&mut out_data) + .wrap_err("Failed to write DX10 header")?; + + if let Some(stream) = &self.stream { + let data = Self::reorder_stream_mipmap( + stream, + bits_per_block, + bytes_per_block, + block_size, + pitch, + ) + .wrap_err("Failed to reorder stream chunks")?; + + out_data + .write_all(&data) + .wrap_err("Failed to write streamed mipmap data")?; + } else { + out_data + .write_all(data.remaining_slice()) + .wrap_err("Failed to write texture data")?; + }; + + Ok(UserFile::with_name(out_data.into_inner(), name)) + } + #[tracing::instrument(skip(self))] fn to_user_files(&self, name: String) -> Result> { let mut files = Vec::with_capacity(2); @@ -369,122 +587,38 @@ impl Texture { files.push(UserFile::with_name(self.data.clone(), name)); } + match self + .create_dds_user_file(name) + .wrap_err("Failed to create DDS file") { - let mut data = Cursor::new(&self.data); - let mut dds_header = - dds::DDSHeader::from_binary(&mut data).wrap_err("Failed to read DDS header")?; - - eyre::ensure!( - dds_header.pixel_format.flags.contains(dds::DDPF::FOURCC) - && dds_header.pixel_format.four_cc == dds::FOURCC_DX10, - "Only DX10 textures are currently supported." - ); - - let dx10_header = - dds::Dx10Header::from_binary(&mut data).wrap_err("Failed to read DX10 header")?; - - match dx10_header.dxgi_format { - DXGIFormat::BC1_UNORM - | DXGIFormat::BC3_UNORM - | DXGIFormat::BC4_UNORM - | DXGIFormat::BC5_UNORM - | DXGIFormat::BC6H_UF16 - | DXGIFormat::BC7_UNORM => {} - _ => { - eyre::bail!( - "Unsupported DXGI format: {} (0x{:0X})", - dx10_header.dxgi_format, - dx10_header.dxgi_format.to_u32().unwrap_or_default() + Ok(dds) => files.push(dds), + Err(err) => { + if cfg!(debug_assertions) { + tracing::error!( + "{:?}", + err.with_section(|| { + "Running in debug mode, continuing to produce raw files".header("Note:") + }) ); + } else { + return Err(err); } } - - let stingray_image_format = - dds::stripped_format_from_header(&dds_header, &dx10_header)?; - eyre::ensure!( - stingray_image_format.image_type == ImageType::Image2D, - "Unsupported image type: {}", - stingray_image_format.image_type, - ); - - let block_size = 4 * dds_header.pitch_or_linear_size / dds_header.width; - let bits_per_block: usize = match block_size { - 8 => 128, - 16 => 64, - block_size => eyre::bail!("Unsupported block size {}", block_size), - }; - - let pitch = self.header.width / 4 * block_size; - let bytes_per_block = self.header.width / bits_per_block / 4; - - tracing::debug!( - "block_size = {} | pitch = {} | bits_per_block = {} | bytes_per_block = {}", - block_size, - pitch, - bits_per_block, - bytes_per_block - ); - - let mut out_data = Cursor::new(Vec::with_capacity(self.data.len())); - - // Currently, we only extract the largest mipmap, - // so we need to set the dimensions accordingly, and remove the - // flag. - dds_header.width = self.header.width; - dds_header.height = self.header.height; - dds_header.mipmap_count = 0; - dds_header.flags &= !dds::DDSD::MIPMAPCOUNT; - - dds_header - .to_binary(&mut out_data) - .wrap_err("Failed to write DDS header")?; - - dx10_header - .to_binary(&mut out_data) - .wrap_err("Failed to write DX10 header")?; - - if let Some(stream) = &self.stream { - let data = Self::reorder_stream_mipmap( - stream, - bits_per_block, - bytes_per_block, - block_size, - pitch, - ) - .wrap_err("Failed to reorder stream chunks")?; - - out_data - .write_all(&data) - .wrap_err("Failed to write streamed mipmap data")?; - } else { - out_data - .write_all(data.remaining_slice()) - .wrap_err("Failed to write texture data")?; - }; - - files.push(UserFile::with_name(out_data.into_inner(), name)); - } + }; Ok(files) } } -#[tracing::instrument(skip(ctx, data), fields(buf_len = data.as_ref().len()))] +#[tracing::instrument(skip(ctx, data, stream_data), fields(data_len = data.as_ref().len()))] pub(crate) async fn decompile_data( ctx: &crate::Context, name: String, data: impl AsRef<[u8]>, - stream_file_name: Option, + stream_data: Option>, ) -> Result> { - let mut r = Cursor::new(data.as_ref()); - let mut stream_r = if let Some(file_name) = stream_file_name { - let stream_data = fs::read(&file_name) - .await - .wrap_err_with(|| format!("Failed to read stream file '{}'", file_name.display()))?; - Some(Cursor::new(stream_data)) - } else { - None - }; + let mut r = Cursor::new(data); + let mut stream_r = stream_data.map(Cursor::new); let texture = Texture::from_binary(ctx, &mut r, stream_r.as_mut())?; texture @@ -498,38 +632,47 @@ pub(crate) async fn decompile( name: String, variant: &BundleFileVariant, ) -> Result> { - if !variant.external() { + let data_file = variant.data_file_name().map(|name| match &ctx.game_dir { + Some(dir) => dir.join("bundle").join(name), + None => PathBuf::from("bundle").join(name), + }); + + if variant.external() { + let Some(path) = data_file else { + eyre::bail!("File is marked external but has no data file name"); + }; + + tracing::debug!( + "Decompiling texture from external file '{}'", + path.display() + ); + + let data = fs::read(&path) + .await + .wrap_err_with(|| format!("Failed to read data file '{}'", path.display())) + .with_suggestion(|| { + "Provide a game directory in the config file or make sure the `data` directory is next to the provided bundle." + })?; + + decompile_data(ctx, name, data, None::<&[u8]>).await + } else { tracing::debug!("Decompiling texture from bundle data"); - let stream_file_name = variant.data_file_name().map(|name| match &ctx.game_dir { - Some(dir) => dir.join("bundle").join(name), - None => PathBuf::from("bundle").join(name), - }); + let stream_data = match data_file { + Some(path) => { + let data = fs::read(&path) + .await + .wrap_err_with(|| format!("Failed to read data file '{}'", path.display())) + .with_suggestion(|| { + "Provide a game directory in the config file or make sure the `data` directory is next to the provided bundle." + })?; + Some(data) + } + None => None, + }; - return decompile_data(ctx, name, variant.data(), stream_file_name).await; + decompile_data(ctx, name, variant.data(), stream_data).await } - - let Some(file_name) = variant.data_file_name() else { - eyre::bail!("Texture file has no data and no data file"); - }; - - tracing::debug!("Decompiling texture from external file '{}'", file_name); - - let path = match &ctx.game_dir { - Some(dir) => dir.join("bundle").join(file_name), - None => PathBuf::from("bundle").join(file_name), - }; - - tracing::trace!(path = %path.display()); - - let data = fs::read(&path) - .await - .wrap_err_with(|| format!("Failed to read data file '{}'", path.display())) - .with_suggestion(|| { - "Provide a game directory in the config file or make sure the `data` directory is next to the provided bundle." - })?; - - decompile_data(ctx, name, &data, None).await } #[tracing::instrument(skip(sjson, name), fields(sjson_len = sjson.as_ref().len(), name = %name.display()))] diff --git a/lib/sdk/src/filetype/texture/dds.rs b/lib/sdk/src/filetype/texture/dds.rs index 28775f7..ca0b73f 100644 --- a/lib/sdk/src/filetype/texture/dds.rs +++ b/lib/sdk/src/filetype/texture/dds.rs @@ -7,10 +7,10 @@ use color_eyre::Result; use num_derive::{FromPrimitive, ToPrimitive}; use num_traits::{FromPrimitive as _, ToPrimitive as _}; +use crate::binary; use crate::binary::sync::{ReadExt, WriteExt}; const MAGIC_DDS: u32 = 0x20534444; -pub const FOURCC_DX10: u32 = 0x30315844; bitflags! { #[derive(Clone, Copy, Debug)] @@ -75,27 +75,8 @@ bitflags! { } } -fn flags_from_bits(bits: T::Bits) -> T -where - ::Bits: std::fmt::Binary, -{ - if let Some(flags) = T::from_bits(bits) { - flags - } else { - let unknown = bits & !T::all().bits(); - - tracing::warn!( - "Unknown bits found for '{}': known = {:0b}, unknown = {:0b}", - std::any::type_name::(), - T::all().bits(), - unknown - ); - - T::from_bits_truncate(bits) - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq, FromPrimitive, ToPrimitive)] +#[repr(u32)] pub enum D3D10ResourceDimension { Unknown = 0, Buffer = 1, @@ -107,6 +88,7 @@ pub enum D3D10ResourceDimension { #[allow(clippy::upper_case_acronyms)] #[allow(non_camel_case_types)] #[derive(Clone, Copy, Debug, strum::Display, FromPrimitive, ToPrimitive)] +#[repr(u32)] pub enum DXGIFormat { UNKNOWN = 0, R32G32B32A32_TYPELESS = 1, @@ -243,7 +225,7 @@ pub struct Dx10Header { } impl Dx10Header { - #[tracing::instrument(skip(r))] + #[tracing::instrument("Dx10Header::from_binary", skip(r))] pub fn from_binary(mut r: impl ReadExt) -> Result { let dxgi_format = r .read_u32() @@ -251,7 +233,7 @@ impl Dx10Header { let resource_dimension = r.read_u32().map(|val| { D3D10ResourceDimension::from_u32(val).unwrap_or(D3D10ResourceDimension::Unknown) })?; - let misc_flag = r.read_u32().map(flags_from_bits)?; + let misc_flag = r.read_u32().map(binary::flags_from_bits)?; let array_size = r.read_u32()? as usize; let misc_flags2 = r.read_u32()?; @@ -264,7 +246,7 @@ impl Dx10Header { }) } - #[tracing::instrument(skip(w))] + #[tracing::instrument("Dx10Header::to_binary", skip(w))] pub fn to_binary(&self, mut w: impl WriteExt) -> Result<()> { w.write_u32( self.dxgi_format @@ -284,10 +266,30 @@ impl Dx10Header { } } +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display, FromPrimitive, ToPrimitive)] +#[repr(u32)] +pub enum FourCC { + Empty = u32::MAX, + DXT1 = 0x31545844, + DXT2 = 0x33545844, + DXT5 = 0x35545844, + AXI1 = 0x31495441, + AXI2 = 0x32495441, + DX10 = 0x30315844, + D3D_A16B16G16R16 = 0x24, + D3D_R16F = 0x6F, + D3D_G16R16F = 0x70, + D3D_A16B16G16R16F = 0x71, + D3D_R32F = 0x72, + D3D_G32R32F = 0x73, + D3D_A32B32G32R32F = 0x74, +} + #[derive(Clone, Copy, Debug)] pub struct DDSPixelFormat { pub flags: DDPF, - pub four_cc: u32, + pub four_cc: FourCC, pub rgb_bit_count: u32, pub r_bit_mask: u32, pub g_bit_mask: u32, @@ -296,7 +298,7 @@ pub struct DDSPixelFormat { } impl DDSPixelFormat { - #[tracing::instrument(skip(r))] + #[tracing::instrument("DDSPixelFormat::from_binary", skip(r))] pub fn from_binary(mut r: impl ReadExt) -> Result { let size = r.read_u32()? as usize; eyre::ensure!( @@ -305,8 +307,17 @@ impl DDSPixelFormat { size ); - let flags = r.read_u32().map(flags_from_bits)?; - let four_cc = r.read_u32()?; + let flags: DDPF = r.read_u32().map(binary::flags_from_bits)?; + + let four_cc = if flags.contains(DDPF::FOURCC) { + r.read_u32().and_then(|bytes| { + FourCC::from_u32(bytes).ok_or_eyre(format!("Unknown FourCC value: {:08X}", bytes)) + })? + } else { + r.skip_u32(0)?; + FourCC::Empty + }; + let rgb_bit_count = r.read_u32()?; let r_bit_mask = r.read_u32()?; let g_bit_mask = r.read_u32()?; @@ -324,13 +335,13 @@ impl DDSPixelFormat { }) } - #[tracing::instrument(skip(w))] + #[tracing::instrument("DDSPixelFormat::to_binary", skip(w))] pub fn to_binary(&self, mut w: impl WriteExt) -> Result<()> { // Structure size w.write_u32(32)?; w.write_u32(self.flags.bits())?; - w.write_u32(self.four_cc)?; + w.write_u32(self.four_cc.to_u32().unwrap_or_default())?; w.write_u32(self.rgb_bit_count)?; w.write_u32(self.r_bit_mask)?; w.write_u32(self.g_bit_mask)?; @@ -356,7 +367,7 @@ pub struct DDSHeader { } impl DDSHeader { - #[tracing::instrument(skip(r))] + #[tracing::instrument("DDSHeader::from_binary", skip(r))] pub fn from_binary(mut r: impl ReadExt) -> Result { r.skip_u32(MAGIC_DDS).wrap_err("Invalid magic bytes")?; @@ -367,7 +378,7 @@ impl DDSHeader { size ); - let flags = r.read_u32().map(flags_from_bits)?; + let flags = r.read_u32().map(binary::flags_from_bits)?; let height = r.read_u32()? as usize; let width = r.read_u32()? as usize; let pitch_or_linear_size = r.read_u32()? as usize; @@ -378,8 +389,8 @@ impl DDSHeader { r.seek(SeekFrom::Current(11 * 4))?; let pixel_format = DDSPixelFormat::from_binary(&mut r)?; - let caps = r.read_u32().map(flags_from_bits)?; - let caps_2 = r.read_u32().map(flags_from_bits)?; + let caps = r.read_u32().map(binary::flags_from_bits)?; + let caps_2 = r.read_u32().map(binary::flags_from_bits)?; // Skip unused and reserved bytes r.seek(SeekFrom::Current(3 * 4))?; @@ -397,7 +408,7 @@ impl DDSHeader { }) } - #[tracing::instrument(skip(w))] + #[tracing::instrument("DDSHeader::to_binary", skip(w))] pub fn to_binary(&self, mut w: impl WriteExt) -> Result<()> { w.write_u32(MAGIC_DDS)?; @@ -422,7 +433,8 @@ impl DDSHeader { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Display)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] +#[repr(u32)] pub enum ImageType { Image2D = 0, Image3D = 1,