wgpu streaming (5/4): per-chunk indices + LRU/distance eviction (stopgap)

Defers index buffers per-chunk (alongside vertex bytes) so streaming
fully delivers on its "don't load until visible" contract — the previous
per-model index buffer was upfront-loaded and tipped scenes >~1.5 GB into
allocator OOM at frame 1.

Adds residency tracking + a two-phase evictor: (1) drop LRU non-visible
chunks first, (2) if everything resident is visible-this-frame, drop the
farthest-from-eye chunk only when the candidate to load is closer. This
gives monotonic convergence to "closest visible chunks fit the budget"
instead of "first 4 win, rest never load."

Default budget set to 1 GB — explicitly a stopgap, documented inline.
The per-machine OOM ceiling on wgpu-native (caused by allocator
fragmentation from one VkDeviceMemory per createBuffer call) cannot be
solved by tuning this knob. The proper fix is a probed single-pool
buffer with sub-allocation, tracked under task #16.

Caveat: LOD1 indices are now force-disabled when chunking — per-chunk
buffers only carry LOD0. Re-enabling needs LOD1 to participate in the
chunk plan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-28 11:15:11 +10:00
parent 5a3e5167df
commit 71e61dd8a5
4 changed files with 350 additions and 75 deletions
+25 -6
View File
@@ -76,6 +76,11 @@ struct WgpuModelGpuData {
// path always sets is_resident=true so existing code is unchanged.
struct Chunk {
WGPUBuffer vertex_storage = nullptr;
// Per-chunk index buffer (was previously per-model). Each chunk
// covers a contiguous range of mesh ids, so its indices form a
// contiguous slice of the model's overall index data. Storing
// per-chunk lets streaming defer index loading alongside vertices.
WGPUBuffer index_buffer = nullptr;
WGPUBuffer visible_draws_buffer = nullptr;
WGPUBuffer prefix_sums_buffer = nullptr;
WGPUBuffer per_chunk_uniform = nullptr;
@@ -103,6 +108,12 @@ struct WgpuModelGpuData {
uint64_t vertex_byte_offset = 0; // 0 == start of vertex section
uint64_t vertex_byte_size = 0;
// Same for indices. index_first_u32 is in u32 units relative to
// the start of the index section (sidecar stores raw u32 indices,
// no byte-level offset is needed beyond multiplying by 4).
uint64_t index_first_u32 = 0;
uint64_t index_count = 0;
// World-space AABB covering every instance whose mesh lives in
// this chunk. Used by cull to reject whole chunks against the
// frustum before iterating instances — and by the streaming
@@ -113,6 +124,12 @@ struct WgpuModelGpuData {
float aabb_max[3] = { -std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity() };
// LRU marker for streaming eviction. Updated to the window's
// streaming_frame_idx_ every frame the chunk is rendered (i.e.
// total_visible_draws > 0). The evictor picks the smallest value
// among non-visible resident chunks when it needs to free VRAM.
uint64_t last_visible_frame_idx = 0;
};
std::vector<Chunk> chunks;
@@ -121,17 +138,19 @@ struct WgpuModelGpuData {
// from this file. Empty path = legacy non-streaming load.
std::string streaming_file_path;
uint64_t streaming_vertex_section_offset = 0;
uint64_t streaming_index_section_offset = 0;
// For each mesh in meshes[], the chunk it lives in plus the chunk-local
// vertex offset (where its vertex range starts in chunk's vertex_storage).
// Populated at applyCachedModel time.
// offsets into that chunk's vertex_storage and index_buffer. Populated
// at applyCachedModel time; consumed by cullModelCpuCompute when it
// populates VisibleDrawGpu entries.
std::vector<uint32_t> mesh_chunk_idx;
std::vector<uint32_t> mesh_chunk_local_base_vertex;
std::vector<uint32_t> mesh_chunk_local_ebo_first_u32;
// Model-shared buffers (NOT chunked — sizes safely under 128 MB on real
// scenes: index buffer up to ~76 MB on a 19 M-index model; instance and
// mesh storage are far smaller).
WGPUBuffer index_buffer = nullptr; // u32 mesh-local indices (read as storage)
// Model-shared buffers. Mesh + instance storage are small (<10 MB on
// any real scene we've seen); the chunked index buffer lives in Chunk
// alongside vertex_storage so streaming can defer both together.
WGPUBuffer mesh_storage = nullptr; // MeshGpu[]: aabb_min/max
WGPUBuffer instance_storage = nullptr; // InstanceGpu[]: transform + ids