diff --git a/ROADMAP.md b/ROADMAP.md index cb43e5957..e638ac6c6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -266,6 +266,14 @@ the batched hatch pipeline (`hatch_batched_gpu.rs`) to wires: At 100 k wires that collapses thousands of draw calls into one. If iced 0.14's widget-pipeline limits allow, immediate win. +**Follow-up (landed): separate overlay buffer.** Command-preview / interim / +grip-drag wires were appended into the main wire buffer, so every drag frame +re-uploaded the entire (batched) base set. They now ride in their own small +per-frame `gpu_preview_wires` buffer drawn on top in the wire pass, so the +resident base buffer stays untouched during a drag — only the tiny overlay +re-uploads. Combined with the no-re-tess work, a grip/command drag now costs +one small overlay upload per move, nothing else. + ### 3.4 Hardware instancing for repeated block inserts When the same block defn is `INSERT`-ed N times (every door / window in diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index df7e0a5d3..cc7da472c 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -81,6 +81,10 @@ pub struct Pipeline { wire_pixel_scissors: Vec>, /// Ghost copies (25% alpha) of selected wires for the X-ray depth pass. gpu_selected_wires: Vec, + /// Command-preview / interim / grip-drag overlay wires. Re-uploaded every + /// frame they are present (small), drawn on top of the base wire pass — so + /// a live drag never re-uploads the resident base buffer. + gpu_preview_wires: Vec, /// Phase 4-B — single batched-hatch GPU resource. Drawn in one /// indexed call with per-instance visibility masking the rest. /// Legacy per-hatch `Vec` + scissor / skip-flag plumbing @@ -882,6 +886,7 @@ impl Pipeline { gpu_wires: vec![], wire_pixel_scissors: vec![], gpu_selected_wires: vec![], + gpu_preview_wires: vec![], gpu_hatch_batched: None, gpu_wipeouts: vec![], wipeout_skip_flags: vec![], @@ -974,6 +979,22 @@ impl Pipeline { self.gpu_selected_wires = WireGpu::from_run(device, &selected, depth_map, None, false); } + /// Upload the live overlay (command preview / interim / grip-drag) wires. + /// Small and refreshed each frame they're present; kept separate from the + /// base wire buffer so a drag never re-uploads the resident base set. + pub fn upload_preview_wires( + &mut self, + device: &wgpu::Device, + wires: &[WireModel], + depth_map: &rustc_hash::FxHashMap, + ) { + self.gpu_preview_wires = if wires.is_empty() { + vec![] + } else { + WireGpu::from_run(device, wires, depth_map, None, false) + }; + } + /// Recompute pixel scissor rects for viewport-clipped wires from the current view_proj. /// Called every frame from prepare() because scissor pixels shift with pan/zoom. pub fn compute_wire_scissors(&mut self, view_proj: glam::Mat4, clip_w: u32, clip_h: u32) { @@ -1559,6 +1580,14 @@ impl Pipeline { if scissor_active { pass.set_scissor_rect(0, 0, vp.width, vp.height); } + // Live overlay wires (command preview / interim / grip drag) on top + // of the base pass, same pipeline + depth test. No scissor. + for pw in &self.gpu_preview_wires { + if pw.instance_count > 0 { + pass.set_vertex_buffer(0, pw.instance_buffer.slice(..)); + pass.draw(0..6, 0..pw.instance_count); + } + } } // ── Pass 6: wipeout fills (drawn after wires to mask them) ──────── diff --git a/src/scene/render.rs b/src/scene/render.rs index 4da230fa8..9d640f288 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -32,6 +32,10 @@ pub struct CameraState { #[derive(Debug)] pub struct ViewportData { pub(super) wires: Arc>, + /// Live command-preview / interim / grip-drag overlay wires. Kept out of + /// the main `wires` buffer so a drag re-uploads only this small set each + /// frame, never the resident base buffer. Drawn on top in the wire pass. + pub(super) preview_wires: Arc>, /// 3DFACE entity wires — separated so they are uploaded to the dedicated /// face3d pipeline (fill + batched edges) instead of N individual WireGpu. pub(super) face3d_wires: Arc>, @@ -272,6 +276,10 @@ impl shader::Primitive for Primitive { ); inner.cached_selection = sel_key; } + // 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[..], &vp.draw_depths); let vproj = vp.uniforms.view_proj; inner.compute_wire_scissors(vproj, clip_size.width, clip_size.height); inner.compute_wipeout_scissors(vproj, clip_size.width, clip_size.height); @@ -642,13 +650,14 @@ impl Scene { } else { (self.model_wires_for_viewport_arc(inst.handle, full.height), None) }; - // Wire-buffer content id for the upload gate. Live preview / interim - // wires are appended below and vary frame-to-frame, so any frame - // carrying them forces a fresh upload, as do the non-tile paths. - let has_overlay = self.interim_wire.is_some() || !self.preview_wires.is_empty(); + // Wire-buffer content id for the upload gate. Preview / interim wires + // are NOT part of this buffer anymore (they go in a separate per-frame + // overlay buffer below), so the base id is the stable tile content gen + // — a drag no longer re-uploads the whole base wire set every move. + // Non-tile paths have no stable id and force a re-upload. let wire_content_id = match tile_wire_gen { - Some(g) if !has_overlay => g, - _ => { + Some(g) => g, + None => { let n = self.wire_force_nonce.get().wrapping_add(1); self.wire_force_nonce.set(n); n | (1u64 << 63) @@ -676,12 +685,15 @@ impl Scene { (Arc::new(f), Arc::new(o)) } }; - // Reuse the cached `other` Arc directly when no live preview/interim - // wires need appending; otherwise clone once to extend. - let all_wires = if !has_overlay { - other_arc + // Base wire set — the cached `other` Arc directly, never cloned to + // append overlays. Preview / interim wires ride in their own small + // per-frame buffer so the (potentially huge) base buffer stays resident + // and unchanged while a command preview or grip drag is live. + let all_wires = other_arc; + let preview_wires = if self.interim_wire.is_none() && self.preview_wires.is_empty() { + Arc::new(Vec::new()) } else { - let mut v = (*other_arc).clone(); + let mut v: Vec = Vec::with_capacity(self.preview_wires.len() + 1); if let Some(iw) = &self.interim_wire { v.push(iw.clone()); } @@ -749,6 +761,7 @@ impl Scene { Some(ViewportData { wires: all_wires, + preview_wires, face3d_wires, draw_depths: self.draw_depth_map(), hatches,