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
@@ -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