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>
Poison recovery, OsStr::to_str and char predicates only; S4325 casts are
f64 to f32 and stay.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Shadow maps use the full scene-derived size (point-light faces capped
at 2048) under a 26M-texel scene budget halved largest-first; the PCF
filter measures its reach in output pixels with a two-texel minimum,
samples texel centres on a jittered adaptive 5..10 grid, and compares
each texel against the receiver plane's own depth so wide kernels never
self-shadow lit casters. The WGSL mirror is parenthesised (Tint rejects
the old form) and guarded by a shader-wide grammar test; the CPU
reference and shader remain step-for-step parity-tested.
On an adjustment layer Motion Tile kept the comp-sized buffer whatever the
Output Width / Height, and the adjustment draw dropped any stack result whose
size differed from the canvas. After Effects grows (or crops) the comp
rectangle there as on any layer, so a Transform after it brings the extra
tiles into view, as in the frames attached to #1727 and #1409.
- motion_tile_frame: one output frame for the CPU and GPU effect; on an
adjustment it grows about the comp rectangle on whole pixels, keeps the
buffer at 100 % and past 16 384 pixels a side.
- adjustment_crop: the CPU and GPU adjustment draws put a stack result whose
frame changed back onto the canvas instead of dropping it (Enc::crop on
the GPU).
Tests: the #1727 After Effects frames (910 x 340 % and 90 x 340 % then
Scale 12 %), tiles past the frame and cropping on CPU, GPU vs CPU for
adjustment and full-frame layers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review: styled() kept the buffers it rendered instead of the ones LayerCache::insert returned, so
without a blending-space conversion concurrent misses still returned separate body and pass
buffers (and the GPU uploaded each).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preview workers that start together miss the layer cache together, and each kept its own copy
of every layer; on the GPU each copy was uploaded and packed into its own Classic 3D atlas, which
ran the device out of memory on a cold start. LayerCache::insert now keeps the first buffer of a
key and returns it, and the renderer goes on with the returned buffer, so the workers share one.
Renders don't wait for each other: on the shared frame pool a blocking wait could end up beneath
the work it waits for (tried, it stalled playback for the wait's timeout).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review: mask motion blur draws an animated mask at the shutter's other times, but the layer key
hashed its value at this time only. Two frames with the same mask value (a hold before a move)
shared a cached layer, with the wrong blur. This applied to solids, text and shapes already, and
to stills since this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Footage layers had no layer cache key, so a still made a new layer buffer every frame on every
preview worker, and the GPU compositor uploaded it again each time (its upload cache is keyed
by buffer). Stills (without a proxy) are now keyed like text and shape layers, without the
source time; movies and sequences are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Above MAX_GRID_PX the padded feather grid fell back to the unpadded buffer, so a
feathered Add mask just outside a very large layer (e.g. 8192 x 8192, feather 40)
covered nothing and the whole layer vanished (CodeRabbit on #1874). The grid is now
coarsened by 2, 4, ... until it fits; radii and padding scale with it, so the feather
of an edge outside the layer still fades into it. The doc comment on the Linear falloff
no longer says it is unchanged: wide linear ramps now go through the coarse grid.
Regression tests: a_grid_too_big_is_coarsened_and_keeps_its_padding,
a_coarsened_grid_still_feathers_a_mask_outside_the_buffer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects gives Repeater copy i the opacity Start + (End - Start) * i / Copies,
so the last copy stops one step short of End Opacity. EffectCraft reached End
Opacity at the last copy (i / (Copies - 1)). Measured in AE 2026 (case 709: 3
copies, End Opacity 40): copies at 100 / 80 / 60 % (EffectCraft 100 / 70 / 40);
the overlap of the two copies 92 % (EffectCraft 82 %). Now within 2/255 at the
sampled pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Smooth feather is three separable box passes of 2 round(0.37 F) + 1
pixels (a quadratic B-spline reaching ~1.11 F either side), fitted to
After Effects 2026 renders of feather 100, 300 and 1000 (rows, columns,
corner diagonal, case 398): max error under 1/255, where a Gaussian of
any width misses by 3/255 or more (it was sigma = F / 2). The coarse
grid for wide feathers picks the step whose three windows add up
closest to the full-resolution ones. Linear is unchanged (unmeasured).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mask coverage was rasterised into the layer-sized buffer and then
blurred, so coverage outside the layer counted as 0: a feathered mask
past the layer edge faded at that edge, and one just outside the layer
gave nothing (the comment said the buffer grew; nothing grew it).
A feathered mask is now rasterised on a grid padded by the blur's whole
support (3 sigma + 3 for the Gaussian, the box radius for a linear
falloff), blurred there and cropped back to the buffer. Wide feathers
(sigma > 32 px) use a grid coarser per axis so the blur spans ~16 grid
pixels, which keeps the allocation near the buffer's size (feather 1000
on 1920x1080: a 160x134 grid instead of ~20 Mpx); the grid is capped,
feathers are clamped and non-finite ones ignored. Unfeathered masks and
masks inside the layer give the same coverage as before. The GPU uses
these CPU masks (adjustment footprint, masked sources): no GPU change.
Tests: case 398 of the .aep corpus rebuilt in the project model (red
solid, adjustment layer with a default Gradient Ramp, a 1920x200 mask
below the layer, feather 1000) within 3 levels of the Gaussian and
within 14 of After Effects' render; a mask right of a solid, a mask
larger than the layer (edges stay opaque), linear falloff, half
resolution, and huge or broken feathers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects 2026 in a 32 bpc project, Levels (Individual Controls) with RGB
(0.1, 0.9, gamma 1.5, 0.05, 0.95), Red Gamma 0.8, Green Input White 0.8 and
Blue Output Black 0.2 on grey steps 0..255 (case 904 E07): every step matches
"each channel's controls, then the RGB controls" with nothing clipped
(values below Input Black end below Output Black, above Input White above
Output White). EffectCraft ran RGB first and clipped "Off for 32 bpc Color"
(it could not see the bit depth): up to 27/255 off on 95 % of the grid; now
0 grid points off, max 1/255, CPU and GPU.
EffectEnv gains float_depth (the renderer sets it for 32 bpc projects), so
"Off for 32 bpc Color" clips in 8 / 16 bpc projects as before. Unclipped
values below 0 take the gamma mirrored through 0. Plain Levels keeps its
order (its channel controls were not measured); its clipping follows the same
rule. EffectCraft's own projects default to 8 bpc and are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects 2026 (sRGB working space, not linearised) maps red, green,
blue and 50% grey solids at Tint's defaults to 76, 150, 29 and 128/255:
Rec. 601 weights on the working-space values, not Rec. 709. CPU and GPU
kernel changed; tests pin the four greys within 1/255.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Units set to Index the selector already counts characters (words,
lines), but the timeline kept showing Start, End and Offset as
percentages ("3.0%"), so switching looked like it did nothing. They are
now shown without the % sign while Units is Index
(render::text::in_index_units).
Engine part of viewer picking: replace / add / remove the top-coverage ID at a
layer-space point in one undo step, at the time the Selection was read. The
aux channels come from the render's own path (Renderer::layer_aux, shared with
the effect host; footage at its source time, 3D precomps) through a renderer
set up like Session::render (expressions, render prefs, layer cache). An
optional scale snaps the point to the pixel a preview at that resolution
samples. New effects::crypto_select quotes names and falls back to <hex> for
unnamed, wildcard-looking or ambiguous names. effect.cryptomatteAt is the
read-only hover query.
Synthetic bold grows a glyph's outline in its own space, so a
non-uniform character Scale stretches it more along one axis. The patch
and layer bounds reserved it through mean_scale and could clip the
stretched side; they now reserve how far the bold reaches along x and y.
A run of solid fills grows its glyphs together in layer space by one
radius, which only matches when the bold stays round there: a bold glyph
whose matrix stretches or skews is no longer gathered into a run.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replacing a virtual file bumps a counter the UI lookup reads. The content hash runs on that miss, from a callback, so the frame path does not stat every footage item or hash on every store write.
The layout marks a glyph synth_bold when Faux Bold is on, or when a bold
style is asked of a family without a bold face, but the renderer never
read it, so both drew as regular.
PlacedGlyph now carries the bold radius (0.03 em, which brings Inter
Regular's stems to Inter Bold's). A bold fill is the outline's coverage
merged with a round stroke twice that wide, and a bold glyph's stroke is
the band between the outline grown by radius ± half the stroke width, so
it follows the bolder shape. Runs of solid fills only merge glyphs of the
same bold radius, and the patch and layer bounds grow by it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Advanced 3D renderer folded Accepts Shadows (Off/On/Only) into a
bool, so a shadow catcher set to Only drew as an ordinary lit surface.
The material now carries shadows_only. Such a fragment draws black at
the strongest shadow a light casts on it, as Classic 3D does, and it
goes in the blended pass so what is behind it shows through. The CPU
shader and advanced3d.wgsl get the same branch.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Turning Solo on or off hides or shows every other layer at every frame, so it
now recaches the whole comp. The kept span is widened by the comp's shutter
(phase and angle), since a frame's motion blur samples cameras, lights and
transforms up to three frames away. Comp::any_solo is shared with the
renderer so both read Solo the same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>