// Hatch rendering — the single, canonical GPU hatch renderer. One draw // call for all hatches, per-instance data fetched from storage buffers in // the vertex shader. There is NO per-family cap here: every pattern line // family is uploaded (`family_count` per instance) and the fragment shader // loops over all of them, so complex baked patterns (ROOFING, GRAVEL, // CAROLINA-LEDGESTONE with dozens–hundreds of families) draw in full, // matching the PDF export. Unavailable on devices that lack vertex-stage // storage buffers. The old per-hatch renderer with the // 16-family cap now lives in `wipeout_gpu.rs` and serves only wipeouts. // // Data layout — three storage buffers fed from `HatchModel`s: // // InstanceBuffer (binding 0) : HatchInstance[] (128 B each) // color, color2, mode, gradient, // pattern angle/scale, world_origin, // boundary_offset, boundary_count, // family_offset, family_count, // dash_offset, dash_count, aabb, // visibility flag (CPU writes; GPU // skip) // BoundaryBuffer (binding 1) : vec4[] (all boundary // verts concatenated; NaN markers // preserved as separators just like // the per-hatch path) // FamilyBuffer (binding 2) : LineFamilyGpu[] (all line // families concatenated) // DashBuffer (binding 3) : f32[] (all dash // lengths concatenated) // // The vertex buffer holds each hatch's tessellated boundary triangles as // per-vertex local-space positions (`local_xy`) plus a per-vertex // `instance_index` (u32 attribute) — instance_index lets us avoid relying // on `@builtin(instance_index)` for portability. An instance whose // boundary failed to tessellate instead gets an AABB quad here, with // `poly_test == 1` on its `HatchInstance` so the fragment shader still // runs the `in_polygon` test to clip the quad to the real shape; on the // tessellated fast path (`poly_test == 0`) the triangles already bound // the fill and the fragment shader skips that test. When the visibility // flag is 0, the vertex shader returns a degenerate position and the // fragment shader runs zero times for that instance. // // Two storage usages — vertex shader reads InstanceBuffer + Boundary // for the AABB / boundary range; fragment shader reads // InstanceBuffer + Boundary + Family + Dash. Both stages share group // 1 with `read_only` access. use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily}; use iced::wgpu; use iced::wgpu::util::DeviceExt; // ── GPU structs ──────────────────────────────────────────────────────────── // // Layout matches the WGSL `HatchInstance` exactly. `repr(C)` + manual // padding keeps WGSL's 16-byte alignment rules satisfied for arrays of // this struct. #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub struct HatchInstance { pub color: [f32; 4], // 0 pub color2: [f32; 4], // 16 (gradient end) pub aabb: [f32; 4], // 32 (local-space xmin,ymin,xmax,ymax) pub world_origin: [f32; 2], // 48 (anchor high half, added back in VS) pub world_origin_low: [f32; 2], // 56 (anchor low residual — double-single) pub angle_offset: f32, // 64 pub scale: f32, // 68 pub grad_cos: f32, // 72 pub grad_sin: f32, // 76 pub grad_min: f32, // 80 pub grad_range: f32, // 84 pub mode: u32, // 88 (0=pattern, 1=solid, 2=gradient) pub visible: u32, // 92 (CPU sets to 0 to skip) pub boundary_offset: u32, // 96 (first boundary vert index) pub boundary_count: u32, // 100 pub family_offset: u32, // 104 pub family_count: u32, // 108 /// Signed draw-order depth (-1,1); 0.0 = neutral. Applied as a clip-z /// bias in the vertex shader so this fill orders against other types. pub draw_depth: f32, // 112 /// 1 = run the per-fragment `in_polygon` boundary test (fallback path for /// an instance whose boundary failed to tessellate); 0 = the rasterized /// triangles already bound the fill, skip the test. pub poly_test: u32, // 116 /// Gradient shape (`GradientKind::shader_kind`), bit 4 = inverted stops. pub grad_kind: u32, // 120 pub _pad2: u32, // 124 } const _: () = assert!(std::mem::size_of::() == 128); /// Split a hatch's f64 world-origin anchor into double-single (high, low) f32 /// pairs so the GPU keeps sub-unit precision at UTM-scale coordinates. #[inline] fn split_origin_ds(o: [f64; 2]) -> ([f32; 2], [f32; 2]) { let hx = o[0] as f32; let hy = o[1] as f32; ([hx, hy], [(o[0] - hx as f64) as f32, (o[1] - hy as f64) as f32]) } /// Mirrors the per-family struct used by the existing per-hatch shader, /// but the dash slice lives in a separate concatenated DashBuffer (the /// old shader had it embedded). `dash_offset` / `dash_count` index into /// that flat array. #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub struct LineFamilyGpu { pub cos_a: f32, // 0 pub sin_a: f32, // 4 pub x0: f32, // 8 pub y0: f32, // 12 pub dx: f32, // 16 pub dy: f32, // 20 pub perp_step: f32, // 24 pub along_step: f32, // 28 pub line_width: f32, // 32 pub period: f32, // 36 pub n_dashes: u32, // 40 pub dash_offset: u32, // 44 } const _: () = assert!(std::mem::size_of::() == 48); /// Per-vertex data — tessellated boundary triangles in local (world_origin- /// relative) space, each carrying its instance index (avoids relying on /// `@builtin(instance_index)` across backends). Instances whose boundary /// failed to tessellate emit an AABB quad here instead (see `build`). #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub struct HatchVertex { pub local_xy: [f32; 2], // 0 — local-space position pub instance_index: u32, // 8 — index into InstanceBuffer } impl HatchVertex { pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> { wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Vertex, attributes: &[ wgpu::VertexAttribute { offset: 0, shader_location: 0, format: wgpu::VertexFormat::Float32x2, }, wgpu::VertexAttribute { offset: 8, shader_location: 1, format: wgpu::VertexFormat::Uint32, }, ], } } } // ── Batch builder ────────────────────────────────────────────────────────── /// Pack a list of `HatchModel`s into the four concatenated storage /// buffers + the per-vertex buffer needed by `hatch.wgsl`. /// Returns `None` when the input slice is empty (caller skips the /// hatch render pass entirely). pub struct HatchGpu { pub vertex_buffer: wgpu::Buffer, pub vertex_count: u32, // The four storage buffers below are referenced via `bind_group` — // dropping them would invalidate it, but the bind group is the // only direct consumer. Keep them as fields to keep ownership in // one place; `#[allow(dead_code)]` silences the read-never warning. #[allow(dead_code)] pub instance_buffer: wgpu::Buffer, #[allow(dead_code)] pub boundary_buffer: wgpu::Buffer, #[allow(dead_code)] pub family_buffer: wgpu::Buffer, #[allow(dead_code)] pub dash_buffer: wgpu::Buffer, /// Per-instance visibility flag (1=draw, 0=skip). Stored in its /// own small storage buffer so per-frame updates don't have to /// touch the large `instance_buffer`. Vertex shader reads /// `visibility[instance_index]` — when 0 it emits an out-of-NDC /// clip position so the GPU clips the primitive before the /// fragment stage runs. pub visibility_buffer: wgpu::Buffer, pub bind_group: wgpu::BindGroup, #[allow(dead_code)] pub instance_count: u32, /// CPU mirror — `update_visibility` re-uploads this whole slice /// when any flag changes. ~4 B per hatch, so 40 KB / 10 k hatches /// per pan tick. Far cheaper than touching the 128 B-per-instance /// data. pub visibility: Vec, /// CPU-side mirror of each instance's local-space AABB (world- /// offset-subtracted, world_origin already added back). Used by /// `compute_hatch_lod` to evaluate the sub-pixel + frustum cull /// without reading back from the GPU. pub instance_aabbs: Vec<[f32; 4]>, } impl HatchGpu { /// One-time build from the full hatch list. Re-uploaded only when /// `geometry_epoch` advances (mirrors the existing per-hatch /// upload trigger). Per-frame visibility flips go through /// [`upload_visibility`]. pub fn build( device: &wgpu::Device, bgl: &wgpu::BindGroupLayout, hatches: &[HatchModel], ) -> Option { if hatches.is_empty() { return None; } let mut instances: Vec = Vec::with_capacity(hatches.len()); let mut boundary: Vec<[f32; 4]> = Vec::new(); let mut families: Vec = Vec::new(); let mut dashes: Vec = Vec::new(); for h in hatches { let boundary_offset = boundary.len() as u32; for &[x, y] in h.boundary.iter() { // NaN sub-loop separators become the finite sentinel on the // GPU: Intel drivers fold the shader NaN self-compare to // `true`, turning separators into vertices and bleeding the // fill outside its boundary (#386, #416). if x.is_finite() && y.is_finite() { boundary.push([x, y, 0.0, 0.0]); } else { let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP; boundary.push([s, s, 0.0, 0.0]); } } let boundary_count = boundary.len() as u32 - boundary_offset; let family_offset = families.len() as u32; let mut family_count = 0u32; let mut grad_kind = 0u32; let (mode, color2, grad_cos, grad_sin, grad_min, grad_range) = match &h.pattern { HatchPattern::Solid => (1u32, [0.0; 4], 0.0, 0.0, 0.0, 1.0), HatchPattern::Gradient { angle_deg, color2, kind, invert } => { grad_kind = kind.shader_kind() | if *invert { 16 } else { 0 }; if kind.radial() { // Radial fill: the boundary is stored relative to its // centre (`world_origin`), so the centre is the local // origin; radius = the farthest boundary vertex. mode 3. let radius = radial_radius(&h.boundary); (3u32, *color2, 0.0, 0.0, 0.0, radius) } else { let r = angle_deg.to_radians(); // Gradient projection range (proj_min / proj_range) — // computed at upload time, identical to per-hatch path. let (gmin, gmax) = boundary_projection_range(&h.boundary, r); let grange = (gmax - gmin).max(1.0); (2u32, *color2, r.cos(), r.sin(), gmin, grange) } } HatchPattern::Pattern(fams) => { for fam in fams { let dash_offset = dashes.len() as u32; for &d in &fam.dashes { dashes.push(d); } let n_dashes = (dashes.len() as u32 - dash_offset).min(u32::MAX); // QCAD PAT local-frame convention (mirrors // `build_family_batch` in hatch_gpu.rs): `dy` is // the perpendicular spacing, `dx` is the along-line // phase shift — both in family-local coords. The // shader applies cos_off/sin_off to rotate them. let perp_step = fam.dy; let along_step = fam.dx; // Screen-space derivative drives 1-px line width // in the shader; this stored field is unused. let line_width = 0.0_f32; let period: f32 = fam.dashes.iter().map(|d| d.abs()).sum(); families.push(LineFamilyGpu { cos_a: fam.angle_deg.to_radians().cos(), sin_a: fam.angle_deg.to_radians().sin(), x0: fam.x0, y0: fam.y0, dx: fam.dx, dy: fam.dy, perp_step, along_step, line_width, period: if n_dashes > 0 { period } else { 0.0 }, n_dashes, dash_offset, }); family_count += 1; } (0u32, [0.0; 4], 0.0, 0.0, 0.0, 1.0) } }; // Boundary AABB in local space (matches the corner quad // emitted by the vertex shader). The verts are already in // `world_origin`-relative coords (see scene/mod.rs hatch // packing), so this AABB lives in that frame. let mut min_x = f32::INFINITY; let mut min_y = f32::INFINITY; let mut max_x = f32::NEG_INFINITY; let mut max_y = f32::NEG_INFINITY; for &[x, y] in h.boundary.iter() { if x.is_finite() && y.is_finite() { if x < min_x { min_x = x; } if y < min_y { min_y = y; } if x > max_x { max_x = x; } if y > max_y { max_y = y; } } } if !min_x.is_finite() { // Empty / all-NaN — skip but keep the slot so indices // stay in lockstep with the input list (visibility=0). let (wo_hi, wo_lo) = split_origin_ds(h.world_origin); instances.push(HatchInstance { color: h.color, color2, aabb: [0.0, 0.0, 0.0, 0.0], world_origin: wo_hi, world_origin_low: wo_lo, angle_offset: h.angle_offset, scale: h.scale.max(1e-6), grad_cos, grad_sin, grad_min, grad_range, mode, visible: 0, boundary_offset, boundary_count, family_offset, family_count, draw_depth: h.draw_depth, poly_test: 1, grad_kind, _pad2: 0, }); continue; } // Pad the AABB so the quad covers any pattern halo + the // family origin. Mirrors the per-hatch shader's quad sizing // logic — `diag * 0.8 + max_spacing * 2 * scale`. let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt(); // `perp_step.abs()` per family — uses the same QCAD local- // frame convention as `LineFamilyGpu.perp_step` above so // the quad padding matches what the shader will sample. let max_spacing = match &h.pattern { HatchPattern::Pattern(fs) => fs .iter() .map(|f| f.dy.abs()) .fold(0.0f32, f32::max), _ => 5.0, }; let pad = (diag * 0.8 + max_spacing * 2.0 * h.scale).max(1.0); let (wo_hi, wo_lo) = split_origin_ds(h.world_origin); instances.push(HatchInstance { color: h.color, color2, aabb: [min_x - pad, min_y - pad, max_x + pad, max_y + pad], world_origin: wo_hi, world_origin_low: wo_lo, angle_offset: h.angle_offset, scale: h.scale.max(1e-6), grad_cos, grad_sin, grad_min, grad_range, mode, visible: 1, boundary_offset, boundary_count, family_offset, family_count, draw_depth: h.draw_depth, // Always resolve inside/outside per fragment on the GPU (the // boundary is uploaded below). Dropping CPU pre-triangulation // keeps the vertex buffer at 6 verts/hatch regardless of // boundary complexity, so a hatch-dense drawing can't overflow // the device buffer limit. Each instance draws its AABB quad. poly_test: 1, grad_kind, _pad2: 0, }); } // Empty fallbacks — storage buffers can't be zero-sized. if boundary.is_empty() { boundary.push([0.0; 4]); } if families.is_empty() { families.push(LineFamilyGpu::default_filler()); } if dashes.is_empty() { dashes.push(0.0); } // Vertex buffer — one AABB quad (two triangles, BL,BR,TL, BR,TR,TL) per // instance; the GPU shader clips each fragment to the boundary via the // in_polygon test (poly_test == 1). 6 verts/hatch, independent of // boundary complexity, so the buffer can never overflow the device // limit no matter how dense the drawing. let mut verts: Vec = Vec::with_capacity(instances.len() * 6); for (i, inst) in instances.iter().enumerate() { let [xmin, ymin, xmax, ymax] = inst.aabb; let quad = [ [xmin, ymin], [xmax, ymin], [xmin, ymax], [xmax, ymin], [xmax, ymax], [xmin, ymax], ]; for c in quad { verts.push(HatchVertex { local_xy: c, instance_index: i as u32 }); } } let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.vertex"), contents: bytemuck::cast_slice(&verts), usage: wgpu::BufferUsages::VERTEX, }); let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.instances"), contents: bytemuck::cast_slice(&instances), usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, }); let boundary_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.boundary"), contents: bytemuck::cast_slice(&boundary), usage: wgpu::BufferUsages::STORAGE, }); let family_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.families"), contents: bytemuck::cast_slice(&families), usage: wgpu::BufferUsages::STORAGE, }); let dash_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.dashes"), contents: bytemuck::cast_slice(&dashes), usage: wgpu::BufferUsages::STORAGE, }); let visibility: Vec = instances.iter().map(|i| i.visible).collect(); let visibility_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.visibility"), contents: bytemuck::cast_slice(&visibility), usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("hatch.bg"), layout: bgl, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: instance_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, resource: boundary_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 2, resource: family_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 3, resource: dash_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 4, resource: visibility_buffer.as_entire_binding(), }, ], }); // CPU AABB mirror — local-space rect with world_origin added // back, ready for `aabb_offscreen` / `aabb_below_pixel` to // compare against the camera view_proj. let instance_aabbs: Vec<[f32; 4]> = instances .iter() .map(|i| { let ox = i.world_origin[0]; let oy = i.world_origin[1]; [ i.aabb[0] + ox, i.aabb[1] + oy, i.aabb[2] + ox, i.aabb[3] + oy, ] }) .collect(); Some(Self { vertex_buffer, vertex_count: verts.len() as u32, instance_buffer, boundary_buffer, family_buffer, dash_buffer, visibility_buffer, bind_group, instance_count: instances.len() as u32, visibility, instance_aabbs, }) } /// Push the CPU `visibility` slice to GPU. Call when any /// element changes (typically per-frame from compute_hatch_lod). pub fn upload_visibility(&self, queue: &wgpu::Queue) { queue.write_buffer( &self.visibility_buffer, 0, bytemuck::cast_slice(&self.visibility), ); } /// Group-1 bind group layout — shared by the pipeline so it can be /// constructed once at startup. All four bindings are read-only /// storage and visible to both VS (AABB+visibility lookup) and FS /// (boundary / family / dash sampling). #[allow(dead_code)] pub fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { let entry = |binding: u32| wgpu::BindGroupLayoutEntry { binding, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 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("hatch.bgl"), entries: &[entry(0), entry(1), entry(2), entry(3), entry(4)], }) } } impl LineFamilyGpu { fn default_filler() -> Self { Self { cos_a: 1.0, sin_a: 0.0, x0: 0.0, y0: 0.0, dx: 1.0, dy: 0.0, perp_step: 1.0, along_step: 1.0, line_width: 0.0, period: 0.0, n_dashes: 0, dash_offset: 0, } } } // ── Helpers ─────────────────────────────────────────────────────────────── /// Project every boundary vertex onto the gradient direction /// `(cos θ, sin θ)` and return the (min, max) projection. Used to set /// up the gradient's normalized parameter range. Same math as the /// per-hatch path; duplicated here to keep this module self-contained. fn boundary_projection_range(boundary: &[[f32; 2]], theta: f32) -> (f32, f32) { let (cs, sn) = (theta.cos(), theta.sin()); let mut lo = f32::INFINITY; let mut hi = f32::NEG_INFINITY; for &[x, y] in boundary { if !x.is_finite() || !y.is_finite() { continue; } let p = x * cs + y * sn; if p < lo { lo = p; } if p > hi { hi = p; } } if !lo.is_finite() { return (0.0, 1.0); } (lo, hi) } /// Radius of a radial gradient — the distance from the boundary centre (the /// local origin, since vertices are stored relative to it) to the farthest /// vertex, so `t = 1` (the end colour) reaches the corners. fn radial_radius(boundary: &[[f32; 2]]) -> f32 { boundary .iter() .filter(|p| p[0].is_finite() && p[1].is_finite()) .map(|p| (p[0] * p[0] + p[1] * p[1]).sqrt()) .fold(0.0_f32, f32::max) .max(1.0) } // PatFamily is re-exported by hatch_model so we don't need to import // it explicitly anywhere else — but rust needs the type referenced to // confirm the layout assumption above. #[allow(dead_code)] fn _assert_patfamily_fields(f: &PatFamily) -> (f32, f32, f32) { (f.angle_deg, f.x0, f.y0) }