diff --git a/src/ifcviewer-wgpu/WgpuBufferPool.cpp b/src/ifcviewer-wgpu/WgpuBufferPool.cpp index 46233ab3fe..015ce13e33 100644 --- a/src/ifcviewer-wgpu/WgpuBufferPool.cpp +++ b/src/ifcviewer-wgpu/WgpuBufferPool.cpp @@ -37,6 +37,7 @@ void WgpuBufferPool::configure(WGPUInstance instance, WGPUDevice device, device_ = device; usage_ = usage; per_sub_buffer_capacity_ = per_sub_buffer_capacity; + last_growth_size_ = per_sub_buffer_capacity; label_prefix_ = label_prefix ? label_prefix : ""; } @@ -49,78 +50,86 @@ void WgpuBufferPool::destroy() { instance_ = nullptr; usage_ = 0; per_sub_buffer_capacity_ = 0; + last_growth_size_ = 0; growth_disabled_ = false; label_prefix_.clear(); } bool WgpuBufferPool::addSubBuffer() { if (!device_ || per_sub_buffer_capacity_ == 0) return false; - // A previous addSubBuffer at this capacity was refused — don't retry - // every alloc and re-log. The driver's per-allocation cap won't move - // without something freeing first, which only destroy() represents. if (growth_disabled_) return false; - // wgpu-native classifies "Not enough memory left" as Validation, not - // OutOfMemory — so we push both filters (nested: OOM inner, Validation - // outer). Either firing means the driver refused the allocation. - wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation); - wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory); + // 64 MB floor: smaller sub-buffers aren't worth the per-allocation + // bookkeeping cost (one bind group per chunk, free-list overhead). + // If the driver won't grant even 64 MB the pool is genuinely at + // its ceiling; growth_disabled_ latches and future grow attempts + // skip the doomed retry. + constexpr uint64_t MIN_SUB_BUFFER_BYTES = 64ull * 1024 * 1024; + uint64_t try_size = last_growth_size_ > 0 + ? last_growth_size_ + : per_sub_buffer_capacity_; + if (try_size < MIN_SUB_BUFFER_BYTES) try_size = MIN_SUB_BUFFER_BYTES; - char label[128]; - std::snprintf(label, sizeof(label), "%s.sub%zu", - label_prefix_.c_str(), sub_pools_.size()); + while (try_size >= MIN_SUB_BUFFER_BYTES) { + // wgpu-native classifies "Not enough memory left" as Validation, + // not OutOfMemory. Nested scopes: OOM inner, Validation outer. + wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation); + wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory); - WGPUBufferDescriptor desc = {}; - desc.usage = usage_; - desc.size = per_sub_buffer_capacity_; - desc.label.data = label; - desc.label.length = std::strlen(label); - WGPUBuffer buf = wgpuDeviceCreateBuffer(device_, &desc); + char label[128]; + std::snprintf(label, sizeof(label), "%s.sub%zu", + label_prefix_.c_str(), sub_pools_.size()); - struct PopResult { bool done = false; bool error = false; }; - auto pop = [&](PopResult& pr) { - WGPUPopErrorScopeCallbackInfo pcb = {}; - pcb.mode = WGPUCallbackMode_AllowProcessEvents; - pcb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type, - WGPUStringView, void* ud1, void* /*ud2*/) { - auto* p = static_cast(ud1); - p->done = true; - p->error = (type != WGPUErrorType_NoError); + WGPUBufferDescriptor desc = {}; + desc.usage = usage_; + desc.size = try_size; + desc.label.data = label; + desc.label.length = std::strlen(label); + WGPUBuffer buf = wgpuDeviceCreateBuffer(device_, &desc); + + struct PopResult { bool done = false; bool error = false; }; + auto pop = [&](PopResult& pr) { + WGPUPopErrorScopeCallbackInfo pcb = {}; + pcb.mode = WGPUCallbackMode_AllowProcessEvents; + pcb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type, + WGPUStringView, void* ud1, void* /*ud2*/) { + auto* p = static_cast(ud1); + p->done = true; + p->error = (type != WGPUErrorType_NoError); + }; + pcb.userdata1 = ≺ + wgpuDevicePopErrorScope(device_, pcb); + while (!pr.done) wgpuInstanceProcessEvents(instance_); }; - pcb.userdata1 = ≺ - wgpuDevicePopErrorScope(device_, pcb); - while (!pr.done) wgpuInstanceProcessEvents(instance_); - }; - PopResult oom_pop, validation_pop; - pop(oom_pop); - pop(validation_pop); + PopResult oom_pop, validation_pop; + pop(oom_pop); + pop(validation_pop); - if (!buf || oom_pop.error || validation_pop.error) { + if (buf && !oom_pop.error && !validation_pop.error) { + SubPool sp; + sp.buffer = buf; + sp.capacity = try_size; + sp.used = 0; + sp.free_ranges.push_back({0, try_size}); + sub_pools_.push_back(std::move(sp)); + last_growth_size_ = try_size; + qInfo().noquote().nospace() + << "[wgpu pool] added sub-buffer " << (sub_pools_.size() - 1) + << " (" << (try_size / (1024 * 1024)) << " MB); pool total now " + << (total_capacity_bytes() / (1024 * 1024)) << " MB"; + return true; + } if (buf) wgpuBufferRelease(buf); - // Log once — set growth_disabled_ so subsequent allocs don't - // re-try at this size. The pool runs at its hardware-limited - // ceiling from here; eviction handles the rest. - qInfo().noquote().nospace() - << "[wgpu pool] driver refused sub-buffer " << sub_pools_.size() - << " at " << (per_sub_buffer_capacity_ / (1024 * 1024)) - << " MB; pool capped at " << (total_capacity_bytes() / (1024 * 1024)) - << " MB across " << sub_pools_.size() << " sub-buffer(s) — growth disabled"; - growth_disabled_ = true; - return false; + try_size /= 2; } - SubPool sp; - sp.buffer = buf; - sp.capacity = per_sub_buffer_capacity_; - sp.used = 0; - sp.free_ranges.push_back({0, per_sub_buffer_capacity_}); - sub_pools_.push_back(std::move(sp)); - qInfo().noquote().nospace() - << "[wgpu pool] added sub-buffer " << (sub_pools_.size() - 1) - << " (" << (per_sub_buffer_capacity_ / (1024 * 1024)) << " MB); pool total now " - << (total_capacity_bytes() / (1024 * 1024)) << " MB"; - return true; + << "[wgpu pool] driver refused growth even at " + << (MIN_SUB_BUFFER_BYTES / (1024 * 1024)) << " MB; pool capped at " + << (total_capacity_bytes() / (1024 * 1024)) + << " MB across " << sub_pools_.size() << " sub-buffer(s) — growth disabled"; + growth_disabled_ = true; + return false; } WgpuBufferPool::Slice WgpuBufferPool::alloc(uint64_t size, uint64_t align) { diff --git a/src/ifcviewer-wgpu/WgpuBufferPool.h b/src/ifcviewer-wgpu/WgpuBufferPool.h index d0979de0e1..532ce64823 100644 --- a/src/ifcviewer-wgpu/WgpuBufferPool.h +++ b/src/ifcviewer-wgpu/WgpuBufferPool.h @@ -97,10 +97,18 @@ public: // Per-sub-buffer count, for diagnostics / logging. size_t sub_buffer_count() const { return sub_pools_.size(); } uint64_t per_sub_buffer_capacity_bytes() const { return per_sub_buffer_capacity_; } + // Best estimate of the size a *future* sub-buffer would land at: + // last_growth_size_ if we've ever grown (or just been configured), + // else the configured per_sub_buffer_capacity. After the driver + // refuses a size, halve-on-failure in addSubBuffer pushes this down + // so callers' "can this chunk fit via growth?" check stays honest. + uint64_t next_growth_size_bytes() const { + return last_growth_size_ > 0 ? last_growth_size_ : per_sub_buffer_capacity_; + } // Whether the pool can still attempt to add a sub-buffer. Flips to - // false the first time addSubBuffer is refused — eviction callers - // need this to know whether a future alloc could rescue them by - // growing, or whether eviction is the only path. + // false the first time addSubBuffer is refused even at the floor + // size — eviction callers need this to know whether a future alloc + // could rescue them, or whether eviction is the only path. bool can_grow() const { return !growth_disabled_ && per_sub_buffer_capacity_ > 0; } private: @@ -112,12 +120,18 @@ private: std::vector free_ranges; }; - // Append a new sub-buffer at per_sub_buffer_capacity_, wrapped in an - // OOM/Validation error scope so a failed allocation doesn't take the - // device down. Returns false on driver OOM (caller should treat as - // "pool is at its hardware-limited maximum"). After a failure, sets - // growth_disabled_ so subsequent allocs don't keep retrying (and - // log-spamming) at the same size that just refused. + // Append a new sub-buffer to the pool. Starts at last_growth_size_ + // (initially per_sub_buffer_capacity_) and halves on driver refusal + // before giving up — many Vulkan drivers cap single VkDeviceMemory + // allocations at a couple GB (e.g. NVIDIA: maxStorageBufferBindingSize + // is exactly 2 GB on consumer GeForce cards) or refuse big contiguous + // allocations once heap is fragmented, but happily grant smaller ones. + // Halving turns "stop at first refused 2 GB" into "2 GB + 1 GB + …", + // which on a 4 GB card lets us reach 3 GB total instead of 2 GB. + // Wrapped in OOM/Validation error scopes so failed attempts don't + // take the device down. Returns true on success at some size + // ≥ MIN_SUB_BUFFER_BYTES; false only when even the minimum size is + // refused, at which point growth_disabled_ latches. bool addSubBuffer(); std::vector sub_pools_; @@ -126,6 +140,13 @@ private: WGPUDevice device_ = nullptr; WGPUBufferUsage usage_ = 0; uint64_t per_sub_buffer_capacity_ = 0; + // The largest size addSubBuffer last *succeeded* at, in bytes. + // Starts at per_sub_buffer_capacity_ (the probe's discovered max) + // and decays as the driver refuses larger allocations. Future grow + // attempts start from here rather than re-trying the max every + // time — once the driver has refused 2 GB, retrying 2 GB on every + // subsequent grow is wasted work. + uint64_t last_growth_size_ = 0; bool growth_disabled_ = false; std::string label_prefix_; }; diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp index 38e23b25e6..4563ee309d 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp @@ -3588,11 +3588,11 @@ void WgpuViewportWindow::driveStreamingLoads() { // avoids wasted evict-then-fail loops. auto pool_can_fit = [&](uint64_t bytes) -> bool { if (pool_.largest_free_run_bytes() >= bytes) return true; - // Growth might still rescue us — but only if growth hasn't been - // refused at this size already. After a refusal, eviction is the - // sole path; the eviction loop must run until largest_free_run - // catches up. - if (pool_.can_grow() && pool_.per_sub_buffer_capacity_bytes() >= bytes) return true; + // Growth might still rescue us. Use next_growth_size_bytes() + // rather than per_sub_buffer_capacity_bytes() — after a refusal + // at e.g. 2 GB, halve-on-failure pushes the next achievable + // sub-buffer down to 1 GB; saying "fits if ≤2 GB" would lie. + if (pool_.can_grow() && pool_.next_growth_size_bytes() >= bytes) return true; return false; };