Commit Graph
121 Commits
Author SHA1 Message Date
Brandon ThomasandClaude Opus 5.5 4b83e088eb Merge main into the playback branch (mixer recordStart moves to the end of preroll; GopCache keeps main's random-access key walk-back)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 08:30:04 -04:00
Brandon ThomasandClaude Opus 5.5 2a6e27b35f M4.6: playback scheduling: preroll, scrub supersede, real-time frame skipping under CPU overload, hidden-window drops
- 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>
2026-10-01 08:10:50 -04:00
Brandon ThomasandClaude Opus 5.5 9baa34fa45 Merge M10.1–2 + M3.2/M3.11 into M7.2 (audio mixer): preferences gain audio + playback + trim groups; ROADMAP refreshed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:17:12 -04:00
Brandon ThomasandClaude Opus 5.5 24ef93702c M10.1: system font scan holds a lock for the whole scan — concurrent resolve() could see an empty system font list (found by system_scan_finds_fonts under parallel tests)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:13:05 -04:00
Brandon ThomasandClaude Opus 5.5 c42fa205cd M7.5: Audio Gain dialog (set / adjust / normalize max peak / normalize all peaks) and default audio transitions
- 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>
2026-10-01 02:11:57 -04:00
Brandon ThomasandClaude Opus 5.5 14ae01885f M7.2: Audio Track Mixer and Audio Clip Mixer panels, timeline track keyframes, live recording
- 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>
2026-10-01 02:10:29 -04:00
Brandon ThomasandClaude Opus 5.5 04e714510b M4.6: GPU path converts layer texels on the frame workers, not on the UI thread
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>
2026-10-01 02:10:14 -04:00
Brandon Thomas 1f6f375e4d Merge branch 'main' into worktree-agent-a02f08c9afeef5155 2026-10-01 02:07:12 -04:00
Brandon ThomasandClaude Opus 5.5 07c584f1a2 Merge M3.2/M3.11 (Trim Monitor, dynamic trimming, keyboard shortcuts) into M10.1–2; project schema v3 for graphic clips so older builds refuse them cleanly
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:07:12 -04:00
Brandon ThomasandClaude Opus 5.5 0a8bd59a09 M1.2: MP4 muxer wrote all-empty sample tables as uniform stsz size 0 (= 'table follows'), producing unreadable files; write a table instead (regression test; found by progressive_roundtrip proptest)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:06:38 -04:00
Brandon Thomas 97967d865a Merge main into M10 (text engine + graphics)
# Conflicts:
#	ROADMAP.md
2026-10-01 01:57:41 -04:00
Brandon ThomasandClaude Opus 5.5 daaee5072c M10.2: Type tool with on-monitor text editing, shape and pen tools, layer boxes and handles, graphic editor in Properties / Essential Graphics
- 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>
2026-10-01 01:57:18 -04:00
Brandon ThomasandClaude Opus 5.5 985a848c5e M3.11: keyboard shortcuts editor + presets
- 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>
2026-10-01 01:56:47 -04:00
Brandon ThomasandClaude Opus 5.5 5823e22e89 M4.6: cancel frame jobs playback no longer needs (stale in-flight jobs, prefetch after Stop)
- 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>
2026-10-01 01:54:44 -04:00
Brandon ThomasandClaude Opus 5.5 d28b28214d M3.2: Trim Monitor + dynamic J/K/L trimming
- 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>
2026-10-01 01:46:40 -04:00
Brandon ThomasandClaude Opus 5.5 94b71a9346 M4.6: GOP cache: no re-decode from the keyframe during playback; ProRes/MJPEG frames decode in parallel
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>
2026-10-01 01:46:29 -04:00
Brandon ThomasandClaude Opus 5.5 4918555906 M10.2: graphic clips — text and shape layers in the project model, vector renderer (CPU + GPU plan), graphics.* commands
- 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>
2026-10-01 01:38:04 -04:00
Brandon ThomasandClaude Opus 5.5 6b00314e96 M7.2: audio mixer graph, track automation and mixer commands
- 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>
2026-10-01 01:29:49 -04:00
Brandon ThomasandClaude Opus 5.5 b141c406e9 M10.1: text engine (crates/text): font database with bundled OFL fonts and system-font discovery, harfrust shaping, bidi, line breaking, paragraph layout, glyph cache and rasteriser; captions and Timecode/Clip Name burn-ins use it
- 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>
2026-10-01 01:22:30 -04:00
Brandon ThomasandClaude Opus 5.5 5110b694d8 M4.5: fix UI hang after preview render (macOS App Nap starved the hidden app; ui.screenshot waits forever)
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>
2026-10-01 01:17:13 -04:00
Brandon ThomasandClaude Opus 5.5 0aaa4f41de Merge main into M9.8 (VP9): Opus + VP9 together in codecs/mkv; vp9 at opt-level 3 in dev builds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:05:53 -04:00
Brandon ThomasandClaude Opus 5.5 10a9ae76d1 M9.8: VP9 README — benchmark numbers (alternated before/after runs)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:04:44 -04:00
Brandon ThomasandClaude Opus 5.5 68e5e91bfe Merge main into the playback branch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:52:31 -04:00
Brandon Thomas 4eb1ffaa59 Merge branch 'main' into worktree-agent-a4e518618b8b67600 2026-10-01 00:46:36 -04:00
Brandon ThomasandClaude Opus 5.5 5170376dcb M4.6: headless playback benchmark (cargo xtask bench-playback) and a correct dropped-frame count
- 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>
2026-10-01 00:44:23 -04:00
Brandon ThomasandClaude Opus 5.5 9db963e0e6 M9.8: VP9 wired into codecs — WebM/MKV V_VP9 and MP4 vp09 (+vpcC) import, 8/10/12-bit 4:2:0/4:2:2/4:4:4 (4:4:0 expanded, RGB to RGBA) output, key-frame-checked GOP random access, Decoder::reset / is_keyframe, oracle seek tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:42:11 -04:00
Brandon ThomasandClaude Opus 5.5 7dcdef6974 T5: headless scripted UI tests (egui_kittest + control channel)
crates/ui-egui/tests/scripted.rs runs the real FilmcraftApp under
egui_kittest's eframe harness (no window/GPU) and drives it through
ControlRequests, stepping frames until each reply arrives; synthetic
ui.click input is fed through the app's raw_input_hook. Tests: demo
project + widget registry, razor/undo/redo with timeline locate,
source insert, apply Gaussian Blur and see it in Effect Controls,
playback toggle/stop, click a tool button by automation id, error
replies. docs/testing.md documents coverage and limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:39:14 -04:00
Brandon ThomasandClaude Opus 5.5 8eb8f5ff68 T4: BS.1770 loudness oracle test against ffmpeg ebur128
Seven generated signals (sine, pink noise at 48/96 kHz, speech-like at
48/44.1 kHz, gated stereo with silence and sub-gate passages, fs/4
inter-sample peak) written as float WAV and measured by LoudnessMeter
and ffmpeg ebur128 (metadata, 3 decimals per 100 ms). Tolerances: M/S
+-0.1 LU every 100 ms, I +-0.1 LU, LRA +-0.5 LU, true peak +-0.2 dB.
Measured: M/S <= 0.0005, I <= 0.007, LRA <= 0.04 LU, TP <= 0.045 dB.
On the fs/4 tone ffmpeg's true peak is +0.6 dB above the analytic
value, so that case is checked against the analytic peak (ours -0.053).
testkit gains a float WAV writer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:32:25 -04:00
Brandon ThomasandClaude Opus 5.5 7655447233 T3: golden-image tests for the renderer + GPU parity; fix stale GPU uploads
crates/golden (test-only, L3): nine procedural 320x180 scenes (transform
and opacity, blend modes, Gaussian Blur, Lumetri basic, Crop, Cross
Dissolve / Dip to Black / Wipe mid-transition, timecode/clip-name text)
rendered on the CPU and compared with committed goldens (PSNR >= 45 dB,
max <= 12, p99 <= 2); FILMCRAFT_BLESS=1 regenerates them with
attribution sidecars. testkit::golden adds PNG I/O, diff metrics and
failure images. The same scenes are composited on the GPU and compared
with the CPU.

GPU parity exposed a real bug: GpuCompositor cached uploads by
pixel-buffer address without owning the buffer, so a new frame
allocated at a freed frame's address was drawn with a stale texture
(whole wrong frames, PSNR ~12 dB). The cache now keeps the buffer
alive; regression test upload_cache_keeps_buffers_alive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:26:14 -04:00
Brandon ThomasandClaude Opus 5.5 34f53fe9cf Merge main into M11.1–2 (autosave/recovery): previews follow save/open/recover; bare pre-envelope projects with version 1 or 2 load as schema v1 (regression test)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:23:01 -04:00
Brandon ThomasandClaude Opus 5.5 0d1f21d393 M9.8: VP9 decode speedup — lane-parallel loop filter (i16/i32 lanes), banded parallel loop filter, vectorizable MC filters, per-size transform buffers, buffer pools, parallel strip assembly and picture output
1080p 8-bit bench: 55 -> 98 fps single-thread CPU time; 45 -> 115 fps wall with 14 threads
(best of 4 on a heavily loaded machine). Still bit-exact against the ffmpeg oracle on the
whole fixture matrix (1 thread and thread pool). Fixture generation uses per-call unique
temporary names (concurrent tests generating the same fixture raced on rename).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:21:03 -04:00
Brandon ThomasandClaude Opus 5.5 3704002f89 T2: cargo xtask fixtures pre-generates the oracle fixture matrix
Runs each crate's ignored generate_fixtures test (h264, hevc, isobmff,
matroska, prores) - the same generators the oracle tests use - and
prints made/cached/skipped per fixture plus a summary. Optional crate
filter: cargo xtask fixtures hevc prores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:20:59 -04:00
Brandon ThomasandClaude Opus 5.5 8780d5ffa2 T1: shared ffmpeg/ffprobe discovery in filmcraft-testkit
New dev-dependency-only crate crates/testkit (L0, no FilmCraft deps):
FILMCRAFT_FFMPEG/FILMCRAFT_FFPROBE, then PATH (incl. .exe), then known
dirs; SKIPPED lines and FILMCRAFT_REQUIRE_ORACLES=1 to make skips fail;
require_ffmpeg!/require_ffprobe!/require_oracles!; fixtures_dir() under
<workspace>/target/fixtures with per-thread temp names and atomic
generation. Oracle helpers in aac, opus, h264, hevc, h264enc, isobmff,
matroska, prores and export migrated (codecs left on its local lookup).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:10:59 -04:00
Brandon ThomasandClaude Opus 5.5 516978b4d4 M11.2: auto-save ring + crash-recovery journal
- Engine autosave module: Preferences (autoSave.enabled/intervalMinutes=5/maxVersions=20/
  saveCurrentProject/recoveryJournal/recoveryIntervalSeconds), persisted in the per-user data
  dir; prefs.get/set/reset commands.
