The merged Japanese interface (#38) looked only for a system font. When FilmCraft is built with craft-fonts, theme::install already appends the Japanese faces to every font family, so switching to Japanese uses them (no system scan) and works on the web too; otherwise it falls back to an installed system font as before. Docs use an absolute CRAFT_FONTS_DIR (build.rs runs in crates/text, so a relative path resolves from there). Co-Authored-By: Claude Opus 5.5 <[email protected]>
14 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)
FILMCRAFT_CONTROL_PORT=9876 works like --control 9876, and FILMCRAFT_CPU_COMPOSITE=1
disables the GPU compositor.
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.
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.
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). - 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.