mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 14:41:25 +00:00
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>
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -105,6 +106,12 @@ public:
|
||||
uint64_t next_growth_size_bytes() const {
|
||||
return last_growth_size_ > 0 ? last_growth_size_ : per_sub_buffer_capacity_;
|
||||
}
|
||||
// Smallest sub-buffer worth adding: below this the per-allocation
|
||||
// bookkeeping (one bind group per chunk, free-list overhead) outweighs
|
||||
// the space. Growth that cannot reach the floor — the driver refusing,
|
||||
// or the budget leaving less than this — is not attempted.
|
||||
static constexpr uint64_t MIN_SUB_BUFFER_BYTES = 64ull * 1024 * 1024;
|
||||
|
||||
// Whether the pool can still attempt to add a sub-buffer. Flips to
|
||||
// false the first time addSubBuffer is refused even at the floor
|
||||
// size — eviction callers need this to know whether a future alloc
|
||||
@@ -112,7 +119,8 @@ public:
|
||||
bool can_grow() const {
|
||||
return !growth_disabled_ && per_sub_buffer_capacity_ > 0
|
||||
&& (max_total_capacity_bytes_ == 0
|
||||
|| total_capacity_bytes() < max_total_capacity_bytes_);
|
||||
|| total_capacity_bytes() + MIN_SUB_BUFFER_BYTES
|
||||
<= max_total_capacity_bytes_);
|
||||
}
|
||||
|
||||
// Whether a growth is in flight. On web that window is real time — a
|
||||
@@ -127,6 +135,20 @@ public:
|
||||
// is a bad_alloc that -fno-exceptions turns into an uncatchable abort, so
|
||||
// the async grow-OOM detection can't save us — we must stop first.
|
||||
void setMaxTotalCapacity(uint64_t max_bytes) { max_total_capacity_bytes_ = max_bytes; }
|
||||
uint64_t max_total_capacity_bytes() const { return max_total_capacity_bytes_; }
|
||||
|
||||
// Release whole sub-buffers, newest first, until total capacity is
|
||||
// ≤ target_bytes (or nothing is left). Before each sub-buffer is
|
||||
// dropped, `evict_sub_buffer(sub_idx)` is invoked so the owner can
|
||||
// free every slice that lives in it — the pool does not know what a
|
||||
// slice holds, and a sub-buffer is only released once it is empty.
|
||||
// Releasing from the back keeps every surviving Slice::sub_idx valid.
|
||||
// Returns the number of bytes released. This is how the cache yields
|
||||
// memory to the required tier (see GpuBudget); on web a provisional
|
||||
// sub-buffer that is still validating is left alone and the shrink is
|
||||
// retried once that resolves.
|
||||
uint64_t shrinkToCapacity(uint64_t target_bytes,
|
||||
const std::function<void(int sub_idx)>& evict_sub_buffer);
|
||||
|
||||
// Proactively add a sub-buffer (no allocation). On web this kicks off the
|
||||
// async provisional-validation cycle so validated free space appears a
|
||||
@@ -161,6 +183,9 @@ private:
|
||||
// capacity/free tallies skip provisional sub-pools so an
|
||||
// unvalidated (possibly invalid) buffer is never handed out.
|
||||
bool provisional = false;
|
||||
// False only for addSubBufferForTesting's fake handles: release
|
||||
// paths (shrinkToCapacity, destroy) then skip the wgpu calls.
|
||||
bool owns_handle = true;
|
||||
};
|
||||
|
||||
// Append a new sub-buffer to the pool. Starts at last_growth_size_
|
||||
|
||||
Reference in New Issue
Block a user