Commit Graph
70 Commits
Author SHA1 Message Date
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
skylar taylor-barrick@skylartaylor <26588250+skylartaylor@users.noreply.github.com>FredApps
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
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
BFlat a7f1d11620 Merge pull request #1326 from tylerjenningsw/fix/gpu-frame-validation
GPU: contain frame-local validation failures (refs #1319)
2026-10-30 01:16:36 +07:00
bflatasticandClaude Opus 5.5 c526bb554a GPU: without_kernel copies the software and yuv fields too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 10:10:45 +07:00
bflatastic 75e878f178 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-29 10:10:44 +07:00
Matej Kozlovský 6bcb76b9dc GPU: Classic 3D builds with FXC, so the GPU compositor starts on DirectX 12 (#613)
occlusion computes its three channels as one vector instead of writing a
vector component through a loop index, which FXC cannot address and tried
to unroll every enclosing loop for (X3550/X3511). classic3d now also
builds like the effect kernels: if a compiler rejects it, 3D runs render
on the CPU instead of the whole compositor failing to start. The ignored
FXC diagnostic test now checks classic3d instead of the stale pointwise
failure.
2026-10-28 17:08:46 +01:00
Matej Kozlovský 6e69b91a80 Export: H.264 frames convert to YUV 4:2:0 on the GPU
Accelerator::comp_frame_yuv420 (default None) renders a top-level comp frame and
converts it over the background to the H.264 export's 8-bit BT.709 limited-range
4:2:0, every byte within one level of the CPU conversion (the GPU kernel follows
it operation for operation, including NaN and half-away-from-zero ties, but a
driver may contract its arithmetic). A 4K frame then reads back 12 MB of planes
instead of 133 MB of RGBA f32.

The H.264 export asks the accelerator for planes when a frame needs nothing after
the render (progressive, project depth, no crop/resize, rendered at the output
size) and otherwise renders and converts on the CPU as before. Deferred
readbacks (a browser worker) keep the pass-based path. Sizes the kernel can't
take (width not a multiple of 8, odd height) are declined before any GPU work.

Like Cx::frames, h264_frames caps GPU frames in flight only while the
accelerator is up (map_frames_while with the same liveness check): once the GPU
stops for good, the remaining frames render on the CPU side by side instead of
one at a time.

rgba_to_yuv420 moves to effectcraft_raster::yuv so the export and the GPU tests
share one reference. The GPU tests skip without an adapter (tests::gpu()), as
the crate's others do.
2026-10-28 13:19:24 +01:00
Dylan M. Taylor 7926b8bd31 Refactor gpu for strict Clippy checks 2026-10-28 06:09:21 +00:00
skylar taylor-barrick d9cc373169 M0.37: specialize median histograms for software Vulkan 2026-10-26 19:27:46 +01:00
aiongg a6d8d23dfc Merge #2045: GPU: devices favour memory use, so Purge and Close give video memory back (@arnav-hiwarkar) 2026-10-24 01:21:39 +00:00
arnav-hiwarkarandClaude Opus 5.5 2b3732153b GPU: devices favour memory use, so Purge and Close give video memory back
Both devices EffectCraft creates now ask for `MemoryHints::MemoryUsage`:

- the window device (`apps/effectcraft/src/main.rs`, through a new `device_descriptor` helper);
- the headless compositor device (`crates/gpu/src/context.rs`).

The limits, labels, features and error paths are unchanged. The devices tests create stay as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:45:28 +05:30
dedandClaude Opus 5.5 915ba28a9d Motion Tile's Output size works on adjustment layers (#1727)
On an adjustment layer Motion Tile kept the comp-sized buffer whatever the
Output Width / Height, and the adjustment draw dropped any stack result whose
size differed from the canvas. After Effects grows (or crops) the comp
rectangle there as on any layer, so a Transform after it brings the extra
tiles into view, as in the frames attached to #1727 and #1409.

- motion_tile_frame: one output frame for the CPU and GPU effect; on an
  adjustment it grows about the comp rectangle on whole pixels, keeps the
  buffer at 100 % and past 16 384 pixels a side.
- adjustment_crop: the CPU and GPU adjustment draws put a stack result whose
  frame changed back onto the canvas instead of dropping it (Enc::crop on
  the GPU).

Tests: the #1727 After Effects frames (910 x 340 % and 90 x 340 % then
Scale 12 %), tiles past the frame and cropping on CPU, GPU vs CPU for
adjustment and full-frame layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:35:47 +03:00
Matej Kozlovský f8bd967861 gpu: honour wgpu env overrides in headless/CLI (with_env, WGPU_ADAPTER_NAME, WGPU_POWER_PREF)
GpuContext::request() now applies InstanceDescriptor::with_env() (WGPU_BACKEND,
WGPU_DX12_COMPILER, WGPU_VALIDATION, WGPU_DEBUG, ...), the same variables the
desktop app honours via egui-wgpu, and requests PowerPreference::from_env(),
defaulting to HighPerformance (an unknown WGPU_POWER_PREF logs a warning).

WGPU_ADAPTER_NAME (CLI/headless only; the desktop ignores it) picks the first
adapter whose name contains the trimmed value, case-insensitively. Empty or
blank counts as unset. Only the name is matched (backend via WGPU_BACKEND). A non-matching name is an
error that says so and lists each distinct adapter once as 'name (backend)', instead of wgpu::util::initialize_adapter_from_env's panic;
no adapters at all gets its own message.
2026-10-16 20:12:35 +02:00
bflatasticandClaude Opus 5.5 45eacf1b17 GPU: upload_buf keeps its own doc comment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:33:50 +07:00
Matej Kozlovský 77a8d6ae73 gpu: upload cache drops textures of freed buffers on every insert
Entries whose layer buffer was freed were pruned only once the cache went over its budget
(1.5 GiB), so an animated layer, which makes a new buffer every frame, left a dead 4K texture
per frame allocated until the cache filled up.
2026-10-15 10:19:42 +02:00
tylerjenningsw 864b9e4ede GPU: no texture of a failed frame returns to the pool, even one out or waiting on another encoder (refs #1319) 2026-10-13 14:12:15 -04:00
tylerjenningsw 1b3a70a7bc GPU: discard_kept is public like clear_uploads, so wasm builds don't flag it unused (refs #1319) 2026-10-13 13:43:33 -04:00
tylerjenningsw babdcd3b9f GPU: the frame guard contains validation errors and drops the failed frame's textures (refs #1319) 2026-10-13 13:36:04 -04:00
tylerjenningsw ec29d78ca2 GPU: working textures are labelled with their size (refs #1319) 2026-10-13 13:25:42 -04:00
Matej Kozlovský d8ff1189dc GPU: the upload cache budget can be set per context and warns once when full, instead of evicting layer textures silently (#687) 2026-10-12 09:47:28 +02:00
Matej Kozlovský 9c5c187292 GPU: an Enc reuses the working textures it released at once instead of allocating new ones for every intermediate of a frame (#687) 2026-10-12 09:47:28 +02:00
Mara Johnson c88d2b3a7f fix(render): prevent render hang and system lag (#2081) 2026-10-11 00:36:38 -04:00
Claude 3c063b5075 GPU: a failed allocation reported as an internal error, or a lost device, no longer crashes the app (#591)
On Adreno (Windows ARM64) a failed vkAllocateMemory surfaces as an internal
ERROR_INITIALIZATION_FAILED, not out-of-memory. Gpu::within_memory only scoped
OutOfMemory, and wgpu's DeviceExt::create_buffer_init maps the new buffer at
creation and panics ("Failed to get mapped range") when the allocation failed,
taking down a frame thread. Buffers with initial contents are now created unmapped
and filled through queue.write_buffer (context::buffer_init), so a failed
allocation is only an invalid buffer and an error for the caller's scope, and
within_memory scopes Internal errors too, discarding the frame for the CPU path
as it does for out-of-memory.

The device was then lost, and egui-wgpu panicked on the main thread staging the
next frame's index buffer on the dead device. Once the host reports the
presentation device lost, the app draws nothing more (no mesh to stage), auto-saves
unsaved changes, keeps the crash-recovery marker so the next launch offers them,
and closes its window.

Tests: a device with a small buffer limit gets an over-limit and an unaligned
buffer_init without a panic and sees the error in its scope; the UI harness
reports a device loss and checks the next frame has no shapes, closes the window
and wrote the auto-save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 22:54:56 +00:00
Claude e2a47495eb GPU: an effect kernel the shader compiler rejects no longer turns the GPU compositor off (#613)
GpuContext::new built every compute pipeline up front and failed as a
whole when one didn't build. On DirectX 12, FXC (wgpu's default D3D shader
compiler) aborts on the `pointwise` effect kernel, so the whole app fell
back to CPU compositing.

The compositor's own kernels are still required, but the effect kernels
(`pointwise`, the blur/glow/shadow passes and every fx_* kernel) are now
optional: one that fails to build is logged and listed
(GpuContext::unavailable_kernels), and a GPU effect chain that dispatches a
missing kernel reports it (Enc::take_missing) so those effects render on
the CPU while everything else stays on the GPU.

This does not make FXC accept `pointwise` (that needs a Windows/FXC box to
bisect); it limits the damage to the effects using it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 21:17:18 +00:00
bflatasticandClaude Opus 5.5 9d85d4f116 Viewer: Auto resolution follows the magnification; minified frames are averaged (#417)
Cause: Auto dropped to Half below 75 % magnification, so text between
50 % and 75 % was rendered with fewer pixels than the screen shows and
looked soft. Below 100 % the frame texture was minified with a plain
bilinear sampler and no mip levels (Full at 46 % skipped most frame
pixels), so edges stair-stepped.

Fix:
- Auto renders the pixels the magnification needs, as in After Effects:
  the coarsest whole factor that still gives every screen pixel a
  rendered one (Full above 50 %, Half down to 33.3 %, Third down to
  25 %, Quarter below).
- GPU frames: the display texture carries a full mip chain, each level
  the 2 x 2 mean of the one above (display.wgsl `mip`, same bind group
  layout as `display`), sampled trilinearly with Viewer Zoom Quality
  More Accurate (Faster keeps the nearest pixel of the full-size frame).
- CPU frames (egui-wgpu textures have no mips): uploaded averaged down
  by the whole factor that leaves about one texel per screen pixel, the
  same box average as the texture-limit fit (#201), re-uploaded when the
  magnification calls for another factor; drawn with texture coordinates
  that keep each texel over its pixels when the size isn't a multiple.
  Show Channel / exposure textures follow.

Tests: gpu tests::display_frames_are_mipmapped (level count and level 1
= 2 x 2 means of level 0), viewer_tools
auto_resolution_follows_the_magnification (60 % was Half) and
minified_frames_keep_their_texels_in_place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:17:33 +07:00
bflatasticandClaude Opus 5.5 fdb655e78e GPU: the compositor declines OpenGL adapters at once (#243)
On OpenGL the compositor translated its compute kernels for tens of
seconds (22 s on an RTX 4090) before one failed validation and it fell
back to CPU compositing, all before the window's first frame. It now
declines GL adapters up front, as it declines adapters without compute
shaders. OpenGL is what Window Graphics falls back to for drivers that
crash with DirectX 12 or Vulkan.

Test: opengl_adapters_get_cpu_compositing_at_once (gpu tests_init;
skips without an OpenGL adapter).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:57:08 +07:00
bflatasticandClaude Opus 5.5 80dff0fb0e GPU: bounded readbacks block on the GPU instead of sleeping between polls
#139's bounded read_buffer polled the device without waiting and slept up
to 10 ms between polls, adding up to 10 ms to every readback (the desktop
viewer reads every GPU frame back): the gpu crate's tests took 227 s
against 119 s before. It now waits with PollType::Wait and the remaining
deadline as its timeout (a timeout still retires the context), then takes
the result, giving another thread that polled the same device up to 1 ms
to deliver its callback. The tests take 71–79 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:52:53 +07:00
Dexter 94dbccf69f fix: contain GPU initialization and retire failed readbacks 2026-10-06 16:40:12 -05:00
bflatasticandClaude Opus 5.5 e64076e432 GPU: running out of video memory falls back to the CPU instead of crashing (#106)
The desktop app shares egui-wgpu's device, which had no uncaptured-error
handler, so wgpu's default one panicked on an out-of-memory error on
every frame thread; the panicking frame stayed "in progress" forever and
never rendered again. The GPU frame budget was a fixed 1 GiB.

- GpuContext::new logs uncaptured errors once its pipelines are built
  (errors while building them still panic there, which the app catches
  to fall back to CPU compositing).
- Gpu::within_memory runs GPU work in an out-of-memory error scope; on an
  error the result is discarded and uploaded layers and pooled textures
  are freed.
- Frame workers render each frame within it; out of memory, the frame
  renders on the CPU and the RAM preview keeps half as many GPU frames
  (down to 64 MiB), since wgpu can't tell how much video memory there is.
- A frame whose render panics renders again on the CPU, or is released,
  so the viewer asks for it again.

Tests: a_frame_whose_render_panics_is_released (failed before),
running_out_of_video_memory_halves_the_gpu_frame_budget and
within_memory_returns_the_work_when_there_is_room.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 02:06:15 +07:00
Dexter 39e6907b5f G2: improve editing reliability and Windows rendering fidelity 2026-10-06 00:00:31 -05:00
Brandon ThomasandClaude Opus 5.5 3767332efd GPU compositor declines devices whose limits its pipelines exceed
egui-wgpu creates its device on GL with WebGL2's limits (no storage buffers), so the desktop
app on FreeBSD built the compositor's pipelines on it: validation errors (only logged in
release builds) and wrong frames. GpuContext::new now checks the limits its pipelines bind
(storage buffers and textures per stage, bind groups, workgroup sizes) and returns Err, which
keeps the app on the CPU. ui_gpu skips its GPU-vs-CPU comparison when that happens on GL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:52:04 -04:00
Brandon ThomasandClaude Opus 5.5 6eedf8e2c5 GPU on wgpu's OpenGL backend: renderable depth target, portable shaders, serialised tests
FreeBSD's CI VM (Mesa llvmpipe over GL) failed 22 effectcraft-gpu tests:

- Advanced 3D's camera-depth target was R32Float, which GLES can't render to without
  EXT_color_buffer_float: create_render_pipeline failed validation (release builds only log
  that and draw nothing). The target is now R32Uint holding the f32 bits (renderable on every
  backend, lossless), and GpuContext checks the adapter's format features (render attachment,
  blendable colour) and independent blending up front; when they're missing the render
  pipelines are never built and Advanced 3D renders on the CPU.
- Signed `%` in the shaders: naga emits GLSL `%`, undefined for negative operands, which broke
  the VR effects' horizontal wrap and Cell Pattern's tiling. A shared `imod` never takes `%` of
  a negative number.
- asin/acos: Mesa's are coarse approximations (~1e-4 rad), visible in every lens/sphere/cylinder
  mapping. `asin_p`/`acos_p` (Cephes' asinf polynomial) are f32-accurate on every backend.
- "Could not lock adapter context": the GL backend has one context lock per adapter with a
  timeout, so tests driving devices concurrently panic. Every gpu test helper that hands out a
  device holds one process-wide lock for the calling test; ui_web_job_gpu's two device tests
  share a lock too.
- On GL, up to 1 % of pixels may flip across a decision boundary (division through a
  reciprocal, unfused fma); the GPU-vs-CPU comparisons allow that there only.

Also adds the FreeBSD workflow (from the FreeBSD CI PR) so this branch is checked there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:08:21 -04:00
Brandon ThomasandClaude Opus 5.5 fe89ab9146 Never crash: render and gpu crates without panic!()
render: the debug-only quantisation check uses debug_assert! instead of panic!. gpu: the
test-only panic on a wgpu validation error is cfg(test) with a narrow allow, and release
builds only log. Test-only unreachable!() calls become panic!(). Denies the panic lints in both
crates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:20:37 -04:00
Brandon ThomasandClaude Opus 5.5 c59e0a6407 Merge main into M13.28 worktree
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 02:57:55 -04:00
Brandon ThomasandClaude Opus 5.5 c4315818da M13.28: GPU generators (fx_gen2): Lightning, Advanced Lightning, Beam, Lens Flare, Radio Waves, Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads, Audio Spectrum/Waveform, Basic Text, Path Text
The geometry stays on the CPU as per-frame plans shared with the CPU effects (bolts, mask
strokes, scribbles, contour segments, audio marks, glyph strokes, waves, flood-fill region,
eyedropper sample); segments and convex polygons are tile-binned and rasterised on the GPU
with the CPU's coverage functions. CPU output unchanged (gen_golden). CC Threads' exact ties
at round parameter values are a documented exception; Fractal stays on the CPU (needs f64).
Regenerated docs/effects.md, docs/parity.md EFF-5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 02:56:00 -04:00
Brandon ThomasandClaude Opus 5.5 1c855c3e07 M13.29: GPU effects part D (particles and remaining fallbacks)
GPU render passes for CC Particle World / Systems II, Particle Playground,
CC Ball Action, CC Pixel Polly, CC Scatterize and Curl Noise (fx_particles,
chunked multi-pass raster); Shatter wireframe views, Foam extras, Timewarp
with a Matte Layer, strong Warp Fisheye/Twist (CPU f64 inverse map) and
custom .ocio configs no longer fall back to the CPU. CPU refactors pinned by
particle_golden / timewarp_golden / sim_golden hashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 01:35:56 -04:00
Brandon ThomasandClaude Opus 5.5 5df9cde182 M13.28: GPU pixel ports (fx_pixel2): CC Cross/Radial/Vector Blur, Reduce Interlace Flicker, CC Composite, Color Link, Cineon, HDR Compander/Highlight Compression, Grow Bounds, CC Overbrights, CC Block Load, CC Burn Film, CC Glass/Plastic, CC HexTile, CC Mr. Smoothie, Inner/Outer Key, CC Simple Wire Removal, Basic 3D
Inner/Outer Key's trimap and Cleanup strokes, Color Link's sample and CC Glass/Plastic's
light are shared with the CPU effects (CPU output unchanged: gen_golden). CC Plastic now
reads its Bump Layer (it read the nonexistent bumpMap id).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 01:33:54 -04:00
Brandon Thomas 808ca70d47 Merge origin/main (M13.22 GPU effects part A) into M13.23: both GPU effect lists (236), docs recounted 2026-10-04 00:17:37 -04:00
Brandon Thomas 7c675124ae Merge origin/main (M13.24) into M13.23 2026-10-03 23:59:41 -04:00
Brandon Thomas ca9a30bf0c Merge branch 'main' into worktree-agent-a38a174c36aa9f23a 2026-10-03 19:52:55 -04:00
Brandon ThomasandClaude Opus 5.5 1ec9336d7c M13.23: GPU simulation render passes
CC Rainfall, Snowfall, Star Burst, Bubbles, Drizzle, Hair, Mr. Mercury,
Caustics, Wave World, Foam, Shatter, Card Dance and Card Wipe render on the GPU
(fx_sim.rs / fx_sim.wgsl). The per-frame simulation / layout stays on the CPU
and is shared with the CPU effect through plans (effects::sprite_plan,
piece_plan, bubble_list, mercury_blobs, drizzle_drops, wave_world_plan,
caustics_setup); a tiled item rasteriser composites sprites, bubbles, blob
fields and textured pieces in the CPU's order and shading. Layer-dependent
sprite colours (refracting rain, star-burst and hair roots) are sampled per
pixel; piece plans read the frame back once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 18:35:22 -04:00
Brandon ThomasandClaude Opus 5.5 1b726349ab M13.24: web app depth (WEB-1) — GPU effects in frame workers, OPFS disk cache, storage manager
- GPU in the browser: each frame worker opens its own WebGPU device (Gpu::request_deferred) and
  renders the GPU compositor and GPU effects with deferred readbacks (effectcraft-gpu deferred):
  readbacks keyed by content start with map_async and miss, the pass returns placeholders, the
  layer cache's gate drops inserts, and FrameServer::resume renders again once the device
  delivered. Backend Auto times whole frames per comp in the workers. Frames with effects go to
  GPU workers instead of the page. CPU fallback without WebGPU in workers (?nogpuworkers).
- Fix: a WGSL constant (3.40282347e38) that Chrome rejects invalidated every GPU kernel in Chrome
  (the page's compositor too); a test checks every float literal fits f32.
- Disk cache in the Origin Private File System: workers write sealed frame entries with sync
  access handles (temp file + move), the page keeps the DiskIndex (LRU under the settings' limit,
  at most half the quota) and reads hits instead of rendering; survives reloads; blue cache bar.
- Storage manager: Settings ▸ Disk ▸ Browser Storage (usage/quota, persistent storage, Clear
  buttons, automation ids) and storage.info / storage.persist / storage.clear (StorageHost).
- Docs: web.md (GPU workers, deferred passes, disk cache, storage manager, threading design:
  one engine instance per worker), architecture, preferences, parity (WEB-1), roadmap.
- Tests: deferred passes vs CPU on a real device, placeholder gating, frame server passes and
  Auto, GPU worker routing + disk cache across a reload, storage commands and widgets, store
  usage by kind; smoke test: GPU effect frames in workers with pixel parity, disk hits after a
  reload, storage manager.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 18:35:03 -04:00
Brandon ThomasandClaude Opus 5.5 0c4e533460 M13.22: GPU compositing of Advanced 3D runs with blend modes, track mattes (2D and 3D), Preserve Transparency, and environment backgrounds (Classic and Advanced 3D skies)
render: three_d::adv::SplitRun (Renderer::split_adv_run) hands draw_run's split path to an
accelerator as data (main scene, special layers far to near, 3D mattes drawn solo);
Renderer::sky_draw / three_d::SkyDraw for Environment Light Background layers. gpu: the walk
draws skies (adv_sky kernel), resolves each special layer, hides it behind the nearer main
scene (occlude kernel) and composites it through the 2D path without readbacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 18:26:06 -04:00
Brandon ThomasandClaude Opus 5.5 a909de43b7 M13.22: GPU time effects (Time Difference, Time Displacement, CC Force Motion Blur, CC Wide Time, Pixel Motion Blur, Timewarp's frame building; motion vectors and frame fetches on the CPU)
Timewarp without a Matte Layer is now planned once (time_fx::timewarp_plan: frames, shutter
instants, motion fields) and built by the CPU or the GPU from the plan; with a Matte Layer it
renders on the CPU (catalog::gpu_supported).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:57:03 -04:00
Brandon ThomasandClaude Opus 5.5 2e5ea051a8 M13.23: GPU Immersive Video effects
VR Blur, Chromatic Aberrations, Color Gradients, Converter (all projections and
cube layouts), De-Noise, Digital Glitch, Fractal Noise, Glow, Plane to Sphere,
Rotate Sphere, Sharpen and Sphere to Plane run on the GPU (fx_vr.rs /
fx_vr.wgsl) with the CPU's equirectangular maths: seam-wrapping, pole-continuing
sampling and latitude-widened box blurs, per eye for stereo layouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:53:23 -04:00
Brandon ThomasandClaude Opus 5.5 d0b92948ee M13.22: GPU transitions and perspective (Block Dissolve, CC Glass/Grid/Image Wipe, CC Jaws, CC Line Sweep, CC Radial ScaleWipe, CC Scale Wipe, CC Twister, CC WarpoMatic, Radial Shadow, CC Bender, CC Blobbylize, CC Cylinder, CC Sphere, CC Spotlight, CC Environment, 3D Glasses) and text (Numbers, Timecode: CPU glyph coverage, GPU composite)
CC Environment now fades nearly transparent environment texels to black continuously
(colour / max(alpha, 1e-3)) instead of a hard 1e-6 un-premultiply cut-off, which turned the
filtered map's float noise into colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:48:02 -04:00
Brandon ThomasandClaude Opus 5.5 e3e35a7ec2 M13.23: GPU colour management (LUTs, OCIO, Color Profile Converter, Lumetri LUT/look)
Apply Color LUT, OCIO CDL / Color Space / Display / File / Look Transform and
Color Profile Converter compile to colour programs (effects::color_program,
built next to the CPU effects) that fx_lut.wgsl interprets per pixel; LUTs
(1D, 3D, cineSpace shapers, inverses) are read from the storage buffer with the
CPU's nearest / trilinear / tetrahedral interpolation. Lumetri's Input LUT and
Look no longer force the CPU. Custom .ocio configs stay on the CPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:43:24 -04:00