WIP: Implement texture files #191

Draft
lucas wants to merge 14 commits from feat/textures into master
6 changed files with 645 additions and 107 deletions
Showing only changes of commit 233389ebb1 - Show all commits

42
Cargo.lock generated
View file

@ -2308,6 +2308,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.90",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -3066,6 +3077,12 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "rustybuzz"
version = "0.6.0"
@ -3157,11 +3174,14 @@ dependencies = [
"glob",
"luajit2-sys",
"nanorand",
"num-derive",
"num-traits",
"oodle",
"path-slash",
"pin-project-lite",
"serde",
"serde_sjson",
"strum",
"tokio",
"tokio-stream",
"tracing",
@ -3421,6 +3441,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.90",
]
[[package]]
name = "subtle"
version = "2.6.1"

View file

@ -38,6 +38,8 @@ luajit2-sys = { path = "lib/luajit2-sys" }
minijinja = { version = "2.0.1", default-features = false }
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"
@ -50,6 +52,7 @@ serde = { version = "1.0.152", features = ["derive", "rc"] }
serde_sjson = { path = "lib/serde_sjson" }
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"] }

View file

@ -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 }

View file

@ -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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<UserFile>> {
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));
}
// 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.remaining_slice())
.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,

View file

@ -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<T: bitflags::Flags>(bits: T::Bits) -> T
where
<T as bitflags::Flags>::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>(),
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<Self> {
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<Self> {
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<Self> {
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, 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;

View file

@ -1,3 +1,4 @@
#![feature(cursor_remaining)]
#![feature(test)]
mod binary;