use crate::error::Error;
const BC7_PARALLEL_MIN_BLOCKS: usize = 16_384;
fn alloc_and_decode(
data: &[u8],
width: u32,
height: u32,
block_bytes: usize,
f: impl FnOnce(&mut [u8]) -> Result<(), Error>,
) -> Result<Vec<u8>, Error> {
let (_, _, expected) = block_grid(width, height, block_bytes)?;
if data.len() < expected {
return Err(Error::TruncatedData);
}
let need = (width as usize)
.checked_mul(height as usize)
.and_then(|n| n.checked_mul(4))
.ok_or(Error::OutOfBounds)?;
let mut out = vec![0u8; need];
f(&mut out)?;
Ok(out)
}
pub fn decode_bc1(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, Error> {
alloc_and_decode(data, width, height, 8, |o| {
decode_bc1_into(data, width, height, o)
})
}
pub fn decode_bc1_into(data: &[u8], width: u32, height: u32, out: &mut [u8]) -> Result<(), Error> {
decode_rgba_blocks_into(data, width, height, 8, out, |block, dst, pitch| {
bcdec_rs::bc1(block, dst, pitch);
})
}
pub fn decode_bc2(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, Error> {
alloc_and_decode(data, width, height, 16, |o| {
decode_bc2_into(data, width, height, o)
})
}
pub fn decode_bc2_into(data: &[u8], width: u32, height: u32, out: &mut [u8]) -> Result<(), Error> {
decode_rgba_blocks_into(data, width, height, 16, out, |block, dst, pitch| {
bcdec_rs::bc2(block, dst, pitch);
})
}
pub fn decode_bc3(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, Error> {
alloc_and_decode(data, width, height, 16, |o| {
decode_bc3_into(data, width, height, o)
})
}
pub fn decode_bc3_into(data: &[u8], width: u32, height: u32, out: &mut [u8]) -> Result<(), Error> {
decode_rgba_blocks_into(data, width, height, 16, out, |block, dst, pitch| {
bcdec_rs::bc3(block, dst, pitch);
})
}
pub fn decode_bc4(
data: &[u8],
width: u32,
height: u32,
is_signed: bool,
) -> Result<Vec<u8>, Error> {
alloc_and_decode(data, width, height, 8, |o| {
decode_bc4_into(data, width, height, is_signed, o)
})
}
pub fn decode_bc4_into(
data: &[u8],
width: u32,
height: u32,
is_signed: bool,
out: &mut [u8],
) -> Result<(), Error> {
let (blocks_x, blocks_y, expected) = block_grid(width, height, 8)?;
if data.len() < expected {
return Err(Error::TruncatedData);
}
let out_w = width as usize;
let out_h = height as usize;
check_out_len(out, out_w, out_h)?;
let mut block_r = [0u8; 16];
for by in 0..blocks_y {
for bx in 0..blocks_x {
let bi = (by * blocks_x + bx) * 8;
bcdec_rs::bc4(&data[bi..bi + 8], &mut block_r, 4, is_signed);
blit_r_to_rgba(&block_r, out, out_w, out_h, bx * 4, by * 4);
}
}
Ok(())
}
pub fn decode_bc5(
data: &[u8],
width: u32,
height: u32,
is_signed: bool,
) -> Result<Vec<u8>, Error> {
alloc_and_decode(data, width, height, 16, |o| {
decode_bc5_into(data, width, height, is_signed, o)
})
}
pub fn decode_bc5_into(
data: &[u8],
width: u32,
height: u32,
is_signed: bool,
out: &mut [u8],
) -> Result<(), Error> {
let (blocks_x, blocks_y, expected) = block_grid(width, height, 16)?;
if data.len() < expected {
return Err(Error::TruncatedData);
}
let out_w = width as usize;
let out_h = height as usize;
check_out_len(out, out_w, out_h)?;
let mut block_rg = [0u8; 32];
for by in 0..blocks_y {
for bx in 0..blocks_x {
let bi = (by * blocks_x + bx) * 16;
bcdec_rs::bc5(&data[bi..bi + 16], &mut block_rg, 8, is_signed);
blit_rg_to_rgba(&block_rg, out, out_w, out_h, bx * 4, by * 4);
}
}
Ok(())
}
pub fn decode_bc7(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, Error> {
alloc_and_decode(data, width, height, 16, |o| {
decode_bc7_into(data, width, height, o)
})
}
pub fn decode_bc7_into(data: &[u8], width: u32, height: u32, out: &mut [u8]) -> Result<(), Error> {
let (blocks_x, blocks_y, expected) = block_grid(width, height, 16)?;
if data.len() < expected {
return Err(Error::TruncatedData);
}
let out_w = width as usize;
let out_h = height as usize;
check_out_len(out, out_w, out_h)?;
let aligned = width % 4 == 0 && height % 4 == 0;
let parallel = aligned
&& blocks_y >= 2
&& blocks_x.saturating_mul(blocks_y) >= BC7_PARALLEL_MIN_BLOCKS;
if parallel {
decode_bc7_parallel(data, out, out_w, blocks_x, blocks_y);
} else if aligned {
decode_bc7_direct(data, out, out_w, blocks_x, blocks_y);
} else {
decode_bc7_scratch(data, out, out_w, out_h, blocks_x, blocks_y);
}
Ok(())
}
fn check_out_len(out: &[u8], out_w: usize, out_h: usize) -> Result<(), Error> {
let need = out_w
.checked_mul(out_h)
.and_then(|n| n.checked_mul(4))
.ok_or(Error::OutOfBounds)?;
if out.len() != need {
return Err(Error::OutOfBounds);
}
Ok(())
}
fn decode_rgba_blocks_into(
data: &[u8],
width: u32,
height: u32,
block_bytes: usize,
out: &mut [u8],
decode_block: impl Fn(&[u8], &mut [u8], usize),
) -> Result<(), Error> {
let (blocks_x, blocks_y, expected) = block_grid(width, height, block_bytes)?;
if data.len() < expected {
return Err(Error::TruncatedData);
}
let out_w = width as usize;
let out_h = height as usize;
check_out_len(out, out_w, out_h)?;
let pitch = out_w * 4;
if width % 4 == 0 && height % 4 == 0 {
for by in 0..blocks_y {
for bx in 0..blocks_x {
let bi = (by * blocks_x + bx) * block_bytes;
let offset = (by * 4 * out_w + bx * 4) * 4;
decode_block(&data[bi..bi + block_bytes], &mut out[offset..], pitch);
}
}
} else {
let mut scratch = [0u8; 64];
for by in 0..blocks_y {
for bx in 0..blocks_x {
let bi = (by * blocks_x + bx) * block_bytes;
decode_block(&data[bi..bi + block_bytes], &mut scratch, 16);
blit_rgba4(&scratch, out, out_w, out_h, bx * 4, by * 4);
}
}
}
Ok(())
}
fn decode_bc7_direct(
data: &[u8],
out: &mut [u8],
out_w: usize,
blocks_x: usize,
blocks_y: usize,
) {
let pitch = out_w * 4;
for by in 0..blocks_y {
for bx in 0..blocks_x {
let bi = (by * blocks_x + bx) * 16;
let offset = (by * 4 * out_w + bx * 4) * 4;
let (blk, dst) = (&data[bi..bi + 16], &mut out[offset..]);
if !bc7_mode6_block(blk, dst, pitch) {
bcdec_rs::bc7(blk, dst, pitch);
}
}
}
}
fn decode_bc7_scratch(
data: &[u8],
out: &mut [u8],
out_w: usize,
out_h: usize,
blocks_x: usize,
blocks_y: usize,
) {
let mut scratch = [0u8; 64];
for by in 0..blocks_y {
for bx in 0..blocks_x {
let bi = (by * blocks_x + bx) * 16;
let blk = &data[bi..bi + 16];
if !bc7_mode6_block(blk, &mut scratch, 16) {
bcdec_rs::bc7(blk, &mut scratch, 16);
}
blit_rgba4(&scratch, out, out_w, out_h, bx * 4, by * 4);
}
}
}
fn decode_bc7_parallel(
data: &[u8],
out: &mut [u8],
out_w: usize,
blocks_x: usize,
blocks_y: usize,
) {
let pitch = out_w * 4;
let strip_bytes = 4 * pitch;
static CORES: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
let cores = *CORES.get_or_init(|| {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
});
let workers = cores.clamp(1, blocks_y);
let mut ranges: Vec<(usize, usize)> = Vec::with_capacity(workers);
let base = blocks_y / workers;
let extra = blocks_y % workers;
let mut start = 0;
for w in 0..workers {
let len = base + usize::from(w < extra);
ranges.push((start, start + len));
start += len;
}
std::thread::scope(|s| {
let mut rest = out;
let mut consumed_rows = 0usize;
for &(by0, by1) in &ranges {
let row0 = by0 * 4;
debug_assert_eq!(row0, consumed_rows);
let strip_len = (by1 - by0) * strip_bytes;
let (band, tail) = rest.split_at_mut(strip_len);
rest = tail;
consumed_rows = by1 * 4;
s.spawn(move || {
for by in by0..by1 {
let local_y = by - by0;
for bx in 0..blocks_x {
let bi = (by * blocks_x + bx) * 16;
let offset = (local_y * 4 * out_w + bx * 4) * 4;
let (blk, dst) = (&data[bi..bi + 16], &mut band[offset..]);
if !bc7_mode6_block(blk, dst, pitch) {
bcdec_rs::bc7(blk, dst, pitch);
}
}
}
});
}
debug_assert!(rest.is_empty());
});
}
fn block_grid(width: u32, height: u32, block_bytes: usize) -> Result<(usize, usize, usize), Error> {
if width == 0 || height == 0 {
return Err(Error::InvalidField("zero image dimension".into()));
}
let blocks_x = (width as usize + 3) / 4;
let blocks_y = (height as usize + 3) / 4;
let expected = blocks_x
.checked_mul(blocks_y)
.and_then(|n| n.checked_mul(block_bytes))
.ok_or(Error::OutOfBounds)?;
Ok((blocks_x, blocks_y, expected))
}
#[inline]
fn blit_rgba4(scratch: &[u8; 64], out: &mut [u8], out_w: usize, out_h: usize, px0: usize, py0: usize) {
let copy_w = 4.min(out_w - px0);
let copy_h = 4.min(out_h - py0);
for row in 0..copy_h {
let src = row * 16;
let dst = ((py0 + row) * out_w + px0) * 4;
out[dst..dst + copy_w * 4].copy_from_slice(&scratch[src..src + copy_w * 4]);
}
}
#[inline]
fn blit_r_to_rgba(block_r: &[u8; 16], out: &mut [u8], out_w: usize, out_h: usize, px0: usize, py0: usize) {
let copy_w = 4.min(out_w - px0);
let copy_h = 4.min(out_h - py0);
for row in 0..copy_h {
for col in 0..copy_w {
let v = block_r[row * 4 + col];
let dst = ((py0 + row) * out_w + px0 + col) * 4;
out[dst] = v;
out[dst + 1] = 0;
out[dst + 2] = 0;
out[dst + 3] = 255;
}
}
}
#[inline]
fn blit_rg_to_rgba(
block_rg: &[u8; 32],
out: &mut [u8],
out_w: usize,
out_h: usize,
px0: usize,
py0: usize,
) {
let copy_w = 4.min(out_w - px0);
let copy_h = 4.min(out_h - py0);
for row in 0..copy_h {
for col in 0..copy_w {
let src = row * 8 + col * 2;
let dst = ((py0 + row) * out_w + px0 + col) * 4;
out[dst] = block_rg[src];
out[dst + 1] = block_rg[src + 1];
out[dst + 2] = 0;
out[dst + 3] = 255;
}
}
}
const BC7_WEIGHTS4: [u32; 16] = [0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64];
#[inline]
fn bc7_mode6_block(blk: &[u8], out: &mut [u8], pitch: usize) -> bool {
if blk[0] != 0x40 {
return false;
}
let Ok(bytes) = <[u8; 16]>::try_from(&blk[..16]) else {
return false;
};
let b = u128::from_le_bytes(bytes);
let f = |shift: u32| ((b >> shift) & 0x7f) as u32;
let p0 = ((b >> 63) & 1) as u32;
let p1 = ((b >> 64) & 1) as u32;
let e0 = [
(f(7) << 1) | p0,
(f(21) << 1) | p0,
(f(35) << 1) | p0,
(f(49) << 1) | p0,
];
let e1 = [
(f(14) << 1) | p1,
(f(28) << 1) | p1,
(f(42) << 1) | p1,
(f(56) << 1) | p1,
];
let idx = b >> 65;
for i in 0..16usize {
let w = if i == 0 {
BC7_WEIGHTS4[(idx & 0x7) as usize]
} else {
BC7_WEIGHTS4[((idx >> (3 + (i - 1) * 4)) & 0xf) as usize]
};
let iw = 64 - w;
let o = (i / 4) * pitch + (i % 4) * 4;
out[o] = ((e0[0] * iw + e1[0] * w + 32) >> 6) as u8;
out[o + 1] = ((e0[1] * iw + e1[1] * w + 32) >> 6) as u8;
out[o + 2] = ((e0[2] * iw + e1[2] * w + 32) >> 6) as u8;
out[o + 3] = ((e0[3] * iw + e1[3] * w + 32) >> 6) as u8;
}
true
}
#[cfg(test)]
mod bc7_mode6_tests {
use super::{bc7_mode6_block, BC7_WEIGHTS4};
#[test]
fn mode6_matches_the_general_decoder() {
let mut state = 0x243f_6a88_85a3_08d3u64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for case in 0..20_000 {
let mut blk = [0u8; 16];
match case {
0 => {}
1 => blk.iter_mut().for_each(|b| *b = 0xff),
_ => {
let (a, b) = (next(), next());
blk[..8].copy_from_slice(&a.to_le_bytes());
blk[8..].copy_from_slice(&b.to_le_bytes());
}
}
blk[0] = 0x40;
let mut ours = [0u8; 64];
assert!(bc7_mode6_block(&blk, &mut ours, 16), "case {case}: not recognised");
let mut theirs = [0u8; 64];
bcdec_rs::bc7(&blk, &mut theirs, 16);
assert_eq!(
ours, theirs,
"case {case}: mode-6 fast path diverged
block {blk:02x?}"
);
}
}
#[test]
fn other_modes_are_declined() {
for mode in 0..8u32 {
let mut blk = [0u8; 16];
blk[0] = 1 << mode;
let mut px = [0u8; 64];
assert_eq!(
bc7_mode6_block(&blk, &mut px, 16),
mode == 6,
"mode {mode} handled incorrectly"
);
}
let mut px = [0u8; 64];
assert!(!bc7_mode6_block(&[0u8; 16], &mut px, 16));
}
#[test]
fn weights_are_the_spec_table() {
assert_eq!(BC7_WEIGHTS4[0], 0);
assert_eq!(BC7_WEIGHTS4[15], 64);
assert!(BC7_WEIGHTS4.windows(2).all(|w| w[0] < w[1]));
}
}