fix(hatch): clip fill with kernel mesh
Resolve boundary topology before rendering so corner-aligned pattern rows cannot flip shader parity. Chunk indexed geometry to device buffer limits and retain the texture compatibility transport. Refs #489
This commit is contained in:
parent
448869ac35
commit
5b9fffdfba
9 changed files with 259 additions and 416 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -891,7 +891,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1b84d6c#1b84d6c926d8053690a9975ccad6814800790008"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=8d05593#8d05593221cf6158076c38e98fef54ab767b81d9"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ rfd = "0.17"
|
|||
clap = { version = "4", features = ["derive"] }
|
||||
env_logger = "0.11"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4736846", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1b84d6c", features = ["acis", "offset"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "8d05593", features = ["acis", "offset"] }
|
||||
acadifc = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "47b5603", optional = true }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
// HatchModel — CPU-side hatch fill data; rendered entirely on the GPU.
|
||||
//
|
||||
// The boundary is a closed polygon in world XY coordinates.
|
||||
// The GPU fragment shader performs point-in-polygon and hatch-line tests so
|
||||
// no line geometry is ever tessellated on the CPU.
|
||||
// CPU-side hatch fill data. The kernel triangulates the boundary and the GPU
|
||||
// evaluates the pattern inside that mesh.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -207,6 +204,40 @@ pub struct HatchModel {
|
|||
}
|
||||
|
||||
impl HatchModel {
|
||||
/// Indexed local-space fill mesh with even-odd loop containment.
|
||||
pub(crate) fn fill_mesh(&self) -> (Vec<[f32; 2]>, Vec<u32>) {
|
||||
let mut rings = Vec::new();
|
||||
let mut ring = Vec::new();
|
||||
for &[x, y] in self.boundary.iter() {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
ring.push([x as f64, y as f64]);
|
||||
} else if ring.len() >= 3 {
|
||||
rings.push(std::mem::take(&mut ring));
|
||||
} else {
|
||||
ring.clear();
|
||||
}
|
||||
}
|
||||
if ring.len() >= 3 {
|
||||
rings.push(ring);
|
||||
}
|
||||
|
||||
let (points, triangles) = cadkernel::geom2d::triangulate_rings(&rings);
|
||||
let vertices = points
|
||||
.into_iter()
|
||||
.map(|[x, y]| [x as f32, y as f32])
|
||||
.collect();
|
||||
let mut indices = Vec::with_capacity(triangles.len() * 3);
|
||||
for triangle in triangles {
|
||||
for index in triangle {
|
||||
let Ok(index) = u32::try_from(index) else {
|
||||
return (Vec::new(), Vec::new());
|
||||
};
|
||||
indices.push(index);
|
||||
}
|
||||
}
|
||||
(vertices, indices)
|
||||
}
|
||||
|
||||
/// CPU-side rasteriser for `HatchPattern::Pattern` — produces the line
|
||||
/// segments inside the boundary so non-GPU consumers (PDF export,
|
||||
/// `paper_canvas`, print preview) can draw the actual pattern instead
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@ impl DeviceCapabilities {
|
|||
/// Indexed wires read one per-wire constants storage buffer.
|
||||
const WIRE_STORAGE_BINDINGS: u32 = 1;
|
||||
|
||||
/// Batched hatch uses five storage bindings in one shader stage:
|
||||
/// instances, boundaries, families, dashes, and visibility.
|
||||
const HATCH_STORAGE_BINDINGS: u32 = 5;
|
||||
/// Batched hatch stores instances, families, dashes, and visibility.
|
||||
const HATCH_STORAGE_BINDINGS: u32 = 4;
|
||||
|
||||
pub fn detect(device: &wgpu::Device) -> Self {
|
||||
Self::from_limits(&device.limits())
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ impl HatchBackendKind {
|
|||
|
||||
enum HatchBackend {
|
||||
Storage {
|
||||
resident: Option<StorageHatchBatch>,
|
||||
preview: Option<StorageHatchBatch>,
|
||||
resident: Vec<StorageHatchBatch>,
|
||||
preview: Vec<StorageHatchBatch>,
|
||||
},
|
||||
Texture {
|
||||
resident: Vec<TextureHatch>,
|
||||
|
|
@ -137,8 +137,8 @@ impl HatchGpu {
|
|||
});
|
||||
let backend = match backend_kind {
|
||||
HatchBackendKind::Storage => HatchBackend::Storage {
|
||||
resident: None,
|
||||
preview: None,
|
||||
resident: Vec::new(),
|
||||
preview: Vec::new(),
|
||||
},
|
||||
HatchBackendKind::Texture => HatchBackend::Texture {
|
||||
resident: Vec::new(),
|
||||
|
|
@ -223,28 +223,29 @@ impl HatchGpu {
|
|||
queue: &wgpu::Queue,
|
||||
mut is_visible: impl FnMut([f32; 4]) -> bool,
|
||||
) -> usize {
|
||||
let HatchBackend::Storage {
|
||||
resident: Some(batch),
|
||||
..
|
||||
} = &mut self.backend
|
||||
let HatchBackend::Storage { resident, .. } = &mut self.backend
|
||||
else {
|
||||
return 0;
|
||||
};
|
||||
for index in 0..batch.unique_source_count {
|
||||
batch.source_visibility[index] = u32::from(is_visible(batch.source_aabbs[index]));
|
||||
let mut updated = 0;
|
||||
for batch in resident {
|
||||
for index in 0..batch.unique_source_count {
|
||||
batch.source_visibility[index] = u32::from(is_visible(batch.source_aabbs[index]));
|
||||
}
|
||||
for index in batch.unique_source_count..batch.source_visibility.len() {
|
||||
batch.source_visibility[index] = 1;
|
||||
}
|
||||
for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() {
|
||||
batch.placements[index].visible = if index == 0 && batch.unique_source_count > 0 {
|
||||
1
|
||||
} else {
|
||||
u32::from(is_visible(aabb))
|
||||
};
|
||||
}
|
||||
batch.upload_visibility(queue);
|
||||
updated += batch.instance_aabbs.len() + batch.unique_source_count;
|
||||
}
|
||||
for index in batch.unique_source_count..batch.source_visibility.len() {
|
||||
batch.source_visibility[index] = 1;
|
||||
}
|
||||
for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() {
|
||||
batch.placements[index].visible = if index == 0 && batch.unique_source_count > 0 {
|
||||
1
|
||||
} else {
|
||||
u32::from(is_visible(aabb))
|
||||
};
|
||||
}
|
||||
batch.upload_visibility(queue);
|
||||
batch.instance_aabbs.len() + batch.unique_source_count
|
||||
updated
|
||||
}
|
||||
|
||||
pub fn draw<'pass>(
|
||||
|
|
@ -258,12 +259,13 @@ impl HatchGpu {
|
|||
pass.set_stencil_reference(stencil_reference);
|
||||
match &self.backend {
|
||||
HatchBackend::Storage { resident, preview } => {
|
||||
for batch in [resident.as_ref(), preview.as_ref()].into_iter().flatten() {
|
||||
for batch in resident.iter().chain(preview) {
|
||||
pass.set_bind_group(1, &batch.bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
|
||||
pass.set_vertex_buffer(1, batch.placement_buffer.slice(..));
|
||||
for (vertices, instances) in &batch.draws {
|
||||
pass.draw(vertices.clone(), instances.clone());
|
||||
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||
for (indices, instances) in &batch.draws {
|
||||
pass.draw_indexed(indices.clone(), 0, instances.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -272,7 +274,11 @@ impl HatchGpu {
|
|||
pass.set_bind_group(1, &hatch.bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
|
||||
pass.set_vertex_buffer(1, hatch.placement_buffer.slice(..));
|
||||
pass.draw(0..6, 0..hatch.instance_count);
|
||||
pass.set_index_buffer(
|
||||
hatch.index_buffer.slice(..),
|
||||
wgpu::IndexFormat::Uint32,
|
||||
);
|
||||
pass.draw_indexed(0..hatch.index_count, 0, 0..hatch.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -294,11 +300,11 @@ mod tests {
|
|||
#[test]
|
||||
fn selects_backend_from_device_limits() {
|
||||
assert_eq!(
|
||||
HatchBackendKind::select(capabilities(5), false),
|
||||
HatchBackendKind::select(capabilities(4), false),
|
||||
HatchBackendKind::Storage
|
||||
);
|
||||
assert_eq!(
|
||||
HatchBackendKind::select(capabilities(4), false),
|
||||
HatchBackendKind::select(capabilities(3), false),
|
||||
HatchBackendKind::Texture
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,5 @@
|
|||
// 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<f32>[] (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.
|
||||
// Batched hatch rendering. The kernel triangulates each boundary once; the
|
||||
// fragment shader only evaluates the fill pattern inside that mesh.
|
||||
|
||||
use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily};
|
||||
use iced::wgpu;
|
||||
|
|
@ -70,23 +27,16 @@ pub struct HatchInstance {
|
|||
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
|
||||
pub family_offset: u32, // 96
|
||||
pub family_count: u32, // 100
|
||||
/// 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
|
||||
pub draw_depth: f32, // 104
|
||||
/// Gradient shape (`GradientKind::shader_kind`), bit 4 = inverted stops.
|
||||
pub grad_kind: u32, // 120
|
||||
pub _pad2: u32, // 124
|
||||
pub grad_kind: u32, // 108
|
||||
}
|
||||
|
||||
const _: () = assert!(std::mem::size_of::<HatchInstance>() == 128);
|
||||
const _: () = assert!(std::mem::size_of::<HatchInstance>() == 112);
|
||||
|
||||
/// 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.
|
||||
|
|
@ -120,10 +70,7 @@ pub struct LineFamilyGpu {
|
|||
|
||||
const _: () = assert!(std::mem::size_of::<LineFamilyGpu>() == 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`).
|
||||
/// Kernel mesh vertex in local space with its source instance.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub(super) struct HatchVertex {
|
||||
|
|
@ -184,6 +131,7 @@ impl HatchPlacement {
|
|||
/// hatch render pass entirely).
|
||||
pub(super) struct StorageHatchBatch {
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
pub index_buffer: wgpu::Buffer,
|
||||
pub placement_buffer: wgpu::Buffer,
|
||||
pub draws: Vec<(std::ops::Range<u32>, std::ops::Range<u32>)>,
|
||||
// The four storage buffers below are referenced via `bind_group` —
|
||||
|
|
@ -191,7 +139,6 @@ pub(super) struct StorageHatchBatch {
|
|||
// 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
|
||||
|
|
@ -205,8 +152,7 @@ pub(super) struct StorageHatchBatch {
|
|||
#[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.
|
||||
/// per pan tick. Far cheaper than touching the instance data.
|
||||
pub placements: Vec<HatchPlacement>,
|
||||
pub source_visibility: Vec<u32>,
|
||||
pub source_aabbs: Vec<[f32; 4]>,
|
||||
|
|
@ -218,24 +164,76 @@ pub(super) struct StorageHatchBatch {
|
|||
pub instance_aabbs: Vec<[f32; 4]>,
|
||||
}
|
||||
|
||||
fn hatch_buffer_cost(hatch: &HatchModel) -> [u64; 7] {
|
||||
let boundary = hatch.boundary.len() as u64;
|
||||
let (families, dashes) = match &hatch.pattern {
|
||||
HatchPattern::Pattern(families) => (
|
||||
families.len() as u64,
|
||||
families.iter().map(|family| family.dashes.len() as u64).sum(),
|
||||
),
|
||||
_ => (0, 0),
|
||||
};
|
||||
[
|
||||
boundary.saturating_mul(std::mem::size_of::<HatchVertex>() as u64),
|
||||
boundary.saturating_mul(24),
|
||||
std::mem::size_of::<HatchInstance>() as u64,
|
||||
families.saturating_mul(std::mem::size_of::<LineFamilyGpu>() as u64),
|
||||
dashes.saturating_mul(std::mem::size_of::<f32>() as u64),
|
||||
std::mem::size_of::<u32>() as u64,
|
||||
std::mem::size_of::<HatchPlacement>() as u64,
|
||||
]
|
||||
}
|
||||
|
||||
impl StorageHatchBatch {
|
||||
/// 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`].
|
||||
/// Builds whole-hatch chunks within the device's buffer limits.
|
||||
pub(super) fn build(
|
||||
device: &wgpu::Device,
|
||||
bgl: &wgpu::BindGroupLayout,
|
||||
hatches: &[HatchModel],
|
||||
) -> Option<Self> {
|
||||
) -> Vec<Self> {
|
||||
if hatches.is_empty() {
|
||||
return None;
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let limits = device.limits();
|
||||
let chunk_limit = limits
|
||||
.max_buffer_size
|
||||
.min(limits.max_storage_buffer_binding_size as u64);
|
||||
let mut ranges = Vec::new();
|
||||
let mut start = 0;
|
||||
let mut used = [0u64; 7];
|
||||
for (index, hatch) in hatches.iter().enumerate() {
|
||||
let cost = hatch_buffer_cost(hatch);
|
||||
let overflow = used
|
||||
.iter()
|
||||
.zip(cost)
|
||||
.any(|(used, cost)| used.saturating_add(cost) > chunk_limit);
|
||||
if overflow && index > start {
|
||||
ranges.push(start..index);
|
||||
start = index;
|
||||
used = [0; 7];
|
||||
}
|
||||
for (used, cost) in used.iter_mut().zip(cost) {
|
||||
*used = used.saturating_add(cost);
|
||||
}
|
||||
}
|
||||
ranges.push(start..hatches.len());
|
||||
ranges
|
||||
.into_iter()
|
||||
.filter_map(|range| Self::build_chunk(device, bgl, &hatches[range]))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_chunk(
|
||||
device: &wgpu::Device,
|
||||
bgl: &wgpu::BindGroupLayout,
|
||||
hatches: &[HatchModel],
|
||||
) -> Option<Self> {
|
||||
|
||||
let mut instances: Vec<HatchInstance> = Vec::with_capacity(hatches.len());
|
||||
let mut boundary: Vec<[f32; 4]> = Vec::new();
|
||||
let mut families: Vec<LineFamilyGpu> = Vec::new();
|
||||
let mut dashes: Vec<f32> = Vec::new();
|
||||
let mut meshes = Vec::with_capacity(hatches.len());
|
||||
let mut slots = rustc_hash::FxHashMap::default();
|
||||
let mut groups: Vec<Vec<&HatchModel>> = Vec::new();
|
||||
for (index, hatch) in hatches.iter().enumerate() {
|
||||
|
|
@ -255,22 +253,9 @@ impl StorageHatchBatch {
|
|||
|
||||
for group in &groups {
|
||||
let h = group[0];
|
||||
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 mesh = h.fill_mesh();
|
||||
let has_mesh = !mesh.0.is_empty() && !mesh.1.is_empty();
|
||||
meshes.push(mesh);
|
||||
let family_offset = families.len() as u32;
|
||||
let mut family_count = 0u32;
|
||||
|
||||
|
|
@ -301,11 +286,8 @@ impl StorageHatchBatch {
|
|||
dashes.push(d);
|
||||
}
|
||||
let n_dashes = (dashes.len() as u32 - dash_offset).min(u32::MAX);
|
||||
// QCAD PAT local-frame convention (mirrors
|
||||
// the storage batch convention: `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.
|
||||
// PAT local frame: perpendicular spacing and
|
||||
// along-line phase.
|
||||
let perp_step = fam.dy;
|
||||
let along_step = fam.dx;
|
||||
// Screen-space derivative drives 1-px line width
|
||||
|
|
@ -366,39 +348,19 @@ impl StorageHatchBatch {
|
|||
grad_range,
|
||||
mode,
|
||||
visible: 0,
|
||||
boundary_offset,
|
||||
boundary_count,
|
||||
family_offset,
|
||||
family_count,
|
||||
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
|
||||
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],
|
||||
aabb: [min_x, min_y, max_x, max_y],
|
||||
world_origin: wo_hi,
|
||||
world_origin_low: wo_lo,
|
||||
angle_offset: h.angle_offset,
|
||||
|
|
@ -408,26 +370,14 @@ impl StorageHatchBatch {
|
|||
grad_min,
|
||||
grad_range,
|
||||
mode,
|
||||
visible: 1,
|
||||
boundary_offset,
|
||||
boundary_count,
|
||||
visible: u32::from(has_mesh),
|
||||
family_offset,
|
||||
family_count,
|
||||
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
|
||||
// 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());
|
||||
}
|
||||
|
|
@ -435,21 +385,33 @@ impl StorageHatchBatch {
|
|||
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<HatchVertex> = 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 mut verts = Vec::new();
|
||||
let mut indices = Vec::new();
|
||||
let mut mesh_ranges = Vec::with_capacity(meshes.len());
|
||||
for (instance_index, (points, mesh_indices)) in meshes.into_iter().enumerate() {
|
||||
let Ok(base) = u32::try_from(verts.len()) else {
|
||||
return None;
|
||||
};
|
||||
let Ok(start) = u32::try_from(indices.len()) else {
|
||||
return None;
|
||||
};
|
||||
verts.extend(points.into_iter().map(|local_xy| HatchVertex {
|
||||
local_xy,
|
||||
instance_index: instance_index as u32,
|
||||
}));
|
||||
for index in mesh_indices {
|
||||
let Some(index) = base.checked_add(index) else {
|
||||
return None;
|
||||
};
|
||||
indices.push(index);
|
||||
}
|
||||
let Ok(end) = u32::try_from(indices.len()) else {
|
||||
return None;
|
||||
};
|
||||
mesh_ranges.push(start..end);
|
||||
}
|
||||
if indices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut placements = Vec::new();
|
||||
let mut draws = Vec::with_capacity(groups.len());
|
||||
|
|
@ -461,7 +423,10 @@ impl StorageHatchBatch {
|
|||
draw_depth: 0.0,
|
||||
visible: 1,
|
||||
});
|
||||
draws.push((0..unique_source_count as u32 * 6, 0..1));
|
||||
let end = mesh_ranges[unique_source_count - 1].end;
|
||||
if end > 0 {
|
||||
draws.push((0..end, 0..1));
|
||||
}
|
||||
let mut union = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
|
||||
for inst in instances.iter().take(unique_source_count) {
|
||||
union[0] = union[0].min(inst.aabb[0] + inst.world_origin[0]);
|
||||
|
|
@ -500,10 +465,32 @@ impl StorageHatchBatch {
|
|||
inst.aabb[3] + inst.world_origin[1] + high[1],
|
||||
]);
|
||||
}
|
||||
draws.push((
|
||||
source_index as u32 * 6..source_index as u32 * 6 + 6,
|
||||
placement_start..placements.len() as u32,
|
||||
));
|
||||
if !mesh_ranges[source_index].is_empty() {
|
||||
draws.push((
|
||||
mesh_ranges[source_index].clone(),
|
||||
placement_start..placements.len() as u32,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let visibility: Vec<u32> = instances.iter().map(|instance| instance.visible).collect();
|
||||
let limits = device.limits();
|
||||
let buffer_fits = |count: usize, stride: usize| {
|
||||
(count as u64).saturating_mul(stride as u64) <= limits.max_buffer_size
|
||||
};
|
||||
let storage_fits = |count: usize, stride: usize| {
|
||||
(count as u64).saturating_mul(stride as u64)
|
||||
<= limits.max_storage_buffer_binding_size as u64
|
||||
};
|
||||
if !buffer_fits(verts.len(), std::mem::size_of::<HatchVertex>())
|
||||
|| !buffer_fits(indices.len(), std::mem::size_of::<u32>())
|
||||
|| !buffer_fits(placements.len(), std::mem::size_of::<HatchPlacement>())
|
||||
|| !storage_fits(instances.len(), std::mem::size_of::<HatchInstance>())
|
||||
|| !storage_fits(families.len(), std::mem::size_of::<LineFamilyGpu>())
|
||||
|| !storage_fits(dashes.len(), std::mem::size_of::<f32>())
|
||||
|| !storage_fits(visibility.len(), std::mem::size_of::<u32>())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
|
|
@ -511,6 +498,11 @@ impl StorageHatchBatch {
|
|||
contents: bytemuck::cast_slice(&verts),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("hatch.index"),
|
||||
contents: bytemuck::cast_slice(&indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("hatch.placements"),
|
||||
contents: bytemuck::cast_slice(&placements),
|
||||
|
|
@ -521,11 +513,6 @@ impl StorageHatchBatch {
|
|||
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),
|
||||
|
|
@ -537,7 +524,6 @@ impl StorageHatchBatch {
|
|||
usage: wgpu::BufferUsages::STORAGE,
|
||||
});
|
||||
|
||||
let visibility: Vec<u32> = 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),
|
||||
|
|
@ -554,18 +540,14 @@ impl StorageHatchBatch {
|
|||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: boundary_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: family_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
binding: 2,
|
||||
resource: dash_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
binding: 3,
|
||||
resource: visibility_buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
|
|
@ -582,10 +564,10 @@ impl StorageHatchBatch {
|
|||
|
||||
Some(Self {
|
||||
vertex_buffer,
|
||||
index_buffer,
|
||||
placement_buffer,
|
||||
draws,
|
||||
instance_buffer,
|
||||
boundary_buffer,
|
||||
family_buffer,
|
||||
dash_buffer,
|
||||
visibility_buffer,
|
||||
|
|
@ -614,10 +596,7 @@ impl StorageHatchBatch {
|
|||
);
|
||||
}
|
||||
|
||||
/// 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).
|
||||
/// Shared layout for instance, family, dash, and visibility storage.
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
||||
let entry = |binding: u32| wgpu::BindGroupLayoutEntry {
|
||||
|
|
@ -632,7 +611,7 @@ impl StorageHatchBatch {
|
|||
};
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("hatch.bgl"),
|
||||
entries: &[entry(0), entry(1), entry(2), entry(3), entry(4)],
|
||||
entries: &[entry(0), entry(1), entry(2), entry(3)],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,5 @@
|
|||
// Storage-free hatch renderer — texture-backed, UNCAPPED.
|
||||
//
|
||||
// Devices without storage buffers use this per-hatch renderer. It reuses the
|
||||
// storage-free hatch algorithm (see wipeout.wgsl / hatch_texture.wgsl) but packs the
|
||||
// variable-length boundary / family / dash arrays into ONE RGBA32F data texture
|
||||
// read via textureLoad — removing the MAX_FAMILIES / MAX_HATCH_BOUNDARY_VERTS /
|
||||
// MAX_DASHES caps of the uniform (WipeoutGpu) path. Every hatch type — solid,
|
||||
// gradient, and arbitrarily complex line patterns — renders in compat mode.
|
||||
//
|
||||
// Storage-capable devices use the sibling storage backend; wipeout masks use
|
||||
// wipeout_gpu.rs.
|
||||
// Texture-backed hatch renderer for devices without storage buffers. Boundary
|
||||
// geometry comes from the same kernel mesh as the storage backend.
|
||||
|
||||
use crate::scene::model::hatch_model::{HatchModel, HatchPattern};
|
||||
use iced::wgpu;
|
||||
|
|
@ -71,7 +62,7 @@ struct TextureHatchUniform {
|
|||
color: [f32; 4], // 0
|
||||
color2: [f32; 4], // 16
|
||||
mode: u32, // 32
|
||||
vcount: u32, // 36
|
||||
_reserved: u32, // 36
|
||||
angle_offset: f32, // 40
|
||||
scale: f32, // 44
|
||||
grad_cos: f32, // 48
|
||||
|
|
@ -90,7 +81,9 @@ struct TextureHatchUniform {
|
|||
|
||||
pub(super) struct TextureHatch {
|
||||
pub(super) vertex_buffer: wgpu::Buffer,
|
||||
pub(super) index_buffer: wgpu::Buffer,
|
||||
pub(super) placement_buffer: wgpu::Buffer,
|
||||
pub(super) index_count: u32,
|
||||
pub(super) instance_count: u32,
|
||||
pub(super) bind_group: wgpu::BindGroup,
|
||||
/// Reserved for per-frame AABB LOD (mirrors `WipeoutGpu`); not yet wired
|
||||
|
|
@ -125,7 +118,7 @@ impl TextureHatch {
|
|||
}
|
||||
groups
|
||||
.into_iter()
|
||||
.map(|group| Self::new(device, queue, &group, bgl1))
|
||||
.filter_map(|group| Self::new(device, queue, &group, bgl1))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +157,7 @@ impl TextureHatch {
|
|||
queue: &wgpu::Queue,
|
||||
models: &[&HatchModel],
|
||||
bgl1: &wgpu::BindGroupLayout,
|
||||
) -> Self {
|
||||
) -> Option<Self> {
|
||||
let model = models[0];
|
||||
// ── Decode pattern mode (mirrors WipeoutGpu::new) ─────────────────
|
||||
// The gradient shape (kind + invert bit) rides in the mode's high
|
||||
|
|
@ -197,41 +190,26 @@ impl TextureHatch {
|
|||
max_y = max_y.max(y);
|
||||
}
|
||||
|
||||
let max_spacing = match &model.pattern {
|
||||
HatchPattern::Pattern(families) => {
|
||||
families.iter().map(|f| f.dy.abs()).fold(0.0f32, f32::max)
|
||||
}
|
||||
_ => 5.0,
|
||||
};
|
||||
let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt();
|
||||
let pad = (diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0);
|
||||
|
||||
// Anchor pattern phase at `world_origin` with the boundary stored raw,
|
||||
// matching the storage backend — NOT WipeoutGpu,
|
||||
// whose f64 origin grid-snap is dead code (wipeouts are always solid)
|
||||
// and would phase-shift every line pattern relative to desktop. No drift.
|
||||
let origin = model.world_origin;
|
||||
let drift = [0.0f32, 0.0f32];
|
||||
let (x0, x1, y0, y1) = (
|
||||
min_x + drift[0] - pad,
|
||||
max_x + drift[0] + pad,
|
||||
min_y + drift[1] - pad,
|
||||
max_y + drift[1] + pad,
|
||||
);
|
||||
|
||||
let quad = [
|
||||
HatchVertex { pos: [x0, y0, 0.0], _pad: 0.0 },
|
||||
HatchVertex { pos: [x1, y0, 0.0], _pad: 0.0 },
|
||||
HatchVertex { pos: [x1, y1, 0.0], _pad: 0.0 },
|
||||
HatchVertex { pos: [x0, y0, 0.0], _pad: 0.0 },
|
||||
HatchVertex { pos: [x1, y1, 0.0], _pad: 0.0 },
|
||||
HatchVertex { pos: [x0, y1, 0.0], _pad: 0.0 },
|
||||
];
|
||||
let (mesh_points, indices) = model.fill_mesh();
|
||||
if mesh_points.is_empty() || indices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let vertices: Vec<HatchVertex> = mesh_points
|
||||
.into_iter()
|
||||
.map(|[x, y]| HatchVertex { pos: [x, y, 0.0], _pad: 0.0 })
|
||||
.collect();
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("hatch.texture.vbuf"),
|
||||
contents: bytemuck::cast_slice(&quad),
|
||||
contents: bytemuck::cast_slice(&vertices),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("hatch.texture.ibuf"),
|
||||
contents: bytemuck::cast_slice(&indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
let base = model
|
||||
.render_instance
|
||||
.map_or([0.0; 3], |instance| instance.translation);
|
||||
|
|
@ -290,21 +268,8 @@ impl TextureHatch {
|
|||
(0.0, 1.0)
|
||||
};
|
||||
|
||||
// ── Pack the data texture: boundary | families | dashes ───────────
|
||||
// ── Pack the data texture: families | dashes ─────────────────────
|
||||
let mut texels: Vec<[f32; 4]> = Vec::new();
|
||||
// Boundary section (texels 0..vcount). NaN separators become the
|
||||
// finite GPU sentinel — the shader NaN self-compare is folded to
|
||||
// `true` on some drivers (#386, #416).
|
||||
for &[x, y] in model.boundary.iter() {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
texels.push([x + drift[0], y + drift[1], 0.0, 0.0]);
|
||||
} else {
|
||||
let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP;
|
||||
texels.push([s, s, 0.0, 0.0]);
|
||||
}
|
||||
}
|
||||
let vcount = texels.len() as u32;
|
||||
|
||||
// Family section (3 texels each) + a flat dash pool.
|
||||
let fam_off = texels.len() as u32;
|
||||
let mut n_families = 0u32;
|
||||
|
|
@ -320,7 +285,7 @@ impl TextureHatch {
|
|||
0.0
|
||||
};
|
||||
let angle_r = fam.angle_deg.to_radians();
|
||||
// QCAD PAT convention: perp_step = dy, along_step = dx.
|
||||
// PAT local frame: perpendicular spacing and along-line phase.
|
||||
texels.push([angle_r.cos(), angle_r.sin(), fam.x0, fam.y0]);
|
||||
texels.push([fam.dx, fam.dy, fam.dy, fam.dx]);
|
||||
// Counts as exact f32 (small integers → no denormal/bitcast risk).
|
||||
|
|
@ -385,7 +350,7 @@ impl TextureHatch {
|
|||
color: model.color,
|
||||
color2,
|
||||
mode,
|
||||
vcount,
|
||||
_reserved: 0,
|
||||
angle_offset: model.angle_offset,
|
||||
// Clamp like the desktop renderer so scale==0 can't make perp_step 0
|
||||
// → round(perp/0)=NaN → an invisible hatch.
|
||||
|
|
@ -433,14 +398,16 @@ impl TextureHatch {
|
|||
[min_x, min_y, max_x, max_y]
|
||||
};
|
||||
|
||||
Self {
|
||||
Some(Self {
|
||||
vertex_buffer,
|
||||
index_buffer,
|
||||
placement_buffer,
|
||||
index_count: indices.len() as u32,
|
||||
instance_count: placements.len() as u32,
|
||||
bind_group,
|
||||
world_aabb,
|
||||
_uniform_buf,
|
||||
_data_tex: data_tex,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,10 @@
|
|||
// Phase 4-B — batched hatch shader. All hatches in one draw call;
|
||||
// per-instance data fetched from storage buffers indexed by the
|
||||
// `instance_index` vertex attribute (passed from the per-vertex
|
||||
// (local_xy, instance_index) stream of tessellated boundary-triangle
|
||||
// positions so we don't depend on @builtin(instance_index) edge cases
|
||||
// across backends).
|
||||
// Batched hatch shader. Kernel mesh vertices carry their source instance.
|
||||
//
|
||||
// Layout — matches `hatch_gpu/storage.rs`:
|
||||
// group 1 binding 0 InstanceBuffer HatchInstance[] (128 B / inst)
|
||||
// group 1 binding 1 BoundaryBuffer vec4<f32>[] (xy in .xy)
|
||||
// group 1 binding 2 FamilyBuffer LineFamilyGpu[] (48 B / fam)
|
||||
// group 1 binding 3 DashBuffer f32[]
|
||||
//
|
||||
// The vertex shader emits tessellated boundary triangles in local space
|
||||
// (an instance whose boundary failed to tessellate falls back to an
|
||||
// AABB quad with `poly_test == 1`, so the fragment shader still runs
|
||||
// `in_polygon` to clip it to the real shape). A `visible == 0` instance
|
||||
// gets an out-of-NDC clip position so the fragment shader never runs
|
||||
// for it — that's the GPU-side cull (Phase 4-B equivalent of
|
||||
// `compute_hatch_lod` writing `hatch_skip_flags`).
|
||||
// group 1 binding 0 InstanceBuffer HatchInstance[]
|
||||
// group 1 binding 1 FamilyBuffer LineFamilyGpu[]
|
||||
// group 1 binding 2 DashBuffer f32[]
|
||||
// group 1 binding 3 Visibility u32[]
|
||||
|
||||
// ── Group 0: shared frame uniforms (matches hatch.wgsl) ──────────────────
|
||||
|
||||
|
|
@ -53,14 +40,10 @@ struct HatchInstance {
|
|||
grad_range: f32,
|
||||
mode: u32, // 0=pattern, 1=solid, 2=gradient
|
||||
visible: u32, // 0 = skip (CPU writes via compute_hatch_lod)
|
||||
boundary_offset: u32,
|
||||
boundary_count: u32,
|
||||
family_offset: u32,
|
||||
family_count: u32,
|
||||
draw_depth: f32, // signed (-1,1) draw-order bias; 0 = neutral
|
||||
poly_test: u32, // 1 = run in_polygon (fallback), 0 = skip
|
||||
grad_kind: u32, // shape (0=linear,1=cyl,2=sph,3=hemi,4=curved), bit4=invert
|
||||
_pad2: u32,
|
||||
}
|
||||
|
||||
// Draw-order depth bias (see wire.wgsl). Higher draw_depth → smaller z →
|
||||
|
|
@ -83,14 +66,13 @@ struct LineFamily {
|
|||
}
|
||||
|
||||
@group(1) @binding(0) var<storage, read> instances: array<HatchInstance>;
|
||||
@group(1) @binding(1) var<storage, read> boundary: array<vec4<f32>>;
|
||||
@group(1) @binding(2) var<storage, read> families: array<LineFamily>;
|
||||
@group(1) @binding(3) var<storage, read> dashes: array<f32>;
|
||||
@group(1) @binding(1) var<storage, read> families: array<LineFamily>;
|
||||
@group(1) @binding(2) var<storage, read> dashes: array<f32>;
|
||||
// Per-instance visibility (Phase 4-B sub-pixel + frustum skip).
|
||||
// CPU writes `1` to draw / `0` to skip every frame; vertex shader
|
||||
// emits an out-of-NDC clip position for 0-instances so the GPU
|
||||
// rasterizer culls the primitive before any fragment runs.
|
||||
@group(1) @binding(4) var<storage, read> visibility: array<u32>;
|
||||
@group(1) @binding(3) var<storage, read> visibility: array<u32>;
|
||||
|
||||
// ── Vertex shader ────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -144,58 +126,6 @@ struct VOut {
|
|||
return o;
|
||||
}
|
||||
|
||||
// ── Point-in-polygon (ray casting) over a sub-range of BoundaryBuffer ────
|
||||
|
||||
fn valid_vertex(p: vec2<f32>) -> bool {
|
||||
// Sub-loop separators are a huge finite sentinel (1e30, see
|
||||
// GPU_BOUNDARY_SEP), not NaN: `x == x` NaN detection gets folded to
|
||||
// `true` by some drivers (Intel Mesa fast math), which made separators
|
||||
// read as real vertices and bleed fills past their boundary (#386, #416).
|
||||
return abs(p.x) < 1.0e29 && abs(p.y) < 1.0e29;
|
||||
}
|
||||
|
||||
fn edge_crosses(p: vec2<f32>, a: vec2<f32>, c: vec2<f32>) -> bool {
|
||||
if (a.y > p.y) != (c.y > p.y) {
|
||||
let x_int = (c.x - a.x) * (p.y - a.y) / (c.y - a.y) + a.x;
|
||||
return p.x < x_int;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn in_polygon(p: vec2<f32>, offset: u32, count: u32) -> bool {
|
||||
var inside = false;
|
||||
var prev = vec2<f32>(0.0, 0.0);
|
||||
var first = vec2<f32>(0.0, 0.0);
|
||||
var have_prev = false;
|
||||
for (var i = 0u; i < count; i++) {
|
||||
let vi = boundary[offset + i].xy;
|
||||
if !valid_vertex(vi) {
|
||||
// Close the sub-loop that just ended (last → first edge). An
|
||||
// unclosed boundary — e.g. a SOLID's 4 corners, which are not
|
||||
// repeated — otherwise miscounts crossings and the fill bleeds
|
||||
// outside the shape. (#140)
|
||||
if have_prev && edge_crosses(p, prev, first) {
|
||||
inside = !inside;
|
||||
}
|
||||
have_prev = false;
|
||||
continue;
|
||||
}
|
||||
if have_prev {
|
||||
if edge_crosses(p, prev, vi) {
|
||||
inside = !inside;
|
||||
}
|
||||
} else {
|
||||
first = vi;
|
||||
}
|
||||
prev = vi;
|
||||
have_prev = true;
|
||||
}
|
||||
if have_prev && edge_crosses(p, prev, first) {
|
||||
inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
// ── Per-family hatch test (same math as hatch.wgsl, dashes from
|
||||
// global DashBuffer instead of per-hatch FamilyBatch) ────────────────────
|
||||
|
||||
|
|
@ -282,15 +212,7 @@ fn check_family(
|
|||
|
||||
let inst = instances[v.instance_index];
|
||||
|
||||
// 1. Boundary test — only on the fallback path (poly_test==1). On the
|
||||
// tessellated fast path the triangles already bound the fill.
|
||||
if inst.poly_test == 1u {
|
||||
if !in_polygon(v.xz, inst.boundary_offset, inst.boundary_count) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Mode dispatch.
|
||||
// Mode dispatch.
|
||||
if inst.mode == 1u {
|
||||
return inst.color;
|
||||
} else if inst.mode == 2u {
|
||||
|
|
@ -319,12 +241,11 @@ fn check_family(
|
|||
t = 1.0 - t;
|
||||
}
|
||||
// Radial stops run OUTSIDE-IN: colour 1 at the rim, colour 2 at the
|
||||
// centre (AutoCAD's SPHERICAL/HEMISPHERICAL convention; the INV bit
|
||||
// above swaps them back).
|
||||
// centre; the INV bit above swaps them back.
|
||||
return mix(inst.color2, inst.color, t);
|
||||
}
|
||||
|
||||
// 3. Pattern LOD. Keep every family visible until all family spacings
|
||||
// Pattern LOD. Keep every family visible until all family spacings
|
||||
// project below 2 px, then substitute one solid fill. A single dense
|
||||
// family must neither hide itself nor turn a complex hatch solid.
|
||||
var all_families_subpixel = inst.family_count > 0u && u.world_per_pixel > 0.0;
|
||||
|
|
@ -343,7 +264,7 @@ fn check_family(
|
|||
return inst.color;
|
||||
}
|
||||
|
||||
// 4. Pattern evaluation.
|
||||
// Pattern evaluation.
|
||||
let cos_off = cos(inst.angle_offset);
|
||||
let sin_off = sin(inst.angle_offset);
|
||||
for (var i = 0u; i < inst.family_count; i++) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,7 @@
|
|||
// Hatch texture shader — storage-free, UNCAPPED variant of wipeout.wgsl.
|
||||
//
|
||||
// Devices without vertex/fragment storage buffers cannot use the batched
|
||||
// storage-buffer renderer (hatch.wgsl). This shader keeps the exact
|
||||
// wipeout.wgsl hatch algorithm (in_polygon + per-family line/dash/dot + solid +
|
||||
// gradient) but reads the variable-length boundary / family / dash arrays from a
|
||||
// single RGBA32F data texture via textureLoad instead of fixed-size uniforms —
|
||||
// removing the MAX_FAMILIES (16) / MAX_HATCH_BOUNDARY_VERTS (1024) / MAX_DASHES
|
||||
// (128) caps of the uniform path.
|
||||
// Texture-backed hatch shader. The kernel mesh bounds the fill; this shader
|
||||
// evaluates only the pattern, solid, or gradient colour.
|
||||
//
|
||||
// Data texture layout (row-major, width = h.tex_width, one vec4 per texel):
|
||||
// [ 0 .. vcount ) boundary verts, .xy = local XY, NaN = break
|
||||
// [ fam_off .. fam_off+3*n_fam ) families, 3 texels each (see load_family)
|
||||
// [ dash_off .. ) dash values, 4 per texel (RGBA)
|
||||
|
||||
|
|
@ -36,7 +28,7 @@ struct HatchUniforms {
|
|||
color: vec4<f32>, // 0
|
||||
color2: vec4<f32>, // 16
|
||||
mode: u32, // 32: 0=pattern, 1=solid, 2=gradient
|
||||
vcount: u32, // 36: boundary vertex count
|
||||
_reserved: u32, // 36
|
||||
angle_offset: f32, // 40
|
||||
scale: f32, // 44
|
||||
grad_cos: f32, // 48
|
||||
|
|
@ -119,56 +111,6 @@ struct VOut {
|
|||
return o;
|
||||
}
|
||||
|
||||
// ── Point-in-polygon (ray casting) ────────────────────────────────────────
|
||||
|
||||
fn valid_vertex(p: vec2<f32>) -> bool {
|
||||
// Sub-loop separators are a huge finite sentinel (1e30, see
|
||||
// GPU_BOUNDARY_SEP), not NaN: `x == x` NaN detection gets folded to
|
||||
// `true` by some drivers (Intel Mesa fast math), which made separators
|
||||
// read as real vertices and bleed fills past their boundary (#386, #416).
|
||||
return abs(p.x) < 1.0e29 && abs(p.y) < 1.0e29;
|
||||
}
|
||||
|
||||
fn edge_crosses(p: vec2<f32>, a: vec2<f32>, c: vec2<f32>) -> bool {
|
||||
if (a.y > p.y) != (c.y > p.y) {
|
||||
let x_int = (c.x - a.x) * (p.y - a.y) / (c.y - a.y) + a.x;
|
||||
return p.x < x_int;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn in_polygon(p: vec2<f32>) -> bool {
|
||||
var inside = false;
|
||||
let n = h.vcount;
|
||||
var prev = vec2<f32>(0.0, 0.0);
|
||||
var first = vec2<f32>(0.0, 0.0);
|
||||
var have_prev = false;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let vi = texel(i).xy;
|
||||
if !valid_vertex(vi) {
|
||||
// NaN sentinel closes the current sub-loop (last → first edge). (#140)
|
||||
if have_prev && edge_crosses(p, prev, first) {
|
||||
inside = !inside;
|
||||
}
|
||||
have_prev = false;
|
||||
continue;
|
||||
}
|
||||
if have_prev {
|
||||
if edge_crosses(p, prev, vi) {
|
||||
inside = !inside;
|
||||
}
|
||||
} else {
|
||||
first = vi;
|
||||
}
|
||||
prev = vi;
|
||||
have_prev = true;
|
||||
}
|
||||
if have_prev && edge_crosses(p, prev, first) {
|
||||
inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
// ── Per-family hatch test (identical math to wipeout.wgsl) ─────────────────
|
||||
|
||||
// `ddx_xz`/`ddy_xz` are screen-space derivatives of `xz`, taken once in
|
||||
|
|
@ -244,8 +186,6 @@ fn check_family(
|
|||
let ddx_xz = dpdx(v.xz);
|
||||
let ddy_xz = dpdy(v.xz);
|
||||
|
||||
if !in_polygon(v.xz) { discard; }
|
||||
|
||||
let base_mode = h.mode & 0xFFu;
|
||||
let gk = (h.mode >> 8u) & 15u;
|
||||
let ginv = ((h.mode >> 8u) & 16u) != 0u;
|
||||
|
|
|
|||
Loading…
Reference in a new issue