diff --git a/lib/sdk/src/filetype/texture.rs b/lib/sdk/src/filetype/texture.rs index 7912e45..58c0dbf 100644 --- a/lib/sdk/src/filetype/texture.rs +++ b/lib/sdk/src/filetype/texture.rs @@ -14,6 +14,8 @@ use crate::bundle::file::UserFile; use crate::murmur::{HashGroup, IdString32, IdString64}; use crate::{BundleFile, BundleFileType, BundleFileVariant}; +mod dds; + #[derive(Clone, Debug, Deserialize, Serialize)] struct TextureDefinition { common: TextureDefinitionPlatform, @@ -53,6 +55,7 @@ struct TextureHeader { n_streamable_mipmaps: u32, width: u32, height: u32, + mip_info_size: u32, } impl TextureHeader { @@ -66,18 +69,19 @@ impl TextureHeader { let width = r.read_u32()?; let height = r.read_u32()?; - // Don't quite know yet what this is, only that it is related to mipmaps. - // The reference to "streamable mipmaps" comes from VT2, so far. - // As such, it might be related to the stream file, but since all texture files have it, - // The engine calculates some offset and then moves 68 bytes at that offset to the beginning. - // Hence the split between `68` and `60` in the length. - r.seek(SeekFrom::Current(68 + 60))?; + r.skip_u32(0)?; + + // A section of 15 pairs of two u32 + r.seek(SeekFrom::Current(2 * 4 * 15))?; + + let mip_info_size = r.read_u32()?; Ok(Self { flags, n_streamable_mipmaps, width, height, + mip_info_size, }) } @@ -94,9 +98,12 @@ impl TextureHeader { w.write_u32(self.height)?; // See `from_binary` about this unknown section. - let buf = [0; 148]; + let buf = [0; (2 * 4 * 15) + 4]; w.write_all(&buf)?; + // TODO: For now we write `0` here, until the mipmap section is figured out + w.write_u32(0)?; + Ok(()) } } @@ -154,12 +161,10 @@ impl Texture { let header = TextureHeader::from_binary(&mut r)?; - let meta_size = r.read_u32()?; - eyre::ensure!( - meta_size == 0 || stream_r.is_some(), - "Compression chunks and stream file don't match up. meta_size = {}, stream = {}", - meta_size, + 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, stream_r.is_some() ); diff --git a/lib/sdk/src/filetype/texture/dds.rs b/lib/sdk/src/filetype/texture/dds.rs new file mode 100644 index 0000000..135d24c --- /dev/null +++ b/lib/sdk/src/filetype/texture/dds.rs @@ -0,0 +1,203 @@ +use bitflags::bitflags; +use color_eyre::eyre; +use color_eyre::Result; + +bitflags! { + #[derive(Clone, Copy, Debug)] + pub struct DDSD: u32 { + /// Required + const CAPS = 0x1; + /// Required + const HEIGHT = 0x2; + /// Required + const WIDTH = 0x4; + /// Pitch for an uncompressed texture + const PITCH = 0x8; + /// Required + const PIXELFORMAT = 0x1000; + /// Required in a mipmapped texture + const MIPMAPCOUNT = 0x20000; + /// Pitch for a compressed texture + const LINEARSIZE = 0x80000; + /// Required in a depth texture + const DEPTH = 0x800000; + } + + #[derive(Clone, Copy, Debug)] + pub struct DDSCAPS: u32 { + const COMPLEX = 0x8; + const MIPMAP = 0x400000; + const TEXTURE = 0x1000; + } + + #[derive(Clone, Copy, Debug)] + pub struct DDSCAPS2: u32 { + const CUBEMAP = 0x200; + const CUBEMAP_POSITIVEX = 0x400; + const CUBEMAP_NEGATIVEX = 0x800; + const CUBEMAP_POSITIVEY = 0x1000; + const CUBEMAP_NEGATIVEY = 0x2000; + const CUBEMAP_POSITIVEZ = 0x4000; + const CUBEMAP_NEGATIVEZ = 0x8000; + const VOLUME = 0x200000; + + const CUBEMAP_ALLFACES = Self::CUBEMAP_POSITIVEX.bits() + | Self::CUBEMAP_NEGATIVEX.bits() + | Self::CUBEMAP_POSITIVEY.bits() + | Self::CUBEMAP_NEGATIVEY.bits() + | Self::CUBEMAP_POSITIVEZ.bits() + | Self::CUBEMAP_NEGATIVEZ.bits(); + } + + #[derive(Clone, Copy, Debug)] + pub struct DDPF: u32 { + const ALPHAPIXELS = 0x1; + const ALPHA = 0x2; + const FOURCC = 0x4; + const RGB = 0x40; + const YUV = 0x200; + const LUMINANCE = 0x20000; + } + + #[derive(Clone, Copy, Debug)] + pub struct DdsResourceMiscFlags: u32 { + const TEXTURECUBE = 0x4; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum D3D10ResourceDimension { + Unknown = 0, + Buffer = 1, + Texture1D = 2, + Texture2D = 3, + Texture3D = 4, +} + +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 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 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], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ImageType { + Image2D = 0, + Image3D = 1, + ImageCube = 2, + Unknown = 3, + Image2dArray = 4, + ImagecubeArray = 5, +} + +/// A stripped version of `ImageType` that only contains just the data needed +/// to read a DDS image stream. +pub struct StrippedImageFormat { + pub image_type: ImageType, + pub width: u32, + pub height: u32, + pub layers: u32, + pub mip_levels: u32, +} + +// This is a stripped down version of the logic that the engine implements to fill +// `stingray::ImageFormat`. With the `type` field we need to distinguish between `IMAGE3D` +// and everything else, and we need the various dimensions filled to calculate the chunks. +pub fn stripped_format_from_header( + dds_header: &DDSHeader, + dx10_header: &Dx10Header, +) -> Result { + let mut image_format = StrippedImageFormat { + image_type: ImageType::Unknown, + width: dds_header.width, + height: dds_header.height, + layers: 0, + mip_levels: 0, + }; + + if dds_header.mipmap_count > 0 { + image_format.mip_levels = dds_header.mipmap_count; + } else { + image_format.mip_levels = 1; + } + + // INFO: These next two sections are conditional in the engine code, + // based on a lot of stuff in "fourcc" and other fields. But it might + // actually be fine to just do it like this, as this seems universal + // to DDS. + // Will have to check how it plays out with actual assets. + + if dds_header.caps_2.contains(DDSCAPS2::CUBEMAP) { + image_format.image_type = ImageType::ImageCube; + image_format.layers = 6; + } else if dds_header.caps_2.contains(DDSCAPS2::VOLUME) { + image_format.image_type = ImageType::Image3D; + image_format.layers = dds_header.depth; + } else { + image_format.image_type = ImageType::Image2D; + image_format.layers = 1; + } + + if dx10_header.resource_dimension == D3D10ResourceDimension::Texture2D { + if dx10_header.misc_flag == DdsResourceMiscFlags::TextureCube { + image_format.image_type = ImageType::ImageCube; + if dx10_header.array_size > 1 { + image_format.layers = dx10_header.array_size; + } else { + image_format.layers = 6; + } + } else { + image_format.image_type = ImageType::Image2D; + image_format.layers = dx10_header.array_size; + } + } else if dx10_header.resource_dimension == D3D10ResourceDimension::Texture3D { + image_format.image_type = ImageType::Image3D; + image_format.layers = dds_header.depth; + } + + if dx10_header.array_size > 1 { + match image_format.image_type { + ImageType::Image2D => image_format.image_type = ImageType::Image2dArray, + ImageType::ImageCube => image_format.image_type = ImageType::ImagecubeArray, + ImageType::Image3D => { + eyre::bail!("3D-Arrays are not a supported image format") + } + _ => {} + } + } + + Ok(image_format) +}