Commit Graph
3646 Commits
Author SHA1 Message Date
vangel76 5f4e7ffc18 Merge remote-tracking branch 'upstream/main' into aep-audit-3300 2026-11-08 16:21:58 +01:00
vangel76 2e24ae47a1 aep export: the time remap and Transform keys use the shared key-list and match-name helpers; doc comments for the audit's functions (#3300) 2026-11-08 16:21:57 +01:00
FredApps 07be19d402 Merge pull request #3255 from vangel76/aep-import-puppet-3093
AEP import: Puppet arrives as EffectCraft's Puppet, its meshes and pins are reported
2026-11-08 15:46:00 +01:00
FredApps 7ca249d085 Merge pull request #3120 from vangel76/aep-import-thirdparty
.aep import: map CC Color Neutralizer, EXtractoR and IDentifier
2026-11-08 15:45:08 +01:00
FredApps 5832219d76 Merge pull request #3248 from vangel76/text-layer-name-follow-3233
Text layers: the name follows the text until the user renames the layer
2026-11-08 15:44:58 +01:00
FredApps 362b39038f Merge pull request #3256 from vangel76/clamp-time-stretch-3247
Time stretch: hold a loaded or imported stretch to ±1 % to ±9900 %
2026-11-08 15:44:45 +01:00
vangel76 b80ac9edb9 aep: read imports through a flat tree index (#3300)
Import reads an .aep through TreeIndex, one depth-first array of chunks that
borrows the file's bytes, instead of building and freeing the owned Chunk tree
(one Vec per list). The parser mirrors read::Parser step for step: the same
chunk count order against max_chunks, the same depth limit and opaque
fallbacks, the same errors. The typed view reads through Node, a handle to a
chunk of either tree, so the accessors work unchanged on both; .aepx keeps its
owned tree. Chunk, parse and write stay for export and editing.

The array starts small and doubles, never past what the input can hold or the
chunk limit allows; a failed allocation is TooLarge, a tree past u32 indices
TooManyChunks.
2026-11-08 15:00:52 +01:00
vangel76 d2ca544404 Merge remote-tracking branch 'origin/aep-cleanup-3281' into aep-audit-3300 2026-11-08 14:23:06 +01:00
vangel76 a7456fac62 Merge remote-tracking branch 'origin/aep-export-dedup-3278' into aep-audit-3300 2026-11-08 14:23:06 +01:00
vangel76 47d988f658 Merge remote-tracking branch 'origin/aep-export-template-3277' into aep-audit-3300 2026-11-08 14:23:06 +01:00
vangel76 2428b8091e Merge remote-tracking branch 'origin/aep-textdoc-3276' into aep-tree-index-3300 2026-11-08 14:15:51 +01:00
vangel76 d6ec32cbaa aep export: correct the order the natural sort key doc gives for equal numbers (#3277) 2026-11-08 06:19:17 +01:00
vangel76 c8ccf807e1 #3233: AEP import names an unnamed text layer as a following name is cut, so export and import agree 2026-11-08 04:53:01 +01:00
vangel76 e41d6d9d68 Merge remote-tracking branch 'upstream/main' into text-layer-name-follow-3233 2026-11-08 04:42:27 +01:00
vangel76 d2359d607e aep export: the every-layout test counts from a baseline, not from zero (#3278) 2026-11-08 04:41:46 +01:00
vangel76 0643687832 Merge remote-tracking branch 'upstream/main' into aep-container-alloc-3275 2026-11-08 04:36:37 +01:00
vangel76 245e9b6844 aep container: check bdata digits and size before reserving (#3275)
decode_hex reserved the decoded size before it knew the value decodes,
so a large bdata with a bad digit reserved half its length first. It now
refuses a value larger than a chunk's 32-bit size field (TooLarge), checks
every digit in one vectorisable pass, and only then reserves, with
try_reserve_exact so a value that does not fit in memory is an error
instead of an abort.
2026-11-08 04:36:37 +01:00
vangel76 02024d6e32 Merge remote-tracking branch 'upstream/main' into aep-export-template-3277 2026-11-08 04:36:19 +01:00
vangel76 8439daea0b aep export: sort keys as one compact byte string, bounded by the name (#3277)
The cached natural keys were Vecs of (bool, String, u128) segments: a 255-byte
name of alternating letters and digits took about 14 KB, held for every item at
once. Each key is now one byte string whose byte order is the natural order,
at most 5 bytes per name byte. Order and output are unchanged.
2026-11-08 04:36:18 +01:00
vangel76 f5ec122466 aep: drop a local check tool committed by mistake (#3281) 2026-11-08 04:34:49 +01:00
vangel76 66cbb9d89e aep: review fixes for #3281: NTSC table beside TimeBase, real-path NTSC round trip, light and camera defaults on import, doc wording 2026-11-08 04:34:39 +01:00
vangel76 4c1a75aa9e Merge remote-tracking branch 'upstream/main' into aep-cleanup-3281 2026-11-08 04:06:54 +01:00
vangel76 9e2461414a aep: remove test-only container accessors, share report and time-base code (#3281)
- Remove 35 public container accessors that only tests called (header,
  camera/light, keys, footage, masks, shapes, version) and the dead
  StyleMap::switch_name. Tests now check the importer's own reading of the
  same bytes, or the decoder it uses; three import tests gain the values
  the removed checks covered.
- Import and export reports share one loss entry (Loss<C>) with record,
  total, count and summary; the JSON shape is pinned for both.
- Collector::lose_with builds a detail only below the detail cap (callers
  switch later).
- The export uses the container's TimeBase; one NTSC table serves import
  and export; TimeBase::frames saturates like units instead of returning 0.
2026-11-08 04:04:49 +01:00
FredAppsandClaude Opus 5.5 3e0c77ca68 Preview: the sound plays on through stalls, the picture drops frames (#2473) (#3279)
* Preview: the sound plays on through stalls, the picture drops frames (#2473)

A preview whose rendering fell behind stopped its sound at the first frame
that wasn't cached and started it again once frames were cached, which
chopped it into bursts. Now the sound plays on: the picture shows the latest
frame rendered up to the sound, and the prefetch and the cache's eviction
order follow the sound's frame rather than the picture's.

The stall backoff from #3272 no longer has anything to do and is removed.
The start of a preview still waits for its first frames (#947).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Preview: a picture behind its sound never shows past the sound's frame

`latest_cached_before` stopped only on the sound's exact frame, which a
picture off the plan's frame step steps over and a sound behind the picture
in a non-looping preview never reaches, so it could return a frame ahead of
the sound. The walk now stops at the first frame that isn't between the
picture and the sound in play order (across the loop's end once the sound
has wrapped). Also brings `Playback::starting`'s doc up to date.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:03:31 +01:00
vangel76 30a6e571c9 aep: review fixes for #3276 text documents and markers
text_document reads the first btdk in the Source Text record, the documents
text_document_count counts. The old slot checks (same, is_neutral, undecoded_fields)
join the test oracle and are compared on 20 000 generated slot dictionaries; mixed
dictionary/array chains and a document left open at the depth limit join the parser
differential. An escaped string's byte order mark is drained without a copy.
2026-11-08 03:58:10 +01:00
vangel76 9e82a5e704 aep export: review fixes for #3278
- tree::Fixed replaces tree::fixed: a fixed layout writes every field that
  fits (as the old code did) and keeps the first error; finish() logs it
  once with the layout's description and counts it per thread, so the
  every-layout test cannot race with other tests.
- base::bytes logs one line naming its length and fields.
- masks::build_atom errors when the prototype has no Mask Shape instead of
  dropping the path of every mask quietly.
- log sorted into the dependency list.
2026-11-08 03:56:50 +01:00
vangel76 a4f9a97525 aep export: pin the key-time loop's report, name the layout in the base::bytes log (#3278) 2026-11-08 03:43:04 +01:00
vangel76 501e1bd4bf Merge commit 'ae35ea7d' into aep-export-dedup-3278 2026-11-08 03:43:03 +01:00
vangel76 5f37f78117 aep export: one key-time loop, key-list rewrite, tdmn decoder and byte writer for the listed modules (#3278)
In masks, shapes, styles, synth, fxsynth, base, camlight, skeleton,
textanim and tree (effects.rs, plan.rs and emit.rs keep their copies for
the follow-up work):
- New export/keys.rs: the key-time loop (timed_keys), the key-list rewrite
  (fill_key_list, set_key_list, replace_cdat) and the spatial key record,
  shared by masks, shapes and styles.
- One tdmn decoder (synth::tdmn_name, lossy UTF-8 up to the first NUL, as the
  importer reads it) and one encoder (synth::tdmn) instead of the copies in
  masks, styles, shapes, textanim, camlight and skeleton.
- One byte writer: tree::put, which errors. base::bytes, synth::tdb4,
  synth::key_list, fxsynth::pard, masks::shph and the keyed path records write
  through it; their fixed layouts keep the infallible signatures behind
  tree::fixed, which logs, and a test builds every layout and requires no error.
- camlight::put_group uses the shared group split (tree::split_group), which
  errors on a match name without a node instead of stopping quietly.

Export output is byte-identical on the corpus and the synthetic fixtures.
2026-11-08 03:43:01 +01:00
vangel76 6c84c81060 aep: an escaped text string drops its byte order mark without a range drain (#3276) 2026-11-08 03:42:48 +01:00
vangel76 34715bbd86 aep: text-dict test builds its prefix without repeat(1) (#3276) 2026-11-08 03:37:19 +01:00
vangel76 ff5952ee5d Merge remote-tracking branch 'upstream/main' into aep-textdoc-3276 2026-11-08 03:29:45 +01:00
vangel76 32c79babc8 aep: text documents are read without a String per token, defaults built once (#3276)
TextDocument::parse reads a borrowed tree: names, bare words and strings without
escapes point into the body, keys are borrowed when they are /names, numbers parse from
the borrowed bytes, and dictionaries and arrays are moved into vectors of their exact size
when they close. The style and paragraph defaults are built once instead of per document.
parse_text_dict keeps its public TextValue result (the borrowed tree turned owned).

The old owning parser stays as a test-only oracle: a differential test compares both on
40 000 generated bodies (every key form the owned keys canonicalise, non-UTF-8 names,
escapes, unclosed strings, non-finite numbers), every cut of a realistic document and
nesting around the depth limit.
2026-11-08 03:27:12 +01:00
vangel76 cdb81c06ec aep export: note what the concurrency test covers and why a failed template build is not kept (#3277) 2026-11-08 03:26:16 +01:00
vangel76 efa0c94d2b aep container: review fixes (#3275)
- read.rs: reserve with try_reserve_exact, so a raised chunk limit on a
  small target cannot abort before parsing; the capped reservation and a
  deep, retry-heavy nest of broken lists are pinned by tests.
- write.rs: a tree past the 32-bit container size fails with TooLarge
  before anything is serialised.
- aepx.rs: drop an unneeded local and lifetime annotation.
2026-11-08 03:24:05 +01:00
vangel76 d8d1028b3f Merge remote-tracking branch 'upstream/main' into aep-container-alloc-3275 2026-11-08 03:17:17 +01:00
vangel76 38d51dd185 aep container: size sibling lists, hex and write output once; borrow plain .aepx attributes (#3275)
- read.rs: count the chunk headers of a sequence first and allocate the
  children Vec once (bounded by the input and by the chunk limit).
- aepx.rs: decode_hex sizes its output once; attribute values without
  an entity are borrowed instead of copied through decode_entities.
- typed.rs: PropGroup::get compares match names without building a
  String (same result as the lossy compare, invalid UTF-8 included).
- write.rs: reserve the exact output size up front (try_reserve, so a
  hand-built tree cannot abort on the allocation).
- read.rs, settings.rs, values.rs: use the shared bytes.rs readers.

Output is identical on the corpus (tree, written bytes, import).
2026-11-08 03:17:17 +01:00
vangel76 a18e3ce336 Merge remote-tracking branch 'upstream/main' into aep-export-template-3277 2026-11-08 03:15:24 +01:00
vangel76 8bf184a2d4 aep export: build the prototype template once, sort names with cached keys (#3277)
The template every export clones from is immutable; it is now built by the
first export of the process and kept (about 0.93 MB). The name sort builds
each natural key once instead of on every comparison. Root solids move into
the Solids folder instead of being copied. The two key-list rewrites share one
emit-local helper and match names compare as bytes instead of a decoded String.
Output is byte-identical.
2026-11-08 03:15:24 +01:00
vangel76 7694c11170 aep: a layer's markers and Source Text are read where they are stored, not searched for (#3276)
A layer's marker list is a direct child of its root tdgp, and its Source Text record
btds a direct child of the ADBE Text Properties group. Markers walked the whole layer
subtree twice per layer (most layers have no marker list, so the walk always ran to the
end), and the three text-document accessors walked it once each.
2026-11-08 03:10:16 +01:00
FredAppsandClaude Opus 5.5 ae35ea7d54 Preview: each stall doubles the frames the held sound waits for (#3260) (#3272)
* Preview: each stall doubles the frames the held sound waits for (#3260)

A preview that renders just below real time stopped its sound at the first
uncached frame, then started it again as soon as the silent preview kept up
with `ahead` frames cached. Those ran out within a few frames, so the sound
started and stopped over and over, which plays as distortion (#2473, #3260).

Each stall that stops the sound now doubles the cached frames ahead it waits
for before starting again, up to what fits in the RAM preview. A new preview
starts counting from zero.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Preview: cap the held sound's wait at frames the prefetch can keep cached

Once the doubling reached the RAM preview's `fit`, the held sound waited for
offsets 0..fit, but the prefetch only requests offsets below fit - 1, and
while the silent preview moves on its last requests are still rendering. The
sound then stayed silent almost to the end of the preview even after
rendering recovered.

The backoff now stops at half of the frames the prefetch requests, and never
below the wait before any stall. A new test caches only the frames playback
itself requests, with a RAM preview of `ahead + 2` frames.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 03:06:55 +01:00
vangel76 25ae9ad266 Merge remote-tracking branch 'upstream/main' into aep-import-puppet-3093
# Conflicts:
#	docs/aep-format.md
2026-11-08 02:39:14 +01:00
vangel76 b90950c6a2 .aep import: a Channel Info record without its data is reported as damaged
import_extra returned as soon as the arbitrary data was missing, so an
EXtractoR or IDentifier value record whose aRbs/aRbp payload is gone was
silently imported with the default channels; the payload may have chosen
other ones (Z). As for Lumetri's data parameters, a stored record without its
payload now adds a DamagedData loss; a parameter with no record stays the
silent default. The test now asserts the report.
2026-11-08 02:32:56 +01:00
vangel76 77c3fad6c5 #3233: an empty Layer Settings name follows the text again; document the name rule on layer.newText, layer.setText and layer.settings 2026-11-08 02:32:11 +01:00
vangel76 67a39d9a2b .aep import: EXtractoR (copy) reads the effect input, not the EXR plane of that name
After Effects' Channel Info kind 2 "(copy)" was imported as R, G, B or A.
EffectCraft's EXtractoR resolves those names through the footage's aux planes
first, and an OpenEXR keeps its own R/G/B/A planes, so "(copy)" read the
original file plane and dropped what an earlier effect did to the channel.

"(copy)" is now a reserved EXtractoR channel name (EXTRACTOR_COPY) that the
CPU and GPU renderers never look up in the aux planes: it is the input's own
channel of the same position. The importer writes it for kind 2.

Render regression: a modified input buffer with same-named R/G/B/A aux planes
of other values; GPU parity case with (copy) on an aux scene with R/G/B.
2026-11-08 02:32:11 +01: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
vangel76 8af16ac7a4 Merge remote-tracking branch 'upstream/main' into aep-import-thirdparty 2026-11-08 02:19:29 +01:00
Valentin Klein 514e16c027 .aep import: effects without definitions borrow them from another instance (#3223)
Refs #3170 (the Color Control part).

Some files have no project-level effect definition list (EfdG); it was seen in
17 projects, all from inside After Effects-made Motion Graphics templates. In
those, one instance per effect type keeps the full parameter definitions
(parT) and every other instance of the type has an empty parT. The others
were reported "without readable parameter definitions" and dropped (129
Color Controls of one template). The definition index now falls back to the
parT of the first instance of the same match name that has its own
(DefinitionSource::OtherInstance). Values still come from each instance's own
records; a value record that borrowed definitions do not name is reported
instead of vanishing.

The "Color ... holds an unreadable value" report came from 0.21.1, which read
only written records (tdsb 1). Reading default-state records (tdsb 3, #3126)
probably fixes it, but that is not confirmed for Color Control; a synthetic
test now pins a default-state colour record.
2026-11-08 01:57:47 +01:00
Valentin Klein 99e24f5867 .aep import: image sequences without a filename prefix or with Force alphabetical order (#3213)
* AEP import: read image sequences without a filename prefix and with Force alphabetical order (#3174)

After Effects leaves out an empty filename prefix, so only one Utf8 (the
suffix, such as `.png`) follows the alias. The single part is now placed by
the first file name stored in `opti`: a name starting with it has it as the
prefix, one ending with it as the suffix; without that evidence a part
starting with `.` is the suffix and anything else is not read.

A sequence imported with Force alphabetical order stores its files in
`LIST StVc` (`StVS` with the count as a little-endian u32, then one Utf8 per
file name) instead of prefix and suffix; that list now replaces the
numbering. A list with a name that is not a plain file name (a separator, a
colon, `.` or `..`) is not read, since the names are joined to the folder.

A folder alias whose naming still cannot be read is reported as an import
loss instead of silently linking the folder. docs/aep-format.md describes
the empty prefix and StVc/StVS.

* AEP import: a listed sequence file name with a control character such as NUL is not read (#3174)

A NUL or another control character in a Force alphabetical order file name
now makes the whole list unreadable, like a separator or a dot directory, so
the import reports the naming instead of linking files that cannot exist.

* AEP import: a sequence file list without a readable count is not read (#3174)

After Effects writes the StVS count before the names. A StVc list without
StVS, or with one shorter than its u32, is now reported as unreadable naming
instead of taking every listed name.

* Doc comments for the functions this PR touches
2026-11-08 01:33:26 +01:00
vangel76 2e738606f3 Merge remote-tracking branch 'upstream/main' into aep-import-thirdparty 2026-11-07 19:42:21 +01:00