// 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 with `mapped_at_creation` and write `data` directly /// into the mapped slice. Skips the intermediate staging copy that /// `create_buffer_init` performs and avoids holding a second `Vec` worth of /// memory during upload — meaningful on cold open where wire buffers can run /// into the hundreds of MB. fn instance_buffer_mapped( device: &wgpu::Device, label: &str, data: &[WireInstance], ) -> wgpu::Buffer { let bytes: &[u8] = bytemuck::cast_slice(data); // wgpu rejects size-0 buffers; the renderer already guards `instance_count` // before issuing a draw, so a placeholder allocation is fine here. let size = bytes.len().max(std::mem::size_of::()) as u64; let buf = device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), size, usage: wgpu::BufferUsages::VERTEX, mapped_at_creation: true, }); { let mut view = buf.slice(..).get_mapped_range_mut(); view[..bytes.len()].copy_from_slice(bytes); } buf.unmap(); buf } // ── 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::() 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 eight scalars = 80 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. #[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 align_end: f32, pub align_total: f32, pub _pad0: f32, pub _pad1: f32, pub _pad2: 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 { 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, } #[cfg(target_arch = "wasm32")] impl WireInstance { pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> { // Offsets come from the struct layout (must match the shader location // indices in wire.wgsl). The low residuals are appended at locations // 10/11 so the existing 0-9 stay stable. 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, color) as u64, shader_location: 2, format: wgpu::VertexFormat::Unorm8x4 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_a) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_b) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, half_width) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pattern_length) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pat0) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32x4 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pat1) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32x4 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, draw_depth) as u64, shader_location: 9, format: wgpu::VertexFormat::Float32 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_a_low) as u64, shader_location: 10, format: wgpu::VertexFormat::Float32x3 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_b_low) as u64, shader_location: 11, format: wgpu::VertexFormat::Float32x3 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, align_end) as u64, shader_location: 12, format: wgpu::VertexFormat::Float32 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, align_total) as u64, shader_location: 13, format: wgpu::VertexFormat::Float32 }, ]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: ATTRS, } } } // ── GPU handle ──────────────────────────────────────────────────────────── pub struct WireGpu { pub instance_buffer: wgpu::Buffer, pub instance_count: u32, /// Paper-space bbox [x0, y0, x1, y1] for GPU scissor clipping. /// Set only for viewport-projected wires; None for regular wires. pub vp_scissor: Option<[f32; 4]>, /// `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 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>, } /// 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, (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 mut dists = vec![0.0_f32; n]; let mut has_break = false; 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; // plinegen=false: reset to 0 at the first real point after a NaN separator. dists[i] = if !wire.plinegen && !p[0].is_finite() && q[0].is_finite() { 0.0 } else { dists[i - 1] }; } else { let dx = q[0] - p[0]; let dy = q[1] - p[1]; let dz = q[2] - p[2]; dists[i] = dists[i - 1] + (dx * dx + dy * dy + dz * dz).sqrt(); } } let pat_len = wire.pattern_length; if pat_len <= 1e-6 || has_break || n < 2 { return (dists, 0.0, 0.0); } let total = dists[n - 1]; if total <= 1e-6 { return (dists, 0.0, 0.0); } // 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() } /// 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 { 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 low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) }; 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), 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, }); } 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"))] 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 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, _pad0: 0.0, _pad1: 0.0, _pad2: 0.0, }; 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 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, }); } (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). pub(crate) fn wire_draw_depth( wire: &WireModel, depth_map: &rustc_hash::FxHashMap, ) -> f32 { wire .name .parse::() .ok() .and_then(|h| depth_map.get(&h).copied()) .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 { use wgpu::util::DeviceExt; // 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 = 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 /// (or, past the 256 MB GPU limit, a few) instance buffer(s), then stamp /// the shared `scissor` / `mesh_edge` onto each so the draw loop treats the /// whole run as a single batch. /// /// 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, wires: &[WireModel], depth_map: &rustc_hash::FxHashMap, scissor: Option<[f32; 4]>, mesh_edge: bool, const_bgl: Option<&wgpu::BindGroupLayout>, ) -> Vec { const MAX_INSTANCES: usize = 268_435_456 / std::mem::size_of::(); #[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, 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![]; } 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() } #[cfg(target_arch = "wasm32")] { let _ = const_bgl; let per: Vec> = wires .iter() .map(|w| { let dd = if mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) }; emit_wire_instances(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_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() } } /// Merge multiple WireModels into GPU instance buffers, chunked to fit the /// 256 MB GPU limit. Each wire keeps its own color and pattern — they live /// per-instance. pub fn from_batch( device: &wgpu::Device, 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![]; } // GPU max buffer size is 256 MB; chunk to stay within the limit. const MAX_INSTANCES: usize = 268_435_456 / std::mem::size_of::(); #[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, 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![]; } 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 = 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() } } }