- Background worker (filmcraft-autosave thread): the UI thread only hands over the Arc<Project>
  snapshot; JSON encoding and atomic writes happen on the worker. Premiere-style auto-saves to
  <project dir>/Auto-Save/<name>-YYYY-MM-DD_HH-MM-SS.fcproj (local time), only when changed,
  rotated to Maximum Project Versions, optionally also saving the project.
- Recovery journal: debounced snapshot of unsaved changes in <data>/Recovery/<session>/ guarded
  by an OS file lock; on launch, sessions whose lock is free and that left a snapshot are offered.
  Clean quit removes the journal (or keeps it, marked cleanExit, if changes were unsaved).
- Commands: file.recover, file.discardRecovery, file.recoveryList, file.autoSaveNow,
  file.autoSaveStatus, file.listAutoSaves.
- UI: Preferences ▸ Auto Save page (Edit ▸ Preferences, Cmd+,; FilmCraft ▸ Settings… on macOS),
  "Recover Unsaved Changes" dialog at startup, Revert confirmation; all with automation ids.
- Desktop: --recover / --no-recover / --data-dir; local-time offset via chrono (clock).
- Verified in the app: edits, kill -9, relaunch, dialog, Recover restores the edits (unsaved, original path).
- docs/project-files.md: auto-save, recovery and measured costs (2,000 clips: 2.5 ms encode,
  1.4 MB, ~30 ms atomic write+fsync, all off the UI thread). ROADMAP M11 updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:10:23 -04:00
