/********************************************************************************
* *
* 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 . *
* *
********************************************************************************/
#include "WgpuBufferPool.h"
#include
#include
#include
WgpuBufferPool::~WgpuBufferPool() {
destroy();
}
void WgpuBufferPool::configure(WGPUInstance instance, WGPUDevice device,
WGPUBufferUsage usage,
uint64_t per_sub_buffer_capacity,
const char* label_prefix) {
destroy();
instance_ = instance;
device_ = device;
usage_ = usage;
per_sub_buffer_capacity_ = per_sub_buffer_capacity;
label_prefix_ = label_prefix ? label_prefix : "";
}
void WgpuBufferPool::destroy() {
for (auto& sp : sub_pools_) {
if (sp.buffer) wgpuBufferRelease(sp.buffer);
}
sub_pools_.clear();
device_ = nullptr;
instance_ = nullptr;
usage_ = 0;
per_sub_buffer_capacity_ = 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);
char label[128];
std::snprintf(label, sizeof(label), "%s.sub%zu",
label_prefix_.c_str(), sub_pools_.size());
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);
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_);
};
PopResult oom_pop, validation_pop;
pop(oom_pop);
pop(validation_pop);
if (!buf || oom_pop.error || validation_pop.error) {
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;
}
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;
}
WgpuBufferPool::Slice WgpuBufferPool::alloc(uint64_t size, uint64_t align) {
Slice out;
if (size == 0 || align == 0) return out;
// First-fit across all sub-buffers. When none fits, try to grow by
// adding another sub-buffer and retry once.
for (int attempt = 0; attempt < 2; ++attempt) {
for (size_t sp_idx = 0; sp_idx < sub_pools_.size(); ++sp_idx) {
SubPool& sp = sub_pools_[sp_idx];
for (size_t i = 0; i < sp.free_ranges.size(); ++i) {
const FreeRange& r = sp.free_ranges[i];
const uint64_t aligned = (r.offset + (align - 1)) & ~(align - 1);
const uint64_t pad = aligned - r.offset;
if (pad >= r.size) continue;
if (size > r.size - pad) continue;
const uint64_t post_off = aligned + size;
const uint64_t post_size = (r.offset + r.size) - post_off;
if (pad == 0 && post_size == 0) {
sp.free_ranges.erase(sp.free_ranges.begin() + i);
} else if (pad == 0) {
sp.free_ranges[i] = {post_off, post_size};
} else if (post_size == 0) {
sp.free_ranges[i] = {r.offset, pad};
} else {
sp.free_ranges[i] = {r.offset, pad};
sp.free_ranges.insert(sp.free_ranges.begin() + i + 1,
{post_off, post_size});
}
sp.used += size;
out.buffer = sp.buffer;
out.offset = aligned;
out.size = size;
out.sub_idx = int(sp_idx);
return out;
}
}
// Existing sub-buffers can't fit. Grow once before giving up.
if (attempt == 0) {
if (!addSubBuffer()) break;
}
}
return out;
}
void WgpuBufferPool::free(const Slice& s) {
if (!s.valid()) return;
if (s.sub_idx < 0 || size_t(s.sub_idx) >= sub_pools_.size()) return;
SubPool& sp = sub_pools_[size_t(s.sub_idx)];
assert(s.offset + s.size <= sp.capacity);
size_t i = 0;
while (i < sp.free_ranges.size() && sp.free_ranges[i].offset < s.offset) ++i;
sp.free_ranges.insert(sp.free_ranges.begin() + i, {s.offset, s.size});
sp.used -= s.size;
if (i + 1 < sp.free_ranges.size()
&& sp.free_ranges[i].offset + sp.free_ranges[i].size == sp.free_ranges[i + 1].offset) {
sp.free_ranges[i].size += sp.free_ranges[i + 1].size;
sp.free_ranges.erase(sp.free_ranges.begin() + i + 1);
}
if (i > 0
&& sp.free_ranges[i - 1].offset + sp.free_ranges[i - 1].size == sp.free_ranges[i].offset) {
sp.free_ranges[i - 1].size += sp.free_ranges[i].size;
sp.free_ranges.erase(sp.free_ranges.begin() + i);
}
}
uint64_t WgpuBufferPool::total_capacity_bytes() const {
uint64_t s = 0;
for (const auto& sp : sub_pools_) s += sp.capacity;
return s;
}
uint64_t WgpuBufferPool::total_used_bytes() const {
uint64_t s = 0;
for (const auto& sp : sub_pools_) s += sp.used;
return s;
}
uint64_t WgpuBufferPool::largest_free_run_bytes() const {
uint64_t m = 0;
for (const auto& sp : sub_pools_) {
for (const auto& r : sp.free_ranges) {
if (r.size > m) m = r.size;
}
}
return m;
}