forked from 1b-agent/opencadstudio-upstream
perf: store block-wire segments once instead of six times
The layout-switch OOM names its buffer: `Buffer with 'block_wire.vertices' label is invalid`, right after `uncaptured wgpu error #1 … Out of Memory`. That buffer is the largest thing this renderer allocates, and five sixths of it is duplication. A segment reaches the shader once per corner of its quad, and the packed path satisfies that by writing the whole 60-byte record six times — 360 bytes a segment — with `vs_main` recovering the corner from `vertex_index % 6`. Six identical copies, on a 2 GB card. `block_wire_storage.wgsl` puts the segments in a read-only storage buffer indexed by `vertex_index / 6` instead, so each record is stored once. The geometry vertex buffer disappears; the placements move to slot 0 and the segments arrive through the definition's bind group, which becomes per-chunk because each chunk binds its own slice. This is the pattern `wire_indexed.wgsl` already uses for per-wire constants, down to the capability gate: `supports_wire_storage` asks for exactly one vertex-stage storage binding, which is exactly what this needs. Where it is false — WebGL, the forced compatibility renderer — the packed shader and its vertex buffer are built as before. `Segment` is declared with scalar members rather than `vec3<f32>`, which would align to 16 in storage and reintroduce the padding this removes. A test validates the shader through naga and asserts the struct's stride equals `size_of::<BlockWireVertex>()`, because a layout mismatch would otherwise surface only as garbage geometry on a real device. `block-wire-geometry` reports the bytes uploaded and what the packed path would have taken, so the saving is measured rather than assumed. The cache value becomes `Arc<Vec<BlockGeometryChunk>>` while keeping the translation-aware key from #1006 — both modes still have to invalidate when the base instance moves, because the records hold its world coordinates either way. `const_bgl` and the mode are decided together when the pipeline is built, so they travel as one `BlockWireTarget` rather than pushing `from_wires` past the argument limit. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
0e4f755e6f
commit
6599b82776
4 files changed
+634
-107
No files matched your search
@@ -30,19 +30,20 @@ fn block_edits_preserve_cache_coordinates_and_arena_partition() {
|
||||
let depth = rustc_hash::FxHashMap::from_iter([(1, [0.1, 0.01]), (2, [0.2, 0.01])]);
|
||||
let original = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth);
|
||||
let unchanged = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth);
|
||||
assert_eq!(original[0].vertex_buffer, unchanged[0].vertex_buffer);
|
||||
assert_eq!(original[0].geometry_id(), unchanged[0].geometry_id());
|
||||
assert_eq!(original[0].instance_count, 2);
|
||||
|
||||
let removed = pipeline.upload_block_wires(&device, &queue, &[&second], &depth);
|
||||
assert_ne!(
|
||||
original[0].vertex_buffer, removed[0].vertex_buffer,
|
||||
original[0].geometry_id(),
|
||||
removed[0].geometry_id(),
|
||||
"removing the base instance must replace vertices baked at its old position"
|
||||
);
|
||||
let moved = block(2, 30.0);
|
||||
let moved_gpu = pipeline.upload_block_wires(&device, &queue, &[&moved], &depth);
|
||||
assert_ne!(removed[0].vertex_buffer, moved_gpu[0].vertex_buffer);
|
||||
assert_ne!(removed[0].geometry_id(), moved_gpu[0].geometry_id());
|
||||
let restored = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth);
|
||||
assert_ne!(moved_gpu[0].vertex_buffer, restored[0].vertex_buffer);
|
||||
assert_ne!(moved_gpu[0].geometry_id(), restored[0].geometry_id());
|
||||
assert_eq!(restored[0].instance_count, 2);
|
||||
assert_eq!(
|
||||
pipeline.block_geometry.len(),
|
||||
|
||||
+62
-20
@@ -94,6 +94,10 @@ pub struct Pipeline {
|
||||
/// compatibility mode. Passed to `WireGpu::from_run` / `from_batch`.
|
||||
pub(crate) wire_const_bgl: Option<wgpu::BindGroupLayout>,
|
||||
block_wire_const_bgl: wgpu::BindGroupLayout,
|
||||
/// Which block-wire pipeline was built. Decides whether a definition's
|
||||
/// segments go to a vertex buffer six times over or to a storage buffer
|
||||
/// once.
|
||||
block_wire_mode: wire_gpu::WirePipelineMode,
|
||||
wipeout_pipeline: wgpu::RenderPipeline,
|
||||
/// Capability-selected hatch renderer. Storage and texture transports are
|
||||
/// private backends behind one upload/LOD/draw lifecycle.
|
||||
@@ -581,7 +585,8 @@ impl Pipeline {
|
||||
bind_group_layouts: &wire_bgls,
|
||||
immediate_size: 0,
|
||||
});
|
||||
let block_wire_const_bgl = wire_gpu::block_const_bind_group_layout(device);
|
||||
let block_wire_const_bgl =
|
||||
wire_gpu::block_const_bind_group_layout(device, wire_mode.uses_storage());
|
||||
let block_wire_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("block_wire.pipeline_layout"),
|
||||
bind_group_layouts: &[Some(&frame_bgl), Some(&block_wire_const_bgl)],
|
||||
@@ -602,9 +607,13 @@ impl Pipeline {
|
||||
});
|
||||
let block_wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("block_wire.shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
|
||||
"../../shaders/block_wire.wgsl"
|
||||
))),
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
|
||||
if wire_mode.uses_storage() {
|
||||
include_str!("../../shaders/block_wire_storage.wgsl")
|
||||
} else {
|
||||
include_str!("../../shaders/block_wire.wgsl")
|
||||
},
|
||||
)),
|
||||
});
|
||||
|
||||
// Stencil test shared by every paper content pipeline: draw only where
|
||||
@@ -880,6 +889,14 @@ impl Pipeline {
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let block_wire_buffers: &[wgpu::VertexBufferLayout<'_>] = if wire_mode.uses_storage() {
|
||||
&[wire_gpu::BlockWireInstance::layout()]
|
||||
} else {
|
||||
&[
|
||||
wire_gpu::BlockWireVertex::layout(),
|
||||
wire_gpu::BlockWireInstance::layout(),
|
||||
]
|
||||
};
|
||||
let make_block_wire_pipeline = |
|
||||
label: &'static str,
|
||||
fragment: &'static str,
|
||||
@@ -892,10 +909,10 @@ impl Pipeline {
|
||||
vertex: wgpu::VertexState {
|
||||
module: &block_wire_shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[
|
||||
wire_gpu::BlockWireVertex::layout(),
|
||||
wire_gpu::BlockWireInstance::layout(),
|
||||
],
|
||||
// Storage mode has no geometry vertex buffer: the
|
||||
// instances become slot 0 and the segments arrive through
|
||||
// the bind group.
|
||||
buffers: block_wire_buffers,
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
@@ -2215,6 +2232,7 @@ impl Pipeline {
|
||||
block_wire_xray_pipeline,
|
||||
wire_const_bgl,
|
||||
block_wire_const_bgl,
|
||||
block_wire_mode: wire_mode,
|
||||
wipeout_pipeline,
|
||||
hatch_gpu,
|
||||
image_pipeline,
|
||||
@@ -2349,7 +2367,10 @@ impl Pipeline {
|
||||
wires,
|
||||
depth_map,
|
||||
None,
|
||||
&self.block_wire_const_bgl,
|
||||
wire_gpu::BlockWireTarget {
|
||||
const_bgl: &self.block_wire_const_bgl,
|
||||
mode: self.block_wire_mode,
|
||||
},
|
||||
Some(&mut self.block_geometry),
|
||||
)
|
||||
}
|
||||
@@ -2415,7 +2436,10 @@ impl Pipeline {
|
||||
&block_wires,
|
||||
depth_map,
|
||||
None,
|
||||
&self.block_wire_const_bgl,
|
||||
wire_gpu::BlockWireTarget {
|
||||
const_bgl: &self.block_wire_const_bgl,
|
||||
mode: self.block_wire_mode,
|
||||
},
|
||||
Some(&mut self.block_geometry),
|
||||
);
|
||||
|
||||
@@ -2554,7 +2578,10 @@ impl Pipeline {
|
||||
&selected_blocks,
|
||||
depth_map,
|
||||
Some(tint),
|
||||
&self.block_wire_const_bgl,
|
||||
wire_gpu::BlockWireTarget {
|
||||
const_bgl: &self.block_wire_const_bgl,
|
||||
mode: self.block_wire_mode,
|
||||
},
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
@@ -2566,7 +2593,10 @@ impl Pipeline {
|
||||
&hover_blocks,
|
||||
depth_map,
|
||||
Some(WireModel::HOVER),
|
||||
&self.block_wire_const_bgl,
|
||||
wire_gpu::BlockWireTarget {
|
||||
const_bgl: &self.block_wire_const_bgl,
|
||||
mode: self.block_wire_mode,
|
||||
},
|
||||
None,
|
||||
));
|
||||
self.gpu_selected_block_wires = block_gpu;
|
||||
@@ -4288,10 +4318,7 @@ impl Pipeline {
|
||||
});
|
||||
block_black_active = use_black;
|
||||
}
|
||||
pass.set_bind_group(1, wire.const_bind_group.as_ref(), &[]);
|
||||
pass.set_vertex_buffer(0, wire.vertex_buffer.slice(..));
|
||||
pass.set_vertex_buffer(1, wire.instance_buffer.slice(..));
|
||||
pass.draw(0..wire.vertex_count, 0..wire.instance_count);
|
||||
bind_and_draw_block_wire(&mut pass, wire);
|
||||
}
|
||||
// Live overlay wires (command preview / interim / grip drag) always
|
||||
// on top: the xray pipeline (depth_compare=Always, no depth write)
|
||||
@@ -4480,10 +4507,7 @@ impl Pipeline {
|
||||
if wire.instance_count == 0 {
|
||||
continue;
|
||||
}
|
||||
pass.set_bind_group(1, wire.const_bind_group.as_ref(), &[]);
|
||||
pass.set_vertex_buffer(0, wire.vertex_buffer.slice(..));
|
||||
pass.set_vertex_buffer(1, wire.instance_buffer.slice(..));
|
||||
pass.draw(0..wire.vertex_count, 0..wire.instance_count);
|
||||
bind_and_draw_block_wire(&mut pass, wire);
|
||||
}
|
||||
}
|
||||
if let Some(atlas) = &self.text_atlas_gpu {
|
||||
@@ -4705,6 +4729,24 @@ fn aabb_below_pixel(
|
||||
(max_px - min_px).max(max_py - min_py) < threshold_px
|
||||
}
|
||||
|
||||
/// Bind one block-wire batch and draw it.
|
||||
///
|
||||
/// The two pipeline modes lay the vertex slots out differently — packed puts
|
||||
/// 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.
|
||||
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 {
|
||||
Some(vertices) => {
|
||||
pass.set_vertex_buffer(0, vertices.slice(..));
|
||||
pass.set_vertex_buffer(1, wire.instance_buffer.slice(..));
|
||||
}
|
||||
None => pass.set_vertex_buffer(0, wire.instance_buffer.slice(..)),
|
||||
}
|
||||
pass.draw(0..wire.vertex_count, 0..wire.instance_count);
|
||||
}
|
||||
|
||||
fn create_depth_texture(device: &wgpu::Device, size: Size<u32>) -> wgpu::Texture {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("viewer.depth_texture"),
|
||||
|
||||
+244
-83
@@ -334,7 +334,9 @@ impl BlockWireInstance {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BlockWireGpu {
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
/// Packed mode only; `None` in storage mode, where the segments reach the
|
||||
/// shader through `const_bind_group` instead.
|
||||
pub vertex_buffer: Option<wgpu::Buffer>,
|
||||
pub instance_buffer: wgpu::Buffer,
|
||||
pub vertex_count: u32,
|
||||
pub instance_count: u32,
|
||||
@@ -342,19 +344,38 @@ pub struct BlockWireGpu {
|
||||
pub const_bind_group: std::sync::Arc<wgpu::BindGroup>,
|
||||
}
|
||||
|
||||
pub fn block_const_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("block_wire_const.bgl"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
/// Binding 0 is the definition's constants. In storage mode binding 1 carries
|
||||
/// that chunk's segments, which is why the bind group is per-chunk there and
|
||||
/// shared across chunks in packed mode.
|
||||
pub fn block_const_bind_group_layout(
|
||||
device: &wgpu::Device,
|
||||
uses_storage: bool,
|
||||
) -> wgpu::BindGroupLayout {
|
||||
let mut entries = vec![wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}];
|
||||
if uses_storage {
|
||||
entries.push(wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
}
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("block_wire_const.bgl"),
|
||||
entries: &entries,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -669,23 +690,64 @@ pub(crate) fn wire_draw_depth(
|
||||
/// Vertices use the first instance's world coordinates, so changing that base
|
||||
/// must invalidate the geometry as well as rebuild the relative placements.
|
||||
pub type BlockGeometryCache = rustc_hash::FxHashMap<
|
||||
(u64, bool, [u32; 4], [u64; 3]),
|
||||
(
|
||||
std::sync::Arc<Vec<(wgpu::Buffer, u32)>>,
|
||||
std::sync::Arc<wgpu::BindGroup>,
|
||||
),
|
||||
BlockGeometryKey,
|
||||
std::sync::Arc<Vec<BlockGeometryChunk>>,
|
||||
>;
|
||||
|
||||
/// 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
|
||||
/// they travel as one argument.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct BlockWireTarget<'a> {
|
||||
pub const_bgl: &'a wgpu::BindGroupLayout,
|
||||
pub mode: WirePipelineMode,
|
||||
}
|
||||
|
||||
/// `(source_id, mesh_edge, colour bits, base translation bits)`.
|
||||
pub type BlockGeometryKey = (u64, bool, [u32; 4], [u64; 3]);
|
||||
|
||||
/// One drawable slice of a block definition's geometry.
|
||||
///
|
||||
/// The two pipeline modes put the segments in different places, so the chunk
|
||||
/// carries whichever the mode produced:
|
||||
///
|
||||
/// * packed — `vertex_buffer` holds six copies of each segment, one per corner
|
||||
/// of its quad, and `bind_group` is the definition's constants alone;
|
||||
/// * storage — the segments are in a read-only storage buffer inside
|
||||
/// `bind_group`, one copy each, and `vertex_buffer` is `None`.
|
||||
///
|
||||
/// `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>,
|
||||
}
|
||||
|
||||
impl BlockWireGpu {
|
||||
/// Identity of the geometry this batch draws, comparable across pipeline
|
||||
/// modes.
|
||||
///
|
||||
/// Packed mode keeps the segments in `vertex_buffer`; storage mode keeps
|
||||
/// them inside `const_bind_group`, which is per-chunk there, and leaves
|
||||
/// `vertex_buffer` `None`. Both are built on the same cache miss and
|
||||
/// reused together on a hit, so the pair answers "is this the same
|
||||
/// geometry as before?" in either mode — which `vertex_buffer` alone
|
||||
/// cannot once it is always `None`.
|
||||
pub fn geometry_id(&self) -> (Option<&wgpu::Buffer>, &wgpu::BindGroup) {
|
||||
(self.vertex_buffer.as_ref(), self.const_bind_group.as_ref())
|
||||
}
|
||||
|
||||
pub fn from_wires(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[&WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
color_override: Option<[f32; 4]>,
|
||||
const_bgl: &wgpu::BindGroupLayout,
|
||||
target: BlockWireTarget<'_>,
|
||||
mut cache: Option<&mut BlockGeometryCache>,
|
||||
) -> Vec<Self> {
|
||||
let BlockWireTarget { const_bgl, mode } = target;
|
||||
let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> =
|
||||
rustc_hash::FxHashMap::default();
|
||||
let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new();
|
||||
@@ -710,7 +772,12 @@ impl BlockWireGpu {
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
let mut live_keys = rustc_hash::FxHashSet::default();
|
||||
let mut live_keys: rustc_hash::FxHashSet<BlockGeometryKey> =
|
||||
rustc_hash::FxHashSet::default();
|
||||
// Geometry bytes actually sent to the device this call, so the storage
|
||||
// path's saving is visible rather than assumed.
|
||||
let mut uploaded_bytes = 0usize;
|
||||
let mut uploaded_segments = 0usize;
|
||||
for (mesh_edge, group) in groups {
|
||||
let Some(&source) = group.first() else {
|
||||
continue;
|
||||
@@ -727,21 +794,13 @@ impl BlockWireGpu {
|
||||
);
|
||||
live_keys.insert(cache_key);
|
||||
// Reuse this definition's geometry when it is already on the GPU.
|
||||
if let Some((chunks, bind_group)) = cache
|
||||
if let Some(chunks) = cache
|
||||
.as_deref()
|
||||
.and_then(|c| c.get(&cache_key))
|
||||
.map(|(v, b)| (std::sync::Arc::clone(v), std::sync::Arc::clone(b)))
|
||||
.map(std::sync::Arc::clone)
|
||||
{
|
||||
let instances = block_instances(&group, base, mesh_edge, depth_map);
|
||||
push_block_batches(
|
||||
&mut out,
|
||||
device,
|
||||
queue,
|
||||
&chunks,
|
||||
&instances,
|
||||
mesh_edge,
|
||||
&bind_group,
|
||||
);
|
||||
push_block_batches(&mut out, device, queue, &chunks, &instances, mesh_edge);
|
||||
continue;
|
||||
}
|
||||
let (segments, mut constant) = emit_wire_native(source, 0, color, 0.0);
|
||||
@@ -749,34 +808,15 @@ impl BlockWireGpu {
|
||||
continue;
|
||||
}
|
||||
constant.draw_depth = 0.0;
|
||||
let mut vertices = Vec::with_capacity(segments.len() * 6);
|
||||
for segment in segments {
|
||||
let vertex = BlockWireVertex {
|
||||
let records: Vec<BlockWireVertex> = segments
|
||||
.iter()
|
||||
.map(|segment| BlockWireVertex {
|
||||
pos_a: segment.pos_a,
|
||||
pos_a_low: segment.pos_a_low,
|
||||
pos_b: segment.pos_b,
|
||||
pos_b_low: segment.pos_b_low,
|
||||
distances: [segment.distance_a, segment.distance_b],
|
||||
taper_ratio: segment.taper_ratio,
|
||||
};
|
||||
vertices.extend_from_slice(&[vertex; 6]);
|
||||
}
|
||||
// Bound uploads without splitting a segment's six vertices.
|
||||
let max_verts =
|
||||
super::gpu_budget::max_elements_grouped::<BlockWireVertex>(device, 6);
|
||||
let vertex_chunks: Vec<(wgpu::Buffer, u32)> = vertices
|
||||
.chunks(max_verts)
|
||||
.map(|chunk| {
|
||||
(
|
||||
super::gpu_upload::upload_buffer(
|
||||
device,
|
||||
queue,
|
||||
"block_wire.vertices",
|
||||
chunk,
|
||||
wgpu::BufferUsages::VERTEX,
|
||||
),
|
||||
chunk.len() as u32,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
// Queue uploads avoid mapping a failed allocation.
|
||||
@@ -787,41 +827,106 @@ impl BlockWireGpu {
|
||||
std::slice::from_ref(&constant),
|
||||
wgpu::BufferUsages::UNIFORM,
|
||||
);
|
||||
let const_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(),
|
||||
}],
|
||||
},
|
||||
));
|
||||
let vertex_chunks = std::sync::Arc::new(vertex_chunks);
|
||||
// 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,
|
||||
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 {
|
||||
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);
|
||||
if let Some(cache) = cache.as_deref_mut() {
|
||||
cache.insert(
|
||||
cache_key,
|
||||
(
|
||||
std::sync::Arc::clone(&vertex_chunks),
|
||||
std::sync::Arc::clone(&const_bind_group),
|
||||
),
|
||||
);
|
||||
cache.insert(cache_key, std::sync::Arc::clone(&chunks));
|
||||
}
|
||||
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,
|
||||
);
|
||||
push_block_batches(&mut out, device, queue, &chunks, &instances, mesh_edge);
|
||||
}
|
||||
// Release geometry that this build no longer uses.
|
||||
if let Some(cache) = cache {
|
||||
cache.retain(|key, _| live_keys.contains(key));
|
||||
}
|
||||
if crate::perf::enabled() && uploaded_segments > 0 {
|
||||
crate::perf_record!(
|
||||
"[perf] block-wire-geometry mode={} definitions={} segments={} bytes={} \
|
||||
bytes_if_packed={}",
|
||||
if mode.uses_storage() { "storage" } else { "packed" },
|
||||
live_keys.len(),
|
||||
uploaded_segments,
|
||||
uploaded_bytes,
|
||||
uploaded_segments * std::mem::size_of::<BlockWireVertex>() * 6,
|
||||
);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -863,16 +968,15 @@ fn block_instances(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// One batch per (vertex chunk × instance chunk). Instance buffers are built
|
||||
/// once and shared across vertex chunks rather than re-uploaded per chunk.
|
||||
/// 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,
|
||||
vertex_chunks: &[(wgpu::Buffer, u32)],
|
||||
chunks: &[BlockGeometryChunk],
|
||||
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
|
||||
@@ -884,15 +988,15 @@ fn push_block_batches(
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for (vertex_buffer, vertex_count) in vertex_chunks {
|
||||
for chunk in chunks {
|
||||
for (instance_buffer, instance_count) in &instance_chunks {
|
||||
out.push(BlockWireGpu {
|
||||
vertex_buffer: vertex_buffer.clone(),
|
||||
vertex_buffer: chunk.vertex_buffer.clone(),
|
||||
instance_buffer: instance_buffer.clone(),
|
||||
vertex_count: *vertex_count,
|
||||
vertex_count: chunk.vertex_count,
|
||||
instance_count: *instance_count,
|
||||
is_3d_mesh_edge: mesh_edge,
|
||||
const_bind_group: const_bind_group.clone(),
|
||||
const_bind_group: std::sync::Arc::clone(&chunk.bind_group),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1271,3 +1375,60 @@ impl WireGpu {
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod block_wire_storage_tests {
|
||||
use super::BlockWireVertex;
|
||||
|
||||
fn module(source: &str) -> naga::Module {
|
||||
naga::front::wgsl::parse_str(source).expect("shader parses")
|
||||
}
|
||||
|
||||
fn validate(module: &naga::Module) -> naga::valid::ModuleInfo {
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
)
|
||||
.validate(module)
|
||||
.expect("shader validates")
|
||||
}
|
||||
|
||||
/// A layout mismatch here is invisible until a block draws as garbage on a
|
||||
/// real device, so the stride is checked against the Rust record instead.
|
||||
#[test]
|
||||
fn segment_struct_matches_the_rust_record() {
|
||||
let module = module(include_str!("../../shaders/block_wire_storage.wgsl"));
|
||||
validate(&module);
|
||||
let mut layouter = naga::proc::Layouter::default();
|
||||
layouter
|
||||
.update(module.to_ctx())
|
||||
.expect("layouts are computable");
|
||||
let (handle, _) = module
|
||||
.types
|
||||
.iter()
|
||||
.find(|(_, ty)| ty.name.as_deref() == Some("Segment"))
|
||||
.expect("Segment is declared");
|
||||
assert_eq!(
|
||||
layouter[handle].size as usize,
|
||||
std::mem::size_of::<BlockWireVertex>(),
|
||||
"WGSL Segment and Rust BlockWireVertex must agree byte for byte; \
|
||||
a vec3 member would pad to 16 and break this",
|
||||
);
|
||||
assert_eq!(layouter[handle].alignment * 1u32, 4);
|
||||
}
|
||||
|
||||
/// The two shaders must stay interchangeable: same entry points, same
|
||||
/// fragment behaviour. Only the geometry source differs.
|
||||
#[test]
|
||||
fn both_block_wire_shaders_expose_the_same_entry_points() {
|
||||
let packed = module(include_str!("../../shaders/block_wire.wgsl"));
|
||||
let storage = module(include_str!("../../shaders/block_wire_storage.wgsl"));
|
||||
validate(&packed);
|
||||
let names = |m: &naga::Module| {
|
||||
let mut n: Vec<String> = m.entry_points.iter().map(|e| e.name.clone()).collect();
|
||||
n.sort();
|
||||
n
|
||||
};
|
||||
assert_eq!(names(&packed), names(&storage));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user