36 Commits
Author SHA1 Message Date
ca1ff9cfc2 .aep import: map the Cryptomatte plug-in to EffectCraft's Cryptomatte (#3109)
* AEP import: map the Cryptomatte plug-in to EffectCraft's Cryptomatte

The importer reported every instance of the free Cryptomatte plug-in
(match name `Cryptomatte`) as a lost effect. It now becomes
`ec.3d.cryptomatte`:

- CryptomatteData decodes the plug-in's Settings data (its own
  little-endian layout, documented in docs/aep-format.md 3.7): the
  Cryptomatte layer name and the selection text, both carried verbatim.
- Output maps to Display / Matte Only (Colors -> Colors, Matted RGBA ->
  Matte, Matte Only -> Matte Only). Matted Colors, Selection Mode when on,
  keys or expressions and anything else stored are reported, and
  unreadable data is reported as damaged.
- EffectCraft's Cryptomatte gains a Layer Name text parameter. A layer the
  footage has under exactly that name wins; otherwise the Layer popup
  decides as before. The CPU, GPU and pick paths share one resolver
  (AuxChannels::resolve_crypto_layer_named). Without it, plug-in layer
  names such as Crypto_Material would silently fall back to the first
  layer.

On a real production project this takes the effect losses from 132 to 44.

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

* AEP import: check Cryptomatte against an After Effects 2026 sample of our own

A sample made in After Effects 2026 (plug-in 2.6.6: a fresh effect,
Selection Mode on, each Output item, with read-backs and renders)
corrected three points of the mapping:

- Selection Mode on was dropped silently: After Effects writes it as a
  default-state record (tdsb 3) holding 1, which the general reader does
  not take. The Cryptomatte step reads that record itself and reports it.
- No Output record is now the plug-in's default Colors, also for an
  instance that borrows the project's definition (every instance after
  the first), which was reported as having no reliable default and fell
  back to EffectCraft's Preview. Keyed or unreadable Output values fall
  back to Colors too, still reported.
- Matted Colors imports as Display Matte (the same matte over the layer's
  own colours) instead of Preview, still reported as an approximation.

docs/aep-format.md 3.7 gets the sampled Output default, the tdsb 3
Selection Mode record and the real fresh Settings data (an empty text of
length 1 and its MurmurHash3, twice, not cry1 and zeros); docs/aep-import.md
the render comparison with After Effects (Matted RGBA matches; Matte Only
differs in alpha; Colors with no layer chosen shows another layer).
Tests: the exact fresh Settings bytes, tdsb 3 Selection Mode, borrowed
definitions, and an opt-in test against the sample once it is in the
corpus (aep/cryptomatte.aep, ignored until the corpus pin includes it).

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

* AEP import: write Cryptomatte's popup items in the test as After Effects does

The injected Cryptomatte definition wrote `pdnm` and `fnam` as `LIST`
chunks. After Effects writes them as plain chunks whose body is chunks
(`NESTED_IDS`), and the reader only looks there, so the tests never read
Output's item names: they passed through the fallback for a definition
with four items and no `pdnm`, which uses the plug-in's own list.

The harness now writes both as After Effects does. New tests check that
the injected definition reads back with its items and instance name,
that Output is mapped by the item the stored index names (items in
another order, case, an unknown item), and that the fallback without a
`pdnm` still works. With the old `LIST` form, or with the mapping
ignoring the items, the new tests fail.

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

* AEP import: check Cryptomatte's layer and selection against presets of our own

The After Effects 2026 sample 1400 now has two preset compositions that choose a layer and
pick one object (box_red on CryptoObject) or one material (mat_paint on CryptoMaterial).
CryptomatteData reads both as they are; the picked tokens are the EXR manifest's hashes,
which EffectCraft's selection grammar reads the same as the names.

- The u32 at 108 is MurmurHash3 of the selection text with its NUL (three texts).
- A unit test pins the 127-byte preset data; the sample test now covers all eight
  compositions with their layer and selection.
- Render check: EffectCraft's matte covers exactly the pixels After Effects draws as
  selected, edge coverage within 0.0002. docs/aep-format.md 3.7 and docs/aep-import.md.

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

