M-HW: NVENC on top of the shared hardware-encoding setting (#183); never-crash fixes

Merge origin/main into the NVENC branch. #183 already provides the
HardwareEncoding enum, ExportSettings.hardware_encoding, the
hardwareEncoding export param and the Export > Video > Hardware Encoding
checkbox, so the duplicates from this branch are dropped and NVENC uses
them. filmcraft-export is now an all-platform dependency of
filmcraft-platform (VideoToolbox and NVENC both register encoders).

Never-crash fixes in the NVENC backend:
- submit() rejects a driver input pitch narrower than a row (and unlocks
  the buffer); fill_nv12 uses get_mut instead of slicing rows.
- composition offsets use saturating math and i32::try_from; frame-rate,
  size and timestamp casts are checked; sizes above u16::MAX decline.
- Annex B scanning and NAL type checks use get()/first().
- A failed nvEncOpenEncodeSessionEx that returns a handle destroys it.

ffi.rs carries NVIDIA's MIT notice for nvEncodeAPI.h, listed in
ATTRIBUTION.md under a new "Third-party source code" section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-07 18:16:12 -04:00
co-authored by Claude Opus 5.5
102 changed files with 7655 additions and 654 deletions
+16 -2
View File
@@ -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; NVIDIA NVENC H.264 encoding behind the Export encoder registry (Windows). 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 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)) |
@@ -254,7 +254,8 @@ file ──► codecs (MP4/MOV, MKV, audio) demux + decode, GOP-aware see
samples): the `avcC` / `hvcC` sample becomes Annex B, the MFT is given the process's Direct3D 11
video device through an `IMFDXGIDeviceManager` so it decodes with DXVA, and its NV12 (8-bit) /
P010 (10-bit) texture is read back through a staging texture (the one GPU to CPU copy) into
planar `Yuv8` / `Yuv16`. H.264 Baseline / Main / High and HEVC Main / Main 10, 4:2:0, progressive;
planar `Yuv8` / `Yuv16`. H.264 Baseline / Main / High, HEVC Main / Main 10, VP9 profiles 0 / 2 and AV1 main
(8- and 10-bit), 4:2:0, progressive (VP9 / AV1 samples go in as they are, not as Annex B);
everything else, and any stream the GPU's DXVA decoder does not list, is declined. A decoder that
would hand back system-memory pictures (Microsoft's decoder MFTs do that when DXVA is not
available) fails the stream, so the hybrid continues with our decoder and Windows' software
@@ -295,6 +296,19 @@ file ──► codecs (MP4/MOV, MKV, audio) demux + decode, GOP-aware see
and handed to the GPU as an image (a layer image keeps its clip's blend mode), so both paths
give the same picture. Setting `FILMCRAFT_CPU_COMPOSITE=1`
forces the CPU path in the desktop app.
- **CPU compositor shortcuts.** The CPU compositor, which is what export runs, works on
premultiplied linear `f32` images, 33 MB at 1080p, so what it does not allocate, convert or mix
is time saved. Three shortcuts, each of which gives the general path's bits (compared bit for
bit, with every blend mode, in `render/src/region_tests.rs`): the canvas stays unallocated
until a layer needs it, and an opaque bottom layer (an 8-bit or 16-bit Y'CbCr frame without an
alpha plane, opacity 100 %, Normal) simply becomes the canvas; a plain media frame with an alpha
plane (a ProRes 4444 banner) is converted and mixed only inside the rectangle where its alpha is
not zero (`VideoFrame::alpha_region`, `to_linear_f32_region`, `blend::composite_at`), the rest
being transparent anyway; and working images are recycled through `filmcraft_frame::pool`
(`take_f32_overwritten` / `recycle_f32`: the buffer comes back with its old contents, so there is
no zero-fill) by the export pipeline once a frame is converted to 8 bits. A clip with effects,
opacity masks, frame blending, colour management, or a picture that is not placed one to one on
the output takes the general path.
- **Frame scheduling.** `crates/ui-egui/src/frames.rs` runs a small pool of worker threads with
prioritised jobs: the frame on screen first, then playback prefetch, then thumbnails. The UI never
decodes. It shows the exact frame when it is ready and holds the nearest cached frame meanwhile.