diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 8e983d497..192ef6a16 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -213,6 +213,9 @@ pub struct Pipeline { /// shared resident buffer. pub(crate) gpu_wires: std::sync::Arc>, pub(crate) gpu_block_wires: std::sync::Arc>, + /// Block geometry that survives an edit, keyed by the definition it + /// expands. See `wire_gpu::BlockGeometryCache`. + pub(crate) block_geometry: wire_gpu::BlockGeometryCache, /// Persistent per-entity wire instance arena (capability-selected format). /// When active, `gpu_wires` is a thin wrapper over this arena's buffers and an /// edit patches one entity's slab in place instead of rebuilding every wire. @@ -2279,6 +2282,7 @@ impl Pipeline { surface_format: format, gpu_wires: std::sync::Arc::new(vec![]), gpu_block_wires: std::sync::Arc::new(vec![]), + block_geometry: Default::default(), wire_arena: None, wire_arena_mesh: None, wire_arena_fallback: std::sync::Arc::new(Vec::new()), @@ -2337,7 +2341,7 @@ impl Pipeline { /// `MultiPipeline::wire_buffer_cache`). Takes `&self` (reads only the const /// bind-group layout) so the shared cache — not the slot — owns the result. pub fn build_wire_buffers( - &self, + &mut self, device: &wgpu::Device, queue: &wgpu::Queue, wires: &[WireModel], @@ -2397,6 +2401,7 @@ impl Pipeline { depth_map, None, &self.block_wire_const_bgl, + Some(&mut self.block_geometry), ); // Index handle → wire slots once, here, so the per-hover selection @@ -2523,6 +2528,7 @@ impl Pipeline { depth_map, Some(tint), &self.block_wire_const_bgl, + None, ) } else { Vec::new() @@ -2534,6 +2540,7 @@ impl Pipeline { depth_map, Some(WireModel::HOVER), &self.block_wire_const_bgl, + None, )); self.gpu_selected_block_wires = block_gpu; if let Some(started) = perf_started { diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 9d65d44a7..3fe96e415 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -665,6 +665,26 @@ pub(crate) fn wire_draw_depth( } } +/// Per-definition block geometry that survives an edit. +/// +/// A block group's vertex buffers and constants depend only on the definition +/// it expands, identified by `source_id` — minted once when the block cache +/// expands a definition, and unchanged for as long as that expansion is +/// cached. Adding an unrelated entity changes neither, yet the whole block +/// half of the upload was rebuilt every time: 240 ms of the 350 ms an edit +/// cost on a drawing with 11 380 block wires, all of it identical to the +/// previous frame. +/// +/// Keyed by `(source_id, mesh_edge, colour)` because a highlight tint changes +/// the constants while leaving the vertices alone. +pub type BlockGeometryCache = rustc_hash::FxHashMap< + (u64, bool, u32), + ( + std::sync::Arc>, + std::sync::Arc, + ), +>; + impl BlockWireGpu { pub fn from_wires( device: &wgpu::Device, @@ -673,8 +693,8 @@ impl BlockWireGpu { depth_map: &rustc_hash::FxHashMap, color_override: Option<[f32; 4]>, const_bgl: &wgpu::BindGroupLayout, + mut cache: Option<&mut BlockGeometryCache>, ) -> Vec { - let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> = rustc_hash::FxHashMap::default(); let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new(); @@ -699,6 +719,8 @@ impl BlockWireGpu { } let mut out = Vec::new(); + let mut live_keys: rustc_hash::FxHashSet<(u64, bool, u32)> = + rustc_hash::FxHashSet::default(); for (mesh_edge, group) in groups { let Some(&source) = group.first() else { continue; @@ -707,6 +729,30 @@ impl BlockWireGpu { continue; }; let color = color_override.unwrap_or(source.color); + let cache_key = ( + base.source_id, + mesh_edge, + color.iter().fold(0u32, |acc, c| acc.rotate_left(8) ^ c.to_bits()), + ); + live_keys.insert(cache_key); + // Reuse this definition's geometry when it is already on the GPU. + if let Some((chunks, bind_group)) = cache + .as_deref() + .and_then(|c| c.get(&cache_key)) + .map(|(v, b)| (std::sync::Arc::clone(v), std::sync::Arc::clone(b))) + { + let instances = block_instances(&group, base, mesh_edge, depth_map); + push_block_batches( + &mut out, + device, + queue, + &chunks, + &instances, + mesh_edge, + &bind_group, + ); + continue; + } let (segments, mut constant) = emit_wire_native(source, 0, color, 0.0); if segments.is_empty() { continue; @@ -770,63 +816,112 @@ impl BlockWireGpu { }], }, )); - let instances: Vec = group - .iter() - .filter_map(|wire| { - let instance = wire.render_instance?; - let delta = [ - instance.translation[0] - base.translation[0], - instance.translation[1] - base.translation[1], - instance.translation[2] - base.translation[2], - ]; - let high = delta.map(|value| value as f32); - Some(BlockWireInstance { - translation: high, - translation_low: [ - (delta[0] - high[0] as f64) as f32, - (delta[1] - high[1] as f64) as f32, - (delta[2] - high[2] as f64) as f32, - ], - depth: [ - if mesh_edge { - 0.0 - } else { - wire_draw_depth(wire, depth_map) - }, - 0.0, - ], - }) - }) - .collect(); - let max_instances = super::gpu_budget::max_elements::(device); - // Build each instance buffer once and reuse it across vertex - // chunks, rather than re-uploading it per chunk. - let instance_chunks: Vec<(wgpu::Buffer, u32)> = instances - .chunks(max_instances) - .map(|chunk| { + let vertex_chunks = std::sync::Arc::new(vertex_chunks); + if let Some(cache) = cache.as_deref_mut() { + cache.insert( + cache_key, ( - instance_buffer(device, queue, "block_wire.instances", chunk), - chunk.len() as u32, - ) - }) - .collect(); - for (vertex_buffer, vertex_count) in &vertex_chunks { - for (instance_buffer, instance_count) in &instance_chunks { - out.push(Self { - vertex_buffer: vertex_buffer.clone(), - instance_buffer: instance_buffer.clone(), - vertex_count: *vertex_count, - instance_count: *instance_count, - is_3d_mesh_edge: mesh_edge, - const_bind_group: const_bind_group.clone(), - }); - } + std::sync::Arc::clone(&vertex_chunks), + std::sync::Arc::clone(&const_bind_group), + ), + ); } + let instances = block_instances(&group, base, mesh_edge, depth_map); + push_block_batches( + &mut out, + device, + queue, + &vertex_chunks, + &instances, + mesh_edge, + &const_bind_group, + ); + } + // Definitions that are gone — the block cache was rebuilt and minted + // fresh source ids — would otherwise hold their vertex buffers on the + // GPU forever. + if let Some(cache) = cache { + cache.retain(|key, _| live_keys.contains(key)); } out } } +/// Per-instance placements for one block group. Rebuilt even when the geometry +/// is reused: each instance carries its own draw depth, and the draw-depth map +/// shifts whenever anything is added or removed. They are also small — a few +/// thousand placements against millions of vertices. +fn block_instances( + group: &[&WireModel], + base: crate::scene::model::instance_model::RenderInstance, + mesh_edge: bool, + depth_map: &rustc_hash::FxHashMap, +) -> Vec { + group + .iter() + .filter_map(|wire| { + let instance = wire.render_instance?; + let delta = [ + instance.translation[0] - base.translation[0], + instance.translation[1] - base.translation[1], + instance.translation[2] - base.translation[2], + ]; + let high = delta.map(|value| value as f32); + Some(BlockWireInstance { + translation: high, + translation_low: [ + (delta[0] - high[0] as f64) as f32, + (delta[1] - high[1] as f64) as f32, + (delta[2] - high[2] as f64) as f32, + ], + depth: [ + if mesh_edge { + 0.0 + } else { + wire_draw_depth(wire, depth_map) + }, + 0.0, + ], + }) + }) + .collect() +} + +/// One batch per (vertex chunk × instance chunk). Instance buffers are built +/// once and shared across vertex chunks rather than re-uploaded per chunk. +fn push_block_batches( + out: &mut Vec, + device: &wgpu::Device, + queue: &wgpu::Queue, + vertex_chunks: &[(wgpu::Buffer, u32)], + instances: &[BlockWireInstance], + mesh_edge: bool, + const_bind_group: &std::sync::Arc, +) { + let max_instances = super::gpu_budget::max_elements::(device); + let instance_chunks: Vec<(wgpu::Buffer, u32)> = instances + .chunks(max_instances) + .map(|chunk| { + ( + instance_buffer(device, queue, "block_wire.instances", chunk), + chunk.len() as u32, + ) + }) + .collect(); + for (vertex_buffer, vertex_count) in vertex_chunks { + for (instance_buffer, instance_count) in &instance_chunks { + out.push(BlockWireGpu { + vertex_buffer: vertex_buffer.clone(), + instance_buffer: instance_buffer.clone(), + vertex_count: *vertex_count, + instance_count: *instance_count, + is_3d_mesh_edge: mesh_edge, + const_bind_group: const_bind_group.clone(), + }); + } + } +} + /// Upload one constant chunk; its instances use chunk-local wire IDs. fn build_const_bind_group( device: &wgpu::Device,