Choosing an OpenGL Vertex Buffer Update Strategy: SubData, Orphaning, or Persistent Mapping
Per-frame vertex uploads can stall your render loop through hidden driver synchronization. Compare glBufferSubData, buffer orphaning, and persistent coherent mapping, with a fenced ring-buffer implementation and a validation plan.
31 Oct 2025, 09:24 UTC

If you stream vertex data every frame — particles, UI quads, CPU-side animation — the naive glBufferSubData call can quietly stall your render loop. When the buffer is still referenced by in-flight GPU commands, the driver must either block the CPU until the GPU finishes reading or allocate an internal copy. Both outcomes show up as inconsistent frame times that profiling often misattributes elsewhere.
The takeaway: for per-frame dynamic data, use buffer orphaning as a portable default, and move to persistent coherent mapped buffers (ARB_buffer_storage) when you can rely on OpenGL 4.4 or the extension and are willing to manage synchronization yourself.
The decision and its constraints
The core constraint is the CPU/GPU producer-consumer relationship. The CPU writes frame N+1's vertex data while the GPU may still be drawing frame N from the same buffer. Any strategy you pick is really a choice about who handles that hazard: the driver (implicitly, with possible stalls) or you (explicitly, with fences).
Assumptions for this guide: desktop OpenGL 3.3+ core context as a baseline, with 4.4 or ARB_buffer_storage required for the persistent-mapping path. Driver behavior for orphaning and usage hints is implementation-defined, so benchmark on your target hardware rather than assuming a universal winner.
Comparing the three supported options
| Strategy | GL requirement | Sync burden | Driver overhead | Complexity | Choose when |
|---|---|---|---|---|---|
glBufferSubData | Any | None (implicit) | High: risk of stalls or hidden copies | Very low | Small or infrequent updates; non-critical paths |
Orphaning (glBufferData with NULL, or GL_MAP_INVALIDATE_BUFFER_BIT) | 1.5+ | None (implicit) | Medium: driver allocates fresh backing storage | Low | Per-frame updates where 4.4 isn't guaranteed |
Persistent coherent mapping (glBufferStorage) | 4.4 / ARB_buffer_storage | Manual fences | Very low after setup | High | High-frequency streaming on known hardware |
Why glBufferSubData stalls
glBufferSubData gives the driver no hint that the old contents are disposable. If a draw call referencing the buffer is still queued or executing, a conforming driver cannot let your write clobber that data. It either waits (a pipeline bubble) or internally "renames" the buffer — allocating new storage behind your back. You get correctness for free, but no control over the cost.
Orphaning: the portable middle ground
Orphaning tells the driver the old contents are dead, so it can hand you fresh storage immediately while the GPU drains the old one. Two equivalent forms:
// Form 1: re-specify the whole buffer with a NULL data pointer
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, size, NULL, GL_STREAM_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, 0, size, vertices);
// Form 2: map with the invalidate bit, then write into the mapping
void* p = glMapBufferRange(GL_ARRAY_BUFFER, 0, size,
GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
memcpy(p, vertices, size);
glUnmapBuffer(GL_ARRAY_BUFFER);Run this on the GL thread that owns the context; no special privileges are needed beyond a current context. Use GL_STREAM_DRAW for data rewritten every frame and GL_DYNAMIC_DRAW for occasional updates. Wrong hints rarely break correctness but can cost measurable performance on some drivers.
Persistent coherent mapping: fastest, but you own synchronization
With glBufferStorage you map once at startup and keep the pointer. GL_MAP_PERSISTENT_BIT allows writes while the buffer is in use; GL_MAP_COHERENT_BIT makes CPU writes visible to the GPU without explicit flushes. The catch: nothing stops you from overwriting a region the GPU is still reading. You must fence.
The standard pattern is a ring buffer with one fence per segment:
// Setup (once)
const int SEGMENTS = 3; // ring depth; tune to real frame latency
GLbitfield flags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT
| GL_MAP_COHERENT_BIT;
glBufferStorage(GL_ARRAY_BUFFER, SEGMENTS * segSize, nullptr, flags);
auto* base = static_cast<std::byte*>(glMapBufferRange(
GL_ARRAY_BUFFER, 0, SEGMENTS * segSize,
flags | GL_MAP_INVALIDATE_BUFFER_BIT));
// Per frame: wait, write, draw, fence
int seg = frame % SEGMENTS;
if (fences[seg]) {
glClientWaitSync(fences[seg], 0, /*timeout ns*/ 16'000'000);
glDeleteSync(fences[seg]);
}
memcpy(base + seg * segSize, vertices, segSize);
// ... issue draws that read this segment ...
fences[seg] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);Two cautions worth repeating: a fixed ring depth of three is a common recommendation, but the correct depth depends on your actual frame latency — measure it. And coherent mapping is convenient, not always fastest; on some architectures explicit glFlushMappedBufferRange with a non-coherent persistent map performs better.
Validating the choice
Don't trust intuition here — driver behavior varies by vendor. A practical validation plan:
- Check feature availability before selecting a path:
glGetIntegerv(GL_NUM_EXTENSIONS)plusglGetStringi(GL_EXTENSIONS, i)to look forGL_ARB_buffer_storage, or check the context version for 4.4+. - Build a microbenchmark that streams a fixed byte count per frame through each strategy and record frame times; use a profiler such as RenderDoc to see where CPU time inside GL calls goes.
- Enable a debug context (
GL_DEBUG_OUTPUT) and watch for driver performance warnings when callingglBufferSubDataon in-flight buffers — several vendors emit exactly this hint. - For the persistent path, deliberately remove the fence wait and confirm your test harness detects corrupted output. This proves the sync logic is actually being exercised rather than silently unnecessary.
Limitations
Orphaning behavior is implementation-defined; some drivers already rename buffers internally for glBufferSubData, narrowing the gap. Persistent mapping is unavailable before GL 4.4 and on OpenGL ES before 3.0 (or without EXT_buffer_storage), where orphaning plus unsynchronized-then-invalidate range mapping is the portable fallback. Finally, mixing unsynchronized mapping with reads of the same buffer elsewhere in the frame can produce subtle, GPU-dependent artifacts — keep each frame's writes and reads in disjoint regions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.