collective

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

diff --git a/crates/psd/src/compression.rs b/crates/psd/src/compression.rs--- a/crates/psd/src/compression.rs+++ b/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 };