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 <noreply@anthropic.com>
This commit is contained in:
parent
0db513e07a
commit
0317dad14e
3 changed files with 515 additions and 102 deletions
|
|
@ -32,6 +32,11 @@ pub struct Pipeline {
|
||||||
/// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false.
|
/// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false.
|
||||||
/// Used to draw ghost copies of selected wires through occluding geometry.
|
/// Used to draw ghost copies of selected wires through occluding geometry.
|
||||||
wire_xray_pipeline: wgpu::RenderPipeline,
|
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,
|
hatch_pipeline: wgpu::RenderPipeline,
|
||||||
/// Phase 4-B — single-draw batched hatch pipeline. Per-instance
|
/// Phase 4-B — single-draw batched hatch pipeline. Per-instance
|
||||||
/// data lives in storage buffers; one draw call covers every
|
/// data lives in storage buffers; one draw call covers every
|
||||||
|
|
@ -181,9 +186,19 @@ impl Pipeline {
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Wire 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 {
|
let wire_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
label: Some("wire.pipeline_layout"),
|
label: Some("wire.pipeline_layout"),
|
||||||
bind_group_layouts: &[&frame_bgl],
|
bind_group_layouts: wire_bgls,
|
||||||
push_constant_ranges: &[],
|
push_constant_ranges: &[],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -192,6 +207,11 @@ impl Pipeline {
|
||||||
|
|
||||||
let wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("wire.shader"),
|
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!(
|
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
|
||||||
"../../shaders/wire.wgsl"
|
"../../shaders/wire.wgsl"
|
||||||
))),
|
))),
|
||||||
|
|
@ -909,6 +929,10 @@ impl Pipeline {
|
||||||
Self {
|
Self {
|
||||||
wire_pipeline,
|
wire_pipeline,
|
||||||
wire_xray_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_pipeline,
|
||||||
hatch_batched_pipeline,
|
hatch_batched_pipeline,
|
||||||
image_pipeline,
|
image_pipeline,
|
||||||
|
|
@ -983,7 +1007,7 @@ impl Pipeline {
|
||||||
{
|
{
|
||||||
j += 1;
|
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;
|
i = j;
|
||||||
}
|
}
|
||||||
self.gpu_wires = batches;
|
self.gpu_wires = batches;
|
||||||
|
|
@ -1044,7 +1068,8 @@ impl Pipeline {
|
||||||
if let Some(h) = hover {
|
if let Some(h) = hover {
|
||||||
push(h.value(), WireModel::HOVER, wires, &mut out);
|
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.
|
/// Upload the live overlay (command preview / interim / grip-drag) wires.
|
||||||
|
|
@ -1059,7 +1084,7 @@ impl Pipeline {
|
||||||
self.gpu_preview_wires = if wires.is_empty() {
|
self.gpu_preview_wires = if wires.is_empty() {
|
||||||
vec![]
|
vec![]
|
||||||
} else {
|
} 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
|
// Edge buffer is always built from `face3d_wires`, so 3DFACE
|
||||||
// outlines stay on the screen regardless of mode.
|
// 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
|
// Fill buffer split: 3D quads + PolyfaceMesh / PolygonMesh face
|
||||||
// tris go to `vertex_buffer_3d` (gated by `keep_3d_mesh_fills`);
|
// tris go to `vertex_buffer_3d` (gated by `keep_3d_mesh_fills`);
|
||||||
// 2D fills (text-LOD greek, MultiLeader background) go to
|
// 2D fills (text-LOD greek, MultiLeader background) go to
|
||||||
|
|
@ -1612,6 +1638,9 @@ impl Pipeline {
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
for edges in &self.gpu_face3d_edges {
|
for edges in &self.gpu_face3d_edges {
|
||||||
if edges.instance_count > 0 {
|
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.set_vertex_buffer(0, edges.instance_buffer.slice(..));
|
||||||
pass.draw(0..6, 0..edges.instance_count);
|
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.set_vertex_buffer(0, wire.instance_buffer.slice(..));
|
||||||
pass.draw(0..6, 0..wire.instance_count);
|
pass.draw(0..6, 0..wire.instance_count);
|
||||||
}
|
}
|
||||||
|
|
@ -1685,6 +1717,9 @@ impl Pipeline {
|
||||||
pass.set_pipeline(&self.wire_xray_pipeline);
|
pass.set_pipeline(&self.wire_xray_pipeline);
|
||||||
for pw in &self.gpu_preview_wires {
|
for pw in &self.gpu_preview_wires {
|
||||||
if pw.instance_count > 0 {
|
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.set_vertex_buffer(0, pw.instance_buffer.slice(..));
|
||||||
pass.draw(0..6, 0..pw.instance_count);
|
pass.draw(0..6, 0..pw.instance_count);
|
||||||
}
|
}
|
||||||
|
|
@ -1775,6 +1810,9 @@ impl Pipeline {
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
for wire in &self.gpu_selected_wires {
|
for wire in &self.gpu_selected_wires {
|
||||||
if wire.instance_count > 0 {
|
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.set_vertex_buffer(0, wire.instance_buffer.slice(..));
|
||||||
pass.draw(0..6, 0..wire.instance_count);
|
pass.draw(0..6, 0..wire.instance_count);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,90 @@ fn instance_buffer_mapped(
|
||||||
|
|
||||||
// ── Instance layout ───────────────────────────────────────────────────────
|
// ── 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)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct WireInstance {
|
pub struct WireInstance {
|
||||||
|
|
@ -82,6 +166,7 @@ pub struct WireInstance {
|
||||||
pub draw_depth: f32,
|
pub draw_depth: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
impl WireInstance {
|
impl WireInstance {
|
||||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||||
// Offsets come from the struct layout (must match the shader location
|
// 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
|
/// in shaded modes so the surface reads as a clean solid; pure
|
||||||
/// wireframe / HiddenLine / *WithEdges modes draw them.
|
/// wireframe / HiddenLine / *WithEdges modes draw them.
|
||||||
pub is_3d_mesh_edge: bool,
|
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
|
/// 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
|
/// 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
|
/// the same emission logic, and so the batched path can `par_iter` across
|
||||||
/// wires on cold open.
|
/// wires on cold open.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
fn pack_color(color: [f32; 4]) -> [u8; 4] {
|
fn pack_color(color: [f32; 4]) -> [u8; 4] {
|
||||||
[
|
[
|
||||||
(color[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
|
(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> {
|
/// Cumulative arc-length per point (NaN-break aware), with the dash pattern
|
||||||
let color_u8 = pack_color(color);
|
/// centred on the wire. Shared by the wasm and native emission paths.
|
||||||
let pat0 = [
|
fn wire_distances(wire: &WireModel) -> Vec<f32> {
|
||||||
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 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 dists = vec![0.0_f32; n];
|
||||||
let mut has_break = false;
|
let mut has_break = false;
|
||||||
for i in 1..n {
|
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
|
#[inline]
|
||||||
// (interactive / preview wires that don't need sub-f32 precision).
|
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 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);
|
let mut instances: Vec<WireInstance> = Vec::with_capacity(seg_count);
|
||||||
for i in 0..seg_count {
|
for i in 0..seg_count {
|
||||||
let a = wire.points[i];
|
let a = wire.points[i];
|
||||||
let b = wire.points[i + 1];
|
let b = wire.points[i + 1];
|
||||||
if !a[0].is_finite()
|
if !finite3(a) || !finite3(b) {
|
||||||
|| !a[1].is_finite()
|
|
||||||
|| !a[2].is_finite()
|
|
||||||
|| !b[0].is_finite()
|
|
||||||
|| !b[1].is_finite()
|
|
||||||
|| !b[2].is_finite()
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
instances.push(WireInstance {
|
instances.push(WireInstance {
|
||||||
|
|
@ -242,6 +325,51 @@ fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Ve
|
||||||
instances
|
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
|
/// 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
|
/// handle encoded in its `name`. Falls back to 0.0 (transient / preview
|
||||||
/// wires that carry no document handle).
|
/// 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)
|
.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 {
|
impl WireGpu {
|
||||||
|
|
||||||
/// Merge a run of WireModels that share scissor + mesh-edge state into one
|
/// 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>,
|
depth_map: &rustc_hash::FxHashMap<u64, f32>,
|
||||||
scissor: Option<[f32; 4]>,
|
scissor: Option<[f32; 4]>,
|
||||||
mesh_edge: bool,
|
mesh_edge: bool,
|
||||||
|
const_bgl: Option<&wgpu::BindGroupLayout>,
|
||||||
) -> Vec<Self> {
|
) -> Vec<Self> {
|
||||||
use crate::par::prelude::*;
|
|
||||||
const MAX_INSTANCES: usize = 268_435_456 / std::mem::size_of::<WireInstance>();
|
const MAX_INSTANCES: usize = 268_435_456 / std::mem::size_of::<WireInstance>();
|
||||||
let per: Vec<Vec<WireInstance>> = wires
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
.par_iter()
|
{
|
||||||
.map(|w| emit_wire_instances(w, w.color, wire_draw_depth(w, depth_map)))
|
use crate::par::prelude::*;
|
||||||
.collect();
|
// Global `wire_id` = wire index; one shared WireConst buffer for all
|
||||||
let mut instances: Vec<WireInstance> = Vec::with_capacity(per.iter().map(Vec::len).sum());
|
// chunks. Indexed `collect` preserves wire order (the pass relies on
|
||||||
for mut v in per {
|
// submission order for depth-biased / transparent overlap).
|
||||||
instances.append(&mut v);
|
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() {
|
#[cfg(target_arch = "wasm32")]
|
||||||
return vec![];
|
{
|
||||||
|
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
|
/// Merge multiple WireModels into GPU instance buffers, chunked to fit the
|
||||||
|
|
@ -307,58 +509,80 @@ impl WireGpu {
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
wires: &[WireModel],
|
wires: &[WireModel],
|
||||||
depth_map: &rustc_hash::FxHashMap<u64, f32>,
|
depth_map: &rustc_hash::FxHashMap<u64, f32>,
|
||||||
|
const_bgl: Option<&wgpu::BindGroupLayout>,
|
||||||
) -> Vec<Self> {
|
) -> Vec<Self> {
|
||||||
let total_segs: usize = wires.iter().map(|w| w.points.len().saturating_sub(1)).sum();
|
let total_segs: usize = wires.iter().map(|w| w.points.len().saturating_sub(1)).sum();
|
||||||
if total_segs == 0 {
|
if total_segs == 0 {
|
||||||
return vec![];
|
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.
|
// 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>();
|
const MAX_INSTANCES: usize = 268_435_456 / std::mem::size_of::<WireInstance>();
|
||||||
|
|
||||||
instances
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
.chunks(MAX_INSTANCES)
|
{
|
||||||
.enumerate()
|
use crate::par::prelude::*;
|
||||||
.map(|(i, chunk)| {
|
// `block_cache` groups wires by style upstream; order within a batch
|
||||||
let label = format!("wire.batch.ibuf.{i}");
|
// doesn't affect correctness, but indexed `collect` gives each wire a
|
||||||
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
// stable `wire_id` into the shared WireConst buffer.
|
||||||
Self {
|
let per: Vec<(Vec<WireInstance>, WireConst)> = wires
|
||||||
instance_buffer,
|
.par_iter()
|
||||||
instance_count: chunk.len() as u32,
|
.enumerate()
|
||||||
vp_scissor: None,
|
.map(|(idx, w)| {
|
||||||
is_3d_mesh_edge: false,
|
emit_wire_native(w, idx as u32, w.color, wire_draw_depth(w, depth_map))
|
||||||
}
|
})
|
||||||
})
|
.collect();
|
||||||
.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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
151
src/shaders/wire_indexed.wgsl
Normal file
151
src/shaders/wire_indexed.wgsl
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue