Skip to content

WebGPURenderer: Make MSAA with MRT work with WebGL backend. #31228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 6, 2025

Conversation

Mugen87
Copy link
Collaborator

@Mugen87 Mugen87 commented Jun 5, 2025

Fixed #30995.

Description

The PR tries to make the combination of MSAA and MRT work for the WebGL backend.

Unfortunately, the combination only works for the first attachment so far. I don't understand why additional attachments are not properly blit although the WebGL backend uses the same approach like in WebGLRenderer. The problem can be reproduced with below example (it always runs with the WebGL backend for testing).

https://rawcdn.githack.com/Mugen87/three.js/c4d55169b9dc56732cf43e5143cd31226a6d5c8e/examples/webgpu_mrt.html

The expected result looks like so: https://threejs.org/examples/webgpu_mrt

@RenaudRohlinger Since you have implemented the initial support in WebGLRenderer, did you ever encounter such a problem?

@cabanier I do not see any WebGL warnings in the browser console but do you see an obvious flaw in the framebuffer setup? I'm a bit out of ideas at the moment why the blit works partially but without reporting errors. A fresh pair of eyes would help^^.

The PR only implements the resolve. The setup of MSAA render buffers for each attachment has already been implemented by #27473.

if ( samples > 0 && useMultisampledRTT === false && ! renderTarget.multiview ) {
if ( msaaFb === undefined ) {
const invalidationArray = [];
msaaFb = gl.createFramebuffer();
state.bindFramebuffer( gl.FRAMEBUFFER, msaaFb );
const msaaRenderbuffers = [];
const textures = descriptor.textures;
for ( let i = 0; i < textures.length; i ++ ) {
msaaRenderbuffers[ i ] = gl.createRenderbuffer();
gl.bindRenderbuffer( gl.RENDERBUFFER, msaaRenderbuffers[ i ] );
invalidationArray.push( gl.COLOR_ATTACHMENT0 + i );
if ( depthBuffer ) {
const depthStyle = stencilBuffer ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT;
invalidationArray.push( depthStyle );
}
const texture = descriptor.textures[ i ];
const textureData = this.get( texture );
gl.renderbufferStorageMultisample( gl.RENDERBUFFER, samples, textureData.glInternalFormat, descriptor.width, descriptor.height );
gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.RENDERBUFFER, msaaRenderbuffers[ i ] );
}
renderTargetContextData.msaaFrameBuffer = msaaFb;
renderTargetContextData.msaaRenderbuffers = msaaRenderbuffers;
if ( depthRenderbuffer === undefined ) {
depthRenderbuffer = gl.createRenderbuffer();
this.textureUtils.setupRenderBufferStorage( depthRenderbuffer, descriptor, samples );
renderTargetContextData.depthRenderbuffer = depthRenderbuffer;
const depthStyle = stencilBuffer ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT;
invalidationArray.push( depthStyle );
}
renderTargetContextData.invalidationArray = invalidationArray;
}
currentFrameBuffer = renderTargetContextData.msaaFrameBuffer;
} else {

Copy link

github-actions bot commented Jun 5, 2025

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 337.4
78.67
337.4
78.67
+0 B
+0 B
WebGPU 553.89
153.48
554.6
153.57
+707 B
+96 B
WebGPU Nodes 553.24
153.32
553.95
153.42
+707 B
+97 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 468.59
113.33
468.59
113.33
+0 B
+0 B
WebGPU 628.86
170.19
629.57
170.29
+707 B
+104 B
WebGPU Nodes 583.72
159.53
584.42
159.62
+707 B
+96 B

for ( let i = 0; i < textures.length; i ++ ) {

const { textureGPU } = this.get( textures[ i ] );

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure but I think you should bind the MSAA FBO and the FBO here @Mugen87:

state.bindFramebuffer( _gl.FRAMEBUFFER, msaaFrameBuffer);

gl.framebufferRenderbuffer( gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.RENDERBUFFER, msaaRenderbuffers[ i ] );

state.bindFramebuffer( _gl.FRAMEBUFFER, fb);

gl.framebufferTexture2D( gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, textureGPU, 0 );

Copy link
Collaborator

@RenaudRohlinger RenaudRohlinger Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also unbind before the loop:

state.bindFramebuffer( _gl.READ_FRAMEBUFFER, null );
state.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, null );

and rebind after:

state.bindFramebuffer( gl.READ_FRAMEBUFFER, msaaFrameBuffer );

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, that does not do any difference. In fact, it seems some bindings in WebGLRenderer are redundant. A single bind of the draw and read buffer at the beginning of the blit process should be sufficinet. Removing all these bindings in WebGLRenderer does not break webgl_multiple_rendertargets.

@cabanier
Copy link
Contributor

cabanier commented Jun 5, 2025

@cabanier I do not see any WebGL warnings in the browser console but do you see an obvious flaw in the framebuffer setup? I'm a bit out of ideas at the moment why the blit works partially but without reporting errors. A fresh pair of eyes would help^^.

Could this be that you're trying to read from the renderbuffers or before they're resolved to textures?
I ran your example on Quest (which supports rendering directly to a texture) and it was rendering correctly.

@cabanier
Copy link
Contributor

cabanier commented Jun 5, 2025

@Mugen87 did you figure it out? I'm debugging with renderdoc to see where things go wrong but if you already solved it, I can stop.

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Jun 5, 2025

10 minutes ago I found out a missing call of gl.drawBuffers() is the issue^^.

When I understand the WebGL spec correctly, whatever you do with gl.drawBuffers() is part of the current bound framebuffer state. When we reoder the attachments during the resolve, it seems this state is lost so we must make sure gl.drawBuffers() is called again for the respective framebuffer.

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Jun 5, 2025

In any event, thanks for support! The hint with the renderbuffers was a good starting point.

If all works, I think we can back-port part of this implementation to WebGLRenderer since it works with less gl.bindFramebuffer() commands during the resolve.

@Mugen87 Mugen87 marked this pull request as ready for review June 5, 2025 21:32
@Mugen87 Mugen87 added this to the r178 milestone Jun 5, 2025
@Mugen87 Mugen87 merged commit 25694af into mrdoob:dev Jun 6, 2025
12 checks passed
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.

Does the WebGPU renderer not support MSAA anti aliasing when using WebGL mode?
3 participants