perf(scene): draw preview/grip overlay from a separate GPU buffer

Command-preview / interim / grip-drag wires were appended into the main
wire buffer (all_wires), so every drag frame re-packed and re-uploaded the
entire batched base set — O(N) per move even though only the small overlay
changed.

Split them out: build_primitive keeps `wires` as the cached base only and
passes the overlay as a separate `preview_wires` Arc. The base keeps the
stable tile content id (no per-frame nonce), so its buffer stays resident
and is not re-uploaded during a drag. The overlay uploads to its own small
gpu_preview_wires buffer each frame and draws on top in the wire pass
(same pipeline, same order/depth). The dragged entity still renders
highlighted via its selected-path tessellation.

With the no-re-tess changes, a grip / command drag now costs a single
small overlay upload per move and nothing else.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-05 10:57:38 +03:00
1 parent 55022ae60e
commit e37dfa5890
3 files changed
+61 -11

No files matched your search

+8
View File
@@ -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
+29
View File
@@ -81,6 +81,10 @@ pub struct Pipeline {
wire_pixel_scissors: Vec<Option<[u32; 4]>>,
/// Ghost copies (25% alpha) of selected wires for the X-ray depth pass.
gpu_selected_wires: Vec<WireGpu>,
/// 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<WireGpu>,
/// Phase 4-B — single batched-hatch GPU resource. Drawn in one
/// indexed call with per-instance visibility masking the rest.
/// Legacy per-hatch `Vec<HatchGpu>` + 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<u64, f32>,
) {
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) ────────
+24 -11
View File
@@ -32,6 +32,10 @@ pub struct CameraState {
#[derive(Debug)]
pub struct ViewportData {
pub(super) wires: Arc<Vec<WireModel>>,
/// 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<Vec<WireModel>>,
/// 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<Vec<WireModel>>,
@@ -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<WireModel> = 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,