Merge pull request #300 from dcarnelutti/platform/warm-hevc-probe

platform: ask for the hardware HEVC encoder in the background at startup
This commit is contained in:
Brandon Thomas 2026-10-09 09:25:32 -04:00 committed by GitHub
commit 0bae537ad0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 64 additions and 7 deletions

View File

@ -145,8 +145,10 @@ built-in encoder, which changes three things from H.264:
- **Choosing the format is the opt-in.** No `hardware_encoding` setting: Export ▸ Format ▸ H.265
(HEVC), or `"format": "hevc"` in `file.exportMedia` (`h265` is accepted too). The format is
listed as available only on a machine with a hardware HEVC encoder: `register()` hands
`hardware_encode::hevc_available` (one small hardware session, created on the first question) to
`filmcraft_export::register_format_probe`.
`hardware_encode::hevc_available` (one small hardware session, about 0.1 s) to
`filmcraft_export::register_format_probe`, and asks it right away on a thread of its own
(`warm_hevc_probe`), so the first draw of the format list does not create the session on the UI
thread.
- **There is no fallback encoder.** What the hardware path does not take is an error, not a
different encoder: two-pass VBR is refused up front (`ExportSettings::validate`), HDR sequences
are exported as SDR (the H.265 path is 8-bit), and odd sizes, non-square pixels or a machine

View File

@ -13,7 +13,7 @@
//! configurations are declined, and a declined export fails with "encoder not available" instead of
//! falling back; [`hevc_available`] tells the format list whether this machine can do it at all.
use std::sync::OnceLock;
use std::sync::{Once, OnceLock};
use filmcraft_export::{BitrateMode, EncodedPacket, EncoderFrame, ExportError, ExportSettings, Format, H264Pass, H264Profile, HardwareEncoding, VideoEncoder};
use filmcraft_isobmff::{AvcConfig, HevcConfig, SampleEntry};
@ -66,11 +66,13 @@ pub fn config_for(w: u32, h: u32, rate: FrameRate, s: &ExportSettings) -> Result
})
}
/// Whether this machine has a hardware HEVC encoder (asked once, by creating a small session): what
/// makes `Format::Hevc` available.
static HEVC_PROBE: OnceLock<bool> = OnceLock::new();
/// Whether this machine has a hardware HEVC encoder (asked once, by creating a small session, which
/// takes about 0.1 s): what makes `Format::Hevc` available. [`warm_hevc_probe`] asks it ahead of the
/// first caller.
pub fn hevc_available() -> bool {
static AVAILABLE: OnceLock<bool> = OnceLock::new();
*AVAILABLE.get_or_init(|| {
*HEVC_PROBE.get_or_init(|| {
let probe = VtConfig {
width: 640,
height: 360,
@ -88,6 +90,27 @@ pub fn hevc_available() -> bool {
})
}
/// Whether [`hevc_available`] has its answer yet (diagnostics and tests).
pub fn hevc_probed() -> bool {
HEVC_PROBE.get().is_some()
}
/// Ask [`hevc_available`] now, on a thread of its own, so that the first draw of the format list does
/// not spend its 0.1 s creating a hardware session on the UI thread. Whoever asks while the thread
/// runs waits for the same answer (no longer than asking first); if the thread cannot start, or the
/// probe panics, the first caller asks, as before. Only the first call starts a thread.
pub fn warm_hevc_probe() {
static STARTED: Once = Once::new();
STARTED.call_once(|| {
let spawned = std::thread::Builder::new().name("filmcraft-hevc-probe".into()).spawn(|| {
let _ = std::panic::catch_unwind(hevc_available);
});
if let Err(why) = spawned {
log::info!("HEVC probe not started ahead of time: {why}");
}
});
}
/// The encoder factory `register` puts in front of the built-in ones: a hardware encoder when the
/// export asks for one (H.264 with `hardware_encoding`; HEVC always) and the OS takes the
/// configuration, `None` (the built-in encoder, if there is one) otherwise.

View File

@ -68,6 +68,9 @@ pub fn register() -> Availability {
filmcraft_codecs::register_video_decoder(videotoolbox_factory);
filmcraft_export::register_encoder(hardware_encode::videotoolbox_encoder_factory);
filmcraft_export::register_format_probe(filmcraft_export::Format::Hevc, hardware_encode::hevc_available);
// the probe creates a hardware session (about 0.1 s): answer it now, off the UI thread,
// before the first draw of the format list asks
hardware_encode::warm_hevc_probe();
filmcraft_codecs::hw::set_hw_backend("VideoToolbox");
Availability::Available("VideoToolbox")
}

View File

@ -0,0 +1,29 @@
//! `register()` answers the HEVC availability probe in the background, so the first draw of the
//! format list does not create a hardware session on the UI thread. One test in this file on
//! purpose: anything else that asked `hevc_available()` first would make the wait below meaningless.
#![cfg(target_os = "macos")]
use std::time::{Duration, Instant};
use filmcraft_platform::hardware_encode::{hevc_available, hevc_probed, warm_hevc_probe};
#[test]
fn register_answers_the_hevc_probe_in_the_background() {
assert!(!hevc_probed(), "nothing has asked yet");
filmcraft_platform::register();
// nobody calls `hevc_available()` here: only the thread `register` started can answer
let t0 = Instant::now();
while !hevc_probed() {
assert!(t0.elapsed() < Duration::from_secs(60), "register() did not start the probe");
std::thread::sleep(Duration::from_millis(5));
}
// asking again, or registering again, changes nothing
let answer = hevc_available();
warm_hevc_probe();
filmcraft_platform::register();
assert_eq!(hevc_available(), answer);
assert!(hevc_probed());
}