Commit Graph
1162 Commits
Author SHA1 Message Date
Valentin KleinandClaude Sonnet 5.5 9408d70da0 Disk cache: stamp footage files for the salt off the UI thread (#3106)
* Disk cache: stamp footage files for the salt off the UI thread

Importing an After Effects project whose footage includes long image
sequences on a network share froze the desktop app for 15-27 s. The
import itself takes about 0.25 s; the time went into the edit's bump(),
where refresh_disk_salt() stats every footage file (13,466 sequence
frames in the measured project, ~15 s) on the UI thread whenever a disk
cache is attached. File > Open and footage reload hit the same path.

With Session::disk_salt_background (set by the desktop app) the salt is
taken on a worker thread and picked up by poll_disk_salt() from the UI
tick. Until it lands the layer cache runs memory-only, so keys under the
old salt (other files, or another project's items) are never read. A
newer footage change supersedes a salt still being taken. Headless
sessions keep taking it synchronously.

A settings change that keeps the disk cache folder no longer re-stamps
every footage file either.

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

* Disk cache: opening a project drops the footage salt being taken

A salt job still running for the old project was kept when the new project named the
same footage, so the salt stamped before the files changed stayed in use.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:19:27 +01:00
Valentin KleinandClaude Sonnet 5.5 ae068ff931 EXR export: write the output space's chromaticities attribute (#3038)
* EXR export: write the output space's chromaticities attribute (#3037)

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

* EXR chromaticities: tests for unmanaged and PQ ProEXR, every space once

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

* EXR export: note why the comp background is added before decoding (review point)

CodeRabbit asked to convert Comp.background to the output space before
compositing. Every output path (Image::to_rgba8_over) adds the background to
the output-encoded frame as it is, so the EXR now matches the other outputs;
converting would make the EXR colour differ from the PNG of the same comp.
Comment only.

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

* Format the merged code with rustfmt

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:19:05 +01:00
ash 7fc1be0bf2 Report offline footage and recover automatically when media returns (#3161)
* 3118: report and recover offline footage consistently

* 3118: preserve manual reload and refresh data watch paths

* 3118: document availability and reload contracts
2026-11-06 11:38:57 +01:00
Valentin Klein ce9eaec650 Add After Effects project export (#2594)
Add After Effects project export with chunks built in code, explicit loss reporting, engine and UI integration, and regression coverage.
2026-11-06 09:23:16 +01:00
ash bc4b96a5fc Reveal newly created mask controls in the Timeline (#3149)
* 3145: Reveal newly created mask controls in the Timeline

* 3145: Document mask reveal contracts and regression cases

* 3145: Make stale-event regression cases exhaustive
2026-11-06 08:53:04 +01:00
vangel76andClaude Sonnet 5.5 f9b860838c Merge upstream/main into perf/exr-output-options
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 00:49:29 +01:00
FredApps 8b63d4308c Merge pull request #2916 from vangel76/fix/exr-scene-linear-output
EXR export: no compand / tone mapping, and the output space's own curve (#2913)
2026-11-06 00:12:32 +01:00
FredApps 39af86e9f4 Merge pull request #2929 from vangel76/perf/aux-footage-hint-474
perf(media): tell the media pool which EXR footage effects read (#474)
2026-11-06 00:12:06 +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
vangel76andClaude Sonnet 5.5 ab6e76973a ProEXR test: a non-sRGB output curve (Rec. 709) decodes with its own curve
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 18:47:54 +01:00
a439b1bb19 Effect ▸ Install Effect Plug-in… and plug-in bundles (wasm + ScriptUI panel in one go) (#2749)
* plugins: install effect plug-in bundles (wasm + ScriptUI panels) in one go

* plugins: address review - keep listing errors, refuse replacing an installed plug-in

* plugins: report unreadable bundle entries when their metadata cannot be read

* plugins: surface partial install errors in the menu bar, cover the install flow with tests

* plugins: surface partial install errors for async dialog answers too

* plugins: show result errors as compact status text

* plugins: harden installs - confined automation, no panel overwrite, size cap, cleanup errors

* plugins: bound install reads from one handle, detect unreadable panels, clarify install docs

* plugins: drop an unneeded mut in the bounded read

* plugins: saturate the bounded read limit

* plugins: install into the session's Plug-ins folder, refuse hidden single installs and case-variant panels

- Install .wasm files into Session::plugins_dir() (the folder the Plug-in Library
  from #2524 uses), not a path rebuilt from the settings folder; the host tests
  now point both at their temp folder so they never touch the real settings.
- A single install of a hidden file (".x.wasm") was skipped and reported as a
  success that installed nothing: it is now an error.
- Installed names must be bare file names (and not Windows device names on
  Windows), as file.installScript requires, so every installed panel is listed
  and can be uninstalled.
- A panel whose name differs from an installed one only in case replaced it on
  Windows and macOS: compare ignoring case and check the file on disk too.
- The loaded plug-in's source is the installed copy, not the file it came from.
- Document the `bundle` menu parameter, so checked execution accepts it.

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

* plugins: drop needless to_vec in the bundle install test (clippy)

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

* plugins: keep the loader test's byte vectors, drop to_vec in the install test (clippy)

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

---------

Co-authored-by: STAR Web Studio <hello.starwebstudio@gmail.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:35:54 +01:00
611a282bea Plug-ins: external effect helpers (safe, out-of-process native effects) + plug-in API 2 (#889)
* Plug-ins: API 2 (PluginHost, padding, layer_span)

Split out of the OpenFX work so it can be reviewed on its own. Additive; the WebAssembly ABI
stays at 1.

- point3 / layer / text / hidden parameters, integer sliders, parameter groups.
- EffectPlugin::padding (transparent pixels around the buffer, at most 4096 per side).
- EffectPlugin::render_v2 with a PluginHost: the input at other times, other layers, parameters
  at other times, frame rate, per-instance key, the layer's source range.
- EffectEnv::layer_span from the renderer; MAX_PLUGINS 1024.
- plugin_error: a plug-in effect's last render error, for the UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS

* Plug-ins: external effect helpers (crates/helper-host)

Native plug-in formats run in separate helper programs, never in EffectCraft's process
(docs/adr/0001-external-effect-helpers.md). New safe-Rust crate effectcraft-helper-host (L3,
native only, unsafe forbidden): it finds helper programs, starts each as a child process and
registers its effects through plug-in API 2, so the UI and command surface need nothing new.

- Protocol v1 (docs/external-helpers.md): JSON lines over stdin / stdout (helper replies are
  prefixed so plug-in chatter is ignored); pixels go through a frame exchange file (/dev/shm on
  Linux), never through the pipe. Render-time callbacks for the input at other times, other layers
  and parameters at other times, with nested renders through the same helper.
- A helper that exits, hangs past its timeout or breaks the protocol is stopped. The render fails
  with a message, the frame is left as it was, and the helper restarts for the next render. An
  effect that stops its helper 3 times is turned off (helper_blocklist.txt).
- Opt-in: nothing starts helpers unless the user asks (Effect > Scan for Effect Helpers), or opts
  in to starting them at launch with EFFECTCRAFT_HELPERS=1, an environment variable, so writing
  to the settings folder alone can't run code. effect.helpers.scan / load are refused for
  confined agent sessions (confine.rs), like file.executeFile.
- Discovery: helpers/ next to the app (Contents/Helpers on macOS), helpers/ in the settings
  folder, EFFECTCRAFT_HELPER_PATH.
- Commands effect.helpers.list / scan / load; Effect menu entries (translated); the last render
  error of an effect shows as a warning sign after its name in Effect Controls.
- serve(): the helper side for Rust helpers. tests/it/main.rs runs the whole protocol (render,
  padding, callbacks, nesting, threads, crash, restart, block list, hang) against a fake helper,
  with nothing to install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS

* Effect helpers: address review (start failures, frame file mode, nextest listing)

- A helper that can't start (not installed any more, restarting too often, no frame exchange
  file) is reported but no longer charged to the effect being rendered: it is Failure::Refused,
  not Failure::Broken, so it never counts toward turning that effect off. New check in
  tests/it: once the helper is out of restarts, renders of an effect fail without blocking it
  (fails without the fix: the effect was turned off after 3 such renders).
- The frame exchange file is created owner-only (0o600) on Unix: it holds the user's frames and
  /dev/shm is shared. Asserted in the frames unit test.
- tests/it (harness = false) speaks the libtest listing protocol cargo-nextest uses:
  `--list --format terse` prints `helper_protocol: test` (nothing with `--ignored`), and
  `helper_protocol --exact` runs the checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS

* Effect helpers: bound writes to a helper, drain stderr, keep callbacks in step

- Writes to a helper go through a writer thread with the request timeout: a
  helper that stops reading its stdin (hung with a large callback answer or
  render request pending) no longer blocks the render thread forever while
  holding the helper.
- stderr is drained as bytes (lossy, in 64 KiB pieces) to the end: output that
  isn't UTF-8 stopped the reader, closing the pipe under the helper (EPIPE /
  SIGPIPE on its next write); lines were unbounded.
- Callback images that can't be written are answered with `null` instead of
  failing the render while the helper waits (which left the protocol out of
  step); a frame that can't be written before the request is sent is not
  charged to the effect.
- Mid-render calls use the render's own process: no respawn or heartbeat ping
  while the helper is waiting for an answer.
- Waiting for a busy helper is bounded (load timeout) instead of forever;
  status() no longer blocks on a running render.
- Reap the child when start-up fails after spawning; cap the block list read.
- Scripts start helpers only with Allow Scripts to Write Files and Access
  Network on (they run native code), as for File.execute().
- helper-host declares the workspace's no-panic crate lints; the desktop
  main() keeps its doc comment.

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

* Effect helpers: rustfmt the fake provider, build the test session in its initializer (clippy)

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

* Plug-in Library: offer WebAssembly API 1 releases only; a helper already stopped mid-render isn't charged again

- WasmPlugin loads only modules at the WebAssembly ABI version, which stays 1 while
  PLUGIN_API_VERSION is 2: share it as effects::plugin::WASM_API_VERSION and have the
  library's compatibility check use it, so it never installs an API 2 release that
  can't load.
- with_running: a process that is already gone was stopped by what broke it (a nested
  render, charged there), so this render fails without charging its own effect.

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

* frames: plug-in effects count as reading other frames (plug-in API 2)

With main's narrowed frame invalidation (#2795), an edit inside a precomp under an
effect that reads its layer at other times must reach the layer's whole span.
Plug-in API 2 gives every plug-in effect the input at other times, so any effect
outside ec. counts.

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-05 11:11:57 +01:00
4e8398f8ca G8: Plug-in Library: install, preview and update community WebAssembly effects (#2524)
* G8: plug-in library

Community Plug-in Library for WebAssembly effect plug-ins (#376): one
reviewed catalog (index.json), install / update / remove from Settings >
Plug-in Library (also Effect > Plug-in Library...), library.* commands,
start-up loading of installed packages with SHA-256 checks. Downloads go
through the system curl (https, GitHub hosts), as trained models do.

Preview before installing (library.preview): the release is downloaded,
checked like an install, and rendered in the sandbox on a built-in sample
frame at default settings (2 s at 12 fps) with a Before/After split, plus
the effect's parameters, "Open on GitHub" and an optional author video
link (catalog field preview.video). Authors need to do nothing for it.

Hardening: a module that fails to load leaves the installed copy alone,
Update All updates every package in one job, minApp also applies to an
explicit version, curl ignores ~/.curlrc and URL globbing, unique temp
files, percent-decoded file URLs. Plug-in effect slots grow from 64 to
256 with a clearer overflow error.

Docs: plugins.md "Plug-in Library", publishing-plugins.md (author guide
from zero to listed), gaps.md; examples/plugin-catalog is the starting
content of the catalog repository (schema, validation workflow).

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

* contributors: credit iseemhlyfta

* G8: plug-in library: doc comments on the library's functions

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

* G8: plug-in catalog check: require one object with schema 1 and a packages array

An empty file or several JSON values passed jq empty, and with
SKIP_SCHEMA=1 nothing else checked the catalog's shape.

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

* G8: plug-in library: one preview texture set; catalog schema requires effects

The preview kept a texture set per package version previewed (about
6 MB each) for the whole session; it now keeps one, replaced when
another package is previewed. Releases in the catalog schema must list
at least one effect id, as the author guide says.

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

* G8: plug-in catalog schema: effect ids follow the manifest's id rules

Letters, digits, '.', '_' and '-', and not the reserved 'ec.' prefix,
so library.missing can match them.

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

* G8: refuse Plug-in Library jobs while automation file access is confined

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

* G8: split the Plug-in Library page into toolbar, card, links and actions

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

* G8: show Plug-in Library toasts without a Roto job; cache preview frame checks

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

* G8: log Plug-in Library packages that fail to load at start-up

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

* G8: check an update before replacing a loaded package; Update All past failures

- An update to a package that is already loaded is now compiled and its manifest checked
  (through the previewer, without registering it) before the new file is written and the old
  version's folder deleted. Before, a release with the right SHA-256 that didn't load replaced
  the working version and failed at the next start.
- Package ids that differ only in case, or end in a dot, would share one folder on Windows and
  macOS: the catalog keeps the first of them, and ids can't end in a dot (schema too).
- Update All: a package that can't be picked or installed no longer stops the others; the
  result names what was updated and what wasn't.
- Downloads: at most 5 redirects, and one that ends off GitHub's hosts is refused, as the module
  docs say.
- A job that panics no longer leaves the library busy; poisoned status locks are used, not
  dropped.

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

* G8: rustfmt the update test

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-05 05:51:17 +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
Valentin Klein bb78d55825 After Effects project import (.aep / .aepx); export follows (#2558)
Add After Effects project import with bounded parsing, explicit loss reports, and regression coverage.

Fetch the cleaned external test corpus at commit 97262276f625afdeeb61787e4cbb74b3f17af510 and verify each fixture against the pinned SHA-256 manifest.
2026-11-04 12:06:08 +01:00
vangel76 06438a1b9f Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-04 00:14:35 +01:00
vangel76andClaude Sonnet 5.5 653157532b Merge upstream/main into fix/exr-scene-linear-output
Conflict: crates/gpu/src/tests.rs (both sides added a test; kept both).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 00:04:11 +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
08362b9c04 GPU: free effect working textures after each call instead of at frame end (#1523)
* GPU: free effect working textures after each call instead of at frame end

A GPU frame keeps its encoder open while the CPU renderer runs layers with GPU
effects (Gpu::effects -> run_chain). Each call's Enc handed its textures back
to the shared pool, where they waited for every open encoder, so all effect
intermediates of a frame stayed allocated until the frame ended (17.7 GB by
layer 17 of a heavy 4K comp, then OOM). Effect calls now borrow a pool owner
(Enc::lent) that is given back once the call's work is submitted or
discarded; its kept textures are trimmed to the pool budget, so the rest are
freed after each call.

* GPU: one pool budget for idle effect calls, trimmed at the end of a render

Review of #1523: each idle lent owner kept up to POOL_BUDGET of textures of
its own, outside the shared accounting, and there were as many idle owners as
effect calls ever ran at once (16 frame threads: ~12 GB after a 4K render).

Giving a lent owner back now moves its kept textures to the shared free list,
so one POOL_BUDGET bounds what the free list and every finished effect call
keep. They skip Pool::release's wait for open encoders: only the lent Enc
recorded work on them, and that work is submitted or discarded. Each lend is
a fresh owner id, so a lease that outlives its call (an image handed to
another encoder) is released through Pool::release; this does not rely on
the result being read back.

Accelerator::render_end (called when an export ends) drops the kept textures.

* GPU: evict the pool's oldest free textures, guard escaped leases

Review of #1523 (follow-up to 0a884fb6):

- Pool::make_free rejected a texture when the free list was full and never
  evicted, so once sizes that no longer occur (per-frame blur bounds, padded
  layer sizes) filled the budget, every later free was a drop and every take
  an allocation. It now evicts the least recently freed textures until the
  new one fits.
- A lease read by another Enc (dispatch, copy, readback, display) or handed
  to a cache is marked escaped and always released through the guarded
  Pool::release, so a lent call can no longer reuse a texture that a still
  open encoder reads.
- Docs: POOL_BUDGET caps the free list only; textures waiting for open
  encoders and those of live Encs are outside it.
- export_async calls render_end from a drop guard, so the pool is also
  trimmed when an export panics or its future is dropped.
- Tests use a private context with its own pool budget instead of a global
  override; new tests for stale sizes and an image read by an open encoder.

* GPU: evict only stale pool sizes, count evicted textures until the GPU frees them

Evicting the least recently freed texture for every newcomer thrashed when a frame
frees more than the budget in a repeating order (blur/glow intermediates): each free
evicted the size the next call takes first, so misses came back to the old level and
evict + reallocate raised the peak.

The free list now evicts only sizes nobody asked for in the last 2 output frames
(Accelerator::frame_end, called by the export per frame); otherwise the incoming
texture is dropped and the kept set keeps hitting. Evicted bytes count against the
budget until the work submitted so far is done, so evictions run at most one budget
ahead of the GPU.

* GPU: lower the shared pool budget to 256 MB

On 4K RTX 3090 runs 256 MB matches the old code's peak reserved on d_blurglow
(9204 MB) and is 256-512 MB above it on c_effects (6592 vs 6080 MB), while
staying faster.

* GPU: end frames on the multi-frame render paths, route recording through Enc::record

Only the export pipeline called frame_end, so off export (preview, remote
frames) the pool's clock stood still, no size went stale and a full free
list dropped every newcomer. passes::comp_frame (exports), remote frames
and the preview workers now end each frame; test without pool_frame_end.

Docs: the frame clock is coarse with many frames in flight; dying only
throttles eviction, approximately; no cache path escapes images. Enc::record
is the single way to get the encoder outside context.rs and marks the
images it names (Enc::uses).

* GPU: wait for the empty submission while evicted pool textures still count

stale_sizes_do_not_starve_a_steady_size failed with 0 hits and 20 misses on
hardware adapters: pool_frame_end submitted an empty batch and then only did a
non-blocking Poll, and the queue's work-done callback did not run before the
next frame, so Pool::dying never drained and make_free stopped evicting stale
sizes.

While dying is non-zero, pool_frame_end now waits on that empty submission for
up to the readback timeout (the pool lock is already released). A wait timeout
just moves on and does not retire the context or its readbacks; other poll
errors still retire them. Without dying textures it keeps the cheap Poll.

Enc::crop records through Enc::record(&[src, &dst]) so both pooled images are
registered like on every other recording path (review).

* GPU: do not repeat the pool frame-end wait until evicted textures drain

A wait that frees none of the evicted bytes (timeout, or a WebGPU poll that
does not block) marks the pool stalled; later frames only poll until dying
drops. Test covers the no-repeat and partial-progress cases; dying decrement
saturates.

* Remote frames: end a pending frame that a new render replaces

A render arriving while a multi-pass frame waits for readbacks dropped that frame without
`frame_end`, so the GPU pool's clock skipped it. Errors return before `frame_begin`.

* Render: end the frame even when a comp frame panics

`passes::comp_frame` called `frame_end` only after a normal return, so a frame that
panicked (or whose future was dropped between passes) never aged the GPU pool.

* GPU pool: log the reuse rate at render end; test reuse across effect calls and frames

Log cumulative hits/misses (and reuse %) once per render before trimming. Tests
cover reuse within an open frame across effect calls, and across repeated frames.

---------

Co-authored-by: Matej Kozlovský <MatejKozlovsky@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 21:12:19 +01:00
Dylan M. Taylor 2f31c22a09 fix(prefs): when recovering from a bad value, try only the settings' own pages one by one (#3010) 2026-11-03 14:10:24 -05:00
vangel76 e3ab397a7e Merge remote-tracking branch 'origin/perf/exr-decode-cache-474' into m2929-merge 2026-11-03 19:20:26 +01:00
vangel76 4aaf9c4bb4 Merge remote-tracking branch 'upstream/main' into m2915-merge 2026-11-03 19:08:37 +01:00
Brandon Thomas 859808e062 Merge PR #2979 2026-11-03 09:30:23 -05:00
Brandon ThomasandClaude Opus 5.5 26228247e2 Follow-ups to the batch: name numbering keeps no double space (#2908), Material Options error names the parameters (#2924), corrupt file timecodes stay on timecode (#2835), restore #2934's scroll-gutter asserts, #2825's Motion Tile test compiles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:30:22 -05:00
Brandon Thomas f5e81cd1f9 Merge PR #2957 2026-11-03 09:08:08 -05:00
Brandon Thomas 5dc64bb227 Merge PR #2885 2026-11-03 09:04:55 -05:00
Brandon Thomas 3072f009bf Merge PR #2891 2026-11-03 09:04:54 -05:00
Brandon Thomas 0a91567425 Merge PR #2834 2026-11-03 09:04:45 -05:00
Brandon Thomas 0e3d54f982 Merge PR #2909 2026-11-03 09:04:45 -05:00
Brandon Thomas 2fb7003643 Merge PR #2924 2026-11-03 09:04:45 -05:00
Claude a5e0da657e Pre-compose: layers left behind drop links to moved layers, default names are numbered
Unselected layers parented to, or track-matted by, a layer that moved into the
new composition kept ids that no longer exist in the outer comp. The command's
default name was always "Pre-comp 1"; it now picks the lowest unused number.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
2026-11-03 13:58:54 +00:00
Matej Kozlovský 6b1f91ffb8 Cryptomatte: read an ID's coverage from the kept probe (#473)
Session::crypto_coverage returns the coverage of one Cryptomatte ID over the layer, read from
the probe Session::crypto_probe_job keeps: at most max_side samples on the long side, each at
the centre of its cell, or None until the probe is built. It is what the Cryptomatte
eyedropper's hover highlight will draw.

Tests cover the coverage of an ID from the kept probe and the cost of a highlight with six
ranks.
2026-11-03 07:22:46 +01:00
Bar BorerandClaude Opus 5.5 8a37ff6b43 Tests: trim the midpoint snap tests to what #2855 changes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 05:07:59 +02:00
vangel76andClaude Opus 5.5 0d2153e259 test(media): review follow-ups for the aux footage set (#474)
- A watch-folder render gives the host session's set back after its own
  project replaced it on the shared footage source.
- Guard gpu fx_depth::IDS against reads_aux drifting apart.
- Count picture and channel decodes per thread instead of reading the
  shared channel cache, so parallel tests can't make the checks flaky.
- The engine test fails when no set was ever sent.
- Docs: Media Replacement items are learned at their first frame.

Refs #474

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 03:27:25 +01:00
vangel76andClaude Opus 5.5 5b31b96cde perf(media): tell the media pool which EXR footage effects read (#474)
The pool learned that an effect reads a footage item's channels only at the
first aux() call, so the first frame was decoded twice (picture, then in
full), and the hint was never cleared after the effect was removed.

The session now gives the footage source the whole set of files read by
enabled 3D Channel effects (FootageSource::set_aux_footage) with every
project change (Session::bump: edits, undo, redo, history jumps, rollbacks,
project open), sending it only when it changed. The pool keeps that set and,
as a safety net, what aux() learns; a new set drops what was learned.

Refs #474

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 03:21:40 +01:00
JonRC 583ba084b6 Fix #2917: material.set names the Material Options a layer doesn't have
A 3D model layer keeps its file's materials, so it has no Base Color,
Metallic, Roughness or Emissive. material.set refused them with "no
Material Options property matched (..., baseColor, metallic, roughness,
...)", naming the parameters it had just refused, and next to a
parameter the layer has it dropped them and reported success.

A parameter that none of the given layers has now fails the command
(nothing is set), naming it and the properties the first layer has. A
value of the wrong type is its own error. The parameter list says which
layer kinds take which.
2026-11-02 23:13:02 -03:00
Bar BorerandClaude Opus 5.5 fa0c260630 Puppet: refuse only a mesh that repeats one already there (#2865)
Cut the fix down: a click off every mesh makes nothing only when the art it resolves to
already has a mesh with that outline. The nearest-art fallback in `region` stays, so a
near miss beside separate art still meshes it. `mesh::outline` is as it was and the
`newMesh` option of puppet.addPin is back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 04:00:19 +02:00
vangel76andClaude Opus 5.5 406c9a9648 EXR export: raw alpha for Alpha-only files, skip_hdr_step name, more tests
- Rename RenderOpts::scene_output to skip_hdr_step (Pipe::without_hdr_step):
  the consumer decodes with Pipe::output_linear or applies the step itself.
- Alpha-only EXRs hold the alpha as it is in every output space (it is
  coverage, not encoded colour; it used to be sRGB-decoded).
- Tests: a half-opacity layer over a coloured background (premultiplied and
  RGB-over-background paths, golden hashes taken before the change), Rec.
  2100 PQ and HLG EXR outputs, raw alpha, and a visible skip of the GPU test
  without an adapter.
- docs/architecture.md: the Working Gamma curve for unmanaged projects, and
  no HDR step for unmanaged projects or PQ / HLG outputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:57:53 +01:00
vangel76andClaude Opus 5.5 51d7e530a0 Follow-up to #2911: B44 at float is labelled uncompressed, half holds over-range samples
B44 and B44A pack only half floats, so at 32-bit float the menu and the
render log say the samples stay uncompressed. Half-float frames hold samples
above 65 504 at that limit instead of writing infinity. The EXR parameters
move into their own helper; exr_out is public for other writers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:52:06 +01:00
vangel76andClaude Opus 5.5 684e05859f EXR export: no compand / tone mapping, and the output space's own curve
OpenEXR renders (Render Queue sequences and Save Frame As ProEXR) received
the frame after Project Settings > HDR, so Compand and Tone Map (the default
of every OCIO project) compressed highlights and changed values below 1.0
before the writer decoded them, always with the sRGB curve.

- RenderOpts::scene_output: the top-level comp converts to the output space
  without the HDR step (CPU and GPU walks, via Renderer::output_pipe).
- Pipe::output_linear: the writers decode with the output space's curve
  (the Working Gamma's when unmanaged) instead of a fixed sRGB curve.
- Render Queue EXR jobs and ProEXR set the flag; PSD layers, 8-bit stills,
  movies and the viewer keep the HDR step. The flag is part of the viewer
  frame and disk cache keys (opts_hash).

Golden hashes, taken before the change, pin PNG/TIFF/Targa bytes for
8/16/32 bpc x Clip/Compand/ToneMap, Clip-mode EXR samples with an sRGB
output, and the layered PSD bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:47:24 +01:00
vangel76andClaude Opus 5.5 b5fe54c08b Render Queue OpenEXR: choose compression and half or 32-bit float (#2911)
OpenEXR sequence output modules get two settings, exrCompression (none,
rle, zips, zip, piz, pxr24, b44, b44a, dwaa, dwab; scan lines in each
method's own block height) and exrBitDepth (half or float). The defaults
are ZIP and 32-bit float, so existing projects render the same bytes as
before. They go through the project file, Output Module templates, the
renderQueue commands and renderQueue.formats, the CLI (--exr-compression,
--exr-depth), the Output Module menu (lossy methods are marked) and the
render log. Half rounds each sample to the nearest half float.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:37:07 +01:00
JonRC 0b59d2b8c4 Follow-up to #2907: say that a uid wins over an index for animator
animator_uid matches a uid before an index, so an animator whose uid
equals an index is picked over the one at that position. The four
commands that take `animator` (layer.addTextAnimatorProperty,
layer.addTextSelector, text.addSelector, text.animatorFontAxes) now say
so; text.addSelector also said only `uid|index` before. The test gives a
second animator uid 1 and checks that `animator: 1` picks it.
2026-11-02 21:14:22 -03:00
JonRC 5643f39769 Fix #2907: say that text animator indexes count from 1
layer.addTextAnimatorProperty, layer.addTextSelector and
text.animatorFontAxes take `animator: uid|index`, counted from 1 as the
code and `#n` references do, but the parameters didn't say so and
`animator: 0` failed with only "no animator 0". The parameters now say
1-based, and the error says what the value can be.
2026-11-02 20:32:48 -03:00
Bar BorerandClaude Opus 5.5 f20dd38f94 Puppet: a click on deformed-away space creates nothing (#2865)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:39:17 +02:00
Bar BorerandClaude Opus 5.5 39119ff9ce Viewer: dragged layers snap to other layers' midpoints and center lines (#2855)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:36:57 +02:00
JonRC ace329d126 Roto Brush: a Freeze job reads the frozen cache once (#2833)
frame_matte looked up the frozen mattes for every frame, and that hashes
the whole serialized cache and recomputes its keys, so freezing a long
span again grew with frames times cache size. The Freeze job now
resolves the applicable cache once (applying_frozen) and passes it to
frame_matte.
2026-11-02 12:32:25 -03:00
JonRC a2f6cd3ed5 3D Camera Tracker: a partial solve is not reported as solved (#2822)
camera.solveStatus said `solved: true` for any stored solve, but the solver
keeps the frames it cannot resect as unsolved and interpolates the camera
there. `solved` now needs every frame, the same check camera.exportBlender
makes, and `partial` marks a solve with gaps, next to `solvedFrames`.
2026-11-02 09:15:05 -03:00
Brandon Thomas a658ec736a Merge PR #1266 2026-11-01 21:50:53 -05:00
tylerjenningswandChewybone-sudo 3ab2167e47 Auto-Trace: traced outlines map back into layer space (#2764)
Co-authored-by: Chewybone-sudo <347238078+Chewybone-sudo@users.noreply.github.com>
2026-11-01 09:05:56 -05:00
tylerjenningswandChewybone-sudo 49774ca5f9 Auto-Trace: test that traced outlines map back into layer space (#2764)
Co-authored-by: Chewybone-sudo <347238078+Chewybone-sudo@users.noreply.github.com>
2026-11-01 09:02:42 -05:00