Repository navigation
metal: grow per-buffer rotation Vec on demand instead of pre-allocating 30 slots - #644
Merged
Merged
Conversation
…ng 30 slots The Metal backend pre-allocated `BUFFERS_IN_ROTATION = 30` `MTLBuffer`s per logical `Buffer` to handle the within-frame `update; draw; update; draw` pattern (Metal's update doesn't synchronously claim ownership, so the previous data has to outlive its draw call somehow). 30 was an arbitrary upper bound — large enough to cover any plausible per-frame multi-update, small enough to compile. For a non-trivial app this multiplies real GPU memory by ~30×. An Xcode GPU capture on a 2D game with ~100 macroquad-binding slots showed 1.5 GiB of buffer memory, of which ~5 % was ever actually touched per frame; the remaining 95 % was unused rotation slots. The pressure showed up as per-level lag as iOS GC'd around the over-allocated pool. The GL backend doesn't need this — the driver handles buffer object orphaning behind `glBufferSubData` and only keeps as many backing stores as the in-flight frame actually requires. This patch brings Metal in line with that behaviour: `Buffer.raw` becomes a `Vec<ObjcId>` initialised with one `MTLBuffer`. `buffer_update` grows it lazily — when `next_value` exceeds `raw.len()`, allocate one more backing buffer with the cached `MTLResourceOptions` and push. After a few frames hit the app's actual per-frame max, growth stops. Apps that update each buffer once per frame carry a single backing buffer; apps that flush several times per frame on the same buffer grow that buffer's `raw` to match. Total GPU memory tracks actual usage instead of a worst-case ceiling. Same-app comparison: the 1.5 GiB of buffer memory dropped to ~120 MiB, the per-level lag from rotating through bloated GC'd pools disappeared, and the magic 30 is gone from the codebase. `Buffer` loses `Copy` (Vec isn't Copy); verified no caller copies `Buffer` by value. `delete_buffer` already iterated `buffer.raw` and needs no change.
benface
force-pushed
the
pr-metal-grow-buffer-rotation
branch
from
June 13, 2026 22:45
ec12a39 to
8355b46
Compare
Owner
|
That was a lot of allocated buffers! Thansk for PR! Merging, but this made me think that we need examples/regressio_*.rs examples. For this case - with this |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Metal backend pre-allocates
BUFFERS_IN_ROTATION = 30MTLBuffers per logicalBufferto handle the within-frameupdate; draw; update; drawpattern — Metal'supdatedoesn't synchronously claim ownership, so each update needs its own backing buffer to keep the previous draw's data alive. 30 was an arbitrary upper bound on per-frame multi-update count. For a non-trivial app this multiplies GPU buffer memory by ~30×; most of the slots are never touched.Replace
Buffer.rawwith aVec<ObjcId>initialised with a single backing buffer.buffer_updategrows it lazily — whennext_valueexceedsraw.len(), allocate one moreMTLBufferwith the cachedMTLResourceOptionsand push. After a few frames hit the app's actual per-frame max, growth stops. Apps that update each buffer once per frame carry a single backing buffer; apps that flush several times per frame on the same buffer grow that buffer'srawto match. The GL backend already does this transparently via driver-side buffer object orphaning behindglBufferSubData; this brings Metal in line with that behaviour.Reproduction
Surfaced by an Xcode GPU Workload capture on a 2D game with ~100 macroquad-binding slots: 1.5 GiB of buffer memory, of which only the first slot per binding was ever touched per frame. The over-allocated pool caused per-level lag as iOS GC'd around it. After the patch the same app sat at ~120 MiB of buffer memory and the lag disappeared.
BufferlosesCopy(Vec isn't Copy); verified no caller copiesBufferby value.delete_bufferalready iteratedbuffer.rawand needs no change.