ifcviewer: stop the live budget from over-shrinking and from going stale

A 66-model session showed the pool reach a 1938 MB ceiling, the next
poll lower the budget to 1756, and the shrink drop 402 MB (73+73+256)
for a 182 MB excess, which the pool then spent seconds re-growing. Two
causes.

The release granularity is whole sub-buffers but the shrink ran "until
capacity ≤ target", so the last 36 MB of excess cost a 256 MB
sub-buffer. shrinkToCapacity now never undershoots — it releases only
while doing so keeps capacity ≥ target, leaving a sub-buffer's worth of
excess for the margin to absorb — and the pressure path uses a separate
releaseAtLeast(bytes), whose contract is the opposite: free at least
what the failed allocation needs, whatever the granularity. Resident
geometry is also only evicted once the pool is over budget by half the
margin (GpuBudget::shrinkTarget), so report jitter does not trigger a
shrink-and-reload.

The ceiling was a second old when the pool grew into it, and the upload
staging that rides on growth had pushed device free memory to ~74 MB —
below the driver's observed refusal point — before the next scheduled
poll. pollDeviceMemory now re-derives the budget immediately after any
sub-buffer is added, so the next growth decision sees the device as it
is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-08-23 19:19:44 +10:00
parent 6201c4052b
commit 24616ed655
7 changed files with 152 additions and 42 deletions
+22 -10
View File
@@ -137,18 +137,26 @@ public:
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.
// Release whole sub-buffers, newest first. Before each 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. Both return the
// 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 caller retries once it resolves.
//
// shrinkToCapacity never goes *below* target_bytes: a sub-buffer is
// released only while doing so keeps capacity ≥ target, so an excess
// smaller than the newest sub-buffer releases nothing (the budget's
// margin absorbs it) instead of dropping 256 MB for the last 36.
uint64_t shrinkToCapacity(uint64_t target_bytes,
const std::function<void(int sub_idx)>& evict_sub_buffer);
// releaseAtLeast frees sub-buffers until at least `bytes` have gone
// (or nothing is left) — for a failed required allocation that needs
// that much back no matter the granularity.
uint64_t releaseAtLeast(uint64_t 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
@@ -201,6 +209,10 @@ private:
// ≥ MIN_SUB_BUFFER_BYTES; false only when even the minimum size is
// refused, at which point growth_disabled_ latches.
bool addSubBuffer();
// Drop the newest sub-buffer after `evict_sub_buffer` empties it.
// Returns its capacity; 0 when the pool is empty or the newest
// sub-buffer is still provisional (web).
uint64_t releaseNewestSubBuffer(const std::function<void(int sub_idx)>& evict_sub_buffer);
#if defined(__EMSCRIPTEN__)
// Web-only async-growth resolver. Called from the AllowSpontaneous