Skip to content

WebGPU: allow sampling multisampled render targets (custom MSAA resolves for HDR color + depth) #9059

Description

@SashaRX

Area: graphics · WebGPU

Problem

On WebGPU there is currently no supported way to bind a multisampled render
target as a texture in a user shader. Two things block a custom MSAA resolve:

  1. The multisampled COLOR buffer is created with RENDER_ATTACHMENT usage only —
    there is no option to add TEXTURE_BINDING, so its samples can never be read
    in a shader. See WebgpuRenderTarget.initColor
    (src/platform/graphics/webgpu/webgpu-render-target.js):

    const multisampledTextureDesc = {
      size: [width, height, 1], dimension: '2d', sampleCount: samples, format,
      usage: transientColor
        ? GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TRANSIENT_ATTACHMENT
        : GPUTextureUsage.RENDER_ATTACHMENT   // <- no TEXTURE_BINDING option
    };
  2. BindGroupFormat/BindTextureFormat cannot express a multisampled texture
    binding. BindTextureFormat's constructor has no multisampled parameter, and
    WebgpuBindGroupFormat hardcodes it off
    (src/platform/graphics/webgpu/webgpu-bind-group-format.js):

    const multisampled = false;   // <- never true
    ...
    texture: { sampleType: gpuSampleType, viewDimension, multisampled }

    So even though the WGSL reflector already recognises texture_multisampled_2d
    and texture_depth_multisampled_2d
    (webgpu-shader-processor-wgsl.js, textureBaseInfo), a shader that binds one
    gets a bind-group layout that says multisampled: false → validation error.

The engine already does exactly this resolve internally — WebgpuResolver
(src/platform/graphics/webgpu/webgpu-resolver.js) binds
texture_depth_multisampled_2d, does textureLoad(img, coord, 0u), and runs a raw
layout: "auto" pipeline that bypasses BindGroupFormat. The capability exists;
it just isn't reachable from user code. (Internal MS depth buffers even opt into
TEXTURE_BINDING already — else if (samples > 1) in the depth init path — but MS
color never does, and neither is bindable through a BindGroupFormat.)

This is the failure class the WebGPU spec deliberately pushed to userland: there is
no resolveTarget for depth (gpuweb/gpuweb#108), and engines that need per-sample
resolves do a manual pass — the same approach Godot took for its depth resolve
(godotengine/godot#80991).

Use case

A planetary-atmosphere HDR pipeline (Hillaire 2020 scattering on shell geometry,
WebGPU + WebGL2) needs custom MSAA resolves that the hardware box resolve
cannot provide:

  • Color: a reversible / policy-driven color resolve (the UE4/Karis
    "reversible tonemap" resolve family). On clean frames we tonemap-in-resolve
    per sample straight to LDR; when HDR post passes still follow (volumetric fog,
    aerial perspective) we need a coverage-linear per-sample mean kept in HDR so the
    later composite scene·T + L stays energy-consistent with the resolved depth.
    The hardware resolve gives neither.
  • Depth: depth-consuming post passes (fog, aerial perspective) need the
    multisampled scene depth resolved into custom channels — coverage-weighted mean,
    MIN (nearest-geometry classifier), and coverage — not sample 0 and not an
    averaged depth. That requires reading all samples of the MS depth buffer in a
    shader.

Current workaround (honest)

  1. Monkey-patch device.wgpu.createTexture to OR TEXTURE_BINDING into the usage
    of multisampled HDR-color and depth textures before the scene RT initialises.
  2. Run the resolves as raw-WebGPU fullscreen passes with a hand-built
    createBindGroupLayout({ texture: { multisampled: true, sampleType: 'unfilterable-float' } }), bypassing BindGroupFormat entirely (exactly as the
    internal WebgpuResolver does), encoded into the engine's shared frame command
    encoder via getCommandEncoder() so ordering vs. the scene render is correct.

This works but reaches into private engine internals (rt.impl.colorAttachments[0] .multisampledBuffer, texture.impl.gpuTexture, getCommandEncoder) and breaks any
time those shapes change.

Proposed API

  • A RenderTarget/Texture opt-in to make the multisampled color buffer
    bindable, e.g. new RenderTarget({ ..., samples: 4, bindMultisampled: true })
    (or a texture flag), which ORs TEXTURE_BINDING into the MS color descriptor —
    mirroring the TEXTURE_BINDING the depth path already sets.
  • Multisampled support in BindGroupFormat: a multisampled flag on
    BindTextureFormat (and matching texture_depth_multisampled_2d), so
    WebgpuBindGroupFormat can emit multisampled: true for
    texture_multisampled_2d / texture_depth_multisampled_2d bindings the WGSL
    reflector already understands. A TEXTUREDIMENSION_2D_MS enum could carry it, or
    a boolean on the format — the reflection side is already in place.
  • Optionally, expose the existing WebgpuResolver depth-resolve entry point (or a
    general per-sample resolve hook) so users don't re-implement the raw pipeline.

Prior internal art that already does most of this: #5485 (custom shader-based MS
depth resolve), #6917/#6932 (partial MS-RT support). Adjacent precedent for exposing
WebGPU texture-bind controls to user shaders: #8734.

Happy to contribute a PR if the shape is agreed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions