use crate::TranscodeTarget;
use web_sys::GpuTextureFormat;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TranscodeCaps {
pub bc: bool,
pub etc2: bool,
pub astc: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceCodec {
Etc1s,
Uastc,
}
const KTX2_IDENTIFIER: [u8; 12] = [
0xAB, b'K', b'T', b'X', b' ', b'2', b'0', 0xBB, 0x0D, 0x0A, 0x1A, 0x0A,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ktx2Sniff {
pub codec: SourceCodec,
pub width: u32,
pub height: u32,
pub has_alpha: bool,
}
pub fn sniff_basis_ktx2(bytes: &[u8]) -> Option<Ktx2Sniff> {
if bytes.len() < 48 || bytes[0..12] != KTX2_IDENTIFIER {
return None;
}
let u32_at = |offset: usize| u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap());
let vk_format = u32_at(12);
let width = u32_at(20);
let height = u32_at(24);
let scheme = u32_at(44);
if vk_format != 0 {
return None;
}
let codec = if scheme == 1 {
SourceCodec::Etc1s
} else {
SourceCodec::Uastc
};
let has_alpha = match codec {
SourceCodec::Etc1s => etc1s_has_alpha(bytes).unwrap_or(true),
SourceCodec::Uastc => true,
};
Some(Ktx2Sniff {
codec,
width,
height,
has_alpha,
})
}
fn etc1s_has_alpha(bytes: &[u8]) -> Option<bool> {
let sgd_offset = u64::from_le_bytes(bytes.get(64..72)?.try_into().ok()?) as usize;
let alpha_len_at = sgd_offset.checked_add(20 + 16)?;
let alpha_len = u32::from_le_bytes(bytes.get(alpha_len_at..alpha_len_at + 4)?.try_into().ok()?);
Some(alpha_len != 0)
}
pub fn dims_block_compatible(width: u32, height: u32) -> bool {
width % 4 == 0 && height % 4 == 0 && width > 0 && height > 0
}
pub fn select_transcode_target(
caps: TranscodeCaps,
codec: SourceCodec,
has_alpha: bool,
) -> TranscodeTarget {
match codec {
SourceCodec::Uastc => {
if caps.astc {
TranscodeTarget::Astc4x4
} else if caps.bc {
TranscodeTarget::Bc7
} else if caps.etc2 {
TranscodeTarget::Etc2Rgba
} else {
TranscodeTarget::Rgba32
}
}
SourceCodec::Etc1s => {
if !has_alpha {
if caps.etc2 {
return TranscodeTarget::Etc1Rgb;
} else if caps.bc {
return TranscodeTarget::Bc1;
}
}
if caps.etc2 {
TranscodeTarget::Etc2Rgba
} else if caps.bc {
TranscodeTarget::Bc7
} else if caps.astc {
TranscodeTarget::Astc4x4
} else {
TranscodeTarget::Rgba32
}
}
}
}
pub fn select_transcode_target_checked(
caps: TranscodeCaps,
codec: SourceCodec,
has_alpha: bool,
width: u32,
height: u32,
) -> TranscodeTarget {
if !dims_block_compatible(width, height) {
return TranscodeTarget::Rgba32;
}
select_transcode_target(caps, codec, has_alpha)
}
pub fn select_normal_transcode_target(caps: TranscodeCaps, codec: SourceCodec) -> TranscodeTarget {
if caps.bc {
TranscodeTarget::Bc5
} else if caps.etc2 {
TranscodeTarget::EacRg11
} else {
select_transcode_target(caps, codec, true)
}
}
pub fn select_normal_transcode_target_checked(
caps: TranscodeCaps,
codec: SourceCodec,
width: u32,
height: u32,
) -> TranscodeTarget {
if !dims_block_compatible(width, height) {
return TranscodeTarget::Rgba32;
}
select_normal_transcode_target(caps, codec)
}
pub fn target_is_two_plane(target: TranscodeTarget) -> bool {
matches!(target, TranscodeTarget::Bc5 | TranscodeTarget::EacRg11)
}
pub fn texture_format_for_target(target: TranscodeTarget, srgb: bool) -> Option<GpuTextureFormat> {
Some(match (target, srgb) {
(TranscodeTarget::Astc4x4, false) => GpuTextureFormat::Astc4x4Unorm,
(TranscodeTarget::Astc4x4, true) => GpuTextureFormat::Astc4x4UnormSrgb,
(TranscodeTarget::Bc7, false) => GpuTextureFormat::Bc7RgbaUnorm,
(TranscodeTarget::Bc7, true) => GpuTextureFormat::Bc7RgbaUnormSrgb,
(TranscodeTarget::Etc2Rgba, false) => GpuTextureFormat::Etc2Rgba8unorm,
(TranscodeTarget::Etc2Rgba, true) => GpuTextureFormat::Etc2Rgba8unormSrgb,
(TranscodeTarget::Bc3, false) => GpuTextureFormat::Bc3RgbaUnorm,
(TranscodeTarget::Bc3, true) => GpuTextureFormat::Bc3RgbaUnormSrgb,
(TranscodeTarget::Bc1, false) => GpuTextureFormat::Bc1RgbaUnorm,
(TranscodeTarget::Bc1, true) => GpuTextureFormat::Bc1RgbaUnormSrgb,
(TranscodeTarget::Etc1Rgb, false) => GpuTextureFormat::Etc2Rgb8unorm,
(TranscodeTarget::Etc1Rgb, true) => GpuTextureFormat::Etc2Rgb8unormSrgb,
(TranscodeTarget::Bc5, false) => GpuTextureFormat::Bc5RgUnorm,
(TranscodeTarget::EacRg11, false) => GpuTextureFormat::EacRg11unorm,
(TranscodeTarget::Bc5 | TranscodeTarget::EacRg11, true) => return None,
(TranscodeTarget::Rgba32, false) => GpuTextureFormat::Rgba8unorm,
(TranscodeTarget::Rgba32, true) => GpuTextureFormat::Rgba8unormSrgb,
})
}
#[cfg(test)]
mod tests {
use super::*;
const DESKTOP: TranscodeCaps = TranscodeCaps {
bc: true,
etc2: false,
astc: false,
};
const MOBILE: TranscodeCaps = TranscodeCaps {
bc: false,
etc2: true,
astc: true,
};
const APPLE: TranscodeCaps = TranscodeCaps {
bc: true,
etc2: true,
astc: true,
};
const NONE: TranscodeCaps = TranscodeCaps {
bc: false,
etc2: false,
astc: false,
};
#[test]
fn ladders() {
use SourceCodec::*;
use TranscodeTarget::*;
assert_eq!(select_transcode_target(DESKTOP, Uastc, true), Bc7);
assert_eq!(select_transcode_target(DESKTOP, Etc1s, true), Bc7);
assert_eq!(select_transcode_target(MOBILE, Uastc, true), Astc4x4);
assert_eq!(select_transcode_target(MOBILE, Etc1s, true), Etc2Rgba);
assert_eq!(select_transcode_target(APPLE, Uastc, true), Astc4x4);
assert_eq!(select_transcode_target(APPLE, Etc1s, true), Etc2Rgba);
assert_eq!(select_transcode_target(NONE, Uastc, true), Rgba32);
assert_eq!(select_transcode_target(NONE, Etc1s, true), Rgba32);
}
#[test]
fn opaque_etc1s_takes_the_half_rate_rung() {
use SourceCodec::*;
use TranscodeTarget::*;
assert_eq!(select_transcode_target(DESKTOP, Etc1s, false), Bc1);
assert_eq!(select_transcode_target(MOBILE, Etc1s, false), Etc1Rgb);
assert_eq!(select_transcode_target(APPLE, Etc1s, false), Etc1Rgb);
let astc_only = TranscodeCaps {
bc: false,
etc2: false,
astc: true,
};
assert_eq!(select_transcode_target(astc_only, Etc1s, false), Astc4x4);
assert_eq!(select_transcode_target(NONE, Etc1s, false), Rgba32);
assert_eq!(select_transcode_target(DESKTOP, Uastc, false), Bc7);
assert_eq!(select_transcode_target(MOBILE, Uastc, false), Astc4x4);
assert_eq!(
texture_format_for_target(Bc1, true),
Some(GpuTextureFormat::Bc1RgbaUnormSrgb)
);
assert_eq!(
texture_format_for_target(Etc1Rgb, true),
Some(GpuTextureFormat::Etc2Rgb8unormSrgb)
);
}
#[test]
fn normal_ladder() {
use SourceCodec::*;
use TranscodeTarget::*;
assert_eq!(select_normal_transcode_target(DESKTOP, Uastc), Bc5);
assert_eq!(select_normal_transcode_target(APPLE, Uastc), Bc5);
assert_eq!(select_normal_transcode_target(MOBILE, Uastc), EacRg11);
assert_eq!(select_normal_transcode_target(NONE, Uastc), Rgba32);
let astc_only = TranscodeCaps {
bc: false,
etc2: false,
astc: true,
};
assert_eq!(select_normal_transcode_target(astc_only, Uastc), Astc4x4);
assert_eq!(
select_normal_transcode_target_checked(DESKTOP, Uastc, 100, 30),
Rgba32
);
assert!(target_is_two_plane(Bc5));
assert!(target_is_two_plane(EacRg11));
assert!(!target_is_two_plane(Astc4x4));
assert!(!target_is_two_plane(Rgba32));
}
#[test]
fn sniffs_basis_ktx2_headers() {
let mut header = vec![0u8; 48];
header[0..12].copy_from_slice(&KTX2_IDENTIFIER);
header[20..24].copy_from_slice(&1024u32.to_le_bytes());
header[24..28].copy_from_slice(&512u32.to_le_bytes());
header[44..48].copy_from_slice(&1u32.to_le_bytes());
assert_eq!(
sniff_basis_ktx2(&header),
Some(Ktx2Sniff {
codec: SourceCodec::Etc1s,
width: 1024,
height: 512,
has_alpha: true,
})
);
header[44..48].copy_from_slice(&2u32.to_le_bytes());
assert_eq!(
sniff_basis_ktx2(&header),
Some(Ktx2Sniff {
codec: SourceCodec::Uastc,
width: 1024,
height: 512,
has_alpha: true,
})
);
header[12..16].copy_from_slice(&37u32.to_le_bytes()); assert_eq!(sniff_basis_ktx2(&header), None);
assert_eq!(sniff_basis_ktx2(b"glTF whatever"), None);
}
#[test]
fn sniffs_etc1s_alpha_from_the_sgd() {
let sgd_offset = 200usize;
let mut buf = vec![0u8; sgd_offset + 40];
buf[0..12].copy_from_slice(&KTX2_IDENTIFIER);
buf[20..24].copy_from_slice(&8u32.to_le_bytes()); buf[24..28].copy_from_slice(&8u32.to_le_bytes()); buf[44..48].copy_from_slice(&1u32.to_le_bytes()); buf[64..72].copy_from_slice(&(sgd_offset as u64).to_le_bytes());
buf[sgd_offset + 36..sgd_offset + 40].copy_from_slice(&0u32.to_le_bytes());
assert!(!sniff_basis_ktx2(&buf).unwrap().has_alpha);
buf[sgd_offset + 36..sgd_offset + 40].copy_from_slice(&4772u32.to_le_bytes());
assert!(sniff_basis_ktx2(&buf).unwrap().has_alpha);
let truncated = buf[..sgd_offset + 10].to_vec();
assert!(sniff_basis_ktx2(&truncated).unwrap().has_alpha);
}
#[test]
fn multiple_of_four_guard() {
assert!(dims_block_compatible(1024, 256));
assert!(!dims_block_compatible(1023, 256));
assert!(!dims_block_compatible(1024, 2));
assert!(!dims_block_compatible(0, 0));
assert_eq!(
select_transcode_target_checked(APPLE, SourceCodec::Uastc, true, 100, 30),
TranscodeTarget::Rgba32
);
assert_eq!(
select_transcode_target_checked(APPLE, SourceCodec::Uastc, true, 100, 32),
TranscodeTarget::Astc4x4
);
}
#[test]
fn formats_carry_srgb_in_the_format() {
assert_eq!(
texture_format_for_target(TranscodeTarget::Bc7, true),
Some(GpuTextureFormat::Bc7RgbaUnormSrgb)
);
assert_eq!(
texture_format_for_target(TranscodeTarget::Bc7, false),
Some(GpuTextureFormat::Bc7RgbaUnorm)
);
assert_eq!(
texture_format_for_target(TranscodeTarget::Rgba32, true),
Some(GpuTextureFormat::Rgba8unormSrgb)
);
assert_eq!(texture_format_for_target(TranscodeTarget::Bc5, true), None);
assert_eq!(
texture_format_for_target(TranscodeTarget::Bc5, false),
Some(GpuTextureFormat::Bc5RgUnorm)
);
assert_eq!(
texture_format_for_target(TranscodeTarget::Etc1Rgb, true),
Some(GpuTextureFormat::Etc2Rgb8unormSrgb)
);
}
}