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>
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.
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 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.
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