perf(render): instance block geometry

Share source buffers across block placements and render nested content with GPU instance transforms.\n\nChunk silhouettes and compact mesh material data so large drawings stay under device buffer limits and remain responsive.
This commit is contained in:
Hakan Seven committed 2026-08-12 02:33:10 +03:00
1 parent 7bfb617745
commit 4fd9b98da4
68 files changed
+3417 -547

No files matched your search

+176
View File
@@ -85,6 +85,37 @@ pub struct Face3DChunk {
pub vertex_count: u32,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Face3DInstance {
pub translation: [f32; 3],
pub translation_low: [f32; 3],
pub draw_depth: f32,
pub _pad: [f32; 3],
}
impl Face3DInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(Face3DInstance, translation) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(Face3DInstance, translation_low) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(Face3DInstance, draw_depth) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
}
pub struct BlockFace3DChunk {
pub vertex_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
pub vertex_count: u32,
pub instance_count: u32,
}
pub struct Face3DGpu {
/// 3DFACE quads + PolyfaceMesh / PolygonMesh face triangles.
/// HiddenLine routes this through the depth-only pipeline so the
@@ -95,6 +126,8 @@ pub struct Face3DGpu {
/// source wire has an empty `points` list. Always rendered with the
/// normal face3d pipeline (visible in every mode).
pub chunks_2d: Vec<Face3DChunk>,
pub block_chunks_3d: Vec<BlockFace3DChunk>,
pub block_chunks_2d: Vec<BlockFace3DChunk>,
}
impl Face3DGpu {
@@ -179,6 +212,9 @@ impl Face3DGpu {
if wire.fill_tris.is_empty() {
continue;
}
if wire.render_instance.is_some() {
continue;
}
if !show_2d_solid_fills && wire.fill_is_2d_solid {
continue;
}
@@ -236,13 +272,153 @@ impl Face3DGpu {
}
}
let (block_chunks_3d, block_chunks_2d) = upload_block_chunks(
device,
all_wires,
keep_3d_mesh_fills,
show_2d_solid_fills,
depth_map,
);
Self {
chunks_3d: upload_chunks(device, &verts_3d, "face3d.vbuf.3d"),
chunks_2d: upload_chunks(device, &verts_2d, "face3d.vbuf.2d"),
block_chunks_3d,
block_chunks_2d,
}
}
}
fn upload_block_chunks(
device: &wgpu::Device,
wires: &[WireModel],
keep_3d_mesh_fills: bool,
show_2d_solid_fills: bool,
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> (Vec<BlockFace3DChunk>, Vec<BlockFace3DChunk>) {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new();
for wire in wires {
let Some(instance) = wire.render_instance else {
continue;
};
if wire.fill_tris.is_empty()
|| (!show_2d_solid_fills && wire.fill_is_2d_solid)
|| (wire.fill_is_3d && !keep_3d_mesh_fills)
{
continue;
}
let key = (instance.source_id, wire.fill_is_3d);
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push((wire.fill_is_3d, Vec::new()));
slot
});
groups[slot].1.push(wire);
}
let mut chunks_3d = Vec::new();
let mut chunks_2d = Vec::new();
for (is_3d, group) in groups {
let Some(&source) = group.first() else {
continue;
};
let Some(base) = source.render_instance else {
continue;
};
let [r, g, b, a] = source.color;
let color = if is_3d { [r * 0.45, g * 0.45, b * 0.45, a] } else { source.color };
let vertices: Vec<Face3DVertex> = source
.fill_tris
.iter()
.enumerate()
.map(|(index, &position)| Face3DVertex {
position,
color,
draw_depth: 0.0,
position_low: source
.fill_tris_low
.get(index)
.copied()
.unwrap_or([0.0; 3]),
})
.collect();
if vertices.is_empty() {
continue;
}
let budget = (device.limits().max_buffer_size as usize / 10) * 9;
let max_vertices = ((budget / std::mem::size_of::<Face3DVertex>()).max(3) / 3) * 3;
let max_instances = ((device.limits().max_buffer_size as usize / 10) * 9
/ std::mem::size_of::<Face3DInstance>())
.max(1);
let instances: Vec<Face3DInstance> = group
.iter()
.filter_map(|wire| {
let instance = wire.render_instance?;
let delta = [
instance.translation[0] - base.translation[0],
instance.translation[1] - base.translation[1],
instance.translation[2] - base.translation[2],
];
let high = delta.map(|value| value as f32);
let (mut zmin, mut zmax) = (f32::INFINITY, f32::NEG_INFINITY);
for point in &wire.fill_tris {
zmin = zmin.min(point[2]);
zmax = zmax.max(point[2]);
}
let draw_depth = if is_3d || zmax - zmin > 1e-4 {
0.0
} else {
super::wire_gpu::wire_draw_depth(wire, depth_map)
};
Some(Face3DInstance {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
(delta[2] - high[2] as f64) as f32,
],
draw_depth,
_pad: [0.0; 3],
})
})
.collect();
let instance_buffers: Vec<(wgpu::Buffer, u32)> = instances
.chunks(max_instances)
.map(|chunk| {
(
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("face3d.block.instances"),
contents: bytemuck::cast_slice(chunk),
usage: wgpu::BufferUsages::VERTEX,
}),
chunk.len() as u32,
)
})
.collect();
for vertex_chunk in vertices.chunks(max_vertices) {
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("face3d.block.vertices"),
contents: bytemuck::cast_slice(vertex_chunk),
usage: wgpu::BufferUsages::VERTEX,
});
for (instance_buffer, instance_count) in &instance_buffers {
let chunk = BlockFace3DChunk {
vertex_buffer: vertex_buffer.clone(),
instance_buffer: instance_buffer.clone(),
vertex_count: vertex_chunk.len() as u32,
instance_count: *instance_count,
};
if is_3d {
chunks_3d.push(chunk);
} else {
chunks_2d.push(chunk);
}
}
}
}
(chunks_3d, chunks_2d)
}
/// Upload `verts` as one or more VERTEX buffers, each under 90% of the
/// device's `max_buffer_size`, split on whole-triangle boundaries. Also
/// keeps every chunk's vertex count well below `u32::MAX` so the draw
+38 -12
View File
@@ -83,10 +83,10 @@ impl HatchGpu {
include_str!("../../../shaders/hatch_texture.wgsl")
})),
});
let vertex_layout = if uses_storage {
storage::HatchVertex::layout()
let vertex_layouts = if uses_storage {
vec![storage::HatchVertex::layout(), storage::HatchPlacement::layout()]
} else {
texture::vertex_layout()
vec![texture::vertex_layout(), texture::TextureHatchPlacement::layout()]
};
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(if uses_storage {
@@ -98,7 +98,7 @@ impl HatchGpu {
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[vertex_layout],
buffers: &vertex_layouts,
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
@@ -170,11 +170,17 @@ impl HatchGpu {
*resident = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable);
}
HatchBackend::Texture { resident, .. } => {
*resident = hatches
let renderable: Vec<HatchModel> = hatches
.iter()
.filter(|hatch| hatch.boundary.len() >= 3)
.map(|hatch| TextureHatch::new(device, queue, hatch, &self.bind_group_layout))
.cloned()
.collect();
*resident = TextureHatch::from_models(
device,
queue,
&renderable,
&self.bind_group_layout,
);
}
}
}
@@ -195,11 +201,17 @@ impl HatchGpu {
*preview = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable);
}
HatchBackend::Texture { preview, .. } => {
*preview = hatches
let renderable: Vec<HatchModel> = hatches
.iter()
.filter(|hatch| hatch.boundary.len() >= 3)
.map(|hatch| TextureHatch::new(device, queue, hatch, &self.bind_group_layout))
.cloned()
.collect();
*preview = TextureHatch::from_models(
device,
queue,
&renderable,
&self.bind_group_layout,
);
}
}
}
@@ -218,11 +230,21 @@ impl HatchGpu {
else {
return 0;
};
for index in 0..batch.unique_source_count {
batch.source_visibility[index] = u32::from(is_visible(batch.source_aabbs[index]));
}
for index in batch.unique_source_count..batch.source_visibility.len() {
batch.source_visibility[index] = 1;
}
for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() {
batch.visibility[index] = u32::from(is_visible(aabb));
batch.placements[index].visible = if index == 0 && batch.unique_source_count > 0 {
1
} else {
u32::from(is_visible(aabb))
};
}
batch.upload_visibility(queue);
batch.instance_aabbs.len()
batch.instance_aabbs.len() + batch.unique_source_count
}
pub fn draw<'pass>(
@@ -239,14 +261,18 @@ impl HatchGpu {
for batch in [resident.as_ref(), preview.as_ref()].into_iter().flatten() {
pass.set_bind_group(1, &batch.bind_group, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.draw(0..batch.vertex_count, 0..1);
pass.set_vertex_buffer(1, batch.placement_buffer.slice(..));
for (vertices, instances) in &batch.draws {
pass.draw(vertices.clone(), instances.clone());
}
}
}
HatchBackend::Texture { resident, preview } => {
for hatch in resident.iter().chain(preview) {
pass.set_bind_group(1, &hatch.bind_group, &[]);
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
pass.draw(0..6, 0..1);
pass.set_vertex_buffer(1, hatch.placement_buffer.slice(..));
pass.draw(0..6, 0..hatch.instance_count);
}
}
}
+129 -24
View File
@@ -152,6 +152,30 @@ impl HatchVertex {
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct HatchPlacement {
pub translation: [f32; 2],
pub translation_low: [f32; 2],
pub draw_depth: f32,
pub visible: u32,
}
impl HatchPlacement {
pub(super) fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute { offset: 0, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 8, shader_location: 3, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 16, shader_location: 4, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: 20, shader_location: 5, format: wgpu::VertexFormat::Uint32 },
],
}
}
}
// ── Batch builder ──────────────────────────────────────────────────────────
/// Pack a list of `HatchModel`s into the four concatenated storage
@@ -160,7 +184,8 @@ impl HatchVertex {
/// hatch render pass entirely).
pub(super) struct StorageHatchBatch {
pub vertex_buffer: wgpu::Buffer,
pub vertex_count: u32,
pub placement_buffer: wgpu::Buffer,
pub draws: Vec<(std::ops::Range<u32>, std::ops::Range<u32>)>,
// The four storage buffers below are referenced via `bind_group` —
// dropping them would invalidate it, but the bind group is the
// only direct consumer. Keep them as fields to keep ownership in
@@ -175,14 +200,17 @@ pub(super) struct StorageHatchBatch {
/// `visibility[instance_index]` — when 0 it emits an out-of-NDC
/// clip position so the GPU clips the primitive before the
/// fragment stage runs.
pub visibility_buffer: wgpu::Buffer,
#[allow(dead_code)] pub visibility_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
#[allow(dead_code)] pub instance_count: u32,
/// CPU mirror — `update_visibility` re-uploads this whole slice
/// when any flag changes. ~4 B per hatch, so 40 KB / 10 k hatches
/// per pan tick. Far cheaper than touching the 128 B-per-instance
/// data.
pub visibility: Vec<u32>,
pub placements: Vec<HatchPlacement>,
pub source_visibility: Vec<u32>,
pub source_aabbs: Vec<[f32; 4]>,
pub unique_source_count: usize,
/// CPU-side mirror of each instance's local-space AABB (world-
/// offset-subtracted, world_origin already added back). Used by
/// `compute_hatch_lod` to evaluate the sub-pixel + frustum cull
@@ -208,8 +236,25 @@ impl StorageHatchBatch {
let mut boundary: Vec<[f32; 4]> = Vec::new();
let mut families: Vec<LineFamilyGpu> = Vec::new();
let mut dashes: Vec<f32> = Vec::new();
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&HatchModel>> = Vec::new();
for (index, hatch) in hatches.iter().enumerate() {
let key = hatch
.render_instance
.map(|instance| (true, instance.source_id))
.unwrap_or((false, index as u64));
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(hatch);
}
groups.sort_by_key(|group| std::cmp::Reverse(group.len() == 1));
let unique_source_count = groups.iter().take_while(|group| group.len() == 1).count();
for h in hatches {
for group in &groups {
let h = group[0];
let boundary_offset = boundary.len() as u32;
for &[x, y] in h.boundary.iter() {
// NaN sub-loop separators become the finite sentinel on the
@@ -325,7 +370,7 @@ impl StorageHatchBatch {
boundary_count,
family_offset,
family_count,
draw_depth: h.draw_depth,
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
poly_test: 1,
grad_kind,
_pad2: 0,
@@ -368,7 +413,7 @@ impl StorageHatchBatch {
boundary_count,
family_offset,
family_count,
draw_depth: h.draw_depth,
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
// Always resolve inside/outside per fragment on the GPU (the
// boundary is uploaded below). Dropping CPU pre-triangulation
// keeps the vertex buffer at 6 verts/hatch regardless of
@@ -406,12 +451,71 @@ impl StorageHatchBatch {
verts.push(HatchVertex { local_xy: c, instance_index: i as u32 });
}
}
let mut placements = Vec::new();
let mut draws = Vec::with_capacity(groups.len());
let mut instance_aabbs = Vec::new();
if unique_source_count > 0 {
placements.push(HatchPlacement {
translation: [0.0; 2],
translation_low: [0.0; 2],
draw_depth: 0.0,
visible: 1,
});
draws.push((0..unique_source_count as u32 * 6, 0..1));
let mut union = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
for inst in instances.iter().take(unique_source_count) {
union[0] = union[0].min(inst.aabb[0] + inst.world_origin[0]);
union[1] = union[1].min(inst.aabb[1] + inst.world_origin[1]);
union[2] = union[2].max(inst.aabb[2] + inst.world_origin[0]);
union[3] = union[3].max(inst.aabb[3] + inst.world_origin[1]);
}
instance_aabbs.push(union);
}
for (source_index, group) in groups.iter().enumerate().skip(unique_source_count) {
let source = group[0];
let base = source
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let placement_start = placements.len() as u32;
for hatch in group {
let translation = hatch
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let delta = [translation[0] - base[0], translation[1] - base[1]];
let high = [delta[0] as f32, delta[1] as f32];
placements.push(HatchPlacement {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
],
draw_depth: hatch.draw_depth,
visible: 1,
});
let inst = &instances[source_index];
instance_aabbs.push([
inst.aabb[0] + inst.world_origin[0] + high[0],
inst.aabb[1] + inst.world_origin[1] + high[1],
inst.aabb[2] + inst.world_origin[0] + high[0],
inst.aabb[3] + inst.world_origin[1] + high[1],
]);
}
draws.push((
source_index as u32 * 6..source_index as u32 * 6 + 6,
placement_start..placements.len() as u32,
));
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.vertex"),
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.placements"),
contents: bytemuck::cast_slice(&placements),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.instances"),
contents: bytemuck::cast_slice(&instances),
@@ -466,27 +570,20 @@ impl StorageHatchBatch {
},
],
});
// CPU AABB mirror — local-space rect with world_origin added
// back, ready for `aabb_offscreen` / `aabb_below_pixel` to
// compare against the camera view_proj.
let instance_aabbs: Vec<[f32; 4]> = instances
let source_aabbs = instances
.iter()
.map(|i| {
let ox = i.world_origin[0];
let oy = i.world_origin[1];
[
i.aabb[0] + ox,
i.aabb[1] + oy,
i.aabb[2] + ox,
i.aabb[3] + oy,
]
})
.map(|inst| [
inst.aabb[0] + inst.world_origin[0],
inst.aabb[1] + inst.world_origin[1],
inst.aabb[2] + inst.world_origin[0],
inst.aabb[3] + inst.world_origin[1],
])
.collect();
Some(Self {
vertex_buffer,
vertex_count: verts.len() as u32,
placement_buffer,
draws,
instance_buffer,
boundary_buffer,
family_buffer,
@@ -494,7 +591,10 @@ impl StorageHatchBatch {
visibility_buffer,
bind_group,
instance_count: instances.len() as u32,
visibility,
placements,
source_visibility: visibility,
source_aabbs,
unique_source_count,
instance_aabbs,
})
}
@@ -502,10 +602,15 @@ impl StorageHatchBatch {
/// Push the CPU `visibility` slice to GPU. Call when any
/// element changes (typically per-frame from compute_hatch_lod).
pub(super) fn upload_visibility(&self, queue: &wgpu::Queue) {
queue.write_buffer(
&self.placement_buffer,
0,
bytemuck::cast_slice(&self.placements),
);
queue.write_buffer(
&self.visibility_buffer,
0,
bytemuck::cast_slice(&self.visibility),
bytemuck::cast_slice(&self.source_visibility),
);
}
+82 -1
View File
@@ -39,6 +39,29 @@ pub(super) fn vertex_layout<'a>() -> wgpu::VertexBufferLayout<'a> {
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct TextureHatchPlacement {
translation: [f32; 2],
translation_low: [f32; 2],
draw_depth: f32,
_pad: [f32; 3],
}
impl TextureHatchPlacement {
pub(super) fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 8, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 16, shader_location: 3, format: wgpu::VertexFormat::Float32 },
],
}
}
}
// ── Per-hatch uniform (binding 0) — 96 bytes, matches HatchUniforms in
// hatch_texture.wgsl. ──────────────────────────────────────────────────────
@@ -67,6 +90,8 @@ struct TextureHatchUniform {
pub(super) struct TextureHatch {
pub(super) vertex_buffer: wgpu::Buffer,
pub(super) placement_buffer: wgpu::Buffer,
pub(super) instance_count: u32,
pub(super) bind_group: wgpu::BindGroup,
/// Reserved for per-frame AABB LOD (mirrors `WipeoutGpu`); not yet wired
/// into the web hatch draw loop — the native batched path doesn't do
@@ -78,6 +103,32 @@ pub(super) struct TextureHatch {
}
impl TextureHatch {
pub(super) fn from_models(
device: &wgpu::Device,
queue: &wgpu::Queue,
models: &[HatchModel],
bgl1: &wgpu::BindGroupLayout,
) -> Vec<Self> {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&HatchModel>> = Vec::new();
for (index, model) in models.iter().enumerate() {
let key = model
.render_instance
.map(|instance| (true, instance.source_id))
.unwrap_or((false, index as u64));
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(model);
}
groups
.into_iter()
.map(|group| Self::new(device, queue, &group, bgl1))
.collect()
}
/// Group-1 layout: uniform header (binding 0) + non-filterable float data
/// texture (binding 1).
pub(super) fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
@@ -111,9 +162,10 @@ impl TextureHatch {
pub(super) fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
model: &HatchModel,
models: &[&HatchModel],
bgl1: &wgpu::BindGroupLayout,
) -> Self {
let model = models[0];
// ── Decode pattern mode (mirrors WipeoutGpu::new) ─────────────────
// The gradient shape (kind + invert bit) rides in the mode's high
// bits — the 96-byte uniform has no spare slot.
@@ -180,6 +232,33 @@ impl TextureHatch {
contents: bytemuck::cast_slice(&quad),
usage: wgpu::BufferUsages::VERTEX,
});
let base = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let placements: Vec<TextureHatchPlacement> = models
.iter()
.map(|model| {
let translation = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let delta = [translation[0] - base[0], translation[1] - base[1]];
let high = [delta[0] as f32, delta[1] as f32];
TextureHatchPlacement {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
],
draw_depth: model.draw_depth,
_pad: [0.0; 3],
}
})
.collect();
let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.texture.placements"),
contents: bytemuck::cast_slice(&placements),
usage: wgpu::BufferUsages::VERTEX,
});
// ── Gradient projection range (snapped-local space) ───────────────
let (grad_min, grad_range) = if mode == 2 {
@@ -356,6 +435,8 @@ impl TextureHatch {
Self {
vertex_buffer,
placement_buffer,
instance_count: placements.len() as u32,
bind_group,
world_aabb,
_uniform_buf,
+90 -3
View File
@@ -46,6 +46,30 @@ impl ImageVertex {
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ImageInstance {
pub translation: [f32; 3],
pub translation_low: [f32; 3],
pub draw_depth: f32,
pub _pad: [f32; 3],
}
impl ImageInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, translation) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, translation_low) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, draw_depth) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
}
// ── Uniform ───────────────────────────────────────────────────────────────
#[repr(C)]
@@ -62,9 +86,11 @@ struct ImageParams {
pub struct ImageGpu {
pub vertex_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
/// Number of triangle vertices in `vertex_buffer` — 6 for a plain quad, or
/// more when the raster is clipped to a triangulated polygon.
pub vertex_count: u32,
pub instance_count: u32,
pub bind_group: wgpu::BindGroup,
_texture: wgpu::Texture,
_sampler: wgpu::Sampler,
@@ -72,12 +98,39 @@ pub struct ImageGpu {
}
impl ImageGpu {
pub fn new(
pub fn from_models(
device: &wgpu::Device,
queue: &wgpu::Queue,
model: &ImageModel,
models: &[ImageModel],
bgl1: &wgpu::BindGroupLayout,
) -> Vec<Self> {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&ImageModel>> = Vec::new();
for (index, model) in models.iter().enumerate() {
let key = model
.render_instance
.map(|instance| (true, instance.source_id))
.unwrap_or((false, index as u64));
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(model);
}
groups
.into_iter()
.filter_map(|group| Self::new(device, queue, &group, bgl1))
.collect()
}
fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
models: &[&ImageModel],
bgl1: &wgpu::BindGroupLayout,
) -> Option<Self> {
let &model = models.first()?;
if model.pixels.is_empty() || model.width == 0 || model.height == 0 {
return None;
}
@@ -129,7 +182,7 @@ impl ImageGpu {
// ── Opacity uniform ───────────────────────────────────────────────
let params = ImageParams {
opacity: model.opacity.clamp(0.0, 1.0),
draw_depth: model.draw_depth,
draw_depth: 0.0,
_pad: [0.0; 2],
};
let _params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@@ -179,10 +232,44 @@ impl ImageGpu {
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
let base = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let instances: Vec<ImageInstance> = models
.iter()
.map(|model| {
let translation = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let delta = [
translation[0] - base[0],
translation[1] - base[1],
translation[2] - base[2],
];
let high = delta.map(|value| value as f32);
ImageInstance {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
(delta[2] - high[2] as f64) as f32,
],
draw_depth: model.draw_depth,
_pad: [0.0; 3],
}
})
.collect();
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("image.instances"),
contents: bytemuck::cast_slice(&instances),
usage: wgpu::BufferUsages::VERTEX,
});
Some(Self {
vertex_buffer,
instance_buffer,
vertex_count,
instance_count: instances.len() as u32,
bind_group,
_texture: texture,
_sampler,
File diff suppressed because it is too large. Load diff
+709 -109
View File
File diff suppressed because it is too large. Load diff
+189 -1
View File
@@ -68,6 +68,37 @@ impl TextVertex {
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct BlockTextInstance {
pub translation: [f32; 3],
pub translation_low: [f32; 3],
pub draw_depth: f32,
pub _pad: [f32; 3],
}
impl BlockTextInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockTextInstance, translation) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockTextInstance, translation_low) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockTextInstance, draw_depth) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
}
pub struct BlockTextGpu {
pub vertex_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
pub vertex_count: u32,
pub instance_count: u32,
}
/// Split an f64 into the double-single (high f32, low residual f32) pair the
/// shaders reconstruct relative-to-eye.
pub(crate) fn split_ds(v: f64) -> (f32, f32) {
@@ -261,7 +292,12 @@ pub fn create_pipelines(
color_format: wgpu::TextureFormat,
sample_count: u32,
content_stencil: &wgpu::StencilState,
) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
) -> (
wgpu::RenderPipeline,
wgpu::RenderPipeline,
wgpu::RenderPipeline,
wgpu::RenderPipeline,
) {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("text.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/text.wgsl").into()),
@@ -311,6 +347,50 @@ pub fn create_pipelines(
cache: None,
})
};
let block_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("block_text.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/block_text.wgsl").into()),
});
let create_block = |label, depth_write_enabled, depth_compare| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &block_shader,
entry_point: Some("vs_main"),
buffers: &[TextVertex::layout(), BlockTextInstance::layout()],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &block_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: color_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled,
depth_compare,
stencil: content_stencil.clone(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: sample_count,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
};
(
create(
"text.pipeline",
@@ -322,9 +402,117 @@ pub fn create_pipelines(
Some(false),
Some(wgpu::CompareFunction::Always),
),
create_block(
"block_text.pipeline",
Some(true),
Some(wgpu::CompareFunction::LessEqual),
),
create_block(
"block_text.highlight.pipeline",
Some(false),
Some(wgpu::CompareFunction::Always),
),
)
}
pub fn upload_block_vertices(
device: &wgpu::Device,
wires: &[crate::scene::model::wire_model::WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> Vec<BlockTextGpu> {
let refs: Vec<&crate::scene::model::wire_model::WireModel> = wires.iter().collect();
upload_block_vertex_refs(device, &refs, depth_map, None)
}
pub fn upload_block_vertex_refs(
device: &wgpu::Device,
wires: &[&crate::scene::model::wire_model::WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
tint: Option<[f32; 4]>,
) -> Vec<BlockTextGpu> {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&crate::scene::model::wire_model::WireModel>> = Vec::new();
for &wire in wires {
let Some(instance) = wire.render_instance else {
continue;
};
if wire.text_verts.is_empty() {
continue;
}
let slot = *slots.entry(instance.source_id).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(wire);
}
let mut out = Vec::new();
for group in groups {
let Some(&source) = group.first() else {
continue;
};
let Some(base) = source.render_instance else {
continue;
};
let tinted;
let vertices = if let Some(tint) = tint {
tinted = source
.text_verts
.iter()
.map(|vertex| TextVertex {
color: [tint[0], tint[1], tint[2], vertex.color[3]],
..*vertex
})
.collect::<Vec<_>>();
tinted.as_slice()
} else {
source.text_verts.as_slice()
};
let Some(vertex_buffer) = upload_vertices(device, vertices) else {
continue;
};
let instances: Vec<BlockTextInstance> = group
.iter()
.filter_map(|wire| {
let instance = wire.render_instance?;
let delta = [
instance.translation[0] - base.translation[0],
instance.translation[1] - base.translation[1],
instance.translation[2] - base.translation[2],
];
let high = delta.map(|value| value as f32);
Some(BlockTextInstance {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
(delta[2] - high[2] as f64) as f32,
],
draw_depth: super::wire_gpu::wire_draw_depth(wire, depth_map),
_pad: [0.0; 3],
})
})
.collect();
let max_instances = ((device.limits().max_buffer_size as usize / 10) * 9
/ std::mem::size_of::<BlockTextInstance>())
.max(1);
for chunk in instances.chunks(max_instances) {
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_text.instances"),
contents: bytemuck::cast_slice(chunk),
usage: wgpu::BufferUsages::VERTEX,
});
out.push(BlockTextGpu {
vertex_buffer: vertex_buffer.clone(),
instance_buffer,
vertex_count: vertices.len() as u32,
instance_count: chunk.len() as u32,
});
}
}
out
}
/// Upload a finished vertex list; `None` if empty.
pub fn upload_vertices(device: &wgpu::Device, verts: &[TextVertex]) -> Option<wgpu::Buffer> {
if verts.is_empty() {
+90 -3
View File
@@ -45,6 +45,29 @@ impl HatchVertex {
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct WipeoutPlacement {
pub translation: [f32; 2],
pub translation_low: [f32; 2],
pub draw_depth: f32,
pub _pad: [f32; 3],
}
impl WipeoutPlacement {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 8, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 16, shader_location: 3, format: wgpu::VertexFormat::Float32 },
],
}
}
}
// ── Uniform structs ───────────────────────────────────────────────────────
/// Per-hatch parameters (binding 0). Must be 96 bytes (16-byte aligned).
@@ -120,6 +143,8 @@ pub struct FamilyBatchData {
pub struct WipeoutGpu {
pub vertex_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
pub instance_count: u32,
pub bind_group: wgpu::BindGroup,
/// World-space XY bounding rect [min_x, min_y, max_x, max_y] of the
/// boundary polygon. Used by the per-frame LOD pass to skip hatches
@@ -131,7 +156,33 @@ pub struct WipeoutGpu {
}
impl WipeoutGpu {
pub fn new(device: &wgpu::Device, model: &HatchModel, bgl1: &wgpu::BindGroupLayout) -> Self {
pub fn from_models(
device: &wgpu::Device,
models: &[HatchModel],
bgl1: &wgpu::BindGroupLayout,
) -> Vec<Self> {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&HatchModel>> = Vec::new();
for (index, model) in models.iter().enumerate() {
let key = model
.render_instance
.map(|instance| (true, instance.source_id))
.unwrap_or((false, index as u64));
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(model);
}
groups
.into_iter()
.map(|group| Self::new(device, &group, bgl1))
.collect()
}
fn new(device: &wgpu::Device, models: &[&HatchModel], bgl1: &wgpu::BindGroupLayout) -> Self {
let model = models[0];
// ── Decode pattern mode ──────────────────────────────────────────
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
@@ -300,7 +351,7 @@ impl WipeoutGpu {
(origin[0] - origin[0] as f32 as f64) as f32,
(origin[1] - origin[1] as f32 as f64) as f32,
],
draw_depth: model.draw_depth,
draw_depth: 0.0,
_pad: [0.0; 3],
};
@@ -373,16 +424,52 @@ impl WipeoutGpu {
// is in that absolute local space rather than relative to the
// anchor. (Sub-pixel LOD via `aabb_below_pixel` is unaffected:
// it measures a projected diagonal, invariant under translation.)
let base = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let placements: Vec<WipeoutPlacement> = models
.iter()
.map(|model| {
let translation = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let delta = [translation[0] - base[0], translation[1] - base[1]];
let high = [delta[0] as f32, delta[1] as f32];
WipeoutPlacement {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
],
draw_depth: model.draw_depth,
_pad: [0.0; 3],
}
})
.collect();
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("wipeout.instances"),
contents: bytemuck::cast_slice(&placements),
usage: wgpu::BufferUsages::VERTEX,
});
let ox = model.world_origin[0] as f32;
let oy = model.world_origin[1] as f32;
let world_aabb = if min_x.is_finite() && min_y.is_finite() {
[min_x + ox, min_y + oy, max_x + ox, max_y + oy]
let mut aabb = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
for placement in &placements {
aabb[0] = aabb[0].min(min_x + ox + placement.translation[0]);
aabb[1] = aabb[1].min(min_y + oy + placement.translation[1]);
aabb[2] = aabb[2].max(max_x + ox + placement.translation[0]);
aabb[3] = aabb[3].max(max_y + oy + placement.translation[1]);
}
aabb
} else {
[min_x, min_y, max_x, max_y]
};
Self {
vertex_buffer,
instance_buffer,
instance_count: placements.len() as u32,
bind_group,
world_aabb,
_uniform_buf,
+205
View File
@@ -285,6 +285,84 @@ pub struct WireGpu {
pub const_bind_group: Option<std::sync::Arc<wgpu::BindGroup>>,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct BlockWireVertex {
pub pos_a: [f32; 3],
pub pos_a_low: [f32; 3],
pub pos_b: [f32; 3],
pub pos_b_low: [f32; 3],
pub distances: [f32; 2],
pub taper_ratio: [u16; 2],
}
impl BlockWireVertex {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_a) as u64, shader_location: 0, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_b) as u64, shader_location: 1, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_a_low) as u64, shader_location: 2, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_b_low) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, distances) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, taper_ratio) as u64, shader_location: 5, format: wgpu::VertexFormat::Unorm16x2 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: ATTRS,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct BlockWireInstance {
pub translation: [f32; 3],
pub translation_low: [f32; 3],
pub depth: [f32; 2],
}
impl BlockWireInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireInstance, translation) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireInstance, translation_low) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireInstance, depth) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32x2 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
}
#[derive(Clone)]
pub struct BlockWireGpu {
pub vertex_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
pub vertex_count: u32,
pub instance_count: u32,
pub is_3d_mesh_edge: bool,
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,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
})
}
/// Expand one `WireModel` into its per-segment instance stream (1 instance per
/// finite segment). Pulled out so both the single-wire and batched paths share
/// the same emission logic, and so the batched path can `par_iter` across
@@ -563,6 +641,133 @@ pub(crate) fn wire_draw_depth(
}
}
impl BlockWireGpu {
pub fn from_wires(
device: &wgpu::Device,
wires: &[&WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
mesh_names: &rustc_hash::FxHashSet<&str>,
color_override: Option<[f32; 4]>,
const_bgl: &wgpu::BindGroupLayout,
) -> Vec<Self> {
use wgpu::util::DeviceExt;
let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> =
rustc_hash::FxHashMap::default();
let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new();
for &wire in wires {
let Some(instance) = wire.render_instance else {
continue;
};
if wire.points.len() < 2 {
continue;
}
let mesh_edge = mesh_names.contains(wire.name.as_str());
let key = (instance.source_id, mesh_edge);
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push((mesh_edge, Vec::new()));
slot
});
groups[slot].1.push(wire);
}
let mut out = Vec::new();
for (mesh_edge, group) in groups {
let Some(&source) = group.first() else {
continue;
};
let Some(base) = source.render_instance else {
continue;
};
let color = color_override.unwrap_or(source.color);
let (segments, mut constant) = emit_wire_native(source, 0, color, 0.0);
if segments.is_empty() {
continue;
}
constant.draw_depth = 0.0;
let mut vertices = Vec::with_capacity(segments.len() * 6);
for segment in segments {
let vertex = 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]);
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_wire.vertices"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let const_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_wire.const"),
contents: bytemuck::bytes_of(&constant),
usage: 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 instances: Vec<BlockWireInstance> = group
.iter()
.filter_map(|wire| {
let instance = wire.render_instance?;
let delta = [
instance.translation[0] - base.translation[0],
instance.translation[1] - base.translation[1],
instance.translation[2] - base.translation[2],
];
let high = delta.map(|value| value as f32);
Some(BlockWireInstance {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
(delta[2] - high[2] as f64) as f32,
],
depth: [
if mesh_edge {
0.0
} else {
wire_draw_depth(wire, depth_map)
},
0.0,
],
})
})
.collect();
let max_instances = ((device.limits().max_buffer_size as usize / 10) * 9
/ std::mem::size_of::<BlockWireInstance>())
.max(1);
for chunk in instances.chunks(max_instances) {
out.push(Self {
vertex_buffer: vertex_buffer.clone(),
instance_buffer: instance_buffer_mapped(
device,
"block_wire.instances",
chunk,
),
vertex_count: vertices.len() as u32,
instance_count: chunk.len() as u32,
is_3d_mesh_edge: mesh_edge,
const_bind_group: const_bind_group.clone(),
});
}
}
out
}
}
/// Build the shared per-wire `WireConst` storage buffer and its bind group
/// (storage path). All chunks from one build reference the same
/// buffer via their global `wire_id`, so a single bind group is cloned into