Since #595 (v0.19.0) a playing preview and Cache Frames When Idle render one frame per
lane, a lane per core, each recording, submitting and reading back a whole frame on the
GPU the window draws with. On a GPU that can't keep up, the window froze for hundreds of
milliseconds to seconds and the playhead jumped ahead when it came back.
Lanes now take a GPU frame only while fewer than a limit of them composite. The limit
starts at one lane and adapts to the window's frame gaps while a preview plays: a stall
(>100 ms) with lanes compositing halves it, half a second of smooth frames (<50 ms) with
every allowed lane busy adds one, up to every lane; a limit that stalled is tried again
only after 10 s. Frames on the CPU (Software Only, no or retired GPU, GPU memory
pressure) and disk-cache hits use every lane; the pool and the viewer's frame are not
limited, and frames ahead of the playhead still start first.
* WebAssembly ABI 2: neighbouring frames for effect plug-ins (#2773)
A wasm plug-in can export ec_api_version 2 and ec_render2 to receive the layer's previous and next frames, their layer times and validity flags. Neighbours come from PluginHost::input_at, are resampled onto the current grid, and are zeroed when absent. ABI 1 modules (ec_render) load and render unchanged; the manifest api stays independent. A layer with an ABI 2 plug-in keys the render cache on layer time; the Plug-in Library accepts ABI 1-2 releases. Docs, help text and tests updated.
* Fix ABI 2 neighbour times on NTSC frame boundaries
ime * fps is often a few ulps off an integer at rates such as 24000/1001. The old boundary test then took the bracket branch and passed the current frame back as prev (or
ext). Treat a near-integer as a frame boundary within 1e-6 frames, and test 2400 frames at 24000/1001: every boundary must get the frames either side. Also document the loader helpers for the docstring coverage check.
---------
Co-authored-by: justanshh <294019246+justanshh@users.noreply.github.com>
* #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.
Related to #1365. Display labels only; effect, command, parameter, panel and serialized ids and the AEP/script match-name tables are unchanged.
- 69 Cycore CC effects, Key Light, Mocha shape, Lumetri Color/Scopes and the Mercury renderer labels get descriptive names; the old names stay case-insensitive lookup and search aliases.
- Backend::parse accepts the new renderer labels, so reported settings round-trip.
- Display names are unique per language in every effect catalog (new test).
- Warp Stabilizer and Roto Brush keep their names until RFC-0029 settles.
- Mapping in docs/feature-naming.md.
read_aepx, read_aepx_with_limits and parse_any build the owned chunk
tree element by element again, as before the .aepx index, instead of
converting the index (which held the decoded buffer and the owned tree
at the same time). Import keeps the index reader. The tests now
compare the two production readers.
The .aepx reader no longer builds the owned chunk tree element by
element. It decodes every body (hex, text, alias JSON, numbers) into
one buffer and records the chunks as depth-first entries whose bodies
are ranges of that buffer; TreeIndex::from_drafts turns them into the
index the importer reads, which borrows the buffer. read_aepx converts
that index into the owned tree in one pass.
The tree and the errors are those of the previous reader, which the
tests keep as the reference: generated and damaged documents under
seven limit sets, invalid UTF-8, hostile input, the fixture directory,
and imports through both. Bound tests pin the chunk list to the input
and the limit, and check that dropped content leaves nothing behind.
The container's `Pard` is now the only decoder of parameter definitions:
`EffectParam` delegates to it and the export's test reference reads its
encoded table back through it (no big-endian readers of its own). The
export stores a checkbox default as on/off, as the importer reads it.
Inside a strongly connected component of candidate precomp links (a file
with a precomp loop), the per-composition searches share one budget of
link visits; past it, a layer that would need a search is refused and
reported, which never lets a loop in. Below the budget the decisions are
the old ones exactly.
Import: precomp loop check by candidate-graph components (no search
without a loop), one LayerId -> index map for mattes, parents and effect
layer references, popup mapping linear in the file's items, lazy loss
details (lose_with), orientation keys at one time reported, mask path
ease speeds under the property limit, Blending Options nested past the
read depth reported.
Export: precomp loops by components, parent loops found once, cached
natural sort keys, one exported-layer set, parameter definitions built
from the effect table (old encode-and-parse path kept as a test
reference), shared key-time loop, key-list rewrite, spatial record,
tdmn/tdsn helpers, unreadable curve and mesh entries reported.
Corpus import and export dumps are byte-identical to the base.
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.