8000 gl: give a buffer a new store each time it's updated by benface · Pull Request #682 · not-fl3/miniquad · GitHub
[go: up one dir, main page]

Skip to content

gl: give a buffer a new store each time it's updated - #682

Open
benface wants to merge 1 commit into
not-fl3:masterfrom
benface:gl-orphan-stream-buffers
Open

benface wants to merge 1 commit into
not-fl3:masterfrom
benface:gl-orphan-stream-buffers

Conversation

@benface
@benface benface commented Oct 10, 2026
Copy link
Copy Markdown
Contributor

Why

buffer_update wrote into a buffer's existing store with glBufferSubData. A store that draws already queued still read can't be written in place, so the driver has to copy it first. On a Pixel 6 (Mali-G78), every such write flushed the driver's queue and waited on its backend thread.

macroquad refills the same stream buffers for each flush of its batches, and flushes whenever it switches render targets. In a game (CHOMP) whose screens blur the frame behind a modal, about a dozen switches a frame, the render thread spent a third of its time in glBufferSubData, about 6 ms a frame, according to simpleperf --trace-offcpu:

gles_buffer_buffer_sub_data
  gles_bufferp_replica_load_sub_data
    gles_bufferp_get_buffer_write_instance
      cdeps_tracker_get_writable_buffer_instance_partial_update
        cdeps_cow_enqueue
          cmarp_flush
            cmarp_handle_cmar_signals_in_app_thread → pthread_mutex_lock → futex wait

The wait followed the GPU's progress, so a lighter frame, which the GPU clocked down for, waited longer still.

Change

buffer_update gives the buffer a new store holding the data (glBufferData, with the buffer's usage hint), which the driver can hand over without copying or waiting. Metal already writes each update into a buffer slot of its own, so what lay past the data was already gone there. The trait's doc for buffer_update now says so: the data can be shorter than the buffer, and what lies past it is undefined afterwards.

Each store now only holds the data last written to it, rather than as much as the buffer was made for. macroquad makes its stream buffers big enough for its largest batch, so that's less memory too.

Measurements

CHOMP's frame bench on a Pixel 6 at 60 Hz, with a modal open over a level whose frame is blurred behind it, held for 30 s:

master this PR
Frame rate 57.1 fps, 2.5 % of frames late 60.0 fps, 0.3 % late
CPU time per frame, median 14.2 ms 12.5 ms
GPU memory (dumpsys meminfo, GL mtrack), after the bench 212 MB 184 MB

Verification

  • cargo check passes for aarch64-linux-android, wasm32-unknown-unknown, aarch64-apple-darwin, aarch64-apple-ios and aarch64-apple-tvos.
  • The game runs through its frame bench on the Pixel 6.
  • Its deterministic captures through desktop OpenGL on macOS come out byte-identical, but for a few pixels, by up to 8 levels in 255, in frames where it draws a few small pieces each in a flush of its own, so the same buffer is rewritten while the frame's render pass still has a draw queued from it. No draw reads past the data it uploaded (checked for every draw), so it looks like Apple's GL no longer ends and resumes the pass there, which on Apple's GPUs rounds a few blended pixels.

🤖 Generated with Claude Code

`buffer_update` wrote into a buffer's store with `glBufferSubData`. A
store that draws already queued still read can't be written in place,
so the driver copies it first. On a Pixel 6 (Mali-G78), every such
write flushed the driver's queue and waited on its backend thread. A
macroquad game refills the same stream buffers for each flush of its
batches, so a frame that switches render targets a dozen times spent a
third of its render thread's time in `glBufferSubData`: about 6 ms a
frame. The wait followed the GPU's progress, so a lighter frame, which
the GPU clocks down for, waited longer still.

`buffer_update` now gives the buffer a new store holding the data
(`glBufferData`), which the driver can hand over without copying or
waiting. Metal already writes each update into a buffer of its own, so
what lay past the data was already gone there. `buffer_update`'s docs
now say so.

On the Pixel 6, at 60 Hz, a macroquad game with a blurred dim over its
frame went from 57.1 to 60.0 fps, its frames' CPU time from 14.2 to
12.5 ms at the median. Each store now only holds the data last written
to it, rather than as much as the buffer was made for, so the game's
graphics memory went from 212 to 184 MB: macroquad makes its stream
buffers big enough for its largest batch.

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