three-slicer 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -0
- package/README.md +6 -0
- package/data/config-schema-lean.json +1 -1
- package/data/config-schema.json +6 -34
- package/data/filaments.js +1 -1
- package/data/printers.json +1 -1
- package/data/processes.js +1 -1
- package/engine/PARAMS.md +86 -8
- package/engine/src/contour_front_gpu.js +291 -0
- package/engine/src/contour_front_shaders.js +449 -0
- package/engine/src/contour_gpu.js +367 -0
- package/engine/src/contour_gpu_shaders.js +450 -0
- package/engine/src/contour_slice.js +131 -0
- package/engine/src/settings.js +25 -13
- package/engine/src/slicer.worker.js +50 -4
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/engine/src/slicer_core.mt.wasm +0 -0
- package/package.json +2 -2
- package/types/data/filaments.js.d.ts +6 -1
- package/types/data/printers.json.d.ts +13 -3
- package/types/data/processes.js.d.ts +6 -1
- package/types/settings.d.ts +16 -1
- package/types/worker.d.ts +4 -1
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
// WGSL of the GPU front of PASS1 (contour_front_gpu.js): tri_plane -> chain_polys -> cancel_coincident ->
|
|
2
|
+
// build_gpu_input, every layer at once. Ported from the measured experiment (.omc/plans/gpu-work/fullport/gpu_slice.mjs,
|
|
3
|
+
// design report gpu-full-port-trial-2026-10-01.md section 3); edited only with a measurement beside it, as
|
|
4
|
+
// contour_gpu_shaders.js.
|
|
5
|
+
//
|
|
6
|
+
// Invocation index: dispatches put at most 65535 workgroups of 256 on x and the rest on y (contour_gpu.js), so an
|
|
7
|
+
// invocation's index is g.x + g.y * 16776960.
|
|
8
|
+
|
|
9
|
+
export const NONE = 0xffffffff
|
|
10
|
+
export const LIST_LIMIT = 16 // segment ends meeting at one chain key that the matcher sorts; more is reported
|
|
11
|
+
|
|
12
|
+
const TID = /* wgsl */`fn tid(g: vec3<u32>) -> u32 { return g.x + g.y * 16776960u; }`
|
|
13
|
+
|
|
14
|
+
// The kernel interpolates in double and rounds to 1e-6 mm; plain f32 gives another integer in 885k of 1M random
|
|
15
|
+
// cases, double-float (two f32) in none. Metal compiles with fast math, which folds two_sum's error term away (measured:
|
|
16
|
+
// errors of thousands of units), so every intermediate the error terms depend on goes through opaque(): an XOR with a
|
|
17
|
+
// uniform zero the compiler cannot see through. The shader declares that uniform as `dfZero`.
|
|
18
|
+
const DOUBLE_FLOAT = /* wgsl */`
|
|
19
|
+
fn opaque(a: f32) -> f32 { return bitcast<f32>(bitcast<u32>(a) ^ dfZero); }
|
|
20
|
+
fn two_sum(a: f32, b: f32) -> vec2<f32> { let s = opaque(a + b); let bb = opaque(s - a); return vec2<f32>(s, opaque(a - opaque(s - bb)) + opaque(b - bb)); }
|
|
21
|
+
fn quick_two_sum(a: f32, b: f32) -> vec2<f32> { let s = opaque(a + b); return vec2<f32>(s, b - opaque(s - a)); }
|
|
22
|
+
fn df_add(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> {
|
|
23
|
+
var s = two_sum(a.x, b.x); let t = two_sum(a.y, b.y);
|
|
24
|
+
s.y = s.y + t.x; s = quick_two_sum(s.x, s.y); s.y = s.y + t.y; return quick_two_sum(s.x, s.y); }
|
|
25
|
+
fn df_neg(a: vec2<f32>) -> vec2<f32> { return vec2<f32>(-a.x, -a.y); }
|
|
26
|
+
fn df_sub(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> { return df_add(a, df_neg(b)); }
|
|
27
|
+
fn two_prod(a: f32, b: f32) -> vec2<f32> { let p = opaque(a * b); return vec2<f32>(p, fma(a, b, -p)); }
|
|
28
|
+
fn df_mul(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> { var p = two_prod(a.x, b.x); p.y = p.y + (a.x * b.y + a.y * b.x); return quick_two_sum(p.x, p.y); }
|
|
29
|
+
fn df_div(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> {
|
|
30
|
+
let q1 = opaque(a.x / b.x); var r = df_sub(a, df_mul(b, vec2<f32>(q1, 0.0)));
|
|
31
|
+
let q2 = opaque(r.x / b.x); r = df_sub(r, df_mul(b, vec2<f32>(q2, 0.0)));
|
|
32
|
+
let q3 = opaque(r.x / b.x); return df_add(quick_two_sum(q1, q2), vec2<f32>(q3, 0.0)); }
|
|
33
|
+
// nearest integer of a df value below 2^31 in magnitude, ties away from zero (llround)
|
|
34
|
+
fn df_round_i32(a: vec2<f32>) -> i32 {
|
|
35
|
+
let hiInt = round(a.x);
|
|
36
|
+
let rest = two_sum(opaque(a.x - hiInt), a.y);
|
|
37
|
+
let r = rest.x + rest.y;
|
|
38
|
+
var fl = floor(r); let fr = opaque(r - fl);
|
|
39
|
+
if (fr > 0.5 || (fr == 0.5 && r > 0.0)) { fl = fl + 1.0; }
|
|
40
|
+
return i32(hiInt) + i32(fl);
|
|
41
|
+
}`
|
|
42
|
+
|
|
43
|
+
// ---- triangle -> segments (count mode writes the per-triangle count, emit mode the segments)
|
|
44
|
+
export const CUT_SHADER = /* wgsl */`
|
|
45
|
+
@group(0) @binding(0) var<uniform> P: vec4<u32>; // triangles, layers, emit
|
|
46
|
+
@group(0) @binding(1) var<storage, read> tris: array<f32>;
|
|
47
|
+
@group(0) @binding(2) var<storage, read> zs: array<vec2<f32>>; // each layer's z as hi + lo
|
|
48
|
+
@group(0) @binding(3) var<storage, read_write> counts: array<u32>; // count mode: per triangle; emit mode: its first segment
|
|
49
|
+
@group(0) @binding(4) var<storage, read_write> segPoints: array<vec4<i32>>; // x0 y0 x1 y1 (1e-6 mm)
|
|
50
|
+
@group(0) @binding(5) var<storage, read_write> segKeys: array<vec4<i32>>; // the chain keys of both ends (1e-3 mm)
|
|
51
|
+
@group(0) @binding(6) var<storage, read_write> segLayer: array<u32>;
|
|
52
|
+
@group(0) @binding(7) var<uniform> dfZero: u32;
|
|
53
|
+
${DOUBLE_FLOAT}
|
|
54
|
+
${TID}
|
|
55
|
+
fn vBelow(v: f32, L: u32) -> bool { let z = zs[L]; return v < z.x || (v == z.x && z.y > 0.0); } // v < z exactly
|
|
56
|
+
fn zBelow(L: u32, v: f32) -> bool { let z = zs[L]; return z.x < v || (z.x == v && z.y < 0.0); } // z < v exactly
|
|
57
|
+
fn vtx(t: u32, k: u32) -> vec3<f32> { let b = t * 9u + k * 3u; return vec3<f32>(tris[b], tris[b + 1u], tris[b + 2u]); }
|
|
58
|
+
fn df(v: f32) -> vec2<f32> { return vec2<f32>(v, 0.0); }
|
|
59
|
+
fn dfSign(a: vec2<f32>) -> f32 { if (a.x != 0.0) { return a.x; } return a.y; }
|
|
60
|
+
struct Cut { ok: bool, x0: vec2<f32>, y0: vec2<f32>, x1: vec2<f32>, y1: vec2<f32> }
|
|
61
|
+
fn cut(t: u32, L: u32) -> Cut {
|
|
62
|
+
var r: Cut; r.ok = false;
|
|
63
|
+
var cx: array<vec2<f32>, 2>; var cy: array<vec2<f32>, 2>; var n = 0u;
|
|
64
|
+
let z = zs[L];
|
|
65
|
+
for (var e = 0u; e < 3u; e = e + 1u) {
|
|
66
|
+
let s = vtx(t, e); let en = vtx(t, (e + 1u) % 3u);
|
|
67
|
+
let crosses = (vBelow(s.z, L) && !vBelow(en.z, L)) || (vBelow(en.z, L) && !vBelow(s.z, L));
|
|
68
|
+
if (crosses) {
|
|
69
|
+
if (n < 2u) {
|
|
70
|
+
let f = df_div(df_sub(z, df(s.z)), df_sub(df(en.z), df(s.z)));
|
|
71
|
+
cx[n] = df_add(df(s.x), df_mul(f, df_sub(df(en.x), df(s.x))));
|
|
72
|
+
cy[n] = df_add(df(s.y), df_mul(f, df_sub(df(en.y), df(s.y))));
|
|
73
|
+
}
|
|
74
|
+
n = n + 1u;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (n != 2u) { return r; }
|
|
78
|
+
let a = vtx(t, 0u); let b = vtx(t, 1u); let c = vtx(t, 2u);
|
|
79
|
+
let e1x = df_sub(df(b.x), df(a.x)); let e1y = df_sub(df(b.y), df(a.y)); let e1z = df_sub(df(b.z), df(a.z));
|
|
80
|
+
let e2x = df_sub(df(c.x), df(a.x)); let e2y = df_sub(df(c.y), df(a.y)); let e2z = df_sub(df(c.z), df(a.z));
|
|
81
|
+
let nx = df_sub(df_mul(e1y, e2z), df_mul(e1z, e2y));
|
|
82
|
+
let ny = df_sub(df_mul(e1z, e2x), df_mul(e1x, e2z));
|
|
83
|
+
let dx = df_sub(cx[1], cx[0]); let dy = df_sub(cy[1], cy[0]);
|
|
84
|
+
let along = df_sub(df_mul(dy, nx), df_mul(dx, ny));
|
|
85
|
+
r.ok = true;
|
|
86
|
+
if (dfSign(along) < 0.0) { r.x0 = cx[1]; r.y0 = cy[1]; r.x1 = cx[0]; r.y1 = cy[0]; }
|
|
87
|
+
else { r.x0 = cx[0]; r.y0 = cy[0]; r.x1 = cx[1]; r.y1 = cy[1]; }
|
|
88
|
+
return r;
|
|
89
|
+
}
|
|
90
|
+
fn firstLayer(zmin: f32) -> u32 { // lower_bound: the first layer whose z is not below zmin
|
|
91
|
+
var lo = 0u; var hi = P.y;
|
|
92
|
+
while (lo < hi) { let mid = (lo + hi) / 2u; if (zBelow(mid, zmin)) { lo = mid + 1u; } else { hi = mid; } }
|
|
93
|
+
return lo;
|
|
94
|
+
}
|
|
95
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
96
|
+
let t = tid(g); if (t >= P.x) { return; }
|
|
97
|
+
let a = vtx(t, 0u); let b = vtx(t, 1u); let c = vtx(t, 2u);
|
|
98
|
+
let zmin = min(a.z, min(b.z, c.z)); let zmax = max(a.z, max(b.z, c.z));
|
|
99
|
+
var L = firstLayer(zmin); var k = 0u;
|
|
100
|
+
var out = 0u; if (P.z == 1u) { out = counts[t]; }
|
|
101
|
+
loop {
|
|
102
|
+
if (L >= P.y || !zBelow(L, zmax)) { break; }
|
|
103
|
+
let r = cut(t, L);
|
|
104
|
+
if (r.ok) {
|
|
105
|
+
if (P.z == 1u) {
|
|
106
|
+
let s = out + k;
|
|
107
|
+
let m = df(1000000.0); let q = df(1000.0);
|
|
108
|
+
segPoints[s] = vec4<i32>(df_round_i32(df_mul(r.x0, m)), df_round_i32(df_mul(r.y0, m)), df_round_i32(df_mul(r.x1, m)), df_round_i32(df_mul(r.y1, m)));
|
|
109
|
+
segKeys[s] = vec4<i32>(df_round_i32(df_mul(r.x0, q)), df_round_i32(df_mul(r.y0, q)), df_round_i32(df_mul(r.x1, q)), df_round_i32(df_mul(r.y1, q)));
|
|
110
|
+
segLayer[s] = L;
|
|
111
|
+
}
|
|
112
|
+
k = k + 1u;
|
|
113
|
+
}
|
|
114
|
+
L = L + 1u;
|
|
115
|
+
}
|
|
116
|
+
if (P.z == 0u) { counts[t] = k; }
|
|
117
|
+
}`
|
|
118
|
+
|
|
119
|
+
export const HASH_SHADER = /* wgsl */`
|
|
120
|
+
fn mix(h0: u32, v: u32) -> u32 { var h = h0 ^ (v * 0x9E3779B1u); h = (h ^ (h >> 16u)) * 0x85EBCA6Bu; h = (h ^ (h >> 13u)) * 0xC2B2AE35u; return h ^ (h >> 16u); }`
|
|
121
|
+
|
|
122
|
+
// ---- cancel_coincident: segments that coincide exactly in opposite directions are removed in pairs
|
|
123
|
+
export const CANCEL_SHADER = /* wgsl */`
|
|
124
|
+
@group(0) @binding(0) var<uniform> P: vec4<u32>; // segments, table mask
|
|
125
|
+
@group(0) @binding(1) var<storage, read> segPoints: array<vec4<i32>>;
|
|
126
|
+
@group(0) @binding(2) var<storage, read> segLayer: array<u32>;
|
|
127
|
+
@group(0) @binding(3) var<storage, read_write> owner: array<atomic<u32>>;
|
|
128
|
+
@group(0) @binding(4) var<storage, read_write> headForward: array<atomic<u32>>;
|
|
129
|
+
@group(0) @binding(5) var<storage, read_write> headBackward: array<atomic<u32>>;
|
|
130
|
+
@group(0) @binding(6) var<storage, read_write> next: array<u32>;
|
|
131
|
+
@group(0) @binding(7) var<storage, read_write> dead: array<u32>;
|
|
132
|
+
${HASH_SHADER}
|
|
133
|
+
${TID}
|
|
134
|
+
fn startIsLow(p: vec4<i32>) -> bool { return p.x < p.z || (p.x == p.z && p.y < p.w); }
|
|
135
|
+
fn place(s: u32) -> vec4<i32> { let p = segPoints[s]; if (startIsLow(p)) { return p; } return vec4<i32>(p.z, p.w, p.x, p.y); }
|
|
136
|
+
fn samePlace(a: u32, b: u32) -> bool { return segLayer[a] == segLayer[b] && all(place(a) == place(b)); }
|
|
137
|
+
fn zeroLength(s: u32) -> bool { let p = segPoints[s]; return p.x == p.z && p.y == p.w; }
|
|
138
|
+
@compute @workgroup_size(256) fn insert(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
139
|
+
let s = tid(g); if (s >= P.x || zeroLength(s)) { return; }
|
|
140
|
+
let pl = place(s);
|
|
141
|
+
var slot = mix(mix(mix(mix(mix(0u, segLayer[s]), bitcast<u32>(pl.x)), bitcast<u32>(pl.y)), bitcast<u32>(pl.z)), bitcast<u32>(pl.w)) & P.y;
|
|
142
|
+
loop {
|
|
143
|
+
let o = atomicLoad(&owner[slot]);
|
|
144
|
+
if (o == 0u) { let r = atomicCompareExchangeWeak(&owner[slot], 0u, s + 1u); if (r.exchanged) { break; } continue; }
|
|
145
|
+
if (samePlace(o - 1u, s)) { break; }
|
|
146
|
+
slot = (slot + 1u) & P.y;
|
|
147
|
+
}
|
|
148
|
+
if (startIsLow(segPoints[s])) { next[s] = atomicExchange(&headForward[slot], s + 1u); }
|
|
149
|
+
else { next[s] = atomicExchange(&headBackward[slot], s + 1u); }
|
|
150
|
+
}
|
|
151
|
+
// the first min(forward, backward) of each direction by segment order die (exact duplicates: which ones is immaterial)
|
|
152
|
+
fn sortedList(head: u32, out: ptr<function, array<u32, ${LIST_LIMIT}>>) -> u32 {
|
|
153
|
+
var n = 0u; var cur = head;
|
|
154
|
+
while (cur != 0u && n < ${LIST_LIMIT}u) { (*out)[n] = cur - 1u; n = n + 1u; cur = next[cur - 1u]; }
|
|
155
|
+
for (var i = 1u; i < n; i = i + 1u) { let v = (*out)[i]; var j = i; while (j > 0u && (*out)[j - 1u] > v) { (*out)[j] = (*out)[j - 1u]; j = j - 1u; } (*out)[j] = v; }
|
|
156
|
+
return n;
|
|
157
|
+
}
|
|
158
|
+
@compute @workgroup_size(256) fn pair(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
159
|
+
let slot = tid(g); if (slot > P.y) { return; }
|
|
160
|
+
if (atomicLoad(&owner[slot]) == 0u) { return; }
|
|
161
|
+
var f: array<u32, ${LIST_LIMIT}>; var b: array<u32, ${LIST_LIMIT}>;
|
|
162
|
+
let nf = sortedList(atomicLoad(&headForward[slot]), &f); let nb = sortedList(atomicLoad(&headBackward[slot]), &b);
|
|
163
|
+
let pairs = min(nf, nb);
|
|
164
|
+
for (var k = 0u; k < pairs; k = k + 1u) { dead[f[k]] = 1u; dead[b[k]] = 1u; }
|
|
165
|
+
}`
|
|
166
|
+
|
|
167
|
+
// ---- chain: each live segment's successor is a live segment starting at its end key
|
|
168
|
+
export const CHAIN_SHADER = /* wgsl */`
|
|
169
|
+
@group(0) @binding(0) var<uniform> P: vec4<u32>; // segments, table mask
|
|
170
|
+
@group(0) @binding(1) var<storage, read> segKeys: array<vec4<i32>>;
|
|
171
|
+
@group(0) @binding(2) var<storage, read> segLayer: array<u32>;
|
|
172
|
+
@group(0) @binding(3) var<storage, read> dead: array<u32>;
|
|
173
|
+
@group(0) @binding(4) var<storage, read_write> owner: array<atomic<u32>>;
|
|
174
|
+
@group(0) @binding(5) var<storage, read_write> startHead: array<atomic<u32>>;
|
|
175
|
+
@group(0) @binding(6) var<storage, read_write> endHead: array<atomic<u32>>;
|
|
176
|
+
@group(0) @binding(7) var<storage, read_write> startNext: array<u32>;
|
|
177
|
+
@group(0) @binding(8) var<storage, read_write> endNext: array<u32>;
|
|
178
|
+
@group(0) @binding(9) var<storage, read_write> successor: array<u32>; // also [P.x]: matcher overflow count, [P.x+1]: open ends
|
|
179
|
+
${HASH_SHADER}
|
|
180
|
+
${TID}
|
|
181
|
+
fn slotOf(L: u32, x: i32, y: i32) -> u32 { return mix(mix(mix(0u, L), bitcast<u32>(x)), bitcast<u32>(y)) & P.y; }
|
|
182
|
+
@compute @workgroup_size(256) fn insertStarts(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
183
|
+
let s = tid(g); if (s >= P.x) { return; }
|
|
184
|
+
successor[s] = ${NONE}u;
|
|
185
|
+
if (dead[s] != 0u) { return; }
|
|
186
|
+
let k = segKeys[s]; let L = segLayer[s];
|
|
187
|
+
var slot = slotOf(L, k.x, k.y);
|
|
188
|
+
loop {
|
|
189
|
+
let o = atomicLoad(&owner[slot]);
|
|
190
|
+
if (o == 0u) { let r = atomicCompareExchangeWeak(&owner[slot], 0u, s + 1u); if (r.exchanged) { break; } continue; }
|
|
191
|
+
let other = o - 1u; let ok = segKeys[other];
|
|
192
|
+
if (segLayer[other] == L && ok.x == k.x && ok.y == k.y) { break; }
|
|
193
|
+
slot = (slot + 1u) & P.y;
|
|
194
|
+
}
|
|
195
|
+
startNext[s] = atomicExchange(&startHead[slot], s + 1u);
|
|
196
|
+
}
|
|
197
|
+
@compute @workgroup_size(256) fn insertEnds(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
198
|
+
let s = tid(g); if (s >= P.x || dead[s] != 0u) { return; }
|
|
199
|
+
let k = segKeys[s]; let L = segLayer[s];
|
|
200
|
+
var slot = slotOf(L, k.z, k.w);
|
|
201
|
+
loop {
|
|
202
|
+
let o = atomicLoad(&owner[slot]);
|
|
203
|
+
if (o == 0u) { return; } // no segment starts here: an open end (counted by the matcher's absence)
|
|
204
|
+
let other = o - 1u; let ok = segKeys[other];
|
|
205
|
+
if (segLayer[other] == L && ok.x == k.z && ok.y == k.w) { break; }
|
|
206
|
+
slot = (slot + 1u) & P.y;
|
|
207
|
+
}
|
|
208
|
+
endNext[s] = atomicExchange(&endHead[slot], s + 1u);
|
|
209
|
+
}
|
|
210
|
+
fn sortedStarts(head: u32, out: ptr<function, array<u32, ${LIST_LIMIT}>>) -> u32 {
|
|
211
|
+
var n = 0u; var cur = head;
|
|
212
|
+
while (cur != 0u && n < ${LIST_LIMIT}u) { (*out)[n] = cur - 1u; n = n + 1u; cur = startNext[cur - 1u]; }
|
|
213
|
+
if (cur != 0u) { return ${LIST_LIMIT + 1}u; }
|
|
214
|
+
for (var i = 1u; i < n; i = i + 1u) { let v = (*out)[i]; var j = i; while (j > 0u && (*out)[j - 1u] > v) { (*out)[j] = (*out)[j - 1u]; j = j - 1u; } (*out)[j] = v; }
|
|
215
|
+
return n;
|
|
216
|
+
}
|
|
217
|
+
fn sortedEnds(head: u32, out: ptr<function, array<u32, ${LIST_LIMIT}>>) -> u32 {
|
|
218
|
+
var n = 0u; var cur = head;
|
|
219
|
+
while (cur != 0u && n < ${LIST_LIMIT}u) { (*out)[n] = cur - 1u; n = n + 1u; cur = endNext[cur - 1u]; }
|
|
220
|
+
if (cur != 0u) { return ${LIST_LIMIT + 1}u; }
|
|
221
|
+
for (var i = 1u; i < n; i = i + 1u) { let v = (*out)[i]; var j = i; while (j > 0u && (*out)[j - 1u] > v) { (*out)[j] = (*out)[j - 1u]; j = j - 1u; } (*out)[j] = v; }
|
|
222
|
+
return n;
|
|
223
|
+
}
|
|
224
|
+
// chain_polys' greedy walk at one key: an arriving end takes the smallest unused segment starting there. A segment whose
|
|
225
|
+
// two ends share the key (a sub-micron sliver) is taken only when it comes first, and then its own end takes the next
|
|
226
|
+
// start; one left over closes on itself (a 1-segment loop, which chain_polys drops). Arriving ends are served in
|
|
227
|
+
// segment order — the kernel's order whenever one end arrives at a key, which is the manifold case.
|
|
228
|
+
@compute @workgroup_size(256) fn pairEnds(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
229
|
+
let slot = tid(g); if (slot > P.y) { return; }
|
|
230
|
+
if (atomicLoad(&owner[slot]) == 0u) { return; }
|
|
231
|
+
var st: array<u32, ${LIST_LIMIT}>; var en: array<u32, ${LIST_LIMIT}>;
|
|
232
|
+
let ns = sortedStarts(atomicLoad(&startHead[slot]), &st); let ne = sortedEnds(atomicLoad(&endHead[slot]), &en);
|
|
233
|
+
if (ns > ${LIST_LIMIT}u || ne > ${LIST_LIMIT}u) { successor[P.x] = 1u; return; }
|
|
234
|
+
var used = 0u; var selfKey = 0u; // bit masks over st
|
|
235
|
+
for (var i = 0u; i < ns; i = i + 1u) { for (var j = 0u; j < ne; j = j + 1u) { if (en[j] == st[i]) { selfKey = selfKey | (1u << i); } } }
|
|
236
|
+
for (var j = 0u; j < ne; j = j + 1u) {
|
|
237
|
+
var arriving = en[j]; var isSelf = false;
|
|
238
|
+
for (var i = 0u; i < ns; i = i + 1u) { if (st[i] == arriving) { isSelf = true; } }
|
|
239
|
+
if (isSelf) { continue; }
|
|
240
|
+
loop {
|
|
241
|
+
var pick = ${LIST_LIMIT}u;
|
|
242
|
+
for (var i = 0u; i < ns; i = i + 1u) { if ((used & (1u << i)) == 0u) { pick = i; break; } }
|
|
243
|
+
if (pick == ${LIST_LIMIT}u) { break; }
|
|
244
|
+
used = used | (1u << pick); successor[arriving] = st[pick];
|
|
245
|
+
if ((selfKey & (1u << pick)) == 0u) { break; }
|
|
246
|
+
arriving = st[pick];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (var i = 0u; i < ns; i = i + 1u) { if ((selfKey & (1u << i)) != 0u && (used & (1u << i)) == 0u) { successor[st[i]] = st[i]; } }
|
|
250
|
+
}`
|
|
251
|
+
|
|
252
|
+
// ---- pointer jumping: every segment's loop head (the smallest id on its cycle), then its distance to the loop's last
|
|
253
|
+
export const JUMP_SHADER = /* wgsl */`
|
|
254
|
+
@group(0) @binding(0) var<uniform> P: vec4<u32>; // segments, mode (0 init labels, 1 label round, 2 init ranks, 3 rank round)
|
|
255
|
+
@group(0) @binding(1) var<storage, read> successor: array<u32>;
|
|
256
|
+
@group(0) @binding(2) var<storage, read> dead: array<u32>;
|
|
257
|
+
@group(0) @binding(3) var<storage, read_write> valueIn: array<u32>;
|
|
258
|
+
@group(0) @binding(4) var<storage, read_write> nextIn: array<u32>;
|
|
259
|
+
@group(0) @binding(5) var<storage, read_write> valueOut: array<u32>;
|
|
260
|
+
@group(0) @binding(6) var<storage, read_write> nextOut: array<u32>;
|
|
261
|
+
@group(0) @binding(7) var<storage, read> label: array<u32>;
|
|
262
|
+
@group(0) @binding(8) var<storage, read> reach: array<u32>; // NONE: not on a cycle
|
|
263
|
+
${TID}
|
|
264
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
265
|
+
let s = tid(g); if (s >= P.x) { return; }
|
|
266
|
+
if (P.y == 0u) { if (dead[s] != 0u) { valueOut[s] = ${NONE}u; nextOut[s] = ${NONE}u; return; } valueOut[s] = s; nextOut[s] = successor[s]; return; }
|
|
267
|
+
if (P.y == 2u) {
|
|
268
|
+
let nx = successor[s];
|
|
269
|
+
if (dead[s] != 0u || nx == ${NONE}u || reach[s] == ${NONE}u) { valueOut[s] = 0u; nextOut[s] = ${NONE}u; return; }
|
|
270
|
+
if (nx == label[s]) { valueOut[s] = 0u; nextOut[s] = s; return; } // the last segment before the head
|
|
271
|
+
valueOut[s] = 1u; nextOut[s] = nx; return;
|
|
272
|
+
}
|
|
273
|
+
let nx = nextIn[s];
|
|
274
|
+
if (nx == ${NONE}u) { valueOut[s] = valueIn[s]; nextOut[s] = nx; return; }
|
|
275
|
+
if (P.y == 1u) { valueOut[s] = min(valueIn[s], valueIn[nx]); }
|
|
276
|
+
else { if (nx == s) { valueOut[s] = valueIn[s]; } else { valueOut[s] = valueIn[s] + valueIn[nx]; } }
|
|
277
|
+
nextOut[s] = nextIn[nx];
|
|
278
|
+
}`
|
|
279
|
+
|
|
280
|
+
// ---- cycle members: succ^(2^R) of anything lands on a cycle, and every cycle element is hit, so the hit set is exactly
|
|
281
|
+
// the cycle elements. A tail leading into a cycle (a successor that two segments share) is left out as open; without
|
|
282
|
+
// this, a tail's head took a rank that never stops growing and every slot after it was misplaced (measured on offset
|
|
283
|
+
// pieces: 3,923 merges in 2.84M pieces left 6 % of the area).
|
|
284
|
+
export const CYCLES_SHADER = /* wgsl */`
|
|
285
|
+
@group(0) @binding(0) var<uniform> P: vec4<u32>; // segments, mode (0 mark, 1 flag)
|
|
286
|
+
@group(0) @binding(1) var<storage, read_write> reach: array<u32>; // in: succ^(2^R), out: 0 on a cycle, NONE otherwise
|
|
287
|
+
@group(0) @binding(2) var<storage, read_write> hit: array<u32>;
|
|
288
|
+
${TID}
|
|
289
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
290
|
+
let s = tid(g); if (s >= P.x) { return; }
|
|
291
|
+
if (P.y == 0u) { let r = reach[s]; if (r != ${NONE}u) { hit[r] = 1u; } return; }
|
|
292
|
+
reach[s] = select(${NONE}u, 0u, hit[s] == 1u); }`
|
|
293
|
+
|
|
294
|
+
// ---- loops: heads, sort by (layer, head), lay every segment out in loop order, drop what the kernel drops
|
|
295
|
+
export const LOOPS_SHADER = /* wgsl */`
|
|
296
|
+
@group(0) @binding(0) var<uniform> P: vec4<u32>; // segments, heads (padded), layers, stage parameter
|
|
297
|
+
@group(0) @binding(1) var<storage, read> successor: array<u32>;
|
|
298
|
+
@group(0) @binding(2) var<storage, read> label: array<u32>;
|
|
299
|
+
@group(0) @binding(3) var<storage, read> rank: array<u32>; // distance to the loop's last segment
|
|
300
|
+
@group(0) @binding(4) var<storage, read> segLayer: array<u32>;
|
|
301
|
+
@group(0) @binding(5) var<storage, read_write> headFlag: array<u32>; // flag, then (after the scan) the head's index
|
|
302
|
+
@group(0) @binding(6) var<storage, read_write> heads: array<vec2<u32>>; // (layer, head) sorted
|
|
303
|
+
@group(0) @binding(7) var<storage, read_write> polyOf: array<u32>; // head segment -> sorted polygon index
|
|
304
|
+
@group(0) @binding(8) var<storage, read_write> loopStart: array<u32>;// per sorted polygon: its length, then (scan) its first slot
|
|
305
|
+
@group(0) @binding(9) var<storage, read_write> order: array<u32>; // slot -> segment
|
|
306
|
+
@group(0) @binding(10) var<storage, read> reach: array<u32>;
|
|
307
|
+
${TID}
|
|
308
|
+
fn closed(s: u32) -> bool { return successor[s] != ${NONE}u && reach[s] != ${NONE}u; }
|
|
309
|
+
fn isHead(s: u32) -> bool { return closed(s) && label[s] == s; }
|
|
310
|
+
@compute @workgroup_size(256) fn flagHeads(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
311
|
+
let s = tid(g); if (s >= P.x) { return; } headFlag[s] = select(0u, 1u, isHead(s)); }
|
|
312
|
+
@compute @workgroup_size(256) fn gatherHeads(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
313
|
+
let s = tid(g); if (s >= P.x) { return; }
|
|
314
|
+
if (isHead(s)) { heads[headFlag[s]] = vec2<u32>(segLayer[s], s); } }
|
|
315
|
+
@compute @workgroup_size(256) fn padHeads(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
316
|
+
let i = tid(g) + P.w; if (i >= P.y) { return; } heads[i] = vec2<u32>(${NONE}u, ${NONE}u); }
|
|
317
|
+
fn less(a: vec2<u32>, b: vec2<u32>) -> bool { return a.x < b.x || (a.x == b.x && a.y < b.y); }
|
|
318
|
+
@compute @workgroup_size(256) fn bitonic(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
319
|
+
let i = tid(g); if (i >= P.y) { return; }
|
|
320
|
+
let j = P.z; let k = P.w; let partner = i ^ j;
|
|
321
|
+
if (partner <= i) { return; }
|
|
322
|
+
let a = heads[i]; let b = heads[partner];
|
|
323
|
+
let ascending = (i & k) == 0u;
|
|
324
|
+
if (less(b, a) == ascending) { heads[i] = b; heads[partner] = a; }
|
|
325
|
+
}
|
|
326
|
+
@compute @workgroup_size(256) fn lengths(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
327
|
+
let p = tid(g); if (p >= P.w) { return; }
|
|
328
|
+
let h = heads[p].y; polyOf[h] = p; loopStart[p] = rank[h] + 2u; } // the kernel's n + 1 points: start(s0), end(s0..s(n-1))
|
|
329
|
+
@compute @workgroup_size(256) fn place(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
330
|
+
let s = tid(g); if (s >= P.x) { return; }
|
|
331
|
+
if (!closed(s)) { return; }
|
|
332
|
+
let p = polyOf[label[s]];
|
|
333
|
+
let length = loopStart[p + 1u] - loopStart[p];
|
|
334
|
+
order[loopStart[p] + 1u + (length - 2u - rank[s])] = s;
|
|
335
|
+
if (label[s] == s) { order[loopStart[p]] = s | 0x80000000u; } }`
|
|
336
|
+
|
|
337
|
+
// ---- the union's input: a point per kept segment, layer-relative; per-layer and per-polygon tables
|
|
338
|
+
export const OUTPUT_SHADER = /* wgsl */`
|
|
339
|
+
@group(0) @binding(0) var<uniform> P: vec4<u32>; // slots, polygons, layers, stage
|
|
340
|
+
@group(0) @binding(1) var<storage, read> order: array<u32>;
|
|
341
|
+
@group(0) @binding(2) var<storage, read> segPoints: array<vec4<i32>>;
|
|
342
|
+
@group(0) @binding(3) var<storage, read> segLayer: array<u32>;
|
|
343
|
+
@group(0) @binding(4) var<storage, read> successor: array<u32>;
|
|
344
|
+
@group(0) @binding(5) var<storage, read> loopStart: array<u32>; // per polygon: first slot (P.y + 1 entries)
|
|
345
|
+
@group(0) @binding(6) var<storage, read_write> kept: array<u32>; // per slot, then scanned
|
|
346
|
+
@group(0) @binding(7) var<storage, read_write> polyKept: array<u32>; // per polygon: kept count, then valid flag, then scanned
|
|
347
|
+
@group(0) @binding(8) var<storage, read_write> layerAcc: array<atomic<i32>>; // per layer: minx miny maxx maxy count, pad
|
|
348
|
+
@group(0) @binding(9) var<storage, read_write> slotPoly: array<u32>; // slot -> polygon
|
|
349
|
+
${TID}
|
|
350
|
+
fn pointOf(i: u32) -> vec2<i32> { let e = order[i]; let p = segPoints[e & 0x7fffffffu]; if ((e & 0x80000000u) != 0u) { return p.xy; } return p.zw; }
|
|
351
|
+
fn layerOf(i: u32) -> u32 { return segLayer[order[i] & 0x7fffffffu]; }
|
|
352
|
+
@compute @workgroup_size(256) fn slotPolygons(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
353
|
+
let p = tid(g); if (p >= P.y) { return; }
|
|
354
|
+
for (var i = loopStart[p]; i < loopStart[p + 1u]; i = i + 1u) { slotPoly[i] = p; } }
|
|
355
|
+
// a point is kept unless it equals the next point of its loop (write_polygon), and the layer box takes every live end
|
|
356
|
+
@compute @workgroup_size(256) fn keep(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
357
|
+
let i = tid(g); if (i >= P.x) { return; }
|
|
358
|
+
let p = slotPoly[i]; var nx = i + 1u; if (nx == loopStart[p + 1u]) { nx = loopStart[p]; }
|
|
359
|
+
let a = pointOf(i); let b = pointOf(nx);
|
|
360
|
+
kept[i] = select(0u, 1u, any(a != b));
|
|
361
|
+
if (loopStart[p + 1u] - loopStart[p] < 3u) { return; } // chain_polys keeps a loop of 3 points or more
|
|
362
|
+
let L = layerOf(i);
|
|
363
|
+
atomicMin(&layerAcc[L * 8u], a.x); atomicMin(&layerAcc[L * 8u + 1u], a.y);
|
|
364
|
+
atomicMax(&layerAcc[L * 8u + 2u], a.x); atomicMax(&layerAcc[L * 8u + 3u], a.y); }
|
|
365
|
+
@compute @workgroup_size(256) fn polygonCounts(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
366
|
+
let p = tid(g); if (p >= P.y) { return; }
|
|
367
|
+
var n = 0u; for (var i = loopStart[p]; i < loopStart[p + 1u]; i = i + 1u) { n = n + kept[i]; }
|
|
368
|
+
polyKept[p] = select(0u, 1u, n >= 2u && loopStart[p + 1u] - loopStart[p] >= 3u); }
|
|
369
|
+
@compute @workgroup_size(256) fn keepValid(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
370
|
+
let i = tid(g); if (i >= P.x) { return; }
|
|
371
|
+
let p = slotPoly[i];
|
|
372
|
+
if (polyKept[p] == 0u) { kept[i] = 0u; } }`
|
|
373
|
+
|
|
374
|
+
export const WRITE_SHADER = /* wgsl */`
|
|
375
|
+
@group(0) @binding(0) var<uniform> P: vec4<u32>; // slots, polygons, layers
|
|
376
|
+
@group(0) @binding(1) var<storage, read> order: array<u32>;
|
|
377
|
+
@group(0) @binding(2) var<storage, read> segPoints: array<vec4<i32>>;
|
|
378
|
+
@group(0) @binding(3) var<storage, read> segLayer: array<u32>;
|
|
379
|
+
@group(0) @binding(4) var<storage, read> keptFlag: array<u32>; // per slot: kept (0/1)
|
|
380
|
+
@group(0) @binding(5) var<storage, read> keptAt: array<u32>; // per slot: exclusive scan of kept
|
|
381
|
+
@group(0) @binding(6) var<storage, read> layerBox: array<i32>; // per layer: minx miny maxx maxy, 8 words
|
|
382
|
+
@group(0) @binding(7) var<storage, read_write> points: array<vec2<i32>>;
|
|
383
|
+
@group(0) @binding(8) var<storage, read_write> layerCount: array<atomic<u32>>;
|
|
384
|
+
${TID}
|
|
385
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
386
|
+
let i = tid(g); if (i >= P.x || keptFlag[i] == 0u) { return; }
|
|
387
|
+
let e = order[i]; let s = e & 0x7fffffffu; let L = segLayer[s];
|
|
388
|
+
var q = segPoints[s].zw; if ((e & 0x80000000u) != 0u) { q = segPoints[s].xy; }
|
|
389
|
+
points[keptAt[i]] = q - vec2<i32>(layerBox[L * 8u], layerBox[L * 8u + 1u]);
|
|
390
|
+
atomicAdd(&layerCount[L], 1u); }`
|
|
391
|
+
|
|
392
|
+
export const TABLES_SHADER = /* wgsl */`
|
|
393
|
+
@group(0) @binding(0) var<uniform> P: vec4<u32>; // slots, polygons, layers, cell size
|
|
394
|
+
@group(0) @binding(1) var<storage, read> layerBox: array<i32>;
|
|
395
|
+
@group(0) @binding(2) var<storage, read_write> layerStart: array<u32>; // per layer count, scanned (L + 1)
|
|
396
|
+
@group(0) @binding(3) var<storage, read_write> layerInfo: array<u32>; // gw gh cellOffset 0
|
|
397
|
+
@group(0) @binding(4) var<storage, read> polyValidAt: array<u32>; // scanned valid flags (P.y + 1)
|
|
398
|
+
@group(0) @binding(5) var<storage, read> loopStart: array<u32>;
|
|
399
|
+
@group(0) @binding(6) var<storage, read> keptAt: array<u32>;
|
|
400
|
+
@group(0) @binding(7) var<storage, read> order: array<u32>;
|
|
401
|
+
@group(0) @binding(8) var<storage, read> segLayer: array<u32>;
|
|
402
|
+
@group(0) @binding(9) var<storage, read_write> polyInfo: array<u32>; // start count layerStart layerEnd; polyLayer after
|
|
403
|
+
${TID}
|
|
404
|
+
@compute @workgroup_size(1) fn cells() { // a serial pass over the layers (hundreds)
|
|
405
|
+
var cells = 0u;
|
|
406
|
+
for (var L = 0u; L < P.z; L = L + 1u) {
|
|
407
|
+
var gw = 1u; var gh = 1u;
|
|
408
|
+
if (layerBox[L * 8u + 2u] >= layerBox[L * 8u]) {
|
|
409
|
+
gw = u32(layerBox[L * 8u + 2u] - layerBox[L * 8u]) / P.w + 1u; gh = u32(layerBox[L * 8u + 3u] - layerBox[L * 8u + 1u]) / P.w + 1u; }
|
|
410
|
+
layerInfo[L * 4u] = gw; layerInfo[L * 4u + 1u] = gh; layerInfo[L * 4u + 2u] = cells; layerInfo[L * 4u + 3u] = 0u;
|
|
411
|
+
cells = cells + gw * gh;
|
|
412
|
+
}
|
|
413
|
+
layerInfo[P.z * 4u] = cells; }
|
|
414
|
+
@compute @workgroup_size(256) fn polygons(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
415
|
+
let p = tid(g); if (p >= P.y) { return; }
|
|
416
|
+
if (polyValidAt[p + 1u] == polyValidAt[p]) { return; }
|
|
417
|
+
let q = polyValidAt[p];
|
|
418
|
+
let first = keptAt[loopStart[p]]; let count = keptAt[loopStart[p + 1u]] - first;
|
|
419
|
+
let L = segLayer[order[loopStart[p]] & 0x7fffffffu];
|
|
420
|
+
polyInfo[q * 4u] = first; polyInfo[q * 4u + 1u] = count; polyInfo[q * 4u + 2u] = layerStart[L]; polyInfo[q * 4u + 3u] = layerStart[L + 1u];
|
|
421
|
+
polyInfo[polyValidAt[P.y] * 4u + q] = L; }`
|
|
422
|
+
|
|
423
|
+
// per kept point, the grid cells of the segment ending at it (the union pipeline's cell list size)
|
|
424
|
+
export const ENTRIES_SHADER = /* wgsl */`
|
|
425
|
+
@group(0) @binding(0) var<uniform> P: vec4<u32>; // polygons, cell size
|
|
426
|
+
@group(0) @binding(1) var<storage, read> polyInfo: array<u32>;
|
|
427
|
+
@group(0) @binding(2) var<storage, read> points: array<vec2<i32>>;
|
|
428
|
+
@group(0) @binding(3) var<storage, read_write> total: array<atomic<u32>>;
|
|
429
|
+
${TID}
|
|
430
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
431
|
+
let p = tid(g); if (p >= P.x) { return; }
|
|
432
|
+
let first = polyInfo[p * 4u]; let n = polyInfo[p * 4u + 1u]; let c = i32(P.y);
|
|
433
|
+
var a = points[first + n - 1u]; var sum = 0u;
|
|
434
|
+
for (var i = 0u; i < n; i = i + 1u) { let b = points[first + i];
|
|
435
|
+
sum = sum + u32(max(a.x, b.x) / c - min(a.x, b.x) / c + 1) * u32(max(a.y, b.y) / c - min(a.y, b.y) / c + 1); a = b; }
|
|
436
|
+
atomicAdd(&total[0], sum); }`
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
// ---- the layers whose chains did not close: a live segment off every cycle (contour_phase's open_layers)
|
|
440
|
+
export const OPEN_LAYERS_SHADER = /* wgsl */`
|
|
441
|
+
@group(0) @binding(0) var<uniform> P: vec4<u32>; // segments
|
|
442
|
+
@group(0) @binding(1) var<storage, read> dead: array<u32>;
|
|
443
|
+
@group(0) @binding(2) var<storage, read> reach: array<u32>; // CYCLES_SHADER's output: NONE off every cycle
|
|
444
|
+
@group(0) @binding(3) var<storage, read> segLayer: array<u32>;
|
|
445
|
+
@group(0) @binding(4) var<storage, read_write> openLayer: array<u32>;
|
|
446
|
+
${TID}
|
|
447
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
448
|
+
let s = tid(g); if (s >= P.x) { return; }
|
|
449
|
+
if (dead[s] == 0u && reach[s] == ${NONE}u) { openLayer[segLayer[s]] = 1u; } }`
|