8000 gl: keep transient multisampled samples on the tile with EXT_multisampled_render_to_texture by benface · Pull Request #683 · not-fl3/miniquad · GitHub
[go: up one dir, main page]

Skip to content

gl: keep transient multisampled samples on the tile with EXT_multisampled_render_to_texture - #683

Open
benface wants to merge 2 commits into
not-fl3:masterfrom
benface:gl-multisampled-render-to-texture
Open

benface wants to merge 2 commits into
not-fl3:masterfrom
benface:gl-multisampled-render-to-texture

Conversation

@benface
@benface benface commented Oct 10, 2026 •
Copy link
Copy Markdown
Contributor

Builds on #680, which adds TextureParams::transient: this branch's first commit is #680's, and its second is this change.

Why

On OpenGL, a multisampled render pass draws into a renderbuffer, and end_render_pass blits it into the resolve texture. On a tile-based GPU, that blit makes the GPU write every sample out to memory and read them all back: about 40 MB each way for a full-screen 4× target on a 1080×2400 phone, every time it resolves.

OpenGL ES's GL_EXT_multisampled_render_to_texture lets a framebuffer draw into a single-sampled texture multisampled. The samples stay in tile memory and resolve as the GPU writes the tile out, and they need no memory of their own. Mali and Adreno GPUs support it, among others.

Change

TextureParams::transient already says a render texture's contents only last as long as the pass drawing into it, which is the extension's contract. Features::multisampled_render_to_texture reports OpenGL ES 3 with the extension, and with it:

  • A pass with one transient multisampled color attachment and a resolve texture, and no depth or a transient one, attaches the resolve texture with glFramebufferTexture2DMultisampleEXT in place of the renderbuffer. end_render_pass has nothing to blit for it: the resolve texture holds what's drawn once drawing leaves the pass, so a pass begun and ended for every batch, as macroquad does, resolves once.
  • A transient multisampled texture's storage is an implicit multisample buffer (glRenderbufferStorageMultisampleEXT), which can share such a framebuffer and live in tile memory alone.
  • A pass that doesn't clear starts by invalidating its transient attachments (glInvalidateFramebuffer), so the GPU doesn't load them, unless it carries on drawing into the target the last pass begun drew into. That mirrors Metal's DontCare load action in metal: keep transient render textures' samples in tile memory #680, which consecutive passes into the same target don't get either, since they share an encoder.

Every other texture and pass is unchanged, and so is any other context. WebGL never reports the feature, and stand-ins for the three new functions keep the module from importing them from gl.js.

Measurements

A macroquad game (CHOMP) draws each frame into a full-screen transient 4× target, leaves it mid-frame to blur it behind a modal, and copies it to the screen. On a Pixel 6 (Mali-G78) at 60 Hz, with #682 in both builds:

#682 #682 and this PR
A modal over a level, held 30 s 60.0 fps, GPU at 400–572 MHz, mostly 471 59.9 fps, GPU at 251–351 MHz, mostly 302
Play, every phase of the game's bench GPU at 251–572 MHz GPU at 251–351 MHz
GPU memory (GL mtrack), after the bench 184 MB 146 MB

Without #682, this change alone made frames later: buffer updates waited on the driver, and the lighter frames let the GPU clock down, which made those waits longer.

Verification

  • cargo check passes for aarch64-linux-android, wasm32-unknown-unknown, aarch64-apple-darwin, aarch64-apple-ios and aarch64-apple-tvos.
  • The game draws correctly on the Pixel 6, its blurred modals included, and runs through its frame bench there.
  • Desktop OpenGL doesn't report the feature, and the game's deterministic captures through it are byte-identical to master's.

🤖 Generated with Claude Code

benface and others added 2 commits October 9, 2026 21:31
A multisampled render target's samples are stored at the end of every
pass (`StoreAndMultisampleResolve`), so a later pass into the same
target can load them back. An app that clears the target each frame
and only reads what it resolves into never loads them, but still
writes them out every frame, and the multisampled texture still takes
up memory: about 50 MB at an iPhone 17 Pro's full resolution with 4×
antialiasing.

`TextureParams::transient` says a render texture's contents only last
as long as the pass drawing into it. Metal then stores nothing of it
when a pass ends but what it resolves into (`MultisampleResolve`, or
`DontCare` without a resolve texture), a pass that doesn't clear it
starts with `DontCare`, and on a GPU with tile memory, every Apple GPU,
the texture is `MTLStorageModeMemoryless` and takes up no memory at
all. Consecutive passes into the same target are still merged into one
encoder, so a target drawn by one draw call after another keeps its
samples throughout. A transient depth texture isn't stored either.

OpenGL keeps a transient texture as it does any other, which the
contract allows. Everything else is unchanged for textures that aren't
transient.

Drawing a full-screen 4× target that's cleared each frame and resolved
once took an iPhone 17 Pro's GPU from 5.08 to 4.85 ms a frame, and the
app's footprint from 319 to 269 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
…pled_render_to_texture

On OpenGL, a multisampled render pass draws into a renderbuffer, and
`end_render_pass` blits it into the resolve texture. On a tile-based
GPU, the blit makes the GPU write every sample out to memory and read
them all back: about 40 MB each way per resolve of a full-screen 4×
target on a 1080×2400 phone. OpenGL ES's
`GL_EXT_multisampled_render_to_texture` lets a framebuffer draw into a
single-sampled texture multisampled, keeping the samples in tile memory
and resolving them as the GPU writes the tile out.

`TextureParams::transient` already says a render texture's contents
only last as long as the pass drawing into it, which is that
extension's contract. On OpenGL ES 3 with the extension, which
`Features::multisampled_render_to_texture` reports:

- A pass with one transient multisampled color attachment and a resolve
  texture, and no depth or a transient one, attaches the resolve texture
  with `glFramebufferTexture2DMultisampleEXT` instead of the
  renderbuffer, and `end_render_pass` has nothing to blit. The resolve
  texture holds what's drawn once drawing leaves the pass.
- A transient multisampled texture's storage is an implicit multisample
  buffer (`glRenderbufferStorageMultisampleEXT`), which can share such a
  framebuffer and live in tile memory alone.
- A pass that doesn't clear starts by invalidating its transient
  attachments (`glInvalidateFramebuffer`), unless it carries on drawing
  into the target the last pass begun drew into, so the GPU doesn't load
  them.

Everything else is unchanged, and so is any other context. WebGL never
reports the feature, and stand-ins keep the module from importing the
new functions from `gl.js`.

On a Pixel 6 (Mali-G78), at 60 Hz, a macroquad game that draws each
frame into a full-screen transient 4× target, leaves it mid-frame to
blur it and copies it to the screen held 60 fps at a GPU clock of 302
MHz instead of 471, and its graphics memory went from 184 to 146 MB.
That was measured with buffer updates no longer waiting on the driver:
before that, those waits held the frame rate down, and they grew as the
lighter frames let the GPU clock down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0