* Cryptomatte import: report damaged Settings and Selection Mode records

Review follow-ups for #3109:
- CryptomatteData refuses a selection text without its NUL and checks the zero byte
  at 112 for an empty text too.
- A Settings record without its data, and a Selection Mode record whose value cannot
  be read, are reported as damaged data, not taken as the default.
- Layer Name is the effect's last parameter, so the earlier parameters keep their
  indexes and upgraded instances have the same order as new ones.
- A Cryptomatte layer name is matched as given before it is trimmed.

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

* Cryptomatte settings: require the selection's NUL to end its declared length

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

* Cryptomatte: match rank planes by the exact layer name

crypto_rank_planes looked planes up with a case-insensitive whole-name match, so
layers Crypto_A and crypto_a shared planes. The layer prefix now matches
case-sensitively; component letters still match in either case.

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

* Format

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

* AEP: the exporter knows Extra::Cryptomatte; rustfmt

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-06 19:36:42 +01:00
4a89e68f9f Content-Aware Fill: spill the range and stream the output (#2570 phases 2-4) (#3079)
* Content-Aware Fill: bit-pack holes, spill frames to disk, stream outputs (#2570 phases 2-4)

* Content-Aware Fill phases 2-4: bit-pack holes, spill frames, stream outputs (clippy clean)

* Address CodeRabbit review: spill safety, streaming correctness, perf, memory guard (#2570)

* Address the remaining CodeRabbit review findings on Content-Aware Fill (#2570)

A non-streaming fill returned empty frames, progress could jump backwards between
the sink and the fill, the memory guard ignored the membrane frames still resident
for the flow pass (and wasm's in-RAM copies), and the GPU fill stages ran outside
an error scope, so an OOM retired the shared context instead of falling back to
the CPU for that frame.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Content-Aware Fill: sample spilled frames without decoding them, check spill errors (#2570)

Tracing sampled spilled frames by decoding whole frames under the shared cache lock, so
parallel traces across neighbouring frames evicted each other and mostly paid a full-frame
read while every other thread waited. Sample only the 2x2 bilinear support with positioned
reads on an open handle instead, and report a failed spill read in the no-hole branch
rather than streaming black frames as a successful fill.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Content-Aware Fill: spill membrane/propagated frames, unquantized input, surface scratch errors (#2570)

Flow and propagation now write their intermediate frames to scratch spills beside
the output instead of holding the whole range as float RGBA, and the input spill
uses 32-bit floats whatever the project depth so the fill sees the same values as
the old in-memory path. Spill reads/writes and scratch creation report through a
shared error slot that run_plan surfaces instead of masking disk failures as a
cancellation. Sampling uses positioned reads; the guard models the packed masks
plus per-thread scratch.

* Content-Aware Fill: report a membrane-spill write failure instead of cancelling (#2570)

* Content-Aware Fill: include the flow fields in the memory guard (#2570)

* Content-Aware Fill: scale the memory guard with workers and check scratch space (#2570)

The guard's scratch term was a fixed 8 frames, which under-counted a wide
machine with Lighting Correction by several times. It now models the sequential
synthesis stage plus a few frames per worker (from available parallelism) and
takes Lighting Correction into account.

Native fills write three f32 copies of the range to scratch beside the output,
so a range that cannot fit on that volume is now refused up front with the
free space in the message, before any frame is rendered.

Tests cover the worker/lighting budget, the scratch formula, a shared error slot
between the input and secondary spills, an uncreatable spill folder, and the
engine surfacing a spill failure as an error instead of a cancellation.

* Content-Aware Fill: resolve a relative output folder for the scratch check (#2570)

A bare relative output name has an empty parent, so the walk-up found no
existing ancestor and skipped the free-space check. Resolve it against the
process directory first, and cover that in the test.

* Do not reformat the GPU fill tests in the spill branch (#2570)

The file was already not rustfmt-clean upstream, so the formatter only added
noise to a file this PR no longer needs to touch: phase 1's call-site changes
are already on the base branch.

* Content-Aware Fill: measure peak RSS and correct the per-worker scratch model (#2570)

The ignored measure_peak_rss_of_a_fill test fills a spilled 640x360 range and
reports VmHWM. On this 18-worker machine 90 frames peak at 340-470 MB across
runs (35 MB with RAYON_NUM_THREADS=1) and 600 frames at 582 MB, so the scratch
slope is ~7 frames a worker, not the 2 the first guard revision assumed.
Scaling the summary down showed a clean linear slope at 8 and 4 workers, and
Lighting Correction stayed below the flow stage's peak.

Budget 8 frames a worker (9 with Lighting Correction), just above the measured
top of the range, and adjust the guard test to the real numbers. At 600 frames
the measured peak is 582 MB against ~11.1 GB for the old five-copies model, a
19x reduction.

* Contributors: credit Madhav Agarwal in the About window

Adds the self-chosen real and display name for @gmrmad1. The generated
contributors.json picks it up once the PR is merged.

* Address CodeRabbit: size the guard from the rayon pool; migrate GPU tests (#2570)

fill_workers used available_parallelism, but the parallel fill stages run on
the rayon pool, which RAYON_NUM_THREADS can size larger. Use
rayon::current_num_threads so the scratch budget follows the threads that
actually run; wasm keeps rayon's single-thread fallback.

The GPU inpaint tests still used the pre-spill FillInput API. The earlier
'do not reformat' revert dropped their API migration along with the
formatting, leaving cargo test -p effectcraft-gpu uncompilable. Restore the
Frames::Memory / HoleMask / sink migration and verify the 15 inpaint tests.

* Fix gate: flatten the wasm prop lookup and keep the budget wrapper test-only (#2570)

* Address CodeRabbit review: spill error check, global-pool preflight, docs (#2570)

* Include RAM-backed spill in the preflight and guard streaming reads (#2570)

* Resolve relative spill paths, prefer stacked mounts, guard the Edge Blend sink (#2570)

* Make the fill spill anonymous and test the streaming claims (#2570)

The spill was a named file in the output folder that only Drop removed, so
an out-of-memory kill or a force-quit left a multi-gigabyte
effectcraft-fill-* file in the user's project folder, and a failed set_len
after keep() leaked it too. The spill is now an anonymous temporary file
(tempfile_in), which the system reclaims when its last handle closes; every
read and write is positioned on that one handle (pwrite/seek_write join the
existing pread/seek_read), and the folder is kept only so the propagated
frames spill beside the input.

Also:
- Edge Blend checks for a failed read before handing a frame to the sink,
  so a black fallback frame never streams.
- The propagation workers stop as soon as the spill records an error.
- HoleMask::get/set use get()/get_mut(), so a mask whose public words are
  shorter than len cannot panic.

New tests cover the PR's headline claims: a spilled range fills
bit-identically to the same frames in RAM (returned and streamed), the sink
sees frames strictly in order, a truncated spill fails with a recorded
error and streams nothing, a failure mid-stream stops before the next frame,
propagation stops after a failure, the spill has no name in its folder, and
HoleMask round-trips at word boundaries.

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

* Content-Aware Fill: clean up a stopped fill and count two scratch copies (#2570)

- A fill that fails or is cancelled after frames started streaming deletes
  the PNGs it wrote, and the output folder when the fill made it and it is
  left empty (OutputFiles, dropped after the spill so Windows can remove
  the folder).
- After a successful fill, run_plan checks that every frame has a file, and
  apply_plan/add_fill_layer return an error on an empty range or file list
  (a job worker's reply) instead of indexing [0].
- scratch_need charges two f32 copies (32 B/px): flow_pairs drops the
  membrane spill before the propagated one is created, so the third copy
  never coexists.
- The spill-creation error carries a stable "spill: cannot make the scratch
  file" marker; the panel test asserted on the OS wording, which on Windows
  is "Cannot create a file when that file already exists".

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

* Content-Aware Fill: a stopped fill leaves files it didn't create (#2570)

A fill into a given outputDir that already holds an earlier sequence
overwrites its frames as it goes (as before). The cleanup of a failed or
stopped fill deleted every path it wrote, including those earlier frames, so
a layer still using that sequence lost them. It now deletes only the files
the fill created (CodeRabbit on #3079).

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

---------

Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:51:11 +01:00
gmrmad1andFredApps eae1871541 Content-Aware Fill: drop redundant full-range copies (#2570 phase 1) (#2806)
* Content-Aware Fill: drop redundant full-range copies (#2570 phase 1)

* Content-Aware Fill: fix clippy drop_non_drop; take propagated frames instead of cloning (#2570)

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-04 18:12:39 +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
BFlat 07d1de1764 Merge pull request #2534 from klosixgaming-arch/fix/2022-render-banding
export: dither 16/32 bpc renders on the way to 8 bits, and let Render Settings Color Depth set the render's depth (#2022)
2026-10-30 05:09:48 +07:00
Christopher Linnett 280a400be4 Merge origin/main into fix/issue-2092-camera-focus-crash
Conflicts were main's new code next to this branch's additions; both kept:
- crates/effects/src/lib.rs: main's Buf::pad_sides stays as main wrote it
  (Transform's transform_grow bounds that padding itself); Buf::pad keeps
  this branch's effective-pad clamp.
- crates/raster/src/lib.rs: main's quantize_px/round_half_up and this
  branch's MAX_PADDED_SIDE (still equal to limits::MAX_SIZE on main).
2026-10-29 18:44:49 +08:00
Claude 85e061fe0c Merge main into fix/2022-render-banding (#1631)
Resolve the export tests' module list (banding and h264_gpu_yuv both stay). #1631's
yuv420_frame checked Render Settings Color Depth itself; that depth is now the project's
depth for the whole render, so the GPU YUV path is taken only for 8 bpc renders. Above
8 bpc frames take the RGBA path, which dithers, so H.264 exports don't band either.
Test: renders_above_8_bpc_skip_the_planes.
2026-10-29 03:06:31 -05: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
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
Dylan M. Taylor e55e288397 Refactor raster for strict Clippy checks 2026-10-28 06:09:20 +00:00
Claude fa14090e72 G1: dither the final 8-bit conversion so gradients don't band (#2022)
Exports converted the float frame to 8 bits by plain rounding, so a smooth
gradient rendered at 16 or 32 bpc came out as flat bands, the same as at
8 bpc: raising the depth changed nothing visible. After Effects dithers
colours converted to 8 bpc for render and export; do the same.

- raster: dither_u8 and Image::to_rgba8_dithered / to_rgba8_over_dithered
  round each channel up or down against a hash of the pixel position with
  a fixed seed (deterministic, no clock or global RNG). A value already on
  an 8-bit code, as everything rendered at 8 bpc is, is unchanged.
- export: Cx::pixels dithers the colour for every 8-bit output (stills,
  H.264, ProRes, HEVC, AV1, VP9); GIF keeps plain rounding (palette).
- export: the Render Settings' Color Depth now overrides the project's
  depth for the whole render instead of only quantising the finished
  frame, so a 16 bpc render of an 8 bpc project keeps its precision.

Tests: crates/export/tests/it/banding.rs (ramp export at 8/16 bpc and with
Color Depth overrides), raster unit tests for dither_u8.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYXhNN7W7nB6sKjwwU7CDM
2026-10-27 15:38:08 -05:00
NetterSilverandClaude Opus 5.5 db41fa7980 Skip transparent pixels and quantise while compositing
Much of a layer buffer is often transparent (a stroked ring, a glow-padded
text layer, a precomp canvas), yet every destination pixel sampled it
bilinearly, and 8/16 bpc comps then re-quantised the layer's whole rotated
bounding box.

- Bilinear warps skip 16x16 source tiles whose pixels (and a one-pixel
  apron) all have zero alpha: their samples are transparent and were never
  drawn.
- The fast affine warp reports the rectangle it changed and, given the
  comp's levels, quantises each pixel as it blends it, so draw_comp has
  nothing left to re-quantise for that layer. quantize_px moves to
  effectcraft-raster for this.
- quantize_rect skips runs of four all-zero (+0.0) pixels, which quantise
  to themselves.
- to_rgba8 / to_rgba8_over convert row by row instead of per 4-byte item.

Every pixel stays bit-identical: tests compare the warp against the
reference path, fused quantising against quantising after blending, and
the zero-run skip against per-pixel quantising.

With the previous commit, demo comp to H.264 (`cargo xtask bench --scenario
demo --format h264 --frames 300`, 11 interleaved runs on a 16-thread Ryzen):
wall 9.26 s -> 6.97 s median (-24.8%), 8.55 s -> 6.55 s min (-23.3%); CPU
time -23%. Output byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:40:41 +01:00
Christopher Linnett 60da124f11 Merge remote-tracking branch 'origin/main' into fix/issue-2092-camera-focus-crash 2026-10-28 00:13:40 +08: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
Christopher Linnett 34242e8c1b fix: move Buf offsets by the pad Image::padded actually applies (#2092 review follow-up)
Image::padded clamps the pad so each side stays within MAX_PADDED_SIDE, but
Buf::pad still added the requested pad to its offset, so a clamped pad left
the pixels and the offset disagreeing (a hostile pad of u32::MAX shifted the
pixels by 5 and the offset by ~4.3e9). The clamp now lives in one place,
Image::effective_pad, used by both padded() and Buf::pad.

The padded() doc now states what the clamp guarantees (output within the
largest image the engine supports) and that memory budgets belong to callers
that pad by a derived amount (DOF_PAD_BUDGET, GPU fits()).

Adds an effects regression test: Buf::pad(u32::MAX) moves the offset by the
clamped pad and the source pixel stays at its layer position (fail-before:
offset off by u32::MAX). cargo test -p effectcraft-effects 457/457,
-p effectcraft-raster 35/35, -p effectcraft-render 238/238; clippy -D
warnings and fmt clean on touched crates.
2026-10-24 11:49:53 +08:00
Christopher Linnett d3d23c2f74 fix: bound Image::padded() at the API so no caller can reintroduce the oversized-pad OOM (#2092 review follow-up)
Triage found the pad bound lived only in the DoF callers: Image::padded was
pub with a doc comment asking callers to bound pad, so a future caller could
reintroduce the exact oversized-buffer OOM this PR fixes. The cap now lives
inside padded(): pad is clamped so each output side stays <= MAX_PADDED_SIDE
(30 000, mirroring project limits::MAX_SIZE; raster cannot depend on the
project crate per layering), overflow unreachable by construction, no panic in
any profile. The caller-side padded_size guards stay for reject-on-error UX.

Adds raster regression tests: hostile pad (u32::MAX) clamps to cap with source
intact at the clamped offset (fail-before: pre-fix sized an oversized buffer);
legit pads byte-identical (anti-over-clamp). cargo test -p effectcraft-raster
35/35, -p effectcraft-render 238/238, clippy -D warnings clean, fmt clean.
2026-10-24 11:26:30 +08:00
aiongg 3385eafe1f Batch fix: blur: a NaN or vanishing sigma skips the Gaussian pass (#1968) 2026-10-24 01:21:11 +00:00
aiongg 65a199bbd7 Merge #1968: Make Gaussian Blur and Fast Box Blur continuous at small values (#1939) (@MarekSpodBiedry) 2026-10-24 01:20:43 +00:00
Christopher Linnett d2fae0a144 fix: bound Classic 3D depth-of-field blur and reject non-finite focus distances (#2092)
The camera's Set Focus Distance to Layer command compared only 'd <= 0.0', which NaN passes, and two finite but extreme positions can overflow their difference to infinity; a stored non-finite focus distance serializes as null and leaves the project unsavable. Reject it, fall back to the zoom plane in the camera evaluation, and count a non-finite focus or depth as in focus. The Classic 3D blur radius itself was unbounded: a 0%-scaled or very distant layer scales it by up to 1e9, and the padded blur buffer sized from it aborts the process. Cap each plane's radius to a padded buffer that fits the renderer's 16 Mpx layer budget, don't blur planes whose pixels cover under a thousandth of a comp pixel, saturate the padded sizes, and refuse padded buffers past the project's maximum layer size. Regression tests for each guard.
2026-10-22 21:57:41 +08:00
MarekSpodBiedry 3878fdf0ba Make Gaussian Blur and Fast Box Blur continuous at small values (#1939) 2026-10-20 18:49:19 +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
Matej Kozlovský 76098665b0 Cryptomatte: effect.pickCryptomatte picks the ID under a point into the Selection (#473)
Engine part of viewer picking: replace / add / remove the top-coverage ID at a
layer-space point in one undo step, at the time the Selection was read. The
aux channels come from the render's own path (Renderer::layer_aux, shared with
the effect host; footage at its source time, 3D precomps) through a renderer
set up like Session::render (expressions, render prefs, layer cache). An
optional scale snaps the point to the pixel a preview at that resolution
samples. New effects::crypto_select quotes names and falls back to <hex> for
unnamed, wildcard-looking or ambiguous names. effect.cryptomatteAt is the
read-only hover query.
2026-10-19 06:10:27 +02:00
Matej Kozlovský 47dbd8dbd5 Cryptomatte: the rank planes are resolved once per evaluation and the GPU path reduces them on all cores, instead of formatting and looking up channel names for every pixel in a single-threaded loop (#473)
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.
2026-10-12 13:29:03 +02:00
bflatasticandClaude Opus 5.5 27cabcb36b Effects: EXtractoR picks layers and channels of multi-layer EXRs (#295)
EXtractoR's Red, Green, Blue and Alpha are text parameters naming
channels, and Effect Controls drew nothing for text parameters: there
was no way to choose a layer or channel. And a multi-layer OpenEXR file
whose colour is only in named layers (`diffuse.R`, Blender's
`ViewLayer.Combined.R`) didn't import at all: the image decoder wants an
unnamed RGB layer ("does not contain non-deep rgb channels").

- Effect Controls: EXtractoR gets a Layer popup listing the footage's
  OpenEXR layers. Picking one sets red, green, blue and alpha to its
  channels in one undo step, as After Effects' EXtractoR dialog does (a
  one-channel layer such as depth goes in all three colours). Red /
  Green / Blue / Alpha are popups of the layer's own R, G, B, A, every
  channel of the file, and (none). The lists are read only while open,
  so playing an EXR sequence doesn't parse files on the UI thread.
- `layer.channels {layer}` lists a layer's EXR layers and channels and
  what each layer shows in red, green, blue and alpha, for agents.
- Media: an EXR without an unnamed RGB layer imports and shows its
  colour layer (one named combined, beauty or rgba first); its channels
  stay available to EXtractoR and the 3D Channel effects.
- Raster: AuxChannels::layers / layer_rgba and split_channel.

Test: extractor_shows_the_chosen_layer_and_channels_of_a_layered_exr
(host: a layered EXR written with the exr crate imports, layer.channels,
EXtractoR renders spec and depth; it failed to import before),
extractor_offers_the_exr_layers_and_channels (UI: Layer popup, one undo
step, channel popup), layered_exr_without_an_unnamed_rgb_layer_imports
(media).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:32:09 +07:00
Dexter 39e6907b5f G2: improve editing reliability and Windows rendering fidelity 2026-10-06 00:00:31 -05:00
Brandon ThomasandClaude Opus 5.5 786e4e8b97 Never crash: raster and path crates without expect()/unwrap()/unreachable!()
raster: the inpainting pyramid stops at a missing level, and video inpainting falls back to a
plain patch fill if the previous frame is missing, instead of expect(). path: curve fitting of
an empty point list returns a degenerate cubic instead of indexing/unwrapping, and Merge Paths
no longer has an unreachable!() arm. Denies the panic lints in both crates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:41:09 -04:00
Brandon ThomasandClaude Opus 5.5 225f31bf50 Merge main into M9.13 (GPU kernels defer CPU-only controls via catalog::gpu_supported)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 07:29:30 -04:00
Brandon ThomasandClaude Opus 5.5 e362cae4c7 M13.6: panel polish (log-density scopes, narrow-panel layouts) and an offscreen panels snapshot test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 03:21:30 -04:00
Brandon ThomasandClaude Opus 5.5 b8a56eb4a9 M13.6: scopes, content-aware fill (PatchMatch/flow propagation/membrane), cut detection and auto-trace algorithms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 02:21:14 -04:00
Brandon ThomasandClaude Opus 5.5 ce6db35d14 M9.13: Lumetri (HSL Secondary, hue/sat curves, RGB curves, HDR, looks) + Hue/Sat ranges + Levels/Curves channels + Timewarp optical flow
- Lumetri: HSL Secondary key/refine/correction with Show Mask, Hue vs Sat/Hue/Luma,
  Luma vs Sat, Sat vs Sat and per-channel RGB curves, High Dynamic Range (HDR White /
  Specular), Input LUT and Look (custom .cube + built-in looks).
- Hue/Saturation: Channel Control with Reds..Magentas ranges (range + fall-offs).
- Levels: Channel popup and Red/Green/Blue/Alpha controls; Curves: Channel parameter.
- catalog::param_shown: Effect Controls shows only what a popup selects.
- Timewarp Pixel Motion: block-matching flow (raster::flow::block_flow_window) with
  Vector Detail, Smoothing, Error Threshold, Block Size, Weighting, Correct Luminance,
  Filtering, Build From One Image; Matte Layer/Channel, Warp Layer, Show, Source Crops.
- GPU falls back to CPU when channel/range controls are used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 02:20:57 -04:00
Brandon ThomasandClaude Opus 5.5 9a11c577f3 M9.11: 3D Channel effects on auxiliary channels
Aux-channel model (effectcraft_raster::channels3d: depth, object/material
ID, normals, UV, coverage, Cryptomatte ranks + manifests, MurmurHash3);
EffectHost::aux; multi-layer OpenEXR channels via the exr crate
(FootageSource::aux); the compositor's depth / layer-ID / Cryptomatte pass
(Renderer::comp_aux) so the effects work on precomps of 3D comps.
3D Channel Extract, Cryptomatte, Depth Matte, Depth of Field, EXtractoR,
Fog 3D, ID Matte, IDentifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:28:37 -04:00
Brandon ThomasandClaude Opus 5.5 bfa21e3222 M4.3: frame blending
Frame Mix and Pixel Motion for footage and precomp layers when the layer's
Frame Blending switch and the comp's Enable Frame Blending are on and the
source time falls between source frames (rate conform, time stretch, time
remap). Exact frame positions from tick arithmetic. Pixel Motion is a
hierarchical block-matching optical flow (SAD search, sub-pixel parabola
fit, vector median) with a bidirectional warp-blend (raster::flow). Blended
footage frames are cached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:05:52 -04:00
Brandon ThomasandClaude Opus 5.5 a2ff724ac2 M12.1: render cache + parallel compositing fast paths
- render: LayerCache (content-keyed processed-layer cache: source -> masks ->
  effects, keyed by a hash of the layer's non-transform props evaluated at the
  frame time, solid item, switches, scale; time folded in for clock-driven
  effects / wiggle paths). Static and transform-only layers render once;
  edits only re-render the edited layer. Session + UI frame jobs share it.
- effects: TIME_DEPENDENT list + test that every clock-reading effect is listed.
- raster: affine composite fast path (per-row span clipping, unchecked interior
  bilinear, integer-translation copies, inlined Normal blend), checked against
  composite_warp_reference; box/Gaussian blur without transposes, two
  ping-pong buffers; accumulate_warp so motion blur sums into one buffer
  instead of one full-frame image per sub-sample.
- ui: viewer frame is urgent and jumps the prefetch queue; stale-revision jobs
  are dropped; paused prefetch waits for the viewer frame; "Frame Render Time"
  reports viewer frames only; parallel f32 -> Color32 conversion.
- cli: `frame --bench N` (per-layer/per-effect timings, wall + CPU time) and
  `frame --bench-play N` (consecutive frames with/without the layer cache).
- tests: cache transparency (scrub, edits of values/keys/effects/masks/
  solids/text/timing), hit/miss accounting, time-dependent effects, demo
  cached == uncached, fast warp/blur/accumulate == reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:34:33 -04:00
Brandon ThomasandClaude Opus 5.5 3a59a8470d M0.9–M0.10: egui frontend (AE-style panels, viewer, timeline, effect controls, dialogs), desktop app with control channel, host wiring; cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:48:31 -04:00
Brandon ThomasandClaude Opus 5.5 808cd490cd M0.1–M0.7: workspace, time/geom/color/keyframe/project/raster/path/text/effects/render crates
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:05:20 -04:00