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:
Hakan SevenandClaude Opus 4.8 committed 2026-07-15 09:48:20 +03:00
1 parent 9761423b84
commit 95ef3b3890
3 files changed
+157 -55

No files matched your search

+8 -1
View File
@@ -154,7 +154,12 @@ pub struct Pipeline {
/// edit patches one entity's slab in place instead of rebuilding every wire. /// edit patches one entity's slab in place instead of rebuilding every wire.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub(crate) wire_arena: Option<wire_arena::WireArena>, 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"))] #[cfg(not(target_arch = "wasm32"))]
pub(crate) wire_arena_id: u64, pub(crate) wire_arena_id: u64,
/// Pixel scissor rects [x, y, w, h] for viewport-clipped wires. Recomputed each frame. /// 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"))] #[cfg(not(target_arch = "wasm32"))]
wire_arena: None, wire_arena: None,
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
wire_arena_mesh: None,
#[cfg(not(target_arch = "wasm32"))]
wire_arena_id: u64::MAX, wire_arena_id: u64::MAX,
wire_pixel_scissors: vec![], wire_pixel_scissors: vec![],
gpu_selected_wires: vec![], gpu_selected_wires: vec![],
+64 -30
View File
@@ -50,6 +50,12 @@ const HEADROOM_NUM: u64 = 3;
const HEADROOM_DEN: u64 = 2; const HEADROOM_DEN: u64 = 2;
const MIN_INST_CAP: u64 = 4096; const MIN_INST_CAP: u64 = 4096;
const MIN_CONST_CAP: u64 = 1024; 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 { struct Slab {
inst_off: u32, inst_off: u32,
@@ -73,18 +79,29 @@ pub struct WireArena {
/// Tombstoned instances (blanked, not reclaimed) — past half the tail a patch /// Tombstoned instances (blanked, not reclaimed) — past half the tail a patch
/// bails so the caller compacts with a full rebuild. /// bails so the caller compacts with a full rebuild.
tombstoned: u32, 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> { fn handle_of(w: &WireModel) -> Option<Handle> {
crate::scene::Scene::handle_from_wire_name(&w.name) crate::scene::Scene::handle_from_wire_name(&w.name)
} }
/// True if the whole set is arena-eligible: a single batch that draws with no /// True if the set can use the arena: no per-wire scissor (paper viewports). Mesh
/// viewport scissor — no mesh/solid fill and no per-wire scissor. /// 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 { pub fn is_arena_eligible(wires: &[WireModel]) -> bool {
wires wires.iter().all(|w| w.vp_scissor.is_none())
.iter() }
.all(|w| w.fill_tris.is_empty() && 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 /// 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 /// 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 /// 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. /// 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| { wires.iter().any(|w| {
w.color[3] < 0.999 w.color[3] < 0.999
|| handle_of(w).map_or(true, |h| !depth_map.contains_key(&h.value())) || 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. /// 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 out: Vec<(Handle, usize, usize)> = Vec::new();
let mut i = 0; let mut i = 0;
while i < wires.len() { while i < wires.len() {
let h = handle_of(&wires[i])?; let h = handle_of(wires[i])?;
let mut j = i + 1; 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; j += 1;
} }
out.push((h, i, j)); out.push((h, i, j));
@@ -189,13 +206,11 @@ impl WireArena {
pub fn build( pub fn build(
device: &wgpu::Device, device: &wgpu::Device,
queue: &wgpu::Queue, queue: &wgpu::Queue,
wires: &[WireModel], wires: &[&WireModel],
depth_map: &FxHashMap<u64, f32>, depth_map: &FxHashMap<u64, f32>,
const_bgl: &wgpu::BindGroupLayout, const_bgl: &wgpu::BindGroupLayout,
mesh_edge: bool,
) -> Option<Self> { ) -> Option<Self> {
if !is_arena_eligible(wires) {
return None;
}
let ranges = handle_ranges(wires)?; let ranges = handle_ranges(wires)?;
// const slot 0 = blank tombstone target. // const slot 0 = blank tombstone target.
@@ -205,9 +220,12 @@ impl WireArena {
for (h, i, j) in ranges { for (h, i, j) in ranges {
let inst_off = instances.len() as u32; let inst_off = instances.len() as u32;
let const_off = consts_cpu.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 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); let (mut insts, cst) = emit_wire_native(w, wire_id, w.color, dd);
instances.append(&mut insts); instances.append(&mut insts);
consts_cpu.push(cst); consts_cpu.push(cst);
@@ -225,9 +243,17 @@ impl WireArena {
let inst_tail = instances.len() as u32; let inst_tail = instances.len() as u32;
let const_tail = consts_cpu.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; // A batch bigger than one buffer can't be an arena — let the caller chunk
let const_cap = // it via the batched path.
((const_tail as u64 * HEADROOM_NUM / HEADROOM_DEN).max(MIN_CONST_CAP)) as u32; 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 inst_buf = alloc_inst(device, inst_cap as u64);
let const_buf = alloc_const(device, const_cap as u64); let const_buf = alloc_const(device, const_cap as u64);
if inst_tail > 0 { if inst_tail > 0 {
@@ -247,6 +273,7 @@ impl WireArena {
consts_cpu, consts_cpu,
slabs, slabs,
tombstoned: 0, tombstoned: 0,
mesh_edge,
}) })
} }
@@ -265,12 +292,9 @@ impl WireArena {
&mut self, &mut self,
queue: &wgpu::Queue, queue: &wgpu::Queue,
changes: &[(Handle, ChangeKind)], changes: &[(Handle, ChangeKind)],
wires: &[WireModel], wires: &[&WireModel],
depth_map: &FxHashMap<u64, f32>, depth_map: &FxHashMap<u64, f32>,
) -> bool { ) -> bool {
if !is_arena_eligible(wires) {
return false;
}
let Some(ranges) = handle_ranges(wires) else { let Some(ranges) = handle_ranges(wires) else {
return false; return false;
}; };
@@ -282,7 +306,9 @@ impl WireArena {
for &(h, kind) in changes { for &(h, kind) in changes {
let new_range = range_of.get(&h).copied(); 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 matches!(kind, ChangeKind::Removed) || new_range.is_none() {
if let Some(slab) = self.slabs.remove(&h) { if let Some(slab) = self.slabs.remove(&h) {
let blanks = vec![blank_instance(); slab.inst_len as usize]; 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). // Emit into fresh, run-local const slots (patched to absolute below).
let mut insts: Vec<WireInstance> = Vec::new(); let mut insts: Vec<WireInstance> = Vec::new();
let mut csts: Vec<WireConst> = 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 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); let (mut wi, c) = emit_wire_native(w, wire_id, w.color, dd);
insts.append(&mut wi); insts.append(&mut wi);
csts.push(c); csts.push(c);
@@ -339,9 +365,11 @@ impl WireArena {
} }
// Layout changed ⇒ append at the tail. Unsafe to relocate when the set // Layout changed ⇒ append at the tail. Unsafe to relocate when the set
// resolves overlap by submission order (transparency, or a wire with // resolves overlap by submission order: transparency, a wire with no
// no draw-order depth); fall back to a full rebuild instead. // draw-order depth, or the mesh-edge arena (all its wires are forced
if append_unsafe { // 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; return false;
} }
if self.inst_tail + inst_len > self.inst_cap 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 // depth map and re-upload the whole (small) const buffer; the instance
// buffer is untouched. // buffer is untouched.
for (h, slab) in &self.slabs { 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 { for k in 0..slab.const_len {
self.consts_cpu[(slab.const_off + k) as usize].draw_depth = dd; 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_buffer: self.inst_buf.clone(),
instance_count: self.inst_tail, instance_count: self.inst_tail,
vp_scissor: None, vp_scissor: None,
is_3d_mesh_edge: false, is_3d_mesh_edge: self.mesh_edge,
const_bind_group: Some(self.const_bind_group.clone()), const_bind_group: Some(self.const_bind_group.clone()),
}] }]
} }
+85 -24
View File
@@ -369,38 +369,81 @@ impl shader::Primitive for Primitive {
// 2D/3D sets fall through to the shared batched path below. // 2D/3D sets fall through to the shared batched path below.
let mut arena_served = false; let mut arena_served = false;
#[cfg(not(target_arch = "wasm32"))] #[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() if crate::scene::wire_gpu_patch_enabled()
&& inner.wire_const_bgl.is_some() && inner.wire_const_bgl.is_some()
&& crate::scene::pipeline::wire_arena::is_arena_eligible(&vp.wires) && crate::scene::pipeline::wire_arena::is_arena_eligible(&vp.wires)
{ {
use crate::scene::pipeline::wire_arena; use crate::scene::pipeline::wire_arena::{self, WireArena};
// In-place patch from the base the GPU holds, else rebuild. // Split the resident set into the regular 2D wires and the
let patched = if let (Some(arena), Some((base, changes))) = // mesh/solid EDGE wires (which need the mesh-edge draw
(inner.wire_arena.as_mut(), vp.wire_patch.as_ref()) // treatment), one arena each, so both patch incrementally.
{ // Fill-only wires (no segments) are drawn in the face3d pass,
inner.wire_arena_id == *base // not here, so they go in neither.
&& !changes.is_empty() let mesh_names: rustc_hash::FxHashSet<u64> = vp
&& arena.patch(queue, changes, &vp.wires[..], &vp.draw_depths) .wires
} else { .iter()
false .filter(|w| !w.fill_tris.is_empty() && !w.fill_tris_low.is_empty())
}; .filter_map(|w| w.name.parse::<u64>().ok())
if !patched { .collect();
let bgl = inner.wire_const_bgl.as_ref().unwrap(); let regular: Vec<&crate::scene::WireModel> = vp
inner.wire_arena = wire_arena::WireArena::build( .wires
device, .iter()
queue, .filter(|w| {
&vp.wires[..], !w.points.is_empty() && !wire_arena::is_mesh_edge(w, &mesh_names)
&vp.draw_depths, })
bgl, .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(), &regular, &vp.draw_depths)
});
if !reg_ok {
inner.wire_arena =
WireArena::build(device, queue, &regular, &vp.draw_depths, bgl, false);
} }
if let Some(arena) = inner.wire_arena.as_ref() { let mesh_ok = base_ok
inner.gpu_wires = std::sync::Arc::new(arena.wire_gpus()); && inner.wire_arena_mesh.as_mut().map_or(false, |a| {
inner.wire_handle_index = a.patch(queue, changes.unwrap(), &mesh, &vp.draw_depths)
wire_arena::build_handle_index(&vp.wires[..]); });
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; inner.wire_arena_id = vp.wire_content_id;
arena_served = true; arena_served = true;
} else { } else {
inner.wire_arena = None;
inner.wire_arena_mesh = None;
inner.wire_arena_id = u64::MAX; inner.wire_arena_id = u64::MAX;
} }
} }
@@ -436,6 +479,24 @@ impl shader::Primitive for Primitive {
inner.wire_handle_index = built.1; inner.wire_handle_index = built.1;
} // end !arena_served } // end !arena_served
inner.cached_wire_id = vp.wire_content_id; 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 // Selection xray overlay — rebuilt when the selection changes or the
// underlying wires changed. A pick bumps only selection_generation, // underlying wires changed. A pick bumps only selection_generation,