8000 metal: keep transient render textures' samples in tile memory by benface · Pull Request #680 · not-fl3/miniquad · GitHub
[go: up one dir, main page]

Skip to content

metal: keep transient render textures' samples in tile memory - #680

Open
benface wants to merge 1 commit into
not-fl3:masterfrom
benface:metal-transient-textures
Open

benface wants to merge 1 commit into
not-fl3:masterfrom
benface:metal-transient-textures

Conversation

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

Why

On Metal, a multisampled render target stores its samples at the end of every pass (StoreAndMultisampleResolve), so a later pass into the same target can load them back. An app that clears its target each frame and only reads what it resolves into never loads them. It still writes them out every frame, and the multisampled texture still takes up memory. A full-screen 4× target on an iPhone 17 Pro is about 50 MB of both, every frame.

On a tile-based GPU, which every Apple GPU is, those samples can live in tile memory alone (MTLStorageModeMemoryless), if nothing ever stores or loads them.

Change

TextureParams::transient says a render texture's contents only last as long as the pass drawing into it. For a transient texture, on Metal:

  • A pass stores nothing of it when it ends but what it resolves into: MultisampleResolve, or DontCare without a resolve texture.
  • A pass that doesn't clear it starts with DontCare.
  • On a GPU that supports MTLGPUFamilyApple1, the texture is MTLStorageModeMemoryless and takes up no memory at all. Elsewhere it stays Private, with the same load and store actions.
  • A transient depth texture isn't stored either.

Consecutive passes into the same target are still merged into one encoder, so a target drawn by one draw call after another, as macroquad draws, keeps its samples throughout. OpenGL keeps a transient texture as it does any other, which the contract allows. Textures that aren't transient are unchanged.

TextureParams gains a field, so a struct literal that doesn't use ..Default::default() needs transient: false. The one in new_texture_from_rgba8 has it.

Measurements

A macroquad game (CHOMP) draws each frame into a full-screen 4× target, resolved once and copied to the screen. On an iPhone 17 Pro, with a level held idle at 60 Hz, traced with Instruments' Metal System Trace:

master transient
GPU time per frame 5.08 ms 4.92 ms
The frame's pass 3.14 ms 3.04 ms
Physical footprint 318.6 MB 269.2 MB

The final copy to the screen took 1.28 ms in both traces, so the GPU ran at the same clock.

Verification

  • The game's deterministic captures on macOS (Apple silicon, so memoryless) run clean under MTL_DEBUG_LAYER=1 with MTL_DEBUG_LAYER_ERROR_MODE=assert.
  • It runs on the iOS Simulator and on an iPhone 17 Pro.
  • cargo check passes for aarch64-apple-darwin, aarch64-apple-ios and aarch64-apple-tvos.

🤖 Generated with Claude Code

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>
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