diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index 15e58d1db..2d973e48e 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -20,7 +20,6 @@ use crate::scene::model::wire_model::WireModel; use iced::wgpu; -use iced::wgpu::util::DeviceExt; pub(crate) fn planar_solid_faces_view(wire: &WireModel, view_dir: glam::Vec3) -> bool { if !wire.fill_is_2d_solid || wire.fill_is_3d { @@ -187,6 +186,7 @@ impl Face3DGpu { /// legacy planar SOLID interior while keeping other 2-D overlays. pub fn from_wires( device: &wgpu::Device, + queue: &wgpu::Queue, face3d_wires: &[WireModel], all_wires: &[WireModel], keep_3d_mesh_fills: bool, @@ -302,6 +302,7 @@ impl Face3DGpu { let (block_chunks_3d, block_chunks_2d) = upload_block_chunks( device, + queue, all_wires, keep_3d_mesh_fills, show_2d_solid_fills, @@ -309,8 +310,8 @@ impl Face3DGpu { depth_map, ); Self { - chunks_3d: upload_chunks(device, &verts_3d, "face3d.vbuf.3d"), - chunks_2d: upload_chunks(device, &verts_2d, "face3d.vbuf.2d"), + chunks_3d: upload_chunks(device, queue, &verts_3d, "face3d.vbuf.3d"), + chunks_2d: upload_chunks(device, queue, &verts_2d, "face3d.vbuf.2d"), block_chunks_3d, block_chunks_2d, } @@ -319,6 +320,7 @@ impl Face3DGpu { fn upload_block_chunks( device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[WireModel], keep_3d_mesh_fills: bool, show_2d_solid_fills: bool, @@ -415,21 +417,25 @@ fn upload_block_chunks( .chunks(max_instances) .map(|chunk| { ( - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("face3d.block.instances"), - contents: bytemuck::cast_slice(chunk), - usage: wgpu::BufferUsages::VERTEX, - }), + super::gpu_upload::upload_buffer( + device, + queue, + "face3d.block.instances", + chunk, + wgpu::BufferUsages::VERTEX, + ), chunk.len() as u32, ) }) .collect(); for vertex_chunk in vertices.chunks(max_vertices) { - let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("face3d.block.vertices"), - contents: bytemuck::cast_slice(vertex_chunk), - usage: wgpu::BufferUsages::VERTEX, - }); + let vertex_buffer = super::gpu_upload::upload_buffer( + device, + queue, + "face3d.block.vertices", + vertex_chunk, + wgpu::BufferUsages::VERTEX, + ); for (instance_buffer, instance_count) in &instance_buffers { let chunk = BlockFace3DChunk { vertex_buffer: vertex_buffer.clone(), @@ -454,6 +460,7 @@ fn upload_block_chunks( /// range never truncates. fn upload_chunks( device: &wgpu::Device, + queue: &wgpu::Queue, verts: &[Face3DVertex], label: &'static str, ) -> Vec { @@ -462,11 +469,13 @@ fn upload_chunks( 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_buffer: super::gpu_upload::upload_buffer( + device, + queue, + label, + c, + wgpu::BufferUsages::VERTEX, + ), vertex_count: c.len() as u32, }) .collect() diff --git a/src/scene/pipeline/gpu_upload.rs b/src/scene/pipeline/gpu_upload.rs new file mode 100644 index 000000000..98ae90bcb --- /dev/null +++ b/src/scene/pipeline/gpu_upload.rs @@ -0,0 +1,116 @@ +//! Buffer creation that survives an out-of-memory device. +//! +//! `wgpu::util::DeviceExt::create_buffer_init` — and any +//! `mapped_at_creation: true` followed by `get_mapped_range_mut()` — maps the +//! new buffer unconditionally. When the allocation fails wgpu hands back an +//! *invalid* buffer, and mapping an invalid buffer panics: +//! +//! ```text +//! thread 'main' panicked at wgpu-29.0.4/src/backend/wgpu_core.rs:2253:18: +//! Error in Buffer::get_mapped_range: Validation Error +//! Caused by: Buffer with 'block_wire.vertices' label is invalid +//! ``` +//! +//! That panic unwinds through iced's main-thread redraw, trips the `wgpu-hal` +//! swapchain assertion in a destructor, and turns into a **non-unwinding +//! abort** — the process dies with the user's unsaved drawing in it. +//! +//! Writing through the queue instead never maps. A failed allocation produces +//! a validation error on the `write_buffer`, which reaches the handler +//! installed by `install_gpu_error_handler` and degrades the frame, which is +//! what that handler was written for: "A bad frame must degrade, not end the +//! session." + +use iced::wgpu; + +/// Create a buffer and fill it without ever mapping it. +/// +/// `COPY_DST` is added to `usage` because the contents arrive via +/// `Queue::write_buffer`. Callers pass the same usage flags they would have +/// given `create_buffer_init`. +/// +/// Empty `data` still produces a one-element buffer: wgpu rejects zero-sized +/// buffers, and every draw path here already guards its instance/vertex count +/// before issuing a draw, so a placeholder allocation is harmless. +pub fn upload_buffer( + device: &wgpu::Device, + queue: &wgpu::Queue, + label: &str, + data: &[T], + usage: wgpu::BufferUsages, +) -> wgpu::Buffer { + let bytes: &[u8] = bytemuck::cast_slice(data); + upload_bytes(device, queue, label, bytes, std::mem::size_of::(), usage) +} + +/// `upload_buffer` for data already flattened to bytes. `min_size` is the +/// placeholder size used when `bytes` is empty. +pub fn upload_bytes( + device: &wgpu::Device, + queue: &wgpu::Queue, + label: &str, + bytes: &[u8], + min_size: usize, + usage: wgpu::BufferUsages, +) -> wgpu::Buffer { + const ALIGN: usize = wgpu::COPY_BUFFER_ALIGNMENT as usize; + let logical = bytes.len().max(min_size).max(ALIGN); + // `write_buffer` copies whole `COPY_BUFFER_ALIGNMENT` units, so both the + // allocation and the source slice are rounded up to it. Every vertex and + // instance type here is `#[repr(C)]` over 4-byte fields, so the padding + // branch below is a safety net rather than a routine cost. + let size = logical.div_ceil(ALIGN) * ALIGN; + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size: size as u64, + usage: usage | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + if !bytes.is_empty() { + if bytes.len().is_multiple_of(ALIGN) { + queue.write_buffer(&buffer, 0, bytes); + } else { + let mut padded = bytes.to_vec(); + padded.resize(bytes.len().div_ceil(ALIGN) * ALIGN, 0); + queue.write_buffer(&buffer, 0, &padded); + } + } + buffer +} + +/// Allocate `size` bytes and write `data` into the front of it. +/// +/// For buffers deliberately larger than their initial contents — the wire +/// arena reserves headroom so later edits patch in place instead of +/// reallocating. Same no-mapping guarantee as [`upload_buffer`]; `usage` must +/// already contain `COPY_DST` for the later patches, and it is added here +/// regardless. +pub fn alloc_with_prefix( + device: &wgpu::Device, + queue: &wgpu::Queue, + label: &str, + size: u64, + data: &[T], + usage: wgpu::BufferUsages, +) -> wgpu::Buffer { + const ALIGN: u64 = wgpu::COPY_BUFFER_ALIGNMENT; + let size = size.max(ALIGN).div_ceil(ALIGN) * ALIGN; + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size, + usage: usage | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let bytes: &[u8] = bytemuck::cast_slice(data); + if !bytes.is_empty() { + let unit = ALIGN as usize; + if bytes.len().is_multiple_of(unit) { + queue.write_buffer(&buffer, 0, bytes); + } else { + let mut padded = bytes.to_vec(); + padded.resize(bytes.len().div_ceil(unit) * unit, 0); + queue.write_buffer(&buffer, 0, &padded); + } + } + buffer +} diff --git a/src/scene/pipeline/hatch_gpu/mod.rs b/src/scene/pipeline/hatch_gpu/mod.rs index 1585d043e..8731bfd9c 100644 --- a/src/scene/pipeline/hatch_gpu/mod.rs +++ b/src/scene/pipeline/hatch_gpu/mod.rs @@ -167,7 +167,7 @@ impl HatchGpu { .filter(|hatch| hatch.boundary.len() >= 3) .cloned() .collect(); - *resident = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable); + *resident = StorageHatchBatch::build(device, queue, &self.bind_group_layout, &renderable); } HatchBackend::Texture { resident, .. } => { let renderable: Vec = hatches @@ -198,7 +198,7 @@ impl HatchGpu { .filter(|hatch| hatch.boundary.len() >= 3) .cloned() .collect(); - *preview = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable); + *preview = StorageHatchBatch::build(device, queue, &self.bind_group_layout, &renderable); } HatchBackend::Texture { preview, .. } => { let renderable: Vec = hatches diff --git a/src/scene/pipeline/hatch_gpu/storage.rs b/src/scene/pipeline/hatch_gpu/storage.rs index e9ec6c4fc..d44c1eab1 100644 --- a/src/scene/pipeline/hatch_gpu/storage.rs +++ b/src/scene/pipeline/hatch_gpu/storage.rs @@ -3,7 +3,6 @@ use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily}; use iced::wgpu; -use iced::wgpu::util::DeviceExt; // ── GPU structs ──────────────────────────────────────────────────────────── // @@ -188,6 +187,7 @@ impl StorageHatchBatch { /// Builds whole-hatch chunks within the device's buffer limits. pub(super) fn build( device: &wgpu::Device, + queue: &wgpu::Queue, bgl: &wgpu::BindGroupLayout, hatches: &[HatchModel], ) -> Vec { @@ -224,12 +224,13 @@ impl StorageHatchBatch { ranges.push(start..hatches.len()); ranges .into_iter() - .filter_map(|range| Self::build_chunk(device, bgl, &hatches[range])) + .filter_map(|range| Self::build_chunk(device, queue, bgl, &hatches[range])) .collect() } fn build_chunk( device: &wgpu::Device, + queue: &wgpu::Queue, bgl: &wgpu::BindGroupLayout, hatches: &[HatchModel], ) -> Option { @@ -500,42 +501,56 @@ impl StorageHatchBatch { return None; } - let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("hatch.vertex"), - contents: bytemuck::cast_slice(&verts), - usage: wgpu::BufferUsages::VERTEX, - }); - let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("hatch.index"), - contents: bytemuck::cast_slice(&indices), - usage: wgpu::BufferUsages::INDEX, - }); - let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("hatch.placements"), - contents: bytemuck::cast_slice(&placements), - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - }); - let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("hatch.instances"), - contents: bytemuck::cast_slice(&instances), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - }); - let family_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("hatch.families"), - contents: bytemuck::cast_slice(&families), - usage: wgpu::BufferUsages::STORAGE, - }); - let dash_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("hatch.dashes"), - contents: bytemuck::cast_slice(&dashes), - usage: wgpu::BufferUsages::STORAGE, - }); + let vertex_buffer = crate::scene::pipeline::gpu_upload::upload_buffer( + device, + queue, + "hatch.vertex", + &verts, + wgpu::BufferUsages::VERTEX, + ); + let index_buffer = crate::scene::pipeline::gpu_upload::upload_buffer( + device, + queue, + "hatch.index", + &indices, + wgpu::BufferUsages::INDEX, + ); + let placement_buffer = crate::scene::pipeline::gpu_upload::upload_buffer( + device, + queue, + "hatch.placements", + &placements, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + let instance_buffer = crate::scene::pipeline::gpu_upload::upload_buffer( + device, + queue, + "hatch.instances", + &instances, + wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + ); + let family_buffer = crate::scene::pipeline::gpu_upload::upload_buffer( + device, + queue, + "hatch.families", + &families, + wgpu::BufferUsages::STORAGE, + ); + let dash_buffer = crate::scene::pipeline::gpu_upload::upload_buffer( + device, + queue, + "hatch.dashes", + &dashes, + wgpu::BufferUsages::STORAGE, + ); - let visibility_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("hatch.visibility"), - contents: bytemuck::cast_slice(&visibility), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - }); + let visibility_buffer = crate::scene::pipeline::gpu_upload::upload_buffer( + device, + queue, + "hatch.visibility", + &visibility, + wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + ); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("hatch.bg"), diff --git a/src/scene/pipeline/hatch_gpu/texture.rs b/src/scene/pipeline/hatch_gpu/texture.rs index c777b3bb1..7ad49c703 100644 --- a/src/scene/pipeline/hatch_gpu/texture.rs +++ b/src/scene/pipeline/hatch_gpu/texture.rs @@ -199,16 +199,20 @@ impl TextureHatch { .into_iter() .map(|[x, y]| HatchVertex { pos: [x, y, 0.0], _pad: 0.0 }) .collect(); - let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("hatch.texture.vbuf"), - contents: bytemuck::cast_slice(&vertices), - usage: wgpu::BufferUsages::VERTEX, - }); - let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("hatch.texture.ibuf"), - contents: bytemuck::cast_slice(&indices), - usage: wgpu::BufferUsages::INDEX, - }); + let vertex_buffer = crate::scene::pipeline::gpu_upload::upload_buffer( + device, + queue, + "hatch.texture.vbuf", + &vertices, + wgpu::BufferUsages::VERTEX, + ); + let index_buffer = crate::scene::pipeline::gpu_upload::upload_buffer( + device, + queue, + "hatch.texture.ibuf", + &indices, + wgpu::BufferUsages::INDEX, + ); let base = model .render_instance .map_or([0.0; 3], |instance| instance.translation); @@ -231,11 +235,13 @@ impl TextureHatch { } }) .collect(); - let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("hatch.texture.placements"), - contents: bytemuck::cast_slice(&placements), - usage: wgpu::BufferUsages::VERTEX, - }); + let placement_buffer = crate::scene::pipeline::gpu_upload::upload_buffer( + device, + queue, + "hatch.texture.placements", + &placements, + wgpu::BufferUsages::VERTEX, + ); // ── Gradient projection range (snapped-local space) ─────────────── let base_mode = mode & 0xFF; diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 595876a13..a27331615 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -1,6 +1,7 @@ mod device_capabilities; pub mod face3d_gpu; pub mod gpu_budget; +pub mod gpu_upload; pub mod hatch_gpu; pub mod wipeout_gpu; pub mod image_gpu; @@ -2341,6 +2342,7 @@ impl Pipeline { pub fn build_wire_buffers( &self, device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[WireModel], depth_map: &rustc_hash::FxHashMap, ) -> ( @@ -2383,6 +2385,7 @@ impl Pipeline { let refs: Vec<&WireModel> = wires[i..j].iter().collect(); batches.extend(WireGpu::from_run_refs( device, + queue, &refs, depth_map, mesh_edge, @@ -2392,6 +2395,7 @@ impl Pipeline { } let block_batches = BlockWireGpu::from_wires( device, + queue, &block_wires, depth_map, None, @@ -2426,6 +2430,7 @@ impl Pipeline { pub fn upload_selected_wires( &mut self, device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[WireModel], selected: &rustc_hash::FxHashSet, hovered: &rustc_hash::FxHashSet, @@ -2495,6 +2500,7 @@ impl Pipeline { let mut gpu = if let Some(tint) = selected_tint { WireGpu::from_highlight_refs( device, + queue, &selected_regular, tint, depth_map, @@ -2505,6 +2511,7 @@ impl Pipeline { }; gpu.extend(WireGpu::from_highlight_refs( device, + queue, &hover_regular, WireModel::HOVER, depth_map, @@ -2514,6 +2521,7 @@ impl Pipeline { let mut block_gpu = if let Some(tint) = selected_tint { BlockWireGpu::from_wires( device, + queue, &selected_blocks, depth_map, Some(tint), @@ -2524,6 +2532,7 @@ impl Pipeline { }; block_gpu.extend(BlockWireGpu::from_wires( device, + queue, &hover_blocks, depth_map, Some(WireModel::HOVER), @@ -2554,6 +2563,7 @@ impl Pipeline { pub fn upload_text_highlight( &mut self, device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[WireModel], selected: &rustc_hash::FxHashSet, hovered: &rustc_hash::FxHashSet, @@ -2634,10 +2644,11 @@ impl Pipeline { } } self.text_highlight_vcount = out.len() as u32; - self.text_highlight_vbuf = text_gpu::upload_vertices(device, &out); + self.text_highlight_vbuf = text_gpu::upload_vertices(device, queue, &out); let mut block_gpu = if let Some(tint) = selected_tint { text_gpu::upload_block_vertex_refs( device, + queue, &selected_blocks, depth_map, Some(tint), @@ -2647,6 +2658,7 @@ impl Pipeline { }; block_gpu.extend(text_gpu::upload_block_vertex_refs( device, + queue, &hover_blocks, depth_map, Some(WireModel::HOVER), @@ -2670,13 +2682,14 @@ impl Pipeline { pub fn upload_preview_wires( &mut self, device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[WireModel], depth_map: &rustc_hash::FxHashMap, ) { self.gpu_preview_wires = if wires.is_empty() { vec![] } else { - WireGpu::from_run(device, wires, depth_map, false, self.wire_const_bgl.as_ref()) + WireGpu::from_run(device, queue, wires, depth_map, false, self.wire_const_bgl.as_ref()) }; } @@ -2709,7 +2722,7 @@ impl Pipeline { atlas.clear_dirty(); } } - self.text_preview_vbuf = text_gpu::upload_vertices(device, verts); + self.text_preview_vbuf = text_gpu::upload_vertices(device, queue, verts); self.text_preview_vcount = verts.len() as u32; } @@ -2720,6 +2733,7 @@ impl Pipeline { pub fn upload_silhouettes( &mut self, device: &wgpu::Device, + queue: &wgpu::Queue, sets: &[crate::scene::model::mesh_model::MeshLodSet], content_id: u64, view_dir: glam::Vec3, @@ -2728,7 +2742,6 @@ impl Pipeline { let view = glam::DVec3::new(view_dir.x as f64, view_dir.y as f64, view_dir.z as f64) .normalize_or(glam::DVec3::NEG_Z); use crate::scene::pipeline::mesh_gpu::{SilhouetteInstance, SilhouetteVertex}; - use wgpu::util::DeviceExt; let chunk_bytes = gpu_budget::buffer_budget(device); let max_vertices = chunk_bytes / std::mem::size_of::() / 2 * 2; if max_vertices < 2 { @@ -2804,12 +2817,12 @@ impl Pipeline { .chunks(max_instances.max(1)) .map(|instances| { ( - device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("mesh.silhouette.instances"), - contents: bytemuck::cast_slice(instances), - usage: wgpu::BufferUsages::VERTEX, - }, + gpu_upload::upload_buffer( + device, + queue, + "mesh.silhouette.instances", + instances, + wgpu::BufferUsages::VERTEX, ), instances.len() as u32, ) @@ -2911,12 +2924,12 @@ impl Pipeline { if vertex_count == 0 { return; } - let vertex_buffer = device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("mesh.silhouette.vbuf"), - contents: bytemuck::cast_slice(&verts[..vertex_count]), - usage: wgpu::BufferUsages::VERTEX, - }, + let vertex_buffer = gpu_upload::upload_buffer( + device, + queue, + "mesh.silhouette.vbuf", + &verts[..vertex_count], + wgpu::BufferUsages::VERTEX, ); for (instance_buffer, instance_count) in &group.instance_buffers { chunks.push(SilhouetteChunk { @@ -3033,6 +3046,7 @@ impl Pipeline { pub fn upload_face3d( &mut self, device: &wgpu::Device, + queue: &wgpu::Queue, face3d_wires: &[WireModel], all_wires: &[WireModel], wireframe_only: bool, @@ -3044,7 +3058,7 @@ impl Pipeline { // Edge buffer is always built from `face3d_wires`, so 3DFACE // outlines stay on the screen regardless of mode. self.gpu_face3d_edges = - WireGpu::from_batch(device, face3d_wires, depth_map, self.wire_const_bgl.as_ref()); + WireGpu::from_batch(device, queue, face3d_wires, depth_map, self.wire_const_bgl.as_ref()); // Fill buffer split: 3D quads + PolyfaceMesh / PolygonMesh face // tris go to `chunks_3d` (gated by `keep_3d_mesh_fills`); // 2D fills (text-LOD greek, MultiLeader background, dimension arrows) go to @@ -3067,6 +3081,7 @@ impl Pipeline { } else { self.gpu_face3d_fill = Some(Face3DGpu::from_wires( device, + queue, face3d_wires, all_wires, keep_3d_mesh_fills, @@ -3404,9 +3419,9 @@ impl Pipeline { atlas.clear_dirty(); } } - self.text_vbuf = text_gpu::upload_vertices(device, verts); + self.text_vbuf = text_gpu::upload_vertices(device, queue, verts); self.text_vcount = verts.len() as u32; - self.block_text_gpu = text_gpu::upload_block_vertices(device, wires, depth_map); + self.block_text_gpu = text_gpu::upload_block_vertices(device, queue, wires, depth_map); if let Some(started) = perf_started { let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0; if elapsed_ms >= 1.0 { diff --git a/src/scene/pipeline/text_gpu.rs b/src/scene/pipeline/text_gpu.rs index 2519fbe0a..7ad16f053 100644 --- a/src/scene/pipeline/text_gpu.rs +++ b/src/scene/pipeline/text_gpu.rs @@ -14,7 +14,6 @@ #![allow(dead_code)] use iced::wgpu; -use iced::wgpu::util::DeviceExt; use crate::scene::text::glyph_quads::GlyphQuad; use crate::scene::text::sdf_atlas::GlyphAtlas; @@ -462,15 +461,17 @@ pub fn create_pipelines( pub fn upload_block_vertices( device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[crate::scene::model::wire_model::WireModel], depth_map: &rustc_hash::FxHashMap, ) -> Vec { let refs: Vec<&crate::scene::model::wire_model::WireModel> = wires.iter().collect(); - upload_block_vertex_refs(device, &refs, depth_map, None) + upload_block_vertex_refs(device, queue, &refs, depth_map, None) } pub fn upload_block_vertex_refs( device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[&crate::scene::model::wire_model::WireModel], depth_map: &rustc_hash::FxHashMap, tint: Option<[f32; 4]>, @@ -516,7 +517,7 @@ pub fn upload_block_vertex_refs( } else { source.text_verts.as_slice() }; - let Some(vertex_buffer) = upload_vertices(device, vertices) else { + let Some(vertex_buffer) = upload_vertices(device, queue, vertices) else { continue; }; let instances: Vec = group @@ -544,11 +545,13 @@ pub fn upload_block_vertex_refs( let max_instances = super::gpu_budget::max_elements::(device); for chunk in instances.chunks(max_instances) { - let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("block_text.instances"), - contents: bytemuck::cast_slice(chunk), - usage: wgpu::BufferUsages::VERTEX, - }); + let instance_buffer = super::gpu_upload::upload_buffer( + device, + queue, + "block_text.instances", + chunk, + wgpu::BufferUsages::VERTEX, + ); out.push(BlockTextGpu { vertex_buffer: vertex_buffer.clone(), instance_buffer, @@ -561,15 +564,21 @@ pub fn upload_block_vertex_refs( } /// Upload a finished vertex list; `None` if empty. -pub fn upload_vertices(device: &wgpu::Device, verts: &[TextVertex]) -> Option { +pub fn upload_vertices( + device: &wgpu::Device, + queue: &wgpu::Queue, + verts: &[TextVertex], +) -> Option { if verts.is_empty() { return None; } - Some(device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("text.vbuf"), - contents: bytemuck::cast_slice(verts), - usage: wgpu::BufferUsages::VERTEX, - })) + Some(super::gpu_upload::upload_buffer( + device, + queue, + "text.vbuf", + verts, + wgpu::BufferUsages::VERTEX, + )) } // ── Tests ───────────────────────────────────────────────────────────────── diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index f7ae562bc..8f15c5fba 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -255,48 +255,34 @@ fn make_const_bg( fn alloc_inst_initialized( device: &wgpu::Device, + queue: &wgpu::Queue, cap: u64, data: &[WireInstance], ) -> wgpu::Buffer { - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("wire_arena.ibuf"), - size: cap * std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: true, - }); - if !data.is_empty() { - let bytes = bytemuck::cast_slice(data); - let mut mapped = buffer - .slice(..bytes.len() as u64) - .get_mapped_range_mut(); - mapped.copy_from_slice(bytes); - drop(mapped); - } - buffer.unmap(); - buffer + super::gpu_upload::alloc_with_prefix( + device, + queue, + "wire_arena.ibuf", + cap * std::mem::size_of::() as u64, + data, + wgpu::BufferUsages::VERTEX, + ) } fn alloc_const_initialized( device: &wgpu::Device, + queue: &wgpu::Queue, cap: u64, data: &[WireConst], ) -> wgpu::Buffer { - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("wire_arena.cbuf"), - size: cap * std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: true, - }); - if !data.is_empty() { - let bytes = bytemuck::cast_slice(data); - let mut mapped = buffer - .slice(..bytes.len() as u64) - .get_mapped_range_mut(); - mapped.copy_from_slice(bytes); - drop(mapped); - } - buffer.unmap(); - buffer + super::gpu_upload::alloc_with_prefix( + device, + queue, + "wire_arena.cbuf", + cap * std::mem::size_of::() as u64, + data, + wgpu::BufferUsages::STORAGE, + ) } fn blank_const() -> WireConst { @@ -328,7 +314,7 @@ impl WireArena { /// path). pub fn build( device: &wgpu::Device, - _queue: &wgpu::Queue, + queue: &wgpu::Queue, wires: &[&WireModel], depth_map: &FxHashMap, const_bgl: &wgpu::BindGroupLayout, @@ -468,8 +454,8 @@ impl WireArena { .max(MIN_CONST_CAP) .min(MAX_CONSTS)) as u32; let upload_started = iced::time::Instant::now(); - let inst_buf = alloc_inst_initialized(device, inst_cap as u64, &instances); - let const_buf = alloc_const_initialized(device, const_cap as u64, &consts_cpu); + let inst_buf = alloc_inst_initialized(device, queue, inst_cap as u64, &instances); + let const_buf = alloc_const_initialized(device, queue, const_cap as u64, &consts_cpu); let upload_ms = upload_started.elapsed().as_secs_f64() * 1000.0; let const_bind_group = make_const_bg(device, const_bgl, &const_buf); if perf { @@ -934,25 +920,18 @@ fn can_resize_packed_terminal_slab( fn alloc_packed_initialized( device: &wgpu::Device, + queue: &wgpu::Queue, cap: u64, data: &[PackedWireInstance], ) -> wgpu::Buffer { - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("wire_arena.packed.ibuf"), - size: cap * std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: true, - }); - if !data.is_empty() { - let bytes = bytemuck::cast_slice(data); - let mut mapped = buffer - .slice(..bytes.len() as u64) - .get_mapped_range_mut(); - mapped.copy_from_slice(bytes); - drop(mapped); - } - buffer.unmap(); - buffer + super::gpu_upload::alloc_with_prefix( + device, + queue, + "wire_arena.packed.ibuf", + cap * std::mem::size_of::() as u64, + data, + wgpu::BufferUsages::VERTEX, + ) } fn visible_ranges( @@ -998,6 +977,7 @@ fn visible_ranges( impl PackedWireArena { fn build( device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[&WireModel], depth_map: &FxHashMap, mesh_edge: bool, @@ -1078,7 +1058,7 @@ impl PackedWireArena { let inst_cap = ((inst_tail as u64 * HEADROOM_NUM / HEADROOM_DEN) .max(MIN_INST_CAP) .min(MAX_PACKED_INSTANCES)) as u32; - let inst_buf = alloc_packed_initialized(device, inst_cap as u64, &instances); + let inst_buf = alloc_packed_initialized(device, queue, inst_cap as u64, &instances); Some(Self { inst_buf, @@ -1351,7 +1331,7 @@ impl PersistentWireArena { )?) } else { PersistentWireArenaKind::Packed(PackedWireArena::build( - device, wires, depth_map, mesh_edge, + device, queue, wires, depth_map, mesh_edge, )?) }; Some(Self { inner }) diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index e5db97efc..a23eb6faf 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -28,34 +28,20 @@ use crate::scene::model::wire_model::WireModel; use iced::wgpu; -/// Allocate a VERTEX buffer with `mapped_at_creation` and write `data` directly -/// into the mapped slice. Skips the intermediate staging copy that -/// `create_buffer_init` performs and avoids holding a second `Vec` worth of -/// memory during upload — meaningful on cold open where wire buffers can run -/// into the hundreds of MB. -fn instance_buffer_mapped( +/// Allocate a VERTEX buffer and fill it through the queue. +/// +/// This used to map at creation to skip the staging copy. Mapping panics on +/// the invalid buffer an out-of-memory device returns, and that panic aborts +/// the process from inside iced's redraw, so the staging copy is the price of +/// a session that survives a failed allocation. Chunking keeps the transient +/// staging cost bounded. See `super::gpu_upload`. +fn instance_buffer( device: &wgpu::Device, + queue: &wgpu::Queue, label: &str, data: &[T], ) -> wgpu::Buffer { - let bytes: &[u8] = bytemuck::cast_slice(data); - // wgpu rejects size-0 buffers; the renderer already guards `instance_count` - // before issuing a draw, so a placeholder allocation is fine here. - let size = bytes.len().max(std::mem::size_of::()) as u64; - let buf = device.create_buffer(&wgpu::BufferDescriptor { - label: Some(label), - size, - usage: wgpu::BufferUsages::VERTEX, - mapped_at_creation: true, - }); - { - let mut view = buf.slice(..).get_mapped_range_mut(); - if !bytes.is_empty() { - view.slice(..bytes.len()).copy_from_slice(bytes); - } - } - buf.unmap(); - buf + super::gpu_upload::upload_buffer(device, queue, label, data, wgpu::BufferUsages::VERTEX) } // ── Instance layout ─────────────────────────────────────────────────────── @@ -684,6 +670,7 @@ pub(crate) fn wire_draw_depth( impl BlockWireGpu { pub fn from_wires( device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[&WireModel], depth_map: &rustc_hash::FxHashMap, color_override: Option<[f32; 4]>, @@ -753,11 +740,13 @@ impl BlockWireGpu { .chunks(max_verts) .map(|chunk| { ( - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("block_wire.vertices"), - contents: bytemuck::cast_slice(chunk), - usage: wgpu::BufferUsages::VERTEX, - }), + super::gpu_upload::upload_buffer( + device, + queue, + "block_wire.vertices", + chunk, + wgpu::BufferUsages::VERTEX, + ), chunk.len() as u32, ) }) @@ -812,7 +801,7 @@ impl BlockWireGpu { .chunks(max_instances) .map(|chunk| { ( - instance_buffer_mapped(device, "block_wire.instances", chunk), + instance_buffer(device, queue, "block_wire.instances", chunk), chunk.len() as u32, ) }) @@ -840,18 +829,20 @@ impl BlockWireGpu { /// each chunk. fn build_const_bind_group( device: &wgpu::Device, + queue: &wgpu::Queue, bgl: &wgpu::BindGroupLayout, consts: &[WireConst], ) -> std::sync::Arc { - use wgpu::util::DeviceExt; // wgpu rejects zero-sized buffers; pad with one zeroed record when empty. let one = [::zeroed()]; let data: &[WireConst] = if consts.is_empty() { &one } else { consts }; - let buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("wire_const.buf"), - contents: bytemuck::cast_slice(data), - usage: wgpu::BufferUsages::STORAGE, - }); + let buf = super::gpu_upload::upload_buffer( + device, + queue, + "wire_const.buf", + data, + wgpu::BufferUsages::STORAGE, + ); let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("wire_const.bg"), layout: bgl, @@ -869,6 +860,7 @@ impl WireGpu { /// of a large selected polyline or block before packing the overlay. pub fn from_highlight_refs( device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[&WireModel], color: [f32; 4], depth_map: &rustc_hash::FxHashMap, @@ -895,12 +887,13 @@ impl WireGpu { if instances.is_empty() { return Vec::new(); } - let bind_group = build_const_bind_group(device, const_bgl, &consts); + let bind_group = build_const_bind_group(device, queue, const_bgl, &consts); return instances .chunks(MAX_INSTANCES) .map(|chunk| Self { - instance_buffer: instance_buffer_mapped( + instance_buffer: instance_buffer( device, + queue, "wire.highlight.ibuf", chunk, ), @@ -928,8 +921,9 @@ impl WireGpu { instances .chunks(MAX_PACKED_INSTANCES) .map(|chunk| Self { - instance_buffer: instance_buffer_mapped( + instance_buffer: instance_buffer( device, + queue, "wire.highlight.compat.ibuf", chunk, ), @@ -947,6 +941,7 @@ impl WireGpu { /// uses chunked resident buffers. pub fn from_run_refs( device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[&WireModel], depth_map: &rustc_hash::FxHashMap, mesh_edge: bool, @@ -975,8 +970,9 @@ impl WireGpu { return instances .chunks(MAX_INSTANCES) .map(|chunk| Self { - instance_buffer: instance_buffer_mapped( + instance_buffer: instance_buffer( device, + queue, "wire.run.hybrid.compat.ibuf", chunk, ), @@ -1012,11 +1008,11 @@ impl WireGpu { if instances.is_empty() { return Vec::new(); } - let bind_group = build_const_bind_group(device, const_bgl, &consts); + let bind_group = build_const_bind_group(device, queue, const_bgl, &consts); instances .chunks(MAX_INSTANCES) .map(|chunk| Self { - instance_buffer: instance_buffer_mapped(device, "wire.run.hybrid.ibuf", chunk), + instance_buffer: instance_buffer(device, queue, "wire.run.hybrid.ibuf", chunk), first_instance: 0, instance_count: chunk.len() as u32, is_3d_mesh_edge: mesh_edge, @@ -1037,6 +1033,7 @@ impl WireGpu { /// image for transparent / coincident wires. pub fn from_run( device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[WireModel], depth_map: &rustc_hash::FxHashMap, mesh_edge: bool, @@ -1071,11 +1068,11 @@ impl WireGpu { if instances.is_empty() { return vec![]; } - let bg = build_const_bind_group(device, const_bgl, &consts); + let bg = build_const_bind_group(device, queue, const_bgl, &consts); return instances .chunks(MAX_INSTANCES) .map(|chunk| { - let buf = instance_buffer_mapped(device, "wire.run.ibuf", chunk); + let buf = instance_buffer(device, queue, "wire.run.ibuf", chunk); Self { instance_buffer: buf, first_instance: 0, @@ -1108,7 +1105,7 @@ impl WireGpu { instances .chunks(MAX_PACKED_INSTANCES) .map(|chunk| { - let buf = instance_buffer_mapped(device, "wire.run.compat.ibuf", chunk); + let buf = instance_buffer(device, queue, "wire.run.compat.ibuf", chunk); Self { instance_buffer: buf, first_instance: 0, @@ -1125,6 +1122,7 @@ impl WireGpu { /// per-instance. pub fn from_batch( device: &wgpu::Device, + queue: &wgpu::Queue, wires: &[WireModel], depth_map: &rustc_hash::FxHashMap, const_bgl: Option<&wgpu::BindGroupLayout>, @@ -1158,13 +1156,13 @@ impl WireGpu { if instances.is_empty() { return vec![]; } - let bg = build_const_bind_group(device, const_bgl, &consts); + let bg = build_const_bind_group(device, queue, const_bgl, &consts); return instances .chunks(MAX_INSTANCES) .enumerate() .map(|(i, chunk)| { let label = format!("wire.batch.ibuf.{i}"); - let instance_buffer = instance_buffer_mapped(device, &label, chunk); + let instance_buffer = instance_buffer(device, queue, &label, chunk); Self { instance_buffer, first_instance: 0, @@ -1191,7 +1189,7 @@ impl WireGpu { .enumerate() .map(|(i, chunk)| { let label = format!("wire.batch.compat.ibuf.{i}"); - let instance_buffer = instance_buffer_mapped(device, &label, chunk); + let instance_buffer = instance_buffer(device, queue, &label, chunk); Self { instance_buffer, first_instance: 0, diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 3a151c5d8..b52238d7c 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -608,6 +608,7 @@ impl shader::Primitive for Primitive { { inner.upload_face3d( device, + queue, &vp.face3d_wires[..], &vp_wires[..], !face3d_fill_active, @@ -785,6 +786,7 @@ impl shader::Primitive for Primitive { inner.wire_arena_fallback = std::sync::Arc::new( crate::scene::pipeline::WireGpu::from_run_refs( device, + queue, ®ular, &draw_depths, false, @@ -821,6 +823,7 @@ impl shader::Primitive for Primitive { inner.wire_arena_fallback = std::sync::Arc::new( crate::scene::pipeline::WireGpu::from_run_refs( device, + queue, &mesh, &draw_depths, true, @@ -918,7 +921,7 @@ impl shader::Primitive for Primitive { Some(entry) => entry, None => { let entry = - inner.build_wire_buffers(device, &vp_wires[..], &draw_depths); + inner.build_wire_buffers(device, queue, &vp_wires[..], &draw_depths); pipeline .wire_buffer_cache .insert(vp.wire_content_id, entry.clone()); @@ -1000,6 +1003,7 @@ impl shader::Primitive for Primitive { }; inner.upload_selected_wires( device, + queue, &vp_wires[..], &vp.selected_handles, &vp.hover_handles, @@ -1012,6 +1016,7 @@ impl shader::Primitive for Primitive { // base text buffer. inner.upload_text_highlight( device, + queue, &vp_wires[..], &vp.selected_handles, &vp.hover_handles, @@ -1066,7 +1071,7 @@ impl shader::Primitive for Primitive { // Live overlay (command preview / interim / grip drag) — small and // refreshed every frame it's present, so a drag never re-uploads // the resident base wire buffer. - inner.upload_preview_wires(device, &vp.preview_wires[..], &draw_depths); + inner.upload_preview_wires(device, queue, &vp.preview_wires[..], &draw_depths); inner.upload_preview_text(device, queue, &vp.preview_text_verts[..]); // Cull / scissor / LOD project AABBs relative-to-eye (matching the // GPU's RTE path) so the math stays precise at UTM-scale coords. @@ -1087,6 +1092,7 @@ impl shader::Primitive for Primitive { if inner.silhouette_key != silhouette_key { inner.upload_silhouettes( device, + queue, if silhouette_enabled { &vp.meshes[..] } else { &[] }, vp.wire_content_id, vp.view_dir,