Skip to content

Composite and zoom on the GPU - #8

Merged
IAmJSD merged 7 commits into
mainfrom
t3code/fc0d1fea
Aug 23, 2026
Merged

Composite and zoom on the GPU#8
IAmJSD merged 7 commits into
mainfrom
t3code/fc0d1fea

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Aug 23, 2026

Copy link
Copy Markdown
Member

What

A wgpu GPU backend for rendering, slotted into the Compositor seam the CPU compositor left open:

  • crates/compositor-gpu — the layer tree compiles once per composite into a flat op program (plan.rs), which composite.wgsl executes as a per-pixel stack machine: all 27 blend modes (Dissolve hash included), layer masks, clipping stacks, group isolation/pass-through, LUT-compilable adjustment layers and fills. One compute dispatch per tile batch, RGBA8 packing on-GPU for quarter-size readbacks.
  • Viewport resampling (zoom/pan/rotate: crisp/bilinear/box + checkerboard) extracted from the canvas view into compositor/src/viewport.rs, with a GPU twin in viewport.wgsl — the main win on large documents zoomed out.
  • Backend registryschist_compositor::set_backend / backend(); the public composite_* functions dispatch through it, so the canvas cache, tools, exports and plugins all accelerate without changes. *_cpu variants remain the reference.
  • App wiring — on by default when an adapter exists, Preferences ▸ Rendering toggle (rebuilds caches live), SCHIST_GPU=0|1 env override, clean CPU fallback with a log line otherwise.

Correctness

The CPU compositor stays the semantic contract. The WGSL mirrors pixel-ops formula for formula — operand order, guards, and CPU quirks (e.g. a clip-base group's double-applied mask) included. New parity tests (compositor-gpu/tests/parity.rs) hold the GPU to ±1 RGBA8 step across blend modes, masks, groups, clip runs, adjustments, 8/16/32-bit, regions and every viewport sampling mode; they run under lavapipe in CI-like environments.

Anything the shader can't express falls back to the CPU per call, bit-identically: the four non-LUT adjustment kinds (hue/saturation, black & white, threshold, posterize), layers mid-drag (render_offset != 0), and nesting deeper than the shader's fixed stack.

Not in this PR

Tool and filter math (smudge, clone, large-kernel blurs, Liquify, Content-Aware Scale) stays CPU — the README's "Not there yet" now records where a second GPU seam would pay off (whole-canvas filter kernels) and why per-dab brush work deliberately isn't it.

Notes

  • GPUI doesn't expose its render device, so this is a second wgpu instance; batching amortizes the bus crossing, and the damage-driven single-tile path stays cheap either way.
  • naga 27 breaks if gpui's tree enables codespan-reporting/termcolor without naga's own termcolor feature — compositor-gpu declares it explicitly (comment in its Cargo.toml).
  • 513 workspace tests pass, clippy-clean; verified end-to-end under a real window (Xvfb + lavapipe): boots with GPU compositing on (vulkan · llvmpipe) and renders layered PSDs correctly.

🤖 Generated with Claude Code

IAmJSD and others added 7 commits August 23, 2026 17:43
A wgpu compute backend for the Compositor seam (crates/compositor-gpu):
the layer tree compiles to a flat op program that composite.wgsl runs as
a per-pixel stack machine — all 27 blend modes, masks, clipping stacks,
group isolation, LUT adjustments and fills — one dispatch per tile
batch. Viewport resampling (the zoom/pan/rotate path) moves to
compositor/src/viewport.rs with the same dual CPU/GPU implementation.

The CPU compositor stays the semantic reference: set_backend routes the
public composite_* functions, so every call site accelerates untouched,
and parity tests hold the shader to ±1 RGBA8 step across blend modes,
masks, groups, clip runs, adjustments, bit depths, regions and all
viewport sampling modes. Whatever the shader can't express — the four
non-LUT adjustment kinds, layers mid-drag, nesting past the fixed
stack — falls back to the CPU reference per call.

On by default with an adapter, off cleanly without one; toggle in
Preferences ▸ Rendering, override with SCHIST_GPU=0|1. naga 27 needs its
"termcolor" feature once gpui's tree enables codespan-reporting's, hence
the explicit naga dependency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Newer clippy flags chunks_exact with a constant size; as_chunks is
also the idiom pixel-ops already uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wgpu-hal 27's DX12 backend hands gpu-allocator windows-0.58 types, but
the incremental lock resolution had attached gpu-allocator's wide
windows range (>=0.53,<=0.58) to the 0.57 entry gpui's sysinfo keeps
alive — which only fails to compile on Windows. Re-point that one edge;
both windows versions stay in the graph for the crates that need them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Metal's legal fma contraction shifted a backdrop by one ULP across
Color Burn's b=1 discontinuity on the macOS runner — 3 channels in
360k, off by up to 154. Discontinuous blend functions amplify one-ULP
input differences arbitrarily, so the parity harness now grants 0.01%
of channels for such boundary hits while keeping the ±1 rounding bar
for everything else.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On DX12 the composite module's two entry points shared overlapping
bind-group slots, which naga's HLSL backend rejects at pipeline
creation — and the failure only reached a logger, so dispatches
silently no-opped and readbacks returned zeroed buffers (every parity
test red on the Windows runner while the viewport module passed).

Split pack_rgba8 into pack.wgsl, and wrap pipeline creation and every
batch in validation error scopes: a backend that can't run these
shaders now reports 'no GPU' or falls back to the CPU per call instead
of compositing garbage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FXC, the default DX12 shader compiler, miscompiles the switch-heavy op
interpreter — Color Burn and the f32 source path came back as noise on
the Windows runner's WARP adapter while every simpler mode passed.
Statically link DXC and select it (unless WGPU_DX12_COMPILER says
otherwise). Also take a mutex around each batch: error scopes are a
per-device stack, so the multithreaded test runner had threads popping
each other's scopes and blaming the wrong caller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@IAmJSD
IAmJSD merged commit fb6282c into main Aug 23, 2026
3 checks passed
@IAmJSD
IAmJSD deleted the t3code/fc0d1fea branch August 23, 2026 19:20
@IAmJSD

IAmJSD commented Aug 23, 2026

Copy link
Copy Markdown
Member Author

CI is green on all three platforms. Getting there hardened the GPU backend against real driver diversity:

  • DX12/WARP: FXC (the default D3D compiler) miscompiled the switch-heavy op interpreter — Color Burn and the f32 source path returned noise while simpler modes passed. The statically-linked DXC is now selected on Windows (WGPU_DX12_COMPILER still wins). Separately, the pack kernel moved to its own WGSL module because naga's HLSL backend rejects overlapping bind slots across entry points that Vulkan/Metal tolerate.
  • Metal: fma contraction across Color Burn's b=1 discontinuity flipped 3 channels in 360k; the parity harness now allows 0.01% boundary-pixel slack while keeping the ±1 rounding bar.
  • Pipeline creation and every batch now run inside validation error scopes (serialized by a mutex), so a backend that can't run these shaders reports "no GPU" or falls back to the CPU per call instead of silently compositing zeroed buffers — which is exactly how the DX12 failure originally masked itself.

Net effect: the parity suite now genuinely executes on Vulkan (lavapipe), Metal, and DX12 (WARP + DXC) in CI.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant