WIP: Implement texture files #191
7 changed files with 393 additions and 192 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -1123,6 +1123,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666"
|
checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
|
"libz-sys",
|
||||||
"miniz_oxide 0.8.0",
|
"miniz_oxide 0.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2036,6 +2037,17 @@ dependencies = [
|
||||||
"redox_syscall",
|
"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]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
@ -3169,6 +3181,7 @@ dependencies = [
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"csv-async",
|
"csv-async",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
"flate2",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"glob",
|
"glob",
|
||||||
|
|
|
@ -29,6 +29,7 @@ druid = { version = "0.8", features = ["im", "serde", "image", "png", "jpeg", "b
|
||||||
druid-widget-nursery = "0.1"
|
druid-widget-nursery = "0.1"
|
||||||
dtmt-shared = { path = "lib/dtmt-shared" }
|
dtmt-shared = { path = "lib/dtmt-shared" }
|
||||||
fastrand = "2.1.0"
|
fastrand = "2.1.0"
|
||||||
|
flate2 = { version = "1.0.30", features = ["zlib"] }
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
futures-util = "0.3.24"
|
futures-util = "0.3.24"
|
||||||
glob = "0.3.0"
|
glob = "0.3.0"
|
||||||
|
|
|
@ -275,7 +275,13 @@ struct ExtractOptions<'a> {
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
skip(ctx, options),
|
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<P1, P2>(
|
async fn extract_bundle<P1, P2>(
|
||||||
ctx: Arc<sdk::Context>,
|
ctx: Arc<sdk::Context>,
|
||||||
|
@ -318,6 +324,11 @@ where
|
||||||
let bundle = {
|
let bundle = {
|
||||||
let data = fs::read(path.as_ref()).await?;
|
let data = fs::read(path.as_ref()).await?;
|
||||||
let name = Bundle::get_name_from_path(&ctx, path.as_ref());
|
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)?
|
Bundle::from_binary(&ctx, name, data)?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ byteorder = { workspace = true }
|
||||||
color-eyre = { workspace = true }
|
color-eyre = { workspace = true }
|
||||||
csv-async = { workspace = true }
|
csv-async = { workspace = true }
|
||||||
fastrand = { workspace = true }
|
fastrand = { workspace = true }
|
||||||
|
flate2 = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
glob = { workspace = true }
|
glob = { workspace = true }
|
||||||
|
|
|
@ -42,6 +42,26 @@ impl<T: FromBinary> FromBinary for Vec<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub mod sync {
|
pub mod sync {
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||||
|
|
|
@ -5,16 +5,15 @@ use bitflags::bitflags;
|
||||||
use color_eyre::eyre::Context;
|
use color_eyre::eyre::Context;
|
||||||
use color_eyre::{eyre, SectionExt};
|
use color_eyre::{eyre, SectionExt};
|
||||||
use color_eyre::{Help, Result};
|
use color_eyre::{Help, Result};
|
||||||
use num_traits::ToPrimitive as _;
|
use flate2::read::ZlibDecoder;
|
||||||
use oodle::{OodleLZ_CheckCRC, OodleLZ_FuzzSafe};
|
use oodle::{OodleLZ_CheckCRC, OodleLZ_FuzzSafe};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
use crate::binary::sync::{ReadExt, WriteExt};
|
use crate::binary::sync::{ReadExt, WriteExt};
|
||||||
use crate::bundle::file::UserFile;
|
use crate::bundle::file::UserFile;
|
||||||
use crate::filetype::texture::dds::{DXGIFormat, ImageType};
|
|
||||||
use crate::murmur::{HashGroup, IdString32, IdString64};
|
use crate::murmur::{HashGroup, IdString32, IdString64};
|
||||||
use crate::{BundleFile, BundleFileType, BundleFileVariant};
|
use crate::{binary, BundleFile, BundleFileType, BundleFileVariant};
|
||||||
|
|
||||||
mod dds;
|
mod dds;
|
||||||
|
|
||||||
|
@ -43,7 +42,7 @@ struct TextureDefinitionOutput {
|
||||||
}
|
}
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
struct TextureFlags: u32 {
|
struct TextureFlags: u32 {
|
||||||
const STREAMABLE = 0b0000_0001;
|
const STREAMABLE = 0b0000_0001;
|
||||||
const UNKNOWN = 1 << 1;
|
const UNKNOWN = 1 << 1;
|
||||||
|
@ -57,7 +56,7 @@ struct TextureHeaderMipInfo {
|
||||||
size: usize,
|
size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Default)]
|
||||||
struct TextureHeader {
|
struct TextureHeader {
|
||||||
flags: TextureFlags,
|
flags: TextureFlags,
|
||||||
n_streamable_mipmaps: usize,
|
n_streamable_mipmaps: usize,
|
||||||
|
@ -67,13 +66,33 @@ struct TextureHeader {
|
||||||
meta_size: usize,
|
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 {
|
impl TextureHeader {
|
||||||
#[tracing::instrument(skip(r))]
|
#[tracing::instrument(skip(r))]
|
||||||
fn from_binary(mut r: impl ReadExt) -> Result<Self> {
|
fn from_binary(mut r: impl ReadExt) -> Result<Self> {
|
||||||
let flags = r.read_u32().and_then(|bits| {
|
let flags = r.read_u32().map(binary::flags_from_bits)?;
|
||||||
TextureFlags::from_bits(bits)
|
|
||||||
.ok_or_else(|| eyre::eyre!("Unknown bits set in TextureFlags: {:032b}", bits))
|
|
||||||
})?;
|
|
||||||
let n_streamable_mipmaps = r.read_u32()? as usize;
|
let n_streamable_mipmaps = r.read_u32()? as usize;
|
||||||
let width = r.read_u32()? as usize;
|
let width = r.read_u32()? as usize;
|
||||||
let height = 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 {
|
struct Texture {
|
||||||
header: TextureHeader,
|
header: TextureHeader,
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
|
@ -129,6 +148,37 @@ struct Texture {
|
||||||
category: IdString32,
|
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 {
|
impl Texture {
|
||||||
#[tracing::instrument(skip(data, chunks))]
|
#[tracing::instrument(skip(data, chunks))]
|
||||||
fn decompress_stream_data(mut data: impl Read, chunks: impl AsRef<[usize]>) -> Result<Vec<u8>> {
|
fn decompress_stream_data(mut data: impl Read, chunks: impl AsRef<[usize]>) -> Result<Vec<u8>> {
|
||||||
|
@ -214,35 +264,83 @@ impl Texture {
|
||||||
Ok(out)
|
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(
|
fn from_binary(
|
||||||
ctx: &crate::Context,
|
ctx: &crate::Context,
|
||||||
mut r: impl Read + Seek,
|
mut r: impl Read + Seek,
|
||||||
mut stream_r: Option<impl Read>,
|
mut stream_r: Option<impl Read>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
// 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()?;
|
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 compressed_size = r.read_u32()? as usize;
|
||||||
let uncompressed_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];
|
let span = tracing::Span::current();
|
||||||
r.read_exact(&mut comp_buf)?;
|
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,
|
comp_buf,
|
||||||
uncompressed_size,
|
uncompressed_size,
|
||||||
OodleLZ_FuzzSafe::No,
|
OodleLZ_FuzzSafe::No,
|
||||||
OodleLZ_CheckCRC::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!(
|
eyre::ensure!(
|
||||||
|
@ -339,6 +437,126 @@ impl Texture {
|
||||||
serde_sjson::to_string(&texture).wrap_err("Failed to serialize texture definition")
|
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<UserFile> {
|
||||||
|
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))]
|
#[tracing::instrument(skip(self))]
|
||||||
fn to_user_files(&self, name: String) -> Result<Vec<UserFile>> {
|
fn to_user_files(&self, name: String) -> Result<Vec<UserFile>> {
|
||||||
let mut files = Vec::with_capacity(2);
|
let mut files = Vec::with_capacity(2);
|
||||||
|
@ -369,122 +587,38 @@ impl Texture {
|
||||||
files.push(UserFile::with_name(self.data.clone(), name));
|
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);
|
Ok(dds) => files.push(dds),
|
||||||
let mut dds_header =
|
Err(err) => {
|
||||||
dds::DDSHeader::from_binary(&mut data).wrap_err("Failed to read DDS header")?;
|
if cfg!(debug_assertions) {
|
||||||
|
tracing::error!(
|
||||||
eyre::ensure!(
|
"{:?}",
|
||||||
dds_header.pixel_format.flags.contains(dds::DDPF::FOURCC)
|
err.with_section(|| {
|
||||||
&& dds_header.pixel_format.four_cc == dds::FOURCC_DX10,
|
"Running in debug mode, continuing to produce raw files".header("Note:")
|
||||||
"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()
|
|
||||||
);
|
);
|
||||||
|
} 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)
|
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(
|
pub(crate) async fn decompile_data(
|
||||||
ctx: &crate::Context,
|
ctx: &crate::Context,
|
||||||
name: String,
|
name: String,
|
||||||
data: impl AsRef<[u8]>,
|
data: impl AsRef<[u8]>,
|
||||||
stream_file_name: Option<PathBuf>,
|
stream_data: Option<impl AsRef<[u8]>>,
|
||||||
) -> Result<Vec<UserFile>> {
|
) -> Result<Vec<UserFile>> {
|
||||||
let mut r = Cursor::new(data.as_ref());
|
let mut r = Cursor::new(data);
|
||||||
let mut stream_r = if let Some(file_name) = stream_file_name {
|
let mut stream_r = stream_data.map(Cursor::new);
|
||||||
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 texture = Texture::from_binary(ctx, &mut r, stream_r.as_mut())?;
|
let texture = Texture::from_binary(ctx, &mut r, stream_r.as_mut())?;
|
||||||
texture
|
texture
|
||||||
|
@ -498,38 +632,47 @@ pub(crate) async fn decompile(
|
||||||
name: String,
|
name: String,
|
||||||
variant: &BundleFileVariant,
|
variant: &BundleFileVariant,
|
||||||
) -> Result<Vec<UserFile>> {
|
) -> Result<Vec<UserFile>> {
|
||||||
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");
|
tracing::debug!("Decompiling texture from bundle data");
|
||||||
|
|
||||||
let stream_file_name = variant.data_file_name().map(|name| match &ctx.game_dir {
|
let stream_data = match data_file {
|
||||||
Some(dir) => dir.join("bundle").join(name),
|
Some(path) => {
|
||||||
None => PathBuf::from("bundle").join(name),
|
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()))]
|
#[tracing::instrument(skip(sjson, name), fields(sjson_len = sjson.as_ref().len(), name = %name.display()))]
|
||||||
|
|
|
@ -7,10 +7,10 @@ use color_eyre::Result;
|
||||||
use num_derive::{FromPrimitive, ToPrimitive};
|
use num_derive::{FromPrimitive, ToPrimitive};
|
||||||
use num_traits::{FromPrimitive as _, ToPrimitive as _};
|
use num_traits::{FromPrimitive as _, ToPrimitive as _};
|
||||||
|
|
||||||
|
use crate::binary;
|
||||||
use crate::binary::sync::{ReadExt, WriteExt};
|
use crate::binary::sync::{ReadExt, WriteExt};
|
||||||
|
|
||||||
const MAGIC_DDS: u32 = 0x20534444;
|
const MAGIC_DDS: u32 = 0x20534444;
|
||||||
pub const FOURCC_DX10: u32 = 0x30315844;
|
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
@ -75,27 +75,8 @@ bitflags! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||||
|
#[repr(u32)]
|
||||||
pub enum D3D10ResourceDimension {
|
pub enum D3D10ResourceDimension {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
Buffer = 1,
|
Buffer = 1,
|
||||||
|
@ -107,6 +88,7 @@ pub enum D3D10ResourceDimension {
|
||||||
#[allow(clippy::upper_case_acronyms)]
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
#[derive(Clone, Copy, Debug, strum::Display, FromPrimitive, ToPrimitive)]
|
#[derive(Clone, Copy, Debug, strum::Display, FromPrimitive, ToPrimitive)]
|
||||||
|
#[repr(u32)]
|
||||||
pub enum DXGIFormat {
|
pub enum DXGIFormat {
|
||||||
UNKNOWN = 0,
|
UNKNOWN = 0,
|
||||||
R32G32B32A32_TYPELESS = 1,
|
R32G32B32A32_TYPELESS = 1,
|
||||||
|
@ -243,7 +225,7 @@ pub struct Dx10Header {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dx10Header {
|
impl Dx10Header {
|
||||||
#[tracing::instrument(skip(r))]
|
#[tracing::instrument("Dx10Header::from_binary", skip(r))]
|
||||||
pub fn from_binary(mut r: impl ReadExt) -> Result<Self> {
|
pub fn from_binary(mut r: impl ReadExt) -> Result<Self> {
|
||||||
let dxgi_format = r
|
let dxgi_format = r
|
||||||
.read_u32()
|
.read_u32()
|
||||||
|
@ -251,7 +233,7 @@ impl Dx10Header {
|
||||||
let resource_dimension = r.read_u32().map(|val| {
|
let resource_dimension = r.read_u32().map(|val| {
|
||||||
D3D10ResourceDimension::from_u32(val).unwrap_or(D3D10ResourceDimension::Unknown)
|
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 array_size = r.read_u32()? as usize;
|
||||||
let misc_flags2 = r.read_u32()?;
|
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<()> {
|
pub fn to_binary(&self, mut w: impl WriteExt) -> Result<()> {
|
||||||
w.write_u32(
|
w.write_u32(
|
||||||
self.dxgi_format
|
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)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct DDSPixelFormat {
|
pub struct DDSPixelFormat {
|
||||||
pub flags: DDPF,
|
pub flags: DDPF,
|
||||||
pub four_cc: u32,
|
pub four_cc: FourCC,
|
||||||
pub rgb_bit_count: u32,
|
pub rgb_bit_count: u32,
|
||||||
pub r_bit_mask: u32,
|
pub r_bit_mask: u32,
|
||||||
pub g_bit_mask: u32,
|
pub g_bit_mask: u32,
|
||||||
|
@ -296,7 +298,7 @@ pub struct DDSPixelFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DDSPixelFormat {
|
impl DDSPixelFormat {
|
||||||
#[tracing::instrument(skip(r))]
|
#[tracing::instrument("DDSPixelFormat::from_binary", skip(r))]
|
||||||
pub fn from_binary(mut r: impl ReadExt) -> Result<Self> {
|
pub fn from_binary(mut r: impl ReadExt) -> Result<Self> {
|
||||||
let size = r.read_u32()? as usize;
|
let size = r.read_u32()? as usize;
|
||||||
eyre::ensure!(
|
eyre::ensure!(
|
||||||
|
@ -305,8 +307,17 @@ impl DDSPixelFormat {
|
||||||
size
|
size
|
||||||
);
|
);
|
||||||
|
|
||||||
let flags = r.read_u32().map(flags_from_bits)?;
|
let flags: DDPF = r.read_u32().map(binary::flags_from_bits)?;
|
||||||
let four_cc = r.read_u32()?;
|
|
||||||
|
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 rgb_bit_count = r.read_u32()?;
|
||||||
let r_bit_mask = r.read_u32()?;
|
let r_bit_mask = r.read_u32()?;
|
||||||
let g_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<()> {
|
pub fn to_binary(&self, mut w: impl WriteExt) -> Result<()> {
|
||||||
// Structure size
|
// Structure size
|
||||||
w.write_u32(32)?;
|
w.write_u32(32)?;
|
||||||
|
|
||||||
w.write_u32(self.flags.bits())?;
|
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.rgb_bit_count)?;
|
||||||
w.write_u32(self.r_bit_mask)?;
|
w.write_u32(self.r_bit_mask)?;
|
||||||
w.write_u32(self.g_bit_mask)?;
|
w.write_u32(self.g_bit_mask)?;
|
||||||
|
@ -356,7 +367,7 @@ pub struct DDSHeader {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DDSHeader {
|
impl DDSHeader {
|
||||||
#[tracing::instrument(skip(r))]
|
#[tracing::instrument("DDSHeader::from_binary", skip(r))]
|
||||||
pub fn from_binary(mut r: impl ReadExt) -> Result<Self> {
|
pub fn from_binary(mut r: impl ReadExt) -> Result<Self> {
|
||||||
r.skip_u32(MAGIC_DDS).wrap_err("Invalid magic bytes")?;
|
r.skip_u32(MAGIC_DDS).wrap_err("Invalid magic bytes")?;
|
||||||
|
|
||||||
|
@ -367,7 +378,7 @@ impl DDSHeader {
|
||||||
size
|
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 height = r.read_u32()? as usize;
|
||||||
let width = r.read_u32()? as usize;
|
let width = r.read_u32()? as usize;
|
||||||
let pitch_or_linear_size = 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))?;
|
r.seek(SeekFrom::Current(11 * 4))?;
|
||||||
|
|
||||||
let pixel_format = DDSPixelFormat::from_binary(&mut r)?;
|
let pixel_format = DDSPixelFormat::from_binary(&mut r)?;
|
||||||
let caps = r.read_u32().map(flags_from_bits)?;
|
let caps = r.read_u32().map(binary::flags_from_bits)?;
|
||||||
let caps_2 = r.read_u32().map(flags_from_bits)?;
|
let caps_2 = r.read_u32().map(binary::flags_from_bits)?;
|
||||||
|
|
||||||
// Skip unused and reserved bytes
|
// Skip unused and reserved bytes
|
||||||
r.seek(SeekFrom::Current(3 * 4))?;
|
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<()> {
|
pub fn to_binary(&self, mut w: impl WriteExt) -> Result<()> {
|
||||||
w.write_u32(MAGIC_DDS)?;
|
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 {
|
pub enum ImageType {
|
||||||
Image2D = 0,
|
Image2D = 0,
|
||||||
Image3D = 1,
|
Image3D = 1,
|
||||||
|
|
Loading…
Add table
Reference in a new issue