Files
IfcOpenShell/src/ifcviewer/GpuAllocScope.cpp
T
Dion Moult ab99024307 ifcviewer: budget the geometry cache and make required allocations fallible
Loading enough models drove the chunk pool to the driver's refusal point,
after which the first click aborted: the pick attachments are allocated
lazily, wgpu-native reported their OOM as a validation error nobody
observed, and the invalid views reached wgpuQueueSubmit, which panics
across the FFI boundary. Two policy defects compounding: the cache was
allowed to take the last byte, and nothing but the pool's own growth was
treated as fallible.

GPU memory is now two tiers. Required allocations (per-pixel attachments,
a model's metadata buffers, readback staging) are eager, deterministic
and fallible; the chunk pool is an elastic cache that grows only to a
budget and yields whenever a required allocation fails.

- GpuBudget (pure, unit-tested): desktop derives the budget from the
  driver's free-memory report minus a reserve for the attachments at 4K;
  web keeps the wasm-heap cap; either lowers it on pressure. The budget's
  source differs per platform, the mechanism does not.
- GpuAllocScope: the OOM/Validation error-scope dance in one place,
  synchronous on wgpu-native, provisional on Dawn-web. BufferPool's
  inline copy now uses it.
- BufferPool::shrinkToCapacity releases whole sub-buffers newest-first
  after the owner empties them; growth clamps to the budget instead of
  overshooting.
- ViewportCore::allocateRequired runs any required creation under a
  scope and, on failure, lowers the budget, evicts and releases cache
  sub-buffers, waits for the device to reclaim them, and retries until
  it fits or the cache is at its floor. Pick attachments are created with
  the other attachments in configureSurface; render() skips a frame
  rather than submit invalid views; a model whose buffers cannot fit is
  not loaded instead of aborting.

Verified on a 4 GB GeForce: the pool clamps itself at the derived budget
(256+256+67 MB for a 579 MB budget) and, in a standalone check against
the real device, a pool grown to the driver's refusal point observes a
failed required allocation, releases 320 MB and succeeds on retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:42:12 +10:00

93 lines
3.8 KiB
C++

/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "GpuAllocScope.h"
#include <cassert>
namespace {
// Shared between the two pop callbacks; freed by whichever fires last.
struct PendingPop {
GpuAllocScope::Callback on_result;
int remaining = 2;
bool error = false;
// Desktop: points at a flag on end()'s stack frame so the spin-wait
// can observe completion without touching this (freed) object.
bool* done = nullptr;
};
void onPopped(WGPUPopErrorScopeStatus, WGPUErrorType type, WGPUStringView,
void* userdata1, void* /*userdata2*/) {
auto* pending = static_cast<PendingPop*>(userdata1);
if (type != WGPUErrorType_NoError) pending->error = true;
if (--pending->remaining > 0) return;
const bool ok = !pending->error;
bool* done = pending->done;
GpuAllocScope::Callback on_result = std::move(pending->on_result);
delete pending;
on_result(ok);
if (done) *done = true;
}
} // namespace
GpuAllocScope::GpuAllocScope(WGPUInstance instance, WGPUDevice device)
: instance_(instance), device_(device) {
// Validation outer, OutOfMemory inner: each pop sees the errors of
// its own filter, and an OOM reported under either classification
// reaches one of the two.
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation);
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory);
}
GpuAllocScope::~GpuAllocScope() {
assert(ended_ && "GpuAllocScope::end() must be called exactly once");
}
void GpuAllocScope::end(Callback on_result) {
assert(!ended_);
ended_ = true;
bool done = false;
auto* pending = new PendingPop{std::move(on_result)};
WGPUPopErrorScopeCallbackInfo cb = {};
#if defined(__EMSCRIPTEN__)
// Dawn-web resolves pops from the JS event loop; the caller proceeds
// provisionally and hears back in on_result.
cb.mode = WGPUCallbackMode_AllowSpontaneous;
#else
// wgpu-native fires these from wgpuInstanceProcessEvents, which we
// spin below so on_result has run by the time end() returns.
cb.mode = WGPUCallbackMode_AllowProcessEvents;
pending->done = &done;
#endif
cb.callback = onPopped;
cb.userdata1 = pending;
wgpuDevicePopErrorScope(device_, cb); // OutOfMemory (inner)
wgpuDevicePopErrorScope(device_, cb); // Validation (outer)
#if !defined(__EMSCRIPTEN__)
while (!done) wgpuInstanceProcessEvents(instance_);
#else
(void)done;
#endif
}