* Projects: Open Project asks to save unsaved changes first
File ▸ Open Project opened the chosen project at once, so a project with
unsaved changes was replaced without a prompt. Close Project and Quit
(#758) already ask first.
In the desktop app, once the project to open is chosen, the same Save
Project prompt now appears when the open project has unsaved changes,
with the automation ids `openProject.save`, `openProject.dontSave` and
`openProject.cancel`. Save saves the project and then opens the chosen
one; a never-saved project goes through the Save dialog, and canceling
it returns to the prompt. On Linux, where the Save dialog does not
block, the prompt stays open after a never-saved project is saved, as
it does for Close Project and Quit, and a second Save or Don't Save
opens the chosen project. Don't Save opens the chosen project without
saving, and Cancel keeps the open project. When the chosen project
can't be opened, the open project stays open with its changes and the
error goes to the status line. `file.open` with a `path` opens without
asking, as before. The prompt reuses existing strings.
* Contributors: add dotCooCoo (Robert Lee)
* Projects: Open Recent and Home's Recent list ask to save unsaved changes first
File > Open Recent and a click in Home's Recent list sent `file.open`
with a path, so a project with unsaved changes was replaced without a
prompt. Both now call `intercept_open` and show the Save Project prompt
first, as Open Project does. After Save or Don't Save the chosen project
opens; a Recent row on Home then switches to Edit, as it does without
the prompt. `file.open` with a path from the control channel or MCP
still opens without asking.
* Projects: Open Project opens the chosen project after a later Save dialog answer
On Linux the Save dialog answers in a later frame. Save in the Open Project
prompt on a never-saved project checked whether the project was saved right
after showing the dialog, found it unsaved and showed the prompt again; the
answer then saved the project but did not open the chosen one.
Saving and then opening the chosen project now run when the dialog answers.
A canceled dialog or a failed save shows the prompt again, with the reason in
the status line. An answer that arrives after another project was opened,
created or closed to does nothing: `Session::project_generation` counts the
projects a session has held, as in #2437. The prompt offers Save whenever a
Save dialog can be shown, also through `HostHooks::run_dialog`.
* Projects: a save prompt closes when a script replaces its project
The Close Project, Open Project and Quit prompts stayed open when a
script opened, created or closed to another project while they were
shown, and Save or Don't Save then acted on the new project: Don't Save
discarded its changes and Save saved it. Each prompt now records the
project it was asked over (`Session::project_generation`) and closes
when that project has been replaced.
* Projects: a Recent row on Home switches to Edit only once its project opens
With unsaved changes, a click in Home's Recent list switched to Edit as
soon as the Save Project prompt appeared, so Cancel left Edit showing
the current project instead of Home. Home now stays while the prompt is
shown and after Cancel, and switches to Edit after Save or Don't Save
opens the chosen project, also when Save first asks where to save.
* Open Project: a failed save keeps the prompt and the chosen project
When the open project already has a file and saving it fails, the Open Project prompt now stays open with the chosen project, as it does for a project that was never saved. The error shows in the status bar.
* Open Project: only its prompt offers Save through a Save dialog that answers later
Close Project and Quit offer Save for a never-saved project only where the Save dialog answers at once, as on main. After a Save dialog that answers later, only Open Project goes on to its next step.
* Projects: a failed save keeps the Close Project and Quit prompts
When the open project already has a file and saving it fails, the Close Project and Quit prompts now stay open, as the Open Project prompt does. The error shows in the status bar.
* Projects: the Open Project prompt blocks panels in floating windows
While the Open Project prompt is shown, panels in floating windows no longer take input, as for the Close Project and Quit prompts.
* render, gpu: adjustment layers composited on the GPU
An adjustment layer anywhere at a time sent the whole frame to the CPU
renderer, so GPU rendering did nothing for the frames under one. A
full-frame adjustment layer at 100 % opacity, Normal, unmasked and with
only effects the GPU stage runs is now a PlanStep::Adjust: its effects
run in the effect stage on the accumulator (the composite of the tracks
below) and the result replaces it before the tracks above are drawn.
Every other adjustment layer keeps the whole-frame CPU fallback.
The export renderer takes Composite plans without transitions; frames
with a Push still go to the CPU there, as before. perf.stats gpu gains
adjustments / cpuAdjustments.
* docs: adjustment layers on the GPU compositor in parity and performance
hardware-parity.md gains an Adjustment layers row; performance.md gains
the main-against-branch export timings with an adjustment layer.
* render, gpu: adjustment layer review fixes
- is_cpu_frame and plan_frame now ask the same adjustment_ops (evaluated
at the render scale, which is_cpu_frame takes from the RenderOptions):
a Transform that shrinks the frame no longer takes a pooled export
renderer only to be rendered on the CPU and counted as a GPU frame.
- FxOp::eval leaves a Transform with its shutter angle on (motion blur)
to the CPU; the GPU drew it sharp, on clips and adjustment layers.
- The effect stage's texture pool spans the whole composite frame, so an
adjustment step's working textures are reused instead of made again
every frame. Composite plans honour resolve = false (export).
- ExportRenderer::render hands frames with an adjustment layer back to
the CPU when the device has no effect stage, instead of compositing
them on the CPU and uploading the result to read it back.
- adjustment_covers_output checks the full-frame geometry without
building the coverage image.
- The GPU adjustment test also runs without an effect stage.
* render, gpu, export, ui: plan adjustment layers for the device's caps
plan_frame and is_cpu_frame take plan::DeviceCaps (how large a frame
the device runs adjustment layers on; none without an effect stage), so
a frame the compositing device can't adjust becomes a CPU image on the
thread that plans it, never on the UI thread or inside a renderer slot.
- Monitor: the caps are part of the frame key
(Target::SequencePlan(seq, caps)), so a plan made for another device
is never reused.
- Export: FrameRenderer::caps is required; the renderer pool plans with
the smallest caps of its renderers before it takes one.
- The compositor's own CPU fallback stays as a safety net for a plan
made for other caps; the tests check that nothing reaches it.
- Main's own-shutter Transform check replaces the duplicate guard in
gpufx.
Tests: a Push under an adjustment layer exported on the GPU against
render_sequence (forward and reversed, scales 1 and 0.5); routing
without an effect stage in the export pipeline and in the monitor's
frame server; GPU parity against render_sequence with colour and alpha
for a partly transparent canvas, stacked adjustment layers, an empty
canvas, a nested sequence below and one inside a nest, at scales 1 and
0.5; caps boundaries and is_cpu_frame agreeing with plan_frame.
* gpu: end a refused effect image's frame; count steps once drawn
effect_image began an effect stage frame and returned without ending it
when the stage refused the job, so the stage stayed mid-frame and kept
its working images. The compositor's step counters are added once the
whole frame is drawn on the GPU, so a frame that falls back half way
no longer counts the steps it threw away.
* Timeline: a dragged transition snaps like a trimmed clip (#4328)
Dragging a transition, or either of its ends, only rounded to whole frames
and ignored snapping, while clip trims, rolls, stretches and the work area
all snap. The moving end now snaps to clip edges, the cut, the playhead and
markers when snapping is on; a whole move snaps whichever end lands closer,
keeping the first snap the transition can actually reach, and the clip
limits still win. The snap guide shows only for a snap that was reached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Timeline: a dragged transition skips a snap target it can't reach for the next one (#4328)
When the nearest snap target couldn't be reached (the transition's own cut
when that would leave it no length, say), the drag fell back to the raw
frame although a farther target in range was reachable. The snap targets
within reach are now tried nearest first, both ends of a whole move by
on-screen distance, and the first one the transition reaches wins. snap()
itself picks the same target as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Monitor: full-screen Program monitor cinema mode (#3652)
Ctrl+Shift+F enters cinema mode: the Program monitor picture fills the
entire window on a black background with no panels, transport controls
or cursor visible. Esc exits back to the normal layout.
- `view.fullScreenProgram` command (keyboard, CLI, MCP, control channel)
- `UiState.fullscreen_program` bool (serde-defaulted for compat)
- frame.rs early return skips header, dock and status bar
- monitor.rs skips controls/transport and hides the cursor
- keys.rs Esc handler before trim-cancel
Closes#3652
* Monitor: address review feedback on cinema mode (#3652)
- Disable editing overlays (transform handles, graphics, masks) in cinema
mode so clips cannot be accidentally modified during review
- Move Esc handler before dialog guard so cinema exit always works
- Remove default shortcut to avoid Ctrl+Shift+F / Cmd+Shift+F conflict
with Open Search on Windows and Linux
- Save and restore previous fullscreen state on cinema exit
- Save and restore panel focus (route playback to Program while in cinema)
- Use black background for letterbox areas instead of panel_bg
* Monitor: also hide guides and rulers in cinema mode (#3652)
Move the cinema early-return before the overlay/guide/ruler section so
the picture is completely distraction-free during client review.
* Monitor: bypass multicam grid, comparison view and saved zoom in cinema (#3652)
Cinema mode now always shows the composite Program picture fitted to the
display, regardless of the saved Multi-Camera, Comparison View or zoom
settings. The saved settings are not modified.
* Cinema mode: the finished picture, not saved, with UI tests
It shows the composite whatever the monitor displays, without safe margins,
the transparency grid or zoom, and the saved settings are back when it ends.
fullscreen_program is no longer part of the saved state. Tests through the
control channel cover entering, Esc, focus and the playback key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Monitor: the cinema view in its own function, for the complexity limit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cinema mode: no rulers or status dot, and the cursor always hides
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* interchange: AAF/OMF audio tracks keep their names
from_project named every exported audio track A1, A2… and ignored Track::name, so a DAW showed
numbered tracks instead of what the editor called them. The writer now uses the track's name
(with L/R, or the channel, when broken out to mono; still A<n> for unnamed tracks), and
to_project keeps a slot's name unless it is the numbered A<n>/V<n>.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* interchange: classify numbered AAF/OMF track names by chars (a name starting with a multibyte character panicked at t.name[1..]); Unicode import test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: ondarrupeasu <ondarrupeasu@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A .fcproj is read whole, and that read stops at 256 MiB. The host bridge
allowed 512 MiB and copied the bytes first, so a project in between was
stored and then failed. Refuse it before the copy. Media may still be
512 MiB, because it stays a Blob.
Co-authored-by: Cursor <cursoragent@cursor.com>
* #1221: H.264 High 10 and High 10 Intra export in the built-in encoder
Stage 1 of #1221 (plus the intra-only option of stage 5):
- h264enc is generic over the sample type (u8 at 8 bits, u16 at 9-10 bits):
QP' = QP + QpBdOffset for quantisation and lambdas (QPY down to -12 at
10 bits), mb_qp_delta range/wrap, chroma QP mapping, deblocking alpha/beta/tC0
scaled to the bit depth, clipping in prediction, interpolation and
reconstruction. SPS bit depths, profile_idc 110, constraint_set3 for High 10
Intra, avcC high-profile fields, High 10 cpbBrVclFactor for level choice.
New YuvFrame16 / Encoder::encode16, EncoderConfig::bit_depth / intra_only.
- 8-bit output is byte-identical to main's encoder (pinned_decisions passes;
streams compared across profiles, presets, rate modes, slices, B-frames,
threads, two-pass, odd sizes).
- Oracle tests: ffmpeg decodes 10- and 9-bit streams bit-identical to the
reconstruction (presets, B-frames, slices, odd sizes, QP 22-46, CRF -12,
CBR, intra, avcC); ffprobe reports High 10 / High 10 Intra, yuv420p10le.
- Export: H264Profile::High10 (10-bit SDR via rgba_to_yuv420_10, HDR from the
float frames), h264_intra_only, MP4 / MOV / MXF OP1a (10-bit descriptor,
intra coding label), hardware encoders decline both, export dialog profile
entry ("10-bit color" on hover) and an All Intra checkbox, the bit depth in
the export summary, two Broadcast presets, flat h264Profile / h264IntraOnly
command parameters.
Rebased on main: ported into the split slice/ modules, the u32 quantisers
(exactness tests now cover QP' up to 63), main's Encoder::install and
prepare_ahead (High 10 converts in encode), export_mode/settings.rs,
tests/it/high10_oracle.rs; the new strings in every complete catalog
(es, fr, id, it, ja, ko, pt-br, zh-cn, de, ru, cs, pl, sv, tr, bs).
Not yet: High 4:2:2, High 4:4:4 Predictive, lossless, and >8-bit precision
from SDR compositing (SDR frames still arrive as RGBA8).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VECsf5SeZDHM5RDmcvRkM
* #1221: codec-parity header and revision history for High 10
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* export: describe High 10 Master as a 10-bit master; list all APV profiles in file.exportMedia
SDR exports are rendered at 8 bits before the 10-bit conversion, so the preset
cannot promise a master without banding. Catalog keys and translations updated
in all 15 locales. Also list 4444-10, 4444-12 and 400-10, which apv_profile
already accepts.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
The transports, the message types and the origin checks come from the shared crate (storytold/craft-libs#420); FilmCraft keeps what is its own: project or media, the open size limit and the saveAs rule.
A path written twice in one frame went out as saveAs: false the second time
even when the first hand-over failed, because the decisions ran ahead on a
copy that advanced regardless. Each save now reads the host's document just
before it is sent, with the borrow dropped first.
A page that embeds the web build can hand it a file and get every save back instead of a browser download, and hears about unsaved changes: ?host=parent for a same-origin iframe (postMessage, the protocol the GridCraft, PdfCraft, WordCraft and CADCraft bridges use) and data-host="page" on the canvas for a page that loads the module into its own document (non-bubbling CustomEvents on the canvas). The bridge holds a Transport (Parent or Page) with one send; the pure rules are unit-tested; the hosting guide documents both modes.
* feat(presets): native Adobe Premiere Pro .prfpset import and preset library
* Test that opening the Presets folder scans the library; open Presets last in the indent test
The Presets folder's new Import/Scan rows push the Lumetri sub-folders below the
Effects panel's visible area, so the indent test now expands Presets after them.
---------
Co-authored-by: justdxvid <178032210+justdxvid@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* #1468: Speed/Duration ▸ Maintain Audio Pitch time-stretches a clip's audio instead of resampling it
A speed-changed clip's audio was always resampled (varispeed): an octave
up at 200 %, down at 50 %. Premiere's Clip Speed / Duration dialog has
Maintain Audio Pitch; FilmCraft now does too.
- TrackItem::maintain_pitch (serde default, skipped when false);
clip.speedDuration takes "maintainPitch" (left out: unchanged), set on
the clip and its linked partners; sequence.inspect shows it; the dialog
has the checkbox between Reverse Speed and Ripple Edit, prefilled.
- filmcraft-audio-dsp::stretch::Wsola (L1): WSOLA with 30 ms
raised-cosine grains overlapped by half, each moved by up to 8 ms to
where it best continues the previous grain (normalised
cross-correlation, coarse then refined), so crossfades are in phase.
- filmcraft-render::stretch: output hops on an absolute grid of timeline
samples, grain positions chained per clip and kept like audio_fx keeps
effect chains, so ordered reads give the same samples however they are
cut; a seek starts a new chain four grains early; a source span over
2^24 samples falls back to varispeed. Forward, reversed and nested clips.
Tests: tones at 50-300 % keep pitch (2 %) and level (0.03 RMS) without
clicks; a 500 Hz nest at 2x, 0.5x, 3x and 1.5x reversed stays at 500 Hz,
and reads in blocks of 1024/997/4096 match one read within 1e-6; the
command sets, keeps, inspects, undoes and saves the setting.
Translations: es, fr, it, ja, ko, id, pt-BR, zh-CN.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Maintain Audio Pitch: keep the chains of playing clips; say why not PitchShifter
- The per-clip chain cache dropped every chain when it held more than
256 clips, so a clip playing at that moment restarted its chain a few
grains early and its samples changed there. Entries now carry a
last-read stamp, and a new clip evicts only the one read longest ago.
- A test pins the varispeed fallback for a read longer than 2^24
source samples, and that the source is not read for it.
- audio-dsp's README says why the shuttle's PitchShifter does not fit
clip reads: a stream with latency and state, where clips are read by
random access and every reader must get the same samples.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Maintain Audio Pitch: leave locked linked sound alone; README states the stretch's limits
- clip.speedDuration's last loop gave maintain_pitch to every selected
clip, including a linked sound on a locked track that the speed edit
itself skips. Only that assignment is now guarded, so interpolation
behaves as before. Test:
maintain_audio_pitch_leaves_a_locked_linked_sound_alone.
- audio-dsp's README no longer says every reader gets the same samples
for a span: a read in order continues a chain exactly, but a read
that starts a new chain (a seek, or after a cache eviction) can choose
slightly different grains for the same span. Known limits now says so,
and names the 2^24-sample read that falls back to varispeed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* edit: a cut between clips whose Maintain Audio Pitch differs is not a through edit (#1684 review)
* Speed/Duration: send Maintain Audio Pitch only when its checkbox changed, so a mixed selection keeps each clip's setting (#1684 review)
* render: Maintain Audio Pitch keeps one chain per reader, reads only the clip's source span and aligns on the channels it plays; noise tests (#1684 review)
* audio-dsp: README states what the stretch guarantees and its limits (#1684 review)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* Update 2026-10-20 17:50
* Update 2026-10-20 20:22
* Update 2026-10-20 21:09
* Update 2026-10-21 18:10
* Add drag and drop support for folders in project media panel
* Add drag and drop support for folders in project media panel
* Add drag and drop support for folders in project media panel
* Ellipsize oversized folder item count so the folder name stays visible
* Add drag and drop support for folders in project media panel
* Import dropped folders as bins from app/input.rs
Main moved handle_drops into app/input.rs (import_dropped); re-apply this PR's change there: mediaBrowser.import with imageSequence:false, with errors shown in the status bar. Add an engine test for files dropped with an empty folder.
* i18n: update the drop hint in bs, cs, pl, ru, sv and tr
The English key now mentions folders; these six catalogs still had the old key.
* Restore packaging script executable bits and drop the unrelated .gitignore entry
The branch had changed nine packaging scripts from 100755 to 100644 (FredApps' review) and added an .andromity/ ignore line unrelated to this PR.
* Project panel: clip a bin card's name to the space beside its item count
At the smallest icon sizes the room left beside the 'N items' count can be narrower than the one letter plus ellipsis the layout always emits, so the name still ran into the count. Clip the name to its slot and show it in full on hover whenever it does not fit (TheGalitube's review on #2632).
* mediaBrowser.import: report unreadable sub-folders and keep file.import's result shape
A dropped folder whose sub-folder cannot be listed now reports that sub-folder instead of silently leaving its media out. With plain files and folders without media, the folder errors are added to file.import's own errors (an Ingest result stays an object), and when the files fail to import the error still names those folders. Addresses CodeRabbit's review of 353422a.
---------
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* About: credits tables fit without side-scrolling and their columns resize
The Contributors table and the Models table were egui Grids inside a
two-way scroll area: they opened wider than the dialog, so they scrolled
sideways, and Company and Model sat almost touching. Both are now
egui_extras tables. Each column opens at the width of its header and widest
cell, the spare width goes to the text columns (Name; Company and Model),
and every column edge can be dragged. Widening a column grows the table
like a spreadsheet, and only then does it scroll sideways; the header stays
put while the rows scroll. Counts and dates are right-aligned and every
cell is vertically centred.
The sorted column stays highlighted and its direction shows on the
toolbar's sort-order button, so headers no longer reserve room for an
arrow. The opening widths come from fit_widths, a pure function with unit
tests that a fitted table fills its room exactly and that only the text
columns give way when room is short.
The About dialog opens 820 points wide, so every column fits at its natural width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let every credits column be dragged, and refit changed tables
The last column was egui's remainder column, which has no drag handle, so
it could only be resized from the edge before it. It is now an ordinary
column with its own handle.
A table keeps its column widths, so switching to a longer name left the
Name column at its old width and clipped the text. The table now goes
back to the opening fit whenever that fit changes: a longer widest cell
or a resized window. Dragged widths are dropped at that point.
fit_widths could overflow the room when one column hit the minimum,
because the other columns did not take up its share. Columns now give
way in proportion to their width above the minimum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refit credits tables on what they measure, not the fitted widths
Comparing the fitted widths missed the case it was meant for. Name is the
only growing column, so a longer name takes its extra width from the
spare room and the fit comes out the same: a dragged Name width stayed,
clipping the longer name.
It could also drop dragged widths by accident. The fit comes from the
available width inside the scroll area, which scrolling can nudge, so
scrolling a widened table could reset it.
The table now compares the natural widths and the clip rect's width,
which scrolling does not move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Preserve same-second autosave snapshots
Found while testing crates/format/src/autosave.rs:133
Co-Authored-By: GPT-5.6-Luna <noreply@openai.com>
Co-Authored-By: GPT-6-Luna <noreply@openai.com>
* test: cover autosave collision rotation
Co-Authored-By: GPT-5.6-Luna <noreply@openai.com>
* autosave: number a same-second save after the highest collision on disk
Rotation removes the oldest collisions first, so the lowest free number can sit
below a newer save. Reusing it made the new save sort as the oldest and the
next rotation deleted it straight away; continue after the highest instead.
---------
Co-authored-by: GPT-5.6-Luna <noreply@openai.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* render: turn display-rotated video on the GPU (#3467)
A portrait phone clip (4K60 HEVC with a 90° display matrix) dropped ~97 %
of its frames: every decoded 4K frame was turned on the CPU, which is too
slow at 60 fps, and with the GOP cache's lock held.
- GopCache caches decoded frames raw when they need a crop or a rotation;
`frame_late` crops and turns them off the lock, and the new
`frame_late_unturned` hands them over as decoded with the quarter turns
still to apply.
- MediaSource gains `video_frame_unturned` (default: the upright frame, 0
turns). MP4 and Matroska implement it; the pool's wrapping sources and
the frame server's timed source pass it on.
- The render plan folds the turns into the layer matrix of plain layers,
and into the GPU effect stage (`LayerFx::turns`): `fs_fx_source` reads
the source on its decimated grid through the turn, and the CPU reference
decimates then turns, so effects still see the upright picture.
Other paths (CPU-only effects, log media, a clean aperture, export) still
turn on the CPU as before.
* codecs: cache intra-only frames raw too (#3467)
Intra-only decoders (ProRes, DNx) cropped and turned every frame before
caching it, so frame_late_unturned found it already turned and the GPU
had nothing left to do. Their frames are now cached raw like long-GOP
output: frame_late still crops and turns them off the lock, and
frame_late_unturned hands them over as decoded.
* docs: document the unturned-frame entry points (#3467)
* codecs: follow main's frame lookup in the unturned paths (#3467)
main changed `from_tick` (Matroska) to take the frame rate and `snap`, and
MP4 looks frames up with `lookup_time(.., rate, snap)`. `video_frame_unturned`
still used the old calls: the Matroska one no longer compiled, and the MP4 one
could pick another sample than `video_frame` when `snap` is on. Both now use
the same lookup as their `video_frame`.
Also repairs the merge of main into this branch: a closing brace was lost in
the #3467 test (the test module did not compile) and a blank line was doubled
(cargo fmt --check failed).
* codecs: test every display turn, crop with turn, drafts and eviction of raw frames
* gpu: give the neutral-corrector test LayerFx its turns field
Merging main added a LayerFx initializer without the turns field, which
breaks compilation of the gpu tests (E0063).
* interchange: refuse repeated AAF storage ownership and bound weak paths, Bento names and FCP7 link lists
* address review: leave clips cut from an over-large link group unlinked
* address review: assert the truncation warning on the export report
* media: validate offline slate audio before allocating silence
The offline slate sources (OfflineSource in media, SlateSource in the
media pool) allocated silence for any requested length at the channel
count stored in the project, with no check. Transcription asks for a
whole item's stored duration, so an offline item with a huge duration or
channel count requested gigabytes. Both now validate the buffer shape
first, like the file readers.
* address review: compute the oversized frame count as u64 in the offline-source test
* render: cap Long Shadow and Glint lengths at the picture extent
Long Shadow doubled a step until it passed twice the stored length: an
infinite length never ended the loop, each pass cloning and scanning the
image. Glint ran one full-image pass per doubling up to three times its
length. Both lengths are now capped at the picture's width plus height,
beyond which a longer shadow or streak looks the same.
* address review: Glint and Long Shadow keep the requested length for weighting and fade; the extent bounds only the passes
* address review: assert the long shadow fade still depends on length
* engine: keep a shortcut preset named "active" from replacing the active set
Custom shortcut presets are saved as "<name>.json" in the same folder as
active.json, the set restored at startup. A preset named "active" (for
example an imported file, even with activation off) therefore overwrote
the active set. Such a preset is now saved as "active (preset).json".
* address review: redirect an 'active' preset to a collision-free file and keep legacy mixed-case presets reachable
* speech: validate the Whisper attention head count
The Whisper attention layer took its head count from config.json without
checking it: zero heads divided by zero in forward, and a huge count sized
every attention-logit tensor. A count must now be 1..=64 and divide
d_model.
* address review: separate head-count and divisibility errors
* flac: consume encoder batches by offset instead of draining per frame
encode_frames drained each block from the front of the pending buffers,
moving the whole remainder per frame, so one large batch cost quadratic
copying. Blocks are now read at an advancing offset and the buffers are
compacted once.
* address review: drop the wall-clock assertion from the large-batch FLAC test
* tts: look for a pause marker's closing bracket only a marker's length ahead
script::parse searched the whole remaining text for a closing bracket after
every '[' and lowercased the candidate, so text with many unclosed or
distant brackets parsed in quadratic time (the speech panel re-parses on
every redraw). A pause marker is short; the search now looks at most 64
characters ahead.
* address review: drop the wall-clock assertion from the linear parse test
* mcp: parse errors wait for the writer instead of queuing without bound
* address review: backpressure test floods 64K lines and polls the write alongside the reads
* address review: stop the MCP reader when output fails; bound the backpressure test drain
* captions: bound caption outline dilation work
The outline max-filter visited every disc offset for every pixel of the
caption overlay, so a large outline radius over a frame-sized caption cost
area * r^2 work (around 10^11 visits at 4K with the 64 px radius cap).
Spread only covered pixels over the disc, which gives the same maximum
because the disc is symmetric, and shrink the radius until covered pixels
times disc offsets fit a fixed work budget.
* captions: test outline radius reduction on a mixed mask
project_subset filtered items, transcripts and narrations to the selected
closure but kept every source_graphics entry, so File > Export Selection as
Project wrote other items' graphic text and metadata into the new file.
decode_cost_ms followed Subclip parents with no guard (only nested
sequences checked depth), so a subclip that names itself, or a cycle of
subclips, recursed until the stack ran out while drawing the render bar.
The depth bound now applies to every source kind.
A dependent slice segment inherited its header by cloning the previous one,
entry-point table included, and kept that allocation after clearing it, so
many short dependent segments each retained a copy of one large table.
The inherited header now starts with an empty table.
unpack accepted a bit depth of 0: the input requirement became zero bytes,
so one source byte produced a header-sized buffer of zero samples. Depths
outside 1..=16 (the u16 output range) are now rejected up front.
* captions: cap the CEA-708 text an MCC import retains
The 708 decoder retains a full snapshot of every visible window each time a
packet changes the display. Window dimensions are honoured, but a packet of
a few bytes (hiding and showing all windows) still retains up to eight full
windows of text, so an MCC file could expand by hundreds of times into
retained strings.
Track the cue text kept and stop decoding 708 data once 16 MiB is reached,
with an import warning.
* address review: stop the 708 import once an open cue exceeds the text budget
* engine: merge imported presets through a name index
presets.import and export.presets.import removed same-named entries with a
retain over the whole user library for every imported preset, so a file of
many distinct names made the import quadratic on the command thread. Both
now go through presets::merge_by_key, which replaces a same-keyed entry in
place and appends new ones, using a map built once.
* address review: keep one library entry per imported preset key; drop wall-clock assert