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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
// Block-wire vertex shader, storage-buffer variant.
|
||||
//
|
||||
// `block_wire.wgsl` carries the segment endpoints as vertex attributes, and a
|
||||
// segment must reach the shader once per corner of its quad, so every record
|
||||
// is written to VRAM six times: 360 bytes a segment. On a block-heavy drawing
|
||||
// that is the buffer that runs a 2 GB card out of memory — the OOM names
|
||||
// `block_wire.vertices` directly.
|
||||
//
|
||||
// Here the segments live in a read-only storage buffer indexed by
|
||||
// `vertex_index / 6`, so each record is stored once: 60 bytes a segment. The
|
||||
// two shaders are otherwise identical, and must stay so.
|
||||
//
|
||||
// `Segment` is declared with scalar fields rather than `vec3<f32>`, which
|
||||
// would align to 16 bytes in storage and reintroduce padding. All-f32 members
|
||||
// give it align 4 and stride 60, matching `BlockWireVertex` byte for byte.
|
||||
//
|
||||
// Requires storage buffers in the vertex stage; callers select
|
||||
// `block_wire.wgsl` when `DeviceCapabilities::supports_wire_storage` is false.
|
||||
|
||||
struct Uniforms {
|
||||
viewport_size: vec2<f32>,
|
||||
world_per_pixel: f32,
|
||||
lwdisplay_enable: f32,
|
||||
flat_shade: f32,
|
||||
transparency_enable: f32,
|
||||
linetype_scale: f32,
|
||||
lineweight_scale: f32,
|
||||
view_rot: mat4x4<f32>,
|
||||
eye_high: vec3<f32>,
|
||||
_pad_eh: f32,
|
||||
eye_low: vec3<f32>,
|
||||
_pad_el: f32,
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||
|
||||
struct WireConst {
|
||||
color: vec4<f32>,
|
||||
pat0: vec4<f32>,
|
||||
pat1: vec4<f32>,
|
||||
half_width: f32,
|
||||
pattern_length: f32,
|
||||
draw_depth: f32,
|
||||
align_end: f32,
|
||||
align_total: f32,
|
||||
world_half_width: f32,
|
||||
_pad1: f32,
|
||||
_pad2: f32,
|
||||
marker_origin_high: vec4<f32>,
|
||||
marker_origin_low: vec4<f32>,
|
||||
marker_normal_scale: vec4<f32>,
|
||||
}
|
||||
@group(1) @binding(0) var<uniform> wire_const: WireConst;
|
||||
|
||||
struct Segment {
|
||||
pos_a_x: f32, pos_a_y: f32, pos_a_z: f32,
|
||||
pos_a_low_x: f32, pos_a_low_y: f32, pos_a_low_z: f32,
|
||||
pos_b_x: f32, pos_b_y: f32, pos_b_z: f32,
|
||||
pos_b_low_x: f32, pos_b_low_y: f32, pos_b_low_z: f32,
|
||||
distance_a: f32, distance_b: f32,
|
||||
// Two `u16` unorms packed into one word, as `Unorm16x2` delivered them.
|
||||
taper_ratio: u32,
|
||||
}
|
||||
@group(1) @binding(1) var<storage, read> segments: array<Segment>;
|
||||
|
||||
struct VertexIn {
|
||||
@location(6) translation: vec3<f32>,
|
||||
@location(7) translation_low: vec3<f32>,
|
||||
@location(8) depth: vec2<f32>,
|
||||
}
|
||||
|
||||
const DRAW_ORDER_BIAS: f32 = 0.001;
|
||||
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
|
||||
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
|
||||
|
||||
struct VertexOut {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) distance: f32,
|
||||
@location(2) pattern_length: f32,
|
||||
@location(3) pat0: vec4<f32>,
|
||||
@location(4) pat1: vec4<f32>,
|
||||
@location(5) @interpolate(flat) min_elem: f32,
|
||||
@location(6) @interpolate(flat) align_end: f32,
|
||||
@location(7) @interpolate(flat) align_total: f32,
|
||||
@location(8) cap: vec2<f32>,
|
||||
@location(9) @interpolate(flat) cap_ends: vec3<f32>,
|
||||
}
|
||||
|
||||
fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 {
|
||||
if taper_ratio > 0.0 {
|
||||
return max((taper_ratio * world_hw) / u.world_per_pixel, 0.5);
|
||||
}
|
||||
if world_hw > 0.0 {
|
||||
return max(world_hw / u.world_per_pixel, 0.5);
|
||||
}
|
||||
var display_hw = max(px_hw * u.lineweight_scale, 0.5);
|
||||
if u.lineweight_scale < 0.0 {
|
||||
let scale = -u.lineweight_scale;
|
||||
let base_hw = select(
|
||||
min(px_hw * MODEL_LINEWEIGHT_BOOST, MODEL_LINEWEIGHT_MAX_PX * 0.5),
|
||||
0.5,
|
||||
px_hw <= 0.5,
|
||||
);
|
||||
display_hw = max(base_hw * scale, 0.5);
|
||||
}
|
||||
return select(0.5, display_hw, u.lwdisplay_enable > 0.5);
|
||||
}
|
||||
|
||||
fn marker_relative(
|
||||
position_high: vec3<f32>,
|
||||
position_low: vec3<f32>,
|
||||
translation: vec3<f32>,
|
||||
translation_low: vec3<f32>,
|
||||
) -> vec3<f32> {
|
||||
if wire_const.marker_normal_scale.w <= 0.0 {
|
||||
return (position_high + translation - u.eye_high)
|
||||
+ (position_low + translation_low - u.eye_low);
|
||||
}
|
||||
let origin_high = wire_const.marker_origin_high.xyz;
|
||||
let origin_low = wire_const.marker_origin_low.xyz;
|
||||
let origin_relative = (origin_high + translation - u.eye_high)
|
||||
+ (origin_low + translation_low - u.eye_low);
|
||||
let delta = (position_high - origin_high) + (position_low - origin_low);
|
||||
let normal = normalize(wire_const.marker_normal_scale.xyz);
|
||||
let axial = normal * dot(delta, normal);
|
||||
let planar = delta - axial;
|
||||
let origin_clip = u.view_rot * vec4<f32>(origin_relative, 1.0);
|
||||
let projection_scale = max(length(vec3<f32>(
|
||||
u.view_rot[0].y,
|
||||
u.view_rot[1].y,
|
||||
u.view_rot[2].y,
|
||||
)), 1e-12);
|
||||
let view_height = 2.0 * max(abs(origin_clip.w), 1e-6) / projection_scale;
|
||||
let world_size = wire_const.marker_normal_scale.w * 0.01 * view_height;
|
||||
return origin_relative + axial + planar * world_size;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut {
|
||||
let corner = vertex_index % 6u;
|
||||
let seg = segments[vertex_index / 6u];
|
||||
let seg_pos_a = vec3<f32>(seg.pos_a_x, seg.pos_a_y, seg.pos_a_z);
|
||||
let seg_pos_a_low = vec3<f32>(seg.pos_a_low_x, seg.pos_a_low_y, seg.pos_a_low_z);
|
||||
let seg_pos_b = vec3<f32>(seg.pos_b_x, seg.pos_b_y, seg.pos_b_z);
|
||||
let seg_pos_b_low = vec3<f32>(seg.pos_b_low_x, seg.pos_b_low_y, seg.pos_b_low_z);
|
||||
let seg_distances = vec2<f32>(seg.distance_a, seg.distance_b);
|
||||
let seg_taper_ratio = unpack2x16unorm(seg.taper_ratio);
|
||||
let which_end_arr = array<f32, 6>(0.0, 1.0, 1.0, 0.0, 1.0, 0.0);
|
||||
let side_arr = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
|
||||
let which_end = which_end_arr[corner];
|
||||
let side = side_arr[corner];
|
||||
|
||||
let rel_a = marker_relative(seg_pos_a, seg_pos_a_low, in.translation, in.translation_low);
|
||||
let rel_b = marker_relative(seg_pos_b, seg_pos_b_low, in.translation, in.translation_low);
|
||||
let clip_a = u.view_rot * vec4<f32>(rel_a, 1.0);
|
||||
let clip_b = u.view_rot * vec4<f32>(rel_b, 1.0);
|
||||
let screen_a = clip_a.xy / clip_a.w * u.viewport_size * 0.5;
|
||||
let screen_b = clip_b.xy / clip_b.w * u.viewport_size * 0.5;
|
||||
let segment = screen_b - screen_a;
|
||||
let segment_length = length(segment);
|
||||
var direction = vec2<f32>(1.0, 0.0);
|
||||
if segment_length > 1e-4 {
|
||||
direction = segment / segment_length;
|
||||
}
|
||||
let perpendicular = vec2<f32>(-direction.y, direction.x);
|
||||
let clip_position = mix(clip_a, clip_b, which_end);
|
||||
let half_width_a = resolve_hw(
|
||||
seg_taper_ratio.x,
|
||||
wire_const.world_half_width,
|
||||
wire_const.half_width,
|
||||
);
|
||||
let half_width_b = resolve_hw(
|
||||
seg_taper_ratio.y,
|
||||
wire_const.world_half_width,
|
||||
wire_const.half_width,
|
||||
);
|
||||
let half_width = mix(half_width_a, half_width_b, which_end);
|
||||
let extension = which_end * 2.0 - 1.0;
|
||||
let offset_px = perpendicular * half_width * side
|
||||
+ direction * half_width * extension;
|
||||
let ndc_offset = offset_px / (u.viewport_size * 0.5);
|
||||
let final_clip = clip_position
|
||||
+ vec4<f32>(ndc_offset * clip_position.w, 0.0, 0.0);
|
||||
|
||||
let scale = u.linetype_scale;
|
||||
var min_element = wire_const.pattern_length * scale;
|
||||
let elements = array<f32, 8>(
|
||||
wire_const.pat0.x * scale,
|
||||
wire_const.pat0.y * scale,
|
||||
wire_const.pat0.z * scale,
|
||||
wire_const.pat0.w * scale,
|
||||
wire_const.pat1.x * scale,
|
||||
wire_const.pat1.y * scale,
|
||||
wire_const.pat1.z * scale,
|
||||
wire_const.pat1.w * scale,
|
||||
);
|
||||
for (var i = 0u; i < 8u; i++) {
|
||||
let value = abs(elements[i]);
|
||||
if value > 0.0 && value < min_element {
|
||||
min_element = value;
|
||||
}
|
||||
}
|
||||
|
||||
var out: VertexOut;
|
||||
out.clip_pos = final_clip;
|
||||
out.clip_pos.z -= in.depth.x * DRAW_ORDER_BIAS * out.clip_pos.w;
|
||||
out.color = wire_const.color;
|
||||
out.distance = mix(seg_distances.x, seg_distances.y, which_end)
|
||||
+ extension * half_width * u.world_per_pixel;
|
||||
out.pattern_length = wire_const.pattern_length * scale;
|
||||
out.pat0 = wire_const.pat0 * scale;
|
||||
out.pat1 = wire_const.pat1 * scale;
|
||||
out.min_elem = min_element;
|
||||
out.align_end = wire_const.align_end * scale;
|
||||
out.align_total = wire_const.align_total;
|
||||
out.cap = vec2<f32>(
|
||||
which_end * segment_length + extension * half_width,
|
||||
half_width * side,
|
||||
);
|
||||
out.cap_ends = vec3<f32>(segment_length, half_width_a, half_width_b);
|
||||
return out;
|
||||
}
|
||||
|
||||
fn in_dash(
|
||||
distance: f32,
|
||||
pattern_length: f32,
|
||||
pat0: vec4<f32>,
|
||||
pat1: vec4<f32>,
|
||||
align_end: f32,
|
||||
align_total: f32,
|
||||
) -> bool {
|
||||
let elements = array<f32, 8>(
|
||||
pat0.x, pat0.y, pat0.z, pat0.w,
|
||||
pat1.x, pat1.y, pat1.z, pat1.w,
|
||||
);
|
||||
var count = 0u;
|
||||
for (var i = 0u; i < 8u; i++) {
|
||||
if elements[i] != 0.0 {
|
||||
count = i + 1u;
|
||||
}
|
||||
}
|
||||
var value: f32;
|
||||
if align_total > 0.0 {
|
||||
if distance <= align_end || distance >= align_total - align_end {
|
||||
return true;
|
||||
}
|
||||
var first_dash = 0.0;
|
||||
for (var i = 0u; i < count; i++) {
|
||||
if elements[i] > 0.0 {
|
||||
first_dash = elements[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
value = ((distance - align_end + first_dash) % pattern_length
|
||||
+ pattern_length) % pattern_length;
|
||||
} else {
|
||||
value = ((distance % pattern_length) + pattern_length) % pattern_length;
|
||||
}
|
||||
var position = 0.0;
|
||||
let dot_half = u.world_per_pixel * 0.75;
|
||||
for (var i = 0u; i < count; i++) {
|
||||
let element = elements[i];
|
||||
if element == 0.0 {
|
||||
let delta = abs(value - position);
|
||||
if min(delta, pattern_length - delta) <= dot_half {
|
||||
return true;
|
||||
}
|
||||
} else if element > 0.0 {
|
||||
if value >= position && value < position + element {
|
||||
return true;
|
||||
}
|
||||
position += element;
|
||||
} else {
|
||||
position -= element;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn clipped_cap(cap: vec2<f32>, ends: vec3<f32>) -> bool {
|
||||
if cap.x < 0.0 {
|
||||
return length(cap) > ends.y;
|
||||
}
|
||||
if cap.x > ends.x {
|
||||
return length(vec2<f32>(cap.x - ends.x, cap.y)) > ends.z;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn visible_fragment(in: VertexOut) -> bool {
|
||||
if clipped_cap(in.cap, in.cap_ends) {
|
||||
return false;
|
||||
}
|
||||
if in.pattern_length > 0.0 && in.min_elem >= u.world_per_pixel {
|
||||
return in_dash(
|
||||
in.distance,
|
||||
in.pattern_length,
|
||||
in.pat0,
|
||||
in.pat1,
|
||||
in.align_end,
|
||||
in.align_total,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
if !visible_fragment(in) {
|
||||
discard;
|
||||
}
|
||||
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5);
|
||||
return vec4<f32>(in.color.rgb, alpha);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_black(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
if !visible_fragment(in) {
|
||||
discard;
|
||||
}
|
||||
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5);
|
||||
return vec4<f32>(0.0, 0.0, 0.0, alpha);
|
||||
}
|
||||
Reference in new issue
Block a user