* gpu: the off-screen instance reads wgpu's flag variables (#4084)
native_instance() applies with_env() to its flags and backend options, as the window's instance does, so
WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER=1 lets GPU exports use Mesa's Dozen on WSL2, and
WGPU_VALIDATION, WGPU_DX12_COMPILER and the others apply to them. Its backends are unchanged: no GL on
Windows (#3418), and ExportRenderer::new still narrows the adapters by WGPU_BACKEND.
The playback bench example reads the same variables now, and WGPU_POWER_PREF when set.
* bench: keep the default adapter options, not WGPU_POWER_PREF
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
plan_frame plans a Push as a GPU composite (#308) and is_cpu_frame gives
such a frame a pooled renderer, but render_export_prepared and
ExportRenderer::render both refused every composite, so each frame inside
a Push went back to the CPU renderer. plan_frame composites only
transitions the GPU draws, so the export path now runs composites like
layer plans.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* render, gpu: Bokeh Blur gathers through a kernel of whole-pixel weights, 5x faster on the GPU
Each bilinear tap's four weights fall on the same offsets around every pixel (a pixel's centre is
a whole number and a half), so vfx::bokeh_kernel sums them per offset once per frame: about 330
reads at amount 10 instead of 1764. The CPU gathers through it, and the GPU reads it from a small
texture in the gather pass's aux slot, in the same order. 1080x1920, M4 Max: amount 10 hexagon
15 -> 3.2 ms on the GPU, 781 -> 114 ms on the CPU; amount 12 disc 58 -> 6.7 ms on the GPU.
New test bokeh_kernel_matches_the_bilinear_taps keeps the old gather as the reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* render: Bokeh kernel offsets stay within 2^20 pixels; the CPU gather adds them saturating
A huge amount on an image too small to halve kept its whole radius, so an offset could reach
i32::MAX and wrap when added to a pixel position (on the GPU, and on 32-bit CPU targets). An
offset past any image's width reads the edge pixel either way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
New Three-Way Color Corrector (Obsolete folder, like Color Balance): a
hue/balance wheel and a level shift for shadows, midtones and
highlights, tonal range thresholds with softness, and Saturation. All
parameters go through the generic param path (keyframable, undoable).
CPU (FxOp::ThreeWay) and GPU (OP_THREE_WAY) share the arithmetic; the
wheel is converted to an RGB offset on the CPU so the shader stays
simple. Defaults are the identity.
Tested: three_way_color_corrector_works_per_tonal_range (identity, each
wheel affects only its range, saturation 0 is grey) and two GPU/CPU
parity cases in fx_tests.
Fixes#3465
The off-screen device GPU rendering of exports draws with took `request_adapter`'s choice, so on an
Intel Gen 9 to Gen 11 GPU it was created through Vulkan and the export died in `igvk64.dll`
(0xc0000005) as soon as it started, with "Render on GPU" on or `gpuRendering: auto`. It now
comes from `adapter::pick` without a surface: the same adapter as before everywhere else, and the
GPU's DirectX 12 entry on those Intel GPUs. With `WGPU_BACKEND` set, it is chosen among the
adapters of the backends that names, as the window's is, so `WGPU_BACKEND=vulkan` keeps Vulkan for
exports too; when the export instance has none of them (GL on Windows), every adapter stays a
candidate.
Probed on an Intel UHD Graphics 630 (driver 25.20.100.6576): `filmcraft-cli export ...
--settings '{"gpuRendering":"auto"}'` died in `igvk64.dll` at fault offset 0x2f0fd6 within 3 s,
twice.
Intel's Windows Vulkan driver for its Gen 9 to Gen 11 integrated GPUs (6th to 10th generation
Core, and the Atom, Pentium and Celeron graphics of those years) dies with 0xc0000005 in
`igvk64.dll` when a device is created. It is reported on every driver version from 25.20.100.6576
(2019) to 31.0.101.2145, the legacy branch Intel ships for them and its last. FilmCraft crashed at
launch twice before the start-up fallback (#820) moved it to DirectX 12.
The adapters are listed before the crash, so the adapter selector of the default mode can avoid
it: when wgpu's choice is such a GPU through Vulkan, the same GPU's DirectX 12 entry draws.
The families are told apart by PCI device id prefixes, checked against all 442 ids of Linux's
`include/drm/intel/pciids.h`: 133 are Gen 9 to Gen 11 and match, none of the others do.
Tiger Lake and later, discrete GPUs, and `WGPU_BACKEND=vulkan` (no DirectX 12 entry) keep
wgpu's choice.
Probed on an Intel UHD Graphics 630 (0x3e91, driver 25.20.100.6576): a Vulkan device dies in
`igvk64.dll` at fault offset 0x2f0fd6 after the adapters are listed; DirectX 12 creates a device
and runs compute.
* gpu: create wgpu instances without OpenGL so concurrent device setup cannot deadlock (#3418)
Instance::default() enables every backend, GL included. On Windows that starts
WGL threads inside nvoglv64, which is both NVIDIA's OpenGL driver and its
Vulkan ICD, and devices created, used and dropped at once then deadlock there
(vkDestroyDevice in the Vulkan loader's critical section against other
devices' create_texture / queue.submit). adapter::native_instance() asks for
Backends::PRIMARY only, the set adapter.rs already chooses from; the export
renderer and the test helpers use it. New stress test: 16 threads creating,
using and dropping devices at once, joined with a deadline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* gpu: keep OpenGL off only on Windows, where it deadlocks
Linux and macOS machines whose only working backend is GL keep GPU export.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* security(media): bound generator frames, imported audio dimensions, and render images
* fix(media): unwrap render result in demo_frames example
* fix(render, codecs): guard zero-sized images, check post-scale sequence pixels, and bound combined MXF channels
* fix(codecs): annotate channels as u32 in MXF sound track combination
* fix(render): preserve exact output_size scaling calculation for renderable_sequence validation
* fix(media,render): reject unsupported audio dimensions and invalid render scale
Sample rates above MAX_AUDIO_SAMPLE_RATE were clamped without resampling, so a
768 kHz stream was reported as 384 kHz and played at half speed. Reject them
instead, and drop the stream at the four codec call sites rather than
advertising dimensions the samples do not match.
output_size now treats a non-finite or non-positive scale as 1.0 instead of
casting it to zero and collapsing the frame to 1x1.
* fix(media): keep Matroska audio tracks and their metadata together; guard empty images
A track whose advertised sample rate or channel count is refused is dropped before the track list is built, so atracks[k] and audio_streams[k] stay the same track. over_black_with_alpha_rgba8 and Image::transformed return early for an empty image instead of chunking by zero.
The export renderer asked wgpu for an adapter with no power preference,
which takes the first one listed. On a laptop with an Intel integrated
GPU listed before an NVIDIA GPU that is the integrated GPU, whose
read-back memory is uncached: the compositor read each exported 1080p
half-float frame there two bytes at a time (650 ms for one frame, 1.3 s
with four renderers at once), so GPU rendering made exports more than
ten times slower than the CPU renderer.
- ExportRenderer::new asks for the high-performance adapter, as the
app's window does; WGPU_POWER_PREF overrides both.
- Every read-back (accumulator, effect stage, masks) copies each row out
of the mapped buffer in one go before reading it sample by sample:
85 ms instead of 650 ms per frame on the integrated GPU, about the same
on cached memory.
cargo xtask bench --sections export --hw auto on that laptop: h264 61.7
-> 1.42 s, h264_fx 65.3 -> 1.56 s, prores 61.1 -> 4.63 s; the CPU path is
unchanged (h264_fx --hw off 11.76 -> 11.57 s).
An export with Export ▸ GPU rendering on stopped for good on some machines and never finished,
reported on an AMD laptop in #1532's *Not in this PR* as "I have not found the cause" and again in
#799 for the reporting gap. On a Snapdragon X (Adreno X1-85, Windows 11 ARM64, driver
`31.0.137.0`) it reproduced 7 times out of 7, and the isolation is that it has nothing to do with
hardware encoding: `hardwareEncoding: off` hangs the same way. `gpuRendering: off` never does.
The read-back waits for the GPU like this:
```rust
device.poll(wgpu::PollType::Wait { submission_index: None, timeout: Some(8s) }).ok()?;
rx.recv().ok()?.ok()?;
```
The timeout the second line is meant to be bounded by bounds nothing. A fence that has not signalled
inside the timeout comes back from the HAL as `Ok(false)`, and wgpu throws that answer away —
`Device::maintain` in wgpu-core says so in a comment: "we do not care if the wait succeeded or not,
and the `Ok(bool)` variant is ignored". So `poll` returns `Ok(…)` whether or not the GPU finished, a
GPU that has stalled looks exactly like one that has not, the `map_async` callback never arrives,
and the blocking `recv` parks the export thread forever: no progress, no CPU, no error, and the job
stuck at whatever percentage it reached. The eight seconds only delayed it.
`wait_mapped` now takes the callback with a timeout of its own, so the wait is bounded end to end and
a late answer is a `None` — which is what the poll's timeout was always meant to arrange: the frame
goes to the CPU renderer. It covers the LUT and mask read-backs too, which had the same pair inline,
and the comments now say why the poll's own timeout is not enough.
Snapdragon X, demo sequence, 15-25 s H.264 exports, `filmcraft-cli --demo --compact run -`:
| `hardwareEncoding` | `gpuRendering` | before | after |
|---|---|---|---|
| `off` | `auto` | hung > 100 s | 23.9 s |
| `auto` | `auto` | hung > 100 s | 23.5 s |
| `auto` | `auto` (×3) | — | 13.3 s, 15.6 s, 13.0 s |
| `auto` | `off` | 23.5 s | unchanged |
0/5 completed before, 5/5 after, and `gpuRendering: off` is untouched. Output probes back as before
(H.264 Main, BT.709 limited). `cargo fmt --check` clean, clippy clean, `cargo test -p filmcraft-gpu`
34 passed.
This is a driver-independent latent bug rather than a Qualcomm one: any GPU whose read-back does not
finish inside eight seconds had it, and on this hardware every `gpuRendering: auto` export hit it.
Bokeh Blur had no GPU op, so a clip carrying it left the GPU compositor's
layer path and every frame with it composited on the CPU: up to 625
bilinear taps per pixel, about 3.3 s per 1080p frame at a 12 px amount.
- render: Bokeh Blur is an FxOp (parameters evaluated in FxOp::eval, the
CPU reference in FxOp::apply, the id in GPU_EFFECTS). The CPU result is
unchanged; a non-finite amount is now a no-op instead of NaN.
- gpu: four passes on the top-left part of the working textures: each
halving (Image::downsample2), the highlight boost, the iris gather
(Image::sample_bilinear_clamped within the halved image) and the scale
back up (Image::transformed's bilinear).
1080p, RTX 4070 Laptop vs 16 CPU threads: amount 6 947 ms -> ~9 ms,
12 3250 ms -> ~39 ms, 20 617 ms -> ~6 ms, 60 (default) 62 ms -> <1 ms.
Tests: six parity cases (no-op, every halving depth, shapes, rotation,
highlight boost) match the CPU to < 1e-6; hostile rotation / highlight stay
on the CPU, huge and NaN amounts run bounded on the GPU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wgpu compiles DX12 shaders with FXC when dxcompiler.dll is not shipped, which is the case for
FilmCraft's Windows builds. FXC aborted on `fx_px` ("internal error: compilation aborted
unexpectedly"): Unsharp Mask's loop writes `o[k]`, a dynamically indexed vector component, and
FXC cannot compile that write inside a loop. The failed `fx` pipeline sent every DX12 device's
effects, previews and exports to the CPU: the export instance on every Windows GPU (#1616) and
the DX12 fallback on Intel integrated graphics (#1425, #1458).
Unsharp Mask now does the same per-channel arithmetic as vector math. Every other shader already
compiled with FXC.
`gpu_effects_match_cpu_exactly_with_fxc` (Windows only) runs every effect case on DX12 with FXC
forced, on WARP when there is no GPU. On an Intel UHD Graphics 630 it fails before this change
with the FXC error above and passes after it; Unsharp Mask matches the CPU to 4.4e-7.
A PC whose GPUs were installed from different driver packages of one vendor
(an ASUS G513QY: the integrated Radeon on the 2021 package, the RX 6800M on a
2026 one) gives the Vulkan loader two versions of that vendor's driver, and
the RX 6800M is listed once through each. wgpu's request_adapter takes the
first discrete entry, the 2021 driver's, and the window drawn through it dies
inside amdvlk64.dll (0xc0000005) before it paints: the main thread is in the
2021 driver, the fault on a thread of the 2026 one, no Rust frame anywhere.
Left to the #820 ladder, two crashed launches end in DirectX 12 only.
filmcraft_gpu::adapter::choose is wgpu's own rule (the adapters that can draw
to the surface, by device type for WGPU_POWER_PREF, then as enumerated) with
one addition: when the adapter that rule picks is listed again by the same
driver (same backend, device type, PCI vendor and device and driver name; the
same bus id when both report one), the entry exposing the most wgpu features
is used, i.e. the newer version (68 against 60 here, a strict superset).
Without such a duplicate the choice is wgpu's own, and entries of one GPU from
different drivers (RADV and AMDVLK, a vendor's driver and Dozen) are left
alone.
graphics::apply installs it as egui-wgpu's native_adapter_selector in the
default mode on Windows, as #609 was asked to be rebased onto the #827
start-up. DirectX 12 only, software mode and the other platforms keep their
adapter choice, and the ladder is untouched. The adapters and the choice go
to the log.
Tests: the laptop's listing (wgpu's rule picks the entry that crashed, choose
the other one), listings without two versions of one driver over every power
preference (choose equals wgpu's rule), surface, bus id and driver cases, and
one against the adapters of the machine running the tests (the rule is what
request_adapter returns there).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adjustment layers render their effects without an FxEnv, so Transform's
shutter fell back to 24 fps. In a 60 fps sequence a 360° shutter then
spanned 2.5 frames, blurring one frame before the move starts and one
after it ends. FxCtx now carries the sequence frame rate, set wherever a
sequence is known, and Transform uses it.
The only part of #37 not already on main: FxStage::new runs inside a
validation error scope, so a device where Rgba32Float is not renderable
(software Vulkan/GL) logs and renders effects on the CPU instead of panicking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AMD and Intel GPUs expose VA-API EncSlice, not NVENC. The export path
still opened NVENC first and then waited forever on wgpu map_async
(radv). Probe EncSlice before NVENC, fall through to the software
encoder, and bound GPU readback waits to eight seconds so a wedged
device fails the GPU path instead of blocking the export.
Handle zero-softness Extract as a hard cutoff in both CPU and GPU paths.
The former 0.0001-wide substitute fade introduced grey edge pixels and
amplified transfer-function rounding beyond the existing parity tolerance.
Retain the positive-softness calculation and add cutoff, inversion and
premultiplied-alpha regressions without relaxing the comparison tolerance.
Constrain caption font automation targets to the visible part of each
row. A partly clipped row could previously expose a click point outside
the popup, leaving the selected font unchanged. Preserve the existing
font-loading behavior, assets and automation IDs.
On a Rec. 709 sequence every PQ, HLG, BT.2020 or P3 clip left the GPU
path: each frame was converted to a float image on the CPU (curve, HLG
OOTF, gamut matrix, BT.2390 tone mapping, desaturation) and resampled
there. A 2160x3840 10-bit HLG clip took 90 ms per frame (48 ms scaled to
fit a 1080x1920 sequence) against 33 ms at 30 fps; the same clip in SDR
took 18 ms.
The compositor's shader now runs that input transform itself:
InputTransform::gpu_params gives its stages as numbers (log curves and
camera-gamut compression stay on the CPU), PlanLayer::input carries the
transform for Y'CbCr layers on a plain pipeline, and execute_cpu applies
the same transform as the CPU reference. HDR clips now plan in 15-19 ms
per frame, and the GPU result is within one output code of the CPU's for
HLG, PQ (with and without a mastering peak) and BT.2020 SDR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
filmcraft-gpu asks wgpu for no backend (`default-features = false`); a build
that includes the app gets them through eframe. A CLI built on its own
(`cargo run --release -p filmcraft-cli`, as the README and
docs/contributing.md do) has none, and `wgpu::Instance::default()` panics
there: an export with GPU rendering on ended as "internal error (see the
crash log); the job was stopped".
ExportRenderer::new returns None in such a build, like a host without an
adapter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the Lumetri vignette scaled its horizontal coordinate by
`1 + vround * 0.0` and applied aspect correction only for negative
Roundness, so 0 … +100 rendered identically (CPU path, the GPU op and the
GPU shader parameters alike). Negative values moved towards a circle,
the direction positive values should go.
Fix: one helper, `gpufx::lumetri_vignette_aspect`, scales the horizontal
coordinate by aspect^roundness: 0 keeps the ellipse that follows the
frame, +100 is a circle, negative values widen the oval. Non-finite or
out-of-range values fall back safely. The CPU path, the CPU FxOp and the
GPU parameter packing all use it, so they stay in parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GpuRendering defaults to Off (opt-in per export) until measured on Windows and macOS;
docs and ROADMAP say so.
- The shared export device installs an uncaptured-error handler and a device-lost callback
that log once and retire GPU export for the session (frames go to the CPU) instead of
wgpu's default panic.
- RendererPool drops a renderer that panicked and retires the pool, so the rest of the
export renders on the CPU (regression test in gpu_hook_scenarios); the duplicate
hostile-size test that raced on the global registry is folded into it.
- Rebased onto main's render_plan/transitions: render_export_prepared reads the linear
accumulator after render_plan; transition (Composite) plans fall back to the CPU.
- es/ja rows for the new Export strings; web_time::Instant in the export pipeline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port the common slice of Lumetri Color (Basic Correction, Creative without
sharpen, and Vignette) to the GPU effect stage (FxOp::Lumetri, OP_LUMETRI = 31).
Lumetri instances with unsupported sections (input LUT, Look / look LUT,
sharpen, curves, color wheels, HSL secondary, or HDR grading) fall back to CPU.
Parity tests pass within max relative difference 4.28e-7 and 0.000% flips.
🤖 Written by agy (Antigravity Agent, Gemini 3.8 Flash)