diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index d70b53f6a..164e75b48 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -136,7 +136,14 @@ pub struct Pipeline { blit_uniform_buffer: wgpu::Buffer, /// Cached texture format (needed to recreate MSAA / depth textures on resize). surface_format: wgpu::TextureFormat, - gpu_wires: Vec, + /// The resident wire batches, shared by `std::sync::Arc` with every other + /// pipeline slot rendering the *same* `wire_content_id` (see + /// `MultiPipeline::wire_buffer_cache`). A `wgpu::Buffer` is internally + /// ref-counted, so N paper viewports (or Model tiles) drawing one identical + /// resident set hold one copy of the GPU vertex buffers between them — + /// their camera uniforms + scissor stay per-slot, only the geometry is + /// deduplicated. Never mutated in place, only reassigned on content change. + pub(crate) gpu_wires: std::sync::Arc>, /// Pixel scissor rects [x, y, w, h] for viewport-clipped wires. Recomputed each frame. wire_pixel_scissors: Vec>, /// Ghost copies (25% alpha) of selected wires for the X-ray depth pass. @@ -225,8 +232,10 @@ pub struct Pipeline { /// Handle → indices into the resident wire set, built once per wire upload /// (when `cached_wire_id` changes). Lets the selection/hover xray overlay /// gather just the highlighted entity's wires (`O(highlighted)`) instead of - /// scanning and string-parsing every wire on each hover change. - wire_handle_index: rustc_hash::FxHashMap>, + /// scanning and string-parsing every wire on each hover change. Shared by + /// `std::sync::Arc` alongside `gpu_wires` — built once per `wire_content_id` + /// and cloned into every slot that renders it. + pub(crate) wire_handle_index: std::sync::Arc>>, /// Signature of the last frame's rendered scene (camera uniforms, geometry /// / selection generations, wire content id, render-mode flags, clip size, /// live preview). When the next frame's signature matches, the image is @@ -1294,7 +1303,7 @@ impl Pipeline { blit_bind_group, blit_uniform_buffer, surface_format: format, - gpu_wires: vec![], + gpu_wires: std::sync::Arc::new(vec![]), wire_pixel_scissors: vec![], gpu_selected_wires: vec![], gpu_preview_wires: vec![], @@ -1321,7 +1330,7 @@ impl Pipeline { cached_selection: (u64::MAX, u64::MAX), cached_mesh_key: (u64::MAX, u64::MAX), cached_face3d_key: (u64::MAX, false), - wire_handle_index: rustc_hash::FxHashMap::default(), + wire_handle_index: std::sync::Arc::new(rustc_hash::FxHashMap::default()), render_sig: u64::MAX, skip_geometry: false, skip_hatch_frame: false, @@ -1329,11 +1338,19 @@ impl Pipeline { } } - pub fn upload_wires( - &mut self, + /// Build the resident wire batches + handle index for `wires`, wrapped in + /// `Arc` so the caller can cache them by `wire_content_id` and share one + /// copy across every slot that renders that content (see + /// `MultiPipeline::wire_buffer_cache`). Takes `&self` (reads only the const + /// bind-group layout) so the shared cache — not the slot — owns the result. + pub fn build_wire_buffers( + &self, device: &wgpu::Device, wires: &[WireModel], depth_map: &rustc_hash::FxHashMap, + ) -> ( + std::sync::Arc>, + std::sync::Arc>>, ) { // Batch the wire pass: instead of one GPU buffer + one draw call per // WireModel (tens of thousands on a large drawing), merge maximal runs @@ -1372,18 +1389,18 @@ impl Pipeline { batches.extend(WireGpu::from_run(device, &wires[i..j], depth_map, scissor, mesh_edge, self.wire_const_bgl.as_ref())); i = j; } - self.gpu_wires = batches; // Index handle → wire slots once, here, so the per-hover selection // overlay can gather just the highlighted wires instead of scanning + // string-parsing the whole set every time the hovered entity changes. - self.wire_handle_index.clear(); - self.wire_handle_index.reserve(wires.len()); + let mut index: rustc_hash::FxHashMap> = rustc_hash::FxHashMap::default(); + index.reserve(wires.len()); for (idx, w) in wires.iter().enumerate() { if let Ok(h) = w.name.parse::() { - self.wire_handle_index.entry(h).or_default().push(idx as u32); + index.entry(h).or_default().push(idx as u32); } } + (std::sync::Arc::new(batches), std::sync::Arc::new(index)) } /// Build the selection xray overlay: full-brightness copies of the wires @@ -2820,6 +2837,21 @@ fn create_msaa_texture( pub struct MultiPipeline { pub(crate) inners: Vec, format: wgpu::TextureFormat, + /// The resident wire batches, keyed by `wire_content_id` and shared across + /// every slot (and every pane — one `MultiPipeline` backs all of them) that + /// renders the same content. `prepare` builds an entry once on a cache miss + /// then hands `Arc` clones to each slot, so N paper viewports / Model tiles + /// showing one identical resident set upload the wire vertex buffers exactly + /// once between them instead of once per slot. Kept trim by dropping entries + /// no slot still references (`Arc::strong_count == 1`) once it grows past a + /// small bound. + pub(crate) wire_buffer_cache: rustc_hash::FxHashMap< + u64, + ( + std::sync::Arc>, + std::sync::Arc>>, + ), + >, } impl MultiPipeline { @@ -2846,6 +2878,7 @@ impl iced::widget::shader::Pipeline for MultiPipeline { Self { inners: vec![Pipeline::new(device, queue, format)], format, + wire_buffer_cache: rustc_hash::FxHashMap::default(), } } } diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index b3cbc47f6..78e449bbf 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -357,7 +357,35 @@ impl shader::Primitive for Primitive { // independent of the `cur_key` block so a preview/interim wire change // still uploads even when the camera didn't move. if vp.wire_content_id != inner.cached_wire_id { - inner.upload_wires(device, &vp.wires[..], &vp.draw_depths); + // Share one copy of the resident wire buffers across every slot + // (and every pane — one MultiPipeline backs them all) rendering + // this content id: build on a cache miss, then hand out Arc + // clones. Two paper viewports showing the same model, or four + // Model tiles, upload the wire vertices once between them. + // `.cloned()` releases the immutable cache borrow before the + // miss branch takes a mutable one. + let cached = pipeline.wire_buffer_cache.get(&vp.wire_content_id).cloned(); + let built = match cached { + Some(entry) => entry, + None => { + let entry = + inner.build_wire_buffers(device, &vp.wires[..], &vp.draw_depths); + pipeline + .wire_buffer_cache + .insert(vp.wire_content_id, entry.clone()); + // Evict entries no slot still holds (only the cache + // references them). An entry drawn by any pane keeps a + // strong count ≥ 2, so this never drops live geometry. + if pipeline.wire_buffer_cache.len() > 16 { + pipeline + .wire_buffer_cache + .retain(|_, (w, _)| std::sync::Arc::strong_count(w) > 1); + } + entry + } + }; + inner.gpu_wires = built.0; + inner.wire_handle_index = built.1; inner.cached_wire_id = vp.wire_content_id; } // Selection xray overlay — rebuilt when the selection changes or the