// =============================================================================
// DDS Parsing and Decoding
// =============================================================================
#[derive(Copy, Clone)]
enum DdsFormat {
Dxt1,
Dxt3,
Dxt5,
Rgba8888,
}
#[derive(Copy, Clone)]
struct DdsInfo {
width: i32,
height: i32,
mip_count: i32,
format: DdsFormat,
data_offset: usize,
rgba_masks: (u32, u32, u32, u32),
pitch: usize,
}
fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
if offset + 4 > data.len() {
return None;
}
Some(u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]))
}
fn parse_dds(data: &[u8]) -> Option<DdsInfo> {
if data.len() < 128 {
return None;
}
if &data[0..4] != b"DDS " {
return None;
}
let height = read_u32_le(data, 12)? as i32;
let width = read_u32_le(data, 16)? as i32;
let mip_count = read_u32_le(data, 28)? as i32;
let ddspf_offset = 76;
let ddspf_flags = read_u32_le(data, ddspf_offset + 4)?;
let fourcc = read_u32_le(data, ddspf_offset + 8)?;
let rgb_bits = read_u32_le(data, ddspf_offset + 12)?;
let r_mask = read_u32_le(data, ddspf_offset + 16)?;
let g_mask = read_u32_le(data, ddspf_offset + 20)?;
let b_mask = read_u32_le(data, ddspf_offset + 24)?;
let a_mask = read_u32_le(data, ddspf_offset + 28)?;
let flags = read_u32_le(data, 8)?;
let pitch_or_linear = read_u32_le(data, 20)? as usize;
let (format, is_dx10) = match fourcc {
0x31545844 => (DdsFormat::Dxt1, false), // "DXT1"
0x33545844 => (DdsFormat::Dxt3, false), // "DXT3"
0x35545844 => (DdsFormat::Dxt5, false), // "DXT5"
0x30315844 => {
// "DX10" - extended header, need to read DXGI format
if data.len() < 148 {
return None;
}
let dxgi_format = read_u32_le(data, 128)?;
let fmt = match dxgi_format {
71 => DdsFormat::Dxt1, // DXGI_FORMAT_BC1_UNORM
72 => DdsFormat::Dxt1, // DXGI_FORMAT_BC1_UNORM_SRGB
74 => DdsFormat::Dxt3, // DXGI_FORMAT_BC2_UNORM
75 => DdsFormat::Dxt3, // DXGI_FORMAT_BC2_UNORM_SRGB
77 => DdsFormat::Dxt5, // DXGI_FORMAT_BC3_UNORM
78 => DdsFormat::Dxt5, // DXGI_FORMAT_BC3_UNORM_SRGB
_ => return None,
};
(fmt, true)
}
_ => {
let has_rgb = (ddspf_flags & 0x40) != 0;
if has_rgb && rgb_bits == 32 {
(DdsFormat::Rgba8888, false)
} else {
return None;
}
}
};
let pitch = if matches!(format, DdsFormat::Rgba8888) && (flags & 0x8) != 0 && pitch_or_linear > 0 {
pitch_or_linear
} else {
(width as usize) * 4
};
// DX10 header adds 20 bytes after the standard 128-byte header
let data_offset = if is_dx10 { 148 } else { 128 };
Some(DdsInfo {
width,
height,
mip_count: if mip_count > 0 { mip_count } else { 1 },
format,
data_offset,
rgba_masks: (r_mask, g_mask, b_mask, a_mask),
pitch,
})
}
fn mask_to_shift_and_bits(mask: u32) -> Option<(u32, u32)> {
if mask == 0 {
return None;
}
let shift = mask.trailing_zeros();
let bits = mask.count_ones();
Some((shift, bits))
}
fn expand_bits_to_u8(value: u32, bits: u32) -> u8 {
if bits == 0 {
return 0;
}
if bits >= 8 {
return value as u8;
}
let max = (1u32 << bits) - 1;
((value * 255 + (max / 2)) / max) as u8
}
fn decode_dds_rgba(info: DdsInfo, data: &[u8], mip_index: i32) -> Option<(Vec<u8>, i32, i32)> {
if mip_index < 0 {
return None;
}
let mip = mip_index as usize;
if mip >= info.mip_count as usize {
return None;
}
let mut width = info.width as usize;
let mut height = info.height as usize;
let mut offset = info.data_offset;
let block_size = match info.format {
DdsFormat::Dxt1 => 8,
DdsFormat::Dxt3 | DdsFormat::Dxt5 => 16,
DdsFormat::Rgba8888 => 4,
};
let mut pitch = info.pitch;
for _ in 0..mip {
let size = match info.format {
DdsFormat::Rgba8888 => pitch.saturating_mul(height),
_ => {
let blocks_w = (width.max(1) + 3) / 4;
let blocks_h = (height.max(1) + 3) / 4;
blocks_w * blocks_h * block_size
}
};
offset = offset.saturating_add(size);
width = (width / 2).max(1);
height = (height / 2).max(1);
pitch = width * 4;
}
let size = match info.format {
DdsFormat::Rgba8888 => pitch.saturating_mul(height),
_ => {
let blocks_w = (width.max(1) + 3) / 4;
let blocks_h = (height.max(1) + 3) / 4;
blocks_w * blocks_h * block_size
}
};
if offset + size > data.len() {
return None;
}
let mip_data = &data[offset..offset + size];
let decoded = match info.format {
DdsFormat::Dxt1 => decode_texture(
CodecTextureEncoding::Dxt1,
mip_data,
width as u32,
height as u32,
)
.ok()?,
DdsFormat::Dxt3 => decode_texture(
CodecTextureEncoding::Dxt3,
mip_data,
width as u32,
height as u32,
)
.ok()?,
DdsFormat::Dxt5 => decode_texture(
CodecTextureEncoding::Dxt5,
mip_data,
width as u32,
height as u32,
)
.ok()?,
DdsFormat::Rgba8888 => {
let (r_mask, g_mask, b_mask, a_mask) = info.rgba_masks;
let r_info = mask_to_shift_and_bits(r_mask);
let g_info = mask_to_shift_and_bits(g_mask);
let b_info = mask_to_shift_and_bits(b_mask);
let a_info = mask_to_shift_and_bits(a_mask);
let row_bytes = width * 4;
let mut out = vec![0u8; width * height * 4];
for y in 0..height {
let src_row_start = y * pitch;
let dst_row_start = y * row_bytes;
for x in 0..width {
let src_offset = src_row_start + x * 4;
let dst_offset = dst_row_start + x * 4;
let value = u32::from_le_bytes([
mip_data[src_offset],
mip_data[src_offset + 1],
mip_data[src_offset + 2],
mip_data[src_offset + 3],
]);
let r = r_info
.map(|(shift, bits)| expand_bits_to_u8((value & r_mask) >> shift, bits))
.unwrap_or(0);
let g = g_info
.map(|(shift, bits)| expand_bits_to_u8((value & g_mask) >> shift, bits))
.unwrap_or(0);
let b = b_info
.map(|(shift, bits)| expand_bits_to_u8((value & b_mask) >> shift, bits))
.unwrap_or(0);
let a = a_info
.map(|(shift, bits)| expand_bits_to_u8((value & a_mask) >> shift, bits))
.unwrap_or(255);
out[dst_offset] = r;
out[dst_offset + 1] = g;
out[dst_offset + 2] = b;
out[dst_offset + 3] = a;
}
}
out
}
};
Some((decoded, width as i32, height as i32))
}
#[no_mangle]
pub unsafe extern "C" fn GrannyRsGetDdsInfo(
data: *const UInt8,
data_len: Int32,
out_width: *mut Int32,
out_height: *mut Int32,
out_mip_count: *mut Int32,
out_format: *mut Int32,
) -> Bool32 {
if data.is_null()
|| data_len <= 0
|| out_width.is_null()
|| out_height.is_null()
|| out_mip_count.is_null()
|| out_format.is_null()
{
return 0;
}
let slice = core::slice::from_raw_parts(data, data_len as usize);
let info = match parse_dds(slice) {
Some(info) => info,
None => return 0,
};
*out_width = info.width;
*out_height = info.height;
*out_mip_count = info.mip_count;
*out_format = match info.format {
DdsFormat::Dxt1 => 1,
DdsFormat::Dxt3 => 2,
DdsFormat::Dxt5 => 3,
DdsFormat::Rgba8888 => 4,
};
1
}
#[no_mangle]
pub unsafe extern "C" fn GrannyRsDecodeDdsRgba(
data: *const UInt8,
data_len: Int32,
mip_index: Int32,
out_pixels: *mut UInt8,
out_len: Int32,
) -> Int32 {
if data.is_null() || data_len <= 0 || out_pixels.is_null() || out_len <= 0 {
return 0;
}
let slice = core::slice::from_raw_parts(data, data_len as usize);
let info = match parse_dds(slice) {
Some(info) => info,
None => return 0,
};
let (decoded, _, _) = match decode_dds_rgba(info, slice, mip_index) {
Some(result) => result,
None => return 0,
};
if decoded.len() > out_len as usize {
return 0;
}
core::ptr::copy_nonoverlapping(decoded.as_ptr(), out_pixels, decoded.len());
decoded.len() as Int32
}