* 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>
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.
effects-parity.md and gaps.md: the Selection is picked in the viewer. control-protocol.md: the
viewer.cryptoPick element ids. footage.md: a paragraph on the eyedropper.
- A watch-folder render gives the host session's set back after its own
project replaced it on the shared footage source.
- Guard gpu fx_depth::IDS against reads_aux drifting apart.
- Count picture and channel decodes per thread instead of reading the
shared channel cache, so parallel tests can't make the checks flaky.
- The engine test fails when no set was ever sent.
- Docs: Media Replacement items are learned at their first frame.
Refs #474
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pool learned that an effect reads a footage item's channels only at the
first aux() call, so the first frame was decoded twice (picture, then in
full), and the hint was never cleared after the effect was removed.
The session now gives the footage source the whole set of files read by
enabled 3D Channel effects (FootageSource::set_aux_footage) with every
project change (Session::bump: edits, undo, redo, history jumps, rollbacks,
project open), sending it only when it changed. The pool keeps that set and,
as a safety net, what aux() learns; a new set drops what was learned.
Refs #474
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups:
- Files in use are never dropped from the channel cache: planes an effect
holds, the files of the last 4 pictures made of their planes until their
effects ask for the channels, and decodes in progress, which now count
the planes they will allocate (from the header) against the budget. With
4 frames at once over a budget of one file, each file decodes once (test).
- A decode that panics removes its entry instead of leaving it decoding.
- One decode allocates at most 8 GiB of planes (512 MiB on 32-bit and
wasm); a header asking for more is refused with the reason.
- A shared picture whose file has no cached channels (a failed read, a
broken part it doesn't use) is decoded from its own channels.
- Comment why waiting on a decode may block a rayon worker; drop the
subsampling check and test (the exr crate refuses subsampled files).
- Docs: the cache is a soft bound; a single-part file still decompresses
every channel; the first frame and the image crate's route decode twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The channel cache decodes a file once however many callers ask for it at
the same time (the others wait for that decode), and is bounded by the
bytes of its planes (1 GiB, least recently used out first, a file counting
as used when its decode finishes, the newest file always kept) instead of
four files of any size.
- Once an effect has read a footage item's channels, its pictures are made
of the cached planes, also for files with an unnamed RGB layer (bit for
bit what the image crate shows, which still decodes the files the planes
can't stand in for); footage alone decodes only its picture's channels
(a multi-layer file's were all decoded and cached for every frame).
- Channels are decoded straight into their display-window planes, and the
picture is converted from the planes without two interleaved copies.
Measured on a 30-frame 2560x1440 sequence of 32 channels (DWAA), one frame
at a time: footage alone 0.185 -> 0.073 s per frame and 4.4 -> 1.8 GB peak;
with an unnamed RGB layer plus Cryptomatte 0.244 -> 0.159 s; multi-part
plus EXtractoR 0.194 -> 0.127 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Rename RenderOpts::scene_output to skip_hdr_step (Pipe::without_hdr_step):
the consumer decodes with Pipe::output_linear or applies the step itself.
- Alpha-only EXRs hold the alpha as it is in every output space (it is
coverage, not encoded colour; it used to be sRGB-decoded).
- Tests: a half-opacity layer over a coloured background (premultiplied and
RGB-over-background paths, golden hashes taken before the change), Rec.
2100 PQ and HLG EXR outputs, raw alpha, and a visible skip of the GPU test
without an adapter.
- docs/architecture.md: the Working Gamma curve for unmanaged projects, and
no HDR step for unmanaged projects or PQ / HLG outputs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
B44 and B44A pack only half floats, so at 32-bit float the menu and the
render log say the samples stay uncompressed. Half-float frames hold samples
above 65 504 at that limit instead of writing infinity. The EXR parameters
move into their own helper; exr_out is public for other writers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenEXR renders (Render Queue sequences and Save Frame As ProEXR) received
the frame after Project Settings > HDR, so Compand and Tone Map (the default
of every OCIO project) compressed highlights and changed values below 1.0
before the writer decoded them, always with the sRGB curve.
- RenderOpts::scene_output: the top-level comp converts to the output space
without the HDR step (CPU and GPU walks, via Renderer::output_pipe).
- Pipe::output_linear: the writers decode with the output space's curve
(the Working Gamma's when unmanaged) instead of a fixed sRGB curve.
- Render Queue EXR jobs and ProEXR set the flag; PSD layers, 8-bit stills,
movies and the viewer keep the HDR step. The flag is part of the viewer
frame and disk cache keys (opts_hash).
Golden hashes, taken before the change, pin PNG/TIFF/Targa bytes for
8/16/32 bpc x Clip/Compand/ToneMap, Clip-mode EXR samples with an sRGB
output, and the layered PSD bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenEXR sequence output modules get two settings, exrCompression (none,
rle, zips, zip, piz, pxr24, b44, b44a, dwaa, dwab; scan lines in each
method's own block height) and exrBitDepth (half or float). The defaults
are ZIP and 32-bit float, so existing projects render the same bytes as
before. They go through the project file, Output Module templates, the
renderQueue commands and renderQueue.formats, the CLI (--exr-compression,
--exr-depth), the Output Module menu (lossy methods are marked) and the
render log. Half rounds each sample to the nearest half float.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SAM 2's instance now reads wgpu's environment variables, as the render context's does, so a driver such as
Mesa's Dozen (WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER=1) is visible to it, and WGPU_POWER_PREF applies.
An adapter without compute shaders is refused before a device is made on it, so the CPU takes over with one
line instead of a validation error per kernel. The refused adapter is kept for the life of the process, as
the device on it was: dropping it crashed the tests on WSL2's GL driver. Gpu::shared no longer repeats
"SAM 2 compute:" in its errors.
A run reads the project, edits it and writes the whole file back, so two
at once silently dropped the edits of whichever saved first. A run that
saves over the file it opened (--save, or --save-as the same file) now
holds an OS lock on `.<name>.lock` next to the project from before its
read until it exits. The next run says so on stderr, waits, then edits
what the first one saved. Ported from filmcraft-cli
(storytold/filmcraft#3926).
The OS drops the lock however the process ends, so a crash leaves nothing
stale. If the lock can't be taken the run warns and saves as it did
before. A missing project stops the run before it opens anything, with
the same error opening gives, so a project created in between is never
saved without the lock. The desktop app and `mcp` don't take this lock.
camera.solveStatus said `solved: true` for any stored solve, but the solver
keeps the frames it cannot resect as unsolved and interpolates the camera
there. `solved` now needs every frame, the same check camera.exportBlender
makes, and `partial` marks a solve with gaps, next to `solvedFrames`.
Offset Paths' Amount, Line Join, Miter Limit, Copies and Copy Offset,
and Zig Zag's Size, Ridges per segment and Points reported their
internal ids as matchName, so property("ADBE Vector Offset Amount") and
the other documented names returned null. They now report and resolve
by ADBE Vector Offset Amount, Line Join, Miter Limit, Copies and Copy
Offset, and ADBE Vector Zigzag Size, Detail and Points. Lookups by
display name and internal id still work.
The command checks look at the paths an agent gives, but a project it opens or
imports from a read root names its own footage, which was read wherever it is
when the project rendered: render_frame returned its pixels.
Backend::confine now wraps a headless session's footage and services:
ConfinedFootage reads frames, audio, aux channels, models and vector frames
only for footage inside the read roots (problem() says why the rest shows
nothing), and ConfinedServices checks read_file, exists, stamp, list_dir and
is_dir the same way. Writes are left to the command checks.
Test: automation_roots_confine_the_footage_a_project_names covers an absolute
path, `../`, a linked file and a linked folder, each opened and imported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shortcut belongs to the application, a panel or a context (editing text, a paint or
camera tool active, a mask selected). The narrowest active scope runs a chord: an active
context, the focused panel, the panel sharing its shortcuts (Timeline and Composition),
the application. A context's shortcuts never run outside it.
- shortcuts.rs: scope table for engine commands and menu entries, scope_rank,
scope_label; UiCommand can state its scope.
- Conflicts: the same chord in two scopes is an override (both keep working), reported
with overrides: true; shortcuts.conflicts lists same-scope pairs only.
- Scoped now, keys unchanged: Remember / Apply Interpretation and Delete Without
Confirmation (Project), Duplicate Render Item (Render Queue).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds Swedish (Svenska) as an interface language in the menu, panel and
effect catalogs, written from the English source. The effect rows are
partial like Japanese and Georgian: an effect without a Swedish row draws
its English name, so a new effect does not block other PRs (#2244).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>