From 854fd8dfd3ac3ef07b283a59273e41336c21205e Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Fri, 10 Jul 2026 22:13:37 +0200 Subject: [PATCH] fix(texture): make BC dword-swap format-aware (don't mangle DXT5A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dword-swap that fixes DXT1 is a property of the BC1 COLOUR block, not of BCn in general. RE on a DXT5A (BC4) mask showed the alpha block is byte-indexed and wants the endian swap ALONE — dword-swapping it makes it worse. The blanket swap from the previous commit was therefore breaking the (common) DXT5A/DXN alpha & normal masks. Now targeted by format: - DXT1 : swap dwords of each 8-byte colour block (verified). - DXT2_3 / DXT4_5 : swap only the colour half (bytes 8..16) of each 16-byte block; alpha half endian-only. TENTATIVE — these formats are near-absent in the game assets, so unvalidated against a clear image. - DXT5A / DXN : no dword swap (endian only). DXT1 export re-verified clean (weapon skins); 16 formats tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/sylpheed-formats/src/texture.rs | 48 +++++++++++++++++--------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/crates/sylpheed-formats/src/texture.rs b/crates/sylpheed-formats/src/texture.rs index 5c4c8e7..9952a49 100644 --- a/crates/sylpheed-formats/src/texture.rs +++ b/crates/sylpheed-formats/src/texture.rs @@ -313,18 +313,33 @@ impl X360Texture { apply_endian_swap(&mut linear_data, endianness); } - // X360 stores BCn (DXT) blocks with the two 32-bit words of each 64-bit - // sub-block in swapped order relative to the PC/DDS layout: the color - // endpoints live in the *high* dword, the indices in the low one. The - // endian field (k8in16) only fixes byte order *within* the 16-bit words; - // it does not reorder the dwords. Without this swap the endpoints and - // indices are transposed and every DXT texture decodes to noise. This is - // an intrinsic property of the format's guest storage, so it is applied - // for every BCn texture regardless of the endian field. - // (Discovered by RE: de-tiled DXT1 blocks had coherent endpoints only in - // bytes[4..8], with the high-entropy indices in bytes[0..4].) - if format.is_block_compressed() && std::env::var("XPR_NO_BC_DWORD_SWAP").is_err() { - swap_bc_block_dwords(&mut linear_data); + // X360 stores the BC1 *colour* block with its two 32-bit words swapped + // relative to the PC/DDS layout — the colour endpoints live in the high + // dword, the indices in the low one. The endian field (k8in16) only + // fixes byte order *within* the 16-bit words; it does not reorder the + // dwords, so without this the endpoints/indices are transposed and the + // texture decodes to noise. (Discovered by RE: de-tiled DXT1 blocks had + // coherent endpoints only in bytes[4..8].) + // + // This applies to the colour block only. The BC4-style alpha block + // (DXT5A, and the alpha half of DXT2_3/DXT4_5) is byte-indexed and needs + // the endian swap alone — RE-confirmed: dword-swapping DXT5A makes it + // *worse*. So the swap is format-targeted, not blanket. + if std::env::var("XPR_NO_BC_DWORD_SWAP").is_err() { + match format { + // Pure colour block, 8 bytes. + X360TextureFormat::Dxt1 => swap_bc_block_dwords(&mut linear_data), + // 16-byte block = [alpha 8B][colour 8B]; swap only the colour + // half. TENTATIVE: DXT2_3/DXT4_5 are near-absent in the game's + // assets, so this half-swap is unvalidated against a clear image. + X360TextureFormat::Dxt3 | X360TextureFormat::Dxt5 => { + for block in linear_data.chunks_exact_mut(16) { + swap_bc_block_dwords(&mut block[8..16]); + } + } + // BC4 / BC5 (DXT5A / DXN): alpha/normal blocks, endian swap only. + _ => {} + } } Ok(X360Texture { width, height, format, mip_levels: mip_count, is_cubemap, data: linear_data }) @@ -375,12 +390,11 @@ pub fn apply_endian_swap(data: &mut [u8], endianness: u8) { } } -/// Swap the two 32-bit dwords within each 64-bit sub-block, in place. +/// Swap the two 32-bit dwords within each 64-bit unit of `data`, in place. /// -/// Xbox 360 BCn (DXT) textures store each 64-bit block-half with its two 32-bit -/// words in the opposite order to the PC/DDS layout. For BC1/DXT5A (8-byte -/// blocks) this puts the colour endpoints ahead of the indices; for BC2/BC3/DXN -/// (16-byte blocks) it fixes both the alpha half and the colour half. Apply +/// Xbox 360 stores the BC1 *colour* block with its two 32-bit words in the +/// opposite order to the PC/DDS layout, putting the colour endpoints ahead of +/// the indices. Apply to a BC1 buffer (or the colour half of a BC2/BC3 block) /// after the byte-level endian swap. Any trailing bytes that don't fill a full /// 8-byte group are left untouched. pub fn swap_bc_block_dwords(data: &mut [u8]) {