- AudioSettings gains `float`; `bits` takes 16, 24 or 32. `SampleFormat` (Int16 / Int24 /
Int32 / Float32) and `ExportSettings::supports_sample_format` decide what each codec and
container stores: WAV, AIFF and QuickTime PCM all four, MXF integers, FLAC integers.
`validate` refuses float without 32 bits and float in FLAC or MXF.
- WAV float is WAVE_FORMAT_IEEE_FLOAT (EXTENSIBLE for more than two channels) with a `fact`
chunk; AIFF float is AIFF-C `fl32`; QuickTime float is `lpcm` with the float flag; MXF writes
32-bit integers. Float keeps the mix as it is, peaks above 0 dBFS included; 16 and 24 bit
write the same bytes as before.
- FLAC encoder: 8-32 bits. 32-bit stereo is coded independently (a side channel would need 33
bits), the sample range check no longer overflows, and 32-bit frames point at STREAMINFO for
the size, which symphonia 0.5 (FilmCraft's own FLAC import) and ffmpeg both read.
- Export mode: Sample Size shows 16 bit, 24 bit, 32 bit and 32 bit float like Audio Format
does, greying out what the format cannot store with the reason on hover; a size that stops
being available (float after switching to FLAC) becomes 32-bit integer. es, fr, pt-br, id,
ja and zh-cn strings.
- Tests: writer layouts and ffprobe codecs for every new format, round trips through ffmpeg
and FilmCraft's own readers in WAV, AIFF, MOV, MXF and FLAC, headroom of a +6 dB tone,
refused combinations, full-scale 32-bit FLAC through ffmpeg (MD5 too) and symphonia, and a
UI test of the selector.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nzevSsTNZxHPpUngfGTAw
Keep FLAC beside main's new AAC Audio format (21 formats, both audio-only
presets), use is_mp4_with / is_mp4_video in audio_codec() and the Audio
Format picker so MP4 (incl. AV1) offers AAC or FLAC, and add flacLevel to
main's export.media params.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: OpenEXR sequences (#806) were written uncompressed only, because the
workspace had no deflate encoder; the files are 2-4x larger than necessary and
Premiere offers an EXR compression choice.
Fix: a small clean-room DEFLATE (RFC 1951) encoder in a zlib (RFC 1950)
wrapper in filmcraft-export (greedy LZ77 with bounded hash chains; stored,
fixed or dynamic Huffman per block, whichever is smallest; length-limited
codes). The EXR writer gains RLE, ZIPS (1 line) and ZIP (16 lines) chunks with
the format's byte interleave + delta predictor; chunks that do not shrink are
stored raw. New ExportSettings::exr_compression (default None, as before),
`exrCompression` param on file.exportMedia, Compression combo in Export mode
(es/fr/ja/zh-cn strings).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the GIF exporter gave every frame the same delay of
1000·den/num ms, which the encoder truncates to whole centiseconds.
At 24 fps each 41.67 ms frame became 4 cs, so a 4.0 s sequence played
in 3.84 s (4 % fast); other non-integer rates drifted the same way.
Fix: each frame's delay now runs from its rounded start time to the
next frame's (gif_delay_cs), so delays alternate (4, 4, 5, 4, 4, 4 cs
at 24 fps) and add up to the exact duration. Checked i128 math also
replaces the unchecked `(1000 * den) as u32` cast.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Premiere's "AAC Audio" format has no FilmCraft equivalent. AAC
was only written inside H.264 / H.265 / AV1 movies; the only audio-only
exports were Waveform Audio and AIFF.
Fix: a new `aac` export format ("AAC Audio") that encodes the sequence
mix with the existing AAC-LC encoder into an MPEG-4 audio file (.m4a,
encoder delay in an edit list), or a raw ADTS stream when the path ends
in .aac. Bitrate, sample rate, channels and loudness normalisation follow
the audio settings. Adds an "AAC Audio 48 kHz 320 kbps" Audio Only
preset, CLI extension guessing (.m4a / .aac), es/fr/ja/zh-cn strings, and
updates G13 (also noting ProRes 4444 / XQ is already in the export UI).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Premiere offers OpenEXR as an image-sequence export format;
FilmCraft could not write EXR at all (docs/gaps.md G13).
Fix: a new export format (id exr, "OpenEXR") writing numbered .exr stills,
with a preset, the Include Alpha Channel option, the file.exportMedia format
list and es/ja/zh-cn strings. The writer in crates/export/src/raster.rs
follows the public OpenEXR file layout: version 2 single-part scan-line
file, half-float B/G/R(/A) channels, no compression, one scan line per
chunk, Rec.709 chromaticities; values are scene-linear (the pipeline's
transfer undone) with premultiplied alpha, as EXR defines. No new
dependency. ZIP / PIZ / DWA compression stays open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Premiere offers JPEG, Targa and DPX as image-sequence export
formats; FilmCraft only wrote PNG, TIFF and BMP sequences (docs/gaps.md G13).
Fix: three new export formats (ids jpg, tga, dpx), numbered like the other
sequences, with presets, the export UI (JPEG quality slider, Targa alpha)
and es/ja/zh-cn strings. JPEG uses the image crate at the export quality;
Targa (TGA 2.0, uncompressed 24/32-bit) and DPX (SMPTE ST 268, 10-bit RGB
method A, BT.709) are written by a small hostile-input-safe writer in
crates/export/src/raster.rs. Format::is_image_sequence replaces the
hand-kept format lists. OpenEXR stays open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Format::Av1 in export settings and jobs, SampleEntry::av1 (av01 + av1C) in
isobmff, and the Export panel's Format list, profile and encoder rows. No
encoder ships here: like H.265, AV1 is listed greyed out until a platform
encoder registers a probe, and a job without one fails with "AV1 encoder not
available yet". Split out of #409; the VA-API AV1 encoder follows on main's
vaapi::va design.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the ProRes encoder already codes 4444 / 4444 XQ (optionally
with alpha), but export only knew the four 422 flavours: any other
proresProfile fell back to HQ and the encoder was always fed a 4:2:2
frame, so graphics could not round-trip with their alpha.
Fix: proresProfile accepts 4444 and 4444xq. The ProRes export encoder
builds a 4:4:4 frame for them and, with Include Alpha Channel, codes
the straight alpha of the unflattened picture (ExportSettings::
supports_alpha / keeps_alpha). Export Settings offers both profiles and
the alpha checkbox for them; new presets Apple ProRes 4444, 4444 with
Alpha and 4444 XQ; summary text and the MXF coding label name the
profile (and the MXF descriptor keeps its 12-bit 4:4:4 depth).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No conflicts. main's new Chinese catalog requires every tl! string:
added zh-CN for the FLAC labels (format, preset, Audio Format entry,
Compression Level and its hint).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in crates/engine/src/export_tools.rs: both sides added an
import (BitrateMode for CRF, GpuRendering from main); both kept.
main's new Chinese catalog requires every tl! string: added zh-CN for
"Quality (CRF)" and "CRF (constant quality)".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New L0 crate filmcraft-flac, a FLAC encoder written from RFC 9639:
constant / verbatim / fixed / LPC subframes (Levinson-Durbin on Tukey-
windowed autocorrelation, up to order 12), partitioned Rice residuals,
left/side, side/right and mid/side stereo, CRC-8/CRC-16, STREAMINFO with
MD5 (RFC 1321, own implementation), levels 0-8, all lossless.
Export:
- Format::Flac, an audio-only .flac (16/24-bit), streamed one second at
a time like WAV/AIFF; STREAMINFO is rewritten at the end with frame
sizes, total samples and MD5. Preset "FLAC 48 kHz 24-bit".
- AudioCodec::Flac in MP4 and QuickTime (fLaC + dfLa), one frame per
sample with the short last frame timed exactly; the dfLa STREAMINFO
is replaced before the moov is written (Mp4Writer::set_sample_entry).
MXF stays PCM.
- AudioSettings::flac_level (default 5); Export panel: FLAC (lossless)
audio format and Compression Level; file.exportMedia `format: "flac"`,
`audioCodec`, `flacLevel`. Spanish and Japanese strings.
ffmpeg oracle: every stream decodes bit-exactly with the STREAMINFO MD5
equal to ffmpeg's hash; .flac and FLAC-in-MP4/MOV exports decode to the
same samples as a WAV export of the range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GpuRendering defaults to Off (opt-in per export) until measured on Windows and macOS;
docs and ROADMAP say so.
- The shared export device installs an uncaptured-error handler and a device-lost callback
that log once and retire GPU export for the session (frames go to the CPU) instead of
wgpu's default panic.
- RendererPool drops a renderer that panicked and retires the pool, so the rest of the
export renders on the CPU (regression test in gpu_hook_scenarios); the duplicate
hostile-size test that raced on the global registry is folded into it.
- Rebased onto main's render_plan/transitions: render_export_prepared reads the linear
accumulator after render_plan; transition (Composite) plans fall back to the CPU.
- es/ja rows for the new Export strings; web_time::Instant in the export pipeline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WAV and AIFF sizes were 32-bit with unchecked arithmetic: past 4 GiB
(24-bit 5.1 at 96 kHz after about 41 minutes) a debug build panicked
and a release build wrote a wrapped, corrupt header. The whole file was
also built in memory first.
- WAV past 4 GiB is written as RF64 (EBU Tech 3306) with a ds64 chunk;
smaller files stay plain RIFF.
- AIFF has no 64-bit form: a longer one is refused before anything is
written, naming WAV instead.
- The header is written from the known length, then the samples one
second at a time, so memory stays flat; progress advances per second.
- Odd-sized data chunks get their pad byte; hostile lengths, channel
counts and sample rates are errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds BitrateMode::Crf with ExportSettings::crf (0-51, default 23) and
maps it to the built-in encoder's RateControl::Crf, which export never
used. The Export panel offers "CRF (constant quality)" with a Quality
(CRF) slider in place of the bitrate fields; file.exportMedia takes
`crf` and `bitrateMode: "crf"`. H.265 (hardware only) rejects it, and
VideoToolbox / NVENC decline it so the built-in encoder runs. Settings
saved before this read as CRF 23.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NVENC export: keep main's Main 10 / Main dispatch and time both colour
conversions (rgbf_to_yuv420_10, rgba_to_yuv420_8) as Stage::Convert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
encode / encode_10 compute the plane sizes they check with saturating_mul;
fill_p010, rgbf_to_yuv420_10 and the HDR export test use as_chunks instead of
chunks_exact with a constant size (clippy::chunks_exact_to_as_chunks).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merlin's effects-heavy bench scene (GPU3c) settles it: with layers and
effects the GPU compositor exports 2.4x faster at 1080p (13.0 -> 5.0-5.6 s,
737 -> 238 CPU ms/frame) and 3.2x at 4K (38.7-39.3 -> 12.0 s, 2955 -> 775
CPU ms/frame); a single plain clip takes the same time either way
(decoding / encoding bound). Every Auto row rendered all its frames on the
GPU, without fallbacks. The CPU reference renderer stays the fallback for
anything the GPU stage does not run.
Trade-off: the GPU matches the CPU within the compositor's parity
tolerance, not bit for bit, so an export's bytes can depend on the
machine's GPU; Off keeps them byte-reproducible (the determinism tests
register no GPU renderer and are unaffected).
- settings: GpuRendering::Auto is the #[default]; ExportSettings::default.
- test: the default, settings saved without the field, and "off".
- docs/performance.md: GPU3 results (1080p and 4K).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GPU export rendering was ~20 % slower in wall-clock time than CPU
rendering: one renderer behind a mutex made the export workers queue.
- Export workers share a pool of up to four renderers on one wgpu device
(one per worker was no faster: the read-back then contends for memory
bandwidth).
- render_export_prepared composites into the linear accumulator and
copies it to a reused staging buffer in one submission (no sRGB resolve
pass); read_accumulator shares the read-back, which now returns None
instead of stale pooled values when a row is missing.
- f16_to_f32_fast: same result as f16_to_f32 for all 65536 inputs (test).
- Timing counters (export::export_lock_wait, gpu::export_renderer::timing)
printed by the export bench.
RX 7900, same build, Off / Auto / Off: H.264 + AAC 4.67 / 4.71 / 4.76 s,
ProRes 422 4.20 / 4.21 / 4.21 s; CPU per frame 287-292 -> 277 (H.264),
316-317 -> 300 (ProRes). Was 4.86 -> 5.78 s. Parity, not faster: on this
single-clip scene decoding and encoding dominate (rendering is ~15 of
~290 CPU ms per frame), so GPU rendering stays Off by default.
Gates: fmt, clippy (gpu, export, ui-egui), layers, assets; gpu
export_pipeline, golden and export tests pass. Pre-existing on the
previous commit: fx_tests::gpu_effects_match_cpu_exactly fails on this
AMD GPU, and the gpu unit tests segfault in the driver when run in
parallel (pass serially apart from that test).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Set GpuRendering default to Off in ExportSettings so exports use the CPU
reference renderer by default until GPU export achieves lower wall-clock time.
🤖 Written by agy (Antigravity Agent, Gemini 3.8 Flash)
Add off-screen GPU export renderer (filmcraft-gpu::ExportRenderer) reading back
the linear float accumulator as premultiplied RGBA f32, plug it into the
export pipeline via FrameRenderer hook and GpuRendering setting, register
in desktop app and CLI, and record export.gpu_frames statistics.
🤖 Completed by agy (Antigravity Agent, Gemini 3.8 Flash) based on draft by Merlin
A step now renders batch k+1 on the rayon pool while the calling thread
encodes, muxes and mixes the audio of batch k. The file is byte-identical
with or without overlap. A prefetch that hits pending media is dropped and
rendered again, cancel frees it, a render panic becomes an error, and wasm
never overlaps. perf.stats export.stages gains waitMs.
Memory: up to 2 x batch x frame bytes of finished frames are in flight, so
the batch is capped to a 512 MiB budget (1080p keeps 16, 4K SDR 8, 4K HDR 2
frames). Measured peak RSS vs before: 1080p +67..+158 MB; 4K SDR -535..-927
MB; 4K HDR about -2.6 GB (5.2 -> 2.6 GB). Wall time 1.06-1.16x faster.
Known pre-existing web issue, untouched: AudioOut::pull advances before the
pending check, so a retried audio group is lost.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
filmcraft_export::Stage, stage_times(), note_stage() and timed(): the wall
time of setup, loudness, render, encode (and its RGB->YUV convert part),
audio, mux and finish, summed per process like export.hardware. Converters
in the H.264 / ProRes / NVENC / VideoToolbox encoders are timed as Convert.
Two clock reads per stage per batch; the exported file does not change
(determinism tests). Test: an export grows every stage counter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HDR sequences export as real 10-bit HEVC Main 10 through NVENC where the GPU
reports 10-bit HEVC: float BT.2020 R'G'B' -> rgbf_to_yuv420_10 -> P010 input;
VUI BT.2020 + PQ/HLG; HDR10 mastering-display / content-light as SEI on every
IDR (PQ); colr/mdcv/clli in the sample entry. New register_hdr_probe /
hdr_available in the export crate; macOS and GPUs without 10-bit keep
tone-mapping HDR sequences to 8-bit SDR HEVC. ABI asserts extended and
re-verified against nvEncodeAPI.h 12.1 (0 drift). 8-bit SDR HEVC unchanged;
the two #328 tests that asserted HDR is declined now expect Main 10.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wire both camera buttons to captured-monitor frame export with original media at full resolution. Add a movable settings dialog with format/depth selection, a visible destination folder, Browse, overwrite confirmation, and optional project import.
Reuse existing 8-bit still encoding; add JPEG and direct float-to-16-bit PNG/TIFF encoding. Cover monitor routing, captured frame time, proxy bypass, hostile parameters, depth, import, cancellation, overwrite, and fractional header movement with synthetic regressions.
Assisted-by: Codex
The export pipeline flattened every frame over black
(`over_black_rgba8`), so a PNG sequence of a sequence with transparency
came out on a black background, while Export Frame already wrote straight
RGBA. `ExportSettings.alpha` (serde default false, Premiere's default)
makes the pipeline emit straight sRGB RGBA for PNG and TIFF sequences and
`encode_still` keep the channel in TIFF; BMP and video formats ignore it.
Exposed as the `alpha` flat parameter of file.exportMedia and an
"Include Alpha Channel" checkbox in Export mode's Video section, shown
for PNG and TIFF only.
Fixes#160
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- mxf: read_pcm bounds a request by the sample frames the file actually stores (plus 16M
samples of zero padding) instead of a fixed 16,777,216-sample cap, so whole-clip reads of
long stereo MXF (transcription) work again; PCM/AES3 chunk buffers are clamped to the source
length instead of a 256 MiB cap; decode_pcm's output is bounded by its input, so its
fixed cap goes (the AES3 >8-channel refusal stays; a request is also bounded by the source
length divided by the channel count). read_sample keeps only the source-length bound.
- mxf: index_entries no longer fails open() past 1,048,576 entries (the sparse BTreeMap
already bounds memory by real entries); long recordings open.
- project/format: new Sequence/Track/CaptionTrack::check_bounds() hold only the hostile-value
bounds (settings, marks, overflowing time ranges); project load and export ranges use it, so
projects that break only structural rules (overlap, zero duration) still open and export.
check() = check_bounds() + the structural rules, as before.
- project: frame-size cap raised to 268,435,456 pixels, 32768 per side (16K, 16384x8192).
- engine: checked_u32_p accepts integer-valued floats (1920.0); renderable_sequence returns
an EngineError (new Session::try_render_program_at, used by automation render) instead of
eprintln.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Adds `Format::Hevc` (Export > Format > H.265 (HEVC); `"format": "hevc"` in
`file.exportMedia`): HEVC Main, 8-bit 4:2:0, in MP4 (`hvc1`) or QuickTime, with AAC audio,
encoded by the macOS hardware encoder. There is no built-in H.265 encoder (one in pure Rust
is a project of its own, and x265 is GPL), so the format exists only where the OS has one.
- `videotoolbox_encode`: the session wrapper creates HEVC sessions too (`VtProfile::HevcMain`;
the profile also says which codec). Keyframes are the HEVC random access points (NAL types
16 to 21); the parameter sets are VPS / SPS / PPS; the `hvcC` record in the MP4 sample
entry is the one VideoToolbox writes (sample description extension atoms), so profile,
level and flags are the encoder's own. `VtEncoder::uses_hardware()` asks VideoToolbox
whether the session runs on a hardware encoder (a test checks H.264 and HEVC), so a
requirement dropped by accident could never become a silent software encode.
- `hardware_encode`: one adapter for both codecs. Choosing the format is the opt-in (the
`hardware_encoding` setting is for H.264). `hevc_available()` (one small hardware session,
created once) is registered with the new `filmcraft_export::register_format_probe`, so
`available(Format::Hevc)`, the format list of Export mode and `exportFormats` follow the
machine. `Format::has_builtin_encoder` says which formats need such a probe.
- `export`: `Format::is_h26x` where H.264 and H.265 behave alike (MP4 or QuickTime, AAC, size
estimate, summary line). Two-pass VBR is refused up front ("H.265 export has no two-pass
mode") and HDR sequences export as SDR, because there is no encoder to fall back to.
- Export mode: H.265 shows H.264's multiplexer, audio, metadata and bitrate controls, without
profile, level, two-pass or the hardware checkbox, and says "Encoder: Hardware".
Apple M1 (8 cores), 1080p25, a 9:29 timeline (camera clip, ProRes 4444 overlays, AAC,
loudness): hardware H.265 at 10 Mb/s 195 s, hardware H.264 at 20 Mb/s 204 s (CPU 1029 s and
1042 s; the same H.264 export took 193 s and 200 s on other runs). It is not faster: the codec
that compresses further is the more expensive one (raw encoders: 166 and 189 frames/s), and
the export is bound by the CPU compositor, not by the media engine.
Quality per bit, 30 s of the same timeline against a ProRes 422 HQ export (average PSNR over
the three planes, SSIM): 20 Mb/s 46.88 dB / 0.9902 (H.264) and 46.91 dB / 0.9903 (H.265);
10 Mb/s 44.96 / 0.9863 and 45.09 / 0.9863; 6 Mb/s 43.72 / 0.9833 and 44.31 / 0.9843; 4 Mb/s
42.91 / 0.9807 and 43.64 / 0.9825; 3 Mb/s 42.37 / 0.9786 and 43.05 / 0.9807. The same picture
at 10 Mb/s and above; at 3 to 6 Mb/s H.265 needs 22 to 31 % less bitrate for the same PSNR.
Both formats therefore keep the same default bitrate. Details in docs/performance.md (HW3).
Tests: tests/hardware_encode.rs gets the HEVC counterparts of the H.264 ones (what is taken
and declined, a round trip through our HEVC decoder with luma PSNR above 30 dB, keyframes, MP4
and QuickTime exports with AAC, ffmpeg / ffprobe reading `hevc` / `Main` / `hvc1` /
`yuv420p` / BT.709, exact sizes, hostile configurations, encoders dropped at any point, the
format list agreeing with the probe, sessions on the hardware encoder); settings_tests.rs
checks names, ids, defaults, the summary, two-pass refusal and the probe; export_ui.rs the
controls H.265 shows and hides.
Stacked on the hardware H.264 encoder (the first commit of this branch).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Export mode, the status bar and the Progress panel showed "Encoding 37%" and nothing about how
long the rest would take. They now say "37% · 2:05 left".
- `Progress::eta()` (the new `pace` module) is the time left at the job's speed over the last
15 seconds. Each call is also a reading, so the UI polling on every frame it draws the job
feeds it, and none of the places that advance `done` had to change. A trailing window follows
the real speed changes of an export (an intro, a banner, plain camera footage) where an
average since the start lags; a stall makes the estimate grow instead of freezing it; a long
idle start (the loudness pass) is not counted as the job's speed; nothing is shown until a
second of readings has progress, and nothing once the job is done or finished.
- `format_eta`: `45 s`, `2:05`, `1:02:05`, rounded up.
- Shown in the status bar ("Exporting... 37% · 2:05 left", and the `status.job.progress`
automation label), in the Export queue (on its own line: the column is narrow) and in the
Progress panel. The engine adds `etaSeconds` (null while unknown) to every job of `jobs.list`
and to the encoding items of `export.queue.list`; docs/agents.md says so.
Measured on a whole 9:29 export (193 s, Apple M1), asking every 3 s: the first estimate comes
after 6 s; from 30 s on, the predicted end of the export is on average 1.1 s early and never
more than 14.5 s off (7.5 % of the run). The status bar read "9% · 3:00 left" at 21 s and
"97% · 5 s left" at 184 s.
Tests: `pace` (steady job, speed change, stall, idle start, count going back, bounded history,
extreme values, a whole `Progress`), `format_eta`, the engine (`etaSeconds` of jobs and queue
items), and export_ui (status bar label and queue item).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Export encodes H.264 on the CPU with the built-in encoder, which is most of the time
of an export (about two thirds of the busy CPU in a profile of a 20 s export). On
macOS the media engine can do it: a VideoToolbox compression session behind
`filmcraft_export::VideoEncoder`, registered by `filmcraft_platform::register()`.
- `ExportSettings::hardware_encoding` (`HardwareEncoding::Off` | `Auto`; Export > Video >
Hardware Encoding, `"hardwareEncoding": "off|auto"` in `file.exportMedia`, a boolean is
understood too), off by default: the built-in encoder's output is byte-identical on
every machine, a hardware encoder's is not.
- `videotoolbox_encode` (FFI, same containment as the decoder: SAFETY comments, no
panic across the callback, safe API) and `hardware_encode` (safe adapter and factory).
- The factory declines, so the built-in encoder is used, for other formats, MXF, two-pass
VBR, HDR, odd sizes, non-square pixels and any configuration VideoToolbox cannot create a
hardware session for. An encoder that fails mid-export stops it with an error: unlike a
decoder, it cannot hand a half-written stream to another one.
- No B-frames: on real 1080p footage the quality is the same with and without them
(+-0.3 dB at equal bitrate) and the bitrate lands closer to the target, and without
them every frame is presented at its decode time (no composition offsets, no edit list).
- `export::register_encoder` ignores a factory registered twice; `encoder_registered`.
Apple M1, 8 cores, single runs: a 9:29 timeline (camera clip, ProRes 4444 overlays with
alpha, AAC, loudness) exports in 311 s instead of 793 s (2.5x faster); its densest 134 s
in 84 s instead of 395 s (4.7x). The outputs compare at SSIM 0.990 / PSNR 46.5 dB. The encoder
alone runs 200 fps at 1080p (docs/performance.md).
Tests (tests/hardware_encode.rs): what is taken and declined, a round trip through our
software decoder, an export that decodes in our decoder and in ffmpeg, the fallback for
everything hardware declines, exact output sizes, an empty range, hostile configurations
and encoders dropped at any point; the engine's `hardwareEncoding` parameter.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
NVENC through nvEncodeAPI64.dll (API 12.1, FFI layout checked against the C header by generated
tests), registered as an Export encoder in front of the software one; Export > Hardware encoding
(off by default). Declines two-pass VBR, HDR, MXF, unsupported sizes; B-frames, IDR/GOP, VUI, avcC.
Tests: decode with our decoder (PSNR), pipeline export vs software, decline paths.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMorBHwct72udS2LiC3ZHp
- aac: the decoder notes SBR data in fill elements (EXT_SBR_DATA /
EXT_SBR_DATA_CRC), so implicitly signalled HE-AAC is recognised;
Decoder::sbr() covers explicit signalling too.
- codecs: aac_output_rate() gives an AAC stream's output rate (the
extension rate, or twice a <= 24 kHz core when SBR data is present); the
MP4 reader reports it instead of the core rate, so sequences made from an
HE-AAC clip get 44.1 kHz instead of 22.05 kHz. Our decoder now also takes
explicitly signalled HE-AAC (it decodes the AAC-LC core) and, when the
stream rate is twice the core rate, interpolates the core to it with a
streaming half-band upsampler. The SBR high band is not reconstructed yet.
- export: the AAC bitrate is capped at 6 bits per sample and channel
(ISO/IEC 14496-3 frame limit) instead of failing with "bitrate out of
range" (320 kbps stereo at 22.05 kHz).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The five 'mux created' / 'audio' expects in the stepped exporter now return
an ExportError::Encode instead of panicking if the writer or audio pipeline
is missing, and the non-stepped dispatch returns Unsupported instead of
unreachable!(). The crate root gets the never-crash deny attribute, so every
FilmCraft production crate now carries it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rust/clippy 1.99 added `chunks_exact_to_as_chunks` and tightened
`needless_late_init` and `drain_collect`, failing `clippy -D warnings`
on main.
- `chunks_exact(N)` / `chunks_exact_mut(N)` with a constant N become
`as_chunks::<N>()` / `as_chunks_mut::<N>()` (fixed-size arrays, so
several byte reads drop their per-element indexing). relink's
fingerprint uses the `(chunks, remainder)` split; its hash output is
unchanged.
- Late-initialised `let`s in ac3 bit allocation and opus CELT
bands/rate become `let x = if/match …`.
- `dpb.drain(..).collect()` becomes `std::mem::take(&mut dpb)`.
No behaviour change; `cargo xtask ci` passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exporting the same project twice could give different files, because two
export parameters followed the number of cores the process sees:
- The muxer got one chunk per render batch, and a batch is one frame per
rayon thread. The chunk layout (stsc/stco, PCM packet sizes) changed with
the core count; the samples themselves did not.
- The H.264 encoder used one slice per core (capped at one per four MB rows),
so the bitstream and every decoded picture changed with the core count.
On a host whose CPU quota changes (containers, power limits), two exports in a
row differed. Now packets are muxed in fixed groups of 16 output frames
(INTERLEAVE), each followed by its audio, whatever the batch size, and H.264
exports use one slice per four macroblock rows. Hosts with 17 or more threads
already used these values, so their 1080p output does not change.
Tests: determinism_tests (fail on main): same bytes for ProRes, H.264 and
Motion-JPEG at 2/3/5/16 cores and 1/7 frames per step; 64 slices for a
4096-line H.264 export.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>