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:
PanosandClaude Opus 5 committed 2026-09-05 18:53:09 +03:00
1 parent 0e4f755e6f
commit 6599b82776
4 files changed
+634 -107

No files matched your search

+5 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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));
}
}
+323
View File
@@ -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);
}