Skip to content

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

Open

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.

Activity

  1. wowlegend commented on Sep 22, 2026

    @wowlegend

    Data point: postprocessing 6.39.5 clears this for us with halfRes enabled — zero errors over 1672 frames on a real GPU. Your matrix stops at 6.39.1, which is why this may be worth adding.

    Your issue notes "Upgrade postprocessing 6.36 → 6.39.1 | No effect" and "Upgrade three 0.169 → 0.184 | Zero effect on the warning". 6.39.5 is newer than anything in that table and it contains pmndrs/postprocessing@078a6d1, which stopped EffectComposer building its stable depth texture with DepthTexture.clone() and constructs one instead — see pmndrs/postprocessing#750. Since the mechanism you describe is the shared depth-stencil image, a composer that no longer shares one plausibly removes the source side of the blit even though the N8AO side is unchanged.

    postprocessing 6.39.5
    three 0.172.0
    @react-three/postprocessing 3.0.4
    @react-three/fiber 9.5.0
    n8ao via @react-three/postprocessing's <N8AO halfRes … />
    renderer ANGLE (Apple, ANGLE Metal Renderer: Apple M3 Max), WebGL2
    MAX_SAMPLES 4

    Chain: RenderPass → N8AO (halfRes) → GodRays → HueSaturation → BrightnessContrast → Bloom → SMAA → ToneMapping → Noise → Vignette.

    frames: 1672   GL errors: 0 (0.00/frame)   codes: []   console blitFramebuffer hits: 0
    

    Two method notes, since they affect whether a zero means anything:

    1. gl.getError() drained per frame, not a console scrape. Chrome mutes the context after ~256 errors ("too many errors, no more errors will be reported"), so any console-based count is a floor rather than a count — and once muted it hides later errors too.
    2. Real GPU, not SwiftShader. Our CI probe runs --use-angle=swiftshader and reported 0 as well, but that reading is worthless here: SwiftShader does not perform the driver-side validation that emits this message, so it returns 0 whether or not the condition exists. I only trusted the number once it came from ANGLE Metal with 1672 real frames behind it.

    What I have NOT established: that halfRes was ever the operative cause for us. I did not reproduce the failure on 6.39.1 and then fix it by upgrading — I measured the current state only. So this is "6.39.5 + halfRes is clean on this configuration", not "6.39.5 fixes n8ao#53". If it is useful I am happy to run a 6.39.1 control on the same rig to turn that into a before/after.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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