After Effects feathers both edges of the wiped wedge (the start ray too) with
a Gaussian whose width is constant in pixels: Completion 40, Start Angle 30,
Counterclockwise, Feather 20 (AE 2026, case 904 E12) fits sigma = 0.4 x
Feather = 8.0 px at every radius (mean alpha error 0.0002 over the frame).
EffectCraft ramped only the moving edge, linearly over Feather degrees, so
the edge was 14 px wide near the centre and 140 px wide at 400 px out, and
the start edge was hard: 378 of 5184 grid points off (max 111/255, 6.6 % of
pixels > 8), now 1 (max 3), 0.009 % of pixels > 8 (near the centre). CPU and
GPU (the kernel now measures distances to the two rays). 0 % and 100 % are
unchanged, so the three Transitions presets keep their end frames; their
Feather (6 / 10) now means pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects measures the blinds' phase from the layer's centre and softens
both edges of the transparent band with a Gaussian of sigma = Feather / 2; the
band runs from the origin's phase to Completion x Width. EffectCraft measured
from the buffer corner and ramped only one edge linearly over Feather (a hard
edge on the other side). Fitted to AE 2026 renders of Completion 40,
Direction 30, Width 50, Feather 5 on a 960 x 540 and a 480 x 270 layer (the
origin is the centre + 0.6 px): case 715 was 499 of 5184 grid points off
(max 91/255, 7.1 % of pixels > 8), now 28 (0.056 %), all within ~6 px of the
layer edge where AE differs (up to 48/255, not modelled); away from it within
6/255 (test: one AE row of the 480 x 270 render). CPU and GPU (new erf-based
kernel, same term count). Completion 0 / 100 are unchanged, so the
Transitions presets keep their start and end frames.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects' Radial Blur keeps a layer's edges: Zoom 20 on a flat grey 960x540
solid over red (case 713) stays flat and opaque to its edge, and a zoomed
full-frame ramp (case 829) keeps the frame opaque (2/255 at x = 2, 253/255 at
x = 1917 on row 540). EffectCraft sampled transparency outside the layer, so its
edges faded and the red below showed through (713: 3.9 % of the frame off, max
64/255). Now 713 is identical to AE (max 0/255), CPU and GPU. CC Radial Blur
(already matching AE) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Glow Colors = Original Colors, After Effects thresholds each channel of
the straight colour separately and the glow is 8 x (value - threshold +
0.061) per channel (times Intensity), nothing below: fitted to AE 2026
renders of grey 0.65 / 0.8 / 0.9 / 1, orange (1, 0.5, 0) and (0.2, 0.6, 1)
solids at Threshold 40 / 60 / 80 % and Radius 10 / 30 (36 channel values,
exact to the 8-bit capture; an orange solid glows red only). EffectCraft
scaled the colour by a luminance threshold (k = (L - thr) / (1 - thr)), about
3.7 x too dim on white. Cases 806 / 810 / 811 (32 bpc): outside the layer
now within 3/255 of AE (806 white at Intensity 1: 119 = AE, was 32); grid
0 points off for 806-810. The glow's alpha (the blurred layer alpha) and the
Arbitrary Map / A & B colours and alpha-based glows are unchanged.
Stacked on fix-glow-halo (same lines). Not changed: Composite Original's
semantics are swapped against AE's names (EffectCraft's "On Top" adds the
glow over the original, which AE calls "Behind"; AE's "On Top" keeps the
original over the glow), so imported AE Glows at the default Behind show
no glow inside the layer (811 interior 204 vs AE 255).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Glow Based On Color Channels, After Effects gives the glow the layer's whole
alpha (blurred), not the thresholded part, and Intensity scales only its colour:
a layer below the threshold darkens what lies under its edge. Fitted to AE 2026
renders of a grey solid over red (cases 524, 807-809): the halo is the same at
Intensity 1 and 3 and with Composite Original Behind or On Top, and its edge is
an erfc step of sigma = 0.384 x Glow Radius at radius 10 and 30 (was radius / 2).
524 (Glow defaults on a 50 % grey solid): EffectCraft had no halo (max 116/255
off at the edge); now red 248/234/208/165/140 at 7.5/5.5/3.5/1.5/0.5 px outside
vs AE 249/235/206/164/140, max 2/255 over the frame, CPU and GPU. The blur
width applies to every glow; alpha-based glows keep their coverage rule
(thresholded alpha, scaled by Intensity). Not matched yet: the brightness of glows above the
threshold (AE about 3.7 x brighter for a white solid, cases 806, 810, 811).
Co-Authored-By: Claude Opus 5.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>
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 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>
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.
Measured against After Effects at 32 bpc:
- Classic Color Dodge, Classic Color Burn and Classic Difference at partial opacity: AE
fades the source toward the mode's neutral colour (0 for Dodge and Difference, 1 for
Burn) by the opacity and applies the classic formula at full strength (b / (1 − k·o),
|b − k·o|, 1 − (1 − b) / (1 − k·(1 − o))). We mixed the full-strength result by opacity:
up to 45/255 off on ordinary 0–1 footage.
- Overlay, Exclusion, Luminosity, Color Burn and Classic Color Burn keep their plain formula
for values above 1 (HDR passes). They were missing from `supports_hdr`, so their inputs
were clamped to 0–1 (Exclusion was up to 208/255 off); Classic Color Burn also clamped
its result at 0, where AE keeps negative values.
The CPU blend (color::blend_pixel) and the GPU kernel (common.wgsl) change together.
Tests: the reporter's formulas for each case (blend.rs); the CPU/GPU parity test of every
blend mode at every bit depth (over-range values, 70 % opacity) passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
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>
Cause: Auto dropped to Half below 75 % magnification, so text between
50 % and 75 % was rendered with fewer pixels than the screen shows and
looked soft. Below 100 % the frame texture was minified with a plain
bilinear sampler and no mip levels (Full at 46 % skipped most frame
pixels), so edges stair-stepped.
Fix:
- Auto renders the pixels the magnification needs, as in After Effects:
the coarsest whole factor that still gives every screen pixel a
rendered one (Full above 50 %, Half down to 33.3 %, Third down to
25 %, Quarter below).
- GPU frames: the display texture carries a full mip chain, each level
the 2 x 2 mean of the one above (display.wgsl `mip`, same bind group
layout as `display`), sampled trilinearly with Viewer Zoom Quality
More Accurate (Faster keeps the nearest pixel of the full-size frame).
- CPU frames (egui-wgpu textures have no mips): uploaded averaged down
by the whole factor that leaves about one texel per screen pixel, the
same box average as the texture-limit fit (#201), re-uploaded when the
magnification calls for another factor; drawn with texture coordinates
that keep each texel over its pixels when the size isn't a multiple.
Show Channel / exposure textures follow.
Tests: gpu tests::display_frames_are_mipmapped (level count and level 1
= 2 x 2 means of level 0), viewer_tools
auto_resolution_follows_the_magnification (60 % was Half) and
minified_frames_keep_their_texels_in_place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use an explicit FMA for the final quotient correction at Posterize
boundaries. Align the kinetic motion-blur QA comparison with export
settings and credit sanyamsingh001.
Co-authored-by: Codex <codex@openai.com>
Soft Linear was value noise with a quintic fade between two lattice
values per axis. That fade has zero slope across every lattice line and
at every lattice point, so the noise's contours met the lattice at right
angles and flattened around its points. The turbulent fold (1 - |s|) and
high contrast turned that into a visible square grid at each layer's cell
size (Turbulent Basic, Contrast 140, Brightness -35: gradients 1.3x as
strong along the axes as along the diagonals; Spline 1.1x).
Soft Linear is now linear interpolation softened by a one-cell box
filter, i.e. the uniform quadratic B-spline over the 3 x 3 lattice values
around the nearest one (evolution still fades between two slices): smooth
everywhere, with half the neighbours' difference as its slope on lattice
lines, and cheaper than Spline (9 taps instead of 16). Averaging
lowers the noise's spread, so a fixed gain about mid-grey restores the
quintic fade's variance and existing projects keep their contrast and
brightness (issue settings: mean 0.356 -> 0.362, std 0.147 -> 0.152,
axis bias 1.28 -> 1.05).
Only Fractal / Turbulent Noise's Soft Linear changes: generate's
value_noise and its other callers are untouched. noise3 now shares
generate's lattice hash and fade instead of copying them. The GPU
kernels (pointwise Fractal Noise and fxn_fractal) call the same
soft_linear_noise. No golden hashes pin these cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FreeBSD's CI VM (Mesa llvmpipe over GL) failed 22 effectcraft-gpu tests:
- Advanced 3D's camera-depth target was R32Float, which GLES can't render to without
EXT_color_buffer_float: create_render_pipeline failed validation (release builds only log
that and draw nothing). The target is now R32Uint holding the f32 bits (renderable on every
backend, lossless), and GpuContext checks the adapter's format features (render attachment,
blendable colour) and independent blending up front; when they're missing the render
pipelines are never built and Advanced 3D renders on the CPU.
- Signed `%` in the shaders: naga emits GLSL `%`, undefined for negative operands, which broke
the VR effects' horizontal wrap and Cell Pattern's tiling. A shared `imod` never takes `%` of
a negative number.
- asin/acos: Mesa's are coarse approximations (~1e-4 rad), visible in every lens/sphere/cylinder
mapping. `asin_p`/`acos_p` (Cephes' asinf polynomial) are f32-accurate on every backend.
- "Could not lock adapter context": the GL backend has one context lock per adapter with a
timeout, so tests driving devices concurrently panic. Every gpu test helper that hands out a
device holds one process-wide lock for the calling test; ui_web_job_gpu's two device tests
share a lock too.
- On GL, up to 1 % of pixels may flip across a decision boundary (division through a
reciprocal, unfused fma); the GPU-vs-CPU comparisons allow that there only.
Also adds the FreeBSD workflow (from the FreeBSD CI PR) so this branch is checked there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The geometry stays on the CPU as per-frame plans shared with the CPU effects (bolts, mask
strokes, scribbles, contour segments, audio marks, glyph strokes, waves, flood-fill region,
eyedropper sample); segments and convex polygons are tile-binned and rasterised on the GPU
with the CPU's coverage functions. CPU output unchanged (gen_golden). CC Threads' exact ties
at round parameter values are a documented exception; Fractal stays on the CPU (needs f64).
Regenerated docs/effects.md, docs/parity.md EFF-5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GPU render passes for CC Particle World / Systems II, Particle Playground,
CC Ball Action, CC Pixel Polly, CC Scatterize and Curl Noise (fx_particles,
chunked multi-pass raster); Shatter wireframe views, Foam extras, Timewarp
with a Matte Layer, strong Warp Fisheye/Twist (CPU f64 inverse map) and
custom .ocio configs no longer fall back to the CPU. CPU refactors pinned by
particle_golden / timewarp_golden / sim_golden hashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inner/Outer Key's trimap and Cleanup strokes, Color Link's sample and CC Glass/Plastic's
light are shared with the CPU effects (CPU output unchanged: gen_golden). CC Plastic now
reads its Bump Layer (it read the nonexistent bumpMap id).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CC Rainfall, Snowfall, Star Burst, Bubbles, Drizzle, Hair, Mr. Mercury,
Caustics, Wave World, Foam, Shatter, Card Dance and Card Wipe render on the GPU
(fx_sim.rs / fx_sim.wgsl). The per-frame simulation / layout stays on the CPU
and is shared with the CPU effect through plans (effects::sprite_plan,
piece_plan, bubble_list, mercury_blobs, drizzle_drops, wave_world_plan,
caustics_setup); a tiled item rasteriser composites sprites, bubbles, blob
fields and textured pieces in the CPU's order and shading. Layer-dependent
sprite colours (refracting rain, star-burst and hair roots) are sampled per
pixel; piece plans read the frame back once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GPU in the browser: each frame worker opens its own WebGPU device (Gpu::request_deferred) and
renders the GPU compositor and GPU effects with deferred readbacks (effectcraft-gpu deferred):
readbacks keyed by content start with map_async and miss, the pass returns placeholders, the
layer cache's gate drops inserts, and FrameServer::resume renders again once the device
delivered. Backend Auto times whole frames per comp in the workers. Frames with effects go to
GPU workers instead of the page. CPU fallback without WebGPU in workers (?nogpuworkers).
- Fix: a WGSL constant (3.40282347e38) that Chrome rejects invalidated every GPU kernel in Chrome
(the page's compositor too); a test checks every float literal fits f32.
- Disk cache in the Origin Private File System: workers write sealed frame entries with sync
access handles (temp file + move), the page keeps the DiskIndex (LRU under the settings' limit,
at most half the quota) and reads hits instead of rendering; survives reloads; blue cache bar.
- Storage manager: Settings ▸ Disk ▸ Browser Storage (usage/quota, persistent storage, Clear
buttons, automation ids) and storage.info / storage.persist / storage.clear (StorageHost).
- Docs: web.md (GPU workers, deferred passes, disk cache, storage manager, threading design:
one engine instance per worker), architecture, preferences, parity (WEB-1), roadmap.
- Tests: deferred passes vs CPU on a real device, placeholder gating, frame server passes and
Auto, GPU worker routing + disk cache across a reload, storage commands and widgets, store
usage by kind; smoke test: GPU effect frames in workers with pixel parity, disk hits after a
reload, storage manager.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
render: three_d::adv::SplitRun (Renderer::split_adv_run) hands draw_run's split path to an
accelerator as data (main scene, special layers far to near, 3D mattes drawn solo);
Renderer::sky_draw / three_d::SkyDraw for Environment Light Background layers. gpu: the walk
draws skies (adv_sky kernel), resolves each special layer, hides it behind the nearer main
scene (occlude kernel) and composites it through the 2D path without readbacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Timewarp without a Matte Layer is now planned once (time_fx::timewarp_plan: frames, shutter
instants, motion fields) and built by the CPU or the GPU from the plan; with a Matte Layer it
renders on the CPU (catalog::gpu_supported).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VR Blur, Chromatic Aberrations, Color Gradients, Converter (all projections and
cube layouts), De-Noise, Digital Glitch, Fractal Noise, Glow, Plane to Sphere,
Rotate Sphere, Sharpen and Sphere to Plane run on the GPU (fx_vr.rs /
fx_vr.wgsl) with the CPU's equirectangular maths: seam-wrapping, pole-continuing
sampling and latitude-widened box blurs, per eye for stereo layouts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CC Environment now fades nearly transparent environment texels to black continuously
(colour / max(alpha, 1e-3)) instead of a hard 1e-6 un-premultiply cut-off, which turned the
filtered map's float noise into colour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apply Color LUT, OCIO CDL / Color Space / Display / File / Look Transform and
Color Profile Converter compile to colour programs (effects::color_program,
built next to the CPU effects) that fx_lut.wgsl interprets per pixel; LUTs
(1D, 3D, cineSpace shapers, inverses) are read from the storage buffer with the
CPU's nearest / trilinear / tetrahedral interpolation. Lumetri's Input LUT and
Look no longer force the CPU. Custom .ocio configs stay on the CPU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3D Channel Extract, Cryptomatte, Depth Matte, Depth of Field, EXtractoR, Fog 3D,
ID Matte and IDentifier run on the GPU (fx_depth.rs / fx_depth.wgsl): aux planes
upload as a texture and are resampled onto the buffer grid with the CPU's index
arithmetic; tests compare against the CPU at 1x / 2x aux resolution, padded
buffers, half resolution and 8 / 32 bpc composites.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- gpu: pooled working textures (released once every encoder that could read
them is submitted), shared zero images, reused readback staging buffers,
direct readback into Image; NaN-poisoned scratch images under test and
NaN-aware diffs; validation errors panic under test
- render: AutoPick (per comp / scale / path EWMA of CPU vs GPU frame times,
warm-up and periodic re-probe) behind Backend::Auto; viewer uses it too;
cached adjustment footprints (uploaded once)
- gpu fx_warp: Warp, Bezier Warp, CC Bend It, CC Page Turn, Smear, Reshape,
Color Emboss, Cartoon; sliding-histogram median for large radii; CC Power
Pin perspective < 100
- cli: bench --gpu reports Auto and per-frame upload / readback MB
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>