8000 gl: replace a Stream buffer's storage on update instead of writing into it by indicozy · Pull Request #703 · not-fl3/miniquad · GitHub
[go: up one dir, main page]

Skip to content

gl: replace a Stream buffer's storage on update instead of writing into it - #703

Closed
indicozy wants to merge 1 commit into
not-fl3:masterfrom
indicozy:gl-stream-buffer-data
Closed

indicozy wants to merge 1 commit into
not-fl3:masterfrom
indicozy:gl-stream-buffer-data

Conversation

@indicozy
@indicozy indicozy commented Nov 9, 2026
Copy link
Copy Markdown

buffer_update writes into a buffer with glBufferSubData. When the GPU may still read that buffer from an earlier draw, a tiled mobile GPU has to either wait for it or copy it first. Adreno copies the whole buffer. macroquad hits this case every frame: each draw call slot owns a Stream vertex buffer of draw_call_vertex_capacity (10000 vertices, about 400 KB) plus an index buffer, and rewrites it every frame. macroquad#807 removed the CPU-side cost of that, but the GPU-side upload is still made once per draw call.

This PR changes only Stream buffers. For them, buffer_update now uploads with glBufferData of the written size. The driver gets fresh storage and frees the old storage once the GPU is done with it, so nothing is copied, and only the bytes written are allocated. Immutable and Dynamic buffers keep glBufferSubData. The Metal backend already avoids the same hazard by rotating buffers (see #644); this gives the GL path the same property without extra buffers.

Numbers

A macroquad 0.4 game (Razdor, 2.5D map view, a few dozen draw calls per frame), idle map, release build. The only difference between the two builds is this commit (on miniquad 0.4.11):

OnePlus 13 (Adreno 830, driver V@0800.77), Android 16 before after
fps (120 Hz screen) ~60 ~120
CPU frame time p50 / p90 16 / 17 ms 8.3 / 8.8 ms
app work per frame 8–9 ms 1.3–2.9 ms

Earlier, before macroquad batching was reduced on our side, simpleperf on the phone put about 60% of the frame's CPU time in memmove and 14% in ioctl, both under glBufferSubData in the Adreno driver.

Note: desktop results. Desktop drivers don't copy on glBufferSubData, so this change neither helps nor hurts there; the win is specific to tiled mobile GPUs.

  • Linux, Mesa llvmpipe (mean frame time, 700 frames × 3 runs): idle map 10.56 → 10.53 ms, battle 9.19 → 9.10 ms. Rendered frames are identical pixel for pixel.
  • Linux, RTX 4060 (proprietary driver, via VirtualGL; also with the process pinned to one core): CPU work per frame stays within noise (0.6–1.2 ms either way), and buffer_update/glBuffer* are under 0.3% of perf samples in both builds.

Alternatives tried

  • Orphaning first (glBufferData(capacity, NULL) and then glBufferSubData) also removes the copy. But it reallocates the full capacity on every update, which made llvmpipe 55–100% slower in the same test.
  • Rotating several buffer sets in macroquad's QuadGl doesn't help: QuadGl::draw runs several times per frame (every camera change flushes), so the same slots are rewritten within a frame.

This was found and written with Claude Code (AI-assisted). I measured it on the device and reviewed the change myself.

…to it

buffer_update used glBufferSubData for every buffer. When the GPU may still
read the buffer from an earlier draw (the usual case for Stream buffers that
are rewritten every frame), tiled mobile GPUs such as Adreno copy the whole
buffer before the write. For Stream buffers, upload with glBufferData of the
written size instead: the driver hands out fresh storage and drops the old
one once the GPU is done with it. Immutable and Dynamic buffers are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@indicozy
indicozy commented Nov 9, 2026
Copy link
Copy Markdown
Author

Closing as a duplicate of #682, which makes the same change (and was first). Our Adreno measurements are posted there.

@indicozy indicozy closed this Nov 9, 2026
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