Repository navigation
Description
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+stencilThis 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
fix(ci): knip red on my own probe, and the GL ledger cannot answer th…
wowlegend commentedon Sep 22, 2026
Data point:
postprocessing6.39.5 clears this for us withhalfResenabled — 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
postprocessing6.36 → 6.39.1 | No effect" and "Upgradethree0.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 stoppedEffectComposerbuilding its stable depth texture withDepthTexture.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.postprocessingthree@react-three/postprocessing@react-three/fibern8ao@react-three/postprocessing's<N8AO halfRes … />ANGLE (Apple, ANGLE Metal Renderer: Apple M3 Max), WebGL2MAX_SAMPLESChain: RenderPass → N8AO (
halfRes) → GodRays → HueSaturation → BrightnessContrast → Bloom → SMAA → ToneMapping → Noise → Vignette.Two method notes, since they affect whether a zero means anything:
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.--use-angle=swiftshaderand 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
halfReswas 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 +halfResis 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.fix(docs): correct two claims of mine that measurement disproved — th…