The CPU compositor, which is what export runs, works on premultiplied linear f32 images,
33 MB each at 1080p, and every layer was one of them. A profile of a 9:29 export with a
hardware H.264 encoder (so the encoder is no longer the bottleneck) put 61-64 % of the busy
CPU in compositing, 12 % in zeroing and copying memory, 8-11 % each in the float -> 8-bit
and RGBA -> Y'CbCr conversions and 4-7 % in decoding the ProRes overlays: a banner that is
transparent except for its bottom 100 rows still cost a whole conversion and a whole mix,
and the bottom layer paid for a zero-filled canvas and a pass that mixes it over that canvas.
Three changes, each of which gives the bits the general path gives:
- The canvas is allocated when a layer needs it, and an opaque first layer (a Y'CbCr frame
without an alpha plane, opacity 100 %, Normal blend, the size of the output) becomes the
canvas instead of being mixed over zeros.
- A clip of plain media whose frame has 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` and `blend::composite_at` (`composite` is `composite_at` at the
origin; Dissolve's pattern follows the pixel's place in the output). 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, as before.
- `filmcraft_frame::pool` keeps idle f32 images (320 MiB at most). `take_f32_overwritten`
hands one back with its old contents, which the conversions overwrite whole (so no
zero-fill), and the export pipeline gives each frame's image back after the 8-bit
conversion.
Apple M1 (8 cores), a machine that was not idle, so the two builds alternate and the
median is reported:
- Synthetic 1080p frame (`cargo run --release -p filmcraft-render --example frame_bench`,
5 rounds): camera only 8.9 -> 6.5 ms, camera + banner 16.2 -> 7.8 ms, same picture checksum.
- A real project (1080p25 camera clip, ProRes 4444 banner and subscribe bar), 30 s with the
banner exported with the built-in encoder, which is deterministic: 46.4 -> 36.7 s, and
both exports are the same file (same MD5), so every frame of the real pipeline matches.
- The same project with a hardware encoder, 3 rounds: 30 s with the banner 17.4 -> 11.2 s
(CPU 99 -> 56 s), 30 s of the camera alone 11.4 -> 8.8 s, 20 s with the subscribe bar
11.1 -> 7.0 s.
Tests: `render/src/region_tests.rs` compares each shortcut with the general path bit for bit
(an opaque bottom layer, banners over and under a camera, an 8-bit sticker, every blend mode,
several opacities, moved, scaled and effect clips, a bigger source, reduced-resolution
rendering), each render twice so the second starts from recycled buffers, and checks which
layers take a shortcut; the frame tests check that a region is the crop of the whole
conversion, that `alpha_region` is the box around what is not transparent, and that a buffer
full of NaN never leaks into a conversion, for every pixel format. Breaking a shortcut on
purpose (a region one pixel off, an opaque layer taken as the canvas at half opacity, a
conversion that skips the alpha channel) makes them fail.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>