Commit Graph
140 Commits
Author SHA1 Message Date
Matej Kozlovský 5d1b6ce3f2 gpu: match CPU rounding in box blur rows
Horizontal GPU box passes restarted the running sum every 256 pixels, while
the CPU keeps one sum per row; use the full row width as the block so f32
rounding matches. The regression test requires exact equality on llvmpipe
(CI); other adapters only get an absolute 2e-6 sanity bound.
2026-11-10 20:50:18 +01:00
23c1f33c88 Echo: Blend, Composite In Back and In Front match After Effects (#3132)
* Echo: swap Composite In Back and Composite In Front (#3124)

After Effects 2026 renders of the importer sample (opaque test card with a
moving square, one echo at -1/30 s) show Composite In Front keeping the
layer on top (the output equals the current frame) and Composite In Back
putting the echo over the layer. EffectCraft had the two the other way
round, which made ~0.5 % of the pixels differ at the moving edge. The CPU
and GPU (fxn_echo) arms are swapped together.

A regression test rebuilds row 130 of the test card and checks Add,
Maximum, Minimum, Screen and both Composite operators against the measured
After Effects row. Blend is left unchanged: the sample (one echo, intensity
1, decay 1) cannot tell its normalisation apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Echo: Blend sums the weighted frames like Add (#3124)

New After Effects 2026 renders (our own 1110-extra sample, 16-bit PNGs,
opaque test card with a moving square) show that Blend does not normalise:
- 3 echoes at intensity 1 and decay 1 give four times the layer.
- 2 echoes at intensity 0.5 and decay 0.5 give alpha 0.875
  (0.5 + 0.25 + 0.125).

EffectCraft divided by the number of frames. The division is gone on the
CPU and in fxn_echo_fin on the GPU.

The same sample pins Composite In Back with three echoes: each older echo
goes over what is there with its weight as opacity, so the oldest is on
top, as the branch already does. Composite In Front with three echoes
equals the current frame.

A regression test checks four measured 16-bit rows within 4 codes (24 on
square-edge pixels): Blend 3/1/1, Blend 2/0.5/0.5 with alpha, and both
composite modes at 3 echoes, -0.08 s, decay 0.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Echo: doc comments on the CPU and GPU entry points

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-07 00:24:10 +01:00
FredAppsandClaude Opus 5.5 5047d8b5e7 Posterize on the GPU: divide by the levels exactly, as the CPU does (#3179)
Native division isn't correctly rounded on real GPUs, so
posterize_matches_the_cpu_on_every_8bpc_pair failed on NVIDIA and Apple
GPUs while passing on the software renderer hosted CI uses.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 20:04:21 +01:00
Valentin KleinandClaude Opus 5.5 7051fc1f58 Color Stabilizer and Linear Wipe: match After Effects (sample positions, feather) (#3135)
* Color Stabilizer: sample the reference where the points were at the reference time (#3125)

After Effects 2026 samples the reference frame at the positions the
Black / Mid / White Points had at the reference time. With keyframed
points it therefore corrects even a static picture: our importer sample
(colstab-43-keys, Levels, Black Point (64, 90) -> (151, 99) over 1 s)
renders at 0.5 s as the Levels map out = a + b*v of the ramp with
b 1.2935..1.2965 and a -0.2365..-0.2350, which is the Black Point's
current sample (107.5, 94.5) sent to the reference sample at (64, 90).

EffectCraft sampled both frames at the current points, so the card
stayed unchanged (slope 1). The samples now read the points at the
reference time through the host (params_at), falling back to the
current points without one. CPU and GPU share the sampler
(color_stabilizer_maps).

Sample Size's unit is not pinned by the samples (every size 1..40 leaves
the card unchanged in After Effects; that only bounds the sampled area)
and is left as it is.

Part of #3125

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Linear Wipe: After Effects' edge position and feather profile (#3125)

Measured on our importer samples (linwipe-*, 2026-11-05, After Effects
2026, 320 x 180):

- The edge sits at lo + completion * (hi - lo) whatever the feather (the
  set-all render's 50 % line is at -5.25, predicted -5.266); 0 % keeps
  the layer and 100 % clears it even with Feather 32000. EffectCraft
  extended the travel by the feather, 13 px off at Feather 16.
- The feathered edge is the hard matte blurred with the layer's edges
  repeated: three box passes of radius 6 per axis fit Feather 16
  (sigma 6.35..6.6, taken as Feather / 2.5), including the extra
  opacity where the edge meets the layer's left side. EffectCraft drew
  a linear ramp Feather px wide.
- Without feather the edge pixel is anti-aliased (143/255; coverage
  gives 0.6). EffectCraft drew a hard step.

set-all alpha: before max 195, 4646 px (8.1 %) off by more than 2/255;
after max 15, 529 px (0.9 %), all along the layer's left side (at most
2 elsewhere). Feather 0 renders: max 112 -> 10 on the edge column.

CPU: coverage matte, raster::gaussian_blur (3 box passes, edges
repeated); GPU: the same matte from gen_linear_wipe (new modes), its
gaussian_blur, then a multiply, so both match. Sigma is capped at twice
the layer's longer side; NaN values leave the layer as it is.

Projects saved before keep the old ramp: a hidden legacyFeather
parameter that migrate::legacy_default sets on upgrade (as for Ripple
and Block Dissolve).

Part of #3125

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Linear Wipe on the GPU: 100 % clears the layer before the angle is read

A NaN angle left the layer unchanged on the GPU while the CPU cleared it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:36:48 +01:00
vangel76andClaude Opus 5.5 4934de892d Merge upstream/main into cryptomatte-matte-only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:53:40 +01:00
vangel76andClaude Opus 5.5 b9182972b2 HDR Highlight Compression: match After Effects' measured transfer curve (#3123)
The effect used a soft knee on the brightest channel and left everything
below the knee unchanged. After Effects compresses the whole range. Fitting
our own 16-bit renders of an HDR ramp and coloured patches (Amount 0, 84,
90, 100) gives:

  Y    = max(0.30 R + 0.59 G + 0.11 B, 0)
  comp = min(c / (1 + Y), 1)                    per channel
  out  = ((1 - t) c^0.4 + t comp^0.4)^(1/0.4)   t = Amount / 100

A grey value x maps to x / (1 + x) at Amount 100. The worst error against AE
drops from 985 to about 2-4 of 65535 at Amounts 100, 90 and 84. Amount 0
stays identical.

CPU and GPU change together. Tests: a reference table of measured values,
GPU parity at Amount 90, and over-range input that exercises the clip at 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:02:26 +01:00
vangel76andClaude Opus 5.5 50cbbfe310 Cryptomatte: Matte Only puts the matte in RGB over alpha 1 (#3127)
With Matte Only on, Cryptomatte wrote the matte into all four channels:
a premultiplied white only as opaque as the matte. After Effects'
Cryptomatte plug-in shows Matte Only as an opaque grey picture, the
matte in RGB with alpha 1 (its Matte Only render of 2026-11-05 is opaque
black where nothing is selected). The CPU effect and the GPU shader now
both give (m, m, m, 1).

Projects saved before keep their render: a hidden Matte Only In Alpha
switch (off for new instances, presets and AEP imports) is turned on by
migrate::legacy_default when an older instance is upgraded on open, and
then keeps the matte in every channel on CPU and GPU.

Tests: the CPU path for a partly covered pixel, an unselected one and
outside the aux area, in both forms; the upgrade of a saved instance;
the GPU parity test over every display with Matte Only off, on and on
with the legacy switch; the host pick tests read whole pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:38:36 +01:00
JosephandFredApps 6572514eb1 GPU: size the memory cap from the card's video memory on Linux (fixes #1487) (#1706)
On Linux, the discrete-GPU memory cap now comes from the card's video memory: min(3 GiB, ¾ of amdgpu's `mem_info_vram_total`), so a 2 GB RX 550 gets 1.5 GiB. The card is matched by PCI address, falling back to the smallest card with the same ids.

Also folds in CodeRabbit follow-ups from #1856 (PatchMatch flags buffer limit preflight), #1490 (arbitrary-map index wrap), #1836 (round_away for |x| >= 2^23) and #3025 (saturating FrameCache byte accounting).

Fixes #1487

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-04 01:55:02 +01:00
ff68626597 #1605: Posterize and CC Grid Wipe match the CPU on AMD and Intel (#1836)
Posterize: round_away decided half-way by subtracting, and drivers may fuse the
product that feeds it into one fma (AMD does), skipping the product's rounding.
A straight value of 5/6 at level 4 gives 5/6 * 3 = 2.5 exactly in f32 on the
CPU but 2.49999994 fused, so the GPU rounded a level down. round_away now
compares x with t +/- 0.5 (exact for integer t), which no driver can fuse, and
the level divide uses an fma-corrected division (div_rn) like the CPU's.

CC Grid Wipe: the cell lookup floor(q / cs) used WGSL's division, which may be
off by 2.5 ULP; at q = 7, cs = 3.5 AMD and Intel return just under 2 and pick
the neighbouring cell. It uses div_rn too.

New test: posterize matches the CPU exactly on every 8 bpc premultiplied colour
and alpha at three levels (59715 of 65280 pairs differed before at level 4).

RX 6900 XT, Vulkan: effectcraft-gpu 222 passed; the one failure left is CC Hex
Tile, fixed by #1776. DX12 + FXC (on #1571): both tests pass.

Co-authored-by: 00xJ0 <56056764+00xJ0@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 23:56:37 +01:00
8d20cf0c3a GPU.1: Preserve CPU parity in numerical effect kernels (#1490)
Co-authored-by: @skylartaylor <26588250+skylartaylor@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 23:19:29 +01:00
gmrmad1andFredApps 389894e69b gpu: run Content-Aware Fill's PatchMatch and flow on the GPU (#1856)
* gpu: run Content-Aware Fill's PatchMatch on the device

Content-Aware Fill's synthesis is the stage that makes the tool slow: at
1920×1080 with a 300×200 hole, PatchMatch costs ≈ 1.1 s per frame on the CPU,
and it is what Object, Surface and every refining frame spend their time in.

Add a `FillBackend` trait to `effectcraft-raster` whose stages (the membrane
solve, the flow fields, PatchMatch) default to the CPU reference, and
`Accelerator::inpaint()` on the compositor, which is how the engine asks for a
device backend. `effectcraft_gpu::inpaint` implements PatchMatch: the
multi-scale pyramid, the EM rounds, the propagation (a two-pass checkerboard
instead of the CPU's scan order), the random search, the SSD, the
histogram-based σ² and the weighted vote, as compute kernels in
`shaders/inpaint.wgsl`.

The fill's frames are read back to the CPU before it runs (analyses composite
on the CPU by design), so the backend is about the fill's own work; the
orchestration, the flow-guided propagation and the per-frame flow control stay
on the CPU. Only the hole's bounds cross back to the CPU, which is what makes
the offload pay.

Measured on an Intel Arc (MTL) iGPU, 1920×1080, 300×200 hole:

  CPU 1087 ms   GPU 397 ms   (2.7×, 41.0 dB PSNR against the CPU's synthesis)

The membrane (harmonic) solve deliberately stays on the CPU: the same
measurement has it at ≈ 90 ms on the device against ≈ 55 ms on the CPU,
because a fill's full-frame data movement (upload, the known pyramid, a copy)
does not shrink with the hole, while the relaxation does. The device takes only
the stage where its parallel search pays; `membrane_tex` still runs on the
device inside PatchMatch, where it is the coarsest level's start and never
crosses back.

Closes #1839.

* gpu: fix the fill's vote histogram and run its flow on the device

Review follow-ups on the Content-Aware Fill GPU port, plus the fill's flow.

Fixes:

* imp_hist kept about one sample in 256: each of the 256 threads scanned a
  strided subset of the pixels and retained only those whose bin selected it,
  so the vote's sigma² came from a sample correlated with the pixel index (and
  a small hole usually fell back to 1.0, making the weights near-uniform). One
  workgroup now accumulates the whole population through shared atomic bins,
  with a test that pins the sample count.
* imp_pm_prop and imp_pm_rand were dispatched over grow(near, 1) but with
  near's origin, so the ring's left column and top row were never written
  (NaN under test, stale pooled data in production) while a strip past it was.
  `over` now derives the shader's origin from the box it sizes the dispatch
  from, so a dispatch's region and its origin can no longer disagree.
* Gpu::inpaint() requires its kernels to have built. Otherwise every stage
  paid an upload and a failed dispatch to fall back to the CPU, while the fill
  still reported the GPU path.
* imp_rnd's XOR chain parenthesises its multiplicative terms: WGSL's grammar
  makes each XOR operand a unary expression, so `a * b ^ c * d` is rejected
  by spec-compliant parsers (naga accepts it).
* FillBackend documents that its stages may be called concurrently.
* The coarsest membrane's sweep count (64, against the CPU's 200) is now
  justified: matching 200 measured ~6% slower over the fill with no change in
  the result.

And the flow: Gpu::flow runs block_flow's coarse-to-fine search, sub-pixel fit
and vector median on the device. The luma is the CPU's (flow::luma_pair), so
the field matches block_flow's to a float rounding and the propagation sees
the same motion; the hole completion stays on the CPU with the membrane. A
four-frame 1920x1080 fill goes from 1.27x to 1.42x against the CPU, and the
device path is covered for holes from 16x16 to 300x200.

* gpu: keep the fill vote's histogram range on HDR input

imp_hist binned every distance at a fixed 256 bins per unit and clamped to
1023, so the vote's sigma2 topped out at 3.998. A 32-bpc frame's channels can
exceed 1, and masked_frame forwards them unclamped: a patch distance of 1600
then reported sigma2 ~ 4, exp(-1600/(2*sigma2)) rounded to zero in f32, and
imp_pm_vote kept the prior image instead of synthesising.

The histogram now reports the largest finite distance it saw (atomicMax on the
float bits, carried in bin 0's unused .g channel). SDR input keeps the fixed
0..4 range with identical bins; HDR input is re-binned over the observed range
and percentile() maps the bin centre back through it. Nothing clamps the input.

Also group over/sweeps/pm_level's arguments into small structs: the workspace
denies too_many_arguments and the lint policy (#2496) does not allow new
#[allow]s. No behaviour change on those paths.

* engine: don't report Edge Blend fills as device fills

Edge Blend calls only the membrane, which the GPU backend leaves to the CPU
reference, so run_plan's "Filling (GPU)" status was wrong for it whenever a
backend was present. Object and Surface still run the flow and PatchMatch on
the device and keep the label.

CodeRabbit's #1856 finding.

* gpu: run the fill's device stages inside error scopes

An allocation or validation failure inside patch_image/flow_image left an
invalid texture whose later use raised an uncaptured error, retiring the
shared compositor instead of falling back to the CPU for that frame. Wrap both
stages in Gpu::within_memory, whose scopes are native thread-local, so the
fill's rayon workers do not share them.

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 22:24:42 +01:00
rahul05ranjan 540ee9fbc5 Motion Tile: mirror folds sample the edge texel, not the transparent outside
A tile boundary that lands exactly on an output pixel centre puts the tile
fraction at exactly 0 or 1, and the sample position at the source's geometric
edge — where sample_bilinear mixes in half a texel of the transparent outside.
The fold column comes out half-faded: a visible seam line along the mirrored
boundary, which sweeps the tile as Tile Center animates across pixel centres
(#2820). Both the CPU path and the GPU kernel switch to the clamped sampler,
which repeats the edge texel there; interior samples are untouched, and the
default single-tile identity mapping already sampled texel centres.

The regression test puts the folds exactly on pixel centres (tileWidth 50%,
centre on the layer, mirror on) and asserts the fold columns stay opaque and
equal the edge texels they mirror — it fails on the unfixed code at alpha 0.5.

Fixes #2820

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
2026-11-02 12:30:58 +00:00
bflatasticandClaude Opus 5.5 43c7cbb9aa Merge main into #2620; clean merge, no conflicts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 07:47:41 +07:00
bflatastic 5c89837f91 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-30 07:10:36 +07:00
bflatastic 335abf42c4 Merge main into #2474; tests/it/main.rs keeps both ui_env_light_gpu and main's ui_expression_values 2026-10-30 05:53:20 +07:00
becauseno7 0ca3f72ac2 CC Bubbles: bubbles refract the layer's edge pixels on padded buffers too (#2024)
Refraction samples are clamped to the layer rectangle instead of the buffer, so a sample past the
layer's edge in a buffer with transparent padding (a half-resolution preview's, a text or shape
layer's) repeats the layer's edge pixels rather than reaching the pale colour that stands in for
transparent ones. Transparent pixels inside the layer still show it. Same bounds on the GPU path.

Re-pins the four half-resolution CC Bubbles golden hashes (aarch64 macOS); the full-resolution
ones are unchanged. Adds a CPU test and a GPU-vs-CPU parity test on padded buffers.
2026-10-30 04:34:30 +06:00
BFlat 2faaa89c63 Merge pull request #2535 from klosixgaming-arch/fix/943-turbulent-displace-evolution
effects: Turbulent Displace Evolution moves the pattern at a constant rate (#943)
2026-10-30 01:15:12 +07:00
BFlat fbb7dc325a Merge pull request #2466 from dotCooCoo/integration/bugfix-effectcraft-bugfix
Fixes: macOS quit crash, soft shadow jitter, Interpret Footage params, preset upgrade, composition proxies, font menu names
2026-10-30 00:13:49 +07:00
BFlat b62093ae7f Merge pull request #2463 from HEOJUNFO/fix/classic3d-shadow-diffusion
Classic 3D Shadow Diffusion blurs instead of stamping copies (#2444)
2026-10-30 00:13:09 +07:00
BFlat 966fdd3c0a Merge pull request #1776 from sanyamsingh001/fix/hextile-centerline-symmetry
Fix HexTile centerline cell selection
2026-10-30 00:06:21 +07:00
Z User f47b57d3f8 Merge remote-tracking branch 'origin/main' into fix/environment-light-no-effect-cast-shadows 2026-10-29 08:54:59 +00:00
bflatastic 75e878f178 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-29 10:10:44 +07:00
BFlat b950014850 Merge pull request #1873 from vangel76/rfx-blur-stylize
Glow, Radial Blur, Venetian Blinds, Radial Wipe, Fractal Noise and the Transform effect render as in After Effects
2026-10-29 10:09:36 +07:00
Matej Kozlovský 6bcb76b9dc GPU: Classic 3D builds with FXC, so the GPU compositor starts on DirectX 12 (#613)
occlusion computes its three channels as one vector instead of writing a
vector component through a loop index, which FXC cannot address and tried
to unroll every enclosing loop for (X3550/X3511). classic3d now also
builds like the effect kernels: if a compiler rejects it, 3D runs render
on the CPU instead of the whole compositor failing to start. The ignored
FXC diagnostic test now checks classic3d instead of the stale pointwise
failure.
2026-10-28 17:08:46 +01:00
vangel76andClaude Sonnet 5.5 26798dce42 Merge upstream/main into rfx-blur-stylize
Resolve the conflict in crates/gpu/src/fx_stylize.rs: main moved ops::warp to
the ops::WarpOptions parameter struct; the branch's per-sample transform loop
(motion-blur samples, now passing the sample by reference) uses the new
signature with the branch's reference argument kept.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 15:37:16 +01:00
Matej Kozlovský 6e69b91a80 Export: H.264 frames convert to YUV 4:2:0 on the GPU
Accelerator::comp_frame_yuv420 (default None) renders a top-level comp frame and
converts it over the background to the H.264 export's 8-bit BT.709 limited-range
4:2:0, every byte within one level of the CPU conversion (the GPU kernel follows
it operation for operation, including NaN and half-away-from-zero ties, but a
driver may contract its arithmetic). A 4K frame then reads back 12 MB of planes
instead of 133 MB of RGBA f32.

The H.264 export asks the accelerator for planes when a frame needs nothing after
the render (progressive, project depth, no crop/resize, rendered at the output
size) and otherwise renders and converts on the CPU as before. Deferred
readbacks (a browser worker) keep the pass-based path. Sizes the kernel can't
take (width not a multiple of 8, odd height) are declined before any GPU work.

Like Cx::frames, h264_frames caps GPU frames in flight only while the
accelerator is up (map_frames_while with the same liveness check): once the GPU
stops for good, the remaining frames render on the CPU side by side instead of
one at a time.

rgba_to_yuv420 moves to effectcraft_raster::yuv so the export and the GPU tests
share one reference. The GPU tests skip without an adapter (tests::gpu()), as
the crate's others do.
2026-10-28 13:19:24 +01:00
Claude 80b9aacea7 G1: Turbulent Displace Evolution changes the output (#943)
Turbulent Displace read its noise through value_noise, whose quintic fade also
eased the evolution axis. Near every whole revolution the pattern hardly moved:
Evolution 0 and 37 degrees (or 360 and 400) differed by well under a hundredth of
the frame, so different Evolution values looked identical, and the motion sped up
and slowed down within each revolution (the #1816 symptom).

Add value_noise_linear_z (x and y keep the fade, z is linear) on the CPU and in the
WGSL kernels and use it for Turbulent Displace on both paths. value_noise is
unchanged, so the other effects and the pinned simulation goldens are unaffected.

Tests: CPU test that non-equivalent Evolution values (fractional, past 360, past
720) all differ and that Cycle Evolution still matches 0/360; unit test of the
linear z axis; GPU-vs-CPU cases at small and large Evolution.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYXhNN7W7nB6sKjwwU7CDM
2026-10-27 15:34:34 -05:00
dotCooCoo 97ef73d592 Preserve bug fixes after main module moves and bound workspace migration 2026-10-27 07:45:09 -07:00
Z User 978ca419b9 Merge remote-tracking branch 'origin/main' into fix/environment-light-no-effect-cast-shadows 2026-10-27 08:54:33 +00:00
heojunfo 7a665a67b3 Merge remote-tracking branch 'upstream/main' into fix/classic3d-shadow-diffusion 2026-10-27 14:06:18 +09:00
Minher0 743119d6e5 Give the Environment light After Effects' look (#2436)
The default studio environment's level-preserving mean of 1 acted like a
100% white Ambient light: with only the Environment light on, a white floor
rendered at full white (95% of the frame saturated) while its shadow core
went to pitch black — a fog over the scene, nothing like After Effects'
default. Three changes bring the look in line, CPU and GPU alike:

- The default environment is a dim studio now: a solid-angle-weighted mean
  of 0.3 (and a stronger key gradient), so a white floor reads a mid gray
  and the light's colour x intensity still scales the level.
- The shadow occludes only the image's directional (key) fraction of the
  light — |sum of dir x radiance| / sum of radiance, computed beside the
  dominant direction and carried in the scene — so the fill keeps shining
  inside the shadow: an area-light shadow, partial like After Effects'
  (a lone bright spot on black still shadows almost fully; a uniform image
  not at all).
- The environment shadow map's penumbra has a floor — eight output pixels,
  or four map texels where those are coarser — because the map covers the
  whole scene bounding sphere rather than the view, and its texels would
  otherwise stair-step the edge (all the more in a draft render's flat 512
  budget). Shadow Diffusion widens it further, as before.

Tests: the key fraction tracks directionality; the default keeps its dim
level, its direction and its key; the default and a bright key both shadow
the floor (the default partial, the key deep) with Cast Shadows off lit
everywhere; and the full look — a mid-gray lit floor (no fog), a partial
core, and a soft edge ramp in quality and draft renders alike.
2026-10-26 23:36:19 +00:00
skylar taylor-barrick d9cc373169 M0.37: specialize median histograms for software Vulkan 2026-10-26 19:27:46 +01:00
skylar taylor-barrick 5d60eb4449 M0.37: keep median histograms in per-workgroup memory 2026-10-26 18:49:54 +01:00
Minher0 863f0e49c1 Render the Environment light's shadow from its image (#2436)
The Casts Shadows option Environment lights got is only half a fix if
nothing shades for it: an image lights the scene from every direction,
and what occludes that light is the image itself — its dominant bright
region, the sun in a sky map.

- dominant_direction finds it: the radiance-weighted mean texel
  direction (solid-angle weighted, rotation applied). A uniform image
  cancels to zero — a flat default environment shines from everywhere
  at once and casts no shadow, which a uniform environment test pins.
- The shadow is one orthographic map over the scene from that
  direction, after the lights' maps, like a parallel light's: PCF
  widened by Shadow Diffusion, darkened by Shadow Darkness, cleared
  when nothing casts, the image is directionless, or the texel budget
  drops it.
- It darkens the environment's own diffuse and specular light on
  Accepts Shadows surfaces and counts in Cast Shadows Only. CPU and
  GPU (WGSL) do the same; Globals carries the environment's sun
  direction and shadow settings (40 → 44 floats, preflight updated).

A bright-disc environment turned to shine from behind the camera now
shadows a sphere onto the floor behind it, Cast Shadows off lights it
evenly again, and the GPU scene's sky gradient takes a shadow-casting
Environment light. Also: the renderer-switch toasts read "...renderer,
which draws 3D models", the dialog's Casts Shadows checkbox gets an
auto id (dialog.light.casts) the tests click, and the docs' Environment
notes cover the shadow.
2026-10-26 13:05:16 +00:00
heojunfoandClaude Opus 5.5 f589809b59 Classic 3D Shadow Diffusion blurs instead of stamping copies (#2444)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 12:27:44 +09:00
Shuvra Basak fa32010f67 CC Bubbles: bubbles at the layer's edges refract its edge pixels, not a pale fill (#2024)
Near the borders a bubble's refraction (liquid) or reflection (metal) sample
lands outside the layer. sample_bilinear returns transparent there, so the
pale stand-in colour for transparent pixels showed through, a band of washed-out
bubbles along every edge. Sample with edge pixels repeated instead, on the CPU
and in the GPU kernel; the pale colour stays for genuinely transparent pixels.

The CC Bubbles golden hashes are re-pinned (the half-resolution cases pad the
buffer with transparent pixels and are unchanged).
2026-10-24 22:22:48 +00:00
vangel76andClaude Opus 5.5 31c624f478 Merge upstream/main into rfx-blur-stylize
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:23:59 +02:00
dotCooCoo decc8b72f9 Advanced 3D: shadow jitter blends across its lattice cell, so GPU penumbrae match the CPU
The PCF tap jitter was keyed on the receiver's 1/8-texel cell in the
shadow map. The GPU computes the receiver's map position with last-bit
differences from the CPU, so a receiver next to a cell line could take
another cell's jitter on one backend, and every tap moved. In a wide
penumbra that changed the coverage by several 8-bit levels:
gpu_matches_cpu_wide_penumbrae failed on Windows with 15 of 4159
shadowed pixels on an NVIDIA RTX 5080 and 22 on an AMD Radeon iGPU,
against a budget of 8.

tap_jitter now blends the hashes at the four corners of the cell by
the receiver's position in it, in shade.rs and in advanced3d.wgsl. At
the lattice points the jitter is the per-cell hash it was before. A
CPU test checks that the coverage does not jump at a lattice line.
2026-10-24 04:25:03 -07:00
aiongg 017d08bbd0 Merge #2027: Fix #1708: calibrate CC Blobbylize from the AE oracle (@ki2pixel) 2026-10-24 01:20:20 +00:00
aiongg 14900b747d Merge #2211: #2094 (adv): crisp cast shadows so diffusion only softens (@ChristopherLinnett) 2026-10-23 23:22:30 +00:00
aiongg d17c2e86cd Merge #1846: Make Fractal Noise evolution progress linearly (@Silvertree2010) 2026-10-23 23:21:37 +00:00
Christopher Linnett 32ae41c4eb #2094 (adv): crisp cast shadows so diffusion only softens
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.
2026-10-23 16:21:21 +08:00
Camille Moine 374c3bf6b9 fix(effects): clear Cut Away 100 opaque edges
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.
2026-10-21 14:46:21 +02:00
Camille Moine 89d954a10b fix(effects): calibrate CC Blobbylize to the AE oracle
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.
2026-10-21 13:06:40 +02:00
vangel76andClaude Opus 5.5 83c76f266f Review: Glow's WGSL literals tied to GLOW_GAIN / GLOW_KNEE; parity summary consistent
kernels.wgsl repeats Glow's bright-pass gain 8.0 and knee 0.0611 as literals; they now
name effects::GLOW_GAIN / GLOW_KNEE (exported) and the test
glow_constants_match_the_cpu (no GPU needed) fails if they drift. docs/effects-parity.md:
the summary row no longer says no effect is compared with After Effects while the
Blur & Sharpen row reports Radial Blur's comparison (CodeRabbit on #1873), and the
Radial Blur changelog row says CPU only (it has no GPU path).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:49:00 +02:00
vangel76andClaude Opus 5.5 e559a7ce9a Review: tie the colour kernels' literals to the CPU constants; visible GPU skip; B&C wording
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>
2026-10-19 18:38:41 +02:00
vangel76andClaude Opus 5.5 8d41ef7218 Radial Wipe: Feather in pixels on both edges of the wedge, as After Effects
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>
2026-10-19 16:25:44 +02:00
vangel76andClaude Opus 5.5 47b3582a0a Venetian Blinds: stripes from the layer centre, Gaussian feather on both edges, as After Effects
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>
2026-10-19 16:24:05 +02:00
vangel76andClaude Opus 5.5 ae7081077f Radial Blur: samples outside the layer repeat its edge, as After Effects
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>
2026-10-19 16:23:42 +02:00
vangel76andClaude Opus 5.5 a8d4514a1b Glow: per-channel bright pass with After Effects' gain, glows above the threshold as bright as After Effects
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>
2026-10-19 16:23:25 +02:00