Block wires: pack definitions into shared buffers

A drawing whose plan content is built from blocks reaches thousands of
definitions, and each one took a buffer of its own for its segments and
another for its constants, plus one per placement run. The payloads are a
few hundred bytes, but every allocation costs a page: 14k definitions ask a
device for 1.8 GB to hold 12 MB of geometry. D3D12 runs out, the failed
allocations stay invalid, every later frame writes to them, and the session
dies in the staging belt.

The definitions of a build now share one arena: constants in a single
uniform buffer addressed by dynamic offset, segments in a single geometry
buffer addressed by first_vertex, placements in a single instance buffer
addressed by first_instance, with another arena started whenever one of the
three would outgrow the per-buffer budget. The cache holds the records
rather than the buffers, so a build the device rejected poisons nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5 committed 2026-10-09 11:19:33 +03:00
1 parent aa0b87108c
commit 698ec122ba
2 files changed
+375 -236

No files matched your search

+28 -12
View File
@@ -5553,19 +5553,19 @@ impl MultiPipeline {
|| std::sync::Arc::strong_count(&entry.1) > 1
|| std::sync::Arc::strong_count(&entry.3) > 1
});
self.block_geometry.retain(|_, chunks| {
chunks.iter().any(|chunk| std::sync::Arc::strong_count(&chunk.bind_group) > 1)
});
// Block records are host memory a released slot no longer needs;
// the next build re-emits what it still draws.
self.block_geometry.clear();
}
released
}
/// Sum across slots, plus the caches held once for all of them.
pub(crate) fn gpu_live_bytes(&self) -> GpuLiveBytes {
let mut total = GpuLiveBytes {
block_geometry: wire_gpu::block_geometry_bytes(&self.block_geometry),
..Default::default()
};
let mut total = GpuLiveBytes::default();
// Block arenas belong to the batches, and a cached build hands the same
// arenas to several slots, so each one is counted once.
let mut counted = rustc_hash::FxHashSet::default();
for inner in &self.inners {
let slot = inner.gpu_live_bytes();
total.slots += slot.slots;
@@ -5573,6 +5573,15 @@ impl MultiPipeline {
total.render_targets += slot.render_targets;
total.text_atlas += slot.text_atlas;
total.wire_arena += slot.wire_arena;
total.block_geometry +=
wire_gpu::block_arena_bytes(&inner.gpu_block_wires, &mut counted)
+ wire_gpu::block_arena_bytes(
&inner.gpu_selected_block_wires,
&mut counted,
);
}
for entry in self.wire_buffer_cache.values() {
total.block_geometry += wire_gpu::block_arena_bytes(&entry.1, &mut counted);
}
total
}
@@ -5584,16 +5593,23 @@ impl MultiPipeline {
/// the six-per-segment geometry in slot 0 and the placements in slot 1, while
/// storage has no geometry vertex buffer at all and the placements become slot
/// 0. Both draw loops go through here so they cannot drift apart.
/// Many definitions share one arena, so the batch addresses its own slice of
/// it: a dynamic offset for its constants, `first_vertex` for its segments and
/// `first_instance` for its placements.
fn bind_and_draw_block_wire(pass: &mut wgpu::RenderPass<'_>, wire: &wire_gpu::BlockWireGpu) {
pass.set_bind_group(1, wire.const_bind_group.as_ref(), &[]);
match &wire.vertex_buffer {
let arena = wire.arena.as_ref();
pass.set_bind_group(1, arena.bind_group.as_ref(), &[wire.const_offset]);
match &arena.vertex_buffer {
Some(vertices) => {
pass.set_vertex_buffer(0, vertices.slice(..));
pass.set_vertex_buffer(1, wire.instance_buffer.slice(..));
pass.set_vertex_buffer(1, arena.instance_buffer.slice(..));
}
None => pass.set_vertex_buffer(0, wire.instance_buffer.slice(..)),
None => pass.set_vertex_buffer(0, arena.instance_buffer.slice(..)),
}
pass.draw(0..wire.vertex_count, 0..wire.instance_count);
pass.draw(
wire.first_vertex..wire.first_vertex + wire.vertex_count,
wire.first_instance..wire.first_instance + wire.instance_count,
);
}
/// A square `Depth32Float` render target for the shadow pass. `side` is 1 for