use crate::Result;
use crate::read::Bytes;
const IDENTIFIER: [u8; 12] = [
0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Rgba8Srgb,
Rgba8Unorm,
Rgba16Float,
}
impl Format {
pub fn bytes_per_texel(self) -> usize {
match self {
Self::Rgba8Srgb | Self::Rgba8Unorm => 4,
Self::Rgba16Float => 8,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Level {
pub width: u32,
pub height: u32,
pub depth: u32,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Texture {
pub format: Format,
pub width: u32,
pub height: u32,
pub depth: u32,
pub faces: u32,
pub layers: u32,
pub levels: Vec<Level>,
pub usage: Option<String>,
}
impl Texture {
pub fn decode(bytes: &[u8]) -> Result<Self> {
let b = Bytes::new(bytes, "ktx2");
if b.slice(0, 12)? != IDENTIFIER {
return Err(b.err("not a KTX2 file"));
}
let format = match b.u32(12)? {
43 => Format::Rgba8Srgb,
37 => Format::Rgba8Unorm,
97 => Format::Rgba16Float,
other => return Err(b.err(format!("unsupported vkFormat {other}"))),
};
let (width, height) = (b.u32(20)?, b.u32(24)?);
let depth = b.u32(28)?.max(1);
let layers = b.u32(32)?.max(1);
let faces = b.u32(36)?;
if faces != 1 && faces != 6 {
return Err(b.err(format!("{faces} faces")));
}
if b.u32(44)? != 0 {
return Err(b.err("supercompressed textures are not supported"));
}
let level_count = b.u32(40)?.max(1);
let mut levels = Vec::with_capacity(level_count.min(32) as usize);
for i in 0..level_count {
let at = 80 + 24 * i as usize;
let offset = b.u64(at)? as usize;
let length = b.u64(at + 8)? as usize;
let level = |size: u32| (size >> i).max(1);
let (w, h, d) = (level(width), level(height), level(depth));
let expected = [w, h, d, faces, layers]
.into_iter()
.try_fold(format.bytes_per_texel(), |n, v| n.checked_mul(v as usize))
.ok_or_else(|| b.err(format!("level {i} is too big")))?;
if length != expected {
return Err(b.err(format!(
"level {i} has {length} bytes, {w} × {h} × {d} needs {expected}"
)));
}
levels.push(Level {
width: w,
height: h,
depth: d,
data: b.slice(offset, length)?.to_vec(),
});
}
let kvd = b.slice(b.u32(56)? as usize, b.u32(60)? as usize)?;
Ok(Self {
format,
width,
height,
depth,
faces,
layers,
levels,
usage: key_value(Bytes::new(kvd, "ktx2"), "assetc.usage")?,
})
}
pub fn is_cubemap(&self) -> bool {
self.faces == 6
}
pub fn is_volume(&self) -> bool {
self.depth > 1
}
pub fn is_srgb(&self) -> bool {
self.format == Format::Rgba8Srgb
}
}
fn key_value(kvd: Bytes<'_>, key: &str) -> Result<Option<String>> {
let mut at = 0;
while at < kvd.bytes.len() {
let len = kvd.u32(at)? as usize;
let entry = kvd.slice(at + 4, len)?;
let split = entry.iter().position(|&c| c == 0).unwrap_or(entry.len());
if &entry[..split] == key.as_bytes() {
let value = entry.get(split + 1..).unwrap_or_default();
let value = value.strip_suffix(&[0]).unwrap_or(value);
return std::str::from_utf8(value)
.map(|v| Some(v.to_owned()))
.map_err(|_| kvd.err(format!("{key} is not UTF-8")));
}
at = (at + 4 + len).next_multiple_of(4);
}
Ok(None)
}
pub fn f16_to_f32(bits: u16) -> f32 {
let sign = if bits & 0x8000 != 0 { -1.0 } else { 1.0 };
let exponent = (bits >> 10) & 0x1f;
let mantissa = f32::from(bits & 0x3ff);
sign * match exponent {
0 => mantissa * 2f32.powi(-24),
31 if mantissa == 0.0 => f32::INFINITY,
31 => f32::NAN,
e => (1.0 + mantissa / 1024.0) * 2f32.powi(i32::from(e) - 15),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ktx2_with(format: u32, w: u32, h: u32, layers: u32, kvd: &[u8], pixels: &[u8]) -> Vec<u8> {
let mut out = IDENTIFIER.to_vec();
for v in [format, 1, w, h, 0, layers, 1, 1, 0] {
out.extend_from_slice(&v.to_le_bytes());
}
let kvd_at = 80 + 24;
for v in [0, 0, kvd_at, kvd.len() as u32] {
out.extend_from_slice(&v.to_le_bytes());
}
out.extend_from_slice(&[0; 16]);
let at = kvd_at as usize + kvd.len();
out.extend_from_slice(&(at as u64).to_le_bytes());
out.extend_from_slice(&(pixels.len() as u64).to_le_bytes());
out.extend_from_slice(&(pixels.len() as u64).to_le_bytes());
out.extend_from_slice(kvd);
out.extend_from_slice(pixels);
out
}
fn ktx2(format: u32, w: u32, h: u32, pixels: &[u8]) -> Vec<u8> {
ktx2_with(format, w, h, 0, &[], pixels)
}
fn kvd(entries: &[(&str, &str)]) -> Vec<u8> {
let mut out = Vec::new();
for (key, value) in entries {
let len = key.len() + 1 + value.len() + 1;
out.extend_from_slice(&(len as u32).to_le_bytes());
out.extend_from_slice(format!("{key}\0{value}\0").as_bytes());
while !out.len().is_multiple_of(4) {
out.push(0);
}
}
out
}
#[test]
fn reads_levels_and_checks_sizes() {
let t = Texture::decode(&ktx2(43, 2, 1, &[1, 2, 3, 4, 5, 6, 7, 8])).unwrap();
assert_eq!(
(t.format, t.width, t.height, t.depth, t.layers),
(Format::Rgba8Srgb, 2, 1, 1, 1)
);
assert_eq!(t.levels[0].data, [1, 2, 3, 4, 5, 6, 7, 8]);
assert_eq!(t.usage, None);
assert!(Texture::decode(&ktx2(43, 2, 2, &[0; 8])).is_err());
assert!(Texture::decode(&ktx2(99, 1, 1, &[0; 4])).is_err());
}
#[test]
fn reads_layers_and_usage() {
let data = kvd(&[("KTXwriter", "assetc 0.1.0"), ("assetc.usage", "gradient")]);
let t = Texture::decode(&ktx2_with(43, 1, 1, 2, &data, &[0; 8])).unwrap();
assert_eq!((t.layers, t.usage.as_deref()), (2, Some("gradient")));
assert!(Texture::decode(&ktx2_with(43, 1, 1, 2, &data, &[0; 4])).is_err());
let other = kvd(&[("KTXwriter", "x"), ("zz", "y")]);
let t = Texture::decode(&ktx2_with(37, 1, 1, 0, &other, &[0; 4])).unwrap();
assert_eq!(t.usage, None);
let mut bad = kvd(&[("assetc.usage", "data")]);
bad[0] = 200;
assert!(Texture::decode(&ktx2_with(37, 1, 1, 0, &bad, &[0; 4])).is_err());
}
#[test]
fn half_floats() {
assert_eq!(f16_to_f32(0x3c00), 1.0);
assert_eq!(f16_to_f32(0xc000), -2.0);
assert_eq!(f16_to_f32(0x3800), 0.5);
assert_eq!(f16_to_f32(0), 0.0);
}
}