fix(render): chunk Face3D fill buffers to the device limit (#358)

Face3DGpu batched every visible 3DFACE quad and PolyfaceMesh /
PolygonMesh fill triangle into a single vertex buffer. A mesh-heavy
DWG (e.g. a Navisworks import) blows past wgpu's default 256 MB
max_buffer_size once enough layers are thawed; the resulting
uncaptured validation error panics inside iced's main-thread redraw
and aborts the whole process. Split the 3D and 2D fill streams into
per-limit chunks on whole-triangle boundaries — the same scheme
build_mesh_batch already uses (#203) — and register an
on_uncaptured_error handler so any remaining GPU validation error
degrades the frame instead of killing the session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin GriffinandClaude Fable 5 committed 2026-07-16 21:32:20 -07:00
1 parent 5bd683cf4e
commit a204237268
2 files changed
+64 -28

No files matched your search

+42 -19
View File
@@ -73,18 +73,28 @@ impl Face3DVertex {
// ── GPU handle ─────────────────────────────────────────────────────────────
/// One vertex buffer + draw count. Fill data is split into as many chunks
/// as the device's `max_buffer_size` requires: a mesh-heavy DWG (e.g. a
/// Navisworks import) can hold tens of millions of fill triangles, and at
/// 44 B/vertex a single batched buffer blows past the default 256 MB limit
/// once enough layers are thawed — wgpu then raises an uncaptured
/// validation error and aborts the process (#358). Same scheme as the
/// solid-mesh batch chunking in `mesh_gpu.rs` (#203).
pub struct Face3DChunk {
pub vertex_buffer: wgpu::Buffer,
pub vertex_count: u32,
}
pub struct Face3DGpu {
/// 3DFACE quads + PolyfaceMesh / PolygonMesh face triangles.
/// HiddenLine routes this through the depth-only pipeline so the
/// fragments occlude wires behind them without drawing visible
/// pixels.
pub vertex_buffer_3d: wgpu::Buffer,
pub vertex_count_3d: u32,
pub chunks_3d: Vec<Face3DChunk>,
/// Text-LOD greek dim, MultiLeader background, etc. — fills whose
/// source wire has an empty `points` list. Always rendered with the
/// normal face3d pipeline (visible in every mode).
pub vertex_buffer_2d: wgpu::Buffer,
pub vertex_count_2d: u32,
pub chunks_2d: Vec<Face3DChunk>,
}
impl Face3DGpu {
@@ -212,22 +222,35 @@ impl Face3DGpu {
}
}
let vertex_buffer_3d = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("face3d.vbuf.3d"),
contents: bytemuck::cast_slice(&verts_3d),
usage: wgpu::BufferUsages::VERTEX,
});
let vertex_buffer_2d = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("face3d.vbuf.2d"),
contents: bytemuck::cast_slice(&verts_2d),
usage: wgpu::BufferUsages::VERTEX,
});
Self {
vertex_buffer_3d,
vertex_count_3d: verts_3d.len() as u32,
vertex_buffer_2d,
vertex_count_2d: verts_2d.len() as u32,
chunks_3d: upload_chunks(device, &verts_3d, "face3d.vbuf.3d"),
chunks_2d: upload_chunks(device, &verts_2d, "face3d.vbuf.2d"),
}
}
}
/// Upload `verts` as one or more VERTEX buffers, each under 90% of the
/// device's `max_buffer_size`, split on whole-triangle boundaries. Also
/// keeps every chunk's vertex count well below `u32::MAX` so the draw
/// range never truncates.
fn upload_chunks(
device: &wgpu::Device,
verts: &[Face3DVertex],
label: &'static str,
) -> Vec<Face3DChunk> {
let budget = (device.limits().max_buffer_size as usize / 10) * 9; // 10% headroom
let vsize = std::mem::size_of::<Face3DVertex>();
// Round down to a multiple of 3 so triangles never straddle chunks.
let max_verts = ((budget / vsize).max(3) / 3) * 3;
verts
.chunks(max_verts)
.map(|c| Face3DChunk {
vertex_buffer: device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(c),
usage: wgpu::BufferUsages::VERTEX,
}),
vertex_count: c.len() as u32,
})
.collect()
}