* Layer: Un-precompose a selected pre-comp layer back into its parent composition (#2918)
layer.unprecompose copies the nested composition's layers into the parent at the pre-comp
layer's place with timing and position carried over, and refuses what it can't keep exactly.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Un-precompose: refuse a pre-comp layer with Solo or Guide on
A soloed pre-comp layer hides the comp's other layers and a guide layer is
left out of renders; the expanded copies carry neither switch, so the picture
would change. Listed with the other blockers, in the command description and
the agents.md Cookbook entry; the refusal test covers both switches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Un-precompose: warn instead of refusing, only a locked layer stops it
A locked pre-comp layer is still refused, with or without `force`. Every
other former refusal reason is a warning: without `force`,
`layer.unprecompose` returns an error that lists the reasons and changes
nothing. `dryRun` returns `{dryRun, warnings}`. With `force` the layer is
unpacked in one undo step and the result carries `warnings`. Whatever the
pre-comp layer itself carried is dropped, and other layers' parent, track
matte and layer-property links to it are removed.
Layer > Un-precompose in the UI lists the reasons in a prompt with
Unpack Anyway / Cancel (`dialog.unprecompose.force` /
`dialog.unprecompose.cancel`). A layer without reasons unpacks at once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Un-precompose: warn about expressions that refer to the pre-comp layer
An expression on another layer of the comp that names the pre-comp layer
would no longer find it once the layer is unpacked. The expression can
name it in quotes or call `layer(<index>)` with its index, bare or on
thisComp; another comp's comp("…").layer(…) is not counted. Each such
property is now listed once as a warning. Like the other warnings,
`force` unpacks and `dryRun` lists it. The expressions are left as
written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Un-precompose: warn about lost frame sampling and Position expressions, keep markers in order
Review follow-up (CodeRabbit on #3081):
- A nested comp that preserves a frame rate other than the parent's
samples its layers on its own frames, and the expanded copies would
lose that. Motion-blurred nested layers under a different shutter
would also blur with the parent's shutter. Both are now warnings.
- When the pre-comp layer's Position differs from its Anchor Point, the
move is added to the stored Position of the unparented nested layers.
An unparented layer whose Position, or a separated axis, has an
expression may ignore that move, so it is now a warning.
- Copied comp markers are sorted into time order with the comp's own.
- New tests: a 10 fps nested comp with Preserve Frame Rate in a 30 fps
comp, a different shutter, a Position expression with and without a
move, and nested matte and layer-value remapping with shifted markers
merged in order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Un-precompose: warn when an expression's layer numbers shift; the prompt keeps its target
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Un-precompose: a shorter renumbering warning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* 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>
* Echo: swap Composite In Back and Composite In Front (#3124)
After Effects 2026 renders of the importer sample (opaque test card with a
moving square, one echo at -1/30 s) show Composite In Front keeping the
layer on top (the output equals the current frame) and Composite In Back
putting the echo over the layer. EffectCraft had the two the other way
round, which made ~0.5 % of the pixels differ at the moving edge. The CPU
and GPU (fxn_echo) arms are swapped together.
A regression test rebuilds row 130 of the test card and checks Add,
Maximum, Minimum, Screen and both Composite operators against the measured
After Effects row. Blend is left unchanged: the sample (one echo, intensity
1, decay 1) cannot tell its normalisation apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Echo: Blend sums the weighted frames like Add (#3124)
New After Effects 2026 renders (our own 1110-extra sample, 16-bit PNGs,
opaque test card with a moving square) show that Blend does not normalise:
- 3 echoes at intensity 1 and decay 1 give four times the layer.
- 2 echoes at intensity 0.5 and decay 0.5 give alpha 0.875
(0.5 + 0.25 + 0.125).
EffectCraft divided by the number of frames. The division is gone on the
CPU and in fxn_echo_fin on the GPU.
The same sample pins Composite In Back with three echoes: each older echo
goes over what is there with its weight as opacity, so the oldest is on
top, as the branch already does. Composite In Front with three echoes
equals the current frame.
A regression test checks four measured 16-bit rows within 4 codes (24 on
square-edge pixels): Blend 3/1/1, Blend 2/0.5/0.5 with alpha, and both
composite modes at 3 echoes, -0.08 s, decay 0.5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Echo: doc comments on the CPU and GPU entry points
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>
* 3D: Orientation and X/Y/Z Rotation turn Z first, keyed Orientation slerps, as After Effects
After Effects composes a 3D layer's (and a camera's) Orientation and its X/Y/Z
Rotation as Rx * Ry * Rz each, Orientation before the axis rotations; EffectCraft
used Rz * Ry * Rx. Measured on synthetic AE 2026 cases: Orientation [10,20,30]
(488) was 5.2 % of the frame off and now matches to the anti-aliased edge (0.03 %);
X/Y/Z Rotation 10/20/30 (800), Orientation + X Rotation (801) and one-node
cameras (804, 805) likewise.
Keyed Orientation moves along the shortest arc between the keys (quaternion
slerp, eased in time, angles written in [0, 360)): keys [0,0,0] -> [10,20,30]
read back [6.2643, 9.3531, 15.2190] half-way where EffectCraft gave [5,10,15];
[0,0,0] -> [60,80,40] linear and eased match AE's read-backs within 0.002 deg.
lookAt() returns AE's angles (z axis on the target, x axis level; two AE
read-backs within 1e-3 deg). The camera tracker, VR editor, Look At and orbit
commands use the new decomposition.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keyframes: a linear side next to an eased side has 16.67 % influence, as After Effects
In a segment that is Bezier on one side and linear on the other, the linear
side keeps the segment's average speed, but with the 16.67 % influence After
Effects shows for linear keys in the Keyframe Velocity dialog (and reports to
scripts), not 33.3 %. Measured on an After Effects 2026 time-remap render (a
square at x = 50 + 455 * remap): out of a key at 2 s (3.5, speed 0, influence
50 %) into a linear key at 4 s (0.5), AE plays 3.290 at 2.5 s and 2.657 at
3 s; EffectCraft played 3.308 and 2.629 (8 and 13 px off), now the same
square positions to the pixel.
LINEAR_SIDE_INFLUENCE is used by evaluation (scalar, per-dimension, paths and
spatial), side_ease (Graph Editor, velocity dialog, keyIn/OutTemporalEase) and
the Lottie export, so exported easing plays as EffectCraft does. Segments
linear on both sides stay straight lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Timewarp: retime the layer's source without the effects above it, as After Effects
After Effects' Timewarp reads the layer's source (with its masks) at the
retimed times and ignores the effects applied before it: a red solid with a
Gradient Ramp and then Timewarp (Speed 25) renders as the red solid in AE
2026 (EXPORTER e2 read-back). EffectCraft retimed the layer with its
effects (mean difference 127.5/255 over the comp), now identical (0, CPU and
GPU). Echo, Posterize Time and the other time effects keep reading the
effects before them, as measured before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Image sequences: a gap in the numbering holds the previous frame, as After Effects
After Effects 2026 shows the last existing file across a gap in a sequence's
numbering (case 860: files 1, 2, 4, 5 show frame 2 at frame 3; VERIFY
read-back). 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>
* Review: keyed Orientation uses LINEAR_SIDE_INFLUENCE for linear sides; real NaN assert
evaluate_orientation still gave a linear side (and the unreachable missing-key fallback)
1/3 influence while segment_value, side_ease and the Lottie export use
LINEAR_SIDE_INFLUENCE (1/6), so a keyed Orientation next to an eased key moved on a
different curve than every other property (CodeRabbit on #1876). New test:
orientation_linear_side_next_to_an_eased_side_matches_a_scalar (both directions).
The always-true NaN assert in keyed_orientation_slerps_as_after_effects now checks that
a non-finite key comes back as given; Property::value_at says why the match_id
"orientation" routing is safe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format keyframe orientation test (cargo fmt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Move the image-sequence gap change out of this branch
The gap change (hold the previous file instead of colour bars) reverses
#297's documented behaviour and goes to its own PR, as requested in review.
pool.rs, sequence.rs, the footage test and docs/footage.md / docs/agents.md
are back to main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* SAM 2 mask decoder: transposed convolutions as matrix product plus pixel shuffle (#3031)
Both transposed convolutions of the mask decoder have a 2x2 kernel, stride 2
and no padding, so each input pixel owns its own 2x2 output block. That is one
matrix product plus a pixel shuffle, which is about twice as fast on the CPU
as candle's conv_transpose2d. The first convolution also takes the decoder's
key tokens directly, without the transpose and copy to an image.
The candle path stays in the code for the tests. They compare the two to
1e-6 (relative to the largest output value) on the decoder's real sizes,
odd and degenerate sizes, large values, zeros and a single hot pixel, check
that other kernels are refused, and compare a whole decoder forward pass
(single and multi mask) on random weights.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* SAM 2 decoder test: bound the weight discovery loop
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* SAM 2 tracking: prompt frames in frame order, size-check frame reused (#3030)
With several objects prompted on three or more frames, the prompts went to
the tracker object by object. The tracker keeps features for two frames, so
every prompt after the first object fetched and encoded its frame again.
Each prompt frame was also fetched once only to check its size and then
fetched again for the encoder.
Prompts now go to the tracker in frame order (then object order), and the
frame fetched for the size check is handed to the tracker the first time it
asks for that frame (up to 256 MB of pixel data kept; beyond that the frame
is fetched again as before). Objects do not influence each other, so the
results are unchanged.
Tests cover the prompt order, one host fetch per prompt frame, the order of
several prompts of one object on one frame, and the memory cap.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* SAM 2 tracking test: the held-frame cap is cumulative
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* av1enc: motion search with a visited set and early-exit SAD, fixed-size SATD, region-only BlockDecoded snapshot
- motion_search remembered nothing: the diamond search evaluated the previous
centre again and every SAD ran to the last row even when it had already
lost. Full-sample and sub-sample positions are now tracked in a visited
list (costs are deterministic, so a position that was evaluated cannot beat
the best cost again), and the SAD stops after a row once sad + mv cost can no
longer be below the best cost. The stop test is the same f64 expression as
the strict "<" the callers apply, and it is monotone in the SAD, so the
chosen vector is the same.
- satd runs with a fixed block size (satd_k::<4> / satd_k::<8>): the
difference block is a [[i32; K]; K], the vertical butterflies work on whole
rows at once, and the old per-column gather is gone. The sum is an integer
sum of the same values.
- A snapshot copies only the BlockDecoded cells of the block area (at most
16x16 per plane), the only flags coding inside the block can change, instead
of the whole 3x35x35 array.
The encoded stream is unchanged. A unit test compares the new SATD with the
previous implementation for 4..32 sample blocks at 8 and 10 bit.
Fixes#2387
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* av1enc: tests for the region-only BlockDecoded snapshot and the bounded SADs
- `snapshot_restore_brings_back_every_blockdecoded_flag`: for every block
size of a 64x64 superblock (4x4 .. 64x64) at every aligned position in two
superblocks, from an all-clear and a random flag state, marking every
sub-block of every size inside the block and restoring gives back the whole
3x35x35 array, so the 16x16 region covers every flag coding can set and
leaves the neighbours alone.
- `bounded_sad_stops_only_once_the_cost_cannot_win`: `sad_ref` and
`sad_bounded` return the full SAD whenever `sad + lb < bound`, and
otherwise a partial sum that already loses the strict `<` of the callers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* opusenc: memoise SILK rate-loop trials, ring buffer for noise feedback
Rate loop: each probe clones the encoder and the range coder and codes the
whole frame, about 7 trials per frame. The hard-limit check recomputed the
trial of the chosen offset and the final step coded the frame a third time.
Keep every trial (offset, bits, encoder state, range coder): the hard-limit
check now hits the memo and the final step adopts the memoised state and
coder. If the chosen offset is somehow not in the memo it is coded as before.
Quantiser: the noise-feedback history was shifted with copy_within for every
sample (the quantiser runs about 7 times per frame). Keep it in a
double-length ring so the 16 most recent errors are one contiguous slice;
the dot product reads the same pairs in the same order, and the history is
written back to the channel at the end.
The packet streams are byte-identical to before (cmp on 24 configurations
of 90 s: speech-like and music-like input, mono and stereo, SILK NB/MB/WB
and hybrid SWB/FB).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* opusenc: noise-feedback ring as a helper type, equivalence tests for the SILK rate loop
`quantize` was at cognitive complexity 31 on current main (limit 30). The
double-length ring for the noise-feedback history moves into `NfRing`
(push / window / to_array), and the end-of-frame bookkeeping of `encode`
into `end_frame`.
Tests: the ring matches a shifted history over 100 pushes, and an encoder
that adopts the memoised trial produces the same bytes, noise-feedback
history and LPC state, frame after frame, as one that codes every frame
again at the chosen offset (the step before the memo), for 8/12/16 kHz,
mono and stereo, with and without a tight hard limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Subspace Warp: QL eigen solver and banded mesh solve (#2319)
plan() spent 43-96 % of its time in the cyclic Jacobi sym_eigen, whose
absolute stop test never triggers on Gram matrices of magnitude ~1e8, and
then ran a dense Cholesky on a mesh normal matrix of bandwidth ~30.
Use Householder tridiagonalisation plus implicit QL inside subspace.rs
(only the leading subspace is used, so basis and sign do not matter), and
accumulate and factor the mesh normal equations in lower-band storage,
without a Vec per data row. camtrack/linalg.rs is untouched.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Subspace Warp: replace the QL eigen solver with a clean-room symmetric QR
The previous sym_eigen_ql followed a well-known published tred2/tqli listing
too closely for the clean-room rules. It is replaced by sym_eigen_qr, written
from the textbook method (Golub & Van Loan, Matrix Computations, ch. 8) in
its own structure:
- householder_tridiagonal: explicit reflectors with the symmetric rank-2
update B -= v w^T + w v^T (p = beta B v, w = p - (beta p^T v / 2) v), Q
accumulated as rows of Q^T.
- wilkinson_qr_step: one implicit QR step with the Wilkinson shift, Givens
rotations chasing the bulge from the top of the unreduced block down.
- sym_eigen_qr: deflation of negligible off-diagonal entries and the step
driver, split so each function stays under the complexity limit.
It returns None for non-finite input, a size mismatch or no convergence
within 30 n steps; smooth_trajectories then falls back to the Jacobi
sym_eigen, so it can never loop for ever or be worse than before.
Tests: random symmetric matrices (n = 1..61) against Jacobi eigenvalues,
residual |A v - lambda v| and V^T V = I; identity, repeated 3x3 blocks, zero,
1x1, 0x0, rows scaled from 1e-8 to 1e8 and a 1-2-1 tridiagonal; NaN, infinity
and size mismatch return None; the leading-subspace projector of Gram-like
low-rank matrices matches Jacobi. An ignored benchmark times plan() on a
300-frame synthetic parallax shot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Roto Brush: propagate on the band bounding box
propagate() ran the signed distance, gauss, graph cut, model fit and
clean over the whole frame. Work on the crop of the band bounding box
(|sd| <= r plus the core, padded) and paste the labels back; fall back
to the full frame when the crop is not smaller. warp_prior stays
full-frame.
Fixes#2323
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Roto Brush: band crop padded by the prior blur, models fitted on the full frame
Review follow-up for the band bounding-box crop in propagate:
- The crop pad is now the reach of the prior blur (three box passes of
matting::gauss, exposed as matting::gauss_reach) instead of 1 px, so every
band pixel sees the same blur window as on the full frame.
- When the previous frame has no colour models they are fitted on the full
frame before cropping, as the full-frame path does.
- The free pixels and the clean seeds are cut out of the full-frame signed
distance instead of being recomputed on the crop; the seeds equal
core_of(prior.soft), which saves one signed distance per frame.
- The full-frame path and the crop share recut_band/cut_band; full frame is
just the whole rectangle. A frame with no band and no core (no
foreground) skips the cut, which was a no-op.
- The w - 1 / h - 1 underflow on zero-sized frames is gone (band_rect
returns None); regression test for 4x0, 0x4 and 0x0 (panicked before in
debug builds).
Tests: crop vs full frame labels on a disk, a disk at the frame corner, a
donut, r = 2 and r = 3 with prior edges wider than the band, and no
foreground, each with fitted, given and equal colour models; the padded crop
blurs the prior like the full frame at every band pixel while a 1 px pad
does not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Roto Brush: reuse scratch vectors in the distance transform
matting::distance allocated four vectors per column plus a staging copy
and three vectors per row. Run the columns and rows through four reused
scratch vectors instead; the dt1 arithmetic is unchanged.
Fixes#2324
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Roto Brush: keep the distance transform parallel, with per-split scratch
The previous commit made the column and row passes serial, which was measured
on one thread only. Both passes are parallel again: the column pass runs on a
transposed buffer so every column is a contiguous chunk, and rayon's
for_each_init gives each work split one set of scratch vectors instead of
four allocations per line. The square root is folded into the row pass.
A test keeps the old per-line implementation and checks that the output is
bit-identical on a disk, its inverse, 1xN, Nx1, all-true, all-false, seeds on
the frame edge and a sparse 333x211 pattern. An ignored benchmark prints the
1080p median and the rayon thread count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Roto matting: size the distance-transform scratch per pass (columns h, rows w)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* AEP import: map the Cryptomatte plug-in to EffectCraft's Cryptomatte
The importer reported every instance of the free Cryptomatte plug-in
(match name `Cryptomatte`) as a lost effect. It now becomes
`ec.3d.cryptomatte`:
- CryptomatteData decodes the plug-in's Settings data (its own
little-endian layout, documented in docs/aep-format.md 3.7): the
Cryptomatte layer name and the selection text, both carried verbatim.
- Output maps to Display / Matte Only (Colors -> Colors, Matted RGBA ->
Matte, Matte Only -> Matte Only). Matted Colors, Selection Mode when on,
keys or expressions and anything else stored are reported, and
unreadable data is reported as damaged.
- EffectCraft's Cryptomatte gains a Layer Name text parameter. A layer the
footage has under exactly that name wins; otherwise the Layer popup
decides as before. The CPU, GPU and pick paths share one resolver
(AuxChannels::resolve_crypto_layer_named). Without it, plug-in layer
names such as Crypto_Material would silently fall back to the first
layer.
On a real production project this takes the effect losses from 132 to 44.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* AEP import: check Cryptomatte against an After Effects 2026 sample of our own
A sample made in After Effects 2026 (plug-in 2.6.6: a fresh effect,
Selection Mode on, each Output item, with read-backs and renders)
corrected three points of the mapping:
- Selection Mode on was dropped silently: After Effects writes it as a
default-state record (tdsb 3) holding 1, which the general reader does
not take. The Cryptomatte step reads that record itself and reports it.
- No Output record is now the plug-in's default Colors, also for an
instance that borrows the project's definition (every instance after
the first), which was reported as having no reliable default and fell
back to EffectCraft's Preview. Keyed or unreadable Output values fall
back to Colors too, still reported.
- Matted Colors imports as Display Matte (the same matte over the layer's
own colours) instead of Preview, still reported as an approximation.
docs/aep-format.md 3.7 gets the sampled Output default, the tdsb 3
Selection Mode record and the real fresh Settings data (an empty text of
length 1 and its MurmurHash3, twice, not cry1 and zeros); docs/aep-import.md
the render comparison with After Effects (Matted RGBA matches; Matte Only
differs in alpha; Colors with no layer chosen shows another layer).
Tests: the exact fresh Settings bytes, tdsb 3 Selection Mode, borrowed
definitions, and an opt-in test against the sample once it is in the
corpus (aep/cryptomatte.aep, ignored until the corpus pin includes it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* AEP import: write Cryptomatte's popup items in the test as After Effects does
The injected Cryptomatte definition wrote `pdnm` and `fnam` as `LIST`
chunks. After Effects writes them as plain chunks whose body is chunks
(`NESTED_IDS`), and the reader only looks there, so the tests never read
Output's item names: they passed through the fallback for a definition
with four items and no `pdnm`, which uses the plug-in's own list.
The harness now writes both as After Effects does. New tests check that
the injected definition reads back with its items and instance name,
that Output is mapped by the item the stored index names (items in
another order, case, an unknown item), and that the fallback without a
`pdnm` still works. With the old `LIST` form, or with the mapping
ignoring the items, the new tests fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* AEP import: check Cryptomatte's layer and selection against presets of our own
The After Effects 2026 sample 1400 now has two preset compositions that choose a layer and
pick one object (box_red on CryptoObject) or one material (mat_paint on CryptoMaterial).
CryptomatteData reads both as they are; the picked tokens are the EXR manifest's hashes,
which EffectCraft's selection grammar reads the same as the names.
- The u32 at 108 is MurmurHash3 of the selection text with its NUL (three texts).
- A unit test pins the 127-byte preset data; the sample test now covers all eight
compositions with their layer and selection.
- Render check: EffectCraft's matte covers exactly the pixels After Effects draws as
selected, edge coverage within 0.0002. docs/aep-format.md 3.7 and docs/aep-import.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cryptomatte import: report damaged Settings and Selection Mode records
Review follow-ups for #3109:
- CryptomatteData refuses a selection text without its NUL and checks the zero byte
at 112 for an empty text too.
- A Settings record without its data, and a Selection Mode record whose value cannot
be read, are reported as damaged data, not taken as the default.
- Layer Name is the effect's last parameter, so the earlier parameters keep their
indexes and upgraded instances have the same order as new ones.
- A Cryptomatte layer name is matched as given before it is trimmed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cryptomatte settings: require the selection's NUL to end its declared length
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cryptomatte: match rank planes by the exact layer name
crypto_rank_planes looked planes up with a case-insensitive whole-name match, so
layers Crypto_A and crypto_a shared planes. The layer prefix now matches
case-sensitively; component letters still match in either case.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Format
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* AEP: the exporter knows Extra::Cryptomatte; rustfmt
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>
* #1335: accessibility slice 2: Info, Flowchart, Script Console and the 3D Camera Tracker block named, described and keyboard-operable
The audit gets read-outs, graphics and logs as described items (a name is required in a fixed panel), dedicated maximized scenarios for the panels the shared loop squeezes, and minimum counts for the new zero panels.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Review: Enter/Space on a favourite star only toggles it; keyboard activation of a property label; settings page scrolls to focus; sharper UI test assertions
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Refresh the accessibility audit baseline after merging upstream/main
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* a11y audit: hold the machine-dependent Media Browser scenario to its upper bound
The Media Browser opens on the home folder, so a runner with fewer files finds fewer controls and
the 'improved, lower the entry' check failed in CI. The entry stays an upper bound.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Merge upstream/main and updated feat-gap25-a11y-audit into feat-gap25-a11y-panels-2
Brought in the updated feat-gap25-a11y-audit (which already contains upstream/main). Conflict: crates/ui-egui/src/menus.rs, which upstream split into menus/*.rs. Took the slim module and re-applied this branch's Flowchart hunks where the code now lives: nudge() skips the Flowchart (menus/invoke.rs) and Enter is left to the Flowchart in handle_shortcuts (menus/shortcuts.rs).
Also removed a duplicate MACHINE_DEPENDENT const in tests/ui_accessibility_audit.rs that the two branches both added. The audit BASELINE still matches the measured findings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Flowchart: split show() to pass the stricter clippy limits from main
Main now denies cognitive_complexity (30) and too_many_arguments; the
merged flowchart panel exceeded it. The keyboard navigation, the
key-to-node mapping, pan-into-view, node commands, the accessible node
name and the option params move into small helpers with unchanged
behaviour.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Camera tracker a11y test: wait for the analysis result instead of one frame
The test pressed Enter, ran one frame and expected to see the background job.
On a fast runner the job starts, ends and is written back inside that frame,
so the state was never seen. Undo the first analysis, press Enter, and wait
(bounded) for the new analysis step in the undo history.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Accessibility: one hours range for the accessibility pass in ui-parity.md; test that Flowchart automation labels stay English
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Flowchart toolbar: keep the English label for the automation registration
The translated text is shown and spoken; the automation label stays English.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Timeline: hover texts for the icon-only column headers and layer switches
The Timeline's column header is painted as one click target, so its
icon-only headers (A/V Features, Label, #, the eight switches and the
Modes column's T) had nothing to hover and nothing explained them.
HEADER_TIPS lists each one with a short name and an explanation in our
own words. Hover-only widgets on top of the header show the text, carry a
named ColumnHeader AccessKit node and register automation ids
(timeline.header.av.*, timeline.header.switches.*,
timeline.header.modes.preserveTransparency). The header's clicks and
right-click column menu still work underneath. The layer rows' A/V and
switch toggles and the Preserve Transparency box show the same texts.
The accessibility audit's Timeline baseline drops from 89/42 to 72/25
because the switches now have accessible names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Timeline headers: accessible description, Preserve Transparency text, English menu test
The header's node is named before its hover text is added, so screen readers get the
explanation; Preserve Transparency says the layer follows the alpha below it, partial alpha
included; the hover test sets English before checking the translated Hide This item.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge main: keep both sets of timeline tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Timeline tests: close the test before the merged one
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>
* Cryptomatte: the Layer popup lists the footage's Cryptomatte layers (#473)
The popup was the constant CryptoObject / CryptoMaterial / CryptoAsset. It now lists the
Cryptomatte layers of the layer's OpenEXR footage, and those three when there are none. The
stored value keeps its meaning (0-2 the standard layers, 3 and up the file's other layers in
name order), so older projects render the same.
Refs #473
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Cryptomatte Layer popup: timeline editor, one menu helper, probe and fallback tests (#473)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Cryptomatte Layer popup: give further layers of a standard kind a custom slot
A.CryptoObject keeps slot 0; B.CryptoObject and later ones get 3+ instead of
being dropped, so they can be selected, picked and rendered.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Format; note same-kind layers in the footage docs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Format the merged code with rustfmt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Viewer exposure on the full float range of 32 bpc frames (#2797)
32 bpc viewer frames on the desktop are rendered without the HDR step (skip_hdr_step,
#2916) and kept as half floats in the RAM preview. A display pass on egui-wgpu's device
turns each frame into one RGBA8 texture with mips per viewer, in this order: decode,
exposure in linear light, Compand / Tone Map, encode, display colour, Show Channel,
rounding. Before, exposure was an 8-bit lookup applied after tone mapping, clamping and
the display conversion, so -2 EV could not bring values above 1.0 back.
- engine: viewer::DisplayPass describes the pass as data; its pixel() is the CPU
fallback (no device) and the shader's reference. It replaces exposure_lut /
display_transform. FrameEncoding records a frame's curve and the HDR step it still
needs. 32 bpc snapshots are taken without the HDR step; snapshot_rgba8 applies it.
- ui-egui: FrameImage::CpuF16 (half crate, F16C at run time), byte accounting at 8 bytes
a pixel in the cache and the prefetch estimates; display_gpu.rs + display_gpu.wgsl;
main viewer, snapshot and passive viewers draw through the pass; the 8-bit image other
panels read is made on demand with the HDR step; the Info panel shows float values.
- Half-float frames are not written to the disk cache yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewer display pass: f16 disk cache entries, window-device gating, review fixes
- Disk cache: half-float frame entries (format byte in the entry header, byte planes +
LZ4); 8-bit entries keep format 0 and stay valid. 32 bpc viewer frames are written and
read again, and the timeline's disk bar sees them. A damaged half-float entry is
deleted.
- The display pass runs on the window's egui-wgpu device, with or without the GPU
compositor (still building, failed, GL fallback). Its pipelines are made inside error
scopes; a backend that rejects them gets the CPU pass instead of a blank viewer.
- A new LUT of the same size (another ICC profile, Preserve RGB, another display) runs
the pass again.
- f32 to f16: one pass per L1-sized block, the NaN / infinity fix-up on the bits, no
fill of the buffer (4K: 4.7 ms, as the 8-bit conversion).
- Mips of odd-sized levels include the last column / row; the snapshot's half floats
and texture go with the snapshot; no placeholder texture ids; u64 size checks.
- Tests: LUT profiles and negative values against the shader, Bake reruns, the passive
viewer's colour, f16 disk round trips; exact comparisons allow 1 LSB on software
rasterizers. Docs: the display pass and the f16 disk entries in architecture.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewer f16 disk-cache test: temp dir removed on failure too, wait for the queued disk write
Review points: the test's directory was only removed by its last statement
(now tempfile::TempDir), and the frame is released before its disk write is
queued, so flush() could return early (now waits for the entry).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Viewer: oversized half-float frames use the CPU display pass, so exposure/HDR come before the averaging down
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Viewer: fmt and a debug assert for the half-float upload path
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Disk cache: persist_missing also writes half-float frames
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Passive viewer: oversized half-float frames take the CPU display pass instead of leaving a stale texture
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Masks: set the corner roundness of a Rounded Rectangle mask (#3067)
layer.addMask takes a roundness (corner radius in px) for shape rounded, and the new
mask.setRoundness changes it afterwards on a rectangular mask.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Mask roundness: refuse ellipses, keep feather points and key correspondence
mask.setRoundness accepted any closed path whose vertices lie on its bounding
box, so an ellipse was replaced by a rectangle. It now accepts only a plain
rectangle (four corner vertices, straight sides) or a rounded one (eight
vertices, straight sides alternating with circular corners).
The rebuilt path keeps the old start vertex and direction, and its feather
points, so an animated path's other keys and the feather points still line
up. A change between square and rounded corners changes the vertex count; it
is refused when the path is keyed or has feather points instead of leaving
keys with different counts or dropping the points.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Layer: Quick Set Anchor, nine anchor points with Position compensation (#2967)
layer.quickSetAnchor moves the Anchor Point of 2D layers to a corner, edge midpoint or the
centre of their content and shifts Position and its keys so nothing moves. Layer > Transform >
Quick Set Anchor lists the nine points.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Quick Set Anchor: skip layers with an animated Anchor Point, Scale or Rotation
The anchor is set at the current time and the Position shift is worked out
from the current Anchor Point, Scale and Rotation, so on a layer where any of
them is keyed or has an expression the layer would move at other frames.
Such layers are skipped like 3D and locked ones (an error when none is left).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Quick Set Anchor: skip layers with a Position expression or Auto-Orient along the path
An expression on Position (or the separated X/Y) overrides the shifted value,
and Auto-Orient along the path adds a rotation the shift does not account for;
both would make the layer jump, so they are skipped like 3D layers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Quick Set Anchor: error when no selected layer has content bounds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Quick Set Anchor: test separated, keyed Position keeps the layer in place
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Viewer: Alt-drag duplicates the dragged layers (#2968)
Alt/Option-drag of the selected layers in the Composition viewer duplicates them and drags
the copies; the copy and the drag are one undo step. edit.duplicate takes a merge key for it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Alt-drag: duplicate from a 3D gizmo arrow, no double move of a selected child
Alt-dragging a gizmo arrow moved the original; it now duplicates and moves the
copy along the axis. A selected layer whose parent is dragged too follows the
parent, so its own Position no longer gets the drag a second time (with or
without Alt).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Project panel: the Name column fits long names and resizes
The Name column was 40 % of the panel and at most 260 points wide, with
no way to resize it, so long item names (compositions of an imported
After Effects project) ended in "..." even in a wide panel.
It now fits the longest listed name, never narrower than before and at
most the panel's width less the Label column (the optional columns keep
scrolling under it). Dragging the column's edge in the header resizes
it, a double-click on the edge fits the names again. The width is kept
in UiState::project_name_w (None = fit the names); the edge has the
automation id project.nameColumn.edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Project panel: the Name column leaves room for the optional columns; no name fit for a manual width
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Timeline: one selection box spans the layer list and the time graph (#3056)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Timeline box test: say what it tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Timeline box: a box kept in the time graph selects keys only
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Format the timeline box test and box_targets signature
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Timeline box test: drive the real drag across the outline and the time graph
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Color Stabilizer: sample the reference where the points were at the reference time (#3125)
After Effects 2026 samples the reference frame at the positions the
Black / Mid / White Points had at the reference time. With keyframed
points it therefore corrects even a static picture: our importer sample
(colstab-43-keys, Levels, Black Point (64, 90) -> (151, 99) over 1 s)
renders at 0.5 s as the Levels map out = a + b*v of the ramp with
b 1.2935..1.2965 and a -0.2365..-0.2350, which is the Black Point's
current sample (107.5, 94.5) sent to the reference sample at (64, 90).
EffectCraft sampled both frames at the current points, so the card
stayed unchanged (slope 1). The samples now read the points at the
reference time through the host (params_at), falling back to the
current points without one. CPU and GPU share the sampler
(color_stabilizer_maps).
Sample Size's unit is not pinned by the samples (every size 1..40 leaves
the card unchanged in After Effects; that only bounds the sampled area)
and is left as it is.
Part of #3125
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Linear Wipe: After Effects' edge position and feather profile (#3125)
Measured on our importer samples (linwipe-*, 2026-11-05, After Effects
2026, 320 x 180):
- The edge sits at lo + completion * (hi - lo) whatever the feather (the
set-all render's 50 % line is at -5.25, predicted -5.266); 0 % keeps
the layer and 100 % clears it even with Feather 32000. EffectCraft
extended the travel by the feather, 13 px off at Feather 16.
- The feathered edge is the hard matte blurred with the layer's edges
repeated: three box passes of radius 6 per axis fit Feather 16
(sigma 6.35..6.6, taken as Feather / 2.5), including the extra
opacity where the edge meets the layer's left side. EffectCraft drew
a linear ramp Feather px wide.
- Without feather the edge pixel is anti-aliased (143/255; coverage
gives 0.6). EffectCraft drew a hard step.
set-all alpha: before max 195, 4646 px (8.1 %) off by more than 2/255;
after max 15, 529 px (0.9 %), all along the layer's left side (at most
2 elsewhere). Feather 0 renders: max 112 -> 10 on the edge column.
CPU: coverage matte, raster::gaussian_blur (3 box passes, edges
repeated); GPU: the same matte from gen_linear_wipe (new modes), its
gaussian_blur, then a multiply, so both match. Sigma is capped at twice
the layer's longer side; NaN values leave the layer as it is.
Projects saved before keep the old ramp: a hidden legacyFeather
parameter that migrate::legacy_default sets on upgrade (as for Ripple
and Block Dissolve).
Part of #3125
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Linear Wipe on the GPU: 100 % clears the layer before the angle is read
A NaN angle left the layer unchanged on the GPU while the CPU cleared it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* AEP import: the report names the After Effects version of the file (#3115)
svap holds the version of the After Effects that wrote a project, head
bytes 4 to 7 the version the file is written for; Save a Copy As 25.x /
24.x of After Effects 2026 keeps svap and writes the older version into
head. AeVersion decodes the packed word (26.2.1x2), AepFile gains
file_version, saved_by_version and head_revision, and ImportReport gains
aeVersion (fileVersion, savedBy, tested; "unknown" when the file does not
say). A version outside 24 to 26 is imported as usual and gets a
limitation line, not a loss.
The 12 corpus families saved as 2026, 25.x and 24.x copies import to the
same project and report; docs/aep-format.md lists what the copies change
in the container. The fixture tests are ignored until the corpus holds
the files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* AEP import: doc comments on the version tests and helpers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* AEP import docs: tested is a major-version classification from copies saved by After Effects 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* AEP import docs: the importer reports detected losses, not every loss
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* aep tests: compare the imported settings across version copies too
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* AEP import: map HDR Highlight Compression, Echo, Linear Wipe, Offset and Color Stabilizer
The importer reported these five effects as lost. They now become
EffectCraft's effects of the same name with every parameter After Effects
stores (ranges, defaults and popup items from our own After Effects 2026
project, documented in docs/aep-format.md 3.7):
- ADBE HDR ToneMap -> ec.utility.hdrcompression (Amount)
- ADBE Echo -> ec.time.echo (Echo Time, Number Of Echoes, Starting
Intensity, Decay, Echo Operator by item name)
- ADBE Linear Wipe -> ec.transition.linearwipe
- ADBE Offset -> ec.distort.offset (Blend With Original is a stored 0..1
fraction, a percentage here)
- ADBE Deflicker -> ec.color.colorstabilizer (fresh Stabilize default
Levels, points, Sample Size)
A table row can name the range EffectCraft's effect works in
(ParamMap::limit): Echo values or keys beyond 255 echoes or a Decay of 1
are imported as stored and reported.
Tests build each effect into the committed synthetic.aep (fx_inject.rs,
which also checks its pard layout reads back). On the sample project the
effect losses go from 75 to 3 (the out-of-range Echo values).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* AEP import: doc comments on the quick-win test helpers and tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* AEP import: Echo keyframe regression for the range loss
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* #3113: use After Effects' effect match names in scripting
Checked every row of crates/script/src/matchnames.rs against After Effects
26.2 (app.effects, canAddProperty and the layer property trees). Fourteen
effect names were wrong: twelve did not exist (Pixel Motion Blur is
ADBE OFMotionBlur, Shadow/Highlight is ADBE ShadowHighlight, Ellipse is
ADBE ELLIPSE, ...), and Noise and Photo Filter pointed at After Effects'
hidden obsolete versions (now ADBE Noise2 and ADBE PhotoFilterPS). A
light's Casts Shadows is ADBE Casts Shadows.
The old names stay accepted for lookup (addProperty, property(), and
parameters such as "ADBE Noise-0001"), so existing scripts keep working;
.matchName emits only the After Effects name. A names-only fixture of
After Effects' first-party match names backs the new test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Scripting: match names that changed only in case still find effects and parameters
property("ADBE Ellipse") and "ADBE Grow Bounds-0001" found nothing after the names became
ADBE ELLIPSE / ADBE GROW BOUNDS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Disk cache: stamp footage files for the salt off the UI thread
Importing an After Effects project whose footage includes long image
sequences on a network share froze the desktop app for 15-27 s. The
import itself takes about 0.25 s; the time went into the edit's bump(),
where refresh_disk_salt() stats every footage file (13,466 sequence
frames in the measured project, ~15 s) on the UI thread whenever a disk
cache is attached. File > Open and footage reload hit the same path.
With Session::disk_salt_background (set by the desktop app) the salt is
taken on a worker thread and picked up by poll_disk_salt() from the UI
tick. Until it lands the layer cache runs memory-only, so keys under the
old salt (other files, or another project's items) are never read. A
newer footage change supersedes a salt still being taken. Headless
sessions keep taking it synchronously.
A settings change that keeps the disk cache folder no longer re-stamps
every footage file either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Disk cache: opening a project drops the footage salt being taken
A salt job still running for the old project was kept when the new project named the
same footage, so the salt stamped before the files changed stayed in use.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Home screen: leave it when a menu command opens a composition
File > Import > After Effects Project... from the Home screen imported the
project (the toast said so), but the Home screen stayed on top, so it
looked as if the import had done nothing. Only the Home screen's own
buttons left it.
run_engine now leaves the Home screen when a command succeeds and changes
the active composition to one that is open, the rule scheduled script
tasks already follow. Frontend events run afterwards, so Help > In-App
Tutorials and New Project from Template still show it. A command that
opens no composition (or fails) keeps it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Home screen: also leave it for a project that replaces one with the same composition id, a picked file and the New Composition dialog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* interchange: probe EDL media the reader flags offline (#2836)
An EDL line without a SOURCE FILE comment only names its clip. The reader
guesses the path beside the list and flags the media offline, and the
importer never probed offline media, so the file next to the EDL stayed
missing. Offline media is probed like the rest; a path that is not there
still becomes a placeholder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* interchange: probe guessed paths of EDL media only (#2836)
An OTIO missing reference also gets a path built from the clip name and
is flagged offline, so a same-named file beside the document would have
replaced the intended clip silently. Only EDL imports probe offline media.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* interchange: confine guessed EDL paths to the list's folder before probing (review point)
CodeRabbit: an EDL clip without SOURCE FILE gets a path from its clip name; a
name like ../x.mov or an absolute one could link a file outside the EDL folder,
and rebase_to_file_timecode probed it too. Such guessed paths are now not
probed and stay placeholders; SOURCE FILE paths are kept as before.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* EXR export: write the output space's chromaticities attribute (#3037)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* EXR chromaticities: tests for unmanaged and PQ ProEXR, every space once
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* EXR export: note why the comp background is added before decoding (review point)
CodeRabbit asked to convert Comp.background to the output space before
compositing. Every output path (Image::to_rgba8_over) adds the background to
the output-encoded frame as it is, so the EXR now matches the other outputs;
converting would make the EXR colour differ from the PNG of the same comp.
Comment only.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Format the merged code with rustfmt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
After Effects 2026 accepts 0 to 200 for Brush Size of both effects (a
script setting 201 is refused with "out of range 0 to 200"); the range
here ended at 50. The slider keeps its 0 to 50 travel; larger values are
typed.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Skip the startup update check when started for an agent
An app started with --control or EFFECTCRAFT_CONTROL_PORT still checked
for updates at start, and the modal "newer version available" prompt
blocked agent runs until a person dismissed it. Agent launches already
skip crash recovery and the Home screen; now they skip the startup update
check too. Help > Check for Updates is unaffected (it goes through
updates_ui::check / check_requested, independent of the startup flag).
Documents what a control-port launch skips in docs/control-protocol.md and
the main.rs module doc, with a unit test for the startup decision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Control protocol docs: a window-start failure still shows a blocking message box
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The reference test now checks the bound the PR states in the code space of the renders
(the sRGB curve x 6553.5) instead of a linear 2e-3, which allowed about 28 codes at the dark end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A record in another tdsb state is reported as damaged data again, as before the
default-state change. The synthetic test checks Levels' recorded popup directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New After Effects 2026 renders of the same EXR at Amount 25, 50 and 75
(our own 1110-extra sample, 16-bit PNGs) confirm the transfer curve that
was fitted at 84, 90 and 100. The worst channel error over all pixels is
4.0, 5.5 and 4.3 of 65535, against 997, 2415 and 3867 for the old soft
knee. The code is unchanged.
The measured-reference test now covers all six Amounts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The effect used a soft knee on the brightest channel and left everything
below the knee unchanged. After Effects compresses the whole range. Fitting
our own 16-bit renders of an HDR ramp and coloured patches (Amount 0, 84,
90, 100) gives:
Y = max(0.30 R + 0.59 G + 0.11 B, 0)
comp = min(c / (1 + Y), 1) per channel
out = ((1 - t) c^0.4 + t comp^0.4)^(1/0.4) t = Amount / 100
A grey value x maps to x / (1 + x) at Amount 100. The worst error against AE
drops from 985 to about 2-4 of 65535 at Amounts 100, 90 and 84. Amount 0
stays identical.
CPU and GPU change together. Tests: a reference table of measured values,
GPU parity at Amount 90, and over-range input that exercises the clip at 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects records some effect parameters as value records in the
default state (tdsb 3) that still hold the value it uses. A survey of 839
corpus and sample files finds 25 such effect records, all in Lumetri
Color (its hidden controls) and Cryptomatte (Selection Mode on); every one
differs from its pard default and reads back in After Effects as recorded.
No effect record in the default state holds a value that does not count.
The general reader now takes them: EffectParam::recorded_value (the same
name, signature and body as in the Lumetri import) reads a record whatever
its state, value() and is_default() use it, and the importer's mapped
parameters read it, so such a record imports like a written one instead of
being reported as an unreadable value with the default used.
stored_value keeps to written records.
Layer, camera, light, shape and text properties in the default state all
hold After Effects' defaults or values with no effect on that layer
(the point light cone, a polygon's inner radius, a camera's opacity), so
their readers are unchanged. docs/aep-format.md records both findings.
Closes#3126
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Matte Only on, Cryptomatte wrote the matte into all four channels:
a premultiplied white only as opaque as the matte. After Effects'
Cryptomatte plug-in shows Matte Only as an opaque grey picture, the
matte in RGB with alpha 1 (its Matte Only render of 2026-11-05 is opaque
black where nothing is selected). The CPU effect and the GPU shader now
both give (m, m, m, 1).
Projects saved before keep their render: a hidden Matte Only In Alpha
switch (off for new instances, presets and AEP imports) is turned on by
migrate::legacy_default when an older instance is upgraded on open, and
then keeps the matte in every channel on CPU and GPU.
Tests: the CPU path for a partly covered pixel, an unselected one and
outside the aux area, in both forms; the upgrade of a saved instance;
the GPU parity test over every display with Matte Only off, on and on
with the legacy switch; the host pick tests read whole pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add After Effects project import with bounded parsing, explicit loss reports, and regression coverage.
Fetch the cleaned external test corpus at commit 97262276f625afdeeb61787e4cbb74b3f17af510 and verify each fixture against the pinned SHA-256 manifest.
CodeRabbit: the test name and doc claimed the default equals output from
before the setting existed. The reference is the exr crate's SMALL_LOSSLESS
preset (the layout since #2812); renamed and reworded, comparison unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- 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>
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>
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>
Adds tiled (ZIP16, RLE), tiled mip-mapped (ZIP16, PIZ), decreasing line
order, uncompressed, a uint R/G/B picture and a multi-part file whose first
R/G/B part is depth-named (the planes can't stand in for the image crate's
picture there). The new hashes were taken with main's crates/media/src at
a646bdcc (extracted with git archive) and the same test, before the decode
rework.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
- Rename RenderOpts::scene_output to skip_hdr_step (Pipe::without_hdr_step):
the consumer decodes with Pipe::output_linear or applies the step itself.
- Alpha-only EXRs hold the alpha as it is in every output space (it is
coverage, not encoded colour; it used to be sRGB-decoded).
- Tests: a half-opacity layer over a coloured background (premultiplied and
RGB-over-background paths, golden hashes taken before the change), Rec.
2100 PQ and HLG EXR outputs, raw alpha, and a visible skip of the GPU test
without an adapter.
- docs/architecture.md: the Working Gamma curve for unmanaged projects, and
no HDR step for unmanaged projects or PQ / HLG outputs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
B44 and B44A pack only half floats, so at 32-bit float the menu and the
render log say the samples stay uncompressed. Half-float frames hold samples
above 65 504 at that limit instead of writing infinity. The EXR parameters
move into their own helper; exr_out is public for other writers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenEXR renders (Render Queue sequences and Save Frame As ProEXR) received
the frame after Project Settings > HDR, so Compand and Tone Map (the default
of every OCIO project) compressed highlights and changed values below 1.0
before the writer decoded them, always with the sRGB curve.
- RenderOpts::scene_output: the top-level comp converts to the output space
without the HDR step (CPU and GPU walks, via Renderer::output_pipe).
- Pipe::output_linear: the writers decode with the output space's curve
(the Working Gamma's when unmanaged) instead of a fixed sRGB curve.
- Render Queue EXR jobs and ProEXR set the flag; PSD layers, 8-bit stills,
movies and the viewer keep the HDR step. The flag is part of the viewer
frame and disk cache keys (opts_hash).
Golden hashes, taken before the change, pin PNG/TIFF/Targa bytes for
8/16/32 bpc x Clip/Compand/ToneMap, Clip-mode EXR samples with an sRGB
output, and the layered PSD bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenEXR sequence output modules get two settings, exrCompression (none,
rle, zips, zip, piz, pxr24, b44, b44a, dwaa, dwab; scan lines in each
method's own block height) and exrBitDepth (half or float). The defaults
are ZIP and 32-bit float, so existing projects render the same bytes as
before. They go through the project file, Output Module templates, the
renderQueue commands and renderQueue.formats, the CLI (--exr-compression,
--exr-depth), the Output Module menu (lossy methods are marked) and the
render log. Half rounds each sample to the nearest half float.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Synthetic single-layer, multi-layer, multi-part and windowed files in
half, float and uint, with RLE, ZIP, PIZ and DWAA, decoded through the
media pool picture first and channels first. The hashes are taken on
main before the decode is reworked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve crates/vp9enc/src/lf.rs: main moved sb_plane into FrameFilter::sb_plane
(mi grid and limits as struct fields, no allow(too_many_arguments)). The
branch's 8-lane group filtering (apply mask per lane, filter_group) is
re-applied inside that method; the now-unneeded too_many_arguments allow on
filter_group is dropped (7 arguments).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve crates/av1enc/src/decide.rs: main replaced the long try_tx/code_tx
argument lists with the TransformBlock struct. try_tx and code_tx keep the
branch's scratch-buffer design (TxScratch taken out of the decider) and take
a TransformBlock; reconstruct now takes its three output buffers as a
ReconBuffers struct to satisfy the denied clippy::too_many_arguments.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve the conflict in crates/gpu/src/fx_stylize.rs: main moved ops::warp to
the ops::WarpOptions parameter struct; the branch's per-sample transform loop
(motion-blur samples, now passing the sample by reference) uses the new
signature with the branch's reference argument kept.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts resolved in crates/ui-egui/src/panels:
- effects_presets.rs: main split the options menu into contents_menu();
the search field's text-input role and the hamburger's menu-button role
now live in show() / contents_menu() on top of main's structure.
- render_queue.rs: main split the panel into helpers (current_render_strip,
footer, row_menu, log_dropdown, output_to, delete_selected). The progress
bar role, the row focus ring, combo-box roles with keyboard focus return,
the output template text-input and file button roles and the row
keyboard navigation are re-applied on those helpers. The keyboard block
moved into list_keys(), and status/render-time/started cells into small
helpers, to stay under the new cognitive-complexity and argument limits.
- menus/shortcuts.rs: the Enter-owning panel check moved into
panel_owns_enter() to keep handle_shortcuts_impl under the new limit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflict in crates/track/src/camtrack/bundle.rs: kept both the branch's
PtSys/preconditioner/iterative_schur block and upstream's new
add_focal_prior helper. Adapted to upstream's explicit linalg imports
(conjugate_gradient, and I3/log_rot/mt/norm in the tests) and moved the
explicit point elimination into reduce_points so adjust_impl stays under the
cognitive-complexity lint.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflict in crates/track/src/camtrack/bundle.rs (imports): upstream replaced
the linalg glob import with an explicit list; kept that list and added the
branch's SolveError import (use super::{Distortion, SolveError}). Input
validation, size budget and Result-returning adjust/cost are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflict in crates/track/src/face.rs: upstream moved the feature-blob search
into FaceTracker::feature_blobs. Re-applied the branch there: the per-pixel
point-in-polygon loop is replaced by non_skin_inside (one crossing pass per
window row) and the roll rotation is hoisted with face_mapper.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Upstream split lib.rs files into modules and moved engine commands/ to cmd/; this branch's change merges without textual conflicts.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Upstream split lib.rs files into modules and moved engine commands/ to cmd/; this branch's change merges without textual conflicts.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts came from upstream splitting crates/project/src/lib.rs and moving the
frames tests. Re-applied the branch's change in the new places:
- Layer::same_pixels_with now in crates/project/src/layer.rs
- Comp::same_pixels_with, same_settings(o, with_markers), pixel_form_with and the
timing-marker helpers in crates/project/src/comp.rs (same_pixels_ignoring_markers
stays removed)
- Project::reads_markers in crates/project/src/item.rs
- crates/ui-egui/src/frames.rs uses reads_markers/same_pixels_with; the branch's
two marker tests replace the old ones in crates/ui-egui/src/frames/tests.rs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
text/layout: upstream split paragraph() into Para/place_line helpers and moved the tests to
layout/tests.rs. The branch's changes are re-applied there: the resolved paragraph direction
is passed to margins_for/avail_for/align_for (RTL point text with Justify sits flush right),
first_ascent/plausible_first and ParaLine.first/style_first (box first baseline is the d
height, vertical text keeps the ascent), and the Justify All last-line cost in
optimal_breaks. The branch's seven new layout tests moved to layout/tests.rs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts: crates/ui-egui/src/menus.rs (upstream split it into menus/{bar,commands,entries,frontend,invoke,shortcuts,tests}.rs) and panels/settings.rs (ComboBox is now craft_ui::ComboBox).
- menus.rs: took upstream's slim module; re-applied the branch's two hunks in the new locations: nudge() now also skips Effects & Presets and Render Queue (menus/invoke.rs), and Enter is left to those panels in handle_shortcuts (menus/shortcuts.rs).
- panels/settings.rs: kept the branch's accessibility lines (shown_name, value_text) on top of craft_ui::ComboBox.
The accessibility audit BASELINE still matches the measured findings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Keeps the already-pushed branch history; the resolved tree is unchanged: render tests live in
crates/render/src/tests/ (mask_feather.rs registered in tests.rs).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
render: upstream moved the tests_*.rs modules into crates/render/src/tests/; the branch's
tests_mask_feather.rs now lives at tests/mask_feather.rs and is registered in tests.rs.
path: rect() keeps the branch's explicit-outline version with an added NaN guard on the
roundness clamp bound (main's fix for the same panic).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Media Browser opens on the home folder, so a runner with fewer files finds fewer controls and
the 'improved, lower the entry' check failed in CI. The entry stays an upper bound.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflict in crates/text/src/layout.rs (paragraph direction at the top of
paragraph()), against the closure-to-method-path cleanup (4e9fa51f): kept this
branch's direction resolved once from BidiInfo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A panel with a BASELINE entry that is not drawn now fails the audit instead of recording a zero row.
A count below its BASELINE entry now fails until the entry is lowered, so the old allowance cannot hide
a later regression. The star test's walk to "Blur & Sharpen" has the same 40-step guard as the others.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (CodeRabbit on #1875): with the direction unset, Hebrew text resolved to
right-to-left took its start indent on the left. The paragraph direction is now
resolved once from BidiInfo before the margin and alignment helpers, so both use
it. Regression test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: docs/ui-parity.md (Estimate). Kept main's 60-110 h total
without the multi-monitor line and this branch's accessibility figure
(12-25 h, first slice done).
Adapted to main's shared docking layout (#2432):
- tests/ui_accessibility_audit.rs: Preview now gets a short frame at the
top of the right column and its controls below it stay in the tree,
counted against the panel underneath. The harness closes Preview, and
the per-panel pass closes it again after its own scenario. The
not-yet-fixed BASELINE numbers are re-measured in the new layout
(panels are taller, so more controls are visible); the fixed panels
stay at zero.
- tests/ui_effects_presets_keyboard.rs: the taller Effects & Presets list
now draws the starred Gaussian Blur row in Blur & Sharpen, so the
closed-Favorites check counts Gaussian Blur tree items (at most one)
instead of requiring no "Remove from favorites" star at all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (CodeRabbit on #1875): with the paragraph direction unset, a right-to-left
paragraph resolved by BidiInfo was still aligned as left-to-right justified point
text, so it stayed left instead of flush right. eff_align now takes the resolved
direction. The box-text first baseline used the font ascent whenever any run of
the text was vertical; it now looks only at the runs of the first line.
Two regression tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts for the egui 0.37 migration: Id::unique ids, kept the
accessibility names and roles; stop giving unnamed check boxes, twirls and
toggles a role so the kittest accessibility check passes; refresh the audit
baseline.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- delta_for no longer searches 0..255 for every probability: a 256x256 table is
built once.
- BoolEncoder remembers where the trailing run of 0xFF bytes starts, so a carry
no longer rescans the output (the cost was quadratic over long runs).
- Vp9Encoder::new clamps width and height to 65536; larger sizes used to wrap
in the 16-bit frame size fields and write a corrupt stream.
The output is byte-identical for all sizes up to 65536.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The 8x8 and 16x16 SAD uses per-column 16-bit sums, and the cost of a motion
vector (a pure function of the vector) is remembered for the vectors the
search tests again. The output is byte-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The 16 outputs of the 4x4 forward transform are accumulated together in the
same order as before (no serial add chain per output) and the quantiser
truncates instead of calling floor on a non-negative value. The output is
byte-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The encoder's loop filter runs three trial passes per frame with a scalar
filter. Filter the eight positions along an edge together with 16-bit lanes
(ported from the decoder's loop filter); the scalar filter stays as the test
oracle and a new test compares both on 20000 random cases. The output is
byte-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The 16 quarter-sample phase planes are built with 16-bit lane filters (the same
narrowing trick as the decoder), edge replication copies rows instead of
clamping every sample, and mc::predict reads straight from the reference for
blocks away from the edges. The output is byte-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Bitrate mode coded the first frame four times with a fixed gain. Now the
first pass still gets the old correction (30 quantizer steps per halving
of the size, which fits the starting range), and from the second
correction on the next quantizer is placed in the log domain of the
quantizer step, where the frame size is close to a line: false position
between the nearest passes that enclose the target, otherwise a secant
step with the measured slope. The search stops as soon as the size is
within 3 % of the target, when no untried quantizer is left between the
enclosing passes, or when it would repeat a quantizer (a target the
content cannot reach, where the old code coded the same frame at the
lowest quantizer up to four times). At most four passes remain.
The first frame is within 3 % of the target on the same cells as before
and nearer to it on most of the others; two cells where the size jumps
by a factor of 4 or more between neighbouring quantizers (noisy content)
land further away than with the old search. Total passes over the 28
test cells: 112 -> 70.
Part of #2390
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>