ifcviewer: detect web pool-grow OOM via provisional sub-buffers

On web we skip the desktop error-scope spin-wait (it blocks the JS event
loop and hangs the page). The old web addSubBuffer then judged success by
`buf != nullptr` — but Dawn-web returns a NON-NULL error buffer on OOM,
so the pool committed an invalid sub-buffer, alloc handed out slices in
it, and every chunk_bind_group built against it failed ("BindGroup is
invalid" spam + a wgpuQueueSubmit panic). Loading a model larger than the
browser's WebGPU budget triggered exactly this.

Add the grown sub-buffer as *provisional* (alloc and the capacity/free
tallies skip it) and validate it through a non-blocking AllowSpontaneous
PopErrorScope. resolveProvisionalGrowth() clears the flag when it's good,
or drops the sub-buffer and latches growth_disabled_ on a real OOM — at
which point the streaming evictor bounds the working set to what fits
instead of cascading. Only one provisional grow is in flight at a time
(growth_pending_). Desktop keeps its synchronous halve-retry path
unchanged. All 100 unit tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-06-29 12:13:34 +10:00
parent 2237b4acdd
commit 89beee6514
2 changed files with 109 additions and 14 deletions
+18
View File
@@ -132,6 +132,11 @@ private:
uint64_t capacity = 0;
uint64_t used = 0;
std::vector<FreeRange> free_ranges;
// Web-only: true between addSubBuffer creating the buffer and the
// async error scope confirming it didn't OOM. alloc() and the
// capacity/free tallies skip provisional sub-pools so an
// unvalidated (possibly invalid) buffer is never handed out.
bool provisional = false;
};
// Append a new sub-buffer to the pool. Starts at last_growth_size_
@@ -148,6 +153,15 @@ private:
// refused, at which point growth_disabled_ latches.
bool addSubBuffer();
#if defined(__EMSCRIPTEN__)
// Web-only async-growth resolver. Called from the AllowSpontaneous
// PopErrorScope callback addSubBuffer arms: `failed` true means the
// provisional sub-buffer OOM'd → drop it and latch growth_disabled_;
// false means it's good → clear its provisional flag so alloc can use
// it. Clears growth_pending_ either way.
void resolveProvisionalGrowth(bool failed);
#endif
std::vector<SubPool> sub_pools_;
WGPUInstance instance_ = nullptr;
@@ -162,6 +176,10 @@ private:
// subsequent grow is wasted work.
uint64_t last_growth_size_ = 0;
bool growth_disabled_ = false;
// Web-only: a provisional sub-buffer is awaiting async OOM validation.
// Blocks a second concurrent grow so a stalled validation can't spawn
// a pile of sub-buffers.
bool growth_pending_ = false;
std::string label_prefix_;
};