132 lines
5.2 KiB
Rust
132 lines
5.2 KiB
Rust
//! 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 only so far: 8-bit 4:2:0
|
|
//! progressive, Constrained Baseline / Main / High; 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;
|
|
#[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;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
mod linux {
|
|
use filmcraft_bitstream::unescape_rbsp;
|
|
use filmcraft_codecs::hw::{NalCodec, NalStreamInfo};
|
|
use filmcraft_codecs::{CodecError, DecodedFrame, Result, VideoDecoder};
|
|
use filmcraft_h264::params::Sps;
|
|
|
|
use super::ffi;
|
|
use super::h264::Front;
|
|
use super::va::Session;
|
|
use crate::biplanar::Geometry;
|
|
|
|
/// The VA-API profiles that decode an H.264 stream of `profile_idc`, best fit first (a Main or
|
|
/// High decoder also decodes what the smaller profiles allow).
|
|
pub(crate) fn profiles(profile_idc: u8) -> Option<&'static [ffi::VAProfile]> {
|
|
Some(match profile_idc {
|
|
66 => &[ffi::VAProfileH264ConstrainedBaseline, ffi::VAProfileH264Main, ffi::VAProfileH264High],
|
|
77 => &[ffi::VAProfileH264Main, ffi::VAProfileH264High],
|
|
100 => &[ffi::VAProfileH264High],
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
/// A VA-API H.264 decoder for one stream.
|
|
pub struct VaDecoder {
|
|
front: Front<Session>,
|
|
info: NalStreamInfo,
|
|
name: String,
|
|
fed: u64,
|
|
fail_after: Option<u64>,
|
|
}
|
|
|
|
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> {
|
|
if info.codec != NalCodec::H264 {
|
|
return Err("only H.264 is decoded through VA-API so far".into());
|
|
}
|
|
if info.chroma_format_idc != 1 || info.bit_depth_luma != 8 || info.bit_depth_chroma != 8 || info.interlaced {
|
|
return Err(format!(
|
|
"{} 4:2:0 {}-bit {} is not taken",
|
|
if info.interlaced { "interlaced" } else { "progressive" },
|
|
info.bit_depth_luma,
|
|
if info.chroma_format_idc == 1 { "only" } else { "(other chroma formats)" }
|
|
));
|
|
}
|
|
let profiles = profiles(info.profile_idc).ok_or_else(|| format!("H.264 profile_idc {} is not taken", info.profile_idc))?;
|
|
let sps = info
|
|
.parameter_sets
|
|
.iter()
|
|
.find(|n| n.first().is_some_and(|h| h & 0x1f == 7))
|
|
.and_then(|n| Sps::parse(&unescape_rbsp(n.get(1..)?)).ok())
|
|
.ok_or("no readable SPS")?;
|
|
sps.check_supported().map_err(|e| e.to_string())?;
|
|
let mbs = (sps.pic_width_in_mbs, sps.frame_height_in_mbs());
|
|
// the DPB, the picture being decoded, and one spare
|
|
let count = sps.max_dpb_frames().saturating_add(2).min(18);
|
|
let geometry = Geometry { crop: info.crop, bits: 8, color: info.color, par: info.par };
|
|
let session = Session::new(profiles, (sps.width(), sps.height()), count, geometry)?;
|
|
let name = format!("VA-API H.264 ({})", session.vendor());
|
|
let front = Front::new(session, info.length_size, mbs, &info.parameter_sets).map_err(|e| e.to_string())?;
|
|
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()));
|
|
}
|
|
self.front.decode(sample, pts)
|
|
}
|
|
|
|
fn flush(&mut self) -> Vec<DecodedFrame> {
|
|
match self.front.flush() {
|
|
Ok(out) => out,
|
|
Err(e) => {
|
|
log::warn!("{}: {e} while flushing", self.name);
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
self.front.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)
|
|
}
|
|
}
|
|
}
|