// Wire GPU buffers — instanced quad rendering for thick lines. // // Each segment [A→B] is one INSTANCE; the vertex shader expands a 6-vertex // unit quad whose corners are derived from `@builtin(vertex_index)`. This // cuts upload bandwidth by ~6.5× versus the old layout (which duplicated // the segment payload across six vertex records). // // NaN sentinel: text glyphs pack multiple disconnected strokes into one // WireModel, separated by [NaN, NaN, NaN] points. Segments where either // endpoint contains NaN are silently skipped during emission. // // Instance layout (step_mode = Instance): // pos_a [f32; 3] — segment start (high half, world / offset-relative) // pos_a_low [f32; 3] — segment start low residual (double-single pair) // pos_b [f32; 3] — segment end (high) // pos_b_low [f32; 3] — segment end low residual // color [u8; 4] — RGBA, Unorm8x4 → vec4 in shader // distance_a f32 — arc-length at endpoint A // distance_b f32 — arc-length at endpoint B // half_width f32 — half line width in pixels // pattern_length f32 — dash pattern total length // pat0 [f32; 4] — pattern elements 0-3 // pat1 [f32; 4] — pattern elements 4-7 // draw_depth f32 — normalized draw-order depth bias // The high+low pair encodes the f64 source so the relative-to-eye shader // stays precise at UTM-scale coordinates and after a cross-drawing paste. use crate::scene::model::wire_model::WireModel; use iced::wgpu; /// Allocate a VERTEX buffer and fill it through the queue. /// /// This used to map at creation to skip the staging copy. Mapping panics on /// the invalid buffer an out-of-memory device returns, and that panic aborts /// the process from inside iced's redraw, so the staging copy is the price of /// a session that survives a failed allocation. Chunking keeps the transient /// staging cost bounded. See `super::gpu_upload`. fn instance_buffer( device: &wgpu::Device, queue: &wgpu::Queue, label: &str, data: &[T], ) -> wgpu::Buffer { super::gpu_upload::upload_buffer(device, queue, label, data, wgpu::BufferUsages::VERTEX) } // ── Instance layout ─────────────────────────────────────────────────────── // ── Storage path: slim per-segment instance + shared 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 one 64-byte cache line and removes the redundant // per-segment re-fetch of the shared constants. WebGL2 has no vertex-stage // storage buffers, so the compatibility path keeps the self-contained fat // instance. #[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, /// Endpoint width / the per-wire maximum width, normalized by the vertex /// fetch unit. `[0, 0]` means use the constant width. Ratios retain the /// full f32 world-width scale in `WireConst` while making every instance /// exactly one 64-byte cache line. pub taper_ratio: [u16; 2], } 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::VertexAttribute { offset: std::mem::offset_of!(WireInstance, taper_ratio) as u64, shader_location: 7, format: wgpu::VertexFormat::Unorm16x2 }, ]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: ATTRS, } } } /// Per-wire constants shared by every segment of a wire (storage path). std430 /// layout: three vec4, eight scalars, then three vec4 = 128 B, matching `WireConst` in /// wire_indexed.wgsl. `align_end` / `align_total` carry the "A"-type endpoint /// alignment (see `wire_distances`); 0.0 total = no alignment. #[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 align_end: f32, pub align_total: f32, /// World-space half-width for a wide-polyline band. `0.0` = a normal wire /// (uses `half_width`, screen pixels). Non-zero = the vertex shader expands /// the quad by `world_half_width / world_per_pixel` so the band tracks zoom /// in drawing units. pub world_half_width: f32, pub is_tapered: f32, pub _pad2: f32, /// Point-marker origin as a double-single pair. `marker_normal_scale.w` /// stores the viewport-height percentage; zero disables marker scaling. pub marker_origin_high: [f32; 4], pub marker_origin_low: [f32; 4], pub marker_normal_scale: [f32; 4], } 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, }], }) } } // ── Packed compatibility instance (no vertex-stage storage) ──────────────── // // Selected at runtime for devices whose storage-buffer limits are insufficient, // or when --compat-renderer is set. #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct PackedWireInstance { pub pos_a: [f32; 3], pub pos_a_low: [f32; 3], pub pos_b: [f32; 3], pub pos_b_low: [f32; 3], /// RGBA packed as `Unorm8x4` — the vertex shader receives a `vec4` /// in [0, 1] after the GPU does the conversion. 8 bits per channel is /// indistinguishable from f32 at 8-bit display output. pub color: [u8; 4], pub distance_a: f32, pub distance_b: f32, pub half_width: f32, pub pattern_length: f32, pub pat0: [f32; 4], pub pat1: [f32; 4], /// Normalized draw-order depth in (0,1); applied as a small clip-z bias /// in the shader so this wire orders against other entity types. pub draw_depth: f32, /// "A"-type endpoint alignment (see `wire_distances`): the end-dash length /// and the total wire length. `align_total == 0.0` = not aligned. pub align_end: f32, pub align_total: f32, /// World-space half-width for a wide-polyline band (see `WireConst`). `0.0` /// = a normal wire (uses `half_width`, screen pixels). pub world_half_width: f32, /// Per-endpoint world half-width for a tapered band (0 = use the constant /// `world_half_width`). The shader interpolates across the segment. pub world_hw_a: f32, pub world_hw_b: f32, pub marker_origin_high: [f32; 4], pub marker_origin_low: [f32; 4], pub marker_normal_scale: [f32; 4], } impl PackedWireInstance { pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> { // Offsets come from the struct layout (must match the shader location // indices in wire.wgsl). Scalars ride in PACKED vec4/vec2 attributes — // WebGL2 / WebGPU cap vertex attributes at 16 and the one-scalar-per- // location layout had grown to 17, so the pipeline failed to build and // the web viewport drew no lines at all (#414). The struct fields are // laid out so each packed group is contiguous. const ATTRS: &[wgpu::VertexAttribute] = &[ wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pos_a) as u64, shader_location: 0, format: wgpu::VertexFormat::Float32x3 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pos_b) as u64, shader_location: 1, format: wgpu::VertexFormat::Float32x3 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, color) as u64, shader_location: 2, format: wgpu::VertexFormat::Unorm8x4 }, // dists = (distance_a, distance_b, half_width, pattern_length) wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, distance_a) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x4 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pat0) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x4 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pat1) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32x4 }, // misc = (draw_depth, align_end, align_total, world_half_width) wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, draw_depth) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32x4 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pos_a_low) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32x3 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pos_b_low) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32x3 }, // taper = (world_hw_a, world_hw_b) wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, world_hw_a) as u64, shader_location: 9, format: wgpu::VertexFormat::Float32x2 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_origin_high) as u64, shader_location: 10, format: wgpu::VertexFormat::Float32x4 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_origin_low) as u64, shader_location: 11, format: wgpu::VertexFormat::Float32x4 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_normal_scale) as u64, shader_location: 12, format: wgpu::VertexFormat::Float32x4 }, ]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: ATTRS, } } } /// Wire and hatch pipelines switch together: the fast path uses storage /// buffers; the compatibility path carries wire constants in packed vertex /// attributes and hatch data in a texture. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WirePipelineMode { IndexedStorage, Packed, } fn select_pipeline( capabilities: super::device_capabilities::DeviceCapabilities, forced: bool, ) -> WirePipelineMode { if forced || !capabilities.supports_wire_storage() { WirePipelineMode::Packed } else { WirePipelineMode::IndexedStorage } } impl WirePipelineMode { pub fn select( capabilities: super::device_capabilities::DeviceCapabilities, forced: bool, ) -> Self { select_pipeline(capabilities, forced) } pub fn uses_storage(self) -> bool { match self { Self::IndexedStorage => true, Self::Packed => false, } } pub fn layout<'a>(self) -> wgpu::VertexBufferLayout<'a> { match self { Self::IndexedStorage => WireInstance::layout(), Self::Packed => PackedWireInstance::layout(), } } } // ── GPU handle ──────────────────────────────────────────────────────────── #[derive(Clone)] pub struct WireGpu { pub instance_buffer: wgpu::Buffer, /// First instance in a shared arena buffer. Standalone buffers start at 0. pub first_instance: u32, pub instance_count: u32, /// `true` when the source `WireModel` also carries `fill_tris` /// (i.e. it is a 3D mesh face — PolyfaceMesh / PolygonMesh — whose /// outline lives in `points`). The wire pass skips these instances /// 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 for this chunk (group 1); packed instances carry them inline. pub const_bind_group: Option>, } #[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::() 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::() as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: ATTRS, } } } #[derive(Clone)] pub struct BlockWireGpu { /// Packed mode only; `None` in storage mode, where the segments reach the /// shader through `const_bind_group` instead. pub vertex_buffer: Option, 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, } /// Binding 0 is the definition's constants. In storage mode binding 1 carries /// that chunk's segments, which is why the bind group is per-chunk there and /// shared across chunks in packed mode. pub fn block_const_bind_group_layout( device: &wgpu::Device, uses_storage: bool, ) -> wgpu::BindGroupLayout { let mut entries = vec![wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }]; if uses_storage { entries.push(wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, count: None, }); } device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("block_wire_const.bgl"), entries: &entries, }) } /// 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. fn pack_color(color: [f32; 4]) -> [u8; 4] { [ (color[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8, (color[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8, (color[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8, (color[3].clamp(0.0, 1.0) * 255.0 + 0.5) as u8, ] } /// Cumulative arc-length per point (NaN-break aware) plus the `"A"`-type /// alignment pair `(align_end, align_total)`. Shared by the wasm and native /// emission paths. /// /// AutoCAD-style linetypes are implicitly `A` (aligned): a dashed line must /// begin AND end on a solid dash, keeping the interior dashes at their nominal /// length and stretching/shrinking only the two end dashes symmetrically to /// absorb the leftover (so parallel lines share an identical interior). We /// express that on the GPU by handing the shader the total wire length /// (`align_total`) and the end-dash length (`align_end`); the pattern walk then /// forces the two end regions lit and phases the interior to resume right after /// the first dash. `align_total == 0.0` disables it and the shader falls back to /// the legacy centred repeating pattern. /// /// Alignment applies only to a single continuous run (`!has_break`) whose /// pattern begins with a dash. NaN-separated (plinegen=false) polylines and /// non-dash-first patterns keep the legacy centred phase. fn wire_distances(wire: &WireModel) -> (Vec, f32, f32) { let n = wire.points.len(); let explicit = wire.pattern_stations.len() >= n + 1; let (mut dists, has_break, total) = if explicit { ( wire.pattern_stations[..n].to_vec(), !wire.plinegen, wire.pattern_stations[n], ) } else { let mut dists = vec![0.0_f32; n]; let mut has_break = false; // Accumulate arc-length in f64 from double-single deltas (high + low). let mut acc = 0.0_f64; for i in 1..n { let p = wire.points[i - 1]; let q = wire.points[i]; if !p[0].is_finite() || !q[0].is_finite() { has_break = true; if !wire.plinegen && !p[0].is_finite() && q[0].is_finite() { acc = 0.0; } dists[i] = acc as f32; } else { let pl = wire.points_low.get(i - 1).copied().unwrap_or([0.0; 3]); let ql = wire.points_low.get(i).copied().unwrap_or([0.0; 3]); let dx = (q[0] as f64 - p[0] as f64) + (ql[0] as f64 - pl[0] as f64); let dy = (q[1] as f64 - p[1] as f64) + (ql[1] as f64 - pl[1] as f64); let dz = (q[2] as f64 - p[2] as f64) + (ql[2] as f64 - pl[2] as f64); acc += (dx * dx + dy * dy + dz * dz).sqrt(); dists[i] = acc as f32; } } let total = dists.last().copied().unwrap_or(0.0); (dists, has_break, total) }; let pat_len = wire.pattern_length; if pat_len <= 1e-6 || has_break || n < 2 { return (dists, 0.0, 0.0); } if total <= 1e-6 { return (dists, 0.0, 0.0); } // DGN line styles draw the pattern from the START vertex with continuous // phase and are NOT end-aligned. The raw arc-length distances already put // dist 0 at the first vertex, so a dash-first pattern begins a dash exactly // there. Return before the A-type / centring logic that standard linetypes // use (see `WireModel::dash_from_start`). if wire.dash_from_start { return (dists, 0.0, 0.0); } // Shared "A"-type for MLINE elements: the caller fixes the begin/end // solid-dash length (`dash_align_end`, derived once from the multiline's // centre-line length) so every parallel element runs the SAME interior phase // — the shader's interior walk depends on `align_end`, not on the wire's own // length — while `align_total` stays this wire's own length, so each element // still ends on a dash at its own endpoint. if let Some(ae) = wire.dash_align_end { if total <= pat_len { // Shorter than one full period → solid (matches the per-wire path). return (dists, total, total); } return (dists, ae.clamp(1e-4, total * 0.5), total); } // Align only a proper alternating pattern that BEGINS with a dash followed // by a gap — every standard linetype does (DASHED/DASHDOT/CENTER/HIDDEN/…). // Gap-first, dot-first, single-element, or consecutive-dash patterns keep // the legacy centred phase: the A-type interior-resume assumes the element // after the leading dash is a gap, and force-lighting an end dash on a // non-dash-start would paint over a leading blank. if wire.pattern[0] > 0.0 && wire.pattern[1] < 0.0 { let a = wire.pattern[0]; let p = pat_len; if total <= p { // Shorter than one full pattern period → drawn as a single solid // dash spanning the whole line (aligned linetypes can't fit a // dash-gap-dash in less than one period). return (dists, total, total); } // "A" alignment for a dash-first pattern of period P laid out as // [D] [gap] ([dash] [gap])*(k-1) [D] // gives L = 2D + (k-1)a + k(P-a) => D = (L - k*P + a) / 2. // Pick the interior period count k so the end dash D stays near nominal a. let mut k = ((total - a) / p).round().max(1.0); let mut d_end = (total - k * p + a) * 0.5; if d_end <= 1e-4 { // End dash underflowed (period ≫ first dash); drop one period so the // ends stay visible. k = (k - 1.0).max(0.0); d_end = (total - k * p + a) * 0.5; } let d_end = d_end.clamp(1e-4, total * 0.5); return (dists, d_end, total); } // Legacy centred phase for non-aligned patterns (behaviour unchanged from // before A-type). The shader reads phase as `dist % pattern_length`, so a // constant offset shifts it; place the wire midpoint at the first dash's // centre so the two ends stay symmetric. let first_dash = wire .pattern .iter() .copied() .find(|&v| v > 0.0) .unwrap_or_else(|| wire.pattern[0].abs()); let offset = first_dash * 0.5 + total * 0.5; for d in dists.iter_mut() { *d += offset; } (dists, 0.0, 0.0) } #[inline] fn finite3(p: [f32; 3]) -> bool { p[0].is_finite() && p[1].is_finite() && p[2].is_finite() } fn marker_metadata(wire: &WireModel) -> ([f32; 4], [f32; 4], [f32; 4]) { let Some(marker) = wire.point_marker else { for tg in &wire.tangent_geoms { match tg { crate::scene::model::wire_model::TangentGeom::Arc { axis_x, axis_y, .. } | crate::scene::model::wire_model::TangentGeom::PlanarCircle { axis_x, axis_y, .. } => { let ax = glam::DVec3::from_array(*axis_x); let ay = glam::DVec3::from_array(*axis_y); let n = ax.cross(ay).normalize_or(glam::DVec3::Z); return ([0.0; 4], [0.0; 4], [n.x as f32, n.y as f32, n.z as f32, 0.0]); } crate::scene::model::wire_model::TangentGeom::PlanarEllipse { normal, .. } => { let n = glam::DVec3::from_array(*normal).normalize_or(glam::DVec3::Z); return ([0.0; 4], [0.0; 4], [n.x as f32, n.y as f32, n.z as f32, 0.0]); } _ => {} } } if wire.points.len() >= 3 { let p0 = glam::Vec3::from_array(wire.points[0]); for i in 1..wire.points.len() - 1 { let v1 = glam::Vec3::from_array(wire.points[i]) - p0; let v2 = glam::Vec3::from_array(wire.points[i + 1]) - p0; let cross = v1.cross(v2); if cross.length_squared() > 1e-6 { let n = cross.normalize(); return ([0.0; 4], [0.0; 4], [n.x, n.y, n.z, 0.0]); } } } return ([0.0; 4], [0.0; 4], [0.0, 0.0, 1.0, 0.0]); }; let origin = marker.origin; let (hx, lx) = WireModel::split_ds(origin.x); let (hy, ly) = WireModel::split_ds(origin.y); let (hz, lz) = WireModel::split_ds(origin.z); let normal = marker.normal.as_vec3().normalize_or(glam::Vec3::Z); ( [hx, hy, hz, 0.0], [lx, ly, lz, 0.0], [normal.x, normal.y, normal.z, marker.viewport_percent], ) } /// Emit packed per-segment instances (each carries the wire's constants). pub(crate) fn emit_wire_packed( wire: &WireModel, color: [f32; 4], draw_depth: f32, ) -> Vec { 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, align_end, align_total) = wire_distances(wire); let (marker_origin_high, marker_origin_low, marker_normal_scale) = marker_metadata(wire); let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) }; let is_tapered = !wire.taper_widths.is_empty(); let tw = |i: usize| -> f32 { if is_tapered { wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 } else { -1.0 } }; let mut instances: Vec = 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(PackedWireInstance { pos_a: a, pos_a_low: low(i), pos_b: b, pos_b_low: low(i + 1), color: color_u8, distance_a: dists[i], distance_b: dists[i + 1], half_width, pattern_length: wire.pattern_length, pat0, pat1, draw_depth, align_end, align_total, world_half_width: wire.world_width * 0.5, world_hw_a: tw(i), world_hw_b: tw(i + 1), marker_origin_high, marker_origin_low, marker_normal_scale, }); } instances } /// Storage path: emit slim instances (positions + distances + `wire_id`) /// plus the one `WireConst` record every segment of this wire shares. pub(crate) fn emit_wire_native( wire: &WireModel, wire_id: u32, color: [f32; 4], draw_depth: f32, ) -> (Vec, WireConst) { let (dists, align_end, align_total) = wire_distances(wire); let (marker_origin_high, marker_origin_low, marker_normal_scale) = marker_metadata(wire); let is_tapered = !wire.taper_widths.is_empty(); 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, align_end, align_total, world_half_width: wire.world_width * 0.5, is_tapered: if is_tapered { 1.0 } else { 0.0 }, _pad2: 0.0, marker_origin_high, marker_origin_low, marker_normal_scale, }; let n = wire.points.len(); let seg_count = n.saturating_sub(1); if seg_count == 0 { return (Vec::new(), cst); } let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) }; let taper_ratio = |i: usize| -> u16 { if !is_tapered || wire.world_width <= 0.0 { 0 } else { let width = wire.taper_widths.get(i).copied().unwrap_or(0.0); ((width / wire.world_width).clamp(0.0, 1.0) * u16::MAX as f32).round() as u16 } }; let mut instances: Vec = 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, taper_ratio: [taper_ratio(i), taper_ratio(i + 1)], }); } (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). A wire carrying a block-local /// `depth_override` (a wide-polyline band inside a block) composes it into /// the insert's own sub-range so the band orders against its block siblings. pub(crate) fn wire_draw_depth( wire: &WireModel, depth_map: &rustc_hash::FxHashMap, ) -> f32 { let base = wire .name .parse::() .ok() .and_then(|h| depth_map.get(&h).copied()); match (base, wire.depth_override) { (Some([d, half]), Some(local)) => d + local * half, (Some([d, _]), None) => d, (None, _) => 0.0, } } /// Cached block geometry keyed by source, edge mode, colour and base translation. /// Vertices use the first instance's world coordinates, so changing that base /// must invalidate the geometry as well as rebuild the relative placements. pub type BlockGeometryCache = rustc_hash::FxHashMap< BlockGeometryKey, std::sync::Arc>, >; /// How this device draws block wires. The layout and the mode are decided /// together when the pipeline is built and are never chosen independently, so /// they travel as one argument. #[derive(Clone, Copy)] pub struct BlockWireTarget<'a> { pub const_bgl: &'a wgpu::BindGroupLayout, pub mode: WirePipelineMode, } /// Device bytes held by a whole block-geometry cache. pub fn block_geometry_bytes(cache: &BlockGeometryCache) -> u64 { cache .values() .flat_map(|chunks| chunks.iter()) .map(|chunk| chunk.gpu_bytes) .sum() } /// `(source_id, mesh_edge, colour bits, base translation bits)`. pub type BlockGeometryKey = (u64, bool, [u32; 4], [u64; 3]); /// One drawable slice of a block definition's geometry. /// /// The two pipeline modes put the segments in different places, so the chunk /// carries whichever the mode produced: /// /// * packed — `vertex_buffer` holds six copies of each segment, one per corner /// of its quad, and `bind_group` is the definition's constants alone; /// * storage — the segments are in a read-only storage buffer inside /// `bind_group`, one copy each, and `vertex_buffer` is `None`. /// /// `vertex_count` is six per segment either way: in storage mode the shader /// divides `vertex_index` by six to find its segment. pub struct BlockGeometryChunk { pub vertex_buffer: Option, pub vertex_count: u32, pub bind_group: std::sync::Arc, /// Device bytes this chunk holds. Recorded at build because in storage mode /// the buffer is owned by `bind_group` and cannot be measured from here. pub gpu_bytes: u64, } impl BlockWireGpu { /// Identity of the geometry this batch draws, comparable across pipeline /// modes. /// /// Packed mode keeps the segments in `vertex_buffer`; storage mode keeps /// them inside `const_bind_group`, which is per-chunk there, and leaves /// `vertex_buffer` `None`. Both are built on the same cache miss and /// reused together on a hit, so the pair answers "is this the same /// geometry as before?" in either mode — which `vertex_buffer` alone /// cannot once it is always `None`. pub fn geometry_id(&self) -> (Option<&wgpu::Buffer>, &wgpu::BindGroup) { (self.vertex_buffer.as_ref(), self.const_bind_group.as_ref()) } pub fn from_wires( device: &wgpu::Device, queue: &wgpu::Queue, wires: &[&WireModel], depth_map: &rustc_hash::FxHashMap, color_override: Option<[f32; 4]>, target: BlockWireTarget<'_>, mut cache: Option<&mut BlockGeometryCache>, ) -> Vec { let BlockWireTarget { const_bgl, mode } = target; let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> = rustc_hash::FxHashMap::default(); let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new(); for &wire in wires { if color_override.is_none() && !wire.display_visible { continue; } let Some(instance) = wire.render_instance else { continue; }; if wire.points.len() < 2 { continue; } let mesh_edge = color_override.is_none() && wire.fill_is_3d; 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(); let mut live_keys: rustc_hash::FxHashSet = rustc_hash::FxHashSet::default(); // Geometry bytes actually sent to the device this call, so the storage // path's saving is visible rather than assumed. let mut uploaded_bytes = 0usize; let mut uploaded_segments = 0usize; // Definitions whose upload the device rejected, so they were not cached. let mut poisoned = 0usize; 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 cache_key = ( base.source_id, mesh_edge, color.map(f32::to_bits), base.translation.map(f64::to_bits), ); live_keys.insert(cache_key); // Reuse this definition's geometry when it is already on the GPU. if let Some(chunks) = cache .as_deref() .and_then(|c| c.get(&cache_key)) .map(std::sync::Arc::clone) { let instances = block_instances(&group, base, mesh_edge, depth_map); push_block_batches(&mut out, device, queue, &chunks, &instances, mesh_edge); continue; } // A failed allocation still produces a `Buffer`; it is simply // invalid, and every later use of it reports again. Cached, it // freezes the viewport for the rest of the session because the key // keeps hitting and nothing ever rebuilds. Watch the device's error // count across the build and refuse to cache what it condemns. let errors_before = super::gpu_errors_seen(); let (segments, mut constant) = emit_wire_native(source, 0, color, 0.0); if segments.is_empty() { continue; } constant.draw_depth = 0.0; let records: Vec = segments .iter() .map(|segment| BlockWireVertex { pos_a: segment.pos_a, pos_a_low: segment.pos_a_low, pos_b: segment.pos_b, pos_b_low: segment.pos_b_low, distances: [segment.distance_a, segment.distance_b], taper_ratio: segment.taper_ratio, }) .collect(); // Queue uploads avoid mapping a failed allocation. let const_buffer = super::gpu_upload::upload_buffer( device, queue, "block_wire.const", std::slice::from_ref(&constant), wgpu::BufferUsages::UNIFORM, ); // Bound uploads so a block-heavy drawing cannot ask a low-VRAM // device for one multi-hundred-MB buffer. let chunks: Vec = if mode.uses_storage() { // One record per segment. A storage chunk is bounded by the // binding size as well as the buffer budget, and needs no // grouping: the shader indexes segments directly. let max_records = super::gpu_budget::max_storage_elements::(device); records .chunks(max_records) .map(|chunk| { let segments = super::gpu_upload::upload_buffer( device, queue, "block_wire.segments", chunk, wgpu::BufferUsages::STORAGE, ); BlockGeometryChunk { vertex_buffer: None, vertex_count: chunk.len() as u32 * 6, gpu_bytes: std::mem::size_of_val(chunk) as u64, bind_group: std::sync::Arc::new(device.create_bind_group( &wgpu::BindGroupDescriptor { label: Some("block_wire.const.bg"), layout: const_bgl, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: const_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, resource: segments.as_entire_binding(), }, ], }, )), } }) .collect() } else { // Six copies per segment, one per corner of its quad, without // splitting a segment's six vertices across two buffers. let mut vertices = Vec::with_capacity(records.len() * 6); for record in &records { vertices.extend_from_slice(&[*record; 6]); } let max_verts = super::gpu_budget::max_elements_grouped::(device, 6); let shared = std::sync::Arc::new(device.create_bind_group( &wgpu::BindGroupDescriptor { label: Some("block_wire.const.bg"), layout: const_bgl, entries: &[wgpu::BindGroupEntry { binding: 0, resource: const_buffer.as_entire_binding(), }], }, )); vertices .chunks(max_verts) .map(|chunk| BlockGeometryChunk { gpu_bytes: std::mem::size_of_val(chunk) as u64, vertex_buffer: Some(super::gpu_upload::upload_buffer( device, queue, "block_wire.vertices", chunk, wgpu::BufferUsages::VERTEX, )), vertex_count: chunk.len() as u32, bind_group: std::sync::Arc::clone(&shared), }) .collect() }; uploaded_segments += records.len(); uploaded_bytes += records.len() * std::mem::size_of::() * if mode.uses_storage() { 1 } else { 6 }; let chunks = std::sync::Arc::new(chunks); let built_clean = super::gpu_errors_seen() == errors_before; if built_clean { if let Some(cache) = cache.as_deref_mut() { cache.insert(cache_key, std::sync::Arc::clone(&chunks)); } } else { // Draw this frame with what we have — a degraded frame beats a // blank one — but leave the cache clean so the next frame tries // again. `live_keys` still holds the key, so the retain below // does not evict a good entry from an earlier frame. poisoned += 1; } let instances = block_instances(&group, base, mesh_edge, depth_map); push_block_batches(&mut out, device, queue, &chunks, &instances, mesh_edge); } // Release geometry that this build no longer uses. if let Some(cache) = cache { cache.retain(|key, _| live_keys.contains(key)); } if crate::perf::enabled() && uploaded_segments > 0 { crate::perf_record!( "[perf] block-wire-geometry mode={} definitions={} segments={} bytes={} \ bytes_if_packed={} poisoned={}", if mode.uses_storage() { "storage" } else { "packed" }, live_keys.len(), uploaded_segments, uploaded_bytes, uploaded_segments * std::mem::size_of::() * 6, poisoned, ); } out } } /// Placements and draw depths relative to the group's first instance. fn block_instances( group: &[&WireModel], base: crate::scene::model::instance_model::RenderInstance, mesh_edge: bool, depth_map: &rustc_hash::FxHashMap, ) -> Vec { 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() } /// One batch per (geometry chunk × instance chunk). Instance buffers are built /// once and shared across geometry chunks rather than re-uploaded per chunk. fn push_block_batches( out: &mut Vec, device: &wgpu::Device, queue: &wgpu::Queue, chunks: &[BlockGeometryChunk], instances: &[BlockWireInstance], mesh_edge: bool, ) { let max_instances = super::gpu_budget::max_elements::(device); let instance_chunks: Vec<(wgpu::Buffer, u32)> = instances .chunks(max_instances) .map(|chunk| { ( instance_buffer(device, queue, "block_wire.instances", chunk), chunk.len() as u32, ) }) .collect(); for chunk in chunks { for (instance_buffer, instance_count) in &instance_chunks { out.push(BlockWireGpu { vertex_buffer: chunk.vertex_buffer.clone(), instance_buffer: instance_buffer.clone(), vertex_count: chunk.vertex_count, instance_count: *instance_count, is_3d_mesh_edge: mesh_edge, const_bind_group: std::sync::Arc::clone(&chunk.bind_group), }); } } } /// Upload one constant chunk; its instances use chunk-local wire IDs. fn build_const_bind_group( device: &wgpu::Device, queue: &wgpu::Queue, bgl: &wgpu::BindGroupLayout, consts: &[WireConst], ) -> std::sync::Arc { // wgpu rejects zero-sized buffers; pad with one zeroed record when empty. let one = [::zeroed()]; let data: &[WireConst] = if consts.is_empty() { &one } else { consts }; let buf = super::gpu_upload::upload_buffer( device, queue, "wire_const.buf", data, 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 { fn upload_native( device: &wgpu::Device, queue: &wgpu::Queue, const_bgl: &wgpu::BindGroupLayout, mut instances: Vec, consts: &[WireConst], mesh_edge: bool, label: &str, ) -> Vec { let max_consts = super::gpu_budget::max_storage_elements::(device); let max_instances = super::gpu_budget::max_elements::(device); let mut out = Vec::new(); let mut remaining = instances.as_mut_slice(); for (group, constants) in consts.chunks(max_consts).enumerate() { let base = group * max_consts; let count = remaining.partition_point(|item| (item.wire_id as usize) < base + constants.len()); let (items, rest) = remaining.split_at_mut(count); remaining = rest; if items.is_empty() { continue; } for item in items.iter_mut() { item.wire_id -= base as u32; } debug_assert!(items .iter() .all(|item| (item.wire_id as usize) < constants.len())); let bind_group = build_const_bind_group(device, queue, const_bgl, constants); for chunk in items.chunks(max_instances) { out.push(Self { instance_buffer: instance_buffer(device, queue, label, chunk), first_instance: 0, instance_count: chunk.len() as u32, is_3d_mesh_edge: mesh_edge, const_bind_group: Some(bind_group.clone()), }); } } out } /// Build a small selection/hover overlay from borrowed resident wires while /// overriding their colour. Avoids deep-cloning every point/text/fill array /// of a large selected polyline or block before packing the overlay. pub fn from_highlight_refs( device: &wgpu::Device, queue: &wgpu::Queue, wires: &[&WireModel], color: [f32; 4], depth_map: &rustc_hash::FxHashMap, const_bgl: Option<&wgpu::BindGroupLayout>, ) -> Vec { if let Some(const_bgl) = const_bgl { use crate::par::prelude::*; let per: Vec<(Vec, WireConst)> = wires .par_iter() .enumerate() .map(|(idx, &wire)| { emit_wire_native(wire, idx as u32, color, wire_draw_depth(wire, depth_map)) }) .collect(); let mut instances = Vec::with_capacity(per.iter().map(|(items, _)| items.len()).sum()); let mut consts = Vec::with_capacity(per.len()); for (mut items, constant) in per { instances.append(&mut items); consts.push(constant); } if instances.is_empty() { return Vec::new(); } return Self::upload_native( device, queue, const_bgl, instances, &consts, false, "wire.highlight.ibuf", ); } let _ = const_bgl; let max_packed_instances = super::gpu_budget::max_elements::(device); use crate::par::prelude::*; let per: Vec> = wires .par_iter() .map(|wire| { emit_wire_packed(wire, color, wire_draw_depth(wire, depth_map)) }) .collect(); let mut instances = Vec::with_capacity(per.iter().map(Vec::len).sum()); for mut items in per { instances.append(&mut items); } instances .chunks(max_packed_instances) .map(|chunk| Self { instance_buffer: instance_buffer( device, queue, "wire.highlight.compat.ibuf", chunk, ), first_instance: 0, instance_count: chunk.len() as u32, is_3d_mesh_edge: false, const_bind_group: None, }) .collect() } /// Upload borrowed wires when a partition exceeds the arena budget. pub fn from_run_refs( device: &wgpu::Device, queue: &wgpu::Queue, wires: &[&WireModel], depth_map: &rustc_hash::FxHashMap, mesh_edge: bool, const_bgl: Option<&wgpu::BindGroupLayout>, ) -> Vec { let Some(const_bgl) = const_bgl else { let max_instances = super::gpu_budget::max_elements::(device); use crate::par::prelude::*; let per: Vec> = wires .par_iter() .map(|&wire| { let depth = if mesh_edge { 0.0 } else { wire_draw_depth(wire, depth_map) }; emit_wire_packed(wire, wire.color, depth) }) .collect(); let mut instances = Vec::with_capacity(per.iter().map(Vec::len).sum()); for mut items in per { instances.append(&mut items); } return instances .chunks(max_instances) .map(|chunk| Self { instance_buffer: instance_buffer( device, queue, "wire.run.hybrid.compat.ibuf", chunk, ), first_instance: 0, instance_count: chunk.len() as u32, is_3d_mesh_edge: mesh_edge, const_bind_group: None, }) .collect(); }; use crate::par::prelude::*; let per: Vec<(Vec, WireConst)> = wires .par_iter() .enumerate() .map(|(idx, &wire)| { let depth = if mesh_edge { 0.0 } else { wire_draw_depth(wire, depth_map) }; emit_wire_native(wire, idx as u32, wire.color, depth) }) .collect(); let mut instances: Vec = Vec::with_capacity(per.iter().map(|(items, _)| items.len()).sum()); let mut consts = Vec::with_capacity(per.len()); for (mut items, constant) in per { instances.append(&mut items); consts.push(constant); } if instances.is_empty() { return Vec::new(); } Self::upload_native( device, queue, const_bgl, instances, &consts, mesh_edge, "wire.run.hybrid.ibuf", ) } /// Upload a run of wires in submission order, within the buffer budget. /// /// Unlike [`from_batch`], instance order is **guaranteed** to follow wire /// order (parallel `collect` is index-ordered; the flatten is sequential). /// The main wire pass depends on that — depth-biased overlap *and* alpha /// blending both resolve in submission order, so a reorder would change the /// image for transparent / coincident wires. pub fn from_run( device: &wgpu::Device, queue: &wgpu::Queue, wires: &[WireModel], depth_map: &rustc_hash::FxHashMap, mesh_edge: bool, const_bgl: Option<&wgpu::BindGroupLayout>, ) -> Vec { if let Some(const_bgl) = const_bgl { use crate::par::prelude::*; // Indexed collection preserves transparent and coincident wire order. let per: Vec<(Vec, WireConst)> = wires .par_iter() .enumerate() .map(|(idx, w)| { // 3D mesh outline edges are real geometry occluded by true // depth — they must NOT take the draw-order z-bias (which // pulls 2D wires toward the camera), or the hidden edges of a // small / distant mesh peek through its own shaded fill. let dd = if mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) }; emit_wire_native(w, idx as u32, w.color, dd) }) .collect(); let mut instances: Vec = Vec::with_capacity(per.iter().map(|(v, _)| v.len()).sum()); let mut consts: Vec = Vec::with_capacity(per.len()); for (mut v, c) in per { instances.append(&mut v); consts.push(c); } if instances.is_empty() { return vec![]; } return Self::upload_native( device, queue, const_bgl, instances, &consts, mesh_edge, "wire.run.ibuf", ); } let _ = const_bgl; let max_packed_instances = super::gpu_budget::max_elements::(device); let per: Vec> = wires .iter() .map(|w| { let dd = if mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) }; emit_wire_packed(w, w.color, dd) }) .collect(); let mut instances: Vec = 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_packed_instances) .map(|chunk| { let buf = instance_buffer(device, queue, "wire.run.compat.ibuf", chunk); Self { instance_buffer: buf, first_instance: 0, instance_count: chunk.len() as u32, is_3d_mesh_edge: mesh_edge, const_bind_group: None, } }) .collect() } /// Upload wires in chunks that retain each wire’s color and pattern. pub fn from_batch( device: &wgpu::Device, queue: &wgpu::Queue, wires: &[WireModel], depth_map: &rustc_hash::FxHashMap, const_bgl: Option<&wgpu::BindGroupLayout>, ) -> Vec { let total_segs: usize = wires.iter().map(|w| w.points.len().saturating_sub(1)).sum(); if total_segs == 0 { return vec![]; } if let Some(const_bgl) = const_bgl { 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, 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 = Vec::with_capacity(per.iter().map(|(v, _)| v.len()).sum()); let mut consts: Vec = Vec::with_capacity(per.len()); for (mut v, c) in per { instances.append(&mut v); consts.push(c); } if instances.is_empty() { return vec![]; } return Self::upload_native( device, queue, const_bgl, instances, &consts, false, "wire.batch.ibuf", ); } let _ = const_bgl; let max_packed_instances = super::gpu_budget::max_elements::(device); let instances: Vec = wires .iter() .flat_map(|w| emit_wire_packed(w, w.color, wire_draw_depth(w, depth_map))) .collect(); if instances.is_empty() { return vec![]; } instances .chunks(max_packed_instances) .enumerate() .map(|(i, chunk)| { let label = format!("wire.batch.compat.ibuf.{i}"); let instance_buffer = instance_buffer(device, queue, &label, chunk); Self { instance_buffer, first_instance: 0, instance_count: chunk.len() as u32, is_3d_mesh_edge: false, const_bind_group: None, } }) .collect() } } #[cfg(test)] mod block_wire_storage_tests { use super::BlockWireVertex; fn module(source: &str) -> naga::Module { naga::front::wgsl::parse_str(source).expect("shader parses") } fn validate(module: &naga::Module) -> naga::valid::ModuleInfo { naga::valid::Validator::new( naga::valid::ValidationFlags::all(), naga::valid::Capabilities::all(), ) .validate(module) .expect("shader validates") } /// A layout mismatch here is invisible until a block draws as garbage on a /// real device, so the stride is checked against the Rust record instead. #[test] fn segment_struct_matches_the_rust_record() { let module = module(include_str!("../../shaders/block_wire_storage.wgsl")); validate(&module); let mut layouter = naga::proc::Layouter::default(); layouter .update(module.to_ctx()) .expect("layouts are computable"); let (handle, _) = module .types .iter() .find(|(_, ty)| ty.name.as_deref() == Some("Segment")) .expect("Segment is declared"); assert_eq!( layouter[handle].size as usize, std::mem::size_of::(), "WGSL Segment and Rust BlockWireVertex must agree byte for byte; \ a vec3 member would pad to 16 and break this", ); assert_eq!(layouter[handle].alignment * 1u32, 4); } /// The two shaders must stay interchangeable: same entry points, same /// fragment behaviour. Only the geometry source differs. #[test] fn both_block_wire_shaders_expose_the_same_entry_points() { let packed = module(include_str!("../../shaders/block_wire.wgsl")); let storage = module(include_str!("../../shaders/block_wire_storage.wgsl")); validate(&packed); let names = |m: &naga::Module| { let mut n: Vec = m.entry_points.iter().map(|e| e.name.clone()).collect(); n.sort(); n }; assert_eq!(names(&packed), names(&storage)); } #[test] fn wire_shader_validates_with_naga() { let source = include_str!("../../shaders/wire.wgsl"); let module = naga::front::wgsl::parse_str(source).expect("wire.wgsl parses cleanly"); let mut validator = naga::valid::Validator::new( naga::valid::ValidationFlags::all(), naga::valid::Capabilities::all(), ); validator .validate(&module) .expect("wire.wgsl validates cleanly with naga"); } #[test] fn wire_indexed_shader_validates_with_naga() { let source = include_str!("../../shaders/wire_indexed.wgsl"); let module = naga::front::wgsl::parse_str(source).expect("wire_indexed.wgsl parses cleanly"); let mut validator = naga::valid::Validator::new( naga::valid::ValidationFlags::all(), naga::valid::Capabilities::all(), ); validator .validate(&module) .expect("wire_indexed.wgsl validates cleanly with naga"); } }