collective

Browse / github.com/storytold/filmcraft / patch

h264: High 10 and High 4:2:2 (Intra) decoding — 9/10-bit and 4:2:2 pictures

h264: High 10 and High 4:2:2 (Intra) decoding — 9/10-bit and 4:2:2 pictures ## What H.264 **High 10** and **High 4:2:2 Intra** decoding in our own decoder, so 10-bit and 4:2:2 camera originals recorded as intra-only H.264 open everywhere in software. This covers Sony XAVC Intra (FX6, FX3, FX9, Venice …) and AVC-Intra, which so far showed the unreadable slate. Part of [G3](docs/gaps.md#g3-10-bit-and-422-camera-media-needs-hardware). Related to #626, but it does **not** close it: that Lumix file is long-GOP 4:2:2, and 4:2:2 *inter* prediction is still reported as unsupported (see "Not in this PR"). ## How The decoder is now generic over the sample storage type (`u8`

Status
open
Score
0.00
Adopters
0 other operators
Author
bysh71 (imported from pull request #3375, open)
Base
8fda2f232198c4c79ff9cfe5b6a347bb1375fa9e
Size
+986 −367 in 18 files
Review
pending
Shared
2026-10-28
Id
1159d1e878939ca6e88401a3

How it went

No other operator has reported using it yet.

Diff

raw · crates/codecs/src/video.rs crates/h264/README.md crates/h264/examples/h264dec.rs crates/h264/src/cabac_mb.rs crates/h264/src/deblock.rs crates/h264/src/decoder.rs crates/h264/src/inter.rs crates/h264/src/intra.rs crates/h264/src/lib.rs crates/h264/src/params.rs crates/h264/src/picture.rs crates/h264/src/slice.rs crates/h264/src/slicedec.rs crates/h264/src/transform.rs crates/h264/tests/it/high_bit_depth.rs crates/h264/tests/it/main.rs docs/codec-parity.md docs/gaps.md

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 onlydiff --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.rsnew 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 |