forked from 1b-agent/opencadstudio-upstream
perf(render): hoist per-wire constants to a storage buffer (native)
Every wire segment is one instance, and each carried the wire's color, line-weight, dash pattern and draw-depth (~44 B) inline — re-fetched per segment even though they're constant along the wire. On native, move those into a per-wire `WireConst` storage buffer (group 1) indexed by a `wire_id` on the instance. The instance drops from 104 B to 60 B (~42 %) across every entity's wires, and the shared constants are fetched once per wire (cached) instead of once per segment — less vertex-attribute bandwidth and less GPU upload on rebuilds. WebGL2 has no vertex-stage storage buffers, so the wasm build keeps the original self-contained fat instance and wire.wgsl; native uses the new slim instance, wire_indexed.wgsl, and a group-1 bind group built per batch in WireGpu::from_run / from_batch. Visual output is unchanged (colors are now full f32 rather than 8-bit, if anything more precise). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
0db513e07a
commit
0317dad14e
3 files changed
+516
-103
No files matched your search
@@ -32,6 +32,11 @@ pub struct Pipeline {
|
||||
/// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false.
|
||||
/// Used to draw ghost copies of selected wires through occluding geometry.
|
||||
wire_xray_pipeline: wgpu::RenderPipeline,
|
||||
/// Layout for the per-wire `WireConst` storage buffer (group 1 of the wire /
|
||||
/// xray pipelines). `Some` on native; `None` on web (fat instance, no
|
||||
/// storage). Passed to `WireGpu::from_run` / `from_batch` so they can build
|
||||
/// each batch's per-wire bind group.
|
||||
wire_const_bgl: Option<wgpu::BindGroupLayout>,
|
||||
hatch_pipeline: wgpu::RenderPipeline,
|
||||
/// Phase 4-B — single-draw batched hatch pipeline. Per-instance
|
||||
/// data lives in storage buffers; one draw call covers every
|
||||
@@ -181,9 +186,19 @@ impl Pipeline {
|
||||
});
|
||||
|
||||
// ── Wire pipeline ──────────────────────────────────────────────────
|
||||
// Native: per-wire constants live in a storage buffer (group 1), so the
|
||||
// slim instance only carries per-segment data. WebGL2 has no
|
||||
// vertex-stage storage buffers, so the wasm build keeps the fat instance
|
||||
// and a single bind group.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let wire_const_bgl = wire_gpu::WireConst::bind_group_layout(device);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let wire_bgls: &[&wgpu::BindGroupLayout] = &[&frame_bgl, &wire_const_bgl];
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let wire_bgls: &[&wgpu::BindGroupLayout] = &[&frame_bgl];
|
||||
let wire_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("wire.pipeline_layout"),
|
||||
bind_group_layouts: &[&frame_bgl],
|
||||
bind_group_layouts: wire_bgls,
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
@@ -192,6 +207,11 @@ impl Pipeline {
|
||||
|
||||
let wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("wire.shader"),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
|
||||
"../../shaders/wire_indexed.wgsl"
|
||||
))),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
|
||||
"../../shaders/wire.wgsl"
|
||||
))),
|
||||
@@ -909,6 +929,10 @@ impl Pipeline {
|
||||
Self {
|
||||
wire_pipeline,
|
||||
wire_xray_pipeline,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_const_bgl: Some(wire_const_bgl),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
wire_const_bgl: None,
|
||||
hatch_pipeline,
|
||||
hatch_batched_pipeline,
|
||||
image_pipeline,
|
||||
@@ -983,7 +1007,7 @@ impl Pipeline {
|
||||
{
|
||||
j += 1;
|
||||
}
|
||||
batches.extend(WireGpu::from_run(device, &wires[i..j], depth_map, scissor, mesh_edge));
|
||||
batches.extend(WireGpu::from_run(device, &wires[i..j], depth_map, scissor, mesh_edge, self.wire_const_bgl.as_ref()));
|
||||
i = j;
|
||||
}
|
||||
self.gpu_wires = batches;
|
||||
@@ -1044,7 +1068,8 @@ impl Pipeline {
|
||||
if let Some(h) = hover {
|
||||
push(h.value(), WireModel::HOVER, wires, &mut out);
|
||||
}
|
||||
self.gpu_selected_wires = WireGpu::from_run(device, &out, depth_map, None, false);
|
||||
self.gpu_selected_wires =
|
||||
WireGpu::from_run(device, &out, depth_map, None, false, self.wire_const_bgl.as_ref());
|
||||
}
|
||||
|
||||
/// Upload the live overlay (command preview / interim / grip-drag) wires.
|
||||
@@ -1059,7 +1084,7 @@ impl Pipeline {
|
||||
self.gpu_preview_wires = if wires.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
WireGpu::from_run(device, wires, depth_map, None, false)
|
||||
WireGpu::from_run(device, wires, depth_map, None, false, self.wire_const_bgl.as_ref())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1141,7 +1166,8 @@ impl Pipeline {
|
||||
) {
|
||||
// Edge buffer is always built from `face3d_wires`, so 3DFACE
|
||||
// outlines stay on the screen regardless of mode.
|
||||
self.gpu_face3d_edges = WireGpu::from_batch(device, face3d_wires, depth_map);
|
||||
self.gpu_face3d_edges =
|
||||
WireGpu::from_batch(device, face3d_wires, depth_map, self.wire_const_bgl.as_ref());
|
||||
// Fill buffer split: 3D quads + PolyfaceMesh / PolygonMesh face
|
||||
// tris go to `vertex_buffer_3d` (gated by `keep_3d_mesh_fills`);
|
||||
// 2D fills (text-LOD greek, MultiLeader background) go to
|
||||
@@ -1612,6 +1638,9 @@ impl Pipeline {
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
for edges in &self.gpu_face3d_edges {
|
||||
if edges.instance_count > 0 {
|
||||
if let Some(bg) = &edges.const_bind_group {
|
||||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||
}
|
||||
pass.set_vertex_buffer(0, edges.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..edges.instance_count);
|
||||
}
|
||||
@@ -1670,6 +1699,9 @@ impl Pipeline {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(bg) = &wire.const_bind_group {
|
||||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||
}
|
||||
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..wire.instance_count);
|
||||
}
|
||||
@@ -1685,6 +1717,9 @@ impl Pipeline {
|
||||
pass.set_pipeline(&self.wire_xray_pipeline);
|
||||
for pw in &self.gpu_preview_wires {
|
||||
if pw.instance_count > 0 {
|
||||
if let Some(bg) = &pw.const_bind_group {
|
||||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||
}
|
||||
pass.set_vertex_buffer(0, pw.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..pw.instance_count);
|
||||
}
|
||||
@@ -1775,6 +1810,9 @@ impl Pipeline {
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
for wire in &self.gpu_selected_wires {
|
||||
if wire.instance_count > 0 {
|
||||
if let Some(bg) = &wire.const_bind_group {
|
||||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||
}
|
||||
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..wire.instance_count);
|
||||
}
|
||||
|
||||
+322
-98
@@ -60,6 +60,90 @@ fn instance_buffer_mapped(
|
||||
|
||||
// ── Instance layout ───────────────────────────────────────────────────────
|
||||
|
||||
// ── Native: slim per-segment instance + shared per-wire constants ───────────
|
||||
//
|
||||
// Every segment of a wire used to carry the wire's color / line-weight / dash
|
||||
// pattern / draw-depth (~44 B) on each instance — re-fetched once per segment
|
||||
// even though it's constant along the wire. On native we hoist those into a
|
||||
// per-wire `WireConst` storage buffer indexed by `wire_id`, so the instance
|
||||
// keeps only the per-segment data (endpoints + arc-length distances). Cuts the
|
||||
// instance from 104 B to 60 B (~42 %) and removes the redundant per-segment
|
||||
// re-fetch of the shared constants. WebGL2 has no vertex-stage storage buffers,
|
||||
// so the wasm build below keeps the original self-contained fat instance.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct WireInstance {
|
||||
pub pos_a: [f32; 3],
|
||||
pub pos_a_low: [f32; 3],
|
||||
pub pos_b: [f32; 3],
|
||||
pub pos_b_low: [f32; 3],
|
||||
pub distance_a: f32,
|
||||
pub distance_b: f32,
|
||||
/// Index into the per-wire `WireConst` storage buffer (group 1).
|
||||
pub wire_id: u32,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl WireInstance {
|
||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
// Must match `InstanceIn` in wire_indexed.wgsl.
|
||||
const ATTRS: &[wgpu::VertexAttribute] = &[
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_a) as u64, shader_location: 0, format: wgpu::VertexFormat::Float32x3 },
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_b) as u64, shader_location: 1, format: wgpu::VertexFormat::Float32x3 },
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_a_low) as u64, shader_location: 2, format: wgpu::VertexFormat::Float32x3 },
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_b_low) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3 },
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_a) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32 },
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_b) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 },
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, wire_id) as u64, shader_location: 6, format: wgpu::VertexFormat::Uint32 },
|
||||
];
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<WireInstance>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: ATTRS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-wire constants shared by every segment of a wire (native only). std430
|
||||
/// layout: three vec4 then four scalars = 64 B, matching `WireConst` in
|
||||
/// wire_indexed.wgsl.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct WireConst {
|
||||
pub color: [f32; 4],
|
||||
pub pat0: [f32; 4],
|
||||
pub pat1: [f32; 4],
|
||||
pub half_width: f32,
|
||||
pub pattern_length: f32,
|
||||
pub draw_depth: f32,
|
||||
pub _pad: f32,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl WireConst {
|
||||
/// Bind-group layout for the per-wire storage buffer (group 1 of the wire /
|
||||
/// xray pipelines). Read-only storage, visible to the vertex stage.
|
||||
pub fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("wire_const.bgl"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Web (WebGL2): self-contained fat instance (no vertex-stage storage) ─────
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct WireInstance {
|
||||
@@ -82,6 +166,7 @@ pub struct WireInstance {
|
||||
pub draw_depth: f32,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl WireInstance {
|
||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
// Offsets come from the struct layout (must match the shader location
|
||||
@@ -123,12 +208,17 @@ pub struct WireGpu {
|
||||
/// in shaded modes so the surface reads as a clean solid; pure
|
||||
/// wireframe / HiddenLine / *WithEdges modes draw them.
|
||||
pub is_3d_mesh_edge: bool,
|
||||
/// Per-wire constants storage (group 1), shared across all chunks of one
|
||||
/// build. `None` on web (the fat instance carries the constants inline) and
|
||||
/// for empty buffers. The draw loop binds it as group 1 when present.
|
||||
pub const_bind_group: Option<std::sync::Arc<wgpu::BindGroup>>,
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// wires on cold open.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn pack_color(color: [f32; 4]) -> [u8; 4] {
|
||||
[
|
||||
(color[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
|
||||
@@ -138,28 +228,10 @@ fn pack_color(color: [f32; 4]) -> [u8; 4] {
|
||||
]
|
||||
}
|
||||
|
||||
fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Vec<WireInstance> {
|
||||
let color_u8 = pack_color(color);
|
||||
let pat0 = [
|
||||
wire.pattern[0],
|
||||
wire.pattern[1],
|
||||
wire.pattern[2],
|
||||
wire.pattern[3],
|
||||
];
|
||||
let pat1 = [
|
||||
wire.pattern[4],
|
||||
wire.pattern[5],
|
||||
wire.pattern[6],
|
||||
wire.pattern[7],
|
||||
];
|
||||
let half_width = wire.line_weight_px * 0.5;
|
||||
|
||||
/// Cumulative arc-length per point (NaN-break aware), with the dash pattern
|
||||
/// centred on the wire. Shared by the wasm and native emission paths.
|
||||
fn wire_distances(wire: &WireModel) -> Vec<f32> {
|
||||
let n = wire.points.len();
|
||||
let seg_count = n.saturating_sub(1);
|
||||
if seg_count == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut dists = vec![0.0_f32; n];
|
||||
let mut has_break = false;
|
||||
for i in 1..n {
|
||||
@@ -206,22 +278,33 @@ fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Ve
|
||||
}
|
||||
}
|
||||
}
|
||||
dists
|
||||
}
|
||||
|
||||
// Low residual paired index-for-index with points; empty → all-zero
|
||||
// (interactive / preview wires that don't need sub-f32 precision).
|
||||
#[inline]
|
||||
fn finite3(p: [f32; 3]) -> bool {
|
||||
p[0].is_finite() && p[1].is_finite() && p[2].is_finite()
|
||||
}
|
||||
|
||||
/// Web: emit fat per-segment instances (each carries the wire's constants).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Vec<WireInstance> {
|
||||
let color_u8 = pack_color(color);
|
||||
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
|
||||
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
|
||||
let half_width = wire.line_weight_px * 0.5;
|
||||
let n = wire.points.len();
|
||||
let seg_count = n.saturating_sub(1);
|
||||
if seg_count == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let dists = wire_distances(wire);
|
||||
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
|
||||
|
||||
let mut instances: Vec<WireInstance> = Vec::with_capacity(seg_count);
|
||||
for i in 0..seg_count {
|
||||
let a = wire.points[i];
|
||||
let b = wire.points[i + 1];
|
||||
if !a[0].is_finite()
|
||||
|| !a[1].is_finite()
|
||||
|| !a[2].is_finite()
|
||||
|| !b[0].is_finite()
|
||||
|| !b[1].is_finite()
|
||||
|| !b[2].is_finite()
|
||||
{
|
||||
if !finite3(a) || !finite3(b) {
|
||||
continue;
|
||||
}
|
||||
instances.push(WireInstance {
|
||||
@@ -242,6 +325,51 @@ fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Ve
|
||||
instances
|
||||
}
|
||||
|
||||
/// Native: emit slim per-segment instances (positions + distances + `wire_id`)
|
||||
/// plus the one `WireConst` record every segment of this wire shares.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn emit_wire_native(
|
||||
wire: &WireModel,
|
||||
wire_id: u32,
|
||||
color: [f32; 4],
|
||||
draw_depth: f32,
|
||||
) -> (Vec<WireInstance>, WireConst) {
|
||||
let cst = WireConst {
|
||||
color,
|
||||
pat0: [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]],
|
||||
pat1: [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]],
|
||||
half_width: wire.line_weight_px * 0.5,
|
||||
pattern_length: wire.pattern_length,
|
||||
draw_depth,
|
||||
_pad: 0.0,
|
||||
};
|
||||
let n = wire.points.len();
|
||||
let seg_count = n.saturating_sub(1);
|
||||
if seg_count == 0 {
|
||||
return (Vec::new(), cst);
|
||||
}
|
||||
let dists = wire_distances(wire);
|
||||
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
|
||||
let mut instances: Vec<WireInstance> = Vec::with_capacity(seg_count);
|
||||
for i in 0..seg_count {
|
||||
let a = wire.points[i];
|
||||
let b = wire.points[i + 1];
|
||||
if !finite3(a) || !finite3(b) {
|
||||
continue;
|
||||
}
|
||||
instances.push(WireInstance {
|
||||
pos_a: a,
|
||||
pos_a_low: low(i),
|
||||
pos_b: b,
|
||||
pos_b_low: low(i + 1),
|
||||
distance_a: dists[i],
|
||||
distance_b: dists[i + 1],
|
||||
wire_id,
|
||||
});
|
||||
}
|
||||
(instances, cst)
|
||||
}
|
||||
|
||||
/// Looks up a wire's draw-order depth from the per-entity map using the
|
||||
/// handle encoded in its `name`. Falls back to 0.0 (transient / preview
|
||||
/// wires that carry no document handle).
|
||||
@@ -254,6 +382,36 @@ fn wire_draw_depth(wire: &WireModel, depth_map: &rustc_hash::FxHashMap<u64, f32>
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Build the shared per-wire `WireConst` storage buffer and its bind group
|
||||
/// (native only). All instance-buffer chunks from one build reference the same
|
||||
/// buffer via their global `wire_id`, so a single bind group is cloned into
|
||||
/// each chunk.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn build_const_bind_group(
|
||||
device: &wgpu::Device,
|
||||
bgl: &wgpu::BindGroupLayout,
|
||||
consts: &[WireConst],
|
||||
) -> std::sync::Arc<wgpu::BindGroup> {
|
||||
use wgpu::util::DeviceExt;
|
||||
// wgpu rejects zero-sized buffers; pad with one zeroed record when empty.
|
||||
let one = [<WireConst as bytemuck::Zeroable>::zeroed()];
|
||||
let data: &[WireConst] = if consts.is_empty() { &one } else { consts };
|
||||
let buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("wire_const.buf"),
|
||||
contents: bytemuck::cast_slice(data),
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
});
|
||||
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("wire_const.bg"),
|
||||
layout: bgl,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: buf.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
std::sync::Arc::new(bg)
|
||||
}
|
||||
|
||||
impl WireGpu {
|
||||
|
||||
/// Merge a run of WireModels that share scissor + mesh-edge state into one
|
||||
@@ -272,32 +430,76 @@ impl WireGpu {
|
||||
depth_map: &rustc_hash::FxHashMap<u64, f32>,
|
||||
scissor: Option<[f32; 4]>,
|
||||
mesh_edge: bool,
|
||||
const_bgl: Option<&wgpu::BindGroupLayout>,
|
||||
) -> Vec<Self> {
|
||||
use crate::par::prelude::*;
|
||||
const MAX_INSTANCES: usize = 268_435_456 / std::mem::size_of::<WireInstance>();
|
||||
let per: Vec<Vec<WireInstance>> = wires
|
||||
.par_iter()
|
||||
.map(|w| emit_wire_instances(w, w.color, wire_draw_depth(w, depth_map)))
|
||||
.collect();
|
||||
let mut instances: Vec<WireInstance> = Vec::with_capacity(per.iter().map(Vec::len).sum());
|
||||
for mut v in per {
|
||||
instances.append(&mut v);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use crate::par::prelude::*;
|
||||
// Global `wire_id` = wire index; one shared WireConst buffer for all
|
||||
// chunks. Indexed `collect` preserves wire order (the pass relies on
|
||||
// submission order for depth-biased / transparent overlap).
|
||||
let per: Vec<(Vec<WireInstance>, WireConst)> = wires
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, w)| {
|
||||
emit_wire_native(w, idx as u32, w.color, wire_draw_depth(w, depth_map))
|
||||
})
|
||||
.collect();
|
||||
let mut instances: Vec<WireInstance> =
|
||||
Vec::with_capacity(per.iter().map(|(v, _)| v.len()).sum());
|
||||
let mut consts: Vec<WireConst> = Vec::with_capacity(per.len());
|
||||
for (mut v, c) in per {
|
||||
instances.append(&mut v);
|
||||
consts.push(c);
|
||||
}
|
||||
if instances.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let bg = const_bgl.map(|bgl| build_const_bind_group(device, bgl, &consts));
|
||||
instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.map(|chunk| {
|
||||
let buf = instance_buffer_mapped(device, "wire.run.ibuf", chunk);
|
||||
Self {
|
||||
instance_buffer: buf,
|
||||
instance_count: chunk.len() as u32,
|
||||
vp_scissor: scissor,
|
||||
is_3d_mesh_edge: mesh_edge,
|
||||
const_bind_group: bg.clone(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
if instances.is_empty() {
|
||||
return vec![];
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = const_bgl;
|
||||
let per: Vec<Vec<WireInstance>> = wires
|
||||
.iter()
|
||||
.map(|w| emit_wire_instances(w, w.color, wire_draw_depth(w, depth_map)))
|
||||
.collect();
|
||||
let mut instances: Vec<WireInstance> =
|
||||
Vec::with_capacity(per.iter().map(Vec::len).sum());
|
||||
for mut v in per {
|
||||
instances.append(&mut v);
|
||||
}
|
||||
if instances.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.map(|chunk| {
|
||||
let buf = instance_buffer_mapped(device, "wire.run.ibuf", chunk);
|
||||
Self {
|
||||
instance_buffer: buf,
|
||||
instance_count: chunk.len() as u32,
|
||||
vp_scissor: scissor,
|
||||
is_3d_mesh_edge: mesh_edge,
|
||||
const_bind_group: None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.map(|chunk| {
|
||||
let buf = instance_buffer_mapped(device, "wire.run.ibuf", chunk);
|
||||
Self {
|
||||
instance_buffer: buf,
|
||||
instance_count: chunk.len() as u32,
|
||||
vp_scissor: scissor,
|
||||
is_3d_mesh_edge: mesh_edge,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Merge multiple WireModels into GPU instance buffers, chunked to fit the
|
||||
@@ -307,58 +509,80 @@ impl WireGpu {
|
||||
device: &wgpu::Device,
|
||||
wires: &[WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, f32>,
|
||||
const_bgl: Option<&wgpu::BindGroupLayout>,
|
||||
) -> Vec<Self> {
|
||||
let total_segs: usize = wires.iter().map(|w| w.points.len().saturating_sub(1)).sum();
|
||||
if total_segs == 0 {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// Parallel per-wire instance emission. Each wire's stream is
|
||||
// independent — `block_cache` groups wires by style upstream, so order
|
||||
// within a batch does not affect correctness. `reduce` concatenates the
|
||||
// per-wire Vec<WireInstance> chunks while letting rayon spread work
|
||||
// across cores.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let instances: Vec<WireInstance> = wires
|
||||
.par_iter()
|
||||
.map(|wire| emit_wire_instances(wire, wire.color, wire_draw_depth(wire, depth_map)))
|
||||
.reduce(Vec::new, |mut acc, mut chunk| {
|
||||
if acc.is_empty() {
|
||||
chunk
|
||||
} else if chunk.is_empty() {
|
||||
acc
|
||||
} else {
|
||||
acc.append(&mut chunk);
|
||||
acc
|
||||
}
|
||||
});
|
||||
// Web: no thread pool — concatenate the per-wire streams sequentially.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let instances: Vec<WireInstance> = wires
|
||||
.iter()
|
||||
.flat_map(|wire| emit_wire_instances(wire, wire.color, wire_draw_depth(wire, depth_map)))
|
||||
.collect();
|
||||
|
||||
if instances.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// GPU max buffer size is 256 MB; chunk to stay within the limit.
|
||||
const MAX_INSTANCES: usize = 268_435_456 / std::mem::size_of::<WireInstance>();
|
||||
|
||||
instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.enumerate()
|
||||
.map(|(i, chunk)| {
|
||||
let label = format!("wire.batch.ibuf.{i}");
|
||||
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
||||
Self {
|
||||
instance_buffer,
|
||||
instance_count: chunk.len() as u32,
|
||||
vp_scissor: None,
|
||||
is_3d_mesh_edge: false,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use crate::par::prelude::*;
|
||||
// `block_cache` groups wires by style upstream; order within a batch
|
||||
// doesn't affect correctness, but indexed `collect` gives each wire a
|
||||
// stable `wire_id` into the shared WireConst buffer.
|
||||
let per: Vec<(Vec<WireInstance>, WireConst)> = wires
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, w)| {
|
||||
emit_wire_native(w, idx as u32, w.color, wire_draw_depth(w, depth_map))
|
||||
})
|
||||
.collect();
|
||||
let mut instances: Vec<WireInstance> =
|
||||
Vec::with_capacity(per.iter().map(|(v, _)| v.len()).sum());
|
||||
let mut consts: Vec<WireConst> = Vec::with_capacity(per.len());
|
||||
for (mut v, c) in per {
|
||||
instances.append(&mut v);
|
||||
consts.push(c);
|
||||
}
|
||||
if instances.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let bg = const_bgl.map(|bgl| build_const_bind_group(device, bgl, &consts));
|
||||
instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.enumerate()
|
||||
.map(|(i, chunk)| {
|
||||
let label = format!("wire.batch.ibuf.{i}");
|
||||
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
||||
Self {
|
||||
instance_buffer,
|
||||
instance_count: chunk.len() as u32,
|
||||
vp_scissor: None,
|
||||
is_3d_mesh_edge: false,
|
||||
const_bind_group: bg.clone(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = const_bgl;
|
||||
let instances: Vec<WireInstance> = wires
|
||||
.iter()
|
||||
.flat_map(|w| emit_wire_instances(w, w.color, wire_draw_depth(w, depth_map)))
|
||||
.collect();
|
||||
if instances.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
instances
|
||||
.chunks(MAX_INSTANCES)
|
||||
.enumerate()
|
||||
.map(|(i, chunk)| {
|
||||
let label = format!("wire.batch.ibuf.{i}");
|
||||
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
||||
Self {
|
||||
instance_buffer,
|
||||
instance_count: chunk.len() as u32,
|
||||
vp_scissor: None,
|
||||
is_3d_mesh_edge: false,
|
||||
const_bind_group: None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Wire shader (native) — same as wire.wgsl, but the per-wire constants
|
||||
// (color / half_width / dash pattern / draw_depth) live in a per-wire storage
|
||||
// buffer indexed by `wire_id` instead of being replicated on every segment
|
||||
// instance. Cuts the instance from 104 B to 60 B and removes the redundant
|
||||
// per-segment re-fetch of constants. WebGL2 has no vertex-stage storage
|
||||
// buffers, so the wasm build uses wire.wgsl (fat instance) instead.
|
||||
|
||||
struct Uniforms {
|
||||
viewport_size: vec2<f32>,
|
||||
world_per_pixel: f32,
|
||||
lwdisplay_enable: f32,
|
||||
flat_shade: f32,
|
||||
transparency_enable: f32,
|
||||
_pad: vec2<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;
|
||||
|
||||
// Per-wire constants (std430). Must match `WireConst` in wire_gpu.rs.
|
||||
struct WireConst {
|
||||
color: vec4<f32>,
|
||||
pat0: vec4<f32>,
|
||||
pat1: vec4<f32>,
|
||||
half_width: f32,
|
||||
pattern_length: f32,
|
||||
draw_depth: f32,
|
||||
_pad: f32,
|
||||
}
|
||||
@group(1) @binding(0) var<storage, read> wire_consts: array<WireConst>;
|
||||
|
||||
struct InstanceIn {
|
||||
@location(0) pos_a: vec3<f32>,
|
||||
@location(1) pos_b: vec3<f32>,
|
||||
@location(2) pos_a_low: vec3<f32>,
|
||||
@location(3) pos_b_low: vec3<f32>,
|
||||
@location(4) distance_a: f32,
|
||||
@location(5) distance_b: f32,
|
||||
@location(6) wire_id: u32,
|
||||
}
|
||||
|
||||
const DRAW_ORDER_BIAS: f32 = 0.001;
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
@vertex fn vs_main(@builtin(vertex_index) vid: u32, in: InstanceIn) -> VertexOut {
|
||||
let c = wire_consts[in.wire_id];
|
||||
|
||||
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[vid];
|
||||
let side = side_arr[vid];
|
||||
|
||||
let rel_a = (in.pos_a - u.eye_high) + (in.pos_a_low - u.eye_low);
|
||||
let rel_b = (in.pos_b - u.eye_high) + (in.pos_b_low - u.eye_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 ndc_a = clip_a.xy / clip_a.w;
|
||||
let ndc_b = clip_b.xy / clip_b.w;
|
||||
|
||||
let screen_a = ndc_a * u.viewport_size * 0.5;
|
||||
let screen_b = ndc_b * u.viewport_size * 0.5;
|
||||
|
||||
let seg = screen_b - screen_a;
|
||||
let seg_len = length(seg);
|
||||
var perp: vec2<f32>;
|
||||
if seg_len > 1e-4 {
|
||||
let dir = seg / seg_len;
|
||||
perp = vec2<f32>(-dir.y, dir.x);
|
||||
} else {
|
||||
perp = vec2<f32>(0.0, 1.0);
|
||||
}
|
||||
|
||||
let perp_ndc = perp / (u.viewport_size * 0.5);
|
||||
|
||||
let clip_pos = mix(clip_a, clip_b, which_end);
|
||||
|
||||
let hw = select(0.5, c.half_width, u.lwdisplay_enable > 0.5);
|
||||
|
||||
let ndc_offset = perp_ndc * hw * side;
|
||||
let final_clip = clip_pos + vec4<f32>(ndc_offset * clip_pos.w, 0.0, 0.0);
|
||||
|
||||
var min_elem: f32 = c.pattern_length;
|
||||
let elems = array<f32, 8>(
|
||||
c.pat0.x, c.pat0.y, c.pat0.z, c.pat0.w,
|
||||
c.pat1.x, c.pat1.y, c.pat1.z, c.pat1.w,
|
||||
);
|
||||
for (var i = 0u; i < 8u; i++) {
|
||||
let e = abs(elems[i]);
|
||||
if e > 0.0 && e < min_elem { min_elem = e; }
|
||||
}
|
||||
|
||||
var out: VertexOut;
|
||||
out.clip_pos = final_clip;
|
||||
out.clip_pos.z = out.clip_pos.z - c.draw_depth * DRAW_ORDER_BIAS * out.clip_pos.w;
|
||||
out.color = c.color;
|
||||
out.distance = mix(in.distance_a, in.distance_b, which_end);
|
||||
out.pattern_length = c.pattern_length;
|
||||
out.pat0 = c.pat0;
|
||||
out.pat1 = c.pat1;
|
||||
out.min_elem = min_elem;
|
||||
return out;
|
||||
}
|
||||
|
||||
fn in_dash(dist: f32, pat_len: f32, p0: vec4<f32>, p1: vec4<f32>) -> bool {
|
||||
let d = ((dist % pat_len) + pat_len) % pat_len;
|
||||
var pos = 0.0f;
|
||||
let dot_half = u.world_per_pixel * 0.75;
|
||||
let elems = array<f32, 8>(p0.x, p0.y, p0.z, p0.w, p1.x, p1.y, p1.z, p1.w);
|
||||
var count = 0u;
|
||||
for (var i = 0u; i < 8u; i++) {
|
||||
if elems[i] != 0.0 { count = i + 1u; }
|
||||
}
|
||||
for (var i = 0u; i < count; i++) {
|
||||
let elem = elems[i];
|
||||
if elem == 0.0 {
|
||||
let dd = abs(d - pos);
|
||||
if min(dd, pat_len - dd) <= dot_half { return true; }
|
||||
} else if elem > 0.0 {
|
||||
if d >= pos && d < pos + elem { return true; }
|
||||
pos += elem;
|
||||
} else {
|
||||
pos += -elem;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@fragment fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
if in.pattern_length > 0.0 {
|
||||
if in.min_elem >= u.world_per_pixel {
|
||||
if !in_dash(in.distance, in.pattern_length, in.pat0, in.pat1) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
}
|
||||
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5);
|
||||
return vec4<f32>(in.color.rgb, alpha);
|
||||
}
|
||||
Reference in new issue
Block a user