Content r from Center smears out to r / (1 - s), so the buffer is padded by
the farthest corner's reach (capped at 4096, skipped on adjustment layers),
on the CPU and GPU paths alike.
* cargo fmt
* GPU tests: llvmpipe on Vulkan gets the boundary-flip allowance; skip the Reshape boundary cases that crash lavapipe (#2238)
* GPU tests: the llvmpipe boundary-flip allowance is an opt-in of the two tests that need it
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.
Both devices EffectCraft creates now ask for `MemoryHints::MemoryUsage`:
- the window device (`apps/effectcraft/src/main.rs`, through a new `device_descriptor` helper);
- the headless compositor device (`crates/gpu/src/context.rs`).
The limits, labels, features and error paths are unchanged. The devices tests create stay as they were.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gradient antialias could leave a half-covered pixel where an opaque source reaches h = 1. Add an endpoint guard on CPU and GPU and cover an opaque edge in the regression test.
Use the measured Softness kernel (sigma = 0.42 * Softness), threshold the blurred height with a ~1 px antialias, and allow Cut Away 100 to empty the matte. Split the source-matte displacement from the height-field RGB refraction: the matte uses a z=1 bevel normal fitted to the T2 step edges, while RGB follows the measured z=height refraction. Align the documented AE defaults and add CPU regression tests for the cut threshold, Cut Away 100, and the inward matte warp. Update the GPU kernel and parameters to match, keeping CPU/GPU parity.
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>
WGSL cannot read Rust constants, so the Rec. 601 luma (Tint, Tritone, Luma Key /
Extract), Black & White's tint weights and Exposure's 2.2 power stay literals in the
kernels; they now name their Rust source (effects::LUMA_601, BW_TINT_LUMA,
EXPOSURE_LINEAR_GAMMA, newly exported) and the test shader_constants_match_the_cpu
(no GPU needed) fails if a literal drifts. gpu_tint_defaults_match_after_effects_greys
says "skipped: no GPU adapter" when it skips (EFFECTCRAFT_REQUIRE_GPU=1 still makes it
fail). docs/effects-parity.md: Brightness & Contrast is close to, not the same as,
After Effects (up to 6.2/255), and the summary row lists what is measured.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects' Curves effect interpolates its points with a natural cubic
spline (zero curvature at the ends), clamped to 0..1: AE 2026 renders of an
RGB curve (0,0) (125,199) (255,255) and a Red curve (0,0) (128,44)
(255,255) (cases 962 / 963, ROUND4) give exactly the natural spline's values
on every grey step (0, 30, 59, 115, 164, 202, 227, 242, 250, 255 and 0, 0, 1,
6, 19, 44, 83, 135, 194, 255). EffectCraft used a monotone (Fritsch-Carlson)
spline: 34 / 67 / 125 / 171 ... on the RGB curve, up to 10/255 off (ROUND4
measured up to 6 levels on real projects); now within 1/255, CPU and GPU (the
same Hermite tables carry the spline's knot slopes). Lumetri's curves and
Glow's Arbitrary Map keep the monotone spline (not measured).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a Fill Mask, After Effects turns the layer into the fill colour and
applies Opacity to its alpha; nothing of the original shows through. AE 2026
renders Fill (0, 0.5, 1) at Opacity 50 % on a Gradient Ramp over a red solid
as premultiplied (0, 0.251, 0.502) at alpha 0.502 at every pixel; EffectCraft
mixed the fill with the ramp (mean difference 95.5/255), now within 1/255,
CPU and GPU. Invert fills the transparent part the same way. With a Fill
Mask the mix with the original is unchanged (not measured). Lottie's fill
effect (colour, alpha x opacity) now plays the same in EffectCraft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects weighs each of the four points by its inverse squared
distance softened by Blend squared, 1 / (d^2 + Blend^2) in pixels: fitted to
AE 2026 renders of the default gradient at Blend 10, 100 and 300 and case
904 E16 at Blend 50 (power 2.00 at every Blend, softening 0 / 2367 / 9817 /
90127 = about Blend^2; mean error 0.27/255, max 0.9). EffectCraft used
1 / (d^2 / (100 Blend))^1.5, in which Blend cancels out: E16 was 34/255 off
on 99 % of the grid, now 0 grid points off, max 1/255, CPU and GPU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects 2026 keys Luma Key and Extract (Channel Luminance) on Rec. 601
weights of the working-space values: Key Out Darker / Extract Black Point at
65, 166 and 24 on pure red, green and blue keep red (601: 76, 709: 54) and
blue (29 / 18) and key out green (150 / 182) - Rec. 709, which EffectCraft
used, decides all three the other way. Case 904 E18 (Extract 30 / 220 /
softness 10 on grey and colour steps): 513 of 5184 grid points off (pure blue
kept at 231 in AE, keyed out in EC), now 0, max 0. CPU and GPU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects' Luma Key removes everything darker (or brighter) than Threshold
and ignores Tolerance for those two key types; Tolerance only sets the band of
Key Out Similar / Dissimilar. EffectCraft ramped the matte over Tolerance, so
case 719 (a 50 % grey, Key Out Darker, Threshold 140, Tolerance 20) kept about
80 % of the layer where AE keys it out completely.
Measured in AE 2026 on a vertical grey ramp (cases 812-814): Darker keys up to
level 139.9 and keeps 140.1 (EffectCraft now the same row); Similar keys levels
120.3-160.0; with Edge Thin 1 / Edge Feather 2.5 the matte rises over rows
591-596 (EffectCraft within 0.2 of AE's alpha there). CPU and GPU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In After Effects 2026 the non-legacy curve of Brightness -40 is the inverse
of Brightness +40 (within 2.6/255 on a full black-to-white ramp) and Contrast
-50 the inverse of Contrast +50. EffectCraft fitted the negative brightness
with its own power (2^(1.32 |b|) on v), 20/255 off AE at -40; the negatives
now use the reciprocal exponents of the positive curves: Brightness -40 max
6.2/255 (was 19.8), Contrast -50 4.1 (was 7.0); positives unchanged (40:
5.5, 80: 7.8, Contrast 50: 2.7, 90: 5.0; B20 C30: 6.6, which confirms
brightness before contrast). The #681 oracle samples still pass. CPU and GPU.
The positive curves' remaining 5-8/255 needs a better curve family (a
two-point spline fits each capture within 1.6/255), left open.
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>
With Bypass Linear Light Conversion off, Exposure decoded and re-encoded
with the piecewise sRGB curve. After Effects 2026 renders (sRGB working
space, 32 bpc) of Offset 0.0005 / 0.002 / 0.02 / 0.1 on black measure
8 / 15 / 43 / 90, which a pure 2.2 power predicts (8.06 / 15.13 / 43.08 /
89.54) and the sRGB curve does not (1.65 / 6.59 / 38.68 / 89.04).
The effect now uses its own 2.2 power curve on the CPU and in the GPU
kernel; the shared colour functions and other effects are unchanged. As
before, the conversion applies whatever the working space, also a
linearised one. Negative values go through both curves mirrored (input
was clamped at 0 before), so defaults are an identity for 32-bpc values
and a negative Offset darkens past black without NaN.
New test against the AE values; existing Exposure tests still hold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects tints Black & White's gray like a Color blend: the tint colour's
hue and saturation at the gray's luminosity (weights 0.3 / 0.59 / 0.11, channels
outside 0..1 pulled back toward the gray). EffectCraft used the gray as HSL
lightness. Measured in AE 2026, Tint Color [1, 0.5, 0]: a red solid at Reds 10
(case 717) AE (43, 21, 0), EffectCraft was (51, 26, 0), now (43, 21, 0); a
yellow solid at the default sliders (gray 0.6, case 825) AE (255, 129, 2), now
(255, 129, 3); white stays white. The six sliders already matched AE on eight
colours (cases 816, 817). CPU and GPU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects' Tritone weighs the working-space values with Rec. 601 (0.299,
0.587, 0.114), as its Tint does; EffectCraft used Rec. 709. Case 718 (red solid,
Highlights [1,1,0.8], Midtones [0.6,0.2,0.2], Shadows [0,0,0.3], Blend 25):
AE (132, 23, 46), EffectCraft was (113, 16, 49), now (132, 23, 46). On a grey
ramp (case 815) the shadows / midtones / highlights mapping already matched and
still does within 1/255. CPU and GPU (fx_color op 4).
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>
The menu had an extra "Half On" at position 10, where After Effects has
"Full Off", so a project or script written for After Effects set a channel
to 50 % grey instead of turning it off, with no error: the RGB-split recipe
(three copies, each keeping one channel and setting the other two to Full
Off, blended with Add) rendered pastel instead of pure red, green and blue.
After Effects' ten options now come first, in its order, and "Half On" sits
last as an EffectCraft extra, so values 1 to 10 mean what they mean in After
Effects and nothing is lost. Instances saved with the old order keep the
option they chose: upgrade_instance already remaps popup values by label.
The GPU path keeps its own copy of the table, so it is reordered to match;
a GPU-vs-CPU parity case now covers the constant sources at the end of the
menu, where the two tables could drift apart unnoticed. Both paths clamped
the index to a hardcoded 10 and now use SHIFT_ORDER.len() - 1, so the clamp
cannot go stale if the list changes again.
Co-Authored-By: Claude Opus 5 <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>
crypto_aux_image_with takes CryptoAux::{ColorsAndMatte, MatteOnly}, and
CryptoAux::for_display holds the rule for which displays read the ID
colours. A new GPU test checks every display x Matte Only against the CPU,
so the GPU fails the test if the shader starts reading RGB in a mode that
skips it (checked by mutation: forcing MatteOnly fails display 0).
GpuContext::request() now applies InstanceDescriptor::with_env() (WGPU_BACKEND,
WGPU_DX12_COMPILER, WGPU_VALIDATION, WGPU_DEBUG, ...), the same variables the
desktop app honours via egui-wgpu, and requests PowerPreference::from_env(),
defaulting to HighPerformance (an unknown WGPU_POWER_PREF logs a warning).
WGPU_ADAPTER_NAME (CLI/headless only; the desktop ignores it) picks the first
adapter whose name contains the trimmed value, case-insensitively. Empty or
blank counts as unset. Only the name is matched (backend via WGPU_BACKEND). A non-matching name is an
error that says so and lists each distinct adapter once as 'name (backend)', instead of wgpu::util::initialize_adapter_from_env's panic;
no adapters at all gets its own message.