Stream Dynamic OpenGL Data Without Buffer Orphaning
Use persistent mapped buffer storage to stream vertex and uniform data in OpenGL 4.4+ without reallocating buffers every frame.
16 Sept 2026, 18:51 UTC

The problem: paying for buffer reallocation every frame
If you stream dynamic vertex or uniform data in OpenGL, the obvious approach is to call glBufferData each frame with a new pointer and let the driver discard the old store. This is often called buffer orphaning: you abandon the previous allocation so the driver can allocate fresh memory while the GPU finishes reading the old contents. It works, but it shifts cost to the driver. On many drivers the allocation or implicit copy still causes a stall, and the pattern scales poorly as frame rates and vertex counts rise.
The practical alternative is persistent mapped buffer storage, available in desktop OpenGL core profile 4.4 and later, or through GL_ARB_buffer_storage. You allocate an immutable buffer once, map it persistently, and write into it from the CPU while the GPU reads from regions it has already finished with. The driver does not reallocate every frame, but you take over synchronization that the driver previously handled implicitly.
What persistent mapped storage changes
glBufferStorage creates a buffer with a fixed size and usage flags. Unlike glBufferData, you cannot resize or re-specify the store later. You then call glMapBufferRange with flags such as GL_MAP_WRITE_BIT, GL_MAP_PERSISTENT_BIT, and GL_MAP_UNSYNCHRONIZED_BIT. The mapping stays valid for the life of the buffer, so the CPU can write directly into mapped memory without calling glMapBufferRange or glUnmapBuffer each frame.
Two flags matter for correctness:
GL_MAP_COHERENT_BIT: makes CPU writes visible to the GPU without an explicit memory barrier. Convenient, but often slower because the driver may use less write-combining or stronger ordering.- Without
GL_MAP_COHERENT_BIT: you must callglMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT)before the GPU consumes data the CPU just wrote.
GL_MAP_UNSYNCHRONIZED_BIT tells the driver you will not overwrite memory the GPU is still reading. It is a promise, not a guarantee that the driver will never synchronize internally.
Worked example: a fixed ring buffer for streaming vertices
This example assumes a desktop OpenGL 4.4+ core context. Run the initialization code after context creation and before your render loop. It requires no special operating-system permissions, only a valid GL context.
#define FRAME_COUNT 3
#define VERTICES_PER_FRAME 4096
#define VERTEX_SIZE 32 // bytes
GLsizeiptr bufferSize = FRAME_COUNT * VERTICES_PER_FRAME * VERTEX_SIZE;
GLuint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// Immutable store: size and flags are fixed at creation.
glBufferStorage(GL_ARRAY_BUFFER, bufferSize, NULL,
GL_MAP_WRITE_BIT |
GL_MAP_PERSISTENT_BIT |
GL_MAP_UNSYNCHRONIZED_BIT);
// Persistent map: valid until the buffer is deleted.
void* mapped = glMapBufferRange(GL_ARRAY_BUFFER, 0, bufferSize,
GL_MAP_WRITE_BIT |
GL_MAP_PERSISTENT_BIT |
GL_MAP_UNSYNCHRONIZED_BIT);
GLsync fences[FRAME_COUNT] = {0};
int frameIndex = 0;
// Render loop, simplified:
for (;;) {
int region = frameIndex % FRAME_COUNT;
// Wait only if the GPU is still working on this region.
if (fences[region]) {
GLenum wait = glClientWaitSync(fences[region], GL_SYNC_FLUSH_COMMANDS_BIT, 10000000);
// GL_TIMEOUT_EXPIRED means the GPU is not done; ALREADY_SIGNALED or CONDITION_SATISFIED means proceed.
glDeleteSync(fences[region]);
fences[region] = 0;
}
// Write vertex data into this frame's region.
char* destination = (char*)mapped + region * VERTICES_PER_FRAME * VERTEX_SIZE;
fill_vertices(destination, VERTICES_PER_FRAME); // your data generator
// Non-coherent map: make writes visible before drawing.
glMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, VERTEX_SIZE, destination);
glEnableVertexAttribArray(0);
glDrawArrays(GL_TRIANGLES, 0, VERTICES_PER_FRAME);
// Insert a fence so the next lap around the ring knows when this region is free.
fences[region] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
frameIndex++;
}
Expected checks: call glGetError after glBufferStorage and glMapBufferRange during development, and enable GL_DEBUG_OUTPUT or KHR_debug so the driver can report invalid offsets, lengths, or access flags. A return of NULL from glMapBufferRange means the mapping failed.
Risk: writing into a region while the fence for that region is still pending overwrites memory the GPU may be reading. The result is undefined behavior, which can appear as corrupted geometry or a driver stall rather than a clean error.
Verification and limitations
Before calling glBufferStorage, confirm the context supports it:
GLint major = 0, minor = 0;
glGetIntegerv(GL_MAJOR_VERSION, &major);
glGetIntegerv(GL_MINOR_VERSION, &minor);
int hasStorage = (major > 4) || (major == 4 && minor >= 4);
// Or check for GL_ARB_buffer_storage in the extension string on older desktop contexts.
Run this check once after context creation. If the version is lower, fall back to orphaning with glBufferData or use extension loading to confirm GL_ARB_buffer_storage.
Limitations:
- The buffer size is fixed at creation. If your workload grows beyond the allocation, you must create a new buffer object; there is no resize path.
- Persistent mapping does not eliminate synchronization. You still need fences,
glClientWaitSync, or equivalent tracking so the CPU never overwrites in-flight GPU data. - This pattern targets desktop OpenGL. OpenGL ES support depends on the implementation and extension availability; do not assume it in a mobile or web context.
Practical closing checklist
- Verify desktop OpenGL 4.4+ or
GL_ARB_buffer_storagebefore usingglBufferStorage. - Size the ring buffer for the maximum frames in flight you expect, not for a single frame.
- Map once, write into successive regions, and advance the write offset only after the previous region's fence signals completion.
- Use
glMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT)for non-coherent maps before draws. - Enable debug output during development to catch mapping and barrier mistakes early.
Persistent mapped storage trades implicit driver synchronization for explicit application synchronization. If you can manage a ring buffer and fences, it removes per-frame reallocation from the hot path. If your data size changes unpredictably or you cannot track GPU completion, the simpler orphaning approach may still be the safer default.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.