perf(render): share one GPU wire buffer across slots by content id

Each pipeline slot owned its own gpu_wires: every paper viewport and every
Model tile rebuilt an identical resident wire set into its own GPU vertex
buffers and its own handle index. With N content viewports drawing one shared
tessellation that is N copies of the (potentially huge) wire buffers in VRAM
plus N batch+index builds on every content change — the issue-131 memory
exposure multiplied by the viewport count.

A wgpu::Buffer is internally ref-counted, so sharing a WireGpu by Arc shares
its GPU allocation. Make Pipeline.gpu_wires an Arc<Vec<WireGpu>> (and
wire_handle_index an Arc<map>), turn upload_wires into build_wire_buffers(&self)
that returns those Arcs, and add MultiPipeline.wire_buffer_cache keyed by
wire_content_id. prepare() builds an entry once on a cache miss and hands Arc
clones to every slot with that id — so N viewports (and, since one MultiPipeline
backs all panes, N panes) upload the wire vertices exactly once between them.
Camera uniforms and scissor stay per-slot, so the draw still issues one call per
viewport with the right view; only the geometry is deduplicated. The shared Vec
is never mutated in place — only reassigned on a content change — so the sharing
is sound.

The cache drops entries no slot still references (Arc::strong_count == 1) once it
passes a small bound, so a live buffer (held by any pane) is never evicted.
Model multi-tile gets the same dedup for free, and two panes showing the same
model now share buffers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-14 21:19:18 +03:00
commit 0f61a2e582
2 changed files with 73 additions and 12 deletions

View file

@ -357,7 +357,35 @@ impl shader::Primitive for Primitive {
// independent of the `cur_key` block so a preview/interim wire change
// still uploads even when the camera didn't move.
if vp.wire_content_id != inner.cached_wire_id {
inner.upload_wires(device, &vp.wires[..], &vp.draw_depths);
// Share one copy of the resident wire buffers across every slot
// (and every pane — one MultiPipeline backs them all) rendering
// this content id: build on a cache miss, then hand out Arc
// clones. Two paper viewports showing the same model, or four
// Model tiles, upload the wire vertices once between them.
// `.cloned()` releases the immutable cache borrow before the
// miss branch takes a mutable one.
let cached = pipeline.wire_buffer_cache.get(&vp.wire_content_id).cloned();
let built = match cached {
Some(entry) => entry,
None => {
let entry =
inner.build_wire_buffers(device, &vp.wires[..], &vp.draw_depths);
pipeline
.wire_buffer_cache
.insert(vp.wire_content_id, entry.clone());
// Evict entries no slot still holds (only the cache
// references them). An entry drawn by any pane keeps a
// strong count ≥ 2, so this never drops live geometry.
if pipeline.wire_buffer_cache.len() > 16 {
pipeline
.wire_buffer_cache
.retain(|_, (w, _)| std::sync::Arc::strong_count(w) > 1);
}
entry
}
};
inner.gpu_wires = built.0;
inner.wire_handle_index = built.1;
inner.cached_wire_id = vp.wire_content_id;
}
// Selection xray overlay — rebuilt when the selection changes or the