8000 macos: drain an autorelease pool each time round the run loop by benface · Pull Request #676 · not-fl3/miniquad · GitHub
[go: up one dir, main page]

Skip to content

macos: drain an autorelease pool each time round the run loop - #676

Open
benface wants to merge 1 commit into
not-fl3:masterfrom
benface:macos-autorelease-pool
Open

benface wants to merge 1 commit into
not-fl3:masterfrom
benface:macos-autorelease-pool

Conversation

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

Why

macOS's run reimplements [NSApp run], but unlike it, it never drains an autorelease pool. So nothing autoreleased in the loop is ever freed: the events nextEventMatchingMask: returns, and on Metal, every frame's command buffer, render command encoders and render pass descriptor. A command buffer keeps alive what it encoded, so every texture a frame drew with stays alive too, deleted or not.

#507 found the same leak during window resizes, and wraps perform_redraw in a pool. This one puts the pool around each pass of the loop instead, as [NSApp run] does, so the events are freed as well.

Change

Each pass of the loop pushes a pool and pops it at the end. Nothing autoreleased in one pass is used in the next: a frame's command buffer is made and committed within it. This is macOS only: on iOS and tvOS, UIKit's run loop already drains a pool each time round.

Measurements

On its own, this stops the slow growth of an app that deletes nothing. Deleted textures still leak on Metal, because new_texture retains each one twice and delete_texture releases it once. #677 fixes that. Memory only stays flat with both.

Physical footprint of a small program on macOS (Apple silicon, Metal):

master this PR #677 both
A triangle drawn to the window, frames 600 → 1,800 34.4 → 38.5 MB 31.6 → 32.4 MB 34.3 → 38.4 MB 29.8 → 29.9 MB
Also making a 512² render target with depth and a 512² texture each frame, drawing into the target and deleting all three, frames 60 → 300 231 → 1,026 MB 231 → 1,025 MB 171 → 721 MB 35 → 36 MB

A macroquad game (CHOMP) sees the same on macOS:

  • Sitting on a menu, it grew by about 0.5 MB a second, and holds flat with both PRs.
  • Remaking its textures every frame, it grew from 1.4 GB to 4.9 GB in 20 seconds. With either PR alone, it grew just as fast. With both, it held at 767 MB.

Verification

cargo check passes for aarch64-apple-darwin.

🤖 Generated with Claude Code

@benface
benface commented Oct 9, 2026
Copy link
Copy Markdown
Contributor Author

The Android check failed before it checked out the code: Docker Hub refused to pull the notfl3/cargo-apk image ("toomanyrequests: You have reached your unauthenticated pull rate limit"). It's unrelated to this change, and a re-run of the job should pass.

`run` reimplements `[NSApp run]` but, unlike it, never drained an
autorelease pool. So nothing autoreleased outside AppKit's own event
handling was ever freed: on Metal, every frame's command buffer, and
with it every texture and pass the frame drew with, since a command
buffer keeps what it encodes alive.

Each pass of the loop now pushes a pool and pops it at the end, as
`[NSApp run]` does. Nothing autoreleased in one pass is used in the
next: the frame's command buffer is made and committed within it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
@benface
benface force-pushed the macos-autorelease-pool branch from 50ec5a8 to 152d950 Compare October 18, 2026 18:00
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