ifcviewer: carve the margin out of the cache on the first driver growth refusal

On web there is no device-memory query, so the budget sat at the wasm
heap cap while the pool grew until Chrome's GPU process refused
(observed at 1920 MB on a 66-model session). Nothing acted on that
refusal: the cache kept the last byte, and the next attachment
reallocation (orbit resize, 76 MB) had to fail first — a few frames of
invalid-TextureView errors — before pressure feedback carved out room.

The refusal IS the query-less platform's device report. render() now
answers the first one by lowering the budget by the required-tier
margin and shrinking the pool to it, so attachments and model buffers
find headroom without ever failing. Desktop gets the same fallback for
drivers GpuMemory cannot answer for.

Reproduced under Playwright with a native process squeezing the GPU:
Chrome refuses at 512 MB, the margin (256 MB) is released on the next
frame, and the session continues with zero uncaptured WebGPU errors —
previously the same squeeze produced invalid-view frames before
recovery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-08-24 09:36:12 +10:00
parent 6f24133d35
commit 311b75a955
3 changed files with 28 additions and 0 deletions
+7
View File
@@ -123,6 +123,13 @@ public:
<= max_total_capacity_bytes_);
}
// True once the driver (not the budget) has refused growth even at the
// floor size. On platforms with no memory query this is the only device
// report there is: the owner treats the first refusal as a pressure
// event and carves the required-tier margin out of the cache before a
// required allocation has to fail for it (see ViewportCore::render).
bool growth_was_refused() const { return growth_disabled_; }
// Whether a growth is in flight. On web that window is real time — a
// provisional sub-buffer validates asynchronously a frame or two later — so
// the streaming driver has to know that free space is still on its way and
+18
View File
@@ -7732,6 +7732,24 @@ void ViewportCore::render() {
Stopwatch frame_timer;
frame_timer.start();
// The driver refusing pool growth is the query-less platform's device
// report (web has no memory query; the pool just grew until the GPU
// process said no). Answer it once, immediately: lower the budget so
// the required-tier margin comes back out of the cache NOW, instead of
// the next attachment resize having to fail — and paint broken frames —
// before pressure feedback carves the same room.
if (pool_.growth_was_refused() && !pool_growth_refusal_handled_) {
pool_growth_refusal_handled_ = true;
const double mb = 1.0 / (1024.0 * 1024.0);
Log::info() << "[wgpu] driver refused geometry-cache growth at "
<< double(pool_.total_capacity_bytes()) * mb
<< " MB -- reserving the required-tier margin out of the cache";
if (budget_.onPressure(pool_.total_capacity_bytes(),
GpuBudget::kFixedMarginBytes, 0)) {
applyBudgetToPool();
}
}
// Drain any HiZ async readbacks completed since last frame.
if (hiz_enabled_) drainHizReadbacks();
+3
View File
@@ -1155,6 +1155,9 @@ private:
void releaseRenderAttachments();
GpuBudget budget_;
// Latch: the pool's first driver-refused growth has been answered by
// carving the margin out of the cache (see render()).
bool pool_growth_refusal_handled_ = false;
// Adapter ids, read once at init, for matching the driver's memory
// report to the card wgpu is actually using.
std::uint32_t adapter_vendor_id_ = 0;