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>
@indicozy
indicozy commented Nov 9, 2026
Copy link
Copy Markdown

Confirmed on Adreno as well. We hit the same thing independently in a macroquad 0.4.16 game (Razdor) and wrote the same change (#703, now closed in favour of this one; ours only switched BufferUsage::Stream buffers to glBufferData).

OnePlus 13 (Adreno 830, driver V@0800.77, Android 16), 2.5D map at idle, display confirmed at 120 Hz in dumpsys display during both runs, 2 alternating runs per build:

master (glBufferSubData) glBufferData per update
fps ~60 ~120
CPU frame time p50 / p90 16 / 17 ms 8.3 / 8.8 ms
app work per frame ~8 ms ~2.5 ms

An earlier simpleperf run on the same phone put about 60% of the frame's CPU time in memmove and 14% in ioctl under glBufferSubData in the Adreno driver, i.e. the copy-before-write.

Desktop shows no change, as expected: Mesa llvmpipe 10.56 → 10.53 ms per frame (identical frames, pixel for pixel), and an RTX 4060 (proprietary driver) where glBuffer* stays under 0.3% of perf samples either way.

One thing we tried first, in case it saves someone time: orphaning with glBufferData(capacity, NULL) before glBufferSubData also removes the copy, but reallocating the full capacity each update made llvmpipe 55–100% slower. Uploading just the written size, as this PR does, has no such cost.

(Measured on the device by me; written up with Claude Code.)

Tigon32 added a commit to Tigon32/miniquad that referenced this pull request Nov 10, 2026
Tigon32 added a commit to Tigon32/miniquad that referenced this pull request Nov 10, 2026
Tigon32 added a commit to Tigon32/miniquad that referenced this pull request Nov 10, 2026
Tigon32 added a commit to Tigon32/miniquad that referenced this pull request Nov 10, 2026
…ream not-fl3#691, not-fl3#682 (#2)

* Integrate upstream PRs not-fl3#691, not-fl3#682: src/graphics/gl.rs

* Integrate upstream PRs not-fl3#691, not-fl3#682: src/graphics/gl/cache.rs

* Integrate upstream PRs not-fl3#691, not-fl3#682: src/graphics.rs

* ci: add targeted Rust WebAssembly and JS build checks

* ci: allow generating Cargo.lock for library builds
Sign up for free to join this 62C3 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