use crate::decode::{ByteReader, checked_product};
use crate::math::ceil;
use alloc::format;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
pub const MAX_MIP_LEVELS: usize = 32;
pub(crate) const TEXTURE_PAYLOAD_MAGIC: u32 = u32::from_le_bytes(*b"TEX2");
const HEADER_BYTES: usize = 12;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TextureFormat {
Rgba8,
Bc1,
Bc3,
Bc5,
Bc7,
}
impl TextureFormat {
pub fn id(self) -> u32 {
match self {
TextureFormat::Rgba8 => 0,
TextureFormat::Bc1 => 1,
TextureFormat::Bc3 => 2,
TextureFormat::Bc5 => 3,
TextureFormat::Bc7 => 4,
}
}
pub(crate) fn from_id(id: u32) -> Option<Self> {
match id {
0 => Some(TextureFormat::Rgba8),
1 => Some(TextureFormat::Bc1),
2 => Some(TextureFormat::Bc3),
3 => Some(TextureFormat::Bc5),
4 => Some(TextureFormat::Bc7),
_ => None,
}
}
pub fn block_bytes(self) -> Option<usize> {
match self {
TextureFormat::Rgba8 => None,
TextureFormat::Bc1 => Some(8),
TextureFormat::Bc3 | TextureFormat::Bc5 | TextureFormat::Bc7 => Some(16),
}
}
pub fn mip_byte_len(self, width: u32, height: u32) -> Result<usize, String> {
match self.block_bytes() {
None => checked_product("texture mip", &[width as usize, height as usize, 4]),
Some(block) => checked_product(
"texture mip",
&[
width.div_ceil(4) as usize,
height.div_ceil(4) as usize,
block,
],
),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TextureMip {
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TextureImage {
pub format: TextureFormat,
pub mips: Vec<TextureMip>,
}
impl TextureImage {
pub fn rgba8(width: u32, height: u32, pixels: Vec<u8>) -> Self {
TextureImage {
format: TextureFormat::Rgba8,
mips: vec![TextureMip {
width,
height,
data: pixels,
}],
}
}
pub fn width(&self) -> u32 {
self.mips.first().map(|m| m.width).unwrap_or(0)
}
pub fn height(&self) -> u32 {
self.mips.first().map(|m| m.height).unwrap_or(0)
}
pub fn byte_len(&self) -> usize {
self.mips.iter().map(|m| m.data.len()).sum()
}
pub fn into_rgba8(self) -> Result<(u32, u32, Vec<u8>), String> {
if self.format != TextureFormat::Rgba8 {
return Err(format!(
"texture is {:?}, expected RGBA8 for this path",
self.format
));
}
let mip = self
.mips
.into_iter()
.next()
.ok_or("RGBA8 texture has no mip level")?;
Ok((mip.width, mip.height, mip.data))
}
}
pub fn serialise(image: &TextureImage) -> Vec<u8> {
let total: usize = HEADER_BYTES + image.mips.iter().map(|m| 12 + m.data.len()).sum::<usize>();
let mut buf = Vec::with_capacity(total);
buf.extend_from_slice(&TEXTURE_PAYLOAD_MAGIC.to_le_bytes());
buf.extend_from_slice(&image.format.id().to_le_bytes());
buf.extend_from_slice(&(image.mips.len() as u32).to_le_bytes());
for mip in &image.mips {
buf.extend_from_slice(&mip.width.to_le_bytes());
buf.extend_from_slice(&mip.height.to_le_bytes());
buf.extend_from_slice(&(mip.data.len() as u32).to_le_bytes());
buf.extend_from_slice(&mip.data);
}
buf
}
pub fn deserialise(bytes: &[u8]) -> Result<TextureImage, String> {
let mut r = ByteReader::open_payload(bytes, TEXTURE_PAYLOAD_MAGIC, HEADER_BYTES, "texture")?;
let format_id = r.u32()?;
let format = TextureFormat::from_id(format_id)
.ok_or_else(|| format!("texture payload has unknown format_id {}", format_id))?;
let mip_count = r.u32()? as usize;
if mip_count == 0 || mip_count > MAX_MIP_LEVELS {
return Err(format!(
"texture payload declares {} mip levels (expected 1..={})",
mip_count, MAX_MIP_LEVELS
));
}
r.seek(HEADER_BYTES)?;
let mut mips = Vec::with_capacity(mip_count);
for level in 0..mip_count {
let width = r.u32()?;
let height = r.u32()?;
let byte_len = r.u32()? as usize;
let expected = format.mip_byte_len(width, height)?;
if byte_len != expected {
return Err(format!(
"texture payload mip {} ({}x{} {:?}) declares {} bytes, format needs {}",
level, width, height, format, byte_len, expected
));
}
mips.push(TextureMip {
width,
height,
data: r.take(byte_len)?.to_vec(),
});
}
Ok(TextureImage { format, mips })
}
pub fn downscale_rgba(
width: u32,
height: u32,
pixels: Vec<u8>,
max_size: u32,
) -> (u32, u32, Vec<u8>) {
if max_size == 0 || (width <= max_size && height <= max_size) {
return (width, height, pixels);
}
let scale = ceil(width.max(height) as f32 / max_size as f32) as u32;
let scale = scale.max(2);
let dst_w = (width / scale).max(1);
let dst_h = (height / scale).max(1);
let mut out = vec![0u8; (dst_w * dst_h * 4) as usize];
for dy in 0..dst_h {
for dx in 0..dst_w {
let mut acc = [0u32; 4];
let mut n = 0u32;
for sy in 0..scale {
let src_y = dy * scale + sy;
if src_y >= height {
break;
}
for sx in 0..scale {
let src_x = dx * scale + sx;
if src_x >= width {
break;
}
let si = ((src_y * width + src_x) * 4) as usize;
for c in 0..4 {
acc[c] += pixels[si + c] as u32;
}
n += 1;
}
}
let di = ((dy * dst_w + dx) * 4) as usize;
for c in 0..4 {
out[di + c] = acc[c].checked_div(n).unwrap_or(0) as u8;
}
}
}
(dst_w, dst_h, out)
}
#[cfg(test)]
mod tests {
use super::*;
fn round_trip(image: &TextureImage) -> TextureImage {
let bytes = serialise(image);
deserialise(&bytes).expect("deserialise")
}
#[test]
fn rgba8_single_mip_round_trips() {
let image = TextureImage::rgba8(2, 1, vec![1, 2, 3, 4, 5, 6, 7, 8]);
let back = round_trip(&image);
assert_eq!(back, image);
assert_eq!(back.format, TextureFormat::Rgba8);
assert_eq!((back.width(), back.height()), (2, 1));
}
#[test]
fn compressed_multi_mip_round_trips() {
let image = TextureImage {
format: TextureFormat::Bc1,
mips: vec![
TextureMip {
width: 4,
height: 4,
data: vec![0xAB; 8],
},
TextureMip {
width: 2,
height: 2,
data: vec![0xCD; 8],
},
],
};
let back = round_trip(&image);
assert_eq!(back, image);
assert_eq!(back.byte_len(), 16);
}
#[test]
fn deserialise_rejects_bad_magic() {
let mut bytes = serialise(&TextureImage::rgba8(1, 1, vec![0, 0, 0, 0]));
bytes[0] ^= 0xFF;
let err = deserialise(&bytes).unwrap_err();
assert!(err.contains("magic"), "got: {err}");
}
#[test]
fn deserialise_rejects_unknown_format() {
let mut bytes = serialise(&TextureImage::rgba8(1, 1, vec![0, 0, 0, 0]));
bytes[4..8].copy_from_slice(&99u32.to_le_bytes());
let err = deserialise(&bytes).unwrap_err();
assert!(err.contains("unknown format_id"), "got: {err}");
}
#[test]
fn deserialise_rejects_wrong_mip_length() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&TEXTURE_PAYLOAD_MAGIC.to_le_bytes());
bytes.extend_from_slice(&TextureFormat::Bc7.id().to_le_bytes());
bytes.extend_from_slice(&1u32.to_le_bytes());
bytes.extend_from_slice(&4u32.to_le_bytes());
bytes.extend_from_slice(&4u32.to_le_bytes());
bytes.extend_from_slice(&8u32.to_le_bytes());
bytes.extend_from_slice(&[0u8; 8]);
let err = deserialise(&bytes).unwrap_err();
assert!(err.contains("format needs 16"), "got: {err}");
}
fn header(format: TextureFormat, mip_count: u32, width: u32, height: u32, len: u32) -> Vec<u8> {
let mut bytes = TEXTURE_PAYLOAD_MAGIC.to_le_bytes().to_vec();
bytes.extend_from_slice(&format.id().to_le_bytes());
bytes.extend_from_slice(&mip_count.to_le_bytes());
bytes.extend_from_slice(&width.to_le_bytes());
bytes.extend_from_slice(&height.to_le_bytes());
bytes.extend_from_slice(&len.to_le_bytes());
bytes
}
#[test]
fn deserialise_rejects_a_payload_shorter_than_the_header() {
let full = serialise(&TextureImage::rgba8(1, 1, vec![0; 4]));
for len in 0..HEADER_BYTES {
assert!(deserialise(&full[..len]).is_err(), "len {} decoded", len);
}
}
#[test]
fn deserialise_rejects_a_truncated_mip_header() {
let mut bytes = header(TextureFormat::Rgba8, 1, 2, 2, 16);
bytes.truncate(HEADER_BYTES + 6);
let err = deserialise(&bytes).unwrap_err();
assert!(err.contains("unexpected end"), "got: {err}");
}
#[test]
fn deserialise_rejects_truncated_mip_data() {
let mut bytes = header(TextureFormat::Rgba8, 1, 2, 2, 16);
bytes.extend_from_slice(&[0u8; 8]);
let err = deserialise(&bytes).unwrap_err();
assert!(err.contains("unexpected end"), "got: {err}");
}
#[test]
fn deserialise_rejects_dimensions_that_overflow_the_footprint() {
let bytes = header(TextureFormat::Rgba8, 1, u32::MAX, u32::MAX, 16);
let err = deserialise(&bytes).unwrap_err();
assert!(err.contains("overflow"), "got: {err}");
}
#[test]
fn deserialise_rejects_an_absurd_mip_count() {
let bytes = header(TextureFormat::Rgba8, u32::MAX, 1, 1, 4);
let err = deserialise(&bytes).unwrap_err();
assert!(err.contains("mip levels"), "got: {err}");
}
#[test]
fn deserialise_rejects_zero_mips() {
let bytes = header(TextureFormat::Rgba8, 0, 1, 1, 4);
assert!(deserialise(&bytes).is_err());
}
#[test]
fn mip_byte_len_reports_overflow_for_max_dimensions() {
assert!(
TextureFormat::Rgba8
.mip_byte_len(u32::MAX, u32::MAX)
.is_err()
);
assert!(TextureFormat::Bc7.mip_byte_len(u32::MAX, u32::MAX).is_err());
assert_eq!(TextureFormat::Rgba8.mip_byte_len(2, 2).unwrap(), 16);
assert_eq!(TextureFormat::Bc7.mip_byte_len(4, 4).unwrap(), 16);
}
#[test]
fn into_rgba8_rejects_compressed() {
let image = TextureImage {
format: TextureFormat::Bc3,
mips: vec![TextureMip {
width: 4,
height: 4,
data: vec![0; 16],
}],
};
assert!(image.into_rgba8().is_err());
}
#[test]
fn downscale_rgba_noop_within_budget() {
let px = vec![1u8; 8 * 8 * 4];
let (w, h, out) = downscale_rgba(8, 8, px.clone(), 16);
assert_eq!((w, h), (8, 8));
assert_eq!(out, px);
}
#[test]
fn downscale_rgba_halves_oversized() {
let px = vec![128u8; 8 * 8 * 4];
let (w, h, out) = downscale_rgba(8, 8, px, 4);
assert_eq!((w, h), (4, 4));
assert_eq!(out.len(), 4 * 4 * 4);
assert!(out.iter().all(|&v| v == 128));
}
#[test]
fn downscale_rgba_zero_max_is_noop() {
let px = vec![7u8; 4 * 4 * 4];
let (w, h, out) = downscale_rgba(4, 4, px.clone(), 0);
assert_eq!((w, h), (4, 4));
assert_eq!(out, px);
}
}