8000 metal: take a drawable only in frames that draw to the screen by benface · Pull Request #693 · not-fl3/miniquad · GitHub
[go: up one dir, main page]

Skip to content

metal: take a drawable only in frames that draw to the screen - #693

Open
benface wants to merge 1 commit into
not-fl3:masterfrom
benface:metal-present-only-screen-frames
Open

benface wants to merge 1 commit into
not-fl3:masterfrom
benface:metal-present-only-screen-frames

Conversation

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

Why

commit_frame asks the view for its currentDrawable every frame and presents it. currentDrawable waits in -[CAMetalLayer nextDrawable] until the display lets go of one of the layer's drawables, so every frame waits for the display, even a frame drawn only into render targets.

A macroquad game (CHOMP) has a capture mode that plays a level and saves its frames as PNGs. It draws each frame into a render target and reads it back, and nothing it draws goes to the window. In a sample of a running capture on an M2 Max, 43% of the main thread was in nextDrawable, and frames came at the display's refresh rate, 120 Hz.

Change

  • begin_pass notes when a frame begins a pass on the screen, which is what takes one of the view's drawables.
  • commit_frame takes and presents a drawable only for such a frame. A frame drawn only off-screen commits its command buffer without one.
  • A frame with no pass at all commits nothing. It panicked on the missing command buffer before.

Nothing changes for a frame that draws to the screen.

On macOS, frames are also paced by the display link, and macroquad clears the screen at the start of every frame. So an app needs all three changes before its off-screen frames stop waiting for the display:

Measurements

These are the capture's times for 2,400 frames, saving 8 of them, on an M2 Max with a 120 Hz screen. Every frame came out byte-identical to the frames before the changes. Changes on their own made no difference:

Load average Time
Before 25–35 20.9–21.4 s
Before 190–330 22.3 s
No screen clear, with this PR 190–330 21.5 s
swap_interval: Some(0), screen still cleared 190–330 22.8 s
All three 190–330 8.0 s
All three 25–35 6.1–6.5 s

Through OpenGL, which needs only swap_interval: Some(0) and the B36E screen clear off, the same frames took 22.2 s before and 5.2 s after.

Verification

  • cargo check passes for aarch64-apple-darwin, aarch64-apple-ios and x86_64-unknown-linux-gnu.
  • CHOMP's capture tests pass with the three changes. Its captures save the same frames, contact sheets and videos, byte for byte, through Metal and through OpenGL.

🤖 Generated with Claude Code

`commit_frame` asked the view for its `currentDrawable` every frame and
presented it. `currentDrawable` waits in `-[CAMetalLayer nextDrawable]`
until the display lets go of one of the layer's drawables, so every
frame waited for the display, even one drawn only into render targets.

A frame now takes a drawable only when it begins a pass on the screen,
and `commit_frame` presents one only then. An app that draws its frames
off-screen and reads them back, such as a tool saving them to files,
can then draw them as fast as the GPU does. A frame with no pass at all
commits nothing, where it panicked on the missing command buffer.

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