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:
sLuCHa committed 2026-07-07 15:23:54 +02:00
1 parent fd0b8fc1d4
commit b4152ae1fe
6 files changed
+236 -58

No files matched your search

+20 -28
View File
@@ -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.