* 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>
Animator Opacity stacked by multiplying, so once one animator set 0 %
no animator below it could raise it again. Each animator now blends the
opacity left by the ones above it toward its own value. A single Opacity
animator gives the same result as before.
Fixes#2838.
A project file can carry footage with a frame rate of den 0; frame_position
divided by zero while rendering it. Treat non-positive rates as having no
frame to land between.
#2653 (#2618) changed what an adjustment layer in a mode other than Normal
renders, but its cache keys are unchanged, so frames cached before it keep
showing the Normal result. Bumping the render version keeps old entries
from being reused.
An adjustment layer replaced the comp below with its effects' result whatever its blend
mode. As in After Effects, a mode other than Normal now blends the adjusted comp onto the
comp below in that mode, inside the footprint, track matte and opacity (an adjustment
layer with no effects in Screen screens the comp with itself). CPU and GPU paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Noise, paint, the shutter, and the comp scene were still reaching the footprint key, so a finishing adjustment re-uploaded a full frame every frame. The key now uses EffectScope::Footprint and ignores that stack, while a moving mask still re-keys it.
Add Per-Corner Roundness to Rectangle Path: a checkbox (off by default) and four Pixels properties (top-left, top-right, bottom-right, bottom-left). New rectangles initialise the four values from their Roundness, so ticking the box keeps the shape; the radii are animatable and expression-driven.\n\nrect_radii() builds the path clockwise from the top-left with kappa quarter-circle arcs. Overlapping radii scale down like CSS border-radius; negative and non-finite radii are square; rect() delegates to it, so uniform roundness clamps to half the shorter side as before.\n\nLottie rc has one r: four equal static radii export as that value; mixed or animated radii export roundness 0 with a warning. Import puts r into the four per-corner values so ticking the box on an imported rectangle keeps the shape.\n\nTests: path geometry and clamping, rendered per-corner pixels, Lottie equal/mixed/checkbox-off export.
#2410's dependency walk for a soloed 3D model was written before
reads_other_times took the effect count; it asks about all effects (None),
as the other walks do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text strokes were always drawn with round joins. The document gets a
Line Join (`lineJoin: miter|round|bevel` in layer.setText / newText),
Miter for new text as in After Effects, Round for documents saved before
it so their look is kept. The 2D and Advanced 3D (extruded) text strokes
use it, expressions' style copies carry it, and the Character panel has
Miter / Round / Bevel buttons under the stroke width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- viewer.rs: the undecodable-footage notice moved into phase_scene on main; it now
collects its layers in footage_problems, which keeps the proxy and collapsed-precomp
rules of #1701.
- project.rs: main's row layout, with the problem marker going by item_problem.
- movie_bytes.rs: the H.264 pin is FilmCraft 09aaf9b's bytes plus main's 19-byte
BT.709 colr box (104_322 + 19 = 104_341).
render/footage.rs imports ItemKind now that main names its imports.
Carry property, Font and Media Replacement overrides into the collapsed nested EvalCtx so nested layers render beyond the precomp frame instead of falling back to a clipped, flattened image. Key the layer cache on instance overrides to keep distinct collapsed instances isolated.