mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-24 10:10:05 -06:00
M2: review fixes for NVENC H.265
- register() asks the HEVC probe on a thread of its own (the first probe is about half a second: D3D11 device + NVENC session), so the first draw of Export's format list does not wait for it; tests/nvenc_hevc_warm.rs. - A machine with no HEVC encoder is not a declined hardware attempt: the factory returns None without counting `declined`. - HEVC tests skip only when the probe says there is no HEVC encoder; on a machine that has one, a configuration NVENC refuses fails the test. - Wording: NVENC is FilmCraft's only HEVC encoder on Windows (Windows itself ships others). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
deebe2e5b2
commit
29b454ac17
@@ -147,9 +147,11 @@ level to the encoder). The H.264 path is unchanged: the same export is the same
|
||||
|
||||
- **Choosing the format is the opt-in,** as on macOS: Export ▸ Format ▸ H.265 (HEVC) or
|
||||
`"format": "hevc"`. `register()` hands `nvenc::hevc_available` to
|
||||
`filmcraft_export::register_format_probe`: one small HEVC session (a 640×360 encoder, created on
|
||||
the first question and the answer kept; about half a second in a fresh process, nearly all of it
|
||||
opening the Direct3D 11 device and the NVENC session, which every NVENC export pays too). The
|
||||
`filmcraft_export::register_format_probe`: one small HEVC session (a 640×360 encoder, the answer
|
||||
kept; about half a second in a fresh process, nearly all of it opening the Direct3D 11 device and
|
||||
the NVENC session, which every NVENC export pays too). `register()` also asks it on a thread of its
|
||||
own (`nvenc::warm_hevc_probe`), so the first draw of the format list does not wait for it; a
|
||||
caller that asks while it runs waits for the same answer. The
|
||||
Hardware encoding toggle governs H.264 only: with H.265 selected NVENC is used whether it is Auto
|
||||
or Off, because nothing else can encode it.
|
||||
- **What it writes:** HEVC Main, 8-bit 4:2:0, SDR BT.709 limited range (the VUI says so, with the
|
||||
@@ -211,6 +213,7 @@ level to the encoder). The H.264 path is unchanged: the same export is the same
|
||||
| `tests/nvenc_hevc.rs` (Windows, NVIDIA with HEVC) | HEVC from NVENC (1280×720, 6 Mbps, 72 frames, keyframe every 24) decodes with our HEVC decoder at worst 48.9 dB luma / 51.0 dB chroma PSNR; IDR at 0, 24, 48; dts strictly increasing, pts a permutation, `sps_max_num_reorder_pics` within the dts shift; no parameter sets in the samples; the `hvcC`'s profile / tier / level bytes are the SPS's own; VUI BT.709 limited and timing `(1, 24)`; hostile configurations, sizes, planes and encoders dropped mid-stream give errors, never panics; keyframes every 1–2 pictures without B-frames |
|
||||
| `tests/nvenc_hevc_export.rs` (Windows, NVIDIA with HEVC) | H.265 export through the real pipeline, MP4 and QuickTime, toggle Auto and Off: counters (72 frames, 1 session, 0 declined), our decoder against the software H.264 export at worst 54.8 dB luma PSNR, ffprobe `codec_name=hevc`, `profile=Main`, `codec_tag_string=hvc1`, `pix_fmt=yuv420p`, BT.709 limited, 72 frames, 24/1, keyframes at 0 and 48, start 0, `ffmpeg -xerror` clean; with AAC audio; 1920×1080 and 642×362 cropped back from the coded size; every decline (HDR, analysis pass, two-pass, interlaced, non-square pixels, MXF, sizes over 65535, odd sizes, sizes outside the GPU's limits) an error naming NVENC, counted once; H.264 with hardware encoding Off never touches NVENC |
|
||||
| `tests/nvenc_hevc_probe.rs` (Windows) | the HEVC probe in a fresh process: its cost, the cached answer, `available(Hevc)` following it after `register()` (twice) |
|
||||
| `tests/nvenc_hevc_warm.rs` (Windows) | `register()` answers the HEVC question on a thread of its own, before anyone asks; the answer is kept |
|
||||
| `src/nvenc/abi_tests.rs` (Windows) | FFI structs' sizes, alignments, field offsets, constants, GUIDs and the bit-field masks of the `flags` words (H.264 and HEVC) against a C compiler's view of NVIDIA's `nvEncodeAPI.h` (12.1) |
|
||||
| `src/nvenc/{mod,hevc}.rs` unit tests (Windows) | HEVC NAL types, stripping and parameter-set splitting (including `IDR_N_LP`, whose header byte reads as a PPS in H.264, and one-byte or truncated NAL units); the `hvcC` built from real VPS / SPS / PPS, every truncation and bit flip of the SPS, wrong profile, size and NAL types; HEVC levels |
|
||||
|
||||
|
||||
@@ -68,11 +68,13 @@ pub fn register() -> Availability {
|
||||
filmcraft_codecs::hw::set_hw_backend("VideoToolbox");
|
||||
Availability::Available("VideoToolbox")
|
||||
}
|
||||
// Export ▸ H.265: NVENC is the only HEVC encoder on Windows, so the format is available when a
|
||||
// small HEVC session opens (asked once, when the format list first asks)
|
||||
// Export ▸ H.265: NVENC is FilmCraft's only HEVC encoder on Windows, so the format is available
|
||||
// when a small HEVC session opens. That takes about half a second, so it is asked on a thread of
|
||||
// its own now rather than by the first draw of the format list.
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
filmcraft_export::register_format_probe(filmcraft_export::Format::Hevc, nvenc::hevc_available);
|
||||
nvenc::warm_hevc_probe();
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
|
||||
@@ -173,7 +173,7 @@ pub fn factory(format: Format, w: u32, h: u32, rate: FrameRate, s: &ExportSettin
|
||||
}
|
||||
}
|
||||
|
||||
/// The H.265 side of [`factory`]: the only HEVC encoder on Windows, so the Hardware encoding toggle
|
||||
/// The H.265 side of [`factory`]: FilmCraft's only HEVC encoder on Windows, so the Hardware encoding toggle
|
||||
/// does not apply and a request NVENC cannot take is an error, not a fall-through.
|
||||
fn hevc_factory(w: u32, h: u32, rate: FrameRate, s: &ExportSettings) -> Option<Result<Box<dyn VideoEncoder>>> {
|
||||
let declined = |why: &str| {
|
||||
@@ -190,12 +190,12 @@ fn hevc_factory(w: u32, h: u32, rate: FrameRate, s: &ExportSettings) -> Option<R
|
||||
filmcraft_export::note_hw_encode_session();
|
||||
Some(Ok(Box::new(NvencEncoder { enc, w, h, rate, y: Vec::new(), u: Vec::new(), v: Vec::new() })))
|
||||
}
|
||||
Err(why) => {
|
||||
log::info!("hardware H.265 encoding declined: {why}");
|
||||
filmcraft_export::note_hw_encode_declined();
|
||||
// no HEVC encoder here at all: the export's own "encoder not available" error
|
||||
if super::hevc_available() { Some(Err(ExportError::Unsupported(format!("H.265 export with NVENC: {why}")))) } else { None }
|
||||
// no HEVC encoder here at all: not a hardware attempt, the export's own "encoder not available" error
|
||||
Err(why) if !super::hevc_available() => {
|
||||
log::info!("no hardware H.265 encoder: {why}");
|
||||
None
|
||||
}
|
||||
Err(why) => declined(&why),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,8 +120,7 @@ pub fn available() -> bool {
|
||||
/// Whether this system's NVENC can encode HEVC: a session opened and an HEVC encoder created for a
|
||||
/// small picture, once (the answer is kept). What makes the H.265 export format available.
|
||||
pub fn hevc_available() -> bool {
|
||||
static AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*AVAILABLE.get_or_init(|| {
|
||||
*HEVC_AVAILABLE.get_or_init(|| {
|
||||
let probe = Config {
|
||||
width: 640,
|
||||
height: 360,
|
||||
@@ -143,6 +142,32 @@ pub fn hevc_available() -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// The answer of [`hevc_available`], once asked.
|
||||
static HEVC_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
|
||||
/// Ask [`hevc_available`] on a thread of its own (once, however often this is called), so the
|
||||
/// first draw of Export's format list does not wait for the probe session. Whoever asks while it
|
||||
/// runs waits for the same answer; if the thread cannot start, or the probe panics (caught), the
|
||||
/// first caller asks, as before.
|
||||
pub fn warm_hevc_probe() {
|
||||
static STARTED: std::sync::Once = std::sync::Once::new();
|
||||
STARTED.call_once(|| {
|
||||
let spawned = std::thread::Builder::new().name("nvenc-hevc-probe".into()).spawn(|| {
|
||||
if std::panic::catch_unwind(hevc_available).is_err() {
|
||||
log::warn!("the hardware HEVC probe panicked; the format list will ask again");
|
||||
}
|
||||
});
|
||||
if let Err(e) = spawned {
|
||||
log::info!("hardware HEVC probe thread not started: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether [`hevc_available`] has its answer yet (diagnostics and tests).
|
||||
pub fn hevc_probed() -> bool {
|
||||
HEVC_AVAILABLE.get().is_some()
|
||||
}
|
||||
|
||||
impl Nvenc {
|
||||
/// Open an encoder, or say why NVENC does not take this configuration.
|
||||
pub fn new(cfg: &Config) -> Result<Self, String> {
|
||||
|
||||
@@ -48,14 +48,14 @@ static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
type Sources = Vec<(Vec<u8>, Vec<u8>, Vec<u8>)>;
|
||||
|
||||
/// `None` (the test skips) only on a machine without an HEVC encoder; on one that has it, a
|
||||
/// configuration NVENC refuses is a failure, not a skip.
|
||||
fn encode_all(cfg: &Config, frames: usize) -> Option<(Nvenc, Vec<Packet>, Sources)> {
|
||||
let mut enc = match Nvenc::new(cfg) {
|
||||
Ok(e) => e,
|
||||
Err(why) => {
|
||||
eprintln!("SKIPPED: {why}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if !hevc_available() {
|
||||
eprintln!("SKIPPED: no NVENC HEVC encoder");
|
||||
return None;
|
||||
}
|
||||
let mut enc = Nvenc::new(cfg).unwrap_or_else(|why| panic!("NVENC HEVC is available but refused {cfg:?}: {why}"));
|
||||
let (w, h) = (cfg.width as usize, cfg.height as usize);
|
||||
let (mut y, mut u, mut v) = (Vec::new(), Vec::new(), Vec::new());
|
||||
let (mut packets, mut sources) = (Vec::new(), Vec::new());
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//! `register` asks the NVENC HEVC probe on a thread of its own, so the first draw of Export's format
|
||||
//! list does not open an encoder session on the UI thread. Its own test binary: anything else asking
|
||||
//! `hevc_available` first would make the wait meaningless.
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use filmcraft_platform::nvenc::{hevc_available, hevc_probed};
|
||||
|
||||
#[test]
|
||||
fn register_answers_the_hevc_question_in_the_background() {
|
||||
assert!(!hevc_probed(), "nothing has asked yet");
|
||||
filmcraft_platform::register();
|
||||
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(10));
|
||||
}
|
||||
eprintln!("answered in the background after {:?}", t0.elapsed());
|
||||
// the answer is kept: asking and registering again change nothing
|
||||
let answer = hevc_available();
|
||||
filmcraft_platform::register();
|
||||
assert_eq!(hevc_available(), answer);
|
||||
}
|
||||
@@ -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 (Windows, opt-in) and H.265 (Windows, the only H.265 encoder there) encoding 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 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 (Windows, opt-in) and H.265 (Windows, FilmCraft's only H.265 encoder there) encoding 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)) |
|
||||
|
||||
+3
-2
@@ -72,8 +72,9 @@ are never linked or shipped ([AGENTS.md](../AGENTS.md) §2).
|
||||
AV1 fixtures (libvpx-vp9, libaom-av1; 360p, 1080p, 2160p, hidden alt-ref frames, two GOPs) need an ffmpeg with those encoders.
|
||||
|
||||
The NVENC tests (`crates/platform/tests/nvenc.rs` and `nvenc_export.rs` for H.264, `nvenc_hevc.rs`,
|
||||
`nvenc_hevc_export.rs` and `nvenc_hevc_probe.rs` for H.265; Windows only) skip, printing `SKIPPED`,
|
||||
without an NVIDIA GPU with NVENC (H.265: with an HEVC encoder). The FFI layout tests in
|
||||
`nvenc_hevc_export.rs`, `nvenc_hevc_probe.rs` and `nvenc_hevc_warm.rs` for H.265; Windows only) skip,
|
||||
printing `SKIPPED`, without an NVIDIA GPU with NVENC (H.265: when the HEVC probe says there is no HEVC
|
||||
encoder; on a machine that has one, a configuration NVENC refuses fails the test). The FFI layout tests in
|
||||
`crates/platform/src/nvenc/abi_tests.rs` were generated from a C program built with MSVC
|
||||
(`cl` after `vcvars64.bat`) against NVIDIA's MIT-licensed `nvEncodeAPI.h` (12.1); the header and the
|
||||
program are not in the repository. To regenerate them, put the header in a scratch directory and
|
||||
|
||||
Reference in New Issue
Block a user