Brandon Thomas 9e54812b2a Merge branch 'worktree-agent-ae361fad8d71a0eef'
# Conflicts:
#	crates/engine/src/commands.rs
#	crates/export/src/lib.rs
#	crates/ui-egui/src/frames.rs
#	crates/ui-egui/src/state.rs
2026-10-01 00:08:58 -04:00
Brandon Thomas b0ddca7169 Merge branch 'worktree-agent-a726528c34396e2d2'
# Conflicts:
#	ROADMAP.md
2026-10-01 00:07:18 -04:00
Brandon ThomasandClaude Opus 5.5 5c5a10e1b9 M4.5: render bar painted from preview segments (green/yellow/red, merged spans, hover text, automation ids timeline.renderBar.N); playback, Program monitor (CPU and GPU plan paths), audio output and meters read render previews where valid; status-bar job progress with cancel (status.job.cancel); play after rendering previews; previews pre-opened when rendered. Measured on 1080p H.264 + Lumetri/Sharpen/Levels/Tint, 8 s at Full res: 189–191 of ~192 frames dropped before render, 0–1 dropped after (25–46 on the first play right after the render)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:06:38 -04:00
Brandon ThomasandClaude Opus 5.5 098dcc8a61 M10.4: caption UI — caption track rows above the video tracks (select, drag to move/trim, eye/lock headers), Text panel Captions tab (segment list with editable in/out/text, add/split/merge/delete/export, track picker, style strip), Burn Captions Into Video in Export mode, caption file dialogs; docs/captions.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:06:29 -04:00
Brandon ThomasandClaude Opus 5.5 9eb12c0043 M10.4: caption commands in the engine (captions.* — tracks, add at playhead, split/merge/trim/move/delete, text/speaker/timing, style, next/previous, show/hide, list), file.import detects .srt/.vtt/.scc into a new caption track, File ▸ Export ▸ Captions… (SRT/VTT/SCC), Clear/Ripple Delete on selected captions; caption overlays in render + GPU frame plans; burn-in on export (ffmpeg-oracle test)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:06:29 -04:00
Brandon ThomasandClaude Opus 5.5 ba4a04e031 M11.1: .fcproj schema versioning + migrations, atomic saves
- New crate filmcraft-format (L3): files are an envelope {format, schema_version, generator,
  project}; a chain of single-step JSON migrations (v1 bare Project → v2 envelope) upgrades old
  files on load; newer-than-supported files are refused with a clear "update FilmCraft" error;
  damaged/foreign files report as such.
