Files
filmcraft/docs/adr/0001-platform-ffi.md
T
Brandon ThomasandClaude Opus 5.5 c3ec0e85d3 HW1: platform crate + VideoToolbox H.264/HEVC hardware decode (#30)
- New crates/platform (filmcraft-platform, L5): OS media FFI only. The one
  crate allowed `unsafe` (own [lints] with unsafe_code = "deny",
  #[allow(unsafe_code)] only on the videotoolbox module, SAFETY comment on
  every block, safe Result-returning public API). AGENTS.md §0.3 names the
  exception; docs/adr/0001-platform-ffi.md records why and the rules.
- VideoToolbox decoder for avcC / hvcC, 8/10-bit 4:2:0 and 4:2:2: session from
  the sample entry's parameter sets with hardware required, asynchronous
  decode (2 in flight), biplanar CVPixelBuffer copied to planar Yuv8/Yuv16,
  reorder buffer of the stream's own depth, RASL skipped after a starting CRA.
  Output callback under catch_unwind.
- HybridDecoder: transparent mid-stream fallback to the software decoder
  (replays samples since the last restart point, drops already-returned
  pictures). New codecs::software_video_decoder (built-in factories only) and
  codecs::hw (switch, counters, NalStreamInfo shared with software decoders;
  vui_color / sar_par helpers now used by both).
- Settings ▸ Playback ▸ Hardware decoding: Auto / Off (engine applies it to
  the codecs switch); desktop app, CLI/MCP and bench call register().
- perf.stats decode.framesDecoded and decode.hardware counters.
- Tests: bit-exact parity with the software decoders (H.264 High, HEVC Main
  open GOP, Main 10; resets, reseeks, flushes), forced-failure fallback,
  mutation fuzz of samples and parameter sets, cross-platform fallback with a
  stand-in decoder, setting Off, HEVC 4:2:2 vs ffmpeg.
- Bench --hw auto|off; results in docs/performance.md (4K CPU/frame
  ~86-119 ms -> ~4 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:53:28 -04:00

3.9 KiB
Raw Blame History

ADR 0001: unsafe OS media FFI in crates/platform, and nowhere else

  • Status: accepted (2026-10-05, approved by the project owner; recorded in AGENTS.md §0 item 3 and the craftrules never-crash standard)
  • Issue: #30 (hardware acceleration)

Context

FilmCraft is pure Rust and the workspace sets unsafe_code = "forbid". Every codec has a clean-room software decoder, and those remain the reference. But decoding 4K H.264 / HEVC in software costs 85–120 ms of CPU per frame on an M4 Pro, so 4K playback drops frames under load, while every current computer has a hardware video decoder the operating system exposes (VideoToolbox on macOS; Media Foundation / D3D11 on Windows and VA-API on Linux later).

Using it needs FFI into C / Objective-C APIs, which is unsafe Rust. The bindings we use (the objc2-* crates, MIT / Apache-2.0 / Zlib) are generated declarations; nothing GPL/LGPL is linked.

Decision

Allow unsafe in exactly one crate, crates/platform (filmcraft-platform, layer L5), for one job: OS media FFI (hardware decoding now, hardware encoding later). Audio output, dialogs, menus and other OS integration do not go in it.

Containment rules:

  1. The crate does not use lints.workspace = true. Its own [lints] table copies the workspace lints except unsafe_code = "deny" (not forbid), and adds clippy::undocumented_unsafe_blocks = "deny". Only the FFI modules (videotoolbox) carry #[allow(unsafe_code)]; the rest of the crate (the fallback logic in hybrid) has no unsafe.
  2. Every unsafe block has a // SAFETY: comment saying why it is sound.
  3. The public API is safe: no pub unsafe fn, no raw pointers or FFI types in public signatures; failures are Results. The crate keeps the never-crash deny(clippy::unwrap_used, …) attribute of every clean crate.
  4. No panic crosses the FFI boundary: the VideoToolbox output callback runs under catch_unwind and reports a panic as a failed frame.
  5. The crate compiles on every target. OS bindings are target-specific dependencies; elsewhere register() reports Unavailable and does nothing. It is not part of the wasm check (L5).

The fallback guarantee

Registering a hardware decoder never makes a file undecodable, and never changes a picture:

  • The factory declines (the software decoder is used) when Settings ▸ Playback ▸ Hardware decoding is Off, for formats the hardware path does not take (field-coded H.264, bit depths other than 8 / 10, 4:4:4, mismatched luma / chroma depth), and when the OS cannot create a hardware session for the stream (hardware decoding is required, so the OS's own software decoder is never used instead of ours).
  • A hardware decoder that fails mid-stream (decode error, session invalidated by a GPU change or sleep, in-band parameter sets that differ from the sample entry) becomes the software decoder for that stream without the caller noticing: HybridDecoder replays the samples since the last restart point through it and drops pictures already returned. It is logged and counted (perf.stats decode.hardware.fallbacks).
  • Pictures are interchangeable with the software decoder's: same planes (bit-exact on the H.264 High, HEVC Main and Main 10 parity fixtures, including after seeks and flushes), colour, pixel aspect, pts and presentation order. Tests compare the two on every run on macOS, and the fallback logic is tested on every platform with a stand-in hardware decoder.

Consequences

  • 4K H.264 / HEVC decode costs ~4 ms of CPU per frame instead of 85–120 ms (performance.md).
  • Better patent posture for the most encumbered formats: H.264 / HEVC decoding is done by the operating system, which carries the licences, wherever hardware decoding is used.
  • A new review burden: changes to crates/platform need the same scrutiny as any FFI code. Anything else that wants unsafe needs a new decision, not an extension of this one.