8000 metal: grow per-buffer rotation Vec on demand instead of pre-allocating 30 slots by benface · Pull Request #644 · not-fl3/miniquad · GitHub
[go: up one dir, main page]

Skip to content

metal: grow per-buffer rotation Vec on demand instead of pre-allocating 30 slots - #644

Merged
not-fl3 merged 1 commit into
not-fl3:masterfrom
benface:pr-metal-grow-buffer-rotation
Jun 15, 2026
Merged

not-fl3 merged 1 commit into
not-fl3:masterfrom
benface:pr-metal-grow-buffer-rotation

Conversation

@benface
@benface benface commented Jun 13, 2026 •
Copy link
Copy Markdown
Contributor

The Metal backend pre-allocates BUFFERS_IN_ROTATION = 30 MTLBuffers per logical Buffer to handle the within-frame update; draw; update; draw pattern — Metal's update doesn'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.raw with a Vec<ObjcId> initialised with a single backing buffer. buffer_update grows it lazily — when next_value exceeds raw.len(), allocate one more MTLBuffer 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. The GL backend already does this transparently via driver-side buffer object orphaning behind glBufferSubData; 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.

Buffer loses Copy (Vec isn't Copy); verified no caller copies Buffer by value. delete_buffer already iterated buffer.raw and needs no change.

…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
benface force-pushed the pr-metal-grow-buffer-rotation branch from ec12a39 to 8355b46 Compare June 13, 2026 22:45
@not-fl3
not-fl3 commented Jun 15, 2026
Copy link
Copy Markdown
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 update() draw() update() draw() chain.

@not-fl3
not-fl3 merged commit ce22a46 into not-fl3:master Jun 15, 2026
11 checks passed
@benface
benface deleted the pr-metal-grow-buffer-rotation branch June 15, 2026 12:23
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.

2 participants

0