mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-24 10:10:05 -06:00
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:
@@ -59,6 +59,42 @@ What remains per live decoder is unchanged: the H.264 decoder publishes each mac
|
||||
picture as its own allocations (`Frame::make_row`, five per row), which is where the 290 MB per
|
||||
decoder comes from.
|
||||
|
||||
## Results (CPU compositor: layer rectangles, an opaque bottom layer and recycled images, before → after)
|
||||
|
||||
The CPU compositor is what export runs. In a profile of a 9:29 export it took about a third of the
|
||||
busy CPU with the built-in H.264 encoder (the encoder took the other two thirds), and a faster
|
||||
encoder leaves it as the largest cost of an export. It works on premultiplied linear `f32` images,
|
||||
33 MB each at 1080p, and every layer was one of them: a banner that is transparent except for its
|
||||
bottom 100 rows still cost a full conversion and a full mix, and the bottom layer paid for a
|
||||
zero-filled canvas and a pass that mixes it over that canvas.
|
||||
|
||||
Apple M1 (8 cores: 4 performance + 4 efficiency), 2026-10-06, a machine that was not idle, so the
|
||||
two builds ran alternately in the same session and the median is reported.
|
||||
|
||||
### Per frame, synthetic 1080p sources
|
||||
|
||||
`cargo run --release -p filmcraft-render --example frame_bench`: an 8-bit 4:2:0 camera picture and a
|
||||
10-bit 4:4:4 banner with alpha only in its bottom 100 rows; `render_sequence` followed by the
|
||||
conversion to 8-bit RGBA, milliseconds per frame (median of 40 frames, 5 alternating rounds):
|
||||
|
||||
| scenario | before | after |
|
||||
|---|---|---|
|
||||
| camera only | 8.9 | 6.5 |
|
||||
| camera + banner | 16.2 | 7.8 |
|
||||
|
||||
Both builds give the same picture (same checksum).
|
||||
|
||||
### A real export
|
||||
|
||||
30 s of a 1080p25 camera clip with a ProRes 4444 banner, exported with the built-in encoder
|
||||
(deterministic, byte for byte): 46.4 s before, 36.7 s after. **The two exports are the same file**
|
||||
(same bytes, same MD5), so every frame of the real pipeline (camera decode, ProRes decode,
|
||||
compositing, conversion) matches. The encoder is most of that time; with a faster one the
|
||||
compositor's savings are a larger part of the export.
|
||||
|
||||
What is left in a frame without overlays is the Y'CbCr → linear float conversion of the camera
|
||||
picture (4.5 ms in the synthetic case) and the conversion to 8 bits (2 ms).
|
||||
|
||||
## Results (GPU2: standard effects on the GPU compositor, #30, before → after)
|
||||
|
||||
Before = this change with clips that carry standard effects sent back to the CPU layer path in
|
||||
@@ -125,6 +161,99 @@ the draft plan's box decimation of the planes, cheap next to software decoding,
|
||||
CPU cost. Zero-copy upload of the decoded `CVPixelBuffer` into wgpu (no copy-out at all) is the
|
||||
next step (issue #30).
|
||||
|
||||
## Results (HW2: VideoToolbox hardware H.264 encoding, built-in → hardware)
|
||||
|
||||
Apple M1 (8 cores, 16 GB), 2026-10-06, single runs on an otherwise idle machine, release build.
|
||||
Hardware encoding is opt-in per export (`hardware_encoding: auto`); the built-in encoder is unchanged.
|
||||
|
||||
### The encoder alone (100 frames of 1920×1080 25 fps camera footage, High, one-pass VBR, keyframe every 50)
|
||||
|
||||
Frames go in as planar Y'CbCr and out as compressed frames (NV12 fill, `VTCompressionSessionEncodeFrame`,
|
||||
flush); quality is the luma PSNR of our software decoder's output against the input.
|
||||
|
||||
| target | B-frames | real bitrate | luma PSNR | frames/s |
|
||||
|---|---|---|---|---|
|
||||
| 4 Mb/s | off | 3982 kb/s | 41.66 dB | 202 |
|
||||
| 8 Mb/s | off | 7987 kb/s | 43.44 dB | 202 |
|
||||
| 16 Mb/s | off | 15972 kb/s | 45.55 dB | 202 |
|
||||
| 4 Mb/s | on | 4220 kb/s | 41.37 dB | 183 |
|
||||
| 8 Mb/s | on | 8268 kb/s | 43.41 dB | 197 |
|
||||
| 16 Mb/s | on | 16388 kb/s | 45.66 dB | 199 |
|
||||
|
||||
B-frames buy nothing measurable here (±0.3 dB) and overshoot the target by up to 5 %, so the hardware
|
||||
encoder runs without them: frames come out in presentation order and the muxer needs no composition
|
||||
offsets or edit list.
|
||||
|
||||
### Whole exports (1080p25 camera clip with ProRes 4444 overlays with alpha, AAC, loudness normalised to −14 LUFS)
|
||||
|
||||
A 9:29 timeline (569 s, 14 227 frames): the camera clip, an intro, six banners, three subscribe bars
|
||||
and an outro.
|
||||
|
||||
| encoder | wall time | frames/s | vs real time | file |
|
||||
|---|---|---|---|---|
|
||||
| built-in (`filmcraft-h264enc`) | 793 s | 17.9 | 1.4× slower | 1445 MB, 20.0 Mb/s |
|
||||
| hardware (VideoToolbox) | **311 s** | **45.7** | 1.8× faster | 1417 MB, 19.6 Mb/s |
|
||||
|
||||
2.5× faster. The first 134 s of the same timeline, where the overlays are densest (intro, a banner, a
|
||||
subscribe bar and the outro in 134 s):
|
||||
|
||||
| encoder | wall time | frames/s | vs real time | file |
|
||||
|---|---|---|---|---|
|
||||
| built-in (`filmcraft-h264enc`) | 395 s | 8.5 | 2.9× slower | 340 MB, 20.0 Mb/s |
|
||||
| hardware (VideoToolbox) | **84 s** | **39.9** | 1.6× faster | 312 MB, 18.3 Mb/s |
|
||||
|
||||
4.7× faster there. The gain depends on what else an export spends its time on: with the encoder
|
||||
off the CPU, what is left is decoding the camera clip, the CPU compositor (every layer, alpha
|
||||
included) and the RGBA → Y'CbCr conversion.
|
||||
|
||||
The pairs of files have the same frame count and duration, decode in ffmpeg without a message, and
|
||||
compare at SSIM 0.990 / PSNR 46.7 dB over the 134 s (lowest frame: 45.1 dB) and SSIM 0.989–0.990 /
|
||||
PSNR 46.3–46.6 dB at six 3-second spots of the full export. Audio is identical.
|
||||
|
||||
Where the built-in export's time goes (`sample` on the process during a 20 s export, inclusive CPU
|
||||
time in the call tree; the figures overlap because work-stealing mixes the tasks): about two thirds
|
||||
of the busy CPU is in the encoder, about a third in compositing and layer decoding. Moving the encoder to
|
||||
the media engine removes most of the first and lets the next batch render while the previous one
|
||||
encodes (the exporter renders a batch of frames in parallel, then encodes it). What is left is the
|
||||
CPU compositor, the next target of #30 ("GPU export").
|
||||
|
||||
## Results (HW3: VideoToolbox hardware H.265 (HEVC) encoding, against hardware H.264)
|
||||
|
||||
Apple M1 (8 cores, 16 GB), 2026-10-07, single runs on an otherwise idle machine, release build. Both
|
||||
encoders are the M1's media engine. H.265 is `Format::Hevc` (Main, 8-bit 4:2:0, one-pass VBR, no
|
||||
B-frames); H.264 is the opt-in hardware encoder of HW2 (High).
|
||||
|
||||
### Speed
|
||||
|
||||
The raw encoders (ffmpeg's `hevc_videotoolbox` and `h264_videotoolbox` on a 1080p25 test pattern, 500
|
||||
frames): 166 and 189 frames/s. H.265 is the codec that compresses further, not the faster one to encode.
|
||||
|
||||
The 9:29 timeline of HW2 (camera clip, ProRes 4444 overlays, AAC, loudness normalised to −14 LUFS), 20 Mb/s
|
||||
H.264 and 10 Mb/s H.265: **204 s and 195 s** (CPU 1042 s and 1029 s), files of 1417 MB and 719 MB. The
|
||||
same time: the same H.264 export took 193 s and 200 s on other runs. The export is bound by the CPU
|
||||
compositor and the decoders, not by the encoder, so a different hardware codec does not change it.
|
||||
|
||||
### Quality per bit
|
||||
|
||||
30 s with a banner (15–45 s of that timeline), exported by FilmCraft as ProRes 422 HQ (the reference)
|
||||
and by each encoder at several bitrates; ffmpeg's `psnr` and `ssim` of the decoded 4:2:0 pictures against
|
||||
the reference. Average PSNR over the three planes, and SSIM (higher is better):
|
||||
|
||||
| Mb/s | H.264 | H.265 | difference | H.264 Mb/s for H.265's PSNR |
|
||||
|---|---|---|---|---|
|
||||
| 20 | 46.88 dB, 0.9902 | 46.91 dB, 0.9903 | +0.03 dB | beyond the range measured |
|
||||
| 10 | 44.96 dB, 0.9863 | 45.09 dB, 0.9863 | +0.13 dB | 10.5 |
|
||||
| 6 | 43.72 dB, 0.9833 | 44.31 dB, 0.9843 | +0.59 dB | 7.7 |
|
||||
| 4 | 42.91 dB, 0.9807 | 43.64 dB, 0.9825 | +0.73 dB | 5.8 |
|
||||
| 3 | 42.37 dB, 0.9786 | 43.05 dB, 0.9807 | +0.68 dB | 4.3 |
|
||||
|
||||
At 10 Mb/s and above the two give the same picture. At 3–6 Mb/s H.265 reaches the same PSNR with 22–31 %
|
||||
less bitrate. Its worst frame is better at every bitrate (lowest per-frame PSNR 45.9 against 42.1 dB at
|
||||
20 Mb/s, 43.1 against 41.5 dB at 10, 39.5 against 37.8 dB at 3).
|
||||
|
||||
So there is no case for a lower default bitrate: the two formats start at the same 20 Mb/s. H.265
|
||||
pays off where the file size matters, at 3–6 Mb/s; that 9:29 timeline is 428 MB at 6 Mb/s.
|
||||
|
||||
## Results (HW2: Windows Media Foundation / Direct3D 11 hardware decoding, Off → Auto)
|
||||
|
||||
Same binary (release build of the HW2 commit), Settings ▸ Playback ▸ Hardware decoding switched
|
||||
@@ -192,6 +321,34 @@ of 8 s of a 1080p23.976 H.264 clip (191 frames) to H.264 with the default preset
|
||||
|
||||
Quality at equal bitrate: see the PR description.
|
||||
|
||||
## Results (HW2 follow-up: Windows VP9 and AV1 hardware decoding, Off → Auto)
|
||||
|
||||
Same machine and method as the H.264 / HEVC results above (Xeon E5-2680 v4, RTX 5060 driver 617.14,
|
||||
idle, 2026-10-07, two alternating rounds of `--hw off` / `--hw auto`, the same binary). MFTs:
|
||||
`VP9VideoExtensionDecoder` and `AV1VideoExtension` (Microsoft Store codec extensions), DXVA on the
|
||||
GPU's NVDEC (profiles VP9 0 / 2, AV1 main). AV1 fixtures here are libaom (`libsvtav1` is not in this
|
||||
ffmpeg build; the bench falls back to it) and VP9 are libvpx-vp9, as in the bench's `dec_*` specs.
|
||||
`cargo xtask bench --sections decode --only dec_vp9 --repeat 3 --hw off|auto` (likewise `dec_av1`),
|
||||
`--sections playback --only "av1-2160 full" --hw off|auto`.
|
||||
|
||||
| codec | size | CPU ms/frame Off → **Auto** | fps Off → **Auto** (best of 3) |
|
||||
|---|---|---|---|
|
||||
| VP9 | 1080p | 38-43 → **3.5-3.6** | 60 → **281-283** |
|
||||
| VP9 | 2160p | 165-169 → **15-16** | 16 → **70-79** |
|
||||
| AV1 | 1080p | 55-56 → **3.3-3.4** | 22 → **373-374** |
|
||||
| AV1 | 2160p | 219-221 → **10.9-11.3** | 5.5 → **106-107** |
|
||||
|
||||
Every Auto row had `hw frames` = frames × 3 repeats, 3 sessions, 0 fallbacks, 0 declined.
|
||||
|
||||
| playback 8 s, Full | shown/dropped Off → **Auto** | CPU ms/frame Off → **Auto** |
|
||||
|---|---|---|
|
||||
| VP9 2160p | 11-12/180-181 → **191/1** | 117-124 → **24-27** |
|
||||
| AV1 2160p | 0/192 → **192/0** | 47.5-47.9 → **17-20** |
|
||||
|
||||
Software AV1 decodes 4K at 5.5 fps here, so it never plays in real time; with the hardware decoder it
|
||||
plays without a drop. As on the other codecs, the hardware ignores draft mode and what is left on the
|
||||
CPU is the readback and plane conversion.
|
||||
|
||||
## Results (GPU1: blend modes on the GPU compositor, #30, before → after)
|
||||
|
||||
Before = this change with the old whole-frame CPU fallback for non-Normal blend modes put back
|
||||
|
||||
Reference in New Issue
Block a user