Output is bit-identical (all displays, CPU and GPU). At 2560x1440 with 3 ranks and 2000 IDs on 8 cores the CPU effect drops from ~87-135 ms to ~44-54 ms and the GPU path's aux reduction from 38 to 8 ms for Matte, Preview and Matte Only.
Entries whose layer buffer was freed were pruned only once the cache went over its budget
(1.5 GiB), so an animated layer, which makes a new buffer every frame, left a dead 4K texture
per frame allocated until the cache filled up.
Black & White offset each pixel's luminance by (hue weight - 0.5) times
its HSL saturation. HSL saturation is near 1 for any tinted near-black
pixel, so the code or two of colour noise in compressed footage's
shadows came out as bright and black blocks, and a 32-bpc overbright
such as (1.5, 0.5, 0.5) divided by zero and went black.
The gray is now the pixel's smallest channel plus its chroma (largest -
smallest channel) times the same hue-interpolated slider weight. Neutral
pixels keep their value, a pure hue becomes its slider's percentage
(Reds 40% turns pure red into 40% gray), noise moves the gray by no
more than its own size, and nothing divides. The CPU effect and the GPU
kernel change the same way.
The keyed Birth Rate comparison is shared (keyed_agree) and now runs on
CC Particle Systems II too. The same two histories bear 90 particles by
0.9 s at different steps: the GPU's particles match the CPU's, and each
state really came from the GPU.
The two histories now bear the same 180 particles by 0.9 s at different
steps (3/s from 0 to 0.5 s, and from 0.25 to 0.75 s). So a checkpoint
check by birth count alone fails the test, not only no check. A
test-only dispatch counter asserts each GPU state really came from the
GPU rather than the CPU fallback.
With a Birth Rate keyed into a burst, the GPU's particles match the CPU
simulation through the burst and after it. A second history with the
same system key, whose burst starts later (so the same ids are born at
other steps), isn't resumed from the first history's GPU checkpoints.
The simulation runs from layer time 0, but it used the Birth Rate of the
rendered frame for every step. A rate keyed from 3 down to 0 simulated
no births at all once it reached 0, so the burst's particles vanished
that frame although their Longevity had not run out. A ramped rate was
simulated as if its current value had always applied.
Each step now samples Birth Rate at its own layer time through
EffectHost::param_at (with the effect's own index). Without such a host
the current value applies throughout, as before. Particles already alive
keep moving and ageing whatever the rate. The carry and particle ids
continue across rate changes.
Birth Rate is left out of the simulation cache key, so an animated rate
no longer restarts the simulation every frame. In its place:
- The CPU state keeps a hash of the rates it was stepped with (hist). A
checkpoint is resumed only when that matches the current history, so
moving an earlier key is never served from a stale checkpoint.
- The GPU gets the birth schedule of the sampled rates (births_at). Its
checkpoints record which births they hold, and are resumed only when
the request's births before that step are the same.
A constant Birth Rate gives exactly the particles it did. CC Particle
Systems II and CC Mr. Mercury keep their constant-rate simulation.
The pinning regression only exercised All Edges (and Left Edge in the CPU
unit test), so the reported 'does not work with Top/Bottom Edge' had no
coverage to catch or clear it. This holds the pinned first/last row in a
CPU test at Direction 90, and adds CPU/GPU parity cases for Top (4),
Bottom (6), diagonal directions, and single-edge pinning on shape layers.
CPU pinning verified correct on main: the guards in dst_wave_pin/wave_pin
agree; the gap was that neither edge path was tested.
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>
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.
- GPU Pixel Motion Blur (#695) declined without a host; it now averages the shutter's
frames like the CPU effect (tests_fx_time::without_a_host).
- add_item_keeps_footage_longer_than_the_comp_at_full_length (#798) built footage with
the default kind (Still), which adds at comp length; it is a movie.
- docs/effects.md regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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.