Skip to content

Adding a depth-requiring pass after the composer has already rendered breaks MSAA depth format #745

Description

@kvvasuu

Adding a depth-requiring pass after the composer has already rendered breaks MSAA depth format (GL_INVALID_OPERATION blit error / frozen or white screen) — especially impactful in @react-three/postprocessing

Description of the bug

GL_INVALID_OPERATION: glBlitFramebuffer: Depth/stencil buffer format combination not allowed for blit.

spammed every frame, render freezes or goes white. Started in 6.39.0, not present in <6.39.0.

With multisampling > 0, the first render allocates the MSAA depth renderbuffer with a default format (24-bit fixed-point, since no depth texture existed yet). If a depth-requiring pass is added later via addPass(), createDepthTexture() creates a 32-bit float depth texture — the resolve-side attachment picks this up correctly, but the MSAA renderbuffer is never rebuilt and stays at the old format. Confirmed via getFramebufferAttachmentParameter/getRenderbufferParameter:

MSAA renderbuffer:     depthSize: 24, componentType: 0x8c17 (UNSIGNED_NORMALIZED)
Resolve/texture side:  depthSize: 32, componentType: 0x1406 (FLOAT)

Different component types, not just bit depth — three.js's own internal MSAA resolve blit fails on this, before EffectComposer.blitDepthBuffer() even runs.

In plain postprocessing, this is avoidable by calling addPass() for every pass together, synchronously, before the first render. But @react-three/postprocessing's adds passes for its Effect children via an internal useLayoutEffect that reruns (tearing down and re-adding) whenever the set of children changes — which happens on every mount/unmount of an effect component, not just on initial render. Writing all your effects in JSX from the start doesn't guarantee they're added to the composer in one synchronous batch the way manual addPass() calls can be — component mount timing, conditional rendering, and effect-dependent children (like ) can still result in passes being added after the composer has already rendered a frame. This makes the bug much easier to hit in a React app than in a hand-written JS setup, even when the JSX looks "static."

To reproduce

composer.addPass(new RenderPass(scene, camera));
// renders at least one frame here

// later, e.g. on keypress:
composer.addPass(new EffectPass(camera, dofEffect), 1);
composer.addPass(new DepthPickingPass(), 2);
composer.addPass(new CopyPass(), 3);

Works if everything is added before the first render; breaks if added afterward, regardless of order. Also reproduces via @react-three/postprocessing's when mounted conditionally next to another effect.

Library versions used

  • postprocessing: 6.39.0 / 6.39.1 / 6.39.2 (affected),
  • three: 0.182.0–0.185.1 (no difference)

Desktop

  • OS: Windows
  • Browse: Chrome
  • Graphics hardware: AMD RX 6800XT

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions