forked from 1b-agent/opencadstudio-upstream
perf: keep block geometry on the GPU across edits
Adding one entity to a drawing rebuilt every block group's geometry from
scratch. Measured on a 1.17 M-entity drawing with 11 380 block wires, the
per-edit upload split as:
upload-split regular=80.4ms block=230.3ms
The block half is three quarters of it, and every byte of it was
identical to the previous frame: a new line is not a block reference.
`BlockWireGpu::from_wires` re-emitted each group's segments, re-expanded
them six-fold and re-uploaded the vertex buffers anyway.
That work depends only on the block definition being expanded, which
already has a stable identity: `render_instance.source_id`, minted once
when the block cache expands a definition and unchanged while that
expansion stays cached. Cache the vertex chunks and constants under
`(source_id, mesh_edge, colour)` -- colour is in the key because a
highlight tint changes the constants while leaving the vertices alone.
Only the per-instance placements are rebuilt, and they must be: each
carries its own draw depth and the depth map shifts on every add or
remove. They are also small, a few thousand placements against millions
of vertices.
Entries not seen in a build are dropped, so a block-cache rebuild that
mints fresh source ids does not strand old buffers on the GPU.
Measured: the block half falls from 230 ms to 7.4 ms, and the whole
per-edit upload from ~350 ms to ~118 ms.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
13afc1479f
commit
071b4b6a87
2 files changed
+154
-52
No files matched your search
@@ -213,6 +213,9 @@ pub struct Pipeline {
|
||||
/// shared resident buffer.
|
||||
pub(crate) gpu_wires: std::sync::Arc<Vec<WireGpu>>,
|
||||
pub(crate) gpu_block_wires: std::sync::Arc<Vec<BlockWireGpu>>,
|
||||
/// 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 {
|
||||
|
||||
+146
-51
@@ -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<Vec<(wgpu::Buffer, u32)>>,
|
||||
std::sync::Arc<wgpu::BindGroup>,
|
||||
),
|
||||
>;
|
||||
|
||||
impl BlockWireGpu {
|
||||
pub fn from_wires(
|
||||
device: &wgpu::Device,
|
||||
@@ -673,8 +693,8 @@ impl BlockWireGpu {
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
color_override: Option<[f32; 4]>,
|
||||
const_bgl: &wgpu::BindGroupLayout,
|
||||
mut cache: Option<&mut BlockGeometryCache>,
|
||||
) -> Vec<Self> {
|
||||
|
||||
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<BlockWireInstance> = 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::<BlockWireInstance>(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<u64, [f32; 2]>,
|
||||
) -> Vec<BlockWireInstance> {
|
||||
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<BlockWireGpu>,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
vertex_chunks: &[(wgpu::Buffer, u32)],
|
||||
instances: &[BlockWireInstance],
|
||||
mesh_edge: bool,
|
||||
const_bind_group: &std::sync::Arc<wgpu::BindGroup>,
|
||||
) {
|
||||
let max_instances = super::gpu_budget::max_elements::<BlockWireInstance>(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,
|
||||
|
||||
Reference in new issue
Block a user