Keep the bounded GPU readback wait (the #1058 fix); leave out the VA-API encode probe

The probe only logged and always returned None, opened a VA display on every
H.264 Auto export, and made VA-API decode fail to load where libva lacks the
new symbol. A real VA-API encoder can come in its own PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-11 22:41:16 -04:00
co-authored by Claude Opus 5.5
parent 96959e5490
commit 0a3abdc3d0
7 changed files with 1 additions and 98 deletions
-8
View File
@@ -131,14 +131,6 @@ let availability = filmcraft_platform::register(); // Available("VideoToolbox")
log is bounded (600 samples / 256 MB); beyond it one error is returned and the next seek restarts
in software. Streams our decoders cannot decode (HEVC 4:2:2) have no fallback: the error stands.
## Hardware H.264 encoding (Linux)
`register()` puts NVENC (NVIDIA) and a VA-API EncSlice probe (`vaapi::encode`) in front of the
built-in encoder. AMD VCN and Intel QSV expose `VAEntrypointEncSlice`; the probe logs that and
falls through (a slice encoder is not wired yet) so the software encoder runs instead of hanging
on a missing `libnvidia-encode`. Export GPU readback waits at most eight seconds (`filmcraft-gpu`)
so a wedged radv device fails the GPU path instead of blocking forever.
## Hardware H.264 encoding (macOS)
`videotoolbox_encode.rs` (FFI) wraps a VideoToolbox compression session with a hardware encoder
+1 -6
View File
@@ -8,7 +8,6 @@
//! Linux with neither) 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`]; on
//! Linux that is NVENC ([`nvenc`]) after a VA-API EncSlice probe ([`vaapi::encode`]); on
//! macOS and on Windows (NVENC, [`nvenc`]) it also makes the H.265 export format available, and on Windows
//! (Main 10) lets HDR sequences export as HDR H.265.
//!
@@ -70,11 +69,7 @@ pub enum Availability {
/// using FilmCraft's own decoders.
pub fn register() -> Availability {
#[cfg(all(target_os = "linux", target_pointer_width = "64"))]
{
filmcraft_export::register_encoder(nvenc::export::factory);
// Tried first: AMD / Intel EncSlice before NVENC, which otherwise logs a missing NVIDIA library.
filmcraft_export::register_encoder(vaapi::encode::factory);
}
filmcraft_export::register_encoder(nvenc::export::factory);
#[cfg(target_os = "macos")]
{
filmcraft_codecs::register_video_decoder(videotoolbox_factory);
-2
View File
@@ -259,8 +259,6 @@ fn constants_match_the_header() {
assert_eq!(VAProfileH264High as i64, 7, "VAProfileH264High");
assert_eq!(VAProfileH264ConstrainedBaseline as i64, 13, "VAProfileH264ConstrainedBaseline");
assert_eq!(VAEntrypointVLD as i64, 1, "VAEntrypointVLD");
assert_eq!(VAEntrypointEncSlice as i64, 6, "VAEntrypointEncSlice");
assert_eq!(VAEntrypointEncSliceLP as i64, 8, "VAEntrypointEncSliceLP");
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");
-47
View File
@@ -1,47 +0,0 @@
//! VA-API as an Export encoder factory.
//!
//! Linux hardware H.264 encode on AMD and Intel is VA-API `EncSlice` (VCN / QSV), not NVENC.
//! [`factory`] is registered in front of NVENC so those GPUs are probed first. A full slice encoder
//! is not in this crate yet: when the driver exposes `EncSlice` we still return `None` so NVENC
//! (NVIDIA) and then the software encoder run. The probe exists so logs name the real encode path
//! instead of only "NVENC: no libnvidia-encode.so.1", and so a later encoder can take the same
//! factory without changing `register()`.
//!
//! The factory never hangs: `vaQueryConfigEntrypoints` is a driver query, not a session.
use filmcraft_export::{ExportSettings, Format, HardwareEncoding, Result, VideoEncoder};
use filmcraft_time::FrameRate;
/// The H.264 side of the VA-API encoder factory (see the module documentation).
pub fn factory(format: Format, _w: u32, _h: u32, _rate: FrameRate, s: &ExportSettings) -> Option<Result<Box<dyn VideoEncoder>>> {
if s.hardware_encoding != HardwareEncoding::Auto || format != Format::H264 {
return None;
}
#[cfg(target_os = "linux")]
match super::va::h264_encode_probe() {
Ok(driver) => {
log::info!("VA-API H.264 EncSlice present ({driver}); slice encode is not implemented, trying other factories");
None
}
Err(why) => {
log::info!("VA-API H.264 encode declined: {why}");
None
}
}
#[cfg(not(target_os = "linux"))]
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn off_and_non_h264_never_probe() {
let mut s = ExportSettings { hardware_encoding: HardwareEncoding::Off, ..Default::default() };
assert!(factory(Format::H264, 640, 360, FrameRate::FPS_24, &s).is_none());
s.hardware_encoding = HardwareEncoding::Auto;
assert!(factory(Format::ProRes, 640, 360, FrameRate::FPS_24, &s).is_none());
assert!(factory(Format::Hevc, 640, 360, FrameRate::FPS_24, &s).is_none());
}
}
-6
View File
@@ -71,10 +71,6 @@ pub const VAProfileH264ConstrainedBaseline: VAProfile = 13;
pub const VAProfileHEVCMain: VAProfile = 17;
pub const VAProfileHEVCMain10: VAProfile = 18;
pub const VAEntrypointVLD: VAEntrypoint = 1;
/// H.264 / HEVC slice encode (AMD VCN, Intel QSV, and other Mesa encode paths).
pub const VAEntrypointEncSlice: VAEntrypoint = 6;
/// Low-power slice encode, when the driver exposes it separately from [`VAEntrypointEncSlice`].
pub const VAEntrypointEncSliceLP: VAEntrypoint = 8;
pub const VAConfigAttribRTFormat: VAConfigAttribType = 0;
pub const VA_RT_FORMAT_YUV420: c_uint = 0x0000_0001;
@@ -451,8 +447,6 @@ 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 vaQueryConfigEntrypoints =
unsafe extern "C" fn(dpy: VADisplay, profile: VAProfile, entrypoint_list: *mut VAEntrypoint, num_entrypoints: *mut c_int) -> VAStatus;
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(
-1
View File
@@ -10,7 +10,6 @@
//!
//! The FFI is in [`ffi`] (declarations) and `va` (calls); the rest of this module is safe code.
pub mod encode;
pub mod ffi;
pub mod h264;
pub mod hevc;
-28
View File
@@ -30,7 +30,6 @@ struct Api {
vendor_string: ffi::vaQueryVendorString,
set_info_callback: ffi::vaSetInfoCallback,
set_error_callback: ffi::vaSetErrorCallback,
query_config_entrypoints: ffi::vaQueryConfigEntrypoints,
get_config_attributes: ffi::vaGetConfigAttributes,
create_config: ffi::vaCreateConfig,
destroy_config: ffi::vaDestroyConfig,
@@ -81,7 +80,6 @@ fn load() -> Result<Api, String> {
vendor_string: sym!(va, vaQueryVendorString),
set_info_callback: sym!(va, vaSetInfoCallback),
set_error_callback: sym!(va, vaSetErrorCallback),
query_config_entrypoints: sym!(va, vaQueryConfigEntrypoints),
get_config_attributes: sym!(va, vaGetConfigAttributes),
create_config: sym!(va, vaCreateConfig),
destroy_config: sym!(va, vaDestroyConfig),
@@ -585,29 +583,3 @@ pub fn probe() -> &'static Result<String, String> {
// the display opened to answer is the one sessions share
PROBE.get_or_init(|| shared().map(|d| d.vendor().to_string()))
}
/// Whether any H.264 profile on this display exposes slice encode (`EncSlice` or `EncSliceLP`):
/// AMD VCN and Intel QSV do; NVIDIA's proprietary stack does not. The encoder factory uses this to
/// decline quickly instead of opening NVENC on a machine that has no NVIDIA encode library.
pub fn h264_encode_probe() -> Result<String, String> {
let display = shared()?;
let api = display.api;
let dpy = display.dpy;
const PROFILES: [ffi::VAProfile; 3] = [ffi::VAProfileH264ConstrainedBaseline, ffi::VAProfileH264Main, ffi::VAProfileH264High];
let mut why = "no H.264 EncSlice / EncSliceLP entrypoint".to_string();
for profile in PROFILES {
let mut list = [0 as ffi::VAEntrypoint; 32];
let mut n: c_int = c_int::try_from(list.len()).unwrap_or(32);
// SAFETY: `dpy` is an initialised display; `list` has room for `n` entrypoints and `n` is a live output.
let st = unsafe { (api.query_config_entrypoints)(dpy, profile, list.as_mut_ptr(), &mut n) };
if let Err(e) = display.check(st, "vaQueryConfigEntrypoints") {
why = e;
continue;
}
let n = usize::try_from(n.max(0)).unwrap_or(0).min(list.len());
if list[..n].iter().any(|&e| e == ffi::VAEntrypointEncSlice || e == ffi::VAEntrypointEncSliceLP) {
return Ok(display.vendor().to_string());
}
}
Err(why)
}