Bloom / HDR post-process primitives for the WebGPU backend #27

Closed
opened 2026-06-09 14:31:14 +02:00 by jorijnvdgraaf · 0 comments

Summary

Downstream (3DForts #130) wants a bloom effect — an HDR post-process chain (threshold → downsample → blur → upsample → composite+tonemap) running on both backends. Auditing the framework, the Vulkan path can already express this, but the WebGPU backend is missing the primitives an HDR post chain needs. This issue tracks the minimal upstream surface to unblock a cross-backend HDR post-process.

What already works (no change needed)

  • User compute passes, both backends: :PlainComputeShader (WebGPU) / :ComputeShader (Vulkan), dispatched via custom RenderPass subclasses pushed onto window.passes. A multi-pass post chain is expressible.
  • RESOLVE wavefront stage (WebGPURTStage::Resolve, resolve_main(coord, hdr) -> vec4) — a per-pixel tonemap/gamma hook, used by the RTStress/Sponza examples.
  • Vulkan is already capable. Image2D<T> aliases ImageVulkan<T> with full VkFormat (e.g. R16G16B16A16_SFLOAT), storage usage, mip levels, and SamplerVulkan filtering. The RT pipeline writes a heap-bound output slot, so an offscreen HDR target + a composite RenderPass is wireable without framework changes. (Worth confirming the present path doesn't assume RT/UI writes the swapchain directly — see ask 4.)

Gaps (WebGPU backend)

1. No float/HDR GPU textures

Every texture-creation bridge import hardcodes its format and is CPU-upload-only:

  • WebGPU::wgpuCreateImage2D(w, h)rgba8unorm (Crafter.Graphics-WebGPU.cppm)
  • wgpuCreateImage2DArray(...)rgba8unorm layers
  • wgpuCreateAtlasTexture(...)r8unorm
  • Image2D<T> (DOM) is "a thin handle around an rgba8unorm GPUTexture; the only update path is from a CompressedTextureAsset" (Crafter.Graphics-Image2D.cppm).

There is no way to allocate an rgba16float (or any non-rgba8unorm) texture at runtime, and no usage flags for STORAGE_BINDING / RENDER_ATTACHMENT.

Ask: a runtime texture-create path that takes a format (at least rgba16float) and the STORAGE_BINDING | TEXTURE_BINDING usages, with no CompressedTextureAsset upload requirement.

2. No writable storage-texture binding for user textures

UICustomBindingKind (Crafter.Graphics-WebGPUComputeShader.cppm) is Buffer / BufferReadWrite / SampledTexture / SampledTextureArray / Sampler. The only writable texture a compute dispatch gets is the library-managed rgba8unorm ping-pong pair at @group(1) (out: texture_storage_2d<rgba8unorm, write>, prev: texture_2d<f32>).

A bloom mip pyramid needs N distinct writable float targets at different resolutions, bound at arbitrary @group/@binding — the single fixed ping-pong pair can't express it.

Ask: a StorageTexture (write) UICustomBindingKind carrying a texel format, so a compute pass can write a user-owned rgba16float texture at a user-chosen group/binding; and sampling of float textures via the existing SampledTexture kind (sample type float, filterable — rgba16float is filterable per spec).

3. No access to pre-tonemap linear radiance

The wavefront's linear accumulator (wfAccum, array<vec4<f32>> per pixel — see WAVEFRONT-DESIGN.md) is internal, and RESOLVE collapses it directly to the rgba8unorm out image. There's no HDR buffer a downstream pass can threshold/blur, and resolve_main runs after the post chain would need its input.

Ask: one of —

  • (a) let RESOLVE write to a user-provided rgba16float target instead of the rgba8unorm out image (the app then owns the final composite→swapchain pass), or
  • (b) expose wfAccum as a sampleable texture / readable buffer to user passes, or
  • (c) let PipelineRTWebGPU render its out image to a user-provided HDR texture rather than the canvas, so the app's own composite RenderPass does tonemap+gamma+present.

(a) is the smallest change and keeps the existing stage model.

4. Symmetric Vulkan confirmation

Confirm the Vulkan present path supports: RT → offscreen rgba16float heap image, then a user composite RenderPass that writes the swapchain. The heap outImageSlot indirection suggests this already works; flagging in case the present/acquire path assumes the final RT/UI pass targets the swapchain image directly.

Minimal surface that unblocks bloom

  1. WebGPU: runtime rgba16float texture create with storage+sampled usage (gap 1).
  2. WebGPU: StorageTexture write binding kind w/ format + float-texture sampling (gap 2).
  3. WebGPU: a way to get the linear HDR out of the wavefront before tonemap — preferably 3(a) (gap 3).
  4. Confirm/observe the Vulkan offscreen-HDR-target + composite-present path (gap 4).

With these, the bloom chain is identical-shaped on both backends: RT → linear rgba16float → threshold/down/up compute passes over float mip targets → composite (tonemap+gamma) → swapchain.


Filed from a 3DForts #130 (bloom) scoping pass. File refs are against the build-cache-pinned commit; the master checkout predates the WebGPU/wavefront backend.

## Summary Downstream (3DForts #130) wants a **bloom** effect — an HDR post-process chain (threshold → downsample → blur → upsample → composite+tonemap) running on **both** backends. Auditing the framework, the Vulkan path can already express this, but the **WebGPU backend is missing the primitives an HDR post chain needs**. This issue tracks the minimal upstream surface to unblock a cross-backend HDR post-process. ## What already works (no change needed) - **User compute passes**, both backends: `:PlainComputeShader` (WebGPU) / `:ComputeShader` (Vulkan), dispatched via custom `RenderPass` subclasses pushed onto `window.passes`. A multi-pass post chain is expressible. - **`RESOLVE` wavefront stage** (`WebGPURTStage::Resolve`, `resolve_main(coord, hdr) -> vec4`) — a per-pixel tonemap/gamma hook, used by the `RTStress`/`Sponza` examples. - **Vulkan is already capable.** `Image2D<T>` aliases `ImageVulkan<T>` with full `VkFormat` (e.g. `R16G16B16A16_SFLOAT`), storage usage, mip levels, and `SamplerVulkan` filtering. The RT pipeline writes a heap-bound output slot, so an offscreen HDR target + a composite `RenderPass` is wireable without framework changes. (Worth confirming the present path doesn't *assume* RT/UI writes the swapchain directly — see ask 4.) ## Gaps (WebGPU backend) ### 1. No float/HDR GPU textures Every texture-creation bridge import hardcodes its format and is CPU-upload-only: - `WebGPU::wgpuCreateImage2D(w, h)` → `rgba8unorm` (`Crafter.Graphics-WebGPU.cppm`) - `wgpuCreateImage2DArray(...)` → `rgba8unorm` layers - `wgpuCreateAtlasTexture(...)` → `r8unorm` - `Image2D<T>` (DOM) is "a thin handle around an rgba8unorm GPUTexture; the only update path is from a CompressedTextureAsset" (`Crafter.Graphics-Image2D.cppm`). There is no way to allocate an `rgba16float` (or any non-`rgba8unorm`) texture at runtime, and no usage flags for `STORAGE_BINDING` / `RENDER_ATTACHMENT`. **Ask:** a runtime texture-create path that takes a format (at least `rgba16float`) and the `STORAGE_BINDING | TEXTURE_BINDING` usages, with no `CompressedTextureAsset` upload requirement. ### 2. No writable storage-texture binding for user textures `UICustomBindingKind` (`Crafter.Graphics-WebGPUComputeShader.cppm`) is `Buffer / BufferReadWrite / SampledTexture / SampledTextureArray / Sampler`. The only writable texture a compute dispatch gets is the library-managed `rgba8unorm` ping-pong pair at `@group(1)` (`out: texture_storage_2d<rgba8unorm, write>`, `prev: texture_2d<f32>`). A bloom mip pyramid needs *N* distinct writable float targets at different resolutions, bound at arbitrary `@group/@binding` — the single fixed ping-pong pair can't express it. **Ask:** a `StorageTexture` (write) `UICustomBindingKind` carrying a texel format, so a compute pass can write a user-owned `rgba16float` texture at a user-chosen group/binding; and sampling of float textures via the existing `SampledTexture` kind (sample type `float`, filterable — `rgba16float` is filterable per spec). ### 3. No access to pre-tonemap linear radiance The wavefront's linear accumulator (`wfAccum`, `array<vec4<f32>>` per pixel — see `WAVEFRONT-DESIGN.md`) is internal, and `RESOLVE` collapses it directly to the `rgba8unorm` out image. There's no HDR buffer a downstream pass can threshold/blur, and `resolve_main` runs *after* the post chain would need its input. **Ask:** one of — - (a) let `RESOLVE` write to a user-provided `rgba16float` target instead of the `rgba8unorm` out image (the app then owns the final composite→swapchain pass), **or** - (b) expose `wfAccum` as a sampleable texture / readable buffer to user passes, **or** - (c) let `PipelineRTWebGPU` render its out image to a user-provided HDR texture rather than the canvas, so the app's own composite `RenderPass` does tonemap+gamma+present. (a) is the smallest change and keeps the existing stage model. ### 4. Symmetric Vulkan confirmation Confirm the Vulkan present path supports: RT → offscreen `rgba16float` heap image, then a user composite `RenderPass` that writes the swapchain. The heap `outImageSlot` indirection suggests this already works; flagging in case the present/acquire path assumes the final RT/UI pass targets the swapchain image directly. ## Minimal surface that unblocks bloom 1. WebGPU: runtime `rgba16float` texture create with storage+sampled usage (gap 1). 2. WebGPU: `StorageTexture` write binding kind w/ format + float-texture sampling (gap 2). 3. WebGPU: a way to get the linear HDR out of the wavefront before tonemap — preferably 3(a) (gap 3). 4. Confirm/observe the Vulkan offscreen-HDR-target + composite-present path (gap 4). With these, the bloom chain is identical-shaped on both backends: RT → linear `rgba16float` → threshold/down/up compute passes over float mip targets → composite (tonemap+gamma) → swapchain. --- *Filed from a 3DForts #130 (bloom) scoping pass. File refs are against the build-cache-pinned commit; the `master` checkout predates the WebGPU/wavefront backend.*
Sign in to join this conversation.
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#27
No description provided.