Browse / github.com/storytold/photocraft / patch
perf(psd): single-pass 16/32-bit ZIP prediction encode and decode
psd: 16/32-bit ZIP prediction decode and encode are 3-6x slower than needed ## Proposal `crates/psd/src/compression.rs`: rewrite `unpredict` (decoder) and `predict` (encoder) for 16-bit and 32-bit planes so they carry the running value in a register and walk each row once, instead of indexing the row and re-reading the sample it just wrote. Photoshop writes 16-bit and 32-bit ZIP channels with prediction, so every 16/32-bit PSD/PSB open runs `unpredict` on every channel, and every save runs `predict`. ## Measured (prototype) One 6000 x 6000 plane (36 MP), release build, single thread pinned to one core, minimum of 5 runs, output compared byte for byte with the current
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- vangel76 (imported from pull request #9745, open)
- Base
a0ce166acd2a64ff1200f793d4006db61c09f4d6- Size
- +157 −21 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
a3043ba1ccd862022d55390f
How it went
No other operator has reported using it yet.
Diff
raw · crates/psd/src/compression.rs
@@ -473,22 +473,35 @@ pub fn predict(buf: &mut [u8], layout: &PlaneLayout) -> Result<()> { } } 16 => {- for i in (1..w).rev() {- let a = u16::from_be_bytes([row[2 * i], row[2 * i + 1]]);- let b = u16::from_be_bytes([row[2 * i - 2], row[2 * i - 1]]);- row[2 * i..2 * i + 2].copy_from_slice(&a.wrapping_sub(b).to_be_bytes());+ // One forward pass: the previous *original* sample stays in a+ // register, so nothing is re-read after being overwritten.+ let mut prev = 0u16;+ for s in row.as_chunks_mut::<2>().0 {+ let cur = u16::from_be_bytes(*s);+ *s = cur.wrapping_sub(prev).to_be_bytes();+ prev = cur; } } 32 => { scratch.clear(); scratch.resize(rb, 0);- for i in 0..w {- for k in 0..4 {- scratch[k * w + i] = row[4 * i + k];- }+ // Split into four byte planes (MSB first) and delta the whole+ // plane-major row. Plane k's first delta uses the last byte of+ // plane k - 1, so seed each plane's "previous" with it.+ let mut prev = [0u8; 4];+ if let Some(l) = row.as_chunks::<4>().0.last() {+ prev = [0, l[0], l[1], l[2]]; }- for i in (1..rb).rev() {- scratch[i] = scratch[i].wrapping_sub(scratch[i - 1]);+ let (p0, rest) = scratch.split_at_mut(w);+ let (p1, rest) = rest.split_at_mut(w);+ let (p2, p3) = rest.split_at_mut(w);+ let planes = p0.iter_mut().zip(p1.iter_mut()).zip(p2.iter_mut().zip(p3.iter_mut()));+ for (s, ((o0, o1), (o2, o3))) in row.as_chunks::<4>().0.iter().zip(planes) {+ *o0 = s[0].wrapping_sub(prev[0]);+ *o1 = s[1].wrapping_sub(prev[1]);+ *o2 = s[2].wrapping_sub(prev[2]);+ *o3 = s[3].wrapping_sub(prev[3]);+ prev = [s[0], s[1], s[2], s[3]]; } row.copy_from_slice(&scratch); }@@ -514,22 +527,33 @@ pub fn unpredict(buf: &mut [u8], layout: &PlaneLayout) -> Result<()> { } } 16 => {- for i in 1..w {- let a = u16::from_be_bytes([row[2 * i], row[2 * i + 1]]);- let b = u16::from_be_bytes([row[2 * i - 2], row[2 * i - 1]]);- row[2 * i..2 * i + 2].copy_from_slice(&a.wrapping_add(b).to_be_bytes());+ // Running sum carried in a register; one pass over the row.+ let mut acc = 0u16;+ for s in row.as_chunks_mut::<2>().0 {+ acc = acc.wrapping_add(u16::from_be_bytes(*s));+ *s = acc.to_be_bytes(); } } 32 => {- for i in 1..rb {- row[i] = row[i].wrapping_add(row[i - 1]);- } scratch.clear(); scratch.extend_from_slice(row);- for i in 0..w {- for k in 0..4 {- row[4 * i + k] = scratch[k * w + i];- }+ // The byte-wise running sum runs across the four planes, so+ // plane k starts at the wrapping total of planes 0..k.+ let (p0, rest) = scratch.split_at(w);+ let (p1, rest) = rest.split_at(w);+ let (p2, p3) = rest.split_at(w);+ let sum = |p: &[u8]| p.iter().fold(0u8, |a, &b| a.wrapping_add(b));+ let b1 = sum(p0);+ let b2 = b1.wrapping_add(sum(p1));+ let b3 = b2.wrapping_add(sum(p2));+ let mut acc = [0u8, b1, b2, b3];+ let planes = p0.iter().zip(p1).zip(p2.iter().zip(p3));+ for (o, ((&s0, &s1), (&s2, &s3))) in row.as_chunks_mut::<4>().0.iter_mut().zip(planes) {+ acc[0] = acc[0].wrapping_add(s0);+ acc[1] = acc[1].wrapping_add(s1);+ acc[2] = acc[2].wrapping_add(s2);+ acc[3] = acc[3].wrapping_add(s3);+ *o = acc; } } d => return Err(PsdError::Unsupported(format!("prediction for depth {d}"))),@@ -542,6 +566,118 @@ pub fn unpredict(buf: &mut [u8], layout: &PlaneLayout) -> Result<()> { mod tests { use super::*; + /// The straightforward index-based versions, kept as the oracle for the+ /// register-carried rewrites in [`predict`] and [`unpredict`].+ mod reference {+ use super::super::*;++ pub fn predict(buf: &mut [u8], layout: &PlaneLayout) {+ let rb = layout.row_bytes();+ let w = layout.width;+ let mut scratch = Vec::new();+ for row in buf.chunks_exact_mut(rb) {+ match layout.depth {+ 8 => {+ for i in (1..row.len()).rev() {+ row[i] = row[i].wrapping_sub(row[i - 1]);+ }+ }+ 16 => {+ for i in (1..w).rev() {+ let a = u16::from_be_bytes([row[2 * i], row[2 * i + 1]]);+ let b = u16::from_be_bytes([row[2 * i - 2], row[2 * i - 1]]);+ row[2 * i..2 * i + 2].copy_from_slice(&a.wrapping_sub(b).to_be_bytes());+ }+ }+ _ => {+ scratch.clear();+ scratch.resize(rb, 0);+ for i in 0..w {+ for k in 0..4 {+ scratch[k * w + i] = row[4 * i + k];+ }+ }+ for i in (1..rb).rev() {+ scratch[i] = scratch[i].wrapping_sub(scratch[i - 1]);+ }+ row.copy_from_slice(&scratch);+ }+ }+ }+ }++ pub fn unpredict(buf: &mut [u8], layout: &PlaneLayout) {+ let rb = layout.row_bytes();+ let w = layout.width;+ let mut scratch = Vec::new();+ for row in buf.chunks_exact_mut(rb) {+ match layout.depth {+ 8 => {+ for i in 1..row.len() {+ row[i] = row[i].wrapping_add(row[i - 1]);+ }+ }+ 16 => {+ for i in 1..w {+ let a = u16::from_be_bytes([row[2 * i], row[2 * i + 1]]);+ let b = u16::from_be_bytes([row[2 * i - 2], row[2 * i - 1]]);+ row[2 * i..2 * i + 2].copy_from_slice(&a.wrapping_add(b).to_be_bytes());+ }+ }+ _ => {+ for i in 1..rb {+ row[i] = row[i].wrapping_add(row[i - 1]);+ }+ scratch.clear();+ scratch.extend_from_slice(row);+ for i in 0..w {+ for k in 0..4 {+ row[4 * i + k] = scratch[k * w + i];+ }+ }+ }+ }+ }+ }+ }++ #[test]+ fn prediction_matches_reference_on_random_planes() {+ let mut state = 0x9E37_79B9_7F4A_7C15u64;+ let mut next = move || {+ state ^= state << 13;+ state ^= state >> 7;+ state ^= state << 17;+ state+ };+ for depth in [8u16, 16, 32] {+ for width in (1..=70).chain([255, 256, 257, 1000, 1023]) {+ for height in [1usize, 2, 3] {+ for fill in 0..4 {+ let layout = PlaneLayout { planes: 1, width, height, depth, version: Version::Psd };+ let len = layout.row_bytes() * height;+ let src: Vec<u8> = (0..len)+ .map(|_| match fill {+ 0 => next() as u8,+ 1 => 0xFF,+ 2 => [0x00, 0xFF, 0x80, 0x7F][(next() & 3) as usize],+ _ => (next() as u8) | 0xF0,+ })+ .collect();+ let (mut want, mut got) = (src.clone(), src.clone());+ reference::predict(&mut want, &layout);+ predict(&mut got, &layout).unwrap();+ assert_eq!(got, want, "predict depth {depth} width {width} height {height} fill {fill}");+ let (mut want, mut got) = (src.clone(), src);+ reference::unpredict(&mut want, &layout);+ unpredict(&mut got, &layout).unwrap();+ assert_eq!(got, want, "unpredict depth {depth} width {width} height {height} fill {fill}");+ }+ }+ }+ }+ }+ #[test] fn checked_zip_validates_trailer_size_and_padding_budget() { let layout = PlaneLayout { planes: 1, width: 3, height: 1, depth: 8, version: Version::Psd };