* #3593: render_frame and render_preview tile `times` into one contact sheet
* contributors: credit andreaaazo by their GitHub profile name
* #3593: contact-sheet cells default to 320 px, sheets stay under 8000 px
With `times`, `max_side` now defaults to 320 per cell instead of 960, so
64 frames of a 16:9 comp make a 2560 x 1440 sheet instead of 7680 x 4320.
`sheet::MAX_SIDE` drops from 8192 to 7680. `labels` that isn't true or
false is a bad-arguments error, and a bridged cell keeps the time asked
for when the app's reply leaves `time` out.
* 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.
* 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.
* 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).
* 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.
* aep export: note what the concurrency test covers and why a failed template build is not kept (#3277)
* 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.
* aep: text-dict test builds its prefix without repeat(1) (#3276)
* aep: an escaped text string drops its byte order mark without a range drain (#3276)
* 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.
* aep export: pin the key-time loop's report, name the layout in the base::bytes log (#3278)
* 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.
* 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.
* 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.
* aep: review fixes for #3281: NTSC table beside TimeBase, real-path NTSC round trip, light and camera defaults on import, doc wording
* aep: drop a local check tool committed by mistake (#3281)
* 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.
* 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.
* aep export: the every-layout test counts from a baseline, not from zero (#3278)
* aep export: correct the order the natural sort key doc gives for equal numbers (#3277)
* aep: the format notes name the accessors that remain (#3281)
* aep: the format notes name the 1 x 1 mask scale of text and shape layers (#3281)
* 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.
* 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)
* #3300: import .aepx through the flat index
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.
* #3300: read_aepx keeps its own owned-tree reader
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.
* #3300: test keys that round to the same time
* aep: compare the destination projects in the index-equivalence tests
both() and both_aepx() kept only the ImportResult of each path; they now keep
both destination Projects and assert structural and serialized equality, so a
layer or property value regression cannot pass on equal ids and reports.
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* #3170: .aep import of the text animator Stroke Opacity and the Source Text expression
ADBE Text Stroke Opacity maps to the animator's strokeOpacity (percent, as
After Effects shows it). The Source Text expression is imported like any
property's (Utf8 text, tdb4 bytes 119/120); one whose flags give no state
is still reported. New committed fixture text-synthetic.aep, made in After
Effects 2026 with the working space None.
* #3170: .aep export of the Source Text expression and the text animator Stroke Opacity
The Source Text expression is written enabled or disabled as After Effects
stores it (tdb4 bytes 119/120, Utf8 after cdat); a disabled one is no longer
dropped without a report. strokeOpacity is written as ADBE Text Stroke
Opacity with the record After Effects wrote in text-synthetic.aep. A test
exports the fixture and imports it again.
* #3170: doc comments on the functions this change touches
On a precomp layer the key covered only the nested comp's size, rate and
duration, so an edit inside the precomp kept a stale analysis. The key now
writes the precomp layer's Essential Properties and what its comp draws, field
by field and recursively. Each comp is written once per key and nesting is
capped, so shared, deep or cyclic precomps are keyed in bounded time. Docs say
what clears an analysis.
It arrived as a plain number, so Effect Controls neither dragged it over its
slider range nor clamped an edit to its valid range, as After Effects does.
A definition of parameters that are not kept, shared by many instances, was
walked again for each one without spending the budget; long popup item
names were kept per instance with each item counted once.
A parameter past the budget was skipped and the next one tried, so the
rest of the effect, and every later effect without a counterpart, still
read its shared definitions and built popup items. The first parameter
that does not fit now ends the budget; a later effect without a
counterpart is reported on its own line without reading its definitions.
The docs now say that a placeholder's layer parameters report their keys
and expression.
* Export, Media: H.264, ProRes, HEVC, VP9, AV1, AAC, MP4 and Matroska come from craft-libs
crates/export and crates/media use craft-video, craft-audio and craft-media
(one craft-libs rev) instead of filmcraft-h264enc, -prores, -aac, -isobmff,
-matroska and -opus. The av1enc, hevcenc and vp9enc crates are removed:
craft-video carries the same encoders, with the later changes merged in
storytold/craft-libs#829. Their round-trip tests move to crates/export/tests
and run against craft-video.
opusenc stays (craft-libs has no Opus encoder yet), as do filmcraft-mpegts
and the filmcraft-vp9, -hevc and -av1 decoders the tests use as oracles.
* Pin craft-libs at the main commit that has the encoder colour description (e7c3dda7)
Such an item is shown on the document, which the size does not confirm; it is now reported in unsupported_items, when the opti stores the document's size.
An item as big as its layer's bounds was taken for Retain Layer Sizes. When
the layer is also as big as the document but not at its corner, a
document-framed item has the same size, so it may show shifted. The opti
record holds the document's size at bytes 32 and 36 (1000, 1000 in every
record of the issue's project, the .psd's size); with it, that case is
reported. The layer still shows at its own bounds.
A GPU project whose Render Queue job measured the CPU faster (#3335) rendered
every frame of a batch at once on the CPU, one per thread, with no memory cap:
on a 16-thread machine at about 1.4 GB a frame the app ran out of RAM and
exited without an error (0.23.1, Windows).
While a job renders on the CPU after a GPU frame measured its working memory,
CPU frames in flight now fit half the memory the system has free at that size
per frame, 1 to one per thread, settled once per job and logged. The probe's
timed CPU frames use the same cap. EFFECTCRAFT_CPU_INFLIGHT still overrides
it; Software Rendering projects, with no measurement, are unchanged. The host
reads the free memory through the new JobOptions::free_memory.
Footage made from one layer of a layered .psd showed the whole document and
was named after the file. Its opti (tag 8BPS) stores the layer: the record
index (BE i32 at 14, -1 for the merged image), the bounds (LE i32 at 78-93)
and the name (UTF-8 at 344). The item now shows that layer as EffectCraft's
own Photoshop import does (at its own size with Retain Layer Sizes), named
<layer>/<file>. A layer without pixels (an adjustment layer) keeps the merged
image and is reported.
Third-party plug-ins and pseudo effects (expression controls) were dropped on
import, so expressions reading them broke. Such an effect now arrives as an
effect group whose effect id is its After Effects match name: it renders
nothing, like a missing plug-in, and is listed by Find Missing Effects. It
holds the effect's parameters flat, named as in After Effects, with their
values, keys and expressions in the units After Effects' expressions read.
It is still reported (once per match name, "kept without rendering") and
counted in imported.placeholderEffects. The parameters and popup items kept
are bounded per import.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An Orientation with an expression was reported as "not decoded" and lost its
value and keys too. The expression is stored in the otst's tdbs like on any
property, so it now arrives with the stored value or keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shape Direction, blend mode, composite order and the gradient highlight
values were reported when their record was in the default state (tdsb 3),
which holds After Effects' default (docs/aep-format.md 3.4), so a plain
guide rectangle reported "shape direction not imported". They are now read
only from written records, keys or an expression.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prompt showed 3 detail lines per category under a bare count, which read
as the project's total. The headline now says how many occurrences are left
out or changed, every line the report keeps shows, and "… and N more" counts
the rest. The import report records that number as `unlisted` (an unmapped
effect's line covers all its instances); unmapped effects are listed most
used first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings ▸ General ▸ Fit New Layers to Work Area (off by default) makes
new text, solid, shape, null and adjustment layers start at the work
area start and end at its end. It applies to Layer ▸ New, the shape
tools and the Pen tool, and wins over Create Layers at Composition
Start Time. Footage and compositions keep their own length.
architecture.md and web.md still said every job renders on the project's
renderer (#414); since #3335 a GPU job times both compositors on its first
frames and keeps the faster.
It never steps back up during a play, so a looping preview kept playing at a quarter of its resolution after its first loop. Without it the paced first loop caches the viewer's resolution and the next loops play in real time, as in After Effects. The setting stays for whoever prefers a lower first pass.
Settings > Previews > Lower Resolution When Playback Can't Keep Up (on by
default): a preview at Resolution Auto that stalls on uncached frames and
shows under 80 % of its frame rate for two seconds plays on at half the
scale, and two seconds later at a quarter if still slow, no lower than the
Adaptive Resolution Limit. Stopping goes back to the viewer's resolution
and Cache Frames When Idle fills it in. An explicit Resolution, the comp's
or the Preview panel's, is kept as set.
First playback of a 1080p comp with a blur (cache_bench --play): 9.0 to
22.1 frames/s on the CPU (about 27 once at Half), 4.1 to 16.1 with Mercury
Auto on an integrated GPU; plain clips that keep up stay at Full.
A comp left alone for a second fills its RAM preview from the current time,
so the first playback is cached instead of rendering frame by frame. Saved
settings keep their value. Two UI tests that assumed the old default set it
explicitly.
* AG4: xtask ci --affected
cargo xtask ci --affected [--base REV] [--dry-run] runs fmt, clippy, tests (nextest + doctests when available), layers, assets (only on asset changes) and wasm (only for affected L0-L4/web crates) for the packages a change can affect: changed files map to the owning package and to packages whose sources read them via ../ paths, expanded to reverse dependents. Workspace-wide inputs (root Cargo.toml, Cargo.lock, .cargo/, xtask/, toolchain, clippy.toml) fall back to full CI. Documented in docs/testing.md, AGENTS.md and CLAUDE.md.
(cherry picked from commit f3ca84fc2ee3948582cbc94e4c99ee510e829540)
(cherry picked from commit 8a2cdcadf447eafa57ab14016beae4420f002af0)
(cherry picked from commit d8a4301bc1)
(cherry picked from commit 36402c61ac2f7e29ecd76a6cfc89365cc2572e0a)
* AO1: fast CI profile, looser timing tests
- [profile.ci] (release without LTO, 16 codegen units); `xtask ci` runs clippy
and tests with --profile ci instead of --release.
- Wall-clock asserts in engine memory_tick, gpu OpenGL decline and script
socket tests keep their intent with bounds that survive a loaded machine.
- docs/testing.md and CONTRIBUTING.md mention the profile.
(cherry picked from commit b45c8a379351ebdfda0d7076ad4b740baf6f2087)
(cherry picked from commit fa1ea6a107c4c1936cde89b518a05adc73f2b4b0)
(cherry picked from commit e5d5ed80e9)
(cherry picked from commit d2c67660c70764f9c56880a642ad7f0d004a7f92)
* AG6: journal replay repros
Bug reports become replay scripts and crash regression tests become data files:
- engine::replay: a JSON-lines repro format (start line, command steps with
expect_error, expect queries with JSON pointer + equals, expect_no_error),
a never-panicking runner that reports per-step ok/error and the first failure,
and journal export (from the last project open; saves left out; size-capped).
- journal.export {path?} and help.exportRepro {path?} (not journaled).
- Crash capture: when the last-resort guard catches a panic in the outermost
command, the journal plus that command is saved to Repros/crash-*.jsonl in the
settings store (newest 20 kept) and the error names the file.
- Commands run by a journaled command (script steps) are no longer journaled twice.
- effectcraft-cli replay SCRIPT.jsonl [--json] [--keep-going]: exits 1 on a failed
step, unmet expectation or panic.
- crates/host/tests/repros.rs replays every tests/repros/*.jsonl; three samples
from past crash fixes (zero frame rate, Layer Settings with no selection,
New Comp from Selection with hostile overlap).
- Docs: docs/agents.md "Repro scripts"; AGENTS.md: crash fix regression test =
a .jsonl in tests/repros.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c38509d7dafe507856c48c78db65e00b94b0cebc)
(cherry picked from commit e55b56dbf76716adfcbfe4cc773df19a74edc419)
(cherry picked from commit 96a63855a9)
(cherry picked from commit c59351ab3bd697f0868e41230dd883960d94a540)
* xtask: ci and ci --affected run cargo test and nextest with --no-fail-fast
One run now lists every failing crate instead of stopping at the first.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qXL3GPNnY9BSFF2XoU2Yu
(cherry picked from commit e46c7debe738eca62b35bf7392ab77637f6a33ae)
* Agents: confinement write-checks journal.export, help.exportRepro and file.exportTimeline
Review on #859 (split into this PR): their output paths were only read-checked (relative ones
not at all). Test: confinement_covers_every_command_that_writes_a_file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014d65YdNxQmjRkbXRtP2NwH
* #1224: handle bin-only CI targets and BOM repros without weakening timing tests
* #1224: run the full gate when nextest policy changes
---------
Co-authored-by: Elias <hakifx91@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Removes the unused egui_demo_lib, egui_inspection and egui_system_font_provider patches and adds cargo xtask lock-patches to cargo xtask ci. Fixes#3403.
Dropping an .aep or .aepx on the window, or picking one in File > Import > File...
or Multiple Files... (also from the Home screen and the Project panel), imports it
as File > Import > Adobe After Effects Project... does, instead of failing as
unsupported footage: the losses prompt first when parts cannot come over, its
items in a folder named after the file, its first composition open.
- Several projects are asked about one at a time: the others wait in the
prompt's queue, and Cancel skips only the one it asks about. A prompt another
dialog replaced (Save changes?) strands nothing: its projects are tried with
the next import or queued behind the next prompt.
- The other files of a drop or pick import as footage, into the Project panel
only when a project is in the drop. While the prompt shows, a Photoshop file
or image sequence imports with its defaults instead of replacing the prompt,
an Interpret Footage question leaves a note instead, and a dropped .ecproj is
skipped with a note. While Save changes? waits for a dropped .ecproj, the rest
of the drop is skipped with a note instead of landing in the replaced project.
- Errors of every picked project reach the status line on both the blocking and
the non-blocking file dialog.
- The Import File dialogs list .aep, .aepx, .ecproj and .ecprojx; Replace
Footage offers footage only.
- For scripts and agents, file.import hands After Effects projects to
file.importAep; a refusal (new EngineError::Refused) names the menu entry and
`file.importAep` with `allowLosses`.
- File > Import / Export name the entry Adobe After Effects Project..., as
Adobe Premiere Pro Project...; File > Import keeps After Effects' order with
the project imports together and the formats only EffectCraft imports after a
separator. All 13 menu catalogs and the losses dialog title are updated.
- Docs: aep-import, aep-export, footage, file-format-parity, agents.
Supersedes #3312.