Skip to content

WebGL2: "glBlitFramebuffer: Read and write depth stencil attachments cannot be the same image" #53

@botchris

Description

@botchris

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions