- Cryptomatte (#1183): planes shorter than the image read as empty instead of panicking.
- layer.addMarker (#1180): a comp marker no longer replaces one at the same time.
- Desktop (#1005): ignore macOS's -psn_ argument instead of refusing to launch.
- Doc comments put back on their own items (#1153, #1037).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Gradient Ramp wrote alpha 1.0 to every pixel, so a shape layer's transparent area filled up with
the ramp and the layer's bounds showed instead of the shape. The ramp now uses the pixel's own alpha
on the CPU and in the GPU kernel, as the #939 fix does for the noise effects. A regression test
checks that transparent pixels stay transparent at two Blend With Original values.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
3eb3b99 makes the particles replace their layer as in AE, so their 112 pinned
CPU hashes change intentionally; no other effect changed.
(cherry picked from commit 9900eae773e1cad43a6d1bdcacbadc45a84b063a)
(cherry picked from commit 7aa27c0dd7c24b9f24591bb144a95951dc0ec510)
(cherry picked from commit 73377e8eca5c639715e5f3f623ed53ff329638a0)
(cherry picked from commit f07497dfee)
Echo, Posterize Time and the other Time effects read the layer at other times with none of
its effects. After Effects renders them with the effects before them: SceneYard "Task
Preparation UI" applies Echo after CC Particle Systems II on black solids, and AE shows the
echoed fireworks over a white page where we echoed the black solids and blacked it out
(Posterize Time after Turbulent Displace, the usual stop-motion setup, depends on the same).
The host already bounds the count to the effects before the one rendering.
Mean abs error vs AE: Task Preparation UI 0.1855 -> 0.0071, Lightbulb Idea 0.0126 -> 0.0102;
the 15 other corpus projects with a Time effect after another effect are pixel-identical.
(cherry picked from commit 46870452db2656c72406f63245e8b1609ceb04d4)
(cherry picked from commit 334d102582e5a64db32e2f3e55836d55ac96bdaa)
(cherry picked from commit 734da3e35dc57c01b73823458bdd87611ac1bf99)
(cherry picked from commit 87a733e076)
The particles were composited with the original layer by the Transfer Mode, so the solid an
artist applies them to stayed visible. After Effects 2026 frames show no trace of it:
SceneYard "Burnout matchstick" (a red rectangle around the flame), "Documentary Man Falling"
(a black block over the light), "Task Preparation UI" (black solids under fireworks). The
Transfer Mode now only sets how the particles meet each other.
Mean abs error vs AE: Burnout matchstick 0.0191 -> 0.0130, Documentary Man Falling
0.1177 -> 0.0937 (Task Preparation also needs the next commit).
(cherry picked from commit 3eb3b9958f9cd929dcf060865e604d42cc0f9337)
(cherry picked from commit 729638710e63f4da10711fc5e6a1f7ddc42c37aa)
(cherry picked from commit 083332116c936f1b4863f0bb390d0647f2b567a1)
(cherry picked from commit 338421de36)
AuxChannels::crypto_rank_planes resolves a layer's (ID, coverage) planes once; crypto_ranks keeps its signature on top of it. The CPU effect and the GPU effect's aux image (effectcraft_effects::crypto_aux_image, rayon) index the planes per pixel. Output is bit-identical.
Masks are applied before effects, but the noise was composited with the
Opacity alpha alone, so a masked solid filled the whole layer. The result
is now multiplied by the original alpha on the CPU and on both GPU paths
(kernels.wgsl for the default settings, fx_noise.wgsl for the rest).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Problem: computed Object Matte segmentations lived only in the in-memory
cache, so every restart or memory eviction segmented the shot again (After
Effects 26.5 keeps Object Matte results in its disk cache).
Fix: the persistent disk cache gets a third entry kind, Matte (folder
`mattes`, LZ4-compressed FrameSeg bytes, same checksum, atomic writes, LRU
eviction under the one size limit, purged with the rest). Entries are keyed by
the segmentation's chain key and scale; chain keys already hash the Input Key
(the layer's source, timing, masks and effects above), the prompt, the mode and
the model, so any change simply misses. The effects crate gets a SegStore
trait behind its memory cache: Chain::ensure reads from it on a memory miss
and writes what it computes. The engine installs the session's DiskCache as
the store (held weakly, so switching the disk cache off drops it). Roto Brush
segmentations go through the same chain and are persisted too. Frozen mattes
already persist in the project (the Frozen parameter).
Tests: engine tests_roto::object_matte_mattes_persist_in_the_disk_cache (a
second session on the same folder and project gets all tracked frames back
without being able to read a single input frame, renders identically, and a
moved prompt finds nothing); render disk_cache
segmentations_round_trip_and_reopen (round trip, reopen, damaged file is a
miss, size limit evicts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Object Matte computed its frames only on demand while rendering, so
playing a shot stalled on every untracked frame, there was no progress or way
to stop, and nothing kept the mattes once computed.
Fix: Object Matte runs through Roto Brush's job machinery (one job slot,
cancellable with roto.cancel, a worker thread on the desktop, a Web Worker in
the browser, never the UI thread). objectMatte.propagate computes the frames
around the reference frame (clipped to the layer) outward, filling the same
content-keyed segmentation cache the renderer reads; the engine's sync now
queues an instance for background tracking when its frames or prompt change.
objectMatte.freeze stores the final mattes in a new hidden Frozen parameter
(Roto Brush's FrozenCache format) under a key of the input, prompt, span,
model and matte settings; renders use them without segmenting while the key
matches; a frozen prompt is refused until objectMatte.unfreeze. Freeze is one
undo step and is dropped if the prompt changed meanwhile.
objectMatte.status reports progress, frozen state and computed frames. Effect
Controls shows Freeze / Unfreeze and Track / Stop with a status line (progress
banner). The frontend now polls Roto Brush / Object Matte jobs on their own:
before, a finished job (and so a Freeze result) was only picked up while a
Warp Stabilizer job was active. Job progress is set before the worker starts.
Tests: engine tests_roto::object_matte_background_job_cancel_and_freeze
(queued by the edit, returns at once with progress, cancel keeps a partial
result, completion computes all 25 frames, frozen renders recompute nothing,
unfreeze recomputes, undo restores); effects object_matte job_frames and
frozen-matte tests (stored pattern used, key mismatch and damaged data
ignored); ui-egui ui_object_matte Effect Controls Freeze end to end.
docs/agents.md: Object Matte section; docs/effects.md regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Object Matte could only be applied through the objectMatte.apply
command; After Effects 26.2 picks the object directly in the viewer (click it,
or drag a box around it).
Fix: an Object Matte tool (with Roto Brush & Refine Edge in the Alt+W slot,
icon drawn in code). In the Composition viewer a click sets a point prompt and
a drag a box prompt (its centre the point) on the footage, solid or comp layer
under the pointer, converted to layer pixels through the layer's transform.
The layer's Object Matte (the selected one, else its last) is re-prompted;
Shift adds another. Everything goes through objectMatte.apply, which gained
`effect` to re-prompt an instance (point, box or no box, reference frame; the
reach kept unless given), so each prompt is one undo step; the effect is then
selected, so its point controls show in the viewer. Hover draws a crosshair
and the outline of the layer that would get the matte (a live matte preview
would need a segmentation per pointer move). Automation ids:
viewer.objectMatte.crosshair / .target / .box, tools.ObjectMatte.
Tests: ui-egui ui_object_matte (hover, click, drag box re-prompts the same
effect, Shift-click adds one, undo steps); engine
tests_roto::object_matte_reprompt_updates_the_instance. Checked with the
headless snapshot example (hover crosshair, box drag isolating a disk).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: with a trained model installed, Object Matte's optional box reached
MobileSAM only as background points along the box's edges (an approximation),
although the model's prompt encoder takes a box prompt directly.
Fix: a new Roto Brush stroke kind, Box (two opposite corners), that
stroke_prompt turns into the model prompt's bbox (scaled to the matte grid,
corners in any order, degenerate or non-finite boxes ignored). The graph cut
never rasterizes it. Object Matte emits it instead of the edge strokes when a
model segments; the classical fallback keeps the edge strokes. A prior matte's
box no longer replaces a box the user drew on that frame.
Tests: effects object_matte::the_box_reaches_the_model_as_a_box_prompt (a
stand-in model sees the box, at full and half resolution, and no edge points);
track roto::tests::box_stroke_is_the_model_box_prompt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: After Effects 26.2's Object Matte (pick an object with a click,
optionally a box, get a matte that follows it) had no counterpart; only
stroke-driven Roto Brush existed.
Fix: an Object Matte effect (Effect > Matte, ec.matte.objectmatte) turns
the Object Point (and optional box) on a Reference Frame into Roto Brush
strokes and segments them with Roto Brush's engine, content-keyed cache
and the model chosen in Settings > Roto Brush (MobileSAM, classical graph
cut without it). Track Object carries the matte frame by frame up to 600
frames each way; otherwise every frame is segmented on its own. Feather,
Contrast, Shift Edge, Invert and a Matte view. The engine keeps its Input
Key current so upstream edits recompute it. objectMatte.apply creates one
from agents / the CLI. Unusable prompts pass the layer through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: selection_hashes split the Selection on every comma before
stripping quotes, so a name containing a comma (Blender allows a material
called `Green, Glossy`) was cut into two names that match nothing, quoted
or not; only its `<hex>` hash selected it.
Fix: a small tokenizer splits at commas outside quotes. A name in double
or single quotes stays whole, a backslash takes the next character
literally (`Green\, Glossy`), and unquoted lists, wildcards and `<hex>`
hashes parse as before. The CPU effect and the GPU path share the function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The modern curve used g = 2^(2c), so Contrast 100 crushed 0.25 to about 8/255 while AE is about 40/255. Brightness is now a pin-black-and-white power toward white or black, then a milder S-curve g = 1 + 0.53 c, fitted to kasper9n's AE grey ramps. Legacy linear mode is unchanged. The GPU kernel matches the CPU.
Time > Pixel Motion Blur fetched the layer at its shutter times and averaged the decoded
frames as they were, so fast motion rendered as doubled copies and the accepted Vector
Detail parameter changed nothing. Build a TimewarpPlan instead: every shutter instant is
decoded between the two source frames it falls between along a measured motion field — the
same block-matching and in-between synthesis Time > Timewarp uses, with a search radius
wide enough for full-frame displacements and the vector grid tightened from Vector Detail
0 (one vector per 32 px) to 100 (one per 2 px) — and the instants average into the blur.
CPU and GPU share the plan (fx_time renders it with its Timewarp step loop, declining to
the CPU when there is no host), so the existing GPU parity cases for the effect compare
the new path. Falling back to the old average is kept for layers without a time host.
A render integration test moves a textured block 12 px between source frames, renders
the straddling shutter, and asserts the output is neither the still nor the plain frame
overlay, lays trail coverage along the block's path, and changes with Vector Detail;
without the synthesis it fails at the overlay assertion.
An authored layer's effect buffer spanned only its painted content — the glyph
bounding box for text, the drawn bounds for shapes — while the size the stack
measures from (effect_bounds) is the whole comp-sized layer rectangle. Tiling
and displacement effects computed correct output for pixels outside the painted
rectangle and then wrote them into the buffer that was too small, so CC Tiler
repeated only within the text box and CC Smear lost its liquid trail at the
glyph edge (#658). Buf::cover now grows the stack's input to the layer's
rectangle with transparent margins — what a sized source's buffer already is —
applied by run_effect_stack whenever an enabled video effect will run, and
natively on the GPU by reallocating and copying the texture. The placement and
the sized-source paths are unchanged; two regression tests (CC Tiler spans the
region, CC Smear trails past the text bounds) fail without the growth.
A text or shape layer's effect bounds are comp-sized (as in After Effects),
but its buffer surrounds only its content, and each effect grows the buffer
itself when it needs room. CC Smear and CC Tiler never did, so a smear was
cut off where the glyphs end and tiles filled only the text's box.
CC Tiler now covers the whole layer rectangle; CC Smear grows by how far it
moves pixels (Reach x From->To), at most to the layer bounds, so footage
layers, whose buffer is their bounds already, are unchanged. CPU and GPU
alike.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Preserve legacy selection repair, modifier routing, viewer lifecycle and workspace persistence through the shared docking integration. Qualify the complete workspace gate by refreshing the 18 stale Audio Spectrum hashes for the existing linear renderer, with an independent synthetic-tone regression.
cargo +1.95 clippy --workspace --all-targets --release -- -D warnings
stopped on seven `if` blocks that clippy wants folded into their match
arm (psd, text, effects x2, pdf x2, render). Each becomes a match guard.
The behaviour is unchanged: when a guard is false the value falls through
to arms that can't match it and then to `_ => {}`. The one exception is
the PDF PostScript calculator, where a `{` past the nesting limit would
fall through to the operator arm; it keeps its own no-op arm, so it is
still skipped rather than pushed as an operator.
Cause: Apply Color LUT's only parameter, `lut`, was ParamUi::Hidden (set only
through commands), so the effect could not be used from the UI. Effect
Controls also had no widget for any ParamUi::Text parameter (only EXtractoR's
channels had one), so Cryptomatte's Selection, the OCIO effects' config file /
file / config names and Colorama's Output Cycle could not be set either.
Fix: a new ParamUi::File { filters } for paths. Effect Controls shows the
file's name (None when empty) and a Choose... button that opens the host's
file dialog with those extensions and sets the path (prop.set, so agents use
the same command). As in After Effects, applying an effect with an empty file
parameter from the UI (Effect menu, Last Effect, drops, Effects & Presets)
opens the file dialog straight away; cancelling keeps the effect without a
file. Apply Color LUT's LUT (.cube) and the OCIO Config File (.ocio) and File
(.cube/.3dl/.csp/.cc/.ccc/.cdl/.spi1d/.spi3d/.spimtx) use it. Text
parameters get a single-line field that sets the value when it loses the
focus (Esc keeps the old text). Saved projects are upgraded on open: a
Hidden or Text parameter whose spec is now a File parameter takes the File
UI and keeps its path (effects::migrate). docs/effects.md regenerated.
This covers #473's first part (the Selection field); picking mattes in the
viewer, the layer popup from the file's metadata, the GPU path and quoted
commas are separate.
Tests: effects hidden_and_text_paths_become_file_parameters (migration); ui
file_and_text_parameters_have_widgets (the LUT is asked for on apply and
inverts red to cyan, the Choose... button picks another LUT, typing into
Cryptomatte's Selection sets it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit 403863f703999d19d3a83d9976f76bdc7577e682)
Cause: write_on_plan drew a single dab at the current Brush Position. Stroke
Length, Brush Spacing and the Paint/Brush Time Properties were declared but
unused, so an animated brush showed a moving dot that dropped every earlier
mark.
Fix: as After Effects describes Write-on, a dab is laid every Brush Spacing
seconds from the layer's start up to the current time, each lasting Stroke
Length seconds (0: for ever). Their positions come from the host's parameters
at their times (EffectHost::params_at, which the renderer implements). Paint
Time Properties > Opacity and Brush Time Properties > Size (or Size &
Hardness) keep each dab's own opacity / size; otherwise the current values
apply to all. One frame draws at most 4096 dabs (longer strokes space them
out and join them). Without a host the current dab draws, as before. Colour
and hardness stay one per stroke (the paint plan has one colour and hardness),
so Paint Time Properties > Color and per-dab hardness aren't modelled. The GPU
path uses the same plan.
Test: write_on_accumulates_a_stroke_along_the_animated_brush (the report's
fixture: start and midpoint marks stay at 0.5 s with the end still clear, all
marks at 1 s, Stroke Length 0.25 keeps only the recent trail, Brush Spacing
0.5 gives three separate marks, per-dab opacity).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit c82c9aeb685eab0cb36ca69c0b337781f4bad258)
Cause: CC Particle World projected its particles through a fixed camera
(distance 2 layer widths, looking at the layer centre) and never asked the
host for the comp camera, so moving or orbiting a camera layer left the
particles flat on the layer. It also had no Extras > Effect Camera. Separately,
the layer cache key ignored the comp camera and lights, so the effects that
read them (Card Dance / Shatter / Card Wipe Comp Camera, First Comp Light,
Caustics) kept a stale frame when the camera, a light or the layer moved.
Fix: as the Cycore manual describes, Particle World sees its particles through
the comp's active camera layer when there is one (EffectHost::comp_scene now
says whether its camera is a camera layer's, CompScene::camera_layer), through
card3d's comp-camera projection (factored out of Card Dance as comp_proj, with
Proj::view for depth, size scale and screen velocity). Otherwise it uses the
new Extras > Effect Camera: Distance (2), Rotation X / Y / Z (0) and FOV (50)
orbit the world origin; FOV moves the camera so the origin's plane keeps the
scale Distance gives it, so the defaults reproduce the old fixed camera bit for
bit (golden hashes unchanged) and old projects render as before. The Effect
Camera is left out of the simulation cache key, so animating it doesn't
restart the simulation. The plan is shared, so the GPU compositor follows too.
The layer cache folds the comp scene into the key of layers running an effect
in effects::READS_COMP_SCENE (render::comp_scene is shared with the host).
Tests: sim particle_world_follows_the_comp_camera (panning a camera layer moves
the particles, zooming spreads them, the Effect Camera is ignored then, the
default view keeps the Effect Camera, cameras facing away / zero / NaN draw
nothing) and particle_world_effect_camera (missing params = defaults, rotation,
distance and FOV change the view, extremes stay finite, the sim key ignores the
camera); render particle_world_follows_the_comp_camera (a real camera layer
panned 40 px moves the particles left, and the cached render after the move
equals an uncached one: it failed with the old cache key); gpu
particle_world_through_the_comp_camera (CPU vs GPU at 8 / 32 bpc, full and
half resolution). particle_golden / sim_golden unchanged. effects.md
regenerated; parity.md and gaps.md describe the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cause: the config reader didn't know ColorSpaceTransform or
ExponentWithLinearTransform and passed colours through them silently.
Blender's Linear Rec.709 -> sRGB therefore stayed in its E-white XYZ
reference space (the blue cast) without the sRGB curve. Display
colorspaces, view transforms, v2 views, looks, AllocationTransform,
BuiltinTransform and .spi1d / .spimtx files were missing too, and
nothing told the user.
Fix (from the OCIO v2 documentation and the CLF "moncurve", not OCIO's
source): ExponentWithLinear, ColorSpace, Look, DisplayView, Allocation
and Builtin transforms (identity, ACES AP0/AP1 matrices, ACEScct /
ACEScc, the sRGB / P3 / Rec.1886 / gamma / PQ displays). RangeTransform
now clamps and takes one-sided ranges. Display colorspaces meet the
scene reference through the default view transform. Added v2 views,
view looks, the looks section and .spi1d / .spi3d / .spimtx files. A
conversion is built once as a flat list of steps (Config::path /
display_path) for both the CPU effect and the GPU colour program (new
moncurve and clamp ops and the ACEScc curve; a program over the
kernel's 255 ops renders on the CPU). Named-transform resolution is
limited to 16 levels and 4096 steps, and YAML nesting to 64 levels, so
hostile or looping configs stop. Transforms that are still unsupported
(Grading Primary / Tone, ACES output, HLG…), unreadable configs and
unknown spaces are now reported: a warning under the effect in Effect
Controls, and the new effect.warning query.
Tests: ocio_config::blender_like_config_converts_linear_rec709_to_srgb
(CST, display colorspaces, v1 / v2 views, allocation, looks, builtin,
range, loops), moncurve_matches_srgb_and_inverts, builtin_styles,
yaml_nesting_is_bounded, ocio::spi_files_parse,
tests_effects::ocio_effect_warns_about_unsupported_config_transforms,
gpu tests_fx_lut::ocio_custom_config_v2 (GPU = CPU).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effects: effect points on shape layers start at the layer centre (#227)
# Conflicts:
# crates/ui-egui/src/panels/puppet_tool.rs
# crates/ui-egui/src/panels/viewer.rs