1use crate::decode::{ByteReader, checked_product};
26use crate::math::ceil;
27use alloc::format;
28use alloc::string::String;
29use alloc::vec;
30use alloc::vec::Vec;
31
32pub const MAX_MIP_LEVELS: usize = 32;
36
37pub(crate) const TEXTURE_PAYLOAD_MAGIC: u32 = u32::from_le_bytes(*b"TEX2");
38const HEADER_BYTES: usize = 12;
39
40#[derive(Clone, Copy, PartialEq, Eq, Debug)]
44pub enum TextureFormat {
45 Rgba8,
47 Bc1,
49 Bc3,
51 Bc5,
53 Bc7,
55}
56
57impl TextureFormat {
58 pub fn id(self) -> u32 {
60 match self {
61 TextureFormat::Rgba8 => 0,
62 TextureFormat::Bc1 => 1,
63 TextureFormat::Bc3 => 2,
64 TextureFormat::Bc5 => 3,
65 TextureFormat::Bc7 => 4,
66 }
67 }
68
69 pub(crate) fn from_id(id: u32) -> Option<Self> {
70 match id {
71 0 => Some(TextureFormat::Rgba8),
72 1 => Some(TextureFormat::Bc1),
73 2 => Some(TextureFormat::Bc3),
74 3 => Some(TextureFormat::Bc5),
75 4 => Some(TextureFormat::Bc7),
76 _ => None,
77 }
78 }
79
80 pub fn block_bytes(self) -> Option<usize> {
83 match self {
84 TextureFormat::Rgba8 => None,
85 TextureFormat::Bc1 => Some(8),
86 TextureFormat::Bc3 | TextureFormat::Bc5 | TextureFormat::Bc7 => Some(16),
87 }
88 }
89
90 pub fn mip_byte_len(self, width: u32, height: u32) -> Result<usize, String> {
95 match self.block_bytes() {
96 None => checked_product("texture mip", &[width as usize, height as usize, 4]),
97 Some(block) => checked_product(
98 "texture mip",
99 &[
100 width.div_ceil(4) as usize,
101 height.div_ceil(4) as usize,
102 block,
103 ],
104 ),
105 }
106 }
107}
108
109#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct TextureMip {
113 pub width: u32,
115 pub height: u32,
117 pub data: Vec<u8>,
119}
120
121#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct TextureImage {
126 pub format: TextureFormat,
128 pub mips: Vec<TextureMip>,
130}
131
132impl TextureImage {
133 pub fn rgba8(width: u32, height: u32, pixels: Vec<u8>) -> Self {
136 TextureImage {
137 format: TextureFormat::Rgba8,
138 mips: vec![TextureMip {
139 width,
140 height,
141 data: pixels,
142 }],
143 }
144 }
145
146 pub fn width(&self) -> u32 {
148 self.mips.first().map(|m| m.width).unwrap_or(0)
149 }
150
151 pub fn height(&self) -> u32 {
153 self.mips.first().map(|m| m.height).unwrap_or(0)
154 }
155
156 pub fn byte_len(&self) -> usize {
158 self.mips.iter().map(|m| m.data.len()).sum()
159 }
160
161 pub fn into_rgba8(self) -> Result<(u32, u32, Vec<u8>), String> {
165 if self.format != TextureFormat::Rgba8 {
166 return Err(format!(
167 "texture is {:?}, expected RGBA8 for this path",
168 self.format
169 ));
170 }
171 let mip = self
172 .mips
173 .into_iter()
174 .next()
175 .ok_or("RGBA8 texture has no mip level")?;
176 Ok((mip.width, mip.height, mip.data))
177 }
178}
179
180pub fn serialise(image: &TextureImage) -> Vec<u8> {
184 let total: usize = HEADER_BYTES + image.mips.iter().map(|m| 12 + m.data.len()).sum::<usize>();
185 let mut buf = Vec::with_capacity(total);
186 buf.extend_from_slice(&TEXTURE_PAYLOAD_MAGIC.to_le_bytes());
187 buf.extend_from_slice(&image.format.id().to_le_bytes());
188 buf.extend_from_slice(&(image.mips.len() as u32).to_le_bytes());
189 for mip in &image.mips {
190 buf.extend_from_slice(&mip.width.to_le_bytes());
191 buf.extend_from_slice(&mip.height.to_le_bytes());
192 buf.extend_from_slice(&(mip.data.len() as u32).to_le_bytes());
193 buf.extend_from_slice(&mip.data);
194 }
195 buf
196}
197
198pub fn deserialise(bytes: &[u8]) -> Result<TextureImage, String> {
203 let mut r = ByteReader::open_payload(bytes, TEXTURE_PAYLOAD_MAGIC, HEADER_BYTES, "texture")?;
204 let format_id = r.u32()?;
205 let format = TextureFormat::from_id(format_id)
206 .ok_or_else(|| format!("texture payload has unknown format_id {}", format_id))?;
207 let mip_count = r.u32()? as usize;
210 if mip_count == 0 || mip_count > MAX_MIP_LEVELS {
211 return Err(format!(
212 "texture payload declares {} mip levels (expected 1..={})",
213 mip_count, MAX_MIP_LEVELS
214 ));
215 }
216
217 r.seek(HEADER_BYTES)?;
218 let mut mips = Vec::with_capacity(mip_count);
219 for level in 0..mip_count {
220 let width = r.u32()?;
221 let height = r.u32()?;
222 let byte_len = r.u32()? as usize;
223 let expected = format.mip_byte_len(width, height)?;
224 if byte_len != expected {
225 return Err(format!(
226 "texture payload mip {} ({}x{} {:?}) declares {} bytes, format needs {}",
227 level, width, height, format, byte_len, expected
228 ));
229 }
230 mips.push(TextureMip {
231 width,
232 height,
233 data: r.take(byte_len)?.to_vec(),
234 });
235 }
236
237 Ok(TextureImage { format, mips })
238}
239
240pub fn downscale_rgba(
245 width: u32,
246 height: u32,
247 pixels: Vec<u8>,
248 max_size: u32,
249) -> (u32, u32, Vec<u8>) {
250 if max_size == 0 || (width <= max_size && height <= max_size) {
251 return (width, height, pixels);
252 }
253 let scale = ceil(width.max(height) as f32 / max_size as f32) as u32;
254 let scale = scale.max(2);
255 let dst_w = (width / scale).max(1);
256 let dst_h = (height / scale).max(1);
257
258 let mut out = vec![0u8; (dst_w * dst_h * 4) as usize];
259 for dy in 0..dst_h {
260 for dx in 0..dst_w {
261 let mut acc = [0u32; 4];
262 let mut n = 0u32;
263 for sy in 0..scale {
264 let src_y = dy * scale + sy;
265 if src_y >= height {
266 break;
267 }
268 for sx in 0..scale {
269 let src_x = dx * scale + sx;
270 if src_x >= width {
271 break;
272 }
273 let si = ((src_y * width + src_x) * 4) as usize;
274 for c in 0..4 {
275 acc[c] += pixels[si + c] as u32;
276 }
277 n += 1;
278 }
279 }
280 let di = ((dy * dst_w + dx) * 4) as usize;
281 for c in 0..4 {
282 out[di + c] = acc[c].checked_div(n).unwrap_or(0) as u8;
283 }
284 }
285 }
286 (dst_w, dst_h, out)
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 fn round_trip(image: &TextureImage) -> TextureImage {
294 let bytes = serialise(image);
295 deserialise(&bytes).expect("deserialise")
296 }
297
298 #[test]
299 fn rgba8_single_mip_round_trips() {
300 let image = TextureImage::rgba8(2, 1, vec![1, 2, 3, 4, 5, 6, 7, 8]);
301 let back = round_trip(&image);
302 assert_eq!(back, image);
303 assert_eq!(back.format, TextureFormat::Rgba8);
304 assert_eq!((back.width(), back.height()), (2, 1));
305 }
306
307 #[test]
308 fn compressed_multi_mip_round_trips() {
309 let image = TextureImage {
312 format: TextureFormat::Bc1,
313 mips: vec![
314 TextureMip {
315 width: 4,
316 height: 4,
317 data: vec![0xAB; 8],
318 },
319 TextureMip {
320 width: 2,
321 height: 2,
322 data: vec![0xCD; 8],
323 },
324 ],
325 };
326 let back = round_trip(&image);
327 assert_eq!(back, image);
328 assert_eq!(back.byte_len(), 16);
329 }
330
331 #[test]
332 fn deserialise_rejects_bad_magic() {
333 let mut bytes = serialise(&TextureImage::rgba8(1, 1, vec![0, 0, 0, 0]));
334 bytes[0] ^= 0xFF;
335 let err = deserialise(&bytes).unwrap_err();
336 assert!(err.contains("magic"), "got: {err}");
337 }
338
339 #[test]
340 fn deserialise_rejects_unknown_format() {
341 let mut bytes = serialise(&TextureImage::rgba8(1, 1, vec![0, 0, 0, 0]));
342 bytes[4..8].copy_from_slice(&99u32.to_le_bytes());
343 let err = deserialise(&bytes).unwrap_err();
344 assert!(err.contains("unknown format_id"), "got: {err}");
345 }
346
347 #[test]
348 fn deserialise_rejects_wrong_mip_length() {
349 let mut bytes = Vec::new();
351 bytes.extend_from_slice(&TEXTURE_PAYLOAD_MAGIC.to_le_bytes());
352 bytes.extend_from_slice(&TextureFormat::Bc7.id().to_le_bytes());
353 bytes.extend_from_slice(&1u32.to_le_bytes());
354 bytes.extend_from_slice(&4u32.to_le_bytes());
355 bytes.extend_from_slice(&4u32.to_le_bytes());
356 bytes.extend_from_slice(&8u32.to_le_bytes());
357 bytes.extend_from_slice(&[0u8; 8]);
358 let err = deserialise(&bytes).unwrap_err();
359 assert!(err.contains("format needs 16"), "got: {err}");
360 }
361
362 fn header(format: TextureFormat, mip_count: u32, width: u32, height: u32, len: u32) -> Vec<u8> {
364 let mut bytes = TEXTURE_PAYLOAD_MAGIC.to_le_bytes().to_vec();
365 bytes.extend_from_slice(&format.id().to_le_bytes());
366 bytes.extend_from_slice(&mip_count.to_le_bytes());
367 bytes.extend_from_slice(&width.to_le_bytes());
368 bytes.extend_from_slice(&height.to_le_bytes());
369 bytes.extend_from_slice(&len.to_le_bytes());
370 bytes
371 }
372
373 #[test]
374 fn deserialise_rejects_a_payload_shorter_than_the_header() {
375 let full = serialise(&TextureImage::rgba8(1, 1, vec![0; 4]));
376 for len in 0..HEADER_BYTES {
377 assert!(deserialise(&full[..len]).is_err(), "len {} decoded", len);
378 }
379 }
380
381 #[test]
382 fn deserialise_rejects_a_truncated_mip_header() {
383 let mut bytes = header(TextureFormat::Rgba8, 1, 2, 2, 16);
384 bytes.truncate(HEADER_BYTES + 6);
385 let err = deserialise(&bytes).unwrap_err();
386 assert!(err.contains("unexpected end"), "got: {err}");
387 }
388
389 #[test]
390 fn deserialise_rejects_truncated_mip_data() {
391 let mut bytes = header(TextureFormat::Rgba8, 1, 2, 2, 16);
392 bytes.extend_from_slice(&[0u8; 8]);
393 let err = deserialise(&bytes).unwrap_err();
394 assert!(err.contains("unexpected end"), "got: {err}");
395 }
396
397 #[test]
401 fn deserialise_rejects_dimensions_that_overflow_the_footprint() {
402 let bytes = header(TextureFormat::Rgba8, 1, u32::MAX, u32::MAX, 16);
403 let err = deserialise(&bytes).unwrap_err();
404 assert!(err.contains("overflow"), "got: {err}");
405 }
406
407 #[test]
410 fn deserialise_rejects_an_absurd_mip_count() {
411 let bytes = header(TextureFormat::Rgba8, u32::MAX, 1, 1, 4);
412 let err = deserialise(&bytes).unwrap_err();
413 assert!(err.contains("mip levels"), "got: {err}");
414 }
415
416 #[test]
417 fn deserialise_rejects_zero_mips() {
418 let bytes = header(TextureFormat::Rgba8, 0, 1, 1, 4);
419 assert!(deserialise(&bytes).is_err());
420 }
421
422 #[test]
423 fn mip_byte_len_reports_overflow_for_max_dimensions() {
424 assert!(
425 TextureFormat::Rgba8
426 .mip_byte_len(u32::MAX, u32::MAX)
427 .is_err()
428 );
429 assert!(TextureFormat::Bc7.mip_byte_len(u32::MAX, u32::MAX).is_err());
430 assert_eq!(TextureFormat::Rgba8.mip_byte_len(2, 2).unwrap(), 16);
431 assert_eq!(TextureFormat::Bc7.mip_byte_len(4, 4).unwrap(), 16);
432 }
433
434 #[test]
435 fn into_rgba8_rejects_compressed() {
436 let image = TextureImage {
437 format: TextureFormat::Bc3,
438 mips: vec![TextureMip {
439 width: 4,
440 height: 4,
441 data: vec![0; 16],
442 }],
443 };
444 assert!(image.into_rgba8().is_err());
445 }
446
447 #[test]
448 fn downscale_rgba_noop_within_budget() {
449 let px = vec![1u8; 8 * 8 * 4];
450 let (w, h, out) = downscale_rgba(8, 8, px.clone(), 16);
451 assert_eq!((w, h), (8, 8));
452 assert_eq!(out, px);
453 }
454
455 #[test]
456 fn downscale_rgba_halves_oversized() {
457 let px = vec![128u8; 8 * 8 * 4];
458 let (w, h, out) = downscale_rgba(8, 8, px, 4);
459 assert_eq!((w, h), (4, 4));
460 assert_eq!(out.len(), 4 * 4 * 4);
461 assert!(out.iter().all(|&v| v == 128));
462 }
463
464 #[test]
465 fn downscale_rgba_zero_max_is_noop() {
466 let px = vec![7u8; 4 * 4 * 4];
467 let (w, h, out) = downscale_rgba(4, 4, px.clone(), 0);
468 assert_eq!((w, h), (4, 4));
469 assert_eq!(out, px);
470 }
471}