Reverse DDSImage::load

Decompiling the game binary shows a rather elaborate algorithm to load
DDS images from binary. Though comparing it to Microsoft's documentation
on DDS, most of it seems to be pretty standard handling.

However, we don't actually need all of it. The part about calculating
pitch and reading blocks only accesses a subset of the `ImageFormat`
struct, so we can strip our implementation to just that.
This commit is contained in:
Lucas Schwiderski 2024-07-22 11:28:36 +02:00
parent 84cb6ff985
commit 30b9a93fa3
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
2 changed files with 220 additions and 12 deletions

View file

@ -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()
);

View file

@ -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<StrippedImageFormat> {
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)
}