Commit Graph
88 Commits
Author SHA1 Message Date
nazar-gavrylyk 76e4008151 Footage: 8 bpc renders keep plain movie frames as 8-bit levels
An 8 bpc render at Full quantises plain footage straight away, so the
media pool now keeps those movie frames as the 8-bit levels it is
quantised to (a quarter of the memory) and the renderer expands them
into the layer buffer: the same pixels to the bit. Other depths,
reduced renders, masks, colour profiles, fields, frame blending,
Cineon, Invert Alpha, linear light and 10/12-bit or float movies keep
the float path.

First playback of plain 1080p clips at Full caches ~43% faster on the
CPU, and the footage cache holds 4x the frames.
2026-11-10 15:03:10 +01:00
Valentin Klein 7c62272305 .aep import: placed .ai/PDF footage framed by its ArtBox, Null anchors at the corner (#3099) (#3311)
* #3099: placed .ai pages fill the size After Effects stored; null anchors default to the corner

Footage of an .aep keeps the size After Effects stored for a placed PDF / Illustrator / EPS
file (the ArtBox, rounded up), and masks and anchors are measured against it. The page was
drawn as a PDF viewer frames it (CropBox, /Rotate) at its own size, scaled by the width ratio
at full resolution and stretched to the stored aspect below 3/4: masks too large, art off
centre, squeezed at half resolution.

- effectcraft-pdf: Framing::Placed / parse_page_framed (ArtBox clipped to the CropBox,
  /Rotate ignored); boxes with a non-finite corner or span are not used
- Footage::placed_page (set by the .aep importer, serialized only when true): the page is
  framed so and filled into the stored size (effectcraft_render::vector_fit), decoded,
  continuously rasterised and in Create Shapes from Vector Layer; Reload keeps the size;
  the frame cache keys it by size and framing
- .aep import/export: a null's absent Anchor Point is [0, 0], as After Effects reads it
- fixture ai-mask-3099.aep (own synthetic files, scrubbed, profiles stubbed) and tests
  against After Effects 2026's read-back

* #3099: placed-page framing only for .ai and .pdf, EPS as before; doc comments

After Effects' sizing was measured with PDFs named .ai only; an EPS from an .aep keeps its
bounding box at its own size (listed as not measured in docs/gaps.md and docs/aep-import.md).
Doc comments on the touched functions.

* #3099: shapes from a placed page keep its frame (track matte)

Create Shapes from Vector Layer converted every path of a placed page, so bleed, art past the
artboard or the page's background showed where the footage's raster hides them. The layers it
makes from a placed page now take a hidden "<layer> Frame" shape layer (the footage's
width x height) as their Alpha track matte: drawn at the comp's resolution, its edge is the
raster's on shape and image layers alike. The command returns the frame layer's id too.
2026-11-08 21:52:32 -06:00
Valentin KleinandClaude Opus 5.5 84382487bb Disk cache: entries keep the files they read, keys from memory (#3184) (#3191)
* Disk cache: record the source files each frame reads

Part of #3184 (step 1): renders now report which footage files they read,
each with its identity (size and modification time) as read. Nothing uses
the list yet: images, cache keys and cache behaviour are unchanged.

- render: `reads` module with `FileStamp`, `ReadFile`, `ReadFiles` (sorted,
  deduplicated, bounded; marked incomplete when it can't vouch for every
  file) and the thread-safe `ReadLog`. `Renderer::reads` collects a frame's
  files; `FootageSource::recording` lets a source note what it reads.
  `file_stamp` uses the same identity.
- Layer cache entries keep the files their buffer was made of and report
  them on every hit; derived entries (styles, blend space, flattened) keep
  their base's. Buffers from the disk cache or the browser store don't know
  their files yet and mark the list incomplete.
- Media pool: every file read notes its identity, taken from the file it
  opens. Decoded frames, opened movies, GIFs, models and EXR channels keep
  it with the cache entry, so a frame served from memory reports its file
  without touching the disk.
- Viewer frame worker collects each frame's list and carries it to where
  the disk entry is written (step 2 stores it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Automation: confined footage passes on the files it reads

Part of #3184: ConfinedFootage forwards FootageSource::recording to the
source it wraps, so renders of an automation session list their files
instead of coming back incomplete. The read tests also render through the
viewer's CancellableFootage wrapper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: frame entries keep their source files, comp key from memory

Part of #3184 (step 2). Fixes #3175: opening a comp no longer stamps every
footage file of the project before its first frame can show.

- The comp's content key (`disk_cache::comp_content_key`) is taken from the
  project in memory alone: no footage or proxy file is stamped, no
  `VirtualFileId` is asked. `comp_content_key_with` is gone; `frame_key`
  hashes a new entry version, so entries of the earlier format are never
  looked up again (LRU evicts them).
- Frame entries (formats 2 and 3) store the files the frame read
  (`EntryFiles`: size and modification time as read; size and content hash
  through `VirtualFileId` for files from memory; missing files), then the
  pixels. Formats 0 and 1 read as misses and are deleted.
- Check on hit: `get_frame`/`get_frame_f16` look only at the listed files
  (one metadata call each) on the frame worker. A changed, back or unreadable
  file, or a damaged entry, is a miss: the entry is deleted and the frame
  rendered and stored again.
- A frame whose file list is incomplete is shown but not stored. The RAM
  preview keeps each frame's files, so `persist_missing` writes only frames
  with a complete list.
- Remote frame worker: `FrameServer` records each frame's reads
  (`take_files`); the web worker stores them with the entry and the page
  checks them through the file table before using it. The browser's media
  epoch existed only for the old key and is removed. Export does not use
  frame entries.
- The cache indicator still only asks whether an entry exists.

Tests: entry format round trip and damage, the check per file, and in the
viewer: a changed file re-renders exactly its frames (and a reopen reads the
rest from disk); a frame checks only its own files in a project with a large
unused sequence and the key looks at no file; missing, unreadable files and
damaged entries never serve a stale frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: layer entries keep their source files, no footage salt

Part of #3184 (step 3 of 4).

Layer buffers on disk (and in the browser's layer store) now carry the
files they were made of, like frame entries since step 2, and are checked
against exactly those files when used. The project-wide footage salt is
gone: nothing stamps every footage file any more.

- Layer entry format 4: the buffer's EntryFiles, then the pixels. A hit
  checks only the listed files (metadata, or the VirtualFileId hook for
  files from memory; Missing must still be missing); a change, a damaged
  entry or the old format 0 is a miss and the entry is deleted, so the
  re-rendered buffer replaces it. A hit reports its file list, so frames
  made of buffers from disk are complete and get stored again.
- Buffers whose files can't all be named are not written.
- Layer disk keys: layer_disk_key = hash of the layer entry version, the
  renderer version and the layer cache key (which names the footage items
  with their paths and settings, from memory).
- PrefetchStore checks on use; an entry made of changed files is dropped
  and forgotten, so the new buffer is queued over it. FrameServer passes
  its virtual-file hook to its layer cache.
- Removed: footage_salt(_with), file_stamp, footage_stamp,
  set_disk_salt, set_store_salt, Session::refresh_disk_salt,
  poll_disk_salt, footage_fingerprint, disk_salt, disk_salt_background,
  disk_salt_job, DiskSaltJob, the UI's per-frame poll, the salt in
  FrameServer and in the availability worker (with its cache_enabled
  plumbing). The disk cache is attached as soon as it is configured.
- Docs: architecture.md, web.md.

Tests: layer entries check their files (size, time, missing/back,
folder, damaged, old format, incomplete); virtual files; prefetch store;
media tests with a real pool across restarts (only the changed file is
read, projects with the same item ids never share an entry, missing,
unreadable and damaged never serve stale buffers); engine tests that the
cache is attached at once and edits do no file work (#1926, #3106).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: no-salt worker test, sequence paths in the frame-blend key

Part of #3184 (step 3 of 4).

- A frame-blended footage frame's layer-cache key now hashes the
  sequence's file list: with no project-wide salt, two projects whose
  items share an id and a first frame must not share its disk entry.
- Test: a frame worker syncing, patching and rendering a project with an
  unused 5000-frame sequence asks its file hook only about the file its
  buffer reads (the salt asked about every file at every sync).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: the comp key covers what its expressions can read

With an expression anywhere in the project, every comp's disk key hashed
every item of the project, so any edit (another comp, an unused footage
item) threw away every comp's frames on disk. The key now covers what the
comp can draw plus what the expressions among it can reach
(Project::pixel_reach):

- without expressions: the items the comp draws, without silent layers,
  plus the footage its precomp layers swap in (Media Replacement);
- with expressions: every layer of the comps they reach, their markers,
  the comps and footage they name with string literals (comp("Name"),
  footage("Name")) together with which items have those names now, and
  the footage named like a reached source (its data is read by name);
- the whole project when an expression computes a name, uses the object
  model's constructors or internals, dynamic code, computed keys or
  writes, or text the scanner can't read for sure (regex literals,
  template strings, escapes): expression_reach errs towards reaching more.

Markers count only where an expression can read them (#1083). Memory
only, no file is looked at.

Tests: scanner and reach unit tests in the project crate; engine tests
for the key (own-comp expressions keep the key through edits elsewhere,
comp("Other") follows Other and not a third comp, a second comp taking
the name counts, a computed name counts every item, markers read from
another comp); a ui-egui test that the disk frames outside an expression
layer are carried when the comp it names changes.

Part of #3184. Part of #1492.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: Reload Footage drops the entries of its files, docs

File > Reload Footage now deletes the frame and layer entries whose file
lists name the reloaded item's files (DiskCache::drop_files), so a file
replaced by one with the same size and modification time, which the
check on hit can't tell apart, is read again. The index knows keys, not
files: the writer thread reads the files section at the start of each
frame and layer entry (not the pixels) after the writes queued before
it, and deletes the entries that list one of the files. Until it is
done, an entry made of one of them is a miss and frames aren't linked to
new keys. The entries are gone for good, so they don't come back after a
restart. Automatic reloads don't drop anything: they follow a changed
stamp, which the check on hit catches. The browser needs nothing (its
file identities are content hashes).

Removed: disk_cache::seal, which steps 2 and 3 left without callers (the
tests use seal_format). Updated the module, seal_format and forget_media
docs, and docs/architecture.md / docs/web.md describe the final design:
what is cached, keys from memory, file lists checked on hit, nothing
scans the project, incomplete lists stay in memory, Reload Footage drops
entries, edits keep the frames they can't change.

Tests: render unit tests (same size and time passes the check but not
the drop; other files' entries stay; an entry queued before the drop is
dropped; a layer entry too; written again afterwards it is used; the
drop survives a restart; a pending drop is a miss), and an engine test
through file.reloadFootage.

Closes #3184. Part of #1492.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: file stamps keep the whole modification time

A file's modification time was nanoseconds since 1970 as u64: a time before 1970
read as "no time" (two such files of one size looked alike) and times after 2554
were truncated. It is now i128 nanoseconds, negative before the epoch; None only
means the file system has no time. The content-key hash writes a presence tag and
the full value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Automation: a refused footage read leaves the render's file list incomplete

Footage outside the read roots shows nothing and was noted as no file at all, so a
picture made without it had a complete, empty list and could be kept on disk for
a later session with other roots. A refused read during `recording` now marks the
list incomplete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: a buffer kept by another worker comes with its own files

LayerCache::insert returns the files of the buffer it keeps, and Renderer::keep
notes them in the render's log and returns them, so a render that uses another
worker's buffer lists that buffer's files (a file changed between the two reads
then has two identities and the list is incomplete).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: sampleImage and text fonts note the files they read

EvalCtx carries the render's read log. A sampleImage render records its footage
reads, keeps them with its per-thread cached buffer and notes them in the
evaluating render's log on every use. Text notes the font files it is drawn with,
with the identity their bytes were read with; a family that isn't installed, a
fallback or missing glyph, or a font from memory leaves the list incomplete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: frame entries store the whole modification time

A frame entry's file list stores a file's modification time as i128 nanoseconds
since 1970 (negative before), as the file stamps now keep it; the frame entry
format version goes to frame-entry-files-2 so entries in the old layout are not
read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: layer entries store the whole modification time

Layer entries share the frame entries' file list layout, which now stores the
full i128 modification time: the layer entry format version goes to
layer-entry-files-2. The module doc names the font files text is drawn with
among an entry's source files, and what keeps a list incomplete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layer cache: buffers that read other layers are keyed by the switches passed down

A layer whose effect reads another layer (Gradient Wipe, Set Matte, Blend…) gets
that layer drawn by its own renderer. Through a precomp layer, Switches Affect
Nested Comps, the switches inherited from the precomp layers above and motion
blur on change those pixels, but the key held only the layers' own switches: a
buffer made with the setting on was reused (from memory or the disk cache) with
it off. Such layers now derive their key with what the renderer passes down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layer cache: masked buffers are keyed by the mask motion blur gate

A mask with Mask Motion Blur On blurs even when the layer's own switch is off,
as long as the render's gate is open (Render Settings motion blur, and with
Switches Affect Nested Comps the precomp layers above). The key held the
layer's own motion blur state only, so a buffer blurred with the gate open was
reused with it closed (and the other way round), also from the disk cache.
Masked layer, input and footprint keys now tell a closed gate apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: cached buffers list what their expressions and substituted characters read

- Buffers made under `Renderer::recorded` (layer content, a Time effect's input, an adjustment
  layer's footprint) evaluate their masks, shapes and effects with the context that notes into
  the buffer's own read log, so a file an expression reads (`sampleImage`, also at shutter times
  mask motion blur adds and the cache key doesn't see) is in the buffer's files.
- A styled layer served from the cache lists the files of the cached body and passes too, so
  the buffers made of it (flattened, blending space) don't get a complete list when one of them
  came with an incomplete one.
- A character put in by Character Offset or Character Value without a relayout notes the font
  it is drawn with; a fallback face (or none) leaves the list incomplete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: text lists the font file of a run that draws no glyph; tests clean their temp dirs

An empty first paragraph draws no glyph, but in a box its face sets the
first baseline that places every later line. The read list took faces only
from glyphs, so it could be complete without that file. Note each style
run's resolved face as well.

The font-reads and confine tests use tempfile::tempdir(): a fresh
directory per run, removed on panic too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: Reload Footage closes the gaps around its drop (#3184)

- A reload while the disk cache is off is kept on the session
  (pending_disk_drops) and applied when the cache is switched on, before
  anything is read from it.
- Each render takes the cache's drop generation before it reads (frame
  worker; layer cache at the miss). An entry listing a file dropped after
  that is refused under the queue's lock, so a render that was under way
  during the reload can't store pixels from before it. Such a frame is
  shown with an incomplete file list, so persist_missing skips it too.
  The per-file record is bounded (past it, everything older is refused).
- An entry read while its drop was pending is a miss even when the
  writer ends the sweep between the read and the check (a stale hit
  found under load): the pending drops and the generation are taken
  before the entry is read.
- Test: a reopened project serves its frame from disk with no source file
  opened or decoded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: RAM frames keep the drop generation their render started at

Reload Footage empties the RAM preview, but an idle pass of
persist_missing may already hold a frame from before the reload. It
took the cache's drop generation when it ran, after the drop, so the old
pixels could be written back and, with the file's size and time
unchanged, served again after a restart.

Each RAM frame now keeps the disk cache its render started with and
that cache's drop generation then. persist_missing writes a frame only
to that cache, with that generation, and skips it once one of its files
was dropped since; a frame rendered without that cache is not written
from there.

Tests use tempfile directories (the layer drop test, the frame_files
folders under target/).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Doc comments for the functions this PR touches

* Disk cache: cap the free-space reserve at 10 GB, skip encodes the limit refuses

The reserve the cache leaves free on its disk was max(2 GB, 5% of the
disk) (#2551). On a 4 TB disk with 190 GB free that is 200 GB, so a
cache set to 20 GB had a limit of 0: every write was refused, the cache
never held anything, and only a log warning said so.

The reserve is now clamp(5% of the disk, 2 GB, 10 GB). It is there to
keep the cache from filling the disk, not to keep a share of a large
disk free: 10 GB leaves the system room to work on any disk, a small
disk still keeps 2 GB, and a 100 GB disk keeps 5 GB as before. After
Effects has only a maximum cache size and no reserve; a clamp keeps
the floor for small disks, where a fixed 10 GB would take most of the
space.

Writes the limit refuses cost no work: DiskCache::accepts_writes (one
atomic read) is checked before a frame is converted (put_disk_frame)
and before frames and layers are compressed (put_frame, put_frame_f16,
put_layer). The frame workers spent about 8 ms per frame and 14 ms per
layer on encodes that were then refused. persist_missing returns at
once at limit 0 instead of converting the RAM frames on every idle pass.

Settings > Disk shows the limit in effect in MB under Maximum Disk
Cache Size, with a note when free disk space lowers it
(settings.disk.cacheLimit).

* Disk cache: refuse writes below 64 MB of room, show this disk's reserve

Review follow-ups to the reserve fix:

- accepts_writes also refuses while free disk space leaves less than
  64 MB of room (a limit the setting asks for is always used): a few
  frames would fill it, and the rest would be compressed only to be
  refused.
- While writes are refused the idle passes ask for no repaint, so the
  UI keeps a frame coming every 5 s: the free space is read again and
  the RAM frames are written once there is room.
- Settings > Disk names the space kept free on this disk (the reserve
  the last reading applied; the rule from MIN/MAX_DISK_RESERVE when it
  is unknown), and the "no room" note follows accepts_writes.
- The settings.disk.cacheLimit automation label is machine-readable
  and the same in every language: limitMb, maxMb, reserveMb,
  spaceLimited, writes.
- cargo fmt.

* Disk cache: follow 3D model sources in the comp's reach

The reach walk followed comp, footage and solid sources but not
LayerSource::Model. An expression reading a model layer's source
(thisComp.layer("Model").source.name) left the model item out of the
comp's content key, so renaming the model kept the key and the old
frames on disk were served. Every source item is followed now
(LayerSource::item), which also counts the model when no expression
reads it (Advanced 3D draws it).

The same omission is fixed in the RAM preview's content walk
(frames.rs) and in the layer key's walk of referenced layers
(cache/dependencies.rs).

* Disk cache: a render's drop generation travels with its reads; no disk entries for Expression Selectors

Drop generation per render: the layer cache recovered a buffer's drop
generation from the latest miss of its key, so a second render of the
key that started after File > Reload Footage lent its generation to a
render still running from before, and that render's buffer (the file as
it was) was written after the sweep. Now Renderer::recorded starts each
buffer's read log at the store's drop generation before anything is
read (ReadLog::starting_at); ReadFiles carries the earliest generation
through merges, so derived buffers (styles, the blend-space copy, the
flattened layer) and frames that use a buffer from memory carry it too.
LayerCache::insert keeps a buffer made of a file dropped since its
reads began neither on disk nor in memory; a list that doesn't say
counts as made before every drop. DiskCache::dropped_since takes the
list's own generation too. missed_at keeps only the miss time.

Expression Selectors: the layer key evaluates a text Expression
Selector's amount for the first character only, so a per-character
dependency such as textIndex == 1 ? 0 : footage("ref.png").width was
not in the key, and after the footage item's width changed a reopened
session reused the old layer entry. A text layer with an Expression
Selector now marks its reads unkeyed (ReadFiles::is_unkeyed), which
merges like the generation; such buffers stay in memory and are never
written to disk. Frame entries are unaffected: the comp's frame key
hashes what the expressions name. The equality and hash of ReadFiles
ignore both new fields.

* Disk cache: never serve a RAM layer buffer made before a drop of its files

A render that passed the drop check could sit in put_layer's encode
while Reload Footage cleared the layer cache and dropped the file, then
publish its old pixels to memory after the purge; get, and a fresh
render's insert, then served them. The disk-hit promotion went through
the same insert_mem.

Every way the cache hands out a RAM buffer (get, the kept-first path of
insert, an existing entry in insert_mem) now checks the entry's files
against the store's drops from their own generation (ReadFiles::since):
an entry made before a drop of one of its files is forgotten and the
lookup misses, so a fresh render's buffer replaces it. insert_mem also
re-checks a buffer just before publishing it.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 02:30:59 +01:00
52bca964f5 Image sequences: a gap in the numbering holds the previous frame, as After Effects (#3137)
* Image sequences: a gap in the numbering holds the previous frame, as After Effects

Split out of #1876 (originally 7850cbf4), because it reverses the colour bars
documented in #297 and needs its own discussion.

After Effects 2026 shows the last existing file across a gap in a sequence's
numbering (files 1, 2, 4, 5 show frame 2 at frame 3; read back from After
Effects). EffectCraft drew colour bars there. The media pool now holds the
previous file (Footage::sequence_file_held); colour bars remain only where no
file precedes the frame. Missing-frame reports on import are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Image sequences: hold the first file of the preceding number across a gap; fix the import warning

A gap now holds the file sequence_file picks for the preceding number, so
files sharing a number do not switch at the gap. The Report Missing Frames
warning says the previous frame is held instead of "shown as placeholders".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Image sequences: Interpret Footage says a gap holds the previous frame

The Interpret Footage note for a numbered sequence with gaps still said the
missing frames are shown as colour bars; the media pool holds the previous
frame there now. The note, its comment and two test doc comments say so,
and a forms test checks the note's wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-07 00:40:24 +01:00
vangel76andClaude Opus 5.5 0d2153e259 test(media): review follow-ups for the aux footage set (#474)
- A watch-folder render gives the host session's set back after its own
  project replaced it on the shared footage source.
- Guard gpu fx_depth::IDS against reads_aux drifting apart.
- Count picture and channel decodes per thread instead of reading the
  shared channel cache, so parallel tests can't make the checks flaky.
- The engine test fails when no set was ever sent.
- Docs: Media Replacement items are learned at their first frame.

Refs #474

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 03:27:25 +01:00
vangel76andClaude Opus 5.5 5b31b96cde perf(media): tell the media pool which EXR footage effects read (#474)
The pool learned that an effect reads a footage item's channels only at the
first aux() call, so the first frame was decoded twice (picture, then in
full), and the hint was never cleared after the effect was removed.

The session now gives the footage source the whole set of files read by
enabled 3D Channel effects (FootageSource::set_aux_footage) with every
project change (Session::bump: edits, undo, redo, history jumps, rollbacks,
project open), sending it only when it changed. The pool keeps that set and,
as a safety net, what aux() learns; a new set drops what was learned.

Refs #474

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 03:21:40 +01:00
vangel76andClaude Opus 5.5 eedd64906c #474: keep files in use in the channel cache, cap plane memory, fall back on a missing cache entry
Review follow-ups:
- Files in use are never dropped from the channel cache: planes an effect
  holds, the files of the last 4 pictures made of their planes until their
  effects ask for the channels, and decodes in progress, which now count
  the planes they will allocate (from the header) against the budget. With
  4 frames at once over a budget of one file, each file decodes once (test).
- A decode that panics removes its entry instead of leaving it decoding.
- One decode allocates at most 8 GiB of planes (512 MiB on 32-bit and
  wasm); a header asking for more is refused with the reason.
- A shared picture whose file has no cached channels (a failed read, a
  broken part it doesn't use) is decoded from its own channels.
- Comment why waiting on a decode may block a rayon worker; drop the
  subsampling check and test (the exr crate refuses subsampled files).
- Docs: the cache is a soft bound; a single-part file still decompresses
  every channel; the first frame and the image crate's route decode twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 02:25:07 +01:00
vangel76andClaude Opus 5.5 8add8ac7b4 #474: decode a multi-layer EXR once per frame, and only its picture when nothing else reads it
- The channel cache decodes a file once however many callers ask for it at
  the same time (the others wait for that decode), and is bounded by the
  bytes of its planes (1 GiB, least recently used out first, a file counting
  as used when its decode finishes, the newest file always kept) instead of
  four files of any size.
- Once an effect has read a footage item's channels, its pictures are made
  of the cached planes, also for files with an unnamed RGB layer (bit for
  bit what the image crate shows, which still decodes the files the planes
  can't stand in for); footage alone decodes only its picture's channels
  (a multi-layer file's were all decoded and cached for every frame).
- Channels are decoded straight into their display-window planes, and the
  picture is converted from the planes without two interleaved copies.

Measured on a 30-frame 2560x1440 sequence of 32 channels (DWAA), one frame
at a time: footage alone 0.185 -> 0.073 s per frame and 4.4 -> 1.8 GB peak;
with an unnamed RGB layer plus Cryptomatte 0.244 -> 0.159 s; multi-part
plus EXtractoR 0.194 -> 0.127 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 02:06:02 +01:00
tylerjenningsw e675df7cab Media: downmix AAC by where each decoded channel plays (#1701)
The gains of an AAC source now come from the configuration FilmCraft decodes
it with, read from MP4/MOV (esds), Matroska (CodecPrivate) and LATM in MPEG
TS/PS (its first StreamMuxConfig). Channel configurations 3 to 6, 11 and 12
arrive reordered to WAVE order, 4 as L R C Cs, 7 in element order. A program
config element goes by its element positions, except the six structures in
which ffmpeg's encoder fills its own channel order across them (an LFE in a
side SCE, a back centre in a side pair), recognised by its "Lavc" comment.
ADTS can only signal configurations 1 to 7 that FilmCraft decodes, which the
channel count tells apart. A decode of fewer channels than the layout keeps
its first ones (7 of configuration 7's 8 in MPEG-TS LATM).
2026-10-29 18:20:08 -04:00
tylerjenningsw 8bb5dbb81e Media: downmix AAC in the order its channel configuration decodes in (#1701)
FilmCraft reorders AAC configurations 3 to 6, 11 and 12 to WAVE/SMPTE order
and leaves configuration 7 and program config elements as coded, so the
channel count alone can't say which order arrives: ffmpeg writes 7.1 as
configuration 7 (C L R first) and 6.1 as a PCE it fills in its own order
(L R C LFE Cs Ls Rs), while FilmCraft's encoder writes a centre-first PCE.
The order is now read from an MP4/MOV's AudioSpecificConfig when the source
opens; other containers keep a channel-count guess.

The ffmpeg oracle test gains configuration 12 (ffmpeg's 7.1 relabelled) and
skips PCE files ffmpeg 4.2 marks as stereo, which FilmCraft reads as two
channels.
2026-10-29 17:52:57 -04:00
tylerjenningsw 3390e2fff7 Merge remote-tracking branch 'origin/main' into pr1736-avi-backend
# Conflicts:
#	crates/media/src/pool.rs
2026-10-29 17:11:16 -04:00
BFlat fbd7bcd957 Merge pull request #2608 from storytold/fix/2141-header-sized-allocations
Media: header-sized EXR windows and audio sample rates are bounded (#2141)
2026-10-30 03:36:36 +07:00
bflatasticandClaude Opus 5.5 4f20c31c37 Media: header-sized EXR windows and audio sample rates are bounded (#2141)
An EXR's display window sizes a plane per channel, so 60000² over a 1×1 data window
was a multi-gigabyte allocation per channel; past 16384² (the vector footage limit)
the file is refused. An audio header's sample rate scales the samples read, so
0xFFFFFFFF Hz made one conform chunk about 47 GB; rates outside 1–768 kHz are an
error, and the audio reads as silence as for any unreadable source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 03:36:06 +07:00
bflatasticandClaude Opus 5.5 2510e17421 Footage reads refuse anything that isn't a regular file (#2134)
A project can name /dev/zero, a FIFO or a folder as footage; reading those whole
never ended (or ran out of memory). The media layer's whole-file reads (probe,
pool, model side files) and the engine's FsServices::read_file now open the file,
refuse a non-regular one, and reserve its length fallibly, so a sparse file that
claims more than memory is an error too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 03:33:47 +07:00
midasmatterhorn e60f654cc6 media: a model reference into another machine finds its file beside the model, so the pool test asserts the local lookup 2026-10-27 02:53:11 +03:00
skylar taylor-barrick 69b9ce2bd1 M0.37: reconcile shared docking and qualify migration follow-up 2026-10-26 06:47:07 +01:00
tylerjenningsw 42b44c460a Merge main into enhance/filmcraft-avi-backend 2026-10-25 14:13:46 -04:00
Dylan M. TaylorandClaude Sonnet 5.5 4e9fa51f12 sonar: method paths instead of closures (rust:S1612)
Poison recovery, OsStr::to_str and char predicates only; S4325 casts are
f64 to f32 and stay.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 08:17:16 +00:00
aiongg 83af3ac56c Batch fix: media: the zero-rate audio test asserts that the read succeeds (#2182) 2026-10-24 00:37:13 +00:00
aiongg 662b620be4 Merge #2182: Regression tests for #354, #382, #979 and #1497 (@skylartaylor) 2026-10-23 23:32:52 +00:00
aiongg 74f7e76f41 Merge #1917: #1812: stills at reduced resolution are resampled once, and their layers cached (@ded-achtung) 2026-10-23 21:34:54 +00:00
skylar taylor-barrick b0c9d47725 Merge remote-tracking branch 'origin/main' into test/backfill-regression-tests 2026-10-23 12:09:54 -06:00
aiongg 89b8db7718 Footage, Lottie and models: resolve stored paths with craft-docpath's resolve_absolute (#1975)
* Footage: use resolve_absolute for stored paths

* Lottie and models: use resolve_absolute for linked files

* CI: release only with git dependencies from storytold

* Use the shared git-sources check and pin checkout in release.yml

* Resolve a model's side files through resolve_absolute; report C:\x as invalid

* CI: parse cargo config, including the legacy .cargo/config, in the git-sources check
2026-10-23 13:02:36 -04:00
skylar taylor-barrick 1c3522be4a #382: test that audio decoding at a zero mix rate does not divide by zero
Resampling footage audio divided by the requested mix rate, so a rate of 0 panicked. The test decodes a 44.1 kHz WAV at rate 0 and expects a read of the requested length. Without the guard it panics with 'attempt to divide by zero'.
2026-10-22 18:55:49 -06:00
dedandClaude Opus 5.5 4cf6580b19 G5: a reduced still's cache key includes its nominal size (#1812)
Review: two footage items of one file with different nominal sizes could share a sized entry,
though one is resampled and the other kept as decoded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:42:22 +03:00
dedandClaude Opus 5.5 ea1f2fe984 G5: a still decoded larger than its nominal size is resampled like the renderer does (#1812)
Review: still_at_size returned a smart object's pixels unresampled when they differed from the
footage's nominal size. It now follows footage_buf: pixels larger than nominal are resampled to
the requested size, smaller ones are left for the renderer to scale up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 04:21:14 +03:00
dedandClaude Opus 5.5 c33d5c20df G5: a still asked for at its own size is the unsized frame (#1812)
Review: frame_at_size at a still's native size made a second cache key, so the pool decoded
and kept a second full-size copy of a frame thumbnail() or frame() already had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 04:01:07 +03:00
dedandClaude Opus 5.5 57326dc6a7 G5: stills at a reduced render size are resampled once and kept at that size (#1812)
The media pool honoured the requested size only for movies; stills came back
at full size and the renderer resampled them on every frame. A 21120 px wide
plate at Quarter was re-filtered each frame, and four of them (1.85 GB as f32)
did not fit the 1 GiB pool, so they were also decoded again and again.

Stills are now resampled once with effectcraft_raster::resample (the same
call the renderer made, so pixels are unchanged) and the pool keeps the
reduced copy under the sized key.

#1812 project at Quarter, CLI bench: 410 ms -> 33 ms a frame (26 ms with the
layer cache); desktop playback 2% -> 60-99% of real time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 03:43:33 +03:00
aiongg 816ca95800 Footage: follow stored footage paths only on this computer or the project's share
Footage paths stored in a project are resolved through craft-docpath against
the project file when it is opened or imported: relative paths are taken from
the project's folder, and an item whose path is on another computer, names a
device or is a non-file URL is marked missing without being looked at. The
footage check, the changed-on-disk scan, Reload Footage, Relink, Edit Original
and Reveal in Finder resolve the path again before touching the file, and the
Project panel, Metadata panel and waveforms read no file details of missing
footage.

Lottie image assets are resolved against the Lottie file; one on another
computer is skipped with a warning. Files a 3D model names are read only when
local or on the model's own share.

Reveal in Finder refuses a folder that would open as an app or other bundle.

Files imported or relinked in this session are opened as given.
2026-10-16 17:43:58 +00:00
vangel76andClaude Sonnet 5.5 93b06a6eab Bump FilmCraft to 4e2d0bc2 (part of #804)
Problem: the hardware-decoding work (#819) needs a newer FilmCraft than the
pinned 2c5ba7a, and the pin bump should be reviewable apart from it.

Fix: move all filmcraft-* deps to 4e2d0bc2. Adapt to MediaInfo.audio ->
audio_streams and VideoStreamInfo.hdr; ask the pool for a quarter into the
frame because FilmCraft now rounds requests half up; adapt rotation and the
AAC downmix; use Decoder::with_threads(1) in the VP9 tests. No
filmcraft-platform, no hardware setting.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 22:56:38 +02:00
Brandon ThomasandClaude Opus 5.5 6baff9f5fe Integration fixes for the community batch
- Footage sources: the cancellable wrapper (#784) forwards frame_with (#752 float curves)
  and problem (#887); problem's default lives on the trait.
- Media: a called-off decode (#784) is not a footage problem (#887).
- Effect Controls rename (#953): Escape cancels (egui drops focus on Escape the same
  frame), the Rename item is translated; regression test.
- i18n (#873): drop the /out-i18n/ ignore rule and the note about an uncommitted script.
- Tests: text range selector caps (#926) re-borrow the layer; the RAM preview test (#793)
  sets the comp's own Resolution (#462).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:27:59 -04:00
Brandon Thomas 02c134fb41 Merge pull request #904 from vangel76/fix-903-multichannel-downmix
#903: downmix 5.1 and other multichannel audio to stereo

# Conflicts:
#	crates/media/src/lib.rs
2026-10-12 04:20:49 -04:00
Brandon Thomas ab12890a9e Merge pull request #887 from vangel76/fix-886-unsupported-video-notice
#886: say why footage the decoders can't read shows no picture

# Conflicts:
#	crates/media/src/pool.rs
#	crates/media/tests/footage.rs
#	crates/render/src/lib.rs
2026-10-12 04:20:43 -04:00
Brandon ThomasandClaude Opus 5.5 6d4e721d2c Media: Targa/HDR stills keep their Exif orientation; DPX test accepts a stated transfer
#755 decoded stills of an extension-decided format with load_from_memory_with_format,
bypassing #827's oriented decode: load_oriented now takes the format.
The DPX ffmpeg oracle test expected the bare codec name; ffmpeg builds that write
the transfer characteristic make it "DPX 8-bit RGB, linear".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:45:49 -04:00
Brandon ThomasandClaude Opus 5.5 42ef79a2fd Media: movies opened in place keep their orientation (#796 × #827)
Opening movies in place (#796) skipped the whole-file bytes #827 read the
track matrix from, so the pool and the probe lost the turn for movies on
disk. stream::movie_header reads just the ftyp and moov boxes (bounded,
positional reads) and both in-place paths derive the orientation from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:37:34 -04:00
Brandon Thomas c11740f461 Merge pull request #827 from pravdomil/claude/import-exif-orientation
Import: stills and movies respect their orientation

# Conflicts:
#	crates/media/src/pool.rs
2026-10-12 03:23:08 -04:00
Brandon Thomas 4e8e6e10b1 Merge pull request #796 from vangel76/fix-stream-video-open
#794: open movies in place instead of reading them whole

# Conflicts:
#	crates/media/src/lib.rs
2026-10-12 03:22:57 -04:00
Brandon Thomas 565fd4118d Merge pull request #784 from vangel76/fix-329-seek-latency
#329: scrubbing a long movie: superseded viewer frames call off their decodes

# Conflicts:
#	crates/media/tests/footage.rs
2026-10-12 03:22:50 -04:00
Brandon Thomas 1210ba91f4 Merge pull request #768 from vangel76/fix-762-animated-webp
#762: import animated WebP as footage with its frames

# Conflicts:
#	docs/file-format-parity.md
2026-10-12 03:22:30 -04:00
Brandon Thomas 3d02e65f2b Merge pull request #765 from vangel76/gap-dpx-import
#756: import DPX and Cineon stills and sequences

# Conflicts:
#	crates/media/tests/footage.rs
#	crates/ui-egui/src/menus.rs
#	docs/file-format-parity.md
#	docs/gaps.md
2026-10-12 03:20:04 -04:00
Brandon Thomas 05f0d9ec62 Merge pull request #755 from vangel76/gap-tga-hdr-import
#745: import Targa (.tga) and Radiance HDR (.hdr) stills and sequences

# Conflicts:
#	crates/ui-egui/src/menus.rs
2026-10-12 03:20:03 -04:00
vangel76andClaude Sonnet 5.5 64fabbb2fc #903: downmix 5.1 and other multichannel audio to stereo
Problem: footage audio kept only its first two channels, so the centre, LFE and surrounds of a 5.1 file were dropped. AAC 5.1 was worse: FilmCraft's AAC-LC decoder delivers AAC's own order (C L R Ls Rs LFE), so the left output was the centre and front-right was lost.

Fix: crates/media/src/downmix.rs holds one ITU-R BS.775 stereo downmix (centre and surrounds at -3 dB, LFE left out, fixed normalisation by the largest row sum so full scale cannot clip), with channel orders for 3 to 8 channels in WAVE/SMPTE order and AAC's native order, and only the front pair for more than 8. MediaPool::read_stereo applies it, which preview, waveform and export mixdown all read through. Mono and stereo are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 20:36:41 +02:00
vangel76andClaude Sonnet 5.5 74b95b0a9d #886: say why footage the decoders can't read shows no picture
Problem: a movie whose profile the software decoders lack (H.264 High 4:2:2 / 10-bit)
probes fine, then every frame read fails; MediaPool logged the error per frame and returned
None, so the layer rendered black with no sign why in the viewer, Project panel or for agents.

Fix: MediaPool records the reason once per file (codec from the probe plus the decoder's text
without error wrappers; I/O errors stay with missing-footage handling), logs it once, and
exposes it through the new FootageSource::problem. The Composition viewer shows it on the
affected layer, the Project panel marks the item with a warning whose tooltip is the reason,
and project.summary / footage.info carry a "problem" field. Cleared when the file is
forgotten; only set when decoding really failed, so a hardware decoder that handles the file
shows nothing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 18:49:54 +02:00
pravdomilandClaude Opus 5.5 c03b0a7aa7 Import: MP4/MOV movies respect their rotation
A movie shot with a phone held upright stores sideways frames and says so
in its video track header; it imported lying on its side. Probing now
reports the turned size, and decoded frames (full and reduced size) are
turned to match. Only the rotation is used, snapped to a quarter turn;
mirroring, scaling and offsets in the header are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:30:44 +02:00
pravdomilandClaude Opus 5.5 683ec372d9 Import: stills respect their Exif orientation
A photo taken with the camera held sideways (or a mirrored scan) imported
as stored, lying on its side. Probing now reports the size the picture is
seen at, and the still and smart-object decodes apply the rotation or
mirroring to the pixels. A missing or invalid tag decodes as stored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:03:47 +02:00
vangel76andClaude Opus 5.5 d648026ffd #794: open movies in place instead of reading them whole
Problem: probe() and MediaPool's movie sources called std::fs::read and then
filmcraft_codecs::open_bytes, so a clip cost its full size in memory, twice
while probing (a 245 MiB MP4 peaked at 494 MiB just to import).

Fix: container footage on disk is opened through a positional-read
ByteReader (stream::FileReader) and FilmCraft's reader openers
(filmcraft_media::reader::open_reader; MP4/MOV, Matroska/WebM, MXF, Ogg,
MPEG-PS/TS), which read the index and then single samples. Probing takes this
path only when nothing probed before containers (still/model extensions,
PSD/SVG/PDF, image magic) claims the file; everything else, whole-file audio
formats, in-memory files (add_bytes) and wasm32 builds keep the byte path.
Regression test: probing and decoding a frame and audio of a movie with a
256 MiB tail grows peak RSS by <64 MiB (512 MiB before), same results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:42:24 +02:00
vangel76andClaude Opus 5.5 a0decc80dd #329: superseded viewer frames call off their footage decodes
Problem: scrubbing a long movie showed the frame under the pointer
seconds after the pointer stopped. Every viewer frame a free frame-pool
thread had started kept running: each seeked the movie's one decoder and
decoded up to a GOP, and the newest frame waited behind all of them on
the decoder's lock (45 min 1080p H.264/HEVC, GOP 250: 1.8-3.6 s for the
last frame of a drag; a single jump is 0.1-0.4 s). FilmCraft's decoder
already polls a cancellation flag, but nothing set one.

Fix: FootageSource::cancellable / called_off and CancellableFootage (a
per-render view whose frame reads run under the render's own flag, so
reads on other threads answer to the right render). MediaPool runs them
under filmcraft_media::cancel::with_cancel. The frame workers render
through it, and a newer viewer frame calls off the comp's running viewer
frames of other times. A called-off render is not cached (frames, disk,
layer cache). Drag latency is now 0.1-0.3 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:01:54 +02:00
vangel76andClaude Opus 5.5 5ba0c60a15 #762: import animated WebP as footage with its frames
Problem: an animated WebP (e.g. ffmpeg -c:v libwebp_anim) imported as a
still and showed only its first frame; probe_bytes handed every WebP to
the still path. Found while measuring #719.

Fix: a webp_anim module, following the animated GIF path. Probing reads
the RIFF chunk headers (VP8X animation and alpha flags, one ANMF per
frame) and describes a WebP with two or more frames as Video footage
(codec "WebP", alpha Straight or Ignore from VP8X). Frame delays give the
rate: delays within 1 ms of each other read as the standard rate they
round to (24, 25, 29.97...), else 1000 / gcd of the delays so uneven
timing is exact; delays of 10 ms or less count as 100 ms as in browsers.
Frames are decoded once with the image crate's WebP AnimationDecoder and
cached in the pool's existing GIF slot; gif_anim::Anim now keeps frame
starts as Ticks and serves both formats. Canvas x frames beyond the
existing memory cap is refused before decoding; a damaged tail keeps the
frames before it; a file without frames is a clear decode error. Still
WebP stays a still.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:29:26 +02:00
vangel76andClaude Opus 5.5 557c0bdbba #756: import DPX and Cineon stills and sequences
Problem: the Media Browser listed .dpx files and After Effects imports
DPX and Cineon scans (stills and numbered sequences), but import
refused them: nothing could decode either format, and .cin was not even
an image to the browser, so its runs never grouped into sequences.

Fix: a small pure-Rust reader written from the public SMPTE 268M and
Kodak Cineon specifications (crates/media/src/dpx.rs): luma, RGB, RGBA
and ABGR at 8/10/12/16 bits, 10-bit filled method A/B, 12-bit in 16-bit
words, both byte orders, lines padded to 32-bit words, unrotated
orientations and the pixel aspect ratio; the codec names the transfer
(log, linear, ...) and code values are shown as stored. RLE, signed,
float, Y'CbCr, packed 10/12-bit and transposed files are refused with a
clear error; every header field is bounds-checked and the data length
verified before allocating. Probe and frame decode use it first;
is_importable, the Media Browser kinds and the import dialog list
.dpx/.cin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:24:16 +02:00
vangel76andClaude Opus 5.5 cf8b9aebc9 #750: float footage follows Working Gamma and is encoded once
Problem: with no working space and Working Gamma 2.4, a linear EXR still
converted with the sRGB curve (0.18 -> 0.4627, After Effects 0.4902):
float footage was sRGB-encoded at decode, before the render knew the
project's curve. Interpret As Linear Light on float footage then encoded
a second time (0.18 -> 0.7255).

Fix: FootageSource::frame_with asks for float footage with a FloatCurve
(sRGB, a pure 2.4 power curve, or as stored). The renderer asks for the
Working Gamma's curve when unmanaged, sRGB when managed, and the stored
values when Interpret As Linear Light encodes them afterwards.
MediaPool decodes float values that way (Preserve RGB still reads them
as stored) and caches each curve apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:03:10 +02:00