- file.projectManager {destination, mode: collect|consolidate, sequences?,
excludeUnused, handles, preset, includeProxies, includePreviews, dryRun, wait}.
- Collect copies every file the chosen sequences use (nested sequences and subclips
followed), optionally proxies and render previews; consolidate writes only the used
media range plus handles with a transcode preset and re-bases media time so edits,
keyframes and markers stay put. Unused clips can be excluded.
- Dry run reports original / resulting disk space and the file plan; the copy runs
as a job and writes the project last with the new paths and fingerprints.
- UI: Project Manager dialog (sequences, mode, preset, options, destination,
Calculate → sizes) with automation ids pm.*.
- Tests: collected project opens with its media moved away and renders the same
frames; consolidate trims to used range + handles and matches (>38 dB) the original.
- ROADMAP: M11 done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- media.createProxies: background job (jobs.list progress) transcoding with our own
encoders — ProRes 422 Proxy ¼/½, ProRes LT ½, H.264 ¼/½ (audio included) — into
<media dir>/Proxies or a chosen folder; attached when the job finishes
(Session::poll_persistence applies finished media jobs).
- media.attachProxies (duration ±1 frame and frame rate must match; size/aspect may
differ), media.detachProxies, media.reconnectFullRes, media.toggleProxies
(Preferences ▸ Media ▸ enableProxies; bumps the view revision without dirtying the
project), media.proxyPresets.
- Proxy frames are smaller; ProxySource reports the original size and the compositor
derives its pixel scale from the frame, so Motion and effects render the same
picture (44 dB PSNR vs full res with Motion + Gaussian Blur, 48 dB at ½).
Export always renders full resolution.
- ExportSettings::prores_profile (proxy / lt / standard / hq).
- project.ingestSettings + file.import: copy (fingerprint-verified), transcode,
create proxies, copy and create proxies.
- UI: Toggle Proxies button on both monitors (lit when on), Create Proxies dialog,
Project panel Proxy submenu and P badges.
- Measured (one core, 4K H.264 testsrc2): CPU composite 201 → 35 ms/frame at ¼ with
ProRes Proxy ¼, 277 → 131 ms at ½ with H.264 ¼; decode 9.2 → 0.7 ms with H.264 ¼.
- Tests: create+attach+toggle same composite modulo scale, attach checks, export
ignores proxies, ingest copy + proxies, ignored 4K benchmark, headless UI test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- render::color_match: non-ML statistics matching — Oklab means per tonal range (smoothstep
shadow/midtone/highlight weights) plus mean chroma; "Face Detection" down-weights skin-tone
pixels (Oklab hue 20–75°) in the colour statistics and then limits the skin hue shift to 8°;
the grade is solved in Lumetri's own parameters (3 wheels, their lightness, saturation) by
damped Gauss–Newton through the real Lumetri implementation, so the result stays editable
- render::render_clip: one clip's layer at a time (the shot as Lumetri sees it)
- lumetri.applyMatch {clip?, referenceTime|Frame|Timecode, faceDetection} sets the grade in one
undo step and reports the statistics distance before/after; refuses a reference inside the
clip itself
- Lumetri panel: Color Match row (reference timecode, Face Detection, Apply Match; automation
ids lumetri.match.*)
- tests: Oklab reference values, self-match is a no-op, a known grade is recovered (residual
−86 %), skin protection holds the skin hue shift to ≤ 8° (51° without), engine and headless
UI tests
- docs: control protocol (colour commands and ids), testing; ROADMAP M8 row and log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Missing media no longer fails: opening a project scans for missing files
(`media.findMissing`, `file.open` → missingMedia). Missing clips render our own
offline slate (render::offline: red striped field, warning triangle, "MEDIA NOT
FOUND", file name, hint) with silent audio, and the Link Media dialog opens.
- Media identity: imports record the size and a fast content fingerprint (hash of
size + first/last MiB) in MediaClip::identity (optional field, older builds
ignore it, no schema bump).
- media.relink checks file name / extension / fingerprint ("Clip ID") / duration /
media start / frame size+rate+channels, refuses a mismatched fingerprint unless
forced, supports Align Timecode (Project::shift_media_time), and relinks the
others automatically by the derived folder remap (Windows/macOS separators).
- media.autoRelink {from,to} | {folder}, media.search, media.replaceFootage,
media.makeOffline {deleteFiles?}, media.offlineAll, media.status, media.linkMedia.
- Media pool caches sources per reference (path, offline flag), so relink, Make
Offline and undo take effect at once; proxy-aware resolution and a full-res
provider that export uses.
- codecs::gop no longer holds its lock while decoding: rayon decoders inside a
parallel export could steal another frame of the same source and deadlock.
- UI: Link Media dialog (rows, match options, folder search with preview, Locate…,
Link, Offline, Offline All, Cancel), Make Offline dialog, Project panel offline
markers and context menu, media thumbnails keyed by media reference.
- Tests: moved media opens offline (slate rendered) and relinks others by folder,
prefix remap and folder search, fingerprint mismatch refused, Make Offline,
headless UI test of the dialog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- model: SequenceSettings::color (ColorPipeline: working space, wide gamut, Auto Tone Map Media;
serde default Rec. 709) and Interpretation::color_space (override; None = metadata)
- render::colorman: source colour space from the override or VUI/colr/MKV metadata, decode tables
(camera log with super-whites, PQ, HLG + OOTF), gamut matrix, tone mapping into SDR working
spaces, gamut mapping; working → SDR monitor transform at the top level
(RenderOptions::working_output keeps working values for HDR export and scopes). Plain Rec. 709
media in a Rec. 709 sequence keeps the old fast path and stays on the GPU; managed layers and
HDR/wide-gamut sequences composite on the CPU
- export: H.264 and ProRes of a PQ/HLG sequence encode BT.2020 PQ/HLG and signal it: VUI / ProRes
header, colr (nclx/nclc), and for PQ mdcv + clli boxes and H.264 SEI (payloadTypes 137/144);
`sdr: true` (and render previews) export tone-mapped SDR. isobmff reads/writes mdcv and clli
- commands: sequence.colorSettings (Sequence ▸ Color Management…), clip.interpretFootage
(Clip ▸ Modify ▸ Interpret Footage…, Project panel context menu), color.spaces,
media.colorInfo; dialogs with automation ids colorDialog.*
- scopes: HDR sequences show a cd/m² waveform on a PQ scale (203 marked) and a BT.2020
vectorscope from the working-space render (scopes.hdrWaveform)
- display-referred wide-gamut input is desaturated into gamut keeping in-gamut colours exactly;
the soft compression is reserved for camera gamuts
- tests: colorman unit tests, engine (interpret/undo, PQ working space values and display),
goldens log_to_rec709 and hdr_tone_map (own renders + sidecars), export PQ/HLG × ProRes/H.264
read back by our demuxer and ffprobe (color_transfer/primaries/space, mastering display and
content light side data), headless UI (dialogs, HDR scopes)
- docs: architecture §5.2 (pipeline, HDR export, display colour management: no macOS EDR yet)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- crates/ui-egui/src/panels/essential_sound.rs: per-type sections (Loudness with Auto-Match,
Repair, Clarity, Creative, Ducking with Generate Keyframes, Duration note, Pan), preset dropdown
with save/delete, code-drawn type glyphs, automation ids for every control; every control runs an
essentialSound.* command and a slider drag is one undo step
- headless kittest test (essential_sound_ui.rs) with optional wgpu snapshots
- docs: architecture §5.2, control-protocol ids, testing, ROADMAP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Lumetri gains input_lut (Basic Correction) and look_lut (Creative) references: lib:<id> for
LUTs imported into the project's library (embedded .cube/.3dl text, parsed once and cached by
content), builtin:<id> for LUTs generated from code: camera log → Rec. 709 conversions from the
published curves, and the procedural Creative looks (no third-party LUTs)
- section switches basic_on/creative_on/curves_on/wheels_on/vignette_on (+ hsl_on) bypass their
sections; older projects without them render as before
- commands lut.import, lut.list, lut.remove, lut.export (cube/3dl), lumetri.setInputLut,
lumetri.setLook (lut ref or path; imports on the fly), lumetri.setSection; effects.setParam
fills in parameters missing from instances saved by older builds
- Lumetri panel: Input LUT and Look LUT menus (None, built-ins, library, Browse… file dialog),
working section switches (automation ids lumetri.input_lut, lumetri.look_lut,
lumetri.switch.<section>); combo boxes no longer widen the panel
- filmcraft-gpu: GpuLut WGSL compute pass with the same tetrahedral interpolation; parity test
vs Lut3d::apply for 2³–65³ and a non-unit domain (max |Δ| 1.8e-7)
- tests: engine (import/apply/bypass/undo/save, export→import round trips for both formats),
render (built-ins reproduce the looks, library resolution), headless UI (color_ui.rs)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- project::essential: Dialogue/Music/SFX/Ambience settings (Loudness, Repair, Clarity, Creative,
Ducking, Pan, Clip Volume, Mute), our own EQ/reverb/type presets, and apply(): settings become
ordinary clip effects (EffectInstance::essential) in canonical order; only changed parameters
are written, so keyframes survive; auto-match gain → clip gain, Clip Volume → Volume offset,
Pan → Panner
- effect defs: DeEsser, DeReverb (params), Enhance Speech, Stereo Width; render mappings for them,
and DeNoise amount / DeHummer gain now reach the DSP; Mute; render::audio::clip_signal
- engine essentialSound.* (inspect, setType, clearType, set, applyPreset, savePreset,
deletePreset, autoMatch, generateDucking), undoable; per-type loudness targets and user presets
in preferences
- tests: auto-match within 0.001 LU of target (−23 / −16, with and without a chain), ducking
keyframes within 20 ms on a synthetic dialogue+music project and −15.00 dB in the mix, repair
metrics (hum −39 dB, rumble −22 dB, noise floor −13 dB, sibilance −19 dB, reverb tail −10 dB),
render chunking bit-identical and WAV export = render; full Dialogue chain 22× realtime (release)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Play waits for the first PREROLL_FRAMES (6) frames, at most 0.5 s, before starting the clock
and audio: the first frames after Play were due before any decoder could produce them.
- A new on-screen request for a view drops the queued and cancels the running request it
replaces (scrubbing used to decode every visited position oldest-first).
- playback_plan: when per-frame CPU work (effects; decode excluded, it is sequential anyway)
exceeds what the workers can do in real time, start only frames that can still be on time and
space them evenly, instead of every frame arriving late. Cost estimate updated from finished
and (as a lower bound) cancelled prefetch jobs. No change while the workers keep up.
- PlaybackMeter::resync: frames while the window is hidden (occluded/minimized) are not drops.
- Scopes render after the Program prefetch while playing (prio 40, was 2).
- ui.inspect playback.preroll; docs/architecture.md frame scheduling.
bench, same-session A/B, median of 3 (GPU path, Full, 8 s, load avg 22–64):
h264-1080 190/2 → 192/0 shown/dropped (3/3 runs 0 dropped)
stack3 189/3 → 192/0; h264-2160 173/19 → 192/0; demo 186/6 → 189/3
seek-storm: 10 → 15 of 40 seeks shown within 150 ms, 1581 → 1300 ms CPU per seek
after-preview live effects play (median of 2): 118/74 → 134/58, cancelled jobs 74 → 48;
plays from previews 192/0 both
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- clip.audioGain takes mode set|adjust|normalizeMax|normalizeAll (the
old {db, relative} form still works); peaks are measured on the
clips' source ranges. Normalize Max Peak applies one adjustment so the
loudest clip lands on the target; Normalize All Peaks sets each clip.
- clip.audioPeak reports the selection's peak amplitude (with gain).
- Clip > Audio Gain... (G) opens the dialog with the four options, dB
fields and the peak amplitude; automation ids audioGain.*.
- effects.setDefaultTransition sets the default video or audio
transition (Constant Power / Constant Gain / Exponential Fade) that
sequence.applyAudioTransition (Cmd+Shift+D) uses.
- tests: gain modes and peaks, dialog by automation id, transition
curves (power / amplitude preservation, end points).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Audio Track Mixer: strips for audio tracks, submixes and the Mix with
effect/send slots (disclosure), input mapping, output assignment, pan
knob, automation mode, M/S/R (solo safe on submixes), fader with dB
scale and our own taper, stereo meters fed by the playing mix, value
hot text, footer transport; horizontal/vertical scrolling. Fader and
knob drags are mixer.touch while held and mixer.release on let go, so
they are heard live and recorded in Latch/Touch/Write.
- Audio Clip Mixer: per-track strips for the clip under the playhead
(clip Volume / Panner, write-keyframes toggle), clipMixer.set with one
undo step per drag (Session::edit_merged).
- Timeline: per-track "Show Keyframes" button picks clip keyframes or a
track lane (volume, panner, mute, sends, insert params); the lane is
drawn across the track, Pen / Cmd-click adds, diamonds drag in time and
value, right-click deletes.
- Playback starts an automation pass and stopping writes it; the audio
callback reads the newest project snapshot (LiveMix), so mixer moves
are heard while playing. Audio Meters read the graph's live meters
instead of re-mixing at the playhead.
- Automation ids on every control; headless kittest tests drive the
panels by id and by multi-frame drags (FILMCRAFT_UI_SNAPSHOT_DIR renders
offscreen PNGs through wgpu).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GpuCompositor::upload converted f32 RGBA layers (effects rendered on the CPU, captions,
whole-frame fallbacks) and 16-bit YUV frames (ProRes render previews) to half floats on the UI
thread every time a frame was shown: 8.3 M values per 1080p RGBA layer, 4 M per ProRes frame.
filmcraft_gpu::prepare(plan) now does those conversions on the frame worker that built the plan
(parallel chunks; 16-bit codes through a cached per-bit-depth table) and the plan cache stores
them with the plan (frames::GpuPlan), bounded by count and bytes (1 GiB). composite_prepared()
only copies bytes into textures. The texels are bit-identical (new test compares the read-back
output of both paths for f32, 10-bit 4:2:2 and fallback-image plans).
bench, same-session A/B (GPU path, after-preview Full, 2 rounds, load avg 50–95):
UI-thread present time p50/p95: plays from previews 11–31 / 34–77 ms → 3.6–5.8 / 4–14 ms;
live effects play 17–28 / 31–56 ms → 4.1 / 12 ms (first run)
plays from previews: 184/8, 183/9, 189/3, 169/23 → 192/0, 192/0, 191/1, 192/0 shown/dropped
live effects play (CPU-bound, ~6 cores at this load): too noisy to compare (0–117 shown either way)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Program monitor overlay for graphic clips: Type tool click creates a text layer (in the
selected graphic under the playhead or a new clip) and edits it in place with a caret,
selection highlight, arrows/word/line navigation, Shift-select, Cmd+A/C/X/V, Return,
Backspace/Delete, Esc; typing is one undo step. Selection tool selects, moves and scales
layers (bounding box, handles, anchor); double-click edits text. Rectangle/Ellipse tools
drag out shapes; Pen tool places a closed path. Automation ids program.layer.*.
- Properties (when a graphic is selected) and Essential Graphics: Layers list, Align and
Transform (align / distribute), Text (source text, font family + style incl. system fonts,
size, paragraph alignment, tracking, leading, baseline shift, box width, faux bold/italic,
caps, underline, kerning/ligatures), Shape, Appearance (fill, 2 strokes, background,
shadow). Automation ids graphics.*.
- Headless UI test (egui_kittest) drives the whole flow through the control channel; with
FILMCRAFT_UI_SHOTS it renders PNGs via wgpu (works with a locked screen).
- docs/graphics.md; architecture, ROADMAP, ATTRIBUTION (code-drawn glyphs) updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Engine shortcut set (crates/engine/src/shortcuts.rs): bindings as data
(command, canonical chord, optional panel), panel shortcuts override app ones
while the panel has focus; per-OS modifiers (Cmd = Cmd/Ctrl, Ctrl = macOS
Control, folded into Ctrl elsewhere and reported as conflicts); conflict
detection, reassignment, undo/redo, Cancel restores; frontend commands
registered so tools/transport/panels are bindable too.
- Presets: FilmCraft Default, Premiere Pro / Final Cut Pro / Avid Media
Composer Compatible (key tables mapped onto FilmCraft command ids); custom
presets as JSON in <data dir>/Keyboard Shortcuts/, active set persisted,
Save As / Delete / Export / Import (Windows-notation files converted).
- shortcuts.* commands for agents/MCP (list/get/set/clear/undo/redo/conflicts/
forKey/resolve/presets/loadPreset/savePreset/deletePreset/export/import/audit).
- Premiere-default audit: commands without a shortcut get Premiere's key when
free (Shift+D Apply Default Transitions, Shift+J/L slow shuttle, Shift+K Play
Around, Project > Shift+O Open in Source Monitor); Audio Clip/Track Mixer
window shortcuts corrected (Shift+9 / Shift+6).
- Edit > Keyboard Shortcuts... (Cmd+Alt+K): keyboard drawn in code with
app/panel/both colouring and modifier filter, searchable command list with a
record-on-click Shortcut column and +, Key: detail list, conflict warnings,
Undo/Redo/Clear/Cancel/OK; automation ids shortcuts.*.
- Menus (in-app and native macOS key equivalents), command.list and the input
loop follow the active bindings live.
- Tests: 7 engine tests; scripted kittest test (record keys, live menu update,
reassignment, undo/redo, cancel, OK, panel-specific Left in History).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- filmcraft_media::cancel: a per-job cancellation flag passed through a thread-local, so the
MediaSource API is unchanged. Frame workers run each job inside it.
- FrameServer::retain_queue (called by schedule_playback every refresh) now also cancels running
jobs for frames the playhead has passed. Before, a worker blocked on a source's decoder lock
went on to decode its frame after playback had moved 50–100 frames on, found it evicted and
restarted the decoder at the keyframe (decoder trace: "seek i=32 key=0 next=142").
- GopCache gives up (CodecError/MediaError::Cancelled) before seeking or decoding for a cancelled
job and between samples; decoder state stays consistent, so the next request carries on.
- The CPU compositor checks the flag between tracks and effects; cancelled results are never
cached.
- Stop drops queued prefetch jobs and cancels running ones (FrameServer::stop_prefetch). They
used to run to completion after Stop, e.g. 14 frames of live effects rendering competing with
a Render In to Out started right after.
bench, same-session A/B (GPU path, 8 s, load avg 35–41):
after-preview Full live play (1080p + Lumetri/Sharpen/Levels/Tint, CPU-bound):
38/154 → 98/94 shown/dropped, 297 → 267 ms CPU/frame, 84 jobs cancelled
after-preview plays from previews, h264-1080, stack3, h264-2160: unchanged within noise
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Trim Monitor: with edit points selected the Program monitor shows the
outgoing/incoming frames, Out/In timecodes and shift counters, -N/-1/+1/+N
buttons (N = Preferences > Trim > Large Trim Offset), Apply Default
Transitions, play-around loop and exit; drag in a picture trims that side,
between them rolls. Automation ids trimMonitor.*.
- Dynamic trimming in the engine (trim.shuttle / trim.tick / trim.shuttleStop /
trim.cancelDynamic), driven by a caller-supplied clock: L forward, J back,
again = 2x..8x, Shift = slow, K commits as ONE undo step, Esc cancels; holds
at limits; any other command commits first. Live updates re-apply the offset
to the pre-trim project without touching history or the journal.
- trim.playAround: loop/once from preroll to postroll (new Preferences >
Playback preroll/postroll, Trim > playhead determines loop).
- trim.monitor query, trim.applyDefaultTransition (Shift+D); timeline draws
selected edit points (red ripple / yellow roll-trim brackets) and clicking an
edge with Selection/Ripple/Rolling selects it; selecting clips leaves trim mode.
- J/K/L/Space/Shift+K route to trim mode in the UI; new Shuttle Slow L/R and
Play Around commands.
- Tests: 6 engine tests on an exact simulated clock; scripted kittest test
(buttons, L/K/J, one undo step, Space loop, exit); ignored wgpu screenshot test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root causes (seen with the bench's decoder counters):
- A frame-threaded H.264 decoder holds 16–32 pictures in flight. GopCache only continued the
running decoder when it was at most 16 samples past the wanted sample, so a request for a frame
that had been fed but not output yet restarted decoding at the keyframe: 3–13 extra seeks per
8 s play at 4K, each re-decoding up to a GOP. It now tracks the highest pts output since the
restart and continues while the wanted frame is still inside the decoder.
- The byte budget (384 MB) held 31 4K frames, less than the decoder's lead plus the prefetch
window, so frames were evicted before they were shown and re-decoded. The cache now always has
room for 64 frames.
- A request just past the next sync sample restarted there, skipping frames playback still
wanted, which then forced a restart at the earlier keyframe. A running decoder now carries on
through up to 48 samples into the next GOP.
- Intra-only streams (ProRes render previews, MJPEG) decoded one frame at a time under the cache
lock and re-fed up to 64 samples per miss. VideoDecoder::intra_only() lets GopCache decode them
outside the lock with a pool of decoders, one sample per frame.
Output is unchanged (same decoder, same frames); unit tests cover the in-flight, continue-through
and parallel intra cases.
bench (GPU path, 8 s, same-session A/B, load avg 70–120 on 14 cores):
h264-2160 Full: 0–12 → 71–171 shown of 192; decoder seeks 7–10 → 1; 229–355 → 175 ms CPU/frame
h264-2160 Half: 0–15 → 94–159 shown; seeks 6–13 → 1; 247–524 → 173–185 ms CPU/frame
after-preview Full, 1st play after render: 86/106 → 192/0 shown/dropped, 272 → 26 ms CPU/frame,
1716 → 182 samples decoded; 2nd play: 135/57 → 192/0
h264-1080 and stack3 unchanged (1 and 3 seeks before and after)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- project: ItemKind::Graphic (transparent canvas source, hidden from the Project panel,
unlimited duration); graphic layers are hidden effect instances graphic_text /
graphic_shape on the clip (serde for free, keyframes on every animatable property,
Source Text holds), with appearance (fill, two strokes outer/centre/inner, background
box with radius, drop shadow) and transform (position, anchor, scale, rotation,
opacity); eval_layer gives a plain LayerSpec. No schema migration: all additive.
- render: graphic_clip rasterises each layer straight to the output transform (crisp at
any Motion scale/rotation), caches layer rasters, composes a tight image for the GPU
plan; standard effects on a graphic run at source resolution first.
- engine: graphics.newText (Cmd+T) / newShape (new clip above the footage at the playhead
or a layer in an existing clip), setText (typing coalesces into one undo step), set,
selectLayer, deleteLayer, arrangeLayer, align (frame / selection), distribute,
graphics.list, fonts.list. EditorState.graphic_layers holds the layer selection.
- goldens: graphic_title, graphic_shapes (CPU and GPU parity).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- project: MixerStrip on every audio track, submix tracks and the Mix
(automation mode Off/Read/Latch/Touch/Write, lanes for volume/pan/mute/
send levels, effect-parameter automation in the insert's own keyframes,
sends pre/post fader, output routing, record arm, solo safe, input map);
pre/post-fader flag on inserts; serde defaults (no schema migration).
- render: mixer graph (tracks -> submixes -> Mix) in Premiere's order:
pre-fader inserts, pre-fader sends, mute, fader, meter, post-fader
inserts, post-fader sends, pan/balance. Per-sample automation, effect
params on an absolute 64-sample grid (output independent of request
cutting), route-level latency compensation, cached continuation state
with pre-roll, solo/solo-safe logic, -3 dB constant-power pan law for
mono tracks, balance for stereo. LiveMix carries held controls (Touch
ramps back over the automatch time), meters and the newest project.
- engine: mixer.* commands (setStrip, setValue, touch/release,
recordStart/recordStop, submixes, inserts, sends, track keyframes,
writeAutomation, inspect) and the automation recorder (gesture streams
thinned into keyframes, one undo step per pass, Switch to Touch after
Write); Preferences > Audio (automatch time, keyframe thinning).
- tests: gain/pan law, block-boundary automation, solo/mute/solo-safe,
sends, latency alignment, render-vs-playback identity, recorder modes,
perf (24 tracks x EQ+Dynamics+Reverb + submix: 6.3x realtime, one core).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New L1 crate filmcraft-text: fonts (bundled Inter Regular/Medium/SemiBold/Bold/Italic,
JetBrains Mono, Noto Serif; DirectorySource scans system font folders reading only name/OS2
tables; family/style resolution with synthetic bold/italic; per-char fallback), layout
(harfrust kerning/ligatures, unicode-bidi, unicode-linebreak, alignment incl. justify,
leading, tracking, baseline shift, all/small caps, underline, carets/hit-test/selection,
cached), raster (moved from captions; active-edge list, shapes, emboldening), render
(outline + sub-pixel glyph-mask caches; transformed-outline path for rotation/skew),
mask (distance-transform strokes, blur).
- Captions burn-in now shapes and kerns via the engine (font.rs/raster.rs removed).
- Timecode / Clip Name effects draw real text (full clip names, not seven-segment digits);
text_burnin golden re-blessed.
- UI takes its font bytes from the text crate (no duplicate copies in the binary).
- New fonts: Inter Bold, Inter Italic (OFL-1.1, rsms/inter v4.1), Noto Serif Regular
(OFL-1.1, notofonts) with sidecars, ATTRIBUTION.md rows and licence text.
- Perf: 3-line 96 px title at 1080p ~1.6 ms cold / 0.15 ms warm (release).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root cause: not a lock. When the window is hidden or behind others (always when the app is
driven over the control channel), macOS App Nap drops every thread of the process to background
priority 4 and defers its timers. On a loaded machine (load avg ~150) a priority-4 process gets no
CPU: after Render In to Out + play the UI thread stayed runnable inside the run loop's timer
callout, every thread at PRI 4 (`ps -M`), 0 % CPU, control channel silent. Relaunched with App Nap
off the same script ran through: render 2–13 s instead of 15 s, every reply immediate (2/2 runs).
- apps/filmcraft/src/app_nap.rs: hold a user-initiated, latency-critical NSProcessInfo activity
for the life of the process (= NSAppSleepDisabled for a bundle; idle system sleep allowed).
- ui.screenshot: a hidden window or sleeping display never presents, so the screenshot event
never came and the UI repainted at full speed forever (100 % CPU). It now fails after 10 s with
"no frame was presented (window hidden or display asleep)" and polls at 60 Hz meanwhile.
Regression test: app_nap::tests (the activity is taken on macOS). The starvation itself needs a
hidden window on a loaded machine and is checked with the control-channel script in the commit
notes (captions.newTrack/add, markIn/Out, Full, renderInToOut wait, stop, playhead.set,
renderBar, ui.screenshot): all replies within 10 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- crates/ui-egui/examples/bench_playback.rs drives the Program monitor's own FrameServer,
prefetch (now FrameServer::schedule_playback, shared with the monitor) and PlaybackMeter at a
simulated 60 Hz display, optionally compositing on a headless wgpu device. Scenarios:
h264-1080, stack3 (3 × 1080p, scaled/rotated/opacity), h264-2160, demo, after-preview
(live play → Render Effects In to Out → 2 plays), seek-storm. Reports latency percentiles,
on-time ratio, worker/decode wall and thread-CPU time, UI-thread present time, process CPU per
frame, decoder seeks/samples (codecs::gop_stats) and wasted jobs; --json for machine output.
Fixtures are generated with ffmpeg into target/fixtures/playback.
- FrameServer records per-job timings (thread CPU via rustix clocks) while profiling is on.
- Dropped/shown were counted per UI refresh: "shown" counted every refresh that had the frame
(322 "shown" in an 8 s play of 192 frames) and "dropped" counted a frame that arrived a few ms
after its first refresh. PlaybackMeter counts timeline frames: shown when the exact picture was
on screen while due, dropped otherwise, including frames passed over without a refresh.
Baseline (GPU path, 8 s, load avg 104–168 on 14 cores; CPU columns are load-robust):
h264-1080 Full 191/1 shown/dropped, 46 ms CPU/frame; Half 188/4
stack3 Full 189/3, 102 ms CPU/frame; Half 157/35 (3 extra decoder seeks)
h264-2160 Full 8/184, 313 ms CPU/frame, 10 seeks; demo Full 114/78, Half 191/1
after-preview: live 0/192; 1st play after render 124/67, 2nd 119/73 (UI-thread present
p50 21 ms / p95 45 ms); seek-storm: 0 of 40 seeks shown within 150 ms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>