mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-07 03:01:32 +00:00
decode_clip matched the bit depth, bounds-checked and (for 16 and 32 bpp) divided per channel for every pixel. The depth, palette and masks are now turned into a row decoder once per bitmap: a 256-entry colour table for 1/4/8 bpp, plain channel swaps for 24 bpp and default 32 bpp, and precomputed shifts and scale tables for masked 16/32 bpp. Output is byte for byte the same; the old per-pixel code stays in the tests as the reference. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
991a21dee6
commit
c38cd8c407
1 file changed
+331
-42
+331
-42
@@ -107,53 +107,143 @@ pub(crate) fn decode_clip(info: Bytes, bits: Bytes, keep_alpha: bool, src: [i64;
|
||||
}
|
||||
let (ow, oh) = (clip.x1 - clip.x0, clip.r1 - clip.r0);
|
||||
let mut px = vec![0u8; ow.checked_mul(oh)?.checked_mul(4)?];
|
||||
#[cfg(test)]
|
||||
DECODED.with(|d| d.set(d.get() + ow * oh));
|
||||
// The depth, palette and masks are the same for every pixel: pick the row decoder once.
|
||||
let kind = RowKind::new(&hd, pal, keep_alpha);
|
||||
for (y, line) in (clip.r0..clip.r1).zip(px.chunks_exact_mut(ow * 4)) {
|
||||
let src_row = if hd.top_down { y } else { hd.h - 1 - y };
|
||||
let row = src_row * stride;
|
||||
for (x, dst_px) in (clip.x0..clip.x1).zip(line.as_chunks_mut::<4>().0) {
|
||||
*dst_px = pixel(data, row, x, &hd, pal, keep_alpha)?;
|
||||
}
|
||||
let row = data.get(src_row * stride..(src_row + 1) * stride)?;
|
||||
kind.decode_row(row, clip.x0..clip.x1, line.as_chunks_mut::<4>().0)?;
|
||||
}
|
||||
Some((Rgba { w: ow, h: oh, px }, out))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// Pixels decoded on this thread, so the tests can check that the work follows the crop.
|
||||
/// Pixels asked of the decoder on this thread, so the tests can check that the work follows the crop.
|
||||
static DECODED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
fn pixel(data: &[u8], row: usize, x: usize, hd: &Header, pal: &[u8], keep_alpha: bool) -> Option<[u8; 4]> {
|
||||
#[cfg(test)]
|
||||
DECODED.with(|d| d.set(d.get() + 1));
|
||||
let [m_r, m_g, m_b] = hd.masks;
|
||||
let [r, g, b] = match hd.bpp {
|
||||
1 => {
|
||||
let b = *data.get(row + x / 8)?;
|
||||
palette(pal, usize::from((b >> (7 - x % 8)) & 1))
|
||||
/// One channel of a masked 16/32 bpp pixel, with the shift and scale worked out once per bitmap.
|
||||
/// Channels of up to 8 bits (every common 16 bpp layout) go through a table of their values, so a
|
||||
/// pixel costs no division.
|
||||
#[derive(Clone)]
|
||||
struct Channel {
|
||||
mask: u32,
|
||||
shift: u32,
|
||||
max: u64,
|
||||
table: [u8; 256],
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
fn new(mask: u32) -> Channel {
|
||||
let shift = if mask == 0 { 0 } else { mask.trailing_zeros() };
|
||||
let max = u64::from(mask >> shift);
|
||||
let mut table = [0u8; 256];
|
||||
if mask != 0 && max <= 255 {
|
||||
for (val, t) in table.iter_mut().enumerate().take(max as usize + 1) {
|
||||
*t = ((val as u64 * 255 + max / 2) / max) as u8;
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
let b = *data.get(row + x / 2)?;
|
||||
palette(pal, usize::from(if x.is_multiple_of(2) { b >> 4 } else { b & 0x0F }))
|
||||
Channel { mask, shift, max, table }
|
||||
}
|
||||
|
||||
/// The channel of `v` scaled to 8 bits; 0 when the mask is empty.
|
||||
fn get(&self, v: u32) -> u8 {
|
||||
if self.mask == 0 {
|
||||
return 0;
|
||||
}
|
||||
8 => palette(pal, usize::from(*data.get(row + x)?)),
|
||||
24 => {
|
||||
let [b, g, r] = data.get(row + x * 3..row + x * 3 + 3)?.try_into().ok()?;
|
||||
[r, g, b]
|
||||
let val = u64::from((v & self.mask) >> self.shift);
|
||||
if self.max <= 255 {
|
||||
return self.table[usize::from(val as u8)];
|
||||
}
|
||||
16 => {
|
||||
let o = row + x * 2;
|
||||
let v = u32::from(u16::from_le_bytes(data.get(o..o + 2)?.try_into().ok()?));
|
||||
[field(v, m_r), field(v, m_g), field(v, m_b)]
|
||||
((val * 255 + self.max / 2) / self.max) as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// How the pixels of one DIB turn into RGBA, decided once from its header.
|
||||
enum RowKind {
|
||||
/// 1, 4 or 8 bpp: `bpp` bits per index into a table of every possible index.
|
||||
Indexed { bpp: usize, table: Box<[[u8; 4]; 256]> },
|
||||
/// 24 bpp: BGR triples.
|
||||
Bgr24,
|
||||
/// 32 bpp with the default masks: BGRA quads, alpha kept or forced opaque.
|
||||
Bgra32 { keep_alpha: bool },
|
||||
/// 16 bpp, or 32 bpp with other masks.
|
||||
Masked { bpp: usize, ch: Box<[Channel; 3]>, keep_alpha: bool },
|
||||
}
|
||||
|
||||
impl RowKind {
|
||||
fn new(hd: &Header, pal: &[u8], keep_alpha: bool) -> RowKind {
|
||||
match hd.bpp {
|
||||
1 | 4 | 8 => {
|
||||
// An index past the palette reads as black, as in `palette`.
|
||||
let mut table = Box::new([[0u8, 0, 0, 255]; 256]);
|
||||
for (i, t) in table.iter_mut().enumerate().take(1 << hd.bpp) {
|
||||
let [r, g, b] = palette(pal, i);
|
||||
*t = [r, g, b, 255];
|
||||
}
|
||||
RowKind::Indexed { bpp: hd.bpp, table }
|
||||
}
|
||||
24 => RowKind::Bgr24,
|
||||
32 if hd.masks == [0xFF_0000, 0xFF00, 0xFF] => RowKind::Bgra32 { keep_alpha },
|
||||
_ => RowKind::Masked { bpp: hd.bpp, ch: Box::new(hd.masks.map(Channel::new)), keep_alpha },
|
||||
}
|
||||
_ => {
|
||||
let o = row + x * 4;
|
||||
let v = u32::from_le_bytes(data.get(o..o + 4)?.try_into().ok()?);
|
||||
let a = if keep_alpha { (v >> 24) as u8 } else { 255 };
|
||||
return Some([field(v, m_r), field(v, m_g), field(v, m_b), a]);
|
||||
}
|
||||
|
||||
/// Decodes the columns `cols` of one source row `row` (all of its bytes) into `out`. `None` when
|
||||
/// the row is shorter than the columns need.
|
||||
fn decode_row(&self, row: &[u8], cols: std::ops::Range<usize>, out: &mut [[u8; 4]]) -> Option<()> {
|
||||
match self {
|
||||
RowKind::Indexed { bpp: 8, table } => {
|
||||
for (d, s) in out.iter_mut().zip(row.get(cols)?) {
|
||||
*d = table[usize::from(*s)];
|
||||
}
|
||||
}
|
||||
RowKind::Indexed { bpp: 4, table } => {
|
||||
for (d, x) in out.iter_mut().zip(cols) {
|
||||
let b = *row.get(x / 2)?;
|
||||
*d = table[usize::from(if x.is_multiple_of(2) { b >> 4 } else { b & 0x0F })];
|
||||
}
|
||||
}
|
||||
RowKind::Indexed { table, .. } => {
|
||||
for (d, x) in out.iter_mut().zip(cols) {
|
||||
let b = *row.get(x / 8)?;
|
||||
*d = table[usize::from((b >> (7 - x % 8)) & 1)];
|
||||
}
|
||||
}
|
||||
RowKind::Bgr24 => {
|
||||
let src = row.get(cols.start.checked_mul(3)?..cols.end.checked_mul(3)?)?;
|
||||
for (d, s) in out.iter_mut().zip(src.as_chunks::<3>().0) {
|
||||
*d = [s[2], s[1], s[0], 255];
|
||||
}
|
||||
}
|
||||
RowKind::Bgra32 { keep_alpha } => {
|
||||
let src = row.get(cols.start.checked_mul(4)?..cols.end.checked_mul(4)?)?;
|
||||
for (d, s) in out.iter_mut().zip(src.as_chunks::<4>().0) {
|
||||
*d = [s[2], s[1], s[0], if *keep_alpha { s[3] } else { 255 }];
|
||||
}
|
||||
}
|
||||
RowKind::Masked { bpp: 16, ch, .. } => {
|
||||
let [r, g, b] = &**ch;
|
||||
let src = row.get(cols.start.checked_mul(2)?..cols.end.checked_mul(2)?)?;
|
||||
for (d, s) in out.iter_mut().zip(src.as_chunks::<2>().0) {
|
||||
let v = u32::from(u16::from_le_bytes(*s));
|
||||
*d = [r.get(v), g.get(v), b.get(v), 255];
|
||||
}
|
||||
}
|
||||
RowKind::Masked { ch, keep_alpha, .. } => {
|
||||
let [r, g, b] = &**ch;
|
||||
let src = row.get(cols.start.checked_mul(4)?..cols.end.checked_mul(4)?)?;
|
||||
for (d, s) in out.iter_mut().zip(src.as_chunks::<4>().0) {
|
||||
let v = u32::from_le_bytes(*s);
|
||||
*d = [r.get(v), g.get(v), b.get(v), if *keep_alpha { (v >> 24) as u8 } else { 255 }];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Some([r, g, b, 255])
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Palette entries are BGRX quads; an index past the table reads as black rather than failing the image.
|
||||
@@ -161,17 +251,6 @@ fn palette(pal: &[u8], i: usize) -> [u8; 3] {
|
||||
pal.get(i * 4..i * 4 + 4).and_then(|c| <[u8; 4]>::try_from(c).ok()).map_or([0, 0, 0], |c| [c[2], c[1], c[0]])
|
||||
}
|
||||
|
||||
/// Extracts one channel from a masked pixel value and scales it to 8 bits.
|
||||
fn field(v: u32, mask: u32) -> u8 {
|
||||
if mask == 0 {
|
||||
return 0;
|
||||
}
|
||||
let shift = mask.trailing_zeros();
|
||||
let max = u64::from(mask >> shift);
|
||||
let val = u64::from((v & mask) >> shift);
|
||||
((val * 255 + max / 2) / max) as u8
|
||||
}
|
||||
|
||||
/// The columns `x0..x1` and top-down rows `r0..r1` of a bitmap that a clipped source rectangle covers.
|
||||
struct Clip {
|
||||
x0: usize,
|
||||
@@ -350,6 +429,216 @@ mod tests {
|
||||
(info, bits)
|
||||
}
|
||||
|
||||
/// The per-pixel decoder `decode_clip` used before the depth was chosen once per bitmap. Kept as
|
||||
/// the reference the fast decoder must match byte for byte.
|
||||
fn pixel(data: &[u8], row: usize, x: usize, hd: &Header, pal: &[u8], keep_alpha: bool) -> Option<[u8; 4]> {
|
||||
DECODED.with(|d| d.set(d.get() + 1));
|
||||
let [m_r, m_g, m_b] = hd.masks;
|
||||
let [r, g, b] = match hd.bpp {
|
||||
1 => {
|
||||
let b = *data.get(row + x / 8)?;
|
||||
palette(pal, usize::from((b >> (7 - x % 8)) & 1))
|
||||
}
|
||||
4 => {
|
||||
let b = *data.get(row + x / 2)?;
|
||||
palette(pal, usize::from(if x.is_multiple_of(2) { b >> 4 } else { b & 0x0F }))
|
||||
}
|
||||
8 => palette(pal, usize::from(*data.get(row + x)?)),
|
||||
24 => {
|
||||
let [b, g, r] = data.get(row + x * 3..row + x * 3 + 3)?.try_into().ok()?;
|
||||
[r, g, b]
|
||||
}
|
||||
16 => {
|
||||
let o = row + x * 2;
|
||||
let v = u32::from(u16::from_le_bytes(data.get(o..o + 2)?.try_into().ok()?));
|
||||
[field(v, m_r), field(v, m_g), field(v, m_b)]
|
||||
}
|
||||
_ => {
|
||||
let o = row + x * 4;
|
||||
let v = u32::from_le_bytes(data.get(o..o + 4)?.try_into().ok()?);
|
||||
let a = if keep_alpha { (v >> 24) as u8 } else { 255 };
|
||||
return Some([field(v, m_r), field(v, m_g), field(v, m_b), a]);
|
||||
}
|
||||
};
|
||||
Some([r, g, b, 255])
|
||||
}
|
||||
|
||||
/// Extracts one channel from a masked pixel value and scales it to 8 bits.
|
||||
fn field(v: u32, mask: u32) -> u8 {
|
||||
if mask == 0 {
|
||||
return 0;
|
||||
}
|
||||
let shift = mask.trailing_zeros();
|
||||
let max = u64::from(mask >> shift);
|
||||
let val = u64::from((v & mask) >> shift);
|
||||
((val * 255 + max / 2) / max) as u8
|
||||
}
|
||||
|
||||
/// `decode_clip` as it was: the same clip, then `pixel` for every output pixel.
|
||||
fn decode_clip_reference(info: Bytes, bits: Bytes, keep_alpha: bool, src: [i64; 4], dst: [f64; 4]) -> Option<(Rgba, [f64; 4])> {
|
||||
let hd = Header::read(info)?;
|
||||
let pixels = hd.w.checked_mul(hd.h)?;
|
||||
if pixels == 0 || pixels > MAX_PIXELS {
|
||||
return None;
|
||||
}
|
||||
let stride = hd.w.checked_mul(hd.bpp)?.checked_add(31)? / 32 * 4;
|
||||
let data = bits.slice(0, stride.checked_mul(hd.h)?)?;
|
||||
let pal = info.slice(hd.pal_off, hd.pal_n * 4)?;
|
||||
let (clip, out) = clip_rect(hd.w, hd.h, !hd.top_down, src, dst)?;
|
||||
let (ow, oh) = (clip.x1 - clip.x0, clip.r1 - clip.r0);
|
||||
let mut px = vec![0u8; ow.checked_mul(oh)?.checked_mul(4)?];
|
||||
for (y, line) in (clip.r0..clip.r1).zip(px.chunks_exact_mut(ow * 4)) {
|
||||
let src_row = if hd.top_down { y } else { hd.h - 1 - y };
|
||||
let row = src_row * stride;
|
||||
for (x, dst_px) in (clip.x0..clip.x1).zip(line.as_chunks_mut::<4>().0) {
|
||||
*dst_px = pixel(data, row, x, &hd, pal, keep_alpha)?;
|
||||
}
|
||||
}
|
||||
Some((Rgba { w: ow, h: oh, px }, out))
|
||||
}
|
||||
|
||||
/// A DIB with chosen header fields: `masks` makes it BI_BITFIELDS (inline after a 40 byte
|
||||
/// header, or inside a larger header), `clr_used` limits the palette, and the palette, when the
|
||||
/// depth is indexed, has `pal_entries` entries (fewer than the depth names reads as black).
|
||||
fn make_dib_with(
|
||||
w: usize,
|
||||
h: i32,
|
||||
bpp: usize,
|
||||
masks: Option<([u32; 3], usize)>,
|
||||
clr_used: u32,
|
||||
pal_entries: usize,
|
||||
seed: &mut u64,
|
||||
) -> (Vec<u8>, Vec<u8>) {
|
||||
let mut rnd = || {
|
||||
*seed ^= *seed << 13;
|
||||
*seed ^= *seed >> 7;
|
||||
*seed ^= *seed << 17;
|
||||
*seed
|
||||
};
|
||||
let size = masks.map_or(40, |(_, s)| s);
|
||||
let mut info = Vec::new();
|
||||
info.extend((size as u32).to_le_bytes());
|
||||
info.extend((w as i32).to_le_bytes());
|
||||
info.extend(h.to_le_bytes());
|
||||
info.extend(1u16.to_le_bytes());
|
||||
info.extend((bpp as u16).to_le_bytes());
|
||||
info.extend(if masks.is_some() { 3u32 } else { 0 }.to_le_bytes());
|
||||
info.extend([0u8; 12]);
|
||||
info.extend(clr_used.to_le_bytes());
|
||||
info.extend([0u8; 4]);
|
||||
info.resize(size.max(40), 0);
|
||||
if let Some((m, _)) = masks {
|
||||
for (i, v) in m.iter().enumerate() {
|
||||
let at = 40 + i * 4;
|
||||
if size == 40 {
|
||||
info.extend(v.to_le_bytes());
|
||||
} else if let Some(dst) = info.get_mut(at..at + 4) {
|
||||
dst.copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
for _ in 0..pal_entries * 4 {
|
||||
info.push(rnd() as u8);
|
||||
}
|
||||
let stride = (w * bpp).div_ceil(32) * 4;
|
||||
let bits = (0..stride * h.unsigned_abs() as usize).map(|_| rnd() as u8).collect();
|
||||
(info, bits)
|
||||
}
|
||||
|
||||
fn assert_same(info: &[u8], bits: &[u8], what: &str) {
|
||||
for keep_alpha in [false, true] {
|
||||
for src in [[0, 0, 0, 0], [3, 1, 20, 3], [-4, -4, 100, 100], [36, 4, 5, 5], [1, 0, 1, 5], [8, 2, 17, 2], [0, 3, 37, 1]] {
|
||||
let dst = [1.0, 2.0, 30.0, 40.0];
|
||||
let old = decode_clip_reference(Bytes(info), Bytes(bits), keep_alpha, src, dst);
|
||||
let new = decode_clip(Bytes(info), Bytes(bits), keep_alpha, src, dst);
|
||||
match (old, new) {
|
||||
(None, None) => {}
|
||||
(Some((a, ad)), Some((b, bd))) => {
|
||||
assert_eq!((a.w, a.h, &a.px), (b.w, b.h, &b.px), "{what} alpha={keep_alpha} {src:?}");
|
||||
assert_eq!(ad.map(f64::to_bits), bd.map(f64::to_bits), "{what} alpha={keep_alpha} {src:?}");
|
||||
}
|
||||
_ => panic!("old and new disagree on None: {what} alpha={keep_alpha} {src:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_clip_matches_the_per_pixel_reference_at_every_depth_palette_and_mask() {
|
||||
let mut seed = 0x9E37_79B9_7F4A_7C15u64;
|
||||
// Widths that leave padding bits in the row, so a row's last byte is only partly used.
|
||||
for w in [1, 9, 37] {
|
||||
for h in [6, -6] {
|
||||
for bpp in [1usize, 4, 8] {
|
||||
let n = 1usize << bpp;
|
||||
// Full palette, clrUsed shorter than the depth (the rest reads black), a
|
||||
// palette cut short of what clrUsed names, and none at all.
|
||||
for (clr_used, entries) in [(0, n), (3, 3), (n as u32 + 50, n), (0, 1), (0, 0)] {
|
||||
let (info, bits) = make_dib_with(w, h, bpp, None, clr_used, entries, &mut seed);
|
||||
assert_same(&info, &bits, &format!("{bpp} bpp w={w} h={h} clrUsed={clr_used} palette={entries}"));
|
||||
}
|
||||
}
|
||||
let (info, bits) = make_dib_with(w, h, 24, None, 0, 0, &mut seed);
|
||||
assert_same(&info, &bits, &format!("24 bpp w={w} h={h}"));
|
||||
// A colour table on a deep DIB is carried but unused.
|
||||
let (info, bits) = make_dib_with(w, h, 24, None, 5, 5, &mut seed);
|
||||
assert_same(&info, &bits, &format!("24 bpp with palette w={w} h={h}"));
|
||||
// Masks on depths that ignore them still move the palette and the bits.
|
||||
for bpp in [1usize, 8, 24] {
|
||||
let n = if bpp == 24 { 0 } else { 1 << bpp };
|
||||
let (info, bits) = make_dib_with(w, h, bpp, Some(([0xF800, 0x07E0, 0x001F], 40)), 0, n, &mut seed);
|
||||
assert_same(&info, &bits, &format!("{bpp} bpp with masks w={w} h={h}"));
|
||||
}
|
||||
let masks16: [Option<[u32; 3]>; 5] = [
|
||||
None,
|
||||
Some([0xF800, 0x07E0, 0x001F]),
|
||||
Some([0x7C00, 0x03E0, 0x001F]),
|
||||
Some([0x0F00, 0x00F0, 0x000F]),
|
||||
Some([0xF800, 0x0000, 0x001F]),
|
||||
];
|
||||
let masks32: [Option<[u32; 3]>; 6] = [
|
||||
None,
|
||||
Some([0xFF_0000, 0xFF00, 0xFF]),
|
||||
Some([0xFF, 0xFF00, 0xFF_0000]),
|
||||
Some([0x3FF0_0000, 0x000F_FC00, 0x0000_03FF]),
|
||||
Some([0xFFFF_FFFF, 0x0000_FF00, 0]),
|
||||
Some([0x00FF_FF00, 0x00FF_FF00, 0x0000_00FF]),
|
||||
];
|
||||
for (bpp, sets) in [(16usize, masks16.as_slice()), (32, masks32.as_slice())] {
|
||||
for m in sets {
|
||||
// 40: the masks follow the header; 56 and 108: they are inside it.
|
||||
for hsize in [40usize, 56, 108] {
|
||||
let inline = m.map(|m| (m, hsize));
|
||||
let (info, bits) = make_dib_with(w, h, bpp, inline, 0, 0, &mut seed);
|
||||
assert_same(&info, &bits, &format!("{bpp} bpp masks={m:x?} header={hsize} w={w} h={h}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Timing of the per-pixel reference against `decode_clip` on a 40 MP bitmap:
|
||||
/// `cargo test -p wordcraft-metafile --release -- --ignored --nocapture dib_decode_speed`.
|
||||
#[test]
|
||||
#[ignore = "timing only"]
|
||||
fn dib_decode_speed() {
|
||||
let mut seed = 6u64;
|
||||
for bpp in [1usize, 4, 8, 16, 24, 32] {
|
||||
let (info, bits) = make_dib(8000, -5000, bpp, &mut seed);
|
||||
for round in 0..4 {
|
||||
let t = std::time::Instant::now();
|
||||
let a = decode_clip_reference(Bytes(&info), Bytes(&bits), false, [0, 0, 8000, 5000], [0.0, 0.0, 1.0, 1.0]).map(|x| x.0.px);
|
||||
let slow = t.elapsed().as_secs_f64() * 1e3;
|
||||
let t = std::time::Instant::now();
|
||||
let b = decode_clip(Bytes(&info), Bytes(&bits), false, [0, 0, 8000, 5000], [0.0, 0.0, 1.0, 1.0]).map(|x| x.0.px);
|
||||
let fast = t.elapsed().as_secs_f64() * 1e3;
|
||||
assert!(a == b);
|
||||
eprintln!("{bpp:2} bpp round {round}: reference {slow:4.0} ms, decode_clip {fast:4.0} ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_clip_matches_decode_then_clip_blit() {
|
||||
let mut seed = 0x2545_F491_4F6C_DD1Du64;
|
||||
|
||||
Reference in new issue
Block a user