feat(webgpu): HDR post-process primitives for bloom (#27) #28

Merged
catbot merged 2 commits from claude/issue-27 into master 2026-06-09 14:56:03 +02:00
Member

Implements the minimal WebGPU-backend surface to unblock a cross-backend HDR bloom / post-process chain (3DForts #130). Closes the four gaps from the scoping issue.

What changed

Gap 1 — float/HDR textures. WebGPU::wgpuCreateStorageImage2D(w, h, format) + a StorageImage2D type allocate a runtime texture with STORAGE_BINDING | TEXTURE_BINDING | COPY_SRC | COPY_DST and a caller-chosen WebGPUTexelFormat (rgba16float / r32float / rgba8unorm), with no CompressedTextureAsset upload.

Gap 2 — writable storage-texture binding. New UICustomBindingKind::StorageTexture (kind 5) carries a texel format in the freed UICustomBinding::format byte and maps to texture_storage_2d<FMT, write> across all three binding paths (UI custom shader, RT pipeline, plain compute). Float textures already sample through the existing SampledTexture kind (sampleType: float, filterable per spec), so a pass can write an rgba16float mip and a later pass can sample it.

Gap 3 — pre-tonemap linear radiance (option 3a). PipelineRTWebGPU::Init(..., hdrOutputFormat) makes the wavefront RESOLVE stage write the linear accumulator into a user rgba16float texture (RTPass::outTexHandle) instead of the rgba8unorm canvas. The JS bridge swaps binding(6)'s WGSL declaration + bind-group-layout format and skips the ping-pong flip (canvas untouched). Default RGBA8Unorm keeps the canvas path byte-identical — existing RT examples are unaffected.

Gap 4 — Vulkan symmetry (confirmed, no code). The native present path (Window.cpp) records window.passes generically, inserts a SHADER_WRITE → SHADER_READ|WRITE memory barrier between passes, then transitions whatever swapchain image is bound to PRESENT_SRC and presents. It makes no assumption that the final RT/UI pass targets the swapchain, so RT → offscreen rgba16float heap image → composite RenderPass → swapchain already works today.

Validation

  • crafter-build test (native) — green (PushConstantRewrite).
  • New examples/HDRBloom builds for wasm32-wasip1 and runs in Firefox with WebGPU: RT → linear rgba16float scene → threshold → blur (both over float storage targets) → composite (Reinhard tonemap + gamma) → canvas. No WGSL/validation errors; bright emitter cubes bloom, dim Lambert cubes stay sharp.
  • examples/RTStress rebuilds + renders unchanged (backward-compat of the RT path and the _padformat rename).

The threshold/blur passes dispatch from onBeforeUpdate so each lands on its own queue submit — WebGPU only provides the storage-write → sampled-read barrier between submits (or between compute passes), never between dispatches within one pass. The composite runs in-frame as a UI custom shader so it owns the canvas ping-pong.

Screenshots

Resolves #27

HDRBloom — bright cubes glow, dim cubes sharp

Implements the minimal WebGPU-backend surface to unblock a cross-backend HDR bloom / post-process chain (3DForts #130). Closes the four gaps from the scoping issue. ## What changed **Gap 1 — float/HDR textures.** `WebGPU::wgpuCreateStorageImage2D(w, h, format)` + a `StorageImage2D` type allocate a runtime texture with `STORAGE_BINDING | TEXTURE_BINDING | COPY_SRC | COPY_DST` and a caller-chosen `WebGPUTexelFormat` (rgba16float / r32float / rgba8unorm), with no `CompressedTextureAsset` upload. **Gap 2 — writable storage-texture binding.** New `UICustomBindingKind::StorageTexture` (kind 5) carries a texel format in the freed `UICustomBinding::format` byte and maps to `texture_storage_2d<FMT, write>` across all three binding paths (UI custom shader, RT pipeline, plain compute). Float textures already sample through the existing `SampledTexture` kind (`sampleType: float`, filterable per spec), so a pass can write an `rgba16float` mip and a later pass can sample it. **Gap 3 — pre-tonemap linear radiance (option 3a).** `PipelineRTWebGPU::Init(..., hdrOutputFormat)` makes the wavefront RESOLVE stage write the linear accumulator into a user `rgba16float` texture (`RTPass::outTexHandle`) instead of the rgba8unorm canvas. The JS bridge swaps binding(6)'s WGSL declaration + bind-group-layout format and skips the ping-pong flip (canvas untouched). Default `RGBA8Unorm` keeps the canvas path byte-identical — existing RT examples are unaffected. **Gap 4 — Vulkan symmetry (confirmed, no code).** The native present path (`Window.cpp`) records `window.passes` generically, inserts a `SHADER_WRITE → SHADER_READ|WRITE` memory barrier *between* passes, then transitions whatever swapchain image is bound to `PRESENT_SRC` and presents. It makes no assumption that the final RT/UI pass targets the swapchain, so RT → offscreen `rgba16float` heap image → composite `RenderPass` → swapchain already works today. ## Validation - `crafter-build test` (native) — green (`PushConstantRewrite`). - New `examples/HDRBloom` builds for `wasm32-wasip1` and runs in Firefox with WebGPU: RT → linear `rgba16float` scene → threshold → blur (both over float storage targets) → composite (Reinhard tonemap + gamma) → canvas. No WGSL/validation errors; bright emitter cubes bloom, dim Lambert cubes stay sharp. - `examples/RTStress` rebuilds + renders unchanged (backward-compat of the RT path and the `_pad`→`format` rename). The threshold/blur passes dispatch from `onBeforeUpdate` so each lands on its own queue submit — WebGPU only provides the storage-write → sampled-read barrier between submits (or between compute passes), never between dispatches within one pass. The composite runs in-frame as a UI custom shader so it owns the canvas ping-pong. ## Screenshots Resolves #27 ![HDRBloom — bright cubes glow, dim cubes sharp](https://forgejo.catcrafts.net/attachments/9b5154ae-13bf-4ae8-9037-1f44152d99a7)
Adds the minimal WebGPU-backend surface an HDR bloom/post chain needs:

1. wgpuCreateStorageImage2D + StorageImage2D<>: runtime rgba16float (or
   other format) textures with STORAGE_BINDING|TEXTURE_BINDING usage, no
   CompressedTextureAsset upload — GPU-produced HDR targets.
2. UICustomBindingKind::StorageTexture (kind 5): write-only storage-texture
   binding carrying a texel format (the freed UICustomBinding::format byte),
   wired through all three binding paths (UI custom / RT / plain compute).
   Float textures already sample via SampledTexture (sampleType float).
3. PipelineRTWebGPU::Init(..., hdrOutputFormat): RESOLVE writes the linear
   accumulator into a user rgba16float texture (RTPass::outTexHandle)
   instead of the rgba8unorm canvas, leaving the composite→swapchain pass to
   the app. Default RGBA8Unorm keeps the canvas path byte-identical.

Existing RT examples updated for the _pad→format field rename; the default
paths are unchanged (verified RTStress builds + renders).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RT→rgba16float→threshold→blur→composite, end-to-end proof of the three
primitives. threshold/blur run from onBeforeUpdate (one submit each) so the
storage-write→sampled-read dependency gets WebGPU's per-submit barrier
(there is none between dispatches within a compute pass); the composite is a
UI custom shader so it owns the canvas ping-pong. Documents the Vulkan
symmetry (gap 4): the native present path records passes generically and
barriers between them, so the same chain is wireable today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot merged commit c58424d519 into master 2026-06-09 14:56:03 +02:00
catbot deleted branch claude/issue-27 2026-06-09 14:56:03 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Catcrafts/Crafter.Graphics!28
No description provided.