If you’ve ever toggled VSync on and watched your frame rate seesaw between smooth and stuttery, you’ve met the limitations of double buffering. Triple buffering adds a third buffer to the swapchain so that the GPU can keep rendering even when a frame is queued for presentation, delivering smoother animation and higher average FPS with VSync on.
Double buffering
With double buffering, we use two buffers, one for presenting to the screen and one for rendering off-screen, swapping between them as new frames are produced.
Buffer | Role |
---|---|
Front | Scanned out to the display |
Back | GPU renders into this buffer |
VSync ON
Buffer | Behavior |
---|---|
Front | Shown on screen until the next VSync |
Back | Swapped with the front buffer only at VSync |
Key Effects:
- If rendering finishes early, GPU waits (stall).
- If rendering finishes late, Frame is delayed until the next refresh (stutter).
- No tearing is observed, but can cause uneven frame pacing.
VSync OFF
Buffer | Behavior |
---|---|
Front | Can be replaced mid-scanout |
Back | Swapped with the front buffer immediately when ready |
Key Effects:
- GPU never waits, lowest latency.
- Can cause screen tearing (front buffer changes mid-frame).
- Smoother if GPU is consistently faster than refresh rate, but can look jittery otherwise.
Triple buffering
With triple buffering we use three buffers: Front + Back-1 + Back-2. Even if one back buffer is queued for the next VSync, there’s still another one free to render into, keeping the pipeline busy.
Buffer | Role |
---|---|
Front | Scanned out to the display |
Back 1 | First render target |
Back 2 | Second render target |
VSync ON
Buffer | Behavior |
---|---|
Front | Shown on screen until the next VSync |
Back 1 | Queued for presentation once rendering is complete |
Back 2 | GPU can start rendering here immediately, even if Back 1 is still queued |
Key Effects:
- GPU never stalls, always has a buffer to render into.
- No tearing
- Much smoother frame pacing than double-buffered VSync.
Trade-offs to consider:
- Higher memory footprint as one extra full-resolution color buffer + depth/stencil used.
- Slightly higher input latency than double buffering (VSync off), because a displayed frame may be 1 to 2 frames old.
- Slightly higher power usage as the GPU idles less.
VSync OFF
Buffer | Behavior |
---|---|
Front | Can be replaced mid-scanout (tearing possible) |
Back 1 | Can be swapped immediately when rendering is complete |
Back 2 | GPU can start rendering here while Back 1 is waiting to be displayed |
Key Effects:
- GPU does not stall, always has a free buffer to render into.
- Screen tearing can still occur because frames are not synchronized with refresh.
- Lowest possible latency (even lower than triple-buffer + VSync ON).
- Provides little benefit over double-buffer + VSync OFF unless CPU/GPU are out of sync (it helps absorb frame spikes).
Triple buffering vs. “true” triple buffering
Terminology differs:
- Mailbox / flip model: (Vulkan MAILBOX, DXGI flip): the compositor takes the latest rendered frame and drops older ones.great for latency.
- FIFO with 3 images: frames queue in order, can increase latency but guarantees no frame is skipped.
Both avoid GPU stalls but their latency behavior differs. If MAILBOX is available, it’s often the best-feeling option.
VRR (Variable Refresh Rate)
VRR is a display technology (e.g., G-SYNC from NVIDIA, FreeSync from AMD) where the monitor’s refresh rate dynamically adapts to match the GPU’s frame output rate. The result: No tearing, low latency, and smoother frame pacing.
This gives competitive players the best of both worlds: no VSync-induced stutter or lag, but also no screen tearing.
With variable refresh rate:
- Double buffering + VRR already eliminates most VSync stalls and tearing.
- Triple buffering can still help if frame time fluctuates or the VRR window is exceeded (e.g., below min Hz), but the benefit is smaller.
The big picture
For playing a single-player game, watching animations, or working on a 3D application where smoothness matters more than shaving off the last few milliseconds of input delay, triple buffering with VSync enabled becomes the “sweet spot”:
- It removes tearing.
- Keeps frame pacing smooth (no microstutter from GPU stalls).
- Gives a higher average FPS than double-buffered VSync.
- The slight extra input lag (usually 1 frame of latency) is rarely noticeable outside of competitive contexts.
In summary:
Mode | Tearing? | Smoothness | Latency | Best For |
---|---|---|---|---|
Double Buffer + VSync OFF | Yes | Can stutter | Lowest | Competitive esports, latency-critical apps |
Double Buffer + VSync ON | No | Can stutter if GPU misses VSync | Higher (GPU stalls) | Casual players who hate tearing |
Triple Buffer + VSync ON | No | Smooth (no stalls) | Slightly higher than DB - VSync OFF | Most games, general use |
VRR | No | Smooth | Low | Competitive or casual, if hardware supports it |
Modern API mapping
Let’s explore how triple buffering is implemented in modern graphics APIs.
Vulkan
- Swapchain images: choose
minImageCount = 3
. - Present mode determines queueing semantics:
VK_PRESENT_MODE_FIFO_KHR
: Always VSync (queue behaves like triple buffer whenminImageCount ≥ 3
).VK_PRESENT_MODE_MAILBOX_KHR
: “One in flight, one mailboxed”, effectively triple-buffer-like with latest-frame-wins (low latency, no tearing on supported displays).VK_PRESENT_MODE_IMMEDIATE_KHR
: No VSync (can tear).
VkSwapchainCreateInfoKHR sci{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
sci.surface = surface;
sci.minImageCount = 3; // request triple buffering
sci.imageFormat = format;
sci.imageExtent = extent;
sci.presentMode = VK_PRESENT_MODE_FIFO_KHR; // or MAILBOX if available
vkCreateSwapchainKHR(device, &sci, nullptr, &swapchain);
Direct3D 12 / DXGI
- Use flip model swap effects (
DXGI_SWAP_EFFECT_FLIP_DISCARD
orFLIP_SEQUENTIAL
). - Set BufferCount = 3.
- With
WaitableObject
/fences, you can tune in-flight frames.
DXGI_SWAP_CHAIN_DESC1 desc = {};
desc.Width = width;
desc.Height = height;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.BufferCount = 3; // triple buffering
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; // flip model
desc.SampleDesc = {1, 0};
OpenGL
In OpenGL, glSwapInterval(1)
toggles VSync. The actual buffer count is driver / window-system dependent. On many platforms you can request a three buffer swap via the windowing layer (WGL/GLX/EGL attributes) or by using a framework that exposes it.
Practical Approach
- Start with 3 images in your swapchain by default
Creating three buffers (instead of two) ensures the GPU always has a free image to render into, even if one is on screen and another is queued for display. This is what enables triple buffering and prevents GPU stalls. - Limit CPU frames in flight using fences/semaphores
“Frames in flight” means how many frames the CPU has submitted to the GPU before waiting for one to finish.- If you never wait, the CPU can outrun the GPU and produce unbounded latency (your input feels delayed).
- Use a fence per frame to ensure you have only 1 or 2 frames in flight. This keeps the pipeline full but latency predictable.
- Use a frame pacing strategy
When the engine runs faster than the display refresh rate, frames may be unevenly spaced, causing micro-stutter.- Delay presentation slightly to make frame delivery evenly spaced.
- Some engines implement a pacing library or simply sleep until the next ideal present time.
- Measure, don’t guess
- Frame time histogram: Shows the spread of frame times, not just the average FPS.
- Present-to-present intervals: Check that frames are arriving at consistent intervals.
- Input latency: If you are building a game or interactive app, measure from input event to on-screen effect.
Common Pitfalls
There are some common pitfalls to be aware of when implementing triple buffering.
Starvation via unlimited in-flight work
Without fences, the CPU can get several frames ahead of the GPU. This means the frame you just rendered might only display several refreshes later, adding input lag. The solution is to use fences / semaphores to wait when you have too many frames queued.
Excessive memory footprint
Each swapchain image is a full-resolution color buffer.
- Triple buffering means three copies in memory.
- If MSAA is used, you also need resolve targets and a depth/stencil buffer.
- Reuse depth/stencil buffers across swapchain images when possible.
Assuming MAILBOX mode is supported
VK_PRESENT_MODE_MAILBOX_KHR
is great when available, but not all platforms support it. Always query supported present modes and fall back to FIFO if needed.
Here’s a simple frame loop using fences and semaphores in Vulkan:
// 1. Acquire a free swapchain image to render into.
// Waits on a fence/semaphore so we don't exceed MAX_FRAMES_IN_FLIGHT.
uint32_t imageIndex = 0;
vkAcquireNextImageKHR(device, swapchain, UINT64_MAX,
imageAvailableSem[frame], VK_NULL_HANDLE, &imageIndex);
// 2. Submit GPU work for this frame.
// Wait on "image available" semaphore, then signal "render finished".
VkSubmitInfo submit = {};
submit.waitSemaphoreCount = 1;
submit.pWaitSemaphores = &imageAvailableSem[frame]; // wait until image is ready
submit.pSignalSemaphores = &renderFinishedSem[frame]; // signal when done
vkQueueSubmit(graphicsQueue, 1, &submit, inFlightFence[frame]);
// 3. Present the rendered image to the display.
// Wait for rendering to complete before presenting.
VkPresentInfoKHR present = {};
present.waitSemaphoreCount = 1;
present.pWaitSemaphores = &renderFinishedSem[frame];
present.swapchainCount = 1;
present.pSwapchains = &swapchain;
present.pImageIndices = &imageIndex;
vkQueuePresentKHR(present);
// 4. Advance to next frame index (bounded by MAX_FRAMES_IN_FLIGHT).
// If we've submitted too many frames, this will wait on a fence,
// ensuring CPU doesn't outrun GPU.
frame = (frame + 1) % MAX_FRAMES_IN_FLIGHT; // typically 2 or 3
If you enjoyed this post or found the content helpful, I’d greatly appreciate your support! You can contribute via GitHub Sponsors to help me continue creating tutorials, demos, and open-source experiments. Every contribution, no matter the size, makes a difference and helps keep projects like this alive. Thank you!