- atomic_write: temp file in the same dir + fsync + rename + dir fsync; used for every project
  write (FsServices), so a crash mid-save never corrupts the project.
- Engine: file.save/saveAs/open go through the format crate; new file.saveCopy (Save a Copy…,
  Cmd+Alt+S) and file.revert; the first save over an upgraded file keeps
  "<name> (schema vN backup).fcproj". Project.version dropped (the envelope carries it).
- Hand-made v1 fixtures (minimal + edit without later serde-default fields) in
  crates/format/tests/fixtures; 2,000-clip encode/decode timing test.
- docs/project-files.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:54:04 -04:00
Brandon ThomasandClaude Opus 5.5 740fcd95cb M4.5: render-bar segments + render previews in the engine — segments cut at every video edit point, 128-bit content hash relative to the segment start (clips, sources, effects + keyframes, transitions, sequence settings; stable across save/load and ripple moves), measured yellow/red cost estimate; content-addressed ProRes preview cache per project (temp folder until first save), Render Effects In to Out (Enter), Render In to Out, Render Selection, Render Audio (float WAV previews mixed into playback), Delete Render Files (In to Out), sequence.renderBar query; renders run as cancellable jobs on the export pipeline
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:43:26 -04:00
Brandon ThomasandClaude Opus 5.5 421af286dd M10.4: caption tracks in the sequence model (project v2) + filmcraft-captions (SRT/WebVTT/SCC read+write, CEA-608 pop-on/roll-up/paint-on decoder, pop-on encoder with frame-exact load scheduling, burn-in layout + own rasteriser over skrifa outlines with Inter) + caption edit ops (add/split/merge/trim/move/delete, sync-locked insert/extract)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:42:06 -04:00
Brandon Thomas 1c9073de2b Merge branch 'worktree-agent-a2fb03c37eff0a53c' 2026-09-30 23:37:44 -04:00
Brandon ThomasandClaude Opus 5.5 608a83f33e M9.13: Opus in WebM/MKV and MP4 import
crates/codecs decodes Opus with filmcraft-opus (48 kHz output):
- Matroska/WebM A_OPUS: CodecPrivate = OpusHead. CodecDelay removes the
  pre-skip exactly (header pre-skip when CodecDelay is absent); packet starts
  are accumulated from TOC durations because block timestamps are only
  tick-accurate. Random access decodes max(SeekPreRoll, 320 ms) first.
