diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index fb9a6067e..2f6023d94 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -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, /// 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, } 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 { + let budget = (device.limits().max_buffer_size as usize / 10) * 9; // 10% headroom + let vsize = std::mem::size_of::(); + // 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() +} diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index e892e5422..5f30e81f8 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -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); + } } } }