forked from 1b-agent/opencadstudio-upstream
fix(hatch): render fills via the per-fragment in_polygon test again
The batched hatch path pre-triangulated every boundary on the CPU (PR#300) with no cap on the resulting vertex buffer. A hatch-dense drawing (13k solid-filled hatches) built a ~6.75 GB `hatch_batched.vertex` buffer and panicked in `Device::create_buffer` (device max 256 MB), after a long stall spent triangulating on load. Drop the CPU triangulation and always draw each hatch as its AABB quad (6 verts) with `poly_test = 1`, letting the shader resolve inside/outside per fragment against the uploaded boundary — the path the shader always kept as a fallback. The vertex buffer is now 6 verts/hatch regardless of boundary complexity, so no drawing can overflow it. The interaction-LOD flicker PR#300 addressed no longer occurs (OCS_HATCH_LOD defaults off). Removes the now-unused hatch_tess triangulation module. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
492ad940eb
commit
4df41cdf34
3 files changed
+19
-178
No files matched your search
@@ -199,7 +199,6 @@ impl HatchBatchedGpu {
|
||||
}
|
||||
|
||||
let mut instances: Vec<HatchInstance> = Vec::with_capacity(hatches.len());
|
||||
let mut instance_tris: Vec<Vec<[f32; 2]>> = 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();
|
||||
@@ -211,11 +210,6 @@ impl HatchBatchedGpu {
|
||||
}
|
||||
let boundary_count = boundary.len() as u32 - boundary_offset;
|
||||
|
||||
// Triangulate this boundary once (cached with the whole build, which
|
||||
// only re-runs when geometry_epoch advances). Empty → fall back to the
|
||||
// AABB-quad + in_polygon path for this instance.
|
||||
let tris = crate::scene::pipeline::hatch_tess::tessellate_boundary(&h.boundary);
|
||||
instance_tris.push(tris);
|
||||
|
||||
let family_offset = families.len() as u32;
|
||||
let mut family_count = 0u32;
|
||||
@@ -350,12 +344,16 @@ impl HatchBatchedGpu {
|
||||
family_offset,
|
||||
family_count,
|
||||
draw_depth: h.draw_depth,
|
||||
poly_test: if instance_tris.last().map_or(true, |t| t.is_empty()) { 1 } else { 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,
|
||||
_pad1: 0,
|
||||
_pad2: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Empty fallbacks — storage buffers can't be zero-sized.
|
||||
if boundary.is_empty() {
|
||||
boundary.push([0.0; 4]);
|
||||
@@ -367,26 +365,20 @@ impl HatchBatchedGpu {
|
||||
dashes.push(0.0);
|
||||
}
|
||||
|
||||
// Vertex buffer — tessellated triangles per instance, or an AABB quad
|
||||
// fallback (poly_test==1) when tessellation produced nothing.
|
||||
let mut verts: Vec<HatchBatchedVertex> = Vec::new();
|
||||
// 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<HatchBatchedVertex> = Vec::with_capacity(instances.len() * 6);
|
||||
for (i, inst) in instances.iter().enumerate() {
|
||||
let tris = &instance_tris[i];
|
||||
if !tris.is_empty() {
|
||||
for &[x, y] in tris.iter() {
|
||||
verts.push(HatchBatchedVertex { local_xy: [x, y], instance_index: i as u32 });
|
||||
}
|
||||
} else {
|
||||
// Fallback: two triangles over the local-space AABB, same corner
|
||||
// order as the old shader (BL,BR,TL, BR,TR,TL).
|
||||
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(HatchBatchedVertex { local_xy: c, instance_index: i as u32 });
|
||||
}
|
||||
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(HatchBatchedVertex { local_xy: c, instance_index: i as u32 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
//! CPU triangulation of hatch boundaries. Each hatch boundary is a flat
|
||||
//! list of local-space verts with NaN markers separating sub-loops
|
||||
//! (islands / holes). We split on the NaN markers into contours and fill
|
||||
//! them with lyon's even-odd rule — identical coverage to the per-fragment
|
||||
//! `in_polygon` ray-cast in `hatch_batched.wgsl` (including the #140 island
|
||||
//! fix) — so the rasterized triangles can replace that test entirely.
|
||||
|
||||
use lyon_tessellation::math::point;
|
||||
use lyon_tessellation::path::Path;
|
||||
use lyon_tessellation::{
|
||||
BuffersBuilder, FillOptions, FillRule, FillTessellator, FillVertex, VertexBuffers,
|
||||
};
|
||||
|
||||
/// Split a NaN-separated boundary into finite contours.
|
||||
fn boundary_to_contours(boundary: &[[f32; 2]]) -> Vec<Vec<[f32; 2]>> {
|
||||
let mut contours = Vec::new();
|
||||
let mut cur: Vec<[f32; 2]> = Vec::new();
|
||||
for &[x, y] in boundary {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
cur.push([x, y]);
|
||||
} else if !cur.is_empty() {
|
||||
contours.push(std::mem::take(&mut cur));
|
||||
}
|
||||
}
|
||||
if !cur.is_empty() {
|
||||
contours.push(cur);
|
||||
}
|
||||
contours
|
||||
}
|
||||
|
||||
/// Triangulate a hatch boundary into a flat triangle-list in local space.
|
||||
/// Empty on degenerate input or tessellation failure (caller falls back to
|
||||
/// the AABB-quad + `in_polygon` path for that instance).
|
||||
pub fn tessellate_boundary(boundary: &[[f32; 2]]) -> Vec<[f32; 2]> {
|
||||
let contours = boundary_to_contours(boundary);
|
||||
|
||||
let mut builder = Path::builder();
|
||||
for contour in &contours {
|
||||
if contour.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
builder.begin(point(contour[0][0], contour[0][1]));
|
||||
for p in &contour[1..] {
|
||||
builder.line_to(point(p[0], p[1]));
|
||||
}
|
||||
builder.end(true);
|
||||
}
|
||||
let path = builder.build();
|
||||
|
||||
let mut geometry: VertexBuffers<[f32; 2], u32> = VertexBuffers::new();
|
||||
let mut tess = FillTessellator::new();
|
||||
if tess
|
||||
.tessellate_path(
|
||||
&path,
|
||||
&FillOptions::default().with_fill_rule(FillRule::EvenOdd),
|
||||
&mut BuffersBuilder::new(&mut geometry, |v: FillVertex| v.position().to_array()),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut tris = Vec::with_capacity(geometry.indices.len());
|
||||
for &idx in &geometry.indices {
|
||||
if let Some(&p) = geometry.vertices.get(idx as usize) {
|
||||
tris.push(p);
|
||||
}
|
||||
}
|
||||
tris
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// True if `p` lies inside any triangle of a flat triangle-list.
|
||||
fn covered(tris: &[[f32; 2]], p: [f32; 2]) -> bool {
|
||||
fn sign(a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> f32 {
|
||||
(a[0] - c[0]) * (b[1] - c[1]) - (b[0] - c[0]) * (a[1] - c[1])
|
||||
}
|
||||
tris.chunks_exact(3).any(|t| {
|
||||
let (a, b, c) = (t[0], t[1], t[2]);
|
||||
let d1 = sign(p, a, b);
|
||||
let d2 = sign(p, b, c);
|
||||
let d3 = sign(p, c, a);
|
||||
let has_neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
|
||||
let has_pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
|
||||
!(has_neg && has_pos)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_tessellates_and_covers_center() {
|
||||
let sq = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
|
||||
let tris = tessellate_boundary(&sq);
|
||||
assert!(!tris.is_empty(), "square should tessellate");
|
||||
assert_eq!(tris.len() % 3, 0, "triangle-list length must be a multiple of 3");
|
||||
assert!(covered(&tris, [5.0, 5.0]), "center must be covered");
|
||||
assert!(!covered(&tris, [20.0, 20.0]), "outside point must not be covered");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn even_odd_hole_is_not_covered() {
|
||||
// Outer 0..10 square, NaN separator, inner 3..7 square (a hole).
|
||||
let nan = f32::NAN;
|
||||
let boundary = [
|
||||
[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0],
|
||||
[nan, nan],
|
||||
[3.0, 3.0], [7.0, 3.0], [7.0, 7.0], [3.0, 7.0],
|
||||
];
|
||||
let tris = tessellate_boundary(&boundary);
|
||||
assert!(!tris.is_empty());
|
||||
assert!(covered(&tris, [1.0, 1.0]), "point in the ring must be covered");
|
||||
assert!(!covered(&tris, [5.0, 5.0]), "point in the hole must NOT be covered (even-odd)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degenerate_boundary_returns_empty() {
|
||||
assert!(tessellate_boundary(&[[0.0, 0.0], [1.0, 1.0]]).is_empty());
|
||||
assert!(tessellate_boundary(&[]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_island_even_odd_fills_center() {
|
||||
// Even-odd: outer ring filled, hole empty, island inside the hole filled again.
|
||||
let nan = f32::NAN;
|
||||
let boundary = [
|
||||
[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], // outer
|
||||
[nan, nan],
|
||||
[2.0, 2.0], [8.0, 2.0], [8.0, 8.0], [2.0, 8.0], // hole
|
||||
[nan, nan],
|
||||
[3.0, 3.0], [7.0, 3.0], [7.0, 7.0], [3.0, 7.0], // island inside the hole
|
||||
];
|
||||
let tris = tessellate_boundary(&boundary);
|
||||
assert!(!tris.is_empty());
|
||||
assert!(covered(&tris, [5.0, 5.0]), "innermost island must be filled");
|
||||
assert!(!covered(&tris, [2.5, 5.0]), "the hole ring must be empty");
|
||||
assert!(covered(&tris, [0.5, 5.0]), "outer ring must be filled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_intersecting_boundary_is_well_formed() {
|
||||
// Bowtie / figure-8: a self-intersecting single loop. Must not panic and
|
||||
// must return a well-formed triangle-list (possibly empty -> caller falls
|
||||
// back to the AABB-quad + in_polygon path).
|
||||
let bowtie = [[0.0, 0.0], [10.0, 10.0], [0.0, 10.0], [10.0, 0.0]];
|
||||
let tris = tessellate_boundary(&bowtie);
|
||||
assert_eq!(tris.len() % 3, 0);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
pub mod face3d_gpu;
|
||||
pub mod hatch_batched_gpu;
|
||||
pub mod hatch_tess;
|
||||
pub mod hatch_gpu;
|
||||
pub mod image_gpu;
|
||||
pub mod mesh_gpu;
|
||||
|
||||
Reference in new issue
Block a user