diff --git a/crates/codecs/src/video.rs b/crates/codecs/src/video.rs
--- a/crates/codecs/src/video.rs
+++ b/crates/codecs/src/video.rs
@@ -306,25 +306,37 @@ impl H264Decoder {
         let draft = p.draft;
         let (w, h) = (p.width as usize, p.height as usize);
         let (cw, ch) = (p.chroma_width as usize, p.chroma_height as usize);
-        let y = tight_plane(p.y, p.y_stride, w, h);
-        let u = tight_plane(p.u, p.uv_stride, cw, ch);
-        let v = tight_plane(p.v, p.uv_stride, cw, ch);
+        let chroma = if p.chroma_format_idc == 2 { filmcraft_frame::Chroma::C422 } else { filmcraft_frame::Chroma::C420 };
+        let data = match p.wide {
+            Some(wp) => filmcraft_frame::PixelData::Yuv16 {
+                planes: [
+                    Arc::new(tight_plane(wp.y, p.y_stride, w, h)),
+                    Arc::new(tight_plane(wp.u, p.uv_stride, cw, ch)),
+                    Arc::new(tight_plane(wp.v, p.uv_stride, cw, ch)),
+                ],
+                chroma,
+                bits: p.bit_depth,
+                alpha: None,
+            },
+            None => filmcraft_frame::PixelData::Yuv8 {
+                planes: [
+                    Arc::new(tight_plane(p.y, p.y_stride, w, h)),
+                    Arc::new(tight_plane(p.u, p.uv_stride, cw, ch)),
+                    Arc::new(tight_plane(p.v, p.uv_stride, cw, ch)),
+                ],
+                chroma,
+                alpha: None,
+            },
+        };
         let color = vui_color(p.width, p.height, p.color.primaries, p.color.matrix, p.color.transfer, p.color.full_range);
         let par = sar_par(p.sar);
-        let frame = VideoFrame {
-            width: p.width,
-            height: p.height,
-            data: filmcraft_frame::PixelData::Yuv8 { planes: [Arc::new(y), Arc::new(u), Arc::new(v)], chroma: filmcraft_frame::Chroma::C420, alpha: None },
-            color,
-            par,
-            pts: filmcraft_time::Tick::ZERO,
-        };
+        let frame = VideoFrame { width: p.width, height: p.height, data, color, par, pts: filmcraft_time::Tick::ZERO };
         DecodedFrame { pts: p.pts, frame, draft }
     }
 }
 
 /// An H.264 decoder error as a [`CodecError`]: a feature the decoder does not implement (High
