forked from 1b-agent/opencadstudio-upstream
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:
1 parent
5bd683cf4e
commit
a204237268
2 files changed
+64
-28
No files matched your search
@@ -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()
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user