1644 Commits
Author SHA1 Message Date
FredApps 98c08c791e Merge remote-tracking branch 'origin/main' into pr 2026-11-05 14:50:07 +01:00
Zinadin ZidanandClaude Opus 5.5 0c7cf039b9 Timeline: a bin click is pressed and released on the bin, while the bin has the pointer (#1826)
A press on the ruler right beside the marker bin, released a few points
away on the bin, is still a click, and it counted as one on the bin; two
of those added a marker. A click now counts as the bin's only when both
its press and its release are on the bin, in a frame when the bin is
enabled and has the pointer (not a popup above it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:48:34 +06:00
Zinadin ZidanandClaude Opus 5.5 133dcddf4d Timeline: the marker bin pairs its own clicks, in the order they come (#1826)
The bin counted a double-click from egui's flag for the whole frame and
the number of releases over it, so three clicks in one frame, on the bin,
beside it and on it again, still added a marker. It now walks the
frame's pointer events itself, by egui's rules for a click and a
double-click, and adds one only for two clicks in a row that are both on
the bin. It also counts while the bin is dragged or its drag has just
stopped, not only while the pointer is over it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:26:39 +06:00
Zinadin ZidanandClaude Opus 5.5 5e96b2a385 Timeline: the marker bin's double-click needs both clicks on the bin (#1826)
egui takes any click within a few points of the last one for the first
click of a double-click, so a click on the ruler right beside the bin and
then one on the bin added a marker. The bin now remembers when it was
last clicked and adds one only when the first click was on it too (or
both came in one frame over it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:05:43 +06:00
Zinadin ZidanandClaude Opus 5.5 5cb2658e38 Timeline: a marker dragged out of the bin and dropped within one frame is added again (#1826)
Sensing clicks on the marker bin as well as drags held the drag back
until the pointer had moved, so a drag out that was released in that
same frame (a fast drag, or a slow frame) added nothing. The bin senses
drags only again, so its drag starts the moment it is pressed, as
before #1826, and the double-click is read from the pointer over it.

The double-click test now also gives the second press and its release
frames of their own, with the bin dragged in between.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:47:44 +06: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
30d6c6e1d3 Frames cache: edits keep the frames they can't reach (#2795)
* frames: keep cached frames an edit can't reach

Nudging, trimming or splitting a layer re-rendered the whole comp. The RAM
preview now carries every frame whose stack of active layers is unchanged
to the new comp identity; only the segments an edit touches re-render.

* disk cache: never drop frame writes; save frames kept after edits

Frames get their own write lane (bounded by bytes, written before layer
buffers), so a burst of layer writes no longer drops frames and leaves gaps
in the blue bar after reopening. Frames an edit kept in RAM are written to
disk under the comp's new content key when idle.

* frames: music and hidden layers keep the caches

Layers that can't change pixels (audio-only footage, AV layers with Video
off, when nothing reads them and there are no expressions) no longer count
for the RAM preview's dirty segments or the disk cache's comp and footage
keys. Adding, trimming or moving music keeps both caches.
RENDER_VERSION bumps to effectcraft-render-3 for the new key formula.

* ui-egui: background caching benchmark

cache_bench fills a comp's RAM preview the way idle caching does and
prints frames/s with decoder, footage-cache and disk-cache counters.

* frames: soloed music and hidden layers aren't silent

Solo hides every other layer, so soloing a music track or a layer with
Video off changes the picture. silent_layers no longer drops soloed
layers, so the RAM preview and the disk cache see Solo toggles.

* disk cache: audio files a layer reads stay in the layer salt

Audio-only footage left the disk layer salt so importing music keeps the
keys, but Audio Spectrum and Audio Waveform draw from an audio layer.
Footage another layer reads (or any, when a read layer is a precomp)
counts again, so overwriting that file doesn't reuse stale buffers.

* disk cache: retry frame writes the queue refused

put_frame reports whether the frame is on disk or queued. persist_missing
forgets the key of a refused write, so a full frame lane no longer leaves
RAM frames that are never written.

* ui-egui: cache_bench measures idle caching; checks --comp and --slots

Frames go through request_background with one slot per frame thread, as
Cache Frames When Idle does. An unknown --comp is an error (the active
comp is used only without --comp), --slots is at least 1, and the disk
report flushes queued writes instead of sleeping.

* disk cache: RENDER_VERSION effectcraft-render-6

This branch changes the cache-key formula (silent layers, Solo, read
audio in the footage salt), and upstream #2759 already took render-5
for the clip-clock change, so entries from either must not be reused.

* frames: rewrite RAM frames the disk cache evicted

persist_missing kept every disk key it ever queued, so a frame the disk
cache evicted later was never written back. The disk cache now tracks
frame writes still queued or in progress (frame_pending), and
persist_missing only remembers keys while it converts them.

* frames: idle passes continue while RAM frames wait for disk

persist_missing reports whether there may be more to write (a content key
being computed, frames being converted). Cache Frames When Idle then
schedules another pass, so a refused write is retried without waiting for
a UI event, and the passes stop once every frame is on disk or queued.

* frames: rewrite an evicted frame once, not on every idle pass

A disk cache smaller than the RAM preview, or a frame too large for it,
made idle passes write, evict and rewrite the same frames every 500 ms.
Each disk key is now written at most twice under one disk limit (the
write and one rewrite after an eviction); a new limit resets the count.
The persist test waits for the conversion to forget its keys before
checking that no work is left.

* frames: the work area doesn't change pixels

Setting the work area (B, N) cleared every cached frame, in RAM and on
disk: it was compared with the comp's pixel settings and hashed into the
disk key. It is a preview and render range only, like the Resolution, so
it no longer counts.

* frames: audio edits keep frames unless something draws the audio

Changing a clip's Audio Levels, its audio effects or its Audio switch
cleared the clip's cached frames, and the comp's disk cache, although
nothing drew that audio. The audio now counts only for the layers whose
audio is heard (Project::heard_layers): those a layer picks in an effect
(an Audio Waveform or Spectrum, even on itself), the layers of a picked
precomp, and every layer while an expression (which can read Audio
Levels) exists. Music in a precomp that a waveform in the outer comp
picks is no longer treated as silent either.

* frames: an expression only reaches the frames of its own layer

One expression anywhere in the project made every edit clear the
comp's whole cache, and so did editing a layer another layer reads
(its parent, matte, or a layer it picks). Expressions can read
anything, but they only draw in their own layer: an edit now re-renders
the frames where it changed something, plus those of the layers with an
expression and of the layers that read a reached layer. With
expressions, an edit elsewhere (another comp, an item's name, a marker,
music) reaches only the expression layers' frames.

* frames: an edit inside a precomp re-renders only where the precomp shows it

Editing anything inside a precomp cleared every cached frame of the
comps that nest it, even a music level in it. Project::dirty_spans now
follows precomps: it takes the spans their comp changed in, shifts and
scales them by the precomp layer's start and stretch (its whole span
under a time remap or Responsive Design timing), and keeps the outer
comp's other frames. Layers that read a changed precomp (a matte, a
waveform) re-render where they are active.

* disk cache: keep the frames an edit can't reach

The disk cache keys frames on a hash of the whole comp, so every edit
lost the frames that were only on disk (the blue bar), even a rename;
only those still in RAM were written again under the new key. When an
edit keeps RAM frames, it now also queues a disk carry: on the frame
pool, each kept frame of every disk series in use for that comp is hard
linked (or copied) to its new key with DiskCache::link_frame, oldest
edit first. The old entries stay, so undo finds them too.

* frames: a keyframe edit re-renders only around the keys it changed

Changing one keyframe (its value, interpolation or ease, in the
Timeline or the Graph Editor) re-rendered the layer's whole length, so
a key on a full-length layer cleared the whole cache. A layer that
differs only in keyframes of its Transform properties now re-renders
from two keys before the first changed key to two after the last
(auto-Bezier and continuous tangents reach the neighbouring segments),
to its in or out point past the first or last key. Static values,
expressions, roving keys, time remap and time effects still reach the
whole layer; effect keyframes do too for now (simulations and time
effects read other frames).

* frames: music volume keeps frames while no expression reads audio

Any expression in the project (a wiggle on another layer, even a
disabled one) made every layer's audio count as read, so changing a
music layer's Audio Levels re-rendered every expression layer and
changed the comp's disk key. Audio now counts for expressions only when
one may read it (Heard::audio_exprs): its text names audio or walks
properties by index. An audio-only change nothing reads no longer counts
as a change elsewhere in dirty_spans either.

* frames: a keyframe edit with few keys or beside a self-contained expression keeps the other frames

Changing a Scale key of a layer with two or three keys re-rendered the
layer's whole length: with fewer than two keys before (or after) the
changed one, the span fell back to the in (or out) point instead of the
first (or last) key, past which the value holds. And any layer with an
expression re-rendered on every edit, so a wiggle on a full-length layer
cleared the whole cache. An expression layer is now reached by edits
elsewhere only when its text may read outside its layer (it names a
comp, layer, footage, the project or its source); its parent and the
layers it picks are still read through referenced_layers.

* viewer: the caching chip stays up between uncached frames, with a spinner

The "Caching frames…" chip showed only while playback waited for a
frame, so during caching it flickered on and off with every frame that
arrived. It now stays up 0.6 s after the last wait (and hides at once
when playback stops). It is also smaller and quieter: a spinner, then
"Caching frames" without the ellipsis in dimmed smaller text, on a
fainter chip sized to its text.

* preview: the Preview panel's cache bar counts the preview's Resolution

With a preview Resolution set (Half, Third…), the frames a preview
plays are cached at that scale, but the Preview panel's bar counted the
series the viewer shows. When playback stopped, the viewer went back to
the comp's Resolution and the bar dropped from full to the frames cached
there. It now counts the preview's own series (Auto still follows the
viewer). The viewer and the Timeline's cache bar are unchanged.

* Contributors: add nazar-gavrylyk's name

* frames: an expression reading its own layer's markers or audio re-renders that layer

With expressions in the project, a layer's markers count when comparing it, and its audio counts
when an expression may read audio, so a marker or volume edit re-renders the edited layer over
its own span. Comp markers still count only through layers whose expressions name thisComp.

* frames: a keyframe edit on a layer with an expression re-renders the layer

An expression on the keyed layer can read the keyed property at other times (Rotation reading
Position with valueAtTime), so the frames past the changed keys may change too.

* cache_bench: read decoder counters through effectcraft-media

Only crates/media, crates/export and crates/interchange may depend on the FilmCraft crates.

* cache_bench: clips end where the next one starts

* frames: write-back uses the worker's disk keys while a Roto Brush model is active

persist_missing keyed disk frames on FrameKey::opts, which also carries the Roto Brush model
stamp; the worker and disk_frames key on the render opts' hash alone. With a model active every
RAM frame looked missing, was written again under keys nobody reads, and recorded a second disk
series.

* frames: an edit two precomps down reaches the outer comp

A precomp layer whose comp Arc didn't change was skipped, but the comps nested in it may have
(an edit in Inner leaves Mid's Arc as it was), so Outer kept every frame. Every nested comp is
walked now, each once per edit.

* cache_bench: stop when the disk cache can't be cleared

* frames: one disk carry job at a time

carry_disk runs on every timeline paint and idle pass, and spawned a pool job whenever carries
were queued; during a drag the extra jobs waited on the carry lock and held frame-pool threads
from the viewer. A scheduled flag keeps one job, which drains the queue in order until it's
empty.

* frames: a panicking disk carry is skipped, not left holding the job

The carry job clears carry_scheduled only on its normal exit; a panic in a carry would have left
it set for the session, so no later carry ran. Each carry is caught like the content key's
hashing, so the job always reaches its exit.

* frames: an expression reading its own in point or index re-renders its layer

A trim or rename kept the layer's frames outside the cut when the layer
drew the same apart from its span, but an expression on it can read its
own inPoint, outPoint or name (a fade from inPoint), so every frame of
the layer changes. Such a layer is no longer treated as only respanned.

An expression reading `index` reads the layers above it too: a layer
added or removed there shifts it, so `index` now counts as reading
outside the layer.

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

* frames: an edit of a comp reaches the expressions of the precomps it nests

An expression in a precomp can read the comp that nests it
(`comp("Main").layer("Controls")`), but the walk into nested comps only
counted changes outside the edited top comp. Editing a hidden Controls
layer there (or any layer, when the precomp is time-shifted or reads it
at other times) kept every precomp frame in RAM and linked them all on
disk under the new key. Nested comps now see an edit of the comp above
them as an edit elsewhere.

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

* frames: an edit in a precomp under a time effect reaches its whole layer

A precomp layer running Echo, Posterize Time, Time Displacement or
another effect that reads the layer at other times shows a nested change
far from where it maps, but only the mapped span (a nested frame either
side) re-rendered. Such a layer now gives its whole span, like a time
remap. `reads_other_frames` lists those effects (every `ec.time.` and
`ec.sim.` effect and the others that call `self_at`); keyframe spans use
it too.

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

* frames: a keyframe edit beside muted keys re-renders the whole layer

Muted keys are skipped when a property is evaluated, so the keys that
play around an edited key can be any distance away, but the span counted
two stored keys either side. A segment from an earlier playing key kept
its stale frames. Muted keys now give the layer's whole span, like
roving keys.

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

* frames: one echo in the precomp time-effect test (three saturate alike), rustfmt

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

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:46:39 +01:00
Zinadin ZidanandClaude Opus 5.5 958e9dc2f8 Timeline: a double-click on the composition marker bin adds a marker at the current time (#1826)
The bin at the right end of the time ruler only answered a drag, so a
marker had to be dragged out to where the current time was. As in After
Effects, a double-click on it now adds a composition marker at the current
time, one undo step. A single click still adds nothing, and dragging a
marker out of the bin works as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:01:19 +06: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
FredAppsandClaude Opus 5.5 bc3b074ad9 About: credits column widths come from a tested fit_widths (#3039)
* About: credits tables with resizable columns that fit without side-scrolling

Contributors and Models were egui Grids inside a two-way scroll area, so the
table opened wider than the dialog and scrolled sideways, and Company and
Model sat almost touching. Both are now egui_extras tables: each column
opens at its header and widest cell, the spare width goes to the text
columns (Name; Company and Model), and every column edge can be dragged.
Widening a column grows the table like a spreadsheet, and only then does it
scroll sideways; the header stays put while the rows scroll. Counts and
dates are right-aligned and every cell is vertically centred.

The sorted column stays highlighted, with its direction on the toolbar's
arrow button, so headers no longer reserve room for an arrow. The About
dialog is 820 points wide (modals still shrink to the window) so every
column fits at its natural width. egui_extras comes from the pinned egui
fork without default features; it adds enum-map.

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

* About: credits column widths come from a tested fit_widths

The opening widths of the credits tables are a pure function now, with unit
tests that a fitted table fills its room exactly (no side-scrolling, no
gap), that spare width goes to the growing columns, and that too little room
shrinks only those.

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

* Let every credits column be dragged, and refit changed tables

The last column was egui's remainder column, which has no drag handle, so
it could only be resized from the edge before it. It is now an ordinary
column with its own handle.

A table keeps its column widths, so switching to a longer name left the
Name column at its old width and clipped the text. The table now goes
back to the opening fit whenever that fit changes: a longer widest cell
or a resized window. Dragged widths are dropped at that point.

fit_widths could overflow the room when one column hit the minimum,
because the other columns did not take up its share. Columns now give
way in proportion to their width above the minimum.

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

* Refit credits tables on what they measure, not the fitted widths

Comparing the fitted widths missed the case it was meant for. Name is the
only growing column, so a longer name takes its extra width from the
spare room and the fit comes out the same: a dragged Name width stayed,
clipping the longer name.

It could also drop dragged widths by accident. The fit comes from the
available width inside the scroll area, which scrolling can nudge, so
scrolling a widened table could reset it.

The table now compares the natural widths and the clip rect's width,
which scrolling does not move.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 04:33:12 +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
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
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
howdeployandFredApps c4f386a22f Preview: debounce GPU memory pressure and recover the frame budget (#3025)
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 23:00:01 +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
Brandon ThomasandClaude Opus 5.5 d16c720e59 Follow-up to #2981: a missing Chinese effect row draws English instead of failing the build, the font probe sees effect and category characters, coverage totals recounted
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:52:40 -05:00
Brandon Thomas 3ae9c8dbc2 Merge PR #2981 2026-11-03 09:41:24 -05:00
SHXJ b77db389af #2037: complete the Traditional Chinese effect-name catalog
Add 308 Traditional Chinese effect names and 21 category labels, written from the English source with Taiwan creative-software terms. Update the completeness tests to require the zh-hant table and recount the localization docs.
2026-11-03 22:34:55 +08: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 38e33992bd Merge PR #2892 2026-11-03 09:08:08 -05:00
Brandon Thomas f5e81cd1f9 Merge PR #2957 2026-11-03 09:08:08 -05:00
Brandon Thomas 764343e08c Merge PR #2888 2026-11-03 09:08:08 -05:00
Brandon Thomas 369608c826 Merge PR #2978 2026-11-03 09:04:56 -05:00
Brandon Thomas 5dc64bb227 Merge PR #2885 2026-11-03 09:04:55 -05:00
Brandon Thomas e52727fa91 Merge PR #2887 2026-11-03 09:04:55 -05:00
Brandon Thomas 3072f009bf Merge PR #2891 2026-11-03 09:04:54 -05:00
Brandon Thomas 21edd52ae6 Merge PR #2933 2026-11-03 09:04:53 -05:00
Brandon Thomas 8a82962335 Merge PR #2934 2026-11-03 09:04:53 -05:00
Brandon Thomas b97a8f796a Merge PR #2935 2026-11-03 09:04:52 -05:00
Brandon Thomas ff204b37fa Merge PR #2962 2026-11-03 09:04:51 -05:00
zitann2020andDeepSeek V4.1 Flash e8e3262a86 i18n(ja): the work area is ワークエリア in the panel too
The panel catalog said 作業領域 for Work Area while the four menu rows say ワークエリア. The installed After Effects 2026 settles it: Edit > Preferences > Previews reads キャッシュ範囲: ワークエリアと現在の時間.

The row draws the Content-Aware Fill panel range menu and the Time Span form, so a reader met both words for
one thing depending on where they looked. One row, no other change.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
2026-11-03 22:14:57 +09:00
Matej Kozlovský 3eb7ac7c5e Hold the probe build in the remaining crypto-pick reading tests
The "while reading" sections of the waiting-click, in-order and undo/redo
tests now hold the build with the Held wrapper until their asserts are
done, instead of relying on a 4K plate being slow to read.

The undo tests use the shared helpers from ui_crypto_pick.rs, so they now
go through crypto_exr with k = 1 rather than a copy without it.
2026-11-03 12:47:53 +01:00
Bar BorerandClaude Opus 5.5 220d85f4fc Effect Controls: Shift-click selects a range of effects
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:53:55 +02:00
Matej Kozlovský 310c6da745 Cryptomatte eyedropper: hold the probe in the queued-clicks undo test (#473)
clicks_queued_while_reading_are_one_step_each_and_undo_in_order relied on
a blurred 4K plate being slow to read, and failed when the IDs were read
while the clicks were still being pressed. The probe's ID read is now held
(the Held footage from ui_crypto_pick) until the clicks are checked, and
the test asserts that the build reached that read and that nothing was
made while it was held.
2026-11-03 09:36:23 +01:00
Matej Kozlovský f8463fcf7e Cryptomatte eyedropper: Esc that closes a dialog keeps it armed (#473)
The Esc that closed New Solid or Composition Settings also disarmed the
eyedropper. An open dialog now takes Esc like a focused text field or an
open popup, so that Esc only closes the dialog and the next one disarms.
long_sequence asserts both instead of logging a finding.
2026-11-03 08:56:53 +01:00
JonRC 940a57e865 3D Camera Tracker: Effect Controls say how many frames a partial solve solved (#2959)
A solve with unsolved frames drew the same block as a full one, so it
looked complete. It now gets one more line in the warning colour,
"Partial solve: N of M frames solved", under the status line, and the
editor grows by that line only then. The line is an automation element,
so agents can read it too.
2026-11-03 04:08:39 -03:00
Matej Kozlovský 7cea1625f6 Cryptomatte eyedropper: arm it from the Selection in Effect Controls (#473)
An eyedropper next to the Cryptomatte effect's Selection arms the viewer pick, and the status
line says how to use it. It stays armed until Esc or the button (Esc in a text field or a menu
only ends that); disarming, New, Open and Revert end it and let go of the session's probe.

The app runs the pick's bookkeeping at the start and end of each pass, the viewer hands the
pointer to the pick while it is armed and opens its context menu for a right-click the pick
forwarded, and a Selection cut off by a narrow Effect Controls panel shows whole on hover.

UI tests: ui_crypto_pick (arming, hover, clicks, modifiers, drags, waiting clicks, status,
highlight) and ui_crypto_pick_undo (one undo step per pick, the probe kept over undo).
2026-11-03 07:37:53 +01:00
Matej Kozlovský 6b0f72e4be Cryptomatte eyedropper: the viewer pick (#473)
panels::crypto_pick is the eyedropper's viewer side; it is wired in by the next commit. Armed,
the hover names the ID under the cursor and highlights its coverage, a click replaces the
Selection with it, Shift adds, Alt or Ctrl (Option or Cmd on a Mac) removes. Each pick is one
undo step, "Pick Cryptomatte".

The probe the hover needs is built off the UI thread, one at a time for the app
(EffectcraftApp::crypto_probe_busy, re-arming included), not while playing and not until a
scrub has settled for 100 ms; meanwhile the chip says so. Session::crypto_probe_ready asks for
readiness without changing anything; a job is asked for only when it starts.

Clicks made meanwhile are queued, own the build and are made in order at the end of the pass,
after its keys and edits. A click is the frame, point, resolution and project revision its
press saw. If any of them changed before it is made (an edit, Ctrl+Z, a nudge, a frame step),
it and every click waiting with it are dropped and the status says "Pick not made". A pick's
own change of the revision doesn't count as an edit for a click still held. Losing the window
focus drops a held press. A click that takes the focus from the Selection field waits for the
field to commit and is made on top of it.

The overlay takes drags too: a click that drifts picks at its press, and one dragged further
than 24 points picks nothing ("Drag cancelled the pick"). Presses and releases are read from
the pass's events in order rather than from egui's end-of-pass click state, since a slow pass
can hold a whole click. Space, the Hand and the Zoom tool keep their clicks, the middle button
pans, and a right-click is passed on under viewer::FORWARDED_SECONDARY for the viewer to open
its context menu (the viewer side is in the next commit). Errors show in the chip in red,
without the command's prefix, wrapped to the viewer's width.

The hovered ID's coverage (Session::crypto_coverage) is drawn as a texture through the layer's
effect-space transform, rebuilt only when the hovered ID, frame or revision changes, and
exposed to the test harness as viewer.cryptoPick.highlight.

Supporting changes:
- EffectcraftApp::say sets the status line and restarts its 5 s; each status message now gets
  its own 5 s, also when the same text is shown again.
- Points snap to the centre of the shown frame's pixel at the exact 1/n scale the viewer
  renders (frames::render_scale; the frame key keeps 0.333 for Third, which named the next
  row's ID). Cache Frames When Idle renders at that exact scale too, so idle-cached frames are
  on the grid the pick reads.
2026-11-03 07:37:53 +01:00
Bar BorerandClaude Opus 5.5 8ff461b35b Panels: a window widened under the open panel menu gives it its width back (#2864)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 06:17:54 +02:00
Bar BorerandClaude Opus 5.5 dc28883993 Tests: build the viewer tests' EvalCtx from the session's active comp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 05:35:02 +02:00
Bar BorerandClaude Opus 5.5 6c49ef819d Tests: trim the key box tests to what #2857 changes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 05:16:27 +02: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
Bar BorerandClaude Opus 5.5 325c029d06 Viewer: the Rotation tool measures its angle on screen about the anchor (#2870)
One comp-space path: the pivot is the anchor where it shows and a flipped
parent reverses the turn, so the separate parent-space frame and the moved
parent_to_comp go. Under uneven or skewed parents the layer's edge tracks the
pointer slightly less exactly. The empty-layer marker goes back to main's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 04:59:16 +02:00
Bar BorerandClaude Opus 5.5 72d0b3aa08 Viewer, Timeline: a middle-drag pans and moves no layer (#2854)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 04:57:19 +02:00
Bar BorerandClaude Opus 5.5 f1767c3349 Panels: the gutter keeps rows clear of the scroll bar (#2867)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 04:57:14 +02:00
Bar BorerandClaude Opus 5.5 fe6031a36f Panels: the panel menu stays as wide as its contents (#2864)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 04:57:08 +02:00
Bar BorerandClaude Opus 5.5 5593c6e8b3 Tests: give the key box test's comp a frame rate, so its keys stay on whole seconds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 03:57:05 +02:00
Bar BorerandClaude Opus 5.5 9431089493 Puppet: decide a marquee from where the button went down (#2865)
The drag branch asked what was under the pointer when the drag was recognised, by which
time a quick drag has already reached the art, so no gesture started at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 03:37:31 +02:00
ljkx f6fd6de3bb Follow-up to #2841: Composition Settings settles after a width change
- `dialog.comp.view` is the scroll area's viewport (its clip rect). Cut
  by the content's max rect, it lost the scrolled amount at the bottom.
- When the dialog's width changes, the pass laid out at the old width is
  discarded and a repaint requested.
- The measured width keeps room for a vertical scroll bar. Without it,
  switching the language with the dialog open left the two scroll bars
  trading places every frame: the UI never settled and OK was clipped
  every other frame.
- `content_width` becomes `dialog_width`, which returns the dialog's
  width, so the 600-point rule is in one place.

Tests: the view keeps its place when the dialog scrolls, and switching
to French and back with the dialog open settles with OK inside it.
2026-11-03 01:03:48 +01:00