ifcviewer-web: handle device loss so GPU pressure can't freeze the tab

Reported: open a model in the web viewer, then launch the desktop
BonsaiViewer and open another model — the browser tab freezes, and a
fresh web tab then fails with "RequestDevice failed: Not enough memory
left". Root cause is GPU-memory contention: two heavy GPU clients on one
GPU, and the desktop app's allocations starve the browser's WebGPU
process, which reclaims our device.

We can't conjure GPU memory, but we were amplifying the symptom: with no
device-lost handler, render() kept driving a dead device —
wgpuSurfaceGetCurrentTexture returns Lost every frame and the
reconfigure + requestFrame retry becomes a tight per-RAF loop that hangs
the tab. Now the web device descriptor wires a device-lost callback that
latches device_lost_ (ignoring the intentional Destroyed reason from our
own shutdown); render() bails while set, so the loop goes idle instead of
spinning, and the console logs guidance to reload. The fresh-tab
RequestDevice OOM is genuine GPU exhaustion — surfaced as before, now with
a clearer message.

Verified the lost callback doesn't disturb Dawn-web's RequestDevice (all
5 web smoke tests still init + pass); desktop unaffected (device_lost_
stays false). The real contention path can't be reproduced in the headless
harness, so the loss handler itself is covered by review, not a test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-06-30 11:24:56 +10:00
parent a6cfb2651a
commit bc31e91e35
2 changed files with 30 additions and 0 deletions
+22
View File
@@ -1625,6 +1625,24 @@ void ViewportCore::initWgpuAsyncWeb(std::function<void(bool)> on_complete) {
// RequestDevice silently never resolves the promise when passed
// nullptr.
WGPUDeviceDescriptor dd = {};
// Device-lost handler. If another GPU client (e.g. the desktop app
// launched alongside this tab) exhausts GPU memory, the browser can
// reclaim our device. Without this we'd keep driving render() on a
// dead device — wgpuSurfaceGetCurrentTexture returns Lost forever and
// the reconfigure+requestFrame retry becomes a tight loop that hangs
// the tab. Latch a flag so render() bails and the loop goes idle.
// Skip the intentional Destroyed reason (fired by our own shutdown).
dd.deviceLostCallbackInfo.mode = WGPUCallbackMode_AllowSpontaneous;
dd.deviceLostCallbackInfo.callback =
[](const WGPUDevice* /*dev*/, WGPUDeviceLostReason reason,
WGPUStringView msg, void* ud1, void* /*ud2*/) {
if (reason == WGPUDeviceLostReason_Destroyed) return;
auto* core = static_cast<ViewportCore*>(ud1);
core->device_lost_ = true;
Log::warn() << "[wgpu] device lost (GPU memory reclaimed?): "
<< svToStr(msg) << " — reload the page";
};
dd.deviceLostCallbackInfo.userdata1 = c->core;
wgpuAdapterRequestDevice(adapter, &dd, dcb);
};
acb.userdata1 = ctx;
@@ -5400,6 +5418,10 @@ inline float srgbToLinear(float c) {
void ViewportCore::render() {
if (!device_ || !queue_ || !surface_) return;
// Device lost (e.g. GPU memory reclaimed by another client). Stop here so
// we don't busy-loop reconfiguring a dead surface — that retry storm is
// what otherwise freezes the tab. The page logs guidance to reload.
if (device_lost_) return;
Stopwatch frame_timer;
frame_timer.start();
+8
View File
@@ -688,6 +688,14 @@ private:
WGPUTextureFormat surface_view_format_ = WGPUTextureFormat_Undefined;
bool surface_configured_ = false;
public:
// Latched by the device-lost callback (web) when the GPU reclaims our
// device — typically GPU-memory pressure from another client. render()
// bails while set so the loop doesn't hammer a dead surface (which freezes
// the tab). Public so the spontaneous C callback can set it.
bool device_lost_ = false;
private:
// ---- Pipelines + bind-group layouts (built once after init) -------------
//
// Main render pipeline group: one shader module + two bind group