diff --git a/Cargo.lock b/Cargo.lock index a2a8457a..27ddf7c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2103,6 +2103,7 @@ dependencies = [ "filmcraft-export", "filmcraft-frame", "filmcraft-h264", + "filmcraft-hevc", "filmcraft-isobmff", "filmcraft-media", "filmcraft-project", diff --git a/README.md b/README.md index 83ff2dff..ba8fa9fe 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ No FFmpeg inside. The video codecs, AAC, Opus and the containers are our own Rus | **Matroska / WebM** | ✓ | | Lacing, Cues, header stripping, HDR colour metadata | | **Stills** | ✓ | ✓ | Import PNG, JPEG, GIF, WebP, TIFF and BMP; export PNG sequences and animated GIF | -Also imported: MPEG-2 / MPEG-1 video, AC-3, MP2, MXF (OP1a / OP-Atom), MPEG transport and program streams (AVCHD, broadcast, DVD), Ogg and image sequences. Not yet: HEVC and AV1 export, camera RAW, E-AC-3. Hardware decoding works on macOS (VideoToolbox), Windows (Media Foundation) and, for H.264, Linux (VA-API); H.264 export can use NVIDIA's encoder on Windows (opt-in) ([#30](https://github.com/storytold/filmcraft/issues/30)). +Also imported: MPEG-2 / MPEG-1 video, AC-3, MP2, MXF (OP1a / OP-Atom), MPEG transport and program streams (AVCHD, broadcast, DVD), Ogg and image sequences. Not yet: HEVC and AV1 export, camera RAW, E-AC-3. Hardware decoding works on macOS (VideoToolbox), Windows (Media Foundation) and, for H.264 and HEVC, Linux (VA-API); H.264 export can use NVIDIA's encoder on Windows (opt-in) ([#30](https://github.com/storytold/filmcraft/issues/30)).
@@ -287,7 +287,7 @@ We track two numbers ([ROADMAP.md](ROADMAP.md#honest-assessment-2026-10-05)): The biggest gaps today: -- **Speed on big footage.** Hardware decoding works on macOS and Windows, and for H.264 on Linux (VA-API; HEVC, VP9 and AV1 decode in software there). Blend modes and the most common effects run on the GPU, but Lumetri, keys and export rendering still run on the CPU; H.264 encoding can use the hardware encoder on macOS and on Windows with an NVIDIA GPU (opt-in, Export ▸ Hardware encoding), and H.265 export is hardware-only there ([#30](https://github.com/storytold/filmcraft/issues/30)). +- **Speed on big footage.** Hardware decoding works on macOS and Windows, and for H.264 and HEVC on Linux (VA-API; VP9 and AV1 decode in software there). Blend modes and the most common effects run on the GPU, but Lumetri, keys and export rendering still run on the CPU; H.264 encoding can use the hardware encoder on macOS and on Windows with an NVIDIA GPU (opt-in, Export ▸ Hardware encoding), and H.265 export is hardware-only there ([#30](https://github.com/storytold/filmcraft/issues/30)). - **No plugins.** No VST3 / Audio Units or OpenFX hosting. - **Delivery codecs.** H.264 is our only delivery-codec export; no HEVC or AV1 export yet. - **Real-world media and platforms.** Our decoders are bit-exact on conformance streams, but camera and phone files in the wild are less tested. Windows and Linux get far less testing than macOS. diff --git a/ROADMAP.md b/ROADMAP.md index 93cdffcf..598ec0ea 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -36,7 +36,7 @@ before choosing work. | **Correctness in depth** | not tracked | **~60–70%** | A first-time contributor found four real bugs in core paths within hours (#5–#8: AIFF import failed, Slide left linked audio behind, transitions didn't follow ripple trims, export ignored start/end times). User reports: Color Matte always grey with no picker (#29); ALSA audio error on Linux (#23). Settings are "most wired", not all. | | **Codecs and media** | ~90% | **~75%** | Our decoders are bit-exact on conformance streams. Real camera and phone media (variable frame rate, damaged files, unusual containers) is far less tested. No camera RAW (RED, BRAW, ARRIRAW), no E-AC-3. | | **Export** | ~92% | **~70%** | Delivery: H.264 only (ProRes, DNxHR and image sequences cover mastering). No HEVC or AV1 export. AAF and OMF have never been validated in Avid Media Composer or Pro Tools. Export renders and encodes on the CPU only. | -| **Performance and hardware** | ~62% | **~35–40%** | Premiere runs effects, decoding and encoding on the GPU and the hardware media engines. Here, hardware decode works on macOS (VideoToolbox, #33; active in the desktop app only since #41, so releases up to 0.2.1 decoded in software) and Windows (Media Foundation / Direct3D 11 DXVA, H.264, HEVC Main / Main 10, VP9 and AV1 4:2:0 8/10-bit, #30), and for H.264 on Linux (VA-API: 4K H.264 at 11 ms of CPU per frame instead of 417 ms). The GPU does compositing, blend modes (#32) and 31 common effects (GPU2): colour adjustments, crop/flip/transform, blurs, sharpen. Lumetri, keys, masks, the remaining effects and export rendering are CPU; H.264 encoding can use VideoToolbox on macOS (opt-in per export, no B-frames) and NVENC on Windows with an NVIDIA GPU (opt-in per export), and H.265 (HEVC) export exists there only, through the same hardware encoder. Linux decodes only H.264 in hardware (HEVC, VP9 and AV1 there are software) and has no hardware encoder. 8K isn't real-time (AV1 is on Windows with a hardware decoder). The effect list shows Premiere's "GPU accelerated" badge, but only 31 effect ids run on the GPU here; the rest, Lumetri included, run on the CPU. | +| **Performance and hardware** | ~62% | **~35–40%** | Premiere runs effects, decoding and encoding on the GPU and the hardware media engines. Here, hardware decode works on macOS (VideoToolbox, #33; active in the desktop app only since #41, so releases up to 0.2.1 decoded in software) and Windows (Media Foundation / Direct3D 11 DXVA, H.264, HEVC Main / Main 10, VP9 and AV1 4:2:0 8/10-bit, #30), and for H.264 and HEVC on Linux (VA-API: 4K H.264 at 11 ms of CPU per frame instead of 417 ms, 4K HEVC at 10 ms instead of 203 ms). The GPU does compositing, blend modes (#32) and 31 common effects (GPU2): colour adjustments, crop/flip/transform, blurs, sharpen. Lumetri, keys, masks, the remaining effects and export rendering are CPU; H.264 encoding can use VideoToolbox on macOS (opt-in per export, no B-frames) and NVENC on Windows with an NVIDIA GPU (opt-in per export), and H.265 (HEVC) export exists there only, through the same hardware encoder. Linux decodes H.264 and HEVC in hardware (VP9 and AV1 there are software) and has no hardware encoder. 8K isn't real-time (AV1 is on Windows with a hardware decoder). The effect list shows Premiere's "GPU accelerated" badge, but only 31 effect ids run on the GPU here; the rest, Lumetri included, run on the CPU. | | **Stability** | not tracked | **improving, unproven** | The never-crash pass (no panics in product code, last-resort guards) landed on 2026-10-04. There's no field record from real users' projects yet. | | **Plugins and pro ecosystem** | not tracked | **~0–10%** | No audio plugin hosting (VST3 / Audio Units) and no third-party video effects (OpenFX). For many professional editors this alone rules FilmCraft out. Team Projects and Productions are out of scope by design. | | **AI features** | partial | **~25–35%** | Speech to text exists but is off in default builds (the `whisper` feature). Enhance Speech is a DSP chain, not a model. Auto Reframe is approximate. No Generative Extend, and no media-intelligence search or auto colour. | @@ -48,7 +48,7 @@ before choosing work. In priority order. Agents should prefer this work over adding more checklist items. 1. **Hardware acceleration** (#30), the most visible gap to users: - - hardware decode on Linux (VA-API): H.264 is in; HEVC, VP9 and AV1 remain; + - hardware decode on Linux (VA-API): H.264 and HEVC are in; VP9 and AV1 remain; - zero-copy decoded frames into wgpu; - the remaining effects on the GPU: Lumetri, keys, Vignette, Video Limiter, masks (31 common effects are done); - GPU export, then hardware encode: H.264 and H.265 on macOS are in (H.264 opt-in; H.265 has no other encoder), H.264 on NVENC (Windows, opt-in); HEVC on Windows and Linux remain. @@ -100,7 +100,7 @@ matched against `filmcraft-cli commands` and the UI command table. Effects: the | Graphics and captions | 8% | text engine, shapes, per-character styles, our own graphics templates (.fcgt, 8 built-ins, export/install/edit), rolls/crawls, responsive pins + time, captions SRT/VTT/SCC/MCC/STL/TTML/DFXP, transcripts | ~85% | | Export | 8% | own H.264/AAC, ProRes, DNxHR, MXF OP1a/OP-Atom, image sequences, GIF, WAV, 5.1; preset library + user presets, full Export-mode settings, queue, Quick Export; AAF (Edit Protocol) import/export, OMF 2.0 export; no AAF/OMF validation against Avid/Pro Tools yet | ~92% | | Preferences and project management | 5% | Settings dialog with 16 categories (most settings wired), project settings, scratch disks, search bins, templates | ~80% | -| Performance | 5% | 1080p real-time, 3×1080p; 4K H.264 real-time at load ≤140 (−23% decoder cycles, opt-in draft decoding at 1/2–1/4); HEVC 2.5× cheaper, catch-up decoding, `cargo xtask bench` + `perf.stats`; blend modes on the GPU (#32); 8K and AV1 not yet real-time on a loaded machine; only H.264 decode in hardware on Linux (VA-API), and on Windows only decode and opt-in NVENC H.264 export (see the honest assessment: ~35–40% against Premiere) | ~62% | +| Performance | 5% | 1080p real-time, 3×1080p; 4K H.264 real-time at load ≤140 (−23% decoder cycles, opt-in draft decoding at 1/2–1/4); HEVC 2.5× cheaper, catch-up decoding, `cargo xtask bench` + `perf.stats`; blend modes on the GPU (#32); 8K and AV1 not yet real-time on a loaded machine; H.264 and HEVC decode in hardware on Linux (VA-API), and on Windows only decode and opt-in NVENC H.264 export (see the honest assessment: ~35–40% against Premiere) | ~62% | | **Weighted total** | | | **~87%** | ## Estimate to parity @@ -154,7 +154,7 @@ Status: ✅ done · 🟡 in progress · ⬜ not started. Estimates are remaining ## Running now -- **Hardware acceleration (#30):** landed: blend modes on the GPU (#32); VideoToolbox hardware decode + the `platform` FFI crate (#33); 31 common effects on the GPU (GPU2). Windows: Media Foundation / Direct3D 11 H.264 + HEVC decode (HW2) and VP9 + AV1 (HW3), NVENC H.264 export (HW4, opt-in). Linux: VA-API H.264 decode. Next: VA-API HEVC / VP9 / AV1, zero-copy upload, Lumetri on the GPU. +- **Hardware acceleration (#30):** landed: blend modes on the GPU (#32); VideoToolbox hardware decode + the `platform` FFI crate (#33); 31 common effects on the GPU (GPU2). Windows: Media Foundation / Direct3D 11 H.264 + HEVC decode (HW2) and VP9 + AV1 (HW3), NVENC H.264 export (HW4, opt-in). Linux: VA-API H.264 + HEVC decode (HW5). Next: VA-API VP9 / AV1, zero-copy upload, Lumetri on the GPU. - **Next:** [Where we are lacking](#where-we-are-lacking) items 1–5. GPU export waits for the export-crate work (#19 and the never-crash follow-up) to land. ## Log diff --git a/crates/engine/src/settings.rs b/crates/engine/src/settings.rs index 54627097..ca1490ee 100644 --- a/crates/engine/src/settings.rs +++ b/crates/engine/src/settings.rs @@ -955,7 +955,7 @@ static CATEGORIES: &[Category] = &[ b("playback.draftDecode", "Draft decoding at reduced playback resolution (H.264: faster, some frames less filtered)", true), f("playback.hardwareDecoding", "Hardware decoding", Kind::Choice(HW_DECODE), true), Row::Note( - "Hardware decoding: Auto uses the system's video decoder (VideoToolbox on macOS, Media Foundation on Windows, VA-API for H.264 on Linux) for the streams it supports, and FilmCraft's own decoder for everything else or if the hardware fails. Media that is already open keeps its decoder until it is reopened.", + "Hardware decoding: Auto uses the system's video decoder (VideoToolbox on macOS, Media Foundation on Windows, VA-API for H.264 and HEVC on Linux) for the streams it supports, and FilmCraft's own decoder for everything else or if the hardware fails. Media that is already open keeps its decoder until it is reopened.", ), ], }, diff --git a/crates/hevc/src/decoder.rs b/crates/hevc/src/decoder.rs index 4b87295f..48f50066 100644 --- a/crates/hevc/src/decoder.rs +++ b/crates/hevc/src/decoder.rs @@ -25,7 +25,7 @@ struct PendingPic { /// Header of the latest independent slice segment (for dependent slice segments). last_sh: SliceHeader, first_sh: SliceHeader, - rps: RefPicSet, + rps: RefPicSet, poc: i32, output: bool, meta: Arc, @@ -72,7 +72,7 @@ pub struct Decoder { spss: Vec>>, ppss: Vec>>, nal_length_size: Option, - dpb: Dpb, + dpb: Dpb, poc_state: PocState, pending: Option, next_id: u32, @@ -82,7 +82,7 @@ pub struct Decoder { /// RASL pictures associated with the last IRAP are skipped (it had NoRaslOutputFlag = 1). skip_rasl: bool, layout_cache: Vec<(Arc, Arc, Arc, Option>>>)>, - out_queue: VecDeque, + out_queue: VecDeque>, shared: Arc, #[cfg(feature = "threads")] pool: Option, @@ -578,7 +578,7 @@ fn output_meta(sps: &Sps, pts: i64, key: bool, draft: bool) -> OutputMeta { } } -fn make_picture(o: &Output) -> Picture { +fn make_picture(o: &Output) -> 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)); diff --git a/crates/hevc/src/dpb.rs b/crates/hevc/src/dpb.rs index 6bb4e177..d6f029a9 100644 --- a/crates/hevc/src/dpb.rs +++ b/crates/hevc/src/dpb.rs @@ -1,8 +1,11 @@ //! Decoded picture buffer: POC derivation (8.3.1), reference picture set (8.3.2), missing //! reference generation (8.3.3), reference picture lists (8.3.4) and output / bumping (C.5.2). +//! +//! The buffer only does bookkeeping on picture headers; what a picture is (`F`: the software +//! decoder's [`crate::picture::FrameRef`], a hardware decoder's surface) is up to the decoder, so +//! hardware decoders driven by the same headers (VA-API) share this logic and output order. use crate::error::{Result, ensure}; -use crate::picture::{FrameRef, RefPic}; use crate::slice::SliceHeader; use std::sync::Arc; @@ -29,8 +32,8 @@ pub enum Marking { Long, } -pub struct Entry { - pub frame: FrameRef, +pub struct Entry { + pub frame: F, pub poc: i32, pub marking: Marking, pub needed_for_output: bool, @@ -39,31 +42,49 @@ pub struct Entry { } /// A picture leaving the DPB for output. -pub struct Output { - pub frame: FrameRef, +pub struct Output { + pub frame: F, pub poc: i32, pub meta: Arc, } -/// Reference picture set of the current picture (8.3.2) as DPB indices / generated frames. -#[derive(Default)] -pub struct RefPicSet { - pub st_curr_before: Vec, - pub st_curr_after: Vec, - pub lt_curr: Vec, +/// A reference picture of the current picture. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Ref { + pub frame: F, + pub poc: i32, + pub long_term: bool, } -#[derive(Default)] -pub struct Dpb { - pub entries: Vec, +/// Reference picture set of the current picture (8.3.2) as DPB indices / generated frames. +pub struct RefPicSet { + pub st_curr_before: Vec>, + pub st_curr_after: Vec>, + pub lt_curr: Vec>, +} + +impl Default for RefPicSet { + fn default() -> Self { + Self { st_curr_before: Vec::new(), st_curr_after: Vec::new(), lt_curr: Vec::new() } + } +} + +pub struct Dpb { + pub entries: Vec>, pub max_dec_pic_buffering: usize, pub max_num_reorder: usize, /// SpsMaxLatencyPictures (0 = no limit). pub max_latency: u32, } -impl Dpb { - fn bump(&mut self, outs: &mut Vec) -> bool { +impl Default for Dpb { + fn default() -> Self { + Self { entries: Vec::new(), max_dec_pic_buffering: 0, max_num_reorder: 0, max_latency: 0 } + } +} + +impl Dpb { + fn bump(&mut self, outs: &mut Vec>) -> bool { let Some(i) = (0..self.entries.len()).filter(|&i| self.entries[i].needed_for_output).min_by_key(|&i| self.entries[i].poc) else { return false }; let e = &mut self.entries[i]; e.needed_for_output = false; @@ -85,7 +106,7 @@ impl Dpb { } /// Output everything (end of stream or IRAP without no_output_of_prior_pics). - pub fn flush(&mut self, outs: &mut Vec) { + pub fn flush(&mut self, outs: &mut Vec>) { while self.bump(outs) {} for e in &mut self.entries { e.marking = Marking::Unused; @@ -100,7 +121,7 @@ impl Dpb { /// C.5.2.2: removal of pictures before decoding the current (non-IRAP-reset) picture, after the RPS /// has been applied. - pub fn bump_before_decode(&mut self, outs: &mut Vec) { + pub fn bump_before_decode(&mut self, outs: &mut Vec>) { self.remove_unused(); loop { let full = self.entries.len() >= self.max_dec_pic_buffering.max(1); @@ -116,7 +137,7 @@ impl Dpb { } /// C.5.2.3: insert the current picture and apply "additional bumping". - pub fn insert(&mut self, frame: FrameRef, poc: i32, output: bool, meta: Arc, outs: &mut Vec) { + pub fn insert(&mut self, frame: F, poc: i32, output: bool, meta: Arc, outs: &mut Vec>) { for e in &mut self.entries { if e.needed_for_output { e.latency += 1; @@ -138,8 +159,8 @@ impl Dpb { poc: i32, max_poc_lsb: i32, irap_no_rasl: bool, - make_missing: &mut dyn FnMut(i32) -> FrameRef, - ) -> Result { + make_missing: &mut dyn FnMut(i32) -> F, + ) -> Result> { let mut set = RefPicSet::default(); if sh.nal.is_irap() && irap_no_rasl { for e in &mut self.entries { @@ -196,13 +217,13 @@ impl Dpb { self.entries[i].marking = Marking::Long; } } - let mut make = |found: Option, p: i32, lt: bool, entries: &mut Vec| -> RefPic { + let mut make = |found: Option, p: i32, lt: bool, entries: &mut Vec>| -> Ref { match found { - Some(i) => RefPic { frame: entries[i].frame.clone(), poc: entries[i].poc, long_term: lt }, + Some(i) => Ref { frame: entries[i].frame.clone(), poc: entries[i].poc, long_term: lt }, None => { // 8.3.3: generate an unavailable reference picture let frame = make_missing(p); - RefPic { frame, poc: p, long_term: lt } + Ref { frame, poc: p, long_term: lt } } } }; @@ -228,8 +249,8 @@ impl Dpb { } /// Build RefPicList0/1 (8.3.4). -pub fn build_ref_lists(sh: &SliceHeader, rps: &RefPicSet) -> Result<[Vec; 2]> { - let mut lists: [Vec; 2] = [Vec::new(), Vec::new()]; +pub fn build_ref_lists(sh: &SliceHeader, rps: &RefPicSet) -> Result<[Vec>; 2]> { + let mut lists: [Vec>; 2] = [Vec::new(), Vec::new()]; if sh.is_intra() { return Ok(lists); } diff --git a/crates/hevc/src/lib.rs b/crates/hevc/src/lib.rs index a9fa837f..c6a6484c 100644 --- a/crates/hevc/src/lib.rs +++ b/crates/hevc/src/lib.rs @@ -23,7 +23,7 @@ mod cabac; mod decoder; -mod dpb; +pub mod dpb; mod error; mod filter; mod inter; diff --git a/crates/hevc/src/picture.rs b/crates/hevc/src/picture.rs index 5aadbf6e..55d2730f 100644 --- a/crates/hevc/src/picture.rs +++ b/crates/hevc/src/picture.rs @@ -207,9 +207,4 @@ fn copy_line(line: &[u16], x0: i32, out: &mut [i16]) { pub type FrameRef = Arc; /// An entry of RefPicList0/1 of the current slice. -#[derive(Clone)] -pub struct RefPic { - pub frame: FrameRef, - pub poc: i32, - pub long_term: bool, -} +pub type RefPic = crate::dpb::Ref; diff --git a/crates/platform/Cargo.toml b/crates/platform/Cargo.toml index 03f8d186..55e3be06 100644 --- a/crates/platform/Cargo.toml +++ b/crates/platform/Cargo.toml @@ -27,6 +27,7 @@ filmcraft-color = { workspace = true } filmcraft-export = { workspace = true } filmcraft-frame = { workspace = true } filmcraft-h264 = { workspace = true } +filmcraft-hevc = { workspace = true } filmcraft-isobmff = { workspace = true } filmcraft-time = { workspace = true } log = { workspace = true } diff --git a/crates/platform/README.md b/crates/platform/README.md index ecf0fd21..847ef549 100644 --- a/crates/platform/README.md +++ b/crates/platform/README.md @@ -70,30 +70,37 @@ let availability = filmcraft_platform::register(); // Available("VideoToolbox") It declines two-pass VBR, HDR, MXF, interlaced output, sizes outside NVENC's limits and systems without an NVIDIA GPU or driver, and the software encoder runs instead. A failure during an export ends it with an error, since a hardware stream cannot be finished in software. -- **Linux: VA-API, H.264 (`avcC`)**, 8-bit 4:2:0 progressive, Constrained Baseline / Main / High - (`vaapi/`), on Intel (iHD, i965), AMD and other Mesa drivers. `libva.so.2` and `libva-drm.so.2` +- **Linux: VA-API, H.264 (`avcC`) and HEVC (`hvcC`)**: H.264 8-bit 4:2:0 progressive, Constrained + Baseline / Main / High; HEVC Main, Main 10 and Main Still Picture, 4:2:0 8- and 10-bit (`vaapi/`), on Intel (iHD, i965), AMD and other Mesa drivers. `libva.so.2` and `libva-drm.so.2` are loaded at run time (`vaapi/va.rs`; no libva headers needed to build), and each decoder opens its own display on the first DRM render node (`/dev/dri/renderD128`…) with a working driver. VA-API decoding is *stateless*: the host parses the stream and keeps the decoded picture buffer, and the GPU decodes one picture's slice data at a time into a surface. That host side - (`vaapi/h264.rs`, safe code) is the software decoder's own: its SPS / PPS / slice-header parsers, - POC computation and DPB (`filmcraft_h264::dpb`, generic over what a picture is: reference marking - including MMCOs, reference lists with modifications, output order). The two decoders therefore make + (`vaapi/h264.rs`, `vaapi/hevc.rs`, safe code) is the software decoders' own: their parameter-set + and slice-header parsers, POC computation and DPBs (`filmcraft_h264::dpb`, `filmcraft_hevc::dpb`, + generic over what a picture is: H.264 reference marking including MMCOs and reference lists with + modifications; HEVC reference picture sets and lists, RASL pictures of a CRA the run starts at + left out; output order). The two decoders therefore make the same decisions and output the same pictures in the same order, and the parity tests are bit-exact. Each picture is sent as one picture-parameter buffer, the scaling matrices, and a slice parameter + slice data buffer per slice (the NAL unit as stored, emulation prevention included). - Pictures the DPB outputs are read back right away (`vaGetImage` into an NV12 image, then - `biplanar.rs`), **the one GPU to CPU copy**. Surfaces: the stream's DPB size plus two. + HEVC pictures are sent the same way per slice segment (scaling lists only when enabled, converted + to raster order). Pictures the DPB outputs are read back right away (`vaGetImage` into an NV12 or + P010 image, then `biplanar.rs`), **the one GPU to CPU copy**. Surfaces: the stream's DPB size plus two. - *frame_num gaps* (and every start at a non-IDR picture: an open-GOP seek) get "non-existing" frames like the software decoder's: copies of the latest decoded frame, mid-gray when there is none (`vaPutImage`). Pictures predicted from them (the leading pictures of an open GOP after a seek) are concealment in both decoders and can differ: the hardware also reads motion data the stand-ins do not have. Everything from the seek point on is bit-exact. - - *Declined up front* (our decoder is used): HEVC, VP9 and AV1 (not through VA-API yet), H.264 - profiles other than Baseline / Main / High, 10-bit, 4:2:2 / 4:4:4, field / MBAFF coding, no - libva, no render node or driver, a profile or size the driver does not decode. *Mid-stream - errors* (`HybridDecoder` continues in software): FMO, SP / SI slices, data partitioning, a size - or DPB change, a missing reference picture, any driver error. + - *Declined up front* (our decoder is used): VP9 and AV1 (not through VA-API yet), H.264 profiles + other than Baseline / Main / High, 10-bit H.264, HEVC range extensions and profiles other than + Main / Main 10 / Main Still Picture, 4:2:2 / 4:4:4, field / MBAFF coding, no libva, no render + node or driver, a profile or size the driver does not decode. *Mid-stream errors* + (`HybridDecoder` continues in software): FMO, SP / SI slices, data partitioning, a size or DPB + change, a missing reference picture (HEVC: one the RPS names that was never decoded), more than + 15 HEVC reference frames, any driver error. + - An HEVC `flush` ends the stream as in software (references are dropped and the next CRA + leaves out its RASL pictures); the GOP cache always seeks after a flush. - **Other systems:** `register()` does nothing and returns `Availability::Unavailable`. - **`HybridDecoder`** (`hybrid.rs`, safe code): the hardware decoder plus the means to build our software decoder for the same `SampleEntry` (`filmcraft_codecs::software_video_decoder`). On a @@ -165,7 +172,7 @@ level and flags are the encoder's own. If it is missing the export stops with th when Settings ▸ Playback ▸ Hardware decoding is Off, for formats it does not take (field-coded H.264, bit depths other than 8 / 10, 4:4:4 or monochrome, luma / chroma depth mismatch, larger than 8192×8192; on Windows also 4:2:2 and the profiles listed above; on Linux everything but - 8-bit 4:2:0 progressive H.264) and when the OS cannot create a hardware session (VideoToolbox), a + 8-bit 4:2:0 progressive H.264 and 4:2:0 HEVC Main / Main 10) and when the OS cannot create a hardware session (VideoToolbox), a GPU-backed decoder (Media Foundation) or a VA-API configuration and context (Linux). - **Interchangeable:** colour, pixel aspect, pts, presentation order, `is_random_access` and `is_disposable` come from the software decoders' own helpers (`filmcraft_codecs::hw`, @@ -187,9 +194,9 @@ level and flags are the encoder's own. If it is missing the export stops with th | `tests/nvenc.rs` (Windows, NVIDIA) | H.264 from NVENC (1280×720, 6 Mbps, 72 frames) decodes with our decoder at worst 46.9 dB luma PSNR; IDR at 0, 24 and 48; dts / pts right | | `tests/nvenc_export.rs` (Windows, NVIDIA) | Export with hardware encoding against the software encoder through the export pipeline: the two decoded files at worst 54.8 dB luma PSNR; ffmpeg decodes the file without errors; declined cases go to the software encoder; the counters | | `src/nvenc/abi_tests.rs` (Windows) | FFI structs' sizes, alignments, field offsets, constants and GUIDs against a C compiler's view of NVIDIA's `nvEncodeAPI.h` (12.1) | -| `tests/vaapi.rs` (Linux, VA-API) | H.264 High (B-pyramid), Constrained Baseline with 3 slices per picture, Main with explicit weighted prediction (P and B) and 4 references, temporal direct, custom scaling matrices (JVT) with the 8x8 transform and 2 slices, an open-GOP stream, and 1080p / 2160p, 640×360 (coded 368: cropping): every picture **bit-exact** with our software decoder, same pts order, count, colour and aspect, also after `reset` + reseek to every later sync sample (after an open-GOP I picture, from the seek point on), a mid-stream `flush` with decoding carrying on, and a full pass after resets; forced mid-stream failures (`VaDecoder::fail_after`) at five points continue with the software decoder's exact output; interlaced and 10-bit H.264, HEVC and the Off setting are declined; seeded mutation of samples (through the hybrid and straight into the hardware decoder) and of `avcC` records never panics or hangs; 40 decoders created and dropped; concurrent decoders; a decoder moving between threads | -| `src/vaapi/tests.rs` (every OS) | The H.264 front end with a recording stand-in for the hardware, on x264 streams (B-pyramid, weighted prediction, 4 slices, open GOP with temporal direct): output order and pts are the software decoder's, also after seeks and a mid-stream flush; every buffer is consistent (current surface free, references decoded and listed once, reference lists of the active length, slice data offsets inside the slice); damaged samples are errors | -| `src/vaapi/abi_tests.rs` | FFI structs' sizes, alignments, field offsets, constants and bit-field positions against a C compiler's view of libva's `va.h` (VA-API 1.23) | +| `tests/vaapi.rs` (Linux, VA-API) | HEVC Main (open GOP: CRA + RASL), Main 10, scaling lists, 3 slices with WPP (10-bit), weighted prediction with 4 references, transform skip + AMP + B-pyramid (10-bit, open GOP), Main / Main 10 at 1080p and 2160p (compared picture by picture, little memory); H.264 High (B-pyramid), Constrained Baseline with 3 slices per picture, Main with explicit weighted prediction (P and B) and 4 references, temporal direct, custom scaling matrices (JVT) with the 8x8 transform and 2 slices, an open-GOP stream, and 1080p / 2160p, 640×360 (coded 368: cropping): every picture **bit-exact** with our software decoder, same pts order, count, colour and aspect, also after `reset` + reseek to every later sync sample (after an open-GOP I picture, from the seek point on), a mid-stream `flush` with decoding carrying on, and a full pass after resets; forced mid-stream failures (`VaDecoder::fail_after`) at five points continue with the software decoder's exact output; interlaced and 10-bit H.264, 4:2:2 HEVC and the Off setting are declined; an HEVC mid-GOP flush is not checked (it ends the stream, as in software); seeded mutation of samples (through the hybrid and straight into the hardware decoder) and of `avcC` records never panics or hangs; 40 decoders created and dropped; concurrent decoders; a decoder moving between threads | +| `src/vaapi/tests.rs` (every OS) | The H.264 and HEVC front ends with a recording stand-in for the hardware, on x264 streams (B-pyramid, weighted prediction, 4 slices, open GOP with temporal direct) and x265 streams (open GOP with RASL, B-pyramid, Main 10 with 2 slices and weighted prediction): output order and pts are the software decoder's, also after seeks and a mid-stream flush; every buffer is consistent (current surface free, references decoded and listed once, reference lists of the active length, slice data offsets inside the slice); damaged samples are errors | +| `src/vaapi/abi_tests.rs` | FFI structs' sizes, alignments, field offsets, constants and bit-field positions against a C compiler's view of libva's `va.h` and `va_dec_hevc.h` (VA-API 1.23) | Fixtures are made with ffmpeg into `target/fixtures/platform/` (generator only, never linked); tests skip without ffmpeg or without a hardware decoder. @@ -214,12 +221,13 @@ playback with no dropped frames at Full, 1/2 and 1/4. Details in [docs/performance.md](../../docs/performance.md). Hardware decoding, Linux (Intel Iris Xe, iHD driver; `cargo xtask bench --hw off|auto`): CPU per -decoded frame H.264 1080p 104 → 2.3 ms, 2160p 417 → 11.4 ms; decode 133 → 263 fps and 32 → 56 fps. +decoded frame H.264 1080p 104 → 2.3 ms, 2160p 417 → 11.4 ms; HEVC 1080p 46 → 2.3 ms, 2160p +203 → 10.1 ms, Main 10 2160p 212 → 18.4 ms. Details in [docs/performance.md](../../docs/performance.md). ## Not yet Zero-copy upload of decoded pictures into wgpu textures (`CVPixelBuffer`s on macOS, Direct3D 11 textures on Windows); B-frames and 10-bit / HDR HEVC (Main 10) in hardware encoding; HEVC, VP9 and -AV1 through VA-API (Linux), and VA-API encoding; field-coded H.264; HEVC, 10-bit and HDR encoding with NVENC, and encoders from other +VP9 and AV1 through VA-API (Linux), and VA-API encoding; field-coded H.264; HEVC, 10-bit and HDR encoding with NVENC, and encoders from other vendors on Windows (through Media Foundation); VP9 / AV1 4:4:4 and 12-bit on Windows. diff --git a/crates/platform/src/lib.rs b/crates/platform/src/lib.rs index e19aa68b..b203e19a 100644 --- a/crates/platform/src/lib.rs +++ b/crates/platform/src/lib.rs @@ -182,17 +182,15 @@ pub fn media_foundation_factory(entry: &filmcraft_isobmff::SampleEntry) -> Optio } } -/// The VA-API factory: a [`HybridDecoder`] around [`vaapi::VaDecoder`] for H.264 streams this -/// system's VA-API driver decodes, `None` otherwise (other codecs keep the software decoders). +/// The VA-API factory: a [`HybridDecoder`] around [`vaapi::VaDecoder`] for H.264 and HEVC +/// streams this system's VA-API driver decodes, `None` otherwise (other codecs keep the software +/// decoders). #[cfg(target_os = "linux")] pub fn vaapi_factory(entry: &filmcraft_isobmff::SampleEntry) -> Option>> { if !filmcraft_codecs::hw::hardware_decoding() { return None; } let info = filmcraft_codecs::hw::NalStreamInfo::from_entry(entry)?.ok()?; - if info.codec != filmcraft_codecs::hw::NalCodec::H264 { - return None; - } match vaapi::VaDecoder::new(info.clone()) { Ok(va) => Some(Ok(Box::new(HybridDecoder::new(Box::new(va), entry.clone(), info)))), Err(why) => { diff --git a/crates/platform/src/vaapi/abi_tests.rs b/crates/platform/src/vaapi/abi_tests.rs index bb04c62b..69e5ffc0 100644 --- a/crates/platform/src/vaapi/abi_tests.rs +++ b/crates/platform/src/vaapi/abi_tests.rs @@ -1,4 +1,4 @@ -//! The VA-API FFI structures against the C compiler's view of libva's `va.h` (VA-API 1.23, libva +//! The VA-API FFI structures against the C compiler's view of libva's `va.h` and `va_dec_hevc.h` (VA-API 1.23, libva //! 2.23): sizes, alignments, field offsets, constants and bit-field positions. Generated by running a //! C program (built with GCC against the libva-dev headers, x86-64 Linux) that prints them; the //! header itself is not in this repository. To regenerate, compile a program printing `sizeof` / @@ -100,6 +100,149 @@ fn layouts_match_the_header() { assert_eq!(offset_of!(VAImage, entry_bytes), 96, "VAImage.entry_bytes"); assert_eq!(offset_of!(VAImage, component_order), 100, "VAImage.component_order"); assert_eq!(offset_of!(VAImage, va_reserved), 104, "VAImage.va_reserved"); + assert_eq!((size_of::(), align_of::()), (28, 4), "VAPictureHEVC"); + assert_eq!(offset_of!(VAPictureHEVC, picture_id), 0, "VAPictureHEVC.picture_id"); + assert_eq!(offset_of!(VAPictureHEVC, pic_order_cnt), 4, "VAPictureHEVC.pic_order_cnt"); + assert_eq!(offset_of!(VAPictureHEVC, flags), 8, "VAPictureHEVC.flags"); + assert_eq!(offset_of!(VAPictureHEVC, va_reserved), 12, "VAPictureHEVC.va_reserved"); + assert_eq!((size_of::(), align_of::()), (604, 4), "VAPictureParameterBufferHEVC"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, CurrPic), 0, "VAPictureParameterBufferHEVC.CurrPic"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, ReferenceFrames), 28, "VAPictureParameterBufferHEVC.ReferenceFrames"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, pic_width_in_luma_samples), 448, "VAPictureParameterBufferHEVC.pic_width_in_luma_samples"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, pic_height_in_luma_samples), 450, "VAPictureParameterBufferHEVC.pic_height_in_luma_samples"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, pic_fields), 452, "VAPictureParameterBufferHEVC.pic_fields"); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, sps_max_dec_pic_buffering_minus1), + 456, + "VAPictureParameterBufferHEVC.sps_max_dec_pic_buffering_minus1" + ); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, bit_depth_luma_minus8), 457, "VAPictureParameterBufferHEVC.bit_depth_luma_minus8"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, bit_depth_chroma_minus8), 458, "VAPictureParameterBufferHEVC.bit_depth_chroma_minus8"); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, pcm_sample_bit_depth_luma_minus1), + 459, + "VAPictureParameterBufferHEVC.pcm_sample_bit_depth_luma_minus1" + ); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, pcm_sample_bit_depth_chroma_minus1), + 460, + "VAPictureParameterBufferHEVC.pcm_sample_bit_depth_chroma_minus1" + ); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, log2_min_luma_coding_block_size_minus3), + 461, + "VAPictureParameterBufferHEVC.log2_min_luma_coding_block_size_minus3" + ); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, log2_diff_max_min_luma_coding_block_size), + 462, + "VAPictureParameterBufferHEVC.log2_diff_max_min_luma_coding_block_size" + ); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, log2_min_transform_block_size_minus2), + 463, + "VAPictureParameterBufferHEVC.log2_min_transform_block_size_minus2" + ); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, log2_diff_max_min_transform_block_size), + 464, + "VAPictureParameterBufferHEVC.log2_diff_max_min_transform_block_size" + ); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, log2_min_pcm_luma_coding_block_size_minus3), + 465, + "VAPictureParameterBufferHEVC.log2_min_pcm_luma_coding_block_size_minus3" + ); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, log2_diff_max_min_pcm_luma_coding_block_size), + 466, + "VAPictureParameterBufferHEVC.log2_diff_max_min_pcm_luma_coding_block_size" + ); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, max_transform_hierarchy_depth_intra), + 467, + "VAPictureParameterBufferHEVC.max_transform_hierarchy_depth_intra" + ); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, max_transform_hierarchy_depth_inter), + 468, + "VAPictureParameterBufferHEVC.max_transform_hierarchy_depth_inter" + ); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, init_qp_minus26), 469, "VAPictureParameterBufferHEVC.init_qp_minus26"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, diff_cu_qp_delta_depth), 470, "VAPictureParameterBufferHEVC.diff_cu_qp_delta_depth"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, pps_cb_qp_offset), 471, "VAPictureParameterBufferHEVC.pps_cb_qp_offset"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, pps_cr_qp_offset), 472, "VAPictureParameterBufferHEVC.pps_cr_qp_offset"); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, log2_parallel_merge_level_minus2), + 473, + "VAPictureParameterBufferHEVC.log2_parallel_merge_level_minus2" + ); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, num_tile_columns_minus1), 474, "VAPictureParameterBufferHEVC.num_tile_columns_minus1"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, num_tile_rows_minus1), 475, "VAPictureParameterBufferHEVC.num_tile_rows_minus1"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, column_width_minus1), 476, "VAPictureParameterBufferHEVC.column_width_minus1"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, row_height_minus1), 514, "VAPictureParameterBufferHEVC.row_height_minus1"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, slice_parsing_fields), 556, "VAPictureParameterBufferHEVC.slice_parsing_fields"); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, log2_max_pic_order_cnt_lsb_minus4), + 560, + "VAPictureParameterBufferHEVC.log2_max_pic_order_cnt_lsb_minus4" + ); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, num_short_term_ref_pic_sets), 561, "VAPictureParameterBufferHEVC.num_short_term_ref_pic_sets"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, num_long_term_ref_pic_sps), 562, "VAPictureParameterBufferHEVC.num_long_term_ref_pic_sps"); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, num_ref_idx_l0_default_active_minus1), + 563, + "VAPictureParameterBufferHEVC.num_ref_idx_l0_default_active_minus1" + ); + assert_eq!( + offset_of!(VAPictureParameterBufferHEVC, num_ref_idx_l1_default_active_minus1), + 564, + "VAPictureParameterBufferHEVC.num_ref_idx_l1_default_active_minus1" + ); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, pps_beta_offset_div2), 565, "VAPictureParameterBufferHEVC.pps_beta_offset_div2"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, pps_tc_offset_div2), 566, "VAPictureParameterBufferHEVC.pps_tc_offset_div2"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, num_extra_slice_header_bits), 567, "VAPictureParameterBufferHEVC.num_extra_slice_header_bits"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, st_rps_bits), 568, "VAPictureParameterBufferHEVC.st_rps_bits"); + assert_eq!(offset_of!(VAPictureParameterBufferHEVC, va_reserved), 572, "VAPictureParameterBufferHEVC.va_reserved"); + assert_eq!((size_of::(), align_of::()), (264, 4), "VASliceParameterBufferHEVC"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, slice_data_size), 0, "VASliceParameterBufferHEVC.slice_data_size"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, slice_data_offset), 4, "VASliceParameterBufferHEVC.slice_data_offset"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, slice_data_flag), 8, "VASliceParameterBufferHEVC.slice_data_flag"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, slice_data_byte_offset), 12, "VASliceParameterBufferHEVC.slice_data_byte_offset"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, slice_segment_address), 16, "VASliceParameterBufferHEVC.slice_segment_address"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, RefPicList), 20, "VASliceParameterBufferHEVC.RefPicList"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, LongSliceFlags), 52, "VASliceParameterBufferHEVC.LongSliceFlags"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, collocated_ref_idx), 56, "VASliceParameterBufferHEVC.collocated_ref_idx"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, num_ref_idx_l0_active_minus1), 57, "VASliceParameterBufferHEVC.num_ref_idx_l0_active_minus1"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, num_ref_idx_l1_active_minus1), 58, "VASliceParameterBufferHEVC.num_ref_idx_l1_active_minus1"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, slice_qp_delta), 59, "VASliceParameterBufferHEVC.slice_qp_delta"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, slice_cb_qp_offset), 60, "VASliceParameterBufferHEVC.slice_cb_qp_offset"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, slice_cr_qp_offset), 61, "VASliceParameterBufferHEVC.slice_cr_qp_offset"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, slice_beta_offset_div2), 62, "VASliceParameterBufferHEVC.slice_beta_offset_div2"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, slice_tc_offset_div2), 63, "VASliceParameterBufferHEVC.slice_tc_offset_div2"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, luma_log2_weight_denom), 64, "VASliceParameterBufferHEVC.luma_log2_weight_denom"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, delta_chroma_log2_weight_denom), 65, "VASliceParameterBufferHEVC.delta_chroma_log2_weight_denom"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, delta_luma_weight_l0), 66, "VASliceParameterBufferHEVC.delta_luma_weight_l0"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, luma_offset_l0), 81, "VASliceParameterBufferHEVC.luma_offset_l0"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, delta_chroma_weight_l0), 96, "VASliceParameterBufferHEVC.delta_chroma_weight_l0"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, ChromaOffsetL0), 126, "VASliceParameterBufferHEVC.ChromaOffsetL0"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, delta_luma_weight_l1), 156, "VASliceParameterBufferHEVC.delta_luma_weight_l1"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, luma_offset_l1), 171, "VASliceParameterBufferHEVC.luma_offset_l1"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, delta_chroma_weight_l1), 186, "VASliceParameterBufferHEVC.delta_chroma_weight_l1"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, ChromaOffsetL1), 216, "VASliceParameterBufferHEVC.ChromaOffsetL1"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, five_minus_max_num_merge_cand), 246, "VASliceParameterBufferHEVC.five_minus_max_num_merge_cand"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, num_entry_point_offsets), 248, "VASliceParameterBufferHEVC.num_entry_point_offsets"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, entry_offset_to_subset_array), 250, "VASliceParameterBufferHEVC.entry_offset_to_subset_array"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, slice_data_num_emu_prevn_bytes), 252, "VASliceParameterBufferHEVC.slice_data_num_emu_prevn_bytes"); + assert_eq!(offset_of!(VASliceParameterBufferHEVC, va_reserved), 256, "VASliceParameterBufferHEVC.va_reserved"); + assert_eq!((size_of::(), align_of::()), (1016, 4), "VAIQMatrixBufferHEVC"); + assert_eq!(offset_of!(VAIQMatrixBufferHEVC, ScalingList4x4), 0, "VAIQMatrixBufferHEVC.ScalingList4x4"); + assert_eq!(offset_of!(VAIQMatrixBufferHEVC, ScalingList8x8), 96, "VAIQMatrixBufferHEVC.ScalingList8x8"); + assert_eq!(offset_of!(VAIQMatrixBufferHEVC, ScalingList16x16), 480, "VAIQMatrixBufferHEVC.ScalingList16x16"); + assert_eq!(offset_of!(VAIQMatrixBufferHEVC, ScalingList32x32), 864, "VAIQMatrixBufferHEVC.ScalingList32x32"); + assert_eq!(offset_of!(VAIQMatrixBufferHEVC, ScalingListDC16x16), 992, "VAIQMatrixBufferHEVC.ScalingListDC16x16"); + assert_eq!(offset_of!(VAIQMatrixBufferHEVC, ScalingListDC32x32), 998, "VAIQMatrixBufferHEVC.ScalingListDC32x32"); + assert_eq!(offset_of!(VAIQMatrixBufferHEVC, va_reserved), 1000, "VAIQMatrixBufferHEVC.va_reserved"); } #[test] @@ -128,6 +271,15 @@ fn constants_match_the_header() { assert_eq!(VA_PICTURE_H264_SHORT_TERM_REFERENCE as i64, 8, "VA_PICTURE_H264_SHORT_TERM_REFERENCE"); assert_eq!(VA_PICTURE_H264_LONG_TERM_REFERENCE as i64, 16, "VA_PICTURE_H264_LONG_TERM_REFERENCE"); assert_eq!(VA_FOURCC_NV12 as i64, 842094158, "VA_FOURCC_NV12"); + assert_eq!(VA_FOURCC_P010 as i64, 808530000, "VA_FOURCC_P010"); + assert_eq!(VAProfileHEVCMain as i64, 17, "VAProfileHEVCMain"); + assert_eq!(VAProfileHEVCMain10 as i64, 18, "VAProfileHEVCMain10"); + assert_eq!(VA_RT_FORMAT_YUV420_10 as i64, 256, "VA_RT_FORMAT_YUV420_10"); + assert_eq!(VA_PICTURE_HEVC_INVALID as i64, 1, "VA_PICTURE_HEVC_INVALID"); + assert_eq!(VA_PICTURE_HEVC_LONG_TERM_REFERENCE as i64, 8, "VA_PICTURE_HEVC_LONG_TERM_REFERENCE"); + assert_eq!(VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE as i64, 16, "VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE"); + assert_eq!(VA_PICTURE_HEVC_RPS_ST_CURR_AFTER as i64, 32, "VA_PICTURE_HEVC_RPS_ST_CURR_AFTER"); + assert_eq!(VA_PICTURE_HEVC_RPS_LT_CURR as i64, 64, "VA_PICTURE_HEVC_RPS_LT_CURR"); assert_eq!(VA_PADDING_LOW as i64, 4, "VA_PADDING_LOW"); assert_eq!(VA_PADDING_MEDIUM as i64, 8, "VA_PADDING_MEDIUM"); } @@ -156,4 +308,58 @@ fn bit_fields_match_the_header() { assert_eq!(1u32 << pic_bits::DEBLOCKING_FILTER_CONTROL_PRESENT, 0x100, "pic_fields.deblocking_filter_control_present_flag"); assert_eq!(1u32 << pic_bits::REDUNDANT_PIC_CNT_PRESENT, 0x200, "pic_fields.redundant_pic_cnt_present_flag"); assert_eq!(1u32 << pic_bits::REFERENCE_PIC, 0x400, "pic_fields.reference_pic_flag"); + assert_eq!(3u32 << hevc_pic_bits::CHROMA_FORMAT_IDC, 0x3, "hevc pic_fields.chroma_format_idc"); + assert_eq!(1u32 << hevc_pic_bits::SEPARATE_COLOUR_PLANE, 0x4, "hevc pic_fields.separate_colour_plane_flag"); + assert_eq!(1u32 << hevc_pic_bits::PCM_ENABLED, 0x8, "hevc pic_fields.pcm_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::SCALING_LIST_ENABLED, 0x10, "hevc pic_fields.scaling_list_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::TRANSFORM_SKIP_ENABLED, 0x20, "hevc pic_fields.transform_skip_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::AMP_ENABLED, 0x40, "hevc pic_fields.amp_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::STRONG_INTRA_SMOOTHING_ENABLED, 0x80, "hevc pic_fields.strong_intra_smoothing_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::SIGN_DATA_HIDING_ENABLED, 0x100, "hevc pic_fields.sign_data_hiding_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::CONSTRAINED_INTRA_PRED, 0x200, "hevc pic_fields.constrained_intra_pred_flag"); + assert_eq!(1u32 << hevc_pic_bits::CU_QP_DELTA_ENABLED, 0x400, "hevc pic_fields.cu_qp_delta_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::WEIGHTED_PRED, 0x800, "hevc pic_fields.weighted_pred_flag"); + assert_eq!(1u32 << hevc_pic_bits::WEIGHTED_BIPRED, 0x1000, "hevc pic_fields.weighted_bipred_flag"); + assert_eq!(1u32 << hevc_pic_bits::TRANSQUANT_BYPASS_ENABLED, 0x2000, "hevc pic_fields.transquant_bypass_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::TILES_ENABLED, 0x4000, "hevc pic_fields.tiles_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::ENTROPY_CODING_SYNC_ENABLED, 0x8000, "hevc pic_fields.entropy_coding_sync_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::PPS_LOOP_FILTER_ACROSS_SLICES_ENABLED, 0x10000, "hevc pic_fields.pps_loop_filter_across_slices_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::LOOP_FILTER_ACROSS_TILES_ENABLED, 0x20000, "hevc pic_fields.loop_filter_across_tiles_enabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::PCM_LOOP_FILTER_DISABLED, 0x40000, "hevc pic_fields.pcm_loop_filter_disabled_flag"); + assert_eq!(1u32 << hevc_pic_bits::NO_PIC_REORDERING, 0x80000, "hevc pic_fields.NoPicReorderingFlag"); + assert_eq!(1u32 << hevc_pic_bits::NO_BI_PRED, 0x100000, "hevc pic_fields.NoBiPredFlag"); + assert_eq!(1u32 << hevc_slice_parsing_bits::LISTS_MODIFICATION_PRESENT, 0x1, "hevc slice_parsing_fields.lists_modification_present_flag"); + assert_eq!(1u32 << hevc_slice_parsing_bits::LONG_TERM_REF_PICS_PRESENT, 0x2, "hevc slice_parsing_fields.long_term_ref_pics_present_flag"); + assert_eq!(1u32 << hevc_slice_parsing_bits::SPS_TEMPORAL_MVP_ENABLED, 0x4, "hevc slice_parsing_fields.sps_temporal_mvp_enabled_flag"); + assert_eq!(1u32 << hevc_slice_parsing_bits::CABAC_INIT_PRESENT, 0x8, "hevc slice_parsing_fields.cabac_init_present_flag"); + assert_eq!(1u32 << hevc_slice_parsing_bits::OUTPUT_FLAG_PRESENT, 0x10, "hevc slice_parsing_fields.output_flag_present_flag"); + assert_eq!(1u32 << hevc_slice_parsing_bits::DEPENDENT_SLICE_SEGMENTS_ENABLED, 0x20, "hevc slice_parsing_fields.dependent_slice_segments_enabled_flag"); + assert_eq!( + 1u32 << hevc_slice_parsing_bits::PPS_SLICE_CHROMA_QP_OFFSETS_PRESENT, + 0x40, + "hevc slice_parsing_fields.pps_slice_chroma_qp_offsets_present_flag" + ); + assert_eq!(1u32 << hevc_slice_parsing_bits::SAMPLE_ADAPTIVE_OFFSET_ENABLED, 0x80, "hevc slice_parsing_fields.sample_adaptive_offset_enabled_flag"); + assert_eq!(1u32 << hevc_slice_parsing_bits::DEBLOCKING_FILTER_OVERRIDE_ENABLED, 0x100, "hevc slice_parsing_fields.deblocking_filter_override_enabled_flag"); + assert_eq!(1u32 << hevc_slice_parsing_bits::PPS_DISABLE_DEBLOCKING_FILTER, 0x200, "hevc slice_parsing_fields.pps_disable_deblocking_filter_flag"); + assert_eq!( + 1u32 << hevc_slice_parsing_bits::SLICE_SEGMENT_HEADER_EXTENSION_PRESENT, + 0x400, + "hevc slice_parsing_fields.slice_segment_header_extension_present_flag" + ); + assert_eq!(1u32 << hevc_slice_parsing_bits::RAP_PIC, 0x800, "hevc slice_parsing_fields.RapPicFlag"); + assert_eq!(1u32 << hevc_slice_parsing_bits::IDR_PIC, 0x1000, "hevc slice_parsing_fields.IdrPicFlag"); + assert_eq!(1u32 << hevc_slice_parsing_bits::INTRA_PIC, 0x2000, "hevc slice_parsing_fields.IntraPicFlag"); + assert_eq!(1u32 << hevc_slice_bits::LAST_SLICE_OF_PIC, 0x1, "LongSliceFlags.LastSliceOfPic"); + assert_eq!(1u32 << hevc_slice_bits::DEPENDENT_SLICE_SEGMENT, 0x2, "LongSliceFlags.dependent_slice_segment_flag"); + assert_eq!(3u32 << hevc_slice_bits::SLICE_TYPE, 0xc, "LongSliceFlags.slice_type"); + assert_eq!(3u32 << hevc_slice_bits::COLOR_PLANE_ID, 0x30, "LongSliceFlags.color_plane_id"); + assert_eq!(1u32 << hevc_slice_bits::SLICE_SAO_LUMA, 0x40, "LongSliceFlags.slice_sao_luma_flag"); + assert_eq!(1u32 << hevc_slice_bits::SLICE_SAO_CHROMA, 0x80, "LongSliceFlags.slice_sao_chroma_flag"); + assert_eq!(1u32 << hevc_slice_bits::MVD_L1_ZERO, 0x100, "LongSliceFlags.mvd_l1_zero_flag"); + assert_eq!(1u32 << hevc_slice_bits::CABAC_INIT, 0x200, "LongSliceFlags.cabac_init_flag"); + assert_eq!(1u32 << hevc_slice_bits::SLICE_TEMPORAL_MVP_ENABLED, 0x400, "LongSliceFlags.slice_temporal_mvp_enabled_flag"); + assert_eq!(1u32 << hevc_slice_bits::SLICE_DEBLOCKING_FILTER_DISABLED, 0x800, "LongSliceFlags.slice_deblocking_filter_disabled_flag"); + assert_eq!(1u32 << hevc_slice_bits::COLLOCATED_FROM_L0, 0x1000, "LongSliceFlags.collocated_from_l0_flag"); + assert_eq!(1u32 << hevc_slice_bits::SLICE_LOOP_FILTER_ACROSS_SLICES_ENABLED, 0x2000, "LongSliceFlags.slice_loop_filter_across_slices_enabled_flag"); } diff --git a/crates/platform/src/vaapi/ffi.rs b/crates/platform/src/vaapi/ffi.rs index f7a6fe66..4fc2e28e 100644 --- a/crates/platform/src/vaapi/ffi.rs +++ b/crates/platform/src/vaapi/ffi.rs @@ -1,4 +1,4 @@ -// The declarations in this file are transcribed from libva's `va/va.h` and `va/va_drm.h` +// The declarations in this file are transcribed from libva's `va/va.h`, `va/va_dec_hevc.h` and `va/va_drm.h` // (VA-API 1.23, libva 2.23), which carry this notice: // // Copyright (c) 2007-2009 Intel Corporation. All Rights Reserved. @@ -23,8 +23,8 @@ // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -//! VA-API (`va.h`, `va_drm.h`; MIT-licensed headers by Intel) as `libva.so.2` / `libva-drm.so.2` -//! expect it: the H.264 decode buffers this backend fills in, the image structures it reads back +//! VA-API (`va.h`, `va_dec_hevc.h`, `va_drm.h`; MIT-licensed headers by Intel) as `libva.so.2` / +//! `libva-drm.so.2` expect it: the H.264 and HEVC decode buffers this backend fills in, the image structures it reads back //! through, and the entry points it calls. Written from the public header; sizes and field offsets //! are checked against a C compiler's by `abi_tests.rs`. //! @@ -68,10 +68,13 @@ pub const VA_INVALID_SURFACE: VASurfaceID = VA_INVALID_ID; pub const VAProfileH264Main: VAProfile = 6; pub const VAProfileH264High: VAProfile = 7; pub const VAProfileH264ConstrainedBaseline: VAProfile = 13; +pub const VAProfileHEVCMain: VAProfile = 17; +pub const VAProfileHEVCMain10: VAProfile = 18; pub const VAEntrypointVLD: VAEntrypoint = 1; pub const VAConfigAttribRTFormat: VAConfigAttribType = 0; pub const VA_RT_FORMAT_YUV420: c_uint = 0x0000_0001; +pub const VA_RT_FORMAT_YUV420_10: c_uint = 0x0000_0100; /// `vaCreateContext` flag: progressive pictures only. pub const VA_PROGRESSIVE: c_int = 0x1; @@ -89,6 +92,13 @@ pub const VA_PICTURE_H264_SHORT_TERM_REFERENCE: u32 = 0x0000_0008; pub const VA_PICTURE_H264_LONG_TERM_REFERENCE: u32 = 0x0000_0010; pub const VA_FOURCC_NV12: u32 = 0x3231_564E; +pub const VA_FOURCC_P010: u32 = 0x3031_3050; + +pub const VA_PICTURE_HEVC_INVALID: u32 = 0x0000_0001; +pub const VA_PICTURE_HEVC_LONG_TERM_REFERENCE: u32 = 0x0000_0008; +pub const VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE: u32 = 0x0000_0010; +pub const VA_PICTURE_HEVC_RPS_ST_CURR_AFTER: u32 = 0x0000_0020; +pub const VA_PICTURE_HEVC_RPS_LT_CURR: u32 = 0x0000_0040; pub const VA_PADDING_LOW: usize = 4; pub const VA_PADDING_MEDIUM: usize = 8; @@ -226,6 +236,178 @@ pub struct VASliceParameterBufferH264 { pub va_reserved: [u32; VA_PADDING_LOW], } +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct VAPictureHEVC { + pub picture_id: VASurfaceID, + pub pic_order_cnt: i32, + pub flags: u32, + pub va_reserved: [u32; VA_PADDING_LOW], +} + +impl VAPictureHEVC { + /// An unused entry (`VA_INVALID_SURFACE`, `VA_PICTURE_HEVC_INVALID`). + pub const INVALID: Self = Self { picture_id: VA_INVALID_SURFACE, pic_order_cnt: 0, flags: VA_PICTURE_HEVC_INVALID, va_reserved: [0; VA_PADDING_LOW] }; +} + +/// `VAPictureParameterBufferHEVC`; `pic_fields` and `slice_parsing_fields` are C bit-field unions +/// stored as their `value` word (see [`VAPictureParameterBufferH264`]). +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct VAPictureParameterBufferHEVC { + pub CurrPic: VAPictureHEVC, + pub ReferenceFrames: [VAPictureHEVC; 15], + pub pic_width_in_luma_samples: u16, + pub pic_height_in_luma_samples: u16, + pub pic_fields: u32, + pub sps_max_dec_pic_buffering_minus1: u8, + pub bit_depth_luma_minus8: u8, + pub bit_depth_chroma_minus8: u8, + pub pcm_sample_bit_depth_luma_minus1: u8, + pub pcm_sample_bit_depth_chroma_minus1: u8, + pub log2_min_luma_coding_block_size_minus3: u8, + pub log2_diff_max_min_luma_coding_block_size: u8, + pub log2_min_transform_block_size_minus2: u8, + pub log2_diff_max_min_transform_block_size: u8, + pub log2_min_pcm_luma_coding_block_size_minus3: u8, + pub log2_diff_max_min_pcm_luma_coding_block_size: u8, + pub max_transform_hierarchy_depth_intra: u8, + pub max_transform_hierarchy_depth_inter: u8, + pub init_qp_minus26: i8, + pub diff_cu_qp_delta_depth: u8, + pub pps_cb_qp_offset: i8, + pub pps_cr_qp_offset: i8, + pub log2_parallel_merge_level_minus2: u8, + pub num_tile_columns_minus1: u8, + pub num_tile_rows_minus1: u8, + pub column_width_minus1: [u16; 19], + pub row_height_minus1: [u16; 21], + pub slice_parsing_fields: u32, + pub log2_max_pic_order_cnt_lsb_minus4: u8, + pub num_short_term_ref_pic_sets: u8, + pub num_long_term_ref_pic_sps: u8, + pub num_ref_idx_l0_default_active_minus1: u8, + pub num_ref_idx_l1_default_active_minus1: u8, + pub pps_beta_offset_div2: i8, + pub pps_tc_offset_div2: i8, + pub num_extra_slice_header_bits: u8, + /// Bits of `short_term_ref_pic_set()` in the slice header (0 when the SPS's set is used), + /// counted without emulation prevention bytes. + pub st_rps_bits: u32, + pub va_reserved: [u32; VA_PADDING_MEDIUM], +} + +/// `VAPictureParameterBufferHEVC::pic_fields` bit positions (`chroma_format_idc` 2 bits). +pub mod hevc_pic_bits { + pub const CHROMA_FORMAT_IDC: u32 = 0; + pub const SEPARATE_COLOUR_PLANE: u32 = 2; + pub const PCM_ENABLED: u32 = 3; + pub const SCALING_LIST_ENABLED: u32 = 4; + pub const TRANSFORM_SKIP_ENABLED: u32 = 5; + pub const AMP_ENABLED: u32 = 6; + pub const STRONG_INTRA_SMOOTHING_ENABLED: u32 = 7; + pub const SIGN_DATA_HIDING_ENABLED: u32 = 8; + pub const CONSTRAINED_INTRA_PRED: u32 = 9; + pub const CU_QP_DELTA_ENABLED: u32 = 10; + pub const WEIGHTED_PRED: u32 = 11; + pub const WEIGHTED_BIPRED: u32 = 12; + pub const TRANSQUANT_BYPASS_ENABLED: u32 = 13; + pub const TILES_ENABLED: u32 = 14; + pub const ENTROPY_CODING_SYNC_ENABLED: u32 = 15; + pub const PPS_LOOP_FILTER_ACROSS_SLICES_ENABLED: u32 = 16; + pub const LOOP_FILTER_ACROSS_TILES_ENABLED: u32 = 17; + pub const PCM_LOOP_FILTER_DISABLED: u32 = 18; + pub const NO_PIC_REORDERING: u32 = 19; + pub const NO_BI_PRED: u32 = 20; +} + +/// `VAPictureParameterBufferHEVC::slice_parsing_fields` bit positions. +pub mod hevc_slice_parsing_bits { + pub const LISTS_MODIFICATION_PRESENT: u32 = 0; + pub const LONG_TERM_REF_PICS_PRESENT: u32 = 1; + pub const SPS_TEMPORAL_MVP_ENABLED: u32 = 2; + pub const CABAC_INIT_PRESENT: u32 = 3; + pub const OUTPUT_FLAG_PRESENT: u32 = 4; + pub const DEPENDENT_SLICE_SEGMENTS_ENABLED: u32 = 5; + pub const PPS_SLICE_CHROMA_QP_OFFSETS_PRESENT: u32 = 6; + pub const SAMPLE_ADAPTIVE_OFFSET_ENABLED: u32 = 7; + pub const DEBLOCKING_FILTER_OVERRIDE_ENABLED: u32 = 8; + pub const PPS_DISABLE_DEBLOCKING_FILTER: u32 = 9; + pub const SLICE_SEGMENT_HEADER_EXTENSION_PRESENT: u32 = 10; + pub const RAP_PIC: u32 = 11; + pub const IDR_PIC: u32 = 12; + pub const INTRA_PIC: u32 = 13; +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct VASliceParameterBufferHEVC { + pub slice_data_size: u32, + pub slice_data_offset: u32, + pub slice_data_flag: u32, + /// Bytes from the start of the NAL unit (its two-byte header included) to + /// `slice_segment_data()`, counted without emulation prevention bytes; the data buffer itself + /// keeps them. + pub slice_data_byte_offset: u32, + pub slice_segment_address: u32, + /// Indices into `ReferenceFrames` (0xFF: none). + pub RefPicList: [[u8; 15]; 2], + pub LongSliceFlags: u32, + pub collocated_ref_idx: u8, + pub num_ref_idx_l0_active_minus1: u8, + pub num_ref_idx_l1_active_minus1: u8, + pub slice_qp_delta: i8, + pub slice_cb_qp_offset: i8, + pub slice_cr_qp_offset: i8, + pub slice_beta_offset_div2: i8, + pub slice_tc_offset_div2: i8, + pub luma_log2_weight_denom: u8, + pub delta_chroma_log2_weight_denom: i8, + pub delta_luma_weight_l0: [i8; 15], + pub luma_offset_l0: [i8; 15], + pub delta_chroma_weight_l0: [[i8; 2]; 15], + pub ChromaOffsetL0: [[i8; 2]; 15], + pub delta_luma_weight_l1: [i8; 15], + pub luma_offset_l1: [i8; 15], + pub delta_chroma_weight_l1: [[i8; 2]; 15], + pub ChromaOffsetL1: [[i8; 2]; 15], + pub five_minus_max_num_merge_cand: u8, + pub num_entry_point_offsets: u16, + pub entry_offset_to_subset_array: u16, + /// Emulation prevention bytes in the slice segment header. + pub slice_data_num_emu_prevn_bytes: u16, + pub va_reserved: [u32; VA_PADDING_LOW - 2], +} + +/// `VASliceParameterBufferHEVC::LongSliceFlags` bit positions (`slice_type` and `color_plane_id` 2 bits). +pub mod hevc_slice_bits { + pub const LAST_SLICE_OF_PIC: u32 = 0; + pub const DEPENDENT_SLICE_SEGMENT: u32 = 1; + pub const SLICE_TYPE: u32 = 2; + pub const COLOR_PLANE_ID: u32 = 4; + pub const SLICE_SAO_LUMA: u32 = 6; + pub const SLICE_SAO_CHROMA: u32 = 7; + pub const MVD_L1_ZERO: u32 = 8; + pub const CABAC_INIT: u32 = 9; + pub const SLICE_TEMPORAL_MVP_ENABLED: u32 = 10; + pub const SLICE_DEBLOCKING_FILTER_DISABLED: u32 = 11; + pub const COLLOCATED_FROM_L0: u32 = 12; + pub const SLICE_LOOP_FILTER_ACROSS_SLICES_ENABLED: u32 = 13; +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct VAIQMatrixBufferHEVC { + /// `ScalingList[sizeId][matrixId][j]` of the spec for sizeId 0..3, raster order. + pub ScalingList4x4: [[u8; 16]; 6], + pub ScalingList8x8: [[u8; 64]; 6], + pub ScalingList16x16: [[u8; 64]; 6], + pub ScalingList32x32: [[u8; 64]; 2], + pub ScalingListDC16x16: [u8; 6], + pub ScalingListDC32x32: [u8; 2], + pub va_reserved: [u32; VA_PADDING_LOW], +} + #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct VAImageFormat { diff --git a/crates/platform/src/vaapi/h264.rs b/crates/platform/src/vaapi/h264.rs index 1dddf3b3..bc79ee9a 100644 --- a/crates/platform/src/vaapi/h264.rs +++ b/crates/platform/src/vaapi/h264.rs @@ -15,34 +15,13 @@ use std::sync::Arc; use filmcraft_bitstream::{length_prefixed_nals, unescape_rbsp}; use filmcraft_codecs::{CodecError, DecodedFrame, Result}; -use filmcraft_frame::VideoFrame; use filmcraft_h264::dpb::{Dpb, DpbEntry, Output, OutputMeta, RefMark}; use filmcraft_h264::params::{Pps, Sps}; use filmcraft_h264::slice::{NalHeader, Poc, PocState, SliceHeader, SliceType, is_new_picture, nal_type}; +use super::Accel; use super::ffi::{self, VAIQMatrixBufferH264, VAPictureH264, VAPictureParameterBufferH264, VASliceParameterBufferH264, pic_bits, seq_bits}; -/// The hardware a [`Front`] drives: a fixed set of surfaces, picture decoding into one of them and -/// reading one back. -pub trait Accel: Send { - /// The VA surface ids of the decoder's surfaces; [`Front`] refers to them by index. - fn surfaces(&self) -> &[ffi::VASurfaceID]; - /// Decode one picture (its slices: parameters and the NAL unit, header byte and emulation - /// prevention included) into surface `target` (an index into [`Accel::surfaces`]). - fn decode( - &mut self, - target: usize, - pic: &VAPictureParameterBufferH264, - iq: &VAIQMatrixBufferH264, - slices: &[(VASliceParameterBufferH264, Vec)], - ) -> std::result::Result<(), String>; - /// The picture in surface `index`, once decoded. - fn read(&mut self, index: usize) -> std::result::Result; - /// Make surface `index` a copy of surface `from`, or mid-gray (every sample 128) when `None`: - /// the "non-existing" frames of a frame_num gap, as the software decoder makes them. - fn fill(&mut self, index: usize, from: Option) -> std::result::Result<(), String>; -} - /// A picture in the DPB: its surface, field order counts (after an MMCO 5, relative to itself) and /// place in decoding order. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -340,7 +319,7 @@ impl Front { *slot = va_ref(e, surfaces); } let iq = iq_matrix(&p.pps); - self.accel.decode(p.target, &pic, &iq, &p.slices).map_err(decode_err)?; + self.accel.decode_h264(p.target, &pic, &iq, &p.slices).map_err(decode_err)?; self.poc_state.update(&p.first, &p.poc); let mmco5 = p.first.has_mmco5(); diff --git a/crates/platform/src/vaapi/hevc.rs b/crates/platform/src/vaapi/hevc.rs new file mode 100644 index 00000000..730a139f --- /dev/null +++ b/crates/platform/src/vaapi/hevc.rs @@ -0,0 +1,612 @@ +//! The HEVC side of the VA-API decoder, like [`super::h264`]: the software decoder's own parsers +//! (`filmcraft_hevc::params`, `slice`) and decoded picture buffer (`filmcraft_hevc::dpb`: POC, +//! reference picture sets, reference lists, output order) drive a stateless hardware decoder, so +//! both decoders make the same decisions and output the same pictures in the same order, RASL +//! pictures of a CRA the run starts at left out as in software. +//! +//! What the hardware path does not take is an error, so the hybrid decoder replays the run in +//! software: missing reference pictures (the software decoder conceals them), more than 15 +//! reference frames, stream changes the surfaces were not made for, range extensions. +//! +//! Safe code; compiled on every target so its logic is tested everywhere. + +use std::sync::Arc; + +use filmcraft_bitstream::{length_prefixed_nals, unescape_rbsp}; +use filmcraft_codecs::{CodecError, DecodedFrame, Result}; +use filmcraft_hevc::dpb::{Dpb, Marking, Output, OutputMeta, PocState, Ref, RefPicSet, build_ref_lists}; +use filmcraft_hevc::params::{Layout, Pps, ScalingList, Sps}; +use filmcraft_hevc::slice::{NalHeader, SliceHeader, SliceType, nal_type}; + +use super::Accel; +use super::ffi::{ + self, VAIQMatrixBufferHEVC, VAPictureHEVC, VAPictureParameterBufferHEVC, VASliceParameterBufferHEVC, hevc_pic_bits, hevc_slice_bits, + hevc_slice_parsing_bits, +}; + +/// A picture in the DPB: its surface. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Surface { + pub index: usize, +} + +/// A reference picture that was never decoded (8.3.3): not handed to the hardware. +const MISSING: usize = usize::MAX; + +/// The picture whose slice segments are being collected. +struct Pending { + target: usize, + sps: Arc, + pps: Arc, + first: SliceHeader, + /// The latest independent slice segment's header (dependent ones continue it). + last: SliceHeader, + rps: RefPicSet, + /// The surfaces of `pic.ReferenceFrames`, in order. + ref_frames: Vec, + pic: VAPictureParameterBufferHEVC, + poc: i32, + output: bool, + meta: Arc, + slices: Vec<(VASliceParameterBufferHEVC, Vec)>, + all_intra: bool, + any_b: bool, +} + +/// HEVC front end of a stateless hardware decoder (see the module docs). +pub struct Front { + accel: A, + length_size: usize, + /// Coded size (luma samples) and bit depth the surfaces were made for. + size: (u32, u32), + bits: u32, + spss: Vec>>, + ppss: Vec>>, + dpb: Dpb, + poc_state: PocState, + active_sps: Option>, + pending: Option, + first_picture: bool, + skip_rasl: bool, + after_eos: bool, +} + +fn decode_err(e: impl std::fmt::Display) -> CodecError { + CodecError::Decode(e.to_string()) +} + +fn unsupported(what: &str) -> CodecError { + CodecError::Decode(format!("the hardware decoder does not take {what}")) +} + +impl Front { + /// A front end for samples with `length_size`-byte NAL length prefixes whose sample entry + /// carried `parameter_sets` (VPS / SPS / PPS NAL units), decoding into `accel`'s surfaces of + /// `size` luma samples and `bits` bits. + pub fn new(accel: A, length_size: usize, size: (u32, u32), bits: u32, parameter_sets: &[Vec]) -> Result { + let mut f = Self { + accel, + length_size, + size, + bits, + spss: vec![None; 16], + ppss: vec![None; 64], + dpb: Dpb::default(), + poc_state: PocState::default(), + active_sps: None, + pending: None, + first_picture: true, + skip_rasl: false, + after_eos: false, + }; + let mut frames = Vec::new(); + for nal in parameter_sets { + f.handle_nal(nal, 0, &mut frames)?; + } + Ok(f) + } + + pub fn accel(&self) -> &A { + &self.accel + } + + /// Decode one access unit; pictures that left the DPB come back in output order. + pub fn decode(&mut self, sample: &[u8], pts: i64) -> Result> { + let nals = length_prefixed_nals(sample, self.length_size).map_err(decode_err)?; + let mut frames = Vec::new(); + for nal in nals { + self.handle_nal(nal, pts, &mut frames)?; + } + self.submit_pending(&mut frames)?; + Ok(frames) + } + + /// Output every picture still held for reordering; what follows starts like a new stream + /// (a CRA's RASL pictures are left out), as in software. + pub fn flush(&mut self) -> Result> { + let mut frames = Vec::new(); + self.submit_pending(&mut frames)?; + let mut outs = Vec::new(); + self.dpb.flush(&mut outs); + self.read_out(outs, &mut frames)?; + self.first_picture = true; + Ok(frames) + } + + /// Forget every picture (after a seek). + pub fn reset(&mut self) { + self.dpb = Dpb::default(); + self.poc_state = PocState::default(); + self.active_sps = None; + self.pending = None; + self.first_picture = true; + self.skip_rasl = false; + self.after_eos = false; + } + + /// Read back the pictures the DPB output, right away: a surface leaving the DPB is reused. + fn read_out(&mut self, outs: Vec>, frames: &mut Vec) -> Result<()> { + for o in outs { + let frame = self.accel.read(o.frame.index).map_err(decode_err)?; + frames.push(DecodedFrame { pts: o.meta.pts, frame, draft: false }); + } + Ok(()) + } + + fn handle_nal(&mut self, nal: &[u8], pts: i64, frames: &mut Vec) -> Result<()> { + if nal.len() < 2 { + return Ok(()); + } + let hdr = NalHeader::parse(nal).map_err(decode_err)?; + if hdr.layer_id != 0 { + return Ok(()); + } + let rbsp = || unescape_rbsp(nal.get(2..).unwrap_or_default()); + match hdr.nal_type { + nal_type::SPS => { + let sps = Sps::parse(&rbsp()).map_err(decode_err)?; + let slot = self.spss.get_mut(sps.id as usize).ok_or_else(|| decode_err("SPS id out of range"))?; + *slot = Some(Arc::new(sps)); + } + nal_type::PPS => { + let pps = Pps::parse(&rbsp()).map_err(decode_err)?; + let slot = self.ppss.get_mut(pps.id as usize).ok_or_else(|| decode_err("PPS id out of range"))?; + *slot = Some(Arc::new(pps)); + } + nal_type::EOS | nal_type::EOB => { + self.submit_pending(frames)?; + self.after_eos = true; + } + 0..=9 | 16..=21 => self.handle_slice(nal, hdr, pts, frames)?, + // VPS, AUD, SEI, filler, reserved: nothing to decode + _ => {} + } + Ok(()) + } + + fn handle_slice(&mut self, nal: &[u8], hdr: NalHeader, pts: i64, frames: &mut Vec) -> Result<()> { + let rbsp = unescape_rbsp(nal.get(2..).unwrap_or_default()); + let first = rbsp.first().is_some_and(|b| b & 0x80 != 0); + if first { + self.submit_pending(frames)?; + } + if hdr.is_rasl() && (self.skip_rasl || self.first_picture) { + return Ok(()); + } + let (ppss, spss) = (&self.ppss, &self.spss); + let prev = self.pending.as_ref().map(|p| &p.last); + let (sh, pps, sps) = SliceHeader::parse( + &rbsp, + hdr, + |id| { + let missing = |what: String| filmcraft_hevc::Error::MissingParameterSet(what); + let pps = ppss.get(id as usize).and_then(Option::as_ref).ok_or_else(|| missing(format!("PPS {id}")))?; + let sps = spss.get(pps.sps_id as usize).and_then(Option::as_ref).ok_or_else(|| missing(format!("SPS {}", pps.sps_id)))?; + Ok((pps.clone(), sps.clone())) + }, + prev, + ) + .map_err(decode_err)?; + sps.check_supported().map_err(decode_err)?; + pps.check_supported().map_err(decode_err)?; + if first { + self.start_picture(&sh, &sps, &pps, pts, frames)?; + } + let Some(p) = self.pending.as_mut() else { return Err(decode_err("slice segment without the first slice of its picture")) }; + if !Arc::ptr_eq(&p.pps, &pps) { + return Err(decode_err("PPS changed within a picture")); + } + let refs = build_ref_lists(&sh, &p.rps).map_err(decode_err)?; + let params = slice_params(&sh, nal, &refs, &p.ref_frames)?; + if !sh.dependent { + p.last = sh.clone(); + } + p.all_intra &= sh.is_intra(); + p.any_b |= sh.is_b(); + p.slices.push((params, nal.to_vec())); + Ok(()) + } + + fn start_picture(&mut self, sh: &SliceHeader, sps: &Arc, pps: &Arc, pts: i64, frames: &mut Vec) -> Result<()> { + if !sh.first_slice_segment_in_pic || sh.dependent { + return Err(decode_err("invalid first slice segment")); + } + if (sps.width, sps.height) != self.size || sps.bit_depth_luma != self.bits || sps.bit_depth_chroma != self.bits || sps.chroma_format_idc != 1 { + return Err(unsupported("a picture size or format change")); + } + if sps.max_dec_pic_buffering as usize >= self.accel.surfaces().len() { + return Err(unsupported("a DPB larger than the decoder's surfaces")); + } + let hdr = sh.nal; + let changed = self.active_sps.as_ref().is_some_and(|a| a.log2_ctb != sps.log2_ctb); + let mut outs = Vec::new(); + if changed { + self.dpb.flush(&mut outs); + } + self.active_sps = Some(sps.clone()); + self.dpb.max_dec_pic_buffering = sps.max_dec_pic_buffering as usize; + self.dpb.max_num_reorder = sps.max_num_reorder as usize; + self.dpb.max_latency = if sps.max_latency_increase_plus1 != 0 { sps.max_num_reorder + sps.max_latency_increase_plus1 - 1 } else { 0 }; + let irap_no_rasl = hdr.is_irap() && (hdr.is_idr() || hdr.is_bla() || self.first_picture || self.after_eos); + if hdr.is_irap() { + self.skip_rasl = irap_no_rasl; + } + let max_lsb = sps.max_poc_lsb(); + let poc = self.poc_state.compute(sh, max_lsb, irap_no_rasl); + let mut make_missing = |_| Surface { index: MISSING }; + let rps = self.dpb.apply_rps(sh, poc, max_lsb, irap_no_rasl, &mut make_missing).map_err(decode_err)?; + if [&rps.st_curr_before, &rps.st_curr_after, &rps.lt_curr].iter().any(|l| l.iter().any(|r| r.frame.index == MISSING)) { + // the software decoder conceals them with mid-gray pictures + return Err(unsupported("missing reference pictures")); + } + if irap_no_rasl && !self.first_picture { + if sh.no_output_of_prior_pics && hdr.is_idr() { + self.dpb.clear(); + } else { + self.dpb.flush(&mut outs); + } + } else { + self.dpb.bump_before_decode(&mut outs); + } + self.read_out(outs, frames)?; + let surfaces = self.accel.surfaces(); + let target = (0..surfaces.len()).find(|&i| !self.dpb.entries.iter().any(|e| e.frame.index == i)).ok_or_else(|| decode_err("no free surface"))?; + let target_id = surfaces.get(target).copied().ok_or_else(|| decode_err("surface out of range"))?; + let mut pic = picture_params(sps, pps, sh)?; + pic.CurrPic = VAPictureHEVC { picture_id: target_id, pic_order_cnt: poc, flags: 0, va_reserved: [0; ffi::VA_PADDING_LOW] }; + // the reference frames: every picture the RPS kept, flagged with its part of the RPS + let rps_flag = |index: usize| { + let in_list = |l: &[Ref]| l.iter().any(|r| r.frame.index == index); + if in_list(&rps.st_curr_before) { + ffi::VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE + } else if in_list(&rps.st_curr_after) { + ffi::VA_PICTURE_HEVC_RPS_ST_CURR_AFTER + } else if in_list(&rps.lt_curr) { + ffi::VA_PICTURE_HEVC_RPS_LT_CURR + } else { + 0 + } + }; + let refs: Vec<_> = self.dpb.entries.iter().filter(|e| e.marking != Marking::Unused).collect(); + if refs.len() > pic.ReferenceFrames.len() { + return Err(unsupported("more than 15 reference frames")); + } + let mut ref_frames = Vec::with_capacity(refs.len()); + for (slot, e) in pic.ReferenceFrames.iter_mut().zip(&refs) { + let long = if e.marking == Marking::Long { ffi::VA_PICTURE_HEVC_LONG_TERM_REFERENCE } else { 0 }; + let picture_id = surfaces.get(e.frame.index).copied().ok_or_else(|| decode_err("surface out of range"))?; + *slot = VAPictureHEVC { picture_id, pic_order_cnt: e.poc, flags: long | rps_flag(e.frame.index), va_reserved: [0; ffi::VA_PADDING_LOW] }; + ref_frames.push(e.frame.index); + } + let output = sh.pic_output && !(hdr.is_rasl() && self.skip_rasl); + let meta = Arc::new(output_meta(sps, pts, hdr.is_irap())); + self.pending = Some(Pending { + target, + sps: sps.clone(), + pps: pps.clone(), + first: sh.clone(), + last: sh.clone(), + rps, + ref_frames, + pic, + poc, + output, + meta, + slices: Vec::new(), + all_intra: true, + any_b: false, + }); + self.first_picture = false; + self.after_eos = false; + Ok(()) + } + + /// Decode the collected picture, then insert it into the DPB. + fn submit_pending(&mut self, frames: &mut Vec) -> Result<()> { + let Some(mut p) = self.pending.take() else { return Ok(()) }; + let Some(last) = p.slices.last_mut() else { return Err(decode_err("a picture without slices")) }; + last.0.LongSliceFlags |= 1 << hevc_slice_bits::LAST_SLICE_OF_PIC; + p.pic.slice_parsing_fields |= u32::from(p.all_intra) << hevc_slice_parsing_bits::INTRA_PIC; + p.pic.pic_fields |= u32::from(!p.any_b) << hevc_pic_bits::NO_BI_PRED; + let iq = if p.sps.scaling_list_enabled { Some(iq_matrix(p.pps.scaling_list.as_ref().or(p.sps.scaling_list.as_ref()))) } else { None }; + self.accel.decode_hevc(p.target, &p.pic, iq.as_ref(), &p.slices).map_err(decode_err)?; + self.poc_state.update(&p.first, p.poc); + let mut outs = Vec::new(); + self.dpb.insert(Surface { index: p.target }, p.poc, p.output, p.meta, &mut outs); + self.read_out(outs, frames) + } +} + +fn output_meta(sps: &Sps, pts: i64, key: bool) -> OutputMeta { + let vui = sps.vui.clone().unwrap_or_default(); + OutputMeta { + pts, + key, + crop: sps.crop_rect(), + full_range: vui.full_range, + colour_primaries: vui.colour_primaries, + transfer_characteristics: vui.transfer_characteristics, + matrix_coefficients: vui.matrix_coefficients, + sar: vui.sar, + bit_depth: sps.bit_depth_luma, + draft: false, + } +} + +fn flag(on: bool, bit: u32) -> u32 { + u32::from(on) << bit +} + +fn u8_of(v: u32) -> u8 { + u8::try_from(v).unwrap_or(u8::MAX) +} + +fn i8_of(v: i32) -> i8 { + i8::try_from(v).unwrap_or(0) +} + +/// The picture parameters of a picture coded with `sps` / `pps` whose first slice segment is `sh` +/// (the current and reference pictures, `IntraPicFlag` and `NoBiPredFlag` are filled in by the caller). +pub fn picture_params(sps: &Sps, pps: &Pps, sh: &SliceHeader) -> Result { + use hevc_pic_bits as p; + use hevc_slice_parsing_bits as s; + let pic_fields = (sps.chroma_format_idc & 3) << p::CHROMA_FORMAT_IDC + | flag(sps.separate_colour_plane, p::SEPARATE_COLOUR_PLANE) + | flag(sps.pcm, p::PCM_ENABLED) + | flag(sps.scaling_list_enabled, p::SCALING_LIST_ENABLED) + | flag(pps.transform_skip, p::TRANSFORM_SKIP_ENABLED) + | flag(sps.amp, p::AMP_ENABLED) + | flag(sps.strong_intra_smoothing, p::STRONG_INTRA_SMOOTHING_ENABLED) + | flag(pps.sign_data_hiding, p::SIGN_DATA_HIDING_ENABLED) + | flag(pps.constrained_intra_pred, p::CONSTRAINED_INTRA_PRED) + | flag(pps.cu_qp_delta_enabled, p::CU_QP_DELTA_ENABLED) + | flag(pps.weighted_pred, p::WEIGHTED_PRED) + | flag(pps.weighted_bipred, p::WEIGHTED_BIPRED) + | flag(pps.transquant_bypass, p::TRANSQUANT_BYPASS_ENABLED) + | flag(pps.tiles_enabled, p::TILES_ENABLED) + | flag(pps.entropy_coding_sync, p::ENTROPY_CODING_SYNC_ENABLED) + | flag(pps.loop_filter_across_slices, p::PPS_LOOP_FILTER_ACROSS_SLICES_ENABLED) + | flag(pps.loop_filter_across_tiles, p::LOOP_FILTER_ACROSS_TILES_ENABLED) + | flag(sps.pcm_loop_filter_disabled, p::PCM_LOOP_FILTER_DISABLED) + | flag(sps.max_num_reorder == 0, p::NO_PIC_REORDERING); + let slice_parsing_fields = flag(pps.lists_modification_present, s::LISTS_MODIFICATION_PRESENT) + | flag(sps.long_term_refs_present, s::LONG_TERM_REF_PICS_PRESENT) + | flag(sps.temporal_mvp, s::SPS_TEMPORAL_MVP_ENABLED) + | flag(pps.cabac_init_present, s::CABAC_INIT_PRESENT) + | flag(pps.output_flag_present, s::OUTPUT_FLAG_PRESENT) + | flag(pps.dependent_slice_segments_enabled, s::DEPENDENT_SLICE_SEGMENTS_ENABLED) + | flag(pps.slice_chroma_qp_offsets_present, s::PPS_SLICE_CHROMA_QP_OFFSETS_PRESENT) + | flag(sps.sao, s::SAMPLE_ADAPTIVE_OFFSET_ENABLED) + | flag(pps.deblocking_override_enabled, s::DEBLOCKING_FILTER_OVERRIDE_ENABLED) + | flag(pps.deblocking_disabled, s::PPS_DISABLE_DEBLOCKING_FILTER) + | flag(pps.slice_header_extension_present, s::SLICE_SEGMENT_HEADER_EXTENSION_PRESENT) + | flag(sh.nal.is_irap(), s::RAP_PIC) + | flag(sh.nal.is_idr(), s::IDR_PIC); + // tile sizes in CTBs, uniform spacing resolved + let layout = Layout::new(sps, pps).map_err(decode_err)?; + let mut column_width_minus1 = [0u16; 19]; + let mut row_height_minus1 = [0u16; 21]; + if pps.tiles_enabled { + let sizes = |bd: &[u32], out: &mut [u16]| -> Result<()> { + let n = bd.len().saturating_sub(1); + if n > out.len() { + return Err(unsupported("that many tiles")); + } + for (o, w) in out.iter_mut().zip(bd.windows(2)) { + *o = u16::try_from(w[1].saturating_sub(w[0]).saturating_sub(1)).unwrap_or(0); + } + Ok(()) + }; + sizes(&layout.col_bd, &mut column_width_minus1)?; + sizes(&layout.row_bd, &mut row_height_minus1)?; + } + let narrow = |v: u32| u16::try_from(v).map_err(|_| decode_err("picture too large")); + Ok(VAPictureParameterBufferHEVC { + CurrPic: VAPictureHEVC::INVALID, + ReferenceFrames: [VAPictureHEVC::INVALID; 15], + pic_width_in_luma_samples: narrow(sps.width)?, + pic_height_in_luma_samples: narrow(sps.height)?, + pic_fields, + sps_max_dec_pic_buffering_minus1: u8_of(sps.max_dec_pic_buffering.saturating_sub(1)), + bit_depth_luma_minus8: u8_of(sps.bit_depth_luma.saturating_sub(8)), + bit_depth_chroma_minus8: u8_of(sps.bit_depth_chroma.saturating_sub(8)), + pcm_sample_bit_depth_luma_minus1: u8_of(sps.pcm_bit_depth_luma.saturating_sub(1)), + pcm_sample_bit_depth_chroma_minus1: u8_of(sps.pcm_bit_depth_chroma.saturating_sub(1)), + log2_min_luma_coding_block_size_minus3: u8_of(sps.log2_min_cb.saturating_sub(3)), + log2_diff_max_min_luma_coding_block_size: u8_of(sps.log2_ctb.saturating_sub(sps.log2_min_cb)), + log2_min_transform_block_size_minus2: u8_of(sps.log2_min_tb.saturating_sub(2)), + log2_diff_max_min_transform_block_size: u8_of(sps.log2_max_tb.saturating_sub(sps.log2_min_tb)), + log2_min_pcm_luma_coding_block_size_minus3: u8_of(sps.log2_min_pcm.saturating_sub(3)), + log2_diff_max_min_pcm_luma_coding_block_size: u8_of(sps.log2_max_pcm.saturating_sub(sps.log2_min_pcm)), + max_transform_hierarchy_depth_intra: u8_of(sps.max_th_depth_intra), + max_transform_hierarchy_depth_inter: u8_of(sps.max_th_depth_inter), + init_qp_minus26: i8_of(pps.init_qp - 26), + diff_cu_qp_delta_depth: u8_of(pps.diff_cu_qp_delta_depth), + pps_cb_qp_offset: i8_of(pps.cb_qp_offset), + pps_cr_qp_offset: i8_of(pps.cr_qp_offset), + log2_parallel_merge_level_minus2: u8_of(pps.log2_parallel_merge_level.saturating_sub(2)), + num_tile_columns_minus1: u8_of(pps.num_tile_columns.saturating_sub(1)), + num_tile_rows_minus1: u8_of(pps.num_tile_rows.saturating_sub(1)), + column_width_minus1, + row_height_minus1, + slice_parsing_fields, + log2_max_pic_order_cnt_lsb_minus4: u8_of(sps.log2_max_poc_lsb.saturating_sub(4)), + num_short_term_ref_pic_sets: u8_of(u32::try_from(sps.st_rps.len()).unwrap_or(u32::MAX)), + num_long_term_ref_pic_sps: u8_of(u32::try_from(sps.lt_ref_pics.len()).unwrap_or(u32::MAX)), + num_ref_idx_l0_default_active_minus1: u8_of(pps.num_ref_idx_l0_default.saturating_sub(1)), + num_ref_idx_l1_default_active_minus1: u8_of(pps.num_ref_idx_l1_default.saturating_sub(1)), + pps_beta_offset_div2: i8_of(pps.beta_offset_div2), + pps_tc_offset_div2: i8_of(pps.tc_offset_div2), + num_extra_slice_header_bits: u8_of(pps.num_extra_slice_header_bits), + st_rps_bits: u32::try_from(sh.st_rps_bits).unwrap_or(0), + va_reserved: [0; ffi::VA_PADDING_MEDIUM], + }) +} + +/// The 8x8 base list `coded` (up-right diagonal order, as coded and stored) in raster order. +fn raster8(list: &ScalingList, size_id: usize, matrix_id: usize) -> [u8; 64] { + let mut l = list.clone(); + let row = |s: usize| l.lists.get(s).and_then(|m| m.get(matrix_id)).copied(); + if let Some(src) = row(size_id) + && let Some(dst) = l.lists.get_mut(1).and_then(|m| m.get_mut(matrix_id)) + { + *dst = src; + } + let mut out = [16u8; 64]; + for (o, v) in out.iter_mut().zip(l.factor(1, matrix_id)) { + *o = v; + } + out +} + +/// The scaling lists in force (`list`, or the default lists), in raster order. +pub fn iq_matrix(list: Option<&ScalingList>) -> VAIQMatrixBufferHEVC { + let defaults = ScalingList::default_lists(); + let l = list.unwrap_or(&defaults); + let mut iq = VAIQMatrixBufferHEVC { + ScalingList4x4: [[16; 16]; 6], + ScalingList8x8: [[16; 64]; 6], + ScalingList16x16: [[16; 64]; 6], + ScalingList32x32: [[16; 64]; 2], + ScalingListDC16x16: [16; 6], + ScalingListDC32x32: [16; 2], + va_reserved: [0; ffi::VA_PADDING_LOW], + }; + for m in 0..6 { + for (o, v) in iq.ScalingList4x4[m].iter_mut().zip(l.factor(0, m)) { + *o = v; + } + iq.ScalingList8x8[m] = raster8(l, 1, m); + iq.ScalingList16x16[m] = raster8(l, 2, m); + iq.ScalingListDC16x16[m] = l.dc[0][m]; + } + // 32x32: matrixId 0 (intra) and 3 (inter) + for (i, m) in [0, 3].into_iter().enumerate() { + iq.ScalingList32x32[i] = raster8(l, 3, m); + iq.ScalingListDC32x32[i] = l.dc[1][m]; + } + iq +} + +/// Emulation prevention bytes among the first `rbsp_len` RBSP bytes of a NAL unit payload. +fn emulation_bytes_before(payload: &[u8], rbsp_len: usize) -> usize { + let (mut zeros, mut out, mut ep) = (0usize, 0usize, 0usize); + for &b in payload { + if out >= rbsp_len { + break; + } + if zeros >= 2 && b == 3 { + ep += 1; + zeros = 0; + continue; + } + zeros = if b == 0 { zeros + 1 } else { 0 }; + out += 1; + } + ep +} + +/// The slice parameters of the slice segment `sh` (NAL unit `nal`) with reference lists `refs`, +/// whose pictures are `ref_frames` (the surfaces of `ReferenceFrames`, in order). +pub fn slice_params(sh: &SliceHeader, nal: &[u8], refs: &[Vec>; 2], ref_frames: &[usize]) -> Result { + use hevc_slice_bits as b; + let slice_type = match sh.slice_type { + SliceType::B => 0, + SliceType::P => 1, + SliceType::I => 2, + }; + let flags = flag(sh.dependent, b::DEPENDENT_SLICE_SEGMENT) + | slice_type << b::SLICE_TYPE + | flag(sh.sao_luma, b::SLICE_SAO_LUMA) + | flag(sh.sao_chroma, b::SLICE_SAO_CHROMA) + | flag(sh.mvd_l1_zero, b::MVD_L1_ZERO) + | flag(sh.cabac_init_flag, b::CABAC_INIT) + | flag(sh.temporal_mvp, b::SLICE_TEMPORAL_MVP_ENABLED) + | flag(sh.deblocking_disabled, b::SLICE_DEBLOCKING_FILTER_DISABLED) + | flag(sh.collocated_from_l0, b::COLLOCATED_FROM_L0) + | flag(sh.loop_filter_across_slices, b::SLICE_LOOP_FILTER_ACROSS_SLICES_ENABLED); + let header_bytes = sh.data_offset.checked_add(2).ok_or_else(|| decode_err("slice header too long"))?; + let ep = emulation_bytes_before(nal.get(2..).unwrap_or_default(), sh.data_offset); + let mut s = VASliceParameterBufferHEVC { + slice_data_size: u32::try_from(nal.len()).map_err(|_| decode_err("slice too large"))?, + slice_data_offset: 0, + slice_data_flag: ffi::VA_SLICE_DATA_FLAG_ALL, + slice_data_byte_offset: u32::try_from(header_bytes).map_err(|_| decode_err("slice header too long"))?, + slice_segment_address: sh.segment_address, + RefPicList: [[0xFF; 15]; 2], + LongSliceFlags: flags, + collocated_ref_idx: u8_of(sh.collocated_ref_idx), + num_ref_idx_l0_active_minus1: u8_of(sh.num_ref_idx[0].saturating_sub(1)), + num_ref_idx_l1_active_minus1: u8_of(sh.num_ref_idx[1].saturating_sub(1)), + slice_qp_delta: i8_of(sh.qp_delta), + slice_cb_qp_offset: i8_of(sh.cb_qp_offset), + slice_cr_qp_offset: i8_of(sh.cr_qp_offset), + slice_beta_offset_div2: i8_of(sh.beta_offset_div2), + slice_tc_offset_div2: i8_of(sh.tc_offset_div2), + luma_log2_weight_denom: 0, + delta_chroma_log2_weight_denom: 0, + delta_luma_weight_l0: [0; 15], + luma_offset_l0: [0; 15], + delta_chroma_weight_l0: [[0; 2]; 15], + ChromaOffsetL0: [[0; 2]; 15], + delta_luma_weight_l1: [0; 15], + luma_offset_l1: [0; 15], + delta_chroma_weight_l1: [[0; 2]; 15], + ChromaOffsetL1: [[0; 2]; 15], + five_minus_max_num_merge_cand: u8_of(5u32.saturating_sub(sh.max_num_merge_cand)), + num_entry_point_offsets: u16::try_from(sh.entry_points.len()).map_err(|_| decode_err("too many entry points"))?, + entry_offset_to_subset_array: 0, + slice_data_num_emu_prevn_bytes: u16::try_from(ep).unwrap_or(u16::MAX), + va_reserved: [0; ffi::VA_PADDING_LOW - 2], + }; + for (l, list) in refs.iter().enumerate() { + if list.len() > 15 { + return Err(decode_err("reference list longer than 15")); + } + for (i, r) in list.iter().enumerate() { + let idx = ref_frames.iter().position(|&f| f == r.frame.index).ok_or_else(|| decode_err("a reference is not among the reference frames"))?; + s.RefPicList[l][i] = u8::try_from(idx).unwrap_or(0xFF); + } + } + if let Some(w) = &sh.pwt { + let luma_denom = i32::try_from(w.luma_log2_denom).unwrap_or(0); + let chroma_denom = i32::try_from(w.chroma_log2_denom).unwrap_or(0); + s.luma_log2_weight_denom = u8_of(w.luma_log2_denom); + s.delta_chroma_log2_weight_denom = i8_of(chroma_denom - luma_denom); + let lists = [ + (&w.l[0], &mut s.delta_luma_weight_l0, &mut s.luma_offset_l0, &mut s.delta_chroma_weight_l0, &mut s.ChromaOffsetL0), + (&w.l[1], &mut s.delta_luma_weight_l1, &mut s.luma_offset_l1, &mut s.delta_chroma_weight_l1, &mut s.ChromaOffsetL1), + ]; + for (entries, lw, lo, cw, co) in lists { + for (i, e) in entries.iter().take(15).enumerate() { + lw[i] = i8_of(e.luma.0 - (1 << luma_denom)); + lo[i] = i8_of(e.luma.1); + for c in 0..2 { + cw[i][c] = i8_of(e.chroma[c].0 - (1 << chroma_denom)); + co[i][c] = i8_of(e.chroma[c].1); + } + } + } + } + Ok(s) +} diff --git a/crates/platform/src/vaapi/mod.rs b/crates/platform/src/vaapi/mod.rs index 6cc8e4d5..a78d1830 100644 --- a/crates/platform/src/vaapi/mod.rs +++ b/crates/platform/src/vaapi/mod.rs @@ -4,13 +4,15 @@ //! buffer, and the GPU decodes one picture's slice data at a time into a surface. [`h264`] does the //! host side with the software decoder's own parsers and DPB (so the two decide alike), [`va`] //! drives libva (loaded at run time; no libva means no hardware decoder, never a failure to start), -//! and [`VaDecoder`] puts them together as a [`VideoDecoder`]. H.264 only so far: 8-bit 4:2:0 -//! progressive, Constrained Baseline / Main / High; everything else stays in software. +//! and [`VaDecoder`] puts them together as a [`VideoDecoder`]. H.264 8-bit 4:2:0 progressive +//! (Constrained Baseline / Main / High) and HEVC 4:2:0 (Main, Main 10, Main Still Picture), the +//! latter through [`hevc`] the same way; everything else stays in software. //! //! The FFI is in [`ffi`] (declarations) and `va` (calls); the rest of this module is safe code. pub mod ffi; pub mod h264; +pub mod hevc; #[cfg(target_os = "linux")] #[allow(unsafe_code)] pub mod va; @@ -23,21 +25,52 @@ mod tests; #[cfg(target_os = "linux")] pub use linux::VaDecoder; +use filmcraft_frame::VideoFrame; + +/// The hardware a front end ([`h264::Front`], [`hevc::Front`]) drives: a fixed set of surfaces, +/// picture decoding into one of them, and reading one back or filling it. +pub trait Accel: Send { + /// The VA surface ids of the decoder's surfaces; front ends refer to them by index. + fn surfaces(&self) -> &[ffi::VASurfaceID]; + /// Decode one H.264 picture (its slices: parameters and the NAL unit, header byte and + /// emulation prevention included) into surface `target` (an index into [`Accel::surfaces`]). + fn decode_h264( + &mut self, + target: usize, + pic: &ffi::VAPictureParameterBufferH264, + iq: &ffi::VAIQMatrixBufferH264, + slices: &[(ffi::VASliceParameterBufferH264, Vec)], + ) -> std::result::Result<(), String>; + /// Decode one HEVC picture (its slice segments: parameters and the NAL unit as stored) into + /// surface `target`; `iq` when scaling lists are enabled. + fn decode_hevc( + &mut self, + target: usize, + pic: &ffi::VAPictureParameterBufferHEVC, + iq: Option<&ffi::VAIQMatrixBufferHEVC>, + slices: &[(ffi::VASliceParameterBufferHEVC, Vec)], + ) -> std::result::Result<(), String>; + /// The picture in surface `index`, once decoded. + fn read(&mut self, index: usize) -> std::result::Result; + /// Make surface `index` a copy of surface `from`, or mid-gray (every sample half the range) + /// when `None`: the stand-ins for reference pictures that were never decoded, as the software + /// decoders make them. + fn fill(&mut self, index: usize, from: Option) -> std::result::Result<(), String>; +} + #[cfg(target_os = "linux")] mod linux { use filmcraft_bitstream::unescape_rbsp; use filmcraft_codecs::hw::{NalCodec, NalStreamInfo}; use filmcraft_codecs::{CodecError, DecodedFrame, Result, VideoDecoder}; - use filmcraft_h264::params::Sps; - use super::ffi; - use super::h264::Front; use super::va::Session; + use super::{ffi, h264, hevc}; use crate::biplanar::Geometry; /// The VA-API profiles that decode an H.264 stream of `profile_idc`, best fit first (a Main or /// High decoder also decodes what the smaller profiles allow). - pub(crate) fn profiles(profile_idc: u8) -> Option<&'static [ffi::VAProfile]> { + pub(crate) fn h264_profiles(profile_idc: u8) -> Option<&'static [ffi::VAProfile]> { Some(match profile_idc { 66 => &[ffi::VAProfileH264ConstrainedBaseline, ffi::VAProfileH264Main, ffi::VAProfileH264High], 77 => &[ffi::VAProfileH264Main, ffi::VAProfileH264High], @@ -46,44 +79,87 @@ mod linux { }) } - /// A VA-API H.264 decoder for one stream. + /// The VA-API profiles that decode an HEVC stream of `general_profile_idc` (Main, Main 10, + /// Main Still Picture) at `bits`, best fit first. + pub(crate) fn hevc_profiles(profile_idc: u8, bits: u32) -> Option<&'static [ffi::VAProfile]> { + Some(match (profile_idc, bits) { + (1 | 3, 8) => &[ffi::VAProfileHEVCMain, ffi::VAProfileHEVCMain10], + (2, 8) => &[ffi::VAProfileHEVCMain10, ffi::VAProfileHEVCMain], + (2, 10) => &[ffi::VAProfileHEVCMain10], + _ => return None, + }) + } + + enum Front { + H264(Box>), + Hevc(Box>), + } + + /// A VA-API decoder for one H.264 or HEVC stream. pub struct VaDecoder { - front: Front, + front: Front, info: NalStreamInfo, name: String, fed: u64, fail_after: Option, } + /// The first parameter set of NAL unit type `ty` among `sets`, without its header (`header` + /// bytes) and emulation prevention. + fn parameter_set(sets: &[Vec], header: usize, ty: impl Fn(u8) -> bool) -> Option> { + sets.iter().find(|n| n.first().is_some_and(|&h| ty(h))).and_then(|n| Some(unescape_rbsp(n.get(header..)?))) + } + impl VaDecoder { /// A decoder for the stream `info` describes, or why VA-API does not take it on this system. pub fn new(info: NalStreamInfo) -> std::result::Result { - if info.codec != NalCodec::H264 { - return Err("only H.264 is decoded through VA-API so far".into()); - } - if info.chroma_format_idc != 1 || info.bit_depth_luma != 8 || info.bit_depth_chroma != 8 || info.interlaced { + let bits = info.bit_depth_luma; + if info.chroma_format_idc != 1 || info.bit_depth_chroma != bits || info.interlaced { return Err(format!( - "{} 4:2:0 {}-bit {} is not taken", + "{} chroma format {} at {bits}/{}-bit is not taken", if info.interlaced { "interlaced" } else { "progressive" }, - info.bit_depth_luma, - if info.chroma_format_idc == 1 { "only" } else { "(other chroma formats)" } + info.chroma_format_idc, + info.bit_depth_chroma )); } - let profiles = profiles(info.profile_idc).ok_or_else(|| format!("H.264 profile_idc {} is not taken", info.profile_idc))?; - let sps = info - .parameter_sets - .iter() - .find(|n| n.first().is_some_and(|h| h & 0x1f == 7)) - .and_then(|n| Sps::parse(&unescape_rbsp(n.get(1..)?)).ok()) - .ok_or("no readable SPS")?; - sps.check_supported().map_err(|e| e.to_string())?; - let mbs = (sps.pic_width_in_mbs, sps.frame_height_in_mbs()); - // the DPB, the picture being decoded, and one spare - let count = sps.max_dpb_frames().saturating_add(2).min(18); - let geometry = Geometry { crop: info.crop, bits: 8, color: info.color, par: info.par }; - let session = Session::new(profiles, (sps.width(), sps.height()), count, geometry)?; - let name = format!("VA-API H.264 ({})", session.vendor()); - let front = Front::new(session, info.length_size, mbs, &info.parameter_sets).map_err(|e| e.to_string())?; + let geometry = Geometry { crop: info.crop, bits, color: info.color, par: info.par }; + let (front, codec, vendor) = match info.codec { + NalCodec::H264 => { + if bits != 8 { + return Err(format!("{bits}-bit H.264 is not taken")); + } + let profiles = h264_profiles(info.profile_idc).ok_or_else(|| format!("H.264 profile_idc {} is not taken", info.profile_idc))?; + let rbsp = parameter_set(&info.parameter_sets, 1, |h| h & 0x1f == 7).ok_or("no SPS")?; + let sps = filmcraft_h264::params::Sps::parse(&rbsp).map_err(|e| e.to_string())?; + sps.check_supported().map_err(|e| e.to_string())?; + let mbs = (sps.pic_width_in_mbs, sps.frame_height_in_mbs()); + // the DPB, the picture being decoded, and one spare + let count = sps.max_dpb_frames().saturating_add(2).min(18); + let session = Session::new(profiles, (sps.width(), sps.height()), count, geometry)?; + let vendor = session.vendor().to_string(); + (Front::H264(Box::new(h264::Front::new(session, info.length_size, mbs, &info.parameter_sets).map_err(|e| e.to_string())?)), "H.264", vendor) + } + NalCodec::Hevc => { + let profiles = + hevc_profiles(info.profile_idc, bits).ok_or_else(|| format!("HEVC profile {} at {bits}-bit is not taken", info.profile_idc))?; + let rbsp = parameter_set(&info.parameter_sets, 2, |h| (h >> 1) & 0x3f == 33).ok_or("no SPS")?; + let sps = filmcraft_hevc::params::Sps::parse(&rbsp).map_err(|e| e.to_string())?; + sps.check_supported().map_err(|e| e.to_string())?; + if sps.range_extension { + return Err("HEVC range extensions are not taken".into()); + } + let count = (sps.max_dec_pic_buffering as usize).saturating_add(2).min(18); + let size = (sps.width, sps.height); + let session = Session::new(profiles, size, count, geometry)?; + let vendor = session.vendor().to_string(); + ( + Front::Hevc(Box::new(hevc::Front::new(session, info.length_size, size, bits, &info.parameter_sets).map_err(|e| e.to_string())?)), + "HEVC", + vendor, + ) + } + }; + let name = format!("VA-API {codec} ({vendor})"); Ok(Self { front, info, name, fed: 0, fail_after: None }) } @@ -99,11 +175,18 @@ mod linux { if self.fail_after.is_some_and(|n| self.fed > n) { return Err(CodecError::Decode("VA-API decoder failed (test hook)".into())); } - self.front.decode(sample, pts) + match &mut self.front { + Front::H264(f) => f.decode(sample, pts), + Front::Hevc(f) => f.decode(sample, pts), + } } fn flush(&mut self) -> Vec { - match self.front.flush() { + let flushed = match &mut self.front { + Front::H264(f) => f.flush(), + Front::Hevc(f) => f.flush(), + }; + match flushed { Ok(out) => out, Err(e) => { log::warn!("{}: {e} while flushing", self.name); @@ -113,7 +196,10 @@ mod linux { } fn reset(&mut self) { - self.front.reset(); + match &mut self.front { + Front::H264(f) => f.reset(), + Front::Hevc(f) => f.reset(), + } } fn name(&self) -> &str { diff --git a/crates/platform/src/vaapi/tests.rs b/crates/platform/src/vaapi/tests.rs index 8e1a0779..df2fe7a4 100644 --- a/crates/platform/src/vaapi/tests.rs +++ b/crates/platform/src/vaapi/tests.rs @@ -1,5 +1,6 @@ -//! The H.264 front end against a recording stand-in for the hardware, on every system: for x264 -//! streams with B-pyramids, several references, weighted prediction, several slices and open GOPs, +//! The H.264 and HEVC front ends against a recording stand-in for the hardware, on every system: +//! for x264 streams with B-pyramids, several references, weighted prediction, several slices and +//! open GOPs, and x265 streams (Main and Main 10, open GOP with RASL pictures, B-pyramids), //! the pictures come out in the software decoder's order with its pts (whole stream, after seeks, //! after a mid-stream flush), and every buffer handed to the hardware is consistent: the current //! picture's surface is free, every reference is a decoded surface listed in the DPB, reference @@ -13,8 +14,9 @@ use filmcraft_codecs::DecodedFrame; use filmcraft_codecs::hw::NalStreamInfo; use filmcraft_frame::VideoFrame; +use super::Accel; use super::ffi::{self, VAIQMatrixBufferH264, VAPictureH264, VAPictureParameterBufferH264, VASliceParameterBufferH264}; -use super::h264::{Accel, Front}; +use super::h264::Front; use crate::biplanar::{self, Biplanar, Geometry}; /// Records what it is asked to do and checks it; "decodes" nothing. @@ -42,7 +44,52 @@ impl Accel for Recorder { &self.ids } - fn decode( + fn decode_hevc( + &mut self, + target: usize, + pic: &ffi::VAPictureParameterBufferHEVC, + _iq: Option<&ffi::VAIQMatrixBufferHEVC>, + slices: &[(ffi::VASliceParameterBufferHEVC, Vec)], + ) -> Result<(), String> { + assert_eq!(pic.CurrPic.picture_id, self.ids[target], "current picture surface"); + let valid = |p: &ffi::VAPictureHEVC| p.flags & ffi::VA_PICTURE_HEVC_INVALID == 0; + let n = pic.ReferenceFrames.iter().take_while(|p| valid(p)).count(); + assert!(pic.ReferenceFrames[n..].iter().all(|p| !valid(p)), "reference frames packed at the front"); + let ids: BTreeSet<_> = pic.ReferenceFrames[..n].iter().map(|r| r.picture_id).collect(); + assert_eq!(ids.len(), n, "each reference frame once"); + assert!(!ids.contains(&pic.CurrPic.picture_id), "the target is not a reference"); + for r in &pic.ReferenceFrames[..n] { + let i = self.ids.iter().position(|&id| id == r.picture_id).expect("reference is a decoder surface"); + assert!(self.decoded[i], "reference surface {i} holds a picture"); + } + let rps = ffi::VA_PICTURE_HEVC_RPS_ST_CURR_BEFORE | ffi::VA_PICTURE_HEVC_RPS_ST_CURR_AFTER | ffi::VA_PICTURE_HEVC_RPS_LT_CURR; + assert!(!slices.is_empty()); + let last = ffi::hevc_slice_bits::LAST_SLICE_OF_PIC; + for (k, (s, data)) in slices.iter().enumerate() { + assert_eq!(s.slice_data_size as usize, data.len()); + assert!((s.slice_data_byte_offset as usize) <= data.len(), "slice data starts inside the slice"); + assert_eq!(s.LongSliceFlags >> last & 1 == 1, k + 1 == slices.len(), "LastSliceOfPic on the last slice only"); + let ty = (s.LongSliceFlags >> ffi::hevc_slice_bits::SLICE_TYPE) & 3; + let lists: &[(usize, u8)] = match ty { + 2 => &[], + 1 => &[(0, s.num_ref_idx_l0_active_minus1)], + _ => &[(0, s.num_ref_idx_l0_active_minus1), (1, s.num_ref_idx_l1_active_minus1)], + }; + for &(l, minus1) in lists { + let len = minus1 as usize + 1; + for &i in &s.RefPicList[l][..len] { + assert!((i as usize) < n, "list entry {i} is a reference frame"); + assert!(pic.ReferenceFrames[i as usize].flags & rps != 0, "list entries are in the current RPS"); + } + assert!(s.RefPicList[l][len..].iter().all(|&i| i == 0xFF), "nothing past the active entries"); + } + } + self.decoded[target] = true; + self.pictures += 1; + Ok(()) + } + + fn decode_h264( &mut self, target: usize, pic: &VAPictureParameterBufferH264, @@ -223,3 +270,112 @@ fn damaged_samples_are_errors() { let _ = f.flush(); } } + +/// (file, pixel format, x265 parameters). +const HEVC_STREAMS: &[(&str, &str, &str)] = &[ + ("va_front_hevc_open_gop.mp4", "yuv420p", "log-level=error:bframes=4:b-pyramid=1:keyint=12:min-keyint=12:scenecut=0:open-gop=1:ref=3"), + ("va_front_hevc_main10.mp4", "yuv420p10le", "log-level=error:bframes=3:keyint=12:min-keyint=12:scenecut=0:slices=2:weightp=1"), +]; + +fn hevc_fixture(ff: &std::path::Path, name: &str, pix_fmt: &str, params: &str) -> Option { + let out = filmcraft_testkit::fixtures_dir("platform").join(name); + let src = "testsrc2=s=320x184:r=24:d=2"; + let profile = if pix_fmt.contains("10") { "main10" } else { "main" }; + filmcraft_testkit::fixtures::generate(&out, |tmp| { + Command::new(ff) + .args([ + "-y", + "-v", + "error", + "-f", + "lavfi", + "-i", + src, + "-c:v", + "libx265", + "-profile:v", + profile, + "-tag:v", + "hvc1", + "-x265-params", + params, + "-pix_fmt", + pix_fmt, + ]) + .arg(tmp) + .stdin(Stdio::null()) + .status() + .is_ok_and(|s| s.success()) + }) +} + +fn hevc_front(s: &Stream) -> super::hevc::Front { + let info = NalStreamInfo::from_entry(&s.entry).unwrap().unwrap(); + super::hevc::Front::new(Recorder::new(18), info.length_size, info.coded, info.bit_depth_luma, &info.parameter_sets).unwrap() +} + +fn run_hevc(f: &mut super::hevc::Front, samples: &[(Vec, i64)]) -> Vec { + let mut out = Vec::new(); + for (s, p) in samples { + out.extend(pts(&f.decode(s, *p).unwrap())); + } + out.extend(pts(&f.flush().unwrap())); + out +} + +#[test] +fn hevc_output_order_and_buffers_match_the_software_decoder() { + let ff = filmcraft_testkit::require_ffmpeg!(); + for (name, pix_fmt, params) in HEVC_STREAMS { + let Some(path) = hevc_fixture(&ff, name, pix_fmt, params) else { continue }; + let s = read_stream(&path); + let mut f = hevc_front(&s); + let mut sw = filmcraft_codecs::software_video_decoder(&s.entry).unwrap(); + assert_eq!(run_hevc(&mut f, &s.samples), run_software(sw.as_mut(), &s.samples), "{name}"); + // seeks to every sync sample (open-GOP CRA pictures included: RASL pictures left out) + for k in (1..s.samples.len()).filter(|&i| s.sync[i]) { + f.reset(); + sw.reset(); + let end = (k + 13).min(s.samples.len()); + assert_eq!(run_hevc(&mut f, &s.samples[k..end]), run_software(sw.as_mut(), &s.samples[k..end]), "{name} from sample {k}"); + } + } +} + +/// Damaged HEVC samples give errors, never a panic. +#[test] +fn hevc_damaged_samples_are_errors() { + let ff = filmcraft_testkit::require_ffmpeg!(); + let (name, pix_fmt, params) = HEVC_STREAMS[0]; + let Some(path) = hevc_fixture(&ff, name, pix_fmt, params) else { return }; + let s = read_stream(&path); + let mut seed = 0x9E37_79B9_7F4A_7C15u64; + let mut next = || { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + seed + }; + for round in 0..40 { + let mut f = hevc_front(&s); + for (i, (smp, p)) in s.samples.iter().enumerate() { + let mut m = smp.clone(); + if i % 3 == round % 3 { + match next() % 3 { + 0 => m.truncate(next() as usize % m.len().max(1)), + 1 => { + let at = next() as usize % m.len().max(1); + if let Some(b) = m.get_mut(at) { + *b ^= 1 << (next() % 8); + } + } + _ => m.iter_mut().skip(4).take(6).for_each(|b| *b = next() as u8), + } + } + if f.decode(&m, *p).is_err() { + f.reset(); + } + } + let _ = f.flush(); + } +} diff --git a/crates/platform/src/vaapi/va.rs b/crates/platform/src/vaapi/va.rs index 0ed36858..5ee5e06e 100644 --- a/crates/platform/src/vaapi/va.rs +++ b/crates/platform/src/vaapi/va.rs @@ -15,8 +15,8 @@ use std::sync::OnceLock; use filmcraft_frame::VideoFrame; use libloading::Library; +use super::Accel; use super::ffi::{self, VAConfigAttrib, VAImage, VAImageFormat, VASurfaceID}; -use super::h264::Accel; use crate::biplanar::{self, Biplanar, Geometry}; /// The libva entry points (the libraries stay loaded for the life of the process). @@ -216,10 +216,22 @@ pub struct Session { geometry: Geometry, } +/// The surface and image formats of 8-bit (NV12) or 10-bit (P010) 4:2:0 pictures: +/// (render-target format, image fourcc, bits per pixel of the image). +fn formats(bits: u32) -> Result<(c_uint, u32, u32), String> { + match bits { + 8 => Ok((ffi::VA_RT_FORMAT_YUV420, ffi::VA_FOURCC_NV12, 12)), + 10 => Ok((ffi::VA_RT_FORMAT_YUV420_10, ffi::VA_FOURCC_P010, 24)), + _ => Err(format!("{bits}-bit pictures are not decoded through VA-API")), + } +} + impl Session { - /// A decoder for the first of `profiles` the driver decodes (VLD, 8-bit 4:2:0), with `count` - /// surfaces of `size` (coded luma samples); pictures read back are cut to `geometry`. + /// A decoder for the first of `profiles` the driver decodes (VLD, 4:2:0 of `geometry.bits`: + /// 8 or 10), with `count` surfaces of `size` (coded luma samples); pictures read back are cut + /// to `geometry`. pub fn new(profiles: &[ffi::VAProfile], size: (u32, u32), count: usize, geometry: Geometry) -> Result { + let (rt_format, _, _) = formats(geometry.bits)?; let display = Display::open()?; let api = display.api; let dpy = display.dpy; @@ -233,11 +245,11 @@ impl Session { why = e; continue; } - if attr.value & ffi::VA_RT_FORMAT_YUV420 == 0 { - why = format!("profile {profile} does not decode to 8-bit 4:2:0"); + if attr.value & rt_format == 0 { + why = format!("profile {profile} does not decode to {}-bit 4:2:0", geometry.bits); continue; } - attr.value = ffi::VA_RT_FORMAT_YUV420; + attr.value = rt_format; let mut id = ffi::VA_INVALID_ID; // SAFETY: as above; `id` is a live output. let st = unsafe { (api.create_config)(dpy, profile, ffi::VAEntrypointVLD, &mut attr, 1, &mut id) }; @@ -255,7 +267,7 @@ impl Session { let count_c = c_uint::try_from(count).map_err(|_| "too many surfaces")?; let mut surfaces = vec![ffi::VA_INVALID_SURFACE; count]; // SAFETY: `surfaces` has room for `count` ids; no attributes are passed (null, 0). - let st = unsafe { (api.create_surfaces)(dpy, ffi::VA_RT_FORMAT_YUV420, size.0, size.1, surfaces.as_mut_ptr(), count_c, std::ptr::null_mut(), 0) }; + let st = unsafe { (api.create_surfaces)(dpy, rt_format, size.0, size.1, surfaces.as_mut_ptr(), count_c, std::ptr::null_mut(), 0) }; s.display.check(st, "vaCreateSurfaces")?; s.surfaces = surfaces; let (w, h) = (c_int::try_from(size.0).map_err(|_| "picture too wide")?, c_int::try_from(size.1).map_err(|_| "picture too tall")?); @@ -294,16 +306,21 @@ impl Session { self.display.check(st, "vaRenderPicture") } - fn decode_into( + /// Decode one picture into `surface`: the picture parameters `pic`, the scaling matrices `iq` + /// when given, and a parameter + data buffer per slice; the buffers made go into `buffers`. + fn decode_into( &self, surface: VASurfaceID, buffers: &mut Vec, - pic: &ffi::VAPictureParameterBufferH264, - iq: &ffi::VAIQMatrixBufferH264, - slices: &[(ffi::VASliceParameterBufferH264, Vec)], + pic: &P, + iq: Option<&Q>, + slices: &[(S, Vec)], ) -> Result<(), String> { buffers.push(self.value_buffer(ffi::VAPictureParameterBufferType, pic)?); - buffers.push(self.value_buffer(ffi::VAIQMatrixBufferType, iq)?); + if let Some(iq) = iq { + buffers.push(self.value_buffer(ffi::VAIQMatrixBufferType, iq)?); + } + let head = buffers.len(); for (params, data) in slices { buffers.push(self.value_buffer(ffi::VASliceParameterBufferType, params)?); buffers.push(self.buffer(ffi::VASliceDataBufferType, data.as_ptr().cast(), data.len())?); @@ -312,8 +329,9 @@ impl Session { // SAFETY: `surface` is one of this context's render targets. let st = unsafe { (api.begin_picture)(self.display.dpy, self.context, surface) }; self.display.check(st, "vaBeginPicture")?; - let mut rendered = self.render(&mut buffers[..2]); - for pair in buffers[2..].chunks_mut(2) { + let (picture, slice_buffers) = buffers.split_at_mut(head); + let mut rendered = self.render(picture); + for pair in slice_buffers.chunks_mut(2) { if rendered.is_err() { break; } @@ -330,7 +348,8 @@ impl Session { if let Some(image) = self.image { return Ok(image); } - let mut format = VAImageFormat { fourcc: ffi::VA_FOURCC_NV12, byte_order: 1, bits_per_pixel: 12, ..Default::default() }; + let (_, fourcc, bits_per_pixel) = formats(self.geometry.bits)?; + let mut format = VAImageFormat { fourcc, byte_order: 1, bits_per_pixel, ..Default::default() }; let mut image = VAImage { image_id: ffi::VA_INVALID_ID, buf: ffi::VA_INVALID_ID, ..Default::default() }; let (w, h) = (c_int::try_from(self.size.0).map_err(|_| "picture too wide")?, c_int::try_from(self.size.1).map_err(|_| "picture too tall")?); // SAFETY: `format` and `image` are live; the driver fills `image` in. @@ -354,7 +373,8 @@ impl Session { Ok(image) } - /// Every sample of `image` set to 128 (mid-gray in NV12). + /// Every sample of `image` set to mid-gray: 128 in NV12, 512 (0x8000 in the high bits of + /// little-endian 16-bit words) in P010. fn write_gray(&self, image: &VAImage) -> Result<(), String> { let api = self.display.api; let mut ptr: *mut c_void = std::ptr::null_mut(); @@ -363,8 +383,15 @@ impl Session { self.display.check(st, "vaMapBuffer")?; if !ptr.is_null() { // SAFETY: a mapped image buffer holds `data_size` writable bytes at `ptr` until it is - // unmapped below. - unsafe { std::ptr::write_bytes(ptr.cast::(), 128, image.data_size as usize) }; + // unmapped below; the slice does not outlive this block. + let data = unsafe { std::slice::from_raw_parts_mut(ptr.cast::(), image.data_size as usize) }; + if self.geometry.bits > 8 { + for w in data.as_chunks_mut::<2>().0 { + w.copy_from_slice(&0x8000u16.to_le_bytes()); + } + } else { + data.fill(128); + } } // SAFETY: the buffer was mapped above. let st = unsafe { (api.unmap_buffer)(self.display.dpy, image.buf) }; @@ -372,6 +399,18 @@ impl Session { if ptr.is_null() { Err("vaMapBuffer returned no data".into()) } else { Ok(()) } } + fn decode_picture(&mut self, target: usize, pic: &P, iq: Option<&Q>, slices: &[(S, Vec)]) -> Result<(), String> { + let surface = *self.surfaces.get(target).ok_or("surface out of range")?; + let mut buffers = Vec::with_capacity(2 + 2 * slices.len()); + let decoded = self.decode_into(surface, &mut buffers, pic, iq, slices); + for id in buffers { + // SAFETY: each id is a buffer made above on this display, destroyed once; vaEndPicture + // has returned, so the driver no longer reads it. + unsafe { (self.display.api.destroy_buffer)(self.display.dpy, id) }; + } + decoded + } + fn read_image(&self, image: &VAImage) -> Result { let api = self.display.api; let mut ptr: *mut c_void = std::ptr::null_mut(); @@ -394,10 +433,10 @@ impl Session { } } -/// The two planes of a mapped NV12 image. +/// The two planes of a mapped NV12 / P010 image. fn biplanar_view<'a>(image: &VAImage, data: &'a [u8]) -> Result, String> { - if image.format.fourcc != ffi::VA_FOURCC_NV12 || image.num_planes < 2 { - return Err(format!("the read-back image is not NV12 (fourcc {:#x}, {} planes)", image.format.fourcc, image.num_planes)); + if !matches!(image.format.fourcc, ffi::VA_FOURCC_NV12 | ffi::VA_FOURCC_P010) || image.num_planes < 2 { + return Err(format!("the read-back image is not NV12 / P010 (fourcc {:#x}, {} planes)", image.format.fourcc, image.num_planes)); } let [p0, p1, _] = image.pitches; if p0 != p1 { @@ -413,22 +452,24 @@ impl Accel for Session { &self.surfaces } - fn decode( + fn decode_h264( &mut self, target: usize, pic: &ffi::VAPictureParameterBufferH264, iq: &ffi::VAIQMatrixBufferH264, slices: &[(ffi::VASliceParameterBufferH264, Vec)], ) -> Result<(), String> { - let surface = *self.surfaces.get(target).ok_or("surface out of range")?; - let mut buffers = Vec::with_capacity(2 + 2 * slices.len()); - let decoded = self.decode_into(surface, &mut buffers, pic, iq, slices); - for id in buffers { - // SAFETY: each id is a buffer made above on this display, destroyed once; vaEndPicture - // has returned, so the driver no longer reads it. - unsafe { (self.display.api.destroy_buffer)(self.display.dpy, id) }; - } - decoded + self.decode_picture(target, pic, Some(iq), slices) + } + + fn decode_hevc( + &mut self, + target: usize, + pic: &ffi::VAPictureParameterBufferHEVC, + iq: Option<&ffi::VAIQMatrixBufferHEVC>, + slices: &[(ffi::VASliceParameterBufferHEVC, Vec)], + ) -> Result<(), String> { + self.decode_picture(target, pic, iq, slices) } fn read(&mut self, index: usize) -> Result { diff --git a/crates/platform/tests/vaapi.rs b/crates/platform/tests/vaapi.rs index d79caabd..507e7768 100644 --- a/crates/platform/tests/vaapi.rs +++ b/crates/platform/tests/vaapi.rs @@ -1,10 +1,12 @@ -//! VA-API decoding against our software decoder (Linux): every picture of H.264 fixtures covering -//! the stream features the hardware path takes (CAVLC and CABAC, B-pyramids, several references, -//! weighted prediction, temporal direct, custom scaling matrices, several slices per picture, -//! 1080p / 2160p) must be identical (bit-exact planes, colour, pixel aspect, pts and presentation +//! VA-API decoding against our software decoders (Linux): every picture of H.264 and HEVC fixtures +//! covering the stream features the hardware path takes (H.264: CAVLC and CABAC, B-pyramids, +//! several references, weighted prediction, temporal direct, custom scaling matrices, several +//! slices per picture; HEVC Main and Main 10: open GOP with RASL pictures, scaling lists, several +//! slices with wavefront parallel processing, weighted prediction, transform skip and AMP; 1080p / +//! 2160p) must be identical (bit-exact planes, colour, pixel aspect, pts and presentation //! order), also after `reset` + reseek and a mid-stream `flush`; a forced mid-stream failure must //! continue in software with the software decoder's exact output; what the hardware path does not -//! take (interlaced, 10-bit, HEVC so far) is declined; damaged samples and parameter sets give +//! take (interlaced and 10-bit H.264, 4:2:2 HEVC) is declined; damaged samples and parameter sets give //! errors or fall back, never crash or hang. Skips without ffmpeg (fixture generator) or without a //! VA-API driver. #![cfg(target_os = "linux")] @@ -53,17 +55,44 @@ fn h264_fixture(ff: &std::path::Path, name: &str, profile: &str, params: &str) - fixture(ff, name, &["-f", "lavfi", "-i", src, "-c:v", "libx264", "-profile:v", profile, "-preset", "fast", "-x264-params", params, "-pix_fmt", "yuv420p"]) } -/// The small fixtures: the shared H.264 High one and the feature fixtures above. +/// HEVC fixtures for the stream features the hardware path takes: (file, pixel format, x265 +/// parameters). 640x360, three seconds at 24 fps, keyframes every second. +const HEVC: &[(&str, &str, &str)] = &[ + ("va_hevc_scaling_lists.mp4", "yuv420p", "log-level=error:bframes=3:keyint=24:min-keyint=24:scenecut=0:scaling-list=default"), + ("va_hevc_slices_wpp.mp4", "yuv420p10le", "log-level=error:bframes=3:keyint=24:min-keyint=24:scenecut=0:slices=3:wpp=1"), + ("va_hevc_weighted.mp4", "yuv420p", "log-level=error:bframes=3:keyint=24:min-keyint=24:scenecut=0:weightp=1:weightb=1:ref=4"), + ("va_hevc_tskip_amp.mp4", "yuv420p10le", "log-level=error:bframes=4:b-pyramid=1:keyint=24:min-keyint=24:scenecut=0:tskip=1:amp=1:rect=1:open-gop=1"), +]; + +fn hevc_fixture(ff: &std::path::Path, name: &str, pix_fmt: &str, params: &str) -> Option { + let src = "testsrc2=s=640x360:r=24:d=3,noise=alls=12:allf=t"; + let profile = if pix_fmt.contains("10") { "main10" } else { "main" }; + fixture( + ff, + name, + &["-f", "lavfi", "-i", src, "-c:v", "libx265", "-preset", "fast", "-profile:v", profile, "-tag:v", "hvc1", "-x265-params", params, "-pix_fmt", pix_fmt], + ) +} + +/// The small fixtures: the shared H.264 High, HEVC Main (open GOP) and Main 10 ones and the +/// feature fixtures above. fn small_streams(ff: &std::path::Path) -> Vec<(String, Stream)> { let mut out = Vec::new(); - if let Some(path) = named(ff, "h264_high.mp4") { - out.push(("h264_high.mp4".to_string(), read_stream(&path))); + for name in ["h264_high.mp4", "hevc_main.mp4", "hevc_main10.mp4"] { + if let Some(path) = named(ff, name) { + out.push((name.to_string(), read_stream(&path))); + } } for (name, profile, params) in H264 { if let Some(path) = h264_fixture(ff, name, profile, params) { out.push((name.to_string(), read_stream(&path))); } } + for (name, pix_fmt, params) in HEVC { + if let Some(path) = hevc_fixture(ff, name, pix_fmt, params) { + out.push((name.to_string(), read_stream(&path))); + } + } out } @@ -95,7 +124,7 @@ fn parity(name: &str, s: &Stream) { let mut a = decode_all(hw.as_mut(), &s.samples[k..end]); let mut b = decode_all(sw.as_mut(), &s.samples[k..end]); assert_eq!(a.len(), b.len(), "{name} from sample {k}: pictures"); - if !info.nal_types(&s.samples[k].0).contains(&5) { + if info.codec == filmcraft_codecs::hw::NalCodec::H264 && !info.nal_types(&s.samples[k].0).contains(&5) { let start = s.samples[k].1; a.retain(|f| f.pts >= start); b.retain(|f| f.pts >= start); @@ -103,21 +132,24 @@ fn parity(name: &str, s: &Stream) { assert!(!a.is_empty(), "{name}: pictures after seeking to {k}"); assert_same(&format!("{name} from sample {k}"), &a, &b); } - // a flush in the middle of a GOP, then carrying on (references survive a flush) - for d in [&mut hw, &mut sw] { - d.reset(); - } - let mid = s.samples.len() / 2; - let mut a = Vec::new(); - let mut b = Vec::new(); - for (d, out) in [(&mut hw, &mut a), (&mut sw, &mut b)] { - for (smp, pts) in &s.samples[..mid] { - out.extend(d.decode(smp, *pts).unwrap()); + // a flush in the middle of a GOP, then carrying on: H.264 only, where references survive a + // flush (an HEVC flush ends the stream; the GOP cache always seeks after one) + if info.codec == filmcraft_codecs::hw::NalCodec::H264 { + for d in [&mut hw, &mut sw] { + d.reset(); } - out.extend(d.flush()); - out.extend(decode_all(d.as_mut(), &s.samples[mid..])); + let mid = s.samples.len() / 2; + let mut a = Vec::new(); + let mut b = Vec::new(); + for (d, out) in [(&mut hw, &mut a), (&mut sw, &mut b)] { + for (smp, pts) in &s.samples[..mid] { + out.extend(d.decode(smp, *pts).unwrap()); + } + out.extend(d.flush()); + out.extend(decode_all(d.as_mut(), &s.samples[mid..])); + } + assert_same(&format!("{name} with a flush at {mid}"), &a, &b); } - assert_same(&format!("{name} with a flush at {mid}"), &a, &b); // back to the start after seeking around: the whole stream again hw.reset(); sw.reset(); @@ -130,6 +162,40 @@ fn parity(name: &str, s: &Stream) { } } +/// [`parity`] for large streams, with little memory: the two decoders run side by side and each +/// picture is compared and dropped as soon as both have output it (a 4K 10-bit stream held whole +/// takes gigabytes). The whole stream, then from the last sync sample after a reset. +fn parity_streaming(name: &str, s: &Stream) { + let Some(mut hw) = hardware(s) else { return }; + let mut sw = software(s); + let last_sync = (1..s.samples.len()).rev().find(|&i| s.sync[i]).unwrap_or(0); + for start in [0, last_sync] { + hw.reset(); + sw.reset(); + let (mut a, mut b) = (std::collections::VecDeque::new(), std::collections::VecDeque::new()); + let mut compared = 0usize; + let mut compare = |a: &mut std::collections::VecDeque, + b: &mut std::collections::VecDeque| { + while !a.is_empty() && !b.is_empty() { + let (Some(x), Some(y)) = (a.pop_front(), b.pop_front()) else { break }; + assert_same(&format!("{name} from sample {start}"), &[x], &[y]); + compared += 1; + } + }; + for (smp, pts) in &s.samples[start..] { + a.extend(hw.decode(smp, *pts).unwrap()); + b.extend(sw.decode(smp, *pts).unwrap()); + compare(&mut a, &mut b); + } + a.extend(hw.flush()); + b.extend(sw.flush()); + compare(&mut a, &mut b); + assert!(a.is_empty() && b.is_empty(), "{name} from sample {start}: picture counts differ"); + assert!(compared >= s.samples.len().saturating_sub(start) / 2, "{name} from sample {start}: {compared} pictures"); + assert!(hw.name().starts_with("VA-API"), "{name}: stayed in hardware ({})", hw.name()); + } +} + #[test] fn bit_exact_with_the_software_decoder() { let ff = filmcraft_testkit::require_ffmpeg!(); @@ -161,7 +227,7 @@ fn large(size: &str) { "yuv420p", ]; let Some(path) = fixture(&ff, &format!("h264_{h}.mp4"), &args) else { return }; - parity(&format!("h264 {size}"), &read_stream(&path)); + parity_streaming(&format!("h264 {size}"), &read_stream(&path)); } #[test] @@ -170,6 +236,42 @@ fn h264_1080p_and_2160p_are_bit_exact() { large("3840x2160"); } +fn large_hevc(size: &str, pix_fmt: &str) { + let ff = filmcraft_testkit::require_ffmpeg!(); + let (_, h) = size.split_once('x').unwrap(); + let src = format!("testsrc2=s={size}:r=24:d=2,noise=alls=12:allf=t"); + let ten = pix_fmt.contains("10"); + let args = [ + "-f", + "lavfi", + "-i", + &src, + "-c:v", + "libx265", + "-preset", + "ultrafast", + "-profile:v", + if ten { "main10" } else { "main" }, + "-tag:v", + "hvc1", + "-x265-params", + "log-level=error:bframes=4:keyint=24:min-keyint=24:scenecut=0:open-gop=1", + "-pix_fmt", + pix_fmt, + ]; + let name = format!("va_hevc_{}_{h}.mp4", if ten { "main10" } else { "main" }); + let Some(path) = fixture(&ff, &name, &args) else { return }; + parity_streaming(&name, &read_stream(&path)); +} + +#[test] +fn hevc_main_and_main10_1080p_and_2160p_are_bit_exact() { + for size in ["1920x1080", "3840x2160"] { + large_hevc(size, "yuv420p"); + large_hevc(size, "yuv420p10le"); + } +} + #[test] fn mid_stream_failure_continues_in_software() { let ff = filmcraft_testkit::require_ffmpeg!(); @@ -215,9 +317,10 @@ fn declines_what_the_hardware_path_does_not_take() { let s = read_stream(&path); assert!(filmcraft_platform::vaapi_factory(&s.entry).is_none(), "{name}: declined"); } - if let Some(path) = named(&ff, "hevc_main.mp4") { + let args = ["-f", "lavfi", "-i", src, "-c:v", "libx265", "-tag:v", "hvc1", "-x265-params", "log-level=error:keyint=12", "-pix_fmt", "yuv422p10le"]; + if let Some(path) = fixture(&ff, "va_hevc_422.mp4", &args) { let s = read_stream(&path); - assert!(filmcraft_platform::vaapi_factory(&s.entry).is_none(), "HEVC: declined"); + assert!(filmcraft_platform::vaapi_factory(&s.entry).is_none(), "HEVC 4:2:2: declined"); } // Settings ▸ Playback ▸ Hardware decoding Off let Some(path) = named(&ff, "h264_high.mp4") else { return }; diff --git a/docs/adr/0001-platform-ffi.md b/docs/adr/0001-platform-ffi.md index a6da16e0..4c6eb7aa 100644 --- a/docs/adr/0001-platform-ffi.md +++ b/docs/adr/0001-platform-ffi.md @@ -104,7 +104,7 @@ a machine without the encoder) is an error naming the reason instead of a differ ## Addendum (2026-10-08): VA-API hardware decoding on Linux -The Linux backend this decision named: H.264 decoding through VA-API (`vaapi/`). The rules above +The Linux backend this decision named: H.264 and HEVC decoding through VA-API (`vaapi/`). The rules above hold; VA-API differs from the other two backends in three ways. - **libva is loaded at run time** (`libva.so.2`, `libva-drm.so.2`, through `libloading`: ISC, already @@ -115,11 +115,12 @@ hold; VA-API differs from the other two backends in three ways. C compiler's view of it, as for NVENC. Only `vaapi::va` carries `#[allow(unsafe_code)]`; the declarations, the H.264 front end (`vaapi::h264`) and the decoder (`VaDecoder`) are safe code. - **Decoding is stateless:** the host parses the stream and keeps the decoded picture buffer; the GPU - only decodes slice data. FilmCraft does that host side with the software decoder's own parsers and - DPB (`filmcraft_h264::dpb` is generic over what a picture is), so the two decoders decide alike by + only decodes slice data. FilmCraft does that host side with the software decoders' own parsers and + DPBs (`filmcraft_h264::dpb` and `filmcraft_hevc::dpb` are generic over what a picture is), so the two decoders decide alike by construction, and the front end is tested on every OS against a stand-in for the hardware. The one place they cannot agree is concealment: pictures predicted from frames that were never decoded - (the leading pictures of an open GOP after a seek) are concealed by both but can differ. + (the leading pictures of an open H.264 GOP after a seek) are concealed by both but can differ; + HEVC pictures with references that were never decoded go to the software decoder instead. - **One callback into Rust:** libva's error messages go to our log through a callback that runs under `catch_unwind`; its info messages are turned off. diff --git a/docs/architecture.md b/docs/architecture.md index dac14710..3541e9cd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -72,7 +72,7 @@ and `filmcraft-cli`. | `engine` | L4 | `Session`, command registry, undo history, media pool, jobs, interchange glue | | `ui-egui` | L5 | the egui frontend: docking, panels, timeline, monitors, playback, control-channel handlers | | `automation` | L5 | MCP server (`rmcp`, stdio), headless or bridged to the running app | -| `platform` | L5 | OS media FFI only: hardware video decoding (VideoToolbox H.264 / HEVC on macOS; Media Foundation / Direct3D 11 H.264 / HEVC / VP9 / AV1 on Windows; VA-API H.264 on Linux, with our own H.264 parsers and DPB driving the stateless hardware; a no-op elsewhere) behind `codecs::VideoDecoder`, with transparent fallback to our decoders, and hardware H.264 encoding (VideoToolbox, opt-in) and H.265 encoding (VideoToolbox, the only H.265 encoder; the format exists only where a hardware encoder does) and NVIDIA NVENC H.264 encoding (Windows, opt-in) behind `export::VideoEncoder`; H.264 declines to the built-in encoder for what the hardware does not take. The one crate allowed `unsafe` ([ADR 0001](adr/0001-platform-ffi.md), [README](../crates/platform/README.md)) | +| `platform` | L5 | OS media FFI only: hardware video decoding (VideoToolbox H.264 / HEVC on macOS; Media Foundation / Direct3D 11 H.264 / HEVC / VP9 / AV1 on Windows; VA-API H.264 / HEVC on Linux, with our own parsers and DPBs driving the stateless hardware; a no-op elsewhere) behind `codecs::VideoDecoder`, with transparent fallback to our decoders, and hardware H.264 encoding (VideoToolbox, opt-in) and H.265 encoding (VideoToolbox, the only H.265 encoder; the format exists only where a hardware encoder does) and NVIDIA NVENC H.264 encoding (Windows, opt-in) behind `export::VideoEncoder`; H.264 declines to the built-in encoder for what the hardware does not take. The one crate allowed `unsafe` ([ADR 0001](adr/0001-platform-ffi.md), [README](../crates/platform/README.md)) | | `filmcraft` | L6 | desktop binary: eframe/wgpu window, cpal audio output, file dialogs, native macOS menu, TCP control server | | `filmcraft-cli` | L6 | headless CLI: `exec`, `run`, `inspect`, `describe`, `commands`, `import`, `export`, `render`, `probe`, `mcp`; `--bridge` targets the running app | | `filmcraft-web` | L6 | the browser app (wasm32): eframe web runner on WebGPU/WebGL2, Blob-backed services, OPFS recovery, WebAudio, WebCodecs, `window.filmcraft` API ([web.md](web.md)) | diff --git a/docs/performance.md b/docs/performance.md index ba41ef56..e6ac4eaf 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -349,7 +349,7 @@ Software AV1 decodes 4K at 5.5 fps here, so it never plays in real time; with th plays without a drop. As on the other codecs, the hardware ignores draft mode and what is left on the CPU is the readback and plane conversion. -## Results (Linux VA-API H.264 hardware decoding, Off → Auto) +## Results (Linux VA-API H.264 and HEVC hardware decoding, Off → Auto) Same commit, Settings ▸ Playback ▸ Hardware decoding switched with the bench flag (`cargo xtask bench --sections decode --only dec_h --repeat 3 --hw off|auto`), 2026-10-08, Intel @@ -372,6 +372,25 @@ planar Y'CbCr. One picture is decoded at a time and read back as soon as the DPB 4K throughput (56 fps) is bound by that round trip, not by the video engine; overlapping decode and read-back, or zero-copy into wgpu, would raise it. +### HEVC through VA-API (HW5 follow-up) + +Same machine and commands on 2026-10-09 with HEVC added, at a lower load (the software decoders ran +faster than in the table above; compare within a row). H.264 is unchanged; every HEVC stream now +goes through VA-API too (216–360 hardware frames, no fallbacks), bit-exact with the software decoder. + +| codec | size | CPU ms/frame Off → **Auto** | fps Off → **Auto** (best of 3) | +|---|---|---|---| +| H.264 | 1080p | 57.3 → **2.6** | 227 → **241** | +| H.264 | 2160p | 231.8 → **12.6** | 58 → **52** | +| HEVC | 1080p | 45.6 → **2.3** | 141 → **341** | +| HEVC | 2160p | 202.5 → **10.1** | 47 → **76** | +| HEVC Main 10 | 2160p | 211.6 → **18.4** | 42 → **46** | + +At this lower load the multi-threaded software decoders keep up with the one-picture-at-a-time +hardware path on throughput for H.264 and 10-bit HEVC, at 5–20 % of their CPU time; under load +(the run above) the hardware path is ahead on both. 10-bit pictures cost more to read back (P010 is +twice the bytes of NV12 and is shifted down into 16-bit planes). + ## Results (GPU1: blend modes on the GPU compositor, #30, before → after) Before = this change with the old whole-frame CPU fallback for non-Normal blend modes put back