fix: one VRAM-aware buffer budget for every GPU upload path

max_buffer_size is an API validation limit, not physical VRAM. NVIDIA
reports several GB of it on a 2 GB card, so sizing a chunk from it
produces buffers larger than the whole device. That is the same bug as
#203 (mesh_gpu), #358 (face3d_gpu) and the block-wire vertex buffer:
rediscovered three times because each upload path spelled the budget out
for itself.

mesh_gpu.rs was the only site that clamped the result. Move that clamp
into src/scene/pipeline/gpu_budget.rs and route every allocating path
through it, so a new upload path cannot reintroduce the mistake by
copying one of the unclamped expressions.

Five sites had no clamp at all: text_gpu (block-text instances),
wire_gpu (block-wire instances), and face3d_gpu (vertices, instances,
and upload_chunks).

Three behaviour changes worth noting:

  * Silhouette chunks were capped at a private 64 MiB constant and now
    use the shared 32 MiB budget.
  * The storage-hatch chunk limit was the minimum of two device limits,
    both of them validation figures; it is now also capped by the
    budget. The `*_fits` predicates next to it are genuine "would wgpu
    reject this" checks and are left alone.
  * mesh_gpu's instanced path used the unclamped ceiling to decide
    whether a mesh fits in a single buffer, which would still admit a
    multi-GB allocation. It now uses the clamped budget, so a few more
    meshes take the chunked path.

OCS_GPU_CHUNK_MIB overrides the cap. Raising it above the real VRAM is
the cheapest way to force an allocation failure and check that the
renderer degrades instead of aborting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-04 16:20:38 +03:00
1 parent 8552450aa8
commit 771a5b2c5c
7 files changed
+84 -35

No files matched your search

+6 -8
View File
@@ -1580,10 +1580,8 @@ pub fn build_mesh_batch_filtered(
// Derive the caps from the real device limit and vertex size. The previous
// fixed 6 M-vertex cap assumed 40 B/vertex, but `position_low` (RTE) grew
// MeshVertex to 52 B, so 6 M × 52 B = 312 MB blew past the 256 MB cap.
let hard_budget = (device.limits().max_buffer_size as usize / 10) * 9;
let budget = hard_budget.min(32 * 1024 * 1024);
let vsize = std::mem::size_of::<MeshVertex>();
let max_verts = (budget / vsize).max(3);
let budget = super::gpu_budget::buffer_budget(device);
let max_verts = super::gpu_budget::max_elements::<MeshVertex>(device).max(3);
// A triangle contributes three line segments (six standalone edge
// vertices). Bound chunks by the largest buffer produced for wire meshes.
let max_tris =
@@ -1800,9 +1798,9 @@ pub fn build_mesh_batch_filtered(
.verts
.len()
.saturating_mul(std::mem::size_of::<MeshVertex>())
<= hard_budget
<= budget
&& part.indices.len().saturating_mul(std::mem::size_of::<u32>())
<= hard_budget
<= budget
&& part
.set
.instance_source
@@ -1812,7 +1810,7 @@ pub fn build_mesh_batch_filtered(
.edge_verts
.len()
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
<= hard_budget;
<= budget;
let triangle_edges_fit = !part.include_edges
|| !source.edge_verts.is_empty()
|| part
@@ -1820,7 +1818,7 @@ pub fn build_mesh_batch_filtered(
.len()
.saturating_mul(2)
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
<= hard_budget;
<= budget;
feature_edges_fit && triangle_edges_fit
})
&& part