From 9f849ab3ecbbb0808b8c5f44e6d42375a78eee43 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 24 Jul 2024 14:15:51 +0200 Subject: [PATCH] sdk: Implement decompiling streamed mipmaps For now, we only extract the largest mipmap. --- Cargo.lock | 36 +++ Cargo.toml | 3 + lib/sdk/Cargo.toml | 3 + lib/sdk/src/filetype/texture.rs | 319 +++++++++++++++++------ lib/sdk/src/filetype/texture/dds.rs | 384 +++++++++++++++++++++++++--- lib/sdk/src/lib.rs | 1 + 6 files changed, 639 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65ac0dd..68fb0e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2500,6 +2500,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3346,11 +3357,14 @@ dependencies = [ "glob", "luajit2-sys", "nanorand", + "num-derive", + "num-traits", "oodle", "path-slash", "pin-project-lite", "serde", "serde_sjson", + "strum", "tokio", "tokio-stream", "tracing", @@ -3612,6 +3626,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.100", +] + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index 4b083a9..c6243c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ luajit2-sys = { path = "lib/luajit2-sys" } minijinja = { version = "2.0.1", default-features = false, features = ["serde"] } nanorand = "0.7.0" nexusmods = { path = "lib/nexusmods" } +num-derive = "0.4.2" +num-traits = "0.2.19" notify = "8.0.0" oodle = { path = "lib/oodle" } open = "5.0.1" @@ -49,6 +51,7 @@ serde = { version = "1.0.152", features = ["derive", "rc"] } serde_sjson = "1.2.1" steamlocate = "2.0.0-beta.2" strip-ansi-escapes = "0.2.0" +strum = { version = "0.26.3", features = ["derive", "strum_macros"] } time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset", "formatting", "macros"] } tokio = { version = "1.23.0", features = ["rt-multi-thread", "fs", "process", "macros", "tracing", "io-util", "io-std"] } tokio-stream = { version = "0.1.12", features = ["fs", "io-util"] } diff --git a/lib/sdk/Cargo.toml b/lib/sdk/Cargo.toml index 4667a1c..0cd0e4e 100644 --- a/lib/sdk/Cargo.toml +++ b/lib/sdk/Cargo.toml @@ -15,11 +15,14 @@ futures-util = { workspace = true } glob = { workspace = true } luajit2-sys = { workspace = true } nanorand = { workspace = true } +num-derive = { workspace = true } +num-traits = { workspace = true } oodle = { workspace = true } path-slash = { workspace = true } pin-project-lite = { workspace = true } serde = { workspace = true } serde_sjson = { workspace = true } +strum = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } tracing = { workspace = true } diff --git a/lib/sdk/src/filetype/texture.rs b/lib/sdk/src/filetype/texture.rs index 58c0dbf..1565b8c 100644 --- a/lib/sdk/src/filetype/texture.rs +++ b/lib/sdk/src/filetype/texture.rs @@ -1,16 +1,18 @@ -use std::io::{Cursor, Read, Seek, SeekFrom}; +use std::io::{Cursor, Read, Seek, SeekFrom, Write as _}; use std::path::{Path, PathBuf}; use bitflags::bitflags; use color_eyre::eyre::Context; use color_eyre::{eyre, SectionExt}; use color_eyre::{Help, Result}; +use num_traits::ToPrimitive as _; 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}; @@ -49,13 +51,20 @@ bitflags! { } } +#[derive(Copy, Clone, Debug, Default)] +struct TextureHeaderMipInfo { + offset: usize, + size: usize, +} + #[derive(Clone, Debug)] struct TextureHeader { flags: TextureFlags, - n_streamable_mipmaps: u32, - width: u32, - height: u32, - mip_info_size: u32, + n_streamable_mipmaps: usize, + width: usize, + height: usize, + mip_infos: [TextureHeaderMipInfo; 16], + meta_size: usize, } impl TextureHeader { @@ -65,23 +74,26 @@ impl TextureHeader { TextureFlags::from_bits(bits) .ok_or_else(|| eyre::eyre!("Unknown bits set in TextureFlags: {:032b}", bits)) })?; - let n_streamable_mipmaps = r.read_u32()?; - let width = r.read_u32()?; - let height = r.read_u32()?; + let n_streamable_mipmaps = r.read_u32()? as usize; + let width = r.read_u32()? as usize; + let height = r.read_u32()? as usize; - r.skip_u32(0)?; + let mut mip_infos = [TextureHeaderMipInfo::default(); 16]; - // A section of 15 pairs of two u32 - r.seek(SeekFrom::Current(2 * 4 * 15))?; + for info in mip_infos.iter_mut() { + info.offset = r.read_u32()? as usize; + info.size = r.read_u32()? as usize; + } - let mip_info_size = r.read_u32()?; + let meta_size = r.read_u32()? as usize; Ok(Self { flags, n_streamable_mipmaps, width, height, - mip_info_size, + mip_infos, + meta_size, }) } @@ -93,15 +105,16 @@ impl TextureHeader { ); w.write_u32(self.flags.bits())?; - w.write_u32(self.n_streamable_mipmaps)?; - w.write_u32(self.width)?; - w.write_u32(self.height)?; + w.write_u32(self.n_streamable_mipmaps as u32)?; + w.write_u32(self.width as u32)?; + w.write_u32(self.height as u32)?; - // See `from_binary` about this unknown section. - let buf = [0; (2 * 4 * 15) + 4]; - w.write_all(&buf)?; + for info in self.mip_infos { + w.write_u32(info.offset as u32)?; + w.write_u32(info.size as u32)?; + } - // TODO: For now we write `0` here, until the mipmap section is figured out + // TODO: For now we write `0` here, until the meta section is figured out w.write_u32(0)?; Ok(()) @@ -117,6 +130,90 @@ struct Texture { } impl Texture { + #[tracing::instrument(skip(data, chunks))] + fn decompress_stream_data(mut data: impl Read, chunks: impl AsRef<[usize]>) -> Result> { + const RAW_SIZE: usize = 0x10000; + + let chunks = chunks.as_ref(); + + let max_size = chunks.iter().max().copied().unwrap_or(RAW_SIZE); + let mut read_buf = vec![0; max_size]; + + let mut stream_raw = Vec::with_capacity(chunks.iter().sum()); + let mut last = 0; + + for offset_next in chunks { + let size = offset_next - last; + + let span = tracing::info_span!( + "stream chunk", + num_chunks = chunks.len(), + chunk_size_comp = size, + offset = last + ); + let _enter = span.enter(); + + let buf = &mut read_buf[0..size]; + data.read_exact(buf) + .wrap_err("Failed to read chunk from stream file")?; + + let raw = oodle::decompress(buf, RAW_SIZE, OodleLZ_FuzzSafe::No, OodleLZ_CheckCRC::No) + .wrap_err("Failed to decompress stream chunk")?; + eyre::ensure!( + raw.len() == RAW_SIZE, + "Invalid chunk length after decompression" + ); + + stream_raw.extend_from_slice(&raw); + + last = *offset_next; + } + Ok(stream_raw) + } + + #[tracing::instrument(skip(data), fields(data_len = data.as_ref().len()))] + fn reorder_stream_mipmap( + data: impl AsRef<[u8]>, + bits_per_block: usize, + bytes_per_block: usize, + block_size: usize, + pitch: usize, + ) -> Result> { + const CHUNK_SIZE: usize = 0x10000; + let data = data.as_ref(); + + let mut out = Vec::with_capacity(data.len()); + let mut window = vec![0u8; pitch * 64]; + + let row_size = bits_per_block * block_size; + tracing::Span::current().record("row_size", row_size); + + eyre::ensure!( + data.len() % CHUNK_SIZE == 0, + "Stream data does not divide evenly into chunks" + ); + + for (i, chunk) in data.chunks_exact(CHUNK_SIZE).enumerate() { + let chunk_x = (i % bytes_per_block) * row_size; + + let span = tracing::trace_span!("chunk", i, chunk_x = chunk_x); + let _guard = span.enter(); + + if i > 0 && i % bytes_per_block == 0 { + out.extend_from_slice(&window); + } + + for (j, row) in chunk.chunks_exact(row_size).enumerate() { + let start = chunk_x + j * pitch; + let end = start + row_size; + tracing::trace!("{i}/{j} at {}:{}", start, end); + window[start..end].copy_from_slice(row); + } + } + + Ok(out) + } + #[tracing::instrument(skip(ctx, r, stream_r))] fn from_binary( ctx: &crate::Context, @@ -162,19 +259,19 @@ impl Texture { let header = TextureHeader::from_binary(&mut r)?; eyre::ensure!( - header.mip_info_size == 0 || stream_r.is_some(), - "Compression chunks and stream file don't match up. mip_info_size = {}, has_stream = {}", - header.mip_info_size, + header.meta_size == 0 || stream_r.is_some(), + "Compression chunks and stream file don't match up. meta_size = {}, has_stream = {}", + header.meta_size, stream_r.is_some() ); let stream = if let Some(stream_r) = stream_r.as_mut() { // Number of compression chunks in the stream file - let num_chunks = r.read_u32()?; + let num_chunks = r.read_u32()? as usize; r.skip_u16(0)?; { - let num_chunks_1 = r.read_u16()? as u32; + let num_chunks_1 = r.read_u16()? as usize; eyre::ensure!( num_chunks == num_chunks_1, @@ -184,37 +281,15 @@ impl Texture { ); } - const RAW_SIZE: usize = 0x10000; - let mut stream_raw = Vec::new(); - let mut last = 0; + let mut chunks = Vec::with_capacity(num_chunks); - for i in 0..num_chunks { - let offset_next = r.read_u32()? as usize; - let size = offset_next - last; - - let span = tracing::info_span!( - "read stream chunk", - num_chunks, - i, - chunk_size = size, - offset = last - ); - let _enter = span.enter(); - - let mut buf = vec![0; size]; - stream_r - .read_exact(&mut buf) - .wrap_err("Failed to read chunk from stream file")?; - - let raw = - oodle::decompress(&buf, RAW_SIZE, OodleLZ_FuzzSafe::No, OodleLZ_CheckCRC::No) - .wrap_err("Failed to decompress stream chunk")?; - - stream_raw.extend_from_slice(&raw); - - last = offset_next; + for _ in 0..num_chunks { + chunks.push(r.read_u32()? as usize); } + let stream_raw = Self::decompress_stream_data(stream_r, chunks) + .wrap_err("Failed to decompress stream data")?; + Some(stream_raw) } else { None @@ -247,10 +322,6 @@ impl Texture { self.header.to_binary(&mut w)?; - // More data not fully figured out, yet. - let meta_size = 0; - w.write_u32(meta_size)?; - w.write_u32(self.category.to_murmur32().into())?; Ok(()) } @@ -268,19 +339,9 @@ impl Texture { serde_sjson::to_string(&texture).wrap_err("Failed to serialize texture definition") } - #[tracing::instrument] + #[tracing::instrument(skip(self))] fn to_user_files(&self, name: String) -> Result> { - let mut files = Vec::with_capacity(3); - - // TODO: Don't clone. - - if let Some(stream) = &self.stream { - let stream_name = PathBuf::from(&name).with_extension("stream"); - files.push(UserFile::with_name( - stream.clone(), - stream_name.display().to_string(), - )); - } + let mut files = Vec::with_capacity(2); { let data = self.to_sjson(name.clone())?.as_bytes().to_vec(); @@ -291,7 +352,119 @@ impl Texture { files.push(UserFile::with_name(data, name)); } - files.push(UserFile::with_name(self.data.clone(), name)); + // For debugging purposes, also extract the raw files + if cfg!(debug_assertions) { + if let Some(stream) = &self.stream { + let stream_name = PathBuf::from(&name).with_extension("stream"); + files.push(UserFile::with_name( + stream.clone(), + stream_name.display().to_string(), + )); + } + + let name = PathBuf::from(&name) + .with_extension("raw.dds") + .display() + .to_string(); + files.push(UserFile::with_name(self.data.clone(), name)); + } + + { + 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() + ); + } + } + + 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.split().1) + .wrap_err("Failed to write texture data")?; + }; + + files.push(UserFile::with_name(out_data.into_inner(), name)); + } + Ok(files) } } @@ -388,8 +561,8 @@ pub async fn compile( r.seek(SeekFrom::Current(5))?; - let width = r.read_u16()? as u32; - let height = r.read_u16()? as u32; + let width = r.read_u32()? as usize; + let height = r.read_u32()? as usize; (width, height) }; @@ -403,6 +576,8 @@ pub async fn compile( n_streamable_mipmaps: 0, width, height, + mip_infos: [TextureHeaderMipInfo::default(); 16], + meta_size: 0, }, data: dds, stream: None, diff --git a/lib/sdk/src/filetype/texture/dds.rs b/lib/sdk/src/filetype/texture/dds.rs index 135d24c..5ef1b90 100644 --- a/lib/sdk/src/filetype/texture/dds.rs +++ b/lib/sdk/src/filetype/texture/dds.rs @@ -1,6 +1,16 @@ +use std::io::SeekFrom; + use bitflags::bitflags; -use color_eyre::eyre; +use color_eyre::eyre::Context as _; +use color_eyre::eyre::{self, OptionExt as _}; use color_eyre::Result; +use num_derive::{FromPrimitive, ToPrimitive}; +use num_traits::{FromPrimitive as _, ToPrimitive as _}; + +use crate::binary::sync::{ReadExt, WriteExt}; + +const MAGIC_DDS: u32 = 0x20534444; +pub const FOURCC_DX10: u32 = 0x30315844; bitflags! { #[derive(Clone, Copy, Debug)] @@ -65,7 +75,27 @@ bitflags! { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +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)] pub enum D3D10ResourceDimension { Unknown = 0, Buffer = 1, @@ -74,46 +104,325 @@ pub enum D3D10ResourceDimension { Texture3D = 4, } +#[allow(clippy::upper_case_acronyms)] +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, strum::Display, FromPrimitive, ToPrimitive)] +pub enum DXGIFormat { + UNKNOWN = 0, + R32G32B32A32_TYPELESS = 1, + R32G32B32A32_FLOAT = 2, + R32G32B32A32_UINT = 3, + R32G32B32A32_SINT = 4, + R32G32B32_TYPELESS = 5, + R32G32B32_FLOAT = 6, + R32G32B32_UINT = 7, + R32G32B32_SINT = 8, + R16G16B16A16_TYPELESS = 9, + R16G16B16A16_FLOAT = 10, + R16G16B16A16_UNORM = 11, + R16G16B16A16_UINT = 12, + R16G16B16A16_SNORM = 13, + R16G16B16A16_SINT = 14, + R32G32_TYPELESS = 15, + R32G32_FLOAT = 16, + R32G32_UINT = 17, + R32G32_SINT = 18, + R32G8X24_TYPELESS = 19, + D32_FLOAT_S8X24_UINT = 20, + R32_FLOAT_X8X24_TYPELESS = 21, + X32_TYPELESS_G8X24_UINT = 22, + R10G10B10A2_TYPELESS = 23, + R10G10B10A2_UNORM = 24, + R10G10B10A2_UINT = 25, + R11G11B10_FLOAT = 26, + R8G8B8A8_TYPELESS = 27, + R8G8B8A8_UNORM = 28, + R8G8B8A8_UNORM_SRGB = 29, + R8G8B8A8_UINT = 30, + R8G8B8A8_SNORM = 31, + R8G8B8A8_SINT = 32, + R16G16_TYPELESS = 33, + R16G16_FLOAT = 34, + R16G16_UNORM = 35, + R16G16_UINT = 36, + R16G16_SNORM = 37, + R16G16_SINT = 38, + R32_TYPELESS = 39, + D32_FLOAT = 40, + R32_FLOAT = 41, + R32_UINT = 42, + R32_SINT = 43, + R24G8_TYPELESS = 44, + D24_UNORM_S8_UINT = 45, + R24_UNORM_X8_TYPELESS = 46, + X24_TYPELESS_G8_UINT = 47, + R8G8_TYPELESS = 48, + R8G8_UNORM = 49, + R8G8_UINT = 50, + R8G8_SNORM = 51, + R8G8_SINT = 52, + R16_TYPELESS = 53, + R16_FLOAT = 54, + D16_UNORM = 55, + R16_UNORM = 56, + R16_UINT = 57, + R16_SNORM = 58, + R16_SINT = 59, + R8_TYPELESS = 60, + R8_UNORM = 61, + R8_UINT = 62, + R8_SNORM = 63, + R8_SINT = 64, + A8_UNORM = 65, + R1_UNORM = 66, + R9G9B9E5_SHAREDEXP = 67, + R8G8_B8G8_UNORM = 68, + G8R8_G8B8_UNORM = 69, + BC1_TYPELESS = 70, + BC1_UNORM = 71, + BC1_UNORM_SRGB = 72, + BC2_TYPELESS = 73, + BC2_UNORM = 74, + BC2_UNORM_SRGB = 75, + BC3_TYPELESS = 76, + BC3_UNORM = 77, + BC3_UNORM_SRGB = 78, + BC4_TYPELESS = 79, + BC4_UNORM = 80, + BC4_SNORM = 81, + BC5_TYPELESS = 82, + BC5_UNORM = 83, + BC5_SNORM = 84, + B5G6R5_UNORM = 85, + B5G5R5A1_UNORM = 86, + B8G8R8A8_UNORM = 87, + B8G8R8X8_UNORM = 88, + R10G10B10_XR_BIAS_A2_UNORM = 89, + B8G8R8A8_TYPELESS = 90, + B8G8R8A8_UNORM_SRGB = 91, + B8G8R8X8_TYPELESS = 92, + B8G8R8X8_UNORM_SRGB = 93, + BC6H_TYPELESS = 94, + BC6H_UF16 = 95, + BC6H_SF16 = 96, + BC7_TYPELESS = 97, + BC7_UNORM = 98, + BC7_UNORM_SRGB = 99, + AYUV = 100, + Y410 = 101, + Y416 = 102, + NV12 = 103, + P010 = 104, + P016 = 105, + OPAQUE = 106, + YUY2 = 107, + Y210 = 108, + Y216 = 109, + NV11 = 110, + AI44 = 111, + IA44 = 112, + P8 = 113, + A8P8 = 114, + B4G4R4A4_UNORM = 115, + P208 = 130, + V208 = 131, + V408 = 132, + SAMPLER_FEEDBACK_MIN_MIP_OPAQUE, + SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE, +} + +#[derive(Clone, Copy, Debug)] pub struct Dx10Header { /// Resource data formats, including fully-typed and typeless formats. /// See https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format - dxgi_format: u32, - resource_dimension: D3D10ResourceDimension, - misc_flag: DdsResourceMiscFlags, - array_size: u32, - misc_flags2: u32, + pub dxgi_format: DXGIFormat, + pub resource_dimension: D3D10ResourceDimension, + pub misc_flag: DdsResourceMiscFlags, + pub array_size: usize, + pub misc_flags2: u32, } +impl Dx10Header { + #[tracing::instrument(skip(r))] + pub fn from_binary(mut r: impl ReadExt) -> Result { + let dxgi_format = r + .read_u32() + .map(|val| DXGIFormat::from_u32(val).unwrap_or(DXGIFormat::UNKNOWN))?; + 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 array_size = r.read_u32()? as usize; + let misc_flags2 = r.read_u32()?; + + Ok(Self { + dxgi_format, + resource_dimension, + misc_flag, + array_size, + misc_flags2, + }) + } + + #[tracing::instrument(skip(w))] + pub fn to_binary(&self, mut w: impl WriteExt) -> Result<()> { + w.write_u32( + self.dxgi_format + .to_u32() + .ok_or_eyre("DXGIFormat should fit in a u32")?, + )?; + w.write_u32( + self.resource_dimension + .to_u32() + .ok_or_eyre("DXGIFormat should fit in a u32")?, + )?; + w.write_u32(self.misc_flag.bits())?; + w.write_u32(self.array_size as u32)?; + w.write_u32(self.misc_flags2)?; + + Ok(()) + } +} + +#[derive(Clone, Copy, Debug)] pub struct DDSPixelFormat { - /// Structure size. Must be `32`. - size: u32, - flags: DDPF, - four_cc: u32, - rgb_bit_count: u32, - r_bit_mask: u32, - g_bit_mask: u32, - b_bit_mask: u32, - a_bit_mask: u32, + pub flags: DDPF, + pub four_cc: u32, + pub rgb_bit_count: u32, + pub r_bit_mask: u32, + pub g_bit_mask: u32, + pub b_bit_mask: u32, + pub a_bit_mask: u32, } +impl DDSPixelFormat { + #[tracing::instrument(skip(r))] + pub fn from_binary(mut r: impl ReadExt) -> Result { + let size = r.read_u32()? as usize; + eyre::ensure!( + size == 32, + "Invalid structure size. Got 0X{:0X}, expected 0x20", + size + ); + + let flags = r.read_u32().map(flags_from_bits)?; + let four_cc = r.read_u32()?; + let rgb_bit_count = r.read_u32()?; + let r_bit_mask = r.read_u32()?; + let g_bit_mask = r.read_u32()?; + let b_bit_mask = r.read_u32()?; + let a_bit_mask = r.read_u32()?; + + Ok(Self { + flags, + four_cc, + rgb_bit_count, + r_bit_mask, + g_bit_mask, + b_bit_mask, + a_bit_mask, + }) + } + + #[tracing::instrument(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.rgb_bit_count)?; + w.write_u32(self.r_bit_mask)?; + w.write_u32(self.g_bit_mask)?; + w.write_u32(self.b_bit_mask)?; + w.write_u32(self.a_bit_mask)?; + + Ok(()) + } +} + +#[derive(Clone, Copy, Debug)] pub struct DDSHeader { - /// Size of this structure. Must be `124`. - size: u32, /// Flags to indicate which members contain valid data. - flags: DDSD, - height: u32, - width: u32, - pitch_or_linear_size: u32, - depth: u32, - mipmap_count: u32, - reserved_1: [u8; 11], - pixel_format: DDSPixelFormat, - caps: DDSCAPS, - caps_2: DDSCAPS2, - reserved_2: [u8; 3], + pub flags: DDSD, + pub height: usize, + pub width: usize, + pub pitch_or_linear_size: usize, + pub depth: usize, + pub mipmap_count: usize, + pub pixel_format: DDSPixelFormat, + pub caps: DDSCAPS, + pub caps_2: DDSCAPS2, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +impl DDSHeader { + #[tracing::instrument(skip(r))] + pub fn from_binary(mut r: impl ReadExt) -> Result { + r.skip_u32(MAGIC_DDS).wrap_err("Invalid magic bytes")?; + + let size = r.read_u32()?; + eyre::ensure!( + size == 124, + "Invalid structure size. Got 0x{:0X}, expected 0x7C", + size + ); + + let flags = r.read_u32().map(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; + let depth = r.read_u32()? as usize; + let mipmap_count = r.read_u32()? as usize; + + // Skip reserved bytes + 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)?; + + // Skip unused and reserved bytes + r.seek(SeekFrom::Current(3 * 4))?; + + Ok(Self { + flags, + height, + width, + pitch_or_linear_size, + depth, + mipmap_count, + pixel_format, + caps, + caps_2, + }) + } + + #[tracing::instrument(skip(w))] + pub fn to_binary(&self, mut w: impl WriteExt) -> Result<()> { + w.write_u32(MAGIC_DDS)?; + + // Structure size in bytes + w.write_u32(124)?; + w.write_u32(self.flags.bits())?; + w.write_u32(self.height as u32)?; + w.write_u32(self.width as u32)?; + w.write_u32(self.pitch_or_linear_size as u32)?; + w.write_u32(self.depth as u32)?; + w.write_u32(self.mipmap_count as u32)?; + + w.write_all(&[0u8; 11 * 4])?; + + self.pixel_format.to_binary(&mut w)?; + w.write_u32(self.caps.bits())?; + w.write_u32(self.caps_2.bits())?; + + w.write_all(&[0u8; 3 * 4])?; + + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] pub enum ImageType { Image2D = 0, Image3D = 1, @@ -125,12 +434,14 @@ pub enum ImageType { /// A stripped version of `ImageType` that only contains just the data needed /// to read a DDS image stream. +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] pub struct StrippedImageFormat { pub image_type: ImageType, - pub width: u32, - pub height: u32, - pub layers: u32, - pub mip_levels: u32, + pub width: usize, + pub height: usize, + pub layers: usize, + pub mip_levels: usize, } // This is a stripped down version of the logic that the engine implements to fill @@ -172,7 +483,10 @@ pub fn stripped_format_from_header( } if dx10_header.resource_dimension == D3D10ResourceDimension::Texture2D { - if dx10_header.misc_flag == DdsResourceMiscFlags::TextureCube { + if dx10_header + .misc_flag + .contains(DdsResourceMiscFlags::TEXTURECUBE) + { image_format.image_type = ImageType::ImageCube; if dx10_header.array_size > 1 { image_format.layers = dx10_header.array_size; diff --git a/lib/sdk/src/lib.rs b/lib/sdk/src/lib.rs index 9b1806b..41310fc 100644 --- a/lib/sdk/src/lib.rs +++ b/lib/sdk/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(cursor_split)] #![feature(test)] mod binary;