forked from 1b-agent/opencadstudio-upstream
perf(render): split the wire arena into regular + mesh-edge batches
The single-batch arena required a set with no mesh/solid edges, so any drawing with a 3D solid or mesh (this one: 1693 mesh-edge wires of 12200) disqualified the WHOLE set — every edit fell back to build_wire_buffers re-uploading ~265 MB (4.4M instances), stalling ~1s per edit on a shared-memory GPU. Split the resident set into a regular-2D arena and a mesh-edge arena (the wires that need the draw loops mesh-edge skip/black treatment), one WireArena each, passed as &[&WireModel] subsets (no clone). Both patch incrementally; the mesh arena carries is_3d_mesh_edge so the existing draw loop handles it. A changed handle absent from an arenas subset is a no-op for it, so each edit touches only the batch it belongs to. Result: the wire block drops from ~300ms + a 265 MB GPU upload to ~2ms. Two correctness points folded in from adversarial review: - mesh-edge wires are stamped draw_depth 0 (matching WireGpu::from_run) so their hidden back edges dont peek through the shaded fill; the mesh arena also always bails on append (all its wires share depth 0, so relocation would reorder coincident edges). - buffers are capped at wgpus 256 MB limit — a batch larger than one buffer returns None so the caller chunks it via the batched path (fixes a create_buffer panic on the 355 MB regular batch). Still behind OCS_WIRE_GPU_PATCH (default off). OCS_PERF prints the per-edit wire timing + outcome. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
9761423b84
commit
95ef3b3890
3 files changed
+157
-55
No files matched your search
@@ -154,7 +154,12 @@ pub struct Pipeline {
|
||||
/// edit patches one entity's slab in place instead of rebuilding every wire.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) wire_arena: Option<wire_arena::WireArena>,
|
||||
/// The Model content id `wire_arena` currently mirrors (`u64::MAX` = none).
|
||||
/// Second arena for the mesh/solid EDGE wires (drawn with the mesh-edge skip /
|
||||
/// black treatment); the resident set is split into this + `wire_arena` so
|
||||
/// both patch incrementally. Shares `wire_arena_id`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) wire_arena_mesh: Option<wire_arena::WireArena>,
|
||||
/// The Model content id both arenas currently mirror (`u64::MAX` = none).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) wire_arena_id: u64,
|
||||
/// Pixel scissor rects [x, y, w, h] for viewport-clipped wires. Recomputed each frame.
|
||||
@@ -1320,6 +1325,8 @@ impl Pipeline {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_arena: None,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_arena_mesh: None,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_arena_id: u64::MAX,
|
||||
wire_pixel_scissors: vec![],
|
||||
gpu_selected_wires: vec![],
|
||||
|
||||
@@ -50,6 +50,12 @@ const HEADROOM_NUM: u64 = 3;
|
||||
const HEADROOM_DEN: u64 = 2;
|
||||
const MIN_INST_CAP: u64 = 4096;
|
||||
const MIN_CONST_CAP: u64 = 1024;
|
||||
/// wgpu caps a single buffer at 256 MB. An arena keeps ONE instance buffer per
|
||||
/// batch, so a batch whose instances exceed this can't be an arena — build
|
||||
/// returns None and the caller falls back to the chunked batched path. The
|
||||
/// buffer (with headroom) is also clamped here so it never exceeds the limit.
|
||||
const MAX_INSTANCES: u64 = 268_435_456 / std::mem::size_of::<WireInstance>() as u64;
|
||||
const MAX_CONSTS: u64 = 268_435_456 / std::mem::size_of::<WireConst>() as u64;
|
||||
|
||||
struct Slab {
|
||||
inst_off: u32,
|
||||
@@ -73,18 +79,29 @@ pub struct WireArena {
|
||||
/// Tombstoned instances (blanked, not reclaimed) — past half the tail a patch
|
||||
/// bails so the caller compacts with a full rebuild.
|
||||
tombstoned: u32,
|
||||
/// Whether this arena's wires are 3D mesh/solid edges (`is_3d_mesh_edge` on
|
||||
/// the draw batch): the draw loop hides them in clean-shaded modes and draws
|
||||
/// them black in filled-with-edges modes. The regular and mesh-edge subsets
|
||||
/// of the resident set each get their own arena so both patch incrementally.
|
||||
mesh_edge: bool,
|
||||
}
|
||||
|
||||
fn handle_of(w: &WireModel) -> Option<Handle> {
|
||||
crate::scene::Scene::handle_from_wire_name(&w.name)
|
||||
}
|
||||
|
||||
/// True if the whole set is arena-eligible: a single batch that draws with no
|
||||
/// viewport scissor — no mesh/solid fill and no per-wire scissor.
|
||||
/// True if the set can use the arena: no per-wire scissor (paper viewports). Mesh
|
||||
/// fills no longer disqualify it — the render layer splits the set into a regular
|
||||
/// and a mesh-edge subset, one arena each.
|
||||
pub fn is_arena_eligible(wires: &[WireModel]) -> bool {
|
||||
wires
|
||||
.iter()
|
||||
.all(|w| w.fill_tris.is_empty() && w.vp_scissor.is_none())
|
||||
wires.iter().all(|w| w.vp_scissor.is_none())
|
||||
}
|
||||
|
||||
/// True when `w`'s edge segments belong to a mesh/solid whose fill is drawn in a
|
||||
/// separate pass — such wires need the draw loop's mesh-edge treatment, so they
|
||||
/// go in their own arena. `mesh_names` are the entities that emit a fill.
|
||||
pub fn is_mesh_edge(w: &WireModel, mesh_names: &rustc_hash::FxHashSet<u64>) -> bool {
|
||||
!w.points.is_empty() && handle_of(w).map_or(false, |h| mesh_names.contains(&h.value()))
|
||||
}
|
||||
|
||||
/// True when appending a new entity at the tail could change the image, so the
|
||||
@@ -95,7 +112,7 @@ pub fn is_arena_eligible(wires: &[WireModel]) -> bool {
|
||||
/// Surface) are excluded from `draw_depth_map`, so their fallback edge wires
|
||||
/// get draw_depth 0.0. Two coincident opaque such wires share a z-bias and
|
||||
/// resolve by submission order, which a tail relocation would flip.
|
||||
fn append_unsafe(wires: &[WireModel], depth_map: &FxHashMap<u64, f32>) -> bool {
|
||||
fn append_unsafe(wires: &[&WireModel], depth_map: &FxHashMap<u64, f32>) -> bool {
|
||||
wires.iter().any(|w| {
|
||||
w.color[3] < 0.999
|
||||
|| handle_of(w).map_or(true, |h| !depth_map.contains_key(&h.value()))
|
||||
@@ -116,13 +133,13 @@ pub fn build_handle_index(wires: &[WireModel]) -> std::sync::Arc<FxHashMap<u64,
|
||||
}
|
||||
|
||||
/// Group `wires` (draw-order sorted, entity-contiguous) into per-handle ranges.
|
||||
fn handle_ranges(wires: &[WireModel]) -> Option<Vec<(Handle, usize, usize)>> {
|
||||
fn handle_ranges(wires: &[&WireModel]) -> Option<Vec<(Handle, usize, usize)>> {
|
||||
let mut out: Vec<(Handle, usize, usize)> = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < wires.len() {
|
||||
let h = handle_of(&wires[i])?;
|
||||
let h = handle_of(wires[i])?;
|
||||
let mut j = i + 1;
|
||||
while j < wires.len() && handle_of(&wires[j]) == Some(h) {
|
||||
while j < wires.len() && handle_of(wires[j]) == Some(h) {
|
||||
j += 1;
|
||||
}
|
||||
out.push((h, i, j));
|
||||
@@ -189,13 +206,11 @@ impl WireArena {
|
||||
pub fn build(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[WireModel],
|
||||
wires: &[&WireModel],
|
||||
depth_map: &FxHashMap<u64, f32>,
|
||||
const_bgl: &wgpu::BindGroupLayout,
|
||||
mesh_edge: bool,
|
||||
) -> Option<Self> {
|
||||
if !is_arena_eligible(wires) {
|
||||
return None;
|
||||
}
|
||||
let ranges = handle_ranges(wires)?;
|
||||
|
||||
// const slot 0 = blank tombstone target.
|
||||
@@ -205,9 +220,12 @@ impl WireArena {
|
||||
for (h, i, j) in ranges {
|
||||
let inst_off = instances.len() as u32;
|
||||
let const_off = consts_cpu.len() as u32;
|
||||
for w in &wires[i..j] {
|
||||
for &w in &wires[i..j] {
|
||||
let wire_id = consts_cpu.len() as u32;
|
||||
let dd = wire_draw_depth(w, depth_map);
|
||||
// 3D mesh outline edges are occluded by true depth and must NOT
|
||||
// take the draw-order z-bias (or hidden back edges peek through
|
||||
// the shaded fill) — matching WireGpu::from_run.
|
||||
let dd = if mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) };
|
||||
let (mut insts, cst) = emit_wire_native(w, wire_id, w.color, dd);
|
||||
instances.append(&mut insts);
|
||||
consts_cpu.push(cst);
|
||||
@@ -225,9 +243,17 @@ impl WireArena {
|
||||
|
||||
let inst_tail = instances.len() as u32;
|
||||
let const_tail = consts_cpu.len() as u32;
|
||||
let inst_cap = ((inst_tail as u64 * HEADROOM_NUM / HEADROOM_DEN).max(MIN_INST_CAP)) as u32;
|
||||
let const_cap =
|
||||
((const_tail as u64 * HEADROOM_NUM / HEADROOM_DEN).max(MIN_CONST_CAP)) as u32;
|
||||
// A batch bigger than one buffer can't be an arena — let the caller chunk
|
||||
// it via the batched path.
|
||||
if inst_tail as u64 > MAX_INSTANCES || const_tail as u64 > MAX_CONSTS {
|
||||
return None;
|
||||
}
|
||||
let inst_cap = ((inst_tail as u64 * HEADROOM_NUM / HEADROOM_DEN)
|
||||
.max(MIN_INST_CAP)
|
||||
.min(MAX_INSTANCES)) as u32;
|
||||
let const_cap = ((const_tail as u64 * HEADROOM_NUM / HEADROOM_DEN)
|
||||
.max(MIN_CONST_CAP)
|
||||
.min(MAX_CONSTS)) as u32;
|
||||
let inst_buf = alloc_inst(device, inst_cap as u64);
|
||||
let const_buf = alloc_const(device, const_cap as u64);
|
||||
if inst_tail > 0 {
|
||||
@@ -247,6 +273,7 @@ impl WireArena {
|
||||
consts_cpu,
|
||||
slabs,
|
||||
tombstoned: 0,
|
||||
mesh_edge,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -265,12 +292,9 @@ impl WireArena {
|
||||
&mut self,
|
||||
queue: &wgpu::Queue,
|
||||
changes: &[(Handle, ChangeKind)],
|
||||
wires: &[WireModel],
|
||||
wires: &[&WireModel],
|
||||
depth_map: &FxHashMap<u64, f32>,
|
||||
) -> bool {
|
||||
if !is_arena_eligible(wires) {
|
||||
return false;
|
||||
}
|
||||
let Some(ranges) = handle_ranges(wires) else {
|
||||
return false;
|
||||
};
|
||||
@@ -282,7 +306,9 @@ impl WireArena {
|
||||
for &(h, kind) in changes {
|
||||
let new_range = range_of.get(&h).copied();
|
||||
|
||||
// Removed / now-hidden ⇒ tombstone the slab.
|
||||
// Removed / now-hidden ⇒ tombstone the slab. A handle not in THIS
|
||||
// arena's subset (it belongs to the other batch) simply isn't in its
|
||||
// slabs, so this is a no-op for it.
|
||||
if matches!(kind, ChangeKind::Removed) || new_range.is_none() {
|
||||
if let Some(slab) = self.slabs.remove(&h) {
|
||||
let blanks = vec![blank_instance(); slab.inst_len as usize];
|
||||
@@ -298,9 +324,9 @@ impl WireArena {
|
||||
// Emit into fresh, run-local const slots (patched to absolute below).
|
||||
let mut insts: Vec<WireInstance> = Vec::new();
|
||||
let mut csts: Vec<WireConst> = Vec::new();
|
||||
for w in run {
|
||||
for &w in run {
|
||||
let wire_id = csts.len() as u32;
|
||||
let dd = wire_draw_depth(w, depth_map);
|
||||
let dd = if self.mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) };
|
||||
let (mut wi, c) = emit_wire_native(w, wire_id, w.color, dd);
|
||||
insts.append(&mut wi);
|
||||
csts.push(c);
|
||||
@@ -339,9 +365,11 @@ impl WireArena {
|
||||
}
|
||||
|
||||
// Layout changed ⇒ append at the tail. Unsafe to relocate when the set
|
||||
// resolves overlap by submission order (transparency, or a wire with
|
||||
// no draw-order depth); fall back to a full rebuild instead.
|
||||
if append_unsafe {
|
||||
// resolves overlap by submission order: transparency, a wire with no
|
||||
// draw-order depth, or the mesh-edge arena (all its wires are forced
|
||||
// to depth 0, so coincident edges resolve by submission order). Fall
|
||||
// back to a full rebuild instead.
|
||||
if append_unsafe || self.mesh_edge {
|
||||
return false;
|
||||
}
|
||||
if self.inst_tail + inst_len > self.inst_cap
|
||||
@@ -383,7 +411,13 @@ impl WireArena {
|
||||
// depth map and re-upload the whole (small) const buffer; the instance
|
||||
// buffer is untouched.
|
||||
for (h, slab) in &self.slabs {
|
||||
let dd = depth_map.get(&h.value()).copied().unwrap_or(0.0);
|
||||
// Mesh-edge wires keep depth 0 (no draw-order bias); regular wires
|
||||
// take the re-normalised map value.
|
||||
let dd = if self.mesh_edge {
|
||||
0.0
|
||||
} else {
|
||||
depth_map.get(&h.value()).copied().unwrap_or(0.0)
|
||||
};
|
||||
for k in 0..slab.const_len {
|
||||
self.consts_cpu[(slab.const_off + k) as usize].draw_depth = dd;
|
||||
}
|
||||
@@ -406,7 +440,7 @@ impl WireArena {
|
||||
instance_buffer: self.inst_buf.clone(),
|
||||
instance_count: self.inst_tail,
|
||||
vp_scissor: None,
|
||||
is_3d_mesh_edge: false,
|
||||
is_3d_mesh_edge: self.mesh_edge,
|
||||
const_bind_group: Some(self.const_bind_group.clone()),
|
||||
}]
|
||||
}
|
||||
|
||||
+85
-24
@@ -369,38 +369,81 @@ impl shader::Primitive for Primitive {
|
||||
// 2D/3D sets fall through to the shared batched path below.
|
||||
let mut arena_served = false;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _perf = std::env::var_os("OCS_PERF").is_some();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _t0 = std::time::Instant::now();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut _patched = false;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if crate::scene::wire_gpu_patch_enabled()
|
||||
&& inner.wire_const_bgl.is_some()
|
||||
&& crate::scene::pipeline::wire_arena::is_arena_eligible(&vp.wires)
|
||||
{
|
||||
use crate::scene::pipeline::wire_arena;
|
||||
// In-place patch from the base the GPU holds, else rebuild.
|
||||
let patched = if let (Some(arena), Some((base, changes))) =
|
||||
(inner.wire_arena.as_mut(), vp.wire_patch.as_ref())
|
||||
{
|
||||
inner.wire_arena_id == *base
|
||||
&& !changes.is_empty()
|
||||
&& arena.patch(queue, changes, &vp.wires[..], &vp.draw_depths)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !patched {
|
||||
let bgl = inner.wire_const_bgl.as_ref().unwrap();
|
||||
inner.wire_arena = wire_arena::WireArena::build(
|
||||
device,
|
||||
queue,
|
||||
&vp.wires[..],
|
||||
&vp.draw_depths,
|
||||
bgl,
|
||||
);
|
||||
use crate::scene::pipeline::wire_arena::{self, WireArena};
|
||||
// Split the resident set into the regular 2D wires and the
|
||||
// mesh/solid EDGE wires (which need the mesh-edge draw
|
||||
// treatment), one arena each, so both patch incrementally.
|
||||
// Fill-only wires (no segments) are drawn in the face3d pass,
|
||||
// not here, so they go in neither.
|
||||
let mesh_names: rustc_hash::FxHashSet<u64> = vp
|
||||
.wires
|
||||
.iter()
|
||||
.filter(|w| !w.fill_tris.is_empty() && !w.fill_tris_low.is_empty())
|
||||
.filter_map(|w| w.name.parse::<u64>().ok())
|
||||
.collect();
|
||||
let regular: Vec<&crate::scene::WireModel> = vp
|
||||
.wires
|
||||
.iter()
|
||||
.filter(|w| {
|
||||
!w.points.is_empty() && !wire_arena::is_mesh_edge(w, &mesh_names)
|
||||
})
|
||||
.collect();
|
||||
let mesh: Vec<&crate::scene::WireModel> = vp
|
||||
.wires
|
||||
.iter()
|
||||
.filter(|w| wire_arena::is_mesh_edge(w, &mesh_names))
|
||||
.collect();
|
||||
let bgl = inner.wire_const_bgl.as_ref().unwrap();
|
||||
let base_ok = vp
|
||||
.wire_patch
|
||||
.as_ref()
|
||||
.map_or(false, |(base, changes)| {
|
||||
inner.wire_arena_id == *base && !changes.is_empty()
|
||||
});
|
||||
let changes = vp.wire_patch.as_ref().map(|(_, c)| c);
|
||||
|
||||
// Each batch: patch from the shared base if possible, else
|
||||
// rebuild just that batch. They advance together.
|
||||
let reg_ok = base_ok
|
||||
&& inner.wire_arena.as_mut().map_or(false, |a| {
|
||||
a.patch(queue, changes.unwrap(), ®ular, &vp.draw_depths)
|
||||
});
|
||||
if !reg_ok {
|
||||
inner.wire_arena =
|
||||
WireArena::build(device, queue, ®ular, &vp.draw_depths, bgl, false);
|
||||
}
|
||||
if let Some(arena) = inner.wire_arena.as_ref() {
|
||||
inner.gpu_wires = std::sync::Arc::new(arena.wire_gpus());
|
||||
inner.wire_handle_index =
|
||||
wire_arena::build_handle_index(&vp.wires[..]);
|
||||
let mesh_ok = base_ok
|
||||
&& inner.wire_arena_mesh.as_mut().map_or(false, |a| {
|
||||
a.patch(queue, changes.unwrap(), &mesh, &vp.draw_depths)
|
||||
});
|
||||
if !mesh_ok {
|
||||
inner.wire_arena_mesh =
|
||||
WireArena::build(device, queue, &mesh, &vp.draw_depths, bgl, true);
|
||||
}
|
||||
_patched = reg_ok && mesh_ok;
|
||||
|
||||
if let (Some(reg), Some(me)) =
|
||||
(inner.wire_arena.as_ref(), inner.wire_arena_mesh.as_ref())
|
||||
{
|
||||
let mut gpus = reg.wire_gpus();
|
||||
gpus.extend(me.wire_gpus());
|
||||
inner.gpu_wires = std::sync::Arc::new(gpus);
|
||||
inner.wire_handle_index = wire_arena::build_handle_index(&vp.wires[..]);
|
||||
inner.wire_arena_id = vp.wire_content_id;
|
||||
arena_served = true;
|
||||
} else {
|
||||
inner.wire_arena = None;
|
||||
inner.wire_arena_mesh = None;
|
||||
inner.wire_arena_id = u64::MAX;
|
||||
}
|
||||
}
|
||||
@@ -436,6 +479,24 @@ impl shader::Primitive for Primitive {
|
||||
inner.wire_handle_index = built.1;
|
||||
} // end !arena_served
|
||||
inner.cached_wire_id = vp.wire_content_id;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if _perf {
|
||||
let gi: u32 = inner.gpu_wires.iter().map(|w| w.instance_count).sum();
|
||||
let outcome = if !arena_served {
|
||||
"shared-fullupload"
|
||||
} else if _patched {
|
||||
"arena-patch"
|
||||
} else {
|
||||
"arena-build"
|
||||
};
|
||||
eprintln!(
|
||||
"[perf] wire {:>7.1}ms {:<18} wires={} gpu_instances={}",
|
||||
_t0.elapsed().as_secs_f64() * 1000.0,
|
||||
outcome,
|
||||
vp.wires.len(),
|
||||
gi,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Selection xray overlay — rebuilt when the selection changes or the
|
||||
// underlying wires changed. A pick bumps only selection_generation,
|
||||
|
||||
Reference in new issue
Block a user