Commit Graph
4235 Commits
Author SHA1 Message Date
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
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
Dylan M. Taylor e1de87e8c3 Pass the Linux signing key to the release workflow (#3074) 2026-11-04 17:45:18 -05: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
Dylan M. Taylor 58ca9c1c1b OCIO: refuse a .csp LUT whose header claims an oversized table before reserving it (#2938) 2026-11-03 20:23:08 -05: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
025c038af5 GPU: build effect kernels on first use, each family from a module of its own (fixes #1590) (#1705)
* #1590: build effect kernels on first use instead of with the GPU context

GpuContext::new built every compute pipeline up front: the compositor's own
kernels and ~208 effect kernels. In Chrome on Windows that kept the GPU
process busy past its watchdog, so the device was lost ~60 s after every load
(#1590); on DirectX 12 with FXC the window took over a minute to appear (#613).

The compositor's kernels are still built up front and required. Effect kernels
are built the first time a dispatch asks for one, inside the same error scopes,
so one a compiler rejects still renders its effects on the CPU. unavailable_kernels
lists the failures so far; build_all_kernels builds the rest.

RX 6900 XT, Windows: Vulkan context 0.27 s, all effect kernels 0.9 s, GPU tests
77 s -> 26 s; on #1571's DX12/FXC, context ~66 s -> 1.96 s, tests 1306 s -> 252 s,
same results.

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

* #1590: compile each kernel family as a shader module of its own

Building effect kernels on first use was not enough in Chrome: the page and
its frame workers each create a device, every one compiled its required
kernels against the single ~460K-character module, and Dawn compiles the
module again for each pipeline, so the GPU process still passed its watchdog
and the device was lost ~64 s after load (RX 6900 XT, Chrome 155).

Each kernel family (FAMILIES) is now common.wgsl + kernels.wgsl + its own file
and the files it borrows functions from (at most ~120K characters), built with
the family's first kernel; the compositor's kernels, sky and classic3d use
small modules of their own.

Chrome 155 on Windows / RX 6900 XT, release web build, default flags: main
loses the device at ~64 s (60.7 s with ?nogpuworkers); with this, two 100 s
runs keep it, and both frame workers get the GPU. Native Vulkan tests
unchanged (212 passed, the 3 failures that main has, #1605).

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

---------

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

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

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

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

Co-authored-by: 00xJ0 <56056764+00xJ0@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 23:56:37 +01:00
8d20cf0c3a GPU.1: Preserve CPU parity in numerical effect kernels (#1490)
Co-authored-by: @skylartaylor <26588250+skylartaylor@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 23:19:29 +01:00
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
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
6df7eabe05 gpu: memory back-pressure for frames in flight (fixes OOM at 3–4 frames in flight) (#1550)
* 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: cap in-flight frame memory on Vulkan and DX12 (#1550)

wgpu destroys a dropped texture or buffer only once the submissions
that used it have completed and the device is maintained. With several
4K frames in flight on an export or effect-heavy render, frames can
record and drop working textures faster than the GPU retires them,
accumulating freed-but-not-destroyed allocations until a 4-8 GB adapter
runs out of memory (#879).

Add a memory gate (MemCap) checked every 8 effect steps or layers:
- Default cap of 3 GiB on discrete GPUs (low enough for 4-8 GB cards
  while leaving headroom above the 1.5 GiB upload cache and 256 MB
  shared pool) and 2 GiB on integrated/software adapters, overridable
  via EFFECTCRAFT_GPU_MEMCAP_MB (0 disables).
- Compare total_allocated_bytes against non-reclaimable bytes (upload
  cache, pool free list, live leases, staging, zeros) with hysteresis
  and a post-wait floor so the gate only drains when a wait can free
  dropped allocations and does not spin when caches or other open
  encoders hold memory over the cap.
- Submit and wait on the current Enc's own SubmissionIndex via a
  dedicated poll_wait helper (serialized by poll_lock) so a timeout
  simply carries on without retiring the context or its readback pool.

* GPU: warn on invalid EFFECTCRAFT_GPU_MEMCAP_MB, register crop sources, require GPU in the mem-cap child test

- memcap: an EFFECTCRAFT_GPU_MEMCAP_MB value that is not a whole number
  of MiB or overflows 64 bits now logs a warning and uses the default
  cap (it used to fall back silently, or wrap on overflow). Unset still
  means the default and 0 still turns the gate off.
- Enc::crop records through Enc::record, so a foreign source lease is
  marked escaped like in copy_into.
- The mem-cap child test starts its GPU through require_gpu, so
  EFFECTCRAFT_REQUIRE_GPU=1 fails when the GPU does not start, and
  prints a skip line (with the adapter name when the backend has no
  allocator report), which the parent passes on.

* 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 memcap: warn once when kept memory alone is over the cap; test the edge frames

A frame whose non-reclaimable memory exceeds the cap never waits when reclaimable
bytes stay below the hysteresis (waiting cannot free the kept set) and now logs
that once. Tests cover a frame over the cap, two frames that fit one at a time, and a
dropped frame giving its memory back.

---------

Co-authored-by: Matej Kozlovský <MatejKozlovsky@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 21:46:16 +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
Dylan M. Taylor 5464a2380e Mask feather: bound the variable feather's distance pass (#2947)
* Mask feather: bound the variable feather's distance pass

Every flattened segment scanned its box grown by the largest feather radius,
so a path with many vertices and a radius spanning the mask revisited every
pixel once per segment. The pixel visits are now counted first; past a
budget of 2^31 the flattened polyline is thinned (every other point, arc
lengths kept) until it fits, and only a degenerate case falls back to the
hard mask.

* clippy manual_is_multiple_of
2026-11-03 12:32:20 -05:00
Dylan M. Taylor b7dee07cf4 Path booleans: sparse winding numbers and bounded merge runs (#2946)
Winding numbers were dense vectors up to each segment's path index, and the
topology keeps one per segment, so memory grew with the square of the path
count. They now store only the nonzero (path, winding) pairs.

tidy_segments re-checked the whole run on every merge attempt, so a long
chain of collinear pieces cost quadratic time. A run now merges at most 64
pieces; longer runs leave a few extra joints on the same line or curve.
2026-11-03 09:57:48 -05: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 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 e96f47f3c2 Merge PR #2819 2026-11-03 09:08:08 -05:00
Brandon Thomas 7a172b04e0 Merge PR #2825 2026-11-03 09:08:08 -05:00
Brandon Thomas a768cdb42f Merge PR #2878 2026-11-03 09:08:08 -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 072db0ca35 Merge PR #2842
# Conflicts:
#	crates/lottie/src/tests.rs
2026-11-03 09:05:06 -05:00
Brandon Thomas 6334d07fe1 Merge PR #2872
# Conflicts:
#	apps/effectcraft-cli/tests/it/cli.rs
2026-11-03 09:05:01 -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
Brandon Thomas 0d640999e6 Merge PR #2812 2026-11-03 09:04:46 -05:00
Brandon Thomas 0a91567425 Merge PR #2834 2026-11-03 09:04:45 -05:00
Brandon Thomas d6b67ac56c Merge PR #2835 2026-11-03 09:04:45 -05:00
Brandon Thomas f13aef1655 Merge PR #2847 2026-11-03 09:04:45 -05:00
Brandon Thomas 2267ea4970 Merge PR #2852 2026-11-03 09:04:45 -05:00
Brandon Thomas 3170c0e34f Merge PR #2905 2026-11-03 09:04:45 -05:00
Brandon Thomas 708de30891 Merge PR #2908 2026-11-03 09:04:45 -05:00
Brandon Thomas 180d0b9d91 Merge PR #2843 2026-11-03 09:04:45 -05:00
Brandon Thomas 0e3d54f982 Merge PR #2909 2026-11-03 09:04:45 -05:00