Summary
When N8AO runs inside @react-three/postprocessing's EffectComposer, Chrome's WebGL2 driver emits a continuous warning on every frame:
[.WebGL-XXXXXX] GL_INVALID_OPERATION: glBlitFramebuffer: Read and write depth stencil attachments cannot be the same image.
Rate: ~200/sec, indefinitely. The scene still renders correctly and AO output looks fine, but the dev console is unusable.
Environment
|
|
n8ao |
1.10.1 |
@react-three/postprocessing |
2.19.1 |
postprocessing |
6.39.1 |
@react-three/fiber |
8.18.0 |
three |
0.184.0 |
| Browser |
Chrome (current stable, macOS) |
| WebGL |
WebGL2 |
Minimal config that reproduces
<Canvas gl={{ antialias: false, stencil: false }}>
<EffectComposer multisampling={4} stencilBuffer={false}>
<N8AO aoRadius={6} intensity={5} quality="medium" halfRes />
</EffectComposer>
{/* ... scene ... */}
</Canvas>
Diagnosis
The warning is WebGL2's strict validation that glBlitFramebuffer cannot have the same image attached as both the source-read and dest-write depth-stencil attachment.
In dist/N8AO.js:1357 (and the equivalent at :2033):
if (this.configuration.halfRes) {
this.depthDownsampleTarget = new WebGLMultipleRenderTargets(this.width/2, this.height/2, 2);
if (REVISION <= 161) this.depthDownsampleTarget.textures = this.depthDownsampleTarget.texture;
this.depthDownsampleTarget.textures[0].format = RedFormat;
this.depthDownsampleTarget.textures[0].type = FloatType;
// ...
}
When halfRes=true, N8AO allocates a WebGLMultipleRenderTargets (MRT). Three.js, by default, shares the depth-stencil texture between the MRT and the parent inputBuffer of the composer for memory efficiency. When N8AO blits from inputBuffer to depthDownsampleTarget, Chrome's WebGL2 driver flags the operation: the source FB and dest FB hold the same depth-stencil image, which violates the spec.
Disabling halfRes reduces but does not eliminate the spam, writeTargetInternal (:1684) and accumulationRenderTarget (:1622) appear to exhibit the same shared-depth pattern in the AO and accumulation passes.
What we tried (none fixed it)
| Attempt |
Result |
<EffectComposer stencilBuffer={false}> (matches gl.stencil:false) |
Reduced spam ~25% but persists |
Upgrade postprocessing 6.36 → 6.39.1 |
No effect |
halfRes={false} on N8AO |
Still spams (other passes) |
<EffectComposer multisampling={0}> |
Still spams |
Swap to <SSAO /> with enableNormalPass |
Same warning pattern (NormalPass triggers it) |
Upgrade three 0.169 → 0.184 |
Zero effect on the warning |
So this is not three.js / postprocessing / Chrome version-specific, it reproduces across the full version matrix once strict WebGL2 validation is enabled.
Suggested fix
Allocate N8AO's internal render targets with their own depth-stencil textures rather than letting three.js share with inputBuffer. Concretely, on each WebGLRenderTarget / WebGLMultipleRenderTargets created by N8AO:
target.depthTexture = new THREE.DepthTexture(w, h);
target.depthTexture.format = THREE.DepthFormat;
target.depthTexture.type = THREE.UnsignedIntType; // or UnsignedInt248Type for depth+stencil
This forces three.js to skip the implicit depth-sharing logic. Same depth-data is computed (N8AO copies into its own depth via the downsample step), but the underlying GL texture is distinct → the blit no longer violates the read/write same-image rule.
Alternatively: when blitting from inputBuffer to N8AO's targets, mask the GL_DEPTH_BUFFER_BIT / GL_STENCIL_BUFFER_BIT out of the blit mask (only GL_COLOR_BUFFER_BIT is needed in those copies). This is what three.js's MSAA resolve does in newer versions when it detects shared depth.
Summary
When N8AO runs inside
@react-three/postprocessing'sEffectComposer, Chrome's WebGL2 driver emits a continuous warning on every frame:Rate: ~200/sec, indefinitely. The scene still renders correctly and AO output looks fine, but the dev console is unusable.
Environment
n8ao@react-three/postprocessingpostprocessing@react-three/fiberthreeMinimal config that reproduces
Diagnosis
The warning is WebGL2's strict validation that
glBlitFramebuffercannot have the same image attached as both the source-read and dest-write depth-stencil attachment.In
dist/N8AO.js:1357(and the equivalent at:2033):When
halfRes=true, N8AO allocates aWebGLMultipleRenderTargets(MRT). Three.js, by default, shares the depth-stencil texture between the MRT and the parentinputBufferof the composer for memory efficiency. When N8AO blits frominputBuffertodepthDownsampleTarget, Chrome's WebGL2 driver flags the operation: the source FB and dest FB hold the same depth-stencil image, which violates the spec.Disabling
halfResreduces but does not eliminate the spam,writeTargetInternal(:1684) andaccumulationRenderTarget(:1622) appear to exhibit the same shared-depth pattern in the AO and accumulation passes.What we tried (none fixed it)
<EffectComposer stencilBuffer={false}>(matchesgl.stencil:false)postprocessing6.36 → 6.39.1halfRes={false}on N8AO<EffectComposer multisampling={0}><SSAO />withenableNormalPassthree0.169 → 0.184So this is not three.js / postprocessing / Chrome version-specific, it reproduces across the full version matrix once strict WebGL2 validation is enabled.
Suggested fix
Allocate N8AO's internal render targets with their own depth-stencil textures rather than letting three.js share with
inputBuffer. Concretely, on eachWebGLRenderTarget/WebGLMultipleRenderTargetscreated by N8AO:This forces three.js to skip the implicit depth-sharing logic. Same depth-data is computed (N8AO copies into its own depth via the downsample step), but the underlying GL texture is distinct → the blit no longer violates the read/write same-image rule.
Alternatively: when blitting from
inputBufferto N8AO's targets, mask theGL_DEPTH_BUFFER_BIT/GL_STENCIL_BUFFER_BITout of the blit mask (onlyGL_COLOR_BUFFER_BITis needed in those copies). This is what three.js's MSAA resolve does in newer versions when it detects shared depth.