forked from 1b-agent/opencadstudio-upstream
fix(render): size mesh-batch buffers to the device limit
The batch split at a fixed 6M-vertex cap that assumed 40 B/vertex, but MeshVertex grew to 52 B when the double-single `position_low` field was added — so a full chunk was 6M x 52 = 312 MB, past wgpu's 256 MB max_buffer_size, and Device::create_buffer panicked on large models. Derive the caps from device.limits().max_buffer_size and the real vertex size, bound both the vertex buffer and the (fatter) wire-index buffer, and split any single mesh too large for one chunk into triangle-soup sub-chunks so no batch buffer can exceed the device limit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e74a11724
commit
d0dcc6cb4c
1 file changed
+61
-15
@@ -110,11 +110,6 @@ impl Highlight {
|
||||
// so no per-mesh state is needed between draws. Built once per geometry epoch —
|
||||
// selection/hover no longer rebuild it (that tint is dropped in the batch path).
|
||||
|
||||
/// Cap on vertices per batch buffer: 40 B/vertex × 6 M ≈ 240 MB, under the
|
||||
/// default 256 MB `max_buffer_size`. Solids beyond the cap spill into the next
|
||||
/// chunk.
|
||||
const BATCH_MAX_VERTS: usize = 6_000_000;
|
||||
|
||||
pub struct MeshBatchChunk {
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
pub index_buffer: wgpu::Buffer,
|
||||
@@ -155,7 +150,21 @@ fn make_chunk(
|
||||
|
||||
/// Concatenate every set's first non-empty LOD into a few large GPU buffers.
|
||||
/// Returns the chunks plus the total triangle count drawn (for diagnostics).
|
||||
///
|
||||
/// Every emitted buffer stays under the device's `max_buffer_size` (default
|
||||
/// 256 MB). Both the vertex buffer (`size_of::<MeshVertex>()` B/vert) and the
|
||||
/// wire-index buffer (6 u32 = 24 B/triangle — the fattest index buffer) are
|
||||
/// bounded; a single mesh too large for one chunk is split into triangle-soup
|
||||
/// sub-chunks so an XREF-heavy model can never overflow a single buffer (#203).
|
||||
pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<MeshBatchChunk>, u64) {
|
||||
// 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 budget = (device.limits().max_buffer_size as usize / 10) * 9; // 10% headroom
|
||||
let vsize = std::mem::size_of::<MeshVertex>();
|
||||
let max_verts = (budget / vsize).max(3);
|
||||
let max_tris = (budget / (6 * 4)).max(1); // wire-index buffer: 6 u32 per tri
|
||||
|
||||
let mut chunks = Vec::new();
|
||||
let mut verts: Vec<MeshVertex> = Vec::new();
|
||||
let mut indices: Vec<u32> = Vec::new();
|
||||
@@ -165,21 +174,59 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
|
||||
let Some(mesh) = set.lods.iter().find(|m| !m.indices.is_empty()) else {
|
||||
continue;
|
||||
};
|
||||
if !verts.is_empty() && verts.len() + mesh.verts.len() > BATCH_MAX_VERTS {
|
||||
let has_normals = mesh.normals.len() == mesh.verts.len();
|
||||
let vtx = |vi: usize| MeshVertex {
|
||||
position: mesh.verts[vi],
|
||||
normal: if has_normals { mesh.normals[vi] } else { [0.0, 1.0, 0.0] },
|
||||
color: mesh.color,
|
||||
position_low: mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]),
|
||||
};
|
||||
let mesh_tris = mesh.indices.len() / 3;
|
||||
total_tris += mesh_tris as u64;
|
||||
|
||||
// A single mesh larger than a whole chunk: emit as triangle-soup
|
||||
// sub-chunks (corners expanded, no vertex sharing) so each buffer fits.
|
||||
if mesh.verts.len() > max_verts || mesh_tris > max_tris {
|
||||
if !verts.is_empty() {
|
||||
chunks.push(make_chunk(device, &verts, &indices, &wire_indices));
|
||||
verts.clear();
|
||||
indices.clear();
|
||||
wire_indices.clear();
|
||||
}
|
||||
let tris_per = (max_verts / 3).min(max_tris).max(1);
|
||||
let mut t = 0;
|
||||
while t < mesh_tris {
|
||||
let end = (t + tris_per).min(mesh_tris);
|
||||
let (mut sv, mut si, mut swi) = (Vec::new(), Vec::new(), Vec::new());
|
||||
for tri in t..end {
|
||||
let ix = &mesh.indices[tri * 3..tri * 3 + 3];
|
||||
let b = sv.len() as u32;
|
||||
sv.push(vtx(ix[0] as usize));
|
||||
sv.push(vtx(ix[1] as usize));
|
||||
sv.push(vtx(ix[2] as usize));
|
||||
si.extend_from_slice(&[b, b + 1, b + 2]);
|
||||
swi.extend_from_slice(&[b, b + 1, b + 1, b + 2, b + 2, b]);
|
||||
}
|
||||
chunks.push(make_chunk(device, &sv, &si, &swi));
|
||||
t = end;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Flush when adding this mesh would overflow either the vertex buffer
|
||||
// or the wire-index buffer.
|
||||
if !verts.is_empty()
|
||||
&& (verts.len() + mesh.verts.len() > max_verts
|
||||
|| wire_indices.len() / 6 + mesh_tris > max_tris)
|
||||
{
|
||||
chunks.push(make_chunk(device, &verts, &indices, &wire_indices));
|
||||
verts.clear();
|
||||
indices.clear();
|
||||
wire_indices.clear();
|
||||
}
|
||||
let base = verts.len() as u32;
|
||||
let has_normals = mesh.normals.len() == mesh.verts.len();
|
||||
for (i, &pos) in mesh.verts.iter().enumerate() {
|
||||
verts.push(MeshVertex {
|
||||
position: pos,
|
||||
normal: if has_normals { mesh.normals[i] } else { [0.0, 1.0, 0.0] },
|
||||
color: mesh.color,
|
||||
position_low: mesh.verts_low.get(i).copied().unwrap_or([0.0; 3]),
|
||||
});
|
||||
for i in 0..mesh.verts.len() {
|
||||
verts.push(vtx(i));
|
||||
}
|
||||
for &idx in &mesh.indices {
|
||||
indices.push(base + idx);
|
||||
@@ -188,7 +235,6 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
|
||||
let (a, b, c) = (base + tri[0], base + tri[1], base + tri[2]);
|
||||
wire_indices.extend_from_slice(&[a, b, b, c, c, a]);
|
||||
}
|
||||
total_tris += (mesh.indices.len() / 3) as u64;
|
||||
}
|
||||
if !indices.is_empty() {
|
||||
chunks.push(make_chunk(device, &verts, &indices, &wire_indices));
|
||||
|
||||
Reference in new issue
Block a user