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.
18 KiB
Contributing
Read AGENTS.md first. Its rules on assets, clean-room code and licences are absolute and override everything here. Then read architecture.md and the README of the crate you will change.
1. Setup
| All platforms | |
|---|---|
| Rust | stable ≥ 1.95 (edition 2024; egui 0.36 requires it), via rustup |
| wasm target | rustup target add wasm32-unknown-unknown (for cargo xtask wasm) |
| Components | rustup component add rustfmt clippy |
| ffmpeg + ffprobe (optional) | test oracle and fixture generator only; tests skip without it |
| OS | Extra |
|---|---|
| macOS | Xcode Command Line Tools. ffmpeg: brew install ffmpeg (includes libx264/libx265). |
| Linux | C toolchain and the system headers cpal and rfd need: ALSA (libasound2-dev) and GTK 3 (libgtk-3-dev), plus the usual X11/Wayland development packages for winit. ffmpeg from your distribution, built with libx264 and libx265. |
| Windows | MSVC build tools. The oracle tests look for ffmpeg only in /opt/homebrew/bin, /usr/local/bin and /usr/bin, so most of them skip on Windows. |
The oracle tests find ffmpeg at /opt/homebrew/bin, /usr/local/bin or /usr/bin. H.264 and
HEVC fixtures need an ffmpeg built with libx264 and libx265. The VideoToolbox fixtures run only on
macOS.
2. Build and run
cargo run --release -p filmcraft # desktop app with the demo project
cargo run --release -p filmcraft -- --empty # start without the demo project
cargo run --release -p filmcraft -- --control 9876 # plus the JSON-lines control server
cargo run --release -p filmcraft -- a.mp4 b.wav # import media (or open a .fcproj)
cargo run --release -p filmcraft-cli -- commands # list engine commands
cargo run --release -p filmcraft-cli -- render --demo --seconds 3 --out frame.png
cargo run --release -p filmcraft-cli -- mcp --demo # headless MCP server on stdio
cargo xtask web --serve 8765 # the web app on http://127.0.0.1:8765/ (docs/web.md)
| Variable | Effect |
|---|---|
FILMCRAFT_CONTROL_PORT |
Same as --control <port> |
FILMCRAFT_CPU_COMPOSITE=1 |
Disables the GPU compositor |
FILMCRAFT_DEV_DESKTOP_ENTRY=1 |
Linux, opt-in: a run from a source tree installs a user-level desktop entry (and the icons from assets/app-icon) so the taskbar shows the FilmCraft icon. Nothing is written without it; packaged runs never write one |
FILMCRAFT_DATA_DIR |
Same as --data-dir <dir>: auto-save, crash recovery, settings and logs |
RUST_LOG |
Log levels for standard error and the log file (see Logs) |
filmcraft --help lists the app's options. An option it does not know, or a control port that is
not a number (from --control or from FILMCRAFT_CONTROL_PORT), is an error on stderr with exit
code 2 instead of a window.
Dev builds compile dependencies at opt-level = 2 and workspace crates at opt-level = 1. For
playback and codec speed, use --release.
Logs
The desktop app writes its log records to standard error and to Logs/filmcraft.log in the data
directory (~/.local/share/filmcraft/Logs/ or $XDG_DATA_HOME/filmcraft/Logs/ on Linux,
~/Library/Application Support/FilmCraft/Logs/ on macOS, %APPDATA%\FilmCraft\Logs\ on Windows,
or under --data-dir / FILMCRAFT_DATA_DIR), next to the crash logs; Help ▸ Reveal Log Files opens
the folder. A start from a desktop menu or the Dock has no terminal, so this file is what to attach
to a bug report: GPU compositor fallbacks, audio output problems, panicked frame jobs and container
warnings all land there. Each launch moves the previous log to filmcraft.1.log (and that one to
filmcraft.2.log), so the log of a run that crashed survives the next start. The file stops growing
at 16 MiB. --help, --version and command-line errors write no file.
By default FilmCraft's own crates log at info and everything else at warn. RUST_LOG replaces
that with env_logger-style directives, for example RUST_LOG=debug,
RUST_LOG=warn,filmcraft_codecs=trace or RUST_LOG=info,wgpu_core=warn; a directive ending in *
covers every target starting with it (filmcraft*=debug). The logger is
apps/filmcraft/src/logging.rs; the realtime audio callbacks do not log. The web app logs to the
browser console instead.
Building with craft-fonts
Font assets live in storytold/craft-fonts, never in this repo (standard). It is an optional build input, not a Cargo dependency:
git clone https://github.com/storytold/craft-fonts ../craft-fonts
CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo run --release -p filmcraft
Use an absolute path: build.rs runs in crates/text, so a relative CRAFT_FONTS_DIR would
resolve from there. crates/text/build.rs reads its fonts/manifest.txt and embeds the fonts as
filmcraft_text::fonts::CRAFT_FONTS (native: all of them; the web build: only BIZ UDPGothic
Regular, to keep the wasm small). The Japanese ones become the last fallback of every UI font family
(crates/ui-egui/src/theme.rs, BIZ UDPGothic first) and fallback faces in the text engine (Gothic
for sans text, Mincho for serif text). Without CRAFT_FONTS_DIR, CRAFT_FONTS is empty and
FilmCraft uses its bundled and system fonts. A CRAFT_FONTS_DIR that is not a checkout is a build
warning, or an error with CRAFT_FONTS_REQUIRED=1 (release builds set both). Tests on these fonts'
glyphs skip when it is empty; when you touch fonts, run the gates both with and without it.
Chinese media names and track labels use the Hans / Hant craft-fonts faces after the Japanese
fallbacks in every UI font family, regardless of interface language (Japanese and Chinese share code
points, so Japanese text keeps Japanese glyph forms; the Chinese faces cover hanzi the Japanese ones lack). Without those build inputs,
native builds look up an installed Chinese face (for example Microsoft YaHei or Noto Sans CJK SC)
once and reuse it across theme changes. No system font files are bundled or copied into the app.
Web builds depend on the font faces selected by crates/text/build.rs; they cannot scan system fonts.
theme::tests::chinese_names_render_in_every_theme_and_font_family verifies Chinese glyphs in the
timeline's proportional, monospace, medium and semibold families under all three themes.
Arabic and Hebrew work the same way (crates/ui-egui/src/rtl.rs): the Arab / Hebr craft-fonts
faces, else one installed face per script (for example Noto Sans Arabic, SF Arabic or Segoe UI),
are the last fallbacks of every UI font family. egui shapes each font run (letters join) but lays
runs out left to right with no bidi pass, so captions on the timeline and in the Text panel are laid
out in reading order by rtl::Lines (UAX #9 via unicode-bidi, each right-to-left word its own layout
section). A caption being edited shows egui's logical order, since TextEdit places the caret by
character index.
3. Quality gates
Every commit must pass all of these:
| Gate | Command |
|---|---|
| Format | cargo fmt --check (rustfmt.toml: max_width = 160) |
| Lints | cargo clippy --workspace --all-targets -- -D warnings |
| Tests | cargo test --workspace |
| Layering | cargo xtask layers |
| Assets | cargo xtask assets |
| wasm | cargo xtask wasm (checks every L0–L4 crate, filmcraft-ui-egui and filmcraft-web for wasm32-unknown-unknown) |
| All of the above | cargo xtask ci (runs clippy and tests with --release) |
Run them with CRAFT_FONTS_DIR set too when you change font code (craft-fonts).
cargo xtask is an alias in .cargo/config.toml for cargo run -p xtask --.
Never crash
Production code must never crash: a crash loses someone's work, so this outranks feature work. The rules
are in AGENTS.md §0. In short: return Result<T, E> instead of unwrap() / expect() /
panic! / unreachable! / todo! / unimplemented!; no unsafe; use get(), checked_* /
saturating_* and validated bounds on anything that came from a file, a command parameter or the UI;
bound recursion; tolerate lock poisoning; catch panics in background threads and report them as errors.
Clean crates deny the panicking lints outside tests (clippy.toml allows them in tests). Every crash fix
comes with a regression test; parsers, decoders and commands get fuzz / hostile-input tests run under
catch_unwind.
4. Commits
- One task per commit. The subject starts with the task id:
M3.2: trim mode engine — …,M9.9: Matroska/WebM import. Milestones (M0–M16) are listed in ROADMAP.md. If your change has no task id, use the area:docs: …,README: …,ROADMAP: …. - Commit only green states (all gates pass).
- When a milestone lands, update its row in roadmap.md (status, what's done, estimates), the summary in
ROADMAP.md, and the parity doc and gaps.md entry it moves, with their "Last updated" lines and revision history. - AI agents end commit messages with the attribution line their environment requires.
5. Parallel work (several agents or worktrees)
git worktree add ../filmcraft-hevc -b hevc-work
cd ../filmcraft-hevc
export CARGO_TARGET_DIR=target/agent-hevc # separate build dir, no lock fights
- One owner per crate at a time.
- The workspace includes
crates/*andapps/*by glob, so one brokenCargo.tomlbreaks every build. Keep every manifest valid at all times, including half-finished crates. - Test fixtures always go to
<repo>/target/fixtures/, whateverCARGO_TARGET_DIRsays. .mcp.jsonpoints attarget/release/filmcraft-cli. With a custom target dir, use your own path.
6. How to add…
A command
- Add a
cmd!(action) orquery!(read-only) entry tobuild()incrates/engine/src/commands.rs:cmd!("sequence.addEdit", "Add Edit", ["Sequence"], Some("Cmd+K"), r#"{"time":ticks?}"#, has_seq, |s, p| { let t = time_p(s, p, "").unwrap_or(s.playhead()); let tg = s.targeting().targeted; let n = s.edit_sequence("Add Edit", |q, ctx, _| Ok(edit::razor(q, &tg, t, ctx)))?; Ok(json!({"cuts": n.len()})) }),- Id:
area.camelCase, following the menu it appears in. enabled: reuse a predicate (always,has_seq,has_selection,has_in_out, …) or write one that returnsErr("human reason").- Read time with
time_psotime,frame,secondsandtimecodeall work. - Mutate only through
s.edit(…)ors.edit_sequence(…), so the change is undoable and validated. - Put pure timeline logic in
filmcraft-editas a function on&mut Sequence, with unit or property tests there.
- Id:
- Add tests to
crates/engine/src/tests.rs: execute, assert, undo, redo, and check the disabled case. - Frontend-only actions (tools, zoom, panels) go in
UI_COMMANDSincrates/ui-egui/src/menus.rswith a branch inmenus::invoke. - The command now appears in menus (from
menu), shortcuts,filmcraft-cli commands,engine.commandsand MCPcommand_listwithout further work.
A video effect
- Define it in
build_effects()incrates/project/src/effect.rswith thevideo(id, name, CATEGORY, params)helper. Parameter helpers cover floats with slider ranges, choices, bools, angles, points, colours, curves and wheels. The Effects panel, Effect Controls and agent parameter docs are generated from this. - Implement it as a match arm in
render::effects::apply(crates/render/src/effects.rs). It works on a linear-light premultiplied f32Image. Scale pixel-size parameters bycx.px_scaleso reduced-resolution playback matches full resolution. Unknown ids are a no-op. - Add a test in
crates/render/src/tests.rs. - The GPU path needs no change:
render::planpre-renders layers with standard effects on the CPU. To run it on the GPU too, make it anFxOpincrates/render/src/gpufx.rsinstead (parameter evaluation inFxOp::eval, the CPU reference inFxOp::apply, the id inGPU_EFFECTS), add its passes tocrates/gpu/src/fx.rsand its math tofx.wgsl, and a parity case tocrates/gpu/src/fx_tests.rs.
Audio effects implement AudioEffect in crates/audio-dsp and register in its effects()
registry. Then add an audio(…) definition in effect.rs and a Mapping from project parameters
to DSP parameters in crates/render/src/audio_fx.rs. The DSP must give the same output however the
stream is cut into blocks (there are tests for this).
A transition
- Define video transitions with
tr(id, name, FOLDER, badges, params)incrates/project/src/vtransition.rs(folders follow Premiere 26: tenVideo Transitions/*folders plusLegacy/Video Transitions). Parameters are not animatable. Never rename an id: projects store it. Audio transitions areEffectKind::AudioTransitiondefs ineffect.rs. - Implement it in the matching module of
crates/render/src/transitions/(wipe,motion,dissolve,lights,grunge,special): read params throughTx, build pixels withpaint, shape wipes withwipe::field_wipe(feather/border/anti-aliasing for free) and 3D moves withcards. Audio crossfade curves go intransitions::audio_gains. The property tests run over every transition automatically; re-blessgoldens.txt(see testing.md). - It can then be applied with
sequence.applyVideoTransition {"effect": "<id or name>", "params": {...}, "reverse": bool}and edited withsequence.setTransition;effects.list {"folder": "Video Transitions/Wipe", "detail": true}lists it with its parameters.
A codec or container crate
- Spec first. Work only from the public specification (ITU-T, ISO/IEC, IETF RFCs, SMPTE, published container specs). Never copy GPL/LGPL code, and follow AGENTS.md §2 on reading other implementations. Name the spec edition in the crate README.
- Create
crates/<name>as an L0 crate that depends only onfilmcraft-bitstream(plusthiserrorand optionalrayonbehind athreadsfeature). Add[lints] workspace = true, add it to[workspace.dependencies], and add it toLAYERSinxtask/src/main.rs. It must build for wasm32. - Test it against ffmpeg as an oracle (see testing.md):
- generate fixtures with ffmpeg on first use into
target/fixtures/<crate>/; - skip with a message when ffmpeg is missing;
- never commit media.
- generate fixtures with ffmpeg on first use into
- Wire it in:
- decoders: a
VideoDecoderFactoryincrates/codecs(video.rs, default list inlib.rs); - containers: an
Openerincodecs::openers(); - encoders: an
export::VideoEncoder/ audio encoder plus aFormatincrates/export.
- decoders: a
- Write the README: features, API, test matrix, accuracy and speed numbers, limitations. Use
crates/h264/README.mdas the model.
A panel or widget
- Panels live in
crates/ui-egui/src/panels/and are listed inPanelKind(crates/ui-egui/src/dock.rs). - Every interactive element registers an automation id each frame:
Ids are stable, dot-separated and start with the panel (
app.auto.add(&format!("effects.item.{}", d.id), rect, d.name);timeline.track.V1.lock,tools.Razor). - UI state that should survive or be scriptable goes in
crates/ui-egui/src/state.rs(serde), soui.inspectandui.setcan reach it. - Project changes go through
session.execute(…), never by mutating the project directly. - Icons are drawn in code in
crates/ui-egui/src/icons.rs, from scratch (AGENTS.md §1). - Look at the result: run with
--control, drive it, takeui.screenshot(agents.md).
An asset
The rules are in AGENTS.md §1. In short:
-
No Adobe iconography, images, fonts, LUTs, presets or other Adobe assets, in any form, including traced or "inspired-by" copies.
-
Open licences only (MIT, Apache-2.0, BSD, ISC, zlib, OFL, CC0, CC BY, CC BY-SA, or your own work under MIT OR Apache-2.0). No NC or ND licences. If the licence is unclear, leave the asset out.
-
Screenshots show only FilmCraft (or other open projects) with media we generated or that is openly licensed.
-
Fonts live in storytold/craft-fonts, not here. Don't commit new font files to this repo (the Latin UI fonts already in
assets/fonts/stay): add the font to craft-fonts (file, licence, manifest line,ATTRIBUTION.mdrow) and use it throughfilmcraft_text::fonts::CRAFT_FONTS. See Building with craft-fonts andcraftrules/standards/fonts.md.
For each asset file X, commit a sidecar X.attribution. This is the real one for
assets/fonts/Inter-Regular.ttf:
asset: Inter-Regular.ttf
title: Inter Regular (UI typeface)
author: The Inter Project Authors (Rasmus Andersson et al.)
source: https://github.com/rsms/inter
license: OFL-1.1
license-file: assets/fonts/OFL-Inter.txt
added: 2026-09-30 by Claude (agent) for Brandon Thomas
notes: Unmodified font file, embedded in the UI via include_bytes! (crates/ui-egui/src/theme.rs).
For your own work, use source: original work and the project licence (MIT OR Apache-2.0).
Then add a row to ATTRIBUTION.md with the path in backticks. cargo xtask assets checks every tracked file with a media extension (images, fonts, LUTs, audio, video, PDF,
project files…):
- the sidecar exists;
asset,title,author,source,licenseandaddedare filled in;- the licence is not NC, ND, Adobe or proprietary;
- the path appears in
ATTRIBUTION.md; - the path does not mention Adobe or Premiere;
- no sidecar is orphaned.