wgpu: default present mode to Mailbox (was Fifo)

# Why

The stage-1 default was Fifo because it is the only present mode
WebGPU spec *requires* every backend to support — conservative,
"always works", lowest power. But on DX12 Fifo maps to a DXGI
flip-discard swap chain whose default \`MaximumFrameLatency\` is 3,
which queues ~50ms of pre-rendered work between submit and display.
Even when the FPS counter shows 60 the cursor-bound interactions
(marquee, pivot, orbit) feel ~3 frames behind because they *are*.

# What

Mailbox: vsync-aligned (no tearing) with a one-frame queue
(last-frame-wins). ~16ms input→display latency. wgpu-native handles
the fallback to Fifo automatically on backends that don't implement
Mailbox (Vulkan + NVIDIA on Linux is the historical one).

# Trade-off

Mailbox uncaps the render loop: with no vsync gating, the
\`requestUpdate()\` → render → present loop spins at whatever Qt's
event loop allows (~600 Hz on an empty scene), and the GPU does
useful-but-discarded work on each redundant frame. On any non-trivial
scene the GPU is the bottleneck and the loop self-paces near the
display rate. The FPS counter measures \`render()\` invocations, not
unique frames the user actually sees — that's expected.

# Override

WGPU_PRESENT_MODE=fifo restores the old behaviour for the rare
laptop-battery / heat-conscious case. fifo_relaxed / immediate also
still work as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-06-02 16:45:54 +10:00
parent 6b72a60d8c
commit edc598357b
+41 -26
View File
@@ -1927,43 +1927,58 @@ void WgpuViewportWindow::configureSurface(int width_px, int height_px) {
cfg.width = uint32_t(width_px);
cfg.height = uint32_t(height_px);
// Present mode. WGPU_PRESENT_MODE=fifo|fifo_relaxed|mailbox|immediate
// (default fifo). Recommended for fly-mode stutter: fifo_relaxed.
// fifo strict vsync. Frame waiting > display refresh
// pushes the present to the NEXT refresh, producing
// the visible "double-frame" jump when cull or
// chunk-apply briefly exceeds budget.
// fifo_relaxed — adaptive vsync. Syncs to display when frame hits
// the budget; allows tear when it doesn't. Removes
// the missed-vsync stutter while keeping smooth
// presentation when we're under budget. Best
// compromise for the fly-mode jitter case.
// mailbox — uncapped, last-frame-wins, no tearing. Not
// supported on Vulkan + NVIDIA on Linux (falls
// back to Fifo); use fifo_relaxed instead there.
// immediate — uncapped, frames presented as soon as ready,
// may tear. Useful for raw-throughput benchmarking.
WGPUPresentMode pm = WGPUPresentMode_Fifo;
const char* pm_name = "fifo";
// overrides; default is mailbox.
// mailboxDEFAULT. Vsync-aligned (no tearing), one-frame
// queue (last-frame-wins). ~16ms input→display
// latency. wgpu-native falls back to fifo
// automatically on backends that don't implement
// it (Vulkan + NVIDIA on Linux is the historical
// one).
// fifo — strict vsync, 23 frame DXGI/swapchain queue
// (~50ms latency). Most power-efficient, but
// visibly laggy for cursor-bound interactions
// (marquee, pivot, orbit). Was the stage-1 default
// because Fifo is the only mode the WebGPU spec
// *requires* backends to support.
// fifo_relaxed — adaptive vsync. Tears when a frame misses the
// refresh deadline, smooth otherwise. Useful in
// the fly-mode-jitter case where Fifo would
// double-frame on a missed vsync.
// immediate — no vsync at all. Frames presented as soon as
// ready, may tear on motion. Useful for raw
// throughput benchmarking.
WGPUPresentMode pm = WGPUPresentMode_Mailbox;
const char* pm_name = "mailbox";
if (const char* s = std::getenv("WGPU_PRESENT_MODE")) {
if (std::strcmp(s, "fifo_relaxed") == 0) {
if (std::strcmp(s, "fifo") == 0) {
pm = WGPUPresentMode_Fifo; pm_name = "fifo";
} else if (std::strcmp(s, "fifo_relaxed") == 0) {
pm = WGPUPresentMode_FifoRelaxed; pm_name = "fifo_relaxed";
} else if (std::strcmp(s, "mailbox") == 0) {
pm = WGPUPresentMode_Mailbox; pm_name = "mailbox";
} else if (std::strcmp(s, "immediate") == 0) {
pm = WGPUPresentMode_Immediate; pm_name = "immediate";
} else if (std::strcmp(s, "fifo") != 0) {
} else {
qWarning().noquote().nospace()
<< "[wgpu] unknown WGPU_PRESENT_MODE=" << s
<< " (expected fifo|fifo_relaxed|mailbox|immediate); using fifo";
<< " (expected fifo|fifo_relaxed|mailbox|immediate); using mailbox";
}
}
cfg.presentMode = pm;
if (pm != WGPUPresentMode_Fifo && !surface_configured_) {
qInfo().noquote().nospace()
<< "[wgpu] present mode = " << pm_name
<< (pm == WGPUPresentMode_FifoRelaxed
? " (adaptive vsync — sync if in budget, tear if not)"
: " (vsync OFF — framerate uncapped)");
if (!surface_configured_) {
const char* note = "";
switch (pm) {
case WGPUPresentMode_Mailbox:
note = " (vsync-aligned, no queue lag — default)"; break;
case WGPUPresentMode_Fifo:
note = " (strict vsync, may queue 23 frames)"; break;
case WGPUPresentMode_FifoRelaxed:
note = " (adaptive vsync — sync if in budget, tear if not)"; break;
case WGPUPresentMode_Immediate:
note = " (vsync OFF — framerate uncapped, may tear)"; break;
default: break;
}
qInfo().noquote().nospace() << "[wgpu] present mode = " << pm_name << note;
}
cfg.alphaMode = WGPUCompositeAlphaMode_Auto;