#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
use crate::error::{Error, Result};
use ddsfile::{D3DFormat, Dds, DxgiFormat, FourCC};
pub fn decode_dds_to_rgba(dds: &Dds) -> Result<Vec<u8>> {
if let Some(depth) = dds.header.depth {
if depth > 1 {
return Err(Error::DdsError(format!(
"3D/volume textures are not supported (depth={depth})"
)));
}
}
let width = dds.get_width() as usize;
let height = dds.get_height() as usize;
let data = dds
.get_data(0)
.map_err(|e| Error::DdsError(format!("Failed to read DDS data: {e}")))?;
if let Some(dxgi) = dds.get_dxgi_format() {
decode_dxgi_format(data, width, height, dxgi)
} else if let Some(d3d) = dds.get_d3d_format() {
decode_d3d_format(data, width, height, d3d)
} else if let Some(fourcc) = dds.header.spf.fourcc.as_ref() {
decode_fourcc(data, width, height, fourcc.0)
} else {
Err(Error::DdsError("Unknown DDS format".to_string()))
}
}
fn decode_dxgi_format(
data: &[u8],
width: usize,
height: usize,
format: DxgiFormat,
) -> Result<Vec<u8>> {
match format {
DxgiFormat::R8G8B8A8_UNorm | DxgiFormat::R8G8B8A8_UNorm_sRGB => Ok(data.to_vec()),
DxgiFormat::B8G8R8A8_UNorm | DxgiFormat::B8G8R8A8_UNorm_sRGB => {
let mut rgba = data.to_vec();
for chunk in rgba.chunks_exact_mut(4) {
chunk.swap(0, 2);
}
Ok(rgba)
}
DxgiFormat::BC1_UNorm | DxgiFormat::BC1_UNorm_sRGB => {
decode_bc(data, width, height, BcFormat::Bc1)
}
DxgiFormat::BC2_UNorm | DxgiFormat::BC2_UNorm_sRGB => {
decode_bc(data, width, height, BcFormat::Bc2)
}
DxgiFormat::BC3_UNorm | DxgiFormat::BC3_UNorm_sRGB => {
decode_bc(data, width, height, BcFormat::Bc3)
}
DxgiFormat::BC4_UNorm => decode_bc(data, width, height, BcFormat::Bc4),
DxgiFormat::BC5_UNorm => decode_bc(data, width, height, BcFormat::Bc5),
DxgiFormat::BC6H_UF16 => decode_bc6h(data, width, height, false), DxgiFormat::BC6H_SF16 => decode_bc6h(data, width, height, true), DxgiFormat::BC7_UNorm | DxgiFormat::BC7_UNorm_sRGB => {
decode_bc(data, width, height, BcFormat::Bc7)
}
_ => Err(Error::DdsError(format!(
"Unsupported DXGI format: {format:?}"
))),
}
}
fn decode_d3d_format(
data: &[u8],
width: usize,
height: usize,
format: D3DFormat,
) -> Result<Vec<u8>> {
match format {
D3DFormat::A8R8G8B8 => {
let mut rgba = Vec::with_capacity(data.len());
for chunk in data.chunks_exact(4) {
rgba.push(chunk[1]); rgba.push(chunk[2]); rgba.push(chunk[3]); rgba.push(chunk[0]); }
Ok(rgba)
}
D3DFormat::X8R8G8B8 => {
let mut rgba = Vec::with_capacity(data.len());
for chunk in data.chunks_exact(4) {
rgba.push(chunk[1]); rgba.push(chunk[2]); rgba.push(chunk[3]); rgba.push(255); }
Ok(rgba)
}
D3DFormat::R8G8B8 => {
let pixel_count = width * height;
let mut rgba = Vec::with_capacity(pixel_count * 4);
for chunk in data.chunks_exact(3) {
rgba.push(chunk[0]); rgba.push(chunk[1]); rgba.push(chunk[2]); rgba.push(255); }
Ok(rgba)
}
D3DFormat::DXT1 => decode_bc(data, width, height, BcFormat::Bc1),
D3DFormat::DXT2 | D3DFormat::DXT3 => decode_bc(data, width, height, BcFormat::Bc2),
D3DFormat::DXT4 | D3DFormat::DXT5 => decode_bc(data, width, height, BcFormat::Bc3),
_ => Err(Error::DdsError(format!(
"Unsupported D3D format: {format:?}"
))),
}
}
fn decode_fourcc(data: &[u8], width: usize, height: usize, fourcc: u32) -> Result<Vec<u8>> {
match fourcc {
FourCC::BC4_UNORM | FourCC::ATI1 => decode_bc(data, width, height, BcFormat::Bc4),
FourCC::BC4_SNORM => decode_bc(data, width, height, BcFormat::Bc4),
FourCC::BC5_UNORM => decode_bc(data, width, height, BcFormat::Bc5),
FourCC::BC5_SNORM => decode_bc(data, width, height, BcFormat::Bc5),
_ => {
let bytes = fourcc.to_le_bytes();
let fourcc_str: String = bytes.iter().map(|&b| b as char).collect();
Err(Error::DdsError(format!(
"Unsupported FourCC: {fourcc_str} (0x{fourcc:08X})"
)))
}
}
}
#[derive(Clone, Copy)]
enum BcFormat {
Bc1, Bc2, Bc3, Bc4, Bc5, Bc7, }
impl BcFormat {
const fn block_size(self) -> usize {
match self {
Self::Bc1 | Self::Bc4 => 8,
Self::Bc2 | Self::Bc3 | Self::Bc5 | Self::Bc7 => 16,
}
}
}
fn decode_bc(data: &[u8], width: usize, height: usize, format: BcFormat) -> Result<Vec<u8>> {
let mut rgba = vec![0u8; width * height * 4];
let blocks_x = width.div_ceil(4);
let blocks_y = height.div_ceil(4);
let block_size = format.block_size();
let mut block_rgba = [0u8; 64];
let block_pitch = 16;
for by in 0..blocks_y {
for bx in 0..blocks_x {
let block_idx = (by * blocks_x + bx) * block_size;
if block_idx + block_size > data.len() {
break;
}
let block = &data[block_idx..block_idx + block_size];
match format {
BcFormat::Bc1 => bcdec_rs::bc1(block, &mut block_rgba, block_pitch),
BcFormat::Bc2 => bcdec_rs::bc2(block, &mut block_rgba, block_pitch),
BcFormat::Bc3 => bcdec_rs::bc3(block, &mut block_rgba, block_pitch),
BcFormat::Bc4 => bcdec_rs::bc4(block, &mut block_rgba, block_pitch, false), BcFormat::Bc5 => bcdec_rs::bc5(block, &mut block_rgba, block_pitch, false), BcFormat::Bc7 => bcdec_rs::bc7(block, &mut block_rgba, block_pitch),
}
for py in 0..4 {
for px in 0..4 {
let fx = bx * 4 + px;
let fy = by * 4 + py;
if fx >= width || fy >= height {
continue;
}
let src_idx = (py * 4 + px) * 4;
let dst_idx = (fy * width + fx) * 4;
rgba[dst_idx..dst_idx + 4].copy_from_slice(&block_rgba[src_idx..src_idx + 4]);
}
}
}
}
Ok(rgba)
}
fn decode_bc6h(data: &[u8], width: usize, height: usize, signed: bool) -> Result<Vec<u8>> {
const BLOCK_SIZE: usize = 16;
let mut rgba = vec![0u8; width * height * 4];
let blocks_x = width.div_ceil(4);
let blocks_y = height.div_ceil(4);
let mut block_rgb = [0f32; 48];
let block_pitch = 4 * 3;
for by in 0..blocks_y {
for bx in 0..blocks_x {
let block_idx = (by * blocks_x + bx) * BLOCK_SIZE;
if block_idx + BLOCK_SIZE > data.len() {
break;
}
let block = &data[block_idx..block_idx + BLOCK_SIZE];
bcdec_rs::bc6h_float(block, &mut block_rgb, block_pitch, signed);
for py in 0..4 {
for px in 0..4 {
let fx = bx * 4 + px;
let fy = by * 4 + py;
if fx >= width || fy >= height {
continue;
}
let src_idx = (py * 4 + px) * 3;
let dst_idx = (fy * width + fx) * 4;
let r = tone_map_hdr(block_rgb[src_idx]);
let g = tone_map_hdr(block_rgb[src_idx + 1]);
let b = tone_map_hdr(block_rgb[src_idx + 2]);
rgba[dst_idx] = r;
rgba[dst_idx + 1] = g;
rgba[dst_idx + 2] = b;
rgba[dst_idx + 3] = 255; }
}
}
}
Ok(rgba)
}
#[allow(clippy::cast_sign_loss)] fn tone_map_hdr(value: f32) -> u8 {
let v = value.max(0.0);
let mapped = v / (1.0 + v);
let gamma_corrected = mapped.powf(1.0 / 2.2);
(gamma_corrected * 255.0).clamp(0.0, 255.0) as u8
}