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 };
