Commit Graph

68 Commits

Author SHA1 Message Date
Marco Arnulfo e5b1ed90a9 fix(dock): dock a panel along the edge of the whole workspace (fixes #1463) 2026-10-12 23:55:59 +02:00
skylar taylor-barrick 1996723225 UI.6: refresh shared UI integration against current main 2026-10-10 18:51:04 -06:00
Brandon Thomas bf7ef0e894
Merge pull request #794 from impostazioni/fix/alt-drag-duplicate
Timeline: Option-drag copies clips instead of moving them
2026-10-10 18:15:25 -04:00
skylar taylor-barrick baadac2cf8 UI.9: integrate shared docking and preserve workspace behavior 2026-10-10 14:59:52 -06:00
impostazioni a8b3d7f703 Timeline: Option-drag copies clips instead of moving them
Holding Option (Alt) while dragging timeline clips now leaves the
originals in place and drops copies where the pointer lets go, as in
Premiere. The copies are selected; copies of linked clips are linked to
each other, and transitions that would travel with a move are copied.

- edit: copy_items (overwrite / insert, new ids, links and transitions)
- engine: timeline.move takes `copy: true`, returns `copied`
- ui: the drag remembers Option, shows the copy under the pointer while
  the originals stay, copy cursor
- tests: engine copy/link/locked-track test, headless Option-drag UI test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:52:28 +01:00
vangel76 1580ebb520 #266: quitting with unsaved changes asks to save first
Problem: Quit, Cmd+Q or the window's close button exited at once with
unsaved changes, leaving them to the recovery journal, so every launch
after an unsaved session offered a recovery (docs/project-files.md: no
"Save changes?" prompt on quit yet).

Fix: a close request with unsaved changes is cancelled and the existing
Save Project prompt asks Cancel / Don't Save / Save (automation ids
quit.*), as in Premiere. Don't Save discards the changes and their
recovery snapshot; Save on a never-saved project (here and in Close
Project) opens the Save dialog and returns to the prompt when cancelled.
The control channel's app.quit still quits without asking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:33:45 +02:00
Brandon Thomas 33115e9a8a
Merge pull request #698 from vangel76/fix-614-continuous-thumbnails
#614: Head, Head and Tail and Continuous video thumbnails in the Timeline
2026-10-10 14:55:39 -04:00
vangel76 78e9221a34 #614: document timeline.thumbnails outside the ui.set row
The ui.set table row is one long line; #412 (PR #656) also edits it, so
the two PRs conflicted with each other in docs/control-protocol.md
whichever merged second. The thumbnails parameter is now described in
its own paragraph below the table, leaving that row untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:22:42 +02:00
vangel76 c85d0ac973 #614: Head, Head and Tail and Continuous video thumbnails in the Timeline
Problem: a video clip only ever showed its first frame. Show Video
Thumbnails was a plain on/off switch, so a wide clip at high zoom stayed a
flat colour after its head.

Fix: the wrench menu keeps Show Video Thumbnails and adds Head (the
default, as before), Head and Tail and Continuous under it. Continuous
tiles aspect-correct frames across the clip on a grid anchored at its head,
only where the panel shows them (at most 256 per clip), each frame the
source time under the tile's left edge (speed, reverse, frame holds,
multicam angles), requested at thumbnail priority through the existing
frame cache. A clip too narrow for the tiles falls back to its head. The
mode is UI state (`thumbnail_mode`, serde default Head, so saved settings
keep working) and `ui.set {timeline: {thumbnails}}` sets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:08:48 +02:00
vangel76 b6ba78e300 #412: select a single keyframe in Effect Controls
Problem: the keyframe lane highlighted every keyframe whose time equals the
playhead. A click on a keyframe moves the playhead there, so every
parameter's keyframe at that frame lit up (point 4 of #412).

