Drops the per-machine "guess the OOM ceiling" budget knob in favour of
a single buffer pool whose capacity is *probed* at device-init time.
The runtime answers the question: descend from min(maxBufferSize, 4 GB)
through OOM error scopes, accept the largest size that allocates
cleanly. On a desktop wgpu-native v29 box this lands at 2 GB; on
browser-class platforms it'll land at 256 MB – 1 GB depending on the
implementation. Same code path either way.
Architecture:
- WgpuBufferPool (new): single WGPUBuffer + free-list sub-allocator
with adjacent-range coalescing and first-fit. 256 B alignment for
storage-binding offsets.
- Chunks now hold (pool_vertex_offset, pool_vertex_size) and
(pool_index_offset, pool_index_size) instead of per-chunk WGPUBuffer
handles. Load = pool.alloc + queueWriteBuffer. Unload = pool.free.
- Bind groups bind pool_.buffer() at the chunk's specific (offset, size)
for both the vertex and index storage bindings.
- Eviction queries pool.largest_free_run_bytes() instead of a tracked
budget; the two-phase LRU/distance evictor's policy is unchanged.
What this fixes:
- No more gpu-alloc-rs fragmentation OOM: one VkDeviceMemory block
instead of N per-chunk blocks with rounding overhead. On the test
dataset (~3 GB on disk, 562 k visible instances) the wgpu backend
now runs through to render without OOM at any point.
- No --streaming-vram-mb knob, no hardcoded budget constant, no
per-machine calibration. The pool size adapts to whatever the
runtime grants.
Notes:
- Error scope probing: wgpu-native v29 classifies "Not enough memory
left" as WGPUErrorType_Validation, not OutOfMemory. We push both
filters (nested) and treat either firing as probe failure.
- The 4 GB probe cap is principled, not magic: above that, wgpu-native's
advertised maxBufferSize is sometimes a sentinel (1 TB) that just
forces wasteful halving steps. 4 GB is the largest buffer any
realistic WebGPU implementation will grant a single allocation today.
- Pool destroy()/release happens after model release in shutdown() so
the underlying buffer outlives every bind group that references it.
Follow-ups: spatial chunking (task #22) for finer eviction granularity;
cull perf needs work at 100+ models / 1M+ instances (separate from
streaming concerns).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>