- MP4/MOV Opus + dOps: edit list removes pre-skip (dOps pre-skip without
  one); same pre-roll on random access.
- Family-1 surround reordered from Vorbis to WAV/SMPTE channel order;
  corrupt packets are concealed instead of failing.
Tests (ffmpeg/libopus fixtures, skipped when absent): WebM stereo, MKV mono
VoIP (SILK/hybrid), MKV 5.1 multistream, MP4 stereo; sample-aligned against
libopus (52-89 dB sequential, 45-89 dB after random access).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:37:19 -04:00
Brandon ThomasandClaude Opus 5.5 20f29623b5 M9.13: Opus robustness + output-rate oracle tests
Multistream packet-loss concealment conceals the last packet duration on
every stream (conceal(dur)) so all streams stay in step. New robustness
tests: random garbage over every TOC config at all output rates, multistream
garbage, truncated/bit-flipped conformance packets interleaved with losses;
none panic and all output is finite. New oracle test decodes at 8/12/16/24 kHz
against libopus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:27:10 -04:00
Brandon ThomasandClaude Opus 5.5 4c6e1db167 Add LICENSE-MIT and LICENSE-APACHE (the declared MIT OR Apache-2.0 licence had no licence files); README licence section; xtask layer error points at docs/architecture.md; matroska description includes muxer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:25:35 -04:00
Brandon Thomas 6f13cbe6ea Merge branch 'main' into worktree-agent-a2fb03c37eff0a53c 2026-09-30 23:14:19 -04:00
Brandon Thomas bfc4ad47b5 Merge branch 'main' into worktree-agent-adeb64ed6481c199b 2026-09-30 23:14:17 -04:00
Brandon ThomasandClaude Opus 5.5 c03de12b31 M9.13: Opus decoder — LS fractional-delay SILK resampler, ffmpeg/libopus oracle tests, vector diagnostics
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:57:09 -04:00
Brandon ThomasandClaude Opus 5.5 a2fa4c0a69 M9.13: Opus decoder — SILK, hybrid, redundancy/transitions, OpusHead, multistream; all RFC 8251 vectors range-exact
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:50:02 -04:00