Fix: a serde keyframe selection in the UI state (clip, effect index, param,
mask, media time). A click (or drag start) selects exactly that keyframe and
still moves the playhead; highlighting reads the selection. A moved keyframe
stays selected; references to deleted keyframes or clips no longer shown are
dropped. Readable via ui.inspect, settable via ui.set keyframeSelection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:57:20 +02:00
Brandon Thomas a8e38c4e07 Merge remote-tracking branch 'origin/main' into fix-445 2026-10-10 05:47:31 -04:00
Oskar d6e43bcf2c docs: volume grab kind for ui.timeline.hit, clip_audio_ui tests (#223) 2026-10-09 21:40:02 +02:00
lloyd ff8728af08 Anamorphic media: pixel aspect ratio in placement, monitors and New Sequence From Clip
A 1440x1080 clip with 4:3 pixels showed squeezed: the demuxers read its
pixel aspect ratio, but nothing after import used it. New Sequence From
Clip made a 1440x1080 sequence with square pixels, the Motion transform
sized clips from storage pixels (pillarboxed at 1440 wide in a 1920x1080
sequence), and the monitors drew the frame at its storage aspect.

- project: checked_par / sane_par / par_ratio / conformed_size treat
  file and project ratios as hostile (a zero term, or more than 8:1
  either way, is square). MediaClip::pixel_aspect (Interpret Footage
  wins over the file), Project::source_par (subclips follow the parent,
  nested sequences have their own, graphics and adjustment layers have
  none) and Project::conformed_source_size.
- render: motion_matrix takes the source's ratio and works in square
  display units (anchor in source pixels, position in sequence pixels,
  scale and rotation at the display aspect); Scale to Frame Size fits
  the display size. CPU, GPU plan, adjustment regions and the mask /
  graphics overlays share it.
- engine: New Sequence From Clip, New Sequence from an item and
  multicam sequences copy the clip's ratio; default media scaling and
  Fit / Fill frame use the display size; generators made for a
  non-square sequence get its pixels; transcodes render pixel for
  pixel. clip.interpretFootage takes pixelAspect ([num, den] or "file").
- ui: the Source and Program monitors draw the picture at its display
  aspect; overlays map both axes; Sequence Settings shows the display
  aspect (16:9 for 1440x1080 at 4:3).

Tests: render par_tests (fill, GPU plan parity, Scale to Frame Size,
non-square sequences, rotation, Interpret Footage, hostile ratios),
engine par_tests (New Sequence From Clip, overwrite into 1920x1080,
default scaling, Fit / Fill, Interpret Footage with undo and refused
values, hostile file ratios, generators, and ffmpeg-made MP4 and MKV
files), project unit tests, and the headless par_ui monitor test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:28:56 +02:00
Brandon Thomas 97a8eaffec Merge remote-tracking branch 'origin/main' into fix-299
# Conflicts:
#	docs/control-protocol.md
2026-10-09 09:45:27 -04:00
Brandon Thomas a739ffea84 Merge remote-tracking branch 'origin/main' into fix-256
# Conflicts:
#	crates/ui-egui/tests/keyboard_ui.rs
#	docs/control-protocol.md
2026-10-09 09:29:27 -04:00
Brandon Thomas 354bb86332
Merge pull request #304 from justjohn12345/pr/monitor-follows-drags
Program monitor: the picture follows a value that is dragged
2026-10-09 09:23:37 -04:00
Bloxy 4eb34ae6c7 #259: the edge you press is the edge you trim
Pressing a clip edge and dragging inward selected and moved the clip: the press was hit-tested
only once egui recognised a drag, 6 pt later, by when the pointer had left the 7 px edge zone.
And at a cut the left clip's Out edge claimed both sides, so the right clip's In edge could not
be grabbed.

What a press grabs is now worked out on the frame the button goes down (timeline_hit::grab_at,
shared by the cursor, the press and ui.timeline.hit) and acted on by both the click and the
drag. Inside a clip beats outside it, so the side of a cut picks the clip; zero-width clips
have no edges. Trims, rolls, rate stretches and remixes move by how far the pointer moved since
the press, and a moved clip stays under the point it was grabbed by. ui.timeline.hit takes
`modifiers` and reports `kind`, the trim a press would start.

Unit tests for the zones and tools; headless UI tests that drag a pixel or a few per frame at
60 fps (before: the clip moved, the wrong clip was trimmed, a wobbly click selected the clip, a
moved clip lagged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:27:48 +02:00
justjohn12345 d09e95663c Program monitor: the picture follows a value that is dragged
While a value was dragged in Effect Controls or Properties, the Program monitor
kept the picture from before the drag and only caught up when the mouse rested.

Two causes:

- Every step of a drag is a new project revision, so the frame the monitor asks
  for is never ready on the refresh that asks for it, and the stand-in lookup
  only knew frames of the same revision. The monitor now remembers the frames it
  asked for on its latest paused refreshes and, when neither the frame due nor a
  cached frame just before it is ready, shows the newest of those that is. A
  backward or fast scrub had the same gap and gets the same stand-in. Playback
  is unchanged.
- A frame still rendering was cancelled when the same frame was asked for at a
  newer revision. With frames slower than a refresh (CPU effects) each one was
  thrown away for the next. Such a job now finishes; a request for a different
  frame of the view (scrubbing) still cancels it.

perf.stats gains `monitor`: the frame and revision of the picture the Program
monitor shows, against the playhead's frame and the project's revision.

Tests (headless, perf_ui): a parameter changed on every UI pass and a backward
scrub leave the picture at most one step behind; a frame being rendered survives
a request for the same frame at a newer revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:06:13 -04:00
Dario Carnelutti 10879f385e export: free the frame pool when an export ends
The export pipeline hands the float image of every frame it converts back to
`filmcraft_frame::pool` (#184), which keeps up to 320 MiB of them, and the decoders' plane shelves
hold up to 192 MiB of each type. An export's images are the size of its frames, which nothing else
asks for, so a finished export left them idle until the next one.

`pool::trim()` frees every idle buffer. `Pipeline` calls it when it is dropped, one place for every
way an export ends (done, failed, cancelled, dropped half way by a host that steps it, as the web
build does) and for every caller of `export` / `Exporter` (previews, proxies and AAF/OMF mixdowns
share the pipeline). A job still rendering at that moment allocates a few images again: the pool is
only a cache.

Measured with a synthetic 3 s 1080p24 matte exported as PNG frames three times in one process
(Apple M1, `footprint`, 10 s after the last export): 575 MB without the trim, the pool holding
332 MB idle, and 258 MB with it. macOS returns the freed pages over a few seconds, not at once.

Tests: `crates/frame/tests/pool_trim.rs` (every shelf emptied, the pool works afterwards) and
`crates/export/tests/pool_trim.rs` (an image sequence, a finished and a cancelled video, an export
that could not start and a stepped export dropped half way each leave nothing idle; without the
trim they fail).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 10:13:07 -06:00
wstock 32764392f1 control: Ctrl+ chords through ui.key fire shortcuts off macOS (#245)
Bindings are written Cmd+… and parse to `command`; egui's matching then
requires `command`. A physical Ctrl press off macOS carries it
(egui-winit sets command = ctrl there), but ui.key built its event from
parse_shortcut, which mapped "Ctrl" to `ctrl` alone, so an injected
Ctrl+Z matched nothing while Cmd+Z worked. ui.click's `ctrl` modifier
had the same gap.

Off macOS "Ctrl" and "Cmd" are one key (the engine's Chord::effective
already says so), so parse_shortcut maps both to `command` there, and
the control channel synthesises presses the way the OS reports them:
a press of either carries both flags. macOS is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 11:08:19 +00:00
Brandon Thomas 7916cfb80e
Merge pull request #131 from punkhop/fix/audio-plays-ahead
Audio: mix playback 200 ms ahead so a busy Mac doesn't drop sound
2026-10-07 07:08:29 -04:00
Peet Guercio bb5d7ca0a6 Audio: mix playback 200 ms ahead so a busy Mac doesn't drop sound
Problem: playback decoded and mixed every ~10 ms device buffer inside the
cpal callback (AAC decode, effects, opening a clip's file at a cut, rayon,
locks). On a loaded Mac any stall missed the device deadline: static and
dropouts, different on every play.

Solution: play_ahead mixes on its own thread into a lock-free ring kept
200 ms ahead; the callback only copies. An underrun plays silence, is
counted, and the mixer skips the same span so sound stays locked to the
picture. Any device width plays the channels it shares with the mix.
perf.stats reports playback.audio (callbacks, underruns, missingFrames,
minLeadMs). The program mix moved to playback_mix so bench_audio can play a
real project through a simulated 48 kHz device and count dropouts.

Context: on a real 22-sequence project, load average 38-58, 15 s per cut:
mixing in the callback overran 8-170 times, mixing ahead 0 (ring never
below 149 ms). The web build keeps mixing in its callback (no threads).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:47:47 -07:00
Luskish 023127e146 Control: close the connection on input that isn't a request (#119)
A line that is not a JSON object with a string `method` (an HTTP request
line, other non-JSON text, a JSON array, invalid UTF-8, a line over 4 MiB)
now gets one error reply and the connection is closed, so a browser's
cross-origin fetch can't smuggle a command in its body. Connections are
capped at 16. Same transport as LightCraft (storytold/lightcraft#129).

Fixes #119.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:32:13 -04:00
Brandon Thomas 5c8471c9f4
Merge pull request #41 from justjohn12345/pr/decode-memory
MEM1–MEM4 + HW1: clip memory on a timeline from 12 GB to 1.9 GB; hardware decode actually on
2026-10-06 11:38:03 -04:00
Brandon Thomas 475c294678
Merge pull request #45 from punkhop/fix/move-linked-partners
engine: timeline.move takes linked partners along
2026-10-06 11:37:38 -04:00
Brandon Thomas 8dfa8fff4c
Merge pull request #52 from punkhop/fix/explicit-targets-enable
engine: a command whose targets are named in the parameters does not need a selection
2026-10-06 11:37:16 -04:00
Brandon Thomas 8a795324f4
Merge pull request #50 from punkhop/fix/replace-reports-short-media
engine: Replace With Clip reports clips that run past the new media
2026-10-06 11:37:11 -04:00
Brandon Thomas eb3ca3e811
Merge pull request #49 from punkhop/fix/replace-from-bin-keep-source-in
engine: clip.replaceFromBin can keep the clip's source In
2026-10-06 11:37:05 -04:00
Brandon Thomas 508a5a933f
Merge pull request #28 from storybored379/pr/mcp-conventions
MCP: tool annotations, core tools, resources, export progress and cancellation
2026-10-06 07:27:58 -04:00
punkhop 8f74d18829 engine: timeline.move takes linked partners along
Behaviour change to the default: with Linked Selection on (the default),
`timeline.move` now also moves the linked partners of the listed clips.
A partner lands on its own track in the same mode as the listed clip, so
a caller that lists only a picture clip can now overwrite audio at the
destination time. `"linked": false` gives the old behaviour (only the
listed clips move); the timeline panel passes it, so dragging is
unchanged.

Problem: `timeline.move {"moves": [{clip, track, time}]}` moved only the
listed clip. Its linked sound stayed where it was, out of sync, although
Linked Selection was on and `timeline.trim`, slide and select all act on
linked partners. Separately, a `time` near i64::MAX overflowed the tick
addition in the edit (a panic in debug builds, a wrapped position in
release).

Solution: a linked partner that is not itself in `moves` moves by the
same time offset and stays on its own track. A partner that is listed is
moved once, to where the list says. Two kinds of partner are left
exactly as they are: one on a locked track (the listed clip still moves,
as it did before), and one whose position would not change, which is the
case when the listed clip only changes track. Re-placing such a partner
would drop the transitions at its edges and report a move that did not
happen. A new optional `linked` parameter overrides the toggle for one
call. The command returns `{"moved": [ids]}`, the clips that moved.

No clip is moved before the sequence start. When one would be (a split
edit whose sound starts earlier, or several listed clips asked too far
left), all clips of the call land later by the same amount, so the
spacing asked for is kept. This replaces the per-clip clamp for a call
that lists several clips with a time below zero; a single clip still
lands at zero. The offsets are summed in i128 and checked on the way
back: a start past Tick::MAX is a parameter error and nothing moves.

Tests (timeline_move_tests.rs): partners follow, undo / redo, both
listed, `linked: false`, toggle off, `linked: true`; a locked partner; a
picture moved to another track at the same time leaves its sound and the
sound's crossfade untouched; the common stop at the sequence start with
a split edit and with two listed clips; hostile times (i64::MAX,
i64::MIN, junk). All fail without the change, the hostile one with the
overflow panic. docs/control-protocol.md and docs/agents.md describe the
default, the `linked` parameter and the result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:24:23 -07:00
punkhop 6de389c75a engine: a command whose targets are named in the parameters does not need a selection
Problem: a command gated on the selection refused to run although the
caller named what to act on. `clip.replaceFromBin {clips, item}` answered
"select a clip in the Project panel"; `edit.clear`, `edit.rippleDelete`,
`edit.pasteAttributes`, `clip.enable`, `clip.link`, `clip.speedDuration`
and `clip.audioGain` with `clips` answered "no clips selected". A script
or agent had to change the user's selection first. The cause is the order
in Session::execute: `enabled()` runs before the parameters are read, and
it only knows the selection.

Fix: Session::execute asks enablement per call. When `enabled()` passes,
nothing changes. When it fails and the parameters name targets under a
key the command documents as ids (`clips` / `clip`: clips of the active
sequence, `items` / `item`: project items), the same `enabled()` runs
once more with those targets standing in for the selection, and the
selections are put back before the command runs. So only the selection
condition is answered by the named targets: no sequence, an empty
clipboard or the wrong kind of clip still disable the command, ids of
nothing do not count, and a command that does not document the key
(`edit.cut`) still needs a selection. This works for every command
because the predicates already resolve their targets the way `run` does
with empty parameters. `is_enabled` (menus, `engine.commands`) is
unchanged.

`clip.audioGain` already read `clips`; its parameter doc now says so.

Tests (explicit_targets_tests.rs): the commands above with and without
parameters, undo/redo, the selections left untouched, menu enablement
unchanged, and the cases that must still be refused. Four of the five
fail without the change in execute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:05:12 -07:00
punkhop d3ee1dfd22 engine: Replace With Clip reports clips that run past the new media
Problem: when the replacement is shorter than the clip's slot, the clip
silently runs past the end of its media and the renderer holds the last
frame. Nothing in the command's result says so, so a script or agent
that swaps a take cannot tell.

Solution: the result of the three Replace With Clip commands
(`clip.replaceFromSource`, `clip.replaceFromSourceMatchFrame`,
`clip.replaceFromBin`) gains `short`: one `{clip, shortByFrames}` per
replaced clip that now plays past the end of its media, in sequence
frames and never more than the clip's own length. It is empty when none
does; stills and other sources without an end, and held frames, are
never short. The edit itself is not changed, and the parameter docs are
untouched (results are described in docs/control-protocol.md).

Test: `replace_with_clip_reports_clips_that_run_past_the_new_media`
(From Bin with a 12-frame subclip, undo / redo, From Source Monitor,
Match Frame, the cases that are not short, and an In past the end of the
new media capped at the clip's length).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:28:20 -07:00
punkhop 0e3aebee5d engine: clip.replaceFromBin can keep the clip's source In
Problem: `clip.replaceFromBin` always restarts the clip at the new
item's In point (or its start). That matches Premiere's Replace With
Clip > From Bin, but a script or agent swapping one take for another has
no way to keep the clip's place in the media.

Solution: the default is unchanged, and so is its result. An optional
`keepSourceIn` (default false) keeps each clip's current source In. A
subclip that restricts trims has no media outside its range, so there a
kept In outside the range is moved to the subclip's first or last frame;
with `keepSourceIn` the result gains `sourceInClamped`, the clips this
was done for. The parameter doc lists the new parameter only (a result
description in it would take the command out of the Keyboard Shortcuts
list, which reads the doc to see whether a command needs parameters).

Tests: `replace_from_bin_keeps_the_source_in_on_request` (default and
its result, keepSourceIn, undo / redo, the disabled case, subclips before,
after and around the In, an unrestricted subclip, junk values), and
`list_searches_labels_and_keys` now asserts the three Replace With Clip
commands are bindable. docs/control-protocol.md describes the commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:25:50 -07:00
justjohn12345 4f3e119ac2 MEM4: decode into the planes of evicted frames
With MEM1-MEM3 twelve clips on a timeline still took 5.1 GB, only 2.5 GB of
it live: each decoded picture allocated three planes, each eviction freed
them, and the system allocator kept the freed pages.

filmcraft_frame::pool keeps the planes of frames the GOP caches evict (up to
192 MiB per sample type; a frame something else still holds is left alone).
The H.264 decoder, through a plane allocator hook that keeps the crate free
of dependencies, and the VideoToolbox path take their output planes from it.

perf.stats reports decode.planePoolMB and decode.planesReused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:57:16 -04:00
justjohn12345 4486652f07 MEM3: one frame budget shared by all sources
Each GOP cache keeps up to 384 MB of decoded frames (at least 64), and
nothing bounded the total: twelve clips on a timeline held 3.8 GB of frames
nobody was looking at, and no perf.stats counter showed it.

The pool now counts the frames of every cache against a 1 GiB budget. Over
it, caches idle for 3 s give up frames, least recently used cache first.
Caches in use keep their own budget, since a frame evicted before it is
shown costs a re-decode from the keyframe.

perf.stats reports decode.cacheMB and decode.cacheBudgetMB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:39:48 -04:00
justjohn12345 517b6b041b MEM2: idle sources give up their decoders beyond a cap
Every source kept its decoder for good once it had decoded a frame, with its
reference pictures and the pictures its frame threads hold in flight: about
290 MB per H.264 clip, 3.5 GB for twelve clips on a timeline.

The GOP caches now share a pool. Beyond four live decoders, the least
recently used caches that have been idle for 3 s drop theirs and make a new
one when asked again (a restart at the sync sample, as after a seek). Caches
in use keep their decoder whatever the count, so a composite of many layers
never restarts decoders per frame. Other caches are only try-locked, so two
caches trimming each other cannot deadlock.

perf.stats reports decode.liveDecoders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:39:48 -04:00
Brandon Thomas c3ec0e85d3 HW1: platform crate + VideoToolbox H.264/HEVC hardware decode (#30)
- New crates/platform (filmcraft-platform, L5): OS media FFI only. The one
  crate allowed `unsafe` (own [lints] with unsafe_code = "deny",
  #[allow(unsafe_code)] only on the videotoolbox module, SAFETY comment on
  every block, safe Result-returning public API). AGENTS.md §0.3 names the
  exception; docs/adr/0001-platform-ffi.md records why and the rules.
- VideoToolbox decoder for avcC / hvcC, 8/10-bit 4:2:0 and 4:2:2: session from
  the sample entry's parameter sets with hardware required, asynchronous
  decode (2 in flight), biplanar CVPixelBuffer copied to planar Yuv8/Yuv16,
  reorder buffer of the stream's own depth, RASL skipped after a starting CRA.
  Output callback under catch_unwind.
- HybridDecoder: transparent mid-stream fallback to the software decoder
  (replays samples since the last restart point, drops already-returned
  pictures). New codecs::software_video_decoder (built-in factories only) and
  codecs::hw (switch, counters, NalStreamInfo shared with software decoders;
  vui_color / sar_par helpers now used by both).
- Settings ▸ Playback ▸ Hardware decoding: Auto / Off (engine applies it to
  the codecs switch); desktop app, CLI/MCP and bench call register().
- perf.stats decode.framesDecoded and decode.hardware counters.
- Tests: bit-exact parity with the software decoders (H.264 High, HEVC Main
  open GOP, Main 10; resets, reseeks, flushes), forced-failure fallback,
  mutation fuzz of samples and parameter sets, cross-platform fallback with a
  stand-in decoder, setting Off, HEVC 4:2:2 vs ffmpeg.
- Bench --hw auto|off; results in docs/performance.md (4K CPU/frame
  ~86-119 ms -> ~4 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:53:28 -04:00
storybored379 cad16825e3 MCP: progress notifications and cancellation for blocking exports
command_run file.exportMedia with wait: true (headless) runs as the
background export job and uses the MCP progress and cancellation utilities:

- with _meta.progressToken, notifications/progress reports frames done /
  total with the job status, at most every 100 ms (none without a token);
- notifications/cancelled stops the job at the next batch (jobs.cancel),
  deletes the partial output and sends no response;
- the session lock is only held for short polls, so ping, doc_inspect,
  jobs.list etc. are answered while it encodes.

Without wait, and in bridge mode, nothing changes. Test
export_progress_and_cancel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:51:38 -07:00
storybored379 8ee7658303 MCP: filmcraft://document and filmcraft://commands resources, with MCP 2026-07-28 cache hints
- resources/list and resources/read serve the project (tree + active
  sequence, as doc_inspect) and the command catalog (as command_list) as
  JSON, for clients that attach context from resources.
- Results are built with rmcp's types, so resultType follows the negotiated
  revision; for 2026-07-28 clients (current Claude Code) they also carry
  ttlMs and cacheScope, like the generated tools/list. The resource list is
  fixed (10 min), document reads are never cached (0).
- get_info now declares the resources capability; the instructions text
  moves to a const, unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:51:28 -07:00
storybored379 0f70015b93 MCP: tool titles and annotations, command_batch / doc_inspect / render_preview, strict arguments
- Every tool has a title and readOnly / destructive / idempotent / openWorld
  hints, set in its #[tool] attribute, so clients can run the read-only
  tools without asking and ask before edits.
- New tools: command_batch (several commands, stop at the first error by
  default), doc_inspect (project tree + active sequence in one call) and
  render_preview (= render_frame).
- An unknown tool argument is a -32602 error naming it and the accepted
  ones, instead of being ignored.
- A line that is not JSON gets a -32700 reply (id null) instead of being
  dropped without an answer: serve_io filters lines in front of rmcp.
- A panicking command no longer poisons the session lock for the rest of the
  session ("session lock poisoned"); it is reported as "internal error: …"
  (AGENTS.md §0.6).
- docs/agents.md § Conventions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:51:21 -07:00
Brandon Thomas d5e3d7e2ef Merge branch 'main' into worktree-agent-a8349582751c0ec31 2026-10-03 03:51:43 -04:00
Brandon Thomas c1c846aaad M12.7: Project panel and Media Browser parity
List / Icon / Freeform views, Metadata Display, view presets, font size,
Preview Area, bin tabs and windows, hover-scrub In/Out; Media Browser tree,
Favorites, history, file types, thumbnails, Edit Columns, import and drag.
Engine: project_panel and media_browser modules, Services::list_entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 02:14:31 -04:00
Brandon Thomas aa3ad1d950 M4.9: H.264 inverse transforms reconstruct across columns at once
The column pass and the add/clip of idct4/idct8 work on whole rows so they
vectorise (same integer steps in the spec's row-then-column order; checked
against the per-column formulation on random blocks). Docs: draft decoding in
architecture.md and perf.stats fields in control-protocol.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:46:52 -04:00
Brandon Thomas 9dde8107ad Merge main into M6.5 export parity
# Conflicts:
#	crates/engine/src/lib.rs
#	crates/ui-egui/src/control.rs
#	docs/control-protocol.md
2026-10-02 19:59:52 -04:00
Brandon Thomas c59d21181f M6.5: export parity — presets, Export mode settings, queue, Quick Export, image sequences
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:27:19 -04:00
Brandon Thomas 45b8c0c2a0 M8.9 + M12.6: Lumetri Scopes complete; Metadata, Timecode, Events, Progress, Reference Monitor panels
M8.9 (scopes):
- New L3 crate filmcraft-scopes (wasm-clean): Waveform RGB/Luma/YC/YC no Chroma,
  Parade RGB/YUV/RGB-White, Histogram, Vectorscope YUV (75/100 % targets, skin-tone
  line) and HLS; Rec. 601/709/2100 matrices, Clamp Signal, 8-bit/float/HDR (PQ cd/m2)
  scales; numeric summaries; density painting. < 1 ms per scope at 1080p.
- Lumetri Scopes panel rewritten: multi-scope grid, wrench / right-click menu (presets,
  scopes, parade/waveform type, targets, colour space, brightness, scale), footer
  (colour space, Clamp Signal, 8 Bit/Float/HDR). Settings in UiState::panels.
- scopes.read engine query returns levels, histogram bins, waveform/parade columns and
  vectorscope peaks.

M12.6 (panels):
- Metadata panel with editable Name/Label/log fields (metadata.get / metadata.set,
  one undo step, stored in ProjectItem::metadata; no schema change, stays v11).
- Timecode panel (rows: current/media/duration/in-out/remaining, any time display).
- Events panel on a new engine event log (failed/disabled commands, toasts, auto-save
  errors, jobs started/finished/failed/cancelled; events.list / events.clear).
- Progress panel (jobs with progress and Cancel).
- Reference Monitor (parked or ganged to Program; picture or scopes).
- All open from Window > menu, have automation ids; ui.set {"panels": ...} merges state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:32:22 -04:00
Brandon Thomas f5aa00ff7d Merge branch 'main' into worktree-agent-aacf40607c4b686e0
# Conflicts:
#	xtask/src/main.rs
2026-10-02 13:01:34 -04:00
Brandon Thomas 1f6562c08a M4.8: HEVC transform/SAO speed-up, catch-up decoding, perf.stats
- HEVC inverse transform: sum basis rows of non-zero coefficients with a
  const-generic size (bit-exact, tested against the direct product);
  SAO edge offset gets a check-free interior loop. HEVC CPU/frame -60%.
- H.264/HEVC/VP9 planes are moved into VideoFrames, not copied.
- Catch-up decoding: frame jobs tell sources which frames are late
  (filmcraft_media::cancel::with_catch_up); the GOP cache skips
  non-reference samples of late frames (H.264 nal_ref_idc 0, HEVC
  sub-layer non-reference pictures). Scrubs keep the last 2 s decoded.
- perf.stats: engine query + UI/control method (decode counters,
  playback shown/dropped, frame-worker decode/render ms, cache use).
- Timeline waveform source peak cached; bench seek section and --only.
- docs/performance.md with before/after results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:01:16 -04:00
Brandon Thomas a34bc7f038 M3.11: remaining Premiere menu items — Scene Edit Detection, Normalize Mix Track, Simplify Sequence, Find / search bins, Automate to Sequence, Project Settings, ALE export…
- Scene Edit Detection: pure-Rust cut detection (render::scene: luma grid + RGB
  histogram difference, adaptive threshold with sensitivity, min shot length),
  background job with progress/cancel; applies cuts (linked audio too), clip
  markers and subclips in one undo step.
- Sequence: Normalize Mix Track…, Simplify Sequence…, Transcribe Sequence…,
  Captions ▸ Show Active Caption Tracks Only, Apply Default Transitions to
  Selection works on selected clips; Sequence menu in Premiere's order.
- Markers: Add Flash Cue Marker… (MarkerKind::FlashCue).
- File: New ▸ Search Bin (live saved queries), Close, Close All (Other)
  Projects, Save as Template… (+ new project from template), Import from Media
  Browser, Get Media File Properties for ▸ File…/Selection…, Project Settings ▸
  General…/Scratch Disks… (scratch disks drive previews, proxies, auto-save),
  Export ▸ Selection as FilmCraft Project…, Export ▸ Avid Log Exchange…
- Edit: Find… / Find Next (project columns/operators/all-any, timeline clips
  and markers), Edit Original (OS opener host hook).
- Clip: Edit Offline…, Source Settings…, Update Metadata… (XMP sidecars),
  Generate Audio Waveform, Restore Captions from Source Clip (disabled: no
  embedded caption reader), Automate to Sequence…
- View ▸ Dynamic Audio Waveforms; Help ▸ Reveal Log Files…, System
  Compatibility Report… (with the wgpu adapter).
- Dialogs for all of the above (panels::menu_dialogs, ui.set menuDialog).
- Project schema v11 (no-op v10_to_v11, fixture, docs row).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:08:58 -04:00
Brandon Thomas e222d025cf Merge transcripts (M10.5) and clip/project commands (M3.10); clip schema renumbered to v10
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:34:12 -04:00