* Build: use craft-media for bitstream, isobmff and matroska
filmcraft-bitstream, filmcraft-isobmff and filmcraft-matroska re-export
craft_media::{bitstream, isobmff, matroska} from craft-libs. The packages
and their public paths stay, so dependent crates build unchanged. The
integration tests in crates/isobmff/tests and crates/matroska/tests run
against the re-exports.
* Build: pin craft-media at 9db732e
craft-media spells the Matroska colour types in US English: Colour is
Color and VideoInfo::colour is VideoInfo::color. The spec names
element_ids::COLOUR, COLOUR_SPACE and VpcConfig::colour_primaries stay.
* matroska: the oracle skips the duration of a track's last frame
Ported from craft-media: the last audio or video frame of a track cut by
-t is shorter than a codec frame. ffmpeg 8 writes a shortened
BlockDuration and reports the codec frame duration; a remux by our writer
stores DefaultDuration and ffprobe reports the shortened one. Subtitle
cue durations are still compared. This fixes oracle::mjpeg_ac3 with
ffmpeg 8. The HDR test is named hevc_hdr_color, as in craft-media.
* matroska: laced_remux laces every audio track and checks timestamps
craft-media laces only tracks with a DefaultDuration. The remux gives
each audio track without one its mean packet spacing, so Vorbis, FLAC
and PCM lace too, and compares every packet's timestamp with the
source's. Ported from craft-media's oracle_matroska.rs.
* codecs: the Matroska audio timing test laces its Xiph case
craft-media laces only tracks with a DefaultDuration. The test's
Matroska helper sets the AAC access unit's duration (1024 samples,
rounded to the nanosecond), and the test checks that the Xiph-laced file
has laced blocks and the unlaced one has none.
* Build: repin craft-media to f97c84d27 (NAL length-size checks)
craft-libs#245 is on storytold/craft-libs main, so the dependency points there.
* Build: repin craft-media to f0ad6c1e1 (QuickTime v2 PCM entry limits)
ci.yml calls the shared workflow (cached apt packages, nextest, main-only cache saves, Cargo.lock check). FreeBSD and Windows ARM64 run nightly and on pushes to release instead of on pull requests. Depends on storytold/craft-common-actions#69.
* Packaging: pin the Windows signing client and check its hash
* Packaging: pin appimagetool and check its hash
* Packaging: an architecture without a pinned appimagetool hash fails early
* Packaging: pin the AppImage runtime (type2-runtime 20251108) and pass it with --runtime-file
* Packaging: assign the empty hashes with '' (shellcheck SC1007)
* Fonts: pin craft-fonts bfcbf71; CRAFT_FONTS_REQUIRED fails without CRAFT_FONTS_DIR
craft-fonts main adds Noto Sans CJK SC, Noto Sans Arabic, Noto Sans Thai and
Noto Sans Georgian. FilmCraft registers every CRAFT_FONTS entry, so releases
pick them up with the pin bump. ATTRIBUTION.md and NOTICE list them.
With CRAFT_FONTS_REQUIRED set, crates/text/build.rs errors when
CRAFT_FONTS_DIR is unset or empty, not only when it is not a checkout.
Refs storytold/craft-libs#197
* Fonts: only CRAFT_FONTS_REQUIRED=1 enables required mode
* text: the CRAFT_FONTS_REQUIRED message says unset or empty
* fix(engine): use resolve_absolute for stored media paths
* fix(engine): give the reason when a scratch disk folder is refused
* CI: check that git dependencies come from storytold
* CI: release only with git dependencies from storytold
* doc_path: a foreign drive path without a document is refused, not document-relative
* Workflows: one Git-source check: lockfile present and current, no source replacement, storytold hosts only
* Workflows: the Git-source check reads the legacy .cargo/config too
* Workflows: the Git-source check parses cargo config with tomllib
* Tests: one integration-test binary per crate
Each crate with several tests/*.rs files keeps them as modules of tests/it/,
so cargo links one test executable per crate instead of one per file. Files
that change process-wide state or time a workload stay binaries of their own.
* Profiles: line tables for workspace crates, no debug info for dependencies
* Windows ARM64: nightly and on release pushes instead of every push to main
* xtask ci: clippy and tests in the dev profile
The release profile's thin LTO and single codegen unit made linking the test
binaries take most of the gate. The codec and audio crates stay at opt-level 3
in the dev profile, so their tests run at release speed.
* xtask ci: nextest, doctests, no examples, one wasm check
Tests run with cargo nextest when it is installed (cargo test otherwise) and
build with --lib --bins --tests, so examples aren't linked; doctests run
separately. The wasm check keeps going past the first failing crate. VP8
fixture generation locks across processes, since nextest runs each test in
its own.
* VP9 synthetic fixtures: write aside and rename
A test in another process could read a fixture ffmpeg was still writing.
* Fuzz tests: a third of each sweep by default, all of it with FILMCRAFT_FUZZ_ITERS
Every seed, mutation kind and thread count still runs. Header bytes each get
one overwrite value and two bit flips in rotation, so every value and bit
index still occurs across the region. FILMCRAFT_FUZZ_ITERS=N runs the whole
sweep plus N random rounds per seed, as before.
* Platform fallback tests: one failure point per hybrid path
The failure-anywhere test injected a failure at four points around every
restart point; the hybrid's paths differ only in the first GOP, around the
second and last restart points, at the end and never. The replay test needs
the first two GOPs.
* Export tests: shorter ranges with the same checks
The bitrate test measures 0.5–1.5 s, where each preset reaches its own
target (on 0–3 s the simple demo left two presets at the same rate). The
every-preset test exports three frames per preset.
* Audio tests: effects in parallel, a table-driven FFT for the Opus quality metric
The extreme-settings test keeps both settings and the 40 s tail for every
effect, one thread each. The Opus quality metric's FFT builds its twiddles
once and allocates nothing per transform (same algorithm and summation order).
* CI: dev-profile gate with nextest
xtask ci no longer builds with --release, so the release-profile overrides
go; nextest is installed for the test step.
* CI: apt packages through the cached install-apt-packages action
* Review fixes: fuzz tests with a panic hook in their own binaries, loud fixture lock
The fuzz tests of av1, h264, hevc, mpeg2v, vp8 and vp9 count panics through
a process-wide hook, so a panic in another test of the same process would
fail them; they are binaries of their own again. The VP8 fixture lock fails
the test instead of skipping it silently, and the full-sweep doc comments say
how many truncation points the full run samples.
* Fuzz tests: rotate the default subset per run (FILMCRAFT_FUZZ_SEED replays one)
* platform tests: decode-switch tests in their own binaries, one NVENC lock
* packaging: use [[ for bash conditional tests (SonarCloud S7688)
SonarCloud flags every bare [ conditional in bash scripts under packaging/ as a reliability smell (rule S7688). Convert the flagged [ ... ] tests to [[ ... ]]: safer word-splitting and no glob surprises, same semantics for these string and file tests.
* tools: quit the super-linear regex scans in the spec table extractors (SonarCloud S8786)
SonarCloud flags five regular expressions in the AV1/HEVC extract_tables.py tools as super-linear: a lazy (.*) under re.S that rescans the whole remaining spec text at every position, a \s*([^|]+?)\s* sandwich, and \s+(.*) tails that can absorb the same whitespace repeatedly.
- the fence block reader now walks the ~~~~ c / tilde-run pairs with two linear searches instead of the lazy .*? findall; block-pair and 7+ tilde-run semantics are unchanged
- the symbol-row capture restricts its first and last characters away from [|] so the lazy group's backtracking jumps are gone (identical for every non-blank cell; a cell of only spaces is no longer captured, which the table rows never contain)
- the empty-comma cell splitter no longer lets \s* swallow the newline it anchors on; consecutive line breaks inside a cell no longer split (spec cells are single-line rows)
- initValue / ScalingList value captures start on a non-space so \\s+ and .* no longer share a split point (captured text is unchanged for all real values)
Verified old vs new run on synthetic spec markdown: byte-identical spé tables output and stderr.
SonarCloud flags every bare [ conditional in bash scripts under packaging/ as a reliability smell (rule S7688). Convert the flagged [ ... ] tests to [[ ... ]]: safer word-splitting and no glob surprises, same semantics for these string and file tests.
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the hand-rolled civil_from_days / days_from_civil copies (app log
timestamp, Media Browser date column, auto-save file-name stamp, MXF header
timestamp, golden-image attribution date) with chrono as a workspace
dependency. The existing chrono entry on apps/filmcraft (clock feature) was
lifted to the workspace. Output formats are unchanged and pinned by tests.
AGENTS.md now says time goes through chrono.
Follow-up to storytold/cadcraft#1785
* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed
chrono's From<SystemTime> unwraps internally, so a clock past year 262143
panicked. logging::timestamp and golden::today now read the duration since the
epoch and go through the fallible DateTime::from_timestamp, reading an
out-of-range or pre-epoch clock as the epoch as the old saturating code did.
timestamp, today and media_browser::format_date write the year with {:04}
rather than %Y, which prefixes a '+' past year 9999.
Tests cover the far-future and year-10000 cases for timestamp and format_date.
* Don't build an unrepresentable SystemTime in the far-future test
UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.
* time: preserve out-of-range years instead of the epoch (FredApps review)
`DateTime::from_timestamp(..).unwrap_or_default()` changed the existing output:
inputs the old hand-rolled civil-from-days converted (e.g. `10_000_000_000_000`
s → 318857-05-20 17:46:40) collapsed to `1970-01-01 00:00:00`. Restore the old
conversion domain with a cycle-based helper: reduce by whole 400-year Gregorian
cycles (146_097 days), convert the in-range remainder with chrono, and carry the
year back as i128 (saturating to the output type). Only the year leaves chrono's
range; month, day and time of day are always exact.
Sites (all UTC):
- apps/filmcraft logging.rs `timestamp` (SystemTime, u64 secs): pre-1970 still
clamps to the epoch; a far-future clock keeps its year. u64::MAX/2 s →
"292277026596-12-04T15:30:07.000Z", 10e12 s → "318857-05-20T17:46:40.000Z"
(was "1970-01-01..."). SystemTime cases built with a checked add (Windows-safe).
- crates/engine media_browser.rs `format_date` (u64): 10e12 s → "318857-05-20
17:46", u64::MAX → "584554051223-11-09 07:00" (was "1970-01-01 00:00").
- crates/format autosave.rs `civil` (i64 + i32 offset): keeps years, e.g.
i64::MAX → 292277026596-12-04 15:30:07, i64::MIN → -292277022657-01-27. The
separate `unix + offset_secs` overflow guard (checked_add) is kept: only an
overflowing shift (old code wrapped it to garbage) reads as the epoch.
- crates/mxf from_unix (i64): keeps month/day/time, clamps only `year` to the
u16 field as before. 10e12 s → year 65535, May 20 17:46:40; -10e12 s → year 0,
Aug 13 06:13:20.
- crates/testkit golden.rs `today` (u64): same helper; pinned via `utc_parts`.
chrono stays the only calendar logic (NaiveDate::from_num_days_from_ce_opt on the
in-cycle remainder). No new chrono features; still default-features = false, std.
* Name the 400-year range extension as the one exception to the time rule
The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.
* Say the library computes month and day; time-of-day is plain arithmetic
Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
* security(exr): validate chunks before frame allocation
* exr: raise the decoded-buffer cap to 2 GiB so 8K float plates open
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security(prores): bound decoded plane storage
* prores: raise the decoded-plane cap to 1 GiB so 8K 4444 with alpha decodes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security(vp9): bound frame allocation
* vp9: raise the frame-size cap to 2^26 luma samples so level 6.x 8K frames decode
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vp9: keep the frame-size cap below the imports
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security(vp8): bound frame allocation
* vp8: raise the frame-size cap to 2^26 luma samples so 8K frames still decode
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(bitstream): validate NAL prefix size
* bitstream: accept every NAL length width the avcC/hvcC field can declare (1 to 4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security(engine): confine stored media filesystem access
* relink: index symlinked files; skip only symlinked folders
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security(render): bound expanded frame plans
* render: count plan layers as they are pushed instead of re-summing every step
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security(mxf): share resolved descriptor metadata
* mxf: keep every source-package track in the no-material-package fallback
Two packages may reuse a track id and number; comparing only those dropped a real track.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* codecs: walk open-GOP leading pictures back in a loop, not by recursion
MxfVideo::sync_before and MpegVideo::sync_before recursed one GOP back
per leading picture, so a crafted index whose every GOP leads its key
frame recursed once per GOP: tens of thousands of frames for a small
file, a plausible stack overflow in dev builds.
Both now use gop::open_gop_sync, a loop over the same steps. It stops
when the index's sync sample lies past the picture instead of looping,
and reads pts and keys with get().
Fixes#2803
* gop: keep decode_start's doc comment on decode_start
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security(text): keep font scans within configured folders
* fonts: still find symlinked font files; skip only symlinked folders
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fonts: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security(ui): bound export folder elision work
* export mode: keep location_link's doc comment on the function
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security(tts): make malformed-token scanning linear
* tts-text: find a pause marker's ] by byte, so a multi-byte character at byte 64 doesn't hide it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(control): bound synthetic pointer events
* control: check ui.click's count before queuing any input events
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security(engine): bound graphic template thumbnails
* graphic templates: scale a capped thumbnail to fit both sides of the canvas
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* security(proxies): reserve outputs atomically
* proxies: ReservedOutput::file returns an Option instead of expect()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Raise a loaded project's next_id past the ids already in it (fixes#2809)
* Demo project: start new ids past its fixed transition and marker ids (#2809)
* Count transcript, source graphic and narration keys as ids in use on load (#2809)
* Format tests: a transcript's key raises next_id on load (#2809)
* Count ids that are only referenced (clip items, subclip parents, transition ends, sends, outputs, cameras, merged clips) on load (#2809)
* Scroll the timeline clip menu when it is taller than the screen (#2852)
The clip menu has about 35 items and ran off the bottom of short
screens with no way to reach the rest. It now scrolls inside the window,
as the panel menu already does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Let the clip menu move up before it scrolls
The scroll area only took the height left below the pointer, so the menu
stayed where it was opened and scrolled even when the whole menu fit on
the screen, and the clip menu tests in clip_dialogs_ui could no longer
click the items past that height. Asking for the full height lets the
popup move up as before; it only scrolls when the screen is too short.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #703: test that timeline track lines are visible and the playhead head is short
The lines between timeline tracks were drawn in the timeline background colour, so they could not be seen, nothing marked the top of the highest video track, and the playhead head covered the whole ruler. The tests check that separator-coloured lines run under every track and above the top video track, and that the head stays within the bottom 14 points of the ruler. Without the fix no such lines are drawn and the head is 38 points tall.
* #130: test that timecodes and menus use the Premiere-style type
The big timecodes were 15 pt semibold and the menu bar's menus used egui's default text size. The tests check that the timeline, program and reference timecodes are painted in 16 pt regular in the timecode colour, and that File menu items are drawn at 13 pt. Without the fix the timecodes are 15 pt semibold and the menu items are 12 pt.
* #1084: test that the Import captions file button label is legible in light mode
The label was always white, so it disappeared against the light grey button in light mode. The test shows the empty Captions tab in the dark and light themes and requires at least 3:1 contrast between the label and the button fill. Without the fix the light theme fails at 1.04:1.
* #1309: test that the sort arrow does not cover a sorted column's name
In a narrow List-view column the sort arrow was painted over the end of the clipped column name. The test narrows the sorted Name column and requires the name's visible clip rect to end left of the arrow. Without the fix the name stays visible up to x=67 while the arrow starts at x=60.
* #1084: hold the Import captions label to 4.5:1 contrast
The label is normal-size text (12 pt semibold), so WCAG AA asks for 4.5:1, not the 3:1 the test allowed. Both themes clear 4.5:1 today; with the fix reverted the label is at 1.04:1.
* #130: menu bar titles use the 13 pt menu text, like their menus
#130 styled the menus' items with theme::menu_style through MenuConfig, which egui applies to submenus only. The bar's own titles kept egui's default 12 pt, dimmer button text. MenuBar::style takes the same style, so the titles and their menus now share one size and colour, as in a native menu bar. The #130 test now checks the File title too; without this change it fails at 12 pt.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- Read and write CLI inputs/outputs on a blocking pool instead of stalling
the async runtime (rust:S7493).
- Document the FLAC STREAMINFO MD5 as the RFC 9639 integrity signature it
is, and validate the AV1 generator's spec path before reading it.
- Normalize the DCT basis literals that read as numeric suffixes.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Found while testing crates/tts-text/src/normalize.rs:416 (parse_time am/pm suffix) and :947 (parse_wordish a.m. branch); consumed by crates/tts-text/src/phonemize.rs:398 (is_single_letter)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/interchange/src/common.rs:68
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/tts-text/src/normalize.rs:78-97 (normalize, '-' / currency arms)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/captions/src/ttml.rs:358 (parse_time, offset-time branch) and :310 (decimal_ticks)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* security: validate CLI paths/args in dev tools (SonarCloud S2083/S8707/S8705/S7044)
Resolve and validate command-line path arguments before use, enforce HTTPS at
download sinks, and confirm path containment. Hardens local codegen/fetch tools
against path-traversal and command-argument-injection findings.
* ci: exclude dev tool scripts from SonarCloud coverage gate
The extract_tables.py codegen tools aren't part of the unit-tested surface;
exclude tools/, docs/, examples/ from coverage so the new-code coverage gate
doesn't block hardening-only changes.
The async screenshot() handler read and deleted the screenshot with blocking
std::fs calls, stalling the Tokio executor. Switch to tokio::fs::read /
tokio::fs::remove_file (await) and enable the tokio "fs" feature.
Calls storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main
with secrets: inherit. Adds SonarCloud badge to README where missing and
removes the redundant sonar-project.properties (config now passed by the
reusable workflow).
Found while testing crates/time/src/lib.rs:483 (parse_timecode, multi-field branch)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/captions/src/scc.rs:37 (parse_timecode)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/captions/src/vtt.rs:75 (parse, NOTE block check)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Require both ranges to be nonempty before comparing endpoints. This prevents zero and negative durations inside a clip from reporting a false overlap, matching is_empty and intersect.
Validation: 22 time tests and 69 edit tests pass; crate Clippy, formatting, layers and assets pass. Full CI is blocked by three existing UI nonminimal_bool diagnostics; contributor approved publishing with this blocker documented.
Co-authored-by: OpenAI Codex <noreply@openai.com>
Found while testing crates/color/src/lib.rs:298-302 (parse_hex); used by crates/engine/src/commands.rs:3615 (json_to_param, Color params)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/interchange/src/xml.rs:7 (escape, used by start_tag for attributes)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#395 (cfe0c751) appends installed right-to-left faces (system-arab,
system-hebr) after every other fallback, by design. On a machine with an
Arabic system font and craft-fonts without Arabic, they follow the
craft-fonts faces, so japanese_needs_an_installed_font, which expects a
craft-fonts face last, has failed since then.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69a6c4b6 added MimeType= to packaging/linux/ai.storyteller.filmcraft.desktop
but not to the entry a dev build writes, so
dev_icon::tests::desktop_file_matches_the_packaged_fields has failed on
main since then.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xtask ci runs clippy and the tests with --release. With the shipped
release profile (thin LTO, codegen-units = 1) linking the test binaries
alone ran past 70 minutes on a standard runner. CI keeps opt-level but
turns LTO off and uses 16 codegen units.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only path-filtered workflows run on pull requests today, so most PRs land
without a test run. Port of the wordcraft#800 workflow: Linux, standard
GitHub-hosted runner, read-only token, no secrets, craft-fonts pinned to
the release.yml commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- decoder.rs: MaxHSize 16384, MaxVSize 8704 and MaxPicSize 35651584 per AV1 Annex A (the earlier
16384 x 8704 product is four times the area limit); boundary unit tests on frame_size_allowed;
README states the three limits.
- testkit: with_deadline/set_progress (whole-sweep hang guard naming the last input;
FILMCRAFT_HOSTILE_TIMEOUT_SECS overrides) and ffmpeg_has_encoder.
- The nine fuzz_hostile tests drop their per-run elapsed assertions (which could not catch a
hang and could fail a slow runner) and run under the guard.
- A seed whose encoder is absent from ffmpeg still skips (and fails under
FILMCRAFT_REQUIRE_ORACLES=1); an encoder that is present but fails now fails the test.
- mpeg2v failure labels carry the seed name.
The exact 16384 x 8704 boundary stays covered by the frame_size_allowed unit tests;
decoding such a frame needs ~1.5 GB, too much for every cargo test. Empty seeds and the
mpeg2v seed skip now say so, like the other skips.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The cap was 1 << 27, below the 16384 x 8704 it quotes, so a conforming maximum-size
frame was refused. Extract frame_size_allowed() and test the exact boundary, plus a
stream at 16384 x 8704 that must pass the size check. Note the AV1 and MPEG-2 limits
in the crate READMEs. A fuzz seed that ffmpeg cannot encode (encoder missing) now
reports SKIPPED, and fails under FILMCRAFT_REQUIRE_ORACLES=1, instead of passing silently.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CodeRabbit on #2006: --control-input-only drops every command-inbox
command, and Quit now arrives that way instead of as terminate:, so it
left the window open. app.quit is let through; other menu commands are
still ignored, as docs/control-protocol.md says.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The native app menu's Quit item was muda's predefined quit, which calls
AppKit's terminate: and owns the Cmd-Q key equivalent, so the window never
got the close request the unsaved-changes prompt waits for. Quit is now a
regular menu item that sends the app.quit command, which closes the window
through the prompt, on the command's shortcut (Cmd-Q unless remapped).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use the existing begin/merge history contract for character styles, isolating clip, layer and selected ranges. Cover actual pointer gestures, undo/redo, fresh gestures and invalid targets.
ItemId wraps a u64 (item_p parses the `item` param with u64_p), so
reading activeSequence with as_i64 would silently return None for
any id above i64::MAX and make document()'s sequence go null instead
of inspecting the valid id project.inspect reported. Switches to
as_u64 and adds a regression test for that boundary (proven to fail
against as_i64 first).
Addresses a CodeRabbit review comment on #2480.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
document() composed project.inspect and sequence.inspect as two
separate round trips to the app (two lock scopes in headless mode,
two TCP round trips in bridge mode). sequence.inspect with no `item`
answers for whichever sequence is active *now*, not the one
project.inspect just reported, so if the active sequence changes in
that window (another client, or the app's own UI, switching tabs)
the pair comes back inconsistent: project.activeSequence names one
sequence while sequence describes another.
Reproduced against a running bridge-mode app: a real MCP client
hammering doc_inspect while an independent connection flipped
sequence.open between two sequences mismatched on ~300/300 calls, so
this is a wide window, not a rare edge case.
Fix: read project.inspect's activeSequence id first, then pass it
explicitly as sequence.inspect's `item`, so the second call always
describes the sequence the first one named, regardless of what
becomes active afterward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found while testing crates/time/src/lib.rs:423 (format_time, TimeDisplay::Seconds)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/time/src/lib.rs:418 (format_time, TimeDisplay::AudioSamples)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/color/src/lut.rs:290 (Lut::to_cube, the `both` branch)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With the playhead outside a clip the tab shows the clip's text at its
start, but the edit went to graphics.set without a time, which writes at
the playhead clamped into the clip (its last frame). For keyframed text
the shown value stayed and another keyframe changed. Each row now keeps
the time it read its text at and the edit is written there; a new test
covers keyframed text with the playhead past the clip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new stopwatches show for every animatable parameter of the layer's
definition, but effects.toggleAnimation only found parameters stored in
the instance, so a layer saved before a parameter existed answered "no
such param". The parameter is now created from its default first (an
existing one and its keyframes are left alone). Engine test included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An effect in a sub-folder was drawn left of that sub-folder's label (Gaussian Blur at 47 points,
Blur & Sharpen at 55), so folders and effects read as one level. Rows now follow one rule,
`tree_x(depth)`: an effect sits one level deeper than its folder (sub-folder or, for loose
effects, the top folder), with its icon and label where a folder at that depth has them; user and
built-in presets and Lumetri presets the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8DvNHjchh5Gf2q24UsWQn
The first effect added from the library after a clip's fixed effects (Motion, Opacity, Time
Remapping; Volume, Channel Volume, Panner on audio) gets an accent-coloured line on its top edge.
It is drawn on the row's edge, so no row moves; with only fixed effects there is none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8DvNHjchh5Gf2q24UsWQn
#630 shipped German for the menus. This adds the other 3,213 strings, so
de.tsv has every entry of the Spanish catalog (3,590): panels, dialogs,
effects and their parameters, transitions, presets, Settings, shortcut
commands, export presets, tooltips and status messages. Same style as the
menus (du, infinitives, placeholders, ellipses and escapes kept); the
glossary in the header is extended. The 377 menu entries are unchanged.
german_entries_cover_the_original_menu_catalog required every entry to be
a menu label. It becomes german_covers_the_menus_and_has_no_dead_entries,
like the French and Italian tests: every entry needs a Spanish counterpart,
the core menus and the native macOS app menu are required, later strings
may stay English. german_renders_with_bundled_fonts checks the umlauts,
ß and German quotation marks against the bundled fonts.
Docs: ROADMAP.md, docs/localization-parity.md, docs/gaps.md, ui-egui README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJhvSJ5odpWzFw9HhPzAix
Letter spacing (Tracking) does animate in the engine — keyframes on the text
layer interpolate and the rendered width follows them — but the Essential
Graphics panel had no way to start that: its number and point rows had no
stopwatch, so a change there always set the value for the whole clip.
Every animatable number/point row in the panel (Tracking, Leading, Font
Size, Baseline Shift, Position, Anchor, Scale, Rotation, Opacity…) now has
the same stopwatch as Effect Controls. It calls `effects.toggleAnimation` on
the layer; once a property animates, a change in the panel adds or updates a
keyframe at the playhead (graphics.set was already keyframe-aware). A
Type-tool character selection edits character styles, which have no
keyframes, so those rows show no stopwatch while it is active.
Automation id: `graphics.prop.<id>.stopwatch`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8DvNHjchh5Gf2q24UsWQn
A track can carry a colour label, as many NLEs offer. The strip at the
left edge of a track header opens a menu with None and the 16 labels
(with the user's label names and colours, as in Edit > Label). The label
tints the header and the lane.
- `Track.label: Option<Label>`, serde default and skipped when None, so
older projects load and plain tracks save as before
- `timeline.setTrack {label}` (a label name; "" / "None" / null clears),
an undoable Track Settings step; unknown names are refused
- `sequence.inspect` reports each track's `label`
- automation ids `timeline.track.<V1|A1…>.color` and `.color.<Label>`
Renaming, adding and deleting tracks from the header are covered by the
open PRs for #654 and #1630, so this only adds the colour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8DvNHjchh5Gf2q24UsWQn
"Animate zoom, scrolling and menus" (appearance.animations, on by default). Off, the Timeline
zooms and scrolls in one step instead of easing, and egui's fades and slides (menus, collapsing
sections, tooltips) are instant. The style is checked every frame, because a theme or font change
installs it again. Translated in the catalogs that cover the Settings rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8DvNHjchh5Gf2q24UsWQn
The Graphics tab of the Text panel was a placeholder ("Graphics text search
arrives with M10.1–M10.2"), so text in graphics never showed up there.
It now lists the text of every text layer of every graphic clip in the
active sequence, as Premiere's does: in timeline order (then lower tracks
first, then paint order), each entry with its clip's start timecode and
track (and the layer's name when it has one). A search filters by text or
name. The timecode moves the playhead to the clip and selects the clip and
the layer (so Essential Graphics and the Program monitor's handles follow);
the text edits in place and is committed with `graphics.set` when the field
loses focus — one undo step, per-character styles kept as for any text edit.
Text that is keyframed shows its value at the playhead when the playhead is
in the clip.
Automation ids: `text.graphics.search`, `text.graphics.<clip>.<layer>.row`,
`.goto`, `.text`. The three new messages are translated in the catalogs the
coverage test requires.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8DvNHjchh5Gf2q24UsWQn
A `"wait": null` (or any non-boolean) was kept and then read as false,
so the process could exit before the export was written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every frame shown as a timeline tile or a Project panel hover scrub got a
texture of its own (thumb-<item>-<frame>-<width>) that was kept until the
project was replaced, so scrolling, zooming and scrubbing grew GPU (on
integrated GPUs, system) memory without bound. Beyond 512 thumbnail
textures the least recently used are freed, never one drawn in the
current pass.
Refs #1682
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Publish extracted WAVs through each host's no-clobber operation, retry occupied names, and update imported references before project merge. Distinguish blocked directories from file collisions, route AAF/OMF/OMFI extensions consistently in both import modes, and keep the open project on replace-mode parse or extraction failure. Cover native, virtual, and staged-write failures.
Co-Authored-By: Codex <noreply@openai.com>
Rendered previews are full-resolution ProRes (gigabytes a minute), and
PreviewStore::open read each file whole into memory, keeping up to six
open; the render also opened every finished segment right away. Rendering
previews and playing them back filled memory until the OS killed the
app. Native builds now open preview files through a file reader (the
container's index now, frames on demand), refusing any whole-file read,
and a finished segment is opened when it is first played.
Fixes#2185
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Local CLI commands (`run`, `exec`) exit right after the last command, which
killed a background export before anything was written. Long commands now
default to `wait: true` there; an explicit `wait` is kept, bridge mode is
unchanged (the app keeps running).
Fixes#2342
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
craft-libs#169 is merged, so the fork pin is no longer needed. Pinned to
storytold/craft-libs 82a24b006eefa036b43c09a67fb9a3fbd17ffe63 (main after the merge) rather than the older 12281d07,
which predates resolve_absolute and the Windows colon refusal.
Writer::add rejected a duplicate name by comparing the new name with every
sibling through compare_names, which allocates two Vec<u16> per call, so n
children in one storage cost O(n^2) allocating comparisons. An AAF export
puts every marker and component in one storage: 20 000 markers took 58 s,
20 000 clips 56 s, 20 000 connected clips 87 s and 100 000 markers never
finished (an FCPXML of 1 MB is enough). Doubling the input quadrupled the
time; every other exporter stayed linear.
Each storage keeps a set of its children's names as sibling order compares
them (UTF-16 code units, upper-cased), so the duplicate check is a hash
lookup. Same result: case-insensitive, also beyond ASCII, per storage.
20 000 markers now export to AAF in 0.38 s, 100 000 in 2.0 s, 20 000 clips in
0.23 s and 20 000 connected clips in 0.45 s.
Tests: adding 40 000 children must take under 3 s (0.02 s now, 21 s before)
and the duplicate rules are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/ui-egui/src/calc.rs:34-41 (eval)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/tts-text/src/normalize.rs:352 (parse_amount, used by parse_money at :266)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Media and proxy paths read from a project file, a project template or an
interchange file are opened only when they name a local file or a file on
the network share the document itself is on (craft-docpath); a relative
path is taken from the document's folder. Other paths count as missing:
the item renders the offline slate with the reason, the open says how many
were not opened, and Link Media lists them. This covers the open-time
scan, rendering and proxies, the growing-files look and interchange
import. Files imported or linked in this session are opened as given.
The Scratch Disks folders use the same check, and Captured Audio and Video
(voice-over takes, pasted images, the default proxy folder) and Audio
Previews follow the rule Auto Save and Video Previews already follow.
Project Auto Save and Video Previews folders named in a project file are used
only when they are inside the project's own folder or were chosen in Project
Settings > Scratch Disks on this computer (remembered in the preference
autoSave.scratchFolders). Other folders fall back to the default location,
with a log line and a message when the project opens. Network-share and
device paths are refused for these folders.
A folder or bundle, a link to another kind of file, or a file whose extension
the importer doesn't take is refused with a message instead of being handed to
the system's default app.
ColorSignal::apply_to wrote the sample entry's colr box only for HDR, so
SDR ProRes, DNxHR and APV MOVs carried no colour that Apple's media stack
reads (it takes their colour only from colr, not from the codecs' frame
headers): QuickTime Player and Final Cut saw no primaries, transfer or
matrix. SDR exports now get colr with the export's colour too (nclc in
MOV, nclx in MP4); HDR metadata stays PQ-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing apps/filmcraft-cli/src/main.rs:468
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Clip > Multi-Camera > Enable and Flatten were enabled for any selection
and then failed when chosen with plain clips selected ("select nested
sequence or multi-camera clips"). Their enabled checks now ask what the
commands need: a nested sequence clip for Enable, an enabled multi-camera
clip for Flatten (linked partners included), so the menu greys them out
otherwise. Calls naming their clips still work without a selection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main now falls back to CPU effects when an adapter cannot render to
Rgba32Float, so the scene, blend and transition parity tests still run
on such adapters. Only gpu_effect_chains_match_cpu_render needs a float
render target, and it is the one test that skips without it. Also revert
the import reorder, which failed cargo fmt --check on edition 2024.
An avcC High-profile extension or an hvcC record can carry a chroma
format of 0 for a 4:2:0 stream, which labelled such files "YUV 4:0:0"
in Media File Properties (two files of the 92 in #1959). The SPS is what
the decoder decodes, so it is read first; the record's fields are only
the fallback when no SPS can be parsed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
Bound outer export batches by actual renderer-pool capacity before nested Rayon work can steal a sibling frame. Cover image sequences, GIF, stepped export and overlapping audio with the original one-renderer regression.
Addresses #1956.
Full original GPU contribution preserved at `preserve/861-gpu-export-original` (674e8cd68d91184be4fad56d6ecb6e90e83d1f1b).
Co-authored-by: Codex <noreply@openai.com>
Cache decoder failures without allocating silence from the file's sample duration. Zero only the failed packet's overlap with the requested source-rate buffer, including samples from an earlier valid packet when their spans overlap.
Co-authored-by: Codex <noreply@openai.com>
Compute the final ASCII frame-number boundary without slicing inside a UTF-8 character. Preserve Unicode prefixes and add parser and public image-sequence import regressions, including missing frames and timeline placement.
Fixes#1565
Co-authored-by: Codex <noreply@openai.com>
Use a checked localhost prefix probe so Unicode file URLs retain their media paths without slicing inside a UTF-8 character. Add public converter, resolver and EDL import regressions.
Fixes#1591
Co-authored-by: Codex <noreply@openai.com>
Respect per-action linked selection, preserve exact current selections for Clear and Ripple Delete, and keep Option-drag group copying intact. Add regression coverage and align the Spanish and synthetic TIFF test fixtures.
Restores the individual-selection handling originally submitted by @vbasky in #685, with corrected click/drag behavior.
Fixes#1163Fixes#680
Assisted-by: OpenAI Codex
Co-authored-by: Vikram Bhaskaran <vikram.bhaskaran@outlook.com>
Found while testing crates/format/src/autosave.rs:74 (list_auto_saves / rotate, called from write_auto_save at :102)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Lens Blur transition blurred both pictures with a disc of golden-angle
samples and computed every sample's offset (a square root, a sine and a cosine)
again for every pixel, though the offsets depend only on the radius and the
sample count. They are now worked out once per frame (`disc_offsets`) and the
pixels come out bit for bit the same. A 1080p picture at the default radius
blurs in 50 ms instead of 95 ms (1.9x), and a Lens Blur frame blurs two.
The crash-recovery prompt, the Revert confirmation and Settings draw their
buttons through local `button` helpers that never set a hover cursor, so
Discard / Not Now / Recover, Yes / No and Help / Reset… / OK / Cancel kept
the arrow. Both helpers now ask for `CursorIcon::PointingHand`, and
tests/dialog_buttons_ui.rs hovers each button headless and checks the cursor
egui requests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- av1: a 4:2:2 stream (profile bit flipped) could reach blocks with no chroma size (BLOCK_INVALID) and
index MAX_TX_SIZE_RECT out of bounds; reject them with an error in decode_block.
- av1: frame size accepted up to 65536x65536 (gigabytes of buffers from a few header bytes); cap at 16384 per
side and 2^27 samples.
- mpeg2v: 16383x16383 4:4:4 sequences (gigabytes per picture) are refused; area capped at 2^25.
- fuzz_hostile tests (truncation, header byte/bit sweeps, random mutation) for h264, hevc, vp8, vp9, av1,
mpeg2v, apv, prores and dnx; ffmpeg-based ones skip without ffmpeg.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
read_face (crates/text/src/sfnt.rs) also listed faces with none of the
tables skrifa reads glyph outlines from (glyf, CFF, CFF2, VARC), such
as faces whose glyphs are bitmaps only or whose outlines are in Apple's
hvgl table. The renderer draws glyphs only from those outlines, so text
set in such a face is blank. On macOS 27.0.1 the system scan listed
"GB18030 Bitmap" (NISC18030.ttf, bdat bitmaps), so it appeared in the
Font menus.
read_face now returns no face when the table directory has none of
these tables, before it reads the name table. In a collection, the
faces with outlines keep their index. VectorCraft's font scan has the
same check (storytold/vectorcraft#781).
The drop lands inside the demo's long Ambient_Score.wav on A2, and overwriting there splits it,
so the total clip count went up by 3, not 2, and the test failed deterministically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Field labels wrap in their column instead of running left over the
category list, dropdowns cut their current choice to their width,
checkbox labels wrap at the page edge, folder fields leave room for
Browse…, and category names too long for the list end in an ellipsis
with the full name in a tooltip. The page rect is registered as
settings.page; long_translations_stay_on_the_page checks every page
in eight languages against it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A PC whose GPUs were installed from different driver packages of one vendor
(an ASUS G513QY: the integrated Radeon on the 2021 package, the RX 6800M on a
2026 one) gives the Vulkan loader two versions of that vendor's driver, and
the RX 6800M is listed once through each. wgpu's request_adapter takes the
first discrete entry, the 2021 driver's, and the window drawn through it dies
inside amdvlk64.dll (0xc0000005) before it paints: the main thread is in the
2021 driver, the fault on a thread of the 2026 one, no Rust frame anywhere.
Left to the #820 ladder, two crashed launches end in DirectX 12 only.
filmcraft_gpu::adapter::choose is wgpu's own rule (the adapters that can draw
to the surface, by device type for WGPU_POWER_PREF, then as enumerated) with
one addition: when the adapter that rule picks is listed again by the same
driver (same backend, device type, PCI vendor and device and driver name; the
same bus id when both report one), the entry exposing the most wgpu features
is used, i.e. the newer version (68 against 60 here, a strict superset).
Without such a duplicate the choice is wgpu's own, and entries of one GPU from
different drivers (RADV and AMDVLK, a vendor's driver and Dozen) are left
alone.
graphics::apply installs it as egui-wgpu's native_adapter_selector in the
default mode on Windows, as #609 was asked to be rebased onto the #827
start-up. DirectX 12 only, software mode and the other platforms keep their
adapter choice, and the ladder is untouched. The adapters and the choice go
to the log.
Tests: the laptop's listing (wgpu's rule picks the entry that crashed, choose
the other one), listings without two versions of one driver over every power
preference (choose equals wgpu's rule), surface, bus id and driver cases, and
one against the adapters of the machine running the tests (the rule is what
request_adapter returns there).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#827 rewrote apps/filmcraft/src/graphics.rs and the start-up in main.rs, so
the selector of this branch no longer applies. main's tree is taken whole
(graphics.rs, main.rs and the README note about WGPU_ADAPTER_NAME included);
the next commit hooks the selector into the GraphicsStart flow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Effects panel drew a folder's name from the last `/`-separated part of
its key (`<top>/<folder>`), so Audio Effects ▸ Noise Reduction/Restoration
showed as "Restoration" — truncated in English and untranslated in every
other language, since the catalogs translate the full name. Split once,
after the top folder, instead.
Test: effects_panel_lists_the_premiere_transition_folders checks the label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013XWUReif4gqcjU5GsjYioc
The Ruler walked one grid (the tick stride) and labelled a frame only when it
was also a multiple of the label stride. Chosen independently, the two need not
divide: at 24 fps and 100 pt a second, 5-frame ticks against 48-frame labels
meet only every lcm(5, 48) = 240 frames — one timestamp per 10 s, the rest
returning at the next zoom step, where the ladder happens to divide.
Take the tick stride as the finest division of the label stride that still
leaves ticks 12 pt apart, so both are one grid. Every label then falls on a
tick and can no longer be skipped, and the ticks stay evenly spaced around it
instead of bunching either side. Labels are >= 110 pt apart, so a divisor of at
most 9 always meets the minimum. The labelled stride, and so every timecode and
the frame it is drawn at, is unchanged.
Strides are clamped to >= 1 (the loop advances by one and tests the other for
divisibility), and the ladder's products saturate, as FrameRate is plain serde
data from the project file.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the Discord header button, Help menu and macOS app-menu links, About and Home
website links, brand assets and wordmark; rewrite docs, packaging URLs and catalogs.
Add .debrand rules and the craft-skills submodule (debrand skill) to keep upstream
syncs clean.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Adds Korean as an interface language: Edit ▸ Language ▸ 한국어, Settings ▸
General ▸ Interface Language, and System Language for ko / ko-KR locales.
- crates/ui-egui/src/i18n/ko.tsv: 3,573 entries, the Spanish catalog's sources
plus the macOS Settings item, written clean-room from the plain meaning of
the English text (sidecar ko.tsv.attribution, ATTRIBUTION.md row).
- Hangul is drawn with the installed system face cjk.rs already adds (Malgun
Gothic, Apple SD Gothic Neo, Noto Sans CJK KR…); switching is refused with a
message when there is none, like Chinese.
- Test: korean_covers_the_menus_and_has_no_dead_entries.
- Docs: localization-parity, ROADMAP, ui-egui README.
Native-speaker review welcome.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013XWUReif4gqcjU5GsjYioc
The monitors already pan with the middle button (#824). In the Timeline a middle-button drag went
down the left button's path and selected, moved, marqueed, scrubbed or cut, depending on the tool.
It now starts the Hand tool's pan from where the button went down.
ffmpeg keeps a trailing zero byte after the hvcC PPS that the in-band PPS lacks, so the byte-for-byte comparison read it as a parameter-set change and sent the stream to the software decoder from its first sample. A NAL unit never ends with 0x00 (H.264 7.4.1, H.265 7.4.2): those bytes are trailing_zero_8bits, so both sides are compared without them. The platform fallback test used a trailing zero byte as its changed SPS; it now changes nal_ref_idc.
wgpu validates indirect draw and dispatch arguments with a compute shader it
compiles when the device is created. Metal on an Intel Iris Pro (MacBookPro11,3,
macOS 12.7.6) fails to compile it ("Compiler encountered an internal error" in
MTLCompilerService), the device is lost, and the window never opens.
FilmCraft draws and dispatches nothing indirectly, so leave the check out of the
instance flags. WGPU_VALIDATION_INDIRECT_CALL=1 turns it back on. VectorCraft
does the same (storytold/vectorcraft#651).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Program monitor's transform box drew and hit-tested its handles at
their true positions only, and the painter and the ui clip stop at the
picture area. A still moved to or past the frame edge left its resize
handles off the monitor. Handles beyond the monitor now stop at its
edge (the picture itself is not moved), and the box is drawn over the
whole monitor rather than only the picture.
Fixes#1393
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#260 moved Linux file dialogs to a worker thread (run_dialog_off_thread),
which built its rfd filters from the plain extension lists. The
case-insensitive patterns from file_filters::extensions (#262, #390) were
only applied in the synchronous pickers, so on Linux every dialog hid
CLIP.MP4, clip.Mp4 and EDIT.FCPROJ again: import, relink, open file,
open/save project and save as.
prepare_dialog now turns every extension list into those patterns (and
translates the filter names, as before); the project dialogs use
project_patterns(). The literal extension stays first for save suffixes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scan and read copied the 32-byte head of every packet and every pack, PSM
and PES packet (up to 64 KB) with to_vec before looking at it. They now
borrow the window; the borrows end before the next window access. Output is
unchanged: the new large-stream test (two streams, over 2 MiB, MPEG-1 and
MPEG-2 packs, cuts inside the last packets) passes on the old and the new
code, as do the existing round-trip and truncation tests. The allocation
saving follows from the removed to_vec calls; it is not measured (the
workspace forbids unsafe, so there is no counting allocator).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A window refill resized the buffer to at least 1 MiB although read_some can
only fill what the file still holds. Near the end of the file, where the
previous window was shorter, that zeroed about a megabyte for every packet
whose length ran past the end. The buffer is now capped at the bytes left.
Window contents are unchanged: proved against the old refill for many
lengths, positions and sizes, and the zeroed byte count is asserted (at most
1000 against about 500 MiB for the old refill in the tail scenario).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One table lookup per byte instead of eight shift steps. Proved equal to the
old bitwise loop (kept in the test as the reference) for every single byte,
random inputs of 0..4096 bytes, the CRC-32/MPEG-2 check value and the
section-plus-own-CRC-is-zero property.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Unit.pos for transport streams is a byte offset (scan stores off + k *
packet_size), not a packet index; frame_bytes carried the doc line of
frame_info above its own.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Nothing outside one test read them. No behaviour change; the existing
header tests cover parse.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: in ts::scan every packet-parse failure re-read a whole 4096-packet
block from an offset that advanced about one packet. packet() returned None
both for a bad sync byte and for an adaptation_field_length that overruns the
packet, so aligned packets with a bad adaptation field cost one ~770 KB read
each (a 564 KB file made ByteSource::read_at fetch 846 MB). The fallback
offset (`pos + got`) also overshot by k packets, skipping unread data, and the
five-packet sync window only looked at the first packet_size start positions.
Fix: packet() distinguishes Bad::Sync from Bad::Adaptation. A bad adaptation
field means "no payload, grid intact": count a warning and go on with the next
packet. A real sync loss is repaired inside the loaded block (resync(): first
start whose five sync bytes line up, any distance); the block is refilled
from the file at the exact new offset only when it is used up. read() skips a
bad-adaptation packet instead of ending the unit.
Tests: read-volume tests with a counting ByteSource (bad adaptation fields,
sync byte every 189 bytes), and a resync test with a garbage prefix and
mid-stream garbage around the block boundary for 188- and 192-byte packets.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: frame_info indexed byte 6 of an AC-3 header that is only guaranteed
6 bytes long (panic), and gave E-AC-3 fscod2 3 a sample rate of 0. The PES
modes of Splitter never pruned their chunk list. read_unit reserved up to
4 GiB up front, the unit table was uncapped and 4-byte E-AC-3 "frames"
amplified input 16x. PesAsm kept passing continuation packets of a non-PES
unit start, and an adaptation-field-only unit start destroyed the PES in
progress.
Fix: b.get(6)? and None for the reserved rate; no chunk bookkeeping where
positions come from feed(); EAC3_MIN_FRAME = 8; MAX_UNITS (2^23) per stream
with a warning; read_unit preallocates at most 1 MiB and refuses units over
128 MiB; PesAsm gets a dropping state and ignores payload-less unit starts.
Regression tests for each.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: packet_len accepted an MPEG-1 pack header (b[4]>>4==2) with
only 6 bytes available, but scan reads the SCR from head[4..9]. A file
ending inside the last pack header (00 00 01 BA 21 00 00) panicked with
"range end index 9 out of range".
Fix: packet_len requires the full 12-byte MPEG-1 pack header; a shorter
tail is not a packet and takes the normal resync path. The MPEG-2 branch
already needs 14 bytes (b.get(13)) before scan reads 10, so it is fine.
Tests: truncate a valid MPEG-1 and MPEG-2 program stream at every byte
length, plus the minimal 7-byte input; both panicked before the fix.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Remove the selected clips' heads - from each in point through the
playhead - and ripple the sequence closed. Linked clips act as one
group through edit::ripple_trim_group; a playhead past every member's
out point deletes the group whole via edit::ripple_delete_items.
Refused at or before a member's in point, and on locked members.
Tests: crates/engine/src/ripple_head_tests.rs (4 tests, red on main,
green with the patch, negative control by stash round-trip).
Fixes#1357
Problem: the Properties panel laid its sections out in a plain child ui, so
when the panel was shorter than its content (Transform, Crop, Audio, the
speed button) the lower part was cut off and the wheel did nothing.
Fix: put everything below the clip header in a vertical ScrollArea, keeping the
rows 14 px clear of the scrollbar so the keyframe arrows at the row end stay
clickable. The speed button gets the automation id `properties.speed`.
Regression test properties_scroll_ui scrolls a short panel and checks the
speed button moves; it fails without the fix.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: Premiere reads Radiance HDR (.hdr) pictures; FilmCraft listed
the extension nowhere and could not open them.
Fix: clean-room RGBE decoder in filmcraft-media from the public format
description: header with FORMAT, all eight axis orders, flat, old
run-length and new per-channel run-length scan lines. Frames are linear
float (Rec.709), like EXR. Allocations are bounded by the data present;
hostile headers and truncated data return errors. "hdr" joins the
importable still extensions; stills and image sequences share the decoder.
Output equals ffmpeg's decode exactly for flat and RLE files; EXPOSURE and
PRIMARIES lines are ignored and XYZE is refused.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: Shift snapped pen segments (#853) but dragging an existing mask
vertex or the whole mask still moved freely.
Fix: while Shift is held the movement since the press is constrained to
horizontal/vertical/diagonal steps (accumulated raw vs applied totals per
gesture, so each frame's delta stays a single undo step). Unit test for
the pure step function; gaps.md G18 updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: OpenEXR files with PXR24 compression (a common lossy 24-bit
float format from renderers and compositors) were refused with "not
supported yet".
Fix: decode PXR24 chunks (16 lines, deflated byte planes of per-line
sample differences; floats keep their top 24 bits) into the existing
scan-line pipeline. Bounds-checked, no panics on damaged data. Tests:
hand-built chunks with half/float/uint channels and a partial last
chunk, damaged-file cases, and an OpenImageIO-written oracle comparing
our decode with OpenEXR's own (skips without oiiotool). Docs updated;
stale AVI / FLAC-in-MP4 entries removed from G10.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: the Matroska demuxer mapped A_MPEG/L2 and A_MPEG/L3 only, so a
Layer I track was reported as an unsupported codec even though the MP1/MP2
decoder (#809) reads Layer I.
Fix: add Codec::Mp1 for A_MPEG/L1 and route it through the same packet
duration and decoder path as Mp2. Test remuxes synthetic Layer I frames
into Matroska with ffmpeg and compares our decode (max error 3e-5).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The content hash of a nested sequence covered only its video tracks, for
audio segments too. Changing the sound inside a nest (a clip's gain, a
muted track, a new clip) left the outer sequence's rendered audio preview
valid, so playback kept the old mix, or silence. Audio segments now hash
the nested sequence's mix (audio tracks, their clips recursively, master
and submixes) instead of its picture, so editing the picture inside a nest
no longer invalidates its sound either.
Problem: `export --list-presets h264` returned nothing because the query
only matched name, description and category, while `h264` is the format
id the CLI takes everywhere else.
Fix: also match the preset's format id and label, compared with
punctuation stripped, so `h264`, `h.264` and `H.264` are equivalent.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: timeline.nudgeUp/nudgeDown returned an error for the whole selection when any selected clip was already on the outermost track in that direction. A video clip with its linked audio (A1 has no track above) therefore could never be nudged.
Fix: clips with no destination track stay where they are; the command only errors when nothing could move. Regression test covers a linked selection.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The shared release workflow installs trunk, but FilmCraft's web build
runs the wasm-bindgen CLI directly (cargo xtask web), so the 0.7.1 Web
job failed with "wasm-bindgen CLI not found". In CI, package.sh now
installs the CLI at xtask's WASM_BINDGEN version, as the per-app
release.yml did before the move to craft-common-actions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous release build failed in craft-common-actions, not in this
repo: riscv64 packaging got --formats deb,rpm,tar (fixed in
craft-common-actions#3), Linux jobs lacked the ALSA headers
(craft-common-actions#5), and Windows signing needed the AZURE_* secrets
listed in the caller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the release workflow moved to storytold/craft-common-actions, it has
passed only the Apple and WINDOWS_CERTIFICATE secrets. The signing job reads
values from the release environment, but only for names the caller lists, so
the AZURE_* names that Trusted Signing uses came through empty and Windows
builds would ship unsigned. Same fix as storytold/craft-launcher#174.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Phone footage stamps its frames up to a millisecond after the frame grid
(a first frame 34.1 ms long at 30 fps). The frame shown at a time is the
last one starting at or before it, so a sequence frame asked at its exact
start got the picture before: the clip ran one frame behind and showed
frame 0 twice. The request is already rounded to the track timescale
(#73, as WebM in #83), but at 90 kHz a millisecond is 90 units.
When opening an MP4, the video samples of a file that is a constant rate
gone slightly off its grid are put back on it: the grid starts at the
first picture and steps by the nominal frame duration, and the file is
snapped only when every picture is less than a quarter of a frame from
its own grid point and no two pictures share a grid point. Anything
further off is variable frame rate and keeps all its times; a picture one
unit off is left where it is, since that can't be told from timescale
rounding. Only the video sample times move: the audio, the first picture
and the clip duration are unchanged.
During a transition on its track an adjustment layer rendered as nothing, because it has no picture
of its own: Cmd+D at its start left the effects off for the whole dissolve and then cut them in.
In a transition the layer is now the frame below with its effects (opacity, masks and Motion region
included), so a dissolve fades the effects. A fade from or to nothing keeps the layer's blend mode.
Frames where a transition touches an adjustment layer render on the CPU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS, AppKit delivers Ctrl+Click to the winit view as a primary mouse press carrying the Control modifier — rightMouseDown only fires for a real right button or a two-finger trackpad click. winit 0.30 (button 0 → Left) and egui-winit 0.36 (Left → PointerButton::Primary) pass it through untouched, so egui's secondary_clicked() never fires and no context menu opens.
Context menus subscribe to egui's secondary click, so the translation is done once at the input boundary, as the platform convention requires: winit deliberately leaves Ctrl+Click handling to the application (rust-windowing/winit#4469, wontfix), and macOS apps translate it themselves (Firefox does the same in nsCocoaWindow.mm).
raw_input_hook runs before egui begins its pass, when pointer click state is computed — the only seam where a rewritten event has the right effect. Primary presses carrying the Control modifier become Secondary presses; the press and its release are paired so releasing Control mid-click still closes as a context click and never leaks a stray primary click, and a plain press or PointerGone clears any stale pairing. Real right clicks and other buttons pass through unchanged; Windows and Linux keep Ctrl+Click as a selection modifier, and control-channel/automation input is exempt.
Unit tests cover the transformation through egui's real pointer state: a rewritten Ctrl+Click makes pointer.secondary_clicked() true, plain clicks are untouched, and a stale pairing cannot corrupt the next click.
Both were offered for import but could never be imported.
`.y4m` is in VIDEO_EXTENSIONS (and file-format-parity.md lists Y4M as
read), but nothing read it. `codecs::y4m` is a YUV4MPEG2 source written
from the format's public documentation (yuv4mpeg(5)): the W/H/F/A/I/C/X
stream header, FRAME lines with their own parameters, 8-bit 4:2:0 / 4:2:2
/ 4:4:4 / 4:4:4 + alpha / mono and 9–16-bit little-endian
(`420p10`, `422p12`, `mono16`, …) as Yuv8 / Yuv16 frames, XCOLORRANGE=FULL
and the pixel aspect. Frames are indexed once from their header lines and
read on demand (a reader opener too), so large files aren't loaded whole.
Sizes, rates and offsets are checked; 4:1:1 and unknown colour spaces are
an "unsupported" error.
The Media Browser listed `.ale` among project files, but FilmCraft only
writes ALE (File ▸ Export; Premiere doesn't import it either, per
file-format-parity.md), so importing one always failed. It is no longer
listed.
Tests: hand-built Y4M files (4:2:0 frame by frame, 10-bit 4:2:2 with frame
parameters, aspect and full range, mono, the reader path), truncated /
bit-flipped / hostile headers under catch_unwind, and an ffmpeg oracle
(8-bit 4:2:0 and 10-bit 4:2:2 files equal ffmpeg's rawvideo output byte
for byte); the browser no longer classifies `.ale`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TIFFs stored as YCbCr (PhotometricInterpretation 6) without JPEG
compression failed with "does not support the color type Unknown(24)":
the image crate reads YCbCr only inside JPEG compression. ffmpeg writes
these whenever the source is YUV, so an ordinary frame grab
(`ffmpeg -i clip.mp4 -frames:v 1 grab.tiff`) couldn't be imported.
`still::ycbcr_tiff` decodes them from TIFF 6.0 (§21 YCbCr images, §20
ReferenceBlackWhite, §9 PackBits), reading the directory with
filmcraft-dng's TIFF parser: 8-bit chunky data units with subsampling 1, 2
or 4 each way (the image padded to whole data units), converted with the
file's YCbCrCoefficients and ReferenceBlackWhite (or the §21 defaults).
Every size, offset and count is checked, the picture is capped at 2^28
pixels, and separate planes, tiles and other bit depths are an
"unsupported" error. JPEG-compressed and RGB TIFFs still go to the image
crate.
Tests: hand-built files for data-unit placement (4:2:0), video-range
reference levels with PackBits (4:4:4), sizes that aren't whole data
units, and truncated/bit-flipped/hostile files under catch_unwind; an
ffmpeg oracle compares ffmpeg-written 4:2:0, 4:2:2 and 4:4:4 TIFFs (95×63)
with ffmpeg's own RGB decode (every channel within 6).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File ▸ Import and the Media Browser offer `.aac` (it is in AUDIO_EXTENSIONS),
and Export ▸ AAC Audio writes ADTS `.aac` (#814), but no opener read them:
`audio::opener` only accepted mp3/mp2/flac/ogg/aif by extension, so every
`.aac` failed with "unrecognised format".
`AudioFileSource` now decodes a standalone ADTS stream with our AAC decoder
(ISO/IEC 13818-7 §6.2): frame after frame from the first header (after an
ID3v2 tag), resyncing past damage, skipping frames with another
configuration, keeping a frame that doesn't decode as silence, and
upsampling HE-AAC to its output rate. Decoding stops with an error past
4 GiB of samples instead of allocating without bound.
Tests encode a stereo tone with our encoder's ADTS output and import it
(rate, channels, duration, level), and run truncated, bit-flipped and
bad-length streams under catch_unwind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop entry declared no MimeType and the shared-mime-info file was an
empty stub, so no file manager listed FilmCraft under "Open With" for any
file, and a double-clicked .fcproj had no type to open with. The app already
handles both from the command line (`%F`): a project opens, media is
imported (#850).
- mime.xml: define application/x-filmcraft for *.fcproj, a JSON subclass
with the app icon; the same name the macOS UTI declares in Info.plist.in.
- Desktop entry: MimeType lists the project type and the video containers
FilmCraft imports (MP4/M4V, QuickTime, Matroska, WebM, AVI, MXF,
MPEG-TS, MPEG-PS, 3GP). Audio and stills are left out so an NLE doesn't
crowd every image and song's Open With list.
- AppStream: the metainfo <provides> the project type.
- Flatpak bundle: the comment no longer says the MIME file has no types.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adjustment layers render their effects without an FxEnv, so Transform's
shutter fell back to 24 fps. In a 60 fps sequence a 360° shutter then
spanned 2.5 frames, blurring one frame before the move starts and one
after it ends. FxCtx now carries the sequence frame rate, set wherever a
sequence is known, and Transform uses it.
Graphics ▸ Align to Video Frame (Left, Center Horizontally, Right, Top,
Center Vertically, Bottom) only reached layers inside a graphic clip. With
video or image clips selected instead, it now aligns each clip's scaled and
rotated bounds to the frame by moving Motion ▸ Position only, at the
playhead (a keyframe only when Position is already animated, as typing the
value would). Scale, rotation and other keyframes stay untouched; one undo
step. A selected or named graphic clip still aligns its layers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a colour parameter's eyedropper armed (Color Key, Ultra Key, …), a
click on the Program picture did nothing: the selected clip's on-monitor
transform handles (#639) are drawn over the picture and took the click.
The clip is always selected while its effect shows in Effect Controls, so
the eyedropper never fired. While an eyedropper is armed the picture
overlays (transform handles, masks, graphics) are not drawn, so the click
reaches it; they come back once the colour is picked or Esc disarms it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Effect Controls focused, Edit ▸ Select All (⌘A / Ctrl+A) selected
every clip in the Timeline. It now selects every keyframe the panel shows
(effect parameters and mask parameters of the shown clips) and leaves the
Timeline selection alone. With no keyframes shown, or another panel
focused, Select All behaves as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a clip kept its transitions on their old spots, so they jumped to the
dropped position on release instead of moving with the clip. The drag preview now
draws each transition where the drop will leave it, applying the same rule the
edit does: a fade travels with its clip, and a cut transition travels only when
every clip it sits on moves to one track by one offset. A cut transition whose
one side stays behind is dropped on release; the preview keeps it on its cut
while dragging, and Option-drag copies show a ghost transition beside the
copies, duplicating only the transitions whose every clip is copied.
- edit: carried_transitions keeps only all-clips-move transitions; move_items
and copy_items drop or skip the rest
- ui: move_transition_preview draws carried transitions where they land
- tests: timeline_transition_move_ui (fade, both-clips cut, XYZ partial drop,
cross-track drop, Option-drag ghost); transitions_travel_with_moved_clips
updated to the drop-on-partial rule
The APV demuxer is new in ffmpeg 8.0, so ubuntu-24.04 runners failed instead of skipping. testkit gains ffmpeg_with_format_or_skip: when ffmpeg -formats does not list apv it SKIPs naming the detected version, failing only under FILMCRAFT_REQUIRE_ORACLES=1. The apv oracle uses it; docs/testing.md notes ffmpeg >= 8.0.
Fixes#1249.
- 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
Scratch disks, sidecar files, burn-in, legal range, through edits, nudge
units, butt cap and camera shake were mistranslated; render, view preset,
transport and focus now match the rest of the catalog; four macOS menu
and Preferences rows were missing.
AI-assisted, native review welcome.
Models: Opus 5.5, Terra 5.6, DeepSeek V4 Flash, Mistral Large 4
When the web browser can't be opened, a dialog now shows the reason and
the address in a read-only field you can select from, with Copy, and
stays until Close or Escape. The status bar message it replaces cleared
itself after 8 seconds and claimed the address was on the clipboard,
which egui can't confirm (a failed copy is only logged), so the address
could be gone after switching to the browser window.
The Help hints lose their "(no web page)" suffix, and System
Compatibility Report's hint is just "Shows a report on this computer."
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sequence.applyVideoTransition {edge: "out"} with a clip selected put
transitions on both free edges (speed_duration_moves_fade_out_to_the_new_clip_end
failed); with edge given only that side is used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The only part of #37 not already on main: FxStage::new runs inside a
validation error scope, so a device where Rgba32Float is not renderable
(software Vulkan/GL) logs and renders effects on the CPU instead of panicking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The probe only logged and always returned None, opened a VA display on every
H.264 Auto export, and made VA-API decode fail to load where libva lacks the
new symbol. A real VA-API encoder can come in its own PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
de, ru and uk hold only menu labels (their tests check every entry is one);
Indonesian mirrors the Spanish catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Other formats' start and priming (AAC edit lists, …) are handled and
oracle-tested elsewhere; enable_gapless for them could move their audio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hovering a Help menu item now tells you what it does: the links (ArtCraft
Discord, website, app page, GitHub, Report an Issue, FilmCraft Help) show
"Opens in your web browser: <address>", and Reveal Log Files, System
Compatibility Report and About FilmCraft say they stay in FilmCraft (no
web page).
When the host can't open a browser, the click was only logged. Now the
address is copied to the clipboard and the status bar names the reason
and the address.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The filter costs radius² per pixel; with only the image size as a cap, a
project file's radius could make one 4K frame take hours.
Regression test: median_radius_stays_in_the_parameter_range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tiny header naming ~1000 channels on a 64M-pixel line sized each chunk
buffer at hundreds of GB and Vec::with_capacity aborted the process.
Chunks over 256 MB are refused, and the RLE / zlib buffers reserve with
try_reserve_exact. Regression test: huge_scan_lines_are_refused_before_allocating.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#608 drops any timeline drag on Escape before the release runs, but the
live box from #1028 had already changed the selection, and only the
release put it back (timeline_marquee_ui escape_cancels_the_box...).
Escape now restores the Marquee's starting selection when it drops it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #1109 a press on the ruler moves the playhead at once, so the press
already opens the scrub stream. The drag start then calls stop(), which
stopped the audio device but left scrub.open set, so later grains never
restarted it (meters_ui the_meters_move_while_scrubbing: no output).
stop() now marks the scrub stream closed with the device.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pen_overlay read resp.drag_started_by() inside ui.data_mut(), which holds
the context's write lock that drag_started_by needs. A Shift-drag that
placed a point hung for egui's 10 s lock timeout, then panicked
(shift_constrains_pen_mask_segments_to_45_degrees got 3 points, not 4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test ran egui frames without a renderer and dropped the output's
textures delta, which epaint 0.36 turns into a panic. The global panic hook
then failed crash::tests::hook_records_and_logs_worker_panics too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#954 let fraction operands run to any cardinal length, so the "7/2026"
left after an impossible date's "13/" read "seven over two thousand
twenty-six" instead of a slash and the year (normalize::tests::dates).
Three digits covers every case #954 added (7/100, 100/7).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
About FilmCraft, System Compatibility Report, Sequence Settings and the
other modal dialogs were placed with `Window::anchor(CENTER_CENTER)`,
and egui makes anchored windows immovable, so dragging the title bar did
nothing. They now open at the centre of the app window through a
centre pivot and `default_pos` (`widgets::dialog_center`, or the
`CenteredDialog::centered` shorthand on one-line chains) and can be
moved like any other window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rate_stretch_items skips clips on locked tracks and refuses the edit when
every clip is locked, which is what the command loop on main did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Split from #728 on current main. Full validation is recorded in the draft PR; the trim and mask-modifier failures reproduce on unchanged main.
Co-Authored-By: Codex <noreply@openai.com>
#1048 renamed two option labels after the Indonesian catalog (#878) was
written, so indonesian_covers_the_menus_and_has_no_dead_entries failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Quick Export path field was refilled from the stored path every frame,
so typing into it was overwritten. It is now a file name field that keeps
what is typed while it has focus and applies each non-empty value; a typed
or pasted full path replaces the folder too.
Under it the folder is shown as an accent-coloured, underlined link with a
folder icon. Clicking it opens the save dialog in the current folder with
the current file name; a chosen path is applied and shown, cancelling
changes nothing, and Export writes there. A preset change now swaps only
the extension, so a chosen name is kept.
Export mode's File Name and Location fields had the same refill problem
when emptied and use the same draft handling.
The desktop save dialog hook accepts a full path and starts in its folder.
A number field took only a plain number; anything else was thrown away.
Now every number field takes a calculation and applies its result:
the blue drag values (widgets::hot_number, in Effect Controls,
Properties, Lumetri and the mixers) and the dialogs' and Settings'
number fields (egui DragValue, through the new widgets::number_drag).
- New calc module: + - * / (and the multiplication and division signs),
brackets, decimals and signs; a unit (letters, %, degrees) and spaces
are ignored, but letters between digits (1e3, 10x20) are refused
rather than dropped. A calculation starting with * or / applies to
the field's current value; + and - at the start are signs. Bad input
gives nothing, never a panic: the text is capped at 256 characters,
nesting at 64, and only finite results come back.
- hot_number opens with its value selected, so a typed *2 applies to
it; End then /3 adds to it.
- The transition duration field reads timecode first (1.00 is a
second) and takes a calculation in frames when the text is neither a
frame count nor a timecode.
Tests: calc (calculations, units, a leading * or /, bad input);
mixer_ui (the clip mixer's dB value and the Audio Gain dialog take
calculations).
Since UI.6 (#881) control gestures set the modifiers of every frame they
run, so the Shift that the test pushed by hand before plain ui.click calls
was replaced and the #862 constraint test failed on main. The clicks now
pass modifiers {shift: true}, like the drag already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebasing on main's undo coalescing (#887): its run() reads the press memory once before the loop, so every parameter written in the first frame of a press began its own step, and a Program monitor handle that changes position and anchor point together took two. Only the first write of a frame begins the step now. The keyframe menu tests collapse the built-in effects by clicking their headers, which selects the effect since the handles change; they click the twirl arrow.
Follow-ups to the transform handles from trying them by hand:
- Only the left button drags on the Program monitor. A middle or right drag
moved the transform box; the same held for the mask body, vertices,
Bezier, feather and expansion handles and the pen, the Essential Graphics
layer boxes and the monitor guides and rulers, which now take the left
button only too.
- Anywhere just outside the box rotates, beside an edge as beside a corner,
as in Premiere. The rotate pointer is a curved double arrow that faces
away from the box wherever the pointer is around it. A click a little
further out (beyond the rotate zone) still lets go of the box.
- The anchor point is Premiere's circled cross (radius 7, the cross inside
the circle, thin lines crossing at the exact point) and takes hold within
11 points. Over it the pointer is the plain arrow with a small anchor
point beside it, so it reads apart from the move pointer over the picture.
- Effect Controls: a hovered or selected effect header (and mask row) is
highlighted from one point in, so it no longer cuts through the focused
panel's outline (effect_controls::row_fill).
Tests: transform_handles_ui gains only_the_left_button_drags_the_handles,
guides_masks_and_graphics_take_the_left_button_only and
rotating_from_beside_an_edge.
Selecting Motion in Effect Controls (its header or one of its properties) draws a box
around the clip's picture in the Program monitor, with eight handles and the anchor
point. Double-clicking a clip's picture selects its Motion (and the clip). The Transform
effect gets the same box for its own properties while it is the selected effect.
- Drag inside the box: Position. Drag a handle: Scale about the anchor point; with
Uniform Scale off a side handle changes only the width and a top/bottom handle only
the height (Motion: scale / scale_width; Transform: scale_height / scale_width).
Drag just outside a corner: Rotation. Drag the anchor point: Anchor Point, with
Position changing by the same amount so the picture stays put.
- Values go through effects.setParam at the playhead on every frame of the drag, with
Effect Controls' merge/begin handling (effect_controls::run, #201): a stopwatch-on
property gets a keyframe there, and one drag is one undo step even when it changes
two parameters.
- The handles are drawn a little smaller than the Essential Graphics ones (5/4 pt
squares, finer anchor marker) but grab within 8 pt; there is no system rotate
cursor, so a code-drawn one replaces it over the rotate zone.
- Clicking an effect header in Effect Controls now selects the effect instead of
twirling it; only the arrow twirls (new id effectControls.effect.<fx>.twirl). The
selected header is highlighted like a selected mask row.
Engine: EditorState.selected_effect and the effects.select command (journal off, like
masks.select). The selection is the effect id plus which instance of it, not its index,
because applying an effect inserts it first. Selecting an effect clears the mask
selection and selecting or creating a mask clears the effect selection; undo, redo and
effects.remove drop or follow it. effects.setParam's merge also takes a key, so a
gesture that changes several parameters is one undo step.
Automation ids: program.transform.box, program.transform.handle.<0-7>,
program.transform.rotate.<0-3>, program.transform.anchor.
Tests: transform_handles_ui (select, move, corner and edge scale, rotate, anchor,
keyframe at the playhead, one undo step per drag, header selects / arrow twirls,
Transform effect, double-click selection); engine tests for effects.select and the
merge key. Tests that clicked headers to fold effects now click the arrow.
timeline.roll follows linked partners since #870, but Trim mode already
selects the linked audio edit point next to the video one and rolls every
selected point, so a linked cut moved twice (trim_mode_roll_ripple_and_toggle:
+10 frames instead of +5). timeline.roll takes an optional followLinks
(default true); Trim mode passes false.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apply Video / Audio Transition worked only at an edit point under the
playhead. As in Premiere, with clips selected they now add the transition
at the selected clips' free edges (video or audio side only), and with an
edit point selected at that edge only, wherever the playhead is. Edges
that already have a transition keep it; one undo step per press; the
call's effect / frames / params / reverse apply to every edge. With
nothing of that kind selected, the playhead behaviour is unchanged.
Shares the edge collection of Apply Default Transitions to Selection (⇧D).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two more built-in presets follow Default in Settings > Labels > Label
Color Presets. Bright is 16 saturated hues spaced around the colour
wheel plus a gray, named Red to Gray. Roles names the labels after what
clips are (B-Roll, Footage, Dialogue, Graphics, Interview, Sequence,
Visual Effects, Bin, Titles, Music, Sound Effects, Voice-Over, Captions,
Ambience, Color Grade, Stock), ordered so the label defaults for new
video, movies, audio, stills, sequences, Dynamic Link items, bins and
captions land on the matching role. Like Default they can be chosen
and reverted to but not deleted, and an imported preset with one of
their names is renamed "<name> (imported)".
A Label Color Presets row under Label Defaults picks the 16 label names
and colours from a preset. The dropdown lists the built-in "Default"
(the labels FilmCraft ships with) and the presets in the Label Presets
folder beside the preferences, and reads "<name> (Modified)" once the
labels differ from the chosen preset. Icon buttons: Revert (back to the
preset's colours and names), Import (copies a preset file into the
folder and selects it), Export (saves the dialog's labels to a file
named as the preset), Show Folder, and Delete (user presets only).
Choosing a preset or reverting applies on OK like the other fields;
Cancel discards it.
Presets are small JSON files: a name and 16 labels, each a name and a
labels, names trimmed and at most 64 characters, valid colours); a bad
file shows its error in the dialog and changes nothing. The same is
reachable as labels.presets, labels.importPreset, labels.exportPreset
and labels.deletePreset, with the selection stored as labels.preset.
About FilmCraft and Join the ArtCraft Discord… in the macOS app menu take
the translated labels of the same Help menu commands and are updated on
a language switch. Hide, Hide Others and Quit get their text from the
catalogs instead of muda's English defaults and keep their native
actions. menus::NATIVE_APP_MENU lists these native-only labels together
with the Settings submenu, so the app and the tests use the same keys.
German rows for the three new keys; "Quit FilmCraft" is already
translated in es, ja, zh-cn and fr (the app.quit shortcut), the other
languages show English until they have translations. The German menu
test accepts and requires the native app menu labels.
Closes#1145
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
timeline.place kept a sub-frame start and rounded the duration to whole
frames, so the clip ran past time + duration and the sequence ended
between frames. The start and the end now each snap to the nearest frame,
as a timeline drag already does. sequence.inspect's durationFrames now
comes from the same frame_span export uses, so the two always agree.
Fixes#1101
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The interface had no face for Arabic or Hebrew, so caption text, track and media names showed
missing-glyph boxes in the Text panel and on the timeline while the Program monitor drew them.
crates/ui-egui/src/rtl.rs adds the craft-fonts Arab / Hebr faces, else one installed face per
script (Noto Sans Arabic, SF Arabic, Segoe UI...), as the last fallbacks of every UI font family,
as cjk.rs does for Chinese.
egui already shapes each font run with harfrust, so the letters join, but it lays runs out left to
right with no bidi pass and multi-word Arabic read backwards. rtl::Lines lays captions out in
reading order: lines are broken on the logical text as egui wraps it, each line is reordered with
unicode-bidi (UAX #9, as crates/text does for titles) and every right-to-left word is its own
layout section for egui's shaper. Caption blocks on the timeline and the Text panel caption list
use it; a caption being edited keeps egui's logical order, since TextEdit places the caret by
character index. Left-to-right text is laid out exactly as before.
The macOS menu bar built every item enabled and its updater never applied
enablement, so Edit > Undo and Redo stayed selectable with empty history.
Items are now created with their computed state, and the frame loop sends
the menu tree to the native menu whenever it changes; the updater applies
only the labels, shortcuts and enablement that differ.
Fixes#902
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exports and rendered frames now record which offline items (missing,
made offline, unreadable) they drew the placeholder slate for. The engine
reports them as `offlineMedia` in file.exportMedia (wait), jobs.list
export entries and export.queue items; the bridge and MCP long exports
pass it through. filmcraft-cli export and render name each item on
stderr and exit 1, unless --allow-offline accepts the placeholders.
Fixes#1054
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The TTML/DFXP reader ignored begin/end/dur on <body>, so a body begin
offset was lost and captions landed early. Body and div are now treated
as par time containers: their begins offset descendants, their ends
clip them, paragraphs without an end last until the container ends,
and end + dur take the earlier. seq containers warn instead of being
silently mistimed.
Fixes#895
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A key put inside `settings` that ExportSettings doesn't have did nothing
and the export ran as if it were absent: `filmcraft-cli export --settings
'{"loudnessLufs":-14}'` (a flat parameter in the wrong place) exported at
-18.3 LUFS with no warning, and a misspelt nested key was dropped the same
way.
settings_from_params now compares the patch with the resolved settings and
refuses the first key they don't have, matching keys as `merge` does (the
camelCase spelling of a snake_case field, #531, counts):
`settings: unknown key `loudnessLufs` (see export.presets.get for the settings' keys)`.
A null value is accepted anywhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
effects.setParam clamped an explicit `time` into the clip, so a keyframe
past the end replaced one sitting on the last frame and a ramp across a
cut was compressed to finish at the clip end. The same happened before
the start. An explicit time now keeps its keyframe where it was asked,
outside the clip if need be, as effects.moveKeyframe already allows; only
the playhead fallback stays pinned to the clip.
Fixes#1055
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the Timeline followed the playhead only while playing, so a jump
while stopped (Home, End, edit points, markers) left the playhead off screen.
Fix: when the playhead of the shown sequence changes while stopped and falls
outside the view, the view scrolls to it with the page-scroll margin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Shift-click in the Project panel toggled one item like Ctrl/Cmd-click; the items between were never selected and there was no anchor.
Fix: remember the last item clicked without Shift as the anchor (ProjectPanelUi::anchor); Shift-click selects the range anchor..clicked in the order the view shows (List, Icon, Freeform, search filter respected), Ctrl/Cmd+Shift adds it to the selection. Ctrl/Cmd-click still toggles. Pure helper click_selection with unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Premiere writes a stereo clip to FCP7 XML as two mono clip items on two
tracks (sourcetrack trackindex 1 and 2). The importer already picks
channel 0 and channel 1 for them, but left both centred, and a centred
one-channel clip plays on both sides. So both channels summed on L and
on R: +3 dB per channel for an uncorrelated stereo effect (and its L-R
doubled), +6 dB for dual-mono dialogue.
Import: a clip that plays one channel of a split two-channel file is
panned to that channel's side (channel 0 left, channel 1 right). An
Audio Pan filter on the clip item still overrides it.
Export: a one-channel clip always writes its Audio Pan, also when it is
centred, so a centred Breakout to Mono round-trips centred.
Measured with filmcraft-cli (import, export WAV): a split stereo file
and a split dual-mono file now come out at 0.00 dB and sample-identical
to the source (before: +3.01 dB and +6.02 dB per channel); a mono file
on a mono track was and stays at 0 dB.
Tests (crates/interchange/tests/fcp7.rs):
split_stereo_halves_are_panned_to_their_side and
split_stereo_half_keeps_its_audio_pan fail without the import change;
centred_mono_breakout_round_trips_centred fails without the export
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HrAqZmrrDLRpxBJatxvaJ
German (de) landed in #630: the localization parity table and the
ROADMAP language table now list it as menus only, 371 of 3,498 strings
(10.6%), native-reviewed. The localization dimension counts 2
menus-only and 5 none, and 5 of Premiere's 10 languages well and 2
partly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: .dng files were not importable, and a DNG (a TIFF) would have
gone to the TIFF decoder, which reads at best the embedded preview. A
CinemaDNG shot (a folder of numbered DNG frames) could not be edited.
Fix: "dng" joins the importable still extensions. filmcraft-media
develops DNGs with filmcraft-dng into scene-linear Rec. 709 float
frames (transfer Linear, like OpenEXR), so highlights above 1.0 reach
the compositor. A single DNG is a still; a numbered folder imports
through the existing image-sequence path as one clip, whose frame rate
and start time code come from the first frame's CinemaDNG FrameRate and
TimeCodes tags (else Settings > Media > Indeterminate Media Timebase).
Lossy and JPEG XL DNGs are refused as unsupported by name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: camera RAW is at 0% (docs/codec-parity.md, #383). CinemaDNG,
the one cinema RAW format with a public specification, could not be
read at all.
Fix: new L0 crate filmcraft-dng, written from the public DNG 1.4-1.7,
TIFF 6.0 / TIFF-EP, ITU-T T.81 and CinemaDNG specifications only (no
other decoder's source read; editions in the crate README):
- TIFF container with SubIFDs, bounded (64 IFDs, 4 levels, cycles end
the walk) and checked offsets; the main raw IFD is the largest
NewSubFileType 0 CFA / LinearRaw image.
- Raw data: 1-16-bit uncompressed (MSB-first packing) and lossless JPEG
(process 14, predictors 1-7, 1-4 components, point transform, line
restarts), strips and tiles, tiles decoded in parallel; allocations
capped by size and by the data present.
- Processing per the DNG model: ActiveArea, LinearizationTable, per-site
black levels with deltas, white levels, as-shot white balance with
neutral highlight clipping, Malvar-He-Cutler Bayer demosaic (bilinear
for other patterns), DefaultCrop, ColorMatrix / CameraCalibration /
ForwardMatrix with illuminant interpolation, Bradford to linear
Rec. 709, BaselineExposure; output scene-linear RGBA floats.
- CinemaDNG FrameRate, TimeCodes and ReelName tags.
- A test-only writer (feature `writer`) synthesises DNGs. Tests: raw
samples bit-exact for every depth / predictor / layout, processing
against hand-computed values, hostile files (every truncation, 12k
mutations, cyclic IFDs, huge sizes, bad offsets), and LibRaw's
unprocessed_raw as an external oracle reading the same samples
(skipped when not installed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the interpolation filters took their taps as an array of T
slices built per block row with `array::from_fn`. The array went through
the stack and was read back with wider loads than it was written with
(store-forwarding stalls): about 10 % of the 8-tap luma interpolation
time, plus the per-tap Option handling.
Fix: the filters take the taps as a function (row shifted by k, or k-th
row from x), inlined into the multiply-add loop, so the tap addresses stay
in registers. Same arithmetic, same samples.
Tests: the interpolation unit tests (16-bit lanes against the 32-bit
filter, in-place against copied windows), conformance suite, identical
YUV on the five bench streams. User instructions -1 to -4 % on all five;
cycles -2 to -5 % except g250_500 (within the run-to-run noise of the
shared machine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VP8 WebM showed black video: there was no VP8 decoder. New L0 crate
filmcraft-vp8, implemented from RFC 6386 sections 1-19 (tables extracted
from the RFC text, nothing from the attached reference decoder): every
coding tool, versions 0-3, hidden altref frames, size changes. Bit-exact
with libvpx on 62 libvpx VP8 test vectors, real-world FATE samples and
generated fixtures; never panics on corrupted input.
Wired into codecs for Matroska / WebM (V_VP8, including the alpha layer
of transparent WebM) and MP4 (vp08). VP8 colour is BT.601 (RFC 6386 9.2).
A hidden altref frame is a WebM block of its own with the time of the
frame that follows it: the VP8 time lookup now moves on to that shown
frame instead of repeating the previous one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AMD and Intel GPUs expose VA-API EncSlice, not NVENC. The export path
still opened NVENC first and then waited forever on wgpu map_async
(radv). Probe EncSlice before NVENC, fall through to the software
encoder, and bound GPU readback waits to eight seconds so a wedged
device fails the GPU path instead of blocking the export.
Problem: x265 (by default) and other encoders set weighted_pred_flag in the
PPS and then signal the default weights (2^denom, offset 0) for most
slices. Every block of those slices took the scalar 32-bit explicit
weighting path (8-265 / 8-266) instead of the 16-bit-lane default one, and
lost the full-sample copy shortcut.
Fix: when every list a block predicts from has the default weight and a
zero offset for a component, the block uses the default weighted
prediction (8-262 / 8-263) and, for uni-prediction at full-sample
positions, the reference copy. With log2WD = denom + shift1 the weighted
sums are exactly the default ones multiplied by 2^denom, so the samples
are identical.
Tests: a unit test checks the identity for every block width, bit depth
and denominator, and explicit weighting against 8-265 / 8-266 with extreme
weights and offsets; the weighted-prediction conformance fixtures; identical
YUV on the five bench streams. Cycles: -5 % on 1080p x265 medium, -8 % on
the weighted-prediction stream, -3 % on 10-bit, neutral elsewhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: after each picture the decoder classified every 4x4 block for
DecodeStats with an if / else-if chain: about 130 000 unpredictable
branches per 1080p picture, 1.5-2 % of single-thread decoding time.
Fix: `block_counts` sums flag comparisons in one branch-free pass
(inter = coded - intra - skip), with the same results.
Tests: a unit test compares it with the if / else-if chain over every
flag combination; identical decoded YUV on the five bench streams.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: every prediction block copied its whole reference window (block
plus filter margin, w + 7 by h + 7 luma samples) into a 16-bit scratch
buffer before interpolating, one line at a time with a row lookup per line.
After the move to 8-bit storage this copy was the largest single cost of
motion compensation (about 10 % of decoding time on 1080p streams with
much motion), and windows crossing the left or right picture edge were
copied sample by sample with a clamp each.
Fix: when the window lies inside the picture horizontally (almost always),
`Frame::window_rows` hands out the reference lines themselves (lines above
or below the picture replicated by clamping the line index, 8-228) and
the filters read the 8- or 16-bit samples directly: horizontal taps are the
line shifted by k, vertical taps the k-th line. Only windows crossing the
left or right edge are still copied, now as edge fill + run copy + edge
fill. The arithmetic is unchanged, so the prediction is identical.
Tests: in-place interpolation equals interpolation of the copied window
for random fractional vectors, block sizes, both chroma planes and both
sample types (2000 blocks each, inside and across every edge); edge
replicated lines equal the per-sample clamp for every offset; conformance
suite and identical YUV on the five bench streams at 1 and 3 threads.
User instructions on 1080p: -9 % (g250_500), -2 to -6 % elsewhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: every picture was stored with 16-bit samples whatever its bit depth.
For 8-bit streams (almost all Main content) that doubled the memory traffic
of the hottest paths: motion-compensation reference windows and full-sample
copies, deblocking and SAO, the per-picture planes and published rows, and
the output copy, which also had to narrow every sample to u8.
Fix: a `Sample` trait (u8, u16) and `Frame` rows stored in the picture's
type (`Rows::U8` when luma and chroma are 8-bit, `Rows::U16` otherwise).
`PicState`, `SliceDecoder`, the interpolation / weighting stores, intra
prediction and the in-loop filters are generic over the sample type and
monomorphised once per type; the decoder picks the type per picture from
its SPS. All arithmetic stays in the same integer types as before, so the
output is unchanged sample for sample. 8-bit output rows are now copied as
they are. Reading a frame with the other sample type (never done: the DPB
is flushed when the bit depth changes) yields nothing instead of a panic,
and the non-threaded build no longer has an `expect` on unpublished rows.
Tests: new unit tests for the storage type, exact crops of both types and
harmless wrong-type access; the full-sample copy test now runs on u8 and
u16 frames; the ffmpeg conformance suite (with and without the threads
feature); identical decoded YUV on five 1080p/720p streams (8-bit, 10-bit,
weighted prediction) at 1 and 3 threads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the software HEVC decoder was 2.7-4x slower per core than ffmpeg
on 1080p Main. Motion compensation was about half of the profile: the
weighted-prediction stores clipped in emulated 32-bit SSE2 lanes, every
block line of a reference window was a libc memmove call, and full-sample
uni-prediction went through window copy, shift and store. Deblocking
divided per edge to find the slice of each block.
Fix (output unchanged, sample for sample):
- put_uni / put_bi in 16-bit lanes (prediction samples are 14-bit; bi uses
the exact halved-sum identity), dispatched on constant block widths.
- 8-bit first interpolation passes accumulate in 16-bit lanes (the
filtered value stays within -24*255..88*255).
- Full-sample uni-prediction without weighting copies the reference
samples straight into the picture (Frame::copy_block), and block lines
are copied as fixed-size arrays instead of memmove calls.
- Deblocking takes the slices from edge_params instead of dividing per
edge, returns bS 0 for identical motion in one slice, and filters luma
edges on a gathered 4x8 block.
- 8-bit output planes are copied to u8 directly.
Tests: equivalence against the 32-bit formulas and the previous filter
(new unit tests), the ffmpeg conformance suite, and identical decoded YUV
on five 1080p/720p streams (8-bit, 10-bit, weighted prediction).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pressing Escape during an active drag in the Timeline cancels the operation without committing changes on mouse release. For ruler scrub, restores the playhead to its starting time.
Clear used to strip every track item that referenced a deleted Project file. Those clips now stay on the sequence as Media Offline, and media.relink still finds the item. Unused media, bins, and generators are still removed.
Source marks only read `time` (ticks) and otherwise used the Source playhead,
so agents passing `frame:24` marked frame 0. Parse the same time formats as
Program, using the Source item rate.
The vertical layout path never emitted underline rectangles, so layer and
run underlines were a no-op. Place a tall strip to the right of each
underlined cell (vertical-rl) and compare horizontal vs vertical "A" at 60px.
Plain layout() underlines sat on the unshifted baseline while layout_rich()
runs already subtracted baseline_shift. Match the two so underline y tracks
the glyphs for a full-paragraph style.
"Create a bin of subclips from each detected cut point" and "Create a
clip marker at each detected cut point". Drops the old strings from the
catalogs.
The command and its dialog already existed in the Clip menu, but the
clip context menu, where people look for it, didn't offer it. It now sits
after Speed/Duration… and opens the same dialog.
Handle zero-softness Extract as a hard cutoff in both CPU and GPU paths.
The former 0.0001-wide substitute fade introduced grey edge pixels and
amplified transfer-function rounding beyond the existing parity tolerance.
Retain the positive-softness calculation and add cutoff, inversion and
premultiplied-alpha regressions without relaxing the comparison tolerance.
Constrain caption font automation targets to the visible part of each
row. A partly clipped row could previously expose a click point outside
the popup, leaving the selected font unchanged. Preserve the existing
font-loading behavior, assets and automation IDs.
A six-channel clip on a stereo track played only source channels 0 and 1,
so the centre (dialogue in film mixes), LFE and surrounds were dropped.
Fold it down with ITU-R BS.775, as the Mix and the Source Monitor already
do. A six-channel Modify > Audio Channels pick folds the same way; mono
and stereo picks keep playing exactly the picked channels.
The same rule as for drag-and-drop: Escape while the mouse button is held
takes the drag back and leaves everything as it was before the press.
- Value drags in panels (hot numbers, sliders and color pickers in Effect
Controls, Properties, Lumetri and elsewhere): the engine gets
Session::undo_mark / revert_to, and the app marks the history when a press
begins and reverts to the mark on Escape. revert_to drops the steps without
adding them to redo.
- Lumetri made one undo step per frame of a drag; it now goes through
effect_controls::run and merges them into one, as Effect Controls does
(Properties does since #522).
- Drags that commit on release (keyframes, influence handles, graph keys,
trim monitor, caption size, audio FX editor, monitor guides) skip the commit
when Escape ends them. UI-only sliders (Project and Media Browser thumbnail
size, Export settings) go back to their value at the press; a sequence tab
dragged out of the dock goes back home.
- Dialogs: Escape with a button held no longer closes the dialog
(widgets::escape_closes, also for the modal's own close). Dialogs with a
draft keep it at the press and put it back on Escape
(widgets::revert_drag_on_escape), color pickers included.
Tests: reverting_to_a_mark_takes_back_a_gesture_without_redo,
reverting_to_a_lost_mark_does_nothing; escape_drag_ui gains a
Properties value drag, the thumbnail-size slider and the Properties color
picker; clip_dialogs_ui and settings_ui check that a dialog field and a
Settings color go back while the dialog stays open.
egui ends a drag when Escape is pressed, and FilmCraft treated that stop as a
release: the drag was committed wherever the pointer was. Escape now abandons
it: nothing moves, nothing is dropped, nothing goes on the undo stack.
- Timeline: clip moves, trims, rolls, slips, slides and stretches, caption
blocks and automation keyframes. The clips stay put and stay selected.
- Items carried between panels (a clip or sequence from the Project panel,
Source monitor drags, effects, presets, graphics templates): the payload
is cleared in drag_ghost. panels::cancel_drag / drag_cancelled keep a
cancelled drag from restarting until the button comes up.
- Freeform card moves go back, and the next drag works.
- Media Browser drags, which import on drag start, take the import back even
when released over a panel.
Tests: escape_drag_ui (5 tests: Timeline clip, caption, Project panel item,
Freeform card, effect; each with the same drag without Escape as a control)
and escape_takes_back_a_media_browser_drag. Automation keyframes have no
test fixture. The ui-egui suite passes except the two Windows CRLF failures
fixed in #488.
On a Rec. 709 sequence every PQ, HLG, BT.2020 or P3 clip left the GPU
path: each frame was converted to a float image on the CPU (curve, HLG
OOTF, gamut matrix, BT.2390 tone mapping, desaturation) and resampled
there. A 2160x3840 10-bit HLG clip took 90 ms per frame (48 ms scaled to
fit a 1080x1920 sequence) against 33 ms at 30 fps; the same clip in SDR
took 18 ms.
The compositor's shader now runs that input transform itself:
InputTransform::gpu_params gives its stages as numbers (log curves and
camera-gamut compression stay on the CPU), PlanLayer::input carries the
transform for Y'CbCr layers on a plain pipeline, and execute_cpu applies
the same transform as the CPU reference. HDR clips now plan in 15-19 ms
per frame, and the GPU result is within one output code of the CPU's for
HLG, PQ (with and without a mastering peak) and BT.2020 SDR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ticks on the monitors' time ruler were spaced evenly across the
bar whatever the zoom, so zooming or scrolling left them looking
static. They now sit on whole frames, placed through the same mapping
as the playhead, markers and In/Out, with the step chosen from the
on-screen width of a frame as on the Timeline's ruler: minor ticks at
least 12 pt apart and major ticks at least 110 pt apart, the minor step
always dividing the major one. Zooming in spreads them out and fills in
finer steps; scrolling moves them with the time they mark.
Pressing on a grouped tool and dragging the pointer a few points in any
direction opens its flyout straight away, without waiting for the long
press. Letting go over a flyout row picks that tool and closes the
flyout; letting go anywhere else leaves it open, as after a long press.
Right-click and the 0.35 s long press work as before.
While the Program or Source monitor plays and its time ruler is zoomed
in, the zoom range follows the playhead the same way the Timeline does,
honouring Settings > Timeline > Timeline Playback Auto-Scrolling: page
scrolling moves the range so the playhead is near its left edge when it
runs off the right edge or jumps before the left one; smooth scrolling
keeps the playhead in the middle; no scrolling leaves the range alone.
The range keeps its width. Scrubbing, clicking and moving the playhead
while stopped never move it.
The Audio Meters panel now leaves out what doesn't fit instead of
drawing text over other text. As it narrows, the loudness readout drops
its LUFS/dBTP units, then the readout goes, then the dB scale goes so
the bars keep their room. As it gets shorter, the scale numbers thin out
to every 6, 12 or 24 dB and then go, and the channel letters and "dB"
under the bars go. At the default size it looks as before. The scale
numbers are listed under the automation id audioMeters.scale.
While the playhead is scrubbed with audio scrubbing on, the meters (and
the Audio Track Mixer's) show the grains being heard, as they do in
playback.
The clips a box touches are selected as it is dragged, and a clip that
leaves the box as it shrinks is deselected again, instead of the
selection only changing on release. Shift or Ctrl, held when the drag
begins, add to the selection the drag started with, as in the Project
panel. Escape cancels the box and puts that selection back. The
release still runs a single timeline.select, so the drag is one
selection change.
The Program and Source monitors get a zoom scroll bar under their time
ruler, drawn like the Timeline's: drag a handle at either end of the
thumb to zoom in or out (down to five frames), drag the middle to
scroll, or press beside the thumb to move it there. The ruler then
shows only that part: playhead, markers, In/Out handles and ticks are
mapped to it, clicks and scrubs set a time within it, and anything
outside it is not drawn. Each monitor keeps its own range in its view
state (time_zoom).
The transport buttons are now 28 pt square with larger icons, spaced
6 pt apart and centred; on a narrow monitor the spacing tightens before
buttons move to the overflow menu.
The top edge of the highest video track now resizes the video tracks,
like the lines between headers do (same cursor, drag up to grow). The
bottom edge of the last audio track can now also be grabbed from just
below the line, in the empty space under the tracks, so both outer
edges work the same way. Each edge has an automation id
(timeline.track.<V3>.resizeTop, timeline.track.<A1>.resize).
Scrolling over a panel group's tab strip makes the next tab active
(wheel down) or the previous one (wheel up), one tab per wheel notch.
Trackpad scrolling adds up until it reaches a notch, so a small swipe
moves one tab rather than several. It stops at the first and last tab
and does nothing over the panel's content. Works for docked and
floating groups, and steps through the Timeline's sequence tabs too.
Problem: the post stage of a VP9 frame (band assembly, loop filter,
publication) ran on one thread. Later frames wait per band for their
references, so the serial loop filter (~30 % of the profile) set the
pace of every inter frame; 16 threads were only ~1.8x one thread.
Fix: rows of a frame's loop filter run as a wavefront, row r two
superblocks behind row r - 1 (what the raster order of 8.8.1 allows),
taken by the post thread and helper jobs on the decoder's pool. No lock
is held while a superblock is filtered: each row filters its own band
and rows exchange copies of the 8 rows the filter reaches across a band
boundary. Output stays bit-identical; one thread / wasm run the rows in
order as before. A row is only taken by a running thread, so waits
always make progress, and helpers run under catch_unwind (a failure
publishes placeholder bands as before). New conformance fixture
narrow_tall (2 superblock columns) and a repeated multi-thread 1080p
decode test. 1.2-1.5x faster with 16 threads on a shared machine.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mouse wheel over the Program and Source monitors now zooms about
10% per notch instead of 40%, exponential in the wheel travel, so one
notch in followed by one notch out returns to the same magnification.
Pinch zoom, the 10%-1600% range and zooming about the pointer are
unchanged.
file.open restores the saved sequence tabs and views when
timeline.restoreOpenSequences is on, but prefs.schema still marked the
preference wired:false ("stored, no effect yet").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vui_color took the VUI matrix, transfer and range but not the primaries,
so every decoded H.264 / HEVC frame reported BT.709 primaries. Stream info
read them, so a BT.2020 file without a colr box said BT.2020 in its info
and BT.709 on its frames (Rec709 instead of Rec2020 for the renderer).
Pass colour_primaries through, for the software and hardware decoders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the 20 strings the Japanese catalog was missing (caption track
deletion, panel docking, eyedropper, edit point trim types, transport
overflow, effect shutter angle), update the hardware decoding note to the
current English source (NVDEC), and fix house-style slips (half-width
parentheses, a space before パネル, 長さ for Duration, Replicate no longer
shares 複製 with Duplicate).
Add a test that requires a Japanese entry for every menu label, tl!
literal and dynamic name, as the Chinese catalog already has.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without DefaultDuration the rate came from the median step between
neighbouring blocks in file order. Blocks are in decode order, so with
B-frames neighbours are two or more frames apart in presentation time and
a 25 fps clip reported 12.5. Step between the sorted presentation times
instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MP4 reader took the AudioSpecificConfig's channel configuration as a
count, but it is an index into ISO/IEC 14496-3 Table 1.19: configuration 7
(7.1) reported seven channels and dropped the last decoded one, and 12
(7.1 back, what ffmpeg 9 writes for 7.1) reported twelve. Count from the
table (with Amd 4: 11 → 7, 12 and 14 → 8, 13 → 24), and decode
configurations 11 and 12, reordered to the WAV/SMPTE order used for 5.1.
Checked against ffmpeg's decode channel by channel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the Track Select Forward tool (A) a press selected every clip from the
pointer on, but a drag never started, so the clips could not be moved with
the tool as in Premiere Pro. A drag now starts a move of the selection, with
Option copying as with the Selection tool.
A press on a transition over a cut did nothing with these tools; it now
counts as its track, as the clips under it do.
Regression test: track_select_drag_ui (the drag test fails before the fix).
Review on #827: a start marker left pending by any early exit (force-quit,
hang, unrelated crash, two launches racing) moved the user down a rung for
good, with no way back but deleting a file.
- Each start writes its own attempt file (graphics-start/<pid>-<n>.json)
that is removed after two painted frames, on a normal exit, or 15 s after
launch whatever happened. A later launch only counts attempts that are
older than that window, so a hang, a later crash or force-quit, and an
instance that is still starting are never failures.
- graphics-start.json now holds {mode, failures}: two failed starts in a row
in the same mode move on to the next one; a start that paints resets the
count. A wgpu start-up error still moves on at once.
- All files are written to a per-process temp name and renamed into place,
so concurrent launches never see or leave half a file.
- A fallback mode shows a notice (graphics.notice) with Use Default Graphics
(graphics.notice.reset) and Close; es, fr, ja and zh-cn strings.
- --graphics default|dx12|software picks a mode by hand and keeps it;
`--graphics default` undoes a fallback. Modes this platform lacks are
refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ffmpeg's CLI handles Matroska priming differently by version: 9.x trims
the 481-sample CodecDelay, 2023-24 static builds keep the priming
samples, so comparing from sample 2 put the priming ramp against our
tone (max error = peak 0.125). The .mkv case now searches lags within
two samples of 0 (trimmed) and of the stream's own CodecDelay (kept),
over the interior of both decodes; the plain .mp2 file stays at lag 0.
Review nits: the ID3v2 skip offset uses saturating_add (test with the
largest tag size); every Layer II table's sblimit is checked <= 32 at
compile time and clamped to SUBBANDS before indexing the per-subband
arrays (row() is a match, so it cannot index out of range).
Verified with ffmpeg 9.0.2 and the static N-71064 (2024-06) build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Placement already limited overlap to half of each clip, but Cross Dissolve
still used the requested overlapFrames, so a 30-frame dissolve on two
20-frame clips started before the first clip. Apply the clamped per-cut
overlap as the transition length.
ffmpeg's CLI handles Matroska priming differently by version: 9.x trims
CodecDelay, 2023-24 static builds keep the priming samples (AAC 1024,
AC-3 / E-AC-3 256, MP3 1105), so a +-40-sample best-lag search against
its decode landed on a spurious minimum (4 or 24 samples).
matroska_audio_oracle now has ffmpeg encode a reference signal we
generate (low-passed xorshift noise) and asserts our decode lines up
with that signal to the sample, the stream's own CodecDelay trimmed:
version-independent, and it fails on the old ms-rounded trim (-16).
ffmpeg's decode is compared after a best-lag search bounded by the
CodecDelay; its lag is reported, the waveform must match (< 1e-3).
E-AC-3 joins the cases. eac3_oracle's .mkv case accepts lag 0 or the
256-sample priming; MP4 / MOV / TS stay strictly at lag 0.
Verified with ffmpeg 9.0.2 and the static N-71064 (2024-06) build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OK used to close the dialog even after file.newSequence failed, so
reopening reset the name and settings. Stay open on validation
errors so one field can be fixed.
Conflict in apps/filmcraft/src/main.rs: kept the graphics start loop and
app_creator, and carried over --control-input-only (#881) into them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace With Clip ▸ Match Frame used a forward-only offset, so a reversed
clip lined the Source monitor up with the wrong media time (source 50 at
sequence 20 became 69). Align source_in from reverse-aware source mapping.
Non-ripple clip.speedDuration shortened the clip but left one-sided
out-edge transitions at the old timeline range. Reposition them with
the same edge-follow used for a regular trim.
Selecting two clip edges and trimming by frames applied each edge as its own
timeline.trim, so Undo restored only the last clip. Collapse those edits (and
extend-to-playhead / apply-default-transition) into one history snapshot so
Undo and Redo cover every selected point.
A neighbour on only one track used to clamp that partner alone, so picture
and sound stayed linked at different rates. The tightest room now limits
the whole group.
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>
A selection that holds a bin beside items (one id space) made the whole drop
fail with "no such item"; only items are placed now.
Session::edit_group raised the history limit while a group ran and did not
restore it if the group panicked. It now restores it and rolls the group back
before the panic goes on.
The Browse panel's Install… button calls the module's route directly and drops its
result, while only menus.rs surfaced a refused file — so installing another
application's template package (an EffectCraft .ectemplate ZIP renamed to .fcgt,
the case in the report) looked like nothing happened. The engine has refused ZIP
archives with a clear message since M10.7; the route now puts that message on the
status bar itself, whichever caller arrives.
Headless: clicking Install with the picker pointed at a ZIP-named file requires
the status bar to name the ZIP archive; without the surfacing the status stays
empty — the reporter's exact symptom.
Timeline clipitem markers already round-tripped; source-media annotations on
master clips were omitted with an empty export report. Write those markers on
<clip> so re-import keeps them.
Fixesstorytold/filmcraft#900.
Export was overwriting Stack markers with the using clip's markers, so a nest with no instance markers lost its sequence markers, and instance markers were imported onto the nested sequence. Keep sequence markers on the Stack and store clip instance markers in metadata.filmcraft.clip.
FCPXML writes point markers as one frame at the sequence rate. Import
compared duration to 1 tick, so a 24fps one-frame marker became a
range. Treat duration ≤ one sequence frame as a FilmCraft point.
Use ds64.dataSize for an RF64 data-size sentinel instead of the rest of
the file, so chunks after the audio are no longer decoded as samples.
Fixes#925
Prevent concurrent fixture producers from replacing files held open by
external decoders. Add a four-consumer Windows sharing regression.
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Trim, roll, slip, slide, rate stretch and Speed/Duration now fail with
"track is locked" instead of changing clips on a locked track, matching
same-track move. Linked partners on locked tracks are left alone.
file.openDemoProject left Session.path pointing at the previous
.fcproj, so File ▸ Save overwrote that project with the demo.
Match New Project: drop the path (and the associated preview dir)
so Save requires Save As and the original file stays intact.
Startup scan used to delete an unlocked recovery session whenever
session metadata could not be parsed, even if snapshot.fcproj was
intact. Offer that snapshot for recovery instead of removing it.
APNG and animated GIF files opened as stills showing only their first
frame. They now open as movie clips (crates/media/src/animated.rs):
- every frame is decoded and composited onto the full canvas by the
image crate (APNG blend/dispose ops, GIF disposal), keeping alpha
- each frame lasts its own delay; GIF delays <= 10 ms play as 100 ms
and APNG delays < 10 ms as 10 ms, as browsers do
- consecutive identical frames are merged; the clip is one pass of the
animation and its timebase is the most common frame length
- hostile input: at most 10,000 frames / 1 GiB of decoded pictures; a
damaged frame ends the clip at the frames before it; files with one
frame (or none that decode) still open as stills, as before
Tests: media unit tests (timing, transparency, merging, single-frame
files stay stills, truncated/bit-flipped files never panic) and an
engine test that imports an animated GIF and reopens it from a saved
project.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkQ2PDrNFjs9Vm8T44tdjA
Carry #754's "new" track handling into place_one, and add it to items: the
first item adds the track and the rest of the list follows it onto that track.
A drag of Effect Controls / Properties numbers already used merge+begin, but
the press-start memory was only written on frames that emitted setParam.
egui drops unused temp data after a quiet frame, so a Scale scrub that
paused between percents began a new undo step for every 1%. Refresh the
drag session on idle frames while the button is down; still begin a new
step on the next press. Same keep-alive for graphics property drags.
file.reveal is enabled when a selected clip has a local path, greyed out
with no selection, and the OS opener reveals the file (xdg-open of the
parent folder on Linux).
Transport and program streams listed a single audio stream, so file.import
and New Sequence From Clip kept one clip. Probe every audio PID (capped),
decode each independently, and place linked clips per stream. MP4/MKV
already did this; MXF same-rate PCM is still interleaved as one stream.
A focused text field now owns every unmodified key, so an input method can
compose there. Vietnamese Telex builds its diacritics from plain letters
(`a` + `s` → `á`, `a` + `r` → `ả`, `a` + `j` → `ạ`), and the editor bound
`A` `B` `C` `H` `J` `K` `L` `N` `P` `R` `T` `U` `V` `Y` `Z` as single-key tool
shortcuts, which consumed those presses before any field saw them. No
diacritic could be typed (#243).
`egui_wants_keyboard_input` was not enough to guard this: it is read before the
pass lays the UI out, so it still reports the previous pass, and it is true for
any focused widget, so using it would leave the editor without its single-key
tool shortcuts while a slider has focus. `text_edit_focused` is the precise
check.
Chords with a modifier keep firing while a field is focused: an IME does not
consume Ctrl+S and friends. Esc (trim cancel) is handled before the guard and
is unaffected.
Tests: the four cases in `text_focus_shortcut_tests` fail without the guard
(`an_unmodified_key_reaches_a_focused_text_field`,
`the_telex_trigger_keys_are_left_to_the_field`) and pass with it.
Problem: the AV1 in-loop filters ran on one thread per frame (postfilter::apply
had an unused pool). On a single-tile stream they are a quarter to a third of a
frame, and since references are waited for as whole frames, that serial tail
sat on the critical path of every frame that references it.
Fix: the three filters run in bands of 64 luma rows on the decoder's existing
rayon pool, one join per filter per frame. CDEF and loop restoration write only
their own rows of a separate output. The loop filter's edges never touch the
same sample, and only a band's top horizontal edges reach into the band above:
each band refilters the vertical edges of a copy of the 8 rows above it, runs
those edges against the copy, and the samples they changed are written back.
With one thread the code path is the old one. Output stays bit-exact (1 vs 16
threads vs libdav1d; new 4:2:2 / 4:4:4 libaom fixtures). perf.rs gains a
low-delay 1080p and a single-tile 2160p fixture and the all-core stage
breakdown. 16 threads: 1080p GOP 1.16-1.30x, low delay 1.28x, 2160p 1.14x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A damaged file whose frames fail to decode or read again and again
rendered black with no explanation. In the monitors only, after 3
consecutive Decode/Io failures of the same item its failing frames show
the existing MEDIA UNREADABLE slate; a decoded frame clears it. Pending
(still-loading) reads never count, and exports never see it.
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: Indonesian, one of the 12 key languages in
docs/localization-parity.md, had no catalog (0%), so Indonesian users got
the interface in English and System Language never picked Indonesian.
Fix: add crates/ui-egui/src/i18n/id.tsv, clean-room translations of every
entry in the Spanish catalog plus the native macOS Settings menu (3,541),
with a glossary in its header. Language::Id ("Bahasa Indonesia") is
persisted as `id`; it appears in Edit > Language (also the native macOS
menu), Settings > General, and System Language picks it for `id-*` locales
and the legacy `in-*` code older Java/Android runtimes report. Tests cover
the core menus, reject entries without a Spanish counterpart, check glyph
coverage with the bundled fonts, switching/persistence and locale
matching. Attribution sidecar, ATTRIBUTION.md row, docs (gaps.md G14,
localization-parity, ROADMAP, ui-egui README) updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin GOP-cache eviction on the requested pts so a burst past the playhead
does not drop the target and walk the GOP again. Preroll only schedules
the frames that gate clock start. bench_playback no longer meters preroll
jobs as on-clock latency.
This is the seek stall from #848, not the ~3× per-core gap vs ffmpeg.
Problem: #802 gave Edit > Label a colour swatch per label, but the Label
submenus of the Project panel item menu and the Timeline clip menu still
listed bare built-in names: no colour, no custom names from Preferences >
Labels, and no automation ids.
Fix: a shared menus::label_choice draws each choice as Edit > Label does
(the user's name, a swatch of the user's colour in the left gutter) and
registers it (project.itemMenu.label.<Label>,
timeline.clipMenu.edit.label.<Label>); both context menus use it, and their
Label submenu buttons get automation ids too.
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: the Portuguese (Brazil) catalog, one of Premiere's interface
languages and one of the 12 key languages in docs/gaps.md G14, covered
only the menus (348 of ~3,540 strings); panels, dialogs, Settings pages,
effect names, the Keyboard Shortcuts dialog and status messages showed
in English.
Fix: pt-br.tsv now has every entry Spanish has plus the native macOS
Settings item, written from the plain meaning of the English text with
ordinary Brazilian video-editing vocabulary (infinitive commands,
polite imperative instructions, sentence case); the header records the
conventions and a term glossary. Like Japanese and French, tests
require only the menus (now every menu label, not just the core set),
so later strings show in English until translated; a Portuguese entry
without a Spanish one fails, and a new test checks the bundled fonts
cover every Portuguese letter. The core-menu set that Ukrainian,
German, Russian and French must cover moves from pt-br.tsv (no longer
menus-only) to the menus-only Ukrainian catalog, unchanged in content
for those tests. Attribution sidecar and ATTRIBUTION.md entry added.
Docs: G14, localization-parity, ROADMAP language row, ui-egui README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
timeline.roll follows linked partners at the same through-edit when linked
selection is on, so a +12 frame roll on V1 also moves A1. The command
reports linked:false when the cut is not followed as a pair. Reversed
clips keep the unmoved-edge media mapping (#526/#558).
Problem: program-monitor markers.add (and markers.addRange, addRangeInOut,
addChapter, addFlashCue) mapped `color` with Label::from_name and fell back
to Green on any unknown name, so `{color:"Red"}` succeeded and stored Green;
markers.edit silently ignored an unknown colour.
Fix: one marker_color_p helper turns a non-label `color` into a bad-params
error that lists the accepted label names; every marker command (and the
Source monitor path, which already rejected it) uses it. Absent or null
still means the default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clips and markers before sequence start were accepted, then unreachable
in the GUI and cut off on export. Time, frame and seconds before 0 now
return a parameter error; zero remains valid.
Problem: the pen mask tool ignored Shift, so straight horizontal,
vertical or diagonal mask edges could not be drawn precisely (Premiere
26.0 constrains mask lines with Shift). Separately, path_from_json
accepted the `{"p","in","out"}` vertex form through serde first, which
silently dropped the `in`/`out` tangents, so a pen mask's dragged-out
curves were committed as corners.
Fix: with Shift held, a new pen point snaps onto the nearest 45° ray
from the previous point (in screen space), the rubber-band preview
follows, and a Shift-drag constrains the Bézier handle it pulls out.
path_from_json now parses the vertex forms before falling back to the
serialized MaskPath.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the desktop app picked the project among its launch files with a
case-sensitive ends_with(".fcproj"), so CUT.FCPROJ opened through a Windows
file association or the Linux MIME glob was imported as media. Relative
launch paths were stored as given and went missing from another folder.
std::env::args panicked on a non-Unicode argument. filmcraft-cli import
used canonicalize, which stores \\?\ verbatim paths on Windows.
Fix: args::project_and_media finds the project by a case-insensitive
extension and makes every launch path absolute with std::path::absolute
(no verbatim prefix, symlinks kept). Both apps read args_os; the desktop
app keeps --data-dir exactly. filmcraft-cli import uses std::path::absolute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: masks.roundCorners (#813) rounded rectangle/polygon mask corners only from the
command line, control channel or MCP; the Program monitor had no Premiere 26-style
corner-roundness handle.
Fix: a selected mask with sharp corners between straight edges shows a roundness handle
just inside its top-left corner (automation id program.mask.roundness). Dragging it into
the shape moves it along the corner bisector, previews the rounded outline live with the
radius, and on release dispatches one masks.roundCorners (one undo step per drag). The
radius is capped where no corner can grow further. Ellipses, pen masks and rounded corners
show no handle. Tooltip translated (es/fr/ja/zh-cn). New headless UI test
mask_roundness_ui; docs/gaps.md G18 updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the cpal error callback marked the stream failed on its first
error of any kind, so the UI stopped audio and played the rest of the
playback without sound. cpal's ALSA backend reports transient errors
(POLLERR after an xrun, spurious poll returns, short writes) that way
and keeps the stream alive; after POLLERR it no longer calls back.
Reports: a second or two of audio, then silence while meters move (#23).
Fix: AudioOut::recover, called every playback frame, lets CpalOut
restart the stream: at once when the device is gone, or when an error
was reported and no samples were consumed for 300 ms. The restarted
stream reuses the shared play-ahead mixer and played-frame clock and
must keep the sample rate and channel count; at most 3 restarts in
10 s, then playback falls back to the wall clock as before. Errors
while samples keep flowing no longer silence playback. The device
callback runs the mixer under catch_unwind: a panic plays silence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: no test did a real editing job end to end. Codec and container
oracles exist, but nothing imported phone- or camera-like media, edited
it through the command layer and checked the exported file (G6).
Fix: apps/filmcraft-cli/tests/workflows.rs drives the `filmcraft-cli mcp`
binary over stdio on media generated with ffmpeg into target/fixtures:
- a VFR H.264 + AAC MP4: sequence from clip, extract, -6 dB audio gain,
title, H.264 MP4 export;
- a long-GOP H.264 MKV with AAC + AC-3 tracks: ripple trims, colour matte,
centred cross dissolve, save; a new CLI process exports ProRes.
ffprobe / ffmpeg check streams, frame count, duration, which source frame
lands where, A/V sync across the cut, title pixels, the dissolve mix and
audio levels. The tests skip without ffmpeg (testkit oracles).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: DPX import rejected the 10/12-bit packed layout (packing 0)
and the Y'CbCr descriptors (100 CbYCrY, 101 CbYACrYA, 102 CbYCr,
103 CbYCrA) with an "unsupported" error.
Fix: unpack packed components back to back, low bits first, each scan
line on a 32-bit boundary (as ffmpeg reads 12-bit packed files), and
convert Y'CbCr to RGB with the matrix the colorimetric field names
(BT.709; BT.601 for the 601 and composite codes; by picture size when
unspecified), in video range unless the reference codes span the full
code range; 4:2:2 chroma is shared by each pixel pair. Unit tests for
every new layout and hostile headers; an ffmpeg oracle test builds
packed 12-bit RGB and 8-bit 4:2:2 / 4:4:4 / 4:4:4:4 files from
ffmpeg's test picture and matches its decode within one 8-bit step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: FilmCraft exports OpenEXR sequences (#806) but could not open
.exr files at all, so EXR plates and renders from other apps could not
be imported (docs/gaps.md G10).
Fix: a clean-room OpenEXR reader in filmcraft-media (still/exr.rs) for
single-part scan-line files with no, RLE, ZIPS or ZIP compression and
half / float / uint channels, with our own zlib/DEFLATE decoder (no new
dependency). R, G, B, A (or luminance Y) become premultiplied linear
RGBA float frames, the compositor's working format, so values above 1.0
survive; the data window is placed in the display window. Stills open
through open_bytes, sequences through ImageSequenceSource; tiled, deep,
multi-part, subsampled and PIZ / PXR24 / B44 / DWA files are refused
with a clear error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy and Cut snapshot files or an image already on the system clipboard.
Paste ignores that same media so Cmd+V and the Edit menu paste the clips
instead of re-importing stale files the platform left beside the clip names.
A GopCache decodes with its lock held, and that decoding starts rayon work
(rotating iPhone frames, cropping, slice-threaded decoders). On the global pool
a worker that took part of it could, waiting inside it, pick up another render
job asking the same cache for a frame and block on the lock, while the lock
holder waited for the work on that worker's stack: exports of rotated iPhone
HEVC clips stopped making frames in about half the runs.
The locked decoding now runs on a separate decode pool, whose threads never wait
for a cache's lock; a caller that is a rayon worker waits on a plain thread so it
does no other rayon work while holding the lock. The cancel/catch-up/draft/
background hints travel with it (filmcraft_media::cancel::Context). Intra-only
streams crop and rotate before taking the lock again.
Second version of #662 (reverted): the regression test's stand-in decoder summed
with overflow, which panics in debug builds; it now wraps explicitly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `cargo xtask parity` (#781, #782) measured only the menu bar; the
Effects panel's effects and transitions had no measured parity figure
(docs/gaps.md G7).
Fix: `--area effects|transitions` compares the catalogue the engine's
effect definitions list in the Effects panel (new `effect_catalog`
example in filmcraft-project, the panel's own top-folder filter) with a
reference of folder paths or bare names (default the local, never
committed plan/premiere/<area>.json). Same report, caps and `--min` as
menus; placement by folder; Presets / Lumetri Presets and the other
area's items are out of scope, so one panel snapshot serves both areas.
`--area menus` (default) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging files from the explorer onto the Timeline imported them into the
Project bin but never placed them when no sequence was open, because only
the clip-area interact path consumed pending_timeline_drop. The empty
Timeline now creates a sequence from the first imported item and places
any further items after it, matching the existing Project-panel drop.
Audio effects applied to the video side of a pair now land on the linked audio, and Effect Controls lists that clip's parameters and Custom Setup while the picture is selected.
File ▸ Close aliases Close Sequence. The shortcut editor targeted
sequence.close while Cmd+W lived on file.close (and Timeline), so Clear
left the chord bound. Treat the pair as one shortcut family.
Fixesstorytold/filmcraft#773
Problem: a graphics driver that crashes inside wgpu instance or device
creation (Intel Vulkan igvk64.dll #512#413#793, AMD atio6axx.dll #477)
killed every launch the same way until the user set WGPU_BACKEND by hand;
a start-up error wgpu did report ended in an error dialog.
Fix: each start leaves a marker in the data folder (graphics-start.json),
cleared once the window has painted two frames. A marker still pending at
the next launch moves on along the platform's ladder (Windows: Vulkan+DX12,
DX12 only, software/WARP; Linux: default, software/lavapipe or llvmpipe)
and keeps the mode that works. A wgpu start-up error moves on within the
same process (eframe reuses its event loop). Fallback modes don't register
the GPU export renderer, whose own instance would load the failed driver.
An explicit WGPU_BACKEND leaves the marker alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: symphonia rejects every Layer II frame whose bit rate / mode
combination ISO/IEC 11172-3 does not allow (mono above 192 kbit/s, stereo
below 64 kbit/s). ffmpeg writes mono MP2 at 384 kbit/s by default, so such
tracks played as silence in MPEG-TS / PS, Matroska and raw .mp2 files.
Fix: new L0 crate filmcraft-mpa, a clean-room Layer I / II decoder written
from ISO/IEC 11172-3 and 13818-3 (all modes incl. joint stereo, MPEG-1/2/2.5
rates, the disallowed combinations decoded with the allocation table their
bit rate per channel selects). PacketDecoder::mpeg_audio uses it for layers
I and II (packets of several frames too); raw .mp2 files decode with it,
resyncing after damage. Oracle tests vs ffmpeg: 73-84 dB SNR on ffmpeg /
twolame streams and synthetic Layer I / joint stereo frames; fuzzed damaged
frames never panic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: ffmpeg stores the encoder priming as CodecDelay in ns (1024 AAC
samples at 48 kHz = 21.333 ms, 256 AC-3 samples at 44.1 kHz = 5.805 ms)
and block timestamps in ms. The demuxer subtracts the delay rounded to
1 ms, and fixed-length packets were anchored on that rounded stamp, so
the whole track played up to 16 samples late against ffmpeg's decode.
Only Opus undid the rounding.
Fix: one stamp-to-samples conversion for every Matroska audio codec
that undoes the demuxer's rounding and removes the exact CodecDelay
(rounded to the nearest sample; the element holds whole ns), with
checked math against hostile timebases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
timeline.place takes items: [...] and file.newSequence takes fromItems: [...]; both run as
one undo step through the new Session::edit_group, which rolls back if any item fails.
Files dropped from the OS use the same command instead of one timeline.place per file,
so they are one undo step too.
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>
AviSource maps AVI streams onto the decoders, opened from bytes or a
streaming reader (Media Browser probe):
- Video: Motion JPEG, H.264 and HEVC (Annex B, our decoders) and
uncompressed BGR24 / BGRX32 / YUY2 / UYVY / I420 / YV12 / NV12 / grey
(new RawVideoDecoder). Empty "drop frame" chunks hold the picture
before. AVI has no timestamps: H.264 with B-frames is stored in decode
order, so the presentation order comes from the pictures' order counts
(mxf::avc_presentation_order), keeping the slots the pictures occupy.
A writer that kept a fine time base (ffmpeg copying an MP4's 1/600)
fills the slots between pictures with empty chunks; the reported frame
rate is the one the pictures play at. DivX / Xvid and DV are named and
report that they have no decoder; their sound plays.
- Audio: PCM (integer and float) from the chunks; MPEG audio and AC-3
frames, which AVI chunks need not follow, found again across chunk
boundaries with the new filmcraft_mpegts::frame_bytes.
- Motion JPEG frames without Huffman tables (the AVI1 convention) decode
with the standard ITU-T T.81 Annex K tables (also for MOV).
Tests against ffmpeg's decode of the same files: H.264 with B-frames
bit-exact under random access, also copied from a 1/600 MP4;
uncompressed video and PCM bit-exact; JPEG and MP3 / MP2 / AC-3 within
decoder tolerance; drop frames; DivX; JPEG without tables equal to the
same frame with them. Engine: an AVI imports, makes a sequence, plays
picture and sound, and probes without reading it whole.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New L0 crate written from the AVI RIFF file reference and OpenDML 1.02:
avih and per-stream strh / strf (BITMAPINFOHEADER, WAVEFORMATEX(TENSIBLE))
/ strn, and chunk tables from the best index a file has: the OpenDML indx
super index and ix## standard indexes (field indexes too), else idx1
(offsets relative to movi or absolute) plus a scan of RIFF AVIX
extensions it cannot see, else a scan of the movi lists (key frames then
unknown). Frames are read on demand through a ByteSource.
Sizes are clamped to the file, so a cut-off recording opens with what
is there; rec lists nest at most 3 deep and tables are bounded by what
the file could hold. Tests: hand-built files for every index path,
truncation, and every single-byte mutation and truncation (no panic);
ffmpeg-made files against ffprobe; an ignored test on a 1.16 GB OpenDML
file with AVIX extensions.
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>
The ISO-BMFF path created the FLAC decoder with no extra data, so every
FLAC track in an MP4 or MOV failed with "flac: missing extra data". The
decoder now gets the STREAMINFO body that `dfLa` carries after its
4-byte block header, as the Matroska path already did.
Regression test `mp4_flac_audio` (an ffmpeg-made MP4 with FLAC audio)
failed before this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native file dialogs are requested with a continuation (FilmcraftApp::pick for
commands, pick_ui for panel buttons). On Linux the desktop host shows the rfd
dialog on a worker thread (HostHooks::run_dialog) and the result is applied
when it arrives, so the frame loop, playback and the control channel keep
running and GNOME no longer offers to force-quit the window. One dialog at a
time; a path for a dialog closed meanwhile goes nowhere. macOS / Windows keep
their synchronous pickers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PC whose GPUs were installed from different driver packages (an ASUS G513QY:
the integrated Radeon on the 2021 package, the RX 6800M on the 2026 one, each with
its own Vulkan ICD) lists the RX 6800M twice. wgpu's default takes the first
discrete adapter, the stale ICD, and drawing through it dies with 0xc0000005
inside amdvlk64.dll before any Rust frame can catch it.
apps/filmcraft/src/graphics.rs installs an egui-wgpu native_adapter_selector that
keeps wgpu's ranking (device type for WGPU_POWER_PREF, then enumeration order)
between different GPUs but, among entries that are one physical GPU (same backend,
vendor and device id; bus id when both drivers report one), prefers the entry
exposing the most wgpu features, i.e. the newer driver. Adapters that cannot draw
to the window are skipped. With one driver per GPU the rule has nothing to merge
and the choice is wgpu's own. WGPU_ADAPTER_NAME (part of the name, backend or
driver text) picks an adapter by hand; candidates, duplicates and the choice are
logged.
The choice is a pure function over plain candidate data, with regression tests
built from this machine's vulkaninfo listing (wgpu's rule picked the crashing
entry 0; the selector picks entry 1) plus wgpu-order, surface, two-cards and
override cases. Verified on the affected laptop: the release build now opens,
answers the control channel, screenshots and quits (exit 0); it crashed on every
launch before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
@@ -86,12 +86,8 @@ reproduce an image (for example, point lists traced from someone else's icon).
`crates/media`, procedural looks in `crates/render`) are original work under the project licence
and are listed in `ATTRIBUTION.md`.
7. **When in doubt, leave it out** and draw or generate it yourself.
8. **The one exception: first-party ArtCraft brand marks.** The ArtCraft name and logos in `docs/brand/`
are trademarks of the ArtCraft Team, not open source, usable only unmodified and only in the context of
FilmCraft under `docs/brand/LICENSE-brand.txt`; forks and modified versions must remove them. They still
need a sidecar and an `ATTRIBUTION.md` row (licence `LicenseRef-ArtCraft-Trademark`). No other
non-open asset is allowed, and this exception never covers third-party marks (Adobe, Discord, GitHub
and other logos stay out; draw a generic icon instead).
8. **No non-open assets.** Third-party marks (Adobe, GitHub and other logos) stay out; draw a generic icon
instead.
9. **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this
repo.** Don't commit new font files (the small Latin UI fonts already in `assets/fonts/` stay). A
font FilmCraft needs is added to craft-fonts, which the app reads through the optional build input
@@ -127,6 +123,8 @@ See [§0](#0-never-crash). It outranks every other engineering rule.
See `CLAUDE.md`: pure Rust, dependency layering (`cargo xtask layers`), exact `Tick` time, everything is
a command, everything is agent-drivable, and the quality gates (`cargo xtask ci`) before every commit.
- **Time goes through `chrono`** (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math); a crate owns only its output format string. The one exception is range extension: past the years chrono can represent, shift the date by whole 400-year Gregorian cycles (146,097 days, after which the calendar repeats), let chrono compute the month and day, and add the cycles back to the year (splitting seconds into hours, minutes and seconds is plain arithmetic).
Shared real-file test corpora (Photoshop-authored PSDs, etc.) live in
[`storytold/photocraft-corpus`](https://github.com/storytold/photocraft-corpus), explained in
@@ -9,7 +9,11 @@ iconography, images or other Adobe assets; open licences only; every asset attri
| Asset | Author | Source | Licence |
|---|---|---|---|
| `crates/ui-egui/src/i18n/it.tsv` | FilmCraft contributors | Original work: clean-room Italian interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/id.tsv` | FilmCraft contributors | Original work: clean-room Indonesian interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/ko.tsv` | FilmCraft contributors | Original work: clean-room Korean interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/fr.tsv` | FilmCraft contributors | Original work: clean-room French interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/pt-br.tsv` | FilmCraft contributors | Original work: clean-room Brazilian Portuguese interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/ru.tsv` | FilmCraft contributors | Original work: clean-room Russian interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/uk.tsv` | FilmCraft contributors | Original work: clean-room Ukrainian interface translations | MIT OR Apache-2.0 |
| `assets/fonts/Inter-Regular.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
@@ -24,28 +28,28 @@ iconography, images or other Adobe assets; open licences only; every asset attri
| `docs/images/filmcraft-keyframes.png` | FilmCraft contributors | Original work: FilmCraft screenshot; frames from Night of the Living Dead (1968, public domain) | MIT OR Apache-2.0; film frames public domain |
| `docs/images/filmcraft-export.png` | FilmCraft contributors | Original work: FilmCraft screenshot; frame from Night of the Living Dead (1968, public domain) | MIT OR Apache-2.0; film frames public domain |
| `docs/images/filmcraft-assembly.png` | FilmCraft contributors | Original work: FilmCraft screenshot; frames from Carnival of Souls (1962), Night of the Living Dead (1968), thumbnails of all demo media | MIT OR Apache-2.0; film frames public domain |
| `docs/images/filmcraft-lumetri.png` | FilmCraft contributors | Original work: FilmCraft UI crop (Lumetri Color panel) | MIT OR Apache-2.0 |
| `docs/images/filmcraft-color-panel.png` | FilmCraft contributors | Original work: FilmCraft UI crop (color grading panel) | MIT OR Apache-2.0 |
| `docs/images/filmcraft-scopes.png` | FilmCraft contributors | Original work: FilmCraft UI crop (scopes of a Charade (1963) frame, public domain) | MIT OR Apache-2.0 |
| `docs/images/filmcraft-effects.png` | FilmCraft contributors | Original work: FilmCraft screenshot crop; NASA Earth Views (US Government work, public domain) | MIT OR Apache-2.0; footage public domain |
| `docs/images/filmcraft-timeline.png` | FilmCraft contributors | Original work: FilmCraft UI crop; thumbnails from Night of the Living Dead (1968) and Carnival of Souls (1962), CC0 Chopin waveform | MIT OR Apache-2.0; film frames public domain |
| `assets/app-icon/filmcraft.svg` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; canonical vector | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft.svg` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; canonical vector | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `packaging/macos/dmg/background.svg` | @XusBadia | Original work: FilmCraft macOS DMG window background, derived from the FilmCraft app icon (`assets/app-icon/filmcraft.svg`); rendered to `background.tiff` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) |
| `packaging/macos/dmg/background.tiff` | @XusBadia | Rendered from `packaging/macos/dmg/background.svg` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) |
| `assets/app-icon/filmcraft-small.svg` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; lighter vector | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft-1024.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; 1024 px render | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft-macos-512.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; macOS runtime Dock icon (512 px) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft.icns` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; macOS .icns | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft.ico` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; Windows .ico (16-256 px) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; Linux hicolor theme 16 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; Linux hicolor theme 24 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; Linux hicolor theme 32 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; Linux hicolor theme 48 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; Linux hicolor theme 64 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; Linux hicolor theme 128 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; Linux hicolor theme 256 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; Linux hicolor theme 512 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/scalable/apps/ai.storyteller.filmcraft.svg` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; Linux hicolor theme scalable | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `apps/filmcraft-web/web/favicon.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; 128 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft-small.svg` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; lighter vector | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft-1024.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; 1024 px render | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft-macos-512.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; macOS runtime Dock icon (512 px) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft.icns` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; macOS .icns | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft.ico` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; Windows .ico (16-256 px) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; Linux hicolor theme 16 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; Linux hicolor theme 24 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; Linux hicolor theme 32 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; Linux hicolor theme 48 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; Linux hicolor theme 64 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; Linux hicolor theme 128 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; Linux hicolor theme 256 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.filmcraft.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; Linux hicolor theme 512 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/scalable/apps/ai.storyteller.filmcraft.svg` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; Linux hicolor theme scalable | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `apps/filmcraft-web/web/favicon.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner by hand and vectorised; 128 px | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
## Golden test images
@@ -126,24 +130,10 @@ carry each font's licence as `OFL-<family>.txt`.
| BIZ UDPGothic Regular, Bold (Morisawa) | Japanese UI text; Japanese fallback for sans titles (the web build embeds only Regular) | OFL-1.1 |
| Shippori Mincho Regular (FONTDASU) | Japanese fallback for serif titles (native builds) | OFL-1.1 |
| BIZ UDMincho Regular (Morisawa) | Japanese fallback for serif titles (native builds) | OFL-1.1 |
## First-party brand marks
The ArtCraft name, wordmark and logos are trademarks of the ArtCraft Team. They are not open source and
are not covered by the project licence; they may be used only unmodified and only in the context of
FilmCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt). Forks and modified versions must
remove them (AGENTS.md §1.8).
| File | Author | Source | Licence |
|---|---|---|---|
| `docs/brand/artcraft-logo-white.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark, light ink for dark backgrounds) | ArtCraft trademark, not open source; `docs/brand/LICENSE-brand.txt` (not MIT/Apache) |
| `docs/brand/artcraft-logo-white.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark, light ink for dark backgrounds) | ArtCraft trademark, not open source; `docs/brand/LICENSE-brand.txt` (not MIT/Apache) |
| `docs/brand/artcraft-logo.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds) | ArtCraft trademark, not open source; `docs/brand/LICENSE-brand.txt` (not MIT/Apache) |
| `docs/brand/artcraft-logo.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds) | ArtCraft trademark, not open source; `docs/brand/LICENSE-brand.txt` (not MIT/Apache) |
| `docs/brand/artcraft-mark-black.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), black) | ArtCraft trademark, not open source; `docs/brand/LICENSE-brand.txt` (not MIT/Apache) |
| `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), black) | ArtCraft trademark, not open source; `docs/brand/LICENSE-brand.txt` (not MIT/Apache) |
| `docs/brand/artcraft-mark.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft trademark, not open source; `docs/brand/LICENSE-brand.txt` (not MIT/Apache) |
| `docs/brand/artcraft-mark.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft trademark, not open source; `docs/brand/LICENSE-brand.txt` (not MIT/Apache) |
| Noto Sans CJK SC Regular (Google, Adobe) | Chinese UI text and media names; Chinese fallback for titles (native builds) | OFL-1.1 |
| Noto Sans Arabic Regular (Google) | Arabic UI text; Arabic fallback for titles (native builds) | OFL-1.1 |
| Noto Sans Thai Regular (Google) | Thai fallback for titles (native builds) | OFL-1.1 |
| Noto Sans Georgian Regular (Google) | Georgian fallback for titles (native builds) | OFL-1.1 |
@@ -16,6 +16,7 @@ FilmCraft is a clean-room, open-source, pure-Rust non-linear video editor target
- **Pure Rust** in the product. OS media APIs only via Rust bindings in `crates/platform` (hardware decoding; the one crate allowed `unsafe`, see `AGENTS.md` §0.3 and `docs/adr/0001-platform-ffi.md`), optional, behind traits, with our decoders as the fallback.
- **Layering** ([`docs/architecture.md`](docs/architecture.md) §1, enforced by `cargo xtask layers`): nothing below L5 depends on egui/eframe/winit/rfd/cpal. L0 codec/container crates depend only on `filmcraft-bitstream`.
- **Exact time:** all time is `filmcraft_time::Tick` (254 016 000 000/s). Never use f64 seconds for edit math.
- **Time goes through `chrono`** (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math); a crate owns only its output format string. The one exception is range extension: past the years chrono can represent, shift the date by whole 400-year Gregorian cycles (146,097 days, after which the calendar repeats), let chrono compute the month and day, and add the cycles back to the year (splitting seconds into hours, minutes and seconds is plain arithmetic).
- **Everything is a command** (`crates/engine`): id, label, menu path, shortcut, params, enabled(), run(). UI, CLI, control channel and MCP all dispatch by id.
- **Everything is agent-drivable:** every interactive widget registers an automation id; UI state is serde so the control channel can read/set it.
- **Quality gates** before every commit: `cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`, `cargo xtask layers`, `cargo xtask assets`, `cargo xtask wasm` (checks L0–L4 crates). `cargo xtask ci` runs them all.
BIZ UDMincho) — OFL-1.1. Their licence texts ship with the packages as OFL-<family>.txt.
BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic, Noto Sans Thai, Noto Sans Georgian) — OFL-1.1. Their licence texts ship with the packages as OFL-<family>.txt.
Public-domain film and NASA frames and CC0 music shown in README screenshots (docs/images/) — public domain / CC0-1.0. See ATTRIBUTION.md.
<b>Video editing, color and sound; an open-source, clean-room reimplementation of Adobe Premiere Pro, rebuilt in pure Rust.</b>
<b>Video editing, color and sound; an open-source, professional non-linear video editor, built in pure Rust.</b>
</p>
<p align="center">
An open-source, clean-room take on the Adobe Premiere Pro workflow: native on macOS, Windows and Linux, and in the browser via WebAssembly.<br>
By the ArtCraft team.
An open-source, clean-room take on the Adobe Premiere Pro workflow: native on macOS, Windows and Linux, and in the browser via WebAssembly.
</p>
<p align="center">
<a href="#license-and-credits"><img alt="License: MIT OR Apache-2.0" src="https://img.shields.io/badge/license-MIT%20%7C%20Apache--2.0-8b5cf6"></a>
<img alt="Written in pure Rust" src="https://img.shields.io/badge/pure-Rust-6a3fd6?logo=rust&logoColor=white">
<img alt="Runs on macOS, Windows and Linux" src="https://img.shields.io/badge/runs%20on-macOS%20%7C%20Windows%20%7C%20Linux-8b5cf6">
<a href="#status"><img alt="Status: young and moving fast" src="https://img.shields.io/badge/status-young%20and%20moving%20fast-6a3fd6"></a>
</p>
<p align="center">
<a href="https://discord.gg/artcraft"><img alt="Join the ArtCraft community on Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
</p>
<p align="center">
<a href="https://getartcraft.com/apps/filmcraft"><b>FilmCraft on getartcraft.com</b></a> ·
<a href="#license-and-credits"><img alt="License: MIT OR Apache-2.0" src="https://img.shields.io/badge/license-MIT%20%7C%20Apache--2.0-8b5cf6?style=flat-square"></a>
<img alt="Written in pure Rust" src="https://img.shields.io/badge/pure-Rust-6a3fd6?style=flat-square&logo=rust&logoColor=white">
<img alt="Runs on macOS, Windows and Linux" src="https://img.shields.io/badge/runs%20on-macOS%20%7C%20Windows%20%7C%20Linux-8b5cf6?style=flat-square">
<a href="#status"><img alt="Status: young and moving fast" src="https://img.shields.io/badge/status-young%20and%20moving%20fast-6a3fd6?style=flat-square"></a>
<br>
<a href="https://sonarcloud.io/project/overview?id=storytold_filmcraft"><img alt="Scanned by SonarCloud" src="https://img.shields.io/badge/Scanned_by-SonarCloud-4b9fd5?style=flat-square&logo=sonarqubecloud&logoColor=white"></a>
<a href="https://coderabbit.ai"><img alt="Reviewed by CodeRabbit" src="https://img.shields.io/coderabbit/prs/github/storytold/filmcraft?style=flat-square&label=Reviewed%20by%20CodeRabbit&logo=coderabbit"></a>
</p>
<br>
<p align="center">
<img src="docs/images/filmcraft-hero.png" alt="FilmCraft in the Color workspace, mid-way through an Apollo 11 documentary cut from NASA footage: the Program monitor on the Saturn V clearing the launch tower with a Launch Complex 39A lower third and an air-to-ground subtitle, Effect Controls with Lumetri and Scale keyframes on the shot, the Lumetri Color panel, bins of NASA selects, and a timeline with 49 picture cuts, B-roll, titles, a caption track, mission audio, a ducked music bed, named markers, a rendered section and live loudness meters" width="100%">
<img src="docs/images/filmcraft-hero.png" alt="FilmCraft in the Color workspace, mid-way through an Apollo 11 documentary cut from NASA footage: the Program monitor on the Saturn V clearing the launch tower with a Launch Complex 39A lower third and an air-to-ground subtitle, Effect Controls with color grading and Scale keyframes on the shot, the color grading panel, bins of NASA selects, and a timeline with 49 picture cuts, B-roll, titles, a caption track, mission audio, a ducked music bed, named markers, a rendered section and live loudness meters" width="100%">
</p>
<p align="center"><sub><i>Apollo 11 - Tranquility</i>: a three-minute documentary edit of NASA's 1969 launch, landing and moonwalk film, with subtitles from the mission transcript. Every frame in these screenshots comes from public-domain footage, decoded, composited and graded by FilmCraft's own code.</sub></p>
> [!NOTE]
> **ArtCraft is a community of artists from all walks of life.** Digital, generative, music,
> games — if you make things, you're one of us. **[Come say hi on Discord](https://discord.gg/artcraft).**
<p align="center">
<a href="#edit">Edit</a> ·
<a href="#color">Color</a> ·
@@ -66,7 +45,7 @@
<br>
FilmCraft is a non-linear editor for people who know Premiere: the same panels, workspaces, tools and shortcuts, so your hands already know where everything is. Underneath, it is new from the bitstream up. The H.264, HEVC, ProRes and AAC codecs are our own, written in Rust from the public specifications. A GPU compositor works in linear light. Frame math runs on exact integer time, so edits never drift. And every action in the app is a command that an AI agent can drive as precisely as you can.
FilmCraft is a professional non-linear editor: familiar panels, workspaces, tools and shortcuts, so your hands already know where everything is. Underneath, it is new from the bitstream up. The H.264, HEVC, ProRes and AAC codecs are our own, written in Rust from the public specifications. A GPU compositor works in linear light. Frame math runs on exact integer time, so edits never drift. And every action in the app is a command that an AI agent can drive as precisely as you can.
<br>
@@ -84,7 +63,7 @@ FilmCraft is a non-linear editor for people who know Premiere: the same panels,
- **Every trim.** Ripple, roll, slip, slide, rate stretch and razor tools. Trim mode selects edit points as ripple, roll or trim, nudges them a frame at a time (`⌥←``⌥→`, ×5 with `⇧`), toggles the trim type with `⌃T` and extends them to the playhead with `E`. `Q` and `W` ripple-trim to the playhead. The **Trim Monitor** shows both sides of the edit, and **dynamic trimming** trims live while it plays: `L` forward, `J` back, `K` to stop and commit as one undo step.
- **Exact time.** Every edit is computed on integer ticks: 254,016,000,000 per second, which divides evenly by every common frame rate and sample rate. 23.976, 29.97 drop-frame and 59.94 are exact, not approximate.
- **The details pros rely on.** Markers with colours, names and durations; export sequence review notes with **Markers ▸ Export Markers as CSV…**; add edit (`⌘K`) on one or all tracks; nesting; copy, paste and paste insert; ripple delete and close gap; snapping; unlimited undo with a History panel.
- **Your keys.** A Keyboard Shortcuts editor (`⌥⌘K`) with a drawn keyboard, panel-specific shortcuts, conflict warnings and presets for FilmCraft, Premiere Pro, Final Cut Pro and Avid key layouts.
- **Your keys.** A Keyboard Shortcuts editor (`⌥⌘K`) with a drawn keyboard, panel-specific shortcuts, conflict warnings and presets for FilmCraft and standard NLE key layouts.
- **Never lose work.** Saves are atomic, auto-save keeps a rolling set of versions, and a crash-recovery journal written about a second after each edit brings back unsaved changes after a crash or power cut.
<p align="center">
@@ -103,7 +82,7 @@ FilmCraft is a non-linear editor for people who know Premiere: the same panels,
<p align="center"><sub>The Color workspace on <i>Charade</i> (1963), with live scopes beside the Program monitor.</sub></p>
**A complete Lumetri-style grading panel**, in the order a colourist works:
**A complete color grading panel**, in the order a colourist works:
- **Basic Correction:** temperature and tint, exposure, contrast, highlights, shadows, whites, blacks, saturation.
- **Creative:** eight built-in looks with an intensity control, plus faded film, sharpen and vibrance. The looks are colour transforms written in code, not LUT files.
@@ -115,11 +94,11 @@ FilmCraft is a non-linear editor for people who know Premiere: the same panels,
<table>
<tr>
<td width="34%" valign="top" align="center">
<img src="docs/images/filmcraft-lumetri.png" alt="The Lumetri Color panel on Charade (1963): Basic Correction sliders with coloured temperature and tint tracks, collapsed Creative and Curves sections, shadow, midtone and highlight colour wheels, and HSL Secondary and Vignette toggles"><br>
<sub><b>The Lumetri Color panel.</b> Basic Correction, Color Wheels and every other section, each with its own on/off switch.</sub>
<img src="docs/images/filmcraft-color-panel.png" alt="The color grading panel on Charade (1963): Basic Correction sliders with coloured temperature and tint tracks, collapsed Creative and Curves sections, shadow, midtone and highlight colour wheels, and HSL Secondary and Vignette toggles"><br>
<sub><b>The color grading panel.</b> Basic Correction, Color Wheels and every other section, each with its own on/off switch.</sub>
</td>
<td width="66%" valign="top" align="center">
<img src="docs/images/filmcraft-scopes.png" alt="Lumetri Scopes: a green luma waveform and a vectorscope of a Charade (1963) frame"><br>
<img src="docs/images/filmcraft-scopes.png" alt="The video scopes: a green luma waveform and a vectorscope of a Charade (1963) frame"><br>
<sub><b>Scopes that tell the truth.</b> The waveform and vectorscope are computed from the graded frame. All grading happens in linear light, in 32-bit float.</sub>
</td>
</tr>
@@ -130,14 +109,14 @@ FilmCraft is a non-linear editor for people who know Premiere: the same panels,
## Effects and motion
<p align="center">
<img src="docs/images/filmcraft-effects.png" alt="Effects workspace: Effect Controls with Motion, Opacity, Time Remapping and Lumetri Color, NASA's Earth Views from the ISS in the Program monitor, and the Effects browser open on the Dissolve transitions" width="100%">
<img src="docs/images/filmcraft-effects.png" alt="Effects workspace: Effect Controls with Motion, Opacity, Time Remapping and a colour grade, NASA's Earth Views from the ISS in the Program monitor, and the Effects browser open on the Dissolve transitions" width="100%">
</p>
<p align="center"><sub>NASA's <i>Earth Views from the ISS</i> with a look applied, and the Effects browser open on the dissolves.</sub></p>
- **Premiere 26's full Video Effects bin (93 effects in 16 folders), the Legacy bin and the obsolete effects old projects use, plus around 30 transitions.** Blurs (Bokeh, Focus, Compound), keys (Ultra Key, Track Matte), distortions (Corner Pin, Turbulent Displace, Warp Stabilizer), Lights & Glows, Immersive Video (VR) effects on equirectangular footage, Posterize Time, Echo and more. Cross dissolve, dip to black or white, film dissolve, wipes, irises, pushes, slides, zooms, page peel, cube spin and more.
- **A comprehensive Video Effects bin (over 90 effects across 16 categories), legacy effects, plus around 30 transitions.** Blurs (Bokeh, Focus, Compound), keys (Ultra Key, Track Matte), distortions (Corner Pin, Turbulent Displace, Warp Stabilizer), Lights & Glows, Immersive Video (VR) effects on equirectangular footage, Posterize Time, Echo and more. Cross dissolve, dip to black or white, film dissolve, wipes, irises, pushes, slides, zooms, page peel, cube spin and more.
- **Motion and opacity on every clip:** position, scale, rotation, anchor point and anti-flicker, plus 26 blend modes.
- **Keyframes like Premiere's:** linear, Bezier, auto and continuous Bezier, hold, ease in and ease out. Effect Controls shows a keyframe lane for every parameter under a time ruler with the playhead's handle, and each animated parameter opens into **value and velocity graphs** with draggable influence handles. Effect Controls and the Properties panel share the keyframe navigator (◀ ◆ ▶): add or remove the keyframe at the playhead, step to the previous or next one.
- **Advanced keyframing:** linear, Bezier, auto and continuous Bezier, hold, ease in and ease out. Effect Controls shows a keyframe lane for every parameter under a time ruler with the playhead's handle, and each animated parameter opens into **value and velocity graphs** with draggable influence handles. Effect Controls and the Properties panel share the keyframe navigator (◀ ◆ ▶): add or remove the keyframe at the playhead, step to the previous or next one.
- **A GPU compositor** built on wgpu (Metal, Vulkan, DirectX 12, WebGPU). It samples YUV straight from the decoder with footprint supersampling and blends in linear light. A CPU path renders the same frames, and the two are tested against each other.
<p align="center">
@@ -175,15 +154,17 @@ No FFmpeg inside. The video codecs, AAC, Opus and the containers are our own Rus
| **HEVC / H.265** | ✓ | | Main and Main 10, bit-exact on 41 streams (tiles, WPP, PCM, long-term references), about 225 fps at 1080p |
| **VP8** | ✓ | | Every coding tool, bitstream versions 0–3, hidden altref frames, alpha layer of transparent WebM; bit-exact with libvpx on its published VP8 test vectors; in WebM/Matroska and MP4 |
| **VP9** | ✓ | | Profiles 0–3, 8/10/12-bit, 4:2:0 to 4:4:4, tiles and superframes; bit-exact on 50+ streams; in WebM/Matroska and MP4 |
| **Opus** | ✓ | | SILK, CELT, hybrid and multistream surround; range-exact on every RFC 8251 conformance vector; in WebM/Matroska and MP4 |
| **AV1** | ✓ | | Main profile, 8/10-bit 4:2:0 and monochrome, every coding tool incl. film grain, superres and spatial layers; bit-exact with libdav1d on the libaom vectors tested; in MP4 and WebM/Matroska |
| **Avid DNxHD / DNxHR** | ✓ | ✓ | All SMPTE ST 2019-1 CIDs (LB to 444, 8/10/12-bit); export writes DNxHR in MOV |
| **DNxHD / DNxHR** | ✓ | ✓ | All SMPTE ST 2019-1 CIDs (LB to 444, 8/10/12-bit); export writes DNxHR in MOV |
| **MJPEG, PCM** | ✓ | ✓ | |
| **MP3, FLAC, ALAC, Vorbis** | ✓ | | Via the [symphonia](https://github.com/pdeljanov/Symphonia) crate (MPL-2.0) for now, to be replaced by our own |
| **AVI** | ✓ | | AVI 1.0 and OpenDML (files past 1 GB); Motion JPEG, H.264, HEVC and uncompressed RGB / YUV with PCM, MP3, MP2 or AC-3. DivX / Xvid and DV not yet |
| **Stills** | ✓ | ✓ | Import PNG, JPEG, GIF, WebP, TIFF and BMP (animated GIF and APNG as clips, with transparency); export PNG sequences and animated GIF |
Also imported: MPEG-2 / MPEG-1 video, AC-3, MP2, MXF (OP1a / OP-Atom), MPEG transport and program streams (AVCHD, broadcast, DVD), Ogg and image sequences. Not yet: AV1 export, camera RAW, E-AC-3. Hardware decoding works on macOS (VideoToolbox), Windows (Media Foundation) and, for H.264 and HEVC, Linux (VA-API); H.264 export can use NVIDIA's encoder on Windows and Linux (opt-in), and H.265 (HEVC Main, 8-bit) export works through it (NVENC on Windows, VideoToolbox on macOS) ([#30](https://github.com/storytold/filmcraft/issues/30)).
<p align="center"><sub>Export mode, set to write the trailer as H.264 MP4.</sub></p>
- **H.264 MP4 with AAC, using our own encoders.** A 6-second 960×540 render takes 1.3 seconds and decodes in ffmpeg without a single warning, with strict error detection on.
- **Also:** Apple ProRes 422 HQ and Motion JPEG in QuickTime, MXF OP1a and Avid-style OP-Atom (DNxHR, ProRes or H.264 with PCM and start timecode), PNG sequences, animated GIF and WAV.
- **Also:** Apple ProRes 422 HQ and Motion JPEG in QuickTime, MXF OP1a and standard OP-Atom (DNxHR, ProRes or H.264 with PCM and start timecode), PNG sequences, animated GIF and WAV.
- **Background jobs** with progress and cancel, so you keep editing while it renders.
- **Render previews:** the render bar marks segments green, yellow or red; rendered previews are cached by content, so an edit only invalidates what it touches and undo brings the green back.
Move timelines between FilmCraft and every other editor:
- **Final Cut Pro 7 XML (xmeml):** the format Premiere and DaVinci Resolve exchange. It carries nested sequences, transitions, generators, Motion, Opacity, Time Remap and audio levels, with keyframes.
- **FCP 7 XML (xmeml):** industry-standard timeline interchange format. It carries nested sequences, transitions, generators, Motion, Opacity, Time Remap and audio levels, with keyframes.
- **FCPXML 1.9–1.11:** the spine, connected clips as lanes, transitions, retiming and compound clips.
- **OpenTimelineIO:** the open interchange format of the film industry, round-tripping every FilmCraft detail through `metadata.filmcraft`.
- **CMX 3600 EDL:** the oldest format still in use, with drop-frame timecode, dissolves, wipes, speed changes (`M2`) and one EDL per track.
- **AAF (Edit Protocol):** for Avid Media Composer and Pro Tools. Video and audio tracks, dissolves and dips, clip volume with keyframes, markers and source timecode; audio embedded or as separate WAV / AIFF files, trimmed with handles, with clip effects rendered in and broken out to mono, plus an optional video mixdown. Import reads AAF back, extracting embedded audio.
- **AAF (Edit Protocol):** standard timeline and audio interchange format. Video and audio tracks, dissolves and dips, clip volume with keyframes, markers and source timecode; audio embedded or as separate WAV / AIFF files, trimmed with handles, with clip effects rendered in and broken out to mono, plus an optional video mixdown. Import reads AAF back, extracting embedded audio.
- **OMF 2.0:** the audio-post handoff: the audio tracks with crossfades and gain, sample-accurate, with the audio encapsulated or alongside.
Import merges the document's bins, media and sequences into your project as one undoable step, and links each media file it finds on disk.
@@ -221,7 +202,7 @@ Import merges the document's bins, media and sequences into your project as one
## Built for agents
Every menu item, button, slider and drag in FilmCraft is a **command** with an id, typed parameters and an enabled state. There are more than 650 engine commands (`filmcraft-cli commands` lists them), with the rest of Premiere's catalogue on the way. The UI, the CLI, a JSON control channel and an **MCP server** all dispatch the same commands, so Claude or any agent can cut, trim, grade, mix and export exactly the way a person does. The UI can also be driven at the level of mouse and keyboard: every widget has an automation id, and agents can click, drag, type and take screenshots.
Every menu item, button, slider and drag in FilmCraft is a **command** with an id, typed parameters and an enabled state. There are more than 650 engine commands (`filmcraft-cli commands` lists them), with more commands continuously added. The UI, the CLI, a JSON control channel and an **MCP server** all dispatch the same commands, so Claude or any agent can cut, trim, grade, mix and export exactly the way a person does. The UI can also be driven at the level of mouse and keyboard: every widget has an automation id, and agents can click, drag, type and take screenshots.
```jsonc
// over the control channel (JSON lines on TCP) or as MCP tool calls
CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo run --release -p filmcraft
```
The control protocol is documented in [docs/control-protocol.md](docs/control-protocol.md). Stuck, or want to show what you made? Ask in [Discord](https://discord.gg/artcraft).
The control protocol is documented in [docs/control-protocol.md](docs/control-protocol.md).
## Documentation
@@ -275,28 +256,28 @@ The control protocol is documented in [docs/control-protocol.md](docs/control-pr
| [docs/project-files.md](docs/project-files.md) | `.fcproj` format, schema migrations, auto-save and crash recovery |
| [docs/graphics.md](docs/graphics.md) · [docs/captions.md](docs/captions.md) | Text engine, graphic clips and tools; caption tracks and formats |
| [ROADMAP.md](ROADMAP.md) | Stage, parity numbers, languages, what's next and the progress log |
| [docs/target-app-parity.md](docs/target-app-parity.md) · [docs/gaps.md](docs/gaps.md) · [docs/roadmap.md](docs/roadmap.md) | Parity assessment against Premiere Pro, the ranked list of shortfalls, milestones and current focus |
| [docs/target-app-parity.md](docs/target-app-parity.md) · [docs/gaps.md](docs/gaps.md) · [docs/roadmap.md](docs/roadmap.md) | Parity assessment, the ranked list of shortfalls, milestones and current focus |
FilmCraft is young and moving fast. Editing, trimming, multicam, colour, keyframes, effects, titles, captions, mixing, codecs and export work today.
FilmCraft is in **alpha**. We track two numbers ([docs/target-app-parity.md](docs/target-app-parity.md), re-measured 2026-10-10 against Premiere Pro 26.5.2):
FilmCraft is in **alpha**. We track two numbers ([docs/target-app-parity.md](docs/target-app-parity.md), re-measured 2026-10-10):
- **Feature breadth: ~86%.** Premiere Pro's menu items, effects, transitions, panels and formats that exist in FilmCraft (menus 92% and effects 100% measured).
- **Ready for real work: ~56%.** Our honest estimate of how close FilmCraft is to replacing Premiere on real projects.
- **Feature breadth: ~86%.** Professional NLE menu items, effects, transitions, panels and formats that exist in FilmCraft (menus 92% and effects 100% measured).
- **Ready for real work: ~56%.** Our honest estimate of how close FilmCraft is to handling full production work on real projects.
The biggest gaps today:
- **Speed on big footage.** Hardware decoding works on macOS and Windows, and for H.264 and HEVC on Linux (VA-API, or NVDEC with NVIDIA's driver; VP9 and AV1 decode in software there). Blend modes and the most common effects run on the GPU, but Lumetri, keys and export rendering still run on the CPU; H.264 encoding can use the hardware encoder on macOS and on Windows and Linux with an NVIDIA GPU (opt-in, Export ▸ Hardware encoding), and H.265 export is hardware-only on both (HDR sequences export Main 10 PQ / HLG with NVENC; VideoToolbox writes 8-bit SDR) ([#30](https://github.com/storytold/filmcraft/issues/30)).
- **Premiere projects.** `.prproj` files can't be opened yet; bring sequences over as FCP7 XML, AAF or OTIO.
- **Speed on big footage.** Hardware decoding works on macOS and Windows, and for H.264 and HEVC on Linux (VA-API, or NVDEC with NVIDIA's driver; VP9 and AV1 decode in software there). Blend modes and the most common effects run on the GPU, but color grading, keys and export rendering still run on the CPU; H.264 encoding can use the hardware encoder on macOS and on Windows and Linux with an NVIDIA GPU (opt-in, Export ▸ Hardware encoding), and H.265 export is hardware-only on both (HDR sequences export Main 10 PQ / HLG with NVENC; VideoToolbox writes 8-bit SDR) ([#30](https://github.com/storytold/filmcraft/issues/30)).
- **Native `.prproj` project import.** `.prproj` files can't be opened yet; bring sequences over as FCP7 XML, AAF or OTIO.
- **No plugins.** No VST3 / Audio Units or OpenFX hosting.
- **Delivery codecs.** H.264 is our only software delivery-codec export; H.265 exports only through a hardware encoder (NVIDIA on Windows: Main 8-bit SDR and Main 10 HDR PQ / HLG; VideoToolbox on macOS: 8-bit Main, SDR); no AV1 export yet.
- **Real-world media and platforms.** Our decoders are bit-exact on conformance streams, but camera and phone files in the wild are less tested. Windows and Linux get far less testing than macOS.
- **AI features.** Few so far; speech to text is optional and off by default.
Bug reports with real footage are the most useful thing you can send us: [open an issue](https://github.com/storytold/filmcraft/issues) or tell us in [Discord](https://discord.gg/artcraft).
Bug reports with real footage are the most useful thing you can send us: [open an issue](https://github.com/storytold/filmcraft/issues).
| Rendering and output | `render`, `gpu`, `export` |
@@ -316,8 +297,6 @@ Nothing below the front ends depends on a UI toolkit or OS API. `cargo xtask ci`
## Downloads
**New to FilmCraft?** Download it from the [FilmCraft page on getartcraft.com](https://getartcraft.com/apps/filmcraft). That's the easiest way to install it.
**Want a specific build or format?** On GitHub, the [latest release](https://github.com/storytold/filmcraft/releases/latest) has every build listed below, and [all releases](https://github.com/storytold/filmcraft/releases) has earlier versions and their notes. `<ver>` in the file names is the version number, and `SHA256SUMS.txt` lists a checksum for every file.
### Windows
@@ -345,6 +324,13 @@ Remove-Item Env:WGPU_BACKEND # restore the default for later
An explicit `gl` override can bring the driver crash back on affected systems. The macOS, Linux
and web backend defaults are unchanged.
A PC whose GPUs were installed from different driver packages of one vendor (a laptop with an
integrated and a discrete AMD GPU, for example) can list one GPU twice, once through each installed
version of the Vulkan driver, and a window drawn through the stale one crashes inside the driver
(`amdvlk64.dll`, exit code `0xc0000005`). FilmCraft draws with the newer one and says so in
`%APPDATA%\FilmCraft\Logs\filmcraft.log`; installing one driver package for every GPU removes the
duplicate. With one driver per GPU the adapter is the one wgpu picks, as before.
<a href="https://discord.gg/artcraft"><img alt="Join the ArtCraft community on Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
</p>
<h3 align="center">Come make things with us</h3>
<p align="center">
Our Discord is where artists of every kind hang out: people who paint, shoot, draw, cut film,
set type, and people still figuring out what they like to make. Share what you're working on,
ask for help, tell us what's broken, or tell us what you wish these tools could do.
Whatever your medium and however long you've been at it, you're welcome here.
@@ -446,22 +410,10 @@ with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md). Builds
**Footage and music in the screenshots:** NASA's Apollo 11 film and television footage from [images.nasa.gov](https://images.nasa.gov) (launch, Launch Control Center, lunar surface and recovery) and the Apollo 11 air-to-ground voice transcript, all US Government works in the public domain (NASA does not endorse this project); *Night of the Living Dead* (1968), *Carnival of Souls* (1962) and *Charade* (1963), all in the US public domain; *Earth Views from the ISS* by NASA; Chopin's Nocturne Op. 48 No. 1 and Ballade No. 1, performed for Musopen and released under CC0. The media itself is not in this repository. Sources and details for every asset are in [ATTRIBUTION.md](ATTRIBUTION.md).
FilmCraft is an independent implementation. It contains no Adobe code, icons, images, presets or LUTs, and no GPL or LGPL code; every icon is drawn in code and every asset is openly licensed and attributed ([AGENTS.md](AGENTS.md)), apart from the ArtCraft name and logos, which are trademarks of the ArtCraft Team used under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt). ffmpeg is used only as an external test oracle.
The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the
ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as
part of this repository and FilmCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt).
Forks and modified versions must remove them.
FilmCraft is an independent implementation. It contains no Adobe code, icons, images, presets or LUTs, and no GPL or LGPL code; every icon is drawn in code and every asset is openly licensed and attributed ([AGENTS.md](AGENTS.md)). ffmpeg is used only as an external test oracle.
<sub>Adobe, Photoshop, Illustrator, Premiere Pro, Lightroom, Acrobat, After Effects and InDesign are trademarks or registered trademarks of Adobe Inc. in the United States and/or other countries. FilmCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Adobe Inc.; these names are used only to describe the workflows it is compatible with.</sub>
<sub>Made by the <a href="https://getartcraft.com/">ArtCraft</a> team and community.</sub>
</p>
## Star history
[](https://www.star-history.com/?repos=storytold%2Ffilmcraft&type=date&legend=top-left)
**Stage: alpha** · next: beta, ~19 points of ready-for-real-work and ~400–700 h away, plus opening Premiere's `.prproj` ([why](docs/target-app-parity.md#stage); passes the [core-workflow gate](docs/roadmap.md#alpha-gate))
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (full re-measure against Premiere Pro 2026 26.5.2; scorecard, gaps, milestones and localization moved into docs/ per the craftrules progress-docs standard) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (language table: German covers the whole interface) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
Progress toward parity with Adobe Premiere Pro. This page is the summary; the detail is in
| Korean | partial (whole interface; needs an installed Hangul font; native review welcome) | ~100% |
| Japanese | partial (whole interface) | 99.7% |
| German | none | 0% |
| Korean | none | 0% |
| German | partial (whole interface; menus native-reviewed, the rest welcome) | ~100% |
| Vietnamese | none | 0% |
Also shipped: Ukrainian (menus only, 10%). Detail: [docs/localization-parity.md](docs/localization-parity.md).
@@ -106,6 +107,7 @@ Ranked; detail and "done when" in [docs/roadmap.md](docs/roadmap.md) and [docs/g
## Progress log
- **2026-10-12:** Transform's motion-blur shutter on adjustment layers uses the sequence frame rate instead of 24 fps, so a 360° shutter in a 60 fps sequence spans one frame, not 2.5 (#1088)
- **2026-10-10 (changed media):** Settings ▸ Media ▸ Automatically refresh growing files works: media files that change on disk while the project is open (a recording still being written, a render written over the file a clip uses) are read again every *Refresh growing files every* seconds and as soon as the window comes back to the front: the new length, picture and identity, without an undo step (`media.refreshChanged`, `media_watch`).
- **2026-10-10 (re-measure):** full parity re-measure against Premiere Pro 26.5.2: menu label diff 338 / 366 (92%), installed bundle listings (10 UI languages, exporter and codec frameworks), 188 open issues. Breadth ~86% (file formats now counted), ready ~55%, stage **alpha**; progress docs reorganized per the craftrules standard ([target-app-parity](docs/target-app-parity.md), [gaps](docs/gaps.md), [roadmap](docs/roadmap.md), [codec](docs/codec-parity.md), [file-format](docs/file-format-parity.md), [hardware](docs/hardware-parity.md), [UI](docs/ui-parity.md), [localization](docs/localization-parity.md)).
- **2026-10-10:** ~75 community PRs merged: GPU export rendering (Auto) with Vignette, Video Limiter and Lumetri basic / creative / vignette on the GPU (#421); Linux NVDEC H.264 / HEVC decode (#549); text-to-speech narrations with Kokoro-82M (#278); Japanese for the whole interface (#449) and Ukrainian menus (#407); ALSA default-device fallback (#254, fixes #23); drop import into the timeline (#517); system appearance (#505); timecode scrub editing (#576); NVENC RGBA input (#486); marker CSV export (#454); many fixes (#526–#575).
@@ -164,6 +166,9 @@ Ranked; detail and "done when" in [docs/roadmap.md](docs/roadmap.md) and [docs/g
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | minor | Language table: German covers the whole interface (~100%) |
| 2026-10-11 | minor | Language table: German ships the menus (#630, 10.6%) |
| 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% per audience |
| 2026-10-10 | minor | Headline adds mainstream practitioner (~55%) and essentials user (~60%); ready for real work ~55% -> ~56% (formula written out, same inputs); beta distance ~19 points |
| 2026-10-10 | minor | Stage banner links the core-workflow gate (all six workflows pass on macOS; stays alpha) |
log::info!("graphics mode {} chosen with --graphics",mode.name());
Saved{mode,failures: 0}
}
None=>{
ifdecision.moved_on{
letfailed=saved.map_or(first,|s|s.mode).name();
log::warn!(
"the last {FAILURES_TO_MOVE_ON} starts with graphics mode {failed} died before their window painted; starting with {} (`--graphics default` or the notice in the window goes back)",
decision.saved.mode.name()
);
}elseifdecision.saved.failures>0{
log::warn!("the last start with graphics mode {} died before its window painted",decision.saved.mode.name());
title: FilmCraft app icon, canonical vector (owl with ear tufts)
author: Brandon Thomas (FilmCraft owner), for the FilmCraft project
source: original work (drawn by the owner in ArtCraft, then vectorised)
source: original work (drawn by the owner by hand, then vectorised)
license: MIT OR Apache-2.0
license-file: assets/app-icon/LICENSE.txt
added: 2026-10-02 by Claude (agent) for Brandon Thomas
notes: SVG traced at 2048 px with craftrules assets/logo-options/_tools/vectorize_tile.py from the owner's ArtCraft drawing (craftrules assets/app-icons/filmcraft/source.png). Three colours: ink #0b0b0c, paper #efe9dc, app colour #8b5cf6; 512 viewBox, rx=112. See assets/app-icon/README.md.
notes: SVG traced at 2048 px with craftrules assets/logo-options/_tools/vectorize_tile.py from the owner's drawing (craftrules assets/app-icons/filmcraft/source.png). Three colours: ink #0b0b0c, paper #efe9dc, app colour #8b5cf6; 512 viewBox, rx=112. See assets/app-icon/README.md.
and `b10-23-film_grain-50` are **bit-exact** in the default run. The ignored
@@ -58,8 +58,8 @@ and `b10-23-film_grain-50` are **bit-exact** in the default run. The ignored
with scaled inter-layer prediction), `24-monochrome` (8 and 10-bit) and
`16-intra_only-intrabc-extreme-dv` (1080p intra block copy), `23-film_grain-50` (8 and 10-bit).
`tests/oracle_inter.rs` also checks draft mode: every unflagged frame stays bit-exact with
libdav1d and only flagged frames differ, with 1 and 4 threads. `tests/oracle_inter.rs` encodes SVT-AV1 GOPs (hierarchical references, compound prediction,
`tests/it/oracle_inter.rs` also checks draft mode: every unflagged frame stays bit-exact with
libdav1d and only flagged frames differ, with 1 and 4 threads. `tests/it/oracle_inter.rs` encodes SVT-AV1 GOPs (hierarchical references, compound prediction,
OBMC / warped motion, motion-field projection, all loop filters; presets 3–8, 8- and 10-bit, odd
sizes), plus super-resolution (denominator 12) and film-grain streams, and compares every frame
with libdav1d: **bit-exact** on all 6 fixtures.
@@ -70,16 +70,19 @@ with libdav1d: **bit-exact** on all 6 fixtures.
wasm32, or without the default `threads` feature, everything runs on the calling thread). Headers are parsed in order; each frame is
then decoded by a frame worker once the reference frames it reads are finished, so independent
frames (hierarchical GOP layers) decode concurrently; tiles of a frame decode in parallel into
private buffers that are merged afterwards. Results never depend on scheduling (the oracle tests
compare 1 thread, many threads and libdav1d). With frame threads, pictures may be returned by a
private buffers that are merged afterwards; the in-loop filters (deblocking, CDEF, loop
restoration) each run in parallel bands of 64 rows (`postfilter.rs` explains why the bands are
independent). Results never depend on scheduling (the oracle tests compare 1 thread, many threads
and libdav1d). With frame threads, pictures may be returned by a
later `decode_pts` call or by `flush`; each carries the `pts` of the temporal unit that showed it.
`tests/perf.rs` (ignored) prints single-threaded fps (thread CPU time), per-stage ms/frame
(`Decoder::stats()`) and all-core fps for 1080p fixtures. The hot loops (sub-pixel filters,
`tests/it/perf.rs` (ignored) prints single-threaded fps (thread CPU time), per-stage ms/frame
(`Decoder::stats()`) and all-core fps with its per-stage ms/frame for 1080p fixtures (key frames,
GOPs, tiles, low delay) and a single-tile 2160p GOP; `AV1_PERF_ONLY=a,b` picks fixtures. The hot loops (sub-pixel filters,
CDEF, inverse transforms, deblocking, entropy decoding) are written for auto-vectorisation.
Measured 2026-10-01 on a 14-core Apple Silicon Mac (release build, SVT-AV1 preset 8 1080p
fixtures from `tests/perf.rs`), before (`31f8b3d`: hot loops only, no threading) and after tile / frame /
fixtures from `tests/it/perf.rs`), before (`31f8b3d`: hot loops only, no threading) and after tile / frame /
post-filter threading. The machine was heavily loaded by other builds during both runs (load
average 200–400), so the all-core wall figures understate what an idle machine reaches and are
noisy; single-threaded figures use thread CPU time and are comparable.
@@ -122,6 +125,13 @@ motion vectors nor CDFs reach another frame — skip deblocking, CDEF and loop r
(super-resolution still runs, it sets the output size) and are flagged `Picture::draft`. In
SVT-AV1's hierarchical GOPs that is the top layer, half of the frames.
## Limits
Frames beyond the AV1 level 6.x limits (Annex A: at most 16384 wide, 8704 high and 35 651 584 luma samples
in total, so 16384 x 2176 or 8192 x 4352 at most) are refused with `Error::Invalid("frame size")`. The sequence
header can code up to 65536 x 65536, which would ask for gigabytes of buffers from a few bytes of hostile input;
no real stream comes close to level 6.3, the highest defined level.
description="Clean-room AVI (RIFF) and OpenDML demuxer"
version.workspace=true
edition.workspace=true
license.workspace=true
rust-version.workspace=true
[lints]
workspace=true
[dependencies]
[dev-dependencies]
filmcraft-testkit={workspace=true}
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.