-/// 4:2:2, field pictures...) is [`CodecError::Unsupported`], so the media pool shows the
+/// 4:4:4, field pictures...) is [`CodecError::Unsupported`], so the media pool shows the
 /// unreadable slate instead of a black picture (#626); anything else is a decode error.
 fn h264_error(e: filmcraft_h264::Error) -> CodecError {
     match e {
@@ -373,6 +385,7 @@ impl VideoDecoder for H264Decoder {
 /// (`filmcraft_frame::pool`) instead of the allocator.
 fn recycle_h264_planes() {
     filmcraft_h264::set_plane_allocator(filmcraft_frame::pool::take_u8);
+    filmcraft_h264::set_plane_allocator_u16(filmcraft_frame::pool::take_u16);
 }
 
 /// Worker threads each H.264 decoder uses (frame threading; 1 on wasm).
diff --git a/crates/h264/README.md b/crates/h264/README.md
--- a/crates/h264/README.md
+++ b/crates/h264/README.md
@@ -19,7 +19,10 @@ oracles and fixture generators.
 - Slice header: all fields incl. reference list modification, prediction weight table and
   decoded reference picture marking. POC types 0, 1, 2. `frame_num` gap handling ("non-existing"
   frames).
-- Entropy coding: CAVLC and CABAC (all syntax elements needed for frame coding, 4:2:0).
+- Entropy coding: CAVLC and CABAC (all syntax elements needed for frame coding, 4:2:0 and 4:2:2).
+- Sample formats: 8-bit 4:2:0; 9- and 10-bit 4:2:0 (High 10, intra and inter); 4:2:2 at 8–10 bits
+  for intra pictures (High 4:2:2 Intra: Sony XAVC Intra, AVC-Intra). The decoder is generic over the
+  sample storage (`u8` for 8-bit, `u16` above); high bit depth pictures come out in `Picture::wide`.
 - Macroblocks: I (Intra 4x4 / 8x8 / 16x16, I_PCM), P (all partitions, P_8x8ref0, P_Skip),
   B (all partitions and sub-partitions, B_Skip, B_Direct_16x16, B_Direct_8x8), spatial and temporal
   direct prediction with/without `direct_8x8_inference`, constrained intra prediction.
@@ -203,21 +206,20 @@ file; `H264_BENCH_ITERS=n` (and `H264_THREADS=t`, `H264_DRAFT=1`) turns it into
 
 - Interlaced coding (field pictures, PAFF, MBAFF): streams with `frame_mbs_only_flag = 0` return
   `Error::Unsupported`.
-- Only 8-bit 4:2:0. High 10 / 4:2:2 / 4:4:4 / monochrome and lossless
-  (`qpprime_y_zero_transform_bypass`) return `Error::Unsupported`.
+- Inter prediction in 4:2:2 streams (long-GOP High 4:2:2), 4:4:4, monochrome, bit depths above 10,
+  unequal luma / chroma bit depths and lossless (`qpprime_y_zero_transform_bypass`) return
+  `Error::Unsupported`.
 - FMO (slice groups), SP/SI slices, data partitioning (Extended profile), MVC/SVC NAL units.
 - Error concealment is minimal: undecodable slices leave the prediction/previous content; missing
   references are replaced by other references.
 - No SIMD intrinsics (the hot loops are written to auto-vectorise); CABAC residual decoding is
   inherently serial and now dominates.
 
-### Extending to High 10 and 4:2:2
+### Extending to 4:2:2 inter prediction
 
-The chroma format and bit depth are isolated behind `Sps::check_supported`. The parts that are
-format-specific today: `Planes`/`FrameRow` sample type (`u8`; a `u16` variant or a generic sample
-type is needed for bit depths > 8, together with `Clip1` ranges, `QpBdOffset` in the QP
-derivations and the scaled tC0/alpha/beta in deblocking), chroma block geometry (`MbWidthC` /
-`MbHeightC` = 8x8 in `slicedec`, `intra::pred_chroma`, the chroma DC transform and the CABAC/CAVLC
-chroma DC parsing — the 2x4 total_zeros tables and ctxBlockCat 3 `NumC8x8` handling are already in
-place), chroma motion-vector vertical scaling and the chroma deblocking edges (4:2:2 has four
-horizontal chroma edges).
+High 10 and 4:2:2 intra are in place (generic `Sample`, `Format` with bit depth and chroma lines per
+macroblock). For 4:2:2 P/B pictures the remaining pieces are the chroma motion compensation
+(vertical chroma vectors in full-resolution units: `yIntC = yAL + (mvCLX[1] >> 2)`,
+`yFracC = (mvCLX[1] & 3) << 1`, 8.4.2.2.2), 4:2:2 chroma blocks in `mc_rect` / the weighted
+prediction scratch, and inter bS for the horizontal chroma edges 4 and 12 with the 8x8 transform;
+then drop the check in `Decoder::handle_slice`.
diff --git a/crates/h264/examples/h264dec.rs b/crates/h264/examples/h264dec.rs
--- a/crates/h264/examples/h264dec.rs
+++ b/crates/h264/examples/h264dec.rs
@@ -1,4 +1,5 @@
-//! Decode an Annex-B H.264 file to raw yuv420p: `h264dec in.h264 [out.yuv]`.
+//! Decode an Annex-B H.264 file to raw planar YUV (yuv420p / yuv422p, 16-bit little-endian
+//! samples above 8 bits): `h264dec in.h264 [out.yuv]`.
 //! Prints timing information to stderr. `H264_BENCH_ITERS=n` (with `H264_THREADS=t`, and
 //! `H264_DRAFT=1` for draft mode) turns it into a benchmark.
 
@@ -35,9 +36,17 @@ fn main() {
         for p in pics {
             n += 1;
             if let Some(o) = out.as_mut() {
-                o.write_all(&p.y).unwrap();
-                o.write_all(&p.u).unwrap();
-                o.write_all(&p.v).unwrap();
+                if let Some(w) = &p.wide {
+                    // little-endian 16-bit samples (yuv420p10le / yuv422p10le)
+                    for plane in [&w.y, &w.u, &w.v] {
+                        let bytes: Vec<u8> = plane.iter().flat_map(|v| v.to_le_bytes()).collect();
+                        o.write_all(&bytes).unwrap();
+                    }
+                } else {
+                    o.write_all(&p.y).unwrap();
+                    o.write_all(&p.u).unwrap();
+                    o.write_all(&p.v).unwrap();
+                }
             }
         }
     };
diff --git a/crates/h264/src/cabac_mb.rs b/crates/h264/src/cabac_mb.rs
--- a/crates/h264/src/cabac_mb.rs
+++ b/crates/h264/src/cabac_mb.rs
@@ -4,7 +4,7 @@
 use crate::cabac::Cabac;
 use crate::error::{Result, ensure, invalid};
 use crate::mbtypes::*;
-use crate::picture::MbKind;
+use crate::picture::{MbKind, Sample};
 use crate::slice::SliceType;
 use crate::slicedec::{MbCur, SliceDecoder};
 
@@ -33,11 +33,13 @@ const IDENT_CTX: [u8; 64] = {
 };
 /// ctxIdxInc = Min(levelListIdx / NumC8x8, 2) for 4:2:0 chroma DC (ctxBlockCat 3).
 const CHROMA_DC_CTX: [u8; 4] = [0, 1, 2, 2];
+/// The same for 4:2:2 chroma DC (NumC8x8 = 2).
+const CHROMA422_DC_CTX: [u8; 8] = [0, 0, 1, 1, 2, 2, 2, 2];
 
 /// Table 9-40 ctxIdxBlockCatOffset for ctxBlockCat 0..=5: (coded_block_flag, significant/last, abs level).
 const CAT_OFFSET: [(usize, usize, usize); 6] = [(0, 0, 0), (4, 15, 10), (8, 29, 20), (12, 44, 30), (16, 47, 39), (0, 0, 0)];
 
-impl SliceDecoder<'_> {
+impl<S: Sample> SliceDecoder<'_, S> {
     /// Decode slice_data() with CABAC. `rbsp` is the whole slice RBSP.
     pub fn decode_cabac(&mut self, rbsp: &[u8]) -> Result<()> {
         let start = self.sh.header_bits.div_ceil(8) * 8;
@@ -371,7 +373,8 @@ impl SliceDecoder<'_> {
         let n = if bx < 0 { self.nb[0] } else { self.nb[1] };
         let Some(m) = n else { return intra_cur as usize };
         let st = &self.pic.mbs[m];
-        let (x, y) = if bx < 0 { (1, by as usize) } else { (bx as usize, 1) };
+        let bottom = self.fmt.chroma_blocks() / 2 - 1;
+        let (x, y) = if bx < 0 { (1, by as usize) } else { (bx as usize, bottom) };
         (st.nnz_c[comp][y * 2 + x] != 0) as usize
     }
 
@@ -385,6 +388,7 @@ impl SliceDecoder<'_> {
         let (sig_base, last_base, abs_base) =
             if cat == 5 { (402, 417, 426) } else { (105 + CAT_OFFSET[cat].1, 166 + CAT_OFFSET[cat].1, 227 + CAT_OFFSET[cat].2) };
         let (sig_tab, last_tab): (&[u8], &[u8]) = match cat {
+            3 if max_num == 8 => (&CHROMA422_DC_CTX, &CHROMA422_DC_CTX),
             3 => (&CHROMA_DC_CTX, &CHROMA_DC_CTX),
             5 => (&SIG8_FRAME, &LAST8),
             _ => (&IDENT_CTX, &IDENT_CTX),
@@ -490,12 +494,13 @@ impl SliceDecoder<'_> {
             }
         }
         let cbp_c = cbp >> 4;
+        let nb = self.fmt.chroma_blocks();
         if cbp_c != 0 {
             for comp in 0..2 {
                 let bit = 1 + comp as u8;
                 let inc = self.cbf_dc_cond(self.nb[0], bit, intra) + 2 * self.cbf_dc_cond(self.nb[1], bit, intra);
-                let mut lv = [0i32; 4];
-                if self.residual_block_cabac(c, &mut lv, 3, 4, inc) > 0 {
+                let mut lv = [0i32; 8];
+                if self.residual_block_cabac(c, &mut lv, 3, nb, inc) > 0 {
                     self.mb_mut().cbf_dc |= 1 << bit;
                     self.put_chroma_dc(comp, &lv);
                 }
@@ -503,7 +508,7 @@ impl SliceDecoder<'_> {
         }
         if cbp_c == 2 {
             for comp in 0..2 {
-                for b in 0..4 {
+                for b in 0..nb {
                     let (bx, by) = ((b & 1) as i32, (b >> 1) as i32);
                     let inc = self.cbf_chroma_ac_cond(comp, bx - 1, by, intra) + 2 * self.cbf_chroma_ac_cond(comp, bx, by - 1, intra);
                     let mut lv = [0i32; 16];
@@ -525,15 +530,14 @@ impl SliceDecoder<'_> {
         if info.kind == MbKind::IPcm {
             // pcm alignment + samples, then re-initialise the arithmetic decoder
             let pos = c.bit_pos().div_ceil(8);
-            let data = c.data();
-            ensure!(data.len() >= pos + 384, "truncated I_PCM macroblock");
-            let Some(&samples) = data.get(pos..).and_then(|d| d.first_chunk::<384>()) else {
+            let n = self.pcm_bytes();
+            let Some(samples) = c.data().get(pos..pos + n) else {
                 return invalid("truncated I_PCM macroblock");
             };
-            c.set_bit_pos((pos + 384) * 8);
-            c.init_engine()?;
             self.finish_pcm();
-            self.write_pcm(&samples);
+            self.write_pcm(samples)?;
+            c.set_bit_pos((pos + n) * 8);
+            c.init_engine()?;
             self.prev_qp_delta_nz = false;
             return Ok(());
         }
diff --git a/crates/h264/src/deblock.rs b/crates/h264/src/deblock.rs
--- a/crates/h264/src/deblock.rs
+++ b/crates/h264/src/deblock.rs
@@ -1,6 +1,6 @@
-//! Deblocking filter (8.7) for progressive frames, 4:2:0, 8-bit.
+//! Deblocking filter (8.7) for progressive frames, 4:2:0 and 4:2:2, any bit depth.
 
-use crate::picture::{MbKind, MbState};
+use crate::picture::{MbKind, MbState, Sample};
 use crate::slicedec::{PicState, SliceInfo};
 use crate::tables::{ALPHA, BETA, TC0};
 
@@ -142,6 +142,8 @@ struct EdgeParams {
     tc0: [i32; 4],
     alpha: i32,
     beta: i32,
+    /// Largest sample value.
+    max: i32,
 }
 
 /// Filter one edge for `N` lines at once (lane `i` = line `i` across the edge). `s[k][i]` holds
@@ -150,7 +152,7 @@ struct EdgeParams {
 #[inline(always)]
 fn filter_lanes<const N: usize>(s: &mut [[i16; N]; 8], ep: &EdgeParams, chroma: bool) {
     let per = N / 4;
-    let (alpha, beta) = (ep.alpha as i16, ep.beta as i16);
+    let (alpha, beta, max) = (ep.alpha as i16, ep.beta as i16, ep.max as i16);
     let mut bs_on = [false; N];
     let mut tc0 = [0i16; N];
     for k in 0..4 {
@@ -182,7 +184,7 @@ fn filter_lanes<const N: usize>(s: &mut [[i16; N]; 8], ep: &EdgeParams, chroma:
             } else {
                 let tc = tc0[i] + 1;
                 let delta = ((((q0[i] - p0[i]) << 2) + (p1[i] - q1[i]) + 4) >> 3).max(-tc).min(tc);
-                ((p0[i] + delta).clamp(0, 255), (q0[i] - delta).clamp(0, 255))
+                ((p0[i] + delta).max(0).min(max), (q0[i] - delta).max(0).min(max))
             };
             s[3][i] = sel(on[i], np0, p0[i]);
             s[4][i] = sel(on[i], nq0, q0[i]);
@@ -217,8 +219,8 @@ fn filter_lanes<const N: usize>(s: &mut [[i16; N]; 8], ep: &EdgeParams, chroma:
             let avg = (p0[i] + q0[i] + 1) >> 1;
             let np1 = p1[i] + ((p2[i] + avg - (p1[i] << 1)) >> 1).max(-t0).min(t0);
             let nq1 = q1[i] + ((q2[i] + avg - (q1[i] << 1)) >> 1).max(-t0).min(t0);
-            s[3][i] = sel(on[i], (p0[i] + delta).clamp(0, 255), p0[i]);
-            s[4][i] = sel(on[i], (q0[i] - delta).clamp(0, 255), q0[i]);
+            s[3][i] = sel(on[i], (p0[i] + delta).max(0).min(max), p0[i]);
+            s[4][i] = sel(on[i], (q0[i] - delta).max(0).min(max), q0[i]);
             s[2][i] = sel(on[i] & ap, np1, p1[i]);
             s[5][i] = sel(on[i] & aq, nq1, q1[i]);
         }
@@ -227,60 +229,58 @@ fn filter_lanes<const N: usize>(s: &mut [[i16; N]; 8], ep: &EdgeParams, chroma:
 
 /// Filter a vertical edge whose q0 column is at `x`, for `N` lines starting at row `y`.
 #[inline(always)]
-fn filter_vertical<const N: usize>(pix: &mut [u8], stride: usize, x: usize, y: usize, ep: &EdgeParams, chroma: bool) {
-    // Each line's eight samples as one little-endian word: byte k = sample k (p3 .. q3).
-    let mut w = [0u64; N];
-    for (i, w) in w.iter_mut().enumerate() {
+fn filter_vertical<S: Sample, const N: usize>(pix: &mut [S], stride: usize, x: usize, y: usize, ep: &EdgeParams, chroma: bool) {
+    let mut s = [[0i16; N]; 8];
+    for i in 0..N {
         let o = (y + i) * stride + x - 4;
-        let Some(&b) = pix.get(o..).and_then(<[u8]>::first_chunk::<8>) else {
+        let Some(line) = pix.get(o..).and_then(<[S]>::first_chunk::<8>) else {
             return;
         };
-        *w = u64::from_le_bytes(b);
-    }
-    let mut s = [[0i16; N]; 8];
-    for (k, row) in s.iter_mut().enumerate() {
-        for i in 0..N {
-            row[i] = ((w[i] >> (8 * k)) & 0xff) as i16;
+        for (k, row) in s.iter_mut().enumerate() {
+            row[i] = line[k].to_i16();
         }
     }
     filter_lanes(&mut s, ep, chroma);
-    for (i, w) in w.iter_mut().enumerate() {
-        *w = (0..8).fold(0u64, |acc, k| acc | ((s[k][i] as u8 as u64) << (8 * k)));
-    }
-    for (i, w) in w.iter().enumerate() {
+    let (k0, k1) = if chroma { (3, 5) } else { (1, 7) };
+    for i in 0..N {
         let o = (y + i) * stride + x - 4;
-        pix[o..o + 8].copy_from_slice(&w.to_le_bytes());
+        let Some(line) = pix.get_mut(o..).and_then(<[S]>::first_chunk_mut::<8>) else {
+            return;
+        };
+        for k in k0..k1 {
+            line[k] = S::clip(s[k][i] as i32, ep.max);
+        }
     }
 }
 
 /// Filter a horizontal edge whose q0 row is `y`, for `N` columns starting at `x`.
 #[inline(always)]
-fn filter_horizontal<const N: usize>(pix: &mut [u8], stride: usize, x: usize, y: usize, ep: &EdgeParams, chroma: bool) {
+fn filter_horizontal<S: Sample, const N: usize>(pix: &mut [S], stride: usize, x: usize, y: usize, ep: &EdgeParams, chroma: bool) {
     let base = (y - 4) * stride + x;
     let mut s = [[0i16; N]; 8];
     for (k, row) in s.iter_mut().enumerate() {
-        let Some(src) = pix.get(base + k * stride..).and_then(<[u8]>::first_chunk::<N>) else {
+        let Some(src) = pix.get(base + k * stride..).and_then(<[S]>::first_chunk::<N>) else {
             return;
         };
         for i in 0..N {
-            row[i] = src[i] as i16;
+            row[i] = src[i].to_i16();
         }
     }
     filter_lanes(&mut s, ep, chroma);
     let (k0, k1) = if chroma { (3, 5) } else { (1, 7) };
     for (k, row) in s.iter().enumerate().take(k1).skip(k0) {
-        let Some(dst) = pix.get_mut(base + k * stride..).and_then(<[u8]>::first_chunk_mut::<N>) else {
+        let Some(dst) = pix.get_mut(base + k * stride..).and_then(<[S]>::first_chunk_mut::<N>) else {
             return;
         };
         for i in 0..N {
-            dst[i] = row[i] as u8;
+            dst[i] = S::clip(row[i] as i32, ep.max);
         }
     }
 }
 
 /// Deblock one macroblock (macroblocks must be processed in raster order).
 #[allow(clippy::needless_range_loop)]
-pub fn deblock_mb(pic: &mut PicState, addr: usize, mb_w: usize) {
+pub fn deblock_mb<S: Sample>(pic: &mut PicState<S>, addr: usize, mb_w: usize) {
     let q = &pic.mbs[addr];
     if q.slice_num == u32::MAX {
         return;
@@ -311,9 +311,7 @@ pub fn deblock_mb(pic: &mut PicState, addr: usize, mb_w: usize) {
                 bs[dir][0] = [4; 4];
             }
             for e in 1..4 {
-                if !(t8 && e % 2 == 1) {
-                    bs[dir][e] = [3; 4];
-                }
+                bs[dir][e] = [3; 4];
             }
         }
     } else {
@@ -343,9 +341,6 @@ pub fn deblock_mb(pic: &mut PicState, addr: usize, mb_w: usize) {
                 continue;
             }
             for e in 1..4 {
-                if t8 && e % 2 == 1 {
-                    continue;
-                }
                 for k in 0..4 {
                     let (rq, rp) = if dir == 0 { (k * 4 + e, k * 4 + e - 1) } else { (e * 4 + k, e * 4 + k - 4) };
                     bs[dir][e][k] = if (q.nz_mask >> rp) & 1 != 0 || (q.nz_mask >> rq) & 1 != 0 { 2 } else { motion_bs(&qm[rp], &qm[rq]) };
@@ -354,6 +349,10 @@ pub fn deblock_mb(pic: &mut PicState, addr: usize, mb_w: usize) {
         }
     }
     let qp_of = |st: &MbState| if st.kind == MbKind::IPcm { 0 } else { st.qp as i32 };
+    let fmt = pic.planes.fmt;
+    // alpha, beta and tC0 scale with the bit depth (8-232, 8-233, 8-235)
+    let scale = 1 << (fmt.bit_depth - 8);
+    let max = fmt.max();
     let q_qp = qp_of(q);
     let q_qpc = q.qpc;
     let p_qp = [left.map(|n| (qp_of(&pic.mbs[n]), pic.mbs[n].qpc)), top.map(|n| (qp_of(&pic.mbs[n]), pic.mbs[n].qpc))];
@@ -362,14 +361,21 @@ pub fn deblock_mb(pic: &mut PicState, addr: usize, mb_w: usize) {
         let index_a = (qpav + alpha_off).clamp(0, 51) as usize;
         let index_b = (qpav + beta_off).clamp(0, 51) as usize;
         let tc = |b: u8| if (1..4).contains(&b) { TC0[index_a][b as usize - 1] as i32 } else { 0 };
-        EdgeParams { bs: b, tc0: [tc(b[0]), tc(b[1]), tc(b[2]), tc(b[3])], alpha: ALPHA[index_a] as i32, beta: BETA[index_b] as i32 }
+        EdgeParams {
+            bs: b,
+            tc0: [tc(b[0]) * scale, tc(b[1]) * scale, tc(b[2]) * scale, tc(b[3]) * scale],
+            alpha: ALPHA[index_a] as i32 * scale,
+            beta: BETA[index_b] as i32 * scale,
+            max,
+        }
     };
     let width = pic.planes.width;
     let cwidth = pic.planes.cwidth;
     // luma
     for dir in 0..2 {
         for e in 0..4 {
-            if bs[dir][e] == [0; 4] {
+            // with the 8x8 transform only the edges of 8x8 blocks are luma transform edges
+            if bs[dir][e] == [0; 4] || (t8 && e % 2 == 1) {
                 continue;
             }
             let qpp = if e == 0 { p_qp[dir].map(|p| p.0).unwrap_or(q_qp) } else { q_qp };
@@ -380,30 +386,39 @@ pub fn deblock_mb(pic: &mut PicState, addr: usize, mb_w: usize) {
                 continue;
             }
             if dir == 0 {
-                filter_vertical::<16>(&mut pic.planes.y, width, mx * 16 + e * 4, my * 16, &ep, false);
+                filter_vertical::<S, 16>(&mut pic.planes.y, width, mx * 16 + e * 4, my * 16, &ep, false);
             } else {
-                filter_horizontal::<16>(&mut pic.planes.y, width, mx * 16, my * 16 + e * 4, &ep, false);
+                filter_horizontal::<S, 16>(&mut pic.planes.y, width, mx * 16, my * 16 + e * 4, &ep, false);
             }
         }
     }
-    // chroma (4:2:0): edges at chroma samples 0 and 4 use the bS of luma edges 0 and 2
+    // Chroma edges use the bS of the luma edge at the corresponding luma position (8.7.2.1):
+    // vertical chroma edges 0 / 4 are luma edges 0 / 2; horizontal chroma edges 0 / 4 are luma
+    // edges 0 / 2 in 4:2:0, and edges 0 / 4 / 8 / 12 luma edges 0..=3 in 4:2:2 (chroma blocks
+    // always use the 4x4 transform).
+    let hc = fmt.mb_hc;
     for c in 0..2 {
         for dir in 0..2 {
-            for ce in 0..2 {
-                let e = ce * 2;
+            let n_edges = if dir == 1 && hc == 16 { 4 } else { 2 };
+            for ce in 0..n_edges {
+                let e = if n_edges == 4 { ce } else { ce * 2 };
                 if bs[dir][e] == [0; 4] {
                     continue;
                 }
-                let qpp = if e == 0 { p_qp[dir].map(|p| p.1[c]).unwrap_or(q_qpc[c]) } else { q_qpc[c] } as i32;
+                let qpp = if ce == 0 { p_qp[dir].map(|p| p.1[c]).unwrap_or(q_qpc[c]) } else { q_qpc[c] } as i32;
                 let ep = params(qpp, q_qpc[c] as i32, bs[dir][e]);
                 if ep.alpha == 0 || ep.beta == 0 {
                     continue;
                 }
                 let plane = if c == 0 { &mut pic.planes.cb } else { &mut pic.planes.cr };
                 if dir == 0 {
-                    filter_vertical::<8>(plane, cwidth, mx * 8 + ce * 4, my * 8, &ep, true);
+                    if hc == 16 {
+                        filter_vertical::<S, 16>(plane, cwidth, mx * 8 + ce * 4, my * 16, &ep, true);
+                    } else {
+                        filter_vertical::<S, 8>(plane, cwidth, mx * 8 + ce * 4, my * 8, &ep, true);
+                    }
                 } else {
-                    filter_horizontal::<8>(plane, cwidth, mx * 8, my * 8 + ce * 4, &ep, true);
+                    filter_horizontal::<S, 8>(plane, cwidth, mx * 8, my * hc + ce * 4, &ep, true);
                 }
             }
         }
@@ -432,7 +447,7 @@ mod tests {
             let index_a = (next() % 52) as usize;
             let index_b = (next() % 52) as usize;
             let tc = |b: u8| if (1..4).contains(&b) { TC0[index_a][b as usize - 1] as i32 } else { 0 };
-            let ep = EdgeParams { bs, tc0: bs.map(tc), alpha: ALPHA[index_a] as i32, beta: BETA[index_b] as i32 };
+            let ep = EdgeParams { bs, tc0: bs.map(tc), alpha: ALPHA[index_a] as i32, beta: BETA[index_b] as i32, max: 255 };
             let base = (next() % 256) as i32;
             let spread = 1 + (next() % 24) as i32;
             let mut lines = [[0u8; 8]; 16];
@@ -484,9 +499,9 @@ mod speed {
         let (w, h) = (1024usize, 256usize);
         let mut pix: Vec<u8> = (0..w * h).map(|i| (128 + ((i * 7919) % 13) as i32 - 6) as u8).collect();
         let eps = [
-            EdgeParams { bs: [4; 4], tc0: [0; 4], alpha: 40, beta: 9 },
-            EdgeParams { bs: [3; 4], tc0: [2; 4], alpha: 40, beta: 9 },
-            EdgeParams { bs: [2, 1, 0, 2], tc0: [1, 1, 0, 1], alpha: 40, beta: 9 },
+            EdgeParams { bs: [4; 4], tc0: [0; 4], alpha: 40, beta: 9, max: 255 },
+            EdgeParams { bs: [3; 4], tc0: [2; 4], alpha: 40, beta: 9, max: 255 },
+            EdgeParams { bs: [2, 1, 0, 2], tc0: [1, 1, 0, 1], alpha: 40, beta: 9, max: 255 },
         ];
         let t0 = std::time::Instant::now();
         let mut n = 0;
@@ -494,7 +509,7 @@ mod speed {
             for my in 1..h / 16 {
                 for mx in 1..w / 16 {
                     for (j, ep) in eps.iter().enumerate() {
-                        filter_horizontal::<16>(&mut pix, w, mx * 16, my * 16 + j * 4, ep, false);
+                        filter_horizontal::<u8, 16>(&mut pix, w, mx * 16, my * 16 + j * 4, ep, false);
                         n += 1;
                     }
                 }
@@ -506,7 +521,7 @@ mod speed {
             for my in 1..h / 16 {
                 for mx in 1..w / 16 {
                     for (j, ep) in eps.iter().enumerate() {
-                        filter_vertical::<16>(&mut pix, w, mx * 16 + j * 4, my * 16, ep, false);
+                        filter_vertical::<u8, 16>(&mut pix, w, mx * 16 + j * 4, my * 16, ep, false);
                     }
                 }
             }
diff --git a/crates/h264/src/decoder.rs b/crates/h264/src/decoder.rs
--- a/crates/h264/src/decoder.rs
+++ b/crates/h264/src/decoder.rs
@@ -9,11 +9,11 @@
 use crate::dpb::{Dpb, Output, OutputMeta, RefMark};
 use crate::error::{Error, Result, ensure, invalid, unsupported};
 use crate::params::{Pps, Sps};
-use crate::picture::{Frame, FrameRef, MbKind, MbState, Planes, RefPic};
+use crate::picture::{Format, Frame, FrameRef, MbKind, MbState, Planes, RefPic, Sample};
 use crate::slice::{NalHeader, Poc, PocState, SliceHeader, SliceType, is_new_picture, nal_type};
 use crate::slicedec::{PicState, SliceDecoder};
 use crate::transform::LevelScale;
-use crate::{ColorInfo, Picture};
+use crate::{ColorInfo, Picture, WidePlanes};
 use filmcraft_bitstream::{BitReader, annexb_nals, length_prefixed_nals, unescape_rbsp};
 use std::collections::VecDeque;
 use std::sync::{Arc, Mutex};
@@ -76,8 +76,31 @@ pub struct DecodeStats {
 struct Shared {
     error: Mutex<Option<Error>>,
     stats: Mutex<DecodeStats>,
-    /// Recycled picture buffers.
-    pool: Mutex<Vec<PicState>>,
+    /// Recycled picture buffers (8-bit and high bit depth pictures).
+    pool: Mutex<Vec<PicState<u8>>>,
+    pool16: Mutex<Vec<PicState<u16>>>,
+}
+
+/// The recycled picture buffers of one sample type.
+trait PoolSample: Sample {
+    fn pool(shared: &Shared) -> &Mutex<Vec<PicState<Self>>>;
+}
+
+impl PoolSample for u8 {
+    fn pool(shared: &Shared) -> &Mutex<Vec<PicState<u8>>> {
+        &shared.pool
+    }
+}
+
+impl PoolSample for u16 {
+    fn pool(shared: &Shared) -> &Mutex<Vec<PicState<u16>>> {
+        &shared.pool16
+    }
+}
+
+/// Sample format of the pictures of a (supported) SPS.
+fn format_of(sps: &Sps) -> Format {
+    Format { bit_depth: sps.bit_depth_luma, mb_hc: if sps.chroma_array_type() == 2 { 16 } else { 8 } }
 }
 
 /// H.264 decoder.
@@ -349,6 +372,9 @@ impl Decoder {
         if matches!(sh.slice_type, SliceType::Sp | SliceType::Si) {
             return unsupported("SP/SI slices");
         }
+        if sps.chroma_array_type() == 2 && !sh.slice_type.is_intra() {
+            return unsupported("inter prediction in 4:2:2 streams (only intra pictures are implemented)");
+        }
         if sh.redundant_pic_cnt > 0 {
             return Ok(()); // redundant slices are ignored
         }
@@ -408,7 +434,7 @@ impl Decoder {
         // Activate SPS; a resolution change flushes the DPB.
         let changed = match &self.active_sps {
             None => true,
-            Some(a) => a.width() != sps.width() || a.height() != sps.height() || a.max_dpb_frames() != sps.max_dpb_frames(),
+            Some(a) => a.width() != sps.width() || a.height() != sps.height() || a.max_dpb_frames() != sps.max_dpb_frames() || format_of(a) != format_of(sps),
         };
         if changed && self.active_sps.is_some() {
             let mut outs = Vec::new();
@@ -421,6 +447,7 @@ impl Decoder {
         self.dpb.max_reorder = sps.max_num_reorder_frames().min(self.dpb.capacity);
         let mb_w = sps.pic_width_in_mbs as usize;
         let mb_h = sps.frame_height_in_mbs() as usize;
+        let fmt = format_of(sps);
         if sh.idr {
             let mut outs = Vec::new();
             self.dpb.idr(sh.no_output_of_prior_pics, &mut outs);
@@ -442,18 +469,12 @@ impl Decoder {
                 let mut make = |_fnum: u32| {
                     let id = *next_id;
                     *next_id += 1;
-                    let planes = match &last {
-                        Some(f) if f.mb_w == mb_w && f.mb_h() == mb_h => {
-                            let mut p = Planes::new(mb_w * 16, mb_h * 16);
-                            let (y, u, v) = f.copy_cropped((0, 0, mb_w * 16, mb_h * 16));
-                            p.y = y;
-                            p.cb = u;
-                            p.cr = v;
-                            p
-                        }
-                        _ => Planes::gray(mb_w * 16, mb_h * 16),
+                    let frame = if fmt.wide() {
+                        concealment_frame::<u16>(last.as_deref(), id, mb_w, mb_h, fmt)
+                    } else {
+                        concealment_frame::<u8>(last.as_deref(), id, mb_w, mb_h, fmt)
                     };
-                    (Arc::new(Frame::from_planes(id, 0, &planes)), 0, meta.clone())
+                    (Arc::new(frame), 0, meta.clone())
                 };
                 let mut upd = |fnum: u32| poc_state.update_gap_frame(fnum, sps);
                 self.shared.stats.lock().map(|mut s| s.frame_num_gaps += 1).ok();
@@ -466,7 +487,7 @@ impl Decoder {
         let frame_poc = if sh.has_mmco5() { 0 } else { poc.frame() };
         let id = self.next_id;
         self.next_id += 1;
-        let frame = Arc::new(Frame::new(id, frame_poc, mb_w, mb_h));
+        let frame = Arc::new(Frame::new(id, frame_poc, mb_w, mb_h, fmt));
         self.pending = Some(PendingPic { frame, sps: sps.clone(), first: sh.clone(), poc, pts, key: sh.idr, slices: Vec::new() });
         Ok(())
     }
@@ -511,11 +532,36 @@ impl Decoder {
     }
 }
 
+/// A "non-existing" frame for a frame_num gap: a copy of `last` (when it has the same size and
+/// format), else grey.
+fn concealment_frame<S: Sample>(last: Option<&Frame>, id: u32, mb_w: usize, mb_h: usize, fmt: Format) -> Frame {
+    let mut p = Planes::<S>::gray(mb_w * 16, mb_h * 16, fmt);
+    if let Some(f) = last
+        && f.mb_w == mb_w
+        && f.mb_h() == mb_h
+        && f.fmt == fmt
+        && let Some((y, u, v)) = f.copy_cropped::<S>((0, 0, mb_w * 16, mb_h * 16))
+    {
+        p.y = y;
+        p.cb = u;
+        p.cr = v;
+    }
+    Frame::from_planes(id, 0, &p)
+}
+
 /// Decode all slices of one picture and publish its rows.
 fn run_job(frame: FrameRef, slices: Vec<SliceJob>, shared: &Shared, draft: bool) {
-    let reuse = shared.pool.lock().ok().and_then(|mut p| p.pop());
-    let mut pic = match reuse {
-        Some(mut p) if p.mb_w == frame.mb_w && p.mb_h == frame.mb_h() => {
+    if frame.fmt.wide() {
+        run_job_typed::<u16>(frame, slices, shared, draft);
+    } else {
+        run_job_typed::<u8>(frame, slices, shared, draft);
+    }
+}
+
+fn run_job_typed<S: PoolSample>(frame: FrameRef, slices: Vec<SliceJob>, shared: &Shared, draft: bool) {
+    let reuse = S::pool(shared).lock().ok().and_then(|mut p| p.pop());
+    let mut pic: PicState<S> = match reuse {
+        Some(mut p) if p.mb_w == frame.mb_w && p.mb_h == frame.mb_h() && p.planes.fmt == frame.fmt => {
             p.reset(frame.clone());
             p
         }
@@ -536,7 +582,7 @@ fn run_job(frame: FrameRef, slices: Vec<SliceJob>, shared: &Shared, draft: bool)
         Ok(e) => e,
         Err(_) => {
             // Make sure readers never block on this frame.
-            let fallback = Planes::gray(frame.width, frame.height);
+            let fallback = Planes::<S>::gray(frame.width, frame.height, frame.fmt);
             let blank = vec![MbState::default(); frame.mb_w * frame.mb_h()];
             for r in 0..frame.mb_h() {
                 if !frame.is_published(r) {
@@ -570,14 +616,14 @@ fn run_job(frame: FrameRef, slices: Vec<SliceJob>, shared: &Shared, draft: bool)
     if let (Some(e), Ok(mut slot)) = (err, shared.error.lock()) {
         slot.get_or_insert(e);
     }
-    if let Ok(mut p) = shared.pool.lock()
+    if let Ok(mut p) = S::pool(shared).lock()
         && p.len() < 4
     {
         p.push(pic);
     }
 }
 
-fn decode_slice(s: &SliceJob, pic: &mut PicState) -> Result<()> {
+fn decode_slice<S: Sample>(s: &SliceJob, pic: &mut PicState<S>) -> Result<()> {
     let mut sd = SliceDecoder::new(&s.sh, &s.pps, &s.sps, pic, &s.refs, &s.ls)?;
     if s.pps.entropy_coding_mode {
         sd.decode_cabac(&s.rbsp)
@@ -606,9 +652,19 @@ fn output_meta(sps: &Sps, pts: i64, key: bool) -> OutputMeta {
 fn make_picture(o: &Output<FrameRef>) -> Picture {
     let (cx, cy, cw, ch) = o.meta.crop;
     let (cx, cy, cw, ch) = (cx as usize, cy as usize, cw as usize, ch as usize);
-    let (y, u, v) = o.frame.copy_cropped((cx, cy, cw, ch));
-    let (ccw, cch) = (cw.div_ceil(2), ch.div_ceil(2));
+    let fmt = o.frame.fmt;
+    let crop = (cx, cy, cw, ch);
+    let ((y, u, v), wide) = if fmt.wide() {
+        let (y, u, v) = o.frame.copy_cropped::<u16>(crop).unwrap_or_default();
+        ((Vec::new(), Vec::new(), Vec::new()), Some(WidePlanes { y, u, v }))
+    } else {
+        (o.frame.copy_cropped::<u8>(crop).unwrap_or_default(), None)
+    };
+    let (ccw, cch) = (cw.div_ceil(2), if fmt.mb_hc == 16 { ch } else { ch.div_ceil(2) });
     Picture {
+        wide,
+        bit_depth: fmt.bit_depth,
+        chroma_format_idc: fmt.chroma_format_idc(),
         width: cw as u32,
         height: ch as u32,
         chroma_width: ccw as u32,
diff --git a/crates/h264/src/inter.rs b/crates/h264/src/inter.rs
--- a/crates/h264/src/inter.rs
+++ b/crates/h264/src/inter.rs
@@ -1,5 +1,7 @@
 //! Inter prediction sample interpolation (8.4.2.2) and weighted sample prediction (8.4.2.3).
 
+use crate::picture::Sample;
+
 /// A reference plane (used by the plane-based helpers in tests).
 #[cfg(test)]
 #[derive(Clone, Copy)]
@@ -45,48 +47,49 @@ fn tap6(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32) -> i32 {
     a - 5 * b + 20 * c + 20 * d - 5 * e + f
 }
 
+#[cfg(test)]
 #[inline(always)]
 fn clip1(v: i32) -> u8 {
     v.clamp(0, 255) as u8
 }
 
-/// Copy `n` bytes with a fixed-size move for the common block widths.
+/// Copy `n` samples with a fixed-size move for the common block widths.
 #[inline(always)]
-fn copy_n(dst: &mut [u8], src: &[u8], n: usize) {
-    fn fixed<const N: usize>(dst: &mut [u8], src: &[u8]) {
+fn copy_n<S: Sample>(dst: &mut [S], src: &[S], n: usize) {
+    fn fixed<S: Sample, const N: usize>(dst: &mut [S], src: &[S]) {
         if let (Some(d), Some(s)) = (dst.first_chunk_mut::<N>(), src.first_chunk::<N>()) {
             *d = *s;
         }
     }
     match n {
-        16 => fixed::<16>(dst, src),
-        8 => fixed::<8>(dst, src),
-        4 => fixed::<4>(dst, src),
-        2 => fixed::<2>(dst, src),
+        16 => fixed::<S, 16>(dst, src),
+        8 => fixed::<S, 8>(dst, src),
+        4 => fixed::<S, 4>(dst, src),
+        2 => fixed::<S, 2>(dst, src),
         _ => dst[..n].copy_from_slice(&src[..n]),
     }
 }
 
 #[inline(always)]
-fn avg(a: u8, b: u8) -> u8 {
-    ((a as u16 + b as u16 + 1) >> 1) as u8
+fn avg<S: Sample>(a: S, b: S, max: i32) -> S {
+    S::clip((a.to_i32() + b.to_i32() + 1) >> 1, max)
 }
 
 /// Horizontal 6-tap (unclipped) at every position of `row` for `n` outputs: out[c] uses row[c..c + 6].
 #[inline(always)]
-fn htap_row(row: &[u8], out: &mut [i32], n: usize) {
+fn htap_row<S: Sample>(row: &[S], out: &mut [i32], n: usize) {
     let row = &row[..n + 5];
     for (o, w) in out[..n].iter_mut().zip(row.windows(6)) {
-        *o = tap6(w[0] as i32, w[1] as i32, w[2] as i32, w[3] as i32, w[4] as i32, w[5] as i32);
+        *o = tap6(w[0].to_i32(), w[1].to_i32(), w[2].to_i32(), w[3].to_i32(), w[4].to_i32(), w[5].to_i32());
     }
 }
 
 /// Vertical 6-tap (unclipped) over six rows at columns 0..n.
 #[inline(always)]
-fn vtap_row(rows: [&[u8]; 6], out: &mut [i32], n: usize) {
+fn vtap_row<S: Sample>(rows: [&[S]; 6], out: &mut [i32], n: usize) {
     let [a, b, c, d, e, f] = rows.map(|r| &r[..n]);
     for i in 0..n {
-        out[i] = tap6(a[i] as i32, b[i] as i32, c[i] as i32, d[i] as i32, e[i] as i32, f[i] as i32);
+        out[i] = tap6(a[i].to_i32(), b[i].to_i32(), c[i].to_i32(), d[i].to_i32(), e[i].to_i32(), f[i].to_i32());
     }
 }
 
@@ -104,13 +107,15 @@ pub fn mc_luma(p: PlaneRef, x: i32, y: i32, fx: u32, fy: u32, bw: usize, bh: usi
         fetch_window(p, x, y, bw, bh, &mut win);
         (&win[..], WIN)
     };
-    mc_luma_win(src, ss, fx, fy, bw, bh, out, os);
+    mc_luma_win(src, ss, fx, fy, bw, bh, out, os, 255);
 }
 
 /// Luma interpolation from a prepared window: `src[0]` is the sample at (x - 2, y - 2) and `ss` the
 /// window stride; the window covers (bw + 5) x (bh + 5) samples.
 #[allow(clippy::too_many_arguments)]
-pub fn mc_luma_win(src: &[u8], ss: usize, fx: u32, fy: u32, bw: usize, bh: usize, out: &mut [u8], os: usize) {
+pub fn mc_luma_win<S: Sample>(src: &[S], ss: usize, fx: u32, fy: u32, bw: usize, bh: usize, out: &mut [S], os: usize, max: i32) {
+    let clip1 = |v: i32| S::clip(v, max);
+    let avg = |a: S, b: S| avg(a, b, max);
     // window row r (0..bh+5) starts at src[r * ss]; sample G of block (r, c) is at window (r + 2, c + 2)
     let wrow = |r: usize| &src[r * ss..r * ss + bw + 5];
     let mut t = [0i32; 16];
@@ -236,14 +241,14 @@ pub fn mc_chroma(p: PlaneRef, x: i32, y: i32, fx: u32, fy: u32, bw: usize, bh: u
         }
         (&win[..], 9)
     };
-    mc_chroma_win(src, ss, fx, fy, bw, bh, out, os);
+    mc_chroma_win(src, ss, fx, fy, bw, bh, out, os, 255);
 }
 
 /// Chroma interpolation from a prepared (bw + 1) x (bh + 1) window whose first sample is the integer
 /// position of the block.
 #[allow(clippy::too_many_arguments)]
-pub fn mc_chroma_win(src: &[u8], ss: usize, fx: u32, fy: u32, bw: usize, bh: usize, out: &mut [u8], os: usize) {
-    let (fx, fy) = (fx as u16, fy as u16);
+pub fn mc_chroma_win<S: Sample>(src: &[S], ss: usize, fx: u32, fy: u32, bw: usize, bh: usize, out: &mut [S], os: usize, max: i32) {
+    let (fx, fy) = (fx as i32, fy as i32);
     let (w00, w10, w01, w11) = ((8 - fx) * (8 - fy), fx * (8 - fy), (8 - fx) * fy, fx * fy);
     if fx == 0 && fy == 0 {
         for r in 0..bh {
@@ -256,8 +261,8 @@ pub fn mc_chroma_win(src: &[u8], ss: usize, fx: u32, fy: u32, bw: usize, bh: usi
         let r1 = &src[(r + 1) * ss..(r + 1) * ss + bw + 1];
         let o = &mut out[r * os..r * os + bw];
         for (c, o) in o.iter_mut().enumerate() {
-            let v = w00 * r0[c] as u16 + w10 * r0[c + 1] as u16 + w01 * r1[c] as u16 + w11 * r1[c + 1] as u16;
-            *o = ((v + 32) >> 6) as u8;
+            let v = w00 * r0[c].to_i32() + w10 * r0[c + 1].to_i32() + w01 * r1[c].to_i32() + w11 * r1[c + 1].to_i32();
+            *o = S::clip((v + 32) >> 6, max);
         }
     }
 }
@@ -273,7 +278,8 @@ pub enum Weight {
 
 /// Combine prediction(s) into `dst` (stride `ds`). `p0`/`p1` have stride `ps`.
 #[allow(clippy::too_many_arguments)]
-pub fn weighted_store(dst: &mut [u8], ds: usize, p0: Option<&[u8]>, p1: Option<&[u8]>, ps: usize, bw: usize, bh: usize, w: Weight) {
+pub fn weighted_store<S: Sample>(dst: &mut [S], ds: usize, p0: Option<&[S]>, p1: Option<&[S]>, ps: usize, bw: usize, bh: usize, w: Weight, max: i32) {
+    let clip1 = |v: i32| S::clip(v, max);
     match (p0, p1, w) {
         (Some(a), Some(b), Weight::Default) => {
             for r in 0..bh {
@@ -281,7 +287,7 @@ pub fn weighted_store(dst: &mut [u8], ds: usize, p0: Option<&[u8]>, p1: Option<&
                 let ra = &a[r * ps..r * ps + bw];
                 let rb = &b[r * ps..r * ps + bw];
                 for ((d, &x), &y) in d.iter_mut().zip(ra).zip(rb) {
-                    *d = ((x as u32 + y as u32 + 1) >> 1) as u8;
+                    *d = S::clip((x.to_i32() + y.to_i32() + 1) >> 1, max);
                 }
             }
         }
@@ -294,7 +300,7 @@ pub fn weighted_store(dst: &mut [u8], ds: usize, p0: Option<&[u8]>, p1: Option<&
                 let ra = &a[r * ps..r * ps + bw];
                 let rb = &b[r * ps..r * ps + bw];
                 for ((d, &x), &y) in d.iter_mut().zip(ra).zip(rb) {
-                    *d = clip1(((x as i32 * w0 + y as i32 * w1 + round) >> sh) + off);
+                    *d = clip1(((x.to_i32() * w0 + y.to_i32() * w1 + round) >> sh) + off);
                 }
             }
         }
@@ -314,7 +320,7 @@ pub fn weighted_store(dst: &mut [u8], ds: usize, p0: Option<&[u8]>, p1: Option<&
                 let d = &mut dst[r * ds..r * ds + bw];
                 let ra = &a[r * ps..r * ps + bw];
                 for (d, &x) in d.iter_mut().zip(ra) {
-                    *d = clip1(((x as i32 * wt + round) >> log_wd) + o);
+                    *d = clip1(((x.to_i32() * wt + round) >> log_wd) + o);
                 }
             }
         }
@@ -505,9 +511,9 @@ mod tests {
         let a = [100u8; 4];
         let b = [50u8; 4];
         let mut d = [0u8; 4];
-        weighted_store(&mut d, 4, Some(&a), Some(&b), 4, 4, 1, Weight::Default);
+        weighted_store(&mut d, 4, Some(&a), Some(&b), 4, 4, 1, Weight::Default, 255);
         assert_eq!(d, [75; 4]);
-        weighted_store(&mut d, 4, Some(&a), None, 4, 4, 1, Weight::Weighted { log_wd: 5, w0: 16, w1: 0, o0: 3, o1: 0 });
+        weighted_store(&mut d, 4, Some(&a), None, 4, 4, 1, Weight::Weighted { log_wd: 5, w0: 16, w1: 0, o0: 3, o1: 0 }, 255);
         assert_eq!(d, [53; 4]);
     }
 }
diff --git a/crates/h264/src/intra.rs b/crates/h264/src/intra.rs
--- a/crates/h264/src/intra.rs
+++ b/crates/h264/src/intra.rs
@@ -1,4 +1,7 @@
-//! Intra prediction (8.3): Intra_4x4, Intra_8x8 (with reference filtering), Intra_16x16 and chroma.
+//! Intra prediction (8.3): Intra_4x4, Intra_8x8 (with reference filtering), Intra_16x16 and chroma
+//! (4:2:0 and 4:2:2), for any sample type / bit depth.
+
+use crate::picture::{Format, Sample};
 
 /// Neighbouring sample availability for one block.
 #[derive(Clone, Copy, Debug, Default)]
@@ -30,16 +33,16 @@ impl Edge {
     }
 }
 
-fn load_edge(plane: &[u8], stride: usize, x: usize, y: usize, n: usize, a: Avail) -> Edge {
-    let mut e = Edge { top: [128; 16], left: [128; 8], tl: 128 };
+fn load_edge<S: Sample>(plane: &[S], stride: usize, x: usize, y: usize, n: usize, a: Avail, mid: i32) -> Edge {
+    let mut e = Edge { top: [mid; 16], left: [mid; 8], tl: mid };
     if a.top {
         let row = &plane[(y - 1) * stride + x..];
         for i in 0..n {
-            e.top[i] = row[i] as i32;
+            e.top[i] = row[i].to_i32();
         }
         if a.top_right {
             for i in n..2 * n {
-                e.top[i] = row[i] as i32;
+                e.top[i] = row[i].to_i32();
             }
         } else {
             let v = e.top[n - 1];
@@ -50,18 +53,19 @@ fn load_edge(plane: &[u8], stride: usize, x: usize, y: usize, n: usize, a: Avail
     }
     if a.left {
         for i in 0..n {
-            e.left[i] = plane[(y + i) * stride + x - 1] as i32;
+            e.left[i] = plane[(y + i) * stride + x - 1].to_i32();
         }
     }
     if a.top_left {
-        e.tl = plane[(y - 1) * stride + x - 1] as i32;
+        e.tl = plane[(y - 1) * stride + x - 1].to_i32();
     }
     e
 }
 
 /// Directional / DC prediction shared by Intra_4x4 and Intra_8x8 (8.3.1.2.x, 8.3.2.2.x).
-fn pred_nxn(mode: u8, e: &Edge, n: i32, a: Avail, out: &mut [u8], stride: usize) {
-    let mut put = |x: i32, y: i32, v: i32| out[y as usize * stride + x as usize] = v as u8;
+fn pred_nxn<S: Sample>(mode: u8, e: &Edge, n: i32, a: Avail, out: &mut [S], stride: usize, fmt: Format) {
+    let max = fmt.max();
+    let mut put = |x: i32, y: i32, v: i32| out[y as usize * stride + x as usize] = S::clip(v, max);
     match mode {
         0 => {
             for y in 0..n {
@@ -85,7 +89,7 @@ fn pred_nxn(mode: u8, e: &Edge, n: i32, a: Avail, out: &mut [u8], stride: usize)
                 (true, true) => (st + sl + n) >> (shift + 1),
                 (false, true) => (sl + (n >> 1)) >> shift,
                 (true, false) => (st + (n >> 1)) >> shift,
-                _ => 128,
+                _ => fmt.mid(),
             };
             for y in 0..n {
                 for x in 0..n {
@@ -185,14 +189,14 @@ fn pred_nxn(mode: u8, e: &Edge, n: i32, a: Avail, out: &mut [u8], stride: usize)
 }
 
 /// Intra_4x4 prediction written into `plane` at (x, y).
-pub fn pred4x4(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8, a: Avail) {
-    let e = load_edge(plane, stride, x, y, 4, a);
-    pred_nxn(mode, &e, 4, a, &mut plane[y * stride + x..], stride);
+pub fn pred4x4<S: Sample>(plane: &mut [S], stride: usize, x: usize, y: usize, mode: u8, a: Avail, fmt: Format) {
+    let e = load_edge(plane, stride, x, y, 4, a, fmt.mid());
+    pred_nxn(mode, &e, 4, a, &mut plane[y * stride + x..], stride, fmt);
 }
 
 /// Intra_8x8 prediction (with reference sample filtering) written into `plane` at (x, y).
-pub fn pred8x8(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8, a: Avail) {
-    let p = load_edge(plane, stride, x, y, 8, a);
+pub fn pred8x8<S: Sample>(plane: &mut [S], stride: usize, x: usize, y: usize, mode: u8, a: Avail, fmt: Format) {
+    let p = load_edge(plane, stride, x, y, 8, a, fmt.mid());
     let mut f = p;
     if a.top {
         f.top[0] = if a.top_left { (p.tl + 2 * p.top[0] + p.top[1] + 2) >> 2 } else { (3 * p.top[0] + p.top[1] + 2) >> 2 };
@@ -216,26 +220,22 @@ pub fn pred8x8(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8, a:
         }
         f.left[7] = (p.left[6] + 3 * p.left[7] + 2) >> 2;
     }
-    pred_nxn(mode, &f, 8, a, &mut plane[y * stride + x..], stride);
-}
-
-#[inline(always)]
-fn clip(v: i32) -> u8 {
-    v.clamp(0, 255) as u8
+    pred_nxn(mode, &f, 8, a, &mut plane[y * stride + x..], stride, fmt);
 }
 
 /// Intra_16x16 prediction (8.3.3) for the macroblock at luma (x, y).
-pub fn pred16x16(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8, a: Avail) {
+pub fn pred16x16<S: Sample>(plane: &mut [S], stride: usize, x: usize, y: usize, mode: u8, a: Avail, fmt: Format) {
+    let max = fmt.max();
     let mut top = [0i32; 16];
     let mut left = [0i32; 16];
     if a.top {
         for i in 0..16 {
-            top[i] = plane[(y - 1) * stride + x + i] as i32;
+            top[i] = plane[(y - 1) * stride + x + i].to_i32();
         }
     }
     if a.left {
         for i in 0..16 {
-            left[i] = plane[(y + i) * stride + x - 1] as i32;
+            left[i] = plane[(y + i) * stride + x - 1].to_i32();
         }
     }
     let base = y * stride + x;
@@ -243,13 +243,13 @@ pub fn pred16x16(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8,
         0 => {
             for r in 0..16 {
                 for c in 0..16 {
-                    plane[base + r * stride + c] = top[c] as u8;
+                    plane[base + r * stride + c] = S::clip(top[c], max);
                 }
             }
         }
         1 => {
             for r in 0..16 {
-                plane[base + r * stride..base + r * stride + 16].fill(left[r] as u8);
+                plane[base + r * stride..base + r * stride + 16].fill(S::clip(left[r], max));
             }
         }
         2 => {
@@ -259,14 +259,15 @@ pub fn pred16x16(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8,
                 (true, true) => (st + sl + 16) >> 5,
                 (false, true) => (sl + 8) >> 4,
                 (true, false) => (st + 8) >> 4,
-                _ => 128,
-            } as u8;
+                _ => fmt.mid(),
+            };
+            let dc = S::clip(dc, max);
             for r in 0..16 {
                 plane[base + r * stride..base + r * stride + 16].fill(dc);
             }
         }
         _ => {
-            let tl = if a.top_left { plane[(y - 1) * stride + x - 1] as i32 } else { 128 };
+            let tl = if a.top_left { plane[(y - 1) * stride + x - 1].to_i32() } else { fmt.mid() };
             let t = |i: i32| if i < 0 { tl } else { top[i as usize] };
             let l = |i: i32| if i < 0 { tl } else { left[i as usize] };
             let mut h = 0;
@@ -280,34 +281,35 @@ pub fn pred16x16(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8,
             let c = (5 * v + 32) >> 6;
             for r in 0..16 {
                 for cc in 0..16 {
-                    plane[base + r * stride + cc] = clip((aa + b * (cc as i32 - 7) + c * (r as i32 - 7) + 16) >> 5);
+                    plane[base + r * stride + cc] = S::clip((aa + b * (cc as i32 - 7) + c * (r as i32 - 7) + 16) >> 5, max);
                 }
             }
         }
     }
 }
 
-/// Chroma intra prediction for a 4:2:0 macroblock (8x8 block at chroma (x, y)), 8.3.4.
-/// Modes: 0 DC, 1 horizontal, 2 vertical, 3 plane.
-pub fn pred_chroma(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8, a: Avail) {
+/// Chroma intra prediction (8.3.4) for the 8 x `fmt.mb_hc` chroma block of a macroblock at
+/// chroma (x, y). Modes: 0 DC, 1 horizontal, 2 vertical, 3 plane.
+pub fn pred_chroma<S: Sample>(plane: &mut [S], stride: usize, x: usize, y: usize, mode: u8, a: Avail, fmt: Format) {
     const W: usize = 8;
-    const H: usize = 8;
+    let h = fmt.mb_hc.min(16);
+    let (max, mid) = (fmt.max(), fmt.mid());
     let mut top = [0i32; W];
-    let mut left = [0i32; H];
+    let mut left = [0i32; 16];
     if a.top {
         for i in 0..W {
-            top[i] = plane[(y - 1) * stride + x + i] as i32;
+            top[i] = plane[(y - 1) * stride + x + i].to_i32();
         }
     }
     if a.left {
-        for i in 0..H {
-            left[i] = plane[(y + i) * stride + x - 1] as i32;
+        for i in 0..h {
+            left[i] = plane[(y + i) * stride + x - 1].to_i32();
         }
     }
     let base = y * stride + x;
     match mode {
         0 => {
-            for by in 0..H / 4 {
+            for by in 0..h / 4 {
                 for bx in 0..W / 4 {
                     let st: i32 = top[bx * 4..bx * 4 + 4].iter().sum();
                     let sl: i32 = left[by * 4..by * 4 + 4].iter().sum();
@@ -316,7 +318,7 @@ pub fn pred_chroma(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8
                             (true, true) => (st + sl + 4) >> 3,
                             (false, true) => (sl + 2) >> 2,
                             (true, false) => (st + 2) >> 2,
-                            _ => 128,
+                            _ => mid,
                         }
                     } else if bx > 0 {
                         if a.top {
@@ -324,50 +326,54 @@ pub fn pred_chroma(plane: &mut [u8], stride: usize, x: usize, y: usize, mode: u8
                         } else if a.left {
                             (sl + 2) >> 2
                         } else {
-                            128
+                            mid
                         }
                     } else if a.left {
                         (sl + 2) >> 2
                     } else if a.top {
                         (st + 2) >> 2
                     } else {
-                        128
+                        mid
                     };
                     for r in 0..4 {
                         let o = base + (by * 4 + r) * stride + bx * 4;
-                        plane[o..o + 4].fill(dc as u8);
+                        plane[o..o + 4].fill(S::clip(dc, max));
                     }
                 }
             }
         }
         1 => {
-            for r in 0..H {
-                plane[base + r * stride..base + r * stride + W].fill(left[r] as u8);
+            for r in 0..h {
+                plane[base + r * stride..base + r * stride + W].fill(S::clip(left[r], max));
             }
         }
         2 => {
-            for r in 0..H {
+            for r in 0..h {
                 for c in 0..W {
-                    plane[base + r * stride + c] = top[c] as u8;
+                    plane[base + r * stride + c] = S::clip(top[c], max);
                 }
             }
         }
         _ => {
-            let tl = if a.top_left { plane[(y - 1) * stride + x - 1] as i32 } else { 128 };
+            let tl = if a.top_left { plane[(y - 1) * stride + x - 1].to_i32() } else { fmt.mid() };
             let t = |i: i32| if i < 0 { tl } else { top[i as usize] };
             let l = |i: i32| if i < 0 { tl } else { left[i as usize] };
-            let mut h = 0;
+            // yCF = 4 for 4:2:2 (8-138..8-141)
+            let ycf = (h as i32 - 8) / 2;
+            let mut hh = 0;
             let mut v = 0;
             for k in 0..4 {
-                h += (k + 1) * (t(4 + k) - t(2 - k));
-                v += (k + 1) * (l(4 + k) - l(2 - k));
+                hh += (k + 1) * (t(4 + k) - t(2 - k));
+            }
+            for k in 0..4 + ycf {
+                v += (k + 1) * (l(4 + ycf + k) - l(2 + ycf - k));
             }
-            let aa = 16 * (left[H - 1] + top[W - 1]);
-            let b = (34 * h + 32) >> 6;
-            let c = (34 * v + 32) >> 6;
-            for r in 0..H {
+            let aa = 16 * (left[h - 1] + top[W - 1]);
+            let b = (34 * hh + 32) >> 6;
+            let c = ((34 - 29 * (h == 16) as i32) * v + 32) >> 6;
+            for r in 0..h {
                 for cc in 0..W {
-                    plane[base + r * stride + cc] = clip((aa + b * (cc as i32 - 3) + c * (r as i32 - 3) + 16) >> 5);
+                    plane[base + r * stride + cc] = S::clip((aa + b * (cc as i32 - 3) + c * (r as i32 - 3 - ycf) + 16) >> 5, max);
                 }
             }
         }
@@ -392,16 +398,17 @@ mod tests {
         (p, stride)
     }
     const ALL: Avail = Avail { left: true, top: true, top_left: true, top_right: true };
+    const F: Format = Format::YUV420_8;
 
     #[test]
     fn i4_vertical_horizontal_dc() {
         let (mut p, s) = setup();
-        pred4x4(&mut p, s, 4, 4, 0, ALL);
+        pred4x4(&mut p, s, 4, 4, 0, ALL, F);
         assert_eq!(&p[4 * s + 4..4 * s + 8], &[10, 20, 30, 40]);
         assert_eq!(&p[7 * s + 4..7 * s + 8], &[10, 20, 30, 40]);
-        pred4x4(&mut p, s, 4, 4, 1, ALL);
+        pred4x4(&mut p, s, 4, 4, 1, ALL, F);
         assert_eq!(&p[5 * s + 4..5 * s + 8], &[2, 2, 2, 2]);
-        pred4x4(&mut p, s, 4, 4, 2, ALL);
+        pred4x4(&mut p, s, 4, 4, 2, ALL, F);
         // (10+20+30+40 + 1+2+3+4 + 4) >> 3 = 114 >> 3 = 14
         assert_eq!(p[4 * s + 4], 14);
     }
@@ -409,7 +416,7 @@ mod tests {
     #[test]
     fn i4_diag_down_left_hand_computed() {
         let (mut p, s) = setup();
-        pred4x4(&mut p, s, 4, 4, 3, ALL);
+        pred4x4(&mut p, s, 4, 4, 3, ALL, F);
         // pred[0,0] = (t0 + 2 t1 + t2 + 2) >> 2 = (10 + 40 + 30 + 2) >> 2 = 20
         assert_eq!(p[4 * s + 4], 20);
         // pred[3,3] = (t6 + 3 t7 + 2) >> 2 = (70 + 240 + 2) >> 2 = 78
@@ -419,7 +426,7 @@ mod tests {
     #[test]
     fn i4_diag_down_right_hand_computed() {
         let (mut p, s) = setup();
-        pred4x4(&mut p, s, 4, 4, 4, ALL);
+        pred4x4(&mut p, s, 4, 4, 4, ALL, F);
         // x==y: (t0 + 2 tl + l0 + 2) >> 2 = (10 + 10 + 1 + 2) >> 2 = 5
         assert_eq!(p[4 * s + 4], 5);
         // x=1,y=0: (tl + 2 t0 + t1 + 2)>>2 = (5 + 20 + 20 + 2) >> 2 = 11
@@ -432,7 +439,7 @@ mod tests {
     fn i4_top_right_substitution() {
         let (mut p, s) = setup();
         let a = Avail { top_right: false, ..ALL };
-        pred4x4(&mut p, s, 4, 4, 3, a);
+        pred4x4(&mut p, s, 4, 4, 3, a, F);
         // top-right replaced by t3 = 40: pred[3,3] = (40 + 120 + 2) >> 2 = 40
         assert_eq!(p[7 * s + 7], 40);
     }
@@ -441,7 +448,7 @@ mod tests {
     fn i16_plane_flat_is_flat() {
         let s = 32;
         let mut p = vec![77u8; s * 32];
-        pred16x16(&mut p, s, 8, 8, 3, ALL);
+        pred16x16(&mut p, s, 8, 8, 3, ALL, F);
         assert!(p[8 * s + 8..8 * s + 24].iter().all(|&v| v == 77));
     }
 
@@ -453,7 +460,7 @@ mod tests {
             p[3 * s + 4 + i] = if i < 4 { 40 } else { 80 };
             p[(4 + i) * s + 3] = if i < 4 { 8 } else { 16 };
         }
-        pred_chroma(&mut p, s, 4, 4, 0, ALL);
+        pred_chroma(&mut p, s, 4, 4, 0, ALL, F);
         assert_eq!(p[4 * s + 4], (160 + 32 + 4) >> 3); // top-left block: both
         assert_eq!(p[4 * s + 8], 80); // top-right block: top only
         assert_eq!(p[8 * s + 4], 16); // bottom-left: left only
diff --git a/crates/h264/src/lib.rs b/crates/h264/src/lib.rs
--- a/crates/h264/src/lib.rs
+++ b/crates/h264/src/lib.rs
@@ -1,7 +1,8 @@
 //! Clean-room pure-Rust H.264 / AVC decoder, implemented from ITU-T Rec. H.264 (ISO/IEC 14496-10).
 //!
 //! Supported: progressive (frame) coding, 8-bit 4:2:0, Baseline / Main / High profiles — CAVLC and CABAC,
-//! I/P/B slices, 8x8 transform, custom scaling matrices, weighted prediction, spatial/temporal direct,
+//! I/P/B slices; High 10 (4:2:0, 9/10-bit) and High 4:2:2 / High 4:2:2 Intra (4:2:2 intra pictures,
+//! 8–10-bit: Sony XAVC Intra, AVC-Intra), 8x8 transform, custom scaling matrices, weighted prediction, spatial/temporal direct,
 //! multiple slices, long-term references and all MMCOs, deblocking.
 //!
 //! ```no_run
@@ -35,7 +36,7 @@ mod intra;
 mod mbtypes;
 pub mod params;
 mod picture;
-pub use picture::set_plane_allocator;
+pub use picture::{set_plane_allocator, set_plane_allocator_u16};
 pub mod slice;
 mod slicedec;
 #[cfg(test)]
@@ -56,9 +57,23 @@ pub struct ColorInfo {
     pub matrix: u8,
 }
 
-/// A decoded, cropped picture in 8-bit planar 4:2:0.
+/// The planes of a picture with more than 8 bits per sample.
+#[derive(Clone, Debug, Default)]
+pub struct WidePlanes {
+    pub y: Vec<u16>,
+    pub u: Vec<u16>,
+    pub v: Vec<u16>,
+}
+
+/// A decoded, cropped picture: planar 4:2:0 or 4:2:2, 8-bit samples in `y`, `u`, `v`, higher bit
+/// depths in `wide` (`y`, `u`, `v` are then empty).
 #[derive(Clone, Debug)]
 pub struct Picture {
+    /// Samples of a high bit depth picture (`bit_depth` > 8).
+    pub wide: Option<WidePlanes>,
+    pub bit_depth: u32,
+    /// 1 (4:2:0) or 2 (4:2:2).
+    pub chroma_format_idc: u32,
     /// Cropped luma width.
     pub width: u32,
     /// Cropped luma height.
diff --git a/crates/h264/src/params.rs b/crates/h264/src/params.rs
--- a/crates/h264/src/params.rs
+++ b/crates/h264/src/params.rs
@@ -526,11 +526,14 @@ impl Sps {
         if !self.frame_mbs_only {
             return unsupported("interlaced (field / MBAFF) coding");
         }
-        if self.chroma_array_type() != 1 {
-            return unsupported(format!("chroma_format_idc {} (only 4:2:0 is implemented)", self.chroma_format_idc));
+        if !matches!(self.chroma_array_type(), 1 | 2) {
+            return unsupported(format!("chroma_format_idc {} (only 4:2:0 and 4:2:2 are implemented)", self.chroma_format_idc));
         }
-        if self.bit_depth_luma != 8 || self.bit_depth_chroma != 8 {
-            return unsupported(format!("bit depth luma {} chroma {} (only 8-bit is implemented)", self.bit_depth_luma, self.bit_depth_chroma));
+        if self.bit_depth_luma != self.bit_depth_chroma || !(8..=10).contains(&self.bit_depth_luma) {
+            return unsupported(format!(
+                "bit depth luma {} chroma {} (8 to 10 bits, equal for luma and chroma, are implemented)",
+                self.bit_depth_luma, self.bit_depth_chroma
+            ));
         }
         if self.qpprime_y_zero_transform_bypass {
             return unsupported("qpprime_y_zero_transform_bypass (lossless)");
diff --git a/crates/h264/src/picture.rs b/crates/h264/src/picture.rs
--- a/crates/h264/src/picture.rs
+++ b/crates/h264/src/picture.rs
@@ -2,25 +2,141 @@
 
 use std::sync::{Arc, OnceLock};
 
-/// Planar 4:2:0 8-bit sample buffers (MB-aligned dimensions, stride == width).
+/// Storage type of decoded samples: `u8` for 8-bit streams (half the memory and cache traffic of
+/// 16-bit storage), `u16` for higher bit depths. Values are always within `0..1 << bit_depth`.
+pub trait Sample: Copy + Default + PartialEq + Eq + std::fmt::Debug + Send + Sync + 'static {
+    fn to_i32(self) -> i32;
+    fn to_i16(self) -> i16;
+    /// `v` clipped to `0..=max` (`max` = (1 << bit_depth) - 1; always 255 for `u8`).
+    fn clip(v: i32, max: i32) -> Self;
+    /// The samples of a published row if they are stored with this type.
+    fn row_planes(s: &RowSamples) -> Option<&RowPlanes<Self>>;
+    fn wrap_row(p: RowPlanes<Self>) -> RowSamples;
+    /// An empty output plane with room for `len` samples (see [`set_plane_allocator`]).
+    fn alloc_plane(len: usize) -> Vec<Self>;
+}
+
+impl Sample for u8 {
+    #[inline(always)]
+    fn to_i32(self) -> i32 {
+        self as i32
+    }
+    #[inline(always)]
+    fn to_i16(self) -> i16 {
+        self as i16
+    }
+    #[inline(always)]
+    fn clip(v: i32, _max: i32) -> Self {
+        v.clamp(0, 255) as u8
+    }
+    #[inline(always)]
+    fn row_planes(s: &RowSamples) -> Option<&RowPlanes<u8>> {
+        match s {
+            RowSamples::U8(p) => Some(p),
+            RowSamples::U16(_) => None,
+        }
+    }
+    fn wrap_row(p: RowPlanes<u8>) -> RowSamples {
+        RowSamples::U8(p)
+    }
+    fn alloc_plane(len: usize) -> Vec<u8> {
+        PLANE_ALLOCATOR.get().copied().unwrap_or(Vec::with_capacity)(len)
+    }
+}
+
+impl Sample for u16 {
+    #[inline(always)]
+    fn to_i32(self) -> i32 {
+        self as i32
+    }
+    #[inline(always)]
+    fn to_i16(self) -> i16 {
+        self as i16
+    }
+    #[inline(always)]
+    fn clip(v: i32, max: i32) -> Self {
+        v.max(0).min(max) as u16
+    }
+    #[inline(always)]
+    fn row_planes(s: &RowSamples) -> Option<&RowPlanes<u16>> {
+        match s {
+            RowSamples::U16(p) => Some(p),
+            RowSamples::U8(_) => None,
+        }
+    }
+    fn wrap_row(p: RowPlanes<u16>) -> RowSamples {
+        RowSamples::U16(p)
+    }
+    fn alloc_plane(len: usize) -> Vec<u16> {
+        PLANE_ALLOCATOR_U16.get().copied().unwrap_or(Vec::with_capacity)(len)
+    }
+}
+
+/// Sample layout of a picture: bit depth and chroma format (4:2:0 or 4:2:2).
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct Format {
+    /// Bit depth of luma and chroma samples (8..=10).
+    pub bit_depth: u32,
+    /// Chroma lines per macroblock: 8 (4:2:0) or 16 (4:2:2).
+    pub mb_hc: usize,
+}
+
+impl Format {
+    #[cfg(test)]
+    pub const YUV420_8: Format = Format { bit_depth: 8, mb_hc: 8 };
+
+    /// Largest sample value.
+    #[inline(always)]
+    pub fn max(self) -> i32 {
+        (1 << self.bit_depth) - 1
+    }
+    /// The mid-grey sample value (unavailable intra neighbours, 8.3.1.2).
+    #[inline(always)]
+    pub fn mid(self) -> i32 {
+        1 << (self.bit_depth - 1)
+    }
+    /// QpBdOffset (7.4.2.1.1).
+    #[inline(always)]
+    pub fn qp_bd_offset(self) -> i32 {
+        6 * (self.bit_depth as i32 - 8)
+    }
+    /// 4x4 chroma blocks per component and macroblock (4 or 8).
+    #[inline(always)]
+    pub fn chroma_blocks(self) -> usize {
+        self.mb_hc / 2
+    }
+    /// chroma_format_idc (1 or 2).
+    pub fn chroma_format_idc(self) -> u32 {
+        if self.mb_hc == 16 { 2 } else { 1 }
+    }
+    /// Whether samples are stored as `u16`.
+    pub fn wide(self) -> bool {
+        self.bit_depth > 8
+    }
+}
+
+/// Planar 4:2:0 / 4:2:2 sample buffers (MB-aligned dimensions, stride == width).
 #[derive(Clone)]
-pub struct Planes {
-    pub y: Vec<u8>,
-    pub cb: Vec<u8>,
-    pub cr: Vec<u8>,
+pub struct Planes<S> {
+    pub y: Vec<S>,
+    pub cb: Vec<S>,
+    pub cr: Vec<S>,
     pub width: usize,
     pub height: usize,
     pub cwidth: usize,
+    pub fmt: Format,
 }
 
-impl Planes {
-    pub fn new(width: usize, height: usize) -> Self {
-        let (cw, ch) = (width / 2, height / 2);
-        Planes { y: vec![0; width * height], cb: vec![128; cw * ch], cr: vec![128; cw * ch], width, height, cwidth: cw }
+impl<S: Sample> Planes<S> {
+    /// Black luma, neutral chroma.
+    pub fn new(width: usize, height: usize, fmt: Format) -> Self {
+        let (cw, ch) = (width / 2, height / 16 * fmt.mb_hc);
+        let mid = S::clip(fmt.mid(), fmt.max());
+        Planes { y: vec![S::default(); width * height], cb: vec![mid; cw * ch], cr: vec![mid; cw * ch], width, height, cwidth: cw, fmt }
     }
-    pub fn gray(width: usize, height: usize) -> Self {
-        let mut p = Self::new(width, height);
-        p.y.fill(128);
+    pub fn gray(width: usize, height: usize, fmt: Format) -> Self {
+        let mut p = Self::new(width, height, fmt);
+        p.y.fill(S::clip(fmt.mid(), fmt.max()));
         p
     }
 }
@@ -57,16 +173,17 @@ pub struct MbState {
     pub transform_8x8: bool,
     /// coded_block_pattern: bits 0..3 luma, bits 4..5 chroma.
     pub cbp: u8,
-    pub qp: u8,
+    /// QPY (negative values occur at high bit depths).
+    pub qp: i8,
     /// QPc for Cb / Cr (used by deblocking).
-    pub qpc: [u8; 2],
+    pub qpc: [i8; 2],
     pub intra_chroma_mode: u8,
     /// Intra4x4PredMode / Intra8x8PredMode per 4x4 block (raster order).
     pub intra_modes: [u8; 16],
     /// CAVLC: TotalCoeff per luma 4x4 block (raster). CABAC: coded_block_flag.
     pub nnz: [u8; 16],
-    /// Same for chroma AC blocks [Cb, Cr][raster 2x2].
-    pub nnz_c: [[u8; 4]; 2],
+    /// Same for chroma AC blocks [Cb, Cr][raster 2 x 2 (4:2:0) or 2 x 4 (4:2:2)].
+    pub nnz_c: [[u8; 8]; 2],
     /// CABAC coded_block_flag of DC blocks: bit0 luma (I16x16), bit1 Cb, bit2 Cr.
     pub cbf_dc: u8,
     /// Luma 4x4 blocks (raster bit) with non-zero coefficients, for deblocking bS = 2.
@@ -99,7 +216,7 @@ impl Default for MbState {
             intra_chroma_mode: 0,
             intra_modes: [2; 16],
             nnz: [0; 16],
-            nnz_c: [[0; 4]; 2],
+            nnz_c: [[0; 8]; 2],
             cbf_dc: 0,
             nz_mask: 0,
             ref_idx: [[-1; 4]; 2],
@@ -110,14 +227,25 @@ impl Default for MbState {
     }
 }
 
+/// The samples of one published macroblock row.
+pub struct RowPlanes<S> {
+    /// 16 luma lines.
+    pub y: Box<[S]>,
+    /// `mb_hc` lines per chroma component.
+    pub cb: Box<[S]>,
+    pub cr: Box<[S]>,
+}
+
+/// [`RowPlanes`] in the storage type of the picture.
+pub enum RowSamples {
+    U8(RowPlanes<u8>),
+    U16(RowPlanes<u16>),
+}
+
 /// One published macroblock row of a decoded frame: final (deblocked) samples plus the motion data
 /// needed as co-located information for direct prediction.
 pub struct FrameRow {
-    /// 16 luma lines.
-    pub y: Box<[u8]>,
-    /// 8 lines per chroma component.
-    pub cb: Box<[u8]>,
-    pub cr: Box<[u8]>,
+    pub samples: RowSamples,
     /// Per 4x4 block: index mb_x * 16 + raster.
     pub mv: [Box<[[i16; 2]]>; 2],
     /// Per 8x8 block: index mb_x * 4 + b8.
@@ -139,12 +267,23 @@ pub struct Frame {
     pub cwidth: usize,
     pub cheight: usize,
     pub mb_w: usize,
+    pub fmt: Format,
     rows: Box<[OnceLock<FrameRow>]>,
 }
 
 impl Frame {
-    pub fn new(id: u32, poc: i32, mb_w: usize, mb_h: usize) -> Self {
-        Frame { id, poc, width: mb_w * 16, height: mb_h * 16, cwidth: mb_w * 8, cheight: mb_h * 8, mb_w, rows: (0..mb_h).map(|_| OnceLock::new()).collect() }
+    pub fn new(id: u32, poc: i32, mb_w: usize, mb_h: usize, fmt: Format) -> Self {
+        Frame {
+            id,
+            poc,
+            width: mb_w * 16,
+            height: mb_h * 16,
+            cwidth: mb_w * 8,
+            cheight: mb_h * fmt.mb_hc,
+            mb_w,
+            fmt,
+            rows: (0..mb_h).map(|_| OnceLock::new()).collect(),
+        }
     }
 
     pub fn mb_h(&self) -> usize {
@@ -181,13 +320,14 @@ impl Frame {
     }
 
     /// Build a published row from a contiguous plane set (rows of MB row `r`) and motion data.
-    pub fn make_row(planes: &Planes, r: usize, mbs: &[MbState], ref_ids: &dyn Fn(&MbState, usize, i8) -> u32) -> FrameRow {
+    pub fn make_row<S: Sample>(planes: &Planes<S>, r: usize, mbs: &[MbState], ref_ids: &dyn Fn(&MbState, usize, i8) -> u32) -> FrameRow {
         let w = planes.width;
         let cw = planes.cwidth;
+        let hc = planes.fmt.mb_hc;
         let mb_w = w / 16;
         let y = planes.y[r * 16 * w..(r + 1) * 16 * w].into();
-        let cb = planes.cb[r * 8 * cw..(r + 1) * 8 * cw].into();
-        let cr = planes.cr[r * 8 * cw..(r + 1) * 8 * cw].into();
+        let cb = planes.cb[r * hc * cw..(r + 1) * hc * cw].into();
+        let cr = planes.cr[r * hc * cw..(r + 1) * hc * cw].into();
         let row_mbs = &mbs[r * mb_w..(r + 1) * mb_w];
         let mut mv = [vec![[0i16; 2]; mb_w * 16], vec![[0i16; 2]; mb_w * 16]];
         let mut ref_idx = [vec![-1i8; mb_w * 4], vec![-1i8; mb_w * 4]];
@@ -212,14 +352,20 @@ impl Frame {
         let [mv0, mv1] = mv;
         let [ri0, ri1] = ref_idx;
         let [id0, id1] = ref_id;
-        FrameRow { y, cb, cr, mv: [mv0.into(), mv1.into()], ref_idx: [ri0.into(), ri1.into()], ref_id: [id0.into(), id1.into()], intra: intra.into() }
+        FrameRow {
+            samples: S::wrap_row(RowPlanes { y, cb, cr }),
+            mv: [mv0.into(), mv1.into()],
+            ref_idx: [ri0.into(), ri1.into()],
+            ref_id: [id0.into(), id1.into()],
+            intra: intra.into(),
+        }
     }
 
     /// A frame with every row published from `planes` and no motion (intra), e.g. "non-existing"
     /// frames inferred for frame_num gaps.
-    pub fn from_planes(id: u32, poc: i32, planes: &Planes) -> Self {
+    pub fn from_planes<S: Sample>(id: u32, poc: i32, planes: &Planes<S>) -> Self {
         let (mb_w, mb_h) = (planes.width / 16, planes.height / 16);
-        let f = Frame::new(id, poc, mb_w, mb_h);
+        let f = Frame::new(id, poc, mb_w, mb_h, planes.fmt);
         let blank = vec![MbState::default(); mb_w * mb_h];
         for r in 0..mb_h {
             f.publish(r, Frame::make_row(planes, r, &blank, &|_, _, _| u32::MAX));
@@ -228,68 +374,81 @@ impl Frame {
     }
 
     /// Copy the w x h luma window at (x0, y0) (clamped to the picture) into `out` (stride `os`).
+    /// A frame stored with another sample type (only after a format change) reads as mid-grey.
     #[inline]
-    pub fn luma_window(&self, x0: i32, y0: i32, w: usize, h: usize, out: &mut [u8], os: usize) {
+    pub fn luma_window<S: Sample>(&self, x0: i32, y0: i32, w: usize, h: usize, out: &mut [S], os: usize) {
         let (width, height) = (self.width, self.height);
         for r in 0..h {
             let yy = (y0 + r as i32).clamp(0, height as i32 - 1) as usize;
-            let row = self.row(yy >> 4);
+            let Some(row) = S::row_planes(&self.row(yy >> 4).samples) else {
+                out[r * os..r * os + w].fill(S::clip(self.fmt.mid(), self.fmt.max()));
+                continue;
+            };
             let line = &row.y[(yy & 15) * width..(yy & 15) * width + width];
-            copy_line::<24>(line, x0, &mut out[r * os..], w);
+            copy_line::<S, 24>(line, x0, &mut out[r * os..], w);
         }
     }
 
     /// Copy the w x h window of chroma component `c` at (x0, y0) (clamped) into `out`.
     #[inline]
-    pub fn chroma_window(&self, c: usize, x0: i32, y0: i32, w: usize, h: usize, out: &mut [u8], os: usize) {
-        let (cw, ch) = (self.cwidth, self.cheight);
+    pub fn chroma_window<S: Sample>(&self, c: usize, x0: i32, y0: i32, w: usize, h: usize, out: &mut [S], os: usize) {
+        let (cw, ch, hc) = (self.cwidth, self.cheight, self.fmt.mb_hc);
         for r in 0..h {
             let yy = (y0 + r as i32).clamp(0, ch as i32 - 1) as usize;
-            let row = self.row(yy >> 3);
+            let Some(row) = S::row_planes(&self.row(yy / hc).samples) else {
+                out[r * os..r * os + w].fill(S::clip(self.fmt.mid(), self.fmt.max()));
+                continue;
+            };
             let plane = if c == 0 { &row.cb } else { &row.cr };
-            let line = &plane[(yy & 7) * cw..(yy & 7) * cw + cw];
-            copy_line::<16>(line, x0, &mut out[r * os..], w);
+            let line = &plane[(yy % hc) * cw..(yy % hc) * cw + cw];
+            copy_line::<S, 16>(line, x0, &mut out[r * os..], w);
         }
     }
 
-    /// Crop and copy the frame into planar buffers (waits for completion).
-    pub fn copy_cropped(&self, crop: (usize, usize, usize, usize)) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
+    /// Crop and copy the frame into planar buffers (waits for completion). Chroma is cropped
+    /// with the frame's subsampling (vertical only for 4:2:0). None if the frame is stored with
+    /// another sample type.
+    pub fn copy_cropped<S: Sample>(&self, crop: (usize, usize, usize, usize)) -> Option<(Vec<S>, Vec<S>, Vec<S>)> {
         let (cx, cy, cw, ch) = crop;
-        let alloc = plane_allocator();
-        let mut y = alloc(cw * ch);
+        let hc = self.fmt.mb_hc;
+        let mut y = S::alloc_plane(cw * ch);
         for r in cy..cy + ch {
-            let row = self.row(r >> 4);
+            let row = S::row_planes(&self.row(r >> 4).samples)?;
             y.extend_from_slice(&row.y[(r & 15) * self.width + cx..(r & 15) * self.width + cx + cw]);
         }
-        let (ccx, ccy, ccw, cch) = (cx / 2, cy / 2, cw.div_ceil(2), ch.div_ceil(2));
-        let mut u = alloc(ccw * cch);
-        let mut v = alloc(ccw * cch);
+        let (ccy, cch) = if hc == 16 { (cy, ch) } else { (cy / 2, ch.div_ceil(2)) };
+        let (ccx, ccw) = (cx / 2, cw.div_ceil(2));
+        let mut u = S::alloc_plane(ccw * cch);
+        let mut v = S::alloc_plane(ccw * cch);
         for r in ccy..ccy + cch {
-            let row = self.row(r >> 3);
-            let o = (r & 7) * self.cwidth + ccx;
+            let row = S::row_planes(&self.row(r / hc).samples)?;
+            let o = (r % hc) * self.cwidth + ccx;
             u.extend_from_slice(&row.cb[o..o + ccw]);
             v.extend_from_slice(&row.cr[o..o + ccw]);
         }
-        (y, u, v)
+        Some((y, u, v))
     }
 }
 
-static PLANE_ALLOCATOR: std::sync::OnceLock<fn(usize) -> Vec<u8>> = std::sync::OnceLock::new();
+static PLANE_ALLOCATOR: OnceLock<fn(usize) -> Vec<u8>> = OnceLock::new();
+static PLANE_ALLOCATOR_U16: OnceLock<fn(usize) -> Vec<u16>> = OnceLock::new();
 
-/// Where output planes come from: an empty buffer with room for the given number of samples.
-/// The host sets it once to recycle the planes of pictures it is done with; the first call wins.
+/// Where 8-bit output planes come from: an empty buffer with room for the given number of
+/// samples. The host sets it once to recycle the planes of pictures it is done with; the first
+/// call wins.
 pub fn set_plane_allocator(alloc: fn(usize) -> Vec<u8>) {
     let _ = PLANE_ALLOCATOR.set(alloc);
 }
 
-fn plane_allocator() -> fn(usize) -> Vec<u8> {
-    PLANE_ALLOCATOR.get().copied().unwrap_or(Vec::with_capacity)
+/// [`set_plane_allocator`] for the 16-bit planes of high bit depth pictures.
+pub fn set_plane_allocator_u16(alloc: fn(usize) -> Vec<u16>) {
+    let _ = PLANE_ALLOCATOR_U16.set(alloc);
 }
 
 /// Copy `w` samples of `line` starting at x0 into `out`; when possible a fixed-size block of `N`
-/// bytes is copied instead (cheaper than a variable-length copy; `out` must then have room for `N`).
+/// samples is copied instead (cheaper than a variable-length copy; `out` must then have room for `N`).
 #[inline(always)]
-fn copy_line<const N: usize>(line: &[u8], x0: i32, out: &mut [u8], w: usize) {
+fn copy_line<S: Sample, const N: usize>(line: &[S], x0: i32, out: &mut [S], w: usize) {
     if w <= N
         && x0 >= 0
         && let Some(src) = line.get(x0 as usize..).and_then(|l| l.first_chunk::<N>())
@@ -303,7 +462,7 @@ fn copy_line<const N: usize>(line: &[u8], x0: i32, out: &mut [u8], w: usize) {
 
 /// Copy `out.len()` samples of `line` starting at x0, replicating edge samples outside the line.
 #[inline(always)]
-fn copy_clamped(line: &[u8], x0: i32, out: &mut [u8]) {
+fn copy_clamped<S: Sample>(line: &[S], x0: i32, out: &mut [S]) {
     let w = out.len();
     if x0 >= 0 && x0 as usize + w <= line.len() {
         out.copy_from_slice(&line[x0 as usize..x0 as usize + w]);
diff --git a/crates/h264/src/slice.rs b/crates/h264/src/slice.rs
--- a/crates/h264/src/slice.rs
+++ b/crates/h264/src/slice.rs
@@ -304,7 +304,8 @@ impl SliceHeader {
         }
         let slice_qp_delta = r.read_se()?;
         let qp = pps.pic_init_qp + slice_qp_delta;
-        ensure!((0..=51).contains(&qp), "slice QP {qp} out of range");
+        let qp_bd_offset = 6 * sps.bit_depth_luma.saturating_sub(8) as i32;
+        ensure!((-qp_bd_offset..=51).contains(&qp), "slice QP {qp} out of range");
         let mut sp_for_switch = false;
         let mut slice_qs_delta = 0;
         if matches!(slice_type, SliceType::Sp | SliceType::Si) {
diff --git a/crates/h264/src/slicedec.rs b/crates/h264/src/slicedec.rs
--- a/crates/h264/src/slicedec.rs
+++ b/crates/h264/src/slicedec.rs
@@ -25,8 +25,8 @@ pub struct SliceInfo {
 
 /// The picture under construction: private reconstruction buffers plus the shared [`Frame`] that
 /// finished (deblocked) macroblock rows are published into.
-pub struct PicState {
-    pub planes: Planes,
+pub struct PicState<S> {
+    pub planes: Planes<S>,
     pub mbs: Vec<MbState>,
     pub slices: Vec<SliceInfo>,
     pub mb_w: usize,
@@ -44,11 +44,11 @@ pub struct PicState {
     pub skip_deblock: bool,
 }
 
-impl PicState {
+impl<S: Sample> PicState<S> {
     pub fn new(frame: FrameRef) -> Self {
         let (mb_w, mb_h) = (frame.mb_w, frame.mb_h());
         PicState {
-            planes: Planes::new(mb_w * 16, mb_h * 16),
+            planes: Planes::new(mb_w * 16, mb_h * 16, frame.fmt),
             mbs: vec![MbState::default(); mb_w * mb_h],
             slices: Vec::new(),
             mb_w,
@@ -66,7 +66,7 @@ impl PicState {
 
     /// Reuse the buffers of a finished picture of the same size for a new frame.
     pub fn reset(&mut self, frame: FrameRef) {
-        debug_assert_eq!((self.mb_w, self.mb_h), (frame.mb_w, frame.mb_h()));
+        debug_assert_eq!((self.mb_w, self.mb_h, self.planes.fmt), (frame.mb_w, frame.mb_h(), frame.fmt));
         // start_mb() re-initialises each macroblock before use; only availability must be reset
         for m in &mut self.mbs {
             m.slice_num = u32::MAX;
@@ -158,22 +158,29 @@ pub(crate) struct MbCur {
 }
 
 /// Coefficient scratch for one macroblock.
-pub(crate) struct Scratch {
+pub(crate) struct Scratch<S> {
     /// Luma: 16 4x4 blocks (raster block index * 16 + raster coefficient) or 4 8x8 blocks (b8 * 64 + raster).
     pub coef: [i32; 256],
-    /// Chroma: [Cb, Cr][raster 2x2 block * 16 + raster coefficient].
-    pub coef_c: [[i32; 64]; 2],
+    /// Chroma: [Cb, Cr][raster block (2 wide x 2 or 4 tall) * 16 + raster coefficient].
+    pub coef_c: [[i32; 128]; 2],
     /// Luma blocks with coefficients: raster 4x4 bits, or b8 bits in 8x8 mode.
     pub blk_nz: u16,
-    /// Chroma blocks with coefficients (bit per raster 2x2 block).
+    /// Chroma blocks with coefficients (bit per raster block).
     pub c_nz: [u8; 2],
-    pub pred: [[u8; 256]; 2],
-    pub pred_c: [[[u8; 64]; 2]; 2],
+    pub pred: [[S; 256]; 2],
+    pub pred_c: [[[S; 64]; 2]; 2],
 }
 
-impl Scratch {
+impl<S: Sample> Scratch<S> {
     pub fn new() -> Box<Self> {
-        Box::new(Scratch { coef: [0; 256], coef_c: [[0; 64]; 2], blk_nz: 0, c_nz: [0; 2], pred: [[0; 256]; 2], pred_c: [[[0; 64]; 2]; 2] })
+        Box::new(Scratch {
+            coef: [0; 256],
+            coef_c: [[0; 128]; 2],
+            blk_nz: 0,
+            c_nz: [0; 2],
+            pred: [[S::default(); 256]; 2],
+            pred_c: [[[S::default(); 64]; 2]; 2],
+        })
     }
 }
 
@@ -186,16 +193,19 @@ pub(crate) struct Nb {
 
 const NB_NA: Nb = Nb { avail: false, ref_idx: -1, mv: [0, 0] };
 
-pub struct SliceDecoder<'a> {
+pub struct SliceDecoder<'a, S> {
     pub sh: &'a SliceHeader,
     pub pps: &'a Pps,
     pub sps: &'a Sps,
-    pub pic: &'a mut PicState,
+    pub pic: &'a mut PicState<S>,
+    /// Sample format of the picture.
+    pub fmt: Format,
     pub refs: &'a [Vec<RefPic>; 2],
     pub ls: &'a LevelScale,
     pub slice_num: u32,
     pub mb_w: usize,
     pub mb_h: usize,
+    /// QPY of the current macroblock (-QpBdOffsetY..=51).
     pub qp: i32,
     pub chroma_qp_offset: [i32; 2],
     // current macroblock
@@ -206,7 +216,7 @@ pub struct SliceDecoder<'a> {
     pub nb: [Option<usize>; 4],
     pub mv_done: u16,
     pub cur: MbCur,
-    pub s: Box<Scratch>,
+    pub s: Box<Scratch<S>>,
     weights: Weights,
     /// Temporal direct: DistScaleFactor per refIdxL0, and whether pic0 is long-term.
     tdirect: Vec<(i32, bool)>,
@@ -218,21 +228,21 @@ pub struct SliceDecoder<'a> {
 /// and the corresponding chroma blocks into `c` (stride `cs`).
 #[allow(clippy::too_many_arguments)]
 #[inline]
-fn predict_block(f: &Frame, mv: [i16; 2], px: usize, py: usize, bw: usize, bh: usize, y: (&mut [u8], usize), c: [&mut [u8]; 2], cs: usize) {
+fn predict_block<S: Sample>(f: &Frame, mv: [i16; 2], px: usize, py: usize, bw: usize, bh: usize, y: (&mut [S], usize), c: [&mut [S]; 2], cs: usize, max: i32) {
     const LW: usize = 24;
-    let mut win = [0u8; LW * 21];
+    let mut win = [S::default(); LW * 21];
     let ix = px as i32 + (mv[0] as i32 >> 2);
     let iy = py as i32 + (mv[1] as i32 >> 2);
     f.luma_window(ix - 2, iy - 2, bw + 5, bh + 5, &mut win, LW);
-    inter::mc_luma_win(&win, LW, (mv[0] & 3) as u32, (mv[1] & 3) as u32, bw, bh, y.0, y.1);
+    inter::mc_luma_win(&win, LW, (mv[0] & 3) as u32, (mv[1] & 3) as u32, bw, bh, y.0, y.1, max);
     let cx = (px / 2) as i32 + (mv[0] as i32 >> 3);
     let cy = (py / 2) as i32 + (mv[1] as i32 >> 3);
     let (cw, ch) = (bw / 2, bh / 2);
     let (fx, fy) = ((mv[0] & 7) as u32, (mv[1] & 7) as u32);
     for (comp, out) in c.into_iter().enumerate() {
-        let mut w = [0u8; 16 * 9];
+        let mut w = [S::default(); 16 * 9];
         f.chroma_window(comp, cx, cy, cw + 1, ch + 1, &mut w, 16);
-        inter::mc_chroma_win(&w, 16, fx, fy, cw, ch, out, cs);
+        inter::mc_chroma_win(&w, 16, fx, fy, cw, ch, out, cs, max);
     }
 }
 
@@ -241,8 +251,10 @@ fn median(a: i16, b: i16, c: i16) -> i16 {
     a.max(b).min(a.min(b).max(c))
 }
 
-impl<'a> SliceDecoder<'a> {
-    pub fn new(sh: &'a SliceHeader, pps: &'a Pps, sps: &'a Sps, pic: &'a mut PicState, refs: &'a [Vec<RefPic>; 2], ls: &'a LevelScale) -> Result<Self> {
+impl<'a, S: Sample> SliceDecoder<'a, S> {
+    pub fn new(sh: &'a SliceHeader, pps: &'a Pps, sps: &'a Sps, pic: &'a mut PicState<S>, refs: &'a [Vec<RefPic>; 2], ls: &'a LevelScale) -> Result<Self> {
+        let fmt = pic.planes.fmt;
+        ensure!(fmt.mb_hc == 8 || sh.slice_type.is_intra(), "inter prediction in 4:2:2 streams is not implemented");
         let slice_num = pic.slices.len() as u32;
         let chroma_qp_offset = [pps.chroma_qp_index_offset, pps.second_chroma_qp_index_offset];
         pic.slices.push(SliceInfo {
@@ -251,7 +263,7 @@ impl<'a> SliceDecoder<'a> {
             beta_offset: sh.slice_beta_offset_div2 * 2,
             ref_ids: [refs[0].iter().map(|r| r.id()).collect(), refs[1].iter().map(|r| r.id()).collect()],
         });
-        let weights = Self::build_weights(sh, pps, refs, pic.poc);
+        let weights = Self::build_weights(sh, pps, refs, pic.poc, fmt);
         let mut tdirect = Vec::new();
         if sh.slice_type == SliceType::B && !sh.direct_spatial_mv_pred && !refs[1].is_empty() {
             let poc1 = refs[1][0].poc();
@@ -272,6 +284,7 @@ impl<'a> SliceDecoder<'a> {
             pps,
             sps,
             pic,
+            fmt,
             refs,
             ls,
             slice_num,
@@ -292,7 +305,9 @@ impl<'a> SliceDecoder<'a> {
         })
     }
 
-    fn build_weights(sh: &SliceHeader, pps: &Pps, refs: &[Vec<RefPic>; 2], cur_poc: i32) -> Weights {
+    fn build_weights(sh: &SliceHeader, pps: &Pps, refs: &[Vec<RefPic>; 2], cur_poc: i32, fmt: Format) -> Weights {
+        // explicit offsets are in units of 8-bit samples (8-301, 8-302)
+        let o = |v: i32| v * (1 << (fmt.bit_depth - 8));
         let mut w = Weights {
             mode: WpMode::Default,
             luma_denom: 0,
@@ -309,8 +324,8 @@ impl<'a> SliceDecoder<'a> {
                 w.luma_denom = t.luma_log2_denom as i32;
                 w.chroma_denom = t.chroma_log2_denom as i32;
                 for (l, entries) in [&t.l0, &t.l1].into_iter().enumerate() {
-                    w.luma[l] = entries.iter().map(|e| (e.luma_weight, e.luma_offset)).collect();
-                    w.chroma[l] = entries.iter().map(|e| [(e.chroma_weight[0], e.chroma_offset[0]), (e.chroma_weight[1], e.chroma_offset[1])]).collect();
+                    w.luma[l] = entries.iter().map(|e| (e.luma_weight, o(e.luma_offset))).collect();
+                    w.chroma[l] = entries.iter().map(|e| [(e.chroma_weight[0], o(e.chroma_offset[0])), (e.chroma_weight[1], o(e.chroma_offset[1]))]).collect();
                 }
             }
         } else if pps.weighted_bipred_idc == 2 && sh.slice_type.is_b() {
@@ -374,8 +389,10 @@ impl<'a> SliceDecoder<'a> {
         }
     }
 
+    /// QPC for luma QPY `qp` (8.5.8, Table 8-15; negative below 0 at high bit depths).
     fn chroma_qp(&self, qp: i32, c: usize) -> i32 {
-        QPC_TABLE[(qp + self.chroma_qp_offset[c]).clamp(0, 51) as usize] as i32
+        let qpi = (qp + self.chroma_qp_offset[c]).clamp(-self.fmt.qp_bd_offset(), 51);
+        if qpi < 30 { qpi } else { QPC_TABLE[qpi as usize] as i32 }
     }
 
     /// nC for a luma 4x4 block (raster index) - 9.2.1.
@@ -394,9 +411,10 @@ impl<'a> SliceDecoder<'a> {
 
     fn nc_chroma(&self, c: usize, raster: usize) -> i32 {
         let (bx, by) = (raster & 1, raster >> 1);
+        let bottom = self.fmt.chroma_blocks() - 2;
         let cur = self.mb();
         let a = if bx > 0 { Some(cur.nnz_c[c][raster - 1]) } else { self.nb[0].map(|m| self.pic.mbs[m].nnz_c[c][by * 2 + 1]) };
-        let b = if by > 0 { Some(cur.nnz_c[c][raster - 2]) } else { self.nb[1].map(|m| self.pic.mbs[m].nnz_c[c][2 + bx]) };
+        let b = if by > 0 { Some(cur.nnz_c[c][raster - 2]) } else { self.nb[1].map(|m| self.pic.mbs[m].nnz_c[c][bottom + bx]) };
         match (a, b) {
             (Some(a), Some(b)) => (a as i32 + b as i32 + 1) >> 1,
             (Some(a), None) => a as i32,
@@ -739,12 +757,24 @@ impl<'a> SliceDecoder<'a> {
         let ys = self.pic.planes.width;
         let cs = self.pic.planes.cwidth;
         let (cx, cy) = (px / 2, py / 2);
+        let max = self.fmt.max();
         if use0 != use1 && wts.iter().all(|w| *w == Weight::Default) {
             // single-list unweighted prediction: interpolate straight into the picture
             let l = if use0 { 0 } else { 1 };
             let f = &self.refs[l][refs[l] as usize].frame;
             let planes = &mut self.pic.planes;
-            predict_block(f, mvs[l], px, py, bw, bh, (&mut planes.y[py * ys + px..], ys), [&mut planes.cb[cy * cs + cx..], &mut planes.cr[cy * cs + cx..]], cs);
+            predict_block(
+                f,
+                mvs[l],
+                px,
+                py,
+                bw,
+                bh,
+                (&mut planes.y[py * ys + px..], ys),
+                [&mut planes.cb[cy * cs + cx..], &mut planes.cr[cy * cs + cx..]],
+                cs,
+                max,
+            );
             return;
         }
         let s = &mut *self.s;
@@ -754,7 +784,7 @@ impl<'a> SliceDecoder<'a> {
             }
             let Some(rp) = self.refs[l].get(refs[l] as usize) else { continue };
             let [pc0, pc1] = &mut s.pred_c[l];
-            predict_block(&rp.frame, mvs[l], px, py, bw, bh, (&mut s.pred[l][..], 16), [&mut pc0[..], &mut pc1[..]], 8);
+            predict_block(&rp.frame, mvs[l], px, py, bw, bh, (&mut s.pred[l][..], 16), [&mut pc0[..], &mut pc1[..]], 8, max);
         }
         inter::weighted_store(
             &mut self.pic.planes.y[py * ys + px..],
@@ -765,13 +795,14 @@ impl<'a> SliceDecoder<'a> {
             bw,
             bh,
             wts[0],
+            max,
         );
         for c in 0..2 {
             let wc = wts[c + 1];
             let a = if use0 { Some(&s.pred_c[0][c][..]) } else { None };
             let b = if use1 { Some(&s.pred_c[1][c][..]) } else { None };
             let plane = if c == 0 { &mut self.pic.planes.cb } else { &mut self.pic.planes.cr };
-            inter::weighted_store(&mut plane[cy * cs + cx..], cs, a, b, 8, bw / 2, bh / 2, wc);
+            inter::weighted_store(&mut plane[cy * cs + cx..], cs, a, b, 8, bw / 2, bh / 2, wc, max);
         }
     }
 
@@ -891,7 +922,7 @@ impl<'a> SliceDecoder<'a> {
         let Some(blk) = self.s.coef.get_mut(raster * 16..).and_then(<[i32]>::first_chunk_mut::<16>) else {
             return;
         };
-        transform::idct4_add(blk, &mut self.pic.planes.y[py * stride + px..], stride);
+        transform::idct4_add(blk, &mut self.pic.planes.y[py * stride + px..], stride, self.fmt.max());
         blk.fill(0);
     }
 
@@ -905,27 +936,28 @@ impl<'a> SliceDecoder<'a> {
         let Some(blk) = self.s.coef.get_mut(b8 * 64..).and_then(<[i32]>::first_chunk_mut::<64>) else {
             return;
         };
-        transform::idct8_add(blk, &mut self.pic.planes.y[py * stride + px..], stride);
+        transform::idct8_add(blk, &mut self.pic.planes.y[py * stride + px..], stride, self.fmt.max());
         blk.fill(0);
     }
 
     fn add_chroma(&mut self) {
         let stride = self.pic.planes.cwidth;
+        let (max, hc) = (self.fmt.max(), self.fmt.mb_hc);
         for c in 0..2 {
             if self.s.c_nz[c] == 0 {
                 continue;
             }
-            for b in 0..4 {
+            for b in 0..self.fmt.chroma_blocks() {
                 if self.s.c_nz[c] & (1 << b) == 0 {
                     continue;
                 }
                 let px = self.mb_x * 8 + (b & 1) * 4;
-                let py = self.mb_y * 8 + (b >> 1) * 4;
+                let py = self.mb_y * hc + (b >> 1) * 4;
                 let Some(blk) = self.s.coef_c[c].get_mut(b * 16..).and_then(<[i32]>::first_chunk_mut::<16>) else {
                     continue;
                 };
                 let plane = if c == 0 { &mut self.pic.planes.cb } else { &mut self.pic.planes.cr };
-                transform::idct4_add(blk, &mut plane[py * stride + px..], stride);
+                transform::idct4_add(blk, &mut plane[py * stride + px..], stride, max);
                 blk.fill(0);
             }
             self.s.c_nz[c] = 0;
@@ -959,6 +991,7 @@ impl<'a> SliceDecoder<'a> {
     pub fn reconstruct_intra(&mut self) {
         let kind = self.mb().kind;
         let stride = self.pic.planes.width;
+        let fmt = self.fmt;
         let (x0, y0) = (self.mb_x * 16, self.mb_y * 16);
         match kind {
             MbKind::I4x4 => {
@@ -967,7 +1000,7 @@ impl<'a> SliceDecoder<'a> {
                     let _ = blk;
                     let av = self.luma_avail4(bx, by);
                     let mode = self.mb().intra_modes[by * 4 + bx];
-                    intra::pred4x4(&mut self.pic.planes.y, stride, x0 + bx * 4, y0 + by * 4, mode, av);
+                    intra::pred4x4(&mut self.pic.planes.y, stride, x0 + bx * 4, y0 + by * 4, mode, av, fmt);
                     self.add_luma_block4(by * 4 + bx);
                 }
             }
@@ -982,13 +1015,13 @@ impl<'a> SliceDecoder<'a> {
                         _ => false,
                     };
                     let mode = self.mb().intra_modes[by * 4 + bx];
-                    intra::pred8x8(&mut self.pic.planes.y, stride, x0 + bx * 4, y0 + by * 4, mode, av);
+                    intra::pred8x8(&mut self.pic.planes.y, stride, x0 + bx * 4, y0 + by * 4, mode, av, fmt);
                     self.add_luma_block8(b8);
                 }
             }
             MbKind::I16x16 => {
                 let av = Avail { left: self.intra_nb(0), top: self.intra_nb(1), top_left: self.intra_nb(3), top_right: false };
-                intra::pred16x16(&mut self.pic.planes.y, stride, x0, y0, self.cur.info.i16_mode, av);
+                intra::pred16x16(&mut self.pic.planes.y, stride, x0, y0, self.cur.info.i16_mode, av, fmt);
                 for r in 0..16 {
                     self.add_luma_block4(r);
                 }
@@ -998,9 +1031,9 @@ impl<'a> SliceDecoder<'a> {
         let av = Avail { left: self.intra_nb(0), top: self.intra_nb(1), top_left: self.intra_nb(3), top_right: false };
         let cs = self.pic.planes.cwidth;
         let mode = self.mb().intra_chroma_mode;
-        let (cx, cy) = (self.mb_x * 8, self.mb_y * 8);
-        intra::pred_chroma(&mut self.pic.planes.cb, cs, cx, cy, mode, av);
-        intra::pred_chroma(&mut self.pic.planes.cr, cs, cx, cy, mode, av);
+        let (cx, cy) = (self.mb_x * 8, self.mb_y * fmt.mb_hc);
+        intra::pred_chroma(&mut self.pic.planes.cb, cs, cx, cy, mode, av, fmt);
+        intra::pred_chroma(&mut self.pic.planes.cr, cs, cx, cy, mode, av, fmt);
         self.add_chroma();
         self.s.blk_nz = 0;
     }
@@ -1020,33 +1053,47 @@ impl<'a> SliceDecoder<'a> {
         self.s.blk_nz = 0;
     }
 
-    /// Write I_PCM samples.
-    pub fn write_pcm(&mut self, samples: &[u8]) {
+    /// Bytes of the pcm samples of an I_PCM macroblock (7.3.5).
+    pub fn pcm_bytes(&self) -> usize {
+        (256 + 16 * self.fmt.mb_hc) * self.fmt.bit_depth as usize / 8
+    }
+
+    /// Write I_PCM samples from the byte-aligned pcm_sample_luma / pcm_sample_chroma data
+    /// (`pcm_bytes()` long).
+    pub fn write_pcm(&mut self, data: &[u8]) -> Result<()> {
+        let bits = self.fmt.bit_depth;
+        let max = self.fmt.max();
+        let mut r = BitReader::new(data);
         let stride = self.pic.planes.width;
         let (x0, y0) = (self.mb_x * 16, self.mb_y * 16);
-        for r in 0..16 {
-            self.pic.planes.y[(y0 + r) * stride + x0..(y0 + r) * stride + x0 + 16].copy_from_slice(&samples[r * 16..r * 16 + 16]);
+        for row in 0..16 {
+            for col in 0..16 {
+                self.pic.planes.y[(y0 + row) * stride + x0 + col] = S::clip(r.read_bits(bits)? as i32, max);
+            }
         }
         let cs = self.pic.planes.cwidth;
-        let (cx, cy) = (self.mb_x * 8, self.mb_y * 8);
+        let hc = self.fmt.mb_hc;
+        let (cx, cy) = (self.mb_x * 8, self.mb_y * hc);
         for c in 0..2 {
-            let src = &samples[256 + c * 64..256 + c * 64 + 64];
             let plane = if c == 0 { &mut self.pic.planes.cb } else { &mut self.pic.planes.cr };
-            for r in 0..8 {
-                plane[(cy + r) * cs + cx..(cy + r) * cs + cx + 8].copy_from_slice(&src[r * 8..r * 8 + 8]);
+            for row in 0..hc {
+                for col in 0..8 {
+                    plane[(cy + row) * cs + cx + col] = S::clip(r.read_bits(bits)? as i32, max);
+                }
             }
         }
+        Ok(())
     }
 
     /// Common state for an I_PCM MB.
     pub fn finish_pcm(&mut self) {
-        let qpc = [self.chroma_qp(0, 0) as u8, self.chroma_qp(0, 1) as u8];
+        let qpc = [self.chroma_qp(0, 0) as i8, self.chroma_qp(0, 1) as i8];
         let st = self.mb_mut();
         st.kind = MbKind::IPcm;
         st.qp = 0;
         st.qpc = qpc;
         st.nnz = [16; 16];
-        st.nnz_c = [[16; 4]; 2];
+        st.nnz_c = [[16; 8]; 2];
         st.cbf_dc = 7;
         st.cbp = 0x2f;
         st.nz_mask = 0xffff;
@@ -1055,19 +1102,33 @@ impl<'a> SliceDecoder<'a> {
 
     /// Update QP after mb_qp_delta.
     pub fn apply_qp_delta(&mut self, delta: i32) -> Result<()> {
-        ensure!((-26..=25).contains(&delta), "mb_qp_delta {delta} out of range");
-        self.qp = (self.qp + delta + 52) % 52;
+        // 7.4.5: mb_qp_delta in -(26 + QpBdOffsetY / 2)..=25 + QpBdOffsetY / 2; QPY wraps (7-37)
+        let off = self.fmt.qp_bd_offset();
+        ensure!((-(26 + off / 2)..=25 + off / 2).contains(&delta), "mb_qp_delta {delta} out of range");
+        self.qp = (self.qp + delta + 52 + 2 * off).rem_euclid(52 + off) - off;
         Ok(())
     }
 
     pub fn store_qp(&mut self) {
         let qp = self.qp;
-        let qpc = [self.chroma_qp(qp, 0) as u8, self.chroma_qp(qp, 1) as u8];
+        let qpc = [self.chroma_qp(qp, 0) as i8, self.chroma_qp(qp, 1) as i8];
         let st = self.mb_mut();
-        st.qp = qp as u8;
+        st.qp = qp as i8;
         st.qpc = qpc;
     }
 
+    /// QP'Y: the quantiser used for scaling (8.5.9).
+    #[inline]
+    fn qp_prime(&self) -> i32 {
+        self.qp + self.fmt.qp_bd_offset()
+    }
+
+    /// QP'C of component `c`.
+    #[inline]
+    fn qpc_prime(&self, c: usize) -> i32 {
+        self.chroma_qp(self.qp, c) + self.fmt.qp_bd_offset()
+    }
+
     // ------------------------------------------------------------------------------------------
     // Residual storage helpers (shared by CAVLC and CABAC)
     // ------------------------------------------------------------------------------------------
@@ -1078,7 +1139,7 @@ impl<'a> SliceDecoder<'a> {
 
     /// Store a 4x4 luma block given in scan order (levels[0..16]); `ac_only` skips position 0.
     pub fn put_luma4(&mut self, raster: usize, levels: &[i32; 16], ac_only: bool) {
-        let qp = self.qp;
+        let qp = self.qp_prime();
         let ls = &self.ls.ls4[self.luma_list4()][(qp % 6) as usize];
         let blk = &mut self.s.coef[raster * 16..raster * 16 + 16];
         let mut any = false;
@@ -1097,7 +1158,7 @@ impl<'a> SliceDecoder<'a> {
 
     /// Store an 8x8 luma block given in scan order.
     pub fn put_luma8(&mut self, b8: usize, levels: &[i32; 64]) {
-        let qp = self.qp;
+        let qp = self.qp_prime();
         let list = if self.mb().kind.is_intra() { 0 } else { 1 };
         let ls = &self.ls.ls8[list][(qp % 6) as usize];
         let blk = &mut self.s.coef[b8 * 64..b8 * 64 + 64];
@@ -1123,7 +1184,7 @@ impl<'a> SliceDecoder<'a> {
         for (k, &lv) in levels.iter().enumerate() {
             c[ZIGZAG4[k] as usize] = lv;
         }
-        let qp = self.qp;
+        let qp = self.qp_prime();
         let ls00 = self.ls.ls4[0][(qp % 6) as usize][0];
         transform::luma_dc_dequant(&mut c, qp, ls00);
         for (r, &dc) in c.iter().enumerate() {
@@ -1134,14 +1195,23 @@ impl<'a> SliceDecoder<'a> {
         }
     }
 
-    /// Chroma DC levels (4:2:0: 4 values in raster order) for component `c`.
-    pub fn put_chroma_dc(&mut self, c: usize, levels: &[i32; 4]) {
-        let mut v = *levels;
-        let qpc = self.chroma_qp(self.qp, c);
+    /// Chroma DC levels (4:2:0: 4 values in raster order; 4:2:2: 8 values in scan order) for
+    /// component `c`.
+    pub fn put_chroma_dc(&mut self, c: usize, levels: &[i32; 8]) {
+        let qpc = self.qpc_prime(c);
         let list = if self.mb().kind.is_intra() { 1 + c } else { 4 + c };
-        let ls00 = self.ls.ls4[list][(qpc % 6) as usize][0];
-        transform::chroma_dc_dequant_420(&mut v, qpc, ls00);
-        for (b, &dc) in v.iter().enumerate() {
+        let mut v = *levels;
+        if self.fmt.mb_hc == 16 {
+            // 8.5.11.2: qP,DC = QP'c + 3
+            let qp_dc = qpc + 3;
+            let ls00 = self.ls.ls4[list][(qp_dc % 6) as usize][0];
+            transform::chroma_dc_dequant_422(&mut v, qp_dc, ls00);
+        } else {
+            let ls00 = self.ls.ls4[list][(qpc % 6) as usize][0];
+            let Some(dc) = v.first_chunk_mut::<4>() else { return };
+            transform::chroma_dc_dequant_420(dc, qpc, ls00);
+        }
+        for (b, &dc) in v.iter().enumerate().take(self.fmt.chroma_blocks()) {
             if dc != 0 {
                 self.s.coef_c[c][b * 16] = dc;
                 self.s.c_nz[c] |= 1 << b;
@@ -1151,7 +1221,7 @@ impl<'a> SliceDecoder<'a> {
 
     /// Chroma AC levels (scan positions 1..16 in levels[1..]) for block `b` of component `c`.
     pub fn put_chroma_ac(&mut self, c: usize, b: usize, levels: &[i32; 16]) {
-        let qpc = self.chroma_qp(self.qp, c);
+        let qpc = self.qpc_prime(c);
         let list = if self.mb().kind.is_intra() { 1 + c } else { 4 + c };
         let ls = &self.ls.ls4[list][(qpc % 6) as usize];
         let blk = &mut self.s.coef_c[c][b * 16..b * 16 + 16];
@@ -1220,14 +1290,13 @@ impl<'a> SliceDecoder<'a> {
         if info.kind == MbKind::IPcm {
             r.byte_align();
             let pos = r.byte_pos();
-            let data = r.data();
-            ensure!(data.len() >= pos + 384, "truncated I_PCM macroblock");
-            let Some(&samples) = data.get(pos..).and_then(|d| d.first_chunk::<384>()) else {
+            let n = self.pcm_bytes();
+            let Some(samples) = r.data().get(pos..pos + n) else {
                 return invalid("truncated I_PCM macroblock");
             };
-            r.skip(384 * 8)?;
             self.finish_pcm();
-            self.write_pcm(&samples);
+            self.write_pcm(samples)?;
+            r.skip(n * 8)?;
             return Ok(());
         }
         let mut transform_8x8 = false;
@@ -1395,16 +1464,21 @@ impl<'a> SliceDecoder<'a> {
             }
         }
         let cbp_c = cbp >> 4;
+        let nb = self.fmt.chroma_blocks();
         if cbp_c & 3 != 0 {
             for c in 0..2 {
-                let mut lv = [0i32; 4];
-                cavlc::residual_block(r, &mut lv, 0, 3, 4, -1)?;
+                let mut lv = [0i32; 8];
+                if nb == 8 {
+                    cavlc::residual_block(r, &mut lv, 0, 7, 8, -2)?;
+                } else {
+                    cavlc::residual_block(r, &mut lv, 0, 3, 4, -1)?;
+                }
                 self.put_chroma_dc(c, &lv);
             }
         }
         if cbp_c & 2 != 0 {
             for c in 0..2 {
-                for b in 0..4 {
+                for b in 0..nb {
                     let nc = self.nc_chroma(c, b);
                     let mut lv = [0i32; 16];
                     let tc = cavlc::residual_block(r, &mut lv[1..], 0, 14, 15, nc)?;
diff --git a/crates/h264/src/transform.rs b/crates/h264/src/transform.rs
--- a/crates/h264/src/transform.rs
+++ b/crates/h264/src/transform.rs
@@ -1,6 +1,7 @@
 //! Scaling (dequantisation) and inverse transforms (8.5.10 - 8.5.13).
 
 use crate::params::ScalingMatrices;
+use crate::picture::Sample;
 use crate::tables::{norm_adjust4, norm_adjust8};
 
 /// LevelScale4x4 / LevelScale8x8 for all six scaling lists and qP % 6, raster order.
@@ -83,16 +84,38 @@ pub fn chroma_dc_dequant_420(c: &mut [i32; 4], qp: i32, ls00: i32) {
     }
 }
 
-#[inline(always)]
-fn clip_u8(v: i32) -> u8 {
-    v.clamp(0, 255) as u8
+/// 4:2:2 chroma DC (8.5.11): 2x4 transform + scaling. `c` = chroma DC levels in scan order
+/// (c[0..8]); the result is dcC per 4x4 block in block order (raster, 2 wide x 4 tall). `qp_dc`
+/// is QP'c + 3.
+pub fn chroma_dc_dequant_422(c: &mut [i32; 8], qp_dc: i32, ls00: i32) {
+    // scan order -> 4x2 matrix (rows, columns) of (8-330)
+    let m = [[c[0], c[2]], [c[1], c[5]], [c[3], c[6]], [c[4], c[7]]];
+    // A (4x4) * m
+    let mut t = [[0i32; 2]; 4];
+    for j in 0..2 {
+        let (a, b, cc, d) = (m[0][j], m[1][j], m[2][j], m[3][j]);
+        t[0][j] = a.wrapping_add(b).wrapping_add(cc).wrapping_add(d);
+        t[1][j] = a.wrapping_add(b).wrapping_sub(cc).wrapping_sub(d);
+        t[2][j] = a.wrapping_sub(b).wrapping_sub(cc).wrapping_add(d);
+        t[3][j] = a.wrapping_sub(b).wrapping_add(cc).wrapping_sub(d);
+    }
+    for i in 0..4 {
+        let f = [t[i][0].wrapping_add(t[i][1]), t[i][0].wrapping_sub(t[i][1])];
+        for j in 0..2 {
+            c[i * 2 + j] = if qp_dc >= 36 {
+                f[j].wrapping_mul(ls00) << (qp_dc / 6 - 6).min(31)
+            } else {
+                f[j].wrapping_mul(ls00).wrapping_add(1 << (5 - qp_dc / 6)) >> (6 - qp_dc / 6)
+            };
+        }
+    }
 }
 
 /// Inverse 4x4 transform of raster-order `d` and add to `dst` (8.5.12.2, 8.5.14).
 ///
 /// Rows first, then columns (the spec's order: the `>> 1` steps make it matter). The column
 /// pass and the reconstruction run across all columns at once so they vectorise.
-pub fn idct4_add(d: &[i32; 16], dst: &mut [u8], stride: usize) {
+pub fn idct4_add<S: Sample>(d: &[i32; 16], dst: &mut [S], stride: usize, max: i32) {
     let mut t = [[0i32; 4]; 4];
     for (i, row) in t.iter_mut().enumerate() {
         let r = &d[i * 4..i * 4 + 4];
@@ -118,18 +141,18 @@ pub fn idct4_add(d: &[i32; 16], dst: &mut [u8], stride: usize) {
             return;
         };
         for (p, &v) in line.iter_mut().zip(row) {
-            *p = clip_u8(*p as i32 + ((v + 32) >> 6));
+            *p = S::clip(p.to_i32() + ((v + 32) >> 6), max);
         }
     }
 }
 
 /// DC-only shortcut: all AC coefficients zero.
 #[allow(dead_code)]
-pub fn idct4_dc_add(dc: i32, dst: &mut [u8], stride: usize) {
+pub fn idct4_dc_add<S: Sample>(dc: i32, dst: &mut [S], stride: usize, max: i32) {
     let v = (dc + 32) >> 6;
     for i in 0..4 {
         for p in &mut dst[i * stride..i * stride + 4] {
-            *p = clip_u8(*p as i32 + v);
+            *p = S::clip(p.to_i32() + v, max);
         }
     }
 }
@@ -157,7 +180,7 @@ fn idct8_1d(d: [i32; 8]) -> [i32; 8] {
 
 /// Inverse 8x8 transform of raster-order `d` and add to `dst` (8.5.13.2): rows, then columns,
 /// the column pass and reconstruction across all columns at once (they vectorise).
-pub fn idct8_add(d: &[i32; 64], dst: &mut [u8], stride: usize) {
+pub fn idct8_add<S: Sample>(d: &[i32; 64], dst: &mut [S], stride: usize, max: i32) {
     let mut t = [[0i32; 8]; 8];
     for (row, src) in t.iter_mut().zip(d.as_chunks::<8>().0) {
         *row = idct8_1d(*src);
@@ -174,7 +197,7 @@ pub fn idct8_add(d: &[i32; 64], dst: &mut [u8], stride: usize) {
             return;
         };
         for (p, &v) in line.iter_mut().zip(row) {
-            *p = clip_u8(*p as i32 + ((v + 32) >> 6));
+            *p = S::clip(p.to_i32() + ((v + 32) >> 6), max);
         }
     }
 }
@@ -219,11 +242,11 @@ mod tests {
             let d8: [i32; 64] = std::array::from_fn(|_| (next() % (2 * range + 1)) as i32 - range as i32);
             let base: Vec<u8> = (0..24 * 8).map(|_| next() as u8).collect();
             let (mut a, mut b) = (base.clone(), base.clone());
-            idct4_add(&d4, &mut a[3..], 24);
+            idct4_add(&d4, &mut a[3..], 24, 255);
             reference_add(&d4, 4, &mut b[3..], 24);
             assert_eq!(a, b, "4x4 iteration {iter}");
             let (mut a, mut b) = (base.clone(), base);
-            idct8_add(&d8, &mut a[5..], 24);
+            idct8_add(&d8, &mut a[5..], 24, 255);
             reference_add(&d8, 8, &mut b[5..], 24);
             assert_eq!(a, b, "8x8 iteration {iter}");
         }
@@ -235,8 +258,8 @@ mod tests {
         d[0] = 640; // -> +10 everywhere
         let mut a = [100u8; 16];
         let mut b = [100u8; 16];
-        idct4_add(&d, &mut a, 4);
-        idct4_dc_add(640, &mut b, 4);
+        idct4_add(&d, &mut a, 4, 255);
+        idct4_dc_add(640, &mut b, 4, 255);
         assert_eq!(a, b);
         assert!(a.iter().all(|&v| v == 110));
     }
@@ -249,7 +272,7 @@ mod tests {
         d[0] = 64;
         d[1] = 64;
         let mut out = [0u8; 16];
-        idct4_add(&d, &mut out, 4);
+        idct4_add(&d, &mut out, 4, 255);
         for i in 0..4 {
             assert_eq!(&out[i * 4..i * 4 + 4], &[2, 2, 1, 0]);
         }
@@ -260,7 +283,7 @@ mod tests {
         let mut d = [0i32; 64];
         d[0] = 64 * 5;
         let mut out = [10u8; 64];
-        idct8_add(&d, &mut out, 8);
+        idct8_add(&d, &mut out, 8, 255);
         assert!(out.iter().all(|&v| v == 15));
     }
 
diff --git a/crates/h264/tests/it/high_bit_depth.rs b/crates/h264/tests/it/high_bit_depth.rs
new file mode 100644
--- /dev/null
+++ b/crates/h264/tests/it/high_bit_depth.rs
@@ -0,0 +1,227 @@
+//! High 10 / High 4:2:2 (Intra) streams: bit-exactness against ffmpeg's decoder on libx264
+//! fixtures with 4:2:2 and/or 10-bit samples (skipped when ffmpeg is absent).
+
+use crate::common;
+
+use filmcraft_h264::{Decoder, Picture};
+use std::path::PathBuf;
+use std::process::Command;
+
+struct HbdFixture {
+    name: &'static str,
+    width: u32,
+    height: u32,
+    frames: u32,
+    /// libx264 / ffmpeg pixel format (yuv420p10le, yuv422p10le, yuv422p).
+    pix_fmt: &'static str,
+    args: &'static [&'static str],
+}
+
+const FIXTURES: &[HbdFixture] = &[
+    // XAVC Intra-like: High 4:2:2 Intra, 10-bit, CABAC, 8x8 transform, several slices
+    HbdFixture {
+        name: "hbd_422p10_intra_cabac",
+        width: 352,
+        height: 288,
+        frames: 6,
+        pix_fmt: "yuv422p10le",
+        args: &["-profile:v", "high422", "-x264-params", "keyint=1:slices=4"],
+    },
+    HbdFixture {
+        name: "hbd_422p10_intra_cavlc",
+        width: 352,
+        height: 288,
+        frames: 6,
+        pix_fmt: "yuv422p10le",
+        args: &["-profile:v", "high422", "-x264-params", "keyint=1:cabac=0"],
+    },
+    HbdFixture {
+        name: "hbd_422p10_intra_qp_low",
+        width: 176,
+        height: 144,
+        frames: 4,
+        pix_fmt: "yuv422p10le",
+        args: &["-profile:v", "high422", "-qp", "1", "-x264-params", "keyint=1"],
+    },
+    HbdFixture {
+        name: "hbd_422p10_intra_qp_high",
+        width: 176,
+        height: 144,
+        frames: 4,
+        pix_fmt: "yuv422p10le",
+        args: &["-profile:v", "high422", "-qp", "60", "-x264-params", "keyint=1:chroma-qp-offset=4"],
+    },
+    HbdFixture {
+        name: "hbd_422p10_intra_no8x8",
+        width: 176,
+        height: 144,
+        frames: 4,
+        pix_fmt: "yuv422p10le",
+        args: &["-profile:v", "high422", "-x264-params", "keyint=1:8x8dct=0:cqm=jvt"],
+    },
+    HbdFixture {
+        name: "hbd_422p8_intra",
+        width: 352,
+        height: 288,
+        frames: 4,
+        pix_fmt: "yuv422p",
+        args: &["-profile:v", "high422", "-x264-params", "keyint=1"],
+    },
+    HbdFixture {
+        name: "hbd_422p8_intra_cavlc",
+        width: 176,
+        height: 144,
+        frames: 4,
+        pix_fmt: "yuv422p",
+        args: &["-profile:v", "high422", "-x264-params", "keyint=1:cabac=0"],
+    },
+    // High 10: inter prediction with B-frames, weighted prediction
+    HbdFixture {
+        name: "hbd_420p10_inter",
+        width: 352,
+        height: 288,
+        frames: 20,
+        pix_fmt: "yuv420p10le",
+        args: &["-profile:v", "high10", "-x264-params", "bframes=3:weightp=2:ref=3"],
+    },
+    HbdFixture {
+        name: "hbd_420p10_inter_cavlc",
+        width: 176,
+        height: 144,
+        frames: 12,
+        pix_fmt: "yuv420p10le",
+        args: &["-profile:v", "high10", "-x264-params", "cabac=0:bframes=2"],
+    },
+];
+
+fn fixture(name: &str) -> &'static HbdFixture {
+    FIXTURES.iter().find(|f| f.name == name).unwrap_or_else(|| panic!("unknown fixture {name}"))
+}
+
+/// Generate (if needed) the stream and its ffmpeg reference decode (16-bit LE above 8 bits).
+fn ensure(f: &HbdFixture) -> Option<(PathBuf, PathBuf)> {
+    let ff = filmcraft_testkit::ffmpeg_or_skip(f.name)?;
+    let dir = common::fixtures_dir();
+    let h264 = dir.join(format!("{}.h264", f.name));
+    let yuv = dir.join(format!("{}.yuv", f.name));
+    if !h264.exists() {
+        let tmp = filmcraft_testkit::temp_path(&h264);
+        let vf = format!("testsrc2=size={}x{}:rate=25,format={},noise=alls=10:allf=t+u", f.width, f.height, f.pix_fmt);
+        let ok = Command::new(&ff)
+            .args(["-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", &vf, "-frames:v", &f.frames.to_string(), "-c:v", "libx264"])
+            .args(["-pix_fmt", f.pix_fmt])
+            .args(f.args)
+            .args(["-f", "h264"])
+            .arg(&tmp)
+            .status()
+            .is_ok_and(|s| s.success());
+        assert!(ok, "fixture generation failed for {}", f.name);
+        std::fs::rename(&tmp, &h264).unwrap();
+    }
+    if !yuv.exists() {
+        let tmp = filmcraft_testkit::temp_path(&yuv);
+        let ok = Command::new(&ff)
+            .args(["-hide_banner", "-loglevel", "error", "-y", "-i"])
+            .arg(&h264)
+            .args(["-fps_mode", "passthrough", "-f", "rawvideo", "-pix_fmt", f.pix_fmt])
+            .arg(&tmp)
+            .status()
+            .is_ok_and(|s| s.success());
+        assert!(ok, "reference decode failed for {}", f.name);
+        std::fs::rename(&tmp, &yuv).unwrap();
+    }
+    Some((h264, yuv))
+}
+
+/// The picture as the bytes ffmpeg writes for its pixel format.
+fn raw(p: &Picture) -> Vec<u8> {
+    match &p.wide {
+        Some(w) => [&w.y, &w.u, &w.v].into_iter().flat_map(|pl| pl.iter().flat_map(|v| v.to_le_bytes())).collect(),
+        None => [&p.y, &p.u, &p.v].into_iter().flat_map(|pl| pl.iter().copied()).collect(),
+    }
+}
+
+fn check(name: &str, threads: usize) {
+    let f = fixture(name);
+    let Some((h264, yuv)) = ensure(f) else { return };
+    let data = std::fs::read(h264).unwrap();
+    let want = std::fs::read(yuv).unwrap();
+    let mut dec = Decoder::with_threads(threads);
+    let mut pics = Vec::new();
+    for au in common::split_access_units(&data) {
+        pics.extend(dec.decode(au, 0).unwrap_or_else(|e| panic!("{name}: {e}")));
+    }
+    pics.extend(dec.flush());
+    assert!(dec.take_error().is_none(), "{name}: decode error");
+    assert_eq!(pics.len(), f.frames as usize, "{name}: frame count");
+    let wide = f.pix_fmt.ends_with("10le");
+    let c422 = f.pix_fmt.starts_with("yuv422");
+    let frame_bytes = (f.width * f.height) as usize * if c422 { 2 } else { 3 } / if c422 { 1 } else { 2 } * if wide { 2 } else { 1 };
+    for (i, p) in pics.iter().enumerate() {
+        assert_eq!((p.width, p.height), (f.width, f.height));
+        assert_eq!(p.bit_depth, if wide { 10 } else { 8 }, "{name}");
+        assert_eq!(p.chroma_format_idc, if c422 { 2 } else { 1 }, "{name}");
+        assert_eq!(p.wide.is_some(), wide, "{name}");
+        let got = raw(p);
+        let exp = &want[i * frame_bytes..(i + 1) * frame_bytes];
+        if got != exp {
+            let first = got.iter().zip(exp).position(|(a, b)| a != b).unwrap_or(0);
+            panic!("{name} ({threads} threads): frame {i} differs from ffmpeg (first byte {first} of {frame_bytes})");
+        }
+    }
+}
+
+macro_rules! hbd_tests {
+    ($($name:ident),* $(,)?) => {
+        $(
+            #[test]
+            fn $name() {
+                check(stringify!($name), 1);
+                check(stringify!($name), 4);
+            }
+        )*
+    };
+}
+
+hbd_tests!(
+    hbd_422p10_intra_cabac,
+    hbd_422p10_intra_cavlc,
+    hbd_422p10_intra_qp_low,
+    hbd_422p10_intra_qp_high,
+    hbd_422p10_intra_no8x8,
+    hbd_422p8_intra,
+    hbd_422p8_intra_cavlc,
+    hbd_420p10_inter,
+    hbd_420p10_inter_cavlc,
+);
+
+/// 4:2:2 with inter prediction (High 4:2:2 long-GOP) is reported as unsupported, not decoded
+/// into wrong pictures and not a crash.
+#[test]
+fn hbd_422_inter_is_unsupported() {
+    let Some(ff) = filmcraft_testkit::ffmpeg_or_skip("hbd_422_inter") else { return };
+    let path = common::fixtures_dir().join("hbd_422_inter.h264");
+    if !path.exists() {
+        let tmp = filmcraft_testkit::temp_path(&path);
+        let ok = Command::new(&ff)
+            .args(["-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", "testsrc2=size=176x144:rate=25", "-frames:v", "8"])
+            .args(["-c:v", "libx264", "-pix_fmt", "yuv422p10le", "-profile:v", "high422", "-f", "h264"])
+            .arg(&tmp)
+            .status()
+            .is_ok_and(|s| s.success());
+        assert!(ok);
+        std::fs::rename(&tmp, &path).unwrap();
+    }
+    let data = std::fs::read(path).unwrap();
+    let mut dec = Decoder::with_threads(1);
+    let mut err = None;
+    for au in common::split_access_units(&data) {
+        if let Err(e) = dec.decode(au, 0) {
+            err = Some(e);
+            break;
+        }
+    }
+    dec.flush();
+    let err = err.or_else(|| dec.take_error());
+    assert!(err.is_some_and(|e| e.to_string().contains("4:2:2")), "4:2:2 inter slices must be refused");
+}
diff --git a/crates/h264/tests/it/main.rs b/crates/h264/tests/it/main.rs
--- a/crates/h264/tests/it/main.rs
+++ b/crates/h264/tests/it/main.rs
@@ -1,6 +1,7 @@
 mod common;
 mod conformance;
 mod headers;
+mod high_bit_depth;
 mod perf;
 mod pps_validation;
 mod robustness;
diff --git a/docs/codec-parity.md b/docs/codec-parity.md
--- a/docs/codec-parity.md
+++ b/docs/codec-parity.md
@@ -1,6 +1,6 @@
 # Codec parity
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (ProRes alpha final-run conformance and native macOS validation) · **Target:** Adobe Premiere Pro 2026 (26.5.2) and Media Encoder 2026
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (H.264 High 10 and High 4:2:2 Intra decode) · **Target:** Adobe Premiere Pro 2026 (26.5.2) and Media Encoder 2026
 
 Video and audio codecs, decode and encode, with fidelity and how each is tested. Containers,
 project and interchange files are in [file-format-parity.md](file-format-parity.md); hardware
@@ -15,7 +15,7 @@ contents). Percent is ready-for-real-work for that codec: depth, profiles, speed
 
 | Codec | Premiere | Our decode | Our encode | Fidelity and tests | % | Est. |
 |---|---|---|---|---|---|---|
-| H.264 / AVC | decode all common profiles incl. High 10, 4:2:2 (XAVC S-I, AVC-Intra); encode 8/10-bit | own decoder, **8-bit 4:2:0 only** (Baseline / Main / High, CABAC / CAVLC); no High 10 / 4:2:2 / 4:4:4, interlaced field / MBAFF, lossless | own encoder (High / Main / Baseline, B-frames, CBR / VBR / 2-pass); 8-bit 4:2:0 only; hardware VideoToolbox / NVENC | decode bit-exact on 37+ conformance streams, 500–600 fps 1080p; encode ffprobe-checked, byte-identical across machines | 65% | 20–35 h |
+| H.264 / AVC | decode all common profiles incl. High 10, 4:2:2 (XAVC S-I, AVC-Intra); encode 8/10-bit | own decoder (Baseline / Main / High, CABAC / CAVLC), High 10 (9/10-bit 4:2:0) and High 4:2:2 Intra (XAVC Intra, AVC-Intra); no 4:2:2 inter (long-GOP 4:2:2), 4:4:4, interlaced field / MBAFF, lossless | own encoder (High / Main / Baseline, B-frames, CBR / VBR / 2-pass); 8-bit 4:2:0 only; hardware VideoToolbox / NVENC | decode bit-exact on 37+ conformance streams, 500–600 fps 1080p; encode ffprobe-checked, byte-identical across machines | 65% | 20–35 h |
 | HEVC / H.265 | decode Main, Main 10, 4:2:2 10 (XAVC HS); encode 8/10-bit, HDR | own decoder Main / Main 10 / Main Still Picture, bit-exact on 41 fixtures, ~225 fps 1080p; **no 4:2:2 / RExt** | **hardware only** (VideoToolbox 8-bit; NVENC 8-bit and Main 10 HDR on Windows); no software encoder | NVENC round trip 48.9 dB (8-bit), 71.1 dB (10-bit); ffprobe | 60% | 30–60 h |
 | ProRes 422 family / 4444 / XQ | decode + encode all | own decode + encode, all six profiles, alpha | yes (export UI exposes 422 flavours; 4444 / XQ request #342) | ±1 LSB, 318 fps; alpha final-run conformance and native macOS HD decode checked ([details](../crates/prores/README.md#alpha-bitstream-conformance)) | 85% | 3–6 h |
 | ProRes RAW | decode | none (#345) | — | — | 0% | 30–50 h (spec availability uncertain) |
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -1,6 +1,6 @@
 # Where FilmCraft falls short of Premiere Pro
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (new file: ROADMAP's "Where we are lacking" re-measured against Premiere Pro 26.5.2 and the open issue list) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (G3: H.264 High 10 and High 4:2:2 Intra) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
 
 Every known shortfall, one entry each, ranked by what most stops an editor using FilmCraft for real
 work. This is the work list: agents should prefer it over adding checklist items. Numbers are in
@@ -65,8 +65,11 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
 
 ## G3. 10-bit and 4:2:2 camera media needs hardware
 
-- **Missing:** our H.264 decoder is 8-bit 4:2:0 only (`crates/h264/README.md`: no High 10, High
-  4:2:2, 4:4:4, lossless, interlaced field / MBAFF); HEVC has no 4:2:2 / RExt. Most mirrorless and
+- **Have:** H.264 High 10 (9/10-bit 4:2:0, intra and inter) and High 4:2:2 Intra (4:2:2 8–10-bit
+  intra pictures: Sony XAVC Intra, AVC-Intra) in software, bit-exact with ffmpeg.
+- **Missing:** H.264 4:2:2 inter prediction (long-GOP 4:2:2: XAVC S / XAVC L 4:2:2, most
+  mirrorless 4:2:2 MOVs), 4:4:4, lossless, interlaced field / MBAFF (`crates/h264/README.md`);
+  HEVC has no 4:2:2 / RExt. Most mirrorless and
   cinema cameras record exactly these (Sony XAVC S-I / XAVC HS 4:2:2 10-bit, Panasonic, Canon
   XF-AVC, Fujifilm).
 - **Evidence:** #626 (Panasonic Lumix 4K 10-bit 4:2:2 MOV). On macOS VideoToolbox decodes 10-bit
@@ -74,7 +77,7 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
   there is no software fallback, so the clip fails. Premiere decodes them everywhere (and in
   hardware on Apple silicon, Intel and NVIDIA Blackwell).
 - **Impact:** camera originals from most current cameras do not open on Windows or Linux.
-- **Estimate:** H.264 High 10 + High 4:2:2 (chroma geometry, deblocking, transforms) 15–25 h;
+- **Estimate:** H.264 4:2:2 inter (chroma motion compensation, inter chroma deblocking) 5–10 h;
   HEVC Main 4:2:2 10 (RExt subset) 15–25 h. Doc: [codec-parity.md](codec-parity.md).
 
 ## G4. Crashes at start-up, and no test CI on pull requests
@@ -323,3 +326,4 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
 | 2026-10-12 | minor | G10: MP1 (`A_MPEG/L1`) in Matroska imports (#1402) |
 | 2026-10-12 | minor | G10: OpenEXR PXR24 import |
 | 2026-10-24 | minor | G8: dragging the cut on a transition's timeline in Effect Controls moves the cut, and the transition moves with it (#577) |
+| 2026-10-26 | minor | G3: H.264 High 10 and High 4:2:2 Intra decode in software (XAVC Intra, AVC-Intra); 4:2:2 inter remains |
