// Persistent per-entity wire instance arena with storage and packed adapters. // // The normal wire path re-emits EVERY wire into a fresh instance buffer whenever // the resident set's content id changes — so any edit on a drawing whose wires // expand to millions of segments re-uploads the whole (hundreds-of-MB) buffer, // which stalls for ~1s on a shared-memory GPU. This arena instead keeps one // persistent instance buffer (plus its shared WireConst storage) laid out as // per-entity *slabs*, so an edit only writes what changed: // // * Modify in place — a move / rotate / scale / colour change keeps the // entity's segment count, so its slab is overwritten where it sits. // * Add — bump-allocate a fresh slab at the tail. // * A tail Modify whose segment count changed — resize its terminal slab in // place. A non-tail shape change rejects the patch before touching GPU state // so the caller can rebuild this wire arena cleanly. // * Erase — tombstone the slab (blank instances that render nothing). // // Two correctness points make add/remove safe: // * draw_depth_map uses stable sparse labels, so Add/Remove changes only the // named entity's depth. Existing slabs retain their WireConst values and no // whole-const-buffer upload is needed. // * A tail-appended entity draws last, which only mis-orders alpha-blended / // coincident wires. So when the set contains ANY transparent wire we bail to a // full rebuild instead of appending. Opaque overlap resolves by the z-bias, so // it is order-independent and safe to relocate. // // A tombstoned instance points at const slot 0, whose negative pattern length // is a shader-level discard sentinel. When tombstone waste or capacity is // exceeded, `patch` returns false and the caller compacts via a full rebuild. // Because a full rebuild is always the fallback, correctness never rides on the // fast path. // // Scope: a SINGLE batch — the set must have no mesh-edge fills (which force the // draw-order-preserving multi-batch split) and no per-wire scissor (paper content // viewports). Mixed 2D/3D or scissored sets fall back to the batched path. use super::wire_gpu::{ emit_wire_native, emit_wire_packed, wire_draw_depth, PackedWireInstance, WireConst, WireGpu, WireInstance, }; use crate::scene::model::wire_model::WireModel; use crate::scene::ChangeKind; use acadrust::Handle; use iced::wgpu; use rustc_hash::FxHashMap; /// Spare capacity multiplier when (re)allocating, so a run of adds appends /// without reallocating each time. 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::() as u64; const MAX_CONSTS: u64 = 268_435_456 / std::mem::size_of::() as u64; struct Slab { inst_off: u32, inst_len: u32, const_off: u32, const_len: u32, /// World-XY bounds for plan-view draw-range culling. Unbounded whenever a /// source wire does not carry a trustworthy entity AABB. aabb: [f32; 4], /// Entity-level draw depth used when this slab was emitted. Individual /// consts may carry block-local offsets around it; structural edits shift /// the whole slab by the base-depth delta instead of flattening those /// offsets. base_depth: f32, } struct PreparedPatchRun { insts: Vec, csts: Vec, base_depth: f32, aabb: [f32; 4], order_sensitive: bool, } fn can_resize_terminal_slab( slab: &Slab, inst_tail: u32, const_tail: u32, inst_cap: u32, const_cap: u32, new_inst_len: u32, new_const_len: u32, change_count: usize, ) -> bool { change_count == 1 && slab.inst_off + slab.inst_len == inst_tail && slab.const_off + slab.const_len == const_tail && slab.inst_off + new_inst_len <= inst_cap && slab.const_off + new_const_len <= const_cap } pub struct WireArena { inst_buf: wgpu::Buffer, inst_cap: u32, inst_tail: u32, const_buf: wgpu::Buffer, const_bind_group: std::sync::Arc, const_cap: u32, const_tail: u32, /// CPU mirror of the const buffer, used for in-place tail resize/patches. consts_cpu: Vec, slabs: FxHashMap, /// Temporarily hidden Modified slabs. Grip drag blanks these but keeps their /// offsets so commit/cancel can restore the original submission order. vacant: FxHashMap, /// 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, /// Conservative submission-order sensitivity of current arena content. order_sensitive: bool, } fn handle_of(w: &WireModel) -> Option { crate::scene::Scene::handle_from_wire_name(&w.name) } /// True when `w` needs the shaded-mode edge treatment. pub fn is_mesh_edge(w: &WireModel, _mesh_names: &rustc_hash::FxHashSet) -> bool { !w.points.is_empty() && w.fill_is_3d } /// True when appending a new entity at the tail could change the image, so the /// arena must fall back to a full rebuild instead of relocating a slab. Two /// cases where draw order (not the z-bias) decides the winning pixel: /// * transparency — alpha blends in submission order; /// * a wire with NO draw-order depth — 3D solids (Solid3D / Region / Body / /// 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 order_sensitive(wires: &[&WireModel], depth_map: &FxHashMap) -> bool { wires.iter().any(|w| { w.color[3] < 0.999 || handle_of(w).map_or(true, |h| !depth_map.contains_key(&h.value())) }) } /// handle → wire-slot index for the selection / text-highlight overlays, built /// from the resident Vec (independent of the arena's slab layout). pub fn build_handle_index(wires: &[WireModel]) -> std::sync::Arc>> { let mut index: FxHashMap> = FxHashMap::default(); index.reserve(wires.len()); for (idx, w) in wires.iter().enumerate() { if let Ok(h) = w.name.parse::() { index.entry(h).or_default().push(idx as u32); } } std::sync::Arc::new(index) } /// Apply the resident Vec's exact splice operations to the selection/text /// handle index. Avoids reparsing every wire name after a one-entity patch. pub(crate) fn patch_handle_index( index: &mut std::sync::Arc>>, edits: &[crate::scene::WireIndexEdit], ) { let index = std::sync::Arc::make_mut(index); for edit in edits { index.remove(&edit.handle.value()); let old_end = edit.start + edit.old_len; let delta = edit.new_len as isize - edit.old_len as isize; if delta != 0 { for slots in index.values_mut() { for slot in slots { if *slot as usize >= old_end { *slot = (*slot as isize + delta) as u32; } } } } if edit.visible && edit.new_len != 0 { index.insert( edit.handle.value(), (edit.start..edit.start + edit.new_len) .map(|slot| slot as u32) .collect(), ); } } } /// Group `wires` (draw-order sorted, entity-contiguous) into per-handle ranges. fn handle_ranges(wires: &[&WireModel]) -> Option> { let mut out: Vec<(Handle, usize, usize)> = Vec::new(); let mut i = 0; while i < wires.len() { let h = handle_of(wires[i])?; let mut j = i + 1; while j < wires.len() && handle_of(wires[j]) == Some(h) { j += 1; } out.push((h, i, j)); i = j; } Some(out) } fn run_aabb(wires: &[&WireModel]) -> [f32; 4] { let mut out = [ f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY, ]; for wire in wires { let [x0, y0, x1, y1] = wire.aabb; if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() || x0 > x1 || y0 > y1 { return WireModel::UNBOUNDED_AABB; } let pad = (wire.world_width * 0.5).max(0.0); out[0] = out[0].min(x0 - pad); out[1] = out[1].min(y0 - pad); out[2] = out[2].max(x1 + pad); out[3] = out[3].max(y1 + pad); } if out[0].is_finite() { out } else { WireModel::UNBOUNDED_AABB } } fn make_const_bg( device: &wgpu::Device, bgl: &wgpu::BindGroupLayout, buf: &wgpu::Buffer, ) -> std::sync::Arc { std::sync::Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("wire_arena.const.bg"), layout: bgl, entries: &[wgpu::BindGroupEntry { binding: 0, resource: buf.as_entire_binding(), }], })) } fn alloc_inst_initialized( device: &wgpu::Device, cap: u64, data: &[WireInstance], ) -> wgpu::Buffer { let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("wire_arena.ibuf"), size: cap * std::mem::size_of::() as u64, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: true, }); if !data.is_empty() { let bytes = bytemuck::cast_slice(data); let mut mapped = buffer .slice(..bytes.len() as u64) .get_mapped_range_mut(); mapped.copy_from_slice(bytes); drop(mapped); } buffer.unmap(); buffer } fn alloc_const_initialized( device: &wgpu::Device, cap: u64, data: &[WireConst], ) -> wgpu::Buffer { let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("wire_arena.cbuf"), size: cap * std::mem::size_of::() as u64, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: true, }); if !data.is_empty() { let bytes = bytemuck::cast_slice(data); let mut mapped = buffer .slice(..bytes.len() as u64) .get_mapped_range_mut(); mapped.copy_from_slice(bytes); drop(mapped); } buffer.unmap(); buffer } fn blank_const() -> WireConst { let mut blank = ::zeroed(); // Negative pattern length is reserved for arena tombstones. A zero-length // segment still expands to a half-pixel when LWDISPLAY is off, so geometry // alone cannot make a tombstone invisible. blank.pattern_length = -1.0; blank } /// A tombstoned instance. Const slot 0 carries the shader discard sentinel. fn blank_instance() -> WireInstance { WireInstance { pos_a: [0.0; 3], pos_a_low: [0.0; 3], pos_b: [0.0; 3], pos_b_low: [0.0; 3], distance_a: 0.0, distance_b: 0.0, wire_id: 0, taper_ratio: [0; 2], } } impl WireArena { /// Build a fresh arena from the full resident set, or `None` if it isn't a /// single scissor-free batch or a wire is unnamed (caller keeps the batched /// path). pub fn build( device: &wgpu::Device, _queue: &wgpu::Queue, wires: &[&WireModel], depth_map: &FxHashMap, const_bgl: &wgpu::BindGroupLayout, mesh_edge: bool, ) -> Option { let ranges = handle_ranges(wires)?; let perf = crate::perf::enabled(); let total_started = iced::time::Instant::now(); // Reject an oversized batch before parallel emission allocates hundreds // of megabytes. `points.len() - 1` is an upper bound because NaN-break // segments are skipped by emit_wire_native. let max_instances: usize = wires .iter() .map(|w| w.points.len().saturating_sub(1)) .sum(); if max_instances as u64 > MAX_INSTANCES || wires.len() as u64 + 1 > MAX_CONSTS { return None; } struct BuildPlan { handle: Handle, start: usize, end: usize, const_off: u32, base_depth: f32, } struct PackedSlab { handle: Handle, const_off: u32, base_depth: f32, aabb: [f32; 4], instances: Vec, consts: Vec, } // Assign global const slots serially so parallel workers can emit final // wire_id values directly. Indexed parallel collect preserves handle and // wire submission order. let mut next_const = 1u32; let plans: Vec = ranges .into_iter() .map(|(handle, start, end)| { let const_off = next_const; next_const += (end - start) as u32; let base_depth = if mesh_edge { 0.0 } else { depth_map.get(&handle.value()).map_or(0.0, |d| d[0]) }; BuildPlan { handle, start, end, const_off, base_depth, } }) .collect(); let pack_started = iced::time::Instant::now(); use crate::par::prelude::*; let packed: Vec = plans .par_iter() .map(|plan| { let run = &wires[plan.start..plan.end]; let capacity: usize = run .iter() .map(|w| w.points.len().saturating_sub(1)) .sum(); let mut instances: Vec = Vec::with_capacity(capacity); let mut consts: Vec = Vec::with_capacity(run.len()); for (local, &w) in run.iter().enumerate() { let wire_id = plan.const_off + local as u32; // 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 emitted, cst) = emit_wire_native(w, wire_id, w.color, dd); instances.append(&mut emitted); consts.push(cst); } PackedSlab { handle: plan.handle, const_off: plan.const_off, base_depth: plan.base_depth, aabb: run_aabb(run), instances, consts, } }) .collect(); let pack_ms = pack_started.elapsed().as_secs_f64() * 1000.0; let inst_count: usize = packed.iter().map(|slab| slab.instances.len()).sum(); let const_count: usize = 1 + packed.iter().map(|slab| slab.consts.len()).sum::(); if inst_count as u64 > MAX_INSTANCES || const_count as u64 > MAX_CONSTS { return None; } // const slot 0 = blank tombstone target. let mut instances: Vec = Vec::with_capacity(inst_count); let mut consts_cpu: Vec = Vec::with_capacity(const_count); consts_cpu.push(blank_const()); let mut slabs: FxHashMap = FxHashMap::with_capacity_and_hasher(packed.len(), Default::default()); for mut packed_slab in packed { let inst_off = instances.len() as u32; let inst_len = packed_slab.instances.len() as u32; let const_len = packed_slab.consts.len() as u32; instances.append(&mut packed_slab.instances); consts_cpu.append(&mut packed_slab.consts); slabs.insert( packed_slab.handle, Slab { inst_off, inst_len, const_off: packed_slab.const_off, const_len, aabb: packed_slab.aabb, base_depth: packed_slab.base_depth, }, ); } let inst_tail = instances.len() as u32; let const_tail = consts_cpu.len() 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 upload_started = iced::time::Instant::now(); let inst_buf = alloc_inst_initialized(device, inst_cap as u64, &instances); let const_buf = alloc_const_initialized(device, const_cap as u64, &consts_cpu); let upload_ms = upload_started.elapsed().as_secs_f64() * 1000.0; let const_bind_group = make_const_bg(device, const_bgl, &const_buf); if perf { crate::perf_record!( "[perf] arena-build-detail total={:.1}ms pack={:.1} mapped-upload={:.1} handles={} wires={} instances={} instance-bytes={} consts={}", total_started.elapsed().as_secs_f64() * 1000.0, pack_ms, upload_ms, slabs.len(), wires.len(), inst_tail, inst_tail as usize * std::mem::size_of::(), const_tail, ); } Some(Self { inst_buf, inst_cap, inst_tail, const_buf, const_bind_group, const_cap, const_tail, consts_cpu, slabs, vacant: FxHashMap::default(), tombstoned: 0, mesh_edge, order_sensitive: order_sensitive(wires, depth_map), }) } fn write_insts(&self, queue: &wgpu::Queue, off: u32, data: &[WireInstance]) { if data.is_empty() { return; } let sz = std::mem::size_of::() as u64; queue.write_buffer(&self.inst_buf, off as u64 * sz, bytemuck::cast_slice(data)); } /// Apply the changed handles in place; returns false (⇒ full rebuild) when the /// arena can't absorb the change: not eligible, a transparent append, a /// capacity overflow, or too much tombstone waste. pub fn patch( &mut self, queue: &wgpu::Queue, changes: &[(Handle, ChangeKind)], runs: &FxHashMap>, new_handles_are_suffix: bool, depth_map: &FxHashMap, ) -> bool { // Prepare and validate every visible run before mutating either GPU // buffer. Growing/shrinking a Modified slab used to tombstone the old // range and append a new one. Repeated live-polyline updates could then // leave the arena half-patched when a later guard requested a rebuild, // producing alternating old/new submissions. The terminal slab can // grow/shrink without relocation (the common live-polyline case); // other shape changes take the clean full-arena fallback while the // scene itself still re-tessellates only the named entity. let mut prepared: FxHashMap = FxHashMap::default(); for &(h, kind) in changes { let run = runs.get(&h).map(Vec::as_slice).unwrap_or(&[]); if matches!(kind, ChangeKind::Removed) || run.is_empty() { continue; } let mut insts: Vec = Vec::new(); let mut csts: Vec = Vec::new(); for &w in run { let wire_id = csts.len() as u32; 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); } let inst_len = insts.len() as u32; let const_len = csts.len() as u32; if matches!(kind, ChangeKind::Modified) { let known = self.slabs.get(&h).or_else(|| self.vacant.get(&h)); let shape_changed = known .is_some_and(|slab| slab.inst_len != inst_len || slab.const_len != const_len); let can_resize_tail = self.slabs.get(&h).is_some_and(|slab| { can_resize_terminal_slab( slab, self.inst_tail, self.const_tail, self.inst_cap, self.const_cap, inst_len, const_len, changes.len(), ) }); if shape_changed && !can_resize_tail { return false; } } prepared.insert( h, PreparedPatchRun { insts, csts, base_depth: if self.mesh_edge { 0.0 } else { depth_map.get(&h.value()).map_or(0.0, |d| d[0]) }, aabb: run_aabb(run), order_sensitive: order_sensitive(run, depth_map), }, ); } for &(h, kind) in changes { let run = runs.get(&h).map(Vec::as_slice).unwrap_or(&[]); // 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) || run.is_empty() { if let Some(slab) = self.slabs.remove(&h) { let blanks = vec![blank_instance(); slab.inst_len as usize]; self.write_insts(queue, slab.inst_off, &blanks); self.tombstoned += slab.inst_len; if matches!(kind, ChangeKind::Modified) { self.vacant.insert(h, slab); } } if matches!(kind, ChangeKind::Removed) { self.vacant.remove(&h); } continue; } let PreparedPatchRun { mut insts, csts, base_depth, aabb, order_sensitive: run_order_sensitive, } = prepared .remove(&h) .expect("visible wire patch run was prepared"); let inst_len = insts.len() as u32; let const_len = csts.len() as u32; if !self.slabs.contains_key(&h) && self .vacant .get(&h) .is_some_and(|s| s.inst_len == inst_len && s.const_len == const_len) { let slab = self.vacant.remove(&h).unwrap(); self.tombstoned = self.tombstoned.saturating_sub(slab.inst_len); self.slabs.insert(h, slab); } let in_place = self .slabs .get(&h) .map(|s| s.inst_len == inst_len && s.const_len == const_len) .unwrap_or(false); if in_place { let (inst_off, const_off) = { let s = self.slabs.get(&h).unwrap(); (s.inst_off, s.const_off) }; for w in insts.iter_mut() { w.wire_id += const_off; } self.write_insts(queue, inst_off, &insts); for (k, c) in csts.iter().enumerate() { self.consts_cpu[const_off as usize + k] = *c; } // Push the entity's consts to the GPU too — an in-place edit is // NOT structural, so the whole-buffer refresh below won't run and // a colour change would otherwise never reach the shader. let csz = std::mem::size_of::() as u64; queue.write_buffer( &self.const_buf, const_off as u64 * csz, bytemuck::cast_slice(&csts), ); let slab = self.slabs.get_mut(&h).unwrap(); slab.base_depth = base_depth; slab.aabb = aabb; continue; } // A live entity is normally the newest handle and therefore owns // the terminal slab. Resize that slab at the same offsets instead // of tombstoning it and rebuilding the whole arena as its segment // count grows on every click. let can_resize_tail = self.slabs.get(&h).is_some_and(|slab| { can_resize_terminal_slab( slab, self.inst_tail, self.const_tail, self.inst_cap, self.const_cap, inst_len, const_len, changes.len(), ) }); if can_resize_tail { let (inst_off, const_off) = { let slab = self.slabs.get(&h).unwrap(); (slab.inst_off, slab.const_off) }; for instance in &mut insts { instance.wire_id += const_off; } self.write_insts(queue, inst_off, &insts); self.consts_cpu.truncate(const_off as usize); self.consts_cpu.extend(csts.iter().copied()); let csz = std::mem::size_of::() as u64; queue.write_buffer( &self.const_buf, const_off as u64 * csz, bytemuck::cast_slice(&csts), ); self.inst_tail = inst_off + inst_len; self.const_tail = const_off + const_len; let slab = self.slabs.get_mut(&h).unwrap(); slab.inst_len = inst_len; slab.const_len = const_len; slab.base_depth = base_depth; slab.aabb = aabb; self.order_sensitive |= run_order_sensitive; continue; } // Layout changed ⇒ append at the tail. Unsafe to relocate when the set // 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. let is_new = !self.slabs.contains_key(&h); let preserves_submission_order = is_new && new_handles_are_suffix; if (self.order_sensitive || run_order_sensitive || self.mesh_edge) && !preserves_submission_order { return false; } if self.inst_tail + inst_len > self.inst_cap || self.const_tail + const_len > self.const_cap { return false; } self.vacant.remove(&h); if let Some(s) = self.slabs.remove(&h) { let blanks = vec![blank_instance(); s.inst_len as usize]; self.write_insts(queue, s.inst_off, &blanks); self.tombstoned += s.inst_len; } let inst_off = self.inst_tail; let const_off = self.const_tail; for w in insts.iter_mut() { w.wire_id += const_off; } self.write_insts(queue, inst_off, &insts); for c in &csts { self.consts_cpu.push(*c); } // Appended instances immediately reference these constant slots. // Keep the GPU buffer in sync without relying on a full-scene upload. if !csts.is_empty() { let const_size = std::mem::size_of::() as u64; queue.write_buffer( &self.const_buf, const_off as u64 * const_size, bytemuck::cast_slice(&csts), ); } self.inst_tail += inst_len; self.const_tail += const_len; self.slabs.insert( h, Slab { inst_off, inst_len, const_off, const_len, aabb, base_depth, }, ); self.order_sensitive |= run_order_sensitive; } if self.tombstoned > self.inst_tail / 2 { return false; } true } /// One draw batch wrapping the persistent instance buffer. `instance_count` /// is the whole tail (tombstones included — they draw nothing). pub fn wire_gpus(&self) -> Vec { if self.inst_tail == 0 { return vec![]; } vec![WireGpu { instance_buffer: self.inst_buf.clone(), first_instance: 0, instance_count: self.inst_tail, is_3d_mesh_edge: self.mesh_edge, const_bind_group: Some(self.const_bind_group.clone()), }] } /// Draw only plan-view entity ranges that can reach the viewport. The /// instance buffer stays fully resident, so pan/zoom changes only this tiny /// list of offsets — no repack and no GPU upload. Non-plan views use the /// conservative full draw because a 2-D entity AABB does not contain Z. pub fn wire_gpus_visible( &self, view_rot: glam::Mat4, eye: glam::DVec3, clip_w: u32, clip_h: u32, ) -> Vec { let perf = crate::perf::enabled(); let perf_started = perf.then(iced::time::Instant::now); if self.inst_tail == 0 { return vec![]; } let projected_x = view_rot.transform_vector3(glam::Vec3::X); let projected_y = view_rot.transform_vector3(glam::Vec3::Y); let projected_z = view_rot.transform_vector3(glam::Vec3::Z); let xy_scale = projected_x .truncate() .length() .max(projected_y.truncate().length()) .max(f32::MIN_POSITIVE); if projected_z.truncate().length() > xy_scale * 1e-5 { return self.wire_gpus(); } let mut ranges: Vec<(u32, u32)> = self .slabs .values() .filter(|slab| { slab.inst_len > 0 && !super::aabb_offscreen(slab.aabb, view_rot, eye, clip_w, clip_h) }) .map(|slab| (slab.inst_off, slab.inst_off + slab.inst_len)) .collect(); ranges.sort_unstable_by_key(|range| range.0); let mut merged: Vec<(u32, u32)> = Vec::with_capacity(ranges.len()); for (start, end) in ranges { if let Some((_, previous_end)) = merged.last_mut() { if *previous_end == start { *previous_end = end; continue; } } merged.push((start, end)); } let mut ranges = merged; if ranges.is_empty() { return vec![]; } // Cap CPU draw-call overhead on pathologically interleaved draw order. // Merging a few separated visible spans draws their offscreen gap too, // but retains order and still avoids the rest of a multi-million // instance drawing. const MAX_RANGES: usize = 64; if ranges.len() > MAX_RANGES { let group = (ranges.len() + MAX_RANGES - 1) / MAX_RANGES; ranges = ranges .chunks(group) .map(|chunk| (chunk[0].0, chunk[chunk.len() - 1].1)) .collect(); } if perf { let submitted: u64 = ranges .iter() .map(|(start, end)| (end - start) as u64) .sum(); let elapsed_ms = perf_started .map(|started| started.elapsed().as_secs_f64() * 1000.0) .unwrap_or_default(); if submitted < self.inst_tail as u64 || elapsed_ms >= 1.0 { crate::perf_record!( "[perf] wire-cull {:>7.1}ms submitted={} resident={} ranges={}", elapsed_ms, submitted, self.inst_tail, ranges.len(), ); } } ranges .into_iter() .map(|(start, end)| WireGpu { instance_buffer: self.inst_buf.clone(), first_instance: start, instance_count: end - start, is_3d_mesh_edge: self.mesh_edge, const_bind_group: Some(self.const_bind_group.clone()), }) .collect() } } // ── Packed arena adapter ─────────────────────────────────────────────────── /// Persistent arena for devices without vertex-stage storage buffers. Shared /// wire constants stay duplicated in each packed instance, but residency, /// entity-local patches, tombstones, headroom, and visibility ranges match the /// indexed-storage arena above. struct PackedWireArena { inst_buf: wgpu::Buffer, inst_cap: u32, inst_tail: u32, slabs: FxHashMap, vacant: FxHashMap, tombstoned: u32, mesh_edge: bool, order_sensitive: bool, } struct PreparedPackedPatchRun { insts: Vec, base_depth: f32, aabb: [f32; 4], order_sensitive: bool, } const MAX_PACKED_INSTANCES: u64 = 268_435_456 / std::mem::size_of::() as u64; fn blank_packed_instance() -> PackedWireInstance { let mut blank = ::zeroed(); blank.pattern_length = -1.0; blank } fn can_resize_packed_terminal_slab( slab: &Slab, inst_tail: u32, inst_cap: u32, new_inst_len: u32, change_count: usize, ) -> bool { change_count == 1 && slab.inst_off + slab.inst_len == inst_tail && slab.inst_off + new_inst_len <= inst_cap } fn alloc_packed_initialized( device: &wgpu::Device, cap: u64, data: &[PackedWireInstance], ) -> wgpu::Buffer { let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("wire_arena.packed.ibuf"), size: cap * std::mem::size_of::() as u64, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: true, }); if !data.is_empty() { let bytes = bytemuck::cast_slice(data); let mut mapped = buffer .slice(..bytes.len() as u64) .get_mapped_range_mut(); mapped.copy_from_slice(bytes); drop(mapped); } buffer.unmap(); buffer } fn visible_ranges( slabs: &FxHashMap, view_rot: glam::Mat4, eye: glam::DVec3, clip_w: u32, clip_h: u32, ) -> Vec<(u32, u32)> { let mut ranges: Vec<(u32, u32)> = slabs .values() .filter(|slab| { slab.inst_len > 0 && !super::aabb_offscreen(slab.aabb, view_rot, eye, clip_w, clip_h) }) .map(|slab| (slab.inst_off, slab.inst_off + slab.inst_len)) .collect(); ranges.sort_unstable_by_key(|range| range.0); let mut merged: Vec<(u32, u32)> = Vec::with_capacity(ranges.len()); for (start, end) in ranges { if let Some((_, previous_end)) = merged.last_mut() { if *previous_end == start { *previous_end = end; continue; } } merged.push((start, end)); } // Cap CPU draw-call overhead on pathologically interleaved draw order. const MAX_RANGES: usize = 64; if merged.len() > MAX_RANGES { let group = (merged.len() + MAX_RANGES - 1) / MAX_RANGES; merged = merged .chunks(group) .map(|chunk| (chunk[0].0, chunk[chunk.len() - 1].1)) .collect(); } merged } impl PackedWireArena { fn build( device: &wgpu::Device, wires: &[&WireModel], depth_map: &FxHashMap, mesh_edge: bool, ) -> Option { let ranges = handle_ranges(wires)?; let max_instances: usize = wires .iter() .map(|wire| wire.points.len().saturating_sub(1)) .sum(); if max_instances as u64 > MAX_PACKED_INSTANCES { return None; } struct PackedSlab { handle: Handle, base_depth: f32, aabb: [f32; 4], instances: Vec, } use crate::par::prelude::*; let packed: Vec = ranges .par_iter() .map(|&(handle, start, end)| { let run = &wires[start..end]; let base_depth = if mesh_edge { 0.0 } else { depth_map.get(&handle.value()).map_or(0.0, |depth| depth[0]) }; let mut instances = Vec::with_capacity( run.iter() .map(|wire| wire.points.len().saturating_sub(1)) .sum(), ); for &wire in run { let draw_depth = if mesh_edge { 0.0 } else { wire_draw_depth(wire, depth_map) }; instances.extend(emit_wire_packed(wire, wire.color, draw_depth)); } PackedSlab { handle, base_depth, aabb: run_aabb(run), instances, } }) .collect(); let inst_count: usize = packed.iter().map(|slab| slab.instances.len()).sum(); if inst_count as u64 > MAX_PACKED_INSTANCES { return None; } let mut instances = Vec::with_capacity(inst_count); let mut slabs = FxHashMap::with_capacity_and_hasher(packed.len(), Default::default()); for mut packed_slab in packed { let inst_off = instances.len() as u32; let inst_len = packed_slab.instances.len() as u32; instances.append(&mut packed_slab.instances); slabs.insert( packed_slab.handle, Slab { inst_off, inst_len, const_off: 0, const_len: 0, aabb: packed_slab.aabb, base_depth: packed_slab.base_depth, }, ); } let inst_tail = instances.len() as u32; let inst_cap = ((inst_tail as u64 * HEADROOM_NUM / HEADROOM_DEN) .max(MIN_INST_CAP) .min(MAX_PACKED_INSTANCES)) as u32; let inst_buf = alloc_packed_initialized(device, inst_cap as u64, &instances); Some(Self { inst_buf, inst_cap, inst_tail, slabs, vacant: FxHashMap::default(), tombstoned: 0, mesh_edge, order_sensitive: order_sensitive(wires, depth_map), }) } fn write_insts(&self, queue: &wgpu::Queue, off: u32, data: &[PackedWireInstance]) { if data.is_empty() { return; } let size = std::mem::size_of::() as u64; queue.write_buffer( &self.inst_buf, off as u64 * size, bytemuck::cast_slice(data), ); } fn patch( &mut self, queue: &wgpu::Queue, changes: &[(Handle, ChangeKind)], runs: &FxHashMap>, new_handles_are_suffix: bool, depth_map: &FxHashMap, ) -> bool { let mut prepared: FxHashMap = FxHashMap::default(); for &(handle, kind) in changes { let run = runs.get(&handle).map(Vec::as_slice).unwrap_or(&[]); if matches!(kind, ChangeKind::Removed) || run.is_empty() { continue; } let mut insts = Vec::new(); for &wire in run { let draw_depth = if self.mesh_edge { 0.0 } else { wire_draw_depth(wire, depth_map) }; insts.extend(emit_wire_packed(wire, wire.color, draw_depth)); } let inst_len = insts.len() as u32; if matches!(kind, ChangeKind::Modified) { let known = self .slabs .get(&handle) .or_else(|| self.vacant.get(&handle)); let shape_changed = known.is_some_and(|slab| slab.inst_len != inst_len); let can_resize_tail = self.slabs.get(&handle).is_some_and(|slab| { can_resize_packed_terminal_slab( slab, self.inst_tail, self.inst_cap, inst_len, changes.len(), ) }); if shape_changed && !can_resize_tail { return false; } } prepared.insert( handle, PreparedPackedPatchRun { insts, base_depth: if self.mesh_edge { 0.0 } else { depth_map .get(&handle.value()) .map_or(0.0, |depth| depth[0]) }, aabb: run_aabb(run), order_sensitive: order_sensitive(run, depth_map), }, ); } for &(handle, kind) in changes { let run = runs.get(&handle).map(Vec::as_slice).unwrap_or(&[]); if matches!(kind, ChangeKind::Removed) || run.is_empty() { if let Some(slab) = self.slabs.remove(&handle) { let blanks = vec![blank_packed_instance(); slab.inst_len as usize]; self.write_insts(queue, slab.inst_off, &blanks); self.tombstoned += slab.inst_len; if matches!(kind, ChangeKind::Modified) { self.vacant.insert(handle, slab); } } if matches!(kind, ChangeKind::Removed) { self.vacant.remove(&handle); } continue; } let PreparedPackedPatchRun { insts, base_depth, aabb, order_sensitive: run_order_sensitive, } = prepared .remove(&handle) .expect("visible packed wire patch run was prepared"); let inst_len = insts.len() as u32; if !self.slabs.contains_key(&handle) && self .vacant .get(&handle) .is_some_and(|slab| slab.inst_len == inst_len) { let slab = self.vacant.remove(&handle).unwrap(); self.tombstoned = self.tombstoned.saturating_sub(slab.inst_len); self.slabs.insert(handle, slab); } if self .slabs .get(&handle) .is_some_and(|slab| slab.inst_len == inst_len) { let inst_off = self.slabs[&handle].inst_off; self.write_insts(queue, inst_off, &insts); let slab = self.slabs.get_mut(&handle).unwrap(); slab.base_depth = base_depth; slab.aabb = aabb; continue; } let can_resize_tail = self.slabs.get(&handle).is_some_and(|slab| { can_resize_packed_terminal_slab( slab, self.inst_tail, self.inst_cap, inst_len, changes.len(), ) }); if can_resize_tail { let inst_off = self.slabs[&handle].inst_off; self.write_insts(queue, inst_off, &insts); self.inst_tail = inst_off + inst_len; let slab = self.slabs.get_mut(&handle).unwrap(); slab.inst_len = inst_len; slab.base_depth = base_depth; slab.aabb = aabb; self.order_sensitive |= run_order_sensitive; continue; } let is_new = !self.slabs.contains_key(&handle); let preserves_submission_order = is_new && new_handles_are_suffix; if (self.order_sensitive || run_order_sensitive || self.mesh_edge) && !preserves_submission_order { return false; } if self.inst_tail + inst_len > self.inst_cap { return false; } self.vacant.remove(&handle); if let Some(slab) = self.slabs.remove(&handle) { let blanks = vec![blank_packed_instance(); slab.inst_len as usize]; self.write_insts(queue, slab.inst_off, &blanks); self.tombstoned += slab.inst_len; } let inst_off = self.inst_tail; self.write_insts(queue, inst_off, &insts); self.inst_tail += inst_len; self.slabs.insert( handle, Slab { inst_off, inst_len, const_off: 0, const_len: 0, aabb, base_depth, }, ); self.order_sensitive |= run_order_sensitive; } self.tombstoned <= self.inst_tail / 2 } fn wire_gpus(&self) -> Vec { if self.inst_tail == 0 { return Vec::new(); } vec![WireGpu { instance_buffer: self.inst_buf.clone(), first_instance: 0, instance_count: self.inst_tail, is_3d_mesh_edge: self.mesh_edge, const_bind_group: None, }] } fn wire_gpus_visible( &self, view_rot: glam::Mat4, eye: glam::DVec3, clip_w: u32, clip_h: u32, ) -> Vec { if self.inst_tail == 0 { return Vec::new(); } let projected_x = view_rot.transform_vector3(glam::Vec3::X); let projected_y = view_rot.transform_vector3(glam::Vec3::Y); let projected_z = view_rot.transform_vector3(glam::Vec3::Z); let xy_scale = projected_x .truncate() .length() .max(projected_y.truncate().length()) .max(f32::MIN_POSITIVE); if projected_z.truncate().length() > xy_scale * 1e-5 { return self.wire_gpus(); } visible_ranges(&self.slabs, view_rot, eye, clip_w, clip_h) .into_iter() .map(|(start, end)| WireGpu { instance_buffer: self.inst_buf.clone(), first_instance: start, instance_count: end - start, is_3d_mesh_edge: self.mesh_edge, const_bind_group: None, }) .collect() } } enum PersistentWireArenaKind { Indexed(WireArena), Packed(PackedWireArena), } /// Capability-selected arena façade. Callers work with one lifecycle while the /// adapter preserves the best GPU representation the device supports. pub struct PersistentWireArena { inner: PersistentWireArenaKind, } impl PersistentWireArena { pub fn build( device: &wgpu::Device, queue: &wgpu::Queue, wires: &[&WireModel], depth_map: &FxHashMap, const_bgl: Option<&wgpu::BindGroupLayout>, mesh_edge: bool, ) -> Option { let inner = if let Some(const_bgl) = const_bgl { PersistentWireArenaKind::Indexed(WireArena::build( device, queue, wires, depth_map, const_bgl, mesh_edge, )?) } else { PersistentWireArenaKind::Packed(PackedWireArena::build( device, wires, depth_map, mesh_edge, )?) }; Some(Self { inner }) } pub fn patch( &mut self, queue: &wgpu::Queue, changes: &[(Handle, ChangeKind)], runs: &FxHashMap>, new_handles_are_suffix: bool, depth_map: &FxHashMap, ) -> bool { match &mut self.inner { PersistentWireArenaKind::Indexed(arena) => arena.patch( queue, changes, runs, new_handles_are_suffix, depth_map, ), PersistentWireArenaKind::Packed(arena) => arena.patch( queue, changes, runs, new_handles_are_suffix, depth_map, ), } } pub fn wire_gpus(&self) -> Vec { match &self.inner { PersistentWireArenaKind::Indexed(arena) => arena.wire_gpus(), PersistentWireArenaKind::Packed(arena) => arena.wire_gpus(), } } pub fn wire_gpus_visible( &self, view_rot: glam::Mat4, eye: glam::DVec3, clip_w: u32, clip_h: u32, ) -> Vec { match &self.inner { PersistentWireArenaKind::Indexed(arena) => { arena.wire_gpus_visible(view_rot, eye, clip_w, clip_h) } PersistentWireArenaKind::Packed(arena) => { arena.wire_gpus_visible(view_rot, eye, clip_w, clip_h) } } } } #[cfg(test)] mod tests { use super::*; fn slab() -> Slab { Slab { inst_off: 100, inst_len: 10, const_off: 20, const_len: 2, aabb: [0.0; 4], base_depth: 0.0, } } #[test] fn terminal_slab_can_grow_without_relocating_the_arena() { assert!(can_resize_terminal_slab( &slab(), 110, 22, 1000, 100, 18, 3, 1, )); } #[test] fn non_terminal_or_batched_resize_uses_clean_fallback() { assert!(!can_resize_terminal_slab( &slab(), 111, 22, 1000, 100, 18, 3, 1, )); assert!(!can_resize_terminal_slab( &slab(), 110, 22, 1000, 100, 18, 3, 2, )); } #[test] fn packed_terminal_resize_obeys_order_and_capacity() { assert!(can_resize_packed_terminal_slab( &slab(), 110, 1000, 18, 1, )); assert!(!can_resize_packed_terminal_slab( &slab(), 111, 1000, 18, 1, )); assert!(!can_resize_packed_terminal_slab( &slab(), 110, 117, 18, 1, )); assert!(!can_resize_packed_terminal_slab( &slab(), 110, 1000, 18, 2, )); } #[test] fn tombstones_use_the_shader_discard_sentinel() { assert!(blank_const().pattern_length < 0.0); assert!(blank_packed_instance().pattern_length < 0.0); } }