From c38cd8c407d48da436ed505cbf2487728e2c7569 Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Thu, 5 Nov 2026 01:32:25 +0100 Subject: [PATCH] Metafiles: decode DIB bitmaps with the bit depth chosen once per bitmap (#5479) (#5492) 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 --- crates/metafile/src/dib.rs | 373 ++++++++++++++++++++++++++++++++----- 1 file changed, 331 insertions(+), 42 deletions(-) diff --git a/crates/metafile/src/dib.rs b/crates/metafile/src/dib.rs index c359ad94b4..15be8aa3c0 100644 --- a/crates/metafile/src/dib.rs +++ b/crates/metafile/src/dib.rs @@ -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 = 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, 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, Vec) { + 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;