forked from 1b-agent/opencadstudio-upstream
perf(hatch): tessellate fills so navigation no longer flickers (#258)
The batched hatch pass drew a screen-covering AABB quad per hatch and, for every fragment, ran a point-in-polygon ray-cast over the whole boundary plus the pattern test, with a discard that disabled early-Z. Zoomed into a hatch the quad covers the screen, so the shader re-ran per pixel every frame — the cost the PR #273 interaction-LOD worked around by suppressing hatches during navigation, which made fills flicker on and off while zooming. Triangulate each boundary once (lyon, even-odd — same coverage as the old in_polygon, islands/holes included) and rasterize those triangles instead: no screen-filling overdraw, no per-fragment boundary test. The fragment shader keeps the boundary test only on a per-instance fallback path (poly_test == 1) for boundaries that fail to tessellate. Tessellation is cached with the batch build (geometry_epoch) in local space, so the double-single relative-to-eye precision is preserved. With the fast path the interaction-LOD is no longer needed; it is gated behind OCS_HATCH_LOD (default off) as a safety net for now.
This commit is contained in:
1 parent
fd0b8fc1d4
commit
b4152ae1fe
6 files changed
+236
-58
No files matched your search
@@ -1198,6 +1198,16 @@ impl Scene {
|
||||
.map_or(false, |t| t.elapsed().as_millis() < Self::NAV_SETTLE_MS)
|
||||
}
|
||||
|
||||
/// Whether the interaction-LOD hatch suppression is enabled (env
|
||||
/// `OCS_HATCH_LOD`), read once. Default OFF: the tessellated hatch pass is
|
||||
/// cheap enough that suppression — and its zoom flicker (#258) — is not
|
||||
/// needed. Kept behind the flag as a safety net for pathological drawings.
|
||||
pub fn hatch_lod_enabled(&self) -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static ON: OnceLock<bool> = OnceLock::new();
|
||||
*ON.get_or_init(|| std::env::var_os("OCS_HATCH_LOD").is_some())
|
||||
}
|
||||
|
||||
/// True for a short window around navigation — used by the subscription to
|
||||
/// keep requesting frames just past the settle point so the one full-quality
|
||||
/// (hatched) frame actually renders after the cursor stops, even when no
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//
|
||||
// Data layout — three storage buffers fed from `HatchModel`s:
|
||||
//
|
||||
// InstanceBuffer (binding 0) : HatchInstance[] (~96 B each)
|
||||
// InstanceBuffer (binding 0) : HatchInstance[] (128 B each)
|
||||
// color, color2, mode, gradient,
|
||||
// pattern angle/scale, world_origin,
|
||||
// boundary_offset, boundary_count,
|
||||
@@ -22,13 +22,17 @@
|
||||
// DashBuffer (binding 3) : f32[] (all dash
|
||||
// lengths concatenated)
|
||||
//
|
||||
// The vertex buffer holds 6 corner indices repeated per-instance and a
|
||||
// per-vertex `instance_index` (u32 attribute) — instance_index lets us
|
||||
// avoid relying on `@builtin(instance_index)` for portability. The
|
||||
// vertex shader reads the per-instance AABB from the InstanceBuffer
|
||||
// and emits the quad corner for that instance. When the visibility
|
||||
// flag is 0, it returns a degenerate position and the fragment shader
|
||||
// runs zero times for that instance.
|
||||
// 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
|
||||
@@ -68,7 +72,10 @@ pub struct HatchInstance {
|
||||
/// 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
|
||||
pub _pad0: u32, // 116 (pad to 128 = 16-byte stride)
|
||||
/// 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 _pad1: u32, // 120
|
||||
pub _pad2: u32, // 124
|
||||
}
|
||||
@@ -107,18 +114,18 @@ pub struct LineFamilyGpu {
|
||||
|
||||
const _: () = assert!(std::mem::size_of::<LineFamilyGpu>() == 48);
|
||||
|
||||
/// Per-vertex data — 6 verts per instance. Instance_index here so we
|
||||
/// can match WebGL2 backends that don't expose builtin(instance_index)
|
||||
/// uniformly.
|
||||
/// 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 HatchBatchedVertex {
|
||||
pub corner: u32, // 0..6 — which AABB corner
|
||||
pub instance_index: u32, // index into InstanceBuffer
|
||||
pub local_xy: [f32; 2], // 0 — local-space position
|
||||
pub instance_index: u32, // 8 — index into InstanceBuffer
|
||||
}
|
||||
|
||||
impl HatchBatchedVertex {
|
||||
#[allow(dead_code)]
|
||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<HatchBatchedVertex>() as u64,
|
||||
@@ -127,10 +134,10 @@ impl HatchBatchedVertex {
|
||||
wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Uint32,
|
||||
format: wgpu::VertexFormat::Float32x2,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: 4,
|
||||
offset: 8,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Uint32,
|
||||
},
|
||||
@@ -167,7 +174,7 @@ pub struct HatchBatchedGpu {
|
||||
#[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 112 B-per-instance
|
||||
/// per pan tick. Far cheaper than touching the 128 B-per-instance
|
||||
/// data.
|
||||
pub visibility: Vec<u32>,
|
||||
/// CPU-side mirror of each instance's local-space AABB (world-
|
||||
@@ -192,6 +199,7 @@ 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();
|
||||
@@ -203,6 +211,12 @@ 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;
|
||||
|
||||
@@ -293,7 +307,7 @@ impl HatchBatchedGpu {
|
||||
family_offset,
|
||||
family_count,
|
||||
draw_depth: h.draw_depth,
|
||||
_pad0: 0,
|
||||
poly_test: 1,
|
||||
_pad1: 0,
|
||||
_pad2: 0,
|
||||
});
|
||||
@@ -336,7 +350,7 @@ impl HatchBatchedGpu {
|
||||
family_offset,
|
||||
family_count,
|
||||
draw_depth: h.draw_depth,
|
||||
_pad0: 0,
|
||||
poly_test: if instance_tris.last().map_or(true, |t| t.is_empty()) { 1 } else { 0 },
|
||||
_pad1: 0,
|
||||
_pad2: 0,
|
||||
});
|
||||
@@ -353,15 +367,26 @@ impl HatchBatchedGpu {
|
||||
dashes.push(0.0);
|
||||
}
|
||||
|
||||
// Vertex buffer — 6 verts per instance (two triangles for the
|
||||
// AABB quad), indexed by (corner, instance_index).
|
||||
let mut verts: Vec<HatchBatchedVertex> = Vec::with_capacity(instances.len() * 6);
|
||||
for (i, _) in instances.iter().enumerate() {
|
||||
for corner in 0u32..6 {
|
||||
verts.push(HatchBatchedVertex {
|
||||
corner,
|
||||
instance_index: i as u32,
|
||||
});
|
||||
// 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();
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
//! 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,5 +1,6 @@
|
||||
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;
|
||||
|
||||
@@ -1202,7 +1202,7 @@ impl Scene {
|
||||
// actively moving; the scene-render cache holds the full-quality
|
||||
// (hatched) frame once it settles. Only applied to the on-screen
|
||||
// Model / paper content — the paper *sheet* keeps its fills.
|
||||
skip_hatch: !inst.paper_sheet && self.navigating_lod(),
|
||||
skip_hatch: self.hatch_lod_enabled() && !inst.paper_sheet && self.navigating_lod(),
|
||||
geometry_epoch: self.geometry_epoch,
|
||||
camera_generation: self.camera_generation,
|
||||
wire_content_id,
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
// 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
|
||||
// (corner, instance_index) stream so we don't depend on
|
||||
// @builtin(instance_index) edge cases across backends).
|
||||
// (local_xy, instance_index) stream of tessellated boundary-triangle
|
||||
// positions so we don't depend on @builtin(instance_index) edge cases
|
||||
// across backends).
|
||||
//
|
||||
// Layout — matches `hatch_batched_gpu.rs`:
|
||||
// group 1 binding 0 InstanceBuffer HatchInstance[] (112 B / inst)
|
||||
// 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[]
|
||||
//
|
||||
// Vertex shader emits a per-instance AABB quad (two triangles). A
|
||||
// `visible == 0` instance gets a NaN 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`).
|
||||
// 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 0: shared frame uniforms (matches hatch.wgsl) ──────────────────
|
||||
|
||||
@@ -54,7 +58,7 @@ struct HatchInstance {
|
||||
family_offset: u32,
|
||||
family_count: u32,
|
||||
draw_depth: f32, // signed (-1,1) draw-order bias; 0 = neutral
|
||||
_pad0: u32,
|
||||
poly_test: u32, // 1 = run in_polygon (fallback), 0 = skip
|
||||
_pad1: u32,
|
||||
_pad2: u32,
|
||||
}
|
||||
@@ -91,7 +95,7 @@ struct LineFamily {
|
||||
// ── Vertex shader ────────────────────────────────────────────────────────
|
||||
|
||||
struct VIn {
|
||||
@location(0) corner: u32,
|
||||
@location(0) local_xy: vec2<f32>,
|
||||
@location(1) instance_index: u32,
|
||||
}
|
||||
|
||||
@@ -101,21 +105,6 @@ struct VOut {
|
||||
@location(1) @interpolate(flat) instance_index: u32,
|
||||
}
|
||||
|
||||
fn corner_xy(c: u32, aabb: vec4<f32>) -> vec2<f32> {
|
||||
// Two-triangle quad covering the AABB:
|
||||
// 0 BL, 1 BR, 2 TL, 3 BR, 4 TR, 5 TL
|
||||
let xmin = aabb.x; let ymin = aabb.y;
|
||||
let xmax = aabb.z; let ymax = aabb.w;
|
||||
switch c {
|
||||
case 0u: { return vec2<f32>(xmin, ymin); }
|
||||
case 1u: { return vec2<f32>(xmax, ymin); }
|
||||
case 2u: { return vec2<f32>(xmin, ymax); }
|
||||
case 3u: { return vec2<f32>(xmax, ymin); }
|
||||
case 4u: { return vec2<f32>(xmax, ymax); }
|
||||
default: { return vec2<f32>(xmin, ymax); }
|
||||
}
|
||||
}
|
||||
|
||||
@vertex fn vs_main(v: VIn) -> VOut {
|
||||
var o: VOut;
|
||||
let inst = instances[v.instance_index];
|
||||
@@ -132,7 +121,7 @@ fn corner_xy(c: u32, aabb: vec4<f32>) -> vec2<f32> {
|
||||
return o;
|
||||
}
|
||||
|
||||
let local = corner_xy(v.corner, inst.aabb);
|
||||
let local = v.local_xy;
|
||||
// Double-single relative-to-eye: the anchor high half cancels exactly
|
||||
// against eye_high (Sterbenz); local + anchor low + (−eye_low) carry the
|
||||
// residual. `local` is small (boundary-relative), so adding it in the low
|
||||
@@ -270,9 +259,12 @@ fn check_family(
|
||||
@fragment fn fs_main(v: VOut) -> @location(0) vec4<f32> {
|
||||
let inst = instances[v.instance_index];
|
||||
|
||||
// 1. Boundary test.
|
||||
if !in_polygon(v.xz, inst.boundary_offset, inst.boundary_count) {
|
||||
discard;
|
||||
// 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.
|
||||
|
||||
Reference in new issue
Block a user