forked from 1b-agent/opencadstudio-upstream
perf(scene): batch the wire pass into per-run draw calls
Phase 3.3. upload_wires built one GPU instance buffer + one draw call per WireModel — tens of thousands on a large drawing, a major per-frame CPU / driver cost. Merge maximal runs of consecutive wires that share scissor + mesh-edge state into one concatenated instance buffer each (WireGpu:: from_run); the existing draw loop then issues a single draw per run. A 2D model (uniform None scissor, no mesh edges) collapses to one buffer + one draw. Runs are consecutive, never globally regrouped, so the sorted wire order is preserved exactly — depth-biased overlap and alpha blending both resolve in submission order, so the image is unchanged. from_run guarantees instance order (index-ordered collect + sequential flatten). The WireInstance layout, shader, scissor projection and draw loop are untouched; only buffer packing changed. Selection overlay stays per-wire. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
65eba3887a
commit
c6e17deb7e
3 files changed
+83
-6
No files matched your search
+12
-1
@@ -212,7 +212,18 @@ Today every wire GPU buffer is re-uploaded when
|
||||
pool — `HashMap<Handle, GpuSlot>` — uploads only the slots that actually
|
||||
changed. Big win in CAD-edit scenarios.
|
||||
|
||||
### 3.3 Single-draw batched wire pipeline (Phase 4-B-style)
|
||||
### 3.3 Single-draw batched wire pipeline (Phase 4-B-style) ✅ DONE
|
||||
|
||||
`upload_wires` made one GPU buffer + one draw call per `WireModel` (tens of
|
||||
thousands on a large drawing). Now it merges maximal runs of *consecutive*
|
||||
wires sharing scissor + mesh-edge state into one concatenated instance buffer
|
||||
each (`WireGpu::from_run`), so the existing draw loop issues one draw per run
|
||||
— a 2D model collapses to a single buffer + single draw. Runs stay
|
||||
*consecutive* (not globally regrouped) so the sorted draw order is preserved
|
||||
bit-for-bit; depth bias and alpha blending are unchanged. The `WireInstance`
|
||||
layout, shader, scissor logic and draw loop are untouched — only the buffer
|
||||
packing changed. (No iced widget-pipeline limits were hit: the change lives
|
||||
entirely inside the existing custom wgpu pipeline.)
|
||||
|
||||
Every `WireModel` today costs one draw call plus a bind-group swap. Port
|
||||
the batched hatch pipeline (`hatch_batched_gpu.rs`) to wires:
|
||||
|
||||
@@ -907,13 +907,35 @@ impl Pipeline {
|
||||
.and_then(|h| depth_map.get(&h).copied())
|
||||
.unwrap_or(0.0)
|
||||
};
|
||||
self.gpu_wires = wires
|
||||
.iter()
|
||||
.map(|w| WireGpu::new(device, w, depth_of(w)))
|
||||
.collect();
|
||||
// Batch the wire pass: instead of one GPU buffer + one draw call per
|
||||
// WireModel (tens of thousands on a large drawing), merge maximal runs
|
||||
// of *consecutive* wires that share scissor + mesh-edge state into one
|
||||
// concatenated instance buffer each. The draw loop then issues one draw
|
||||
// per run. Runs must be consecutive (not regrouped) so the original
|
||||
// wire order — already sorted by draw order — is preserved; depth bias
|
||||
// and alpha blending both depend on it. Scissor and mesh-edge stay
|
||||
// grouping keys because the draw loop sets one scissor per batch and
|
||||
// skips whole mesh-edge batches in shaded modes.
|
||||
let mut batches: Vec<WireGpu> = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < wires.len() {
|
||||
let scissor = wires[i].vp_scissor;
|
||||
let mesh_edge = !wires[i].fill_tris.is_empty();
|
||||
let mut j = i + 1;
|
||||
while j < wires.len()
|
||||
&& wires[j].vp_scissor == scissor
|
||||
&& (!wires[j].fill_tris.is_empty()) == mesh_edge
|
||||
{
|
||||
j += 1;
|
||||
}
|
||||
batches.extend(WireGpu::from_run(device, &wires[i..j], depth_map, scissor, mesh_edge));
|
||||
i = j;
|
||||
}
|
||||
self.gpu_wires = batches;
|
||||
|
||||
// Full-brightness copies of selected wires, drawn on top of everything
|
||||
// in the selection overlay pass so they're always visible.
|
||||
// in the selection overlay pass so they're always visible. Kept
|
||||
// per-wire — the selection set is small and this overlay is separate.
|
||||
self.gpu_selected_wires = wires
|
||||
.iter()
|
||||
.filter(|w| w.selected)
|
||||
|
||||
@@ -284,6 +284,50 @@ impl WireGpu {
|
||||
g
|
||||
}
|
||||
|
||||
/// Merge a run of WireModels that share scissor + mesh-edge state into one
|
||||
/// (or, past the 256 MB GPU limit, a few) instance buffer(s), then stamp
|
||||
/// the shared `scissor` / `mesh_edge` onto each so the draw loop treats the
|
||||
/// whole run as a single batch.
|
||||
///
|
||||
/// Unlike [`from_batch`], instance order is **guaranteed** to follow wire
|
||||
/// order (parallel `collect` is index-ordered; the flatten is sequential).
|
||||
/// The main wire pass depends on that — depth-biased overlap *and* alpha
|
||||
/// blending both resolve in submission order, so a reorder would change the
|
||||
/// image for transparent / coincident wires.
|
||||
pub fn from_run(
|
||||
device: &wgpu::Device,
|
||||
wires: &[WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, f32>,
|
||||
scissor: Option<[f32; 4]>,
|
||||
mesh_edge: bool,
|
||||
) -> Vec<Self> {
|
||||
use rayon::prelude::*;
|
||||
const MAX_INSTANCES: usize = 268_435_456 / std::mem::size_of::<WireInstance>();
|
||||
let per: Vec<Vec<WireInstance>> = wires
|
||||
.par_iter()
|
||||
.map(|w| emit_wire_instances(w, w.color, wire_draw_depth(w, depth_map)))
|
||||
.collect();
|
||||
let mut instances: Vec<WireInstance> = Vec::with_capacity(per.iter().map(Vec::len).sum());
|
||||
for mut v in per {
|
||||
instances.append(&mut v);
|
||||
}
|
||||
if instances.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.map(|chunk| {
|
||||
let buf = instance_buffer_mapped(device, "wire.run.ibuf", chunk);
|
||||
Self {
|
||||
instance_buffer: buf,
|
||||
instance_count: chunk.len() as u32,
|
||||
vp_scissor: scissor,
|
||||
is_3d_mesh_edge: mesh_edge,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Merge multiple WireModels into GPU instance buffers, chunked to fit the
|
||||
/// 256 MB GPU limit. Each wire keeps its own color and pattern — they live
|
||||
/// per-instance.
|
||||
|
||||
Reference in new issue
Block a user