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