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:
-
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
};
-
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)
- Monkey-patch
device.wgpu.createTexture to OR TEXTURE_BINDING into the usage
of multisampled HDR-color and depth textures before the scene RT initialises.
- 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.
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:
The multisampled COLOR buffer is created with
RENDER_ATTACHMENTusage only —there is no option to add
TEXTURE_BINDING, so its samples can never be readin a shader. See
WebgpuRenderTarget.initColor(
src/platform/graphics/webgpu/webgpu-render-target.js):BindGroupFormat/BindTextureFormatcannot express a multisampled texturebinding.
BindTextureFormat's constructor has nomultisampledparameter, andWebgpuBindGroupFormathardcodes it off(
src/platform/graphics/webgpu/webgpu-bind-group-format.js):So even though the WGSL reflector already recognises
texture_multisampled_2dand
texture_depth_multisampled_2d(
webgpu-shader-processor-wgsl.js,textureBaseInfo), a shader that binds onegets 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) bindstexture_depth_multisampled_2d, doestextureLoad(img, coord, 0u), and runs a rawlayout: "auto"pipeline that bypassesBindGroupFormat. The capability exists;it just isn't reachable from user code. (Internal MS depth buffers even opt into
TEXTURE_BINDINGalready —else if (samples > 1)in the depth init path — but MScolor never does, and neither is bindable through a
BindGroupFormat.)This is the failure class the WebGPU spec deliberately pushed to userland: there is
no
resolveTargetfor depth (gpuweb/gpuweb#108), and engines that need per-sampleresolves 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:
"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 + Lstays energy-consistent with the resolved depth.The hardware resolve gives neither.
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)
device.wgpu.createTextureto ORTEXTURE_BINDINGinto the usageof multisampled HDR-color and depth textures before the scene RT initialises.
createBindGroupLayout({ texture: { multisampled: true, sampleType: 'unfilterable-float' } }), bypassingBindGroupFormatentirely (exactly as theinternal
WebgpuResolverdoes), encoded into the engine's shared frame commandencoder 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 anytime those shapes change.
Proposed API
RenderTarget/Textureopt-in to make the multisampled color bufferbindable, e.g.
new RenderTarget({ ..., samples: 4, bindMultisampled: true })(or a texture flag), which ORs
TEXTURE_BINDINGinto the MS color descriptor —mirroring the
TEXTURE_BINDINGthe depth path already sets.BindGroupFormat: amultisampledflag onBindTextureFormat(and matchingtexture_depth_multisampled_2d), soWebgpuBindGroupFormatcan emitmultisampled: truefortexture_multisampled_2d/texture_depth_multisampled_2dbindings the WGSLreflector already understands. A
TEXTUREDIMENSION_2D_MSenum could carry it, ora boolean on the format — the reflection side is already in place.
WebgpuResolverdepth-resolve entry point (or ageneral 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.