three-slicer 0.3.1 → 0.3.3

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,450 @@
1
+ // The WGSL of the contour union pipeline (contour_gpu.js). This is the shader text of the measured experiment
2
+ // (.omc/plans/gpu-parallel-topology-design.md, appendices D to F17), kept as it ran: every kernel here was checked
3
+ // against Clipper on real slices, so it is edited only with a measurement beside it. The host that binds and
4
+ // dispatches them, and the meaning of every buffer, is contour_gpu.js.
5
+ //
6
+ // Conventions shared by every kernel: a dispatch is two-dimensional (x up to 65535 workgroups of 256, y the overflow),
7
+ // so an invocation's index is g.x + g.y * 16776960; a segment's index is also the id of the vertex it starts at; a
8
+ // crossing's vertex id is segmentCount + its pair index.
9
+
10
+ const PRELUDE = /* wgsl */`// a range from a corrupt scan must not spin the GPU: a dispatch keeps running after its process is killed (measured). Observed max 107 per cell, 13 per segment.
11
+ const RANGE_CAP = 4096u;
12
+ fn badRange(lo: u32, hi: u32) -> bool { return hi < lo || hi - lo > RANGE_CAP; }
13
+
14
+ fn mulu(a: u32, b: u32) -> vec2<u32> { // unsigned 32x32 -> 64 (lo, hi)
15
+ let al = a & 0xffffu; let ah = a >> 16u; let bl = b & 0xffffu; let bh = b >> 16u;
16
+ let ll = al * bl; let lh = al * bh; let hl = ah * bl; let hh = ah * bh;
17
+ var lo = ll; var hi = hh + (lh >> 16u) + (hl >> 16u);
18
+ let t1 = lh << 16u; lo = lo + t1; if (lo < t1) { hi = hi + 1u; }
19
+ let t2 = hl << 16u; lo = lo + t2; if (lo < t2) { hi = hi + 1u; }
20
+ return vec2<u32>(lo, hi);
21
+ }
22
+ fn muls(a: i32, b: i32) -> vec2<u32> { // signed 32x32 -> 64, two's complement
23
+ let neg = (a < 0) != (b < 0);
24
+ var r = mulu(u32(abs(a)), u32(abs(b)));
25
+ if (neg) { r = neg64(r); }
26
+ return r;
27
+ }
28
+ fn neg64(a: vec2<u32>) -> vec2<u32> { var lo = ~a.x; var hi = ~a.y; lo = lo + 1u; if (lo == 0u) { hi = hi + 1u; } return vec2<u32>(lo, hi); }
29
+ fn sub64(a: vec2<u32>, b: vec2<u32>) -> vec2<u32> { var hi = a.y - b.y; if (a.x < b.x) { hi = hi - 1u; } return vec2<u32>(a.x - b.x, hi); }
30
+ fn sign64(a: vec2<u32>) -> i32 { if ((a.y & 0x80000000u) != 0u) { return -1; } if (a.x == 0u && a.y == 0u) { return 0; } return 1; }
31
+ fn cross(ux: i32, uy: i32, vx: i32, vy: i32) -> vec2<u32> { return sub64(muls(ux, vy), muls(uy, vx)); }
32
+ fn orient(ax: i32, ay: i32, bx: i32, by: i32, cx: i32, cy: i32) -> i32 { return sign64(cross(bx - ax, by - ay, cx - ax, cy - ay)); }
33
+ fn u64f(a: vec2<u32>) -> f32 { return f32(a.y) * 4294967296.0 + f32(a.x); } // non-negative values only
34
+ // exact rounded quotient round(num * |d| / den) for 0 <= num < den (u64), |d| < 2^31: restoring division in 128-bit arithmetic
35
+ fn add128u(a: vec4<u32>, b: vec4<u32>) -> vec4<u32> { var r: vec4<u32>; r.x = a.x + b.x; var c = select(0u, 1u, r.x < a.x);
36
+ let y = a.y + b.y; var c1 = select(0u, 1u, y < a.y); r.y = y + c; if (r.y < y) { c1 = 1u; } c = c1;
37
+ let z = a.z + b.z; var c2 = select(0u, 1u, z < a.z); r.z = z + c; if (r.z < z) { c2 = 1u; } c = c2; r.w = a.w + b.w + c; return r; }
38
+ fn sub128u(a: vec4<u32>, b: vec4<u32>) -> vec4<u32> { return add128u(a, add128u(vec4<u32>(~b.x, ~b.y, ~b.z, ~b.w), vec4<u32>(1u, 0u, 0u, 0u))); }
39
+ fn ge128(a: vec4<u32>, b: vec4<u32>) -> bool { if (a.w != b.w) { return a.w > b.w; } if (a.z != b.z) { return a.z > b.z; } if (a.y != b.y) { return a.y > b.y; } return a.x >= b.x; }
40
+ fn shl128(a: vec4<u32>, s: u32) -> vec4<u32> { if (s == 0u) { return a; } let t = 32u - s; return vec4<u32>(a.x << s, (a.y << s) | (a.x >> t), (a.z << s) | (a.y >> t), (a.w << s) | (a.z >> t)); }
41
+ fn roundedShift(num: vec2<u32>, den: vec2<u32>, d: i32) -> i32 {
42
+ if (d == 0) { return 0; }
43
+ let ad = u32(abs(d)); let lo = mulu(num.x, ad); let hi = mulu(num.y, ad);
44
+ var n = vec4<u32>(lo.x, lo.y, 0u, 0u); n = add128u(n, vec4<u32>(0u, hi.x, hi.y, 0u));
45
+ n = shl128(n, 1u); n = add128u(n, vec4<u32>(den.x, den.y, 0u, 0u));
46
+ let dd = shl128(vec4<u32>(den.x, den.y, 0u, 0u), 1u);
47
+ var q = 0u; var sh = shl128(dd, 31u);
48
+ for (var b = 31; b >= 0; b--) { if (ge128(n, sh)) { n = sub128u(n, sh); q = q | (1u << u32(b)); }
49
+ sh = vec4<u32>((sh.x >> 1u) | (sh.y << 31u), (sh.y >> 1u) | (sh.z << 31u), (sh.z >> 1u) | (sh.w << 31u), sh.w >> 1u); }
50
+ if (d < 0) { return -i32(q); } return i32(q);
51
+ }
52
+ struct Params { n: u32, cells: u32, cs: i32, mode: u32 }
53
+ @group(0) @binding(0) var<uniform> params: Params;
54
+ @group(0) @binding(1) var<storage, read> segs: array<vec4<i32>>;
55
+ @group(0) @binding(2) var<storage, read> segInfo: array<vec4<u32>>;
56
+ @group(0) @binding(3) var<storage, read> layerInfo: array<vec4<i32>>; // gw, gh, cellBase, 0
57
+ fn vtx(v: u32) -> vec2<i32> { return segs[v].xy; }
58
+ fn orientSoS(p: u32, q: u32, r: u32) -> i32 { // Simulation of Simplicity: higher id dominates, x displacement before y
59
+ let P = vtx(p); let Q = vtx(q); let R = vtx(r);
60
+ let d = orient(P.x, P.y, Q.x, Q.y, R.x, R.y); if (d != 0) { return d; }
61
+ var ids = array<u32, 3>(p, q, r);
62
+ var cv = array<vec2<i32>, 3>(R - Q, P - R, Q - P);
63
+ for (var round = 0; round < 3; round++) {
64
+ var top = 0u; var found = false;
65
+ for (var k = 0; k < 3; k++) { if (ids[k] != 0xffffffffu && (!found || ids[k] > top)) { top = ids[k]; found = true; } }
66
+ if (!found) { break; }
67
+ var c = vec2<i32>(0, 0);
68
+ for (var k = 0; k < 3; k++) { if (ids[k] == top) { c = c + cv[k]; ids[k] = 0xffffffffu; } }
69
+ if (c.y != 0) { return -sign(c.y); }
70
+ if (c.x != 0) { return sign(c.x); }
71
+ }
72
+ return 0;
73
+ }
74
+ fn cellRange(i: u32) -> vec4<i32> { let s = segs[i]; return vec4<i32>(min(s.x, s.z) / params.cs, min(s.y, s.w) / params.cs, max(s.x, s.z) / params.cs, max(s.y, s.w) / params.cs); }
75
+ `
76
+
77
+ export const BIN_SHADER = PRELUDE + /* wgsl */`
78
+ @group(0) @binding(4) var<storage, read_write> cellCount: array<atomic<u32>>;
79
+ @group(0) @binding(5) var<storage, read> cellStart: array<u32>;
80
+ @group(0) @binding(6) var<storage, read_write> cellList: array<vec2<u32>>;
81
+ @compute @workgroup_size(256) fn count(@builtin(global_invocation_id) gid_raw: vec3<u32>) { let gid = vec3<u32>(gid_raw.x + gid_raw.y * 16776960u, 0u, 0u);
82
+ let i = gid.x; if (i >= params.n) { return; }
83
+ let r = cellRange(i); let L = layerInfo[segInfo[i].x];
84
+ for (var cy = r.y; cy <= r.w; cy++) { for (var cx = r.x; cx <= r.z; cx++) { atomicAdd(&cellCount[u32(L.z + cy * L.x + cx)], 1u); } }
85
+ }
86
+ @compute @workgroup_size(256) fn scatter(@builtin(global_invocation_id) gid_raw: vec3<u32>) { let gid = vec3<u32>(gid_raw.x + gid_raw.y * 16776960u, 0u, 0u);
87
+ let i = gid.x; if (i >= params.n) { return; }
88
+ let r = cellRange(i); let L = layerInfo[segInfo[i].x];
89
+ for (var cy = r.y; cy <= r.w; cy++) { for (var cx = r.x; cx <= r.z; cx++) { let c = u32(L.z + cy * L.x + cx); let k = atomicAdd(&cellCount[c], 1u); if (cellStart[c] + k < arrayLength(&cellList)) { cellList[cellStart[c] + k] = vec2<u32>(i, c); } } }
90
+ }`
91
+
92
+ export const INTERSECT_SHADER = PRELUDE + /* wgsl */`
93
+ @group(0) @binding(4) var<storage, read> cellStart: array<u32>;
94
+ @group(0) @binding(5) var<storage, read> cellList: array<vec2<u32>>;
95
+ @group(0) @binding(6) var<storage, read_write> evCount: array<atomic<u32>>;
96
+ @group(0) @binding(7) var<storage, read_write> events: array<vec4<u32>>;
97
+ @group(0) @binding(8) var<storage, read> evStart: array<u32>;
98
+ @group(0) @binding(9) var<storage, read_write> flags: array<atomic<u32>>; // [0]: pair counter, [1]: collinear pivots
99
+ @group(0) @binding(10) var<storage, read_write> pairEdges: array<vec2<u32>>;
100
+ fn emit(seg: u32, num: vec2<u32>, den: vec2<u32>, delta: i32, px: i32, py: i32, vid: u32) {
101
+ if (params.mode == 0u) { atomicAdd(&evCount[seg], 1u); return; }
102
+ let k = atomicAdd(&evCount[seg], 1u); let at = (evStart[seg] + k) * 2u; if (at + 1u >= arrayLength(&events)) { return; }
103
+ events[at] = vec4<u32>(num.x, num.y, den.x, den.y); events[at + 1u] = vec4<u32>(bitcast<u32>(delta), bitcast<u32>(px), bitcast<u32>(py), vid);
104
+ }
105
+ fn dotw(ux: i32, uy: i32, vx: i32, vy: i32) -> vec2<u32> { let a = muls(ux, vx); let b = muls(uy, vy); var lo = a.x + b.x; var hi = a.y + b.y; if (lo < a.x) { hi = hi + 1u; } return vec2<u32>(lo, hi); }
106
+ @compute @workgroup_size(256) fn intersect(@builtin(global_invocation_id) gid_raw: vec3<u32>) { let gid = vec3<u32>(gid_raw.x + gid_raw.y * 16776960u, 0u, 0u);
107
+ let a0 = gid.x; if (a0 >= arrayLength(&cellList) || params.cells == 0u) { return; }
108
+ let c = cellList[a0].y; if (c >= params.cells) { return; }
109
+ let s0 = cellStart[c]; let s1 = cellStart[c + 1u]; if (a0 < s0 || a0 >= s1 || badRange(s0, s1)) { return; }
110
+ let L = layerInfo[segInfo[cellList[s0].x].x];
111
+ let cl = i32(c) - L.z; let cellX = cl % L.x; let cellY = cl / L.x;
112
+ for (var a = a0; a < a0 + 1u; a++) {
113
+ let i = cellList[a].x; let A = segs[i]; let a1 = segInfo[i].z;
114
+ for (var b = a + 1u; b < s1; b++) {
115
+ let j = cellList[b].x; let Bs = segs[j]; let b1 = segInfo[j].z;
116
+ let lox = max(min(A.x, A.z), min(Bs.x, Bs.z)); let hix = min(max(A.x, A.z), max(Bs.x, Bs.z));
117
+ let loy = max(min(A.y, A.w), min(Bs.y, Bs.w)); let hiy = min(max(A.y, A.w), max(Bs.y, Bs.w));
118
+ if (lox > hix || loy > hiy) { continue; }
119
+ if (lox / params.cs != cellX || loy / params.cs != cellY) { continue; }
120
+ if (i == j || i == b1 || a1 == j || a1 == b1) { continue; } // adjacent edges meet only at their shared vertex
121
+ let o1 = orientSoS(i, a1, j); let o2 = orientSoS(i, a1, b1); let o3 = orientSoS(j, b1, i); let o4 = orientSoS(j, b1, a1);
122
+ if (!(o1 * o2 < 0 && o3 * o4 < 0)) { continue; }
123
+ var den = cross(A.z - A.x, A.w - A.y, Bs.z - Bs.x, Bs.w - Bs.y);
124
+ var numI = vec2<u32>(0u, 0u); var denI = vec2<u32>(1u, 0u); var numJ = vec2<u32>(0u, 0u); var denJ = vec2<u32>(1u, 0u);
125
+ var px = 0; var py = 0;
126
+ if (sign64(den) != 0) {
127
+ numI = cross(Bs.x - A.x, Bs.y - A.y, Bs.z - Bs.x, Bs.w - Bs.y); denI = den;
128
+ if (sign64(denI) < 0) { numI = neg64(numI); denI = neg64(denI); }
129
+ numJ = cross(A.x - Bs.x, A.y - Bs.y, A.z - A.x, A.w - A.y); denJ = neg64(den);
130
+ if (sign64(denJ) < 0) { numJ = neg64(numJ); denJ = neg64(denJ); }
131
+ // the point is computed along the lower-index segment: which of the two a cell lists first depends on atomic order, and the
132
+ // two parametrisations round differently in f32 (measured: 1-unit differences between identical runs)
133
+ // exact integer rounding (f32 rounded 0.84 % of points differently on SwiftShader than on Metal), along the lower-index segment
134
+ if (params.mode == 0u) { }
135
+ else if (i < j) { px = A.x + roundedShift(numI, denI, A.z - A.x); py = A.y + roundedShift(numI, denI, A.w - A.y); }
136
+ else { px = Bs.x + roundedShift(numJ, denJ, Bs.z - Bs.x); py = Bs.y + roundedShift(numJ, denJ, Bs.w - Bs.y); }
137
+ } else { // collinear pair made to cross by SoS: next to the pivot endpoint
138
+ if (params.mode == 1u) { atomicAdd(&flags[1], 1u); }
139
+ let top = max(max(i, a1), max(j, b1)); var pv = a1;
140
+ if (top == b1) { pv = j; } else if (top == j) { pv = b1; } else if (top == a1) { pv = i; }
141
+ let Pv = vtx(pv); px = Pv.x; py = Pv.y;
142
+ if (pv == i) { numI = vec2<u32>(0u, 0u); } else if (pv == a1) { numI = vec2<u32>(1u, 0u); } else { numI = dotw(px - A.x, py - A.y, A.z - A.x, A.w - A.y); denI = dotw(A.z - A.x, A.w - A.y, A.z - A.x, A.w - A.y); }
143
+ if (pv == j) { numJ = vec2<u32>(0u, 0u); } else if (pv == b1) { numJ = vec2<u32>(1u, 0u); } else { numJ = dotw(px - Bs.x, py - Bs.y, Bs.z - Bs.x, Bs.w - Bs.y); denJ = dotw(Bs.z - Bs.x, Bs.w - Bs.y, Bs.z - Bs.x, Bs.w - Bs.y); }
144
+ }
145
+ var vid = 0u;
146
+ if (params.mode == 1u) { let pid = atomicAdd(&flags[0], 1u); vid = params.n + pid; if (pid < arrayLength(&pairEdges)) { pairEdges[pid] = vec2<u32>(i, j); } }
147
+ var dI = -1; if (o3 < 0) { dI = 1; }
148
+ var dJ = -1; if (o1 < 0) { dJ = 1; }
149
+ emit(i, numI, denI, dI, px, py, vid);
150
+ emit(j, numJ, denJ, dJ, px, py, vid);
151
+ }
152
+ }
153
+ }`
154
+
155
+ export const SORT_SHADER = PRELUDE + /* wgsl */`
156
+ @group(0) @binding(4) var<storage, read> evStart: array<u32>;
157
+ @group(0) @binding(5) var<storage, read_write> events: array<vec4<u32>>;
158
+ @group(0) @binding(6) var<storage, read> pairEdges: array<vec2<u32>>;
159
+ fn add64(a: vec2<u32>, b: vec2<u32>) -> vec3<u32> { var lo = a.x + b.x; var c = 0u; if (lo < a.x) { c = 1u; } var hi = a.y + b.y + c; var c2 = 0u; if (hi < a.y || (c == 1u && hi == a.y)) { c2 = 1u; } return vec3<u32>(lo, hi, c2); }
160
+ fn mul128(a: vec2<u32>, b: vec2<u32>) -> vec4<u32> {
161
+ let p00 = mulu(a.x, b.x); let p01 = mulu(a.x, b.y); let p10 = mulu(a.y, b.x); let p11 = mulu(a.y, b.y);
162
+ var r0 = p00.x;
163
+ let s1 = add64(vec2<u32>(p00.y, 0u), vec2<u32>(p01.x, p01.y)); // limbs 1,2 (+carry into 3)
164
+ let s2 = add64(vec2<u32>(s1.x, s1.y), vec2<u32>(p10.x, p10.y));
165
+ let s3 = add64(vec2<u32>(s2.y, s1.z + s2.z), vec2<u32>(p11.x, p11.y));
166
+ return vec4<u32>(r0, s2.x, s3.x, s3.y);
167
+ }
168
+ fn lt128(x: vec4<u32>, y: vec4<u32>) -> bool {
169
+ if (x.w != y.w) { return x.w < y.w; } if (x.z != y.z) { return x.z < y.z; } if (x.y != y.y) { return x.y < y.y; } return x.x < y.x;
170
+ }
171
+ fn before(p: vec4<u32>, q: vec4<u32>) -> bool { return lt128(mul128(p.xy, q.zw), mul128(q.xy, p.zw)); }
172
+ // ---- two events at the same place on a segment: ordered as if every vertex were moved by an infinitesimal amount,
173
+ // higher vertex ids first, x before y (the same perturbation the crossing test uses). The order is the sign of the
174
+ // difference of the two events' parameters' derivatives, each a fraction of signed integers up to 128 bits.
175
+ struct SM { m: vec4<u32>, neg: bool }
176
+ fn isZero128(a: vec4<u32>) -> bool { return a.x == 0u && a.y == 0u && a.z == 0u && a.w == 0u; }
177
+ fn add128(a: vec4<u32>, b: vec4<u32>) -> vec4<u32> {
178
+ var r = vec4<u32>(0u); var carry = 0u;
179
+ for (var k = 0; k < 4; k++) { let s1 = a[k] + b[k]; var c = 0u; if (s1 < a[k]) { c = 1u; } let s2 = s1 + carry; if (s2 < s1) { c = 1u; } r[k] = s2; carry = c; }
180
+ return r; }
181
+ fn sub128(a: vec4<u32>, b: vec4<u32>) -> vec4<u32> { // a >= b
182
+ var r = vec4<u32>(0u); var borrow = 0u;
183
+ for (var k = 0; k < 4; k++) { let d1 = a[k] - b[k]; var c = 0u; if (a[k] < b[k]) { c = 1u; } let d2 = d1 - borrow; if (d1 < borrow) { c = 1u; } r[k] = d2; borrow = c; }
184
+ return r; }
185
+ fn smAdd(a: SM, b: SM) -> SM {
186
+ if (a.neg == b.neg) { return SM(add128(a.m, b.m), a.neg); }
187
+ if (lt128(a.m, b.m)) { return SM(sub128(b.m, a.m), b.neg); }
188
+ return SM(sub128(a.m, b.m), a.neg); }
189
+ fn smNeg(a: SM) -> SM { return SM(a.m, !a.neg); }
190
+ fn smProd(u: vec2<u32>, x: i32) -> SM { return SM(mul128(u, vec2<u32>(u32(abs(x)), 0u)), x < 0); } // unsigned 64 x signed 32
191
+ fn mul256(a: vec4<u32>, b: vec4<u32>) -> array<u32, 8> {
192
+ var r: array<u32, 8>;
193
+ for (var i = 0; i < 4; i++) { var carry = 0u;
194
+ for (var j = 0; j < 4; j++) { let p = mulu(a[i], b[j]); let s1 = r[i + j] + p.x; var c = 0u; if (s1 < p.x) { c = 1u; } let s2 = s1 + carry; if (s2 < s1) { c = c + 1u; } r[i + j] = s2; carry = p.y + c; }
195
+ r[i + 4] = carry; }
196
+ return r; }
197
+ // sign of num1/den1 - num2/den2, by cross-multiplying (a zero denominator gives equal, as on the CPU)
198
+ fn cmpFrac(n1in: SM, d1: SM, n2in: SM, d2: SM) -> i32 {
199
+ var n1 = n1in; var n2 = n2in; if (d1.neg) { n1.neg = !n1.neg; } if (d2.neg) { n2.neg = !n2.neg; }
200
+ let l = mul256(n1.m, d2.m); let r = mul256(n2.m, d1.m);
201
+ var lz = true; var rz = true; var mag = 0;
202
+ for (var k = 7; k >= 0; k--) { if (l[k] != 0u) { lz = false; } if (r[k] != 0u) { rz = false; } if (mag == 0 && l[k] != r[k]) { if (l[k] < r[k]) { mag = -1; } else { mag = 1; } } }
203
+ let ln = n1.neg && !lz; let rn = n2.neg && !rz;
204
+ if (ln != rn) { if (ln) { return -1; } return 1; }
205
+ if (ln) { return -mag; }
206
+ return mag; }
207
+ struct Shift { num: SM, den: SM }
208
+ // the derivative of the event's parameter along segment A when vertex v moves in x (axis 0) or y (axis 1)
209
+ fn shiftOf(A: u32, n: vec2<u32>, d: vec2<u32>, C: u32, v: u32, axis: u32) -> Shift {
210
+ let a0 = A; let a1 = segInfo[A].z; let c0 = C; let c1 = segInfo[C].z;
211
+ let Pa0 = vtx(a0); let Pa1 = vtx(a1); let Pc0 = vtx(c0); let Pc1 = vtx(c1);
212
+ let dAx = Pa1.x - Pa0.x; let dAy = Pa1.y - Pa0.y; let dCx = Pc1.x - Pc0.x; let dCy = Pc1.y - Pc0.y;
213
+ var D = cross(dAx, dAy, dCx, dCy); let Dneg = sign64(D) < 0; if (Dneg) { D = neg64(D); }
214
+ let den = SM(mul128(D, d), Dneg);
215
+ // cg(u) = gx * u.y - gy * u.x, for u = (w - a0) * d - n * dA, and for u = dC
216
+ var cgC = dCy; if (axis == 1u) { cgC = -dCx; }
217
+ var num = SM(vec4<u32>(0u), false); var touched = false;
218
+ if (v == c0 || v == c1) {
219
+ var w = Pc1; if (v == c1) { w = Pc0; }
220
+ var part: SM;
221
+ if (axis == 0u) { part = smAdd(smProd(d, w.y - Pa0.y), smNeg(smProd(n, dAy))); }
222
+ else { part = smNeg(smAdd(smProd(d, w.x - Pa0.x), smNeg(smProd(n, dAx)))); }
223
+ if (v == c0) { num = smAdd(num, part); }
224
+ if (v == c1) { num = smAdd(num, smNeg(part)); }
225
+ touched = true; }
226
+ if (v == c0 && v == c1) { touched = true; }
227
+ if (v == a0) { let lo = d.x - n.x; var hi = d.y - n.y; if (d.x < n.x) { hi = hi - 1u; } num = smAdd(num, smNeg(smProd(vec2<u32>(lo, hi), cgC))); touched = true; }
228
+ if (v == a1) { num = smAdd(num, smNeg(smProd(n, cgC))); touched = true; }
229
+ if (!touched) { return Shift(SM(vec4<u32>(0u), false), SM(vec4<u32>(1u, 0u, 0u, 0u), false)); }
230
+ return Shift(num, den); }
231
+ fn otherEdge(A: u32, vid: u32) -> u32 { let pid = vid - params.n; if (pid >= arrayLength(&pairEdges)) { return A; } let e = pairEdges[pid]; if (e.x == A) { return e.y; } return e.x; }
232
+ // -1: p first, 1: q first, 0: the perturbation does not separate them (two segments on one line)
233
+ fn sosOrder(A: u32, p0: vec4<u32>, p1: vec4<u32>, q0: vec4<u32>, q1: vec4<u32>) -> i32 {
234
+ let Cp = otherEdge(A, p1.w); let Cq = otherEdge(A, q1.w);
235
+ var ids = array<u32, 6>(A, segInfo[A].z, Cp, segInfo[Cp].z, Cq, segInfo[Cq].z);
236
+ for (var round = 0; round < 6; round++) {
237
+ var top = 0u; var found = false;
238
+ for (var k = 0; k < 6; k++) { if (ids[k] != 0xffffffffu && (!found || ids[k] > top)) { top = ids[k]; found = true; } }
239
+ if (!found) { break; }
240
+ for (var k = 0; k < 6; k++) { if (ids[k] == top) { ids[k] = 0xffffffffu; } }
241
+ for (var axis = 0u; axis < 2u; axis++) {
242
+ let sp = shiftOf(A, p0.xy, p0.zw, Cp, top, axis); let sq = shiftOf(A, q0.xy, q0.zw, Cq, top, axis);
243
+ let c = cmpFrac(sp.num, sp.den, sq.num, sq.den);
244
+ if (c != 0) { return c; } } }
245
+ return 0; }
246
+ fn beforeExact(A: u32, k0: vec4<u32>, k1: vec4<u32>, p0: vec4<u32>, p1: vec4<u32>) -> bool {
247
+ if (before(k0, p0)) { return true; }
248
+ if (before(p0, k0)) { return false; }
249
+ if (params.mode == 0u) { return false; }
250
+ return sosOrder(A, k0, k1, p0, p1) < 0; }
251
+ @compute @workgroup_size(256) fn sortEvents(@builtin(global_invocation_id) gid_raw: vec3<u32>) { let gid = vec3<u32>(gid_raw.x + gid_raw.y * 16776960u, 0u, 0u);
252
+ let i = gid.x; if (i >= params.n) { return; }
253
+ let r0 = evStart[i]; let r1 = evStart[i + 1u]; if (badRange(r0, r1)) { return; }
254
+ for (var a = r0 + 1u; a < r1; a++) {
255
+ let k0 = events[a * 2u]; let k1 = events[a * 2u + 1u]; var b = a;
256
+ loop { if (b == r0) { break; } let p = events[(b - 1u) * 2u]; let p1 = events[(b - 1u) * 2u + 1u]; if (!beforeExact(i, k0, k1, p, p1)) { break; } events[b * 2u] = p; events[b * 2u + 1u] = p1; b = b - 1u; }
257
+ events[b * 2u] = k0; events[b * 2u + 1u] = k1;
258
+ }
259
+ }`
260
+
261
+ export const LINK_SHADER = /* wgsl */`
262
+ struct LP { e: u32, v: u32, pad0: u32, pad1: u32 }
263
+ @group(0) @binding(0) var<uniform> lp: LP;
264
+ @group(0) @binding(1) var<storage, read> pv: array<vec4<u32>>; // start vid, end vid, layer
265
+ @group(0) @binding(2) var<storage, read> pc: array<vec4<i32>>; // sx, sy, ex, ey
266
+ @group(0) @binding(3) var<storage, read_write> vcount: array<atomic<u32>>;
267
+ @group(0) @binding(4) var<storage, read> vstart: array<u32>;
268
+ @group(0) @binding(5) var<storage, read_write> vlist: array<u32>;
269
+ @group(0) @binding(6) var<storage, read_write> succ: array<u32>;
270
+ @group(0) @binding(7) var<storage, read_write> labA: array<vec2<u32>>; // label, next
271
+ @group(0) @binding(8) var<storage, read_write> labB: array<vec2<u32>>;
272
+ @compute @workgroup_size(256) fn vcountK(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u); if (g.x >= lp.e) { return; } atomicAdd(&vcount[pv[g.x].x], 1u); }
273
+ @compute @workgroup_size(256) fn vscatter(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u); if (g.x >= lp.e) { return; } let v = pv[g.x].x; let k = atomicAdd(&vcount[v], 1u); vlist[vstart[v] + k] = g.x; }
274
+ @compute @workgroup_size(256) fn successor(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u);
275
+ let e = g.x; if (e >= lp.e) { return; }
276
+ let v = pv[e].y; let a = vstart[v]; let b = vstart[v + 1u];
277
+ if (b == a) { succ[e] = e; labA[e] = vec2<u32>(e, e); return; }
278
+ var best = vlist[a];
279
+ if (b - a > 1024u) { atomicAdd(&vcount[lp.v], 1u); succ[e] = e; labA[e] = vec2<u32>(e, e); return; } // out-degree cap: a corrupt vstart must not spin the GPU
280
+ if (b - a > 1u) { // first outgoing edge clockwise from the reverse of the incoming one
281
+ let q = pc[e]; let rin = atan2(f32(q.y - q.w), f32(q.x - q.z)); var bestAng = 10.0;
282
+ for (var k = a; k < b; k++) { let o = vlist[k]; let r = pc[o]; var ang = rin - atan2(f32(r.w - r.y), f32(r.z - r.x));
283
+ if (!(ang > 0.0)) { ang = ang + 6.2831853; } if (ang > 6.2831853) { ang = ang - 6.2831853; } // a difference of two atan2 lies in [-2pi, 2pi]: one step each, no loop
284
+ if (ang < bestAng) { bestAng = ang; best = o; } }
285
+ }
286
+ succ[e] = best; labA[e] = vec2<u32>(min(e, best), best);
287
+ }
288
+ @compute @workgroup_size(256) fn jumpAB(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u); let e = g.x; if (e >= lp.e) { return; } let s = labA[e]; let t = labA[s.y]; labB[e] = vec2<u32>(min(s.x, t.x), t.y); }
289
+ @compute @workgroup_size(256) fn jumpBA(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u); let e = g.x; if (e >= lp.e) { return; } let s = labB[e]; let t = labB[s.y]; labA[e] = vec2<u32>(min(s.x, t.x), t.y); }
290
+ `
291
+
292
+ export const SCAN_SHADER = /* wgsl */`
293
+ @group(0) @binding(0) var<uniform> sp: vec4<u32>;
294
+ @group(0) @binding(1) var<storage, read_write> data: array<u32>;
295
+ @group(0) @binding(2) var<storage, read_write> sums: array<u32>;
296
+ var<workgroup> tmp: array<u32, 256>;
297
+ @compute @workgroup_size(256) fn scanBlock(@builtin(global_invocation_id) g_raw: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>, @builtin(workgroup_id) wg_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u); let wg = vec3<u32>(wg_raw.x + wg_raw.y * 65535u, 0u, 0u);
298
+ if (wg.x * 256u >= sp.x + 256u) { return; } // a workgroup past the last block (the second dispatch row): nothing to scan, and sums[wg] would be out of range
299
+ let i = g.x; var v = 0u; if (i < sp.x) { v = data[i]; } tmp[l.x] = v; workgroupBarrier();
300
+ for (var off = 1u; off < 256u; off = off * 2u) { var t = 0u; if (l.x >= off) { t = tmp[l.x - off]; } workgroupBarrier(); tmp[l.x] = tmp[l.x] + t; workgroupBarrier(); }
301
+ if (i < sp.x) { data[i] = tmp[l.x] - v; }
302
+ if (l.x == 255u) { sums[wg.x] = tmp[255]; }
303
+ }
304
+ @compute @workgroup_size(256) fn addOff(@builtin(global_invocation_id) g_raw: vec3<u32>, @builtin(workgroup_id) wg_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u); let wg = vec3<u32>(wg_raw.x + wg_raw.y * 65535u, 0u, 0u); if (g.x < sp.x) { data[g.x] = data[g.x] + sums[wg.x]; } }`
305
+
306
+ export const CHECK_SHADER = /* wgsl */`
307
+ @group(0) @binding(0) var<uniform> cp: vec4<u32>; // n, cap
308
+ @group(0) @binding(1) var<storage, read> starts: array<u32>;
309
+ @group(0) @binding(2) var<storage, read_write> bad: array<atomic<u32>>;
310
+ @compute @workgroup_size(256) fn check(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u); let i = g.x; if (i >= cp.x) { return; }
311
+ let lo = starts[i]; let hi = starts[i + 1u]; if (hi < lo || hi - lo > cp.y) { atomicAdd(&bad[0], 1u); } }`
312
+
313
+ export const GLUE_SHADER = PRELUDE + /* wgsl */`
314
+ struct GP { n: u32, polys: u32, fill: u32, mode: u32 }
315
+ @group(0) @binding(4) var<uniform> gp: GP;
316
+ @group(0) @binding(5) var<storage, read> evStart: array<u32>;
317
+ @group(0) @binding(6) var<storage, read> events: array<vec4<u32>>;
318
+ @group(0) @binding(7) var<storage, read> polyInfo: array<vec4<u32>>; // first, count, layer segStart, layer segEnd
319
+ @group(0) @binding(8) var<storage, read_write> work: array<u32>;
320
+ @group(0) @binding(9) var<storage, read_write> polyW: array<i32>;
321
+ fn leQ(v: u32, a0: u32, ay: i32, dA: vec2<i32>) -> bool {
322
+ if (v == a0) { var s = sign(dA.y); if (s == 0) { s = sign(dA.x); } return s >= 0; }
323
+ let d = vtx(v).y - ay; if (d != 0) { return d < 0; } return v < a0; }
324
+ @compute @workgroup_size(256) fn rays(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u);
325
+ let p = g.x; if (p >= gp.polys) { return; }
326
+ let pi = polyInfo[p]; let a0 = pi.x; let a1 = segInfo[a0].z; let A0 = vtx(a0); let dA = vtx(a1) - A0;
327
+ var wn = 0;
328
+ for (var c = pi.z; c < pi.w; c++) { let c1 = segInfo[c].z;
329
+ if (leQ(c, a0, A0.y, dA) == leQ(c1, a0, A0.y, dA)) { continue; }
330
+ var side = 1;
331
+ if (c != a0) {
332
+ if (c1 == a0) { side = orientSoS(c, c1, a1); if (side == 0) { let dc = vtx(c1) - vtx(c); side = sign(dc.x * dA.x + dc.y * dA.y); } }
333
+ else { side = orientSoS(c, c1, a0); } }
334
+ let C0 = vtx(c); let C1 = vtx(c1);
335
+ if (C1.y > C0.y || (C1.y == C0.y && c1 > c)) { if (side > 0) { wn = wn + 1; } } else { if (side < 0) { wn = wn - 1; } } }
336
+ polyW[p] = wn;
337
+ }
338
+ @compute @workgroup_size(256) fn deltaSums(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u);
339
+ let a = g.x; if (a > gp.n) { return; } if (a == gp.n) { work[a] = 0u; return; }
340
+ if (badRange(evStart[a], evStart[a + 1u])) { return; }
341
+ var s = 0; for (var r = evStart[a]; r < evStart[a + 1u]; r++) { s = s + bitcast<i32>(events[r * 2u + 1u].x); } work[a] = bitcast<u32>(s);
342
+ }`
343
+
344
+ export const PIECES_SHADER = PRELUDE + /* wgsl */`
345
+ struct GP { n: u32, polys: u32, fill: u32, mode: u32 }
346
+ @group(0) @binding(4) var<uniform> gp: GP;
347
+ @group(0) @binding(5) var<storage, read> evStart: array<u32>;
348
+ @group(0) @binding(6) var<storage, read> events: array<vec4<u32>>;
349
+ @group(0) @binding(7) var<storage, read> leftW: array<vec2<u32>>; // per segment: (scanned delta sum, polygon index)
350
+ @group(0) @binding(8) var<storage, read> polyInfoW: array<vec4<i32>>; // first, count, start winding, 0
351
+ @group(0) @binding(9) var<storage, read_write> pieceCount: array<u32>;
352
+ @group(0) @binding(10) var<storage, read_write> pv: array<vec4<u32>>; // start vid, end vid, layer
353
+ @group(0) @binding(11) var<storage, read_write> pcd: array<vec4<i32>>; // two entries per piece: coordinates, parent direction
354
+ fn insideOf(w: i32) -> bool { if (gp.fill == 3u) { return (w & 1) != 0; } if (gp.fill == 2u) { return w < 0; } if (gp.fill == 1u) { return w > 0; } return w != 0; }
355
+ @compute @workgroup_size(256) fn pieces(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u);
356
+ let a = g.x; if (a >= gp.n) { return; }
357
+ let lw = leftW[a]; let pw = polyInfoW[lw.y]; let firstScan = leftW[u32(pw.x)].x;
358
+ var wl = pw.z + bitcast<i32>(lw.x - firstScan);
359
+ let S = segs[a]; var x = S.x; var y = S.y; var vid = a; let nxt = segInfo[a].z; let dir = vec2<i32>(S.z - S.x, S.w - S.y); let layer = segInfo[a].x;
360
+ var k = 0u; let base = pieceCount[a]; let r0 = evStart[a]; let r1 = evStart[a + 1u]; if (badRange(r0, r1)) { return; }
361
+ for (var r = r0; r <= r1; r++) {
362
+ var nx = S.z; var ny = S.w; var nv = nxt; var d = 0;
363
+ if (r < r1) { let e = events[r * 2u + 1u]; d = bitcast<i32>(e.x); nx = bitcast<i32>(e.y); ny = bitcast<i32>(e.z); nv = e.w; }
364
+ let il = insideOf(wl); let ir = insideOf(wl - 1);
365
+ if (il != ir) {
366
+ if (gp.mode == 1u) { let o = base + k;
367
+ if (il) { pv[o] = vec4<u32>(vid, nv, layer, 0u); pcd[o * 2u] = vec4<i32>(x, y, nx, ny); pcd[o * 2u + 1u] = vec4<i32>(0, 0, dir.x, dir.y); }
368
+ else { pv[o] = vec4<u32>(nv, vid, layer, 0u); pcd[o * 2u] = vec4<i32>(nx, ny, x, y); pcd[o * 2u + 1u] = vec4<i32>(0, 0, -dir.x, -dir.y); } }
369
+ k = k + 1u; }
370
+ x = nx; y = ny; vid = nv; wl = wl + d; }
371
+ if (gp.mode == 0u) { pieceCount[a] = k; }
372
+ }`
373
+
374
+ export const TIES_SHADER = /* wgsl */`
375
+ @group(0) @binding(0) var<uniform> tp: vec4<u32>;
376
+ @group(0) @binding(1) var<storage, read> evStart: array<u32>;
377
+ @group(0) @binding(2) var<storage, read> events: array<vec4<u32>>;
378
+ @group(0) @binding(3) var<storage, read_write> tieList: array<atomic<u32>>;
379
+ fn mulu(a: u32, b: u32) -> vec2<u32> { let al = a & 0xffffu; let ah = a >> 16u; let bl = b & 0xffffu; let bh = b >> 16u; let ll = al * bl; let lh = al * bh; let hl = ah * bl; let hh = ah * bh;
380
+ var lo = ll; var hi = hh + (lh >> 16u) + (hl >> 16u); let t1 = lh << 16u; lo = lo + t1; if (lo < t1) { hi = hi + 1u; } let t2 = hl << 16u; lo = lo + t2; if (lo < t2) { hi = hi + 1u; } return vec2<u32>(lo, hi); }
381
+ fn add128(a: vec4<u32>, b: vec4<u32>) -> vec4<u32> { var r: vec4<u32>; r.x = a.x + b.x; var c = select(0u, 1u, r.x < a.x);
382
+ let y = a.y + b.y; var c1 = select(0u, 1u, y < a.y); r.y = y + c; if (r.y < y) { c1 = 1u; } c = c1;
383
+ let z = a.z + b.z; var c2 = select(0u, 1u, z < a.z); r.z = z + c; if (r.z < z) { c2 = 1u; } c = c2; r.w = a.w + b.w + c; return r; }
384
+ fn mul128(a: vec2<u32>, b: vec2<u32>) -> vec4<u32> { let p00 = mulu(a.x, b.x); let p01 = mulu(a.x, b.y); let p10 = mulu(a.y, b.x); let p11 = mulu(a.y, b.y);
385
+ var r = vec4<u32>(p00.x, p00.y, p11.x, p11.y); r = add128(r, vec4<u32>(0u, p01.x, p01.y, 0u)); r = add128(r, vec4<u32>(0u, p10.x, p10.y, 0u)); return r; }
386
+ @compute @workgroup_size(256) fn ties(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u);
387
+ let a = g.x; if (a >= tp.x) { return; } if (evStart[a + 1u] < evStart[a] || evStart[a + 1u] - evStart[a] > 4096u) { return; } // RANGE_CAP (this shader has no prelude)
388
+ for (var r = evStart[a] + 1u; r < evStart[a + 1u]; r++) { let p = events[(r - 1u) * 2u]; let q = events[r * 2u];
389
+ if (all(mul128(p.xy, q.zw) == mul128(q.xy, p.zw))) { let k = atomicAdd(&tieList[0], 1u); atomicStore(&tieList[1u + k], a); return; } }
390
+ }`
391
+
392
+ export const ZIP_SHADER = /* wgsl */`
393
+ @group(0) @binding(0) var<uniform> zp: vec4<u32>;
394
+ @group(0) @binding(1) var<storage, read> a: array<u32>;
395
+ @group(0) @binding(2) var<storage, read> b: array<u32>;
396
+ @group(0) @binding(3) var<storage, read_write> o: array<vec2<u32>>;
397
+ @compute @workgroup_size(256) fn zip(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u); if (g.x < zp.x) { o[g.x] = vec2<u32>(a[g.x], b[g.x]); } }`
398
+
399
+ export const DIRECTIONS_SHADER = /* wgsl */`
400
+ @group(0) @binding(0) var<uniform> dp: vec4<u32>;
401
+ @group(0) @binding(1) var<storage, read> pcd: array<vec4<i32>>;
402
+ @group(0) @binding(2) var<storage, read_write> dirOut: array<vec4<i32>>;
403
+ @compute @workgroup_size(256) fn dirs(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u); if (g.x < dp.x) { dirOut[g.x] = pcd[g.x * 2u + 1u]; } }`
404
+
405
+ export const EXPAND_SHADER = /* wgsl */`
406
+ @group(0) @binding(0) var<uniform> ep: vec4<u32>; // segments, polygons
407
+ @group(0) @binding(1) var<storage, read> pts: array<vec2<i32>>;
408
+ @group(0) @binding(2) var<storage, read> polyIn: array<vec4<u32>>; // first, count, layer start, layer end
409
+ @group(0) @binding(3) var<storage, read> polyLayerIn: array<u32>;
410
+ @group(0) @binding(4) var<storage, read_write> segOut: array<vec4<i32>>;
411
+ @group(0) @binding(5) var<storage, read_write> infoOut: array<vec4<u32>>;
412
+ @group(0) @binding(6) var<storage, read_write> segPolyOut: array<u32>;
413
+ @compute @workgroup_size(256) fn expand(@builtin(global_invocation_id) g_raw: vec3<u32>) { let i = g_raw.x + g_raw.y * 16776960u; if (i >= ep.x || ep.y == 0u) { return; }
414
+ var lo = 0u; var hi = ep.y;
415
+ for (var step = 0u; step < 32u; step++) { if (hi - lo <= 1u) { break; } let mid = (lo + hi) / 2u; if (polyIn[mid].x <= i) { lo = mid; } else { hi = mid; } }
416
+ let first = polyIn[lo].x; let count = polyIn[lo].y; let next = first + (i - first + 1u) % count; let prev = first + (i - first + count - 1u) % count;
417
+ let a = pts[i]; let b = pts[next];
418
+ segOut[i] = vec4<i32>(a.x, a.y, b.x, b.y);
419
+ var flag = 0u; if (i == first) { flag = 1u; }
420
+ infoOut[i] = vec4<u32>(polyLayerIn[lo], prev, next, flag);
421
+ segPolyOut[i] = lo; }`
422
+
423
+ export const COMPACT_SHADER = /* wgsl */`
424
+ @group(0) @binding(0) var<uniform> dp: vec4<u32>;
425
+ @group(0) @binding(1) var<storage, read> pcd: array<vec4<i32>>;
426
+ @group(0) @binding(2) var<storage, read> succIn: array<u32>;
427
+ @group(0) @binding(3) var<storage, read> pvIn: array<vec4<u32>>;
428
+ @group(0) @binding(4) var<storage, read_write> compactOut: array<vec4<i32>>;
429
+ @compute @workgroup_size(256) fn compact(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = g_raw.x + g_raw.y * 16776960u; if (g < dp.x) { let c = pcd[g * 2u]; compactOut[g] = vec4<i32>(c.x, c.y, bitcast<i32>(succIn[g]), bitcast<i32>(pvIn[g].z)); } }`
430
+
431
+ export const GATE_SHADER = /* wgsl */`
432
+ @group(0) @binding(0) var<uniform> gu: vec4<u32>; // x: index into src, y: use src (1) or the constant z
433
+ @group(0) @binding(1) var<storage, read> bad: array<u32>;
434
+ @group(0) @binding(2) var<storage, read> src: array<u32>;
435
+ @group(0) @binding(3) var<storage, read_write> dst: array<u32>;
436
+ // a count for a later kernel's uniform, zero once any range check has failed: every later loop is bounded by that count
437
+ @compute @workgroup_size(1) fn gate() { var v = gu.z; if (gu.y == 1u) { v = src[gu.x]; } if (bad[0] != 0u) { v = 0u; } dst[0] = v; }`
438
+
439
+ export const ARGS_SHADER = /* wgsl */`
440
+ @group(0) @binding(0) var<uniform> au: vec4<u32>; // x: workgroup size
441
+ @group(0) @binding(1) var<storage, read> cnt: array<u32>;
442
+ @group(0) @binding(2) var<storage, read_write> args: array<u32>;
443
+ @compute @workgroup_size(1) fn argsK() { let groups = max(1u, (cnt[0] + au.x - 1u) / au.x); args[0] = min(groups, 65535u); args[1] = (groups + 65534u) / 65535u; args[2] = 1u; }`
444
+
445
+ export const POLYGON_WINDING_SHADER = /* wgsl */`
446
+ @group(0) @binding(0) var<uniform> pu: vec4<u32>;
447
+ @group(0) @binding(1) var<storage, read> polyInfo: array<vec4<u32>>;
448
+ @group(0) @binding(2) var<storage, read> polyW: array<i32>;
449
+ @group(0) @binding(3) var<storage, read_write> polyInfoW: array<vec4<i32>>;
450
+ @compute @workgroup_size(256) fn polyWK(@builtin(global_invocation_id) g_raw: vec3<u32>) { let g = vec3<u32>(g_raw.x + g_raw.y * 16776960u, 0u, 0u); let p = g.x; if (p >= pu.x) { return; } let q = polyInfo[p]; polyInfoW[p] = vec4<i32>(i32(q.x), i32(q.y), polyW[p], 0); }`
@@ -0,0 +1,131 @@
1
+ // One slice with PASS1's front on the GPU: the kernel's calls (wasm-core/contour_phase.h) around contour_gpu.js'.
2
+ // Used by the slicer worker, and by the node test with Dawn's device. Two routes:
3
+ // mesh the kernel parses, seats and plans the layers and hands the mesh over (MESH); the GPU cuts, chains and
4
+ // unions every layer (contour_front_gpu.js -> contour_gpu.js) without the contours leaving it; the kernel
5
+ // walks the union's pieces and finishes the slice (INJECT). Taken when the kernel and the GPU module both
6
+ // have it.
7
+ // capture the kernel cuts and chains every layer itself (CAPTURE), the GPU unions them, the kernel finishes (INJECT).
8
+ //
9
+ // The slice never depends on the GPU: a kernel without the contour phase, a slice that does not reach PASS1 (a
10
+ // multi-material or cached one), a device that refuses the input, a pipeline that stops itself — each ends in the
11
+ // kernel's own slice, and the reason is reported. A layer the GPU left open (a chain that does not close, a union
12
+ // whose pieces do not close) gets the kernel's own union inside the GPU slice (contour_phase::assemble) and is counted;
13
+ // on the mesh route the kernel first cuts and chains that layer from the mesh it kept.
14
+ const PHASE_OFF = 0, PHASE_CAPTURE = 1, PHASE_MESH = 3
15
+
16
+ /** Whether this kernel build can take contours from outside. */
17
+ export const kernelHasContourPhase = (kernel) => typeof kernel?.contour_phase_mode === 'function'
18
+ /** Whether this kernel build can hand its mesh to the GPU's cut and chain (the mesh route). */
19
+ export const kernelHasMeshPhase = (kernel) => typeof kernel?.contour_mesh === 'function'
20
+
21
+ /**
22
+ * @param {object} job
23
+ * @param {object} job.kernel the loaded kernel module
24
+ * @param {object} job.contourGpu makeContourGpu()'s result (`front` present: the mesh route)
25
+ * @param {Uint8Array} job.stl
26
+ * @param {string} job.paramsText
27
+ * @param {(done: number, total: number) => void} job.onProgress
28
+ * @param {() => boolean} [job.canceled] read between the kernel calls (the last call clears the cancel flag)
29
+ * @param {boolean} [job.meshRoute] false keeps the capture route even where the mesh route is available
30
+ * @returns {Promise<object>} the kernel's result; `stats.contour_engine` is 'gpu' or 'cpu', with
31
+ * `contour_engine_reason` when the GPU was asked for and not used, or 'reused' when the layers came from the stage
32
+ * cache; `stats.contour_front` says which route cut and chained the layers ('gpu' on the mesh route, 'cpu' on capture)
33
+ */
34
+ export async function sliceWithContourGpu(job) {
35
+ if (job.meshRoute !== false && job.contourGpu.front && kernelHasMeshPhase(job.kernel)) return sliceThroughMesh(job)
36
+ return sliceThroughCapture(job)
37
+ }
38
+
39
+ // the kernel's own slice, with the reason the GPU was not used
40
+ const kernelSlice = ({ kernel, stl, paramsText, onProgress }, reason) => {
41
+ kernel.contour_phase_mode(PHASE_OFF)
42
+ const result = kernel.slice(stl, paramsText, onProgress)
43
+ if (result?.stats) { result.stats.contour_engine = 'cpu'; result.stats.contour_engine_reason = reason }
44
+ return result
45
+ }
46
+
47
+ // a first call that did not stop at PASS1: the slice took a route without its union, or failed or was canceled, and
48
+ // what it returned is the answer
49
+ const notStopped = ({ kernel, paramsText }, result) => {
50
+ kernel.contour_phase_mode(PHASE_OFF)
51
+ if (result?.stats) {
52
+ // reuse_stages 2 is the kernel's stage cache: the layers, contours included, are the previous slice's
53
+ result.stats.contour_engine = 'cpu'
54
+ result.stats.contour_engine_reason = 'this slice has its own contour path'
55
+ if (Number(JSON.parse(paramsText).reuse_stages) >= 2) { result.stats.contour_engine = 'reused'; delete result.stats.contour_engine_reason }
56
+ }
57
+ return result
58
+ }
59
+
60
+ // the first kernel call, in `phase`; it stops at PASS1 when the slice reaches it
61
+ const firstCall = ({ kernel, stl, paramsText, onProgress }, phase) => {
62
+ kernel.contour_phase_mode(phase)
63
+ try { return kernel.slice(stl, paramsText, onProgress) }
64
+ catch (error) { kernel.contour_phase_mode(PHASE_OFF); throw error }
65
+ }
66
+
67
+ // the last kernel call, on the contours assemble() left; the stats say how they were made
68
+ const finish = ({ kernel, stl, paramsText, onProgress }, stats) => {
69
+ let result
70
+ try { result = kernel.slice(stl, paramsText, onProgress) }
71
+ finally { kernel.contour_phase_mode(PHASE_OFF) }
72
+ if (result?.stats) Object.assign(result.stats, { contour_engine: 'gpu' }, stats)
73
+ return result
74
+ }
75
+
76
+ async function sliceThroughMesh(job) {
77
+ const { kernel, contourGpu, canceled = () => false } = job
78
+ const meshStarted = performance.now()
79
+ const meshed = firstCall(job, PHASE_MESH)
80
+ if (!meshed?.captured) return notStopped(job, meshed)
81
+ const meshMs = performance.now() - meshStarted
82
+
83
+ const gpuStarted = performance.now()
84
+ let front
85
+ try {
86
+ const mesh = kernel.contour_mesh() // views into the kernel's heap: read before the kernel allocates again
87
+ front = await contourGpu.front.cut(mesh.tris, mesh.planes)
88
+ } catch (error) { front = { failure: String(error?.message ?? error) } }
89
+ if (canceled()) { kernel.contour_phase_mode(PHASE_OFF); return { error: 'canceled' } }
90
+ if (front.failure) return kernelSlice(job, front.failure)
91
+ kernel.contour_origin_buffer(front.input.layerCount).set(front.origins)
92
+ kernel.contour_open_layers().set(front.openLayers)
93
+ let outcome
94
+ try { outcome = await contourGpu.union(front.input, (pieceCount) => kernel.contour_result_buffer(pieceCount)) }
95
+ catch (error) { outcome = { ok: false, reason: String(error?.message ?? error) } }
96
+ if (canceled()) { kernel.contour_phase_mode(PHASE_OFF); return { error: 'canceled' } }
97
+ if (!outcome.ok) return kernelSlice(job, outcome.reason)
98
+ const assembled = kernel.contour_assemble() // from here the kernel reads the contours on its next slice
99
+ if (assembled.unavailable) return kernelSlice(job, 'a layer the GPU left open has no mesh to be rebuilt from')
100
+ const gpuMs = performance.now() - gpuStarted
101
+
102
+ let openLayers = 0
103
+ for (const flag of front.openLayers) openLayers += flag
104
+ return finish(job, {
105
+ contour_front: 'gpu', contour_capture_ms: meshMs, contour_front_ms: front.ms, contour_gpu_ms: gpuMs,
106
+ contour_fallback_layers: assembled.fallbackLayers, contour_open_layers: openLayers,
107
+ contour_pieces: outcome.pieceCount, contour_crossings: outcome.eventCount,
108
+ })
109
+ }
110
+
111
+ async function sliceThroughCapture(job) {
112
+ const { kernel, contourGpu, canceled = () => false } = job
113
+ const captureStarted = performance.now()
114
+ const captured = firstCall(job, PHASE_CAPTURE)
115
+ if (!captured?.captured) return notStopped(job, captured)
116
+ const captureMs = performance.now() - captureStarted
117
+
118
+ const gpuStarted = performance.now()
119
+ let outcome
120
+ try { outcome = await contourGpu.union(kernel.contour_gpu_input(), (pieceCount) => kernel.contour_result_buffer(pieceCount)) }
121
+ catch (error) { outcome = { ok: false, reason: String(error?.message ?? error) } }
122
+ if (canceled()) { kernel.contour_phase_mode(PHASE_OFF); return { error: 'canceled' } }
123
+ if (!outcome.ok) return kernelSlice(job, outcome.reason)
124
+ const assembled = kernel.contour_assemble() // from here the kernel reads the contours on its next slice
125
+ const gpuMs = performance.now() - gpuStarted
126
+
127
+ return finish(job, {
128
+ contour_front: 'cpu', contour_capture_ms: captureMs, contour_gpu_ms: gpuMs,
129
+ contour_fallback_layers: assembled.fallbackLayers, contour_pieces: outcome.pieceCount, contour_crossings: outcome.eventCount,
130
+ })
131
+ }