diff --git a/crates/vp9/src/inter.rs b/crates/vp9/src/inter.rs
--- a/crates/vp9/src/inter.rs
+++ b/crates/vp9/src/inter.rs
@@ -81,6 +81,9 @@ fn predict_unscaled(r: &RefPlane, x: i32, y: i32, w: usize, h: usize, filter: u8
     // Source view with origin at (x0 - 3, y0 - 3): the reference band itself when the window
     // lies inside the picture and inside one band.
     let span = if inside { r.frame.span(r.plane, (y0 - 3) as usize, (y0 + h as i32 + 5) as usize) } else { None };
+    if inside && span.is_none() && predict_two_bands(r, x0, y0, w, h, fx, fy, filter, max, out, tmp) {
+        return;
+    }
     let (src, ss, so): (&[u16], usize, usize) = match span {
         Some((data, o)) => (data, r.frame.strides[r.plane], o + (x0 - 3) as usize),
         None => {
@@ -120,6 +123,58 @@ fn predict_unscaled(r: &RefPlane, x: i32, y: i32, w: usize, h: usize, filter: u8
     }
 }
 
+/// [`predict_unscaled`] for a window inside the picture whose rows lie in two consecutive bands:
+/// rows are read from the bands directly instead of being copied into a window first. False
+/// (nothing written) if the window spans more than two bands.
+#[allow(clippy::too_many_arguments)]
+fn predict_two_bands(r: &RefPlane, x0: i32, y0: i32, w: usize, h: usize, fx: usize, fy: usize, filter: u8, max: i32, out: &mut [u16], tmp: &mut [u16]) -> bool {
+    let f = r.frame;
+    let p = r.plane;
+    let bh = f.band_h[p];
+    let ss = f.strides[p];
+    let (ya, yb) = ((y0 - 3) as usize, (y0 + h as i32 + 4) as usize + 1);
+    let split = (ya / bh + 1) * bh;
+    if split >= yb {
+        return false;
+    }
+    let (Some((d0, o0)), Some((d1, o1))) = (f.span(p, ya, split), f.span(p, split, yb)) else { return false };
+    let xo = (x0 - 3) as usize;
+    let row = |i: usize| -> &[u16] {
+        let y = ya + i;
+        if y < split { &d0[o0 + i * ss + xo..] } else { &d1[o1 + (y - split) * ss + xo..] }
+    };
+    match (fx, fy) {
+        (0, 0) => {
+            for i in 0..h {
+                out[i * w..i * w + w].copy_from_slice(&row(i + 3)[3..3 + w]);
+            }
+        }
+        (_, 0) => {
+            let fh = taps(filter, fx);
+            for i in 0..h {
+                filter_h(row(i + 3), fh, max, &mut out[i * w..i * w + w]);
+            }
+        }
+        (0, _) => {
+            let fv = taps(filter, fy);
+            for i in 0..h {
+                filter_v_rows(std::array::from_fn(|t| &row(i + t)[3..]), fv, max, &mut out[i * w..i * w + w]);
+            }
+        }
+        _ => {
+            let fh = taps(filter, fx);
+            let fv = taps(filter, fy);
+            for i in 0..h + 7 {
+                filter_h(row(i), fh, max, &mut tmp[i * w..i * w + w]);
+            }
+            for i in 0..h {
+                filter_v(&tmp[i * w..], w, fv, max, &mut out[i * w..i * w + w]);
+            }
+        }
+    }
+    true
+}
+
 /// Horizontal 8-tap filter of one row: `o[j]` from `s[j..j + 8]`. The taps are the outer loop
 /// so that the row vectorizes (same sums, same order of the exact integer additions).
 #[inline(always)]
@@ -188,15 +243,21 @@ fn narrow8<const W: usize>(acc: &[u16; W], o: &mut [u16; W]) {
 /// Vertical 8-tap filter of one row: `o[j]` from `s[j + t * ss]`, t = 0..8.
 #[inline(always)]
 fn filter_v(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut [u16]) {
+    filter_v_rows(std::array::from_fn(|t| s.get(t * ss..).unwrap_or_default()), f, max, o)
+}
+
+/// Vertical 8-tap filter of one row: `o[j]` from `rows[t][j]`, t = 0..8.
+#[inline(always)]
+fn filter_v_rows(rows: [&[u16]; 8], f: &[i16], max: i32, o: &mut [u16]) {
     match o.len() {
-        4 => filter_v_n::<4>(s, ss, f, max, o),
-        8 => filter_v_n::<8>(s, ss, f, max, o),
-        16 => filter_v_n::<16>(s, ss, f, max, o),
-        32 => filter_v_n::<32>(s, ss, f, max, o),
-        64 => filter_v_n::<64>(s, ss, f, max, o),
+        4 => filter_v_n::<4>(&rows, f, max, o),
+        8 => filter_v_n::<8>(&rows, f, max, o),
+        16 => filter_v_n::<16>(&rows, f, max, o),
+        32 => filter_v_n::<32>(&rows, f, max, o),
+        64 => filter_v_n::<64>(&rows, f, max, o),
         _ => {
             for (j, d) in o.iter_mut().enumerate() {
-                let acc: i32 = (0..8).map(|t| f[t] as i32 * s[j + t * ss] as i32).sum();
+                let acc: i32 = (0..8).map(|t| f[t] as i32 * rows[t][j] as i32).sum();
                 *d = ((acc + 64) >> 7).clamp(0, max) as u16;
             }
         }
@@ -204,7 +265,7 @@ fn filter_v(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut [u16]) {
 }
 
 #[inline(always)]
-fn filter_v_n<const W: usize>(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut [u16]) {
+fn filter_v_n<const W: usize>(rows: &[&[u16]; 8], f: &[i16], max: i32, o: &mut [u16]) {
     let Some(o) = o.first_chunk_mut::<W>() else {
         return;
     };
@@ -212,7 +273,7 @@ fn filter_v_n<const W: usize>(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut
         let mut acc = [64u16; W];
         for t in 0..8 {
             let c = f[t] as u16;
-            let Some(p) = s.get(t * ss..).and_then(|s| s.first_chunk::<W>()) else {
+            let Some(p) = rows[t].first_chunk::<W>() else {
                 return;
             };
             for j in 0..W {
@@ -225,7 +286,7 @@ fn filter_v_n<const W: usize>(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut
     let mut acc = [64i32; W];
     for t in 0..8 {
         let c = f[t] as i32;
-        let Some(p) = s.get(t * ss..).and_then(|s| s.first_chunk::<W>()) else {
+        let Some(p) = rows[t].first_chunk::<W>() else {
             return;
         };
         for j in 0..W {
