Hatch/wipeout boundary sub-loops were separated by NaN vertices that the fill shaders detected with `p.x == p.x`. Intel Mesa folds that NaN self-compare to `true` under fast math, so separators read as real vertices, the ray-cast crossing count broke, and fills bled outside their boundary — only on Intel iGPUs. GPU uploads now rewrite the separators to a finite 1e30 sentinel (GPU_BOUNDARY_SEP) and the shaders test `abs(x) < 1e29`, which every driver evaluates identically. The CPU-side NaN representation is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0c578b03f3
commit
805da6428a
7 changed files with 46 additions and 10 deletions
|
|
@ -133,6 +133,13 @@ pub enum HatchPattern {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GPU-side separator between disconnected boundary sub-loops. A finite
|
||||||
|
/// magnitude sentinel instead of NaN: shaders detect it with a plain
|
||||||
|
/// `abs(x) < 1e29` compare, which every driver evaluates identically,
|
||||||
|
/// unlike NaN self-comparison (#386, #416). Boundary verts are local
|
||||||
|
/// (anchor-relative), so real coordinates never approach it.
|
||||||
|
pub const GPU_BOUNDARY_SEP: f32 = 1.0e30;
|
||||||
|
|
||||||
/// A hatched region defined by a closed polygon boundary.
|
/// A hatched region defined by a closed polygon boundary.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct HatchModel {
|
pub struct HatchModel {
|
||||||
|
|
@ -152,6 +159,10 @@ pub struct HatchModel {
|
||||||
/// World-XY coordinates of the boundary polygon vertices, stored as
|
/// World-XY coordinates of the boundary polygon vertices, stored as
|
||||||
/// f32 offsets from `world_origin`. NaN-NaN sentinels separate
|
/// f32 offsets from `world_origin`. NaN-NaN sentinels separate
|
||||||
/// disconnected paths and must be preserved un-shifted by consumers.
|
/// disconnected paths and must be preserved un-shifted by consumers.
|
||||||
|
/// GPU uploads rewrite them to [`GPU_BOUNDARY_SEP`] — some drivers
|
||||||
|
/// (Intel Mesa) fold the shader-side `x == x` NaN test to `true`
|
||||||
|
/// under fast math, which turned separators into real vertices and
|
||||||
|
/// bled fills outside their boundary (#386, #416).
|
||||||
pub boundary: Arc<Vec<[f32; 2]>>,
|
pub boundary: Arc<Vec<[f32; 2]>>,
|
||||||
/// Exact absolute-WCS boundary in f64, set only by the draw commands so a
|
/// Exact absolute-WCS boundary in f64, set only by the draw commands so a
|
||||||
/// typed boundary vertex is persisted without the f32 quantization the
|
/// typed boundary vertex is persisted without the f32 quantization the
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,16 @@ impl HatchGpu {
|
||||||
for h in hatches {
|
for h in hatches {
|
||||||
let boundary_offset = boundary.len() as u32;
|
let boundary_offset = boundary.len() as u32;
|
||||||
for &[x, y] in h.boundary.iter() {
|
for &[x, y] in h.boundary.iter() {
|
||||||
|
// NaN sub-loop separators become the finite sentinel on the
|
||||||
|
// GPU: Intel drivers fold the shader NaN self-compare to
|
||||||
|
// `true`, turning separators into vertices and bleeding the
|
||||||
|
// fill outside its boundary (#386, #416).
|
||||||
|
if x.is_finite() && y.is_finite() {
|
||||||
boundary.push([x, y, 0.0, 0.0]);
|
boundary.push([x, y, 0.0, 0.0]);
|
||||||
|
} else {
|
||||||
|
let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP;
|
||||||
|
boundary.push([s, s, 0.0, 0.0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let boundary_count = boundary.len() as u32 - boundary_offset;
|
let boundary_count = boundary.len() as u32 - boundary_offset;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -203,13 +203,15 @@ impl HatchWebGpu {
|
||||||
|
|
||||||
// ── Pack the data texture: boundary | families | dashes ───────────
|
// ── Pack the data texture: boundary | families | dashes ───────────
|
||||||
let mut texels: Vec<[f32; 4]> = Vec::new();
|
let mut texels: Vec<[f32; 4]> = Vec::new();
|
||||||
// Boundary section (texels 0..vcount). NaN separators preserved (they
|
// Boundary section (texels 0..vcount). NaN separators become the
|
||||||
// survive the RGBA32F fetch byte-for-byte).
|
// finite GPU sentinel — the shader NaN self-compare is folded to
|
||||||
|
// `true` on some drivers (#386, #416).
|
||||||
for &[x, y] in model.boundary.iter() {
|
for &[x, y] in model.boundary.iter() {
|
||||||
if x.is_finite() && y.is_finite() {
|
if x.is_finite() && y.is_finite() {
|
||||||
texels.push([x + drift[0], y + drift[1], 0.0, 0.0]);
|
texels.push([x + drift[0], y + drift[1], 0.0, 0.0]);
|
||||||
} else {
|
} else {
|
||||||
texels.push([f32::NAN, f32::NAN, 0.0, 0.0]);
|
let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP;
|
||||||
|
texels.push([s, s, 0.0, 0.0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let vcount = texels.len() as u32;
|
let vcount = texels.len() as u32;
|
||||||
|
|
|
||||||
|
|
@ -310,12 +310,14 @@ impl WipeoutGpu {
|
||||||
.take(MAX_HATCH_BOUNDARY_VERTS)
|
.take(MAX_HATCH_BOUNDARY_VERTS)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
// Preserve NaN separators — they signal path breaks to the
|
// Path-break separators go up as the finite GPU sentinel, not
|
||||||
// in_polygon ray-cast, must not be shifted into finite numbers.
|
// NaN — the shader NaN self-compare is folded to `true` on some
|
||||||
|
// drivers (#386, #416) — and must not be shifted by `drift`.
|
||||||
if x.is_finite() && y.is_finite() {
|
if x.is_finite() && y.is_finite() {
|
||||||
boundary_data.verts[i] = [x + drift[0], y + drift[1], 0.0, 0.0];
|
boundary_data.verts[i] = [x + drift[0], y + drift[1], 0.0, 0.0];
|
||||||
} else {
|
} else {
|
||||||
boundary_data.verts[i] = [f32::NAN, f32::NAN, 0.0, 0.0];
|
let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP;
|
||||||
|
boundary_data.verts[i] = [s, s, 0.0, 0.0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,11 @@ struct VOut {
|
||||||
// ── Point-in-polygon (ray casting) over a sub-range of BoundaryBuffer ────
|
// ── Point-in-polygon (ray casting) over a sub-range of BoundaryBuffer ────
|
||||||
|
|
||||||
fn valid_vertex(p: vec2<f32>) -> bool {
|
fn valid_vertex(p: vec2<f32>) -> bool {
|
||||||
return p.x == p.x && p.y == p.y;
|
// 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 {
|
fn edge_crosses(p: vec2<f32>, a: vec2<f32>, c: vec2<f32>) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,11 @@ struct VOut {
|
||||||
// ── Point-in-polygon (ray casting) ────────────────────────────────────────
|
// ── Point-in-polygon (ray casting) ────────────────────────────────────────
|
||||||
|
|
||||||
fn valid_vertex(p: vec2<f32>) -> bool {
|
fn valid_vertex(p: vec2<f32>) -> bool {
|
||||||
return p.x == p.x && p.y == p.y;
|
// 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 {
|
fn edge_crosses(p: vec2<f32>, a: vec2<f32>, c: vec2<f32>) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,11 @@ struct VOut {
|
||||||
// ── Point-in-polygon (ray casting) ────────────────────────────────────────
|
// ── Point-in-polygon (ray casting) ────────────────────────────────────────
|
||||||
|
|
||||||
fn valid_vertex(p: vec2<f32>) -> bool {
|
fn valid_vertex(p: vec2<f32>) -> bool {
|
||||||
return p.x == p.x && p.y == p.y;
|
// 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 {
|
fn edge_crosses(p: vec2<f32>, a: vec2<f32>, c: vec2<f32>) -> bool {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue