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.
@@ -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; } }`