From a6d4b30dcaac4de3471c19b8b40129e88a2e1169 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 21 Jul 2026 13:06:09 +0300 Subject: [PATCH] fix(web): pack wire instance attributes under the 16-attribute cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web wire pipeline (fat-instance wire.wgsl) had grown to 17 vertex attributes as the width-band and taper fields were added; WebGL2 and WebGPU cap vertex attributes at 16, so pipeline creation failed and the web viewport drew no lines at all — UI and SDF text still worked, which is why it looked like "the app stopped working" rather than a crash (#414). Scalars now ride in packed vec4/vec2 attributes (10 locations total); the instance struct's memory layout is unchanged. Co-Authored-By: Claude Fable 5 --- src/scene/pipeline/wire_gpu.rs | 31 ++++++++++++----------- src/shaders/wire.wgsl | 45 ++++++++++++++++------------------ 2 files changed, 36 insertions(+), 40 deletions(-) diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 651fdd463..8a4563012 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -195,26 +195,25 @@ pub struct WireInstance { 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. + // 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!(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::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_half_width) as u64, shader_location: 14, format: wgpu::VertexFormat::Float32 }, - wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_hw_a) as u64, shader_location: 15, format: wgpu::VertexFormat::Float32 }, - wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_hw_b) as u64, shader_location: 16, format: wgpu::VertexFormat::Float32 }, + // dists = (distance_a, distance_b, half_width, pattern_length) + wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_a) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pat0) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, 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!(WireInstance, draw_depth) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_a_low) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32x3 }, + wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, 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!(WireInstance, world_hw_a) as u64, shader_location: 9, format: wgpu::VertexFormat::Float32x2 }, ]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, diff --git a/src/shaders/wire.wgsl b/src/shaders/wire.wgsl index e4cfe2038..ee51c2ceb 100644 --- a/src/shaders/wire.wgsl +++ b/src/shaders/wire.wgsl @@ -42,28 +42,25 @@ struct Uniforms { } @group(0) @binding(0) var u: Uniforms; +// Scalars are packed into vec4 attributes: WebGL2 / WebGPU cap vertex +// attributes at 16, and the unpacked layout had grown to 17 — the wire +// pipeline failed to build and the web viewport drew no lines at all (#414). struct InstanceIn { @location(0) pos_a: vec3, @location(1) pos_b: vec3, @location(2) color: vec4, - @location(3) distance_a: f32, - @location(4) distance_b: f32, - @location(5) half_width: f32, - @location(6) pattern_length: f32, - @location(7) pat0: vec4, - @location(8) pat1: vec4, - @location(9) draw_depth: f32, + // distance_a, distance_b, half_width, pattern_length + @location(3) dists: vec4, + @location(4) pat0: vec4, + @location(5) pat1: vec4, + // draw_depth, align_end ("A"-type end-dash length), align_total (total + // wire length), world_half_width (wide-polyline band; 0 = normal wire) + @location(6) misc: vec4, // Double-single low residuals of the endpoints. - @location(10) pos_a_low: vec3, - @location(11) pos_b_low: vec3, - // "A"-type endpoint alignment: end-dash length + total wire length. - @location(12) align_end: f32, - @location(13) align_total: f32, - // World-space half-width for a wide-polyline band. 0 = normal wire. - @location(14) world_half_width: f32, + @location(7) pos_a_low: vec3, + @location(8) pos_b_low: vec3, // Per-endpoint world half-width for a tapered band (0 = use the constant). - @location(15) world_hw_a: f32, - @location(16) world_hw_b: f32, + @location(9) taper: vec2, } // Draw-order depth bias: shifts clip-space z so 2D entities of different @@ -153,8 +150,8 @@ fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 { // A tapered band interpolates a per-endpoint world half-width across the // segment; a constant band uses `world_half_width`. Both clamp to a // half-pixel so a zoomed-out band stays a hairline instead of vanishing. - let hw_a = resolve_hw(in.world_hw_a, in.world_half_width, in.half_width); - let hw_b = resolve_hw(in.world_hw_b, in.world_half_width, in.half_width); + let hw_a = resolve_hw(in.taper.x, in.misc.w, in.dists.z); + let hw_b = resolve_hw(in.taper.y, in.misc.w, in.dists.z); let hw = mix(hw_a, hw_b, which_end); // Extend the quad longitudinally by the end half-width and let the @@ -169,7 +166,7 @@ fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 { // Smallest non-zero dash / gap element, in world units. Used by // the fragment stage to decide when the pattern's finest feature // would render below one pixel and should collapse to a solid line. - var min_elem: f32 = in.pattern_length; + var min_elem: f32 = in.dists.w; let elems = array( in.pat0.x, in.pat0.y, in.pat0.z, in.pat0.w, in.pat1.x, in.pat1.y, in.pat1.z, in.pat1.w, @@ -181,20 +178,20 @@ fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 { var out: VertexOut; out.clip_pos = final_clip; - out.clip_pos.z = out.clip_pos.z - in.draw_depth * DRAW_ORDER_BIAS * out.clip_pos.w; + out.clip_pos.z = out.clip_pos.z - in.misc.x * DRAW_ORDER_BIAS * out.clip_pos.w; out.color = in.color; // Dash arc-length, extrapolated over the cap overhang so the pattern // stays continuous through a joint. - out.distance = mix(in.distance_a, in.distance_b, which_end) + out.distance = mix(in.dists.x, in.dists.y, which_end) + ext * hw * u.world_per_pixel; out.cap = vec2(which_end * seg_len + ext * hw, hw * side); out.cap_ends = vec3(seg_len, hw_a, hw_b); - out.pattern_length = in.pattern_length; + out.pattern_length = in.dists.w; out.pat0 = in.pat0; out.pat1 = in.pat1; out.min_elem = min_elem; - out.align_end = in.align_end; - out.align_total = in.align_total; + out.align_end = in.misc.y; + out.align_total = in.misc.z; return out; }