Skip to content

RFC: Color-only MSAA for offscreen framebuffers #2741

Description

@ibgreen-openai

Summary

Add color-only multisample antialiasing for offscreen framebuffers, with automatic resolve into the framebuffer's texture attachments.

This supersedes #2702, which identified that Texture.samples is currently ignored by the WebGL backend.

Motivation

Offscreen effects often render into a texture and sample that texture in a later pass. WebGL2 supports MSAA for this workflow, but only by rendering into a multisampled renderbuffer and resolving it into a texture with blitFramebuffer.

Today luma.gl does not expose that path:

  • WEBGLTexture allocates ordinary texture storage regardless of Texture.samples.
  • WEBGLFramebuffer only attaches textures.
  • Applications must drop down to raw WebGL renderbuffers and blits.

Proposed API

Put the multisampling request on the framebuffer rather than on the resolved texture:

const colorTexture = device.createTexture({
  width,
  height,
  format: 'rgba8unorm',
  usage: Texture.RENDER | Texture.SAMPLE
});

const framebuffer = device.createFramebuffer({
  width,
  height,
  samples: 4,
  colorAttachments: [colorTexture]
});

The textures in colorAttachments remain ordinary single-sampled textures. After the render pass ends, they contain the resolved result and can be sampled or copied as usual.

WebGL implementation

For samples > 1, WEBGLFramebuffer privately owns:

  • the existing texture-backed framebuffer, used as the resolve destination;
  • a second framebuffer used for rendering;
  • one multisampled renderbuffer per color attachment.

WEBGLRenderPass binds the multisampled framebuffer for clear and draw operations. Its end() resolves every color attachment into the texture-backed framebuffer with:

gl.blitFramebuffer(
  0,
  0,
  width,
  height,
  0,
  0,
  width,
  height,
  gl.COLOR_BUFFER_BIT,
  gl.NEAREST
);

The renderbuffers are recreated on resize and deleted with the framebuffer. They remain backend-private; no public Renderbuffer resource is needed.

Initial scope

  • WebGL2 only.
  • Color attachments only.
  • Support one or more color attachments with a common framebuffer sample count.
  • Resolve automatically when RenderPass.end() is called.
  • Validate that the requested count is supported and that attachments are color-renderable.
  • Reject depth/stencil attachments together with samples > 1 in the first implementation.

Non-goals

  • Multisampled depth or stencil.
  • Sampling an unresolved multisampled attachment.
  • Exposing renderbuffers as public luma.gl resources.
  • Treating Texture.samples as a WebGL MSAA request.

Why not Texture.samples?

In WebGL, the texture receiving the resolved result is physically single-sampled. Reusing Texture.samples would make the texture's metadata and memory accounting inaccurate, and would give it different semantics from a true multisampled WebGPU texture.

A framebuffer-level option expresses the portable intent: multisample rendering into these resolved color textures.

WebGPU follow-up

The same API can later map to a private multisampled GPUTexture plus the supplied color texture as resolveTarget. That follow-up is not required for the initial WebGL implementation.

Validation

Add tests for:

  • allocating the multisampled render framebuffer and color renderbuffers;
  • binding the multisampled framebuffer during the pass;
  • resolving on end();
  • multiple color attachments;
  • resize and destroy cleanup;
  • rejection of depth/stencil attachments in the initial scope;
  • rejection or clamping of unsupported sample counts.

Open questions

  • Should unsupported sample counts throw or be clamped to MAX_SAMPLES?
  • Should the default remain 1, or should canvas/offscreen defaults eventually be aligned?
  • Should a later cross-backend API expose explicit resolve targets for advanced cases?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions