From 3da2aa39c18667abdf1f83bcf3419e18ccd6404a Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Mon, 10 Mar 2025 17:15:14 -0400 Subject: [PATCH] Vendor what we need from texture2ddecoder This recently tripped up cargo-deny because it depends on paste. I looked at it and the parts that we need (a few BCn decoding functions) has only one place where it's used and can easily be replaced. --- Cargo.toml | 6 +-- README.md | 1 + src/bcn/bc1.rs | 50 +++++++++++++++++++++ src/bcn/bc3.rs | 40 +++++++++++++++++ src/bcn/bc5.rs | 10 +++++ src/bcn/color.rs | 110 ++++++++++++++++++++++++++++++++++++++++++++++ src/bcn/macros.rs | 46 +++++++++++++++++++ src/bcn/mod.rs | 18 ++++++++ src/lib.rs | 2 + src/tex.rs | 4 +- 10 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 src/bcn/bc1.rs create mode 100644 src/bcn/bc3.rs create mode 100644 src/bcn/bc5.rs create mode 100644 src/bcn/color.rs create mode 100644 src/bcn/macros.rs create mode 100644 src/bcn/mod.rs diff --git a/Cargo.toml b/Cargo.toml index b0e2618..304b783 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ game_install = [] # enables support for extracting visual data, such as models, textures, materials, etc. # this enables a bunch of dependencies! # tip: can be safely turned off for launchers and other tools that simply need to extract the bare minimum of data -visual_data = ["dep:half", "dep:bitflags", "dep:texture2ddecoder"] +visual_data = ["dep:half", "dep:bitflags"] # testing only features retail_game_testing = [] @@ -69,5 +69,5 @@ half = { version = "2", optional = true } # cannot upgrade to 2.0.0, breaking changes that aren't recoverable: https://github.com/bitflags/bitflags/issues/314 bitflags = { version = "1.3", optional = true } -# needed for dxt/bc decompression -texture2ddecoder = { version = "0.1", optional = true } +# used in bcn decoding +quote = "1.0" diff --git a/README.md b/README.md index cb76fd8..2a6a258 100755 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ Feel free to submit patches to help fix bugs or add functionality. Filing issues - [binrw team](https://binrw.rs) for an awesome Rust library! - [sha1-smol](https://github.com/mitsuhiko/sha1-smol) for a dependency-free SHA1 implementation * [FFXIVTools](https://github.com/dlunch/FFXIVTools) for it's Havok parsing implementation +* [texture2ddecoder](https://github.com/UniversalGameExtraction/texture2ddecoder/) for it's BCn texture decoding functions. And everyone else who writes FFXIV tools! diff --git a/src/bcn/bc1.rs b/src/bcn/bc1.rs new file mode 100644 index 0000000..1833142 --- /dev/null +++ b/src/bcn/bc1.rs @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Rudolf Kolbe +// SPDX-License-Identifier: MIT + +use super::color::{color, rgb565_le}; + +#[inline] +pub fn decode_bc1_block(data: &[u8], outbuf: &mut [u32]) { + let q0 = u16::from_le_bytes([data[0], data[1]]); + let q1 = u16::from_le_bytes([data[2], data[3]]); + let (r0, g0, b0) = rgb565_le(q0); + let (r1, g1, b1) = rgb565_le(q1); + + let mut c: [u32; 4] = [color(r0, g0, b0, 255), color(r1, g1, b1, 255), 0, 0]; + + // C insanity..... + let r0 = r0 as u16; + let g0 = g0 as u16; + let b0 = b0 as u16; + let r1 = r1 as u16; + let g1 = g1 as u16; + let b1 = b1 as u16; + + if q0 > q1 { + c[2] = color( + ((r0 * 2 + r1) / 3) as u8, + ((g0 * 2 + g1) / 3) as u8, + ((b0 * 2 + b1) / 3) as u8, + 255, + ); + c[3] = color( + ((r0 + r1 * 2) / 3) as u8, + ((g0 + g1 * 2) / 3) as u8, + ((b0 + b1 * 2) / 3) as u8, + 255, + ); + } else { + c[2] = color( + ((r0 + r1) / 2) as u8, + ((g0 + g1) / 2) as u8, + ((b0 + b1) / 2) as u8, + 255, + ); + c[3] = color(0, 0, 0, 255); + } + let mut d: usize = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize; + (0..16).for_each(|i| { + outbuf[i] = c[d & 3]; + d >>= 2; + }); +} diff --git a/src/bcn/bc3.rs b/src/bcn/bc3.rs new file mode 100644 index 0000000..1c114b1 --- /dev/null +++ b/src/bcn/bc3.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Rudolf Kolbe +// SPDX-License-Identifier: MIT + +use super::bc1::decode_bc1_block; + +#[inline] +pub fn decode_bc3_alpha(data: &[u8], outbuf: &mut [u32], channel: usize) { + // use u16 to avoid overflow and replicate equivalent behavior to C++ code + let mut a: [u16; 8] = [data[0] as u16, data[1] as u16, 0, 0, 0, 0, 0, 0]; + if a[0] > a[1] { + a[2] = (a[0] * 6 + a[1]) / 7; + a[3] = (a[0] * 5 + a[1] * 2) / 7; + a[4] = (a[0] * 4 + a[1] * 3) / 7; + a[5] = (a[0] * 3 + a[1] * 4) / 7; + a[6] = (a[0] * 2 + a[1] * 5) / 7; + a[7] = (a[0] + a[1] * 6) / 7; + } else { + a[2] = (a[0] * 4 + a[1]) / 5; + a[3] = (a[0] * 3 + a[1] * 2) / 5; + a[4] = (a[0] * 2 + a[1] * 3) / 5; + a[5] = (a[0] + a[1] * 4) / 5; + a[6] = 0; + a[7] = 255; + } + + let mut d: usize = (u64::from_le_bytes(data[..8].try_into().unwrap()) >> 16) as usize; + + let channel_shift = channel * 8; + let channel_mask = 0xFFFFFFFF ^ (0xFF << channel_shift); + outbuf.iter_mut().for_each(|p| { + *p = (*p & channel_mask) | (a[d & 7] as u32) << channel_shift; + d >>= 3; + }); +} + +#[inline] +pub fn decode_bc3_block(data: &[u8], outbuf: &mut [u32]) { + decode_bc1_block(&data[8..], outbuf); + decode_bc3_alpha(data, outbuf, 3); +} diff --git a/src/bcn/bc5.rs b/src/bcn/bc5.rs new file mode 100644 index 0000000..2b9f5f3 --- /dev/null +++ b/src/bcn/bc5.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023 Rudolf Kolbe +// SPDX-License-Identifier: MIT + +use super::bc3::decode_bc3_alpha; + +#[inline] +pub fn decode_bc5_block(data: &[u8], outbuf: &mut [u32]) { + decode_bc3_alpha(data, outbuf, 2); + decode_bc3_alpha(&data[8..], outbuf, 1); +} diff --git a/src/bcn/color.rs b/src/bcn/color.rs new file mode 100644 index 0000000..0cf22d9 --- /dev/null +++ b/src/bcn/color.rs @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2023 Rudolf Kolbe +// SPDX-License-Identifier: MIT + +#![allow(clippy::too_many_arguments)] + +pub static TRANSPARENT_MASK: u32 = { + #[cfg(target_endian = "little")] + { + 0x00ffffff + } + #[cfg(target_endian = "big")] + { + 0xffffff00 + } +}; + +pub static TRANSPARENT_SHIFT: u32 = { + #[cfg(target_endian = "little")] + { + 24 + } + #[cfg(target_endian = "big")] + { + 0 + } +}; + +#[inline] +pub const fn color(r: u8, g: u8, b: u8, a: u8) -> u32 { + u32::from_le_bytes([b, g, r, a]) +} + +// #[cfg(target_endian = "little")] +// #[inline] +// pub fn alpha_mask(a: u8) -> u32 { +// TRANSPARENT_MASK | (a as u32) << 24 +// } + +// #[cfg(target_endian = "big")] +// #[inline] +// pub fn alpha_mask(a: u8) -> u32 { +// TRANSPARENT_MASK | a as u32 +// } + +// #[cfg(target_endian = "little")] +#[inline] +pub const fn rgb565_le(d: u16) -> (u8, u8, u8) { + ( + (d >> 8 & 0xf8) as u8 | (d >> 13) as u8, + (d >> 3 & 0xfc) as u8 | (d >> 9 & 3) as u8, + (d << 3) as u8 | (d >> 2 & 7) as u8, + ) +} + +// #[cfg(target_endian = "big")] +// #[inline] +// pub fn rgb565_le(d: u16) -> (u8, u8, u8) { +// ( +// (d & 0xf8) as u8 | (d >> 5 & 7) as u8, +// (d << 5 & 0xe0) as u8 | (d >> 11 & 0x1c) as u8 | (d >> 1 & 3) as u8, +// (d >> 5 & 0xf8) as u8 | (d >> 10 & 0x7) as u8, +// ) +// } + +// #[cfg(target_endian = "little")] +// #[inline] +// pub fn rgb565_be(d: u16) -> (u8, u8, u8) { +// ( +// (d & 0xf8) as u8 | (d >> 5 & 7) as u8, +// (d << 5 & 0xe0) as u8 | (d >> 11 & 0x1c) as u8 | (d >> 1 & 3) as u8, +// (d >> 5 & 0xf8) as u8 | (d >> 10 & 0x7) as u8, +// ) +// } + +// #[cfg(target_endian = "big")] +// #[inline] +// pub fn rgb565_be(d: u16) -> (u8, u8, u8) { +// ( +// (d >> 8 & 0xf8) as u8 | (d >> 13) as u8, +// (d >> 3 & 0xfc) as u8 | (d >> 9 & 3) as u8, +// (d << 3) as u8 | (d >> 2 & 7) as u8, +// ) +// } + +#[inline] +pub fn copy_block_buffer( + bx: usize, + by: usize, + w: usize, + h: usize, + bw: usize, + bh: usize, + buffer: &[u32], + image: &mut [u32], +) { + let x: usize = bw * bx; + let copy_width: usize = if bw * (bx + 1) > w { w - bw * bx } else { bw }; + + let y_0 = by * bh; + let copy_height: usize = if bh * (by + 1) > h { h - y_0 } else { bh }; + let mut buffer_offset = 0; + + for y in y_0..y_0 + copy_height { + let image_offset = y * w + x; + image[image_offset..image_offset + copy_width] + .copy_from_slice(&buffer[buffer_offset..buffer_offset + copy_width]); + + buffer_offset += bw; + } +} diff --git a/src/bcn/macros.rs b/src/bcn/macros.rs new file mode 100644 index 0000000..3d827d0 --- /dev/null +++ b/src/bcn/macros.rs @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Rudolf Kolbe +// SPDX-License-Identifier: MIT + +// macro to generate generic block decoder functions +macro_rules! block_decoder{ + ($name: ident, $block_width: expr, $block_height: expr, $raw_block_size: expr, $block_decode_func: expr) => { + //#[doc = "Decodes a " $name " encoded texture into an image"] + pub fn $name(data: &[u8], width: usize, height: usize, image: &mut [u32]) -> Result<(), &'static str> { + const BLOCK_WIDTH: usize = $block_width; + const BLOCK_HEIGHT: usize = $block_height; + const BLOCK_SIZE: usize = BLOCK_WIDTH * BLOCK_HEIGHT; + let num_blocks_x: usize = (width + BLOCK_WIDTH - 1) / BLOCK_WIDTH; + let num_blocks_y: usize = (height + BLOCK_WIDTH - 1) / BLOCK_HEIGHT; + let mut buffer: [u32; BLOCK_SIZE] = [crate::bcn::color::color(0,0,0,255); BLOCK_SIZE]; + + if data.len() < num_blocks_x * num_blocks_y * $raw_block_size { + return Err("Not enough data to decode image!"); + } + + if image.len() < width * height { + return Err("Image buffer is too small!"); + } + + let mut data_offset = 0; + (0..num_blocks_y).for_each(|by| { + (0..num_blocks_x).for_each(|bx| { + $block_decode_func(&data[data_offset..], &mut buffer); + crate::bcn::color::copy_block_buffer( + bx, + by, + width, + height, + BLOCK_WIDTH, + BLOCK_HEIGHT, + &buffer, + image, + ); + data_offset += $raw_block_size; + }); + }); + Ok(()) + } + }; +} + +pub(crate) use block_decoder; diff --git a/src/bcn/mod.rs b/src/bcn/mod.rs new file mode 100644 index 0000000..811cd01 --- /dev/null +++ b/src/bcn/mod.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Rudolf Kolbe +// SPDX-License-Identifier: MIT + +extern crate alloc; + +mod bc1; +mod bc3; +mod bc5; +mod color; +mod macros; + +pub use bc1::decode_bc1_block; +pub use bc3::decode_bc3_block; +pub use bc5::decode_bc5_block; + +macros::block_decoder!(decode_bc1, 4, 4, 8, decode_bc1_block); +macros::block_decoder!(decode_bc3, 4, 4, 16, decode_bc3_block); +macros::block_decoder!(decode_bc5, 4, 4, 16, decode_bc5_block); diff --git a/src/lib.rs b/src/lib.rs index 0dec4ac..ff81644 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,3 +171,5 @@ pub mod patchlist; /// Reading SQDB files pub mod sqdb; + +mod bcn; diff --git a/src/tex.rs b/src/tex.rs index 132281f..4334f0f 100644 --- a/src/tex.rs +++ b/src/tex.rs @@ -5,11 +5,13 @@ use std::io::{Cursor, Read, Seek, SeekFrom}; +use crate::bcn::decode_bc1; +use crate::bcn::decode_bc3; +use crate::bcn::decode_bc5; use crate::ByteSpan; use binrw::BinRead; use binrw::binrw; use bitflags::bitflags; -use texture2ddecoder::{decode_bc1, decode_bc3, decode_bc5}; // Attributes and Format are adapted from Lumina (https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Data/Files/TexFile.cs) bitflags! {