forked from 1b-agent/opencadstudio-upstream
Merge pull request #1680 from ramox81/codex/block-wire-buffer-packing
Block wires: pack definitions into shared buffers # Conflicts: # src/scene/pipeline/mod.rs # src/scene/pipeline/wire_gpu.rs
This commit is contained in:
commit
df1f9bd0e7
2 files changed
+375
-239
No files matched your search
+28
-14
@@ -5596,21 +5596,19 @@ impl MultiPipeline {
|
|||||||
|| std::sync::Arc::strong_count(&entry.1) > 1
|
|| std::sync::Arc::strong_count(&entry.1) > 1
|
||||||
|| std::sync::Arc::strong_count(&entry.3) > 1
|
|| std::sync::Arc::strong_count(&entry.3) > 1
|
||||||
});
|
});
|
||||||
self.block_geometry.retain(|_, chunks| {
|
// Block records are host memory a released slot no longer needs;
|
||||||
chunks
|
// the next build re-emits what it still draws.
|
||||||
.iter()
|
self.block_geometry.clear();
|
||||||
.any(|chunk| std::sync::Arc::strong_count(&chunk.bind_group) > 1)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
released
|
released
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sum across slots, plus the caches held once for all of them.
|
/// Sum across slots, plus the caches held once for all of them.
|
||||||
pub(crate) fn gpu_live_bytes(&self) -> GpuLiveBytes {
|
pub(crate) fn gpu_live_bytes(&self) -> GpuLiveBytes {
|
||||||
let mut total = GpuLiveBytes {
|
let mut total = GpuLiveBytes::default();
|
||||||
block_geometry: wire_gpu::block_geometry_bytes(&self.block_geometry),
|
// Block arenas belong to the batches, and a cached build hands the same
|
||||||
..Default::default()
|
// arenas to several slots, so each one is counted once.
|
||||||
};
|
let mut counted = rustc_hash::FxHashSet::default();
|
||||||
for inner in &self.inners {
|
for inner in &self.inners {
|
||||||
let slot = inner.gpu_live_bytes();
|
let slot = inner.gpu_live_bytes();
|
||||||
total.slots += slot.slots;
|
total.slots += slot.slots;
|
||||||
@@ -5618,6 +5616,15 @@ impl MultiPipeline {
|
|||||||
total.render_targets += slot.render_targets;
|
total.render_targets += slot.render_targets;
|
||||||
total.text_atlas += slot.text_atlas;
|
total.text_atlas += slot.text_atlas;
|
||||||
total.wire_arena += slot.wire_arena;
|
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
|
total
|
||||||
}
|
}
|
||||||
@@ -5629,16 +5636,23 @@ impl MultiPipeline {
|
|||||||
/// the six-per-segment geometry in slot 0 and the placements in slot 1, while
|
/// 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
|
/// 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.
|
/// 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) {
|
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(), &[]);
|
let arena = wire.arena.as_ref();
|
||||||
match &wire.vertex_buffer {
|
pass.set_bind_group(1, arena.bind_group.as_ref(), &[wire.const_offset]);
|
||||||
|
match &arena.vertex_buffer {
|
||||||
Some(vertices) => {
|
Some(vertices) => {
|
||||||
pass.set_vertex_buffer(0, vertices.slice(..));
|
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
|
/// A square `Depth32Float` render target for the shadow pass. `side` is 1 for
|
||||||
|
|||||||
+347
-225
@@ -455,21 +455,49 @@ impl BlockWireInstance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
/// One packed upload shared by many block definitions.
|
||||||
pub struct BlockWireGpu {
|
///
|
||||||
|
/// A drawing of block-heavy plan content reaches thousands of definitions, and
|
||||||
|
/// a buffer per definition is what runs a device out of memory: the payloads
|
||||||
|
/// are a few hundred bytes each, but every allocation costs a page of its own —
|
||||||
|
/// on D3D12 enough of them to exhaust a 6 GB card at 12 MB of actual geometry.
|
||||||
|
/// So the definitions share one buffer set: their constants in one uniform
|
||||||
|
/// buffer addressed by dynamic offset, their segments in one geometry buffer
|
||||||
|
/// addressed by `first_vertex`, and their placements in one instance buffer
|
||||||
|
/// addressed by `first_instance`.
|
||||||
|
pub struct BlockArena {
|
||||||
/// Packed mode only; `None` in storage mode, where the segments reach the
|
/// Packed mode only; `None` in storage mode, where the segments reach the
|
||||||
/// shader through `const_bind_group` instead.
|
/// shader through `bind_group` instead.
|
||||||
pub vertex_buffer: Option<wgpu::Buffer>,
|
pub vertex_buffer: Option<wgpu::Buffer>,
|
||||||
pub instance_buffer: wgpu::Buffer,
|
pub instance_buffer: wgpu::Buffer,
|
||||||
pub vertex_count: u32,
|
pub bind_group: std::sync::Arc<wgpu::BindGroup>,
|
||||||
pub instance_count: u32,
|
/// Device bytes of the three buffers, for the live-memory report.
|
||||||
pub is_3d_mesh_edge: bool,
|
pub bytes: u64,
|
||||||
pub const_bind_group: std::sync::Arc<wgpu::BindGroup>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Binding 0 is the definition's constants. In storage mode binding 1 carries
|
#[derive(Clone)]
|
||||||
/// that chunk's segments, which is why the bind group is per-chunk there and
|
pub struct BlockWireGpu {
|
||||||
/// shared across chunks in packed mode.
|
pub arena: std::sync::Arc<BlockArena>,
|
||||||
|
/// First vertex of this definition's slice of the arena. The shader reads
|
||||||
|
/// `vertex_index / 6` as the segment index and `vertex_index % 6` as the
|
||||||
|
/// quad corner, so a slice that starts a whole number of segments in needs
|
||||||
|
/// no shader change — the arithmetic lands on the right record either way.
|
||||||
|
pub first_vertex: u32,
|
||||||
|
pub vertex_count: u32,
|
||||||
|
pub first_instance: u32,
|
||||||
|
pub instance_count: u32,
|
||||||
|
/// Byte offset of this definition's `WireConst` in the arena's uniform
|
||||||
|
/// buffer, passed as the bind group's dynamic offset.
|
||||||
|
pub const_offset: u32,
|
||||||
|
pub is_3d_mesh_edge: bool,
|
||||||
|
/// The records this batch was packed from. The buffers are rebuilt every
|
||||||
|
/// build, so this is what tells a cache hit from a re-emission.
|
||||||
|
pub geometry: std::sync::Arc<BlockGeometryRecords>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Binding 0 is the definition's constants, one dynamic-offset slice of the
|
||||||
|
/// arena's uniform buffer. In storage mode binding 1 carries the arena's
|
||||||
|
/// segments, which every definition in it indexes through `first_vertex`.
|
||||||
pub fn block_const_bind_group_layout(
|
pub fn block_const_bind_group_layout(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
uses_storage: bool,
|
uses_storage: bool,
|
||||||
@@ -479,8 +507,10 @@ pub fn block_const_bind_group_layout(
|
|||||||
visibility: wgpu::ShaderStages::VERTEX,
|
visibility: wgpu::ShaderStages::VERTEX,
|
||||||
ty: wgpu::BindingType::Buffer {
|
ty: wgpu::BindingType::Buffer {
|
||||||
ty: wgpu::BufferBindingType::Uniform,
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
has_dynamic_offset: false,
|
has_dynamic_offset: true,
|
||||||
min_binding_size: None,
|
min_binding_size: std::num::NonZeroU64::new(
|
||||||
|
std::mem::size_of::<WireConst>() as u64,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
}];
|
}];
|
||||||
@@ -1028,8 +1058,13 @@ pub(crate) fn wire_draw_depth(
|
|||||||
/// Cached block geometry keyed by source, edge mode, colour and base translation.
|
/// Cached block geometry keyed by source, edge mode, colour and base translation.
|
||||||
/// Vertices use the first instance's world coordinates, so changing that base
|
/// Vertices use the first instance's world coordinates, so changing that base
|
||||||
/// must invalidate the geometry as well as rebuild the relative placements.
|
/// must invalidate the geometry as well as rebuild the relative placements.
|
||||||
|
///
|
||||||
|
/// The cache holds the records rather than device buffers: the buffers are
|
||||||
|
/// packed arenas shared by every definition of a build, so they cannot be kept
|
||||||
|
/// per definition, and re-packing them is a handful of writes while re-emitting
|
||||||
|
/// the records is the expensive half.
|
||||||
pub type BlockGeometryCache =
|
pub type BlockGeometryCache =
|
||||||
rustc_hash::FxHashMap<BlockGeometryKey, std::sync::Arc<Vec<BlockGeometryChunk>>>;
|
rustc_hash::FxHashMap<BlockGeometryKey, std::sync::Arc<BlockGeometryRecords>>;
|
||||||
|
|
||||||
/// How this device draws block wires. The layout and the mode are decided
|
/// How this device draws block wires. The layout and the mode are decided
|
||||||
/// together when the pipeline is built and are never chosen independently, so
|
/// together when the pipeline is built and are never chosen independently, so
|
||||||
@@ -1040,51 +1075,47 @@ pub struct BlockWireTarget<'a> {
|
|||||||
pub mode: WirePipelineMode,
|
pub mode: WirePipelineMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Device bytes held by a whole block-geometry cache.
|
/// Device bytes the arenas behind these batches hold, counting each arena once
|
||||||
pub fn block_geometry_bytes(cache: &BlockGeometryCache) -> u64 {
|
/// however many batches draw out of it. `seen` carries the arenas already
|
||||||
cache
|
/// counted, so slots that share a cached build are not counted twice.
|
||||||
.values()
|
pub fn block_arena_bytes(
|
||||||
.flat_map(|chunks| chunks.iter())
|
batches: &[BlockWireGpu],
|
||||||
.map(|chunk| chunk.gpu_bytes)
|
seen: &mut rustc_hash::FxHashSet<usize>,
|
||||||
|
) -> u64 {
|
||||||
|
batches
|
||||||
|
.iter()
|
||||||
|
.filter(|batch| seen.insert(std::sync::Arc::as_ptr(&batch.arena) as usize))
|
||||||
|
.map(|batch| batch.arena.bytes)
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `(source_id, mesh_edge, colour bits, base translation bits)`.
|
/// `(source_id, mesh_edge, colour bits, base translation bits)`.
|
||||||
pub type BlockGeometryKey = (u64, bool, [u32; 4], [u64; 3]);
|
pub type BlockGeometryKey = (u64, bool, [u32; 4], [u64; 3]);
|
||||||
|
|
||||||
/// One drawable slice of a block definition's geometry.
|
/// One block definition's geometry on the host: the constants its every
|
||||||
|
/// segment shares, and one record per segment.
|
||||||
///
|
///
|
||||||
/// The two pipeline modes put the segments in different places, so the chunk
|
/// Both pipeline modes pack from this. Packed mode writes six copies of each
|
||||||
/// carries whichever the mode produced:
|
/// record, one per corner of its quad; storage mode writes one and lets the
|
||||||
///
|
/// shader divide `vertex_index` by six. `vertex_count` is six per segment
|
||||||
/// * packed — `vertex_buffer` holds six copies of each segment, one per corner
|
/// either way.
|
||||||
/// of its quad, and `bind_group` is the definition's constants alone;
|
pub struct BlockGeometryRecords {
|
||||||
/// * storage — the segments are in a read-only storage buffer inside
|
pub constant: WireConst,
|
||||||
/// `bind_group`, one copy each, and `vertex_buffer` is `None`.
|
pub records: Vec<BlockWireVertex>,
|
||||||
///
|
|
||||||
/// `vertex_count` is six per segment either way: in storage mode the shader
|
|
||||||
/// divides `vertex_index` by six to find its segment.
|
|
||||||
pub struct BlockGeometryChunk {
|
|
||||||
pub vertex_buffer: Option<wgpu::Buffer>,
|
|
||||||
pub vertex_count: u32,
|
|
||||||
pub bind_group: std::sync::Arc<wgpu::BindGroup>,
|
|
||||||
/// Device bytes this chunk holds. Recorded at build because in storage mode
|
|
||||||
/// the buffer is owned by `bind_group` and cannot be measured from here.
|
|
||||||
pub gpu_bytes: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlockWireGpu {
|
impl BlockWireGpu {
|
||||||
/// Identity of the geometry this batch draws, comparable across pipeline
|
/// Identity of the geometry this batch draws, comparable across pipeline
|
||||||
/// modes.
|
/// modes.
|
||||||
///
|
///
|
||||||
/// Packed mode keeps the segments in `vertex_buffer`; storage mode keeps
|
/// Identity of the geometry this batch draws, comparable across builds.
|
||||||
/// them inside `const_bind_group`, which is per-chunk there, and leaves
|
///
|
||||||
/// `vertex_buffer` `None`. Both are built on the same cache miss and
|
/// The arena buffers are packed fresh every build, so they say nothing
|
||||||
/// reused together on a hit, so the pair answers "is this the same
|
/// about whether the geometry changed. The records behind them do: a cache
|
||||||
/// geometry as before?" in either mode — which `vertex_buffer` alone
|
/// hit hands out the same `Arc`, and anything that invalidates the key —
|
||||||
/// cannot once it is always `None`.
|
/// a new base instance, a colour change — emits new records.
|
||||||
pub fn geometry_id(&self) -> (Option<&wgpu::Buffer>, &wgpu::BindGroup) {
|
pub fn geometry_id(&self) -> *const BlockGeometryRecords {
|
||||||
(self.vertex_buffer.as_ref(), self.const_bind_group.as_ref())
|
std::sync::Arc::as_ptr(&self.geometry)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_wires(
|
pub fn from_wires(
|
||||||
@@ -1119,15 +1150,12 @@ impl BlockWireGpu {
|
|||||||
groups[slot].1.push(wire);
|
groups[slot].1.push(wire);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut out = Vec::new();
|
let mut arena = BlockArenaBuilder::new(device, mode);
|
||||||
let mut live_keys: rustc_hash::FxHashSet<BlockGeometryKey> =
|
let mut live_keys: rustc_hash::FxHashSet<BlockGeometryKey> =
|
||||||
rustc_hash::FxHashSet::default();
|
rustc_hash::FxHashSet::default();
|
||||||
// Geometry bytes actually sent to the device this call, so the storage
|
// Records emitted this call, so a cache that is earning its keep is
|
||||||
// path's saving is visible rather than assumed.
|
// visible rather than assumed.
|
||||||
let mut uploaded_bytes = 0usize;
|
let mut emitted_segments = 0usize;
|
||||||
let mut uploaded_segments = 0usize;
|
|
||||||
// Definitions whose upload the device rejected, so they were not cached.
|
|
||||||
let mut poisoned = 0usize;
|
|
||||||
for (mesh_edge, group) in groups {
|
for (mesh_edge, group) in groups {
|
||||||
let Some(&source) = group.first() else {
|
let Some(&source) = group.first() else {
|
||||||
continue;
|
continue;
|
||||||
@@ -1143,165 +1171,293 @@ impl BlockWireGpu {
|
|||||||
base.translation.map(f64::to_bits),
|
base.translation.map(f64::to_bits),
|
||||||
);
|
);
|
||||||
live_keys.insert(cache_key);
|
live_keys.insert(cache_key);
|
||||||
// Reuse this definition's geometry when it is already on the GPU.
|
// Reuse this definition's records when they are already emitted.
|
||||||
if let Some(chunks) = cache
|
let geometry = match cache
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|c| c.get(&cache_key))
|
.and_then(|c| c.get(&cache_key))
|
||||||
.map(std::sync::Arc::clone)
|
.map(std::sync::Arc::clone)
|
||||||
{
|
{
|
||||||
let instances = block_instances(&group, base, mesh_edge, depth_map);
|
Some(hit) => hit,
|
||||||
push_block_batches(&mut out, device, queue, &chunks, &instances, mesh_edge);
|
None => {
|
||||||
continue;
|
let (segments, mut constant) = emit_wire_native(source, 0, color, 0.0);
|
||||||
}
|
if segments.is_empty() {
|
||||||
// A failed allocation still produces a `Buffer`; it is simply
|
continue;
|
||||||
// invalid, and every later use of it reports again. Cached, it
|
}
|
||||||
// freezes the viewport for the rest of the session because the key
|
constant.draw_depth = 0.0;
|
||||||
// keeps hitting and nothing ever rebuilds. Watch the device's error
|
let records: Vec<BlockWireVertex> = segments
|
||||||
// count across the build and refuse to cache what it condemns.
|
.iter()
|
||||||
let errors_before = super::gpu_errors_seen();
|
.map(|segment| BlockWireVertex {
|
||||||
let (segments, mut constant) = emit_wire_native(source, 0, color, 0.0);
|
pos_a: segment.pos_a,
|
||||||
if segments.is_empty() {
|
pos_a_low: segment.pos_a_low,
|
||||||
continue;
|
pos_b: segment.pos_b,
|
||||||
}
|
pos_b_low: segment.pos_b_low,
|
||||||
constant.draw_depth = 0.0;
|
distances: [segment.distance_a, segment.distance_b],
|
||||||
let records: Vec<BlockWireVertex> = segments
|
taper_ratio: segment.taper_ratio,
|
||||||
.iter()
|
})
|
||||||
.map(|segment| BlockWireVertex {
|
.collect();
|
||||||
pos_a: segment.pos_a,
|
emitted_segments += records.len();
|
||||||
pos_a_low: segment.pos_a_low,
|
let built =
|
||||||
pos_b: segment.pos_b,
|
std::sync::Arc::new(BlockGeometryRecords { constant, records });
|
||||||
pos_b_low: segment.pos_b_low,
|
// Records are device-independent, so a build the device
|
||||||
distances: [segment.distance_a, segment.distance_b],
|
// rejected poisons nothing: the next one packs these same
|
||||||
taper_ratio: segment.taper_ratio,
|
// records into fresh buffers instead of caching a buffer
|
||||||
})
|
// the device condemned under a key that keeps hitting it.
|
||||||
.collect();
|
if let Some(cache) = cache.as_deref_mut() {
|
||||||
// Queue uploads avoid mapping a failed allocation.
|
cache.insert(cache_key, std::sync::Arc::clone(&built));
|
||||||
let const_buffer = super::gpu_upload::upload_buffer(
|
}
|
||||||
device,
|
built
|
||||||
queue,
|
|
||||||
"block_wire.const",
|
|
||||||
std::slice::from_ref(&constant),
|
|
||||||
wgpu::BufferUsages::UNIFORM,
|
|
||||||
);
|
|
||||||
// Bound uploads so a block-heavy drawing cannot ask a low-VRAM
|
|
||||||
// device for one multi-hundred-MB buffer.
|
|
||||||
let chunks: Vec<BlockGeometryChunk> = if mode.uses_storage() {
|
|
||||||
// One record per segment. A storage chunk is bounded by the
|
|
||||||
// binding size as well as the buffer budget, and needs no
|
|
||||||
// grouping: the shader indexes segments directly.
|
|
||||||
let max_records =
|
|
||||||
super::gpu_budget::max_storage_elements::<BlockWireVertex>(device);
|
|
||||||
records
|
|
||||||
.chunks(max_records)
|
|
||||||
.map(|chunk| {
|
|
||||||
let segments = super::gpu_upload::upload_buffer(
|
|
||||||
device,
|
|
||||||
queue,
|
|
||||||
"block_wire.segments",
|
|
||||||
chunk,
|
|
||||||
wgpu::BufferUsages::STORAGE,
|
|
||||||
);
|
|
||||||
BlockGeometryChunk {
|
|
||||||
vertex_buffer: None,
|
|
||||||
vertex_count: chunk.len() as u32 * 6,
|
|
||||||
gpu_bytes: std::mem::size_of_val(chunk) as u64,
|
|
||||||
bind_group: std::sync::Arc::new(device.create_bind_group(
|
|
||||||
&wgpu::BindGroupDescriptor {
|
|
||||||
label: Some("block_wire.const.bg"),
|
|
||||||
layout: const_bgl,
|
|
||||||
entries: &[
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: const_buffer.as_entire_binding(),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: segments.as_entire_binding(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
// Six copies per segment, one per corner of its quad, without
|
|
||||||
// splitting a segment's six vertices across two buffers.
|
|
||||||
let mut vertices = Vec::with_capacity(records.len() * 6);
|
|
||||||
for record in &records {
|
|
||||||
vertices.extend_from_slice(&[*record; 6]);
|
|
||||||
}
|
}
|
||||||
let max_verts =
|
|
||||||
super::gpu_budget::max_elements_grouped::<BlockWireVertex>(device, 6);
|
|
||||||
let shared =
|
|
||||||
std::sync::Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
||||||
label: Some("block_wire.const.bg"),
|
|
||||||
layout: const_bgl,
|
|
||||||
entries: &[wgpu::BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: const_buffer.as_entire_binding(),
|
|
||||||
}],
|
|
||||||
}));
|
|
||||||
vertices
|
|
||||||
.chunks(max_verts)
|
|
||||||
.map(|chunk| BlockGeometryChunk {
|
|
||||||
gpu_bytes: std::mem::size_of_val(chunk) as u64,
|
|
||||||
vertex_buffer: Some(super::gpu_upload::upload_buffer(
|
|
||||||
device,
|
|
||||||
queue,
|
|
||||||
"block_wire.vertices",
|
|
||||||
chunk,
|
|
||||||
wgpu::BufferUsages::VERTEX,
|
|
||||||
)),
|
|
||||||
vertex_count: chunk.len() as u32,
|
|
||||||
bind_group: std::sync::Arc::clone(&shared),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
};
|
||||||
uploaded_segments += records.len();
|
|
||||||
uploaded_bytes += records.len()
|
|
||||||
* std::mem::size_of::<BlockWireVertex>()
|
|
||||||
* if mode.uses_storage() { 1 } else { 6 };
|
|
||||||
let chunks = std::sync::Arc::new(chunks);
|
|
||||||
let built_clean = super::gpu_errors_seen() == errors_before;
|
|
||||||
if built_clean {
|
|
||||||
if let Some(cache) = cache.as_deref_mut() {
|
|
||||||
cache.insert(cache_key, std::sync::Arc::clone(&chunks));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Draw this frame with what we have — a degraded frame beats a
|
|
||||||
// blank one — but leave the cache clean so the next frame tries
|
|
||||||
// again. `live_keys` still holds the key, so the retain below
|
|
||||||
// does not evict a good entry from an earlier frame.
|
|
||||||
poisoned += 1;
|
|
||||||
}
|
|
||||||
let instances = block_instances(&group, base, mesh_edge, depth_map);
|
let instances = block_instances(&group, base, mesh_edge, depth_map);
|
||||||
push_block_batches(&mut out, device, queue, &chunks, &instances, mesh_edge);
|
if instances.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
arena.push(&geometry, &instances, mesh_edge);
|
||||||
}
|
}
|
||||||
// Release geometry that this build no longer uses.
|
// Release geometry that this build no longer uses.
|
||||||
if let Some(cache) = cache {
|
if let Some(cache) = cache {
|
||||||
cache.retain(|key, _| live_keys.contains(key));
|
cache.retain(|key, _| live_keys.contains(key));
|
||||||
}
|
}
|
||||||
if crate::perf::enabled() && uploaded_segments > 0 {
|
let out = arena.finish(device, queue, const_bgl);
|
||||||
|
if crate::perf::enabled() && arena.segments > 0 {
|
||||||
crate::perf_record!(
|
crate::perf_record!(
|
||||||
"[perf] block-wire-geometry mode={} definitions={} segments={} bytes={} \
|
"[perf] block-wire-geometry mode={} definitions={} arenas={} batches={} \
|
||||||
bytes_if_packed={} poisoned={}",
|
segments={} bytes={} bytes_if_per_definition={} emitted={}",
|
||||||
if mode.uses_storage() {
|
if mode.uses_storage() { "storage" } else { "packed" },
|
||||||
"storage"
|
|
||||||
} else {
|
|
||||||
"packed"
|
|
||||||
},
|
|
||||||
live_keys.len(),
|
live_keys.len(),
|
||||||
uploaded_segments,
|
arena.arenas,
|
||||||
uploaded_bytes,
|
out.len(),
|
||||||
uploaded_segments * std::mem::size_of::<BlockWireVertex>() * 6,
|
arena.segments,
|
||||||
poisoned,
|
arena.bytes,
|
||||||
|
BlockArenaBuilder::per_definition_bytes(out.len()),
|
||||||
|
emitted_segments,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Packs the definitions of one build into shared buffers, starting another
|
||||||
|
/// arena whenever constants, geometry or placements would outgrow the
|
||||||
|
/// per-buffer budget.
|
||||||
|
struct BlockArenaBuilder {
|
||||||
|
mode: WirePipelineMode,
|
||||||
|
/// Vertices written per segment: one in storage mode, six in packed mode.
|
||||||
|
per_segment: usize,
|
||||||
|
max_verts: usize,
|
||||||
|
max_consts: usize,
|
||||||
|
max_instances: usize,
|
||||||
|
const_stride: usize,
|
||||||
|
chunks: Vec<BlockArenaChunk>,
|
||||||
|
/// Totals for the perf line.
|
||||||
|
arenas: usize,
|
||||||
|
segments: usize,
|
||||||
|
bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct BlockArenaChunk {
|
||||||
|
verts: Vec<BlockWireVertex>,
|
||||||
|
consts: Vec<WireConst>,
|
||||||
|
instances: Vec<BlockWireInstance>,
|
||||||
|
batches: Vec<PendingBlockBatch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One definition's slice of an arena, waiting for the arena's buffers.
|
||||||
|
struct PendingBlockBatch {
|
||||||
|
first_vertex: u32,
|
||||||
|
vertex_count: u32,
|
||||||
|
first_instance: u32,
|
||||||
|
instance_count: u32,
|
||||||
|
const_offset: u32,
|
||||||
|
is_3d_mesh_edge: bool,
|
||||||
|
geometry: std::sync::Arc<BlockGeometryRecords>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockArenaBuilder {
|
||||||
|
fn new(device: &wgpu::Device, mode: WirePipelineMode) -> Self {
|
||||||
|
let storage = mode.uses_storage();
|
||||||
|
// A uniform binding addressed by dynamic offset must start on the
|
||||||
|
// device's alignment, so the constants are written on that stride.
|
||||||
|
let align = (device.limits().min_uniform_buffer_offset_alignment as usize).max(1);
|
||||||
|
let const_stride = std::mem::size_of::<WireConst>().max(1).div_ceil(align) * align;
|
||||||
|
Self {
|
||||||
|
mode,
|
||||||
|
per_segment: if storage { 1 } else { 6 },
|
||||||
|
max_verts: if storage {
|
||||||
|
super::gpu_budget::max_storage_elements::<BlockWireVertex>(device)
|
||||||
|
} else {
|
||||||
|
super::gpu_budget::max_elements_grouped::<BlockWireVertex>(device, 6)
|
||||||
|
},
|
||||||
|
max_consts: (super::gpu_budget::buffer_budget(device) / const_stride).max(1),
|
||||||
|
max_instances: super::gpu_budget::max_elements::<BlockWireInstance>(device),
|
||||||
|
const_stride,
|
||||||
|
chunks: Vec::new(),
|
||||||
|
arenas: 0,
|
||||||
|
segments: 0,
|
||||||
|
bytes: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append one definition and its placements.
|
||||||
|
fn push(
|
||||||
|
&mut self,
|
||||||
|
geometry: &std::sync::Arc<BlockGeometryRecords>,
|
||||||
|
instances: &[BlockWireInstance],
|
||||||
|
mesh_edge: bool,
|
||||||
|
) {
|
||||||
|
self.segments += geometry.records.len();
|
||||||
|
// A definition's geometry, constants and placements must land in the
|
||||||
|
// same arena — one bind group and one instance buffer draw them — so a
|
||||||
|
// definition too large for one arena is split into pieces that each
|
||||||
|
// carry their own copy of the constants.
|
||||||
|
for run in instances.chunks(self.max_instances) {
|
||||||
|
let mut placed = 0usize;
|
||||||
|
while placed < geometry.records.len() {
|
||||||
|
let index = self.chunk_with_room(run.len());
|
||||||
|
let chunk = &mut self.chunks[index];
|
||||||
|
let room = (self.max_verts - chunk.verts.len()) / self.per_segment;
|
||||||
|
let take = room.min(geometry.records.len() - placed);
|
||||||
|
// Segments already in this arena, times the six vertices the
|
||||||
|
// shader spends on each: storage mode divides `vertex_index` by
|
||||||
|
// six to index the record, packed mode finds the six copies.
|
||||||
|
let first_vertex = (chunk.verts.len() / self.per_segment) as u32 * 6;
|
||||||
|
let records = &geometry.records[placed..placed + take];
|
||||||
|
if self.per_segment == 1 {
|
||||||
|
chunk.verts.extend_from_slice(records);
|
||||||
|
} else {
|
||||||
|
for record in records {
|
||||||
|
chunk.verts.extend_from_slice(&[*record; 6]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let const_offset = (chunk.consts.len() * self.const_stride) as u32;
|
||||||
|
chunk.consts.push(geometry.constant);
|
||||||
|
let first_instance = chunk.instances.len() as u32;
|
||||||
|
chunk.instances.extend_from_slice(run);
|
||||||
|
chunk.batches.push(PendingBlockBatch {
|
||||||
|
first_vertex,
|
||||||
|
vertex_count: take as u32 * 6,
|
||||||
|
first_instance,
|
||||||
|
instance_count: run.len() as u32,
|
||||||
|
const_offset,
|
||||||
|
is_3d_mesh_edge: mesh_edge,
|
||||||
|
geometry: std::sync::Arc::clone(geometry),
|
||||||
|
});
|
||||||
|
placed += take;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The open arena when it has room for one more segment, one more set of
|
||||||
|
/// constants and `instances` placements; a fresh one otherwise. `instances`
|
||||||
|
/// never exceeds one arena's budget, so a fresh arena always has room.
|
||||||
|
fn chunk_with_room(&mut self, instances: usize) -> usize {
|
||||||
|
let fits = self.chunks.last().is_some_and(|chunk| {
|
||||||
|
self.max_verts - chunk.verts.len() >= self.per_segment
|
||||||
|
&& chunk.consts.len() < self.max_consts
|
||||||
|
&& self.max_instances - chunk.instances.len() >= instances
|
||||||
|
});
|
||||||
|
if !fits {
|
||||||
|
self.chunks.push(BlockArenaChunk::default());
|
||||||
|
}
|
||||||
|
self.chunks.len() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload each arena and turn its pending slices into draw batches.
|
||||||
|
fn finish(
|
||||||
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
const_bgl: &wgpu::BindGroupLayout,
|
||||||
|
) -> Vec<BlockWireGpu> {
|
||||||
|
let storage = self.mode.uses_storage();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for chunk in std::mem::take(&mut self.chunks) {
|
||||||
|
let mut const_bytes = vec![0u8; chunk.consts.len() * self.const_stride];
|
||||||
|
for (slot, cst) in chunk.consts.iter().enumerate() {
|
||||||
|
let at = slot * self.const_stride;
|
||||||
|
const_bytes[at..at + std::mem::size_of::<WireConst>()]
|
||||||
|
.copy_from_slice(bytemuck::bytes_of(cst));
|
||||||
|
}
|
||||||
|
let consts = super::gpu_upload::upload_bytes(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
"block_wire.consts",
|
||||||
|
&const_bytes,
|
||||||
|
std::mem::size_of::<WireConst>(),
|
||||||
|
wgpu::BufferUsages::UNIFORM,
|
||||||
|
);
|
||||||
|
let geometry = super::gpu_upload::upload_buffer(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
if storage {
|
||||||
|
"block_wire.segments"
|
||||||
|
} else {
|
||||||
|
"block_wire.vertices"
|
||||||
|
},
|
||||||
|
&chunk.verts,
|
||||||
|
if storage {
|
||||||
|
wgpu::BufferUsages::STORAGE
|
||||||
|
} else {
|
||||||
|
wgpu::BufferUsages::VERTEX
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let instance_buffer =
|
||||||
|
instance_buffer(device, queue, "block_wire.instances", &chunk.instances);
|
||||||
|
let mut entries = vec![wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||||
|
buffer: &consts,
|
||||||
|
offset: 0,
|
||||||
|
size: std::num::NonZeroU64::new(std::mem::size_of::<WireConst>() as u64),
|
||||||
|
}),
|
||||||
|
}];
|
||||||
|
if storage {
|
||||||
|
entries.push(wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: geometry.as_entire_binding(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let bind_group =
|
||||||
|
std::sync::Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("block_wire.const.bg"),
|
||||||
|
layout: const_bgl,
|
||||||
|
entries: &entries,
|
||||||
|
}));
|
||||||
|
let bytes = consts.size() + geometry.size() + instance_buffer.size();
|
||||||
|
self.arenas += 1;
|
||||||
|
self.bytes += bytes;
|
||||||
|
// In storage mode the bind group owns the segments; in packed mode
|
||||||
|
// the draw binds them as vertex buffer 0.
|
||||||
|
let arena = std::sync::Arc::new(BlockArena {
|
||||||
|
vertex_buffer: (!storage).then(|| geometry.clone()),
|
||||||
|
instance_buffer,
|
||||||
|
bind_group,
|
||||||
|
bytes,
|
||||||
|
});
|
||||||
|
out.extend(chunk.batches.into_iter().map(|pending| BlockWireGpu {
|
||||||
|
arena: std::sync::Arc::clone(&arena),
|
||||||
|
first_vertex: pending.first_vertex,
|
||||||
|
vertex_count: pending.vertex_count,
|
||||||
|
first_instance: pending.first_instance,
|
||||||
|
instance_count: pending.instance_count,
|
||||||
|
const_offset: pending.const_offset,
|
||||||
|
is_3d_mesh_edge: pending.is_3d_mesh_edge,
|
||||||
|
geometry: pending.geometry,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a buffer per definition would have cost: payloads of a few hundred
|
||||||
|
/// bytes, but a device page each, which is the footprint that ran a 6 GB
|
||||||
|
/// card out of memory on a drawing holding 12 MB of block geometry.
|
||||||
|
fn per_definition_bytes(definitions: usize) -> u64 {
|
||||||
|
const PAGE: u64 = 64 * 1024;
|
||||||
|
definitions as u64 * 2 * PAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Placements and draw depths relative to the group's first instance.
|
/// Placements and draw depths relative to the group's first instance.
|
||||||
fn block_instances(
|
fn block_instances(
|
||||||
group: &[&WireModel],
|
group: &[&WireModel],
|
||||||
@@ -1339,40 +1495,6 @@ fn block_instances(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One batch per (geometry chunk × instance chunk). Instance buffers are built
|
|
||||||
/// once and shared across geometry chunks rather than re-uploaded per chunk.
|
|
||||||
fn push_block_batches(
|
|
||||||
out: &mut Vec<BlockWireGpu>,
|
|
||||||
device: &wgpu::Device,
|
|
||||||
queue: &wgpu::Queue,
|
|
||||||
chunks: &[BlockGeometryChunk],
|
|
||||||
instances: &[BlockWireInstance],
|
|
||||||
mesh_edge: bool,
|
|
||||||
) {
|
|
||||||
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 chunk in chunks {
|
|
||||||
for (instance_buffer, instance_count) in &instance_chunks {
|
|
||||||
out.push(BlockWireGpu {
|
|
||||||
vertex_buffer: chunk.vertex_buffer.clone(),
|
|
||||||
instance_buffer: instance_buffer.clone(),
|
|
||||||
vertex_count: chunk.vertex_count,
|
|
||||||
instance_count: *instance_count,
|
|
||||||
is_3d_mesh_edge: mesh_edge,
|
|
||||||
const_bind_group: std::sync::Arc::clone(&chunk.bind_group),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upload one constant chunk; its instances use chunk-local wire IDs.
|
/// Upload one constant chunk; its instances use chunk-local wire IDs.
|
||||||
fn build_const_bind_group(
|
fn build_const_bind_group(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
|
|||||||
Reference in new issue
Block a user