mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-14 19:26:59 -04:00
Merge pull request #358 from JordyUSA/vaapi-h264-decode
HW5: Linux VA-API hardware decoding for H.264 and HEVC (Main / Main 10)
This commit is contained in:
29 files changed
+3943
-127
No files matched your search
@@ -21,7 +21,7 @@ panic path, and fix a crash before building on top of it. The cross-app standard
|
||||
code alone (a literal that always parses): a single-item `#[allow(clippy::expect_used)]` with the reason.
|
||||
3. **No `unsafe`** (`unsafe_code = "forbid"` for the workspace). The single exception is
|
||||
`crates/platform`, which needs `unsafe` to call the operating system's media APIs (hardware
|
||||
video decoding through VideoToolbox and Media Foundation / Direct3D 11; later encoding and Linux) and does nothing else. It
|
||||
video decoding through VideoToolbox, Media Foundation / Direct3D 11 and VA-API; hardware encoding) and does nothing else. It
|
||||
uses `unsafe_code = "deny"` with `#[allow(unsafe_code)]` only on its FFI modules, a
|
||||
`// SAFETY:` comment on every `unsafe` block, a safe `Result`-returning public API, and the
|
||||
pure-Rust decoder as the tested fallback ([ADR 0001](docs/adr/0001-platform-ffi.md)).
|
||||
|
||||
Generated
+4
@@ -2097,16 +2097,20 @@ dependencies = [
|
||||
name = "filmcraft-platform"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"filmcraft-bitstream",
|
||||
"filmcraft-codecs",
|
||||
"filmcraft-color",
|
||||
"filmcraft-export",
|
||||
"filmcraft-frame",
|
||||
"filmcraft-h264",
|
||||
"filmcraft-hevc",
|
||||
"filmcraft-isobmff",
|
||||
"filmcraft-media",
|
||||
"filmcraft-project",
|
||||
"filmcraft-render",
|
||||
"filmcraft-testkit",
|
||||
"filmcraft-time",
|
||||
"libloading",
|
||||
"log",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-media",
|
||||
|
||||
@@ -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) and Windows (Media Foundation), with Linux to follow; 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)).
|
||||
|
||||
<br>
|
||||
|
||||
@@ -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; Linux has no hardware path yet. 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.
|
||||
|
||||
+4
-4
@@ -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). 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 has no hardware path. 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);
|
||||
- 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; no hardware path on Linux, 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). Next: VA-API (Linux) decode, 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
|
||||
|
||||
@@ -461,7 +461,10 @@ mod format_tests {
|
||||
#[test]
|
||||
fn startup_registers_the_hardware_decoders() {
|
||||
let hardware = super::register_hardware_decoders();
|
||||
assert_eq!(filmcraft_platform::registered(), cfg!(any(target_os = "macos", target_os = "windows")), "{hardware:?}");
|
||||
// always on macOS and Windows; on Linux when a VA-API driver is there
|
||||
let expected = cfg!(any(target_os = "macos", target_os = "windows"))
|
||||
|| (cfg!(target_os = "linux") && matches!(hardware, filmcraft_platform::Availability::Available(_)));
|
||||
assert_eq!(filmcraft_platform::registered(), expected, "{hardware:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -277,6 +277,9 @@ mod tests {
|
||||
fn startup_registers_the_hardware_decoders_without_a_logger() {
|
||||
assert!(!log::log_enabled!(log::Level::Info));
|
||||
let hardware = super::register_hardware_decoders();
|
||||
assert_eq!(filmcraft_platform::registered(), cfg!(any(target_os = "macos", target_os = "windows")), "{hardware:?}");
|
||||
// always on macOS and Windows; on Linux when a VA-API driver is there
|
||||
let expected = cfg!(any(target_os = "macos", target_os = "windows"))
|
||||
|| (cfg!(target_os = "linux") && matches!(hardware, filmcraft_platform::Availability::Available(_)));
|
||||
assert_eq!(filmcraft_platform::registered(), expected, "{hardware:?}");
|
||||
}
|
||||
}
|
||||
@@ -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) for the H.264 and HEVC 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.",
|
||||
),
|
||||
],
|
||||
},
|
||||
|
||||
+10
-29
@@ -6,11 +6,11 @@
|
||||
//! jobs of later pictures block per row on the reference data they need, so several pictures decode
|
||||
//! concurrently (frame-level parallelism).
|
||||
|
||||
use crate::dpb::{Dpb, Output, OutputMeta};
|
||||
use crate::dpb::{Dpb, Output, OutputMeta, RefMark};
|
||||
use crate::error::{Error, Result, ensure, invalid, unsupported};
|
||||
use crate::params::{Pps, Sps};
|
||||
use crate::picture::{Frame, FrameRef, MbKind, MbState, Planes, RefPic};
|
||||
use crate::slice::{NalHeader, Poc, PocState, SliceHeader, SliceType, nal_type};
|
||||
use crate::slice::{NalHeader, Poc, PocState, SliceHeader, SliceType, is_new_picture, nal_type};
|
||||
use crate::slicedec::{PicState, SliceDecoder};
|
||||
use crate::transform::LevelScale;
|
||||
use crate::{ColorInfo, Picture};
|
||||
@@ -81,7 +81,7 @@ pub struct Decoder {
|
||||
spss: Vec<Option<Arc<Sps>>>,
|
||||
ppss: Vec<Option<Arc<Pps>>>,
|
||||
nal_length_size: Option<usize>,
|
||||
dpb: Dpb,
|
||||
dpb: Dpb<FrameRef>,
|
||||
poc_state: PocState,
|
||||
prev_ref_frame_num: u32,
|
||||
pending: Option<PendingPic>,
|
||||
@@ -89,7 +89,7 @@ pub struct Decoder {
|
||||
active_sps: Option<Arc<Sps>>,
|
||||
ls_cache: Vec<(Arc<Pps>, Arc<LevelScale>)>,
|
||||
/// Pictures leaving the DPB, converted once their decoding job has finished.
|
||||
out_queue: VecDeque<Output>,
|
||||
out_queue: VecDeque<Output<FrameRef>>,
|
||||
shared: Arc<Shared>,
|
||||
#[cfg(feature = "threads")]
|
||||
pool: Option<rayon::ThreadPool>,
|
||||
@@ -106,27 +106,6 @@ impl Default for Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `sh` the first slice of a new primary coded picture relative to `prev` (7.4.1.2.4)?
|
||||
fn is_new_picture(prev: &SliceHeader, sh: &SliceHeader, sps: &Sps) -> bool {
|
||||
if sh.frame_num != prev.frame_num
|
||||
|| sh.pps_id != prev.pps_id
|
||||
|| sh.field_pic != prev.field_pic
|
||||
|| sh.bottom_field != prev.bottom_field
|
||||
|| (sh.nal_ref_idc == 0) != (prev.nal_ref_idc == 0)
|
||||
|| sh.idr != prev.idr
|
||||
|| (sh.idr && prev.idr && sh.idr_pic_id != prev.idr_pic_id)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if sps.pic_order_cnt_type == 0 && (sh.pic_order_cnt_lsb != prev.pic_order_cnt_lsb || sh.delta_pic_order_cnt_bottom != prev.delta_pic_order_cnt_bottom) {
|
||||
return true;
|
||||
}
|
||||
if sps.pic_order_cnt_type == 1 && sh.delta_pic_order_cnt != prev.delta_pic_order_cnt {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// The worker threads [`Decoder::new`] uses: the available cores (at most 16) with the `threads`
|
||||
/// feature on native targets, 1 otherwise.
|
||||
pub fn default_threads() -> usize {
|
||||
@@ -384,7 +363,9 @@ impl Decoder {
|
||||
let Some(pending) = self.pending.as_mut() else {
|
||||
return invalid("slice without a started picture");
|
||||
};
|
||||
let refs = self.dpb.build_ref_lists(&sh, pending.poc.frame(), sps.max_frame_num())?;
|
||||
let refs = self
|
||||
.dpb
|
||||
.build_ref_lists(&sh, pending.poc.frame(), sps.max_frame_num(), |e| RefPic { frame: e.frame.clone(), long_term: e.mark == RefMark::Long })?;
|
||||
if !sh.slice_type.is_intra() {
|
||||
ensure!(!refs[0].is_empty(), "no reference pictures available for inter slice");
|
||||
if sh.slice_type.is_b() {
|
||||
@@ -464,7 +445,7 @@ impl Decoder {
|
||||
}
|
||||
_ => Planes::gray(mb_w * 16, mb_h * 16),
|
||||
};
|
||||
(Arc::new(Frame::from_planes(id, 0, &planes)), meta.clone())
|
||||
(Arc::new(Frame::from_planes(id, 0, &planes)), 0, meta.clone())
|
||||
};
|
||||
let mut upd = |fnum: u32| poc_state.update_gap_frame(fnum, sps);
|
||||
self.shared.stats.lock().map(|mut s| s.frame_num_gaps += 1).ok();
|
||||
@@ -516,7 +497,7 @@ impl Decoder {
|
||||
run_job(frame, slices, &shared, draft);
|
||||
}
|
||||
|
||||
fn emit(&mut self, outs: Vec<Output>) {
|
||||
fn emit(&mut self, outs: Vec<Output<FrameRef>>) {
|
||||
self.out_queue.extend(outs);
|
||||
}
|
||||
}
|
||||
@@ -613,7 +594,7 @@ fn output_meta(sps: &Sps, pts: i64, key: bool) -> OutputMeta {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_picture(o: &Output) -> Picture {
|
||||
fn make_picture(o: &Output<FrameRef>) -> Picture {
|
||||
let (cx, cy, cw, ch) = o.meta.crop;
|
||||
let (cx, cy, cw, ch) = (cx as usize, cy as usize, cw as usize, ch as usize);
|
||||
let (y, u, v) = o.frame.copy_cropped((cx, cy, cw, ch));
|
||||
|
||||
+36
-32
@@ -1,8 +1,11 @@
|
||||
//! Decoded picture buffer: reference marking (8.2.5), reference list construction (8.2.4) and output
|
||||
//! ordering / bumping (C.4).
|
||||
//!
|
||||
//! 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::{Mmco, SliceHeader, SliceType};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -28,8 +31,8 @@ pub struct OutputMeta {
|
||||
pub draft: bool,
|
||||
}
|
||||
|
||||
pub struct DpbEntry {
|
||||
pub frame: FrameRef,
|
||||
pub struct DpbEntry<F> {
|
||||
pub frame: F,
|
||||
pub poc: i32,
|
||||
pub frame_num: u32,
|
||||
pub long_term_frame_idx: u32,
|
||||
@@ -39,8 +42,8 @@ pub struct DpbEntry {
|
||||
pub meta: Arc<OutputMeta>,
|
||||
}
|
||||
|
||||
pub struct Dpb {
|
||||
pub entries: Vec<DpbEntry>,
|
||||
pub struct Dpb<F> {
|
||||
pub entries: Vec<DpbEntry<F>>,
|
||||
/// None = "no long-term frame indices".
|
||||
pub max_long_term_frame_idx: Option<u32>,
|
||||
pub capacity: usize,
|
||||
@@ -48,18 +51,18 @@ pub struct Dpb {
|
||||
}
|
||||
|
||||
/// A picture leaving the DPB for output.
|
||||
pub struct Output {
|
||||
pub frame: FrameRef,
|
||||
pub struct Output<F> {
|
||||
pub frame: F,
|
||||
pub poc: i32,
|
||||
pub meta: Arc<OutputMeta>,
|
||||
}
|
||||
|
||||
impl Dpb {
|
||||
impl<F: Clone> Dpb<F> {
|
||||
pub fn new() -> Self {
|
||||
Dpb { entries: Vec::new(), max_long_term_frame_idx: None, capacity: 16, max_reorder: 16 }
|
||||
}
|
||||
|
||||
fn frame_num_wrap(e: &DpbEntry, cur_frame_num: u32, max_frame_num: u32) -> i32 {
|
||||
fn frame_num_wrap(e: &DpbEntry<F>, cur_frame_num: u32, max_frame_num: u32) -> i32 {
|
||||
if e.frame_num > cur_frame_num { e.frame_num as i32 - max_frame_num as i32 } else { e.frame_num as i32 }
|
||||
}
|
||||
|
||||
@@ -73,7 +76,7 @@ impl Dpb {
|
||||
}
|
||||
|
||||
/// Output the picture with the smallest POC (C.4.5.3 "bumping"). Returns false if none waits.
|
||||
fn bump(&mut self, out: &mut Vec<Output>) -> bool {
|
||||
fn bump(&mut self, out: &mut Vec<Output<F>>) -> bool {
|
||||
let Some(i) = self.entries.iter().enumerate().filter(|(_, e)| e.needed_for_output).min_by_key(|(_, e)| e.poc).map(|(i, _)| i) else {
|
||||
return false;
|
||||
};
|
||||
@@ -85,13 +88,13 @@ impl Dpb {
|
||||
}
|
||||
|
||||
/// Output everything in POC order and clear non-reference entries.
|
||||
pub fn flush(&mut self, out: &mut Vec<Output>) {
|
||||
pub fn flush(&mut self, out: &mut Vec<Output<F>>) {
|
||||
while self.bump(out) {}
|
||||
self.prune();
|
||||
}
|
||||
|
||||
/// IDR: mark all references unused; output (or drop) prior pictures.
|
||||
pub fn idr(&mut self, no_output_of_prior_pics: bool, out: &mut Vec<Output>) {
|
||||
pub fn idr(&mut self, no_output_of_prior_pics: bool, out: &mut Vec<Output<F>>) {
|
||||
for e in &mut self.entries {
|
||||
e.mark = RefMark::Unused;
|
||||
}
|
||||
@@ -122,7 +125,8 @@ impl Dpb {
|
||||
self.prune();
|
||||
}
|
||||
|
||||
/// Insert "non-existing" frames for a frame_num gap (8.2.5.2).
|
||||
/// Insert "non-existing" frames for a frame_num gap (8.2.5.2). `make_frame` gives each one's
|
||||
/// picture and POC.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn fill_frame_num_gap(
|
||||
&mut self,
|
||||
@@ -130,17 +134,17 @@ impl Dpb {
|
||||
frame_num: u32,
|
||||
max_frame_num: u32,
|
||||
max_num_ref_frames: usize,
|
||||
make_frame: &mut dyn FnMut(u32) -> (FrameRef, Arc<OutputMeta>),
|
||||
make_frame: &mut dyn FnMut(u32) -> (F, i32, Arc<OutputMeta>),
|
||||
poc_state: &mut dyn FnMut(u32),
|
||||
) {
|
||||
let mut unused = (prev_ref_frame_num + 1) % max_frame_num;
|
||||
let mut guard = 0;
|
||||
while unused != frame_num && guard < max_frame_num {
|
||||
self.sliding_window(unused, max_frame_num, max_num_ref_frames);
|
||||
let (frame, meta) = make_frame(unused);
|
||||
let (frame, poc, meta) = make_frame(unused);
|
||||
poc_state(unused);
|
||||
self.entries.push(DpbEntry {
|
||||
poc: frame.poc,
|
||||
poc,
|
||||
frame,
|
||||
frame_num: unused,
|
||||
long_term_frame_idx: 0,
|
||||
@@ -154,26 +158,26 @@ impl Dpb {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initial reference picture lists (8.2.4.2) + modification (8.2.4.3) for a frame slice.
|
||||
pub fn build_ref_lists(&self, sh: &SliceHeader, cur_poc: i32, max_frame_num: u32) -> Result<[Vec<RefPic>; 2]> {
|
||||
let mut lists: [Vec<RefPic>; 2] = [Vec::new(), Vec::new()];
|
||||
/// Initial reference picture lists (8.2.4.2) + modification (8.2.4.3) for a frame slice, each
|
||||
/// entry made by `to_ref` from the DPB entry it refers to.
|
||||
pub fn build_ref_lists<R>(&self, sh: &SliceHeader, cur_poc: i32, max_frame_num: u32, to_ref: impl Fn(&DpbEntry<F>) -> R) -> Result<[Vec<R>; 2]> {
|
||||
let mut lists: [Vec<R>; 2] = [Vec::new(), Vec::new()];
|
||||
if sh.slice_type.is_intra() {
|
||||
return Ok(lists);
|
||||
}
|
||||
let cur_fn = sh.frame_num;
|
||||
let pic_num = |e: &DpbEntry| Self::frame_num_wrap(e, cur_fn, max_frame_num);
|
||||
let short: Vec<&DpbEntry> = self.entries.iter().filter(|e| e.mark == RefMark::Short).collect();
|
||||
let mut long: Vec<&DpbEntry> = self.entries.iter().filter(|e| e.mark == RefMark::Long).collect();
|
||||
let pic_num = |e: &DpbEntry<F>| Self::frame_num_wrap(e, cur_fn, max_frame_num);
|
||||
let short: Vec<&DpbEntry<F>> = self.entries.iter().filter(|e| e.mark == RefMark::Short).collect();
|
||||
let mut long: Vec<&DpbEntry<F>> = self.entries.iter().filter(|e| e.mark == RefMark::Long).collect();
|
||||
long.sort_by_key(|e| e.long_term_frame_idx);
|
||||
let to_ref = |e: &&DpbEntry| RefPic { frame: e.frame.clone(), long_term: e.mark == RefMark::Long };
|
||||
let mut entry_lists: [Vec<&DpbEntry>; 2] = [Vec::new(), Vec::new()];
|
||||
let mut entry_lists: [Vec<&DpbEntry<F>>; 2] = [Vec::new(), Vec::new()];
|
||||
if sh.slice_type != SliceType::B {
|
||||
let mut s = short.clone();
|
||||
s.sort_by_key(|e| std::cmp::Reverse(pic_num(e)));
|
||||
entry_lists[0] = s.into_iter().chain(long.iter().copied()).collect();
|
||||
} else {
|
||||
let mut before: Vec<&DpbEntry> = short.iter().copied().filter(|e| e.poc < cur_poc).collect();
|
||||
let mut after: Vec<&DpbEntry> = short.iter().copied().filter(|e| e.poc > cur_poc).collect();
|
||||
let mut before: Vec<&DpbEntry<F>> = short.iter().copied().filter(|e| e.poc < cur_poc).collect();
|
||||
let mut after: Vec<&DpbEntry<F>> = short.iter().copied().filter(|e| e.poc > cur_poc).collect();
|
||||
before.sort_by_key(|e| std::cmp::Reverse(e.poc));
|
||||
after.sort_by_key(|e| e.poc);
|
||||
entry_lists[0] = before.iter().chain(after.iter()).chain(long.iter()).copied().collect();
|
||||
@@ -188,7 +192,7 @@ impl Dpb {
|
||||
let nlists = if sh.slice_type == SliceType::B { 2 } else { 1 };
|
||||
for l in 0..nlists {
|
||||
let n = sh.num_ref_idx_active[l] as usize;
|
||||
let mut list: Vec<Option<&DpbEntry>> = entry_lists[l].iter().copied().map(Some).collect();
|
||||
let mut list: Vec<Option<&DpbEntry<F>>> = entry_lists[l].iter().copied().map(Some).collect();
|
||||
list.truncate(n);
|
||||
list.resize(n, None);
|
||||
if !sh.ref_pic_list_mod[l].is_empty() {
|
||||
@@ -199,7 +203,7 @@ impl Dpb {
|
||||
for m in &sh.ref_pic_list_mod[l] {
|
||||
ensure!(ref_idx < n, "too many ref_pic_list_modification entries");
|
||||
list.push(None); // temporarily one longer
|
||||
let (target, is_long): (Option<&DpbEntry>, bool) = match m.idc {
|
||||
let (target, is_long): (Option<&DpbEntry<F>>, bool) = match m.idc {
|
||||
0 | 1 => {
|
||||
let d = m.value as i32 + 1;
|
||||
let no_wrap = if m.idc == 0 {
|
||||
@@ -249,7 +253,7 @@ impl Dpb {
|
||||
}
|
||||
// Fill missing entries (should not happen in conforming streams) with the first valid entry.
|
||||
let fallback = list.iter().flatten().next().copied().or_else(|| self.entries.last());
|
||||
let out: Vec<RefPic> = list.iter().filter_map(|e| e.or(fallback)).map(|e| to_ref(&e)).collect();
|
||||
let out: Vec<R> = list.iter().filter_map(|e| e.or(fallback)).map(&to_ref).collect();
|
||||
lists[l] = out;
|
||||
}
|
||||
Ok(lists)
|
||||
@@ -327,12 +331,12 @@ impl Dpb {
|
||||
pub fn store_picture(
|
||||
&mut self,
|
||||
sh: &SliceHeader,
|
||||
frame: FrameRef,
|
||||
frame: F,
|
||||
poc: i32,
|
||||
max_frame_num: u32,
|
||||
max_num_ref_frames: usize,
|
||||
meta: Arc<OutputMeta>,
|
||||
out: &mut Vec<Output>,
|
||||
out: &mut Vec<Output<F>>,
|
||||
) {
|
||||
let mut mark = RefMark::Unused;
|
||||
let mut long_idx = 0;
|
||||
@@ -404,7 +408,7 @@ impl Dpb {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Dpb {
|
||||
impl<F: Clone> Default for Dpb<F> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ mod cavlc;
|
||||
mod cavlc_tables;
|
||||
mod deblock;
|
||||
mod decoder;
|
||||
mod dpb;
|
||||
pub mod dpb;
|
||||
mod error;
|
||||
mod inter;
|
||||
mod intra;
|
||||
|
||||
@@ -387,6 +387,27 @@ pub struct PocState {
|
||||
prev_frame_num: u32,
|
||||
}
|
||||
|
||||
/// Is `sh` the first slice of a new primary coded picture relative to `prev` (7.4.1.2.4)?
|
||||
pub fn is_new_picture(prev: &SliceHeader, sh: &SliceHeader, sps: &Sps) -> bool {
|
||||
if sh.frame_num != prev.frame_num
|
||||
|| sh.pps_id != prev.pps_id
|
||||
|| sh.field_pic != prev.field_pic
|
||||
|| sh.bottom_field != prev.bottom_field
|
||||
|| (sh.nal_ref_idc == 0) != (prev.nal_ref_idc == 0)
|
||||
|| sh.idr != prev.idr
|
||||
|| (sh.idr && prev.idr && sh.idr_pic_id != prev.idr_pic_id)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if sps.pic_order_cnt_type == 0 && (sh.pic_order_cnt_lsb != prev.pic_order_cnt_lsb || sh.delta_pic_order_cnt_bottom != prev.delta_pic_order_cnt_bottom) {
|
||||
return true;
|
||||
}
|
||||
if sps.pic_order_cnt_type == 1 && sh.delta_pic_order_cnt != prev.delta_pic_order_cnt {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Output of POC computation.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Poc {
|
||||
|
||||
@@ -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<FrameRef>,
|
||||
poc: i32,
|
||||
output: bool,
|
||||
meta: Arc<OutputMeta>,
|
||||
@@ -72,7 +72,7 @@ pub struct Decoder {
|
||||
spss: Vec<Option<Arc<Sps>>>,
|
||||
ppss: Vec<Option<Arc<Pps>>>,
|
||||
nal_length_size: Option<usize>,
|
||||
dpb: Dpb,
|
||||
dpb: Dpb<FrameRef>,
|
||||
poc_state: PocState,
|
||||
pending: Option<PendingPic>,
|
||||
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<Pps>, Arc<Sps>, Arc<Layout>, Option<Arc<Vec<Vec<u8>>>>)>,
|
||||
out_queue: VecDeque<Output>,
|
||||
out_queue: VecDeque<Output<FrameRef>>,
|
||||
shared: Arc<Shared>,
|
||||
#[cfg(feature = "threads")]
|
||||
pool: Option<rayon::ThreadPool>,
|
||||
@@ -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<FrameRef>) -> Picture {
|
||||
let (cx, cy, cw, ch) = o.meta.crop;
|
||||
let (cx, cy, cw, ch) = (cx as usize, cy as usize, cw as usize, ch as usize);
|
||||
let (y, u, v) = o.frame.copy_cropped((cx, cy, cw, ch));
|
||||
|
||||
+47
-26
@@ -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<F> {
|
||||
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<F> {
|
||||
pub frame: F,
|
||||
pub poc: i32,
|
||||
pub meta: Arc<OutputMeta>,
|
||||
}
|
||||
|
||||
/// 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<RefPic>,
|
||||
pub st_curr_after: Vec<RefPic>,
|
||||
pub lt_curr: Vec<RefPic>,
|
||||
/// A reference picture of the current picture.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Ref<F> {
|
||||
pub frame: F,
|
||||
pub poc: i32,
|
||||
pub long_term: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Dpb {
|
||||
pub entries: Vec<Entry>,
|
||||
/// Reference picture set of the current picture (8.3.2) as DPB indices / generated frames.
|
||||
pub struct RefPicSet<F> {
|
||||
pub st_curr_before: Vec<Ref<F>>,
|
||||
pub st_curr_after: Vec<Ref<F>>,
|
||||
pub lt_curr: Vec<Ref<F>>,
|
||||
}
|
||||
|
||||
impl<F> Default for RefPicSet<F> {
|
||||
fn default() -> Self {
|
||||
Self { st_curr_before: Vec::new(), st_curr_after: Vec::new(), lt_curr: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Dpb<F> {
|
||||
pub entries: Vec<Entry<F>>,
|
||||
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<Output>) -> bool {
|
||||
impl<F> Default for Dpb<F> {
|
||||
fn default() -> Self {
|
||||
Self { entries: Vec::new(), max_dec_pic_buffering: 0, max_num_reorder: 0, max_latency: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Clone> Dpb<F> {
|
||||
fn bump(&mut self, outs: &mut Vec<Output<F>>) -> 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<Output>) {
|
||||
pub fn flush(&mut self, outs: &mut Vec<Output<F>>) {
|
||||
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<Output>) {
|
||||
pub fn bump_before_decode(&mut self, outs: &mut Vec<Output<F>>) {
|
||||
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<OutputMeta>, outs: &mut Vec<Output>) {
|
||||
pub fn insert(&mut self, frame: F, poc: i32, output: bool, meta: Arc<OutputMeta>, outs: &mut Vec<Output<F>>) {
|
||||
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<RefPicSet> {
|
||||
make_missing: &mut dyn FnMut(i32) -> F,
|
||||
) -> Result<RefPicSet<F>> {
|
||||
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<usize>, p: i32, lt: bool, entries: &mut Vec<Entry>| -> RefPic {
|
||||
let mut make = |found: Option<usize>, p: i32, lt: bool, entries: &mut Vec<Entry<F>>| -> Ref<F> {
|
||||
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<RefPic>; 2]> {
|
||||
let mut lists: [Vec<RefPic>; 2] = [Vec::new(), Vec::new()];
|
||||
pub fn build_ref_lists<F: Clone>(sh: &SliceHeader, rps: &RefPicSet<F>) -> Result<[Vec<Ref<F>>; 2]> {
|
||||
let mut lists: [Vec<Ref<F>>; 2] = [Vec::new(), Vec::new()];
|
||||
if sh.is_intra() {
|
||||
return Ok(lists);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
mod cabac;
|
||||
mod decoder;
|
||||
mod dpb;
|
||||
pub mod dpb;
|
||||
mod error;
|
||||
mod filter;
|
||||
mod inter;
|
||||
|
||||
@@ -207,9 +207,4 @@ fn copy_line(line: &[u16], x0: i32, out: &mut [i16]) {
|
||||
pub type FrameRef = Arc<Frame>;
|
||||
|
||||
/// 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<FrameRef>;
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "filmcraft-platform"
|
||||
description = "OS media integration: hardware video decoding through the operating system's codecs (VideoToolbox on macOS), with transparent fallback to the pure-Rust decoders"
|
||||
description = "OS media integration: hardware video decoding through the operating system's codecs (VideoToolbox on macOS, Media Foundation on Windows, VA-API on Linux), with transparent fallback to the pure-Rust decoders"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -21,10 +21,13 @@ type_complexity = "allow"
|
||||
undocumented_unsafe_blocks = "deny"
|
||||
|
||||
[dependencies]
|
||||
filmcraft-bitstream = { workspace = true }
|
||||
filmcraft-codecs = { workspace = true }
|
||||
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 }
|
||||
@@ -49,6 +52,11 @@ windows = { version = "0.62", features = [
|
||||
"Win32_System_LibraryLoader",
|
||||
] }
|
||||
|
||||
# Linux: libva (VA-API) is loaded at run time with libloading (ISC; already in the lockfile through
|
||||
# wgpu), so building needs no libva headers and a system without libva simply decodes in software.
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libloading = "0.8"
|
||||
|
||||
[dev-dependencies]
|
||||
filmcraft-testkit = { workspace = true }
|
||||
filmcraft-media = { workspace = true }
|
||||
|
||||
@@ -8,7 +8,7 @@ It is the one crate of the workspace allowed to contain `unsafe`, under the rule
|
||||
|
||||
```rust
|
||||
// at startup (the desktop app, filmcraft-cli, the bench)
|
||||
let availability = filmcraft_platform::register(); // Available("VideoToolbox") on macOS, Available("Media Foundation") on Windows
|
||||
let availability = filmcraft_platform::register(); // Available("VideoToolbox") on macOS, Available("Media Foundation") on Windows, Available("VA-API") on Linux with a driver
|
||||
```
|
||||
|
||||
## What it does
|
||||
@@ -70,6 +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`) 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`, `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).
|
||||
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): 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
|
||||
@@ -140,13 +171,14 @@ level and flags are the encoder's own. If it is missing the export stops with th
|
||||
- **Never undecodable:** the factory declines (returns `None`, so the software decoder is used)
|
||||
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) and when the OS cannot
|
||||
create a hardware session (VideoToolbox) or a GPU-backed decoder (Media Foundation).
|
||||
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 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`,
|
||||
`video::vui_color`, `sar_par`).
|
||||
- **Never crash:** no `unwrap` / `expect` / `panic!` outside tests; the output callback runs under
|
||||
`catch_unwind`; every `unsafe` block has a `// SAFETY:` comment; the public API is safe.
|
||||
- **Never crash:** no `unwrap` / `expect` / `panic!` outside tests; the output callback (and
|
||||
libva's error-message callback) runs under `catch_unwind`; every `unsafe` block has a `// SAFETY:` comment; the public API is safe.
|
||||
- **Counted:** `perf.stats` `decode.hardware` (frames, software frames, sessions, declined,
|
||||
fallbacks; `filmcraft_codecs::hw::hw_stats`) and `backend` (the registered backend's name,
|
||||
`filmcraft_codecs::hw::hw_backend`). `export.hardware` counts the NVENC encoder's frames, sessions and declines.
|
||||
@@ -162,6 +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) | 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.
|
||||
@@ -185,9 +220,14 @@ M4 Pro, load 150–190 (`cargo xtask bench --hw off|auto`): CPU per decoded fram
|
||||
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; 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; VA-API (Linux)
|
||||
decoders; field-coded H.264; HEVC, 10-bit and HDR encoding with NVENC, and encoders from other
|
||||
textures on Windows); B-frames and 10-bit / HDR HEVC (Main 10) in hardware encoding; HEVC, VP9 and
|
||||
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.
|
||||
@@ -3,8 +3,9 @@
|
||||
//!
|
||||
//! [`register`] puts the platform's hardware decoder factory in front of FilmCraft's own decoders
|
||||
//! (`filmcraft_codecs::register_video_decoder`). Today that is VideoToolbox on macOS for H.264
|
||||
//! (`avcC`) and HEVC (`hvcC`) streams, 8- and 10-bit, 4:2:0 and 4:2:2; on other systems
|
||||
//! registration does nothing and reports [`Availability::Unavailable`]. It also registers a
|
||||
//! (`avcC`) and HEVC (`hvcC`) streams, 8- and 10-bit, 4:2:0 and 4:2:2; Media Foundation / DXVA on
|
||||
//! Windows; VA-API on Linux for H.264 ([`vaapi`]); on other systems (and on Linux without a VA-API
|
||||
//! driver) registration does nothing and reports [`Availability::Unavailable`]. It also registers a
|
||||
//! hardware H.264 encoder factory (`filmcraft_export::register_encoder`) that only acts when an
|
||||
//! export asks for it (`ExportSettings::hardware_encoding` = `Auto`), see [`hardware_encode`].
|
||||
//!
|
||||
@@ -25,10 +26,11 @@
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
// Used by the Windows decoder only; compiled everywhere so their tests run on every system.
|
||||
// Used by the Windows (and, for `biplanar`, Linux) decoders; compiled everywhere so their tests run
|
||||
// on every system.
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
mod annexb;
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "linux")), allow(dead_code))]
|
||||
mod biplanar;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod hardware_encode;
|
||||
@@ -37,6 +39,8 @@ pub mod hybrid;
|
||||
pub mod media_foundation;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod nvenc;
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
pub mod vaapi;
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(unsafe_code)]
|
||||
pub mod videotoolbox;
|
||||
@@ -77,7 +81,23 @@ pub fn register() -> Availability {
|
||||
filmcraft_codecs::hw::set_hw_backend("Media Foundation");
|
||||
Availability::Available("Media Foundation")
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
match vaapi::va::probe() {
|
||||
Ok(driver) => {
|
||||
static ONCE: std::sync::Once = std::sync::Once::new();
|
||||
ONCE.call_once(|| log::info!("hardware decoding through VA-API: {driver}"));
|
||||
filmcraft_codecs::register_video_decoder(vaapi_factory);
|
||||
filmcraft_codecs::hw::set_hw_backend("VA-API");
|
||||
Availability::Available("VA-API")
|
||||
}
|
||||
Err(why) => {
|
||||
log::info!("no VA-API hardware decoding: {why}");
|
||||
Availability::Unavailable("no VA-API driver (libva and a DRM render node)")
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
Availability::Unavailable("no hardware video decoder for this system yet")
|
||||
}
|
||||
@@ -93,7 +113,11 @@ pub fn registered() -> bool {
|
||||
{
|
||||
filmcraft_codecs::video_decoder_registered(media_foundation_factory)
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
filmcraft_codecs::video_decoder_registered(vaapi_factory)
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
false
|
||||
}
|
||||
@@ -110,7 +134,11 @@ pub fn hardware_decoder_for(entry: &filmcraft_isobmff::SampleEntry) -> bool {
|
||||
{
|
||||
media_foundation::stream_info(entry).is_some_and(|info| media_foundation::MfDecoder::new(info).is_ok())
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
filmcraft_codecs::hw::NalStreamInfo::from_entry(entry).and_then(|r| r.ok()).is_some_and(|info| vaapi::VaDecoder::new(info).is_ok())
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
let _ = entry;
|
||||
false
|
||||
@@ -153,3 +181,22 @@ pub fn media_foundation_factory(entry: &filmcraft_isobmff::SampleEntry) -> Optio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<filmcraft_codecs::Result<Box<dyn filmcraft_codecs::VideoDecoder>>> {
|
||||
if !filmcraft_codecs::hw::hardware_decoding() {
|
||||
return None;
|
||||
}
|
||||
let info = filmcraft_codecs::hw::NalStreamInfo::from_entry(entry)?.ok()?;
|
||||
match vaapi::VaDecoder::new(info.clone()) {
|
||||
Ok(va) => Some(Ok(Box::new(HybridDecoder::new(Box::new(va), entry.clone(), info)))),
|
||||
Err(why) => {
|
||||
log::info!("hardware decoding declined for {} video: {why}", entry.codec.name());
|
||||
filmcraft_codecs::hw::note_hw_declined();
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
//! 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` /
|
||||
//! `offsetof` of the structures of `src/vaapi/ffi.rs`, the constants it uses, and the `value` word of
|
||||
//! each `seq_fields` / `pic_fields` bit-field set alone, against libva's MIT-licensed headers.
|
||||
#![allow(clippy::unreadable_literal)]
|
||||
|
||||
use super::ffi::*;
|
||||
use std::mem::{align_of, offset_of, size_of};
|
||||
|
||||
#[test]
|
||||
fn layouts_match_the_header() {
|
||||
assert_eq!((size_of::<VAConfigAttrib>(), align_of::<VAConfigAttrib>()), (8, 4), "VAConfigAttrib");
|
||||
assert_eq!(offset_of!(VAConfigAttrib, type_), 0, "VAConfigAttrib.type");
|
||||
assert_eq!(offset_of!(VAConfigAttrib, value), 4, "VAConfigAttrib.value");
|
||||
assert_eq!((size_of::<VAPictureH264>(), align_of::<VAPictureH264>()), (36, 4), "VAPictureH264");
|
||||
assert_eq!(offset_of!(VAPictureH264, picture_id), 0, "VAPictureH264.picture_id");
|
||||
assert_eq!(offset_of!(VAPictureH264, frame_idx), 4, "VAPictureH264.frame_idx");
|
||||
assert_eq!(offset_of!(VAPictureH264, flags), 8, "VAPictureH264.flags");
|
||||
assert_eq!(offset_of!(VAPictureH264, TopFieldOrderCnt), 12, "VAPictureH264.TopFieldOrderCnt");
|
||||
assert_eq!(offset_of!(VAPictureH264, BottomFieldOrderCnt), 16, "VAPictureH264.BottomFieldOrderCnt");
|
||||
assert_eq!(offset_of!(VAPictureH264, va_reserved), 20, "VAPictureH264.va_reserved");
|
||||
assert_eq!((size_of::<VAPictureParameterBufferH264>(), align_of::<VAPictureParameterBufferH264>()), (672, 4), "VAPictureParameterBufferH264");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, CurrPic), 0, "VAPictureParameterBufferH264.CurrPic");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, ReferenceFrames), 36, "VAPictureParameterBufferH264.ReferenceFrames");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, picture_width_in_mbs_minus1), 612, "VAPictureParameterBufferH264.picture_width_in_mbs_minus1");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, picture_height_in_mbs_minus1), 614, "VAPictureParameterBufferH264.picture_height_in_mbs_minus1");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, bit_depth_luma_minus8), 616, "VAPictureParameterBufferH264.bit_depth_luma_minus8");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, bit_depth_chroma_minus8), 617, "VAPictureParameterBufferH264.bit_depth_chroma_minus8");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, num_ref_frames), 618, "VAPictureParameterBufferH264.num_ref_frames");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, seq_fields), 620, "VAPictureParameterBufferH264.seq_fields");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, num_slice_groups_minus1), 624, "VAPictureParameterBufferH264.num_slice_groups_minus1");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, slice_group_map_type), 625, "VAPictureParameterBufferH264.slice_group_map_type");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, slice_group_change_rate_minus1), 626, "VAPictureParameterBufferH264.slice_group_change_rate_minus1");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, pic_init_qp_minus26), 628, "VAPictureParameterBufferH264.pic_init_qp_minus26");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, pic_init_qs_minus26), 629, "VAPictureParameterBufferH264.pic_init_qs_minus26");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, chroma_qp_index_offset), 630, "VAPictureParameterBufferH264.chroma_qp_index_offset");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, second_chroma_qp_index_offset), 631, "VAPictureParameterBufferH264.second_chroma_qp_index_offset");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, pic_fields), 632, "VAPictureParameterBufferH264.pic_fields");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, frame_num), 636, "VAPictureParameterBufferH264.frame_num");
|
||||
assert_eq!(offset_of!(VAPictureParameterBufferH264, va_reserved), 640, "VAPictureParameterBufferH264.va_reserved");
|
||||
assert_eq!((size_of::<VAIQMatrixBufferH264>(), align_of::<VAIQMatrixBufferH264>()), (240, 4), "VAIQMatrixBufferH264");
|
||||
assert_eq!(offset_of!(VAIQMatrixBufferH264, ScalingList4x4), 0, "VAIQMatrixBufferH264.ScalingList4x4");
|
||||
assert_eq!(offset_of!(VAIQMatrixBufferH264, ScalingList8x8), 96, "VAIQMatrixBufferH264.ScalingList8x8");
|
||||
assert_eq!(offset_of!(VAIQMatrixBufferH264, va_reserved), 224, "VAIQMatrixBufferH264.va_reserved");
|
||||
assert_eq!((size_of::<VASliceParameterBufferH264>(), align_of::<VASliceParameterBufferH264>()), (3128, 4), "VASliceParameterBufferH264");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, slice_data_size), 0, "VASliceParameterBufferH264.slice_data_size");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, slice_data_offset), 4, "VASliceParameterBufferH264.slice_data_offset");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, slice_data_flag), 8, "VASliceParameterBufferH264.slice_data_flag");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, slice_data_bit_offset), 12, "VASliceParameterBufferH264.slice_data_bit_offset");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, first_mb_in_slice), 14, "VASliceParameterBufferH264.first_mb_in_slice");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, slice_type), 16, "VASliceParameterBufferH264.slice_type");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, direct_spatial_mv_pred_flag), 17, "VASliceParameterBufferH264.direct_spatial_mv_pred_flag");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, num_ref_idx_l0_active_minus1), 18, "VASliceParameterBufferH264.num_ref_idx_l0_active_minus1");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, num_ref_idx_l1_active_minus1), 19, "VASliceParameterBufferH264.num_ref_idx_l1_active_minus1");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, cabac_init_idc), 20, "VASliceParameterBufferH264.cabac_init_idc");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, slice_qp_delta), 21, "VASliceParameterBufferH264.slice_qp_delta");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, disable_deblocking_filter_idc), 22, "VASliceParameterBufferH264.disable_deblocking_filter_idc");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, slice_alpha_c0_offset_div2), 23, "VASliceParameterBufferH264.slice_alpha_c0_offset_div2");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, slice_beta_offset_div2), 24, "VASliceParameterBufferH264.slice_beta_offset_div2");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, RefPicList0), 28, "VASliceParameterBufferH264.RefPicList0");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, RefPicList1), 1180, "VASliceParameterBufferH264.RefPicList1");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, luma_log2_weight_denom), 2332, "VASliceParameterBufferH264.luma_log2_weight_denom");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, chroma_log2_weight_denom), 2333, "VASliceParameterBufferH264.chroma_log2_weight_denom");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, luma_weight_l0_flag), 2334, "VASliceParameterBufferH264.luma_weight_l0_flag");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, luma_weight_l0), 2336, "VASliceParameterBufferH264.luma_weight_l0");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, luma_offset_l0), 2400, "VASliceParameterBufferH264.luma_offset_l0");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, chroma_weight_l0_flag), 2464, "VASliceParameterBufferH264.chroma_weight_l0_flag");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, chroma_weight_l0), 2466, "VASliceParameterBufferH264.chroma_weight_l0");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, chroma_offset_l0), 2594, "VASliceParameterBufferH264.chroma_offset_l0");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, luma_weight_l1_flag), 2722, "VASliceParameterBufferH264.luma_weight_l1_flag");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, luma_weight_l1), 2724, "VASliceParameterBufferH264.luma_weight_l1");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, luma_offset_l1), 2788, "VASliceParameterBufferH264.luma_offset_l1");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, chroma_weight_l1_flag), 2852, "VASliceParameterBufferH264.chroma_weight_l1_flag");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, chroma_weight_l1), 2854, "VASliceParameterBufferH264.chroma_weight_l1");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, chroma_offset_l1), 2982, "VASliceParameterBufferH264.chroma_offset_l1");
|
||||
assert_eq!(offset_of!(VASliceParameterBufferH264, va_reserved), 3112, "VASliceParameterBufferH264.va_reserved");
|
||||
assert_eq!((size_of::<VAImageFormat>(), align_of::<VAImageFormat>()), (48, 4), "VAImageFormat");
|
||||
assert_eq!(offset_of!(VAImageFormat, fourcc), 0, "VAImageFormat.fourcc");
|
||||
assert_eq!(offset_of!(VAImageFormat, byte_order), 4, "VAImageFormat.byte_order");
|
||||
assert_eq!(offset_of!(VAImageFormat, bits_per_pixel), 8, "VAImageFormat.bits_per_pixel");
|
||||
assert_eq!(offset_of!(VAImageFormat, depth), 12, "VAImageFormat.depth");
|
||||
assert_eq!(offset_of!(VAImageFormat, red_mask), 16, "VAImageFormat.red_mask");
|
||||
assert_eq!(offset_of!(VAImageFormat, green_mask), 20, "VAImageFormat.green_mask");
|
||||
assert_eq!(offset_of!(VAImageFormat, blue_mask), 24, "VAImageFormat.blue_mask");
|
||||
assert_eq!(offset_of!(VAImageFormat, alpha_mask), 28, "VAImageFormat.alpha_mask");
|
||||
assert_eq!(offset_of!(VAImageFormat, va_reserved), 32, "VAImageFormat.va_reserved");
|
||||
assert_eq!((size_of::<VAImage>(), align_of::<VAImage>()), (120, 4), "VAImage");
|
||||
assert_eq!(offset_of!(VAImage, image_id), 0, "VAImage.image_id");
|
||||
assert_eq!(offset_of!(VAImage, format), 4, "VAImage.format");
|
||||
assert_eq!(offset_of!(VAImage, buf), 52, "VAImage.buf");
|
||||
assert_eq!(offset_of!(VAImage, width), 56, "VAImage.width");
|
||||
assert_eq!(offset_of!(VAImage, height), 58, "VAImage.height");
|
||||
assert_eq!(offset_of!(VAImage, data_size), 60, "VAImage.data_size");
|
||||
assert_eq!(offset_of!(VAImage, num_planes), 64, "VAImage.num_planes");
|
||||
assert_eq!(offset_of!(VAImage, pitches), 68, "VAImage.pitches");
|
||||
assert_eq!(offset_of!(VAImage, offsets), 80, "VAImage.offsets");
|
||||
assert_eq!(offset_of!(VAImage, num_palette_entries), 92, "VAImage.num_palette_entries");
|
||||
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::<VAPictureHEVC>(), align_of::<VAPictureHEVC>()), (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::<VAPictureParameterBufferHEVC>(), align_of::<VAPictureParameterBufferHEVC>()), (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::<VASliceParameterBufferHEVC>(), align_of::<VASliceParameterBufferHEVC>()), (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::<VAIQMatrixBufferHEVC>(), align_of::<VAIQMatrixBufferHEVC>()), (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]
|
||||
fn constants_match_the_header() {
|
||||
assert_eq!(VA_STATUS_SUCCESS as i64, 0, "VA_STATUS_SUCCESS");
|
||||
assert_eq!(VA_STATUS_ERROR_UNSUPPORTED_PROFILE as i64, 12, "VA_STATUS_ERROR_UNSUPPORTED_PROFILE");
|
||||
assert_eq!(VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT as i64, 13, "VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT");
|
||||
assert_eq!(VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT as i64, 14, "VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT");
|
||||
assert_eq!(VA_STATUS_ERROR_RESOLUTION_NOT_SUPPORTED as i64, 19, "VA_STATUS_ERROR_RESOLUTION_NOT_SUPPORTED");
|
||||
assert_eq!(VA_STATUS_ERROR_DECODING_ERROR as i64, 23, "VA_STATUS_ERROR_DECODING_ERROR");
|
||||
assert_eq!(VA_INVALID_ID as i64, 4294967295, "VA_INVALID_ID");
|
||||
assert_eq!(VA_INVALID_SURFACE as i64, 4294967295, "VA_INVALID_SURFACE");
|
||||
assert_eq!(VAProfileH264Main as i64, 6, "VAProfileH264Main");
|
||||
assert_eq!(VAProfileH264High as i64, 7, "VAProfileH264High");
|
||||
assert_eq!(VAProfileH264ConstrainedBaseline as i64, 13, "VAProfileH264ConstrainedBaseline");
|
||||
assert_eq!(VAEntrypointVLD as i64, 1, "VAEntrypointVLD");
|
||||
assert_eq!(VAConfigAttribRTFormat as i64, 0, "VAConfigAttribRTFormat");
|
||||
assert_eq!(VA_RT_FORMAT_YUV420 as i64, 1, "VA_RT_FORMAT_YUV420");
|
||||
assert_eq!(VA_PROGRESSIVE as i64, 1, "VA_PROGRESSIVE");
|
||||
assert_eq!(VAPictureParameterBufferType as i64, 0, "VAPictureParameterBufferType");
|
||||
assert_eq!(VAIQMatrixBufferType as i64, 1, "VAIQMatrixBufferType");
|
||||
assert_eq!(VASliceParameterBufferType as i64, 4, "VASliceParameterBufferType");
|
||||
assert_eq!(VASliceDataBufferType as i64, 5, "VASliceDataBufferType");
|
||||
assert_eq!(VA_SLICE_DATA_FLAG_ALL as i64, 0, "VA_SLICE_DATA_FLAG_ALL");
|
||||
assert_eq!(VA_PICTURE_H264_INVALID as i64, 1, "VA_PICTURE_H264_INVALID");
|
||||
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");
|
||||
}
|
||||
|
||||
/// Each bit-field set alone to its largest value: the word the C compiler makes of it.
|
||||
#[test]
|
||||
fn bit_fields_match_the_header() {
|
||||
assert_eq!(3u32 << seq_bits::CHROMA_FORMAT_IDC, 0x3, "seq_fields.chroma_format_idc");
|
||||
assert_eq!(1u32 << seq_bits::RESIDUAL_COLOUR_TRANSFORM, 0x4, "seq_fields.residual_colour_transform_flag");
|
||||
assert_eq!(1u32 << seq_bits::GAPS_IN_FRAME_NUM_ALLOWED, 0x8, "seq_fields.gaps_in_frame_num_value_allowed_flag");
|
||||
assert_eq!(1u32 << seq_bits::FRAME_MBS_ONLY, 0x10, "seq_fields.frame_mbs_only_flag");
|
||||
assert_eq!(1u32 << seq_bits::MB_ADAPTIVE_FRAME_FIELD, 0x20, "seq_fields.mb_adaptive_frame_field_flag");
|
||||
assert_eq!(1u32 << seq_bits::DIRECT_8X8_INFERENCE, 0x40, "seq_fields.direct_8x8_inference_flag");
|
||||
assert_eq!(1u32 << seq_bits::MIN_LUMA_BI_PRED_SIZE_8X8, 0x80, "seq_fields.MinLumaBiPredSize8x8");
|
||||
assert_eq!(15u32 << seq_bits::LOG2_MAX_FRAME_NUM_MINUS4, 0xf00, "seq_fields.log2_max_frame_num_minus4");
|
||||
assert_eq!(3u32 << seq_bits::PIC_ORDER_CNT_TYPE, 0x3000, "seq_fields.pic_order_cnt_type");
|
||||
assert_eq!(15u32 << seq_bits::LOG2_MAX_PIC_ORDER_CNT_LSB_MINUS4, 0x3c000, "seq_fields.log2_max_pic_order_cnt_lsb_minus4");
|
||||
assert_eq!(1u32 << seq_bits::DELTA_PIC_ORDER_ALWAYS_ZERO, 0x40000, "seq_fields.delta_pic_order_always_zero_flag");
|
||||
assert_eq!(1u32 << pic_bits::ENTROPY_CODING_MODE, 0x1, "pic_fields.entropy_coding_mode_flag");
|
||||
assert_eq!(1u32 << pic_bits::WEIGHTED_PRED, 0x2, "pic_fields.weighted_pred_flag");
|
||||
assert_eq!(3u32 << pic_bits::WEIGHTED_BIPRED_IDC, 0xc, "pic_fields.weighted_bipred_idc");
|
||||
assert_eq!(1u32 << pic_bits::TRANSFORM_8X8_MODE, 0x10, "pic_fields.transform_8x8_mode_flag");
|
||||
assert_eq!(1u32 << pic_bits::FIELD_PIC, 0x20, "pic_fields.field_pic_flag");
|
||||
assert_eq!(1u32 << pic_bits::CONSTRAINED_INTRA_PRED, 0x40, "pic_fields.constrained_intra_pred_flag");
|
||||
assert_eq!(1u32 << pic_bits::PIC_ORDER_PRESENT, 0x80, "pic_fields.pic_order_present_flag");
|
||||
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");
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
// 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.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sub license, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject to
|
||||
// the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice (including the
|
||||
// next paragraph) shall be included in all copies or substantial portions
|
||||
// of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
||||
// IN NO EVENT SHALL INTEL AND/OR ITS SUPPLIERS BE LIABLE FOR
|
||||
// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// 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_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`.
|
||||
//!
|
||||
//! The structures are plain data and compile on every target, so the code that fills them in
|
||||
//! (`vaapi::h264`) is tested everywhere; only `vaapi::va` loads the library (Linux).
|
||||
//!
|
||||
//! FFI module (docs/adr/0001-platform-ffi.md): plain `repr(C)` data, no logic.
|
||||
|
||||
#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals, dead_code)]
|
||||
|
||||
use std::ffi::{c_char, c_int, c_uint, c_void};
|
||||
|
||||
pub type VADisplay = *mut c_void;
|
||||
pub type VAStatus = c_int;
|
||||
pub type VAGenericID = c_uint;
|
||||
pub type VAConfigID = VAGenericID;
|
||||
pub type VAContextID = VAGenericID;
|
||||
pub type VASurfaceID = VAGenericID;
|
||||
pub type VABufferID = VAGenericID;
|
||||
pub type VAImageID = VAGenericID;
|
||||
/// `VAProfile` (a C enum: `int`).
|
||||
pub type VAProfile = c_int;
|
||||
/// `VAEntrypoint` (a C enum: `int`).
|
||||
pub type VAEntrypoint = c_int;
|
||||
/// `VABufferType` (a C enum: `int`).
|
||||
pub type VABufferType = c_int;
|
||||
/// `VAConfigAttribType` (a C enum: `int`).
|
||||
pub type VAConfigAttribType = c_int;
|
||||
pub type VAMessageCallback = Option<unsafe extern "C" fn(user_context: *mut c_void, message: *const c_char)>;
|
||||
|
||||
pub const VA_STATUS_SUCCESS: VAStatus = 0;
|
||||
pub const VA_STATUS_ERROR_UNSUPPORTED_PROFILE: VAStatus = 0x0c;
|
||||
pub const VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT: VAStatus = 0x0d;
|
||||
pub const VA_STATUS_ERROR_UNSUPPORTED_RT_FORMAT: VAStatus = 0x0e;
|
||||
pub const VA_STATUS_ERROR_RESOLUTION_NOT_SUPPORTED: VAStatus = 0x13;
|
||||
pub const VA_STATUS_ERROR_DECODING_ERROR: VAStatus = 0x17;
|
||||
|
||||
pub const VA_INVALID_ID: VAGenericID = 0xffff_ffff;
|
||||
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;
|
||||
|
||||
pub const VAPictureParameterBufferType: VABufferType = 0;
|
||||
pub const VAIQMatrixBufferType: VABufferType = 1;
|
||||
pub const VASliceParameterBufferType: VABufferType = 4;
|
||||
pub const VASliceDataBufferType: VABufferType = 5;
|
||||
|
||||
/// `VASliceParameterBuffer*::slice_data_flag`: the whole slice is in the buffer.
|
||||
pub const VA_SLICE_DATA_FLAG_ALL: u32 = 0x00;
|
||||
|
||||
pub const VA_PICTURE_H264_INVALID: u32 = 0x0000_0001;
|
||||
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;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct VAConfigAttrib {
|
||||
pub type_: VAConfigAttribType,
|
||||
pub value: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct VAPictureH264 {
|
||||
pub picture_id: VASurfaceID,
|
||||
pub frame_idx: u32,
|
||||
pub flags: u32,
|
||||
pub TopFieldOrderCnt: i32,
|
||||
pub BottomFieldOrderCnt: i32,
|
||||
pub va_reserved: [u32; VA_PADDING_LOW],
|
||||
}
|
||||
|
||||
impl VAPictureH264 {
|
||||
/// An unused entry (`VA_INVALID_SURFACE`, `VA_PICTURE_H264_INVALID`).
|
||||
pub const INVALID: Self = Self {
|
||||
picture_id: VA_INVALID_SURFACE,
|
||||
frame_idx: 0,
|
||||
flags: VA_PICTURE_H264_INVALID,
|
||||
TopFieldOrderCnt: 0,
|
||||
BottomFieldOrderCnt: 0,
|
||||
va_reserved: [0; VA_PADDING_LOW],
|
||||
};
|
||||
}
|
||||
|
||||
/// `VAPictureParameterBufferH264`. `seq_fields` and `pic_fields` are C bit-field unions, stored
|
||||
/// here as their `value` word (bits allocated from the least significant end, as GCC and Clang do
|
||||
/// on the little-endian targets libva runs on).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct VAPictureParameterBufferH264 {
|
||||
pub CurrPic: VAPictureH264,
|
||||
pub ReferenceFrames: [VAPictureH264; 16],
|
||||
pub picture_width_in_mbs_minus1: u16,
|
||||
pub picture_height_in_mbs_minus1: u16,
|
||||
pub bit_depth_luma_minus8: u8,
|
||||
pub bit_depth_chroma_minus8: u8,
|
||||
pub num_ref_frames: u8,
|
||||
pub seq_fields: u32,
|
||||
pub num_slice_groups_minus1: u8,
|
||||
pub slice_group_map_type: u8,
|
||||
pub slice_group_change_rate_minus1: u16,
|
||||
pub pic_init_qp_minus26: i8,
|
||||
pub pic_init_qs_minus26: i8,
|
||||
pub chroma_qp_index_offset: i8,
|
||||
pub second_chroma_qp_index_offset: i8,
|
||||
pub pic_fields: u32,
|
||||
pub frame_num: u16,
|
||||
pub va_reserved: [u32; VA_PADDING_MEDIUM],
|
||||
}
|
||||
|
||||
/// `seq_fields` bit positions (`chroma_format_idc` 2 bits, the log2 fields 4, `pic_order_cnt_type` 2).
|
||||
pub mod seq_bits {
|
||||
pub const CHROMA_FORMAT_IDC: u32 = 0;
|
||||
pub const RESIDUAL_COLOUR_TRANSFORM: u32 = 2;
|
||||
pub const GAPS_IN_FRAME_NUM_ALLOWED: u32 = 3;
|
||||
pub const FRAME_MBS_ONLY: u32 = 4;
|
||||
pub const MB_ADAPTIVE_FRAME_FIELD: u32 = 5;
|
||||
pub const DIRECT_8X8_INFERENCE: u32 = 6;
|
||||
pub const MIN_LUMA_BI_PRED_SIZE_8X8: u32 = 7;
|
||||
pub const LOG2_MAX_FRAME_NUM_MINUS4: u32 = 8;
|
||||
pub const PIC_ORDER_CNT_TYPE: u32 = 12;
|
||||
pub const LOG2_MAX_PIC_ORDER_CNT_LSB_MINUS4: u32 = 14;
|
||||
pub const DELTA_PIC_ORDER_ALWAYS_ZERO: u32 = 18;
|
||||
}
|
||||
|
||||
/// `pic_fields` bit positions (`weighted_bipred_idc` 2 bits).
|
||||
pub mod pic_bits {
|
||||
pub const ENTROPY_CODING_MODE: u32 = 0;
|
||||
pub const WEIGHTED_PRED: u32 = 1;
|
||||
pub const WEIGHTED_BIPRED_IDC: u32 = 2;
|
||||
pub const TRANSFORM_8X8_MODE: u32 = 4;
|
||||
pub const FIELD_PIC: u32 = 5;
|
||||
pub const CONSTRAINED_INTRA_PRED: u32 = 6;
|
||||
pub const PIC_ORDER_PRESENT: u32 = 7;
|
||||
pub const DEBLOCKING_FILTER_CONTROL_PRESENT: u32 = 8;
|
||||
pub const REDUNDANT_PIC_CNT_PRESENT: u32 = 9;
|
||||
pub const REFERENCE_PIC: u32 = 10;
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct VAIQMatrixBufferH264 {
|
||||
/// 4x4 scaling lists in raster scan order: Intra Y, Cb, Cr, Inter Y, Cb, Cr.
|
||||
pub ScalingList4x4: [[u8; 16]; 6],
|
||||
/// 8x8 scaling lists in raster scan order: Intra Y, Inter Y.
|
||||
pub ScalingList8x8: [[u8; 64]; 2],
|
||||
pub va_reserved: [u32; VA_PADDING_LOW],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct VASliceParameterBufferH264 {
|
||||
pub slice_data_size: u32,
|
||||
pub slice_data_offset: u32,
|
||||
pub slice_data_flag: u32,
|
||||
/// Bits from the start of the NAL unit (its header byte included) to `slice_data()`, counted
|
||||
/// without emulation prevention bytes; the data buffer itself keeps them.
|
||||
pub slice_data_bit_offset: u16,
|
||||
pub first_mb_in_slice: u16,
|
||||
pub slice_type: u8,
|
||||
pub direct_spatial_mv_pred_flag: u8,
|
||||
pub num_ref_idx_l0_active_minus1: u8,
|
||||
pub num_ref_idx_l1_active_minus1: u8,
|
||||
pub cabac_init_idc: u8,
|
||||
pub slice_qp_delta: i8,
|
||||
pub disable_deblocking_filter_idc: u8,
|
||||
pub slice_alpha_c0_offset_div2: i8,
|
||||
pub slice_beta_offset_div2: i8,
|
||||
pub RefPicList0: [VAPictureH264; 32],
|
||||
pub RefPicList1: [VAPictureH264; 32],
|
||||
pub luma_log2_weight_denom: u8,
|
||||
pub chroma_log2_weight_denom: u8,
|
||||
pub luma_weight_l0_flag: u8,
|
||||
pub luma_weight_l0: [i16; 32],
|
||||
pub luma_offset_l0: [i16; 32],
|
||||
pub chroma_weight_l0_flag: u8,
|
||||
pub chroma_weight_l0: [[i16; 2]; 32],
|
||||
pub chroma_offset_l0: [[i16; 2]; 32],
|
||||
pub luma_weight_l1_flag: u8,
|
||||
pub luma_weight_l1: [i16; 32],
|
||||
pub luma_offset_l1: [i16; 32],
|
||||
pub chroma_weight_l1_flag: u8,
|
||||
pub chroma_weight_l1: [[i16; 2]; 32],
|
||||
pub chroma_offset_l1: [[i16; 2]; 32],
|
||||
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 {
|
||||
pub fourcc: u32,
|
||||
pub byte_order: u32,
|
||||
pub bits_per_pixel: u32,
|
||||
pub depth: u32,
|
||||
pub red_mask: u32,
|
||||
pub green_mask: u32,
|
||||
pub blue_mask: u32,
|
||||
pub alpha_mask: u32,
|
||||
pub va_reserved: [u32; VA_PADDING_LOW],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct VAImage {
|
||||
pub image_id: VAImageID,
|
||||
pub format: VAImageFormat,
|
||||
pub buf: VABufferID,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub data_size: u32,
|
||||
pub num_planes: u32,
|
||||
pub pitches: [u32; 3],
|
||||
pub offsets: [u32; 3],
|
||||
pub num_palette_entries: i32,
|
||||
pub entry_bytes: i32,
|
||||
pub component_order: [i8; 4],
|
||||
pub va_reserved: [u32; VA_PADDING_LOW],
|
||||
}
|
||||
|
||||
/// The `libva.so.2` entry points used (all `VAStatus`-returning unless noted).
|
||||
pub type vaInitialize = unsafe extern "C" fn(dpy: VADisplay, major: *mut c_int, minor: *mut c_int) -> VAStatus;
|
||||
pub type vaTerminate = unsafe extern "C" fn(dpy: VADisplay) -> VAStatus;
|
||||
pub type vaErrorStr = unsafe extern "C" fn(status: VAStatus) -> *const c_char;
|
||||
pub type vaQueryVendorString = unsafe extern "C" fn(dpy: VADisplay) -> *const c_char;
|
||||
pub type vaSetInfoCallback = unsafe extern "C" fn(dpy: VADisplay, callback: VAMessageCallback, user_context: *mut c_void) -> VAMessageCallback;
|
||||
pub type vaSetErrorCallback = unsafe extern "C" fn(dpy: VADisplay, callback: VAMessageCallback, user_context: *mut c_void) -> VAMessageCallback;
|
||||
pub type vaGetConfigAttributes =
|
||||
unsafe extern "C" fn(dpy: VADisplay, profile: VAProfile, entrypoint: VAEntrypoint, attrib_list: *mut VAConfigAttrib, num_attribs: c_int) -> VAStatus;
|
||||
pub type vaCreateConfig = unsafe extern "C" fn(
|
||||
dpy: VADisplay,
|
||||
profile: VAProfile,
|
||||
entrypoint: VAEntrypoint,
|
||||
attrib_list: *mut VAConfigAttrib,
|
||||
num_attribs: c_int,
|
||||
config_id: *mut VAConfigID,
|
||||
) -> VAStatus;
|
||||
pub type vaDestroyConfig = unsafe extern "C" fn(dpy: VADisplay, config_id: VAConfigID) -> VAStatus;
|
||||
/// The last two arguments are `VASurfaceAttrib *attrib_list, unsigned int num_attribs`; this
|
||||
/// backend passes none, so the attribute structure is not declared.
|
||||
pub type vaCreateSurfaces = unsafe extern "C" fn(
|
||||
dpy: VADisplay,
|
||||
format: c_uint,
|
||||
width: c_uint,
|
||||
height: c_uint,
|
||||
surfaces: *mut VASurfaceID,
|
||||
num_surfaces: c_uint,
|
||||
attrib_list: *mut c_void,
|
||||
num_attribs: c_uint,
|
||||
) -> VAStatus;
|
||||
pub type vaDestroySurfaces = unsafe extern "C" fn(dpy: VADisplay, surfaces: *mut VASurfaceID, num_surfaces: c_int) -> VAStatus;
|
||||
pub type vaCreateContext = unsafe extern "C" fn(
|
||||
dpy: VADisplay,
|
||||
config_id: VAConfigID,
|
||||
picture_width: c_int,
|
||||
picture_height: c_int,
|
||||
flag: c_int,
|
||||
render_targets: *mut VASurfaceID,
|
||||
num_render_targets: c_int,
|
||||
context: *mut VAContextID,
|
||||
) -> VAStatus;
|
||||
pub type vaDestroyContext = unsafe extern "C" fn(dpy: VADisplay, context: VAContextID) -> VAStatus;
|
||||
pub type vaCreateBuffer = unsafe extern "C" fn(
|
||||
dpy: VADisplay,
|
||||
context: VAContextID,
|
||||
type_: VABufferType,
|
||||
size: c_uint,
|
||||
num_elements: c_uint,
|
||||
data: *mut c_void,
|
||||
buf_id: *mut VABufferID,
|
||||
) -> VAStatus;
|
||||
pub type vaDestroyBuffer = unsafe extern "C" fn(dpy: VADisplay, buffer_id: VABufferID) -> VAStatus;
|
||||
pub type vaBeginPicture = unsafe extern "C" fn(dpy: VADisplay, context: VAContextID, render_target: VASurfaceID) -> VAStatus;
|
||||
pub type vaRenderPicture = unsafe extern "C" fn(dpy: VADisplay, context: VAContextID, buffers: *mut VABufferID, num_buffers: c_int) -> VAStatus;
|
||||
pub type vaEndPicture = unsafe extern "C" fn(dpy: VADisplay, context: VAContextID) -> VAStatus;
|
||||
pub type vaSyncSurface = unsafe extern "C" fn(dpy: VADisplay, render_target: VASurfaceID) -> VAStatus;
|
||||
pub type vaCreateImage = unsafe extern "C" fn(dpy: VADisplay, format: *mut VAImageFormat, width: c_int, height: c_int, image: *mut VAImage) -> VAStatus;
|
||||
pub type vaDestroyImage = unsafe extern "C" fn(dpy: VADisplay, image: VAImageID) -> VAStatus;
|
||||
pub type vaGetImage =
|
||||
unsafe extern "C" fn(dpy: VADisplay, surface: VASurfaceID, x: c_int, y: c_int, width: c_uint, height: c_uint, image: VAImageID) -> VAStatus;
|
||||
pub type vaPutImage = unsafe extern "C" fn(
|
||||
dpy: VADisplay,
|
||||
surface: VASurfaceID,
|
||||
image: VAImageID,
|
||||
src_x: c_int,
|
||||
src_y: c_int,
|
||||
src_width: c_uint,
|
||||
src_height: c_uint,
|
||||
dest_x: c_int,
|
||||
dest_y: c_int,
|
||||
dest_width: c_uint,
|
||||
dest_height: c_uint,
|
||||
) -> VAStatus;
|
||||
pub type vaMapBuffer = unsafe extern "C" fn(dpy: VADisplay, buf_id: VABufferID, pbuf: *mut *mut c_void) -> VAStatus;
|
||||
pub type vaUnmapBuffer = unsafe extern "C" fn(dpy: VADisplay, buf_id: VABufferID) -> VAStatus;
|
||||
/// `libva-drm.so.2`: the display of a DRM device (render node) file descriptor.
|
||||
pub type vaGetDisplayDRM = unsafe extern "C" fn(fd: c_int) -> VADisplay;
|
||||
@@ -0,0 +1,512 @@
|
||||
//! The H.264 side of the VA-API decoder: everything a stateless hardware decoder needs from the
|
||||
//! host, done with the software decoder's own parsers (`filmcraft_h264::params`, `slice`) and its
|
||||
//! decoded picture buffer (`filmcraft_h264::dpb`: reference marking, reference lists, output
|
||||
//! order), so both decoders make the same decisions and output the same pictures in the same order.
|
||||
//!
|
||||
//! [`Front`] parses each access unit, fills the VA-API picture / matrix / slice buffers and hands
|
||||
//! them to an [`Accel`] (the libva session on Linux, a recording stand-in in tests), then reads
|
||||
//! back the pictures the DPB outputs. What the hardware path does not take is an error, so the
|
||||
//! hybrid decoder replays the run in software: field / MBAFF coding, FMO, SP / SI and data
|
||||
//! partitioning, stream changes the surfaces were not made for.
|
||||
//!
|
||||
//! 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_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};
|
||||
|
||||
/// 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)]
|
||||
pub struct Surface {
|
||||
pub index: usize,
|
||||
pub top: i32,
|
||||
pub bottom: i32,
|
||||
pub seq: u64,
|
||||
}
|
||||
|
||||
/// A frame_num gap frame not given a surface yet.
|
||||
const UNASSIGNED: usize = usize::MAX;
|
||||
|
||||
/// The picture whose slices are being collected.
|
||||
struct Pending {
|
||||
target: usize,
|
||||
sps: Arc<Sps>,
|
||||
pps: Arc<Pps>,
|
||||
first: SliceHeader,
|
||||
poc: Poc,
|
||||
pts: i64,
|
||||
slices: Vec<(VASliceParameterBufferH264, Vec<u8>)>,
|
||||
}
|
||||
|
||||
/// H.264 front end of a stateless hardware decoder (see the module docs).
|
||||
pub struct Front<A: Accel> {
|
||||
accel: A,
|
||||
length_size: usize,
|
||||
/// Coded size (macroblocks) the surfaces were made for.
|
||||
mbs: (u32, u32),
|
||||
spss: Vec<Option<Arc<Sps>>>,
|
||||
ppss: Vec<Option<Arc<Pps>>>,
|
||||
dpb: Dpb<Surface>,
|
||||
poc_state: PocState,
|
||||
prev_ref_frame_num: u32,
|
||||
active_sps: Option<Arc<Sps>>,
|
||||
pending: Option<Pending>,
|
||||
/// Pictures made so far (decoding order of [`Surface::seq`]).
|
||||
seq: u64,
|
||||
}
|
||||
|
||||
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<A: Accel> Front<A> {
|
||||
/// A front end for samples with `length_size`-byte NAL length prefixes whose sample entry
|
||||
/// carried `parameter_sets` (SPS / PPS NAL units), decoding into `accel`'s surfaces of
|
||||
/// `mbs` macroblocks.
|
||||
pub fn new(accel: A, length_size: usize, mbs: (u32, u32), parameter_sets: &[Vec<u8>]) -> Result<Self> {
|
||||
let mut f = Self {
|
||||
accel,
|
||||
length_size,
|
||||
mbs,
|
||||
spss: vec![None; 32],
|
||||
ppss: vec![None; 256],
|
||||
dpb: Dpb::new(),
|
||||
poc_state: PocState::default(),
|
||||
prev_ref_frame_num: 0,
|
||||
active_sps: None,
|
||||
pending: None,
|
||||
seq: 0,
|
||||
};
|
||||
f.load_parameter_sets(parameter_sets)?;
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
fn load_parameter_sets(&mut self, sets: &[Vec<u8>]) -> Result<()> {
|
||||
let mut frames = Vec::new();
|
||||
for nal in sets {
|
||||
self.handle_nal(nal, 0, &mut frames)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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<Vec<DecodedFrame>> {
|
||||
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)?;
|
||||
}
|
||||
// the access unit is complete
|
||||
self.submit_pending(&mut frames)?;
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
/// Output every picture still held for reordering (references stay for what follows).
|
||||
pub fn flush(&mut self) -> Result<Vec<DecodedFrame>> {
|
||||
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)?;
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
/// Forget every picture (after a seek).
|
||||
pub fn reset(&mut self) {
|
||||
self.dpb = Dpb::new();
|
||||
self.poc_state = PocState::default();
|
||||
self.prev_ref_frame_num = 0;
|
||||
self.active_sps = None;
|
||||
self.pending = None;
|
||||
}
|
||||
|
||||
/// Read back the pictures the DPB output, right away: a surface leaving the DPB is reused by
|
||||
/// the next picture.
|
||||
fn read_out(&mut self, outs: Vec<Output<Surface>>, frames: &mut Vec<DecodedFrame>) -> 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<DecodedFrame>) -> Result<()> {
|
||||
let Some(&first) = nal.first() else { return Ok(()) };
|
||||
let rest = nal.get(1..).unwrap_or_default();
|
||||
let hdr = NalHeader::parse(first).map_err(decode_err)?;
|
||||
match hdr.nal_unit_type {
|
||||
nal_type::SPS => {
|
||||
let sps = Sps::parse(&unescape_rbsp(rest)).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 spss: Vec<Option<Sps>> = self.spss.iter().map(|s| s.as_ref().map(|s| (**s).clone())).collect();
|
||||
let pps = Pps::parse(&unescape_rbsp(rest), &spss).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::SLICE | nal_type::IDR => self.handle_slice(nal, hdr, pts, frames)?,
|
||||
nal_type::SLICE_DPA | nal_type::SLICE_DPB | nal_type::SLICE_DPC => return Err(unsupported("data partitioning")),
|
||||
nal_type::END_SEQ | nal_type::END_STREAM => self.submit_pending(frames)?,
|
||||
// SEI, AUD, filler, extensions: nothing to decode
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_slice(&mut self, nal: &[u8], hdr: NalHeader, pts: i64, frames: &mut Vec<DecodedFrame>) -> Result<()> {
|
||||
let rbsp = unescape_rbsp(nal.get(1..).unwrap_or_default());
|
||||
let (ppss, spss) = (&self.ppss, &self.spss);
|
||||
let mut found: Option<(Arc<Pps>, Arc<Sps>)> = None;
|
||||
let (sh, _, _) = SliceHeader::parse(&rbsp, hdr, |id| {
|
||||
let missing = |what: String| filmcraft_h264::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)))?;
|
||||
found = Some((pps.clone(), sps.clone()));
|
||||
Ok((&**pps, &**sps))
|
||||
})
|
||||
.map_err(decode_err)?;
|
||||
let Some((pps, sps)) = found else { return Err(decode_err("slice without a PPS")) };
|
||||
sps.check_supported().map_err(decode_err)?;
|
||||
if pps.num_slice_groups > 1 {
|
||||
return Err(unsupported("slice groups (FMO)"));
|
||||
}
|
||||
if sh.field_pic {
|
||||
return Err(unsupported("field pictures"));
|
||||
}
|
||||
if matches!(sh.slice_type, SliceType::Sp | SliceType::Si) {
|
||||
return Err(unsupported("SP / SI slices"));
|
||||
}
|
||||
if sh.redundant_pic_cnt > 0 {
|
||||
return Ok(()); // redundant slices are ignored, as in the software decoder
|
||||
}
|
||||
let new_pic = match &self.pending {
|
||||
None => true,
|
||||
Some(p) => is_new_picture(&p.first, &sh, &sps) || sh.first_mb_in_slice == 0 || !Arc::ptr_eq(&p.sps, &sps),
|
||||
};
|
||||
if new_pic {
|
||||
self.submit_pending(frames)?;
|
||||
if sh.first_mb_in_slice != 0 && self.active_sps.is_none() {
|
||||
return Err(decode_err("stream does not start with the first slice of a picture"));
|
||||
}
|
||||
self.start_picture(&sh, &sps, &pps, pts, frames)?;
|
||||
}
|
||||
let Some(pending) = self.pending.as_ref() else { return Err(decode_err("slice without a started picture")) };
|
||||
if !Arc::ptr_eq(&pending.pps, &pps) {
|
||||
return Err(unsupported("a picture whose slices use different PPSs"));
|
||||
}
|
||||
let surfaces = self.accel.surfaces();
|
||||
let lists = self
|
||||
.dpb
|
||||
.build_ref_lists(&sh, pending.poc.frame(), sps.max_frame_num(), |e| (va_ref(e, surfaces), e.mark != RefMark::Unused))
|
||||
.map_err(decode_err)?;
|
||||
if !sh.slice_type.is_intra() && (lists[0].is_empty() || (sh.slice_type.is_b() && lists[1].is_empty())) {
|
||||
return Err(decode_err("no reference pictures available for an inter slice"));
|
||||
}
|
||||
// a missing reference (the software decoder conceals it with another picture) is not
|
||||
// something to hand the hardware
|
||||
if lists.iter().flatten().any(|(_, is_ref)| !is_ref) {
|
||||
return Err(decode_err("a reference picture is missing"));
|
||||
}
|
||||
let refs = lists.map(|l| l.into_iter().map(|(p, _)| p).collect::<Vec<_>>());
|
||||
let params = slice_params(&sh, nal.len(), &refs)?;
|
||||
if let Some(p) = self.pending.as_mut() {
|
||||
p.slices.push((params, nal.to_vec()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_picture(&mut self, sh: &SliceHeader, sps: &Arc<Sps>, pps: &Arc<Pps>, pts: i64, frames: &mut Vec<DecodedFrame>) -> Result<()> {
|
||||
if (sps.pic_width_in_mbs, sps.frame_height_in_mbs()) != self.mbs {
|
||||
return Err(unsupported("a picture size change"));
|
||||
}
|
||||
if sps.max_dpb_frames() >= self.accel.surfaces().len() {
|
||||
return Err(unsupported("a DPB larger than the decoder's surfaces"));
|
||||
}
|
||||
// a new SPS with another DPB size outputs and forgets what came before (as in software)
|
||||
let changed = self.active_sps.as_ref().is_some_and(|a| a.max_dpb_frames() != sps.max_dpb_frames());
|
||||
let mut outs = Vec::new();
|
||||
if changed {
|
||||
self.dpb.flush(&mut outs);
|
||||
self.dpb.entries.clear();
|
||||
}
|
||||
self.active_sps = Some(sps.clone());
|
||||
self.dpb.capacity = sps.max_dpb_frames();
|
||||
self.dpb.max_reorder = sps.max_num_reorder_frames().min(self.dpb.capacity);
|
||||
if sh.idr {
|
||||
self.dpb.idr(sh.no_output_of_prior_pics, &mut outs);
|
||||
self.prev_ref_frame_num = 0;
|
||||
} else {
|
||||
let max = sps.max_frame_num();
|
||||
if sh.frame_num != self.prev_ref_frame_num && sh.frame_num != (self.prev_ref_frame_num + 1) % max {
|
||||
self.fill_frame_num_gap(sh, sps, pts)?;
|
||||
}
|
||||
}
|
||||
self.read_out(outs, frames)?;
|
||||
let poc = self.poc_state.compute(sh, sps);
|
||||
let target =
|
||||
(0..self.accel.surfaces().len()).find(|&i| !self.dpb.entries.iter().any(|e| e.frame.index == i)).ok_or_else(|| decode_err("no free surface"))?;
|
||||
self.pending = Some(Pending { target, sps: sps.clone(), pps: pps.clone(), first: sh.clone(), poc, pts, slices: Vec::new() });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// frame_num gap (8.2.5.2; also every start at a non-IDR picture, an open-GOP seek): "non-existing"
|
||||
/// frames as the software decoder makes them, copies of the latest decoded frame (mid-gray when
|
||||
/// there is none), so pictures predicted from them come out the same.
|
||||
fn fill_frame_num_gap(&mut self, sh: &SliceHeader, sps: &Arc<Sps>, pts: i64) -> Result<()> {
|
||||
let max = sps.max_frame_num();
|
||||
let last = self.dpb.entries.iter().filter(|e| !e.non_existing).max_by_key(|e| e.frame.seq).map(|e| e.frame.index);
|
||||
let meta = Arc::new(output_meta(sps, pts, false));
|
||||
let poc_state = &mut self.poc_state;
|
||||
// surfaces are given afterwards, to the frames the sliding window keeps
|
||||
let mut make = |_| (Surface { index: UNASSIGNED, top: 0, bottom: 0, seq: 0 }, 0, meta.clone());
|
||||
let mut update = |frame_num| poc_state.update_gap_frame(frame_num, sps);
|
||||
self.dpb.fill_frame_num_gap(self.prev_ref_frame_num, sh.frame_num, max, sps.max_num_ref_frames as usize, &mut make, &mut update);
|
||||
self.prev_ref_frame_num = (sh.frame_num + max - 1) % max;
|
||||
let used: Vec<usize> = self.dpb.entries.iter().map(|e| e.frame.index).filter(|&i| i != UNASSIGNED).collect();
|
||||
let mut free = (0..self.accel.surfaces().len()).filter(|i| !used.contains(i));
|
||||
let mut fills = Vec::new();
|
||||
for e in self.dpb.entries.iter_mut().filter(|e| e.frame.index == UNASSIGNED) {
|
||||
let index = free.next().ok_or_else(|| decode_err("no free surface for a frame_num gap"))?;
|
||||
self.seq += 1;
|
||||
e.frame = Surface { index, top: 0, bottom: 0, seq: self.seq };
|
||||
fills.push(index);
|
||||
}
|
||||
for index in fills {
|
||||
self.accel.fill(index, last).map_err(decode_err)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode the collected picture, then do its reference marking and DPB insertion.
|
||||
fn submit_pending(&mut self, frames: &mut Vec<DecodedFrame>) -> Result<()> {
|
||||
let Some(p) = self.pending.take() else { return Ok(()) };
|
||||
if p.slices.is_empty() {
|
||||
return Err(decode_err("a picture without slices"));
|
||||
}
|
||||
let surfaces = self.accel.surfaces();
|
||||
let target_id = surfaces.get(p.target).copied().ok_or_else(|| decode_err("surface out of range"))?;
|
||||
if self.dpb.entries.iter().filter(|e| e.mark != RefMark::Unused).count() > 16 {
|
||||
return Err(decode_err("more than 16 reference frames"));
|
||||
}
|
||||
let mut pic = picture_params(&p.sps, &p.pps, &p.first);
|
||||
pic.CurrPic = VAPictureH264 {
|
||||
picture_id: target_id,
|
||||
frame_idx: p.first.frame_num,
|
||||
flags: 0,
|
||||
TopFieldOrderCnt: p.poc.top,
|
||||
BottomFieldOrderCnt: p.poc.bottom,
|
||||
va_reserved: [0; ffi::VA_PADDING_LOW],
|
||||
};
|
||||
for (slot, e) in pic.ReferenceFrames.iter_mut().zip(self.dpb.entries.iter().filter(|e| e.mark != RefMark::Unused)) {
|
||||
*slot = va_ref(e, surfaces);
|
||||
}
|
||||
let iq = iq_matrix(&p.pps);
|
||||
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();
|
||||
if p.first.nal_ref_idc != 0 {
|
||||
self.prev_ref_frame_num = if mmco5 { 0 } else { p.first.frame_num };
|
||||
}
|
||||
// MMCO 5: the picture's order counts become relative to itself (tempPicOrderCnt, 8.2.1)
|
||||
let temp = if mmco5 { p.poc.frame() } else { 0 };
|
||||
self.seq += 1;
|
||||
let surface = Surface { index: p.target, top: p.poc.top.wrapping_sub(temp), bottom: p.poc.bottom.wrapping_sub(temp), seq: self.seq };
|
||||
let fpoc = if mmco5 { 0 } else { p.poc.frame() };
|
||||
let meta = Arc::new(output_meta(&p.sps, p.pts, p.first.idr));
|
||||
let mut outs = Vec::new();
|
||||
self.dpb.store_picture(&p.first, surface, fpoc, p.sps.max_frame_num(), p.sps.max_num_ref_frames as usize, meta, &mut outs);
|
||||
self.read_out(outs, frames)
|
||||
}
|
||||
}
|
||||
|
||||
/// A DPB entry as a VA-API reference picture.
|
||||
fn va_ref(e: &DpbEntry<Surface>, surfaces: &[ffi::VASurfaceID]) -> VAPictureH264 {
|
||||
let long = e.mark == RefMark::Long;
|
||||
VAPictureH264 {
|
||||
picture_id: surfaces.get(e.frame.index).copied().unwrap_or(ffi::VA_INVALID_SURFACE),
|
||||
frame_idx: if long { e.long_term_frame_idx } else { e.frame_num },
|
||||
flags: if long { ffi::VA_PICTURE_H264_LONG_TERM_REFERENCE } else { ffi::VA_PICTURE_H264_SHORT_TERM_REFERENCE },
|
||||
TopFieldOrderCnt: e.frame.top,
|
||||
BottomFieldOrderCnt: e.frame.bottom,
|
||||
va_reserved: [0; ffi::VA_PADDING_LOW],
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
draft: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn flag(on: bool, bit: u32) -> u32 {
|
||||
u32::from(on) << bit
|
||||
}
|
||||
|
||||
/// The picture parameters of a picture coded with `sps` / `pps` whose first slice is `sh` (the
|
||||
/// current and reference pictures are filled in by the caller).
|
||||
pub fn picture_params(sps: &Sps, pps: &Pps, sh: &SliceHeader) -> VAPictureParameterBufferH264 {
|
||||
let seq_fields = (sps.chroma_format_idc & 3) << seq_bits::CHROMA_FORMAT_IDC
|
||||
| flag(sps.separate_colour_plane, seq_bits::RESIDUAL_COLOUR_TRANSFORM)
|
||||
| flag(sps.gaps_in_frame_num_allowed, seq_bits::GAPS_IN_FRAME_NUM_ALLOWED)
|
||||
| flag(sps.frame_mbs_only, seq_bits::FRAME_MBS_ONLY)
|
||||
| flag(sps.mb_adaptive_frame_field, seq_bits::MB_ADAPTIVE_FRAME_FIELD)
|
||||
| flag(sps.direct_8x8_inference, seq_bits::DIRECT_8X8_INFERENCE)
|
||||
// A.3.3.2: bi-prediction of blocks under 8x8 is not allowed from level 3.1
|
||||
| flag(sps.level_idc >= 31, seq_bits::MIN_LUMA_BI_PRED_SIZE_8X8)
|
||||
| (sps.log2_max_frame_num.saturating_sub(4) & 15) << seq_bits::LOG2_MAX_FRAME_NUM_MINUS4
|
||||
| (sps.pic_order_cnt_type & 3) << seq_bits::PIC_ORDER_CNT_TYPE
|
||||
| (sps.log2_max_poc_lsb.saturating_sub(4) & 15) << seq_bits::LOG2_MAX_PIC_ORDER_CNT_LSB_MINUS4
|
||||
| flag(sps.delta_pic_order_always_zero, seq_bits::DELTA_PIC_ORDER_ALWAYS_ZERO);
|
||||
let pic_fields = flag(pps.entropy_coding_mode, pic_bits::ENTROPY_CODING_MODE)
|
||||
| flag(pps.weighted_pred, pic_bits::WEIGHTED_PRED)
|
||||
| (pps.weighted_bipred_idc & 3) << pic_bits::WEIGHTED_BIPRED_IDC
|
||||
| flag(pps.transform_8x8_mode, pic_bits::TRANSFORM_8X8_MODE)
|
||||
| flag(sh.field_pic, pic_bits::FIELD_PIC)
|
||||
| flag(pps.constrained_intra_pred, pic_bits::CONSTRAINED_INTRA_PRED)
|
||||
| flag(pps.bottom_field_pic_order_in_frame_present, pic_bits::PIC_ORDER_PRESENT)
|
||||
| flag(pps.deblocking_filter_control_present, pic_bits::DEBLOCKING_FILTER_CONTROL_PRESENT)
|
||||
| flag(pps.redundant_pic_cnt_present, pic_bits::REDUNDANT_PIC_CNT_PRESENT)
|
||||
| flag(sh.nal_ref_idc != 0, pic_bits::REFERENCE_PIC);
|
||||
// the parsers bound these (frame size by the level limits, qp offsets to ±12, qp to 0..51)
|
||||
let narrow = |v: u32| u16::try_from(v.saturating_sub(1)).unwrap_or(u16::MAX);
|
||||
let small = |v: i32| i8::try_from(v).unwrap_or(0);
|
||||
VAPictureParameterBufferH264 {
|
||||
CurrPic: VAPictureH264::INVALID,
|
||||
ReferenceFrames: [VAPictureH264::INVALID; 16],
|
||||
picture_width_in_mbs_minus1: narrow(sps.pic_width_in_mbs),
|
||||
picture_height_in_mbs_minus1: narrow(sps.frame_height_in_mbs()),
|
||||
bit_depth_luma_minus8: u8::try_from(sps.bit_depth_luma.saturating_sub(8)).unwrap_or(0),
|
||||
bit_depth_chroma_minus8: u8::try_from(sps.bit_depth_chroma.saturating_sub(8)).unwrap_or(0),
|
||||
num_ref_frames: u8::try_from(sps.max_num_ref_frames).unwrap_or(16),
|
||||
seq_fields,
|
||||
num_slice_groups_minus1: 0,
|
||||
slice_group_map_type: 0,
|
||||
slice_group_change_rate_minus1: 0,
|
||||
pic_init_qp_minus26: small(pps.pic_init_qp - 26),
|
||||
pic_init_qs_minus26: small(pps.pic_init_qs - 26),
|
||||
chroma_qp_index_offset: small(pps.chroma_qp_index_offset),
|
||||
second_chroma_qp_index_offset: small(pps.second_chroma_qp_index_offset),
|
||||
pic_fields,
|
||||
frame_num: u16::try_from(sh.frame_num).unwrap_or(0),
|
||||
va_reserved: [0; ffi::VA_PADDING_MEDIUM],
|
||||
}
|
||||
}
|
||||
|
||||
/// The scaling matrices of `pps` (after the SPS / PPS fall-back rules), raster order.
|
||||
pub fn iq_matrix(pps: &Pps) -> VAIQMatrixBufferH264 {
|
||||
VAIQMatrixBufferH264 { ScalingList4x4: pps.scaling.m4, ScalingList8x8: [pps.scaling.m8[0], pps.scaling.m8[1]], va_reserved: [0; ffi::VA_PADDING_LOW] }
|
||||
}
|
||||
|
||||
/// The slice parameters of the slice `sh` (a `nal_len`-byte NAL unit) with reference lists `refs`.
|
||||
pub fn slice_params(sh: &SliceHeader, nal_len: usize, refs: &[Vec<VAPictureH264>; 2]) -> Result<VASliceParameterBufferH264> {
|
||||
let bit_offset = sh.header_bits.checked_add(8).and_then(|b| u16::try_from(b).ok()).ok_or_else(|| decode_err("slice header too long"))?;
|
||||
let mut s = VASliceParameterBufferH264 {
|
||||
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_bit_offset: bit_offset,
|
||||
first_mb_in_slice: u16::try_from(sh.first_mb_in_slice).map_err(|_| decode_err("first_mb_in_slice out of range"))?,
|
||||
slice_type: (sh.slice_type_raw % 5) as u8,
|
||||
direct_spatial_mv_pred_flag: u8::from(sh.direct_spatial_mv_pred),
|
||||
num_ref_idx_l0_active_minus1: u8::try_from(sh.num_ref_idx_active[0].saturating_sub(1)).unwrap_or(31).min(31),
|
||||
num_ref_idx_l1_active_minus1: u8::try_from(sh.num_ref_idx_active[1].saturating_sub(1)).unwrap_or(31).min(31),
|
||||
cabac_init_idc: u8::try_from(sh.cabac_init_idc).unwrap_or(0),
|
||||
slice_qp_delta: i8::try_from(sh.slice_qp_delta).map_err(|_| decode_err("slice_qp_delta out of range"))?,
|
||||
disable_deblocking_filter_idc: u8::try_from(sh.disable_deblocking_filter_idc).unwrap_or(0),
|
||||
slice_alpha_c0_offset_div2: i8::try_from(sh.slice_alpha_c0_offset_div2).unwrap_or(0),
|
||||
slice_beta_offset_div2: i8::try_from(sh.slice_beta_offset_div2).unwrap_or(0),
|
||||
RefPicList0: [VAPictureH264::INVALID; 32],
|
||||
RefPicList1: [VAPictureH264::INVALID; 32],
|
||||
luma_log2_weight_denom: 0,
|
||||
chroma_log2_weight_denom: 0,
|
||||
luma_weight_l0_flag: 0,
|
||||
luma_weight_l0: [0; 32],
|
||||
luma_offset_l0: [0; 32],
|
||||
chroma_weight_l0_flag: 0,
|
||||
chroma_weight_l0: [[0; 2]; 32],
|
||||
chroma_offset_l0: [[0; 2]; 32],
|
||||
luma_weight_l1_flag: 0,
|
||||
luma_weight_l1: [0; 32],
|
||||
luma_offset_l1: [0; 32],
|
||||
chroma_weight_l1_flag: 0,
|
||||
chroma_weight_l1: [[0; 2]; 32],
|
||||
chroma_offset_l1: [[0; 2]; 32],
|
||||
va_reserved: [0; ffi::VA_PADDING_LOW],
|
||||
};
|
||||
for (dst, src) in s.RefPicList0.iter_mut().zip(&refs[0]) {
|
||||
*dst = *src;
|
||||
}
|
||||
for (dst, src) in s.RefPicList1.iter_mut().zip(&refs[1]) {
|
||||
*dst = *src;
|
||||
}
|
||||
// explicit weighted prediction: every entry, the default weight where none was coded
|
||||
if let Some(w) = &sh.pred_weight_table {
|
||||
s.luma_log2_weight_denom = u8::try_from(w.luma_log2_denom).unwrap_or(0);
|
||||
s.chroma_log2_weight_denom = u8::try_from(w.chroma_log2_denom).unwrap_or(0);
|
||||
let w16 = |v: i32| i16::try_from(v).unwrap_or(0);
|
||||
let lists = [
|
||||
(
|
||||
&w.l0,
|
||||
&mut s.luma_weight_l0,
|
||||
&mut s.luma_offset_l0,
|
||||
&mut s.chroma_weight_l0,
|
||||
&mut s.chroma_offset_l0,
|
||||
&mut s.luma_weight_l0_flag,
|
||||
&mut s.chroma_weight_l0_flag,
|
||||
),
|
||||
(
|
||||
&w.l1,
|
||||
&mut s.luma_weight_l1,
|
||||
&mut s.luma_offset_l1,
|
||||
&mut s.chroma_weight_l1,
|
||||
&mut s.chroma_offset_l1,
|
||||
&mut s.luma_weight_l1_flag,
|
||||
&mut s.chroma_weight_l1_flag,
|
||||
),
|
||||
];
|
||||
for (entries, lw, lo, cw, co, lflag, cflag) in lists {
|
||||
if entries.is_empty() {
|
||||
continue;
|
||||
}
|
||||
*lflag = 1;
|
||||
*cflag = 1;
|
||||
for (i, e) in entries.iter().take(32).enumerate() {
|
||||
lw[i] = w16(e.luma_weight);
|
||||
lo[i] = w16(e.luma_offset);
|
||||
for c in 0..2 {
|
||||
cw[i][c] = w16(e.chroma_weight[c]);
|
||||
co[i][c] = w16(e.chroma_offset[c]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
@@ -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<Sps>,
|
||||
pps: Arc<Pps>,
|
||||
first: SliceHeader,
|
||||
/// The latest independent slice segment's header (dependent ones continue it).
|
||||
last: SliceHeader,
|
||||
rps: RefPicSet<Surface>,
|
||||
/// The surfaces of `pic.ReferenceFrames`, in order.
|
||||
ref_frames: Vec<usize>,
|
||||
pic: VAPictureParameterBufferHEVC,
|
||||
poc: i32,
|
||||
output: bool,
|
||||
meta: Arc<OutputMeta>,
|
||||
slices: Vec<(VASliceParameterBufferHEVC, Vec<u8>)>,
|
||||
all_intra: bool,
|
||||
any_b: bool,
|
||||
}
|
||||
|
||||
/// HEVC front end of a stateless hardware decoder (see the module docs).
|
||||
pub struct Front<A: Accel> {
|
||||
accel: A,
|
||||
length_size: usize,
|
||||
/// Coded size (luma samples) and bit depth the surfaces were made for.
|
||||
size: (u32, u32),
|
||||
bits: u32,
|
||||
spss: Vec<Option<Arc<Sps>>>,
|
||||
ppss: Vec<Option<Arc<Pps>>>,
|
||||
dpb: Dpb<Surface>,
|
||||
poc_state: PocState,
|
||||
active_sps: Option<Arc<Sps>>,
|
||||
pending: Option<Pending>,
|
||||
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<A: Accel> Front<A> {
|
||||
/// 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<u8>]) -> Result<Self> {
|
||||
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<Vec<DecodedFrame>> {
|
||||
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<Vec<DecodedFrame>> {
|
||||
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<Output<Surface>>, frames: &mut Vec<DecodedFrame>) -> 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<DecodedFrame>) -> 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<DecodedFrame>) -> 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<Sps>, pps: &Arc<Pps>, pts: i64, frames: &mut Vec<DecodedFrame>) -> 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<Surface>]| 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<DecodedFrame>) -> 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<VAPictureParameterBufferHEVC> {
|
||||
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<Ref<Surface>>; 2], ref_frames: &[usize]) -> Result<VASliceParameterBufferHEVC> {
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//! Hardware decoding through VA-API (Linux: Intel, AMD and other Mesa / vendor drivers).
|
||||
//!
|
||||
//! VA-API decoding is stateless: the application parses the stream and keeps the decoded picture
|
||||
//! 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 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;
|
||||
|
||||
#[cfg(test)]
|
||||
mod abi_tests;
|
||||
#[cfg(test)]
|
||||
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<u8>)],
|
||||
) -> 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<u8>)],
|
||||
) -> std::result::Result<(), String>;
|
||||
/// The picture in surface `index`, once decoded.
|
||||
fn read(&mut self, index: usize) -> std::result::Result<VideoFrame, String>;
|
||||
/// 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<usize>) -> 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 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 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],
|
||||
100 => &[ffi::VAProfileH264High],
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<h264::Front<Session>>),
|
||||
Hevc(Box<hevc::Front<Session>>),
|
||||
}
|
||||
|
||||
/// A VA-API decoder for one H.264 or HEVC stream.
|
||||
pub struct VaDecoder {
|
||||
front: Front,
|
||||
info: NalStreamInfo,
|
||||
name: String,
|
||||
fed: u64,
|
||||
fail_after: Option<u64>,
|
||||
}
|
||||
|
||||
/// The first parameter set of NAL unit type `ty` among `sets`, without its header (`header`
|
||||
/// bytes) and emulation prevention.
|
||||
fn parameter_set(sets: &[Vec<u8>], header: usize, ty: impl Fn(u8) -> bool) -> Option<Vec<u8>> {
|
||||
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<Self, String> {
|
||||
let bits = info.bit_depth_luma;
|
||||
if info.chroma_format_idc != 1 || info.bit_depth_chroma != bits || info.interlaced {
|
||||
return Err(format!(
|
||||
"{} chroma format {} at {bits}/{}-bit is not taken",
|
||||
if info.interlaced { "interlaced" } else { "progressive" },
|
||||
info.chroma_format_idc,
|
||||
info.bit_depth_chroma
|
||||
));
|
||||
}
|
||||
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 })
|
||||
}
|
||||
|
||||
/// Test hook: fail every decode call after the first `n` (a GPU lost mid-stream).
|
||||
pub fn fail_after(&mut self, n: u64) {
|
||||
self.fail_after = Some(n);
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoDecoder for VaDecoder {
|
||||
fn decode(&mut self, sample: &[u8], pts: i64) -> Result<Vec<DecodedFrame>> {
|
||||
self.fed += 1;
|
||||
if self.fail_after.is_some_and(|n| self.fed > n) {
|
||||
return Err(CodecError::Decode("VA-API decoder failed (test hook)".into()));
|
||||
}
|
||||
match &mut self.front {
|
||||
Front::H264(f) => f.decode(sample, pts),
|
||||
Front::Hevc(f) => f.decode(sample, pts),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Vec<DecodedFrame> {
|
||||
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);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
match &mut self.front {
|
||||
Front::H264(f) => f.reset(),
|
||||
Front::Hevc(f) => f.reset(),
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn is_random_access(&self, sample: &[u8]) -> Option<bool> {
|
||||
self.info.is_random_access(sample)
|
||||
}
|
||||
|
||||
fn is_disposable(&self, sample: &[u8]) -> bool {
|
||||
self.info.is_disposable(sample)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
//! 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
|
||||
//! lists have the active number of entries, and slice data offsets lie inside the slice.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
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::Front;
|
||||
use crate::biplanar::{self, Biplanar, Geometry};
|
||||
|
||||
/// Records what it is asked to do and checks it; "decodes" nothing.
|
||||
struct Recorder {
|
||||
ids: Vec<ffi::VASurfaceID>,
|
||||
decoded: Vec<bool>,
|
||||
pictures: usize,
|
||||
frame: VideoFrame,
|
||||
}
|
||||
|
||||
impl Recorder {
|
||||
fn new(count: usize) -> Self {
|
||||
let g = Geometry { crop: (0, 0, 2, 2), bits: 8, color: Default::default(), par: (1, 1) };
|
||||
let frame = biplanar::to_frame(&Biplanar { luma: &[128; 4], chroma: &[128; 2], stride: 2, rows: 2 }, &g).unwrap();
|
||||
Self { ids: (0..count as u32).map(|i| 0x100 + i).collect(), decoded: vec![false; count], pictures: 0, frame }
|
||||
}
|
||||
}
|
||||
|
||||
fn valid(p: &VAPictureH264) -> bool {
|
||||
p.flags & ffi::VA_PICTURE_H264_INVALID == 0
|
||||
}
|
||||
|
||||
impl Accel for Recorder {
|
||||
fn surfaces(&self) -> &[ffi::VASurfaceID] {
|
||||
&self.ids
|
||||
}
|
||||
|
||||
fn decode_hevc(
|
||||
&mut self,
|
||||
target: usize,
|
||||
pic: &ffi::VAPictureParameterBufferHEVC,
|
||||
_iq: Option<&ffi::VAIQMatrixBufferHEVC>,
|
||||
slices: &[(ffi::VASliceParameterBufferHEVC, Vec<u8>)],
|
||||
) -> 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,
|
||||
_iq: &VAIQMatrixBufferH264,
|
||||
slices: &[(VASliceParameterBufferH264, Vec<u8>)],
|
||||
) -> Result<(), String> {
|
||||
assert_eq!(pic.CurrPic.picture_id, self.ids[target], "current picture surface");
|
||||
let refs: Vec<_> = pic.ReferenceFrames.iter().filter(|r| valid(r)).collect();
|
||||
let ref_ids: BTreeSet<_> = refs.iter().map(|r| r.picture_id).collect();
|
||||
assert_eq!(ref_ids.len(), refs.len(), "each reference frame once");
|
||||
assert!(!ref_ids.contains(&pic.CurrPic.picture_id), "the target is not a reference");
|
||||
for r in &refs {
|
||||
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");
|
||||
assert!(r.flags & (ffi::VA_PICTURE_H264_SHORT_TERM_REFERENCE | ffi::VA_PICTURE_H264_LONG_TERM_REFERENCE) != 0, "reference flags");
|
||||
}
|
||||
assert!(!slices.is_empty());
|
||||
for (s, data) in slices {
|
||||
assert_eq!(s.slice_data_size as usize, data.len());
|
||||
assert!((s.slice_data_bit_offset as usize) < data.len() * 8, "slice data starts inside the slice");
|
||||
let ty = s.slice_type;
|
||||
let lists: &[(&[VAPictureH264; 32], u8)] = match ty {
|
||||
2 => &[],
|
||||
0 => &[(&s.RefPicList0, s.num_ref_idx_l0_active_minus1)],
|
||||
_ => &[(&s.RefPicList0, s.num_ref_idx_l0_active_minus1), (&s.RefPicList1, s.num_ref_idx_l1_active_minus1)],
|
||||
};
|
||||
for (list, minus1) in lists {
|
||||
let n = *minus1 as usize + 1;
|
||||
assert!(list[..n].iter().all(valid), "{n} active entries");
|
||||
assert!(list[n..].iter().all(|p| !valid(p)), "nothing past the active entries");
|
||||
assert!(list[..n].iter().all(|p| ref_ids.contains(&p.picture_id)), "list entries are reference frames");
|
||||
}
|
||||
}
|
||||
self.decoded[target] = true;
|
||||
self.pictures += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(&mut self, index: usize) -> Result<VideoFrame, String> {
|
||||
assert!(self.decoded[index], "read back a decoded surface");
|
||||
Ok(self.frame.clone())
|
||||
}
|
||||
|
||||
fn fill(&mut self, index: usize, from: Option<usize>) -> Result<(), String> {
|
||||
assert!(from.is_none_or(|f| self.decoded[f]));
|
||||
self.decoded[index] = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// (file, x264 profile, x264 parameters).
|
||||
const STREAMS: &[(&str, &str, &str)] = &[
|
||||
("va_front_high.mp4", "high", "bframes=3:b-pyramid=normal:keyint=12:min-keyint=12:scenecut=0:ref=4:weightp=2"),
|
||||
("va_front_slices.mp4", "baseline", "keyint=12:min-keyint=12:scenecut=0:slices=4:ref=2"),
|
||||
("va_front_open_gop.mp4", "high", "bframes=3:b-pyramid=strict:keyint=12:min-keyint=12:scenecut=0:open-gop=1:direct=temporal"),
|
||||
];
|
||||
|
||||
fn fixture(ff: &std::path::Path, name: &str, profile: &str, params: &str) -> Option<PathBuf> {
|
||||
let out = filmcraft_testkit::fixtures_dir("platform").join(name);
|
||||
let src = "testsrc2=s=320x184:r=24:d=2";
|
||||
filmcraft_testkit::fixtures::generate(&out, |tmp| {
|
||||
Command::new(ff)
|
||||
.args(["-y", "-v", "error", "-f", "lavfi", "-i", src, "-c:v", "libx264", "-profile:v", profile, "-x264-params", params, "-pix_fmt", "yuv420p"])
|
||||
.arg(tmp)
|
||||
.stdin(Stdio::null())
|
||||
.status()
|
||||
.is_ok_and(|s| s.success())
|
||||
})
|
||||
}
|
||||
|
||||
struct Stream {
|
||||
entry: filmcraft_isobmff::SampleEntry,
|
||||
samples: Vec<(Vec<u8>, i64)>,
|
||||
sync: Vec<bool>,
|
||||
}
|
||||
|
||||
fn read_stream(path: &std::path::Path) -> Stream {
|
||||
let bytes = std::fs::read(path).unwrap();
|
||||
let file = filmcraft_isobmff::open(bytes.clone()).unwrap();
|
||||
let t = file.track_of_kind(filmcraft_isobmff::TrackKind::Video).unwrap();
|
||||
let track = &file.tracks[t];
|
||||
let samples = (0..track.samples.len()).map(|i| (file.read_sample(&bytes, t, i).unwrap(), track.samples[i].pts)).collect();
|
||||
Stream { entry: track.entries[0].clone(), samples, sync: track.samples.iter().map(|s| s.is_sync).collect() }
|
||||
}
|
||||
|
||||
fn front(s: &Stream) -> Front<Recorder> {
|
||||
let info = NalStreamInfo::from_entry(&s.entry).unwrap().unwrap();
|
||||
let mbs = (info.coded.0 / 16, info.coded.1 / 16);
|
||||
Front::new(Recorder::new(18), info.length_size, mbs, &info.parameter_sets).unwrap()
|
||||
}
|
||||
|
||||
fn pts(frames: &[DecodedFrame]) -> Vec<i64> {
|
||||
frames.iter().map(|f| f.pts).collect()
|
||||
}
|
||||
|
||||
fn run_front(f: &mut Front<Recorder>, samples: &[(Vec<u8>, i64)]) -> Vec<i64> {
|
||||
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
|
||||
}
|
||||
|
||||
fn run_software(d: &mut dyn filmcraft_codecs::VideoDecoder, samples: &[(Vec<u8>, i64)]) -> Vec<i64> {
|
||||
let mut out = Vec::new();
|
||||
for (s, p) in samples {
|
||||
out.extend(pts(&d.decode(s, *p).unwrap()));
|
||||
}
|
||||
out.extend(pts(&d.flush()));
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_order_and_buffers_match_the_software_decoder() {
|
||||
let ff = filmcraft_testkit::require_ffmpeg!();
|
||||
for (name, profile, params) in STREAMS {
|
||||
let Some(path) = fixture(&ff, name, profile, params) else { continue };
|
||||
let s = read_stream(&path);
|
||||
let mut f = front(&s);
|
||||
let mut sw = filmcraft_codecs::software_video_decoder(&s.entry).unwrap();
|
||||
let a = run_front(&mut f, &s.samples);
|
||||
assert_eq!(a, run_software(sw.as_mut(), &s.samples), "{name}");
|
||||
assert_eq!(f.accel().pictures, s.samples.len(), "{name}: every picture decoded");
|
||||
// seeks to every sync sample (open-GOP I pictures included: frame_num gap stand-ins)
|
||||
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_front(&mut f, &s.samples[k..end]), run_software(sw.as_mut(), &s.samples[k..end]), "{name} from sample {k}");
|
||||
}
|
||||
// a flush mid-GOP, then on
|
||||
f.reset();
|
||||
sw.reset();
|
||||
let mid = s.samples.len() / 2 + 1;
|
||||
let mut a = run_front(&mut f, &s.samples[..mid]);
|
||||
a.extend(run_front(&mut f, &s.samples[mid..]));
|
||||
let mut b = run_software(sw.as_mut(), &s.samples[..mid]);
|
||||
b.extend(run_software(sw.as_mut(), &s.samples[mid..]));
|
||||
assert_eq!(a, b, "{name} with a flush at {mid}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Damaged samples give errors (the hybrid decoder then continues in software), never a panic.
|
||||
#[test]
|
||||
fn damaged_samples_are_errors() {
|
||||
let ff = filmcraft_testkit::require_ffmpeg!();
|
||||
let (name, profile, params) = STREAMS[0];
|
||||
let Some(path) = fixture(&ff, name, profile, params) else { return };
|
||||
let s = read_stream(&path);
|
||||
let mut seed = 0x2545_F491_4F6C_DD1Du64;
|
||||
let mut next = || {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
seed
|
||||
};
|
||||
for round in 0..40 {
|
||||
let mut f = 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// (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<PathBuf> {
|
||||
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<Recorder> {
|
||||
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<Recorder>, samples: &[(Vec<u8>, i64)]) -> Vec<i64> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
//! libva itself (Linux): `libva.so.2` and `libva-drm.so.2` are loaded at run time (a system without
|
||||
//! them simply has no hardware decoder), a display is opened on a DRM render node, and a [`Session`]
|
||||
//! (decode configuration, context and surfaces for one stream) decodes pictures and reads them back
|
||||
//! as NV12 images.
|
||||
//!
|
||||
//! FFI module (docs/adr/0001-platform-ffi.md): every `unsafe` block has a `// SAFETY:` comment, the
|
||||
//! public API is safe, and the one callback into Rust (libva's error messages) runs under
|
||||
//! `catch_unwind`.
|
||||
|
||||
use std::ffi::{CStr, c_char, c_int, c_uint, c_void};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use filmcraft_frame::VideoFrame;
|
||||
use libloading::Library;
|
||||
|
||||
use super::Accel;
|
||||
use super::ffi::{self, VAConfigAttrib, VAImage, VAImageFormat, VASurfaceID};
|
||||
use crate::biplanar::{self, Biplanar, Geometry};
|
||||
|
||||
/// The libva entry points (the libraries stay loaded for the life of the process).
|
||||
struct Api {
|
||||
_va: Library,
|
||||
_drm: Library,
|
||||
initialize: ffi::vaInitialize,
|
||||
terminate: ffi::vaTerminate,
|
||||
error_str: ffi::vaErrorStr,
|
||||
vendor_string: ffi::vaQueryVendorString,
|
||||
set_info_callback: ffi::vaSetInfoCallback,
|
||||
set_error_callback: ffi::vaSetErrorCallback,
|
||||
get_config_attributes: ffi::vaGetConfigAttributes,
|
||||
create_config: ffi::vaCreateConfig,
|
||||
destroy_config: ffi::vaDestroyConfig,
|
||||
create_surfaces: ffi::vaCreateSurfaces,
|
||||
destroy_surfaces: ffi::vaDestroySurfaces,
|
||||
create_context: ffi::vaCreateContext,
|
||||
destroy_context: ffi::vaDestroyContext,
|
||||
create_buffer: ffi::vaCreateBuffer,
|
||||
destroy_buffer: ffi::vaDestroyBuffer,
|
||||
begin_picture: ffi::vaBeginPicture,
|
||||
render_picture: ffi::vaRenderPicture,
|
||||
end_picture: ffi::vaEndPicture,
|
||||
sync_surface: ffi::vaSyncSurface,
|
||||
create_image: ffi::vaCreateImage,
|
||||
destroy_image: ffi::vaDestroyImage,
|
||||
get_image: ffi::vaGetImage,
|
||||
put_image: ffi::vaPutImage,
|
||||
map_buffer: ffi::vaMapBuffer,
|
||||
unmap_buffer: ffi::vaUnmapBuffer,
|
||||
get_display_drm: ffi::vaGetDisplayDRM,
|
||||
}
|
||||
|
||||
static API: OnceLock<Result<Api, String>> = OnceLock::new();
|
||||
|
||||
fn api() -> Result<&'static Api, String> {
|
||||
API.get_or_init(load).as_ref().map_err(Clone::clone)
|
||||
}
|
||||
|
||||
fn load() -> Result<Api, String> {
|
||||
// SAFETY: loading libva runs its library initialisers, which only set up libva's own state;
|
||||
// it is the system's VA-API runtime, loaded the way every VA-API client loads it.
|
||||
let va = unsafe { Library::new("libva.so.2") }.map_err(|e| format!("libva.so.2: {e}"))?;
|
||||
// SAFETY: as above, for libva's DRM display library.
|
||||
let drm = unsafe { Library::new("libva-drm.so.2") }.map_err(|e| format!("libva-drm.so.2: {e}"))?;
|
||||
macro_rules! sym {
|
||||
($lib:expr, $name:ident) => {{
|
||||
// SAFETY: `ffi::$name` is the C signature of the libva function of that name,
|
||||
// transcribed from va.h and checked against it (abi_tests.rs). The pointer is copied
|
||||
// out; the library it points into is kept loaded in `Api` for the life of the process.
|
||||
let s = unsafe { $lib.get::<ffi::$name>(concat!(stringify!($name), "\0").as_bytes()) };
|
||||
*s.map_err(|e| format!("{}: {e}", stringify!($name)))?
|
||||
}};
|
||||
}
|
||||
Ok(Api {
|
||||
initialize: sym!(va, vaInitialize),
|
||||
terminate: sym!(va, vaTerminate),
|
||||
error_str: sym!(va, vaErrorStr),
|
||||
vendor_string: sym!(va, vaQueryVendorString),
|
||||
set_info_callback: sym!(va, vaSetInfoCallback),
|
||||
set_error_callback: sym!(va, vaSetErrorCallback),
|
||||
get_config_attributes: sym!(va, vaGetConfigAttributes),
|
||||
create_config: sym!(va, vaCreateConfig),
|
||||
destroy_config: sym!(va, vaDestroyConfig),
|
||||
create_surfaces: sym!(va, vaCreateSurfaces),
|
||||
destroy_surfaces: sym!(va, vaDestroySurfaces),
|
||||
create_context: sym!(va, vaCreateContext),
|
||||
destroy_context: sym!(va, vaDestroyContext),
|
||||
create_buffer: sym!(va, vaCreateBuffer),
|
||||
destroy_buffer: sym!(va, vaDestroyBuffer),
|
||||
begin_picture: sym!(va, vaBeginPicture),
|
||||
render_picture: sym!(va, vaRenderPicture),
|
||||
end_picture: sym!(va, vaEndPicture),
|
||||
sync_surface: sym!(va, vaSyncSurface),
|
||||
create_image: sym!(va, vaCreateImage),
|
||||
destroy_image: sym!(va, vaDestroyImage),
|
||||
get_image: sym!(va, vaGetImage),
|
||||
put_image: sym!(va, vaPutImage),
|
||||
map_buffer: sym!(va, vaMapBuffer),
|
||||
unmap_buffer: sym!(va, vaUnmapBuffer),
|
||||
get_display_drm: sym!(drm, vaGetDisplayDRM),
|
||||
_va: va,
|
||||
_drm: drm,
|
||||
})
|
||||
}
|
||||
|
||||
/// libva's error messages, into our log (instead of stderr).
|
||||
extern "C" fn on_error(_user: *mut c_void, message: *const c_char) {
|
||||
let _ = std::panic::catch_unwind(|| {
|
||||
if message.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: libva passes a NUL-terminated message that is valid for the duration of the call.
|
||||
let text = unsafe { CStr::from_ptr(message) }.to_string_lossy();
|
||||
log::warn!("libva: {}", text.trim_end());
|
||||
});
|
||||
}
|
||||
|
||||
/// An initialised VA display on a DRM render node.
|
||||
pub struct Display {
|
||||
api: &'static Api,
|
||||
dpy: ffi::VADisplay,
|
||||
vendor: String,
|
||||
/// The render node; open for as long as the display uses it.
|
||||
_device: File,
|
||||
}
|
||||
|
||||
// SAFETY: a VA display is not tied to the thread that opened it, and a `Display` is only used
|
||||
// through its single owner (`&mut` or `&` of one `Session` at a time), never concurrently.
|
||||
unsafe impl Send for Display {}
|
||||
|
||||
impl Display {
|
||||
/// The first DRM render node with a working VA-API driver.
|
||||
pub fn open() -> Result<Self, String> {
|
||||
let api = api()?;
|
||||
let mut why = "no DRM render node (/dev/dri/renderD*)".to_string();
|
||||
for n in 128..144 {
|
||||
let path = format!("/dev/dri/renderD{n}");
|
||||
let Ok(file) = OpenOptions::new().read(true).write(true).open(&path) else { continue };
|
||||
match Self::init(api, file) {
|
||||
Ok(d) => return Ok(d),
|
||||
Err(e) => why = format!("{path}: {e}"),
|
||||
}
|
||||
}
|
||||
Err(why)
|
||||
}
|
||||
|
||||
fn init(api: &'static Api, device: File) -> Result<Self, String> {
|
||||
// SAFETY: the descriptor is an open render node, kept open by `device` (moved into the
|
||||
// returned display) for as long as the display exists.
|
||||
let dpy = unsafe { (api.get_display_drm)(device.as_raw_fd()) };
|
||||
if dpy.is_null() {
|
||||
return Err("vaGetDisplayDRM failed".into());
|
||||
}
|
||||
// From here on `Drop` terminates the display.
|
||||
let mut d = Self { api, dpy, vendor: String::new(), _device: device };
|
||||
// SAFETY: `dpy` is a display from vaGetDisplayDRM; a null info callback turns libva's info
|
||||
// messages off, and `on_error` matches `VAMessageCallback` and never unwinds.
|
||||
unsafe {
|
||||
(api.set_info_callback)(dpy, None, std::ptr::null_mut());
|
||||
(api.set_error_callback)(dpy, Some(on_error), std::ptr::null_mut());
|
||||
}
|
||||
let (mut major, mut minor): (c_int, c_int) = (0, 0);
|
||||
// SAFETY: `dpy` is valid; the version outputs point to live locals.
|
||||
let st = unsafe { (api.initialize)(dpy, &mut major, &mut minor) };
|
||||
d.check(st, "vaInitialize")?;
|
||||
// SAFETY: `dpy` is an initialised display; the vendor string is NUL-terminated and owned by
|
||||
// the driver for the life of the display (copied out here).
|
||||
let vendor = unsafe { (api.vendor_string)(dpy) };
|
||||
if !vendor.is_null() {
|
||||
// SAFETY: non-null, NUL-terminated (see above).
|
||||
d.vendor = unsafe { CStr::from_ptr(vendor) }.to_string_lossy().into_owned();
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// The driver's description ("Intel iHD driver …").
|
||||
pub fn vendor(&self) -> &str {
|
||||
&self.vendor
|
||||
}
|
||||
|
||||
fn check(&self, st: ffi::VAStatus, what: &str) -> Result<(), String> {
|
||||
if st == ffi::VA_STATUS_SUCCESS {
|
||||
return Ok(());
|
||||
}
|
||||
// SAFETY: vaErrorStr takes any status and returns a static NUL-terminated string.
|
||||
let text = unsafe { (self.api.error_str)(st) };
|
||||
let text = if text.is_null() {
|
||||
String::new()
|
||||
} else {
|
||||
// SAFETY: non-null static NUL-terminated string (see above).
|
||||
unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned()
|
||||
};
|
||||
Err(format!("{what}: {text} ({st:#x})"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Display {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: `dpy` came from vaGetDisplayDRM and is terminated exactly once, here, after
|
||||
// every session object made on it was destroyed (`Session` drops its objects first).
|
||||
unsafe { (self.api.terminate)(self.dpy) };
|
||||
}
|
||||
}
|
||||
|
||||
/// One stream's decoder: a VLD configuration of a profile, its surfaces and context, and the image
|
||||
/// pictures are read back through.
|
||||
pub struct Session {
|
||||
display: Display,
|
||||
config: ffi::VAConfigID,
|
||||
context: ffi::VAContextID,
|
||||
surfaces: Vec<VASurfaceID>,
|
||||
image: Option<VAImage>,
|
||||
/// Coded size of the surfaces (luma samples).
|
||||
size: (u32, u32),
|
||||
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, 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<Self, String> {
|
||||
let (rt_format, _, _) = formats(geometry.bits)?;
|
||||
let display = Display::open()?;
|
||||
let api = display.api;
|
||||
let dpy = display.dpy;
|
||||
let mut config = None;
|
||||
let mut why = "no VA-API profile for the stream".to_string();
|
||||
for &profile in profiles {
|
||||
let mut attr = VAConfigAttrib { type_: ffi::VAConfigAttribRTFormat, value: 0 };
|
||||
// SAFETY: `dpy` is initialised; `attr` is one live attribute (count 1).
|
||||
let st = unsafe { (api.get_config_attributes)(dpy, profile, ffi::VAEntrypointVLD, &mut attr, 1) };
|
||||
if let Err(e) = display.check(st, "vaGetConfigAttributes") {
|
||||
why = e;
|
||||
continue;
|
||||
}
|
||||
if attr.value & rt_format == 0 {
|
||||
why = format!("profile {profile} does not decode to {}-bit 4:2:0", geometry.bits);
|
||||
continue;
|
||||
}
|
||||
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) };
|
||||
match display.check(st, "vaCreateConfig") {
|
||||
Ok(()) => {
|
||||
config = Some(id);
|
||||
break;
|
||||
}
|
||||
Err(e) => why = e,
|
||||
}
|
||||
}
|
||||
let config = config.ok_or(why)?;
|
||||
// From here on `Drop` releases what was made.
|
||||
let mut s = Self { display, config, context: ffi::VA_INVALID_ID, surfaces: Vec::new(), image: None, size, geometry };
|
||||
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, 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")?);
|
||||
let n = c_int::try_from(count).map_err(|_| "too many surfaces")?;
|
||||
let mut context = ffi::VA_INVALID_ID;
|
||||
// SAFETY: `config` and the `count` surfaces were made on this display; `context` is a live output.
|
||||
let st = unsafe { (api.create_context)(dpy, s.config, w, h, ffi::VA_PROGRESSIVE, s.surfaces.as_mut_ptr(), n, &mut context) };
|
||||
s.display.check(st, "vaCreateContext")?;
|
||||
s.context = context;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn vendor(&self) -> &str {
|
||||
self.display.vendor()
|
||||
}
|
||||
|
||||
/// A buffer of `ty` holding a copy of `len` bytes at `data`.
|
||||
fn buffer(&self, ty: ffi::VABufferType, data: *const c_void, len: usize) -> Result<ffi::VABufferID, String> {
|
||||
let size = c_uint::try_from(len).map_err(|_| "buffer too large")?;
|
||||
let mut id = ffi::VA_INVALID_ID;
|
||||
// SAFETY: `data` points to `len` readable bytes (the callers pass a live value or slice);
|
||||
// libva copies them into the new buffer and never writes through the pointer.
|
||||
let st = unsafe { (self.display.api.create_buffer)(self.display.dpy, self.context, ty, size, 1, data.cast_mut(), &mut id) };
|
||||
self.display.check(st, "vaCreateBuffer")?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn value_buffer<T>(&self, ty: ffi::VABufferType, value: &T) -> Result<ffi::VABufferID, String> {
|
||||
self.buffer(ty, (value as *const T).cast(), std::mem::size_of::<T>())
|
||||
}
|
||||
|
||||
fn render(&self, buffers: &mut [ffi::VABufferID]) -> Result<(), String> {
|
||||
let n = c_int::try_from(buffers.len()).map_err(|_| "too many buffers")?;
|
||||
// SAFETY: `buffers` are live buffer ids made on this context, `n` of them.
|
||||
let st = unsafe { (self.display.api.render_picture)(self.display.dpy, self.context, buffers.as_mut_ptr(), n) };
|
||||
self.display.check(st, "vaRenderPicture")
|
||||
}
|
||||
|
||||
/// 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<P, Q, S>(
|
||||
&self,
|
||||
surface: VASurfaceID,
|
||||
buffers: &mut Vec<ffi::VABufferID>,
|
||||
pic: &P,
|
||||
iq: Option<&Q>,
|
||||
slices: &[(S, Vec<u8>)],
|
||||
) -> Result<(), String> {
|
||||
buffers.push(self.value_buffer(ffi::VAPictureParameterBufferType, pic)?);
|
||||
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())?);
|
||||
}
|
||||
let api = self.display.api;
|
||||
// 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 (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;
|
||||
}
|
||||
rendered = self.render(pair);
|
||||
}
|
||||
// a begun picture is always ended, so the context stays usable
|
||||
// SAFETY: the picture was begun on this context above.
|
||||
let st = unsafe { (api.end_picture)(self.display.dpy, self.context) };
|
||||
rendered?;
|
||||
self.display.check(st, "vaEndPicture")
|
||||
}
|
||||
|
||||
fn image(&mut self) -> Result<VAImage, String> {
|
||||
if let Some(image) = self.image {
|
||||
return Ok(image);
|
||||
}
|
||||
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.
|
||||
let st = unsafe { (self.display.api.create_image)(self.display.dpy, &mut format, w, h, &mut image) };
|
||||
self.display.check(st, "vaCreateImage")?;
|
||||
self.image = Some(image);
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
/// The picture in surface `index`, copied into the session's image once decoded.
|
||||
fn get_image(&mut self, index: usize) -> Result<VAImage, String> {
|
||||
let surface = *self.surfaces.get(index).ok_or("surface out of range")?;
|
||||
let image = self.image()?;
|
||||
let api = self.display.api;
|
||||
// SAFETY: `surface` is one of this session's surfaces.
|
||||
let st = unsafe { (api.sync_surface)(self.display.dpy, surface) };
|
||||
self.display.check(st, "vaSyncSurface")?;
|
||||
// SAFETY: the image and the surface were made on this display, with the same size.
|
||||
let st = unsafe { (api.get_image)(self.display.dpy, surface, 0, 0, self.size.0, self.size.1, image.image_id) };
|
||||
self.display.check(st, "vaGetImage")?;
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
/// 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();
|
||||
// SAFETY: `image.buf` is the data buffer of an image made on this display; `ptr` is a live output.
|
||||
let st = unsafe { (api.map_buffer)(self.display.dpy, image.buf, &mut ptr) };
|
||||
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; the slice does not outlive this block.
|
||||
let data = unsafe { std::slice::from_raw_parts_mut(ptr.cast::<u8>(), 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) };
|
||||
self.display.check(st, "vaUnmapBuffer")?;
|
||||
if ptr.is_null() { Err("vaMapBuffer returned no data".into()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
fn decode_picture<P, Q, S>(&mut self, target: usize, pic: &P, iq: Option<&Q>, slices: &[(S, Vec<u8>)]) -> 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<VideoFrame, String> {
|
||||
let api = self.display.api;
|
||||
let mut ptr: *mut c_void = std::ptr::null_mut();
|
||||
// SAFETY: `image.buf` is the data buffer of an image made on this display; `ptr` is a live output.
|
||||
let st = unsafe { (api.map_buffer)(self.display.dpy, image.buf, &mut ptr) };
|
||||
self.display.check(st, "vaMapBuffer")?;
|
||||
let result = if ptr.is_null() {
|
||||
Err("vaMapBuffer returned no data".to_string())
|
||||
} else {
|
||||
// SAFETY: a mapped image buffer holds `data_size` bytes at `ptr` until it is unmapped
|
||||
// below; the slice does not outlive this block.
|
||||
let data = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>().cast_const(), image.data_size as usize) };
|
||||
biplanar_view(image, data).and_then(|b| biplanar::to_frame(&b, &self.geometry))
|
||||
};
|
||||
// SAFETY: the buffer was mapped above; `data` is no longer used.
|
||||
let st = unsafe { (api.unmap_buffer)(self.display.dpy, image.buf) };
|
||||
let frame = result?;
|
||||
self.display.check(st, "vaUnmapBuffer")?;
|
||||
Ok(frame)
|
||||
}
|
||||
}
|
||||
|
||||
/// The two planes of a mapped NV12 / P010 image.
|
||||
fn biplanar_view<'a>(image: &VAImage, data: &'a [u8]) -> Result<Biplanar<'a>, String> {
|
||||
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 {
|
||||
return Err(format!("NV12 planes with different pitches ({p0}, {p1})"));
|
||||
}
|
||||
let luma = data.get(image.offsets[0] as usize..).ok_or("NV12 luma offset out of range")?;
|
||||
let chroma = data.get(image.offsets[1] as usize..).ok_or("NV12 chroma offset out of range")?;
|
||||
Ok(Biplanar { luma, chroma, stride: p0 as usize, rows: usize::from(image.height) })
|
||||
}
|
||||
|
||||
impl Accel for Session {
|
||||
fn surfaces(&self) -> &[VASurfaceID] {
|
||||
&self.surfaces
|
||||
}
|
||||
|
||||
fn decode_h264(
|
||||
&mut self,
|
||||
target: usize,
|
||||
pic: &ffi::VAPictureParameterBufferH264,
|
||||
iq: &ffi::VAIQMatrixBufferH264,
|
||||
slices: &[(ffi::VASliceParameterBufferH264, Vec<u8>)],
|
||||
) -> Result<(), String> {
|
||||
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<u8>)],
|
||||
) -> Result<(), String> {
|
||||
self.decode_picture(target, pic, iq, slices)
|
||||
}
|
||||
|
||||
fn read(&mut self, index: usize) -> Result<VideoFrame, String> {
|
||||
let image = self.get_image(index)?;
|
||||
self.read_image(&image)
|
||||
}
|
||||
|
||||
fn fill(&mut self, index: usize, from: Option<usize>) -> Result<(), String> {
|
||||
let surface = *self.surfaces.get(index).ok_or("surface out of range")?;
|
||||
let image = match from {
|
||||
Some(from) => self.get_image(from)?,
|
||||
None => {
|
||||
let image = self.image()?;
|
||||
self.write_gray(&image)?;
|
||||
image
|
||||
}
|
||||
};
|
||||
let (w, h) = self.size;
|
||||
// SAFETY: the image and the surface were made on this display, with the same size.
|
||||
let st = unsafe { (self.display.api.put_image)(self.display.dpy, surface, image.image_id, 0, 0, w, h, 0, 0, w, h) };
|
||||
self.display.check(st, "vaPutImage")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Session {
|
||||
fn drop(&mut self) {
|
||||
let (api, dpy) = (self.display.api, self.display.dpy);
|
||||
// SAFETY: each object below was made on this display and is destroyed once; the display
|
||||
// itself is terminated afterwards (when the `display` field is dropped).
|
||||
unsafe {
|
||||
if let Some(image) = self.image.take() {
|
||||
(api.destroy_image)(dpy, image.image_id);
|
||||
}
|
||||
if self.context != ffi::VA_INVALID_ID {
|
||||
(api.destroy_context)(dpy, self.context);
|
||||
}
|
||||
if !self.surfaces.is_empty() {
|
||||
let n = c_int::try_from(self.surfaces.len()).unwrap_or(0);
|
||||
(api.destroy_surfaces)(dpy, self.surfaces.as_mut_ptr(), n);
|
||||
}
|
||||
if self.config != ffi::VA_INVALID_ID {
|
||||
(api.destroy_config)(dpy, self.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this system has a working VA-API driver on a DRM render node (checked once), with the
|
||||
/// driver's description, or why not.
|
||||
pub fn probe() -> &'static Result<String, String> {
|
||||
static PROBE: OnceLock<Result<String, String>> = OnceLock::new();
|
||||
PROBE.get_or_init(|| Display::open().map(|d| d.vendor().to_string()))
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
//! 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 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")]
|
||||
|
||||
mod common;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use common::*;
|
||||
use filmcraft_codecs::VideoDecoder;
|
||||
use filmcraft_codecs::hw::NalStreamInfo;
|
||||
use filmcraft_platform::HybridDecoder;
|
||||
use filmcraft_platform::vaapi::VaDecoder;
|
||||
|
||||
/// The hardware decoder for a stream, or `None` (skip) when this machine has none for it.
|
||||
fn hardware(s: &Stream) -> Option<Box<dyn VideoDecoder>> {
|
||||
match filmcraft_platform::vaapi_factory(&s.entry) {
|
||||
Some(Ok(d)) => {
|
||||
assert!(d.name().starts_with("VA-API"), "{}", d.name());
|
||||
Some(d)
|
||||
}
|
||||
Some(Err(e)) => panic!("factory error: {e}"),
|
||||
None => {
|
||||
eprintln!("SKIPPED: no VA-API hardware decoder for {}", s.entry.codec.name());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn software(s: &Stream) -> Box<dyn VideoDecoder> {
|
||||
filmcraft_codecs::software_video_decoder(&s.entry).unwrap()
|
||||
}
|
||||
|
||||
/// H.264 fixtures for the stream features the hardware path takes: (file, x264 profile, x264
|
||||
/// parameters). All 640x360 (coded 368: cropping), three seconds at 24 fps, keyframes every second.
|
||||
const H264: &[(&str, &str, &str)] = &[
|
||||
("va_h264_cbp_slices.mp4", "baseline", "keyint=24:min-keyint=24:scenecut=0:slices=3:ref=3"),
|
||||
("va_h264_main_weighted.mp4", "main", "bframes=3:b-pyramid=normal:keyint=24:min-keyint=24:scenecut=0:ref=4:weightp=2:weightb=1"),
|
||||
("va_h264_temporal_direct.mp4", "high", "bframes=2:b-pyramid=strict:direct=temporal:keyint=24:min-keyint=24:scenecut=0:ref=3:weightb=0"),
|
||||
("va_h264_cqm_jvt.mp4", "high", "bframes=3:keyint=24:min-keyint=24:scenecut=0:cqm=jvt:8x8dct=1:slices=2"),
|
||||
("va_h264_open_gop.mp4", "high", "bframes=3:b-pyramid=normal:keyint=24:min-keyint=24:scenecut=0:open-gop=1"),
|
||||
];
|
||||
|
||||
fn h264_fixture(ff: &std::path::Path, name: &str, profile: &str, params: &str) -> Option<std::path::PathBuf> {
|
||||
let src = "testsrc2=s=640x360:r=24:d=3,noise=alls=12:allf=t";
|
||||
fixture(ff, name, &["-f", "lavfi", "-i", src, "-c:v", "libx264", "-profile:v", profile, "-preset", "fast", "-x264-params", params, "-pix_fmt", "yuv420p"])
|
||||
}
|
||||
|
||||
/// 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<std::path::PathBuf> {
|
||||
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();
|
||||
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
|
||||
}
|
||||
|
||||
/// Every picture of `s` from the hardware decoder equals the software decoder's: whole stream,
|
||||
/// after `reset` + reseek to each later sync sample, after a mid-stream `flush`, and a full pass
|
||||
/// after all the resets.
|
||||
fn parity(name: &str, s: &Stream) {
|
||||
let Some(mut hw) = hardware(s) else { return };
|
||||
let mut sw = software(s);
|
||||
let a = decode_all(hw.as_mut(), &s.samples);
|
||||
let b = decode_all(sw.as_mut(), &s.samples);
|
||||
assert!(a.len() >= 40, "{name}: {} pictures", a.len());
|
||||
assert_same(name, &a, &b);
|
||||
assert!(hw.name().starts_with("VA-API"), "{name}: stayed in hardware ({})", hw.name());
|
||||
|
||||
// reset + reseek to each later sync sample, decode a stretch. After a seek to a non-IDR I picture
|
||||
// (open GOP), the pictures shown before it are predicted from frames that were never decoded:
|
||||
// both decoders conceal them (the software decoder from mid-gray stand-ins, the hardware also
|
||||
// from motion data those stand-ins do not have), so only the pictures from the seek point on
|
||||
// must match, as they must.
|
||||
let info = NalStreamInfo::from_entry(&s.entry).unwrap().unwrap();
|
||||
let syncs: Vec<usize> = (1..s.samples.len()).filter(|&i| s.sync[i]).collect();
|
||||
assert!(!syncs.is_empty(), "{name}: more than one GOP");
|
||||
for &k in syncs.iter().rev() {
|
||||
let end = (k + 17).min(s.samples.len());
|
||||
for d in [&mut hw, &mut sw] {
|
||||
d.reset();
|
||||
}
|
||||
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.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);
|
||||
}
|
||||
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: 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();
|
||||
}
|
||||
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);
|
||||
}
|
||||
// back to the start after seeking around: the whole stream again
|
||||
hw.reset();
|
||||
sw.reset();
|
||||
assert_same(&format!("{name} after resets"), &decode_all(hw.as_mut(), &s.samples), &decode_all(sw.as_mut(), &s.samples));
|
||||
assert!(hw.name().starts_with("VA-API"), "{name}: stayed in hardware ({})", hw.name());
|
||||
// random-access / disposable answers match the software decoder's
|
||||
for (smp, _) in &s.samples {
|
||||
assert_eq!(hw.is_random_access(smp), sw.is_random_access(smp), "{name}");
|
||||
assert_eq!(hw.is_disposable(smp), sw.is_disposable(smp), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// [`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<filmcraft_codecs::DecodedFrame>,
|
||||
b: &mut std::collections::VecDeque<filmcraft_codecs::DecodedFrame>| {
|
||||
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!();
|
||||
let streams = small_streams(&ff);
|
||||
assert!(streams.len() > H264.len(), "fixtures");
|
||||
for (name, s) in streams {
|
||||
parity(&name, &s);
|
||||
}
|
||||
}
|
||||
|
||||
fn large(size: &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 args = [
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
&src,
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-profile:v",
|
||||
"high",
|
||||
"-preset",
|
||||
"veryfast",
|
||||
"-x264-params",
|
||||
"bframes=3:b-pyramid=normal:keyint=24:min-keyint=24:scenecut=0",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
];
|
||||
let Some(path) = fixture(&ff, &format!("h264_{h}.mp4"), &args) else { return };
|
||||
parity_streaming(&format!("h264 {size}"), &read_stream(&path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn h264_1080p_and_2160p_are_bit_exact() {
|
||||
large("1920x1080");
|
||||
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!();
|
||||
for (name, s) in small_streams(&ff) {
|
||||
let name = name.as_str();
|
||||
let info = NalStreamInfo::from_entry(&s.entry).unwrap().unwrap();
|
||||
let Ok(_) = VaDecoder::new(info.clone()) else {
|
||||
eprintln!("SKIPPED: no VA-API hardware decoder for {name}");
|
||||
continue;
|
||||
};
|
||||
let reference = decode_all(software(&s).as_mut(), &s.samples);
|
||||
assert!(!reference.is_empty(), "{name}");
|
||||
// fail at the first sample, inside the first GOP, at a sync sample and just after one
|
||||
let k = (1..s.samples.len()).find(|&i| s.sync[i]).unwrap();
|
||||
for fail_at in [0, 5, k, k + 1, k + 9] {
|
||||
let mut va = VaDecoder::new(info.clone()).unwrap();
|
||||
va.fail_after(fail_at as u64);
|
||||
let mut d = HybridDecoder::new(Box::new(va), s.entry.clone(), info.clone());
|
||||
let before = filmcraft_codecs::hw::hw_stats().fallbacks;
|
||||
let out = decode_all(&mut d, &s.samples);
|
||||
assert!(!d.is_hardware(), "{name}: switched to software");
|
||||
assert!(filmcraft_codecs::hw::hw_stats().fallbacks > before, "{name}: fallback counted");
|
||||
assert_same(&format!("{name} failing at sample {fail_at}"), &out, &reference);
|
||||
d.reset();
|
||||
assert_same(&format!("{name} after the fallback and a reset"), &decode_all(&mut d, &s.samples), &reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What the hardware path does not take is declined (the software decoders keep it): interlaced
|
||||
/// and 10-bit H.264, and HEVC (not decoded through VA-API yet).
|
||||
#[test]
|
||||
fn declines_what_the_hardware_path_does_not_take() {
|
||||
let ff = filmcraft_testkit::require_ffmpeg!();
|
||||
let src = "testsrc2=s=640x360:r=24:d=1";
|
||||
let cases: [(&str, Vec<&str>); 2] = [
|
||||
("va_h264_interlaced.mp4", vec!["-f", "lavfi", "-i", src, "-c:v", "libx264", "-x264-params", "interlaced=1:keyint=12", "-pix_fmt", "yuv420p"]),
|
||||
("va_h264_10bit.mp4", vec!["-f", "lavfi", "-i", src, "-c:v", "libx264", "-profile:v", "high10", "-pix_fmt", "yuv420p10le"]),
|
||||
];
|
||||
for (name, args) in cases {
|
||||
// an ffmpeg whose x264 lacks 10-bit leaves the fixture out
|
||||
let Some(path) = fixture(&ff, name, &args) else { continue };
|
||||
let s = read_stream(&path);
|
||||
assert!(filmcraft_platform::vaapi_factory(&s.entry).is_none(), "{name}: declined");
|
||||
}
|
||||
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 4:2:2: declined");
|
||||
}
|
||||
// Settings ▸ Playback ▸ Hardware decoding Off
|
||||
let Some(path) = named(&ff, "h264_high.mp4") else { return };
|
||||
let s = read_stream(&path);
|
||||
filmcraft_codecs::hw::set_hardware_decoding(false);
|
||||
let off = filmcraft_platform::vaapi_factory(&s.entry).is_none();
|
||||
filmcraft_codecs::hw::set_hardware_decoding(true);
|
||||
assert!(off, "declined while hardware decoding is Off");
|
||||
}
|
||||
|
||||
/// Run `f` on a thread; fail if it panics or takes longer than `limit`.
|
||||
fn bounded(what: &str, limit: Duration, f: impl FnOnce() + Send + 'static) {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
|
||||
let _ = tx.send(r.is_ok());
|
||||
});
|
||||
match rx.recv_timeout(limit) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => panic!("{what}: panicked"),
|
||||
Err(_) => panic!("{what}: hung"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damaged_samples_and_parameter_sets_never_crash_or_hang() {
|
||||
let ff = filmcraft_testkit::require_ffmpeg!();
|
||||
for (name, s) in small_streams(&ff) {
|
||||
let name = name.as_str();
|
||||
if hardware(&s).is_none() {
|
||||
continue;
|
||||
}
|
||||
// damaged samples (bit flips, truncation, corrupt length prefixes): through the hybrid, and
|
||||
// straight into the hardware decoder (no software to fall back to)
|
||||
for seed in 1..=8u64 {
|
||||
let samples = s.samples.clone();
|
||||
let entry = s.entry.clone();
|
||||
bounded(&format!("{name} seed {seed}"), Duration::from_secs(60), move || {
|
||||
let mut seed = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
|
||||
let mut d = filmcraft_platform::vaapi_factory(&entry).unwrap().unwrap();
|
||||
let info = NalStreamInfo::from_entry(&entry).unwrap().unwrap();
|
||||
let mut raw = VaDecoder::new(info).unwrap();
|
||||
for (i, (smp, pts)) in samples.iter().enumerate().take(40) {
|
||||
let mut m = smp.clone();
|
||||
match xorshift(&mut seed) % 4 {
|
||||
0 => {
|
||||
for _ in 0..1 + xorshift(&mut seed) % 8 {
|
||||
let at = (xorshift(&mut seed) as usize) % m.len().max(1);
|
||||
if let Some(b) = m.get_mut(at) {
|
||||
*b ^= 1 << (xorshift(&mut seed) % 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
1 => m.truncate((xorshift(&mut seed) as usize) % m.len().max(1)),
|
||||
2 if m.len() > 4 => m[..4].copy_from_slice(&(xorshift(&mut seed) as u32).to_be_bytes()),
|
||||
_ => {}
|
||||
}
|
||||
let _ = d.decode(&m, *pts);
|
||||
let _ = raw.decode(&m, *pts);
|
||||
if i % 13 == 12 {
|
||||
d.reset();
|
||||
raw.reset();
|
||||
}
|
||||
}
|
||||
let _ = d.flush();
|
||||
let _ = raw.flush();
|
||||
});
|
||||
}
|
||||
// damaged parameter sets: the decoder declines, errors or decodes; never crashes
|
||||
let filmcraft_isobmff::CodecConfig::Avc(a) = &s.entry.codec else { continue };
|
||||
let rec = a.to_bytes();
|
||||
let mut seed = 0x1234_5678u64;
|
||||
for round in 0..24 {
|
||||
let mut r = rec.clone();
|
||||
if round % 3 == 2 {
|
||||
r.truncate(r.len() * (round + 1) / 30);
|
||||
} else {
|
||||
for _ in 0..1 + round % 4 {
|
||||
let at = 6 + (xorshift(&mut seed) as usize) % r.len().saturating_sub(6).max(1);
|
||||
if let Some(b) = r.get_mut(at) {
|
||||
*b ^= 1 << (xorshift(&mut seed) % 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
let samples: Vec<_> = s.samples.iter().take(12).cloned().collect();
|
||||
bounded(&format!("{name} record round {round}"), Duration::from_secs(60), move || {
|
||||
let Ok(info) = NalStreamInfo::from_avcc(&r) else { return };
|
||||
let Ok(mut va) = VaDecoder::new(info) else { return };
|
||||
for (smp, pts) in &samples {
|
||||
if va.decode(smp, *pts).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = va.flush();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creating and dropping decoders again and again (every clip on a timeline makes one) neither
|
||||
/// fails nor leaks VA-API objects into the next one.
|
||||
#[test]
|
||||
fn repeated_initialisation() {
|
||||
let ff = filmcraft_testkit::require_ffmpeg!();
|
||||
let Some(path) = named(&ff, "h264_high.mp4") else { return };
|
||||
let s = read_stream(&path);
|
||||
let reference = decode_all(software(&s).as_mut(), &s.samples[..13]);
|
||||
for round in 0..40 {
|
||||
let Some(mut hw) = hardware(&s) else { return };
|
||||
if round % 8 == 0 {
|
||||
assert_same(&format!("round {round}"), &decode_all(hw.as_mut(), &s.samples[..13]), &reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Several decoders at once on different threads (the frame workers) give the software decoder's
|
||||
/// pictures.
|
||||
#[test]
|
||||
fn concurrent_decoders() {
|
||||
let ff = filmcraft_testkit::require_ffmpeg!();
|
||||
let mut paths: Vec<_> = named(&ff, "h264_high.mp4").into_iter().collect();
|
||||
for (name, profile, params) in H264.iter().take(3) {
|
||||
paths.extend(h264_fixture(&ff, name, profile, params));
|
||||
}
|
||||
let jobs: Vec<_> = paths
|
||||
.into_iter()
|
||||
.chain(named(&ff, "h264_high.mp4"))
|
||||
.map(|path| {
|
||||
std::thread::spawn(move || {
|
||||
let s = read_stream(&path);
|
||||
let Some(mut hw) = hardware(&s) else { return };
|
||||
let n = s.samples.len().min(40);
|
||||
let a = decode_all(hw.as_mut(), &s.samples[..n]);
|
||||
hw.reset();
|
||||
let b = decode_all(hw.as_mut(), &s.samples[..n]);
|
||||
let c = decode_all(software(&s).as_mut(), &s.samples[..n]);
|
||||
assert_same("concurrent", &a, &c);
|
||||
assert_same("concurrent, after a reset", &b, &c);
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
for j in jobs {
|
||||
j.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoder used and moved between threads (decoders migrate between frame workers).
|
||||
#[test]
|
||||
fn decoder_moves_between_threads() {
|
||||
let ff = filmcraft_testkit::require_ffmpeg!();
|
||||
let Some(path) = named(&ff, "h264_high.mp4") else { return };
|
||||
let s = read_stream(&path);
|
||||
let Some(mut hw) = hardware(&s) else { return };
|
||||
let reference = decode_all(software(&s).as_mut(), &s.samples[..30]);
|
||||
let samples = s.samples.clone();
|
||||
let (hw, out) = std::thread::spawn(move || {
|
||||
let out = decode_all(hw.as_mut(), &samples[..30]);
|
||||
(hw, out)
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
assert_same("first thread", &out, &reference);
|
||||
let mut hw = hw;
|
||||
hw.reset();
|
||||
let samples = s.samples.clone();
|
||||
let out = std::thread::spawn(move || decode_all(hw.as_mut(), &samples[..30])).join().unwrap();
|
||||
assert_same("second thread", &out, &reference);
|
||||
}
|
||||
@@ -25,8 +25,8 @@ Containment rules:
|
||||
|
||||
1. The crate does not use `lints.workspace = true`. Its own `[lints]` table copies the workspace
|
||||
lints except `unsafe_code = "deny"` (not `forbid`), and adds
|
||||
`clippy::undocumented_unsafe_blocks = "deny"`. Only the FFI modules (`videotoolbox`, `media_foundation::gpu` / `media_foundation::mft`, and
|
||||
`nvenc::ffi` / `nvenc::session` on Windows) carry `#[allow(unsafe_code)]`; the rest of the crate
|
||||
`clippy::undocumented_unsafe_blocks = "deny"`. Only the FFI modules (`videotoolbox`, `media_foundation::gpu` / `media_foundation::mft`,
|
||||
`nvenc::ffi` / `nvenc::session` on Windows, and `vaapi::va` on Linux) carry `#[allow(unsafe_code)]`; the rest of the crate
|
||||
(the fallback logic in `hybrid`, the decoder logic in `media_foundation`, `annexb`, `biplanar`,
|
||||
the encoder logic in `nvenc`) has no `unsafe`.
|
||||
2. Every `unsafe` block has a `// SAFETY:` comment saying why it is sound.
|
||||
@@ -101,3 +101,28 @@ there will not be one (pure Rust, clean-room: x265 is GPL), so `Format::Hevc` ex
|
||||
OS has a hardware encoder. Choosing the format is the opt-in, `filmcraft_export::available` asks a
|
||||
probe the platform crate registers, and what the hardware path does not take (two-pass, odd sizes,
|
||||
a machine without the encoder) is an error naming the reason instead of a different encoder.
|
||||
|
||||
## Addendum (2026-10-08): VA-API hardware decoding on Linux
|
||||
|
||||
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
|
||||
in the lockfile through wgpu). Building needs no libva headers, and a system without libva, a DRM
|
||||
render node or a working driver starts as before and decodes in software (`register()` reports
|
||||
`Unavailable`). The declarations (`vaapi/ffi.rs`) are transcribed from libva's MIT-licensed
|
||||
`va.h`, and `vaapi/abi_tests.rs` checks sizes, offsets, constants and bit-field positions against a
|
||||
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 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 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.
|
||||
|
||||
The fallback guarantee is unchanged: what the hardware path does not take is declined up front or,
|
||||
mid-stream, handed to the software decoder by `HybridDecoder`.
|
||||
@@ -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 on Windows; 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)) |
|
||||
|
||||
@@ -349,6 +349,48 @@ 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 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
|
||||
Core i5-13500H with Iris Xe graphics (Raptor Lake-P), 7.4 GB RAM, Intel iHD driver 26.1.2 through
|
||||
libva 2.22, load average 5–15. Only H.264 goes through VA-API so far; the HEVC rows decode in
|
||||
software either way and show the run-to-run spread. The decoded pictures are identical
|
||||
(bit-exact parity tests, `crates/platform/tests/vaapi.rs`).
|
||||
|
||||
| codec | size | CPU ms/frame Off → **Auto** | fps Off → **Auto** (best of 3) | hw frames |
|
||||
|---|---|---|---|---|
|
||||
| H.264 | 1080p | 103.7 → **2.3** | 133 → **263** | 360 |
|
||||
| H.264 | 2160p | 416.5 → **11.4** | 32 → **56** | 216 |
|
||||
| HEVC | 1080p | 63.2 → 46.5 | 99 → 131 | 0 |
|
||||
| HEVC | 2160p | 349.5 → 353.7 | 30 → 35 | 0 |
|
||||
| HEVC Main 10 | 2160p | 348.9 → 352.6 | 29 → 29 | 0 |
|
||||
|
||||
What is left on the CPU per H.264 frame is the host side of stateless decoding (parsing, DPB,
|
||||
filling the VA buffers) and the read-back: `vaGetImage` into an NV12 image and the copy into
|
||||
planar Y'CbCr. One picture is decoded at a time and read back as soon as the DPB outputs it, so
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user