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()
}
+22 -9
View File
@@ -281,6 +281,15 @@ pub struct Pipeline {
impl Pipeline {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
// wgpu's default uncaptured-error handler panics, and inside iced's
// main-thread redraw that panic aborts the whole process (#358). A
// validation error on a pathological drawing must not take the app
// (and the user's unsaved work) down with it — log it and keep the
// session alive; the affected frame renders incompletely at worst.
device.on_uncaptured_error(std::sync::Arc::new(|e: wgpu::Error| {
eprintln!("[gpu] uncaptured wgpu error (frame degraded): {e}");
}));
// ── Shared frame uniform buffer (view_proj etc.) ───────────────────
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("viewer.uniform_buffer"),
@@ -1648,9 +1657,9 @@ impl Pipeline {
self.gpu_face3d_edges =
WireGpu::from_batch(device, face3d_wires, depth_map, self.wire_const_bgl.as_ref());
// Fill buffer split: 3D quads + PolyfaceMesh / PolygonMesh face
// tris go to `vertex_buffer_3d` (gated by `keep_3d_mesh_fills`);
// tris go to `chunks_3d` (gated by `keep_3d_mesh_fills`);
// 2D fills (text-LOD greek, MultiLeader background) go to
// `vertex_buffer_2d` and are visible in every mode.
// `chunks_2d` and are visible in every mode.
let keep_3d_mesh_fills = !wireframe_only;
let has_any_2d_fill = all_wires
.iter()
@@ -2176,7 +2185,7 @@ impl Pipeline {
// pipeline in HiddenLine so wires hidden behind them disappear.
// 2D fills (text greek, MultiLeader bg) always draw with colour.
if let Some(ref fill) = self.gpu_face3d_fill {
if fill.vertex_count_3d > 0 || fill.vertex_count_2d > 0 {
if !fill.chunks_3d.is_empty() || !fill.chunks_2d.is_empty() {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("face3d.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -2201,19 +2210,23 @@ impl Pipeline {
});
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
if fill.vertex_count_3d > 0 {
if !fill.chunks_3d.is_empty() {
if hidden_line {
pass.set_pipeline(&self.face3d_depth_pipeline);
} else {
pass.set_pipeline(&self.face3d_pipeline);
}
pass.set_vertex_buffer(0, fill.vertex_buffer_3d.slice(..));
pass.draw(0..fill.vertex_count_3d, 0..1);
for c in &fill.chunks_3d {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.draw(0..c.vertex_count, 0..1);
}
}
if fill.vertex_count_2d > 0 {
if !fill.chunks_2d.is_empty() {
pass.set_pipeline(&self.face3d_pipeline);
pass.set_vertex_buffer(0, fill.vertex_buffer_2d.slice(..));
pass.draw(0..fill.vertex_count_2d, 0..1);
for c in &fill.chunks_2d {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.draw(0..c.vertex_count, 0..1);
}
}
}
}