refactor(hatch): unify capability backends

Keep storage and texture transports behind one renderer lifecycle so Pipeline no longer branches by backend.
This commit is contained in:
Hakan Seven committed 2026-07-29 13:40:46 +03:00
1 parent dc7d03dbe1
commit d80637cda7
7 files changed
+385 -322

No files matched your search

+300
View File
@@ -0,0 +1,300 @@
// 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.
//
// 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)
// ── Group 0: frame uniforms (shared) ──────────────────────────────────────
struct Uniforms {
viewport_size: vec2<f32>,
world_per_pixel: f32,
lwdisplay_enable: f32,
flat_shade: f32,
transparency_enable: f32,
_pad: vec2<f32>,
view_rot: mat4x4<f32>,
eye_high: vec3<f32>,
_pad_eh: f32,
eye_low: vec3<f32>,
_pad_el: f32,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
// ── Group 1: per-hatch data ────────────────────────────────────────────────
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
angle_offset: f32, // 40
scale: f32, // 44
grad_cos: f32, // 48
grad_sin: f32, // 52
grad_min: f32, // 56
grad_range: f32, // 60
origin: vec2<f32>, // 64
origin_low: vec2<f32>, // 72
n_families: u32, // 80
fam_off: u32, // 84: texel offset of the family section
dash_off: u32, // 88: texel offset of the dash section
tex_width: u32, // 92: data texture width in texels
}
@group(1) @binding(0) var<uniform> h: HatchUniforms;
@group(1) @binding(1) var data_tex: texture_2d<f32>;
// One line family, reconstructed from 3 texels.
struct LineFamily {
cos_a: f32,
sin_a: f32,
x0: f32,
y0: f32,
dx: f32,
dy: f32,
perp_step: f32,
along_step: f32,
line_width: f32,
period: f32,
n_dashes: u32,
dash_off: u32,
}
fn texel(i: u32) -> vec4<f32> {
let w = h.tex_width;
return textureLoad(data_tex, vec2<u32>(i % w, i / w), 0);
}
fn load_family(fi: u32) -> LineFamily {
let base = h.fam_off + fi * 3u;
let t0 = texel(base);
let t1 = texel(base + 1u);
let t2 = texel(base + 2u);
var f: LineFamily;
f.cos_a = t0.x; f.sin_a = t0.y; f.x0 = t0.z; f.y0 = t0.w;
f.dx = t1.x; f.dy = t1.y; f.perp_step = t1.z; f.along_step = t1.w;
f.line_width = t2.x; f.period = t2.y;
// Counts stored as exact f32 (small integers, so no denormal/bitcast hazard
// through the float texture fetch).
f.n_dashes = u32(t2.z); f.dash_off = u32(t2.w);
return f;
}
// ── Vertex shader (identical to wipeout.wgsl) ──────────────────────────────
struct VIn { @location(0) pos: vec3<f32> }
struct VOut {
@builtin(position) clip: vec4<f32>,
@location(0) xz: vec2<f32>,
}
@vertex fn vs_main(v: VIn) -> VOut {
var o: VOut;
// Double-single relative-to-eye: anchor high cancels eye_high (Sterbenz);
// boundary-local v.pos + anchor low + (−eye_low) carry the residual.
let hi = vec3<f32>(h.origin.x - u.eye_high.x,
h.origin.y - u.eye_high.y,
-u.eye_high.z);
let lo = vec3<f32>(v.pos.x + h.origin_low.x - u.eye_low.x,
v.pos.y + h.origin_low.y - u.eye_low.y,
v.pos.z - u.eye_low.z);
o.clip = u.view_rot * vec4<f32>(hi + lo, 1.0);
o.xz = vec2<f32>(v.pos.x, v.pos.y);
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
// fs_main: derivative builtins must run in uniform control flow, and the
// per-family loop's early return makes later iterations non-uniform.
fn check_family(
xz: vec2<f32>,
ddx_xz: vec2<f32>,
ddy_xz: vec2<f32>,
fam: LineFamily,
cos_off: f32,
sin_off: f32,
scale: f32,
) -> bool {
let cos_a = fam.cos_a * cos_off - fam.sin_a * sin_off;
let sin_a = fam.sin_a * cos_off + fam.cos_a * sin_off;
let ox = (fam.x0 * cos_off - fam.y0 * sin_off) * scale;
let oz = (fam.x0 * sin_off + fam.y0 * cos_off) * scale;
let px = xz.x - ox;
let pz = xz.y - oz;
let perp_step = fam.perp_step * scale;
let perp = -px * sin_a + pz * cos_a;
let k = round(perp / perp_step);
let dperp = perp - k * perp_step;
let d = abs(dperp);
// perp is linear in xz (offsets are constant), so its derivatives are the
// xz derivatives rotated into the family frame.
let half_px = length(vec2<f32>(
-ddx_xz.x * sin_a + ddx_xz.y * cos_a,
-ddy_xz.x * sin_a + ddy_xz.y * cos_a,
)) * 0.5;
let wpx = length(vec2<f32>(ddx_xz.x, ddy_xz.x));
let wpy = length(vec2<f32>(ddx_xz.y, ddy_xz.y));
if d > half_px * 2.0 { return false; }
if fam.n_dashes == 0u { return d <= half_px; }
let along_step = fam.along_step * scale;
let period = fam.period * scale;
let along = px * cos_a + pz * sin_a;
let t = along - k * along_step;
let t_mod = ((t % period) + period) % period;
var pos = 0.0;
for (var j = 0u; j < fam.n_dashes; j++) {
let idx = fam.dash_off + j;
let sv = texel(h.dash_off + idx / 4u)[idx % 4u] * scale;
if sv > 0.0 {
if d <= half_px && t_mod >= pos && t_mod < pos + sv { return true; }
pos = pos + sv;
} else if sv < 0.0 {
pos = pos - sv;
} else {
let dtv = (t - pos) - round((t - pos) / period) * period;
let owx = -dtv * cos_a + dperp * sin_a;
let owy = -dtv * sin_a - dperp * cos_a;
if abs(owx / wpx) <= 0.5 && abs(owy / wpy) <= 0.5 { return true; }
}
}
return false;
}
// ── Fragment shader ────────────────────────────────────────────────────────
@fragment fn fs_main(v: VOut) -> @location(0) vec4<f32> {
// Taken here, in uniform control flow — see 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;
if base_mode == 1u {
return h.color;
} else if base_mode == 2u {
let proj = v.xz.x * h.grad_cos + v.xz.y * h.grad_sin;
var t = clamp((proj - h.grad_min) / h.grad_range, 0.0, 1.0);
// Shape profile: cylinder mirrors around the middle, curved eases in.
if gk == 1u {
t = 1.0 - abs(2.0 * t - 1.0);
} else if gk == 4u {
t = t * t;
}
if ginv {
t = 1.0 - t;
}
return mix(h.color, h.color2, t);
} else if base_mode == 3u {
// Radial gradient: centre is (grad_cos, grad_sin), radius is grad_range.
let d = length(v.xz - vec2<f32>(h.grad_cos, h.grad_sin));
var t = clamp(d / h.grad_range, 0.0, 1.0);
if gk == 3u {
t = sqrt(t);
}
if ginv {
t = 1.0 - t;
}
// Radial stops run outside-in (colour 2 at the centre) — see hatch.wgsl.
return mix(h.color2, h.color, t);
}
// Pattern LOD: when the densest family projects below 2 px, lines blur into
// a solid fill — return solid instead of looping every family.
if u.world_per_pixel > 0.0 && h.n_families > 0u {
var min_spacing_world: f32 = 1.0e30;
for (var i = 0u; i < h.n_families; i++) {
let s = abs(load_family(i).perp_step) * h.scale;
if s > 0.0 && s < min_spacing_world {
min_spacing_world = s;
}
}
if min_spacing_world / u.world_per_pixel < 2.0 {
return h.color;
}
}
let cos_off = cos(h.angle_offset);
let sin_off = sin(h.angle_offset);
for (var i = 0u; i < h.n_families; i++) {
if check_family(v.xz, ddx_xz, ddy_xz, load_family(i), cos_off, sin_off, h.scale) {
return h.color;
}
}
discard;
// Unreachable, but FXC/DX12 requires a return after every terminal discard.
return vec4<f32>(0.0);
}