From e1761cd58ae5500628a25c350095aea4933c49fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benoi=CC=82t=20Rouleau?= Date: Sat, 10 Oct 2026 01:13:52 -0400 Subject: [PATCH] gl: give a buffer a new store each time it's updated `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 Co-authored-by: multica-agent --- src/graphics.rs | 2 ++ src/graphics/gl.rs | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/graphics.rs b/src/graphics.rs index dbdfffc65..15248f2ba 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -1280,6 +1280,8 @@ pub trait RenderingBackend { /// ``` fn new_buffer(&mut self, type_: BufferType, usage: BufferUsage, data: BufferSource) -> BufferId; + /// Replace the buffer's contents with `data`, which can be shorter than + /// the buffer: what lies past it is undefined afterwards. fn buffer_update(&mut self, buffer: BufferId, data: BufferSource); /// Size of buffer in bytes. diff --git a/src/graphics/gl.rs b/src/graphics/gl.rs index 5fb34c096..7340b0748 100644 --- a/src/graphics/gl.rs +++ b/src/graphics/gl.rs @@ -25,6 +25,8 @@ pub mod raw_gl { struct Buffer { gl_buf: GLuint, buffer_type: BufferType, + /// The `usage` hint its stores get. + gl_usage: GLenum, size: usize, // Dimension of the indices for this buffer, // used only as a type argument for glDrawElements and can be @@ -1376,6 +1378,7 @@ impl RenderingBackend for GlContext { let buffer = Buffer { gl_buf, buffer_type: type_, + gl_usage, size, index_type, }; @@ -1404,7 +1407,11 @@ impl RenderingBackend for GlContext { self.cache.store_buffer_binding(gl_target); self.cache .bind_buffer(gl_target, buffer.gl_buf, buffer.index_type); - unsafe { glBufferSubData(gl_target, 0, size as _, data.ptr as _) }; + // A new store for each update, as Metal gives each update a buffer + // of its own. Writing into the store that draws already queued + // still read makes the driver copy it first, and Mali's driver + // waits on all the work it has queued to do so. + unsafe { glBufferData(gl_target, size as _, data.ptr as _, buffer.gl_usage) }; self.cache.restore_buffer_binding(gl_target); }