Commit Graph
4221 Commits
Author SHA1 Message Date
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
Brandon Thomas 5720043d32 Merge PR #2922 2026-11-03 09:04:45 -05:00
Brandon Thomas 2fb7003643 Merge PR #2924 2026-11-03 09:04:45 -05:00
Brandon Thomas 7758a6b99d Merge PR #2881 2026-11-03 09:04:45 -05:00
Brandon Thomas 7ae139ebf0 Merge PR #2931 2026-11-03 09:04:45 -05:00
Brandon Thomas f50985f9e6 Merge PR #2960 2026-11-03 09:04:45 -05:00
Brandon Thomas e175a272c7 Merge PR #2824 2026-11-03 09:04:45 -05:00
Claude a5e0da657e Pre-compose: layers left behind drop links to moved layers, default names are numbered
Unselected layers parented to, or track-matted by, a layer that moved into the
new composition kept ids that no longer exist in the outer comp. The command's
default name was always "Pre-comp 1"; it now picks the lowest unused number.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
2026-11-03 13:58:54 +00:00
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
Dylan M. Taylor d025b59e6d OCIO config: a mismatched closing bracket ends a flow collection, and multiline flows are assembled in linear time (#2939) 2026-11-03 06:03:05 -05: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