three-slicer 0.3.2 → 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,367 @@
1
+ // PASS1's per-layer union on the GPU: every layer's raw loops in, the union's boundary pieces out, all layers in one
2
+ // submit. The kernel side is wasm-core/contour_phase.h (capture the loops, build this module's input, walk the pieces
3
+ // into contours, and fall back to its own union for any layer whose loops do not close); the worker drives both.
4
+ //
5
+ // What runs here, per layer independently (WGSL: contour_gpu_shaders.js):
6
+ // expand the uploaded start points into segments, links and polygon-of-segment
7
+ // bin segments into a 2 mm grid per layer
8
+ // intersect pairs of segments sharing a cell; a crossing is decided as if every vertex were moved by an
9
+ // infinitesimal amount (higher vertex id first, x before y), and its place along each segment is an exact
10
+ // fraction of 64-bit integers
11
+ // sort each segment's crossings by that fraction; equal fractions by the same perturbation (256-bit compare)
12
+ // winding a ray per polygon for its first segment, then the crossings' deltas along the polygon
13
+ // pieces the stretches between crossings whose two sides differ in NonZero insideness
14
+ // link at every vertex, each piece's successor by direction
15
+ // compact (start x, start y, successor, layer) per piece — what the kernel reads
16
+ //
17
+ // Measured (design appendix F15): 2.3M segments over 849 layers in 50 to 70 ms including the kernel's side, against
18
+ // 2.6 s for the same union on one CPU thread and about 0.3 s on 14. The contours have the CPU union's area (within
19
+ // 2e-10 relative) and other vertices, so a GPU slice's G-code is not the CPU slice's byte for byte.
20
+ //
21
+ // The module takes its GPUDevice from the caller and never decides whether the GPU should be used.
22
+ import { makeContourFront } from './contour_front_gpu.js'
23
+ import {
24
+ BIN_SHADER, INTERSECT_SHADER, SORT_SHADER, LINK_SHADER, SCAN_SHADER, CHECK_SHADER, GLUE_SHADER, PIECES_SHADER,
25
+ TIES_SHADER, ZIP_SHADER, DIRECTIONS_SHADER, EXPAND_SHADER, COMPACT_SHADER, GATE_SHADER, ARGS_SHADER,
26
+ POLYGON_WINDING_SHADER,
27
+ } from './contour_gpu_shaders.js'
28
+
29
+ // Numeric WebGPU constants: the named globals exist in a browser worker but not on a bare node globalThis.
30
+ const BUFFER = { MAP_READ: 0x1, COPY_SRC: 0x4, COPY_DST: 0x8, UNIFORM: 0x40, STORAGE: 0x80, INDIRECT: 0x100 }
31
+ const SHADER_STAGE_COMPUTE = 0x4
32
+ const MAP_MODE_READ = 0x1
33
+
34
+ const CELL_SIZE = 2_000_000 // the grid cell in kernel units (2 mm); contour_phase.cpp counts cell entries with the same value
35
+ const FILL_NONZERO = 0 // PASS1's fill rule (the shaders also know positive, negative and even-odd)
36
+ const WORKGROUP_SIZE = 256
37
+ const MAX_WORKGROUPS_PER_DIMENSION = 65535
38
+ const RANGE_LIMIT = 4096 // a scanned range longer than this is a corrupt scan, and no kernel loops over it
39
+ const VERTEX_DEGREE_LIMIT = 1024 // pieces meeting at one vertex
40
+ const SMALLEST_POOLED_BUFFER = 65536
41
+ const SMALLEST_EVENT_CAPACITY = 65536
42
+ // Crossings per segment the buffers are sized for, tried in order. A slice's contours rarely cross (measured: 80
43
+ // crossings in 2.3M segments on a clean mesh, 64,000 in 1.6M on a scan), so the first fits; a run that overflows its
44
+ // bound reports it and the next factor is tried.
45
+ const EVENTS_PER_SEGMENT = [1, 3]
46
+ // The limits the device is asked for. The defaults (128 MB per storage binding, 8 storage buffers per stage) are below
47
+ // what a large model needs: pieces take 64 bytes per segment at the first event factor.
48
+ const WANTED_BINDING_BYTES = 1 << 30
49
+ const WANTED_STORAGE_BUFFERS = 10
50
+
51
+ /** A device for makeContourGpu from a GPU entry point (navigator.gpu in a worker), or null when there is none. */
52
+ export async function acquireContourDevice(gpu) {
53
+ if (!gpu) return null
54
+ const adapter = await gpu.requestAdapter()
55
+ if (!adapter) return null
56
+ if (adapter.limits.maxStorageBuffersPerShaderStage < WANTED_STORAGE_BUFFERS) return null
57
+ return adapter.requestDevice({ requiredLimits: {
58
+ maxStorageBufferBindingSize: Math.min(adapter.limits.maxStorageBufferBindingSize, WANTED_BINDING_BYTES),
59
+ maxBufferSize: Math.min(adapter.limits.maxBufferSize, WANTED_BINDING_BYTES),
60
+ maxStorageBuffersPerShaderStage: WANTED_STORAGE_BUFFERS,
61
+ } })
62
+ }
63
+
64
+ /**
65
+ * @param {GPUDevice} device from acquireContourDevice (browser) or Dawn's create() (node tests)
66
+ * @returns {Promise<{ union(input, piecesTarget): Promise<object>, front: object, dispose(): void }>} `front` is
67
+ * contour_front_gpu.js on the same device: the cut and chain that feed `union` without leaving the GPU
68
+ */
69
+ export async function makeContourGpu(device) {
70
+ const compile = async (code, name) => {
71
+ const module = device.createShaderModule({ code })
72
+ const info = await module.getCompilationInfo()
73
+ const failure = info.messages.find(message => message.type === 'error')
74
+ if (failure) throw new Error(`contour GPU shader ${name}: line ${failure.lineNum}: ${failure.message}`)
75
+ return module
76
+ }
77
+ // a layout from one letter per binding: u uniform, r read-only storage, w storage
78
+ const BINDING_TYPE = { u: 'uniform', r: 'read-only-storage', w: 'storage' }
79
+ const layoutOf = (kinds) => device.createBindGroupLayout({ entries: [...kinds].map((kind, binding) => (
80
+ { binding, visibility: SHADER_STAGE_COMPUTE, buffer: { type: BINDING_TYPE[kind] } })) })
81
+ const pipelineOf = (module, layout, entryPoint) => device.createComputePipeline({
82
+ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }), compute: { module, entryPoint } })
83
+
84
+ const layouts = {
85
+ expand: layoutOf('urrrwww'), bin: layoutOf('urrrwrw'), intersect: layoutOf('urrrrrwwrww'), sort: layoutOf('urrrrwr'),
86
+ link: layoutOf('urrwrwwww'), scan: layoutOf('uww'), check: layoutOf('urw'), glue: layoutOf('urrrurrrww'),
87
+ pieces: layoutOf('urrrurrrrwww'), ties: layoutOf('urrw'), zip: layoutOf('urrw'), directions: layoutOf('urw'),
88
+ compact: layoutOf('urrrw'), gate: layoutOf('urrw'), args: layoutOf('urw'), polygonWinding: layoutOf('urrw'),
89
+ }
90
+ const modules = {
91
+ expand: await compile(EXPAND_SHADER, 'expand'), bin: await compile(BIN_SHADER, 'bin'),
92
+ intersect: await compile(INTERSECT_SHADER, 'intersect'), sort: await compile(SORT_SHADER, 'sort'),
93
+ link: await compile(LINK_SHADER, 'link'), scan: await compile(SCAN_SHADER, 'scan'),
94
+ check: await compile(CHECK_SHADER, 'check'), glue: await compile(GLUE_SHADER, 'glue'),
95
+ pieces: await compile(PIECES_SHADER, 'pieces'), ties: await compile(TIES_SHADER, 'ties'),
96
+ zip: await compile(ZIP_SHADER, 'zip'), directions: await compile(DIRECTIONS_SHADER, 'directions'),
97
+ compact: await compile(COMPACT_SHADER, 'compact'), gate: await compile(GATE_SHADER, 'gate'),
98
+ args: await compile(ARGS_SHADER, 'args'), polygonWinding: await compile(POLYGON_WINDING_SHADER, 'polygon winding'),
99
+ }
100
+ const pipelines = {
101
+ expand: pipelineOf(modules.expand, layouts.expand, 'expand'),
102
+ cellCount: pipelineOf(modules.bin, layouts.bin, 'count'), cellScatter: pipelineOf(modules.bin, layouts.bin, 'scatter'),
103
+ intersect: pipelineOf(modules.intersect, layouts.intersect, 'intersect'),
104
+ sortEvents: pipelineOf(modules.sort, layouts.sort, 'sortEvents'),
105
+ vertexCount: pipelineOf(modules.link, layouts.link, 'vcountK'), vertexScatter: pipelineOf(modules.link, layouts.link, 'vscatter'),
106
+ successor: pipelineOf(modules.link, layouts.link, 'successor'),
107
+ scanBlock: pipelineOf(modules.scan, layouts.scan, 'scanBlock'), scanAddOffsets: pipelineOf(modules.scan, layouts.scan, 'addOff'),
108
+ checkRanges: pipelineOf(modules.check, layouts.check, 'check'),
109
+ rays: pipelineOf(modules.glue, layouts.glue, 'rays'), deltaSums: pipelineOf(modules.glue, layouts.glue, 'deltaSums'),
110
+ pieces: pipelineOf(modules.pieces, layouts.pieces, 'pieces'), ties: pipelineOf(modules.ties, layouts.ties, 'ties'),
111
+ zip: pipelineOf(modules.zip, layouts.zip, 'zip'), directions: pipelineOf(modules.directions, layouts.directions, 'dirs'),
112
+ compact: pipelineOf(modules.compact, layouts.compact, 'compact'),
113
+ gate: pipelineOf(modules.gate, layouts.gate, 'gate'), dispatchArguments: pipelineOf(modules.args, layouts.args, 'argsK'),
114
+ polygonWinding: pipelineOf(modules.polygonWinding, layouts.polygonWinding, 'polyWK'),
115
+ }
116
+
117
+ // Buffers are pooled across runs, by size rounded up to a power of two: creating the upper-bound buffers anew each
118
+ // run cost more than the rest of the pipeline (measured). A pooled buffer is not zeroed, so every counter is cleared
119
+ // on the GPU before it is used.
120
+ const pool = new Map()
121
+ let lent = []
122
+ let held = [] // bind groups and small buffers of the run in flight
123
+ let encoder = null
124
+ const smallBuffer = (usage) => { const buffer = device.createBuffer({ size: 16, usage }); held.push(buffer); return buffer }
125
+ const pooled = (bytes, usage) => {
126
+ const size = Math.max(SMALLEST_POOLED_BUFFER, 2 ** Math.ceil(Math.log2(Math.max(16, bytes))))
127
+ const key = `${size}:${usage}`
128
+ let buffer = pool.get(key)?.pop()
129
+ if (!buffer) buffer = device.createBuffer({ size, usage: usage | BUFFER.COPY_DST })
130
+ lent.push([key, buffer])
131
+ return buffer
132
+ }
133
+ const release = () => {
134
+ for (const [key, buffer] of lent) {
135
+ if (!pool.has(key)) pool.set(key, [])
136
+ pool.get(key).push(buffer)
137
+ }
138
+ for (const item of held) item.destroy?.()
139
+ lent = []
140
+ held = []
141
+ }
142
+ const ensureEncoder = () => { if (!encoder) encoder = device.createCommandEncoder(); return encoder }
143
+ const bindGroupOf = (layout, buffers) => {
144
+ const group = device.createBindGroup({ layout, entries: buffers.map((buffer, binding) => ({ binding, resource: { buffer } })) })
145
+ held.push(group)
146
+ return group
147
+ }
148
+ // one compute pass over `invocations` items
149
+ const dispatch = (pipeline, layout, buffers, invocations) => {
150
+ let groupsX = Math.ceil(invocations / WORKGROUP_SIZE), groupsY = 1
151
+ if (groupsX > MAX_WORKGROUPS_PER_DIMENSION) { groupsY = Math.ceil(groupsX / MAX_WORKGROUPS_PER_DIMENSION); groupsX = MAX_WORKGROUPS_PER_DIMENSION }
152
+ const pass = ensureEncoder().beginComputePass()
153
+ pass.setPipeline(pipeline); pass.setBindGroup(0, bindGroupOf(layout, buffers)); pass.dispatchWorkgroups(groupsX, groupsY); pass.end()
154
+ }
155
+ // one compute pass whose size a GPU buffer holds (a count that exists only on the GPU)
156
+ const dispatchIndirect = (pipeline, layout, buffers, argumentsBuffer) => {
157
+ const pass = ensureEncoder().beginComputePass()
158
+ pass.setPipeline(pipeline); pass.setBindGroup(0, bindGroupOf(layout, buffers)); pass.dispatchWorkgroupsIndirect(argumentsBuffer, 0); pass.end()
159
+ }
160
+ const clear = (buffer) => ensureEncoder().clearBuffer(buffer)
161
+ const copy = (source, sourceOffset, target, targetOffset, bytes) => ensureEncoder().copyBufferToBuffer(source, sourceOffset, target, targetOffset, bytes)
162
+ const uniform = (values) => {
163
+ const buffer = smallBuffer(BUFFER.UNIFORM | BUFFER.COPY_DST)
164
+ device.queue.writeBuffer(buffer, 0, new Uint32Array(values))
165
+ return buffer
166
+ }
167
+ // in-place exclusive scan of `count` u32
168
+ const scan = (buffer, count) => {
169
+ const blocks = Math.ceil(count / WORKGROUP_SIZE)
170
+ const parameters = uniform([count, 0, 0, 0])
171
+ const blockSums = pooled(blocks * 4 + 4, BUFFER.STORAGE)
172
+ dispatch(pipelines.scanBlock, layouts.scan, [parameters, buffer, blockSums], count)
173
+ if (blocks === 1) return // one block: scanBlock already wrote the exclusive scan
174
+ scan(blockSums, blocks)
175
+ dispatch(pipelines.scanAddOffsets, layouts.scan, [parameters, buffer, blockSums], count)
176
+ }
177
+ // Every kernel that loops over a scanned range runs only behind this: a dispatch keeps the GPU after its page is gone,
178
+ // and one looping over a corrupt range froze the display until a reboot (2026-09-26). The check records into `bad`;
179
+ // the gates below then zero the item counts of the kernels that would loop.
180
+ const checkRanges = (starts, count, limit, bad) =>
181
+ dispatch(pipelines.checkRanges, layouts.check, [uniform([count, limit, 0, 0]), starts, bad], count)
182
+ // `target[targetWord]` = a count taken from a GPU buffer (or `constant` when there is none), or 0 once `bad` is set
183
+ const gate = (bad, source, sourceIndex, target, targetWord, constant = 0) => {
184
+ const out = pooled(16, BUFFER.STORAGE | BUFFER.COPY_SRC)
185
+ let useSource = 1, from = source
186
+ if (!source) { useSource = 0; from = pooled(16, BUFFER.STORAGE) }
187
+ dispatch(pipelines.gate, layouts.gate, [uniform([sourceIndex, useSource, constant, 0]), bad, from, out], 1)
188
+ copy(out, 0, target, targetWord * 4, 4)
189
+ return out
190
+ }
191
+ const dispatchArgumentsFrom = (count) => {
192
+ const argumentsBuffer = pooled(16, BUFFER.STORAGE | BUFFER.INDIRECT)
193
+ dispatch(pipelines.dispatchArguments, layouts.args, [uniform([WORKGROUP_SIZE, 0, 0, 0]), count, argumentsBuffer], 1)
194
+ return argumentsBuffer
195
+ }
196
+ const submit = async () => {
197
+ if (encoder) { device.queue.submit([encoder.finish()]); encoder = null }
198
+ await device.queue.onSubmittedWorkDone()
199
+ }
200
+ // `wordCount` u32 of a buffer, handed to `consume` while the staging buffer is mapped (no second copy)
201
+ const readWords = async (source, wordCount, consume) => {
202
+ const bytes = Math.max(4, wordCount * 4)
203
+ const staging = device.createBuffer({ size: Math.ceil(bytes / 16) * 16, usage: BUFFER.MAP_READ | BUFFER.COPY_DST })
204
+ const readEncoder = device.createCommandEncoder()
205
+ readEncoder.copyBufferToBuffer(source, 0, staging, 0, Math.ceil(bytes / 4) * 4)
206
+ device.queue.submit([readEncoder.finish()])
207
+ await staging.mapAsync(MAP_MODE_READ)
208
+ const result = consume(new Uint32Array(staging.getMappedRange(), 0, wordCount))
209
+ staging.unmap(); staging.destroy()
210
+ return result
211
+ }
212
+
213
+ // One run at one event capacity. Resolves to the summary; the pieces are copied into piecesTarget(pieceCount).
214
+ const run = async (input, eventsPerSegment, piecesTarget) => {
215
+ const segmentCount = input.segmentCount, cellCount = input.cellCount, polygonCount = input.polygonCount, layerCount = input.layerCount
216
+ const cellEntryCount = Math.max(1, input.cellEntryCount)
217
+ const eventCapacity = Math.max(SMALLEST_EVENT_CAPACITY, segmentCount * eventsPerSegment), pairCapacity = Math.ceil(eventCapacity / 2)
218
+ const pieceCapacity = segmentCount + eventCapacity, vertexCapacity = segmentCount + pairCapacity
219
+ const largestBuffer = 2 ** Math.ceil(Math.log2(Math.max(pieceCapacity * 32, eventCapacity * 32)))
220
+ if (largestBuffer > device.limits.maxStorageBufferBindingSize || largestBuffer > device.limits.maxBufferSize)
221
+ return { ok: false, reason: `a ${largestBuffer} byte buffer is over this device's limit`, overflow: false }
222
+ const started = performance.now()
223
+
224
+ // ---- upload: one start point per segment and the per-layer and per-polygon tables
225
+ const segmentParameters = uniform([segmentCount, cellCount, CELL_SIZE, 0]), eventParameters = uniform([segmentCount, cellCount, CELL_SIZE, 1])
226
+ const points = pooled(segmentCount * 8, BUFFER.STORAGE), polygonLayers = pooled(polygonCount * 4, BUFFER.STORAGE)
227
+ const layers = pooled(layerCount * 16, BUFFER.STORAGE), polygons = pooled(polygonCount * 16, BUFFER.STORAGE)
228
+ // an array the kernel built (its heap, written from the host) or one already on this device (contour_front_gpu.js,
229
+ // copied inside this run's encoder, so it is read in submission order)
230
+ const upload = (target, source, wordCount) => {
231
+ if (source.gpuBuffer) { copy(source.gpuBuffer, source.offset ?? 0, target, 0, wordCount * 4); return }
232
+ device.queue.writeBuffer(target, 0, source, 0, wordCount)
233
+ }
234
+ upload(points, input.points, segmentCount * 2)
235
+ upload(polygonLayers, input.polyLayer, polygonCount)
236
+ upload(layers, input.layerInfo, layerCount * 4)
237
+ upload(polygons, input.polyInfo, polygonCount * 4)
238
+ const segments = pooled(segmentCount * 16, BUFFER.STORAGE), segmentInfo = pooled(segmentCount * 16, BUFFER.STORAGE)
239
+ const segmentPolygon = pooled(segmentCount * 4, BUFFER.STORAGE)
240
+ const unused = [0, 1, 2, 3, 4].map(() => pooled(16, BUFFER.STORAGE)) // bindings a pass does not write in its counting mode
241
+ const bad = pooled(16, BUFFER.STORAGE | BUFFER.COPY_SRC) // [0]: a range check failed
242
+ clear(bad)
243
+ dispatch(pipelines.expand, layouts.expand, [uniform([segmentCount, polygonCount, 0, 0]), points, polygons, polygonLayers, segments, segmentInfo, segmentPolygon], segmentCount)
244
+
245
+ // ---- bin: every segment into the grid cells its box covers
246
+ const cellStarts = pooled((cellCount + 1) * 4, BUFFER.STORAGE | BUFFER.COPY_SRC)
247
+ clear(cellStarts)
248
+ dispatch(pipelines.cellCount, layouts.bin, [segmentParameters, segments, segmentInfo, layers, cellStarts, unused[0], unused[1]], segmentCount)
249
+ scan(cellStarts, cellCount + 1)
250
+ checkRanges(cellStarts, cellCount, RANGE_LIMIT, bad)
251
+ gate(bad, null, 0, eventParameters, 1, cellCount)
252
+ gate(bad, null, 0, segmentParameters, 1, cellCount) // the intersect passes loop over cells: 0 cells once a cell range is bad
253
+ const cellCursor = pooled((cellCount + 1) * 4, BUFFER.STORAGE), cellList = pooled(cellEntryCount * 8, BUFFER.STORAGE)
254
+ clear(cellCursor)
255
+ dispatch(pipelines.cellScatter, layouts.bin, [segmentParameters, segments, segmentInfo, layers, cellCursor, cellStarts, cellList], segmentCount)
256
+
257
+ // ---- crossings: count per segment, scan, then write
258
+ const eventStarts = pooled((segmentCount + 1) * 4, BUFFER.STORAGE | BUFFER.COPY_SRC)
259
+ const countingFlags = pooled(16, BUFFER.STORAGE), flags = pooled(16, BUFFER.STORAGE | BUFFER.COPY_SRC) // flags[0]: crossing pairs
260
+ clear(flags); clear(countingFlags); clear(eventStarts)
261
+ dispatch(pipelines.intersect, layouts.intersect, [segmentParameters, segments, segmentInfo, layers, cellStarts, cellList, eventStarts, unused[0], unused[2], countingFlags, unused[1]], cellEntryCount)
262
+ scan(eventStarts, segmentCount + 1)
263
+ checkRanges(eventStarts, segmentCount, RANGE_LIMIT, bad)
264
+ const countingWinding = uniform([segmentCount, polygonCount, FILL_NONZERO, 0]), writingWinding = uniform([segmentCount, polygonCount, FILL_NONZERO, 1])
265
+ const tieParameters = uniform([segmentCount, 0, 0, 0])
266
+ // every kernel that loops over event ranges: 0 segments once one is bad
267
+ for (const target of [eventParameters, countingWinding, writingWinding, tieParameters]) gate(bad, null, 0, target, 0, segmentCount)
268
+ const eventCursor = pooled((segmentCount + 1) * 4, BUFFER.STORAGE)
269
+ clear(eventCursor)
270
+ const events = pooled(eventCapacity * 32, BUFFER.STORAGE), pairs = pooled(pairCapacity * 8, BUFFER.STORAGE)
271
+ dispatch(pipelines.intersect, layouts.intersect, [eventParameters, segments, segmentInfo, layers, cellStarts, cellList, eventCursor, events, eventStarts, flags, pairs], cellEntryCount)
272
+ dispatch(pipelines.sortEvents, layouts.sort, [eventParameters, segments, segmentInfo, layers, eventStarts, events, pairs], segmentCount)
273
+ const ties = pooled((SMALLEST_EVENT_CAPACITY + 1) * 4, BUFFER.STORAGE | BUFFER.COPY_SRC) // [0]: segments holding two events at one place
274
+ clear(ties)
275
+ dispatch(pipelines.ties, layouts.ties, [tieParameters, eventStarts, events, ties], segmentCount)
276
+
277
+ // ---- winding: a ray per polygon, the deltas along it, then the pieces
278
+ const work = pooled((segmentCount + 1) * 4, BUFFER.STORAGE), polygonWinding = pooled(polygonCount * 4, BUFFER.STORAGE)
279
+ const glue = [eventParameters, segments, segmentInfo, layers, countingWinding, eventStarts, events, polygons, work, polygonWinding]
280
+ dispatch(pipelines.rays, layouts.glue, glue, polygonCount)
281
+ dispatch(pipelines.deltaSums, layouts.glue, glue, segmentCount + 1)
282
+ scan(work, segmentCount + 1)
283
+ const polygonsWithWinding = pooled(polygonCount * 16, BUFFER.STORAGE)
284
+ dispatch(pipelines.polygonWinding, layouts.polygonWinding, [uniform([polygonCount, 0, 0, 0]), polygons, polygonWinding, polygonsWithWinding], polygonCount)
285
+ const leftWinding = pooled(segmentCount * 8, BUFFER.STORAGE)
286
+ dispatch(pipelines.zip, layouts.zip, [uniform([segmentCount, 0, 0, 0]), work, segmentPolygon, leftWinding], segmentCount)
287
+ const pieceStarts = pooled((segmentCount + 1) * 4, BUFFER.STORAGE | BUFFER.COPY_SRC)
288
+ clear(pieceStarts)
289
+ const pieceInputs = [eventParameters, segments, segmentInfo, layers]
290
+ dispatch(pipelines.pieces, layouts.pieces, [...pieceInputs, countingWinding, eventStarts, events, leftWinding, polygonsWithWinding, pieceStarts, unused[3], unused[4]], segmentCount)
291
+ scan(pieceStarts, segmentCount + 1)
292
+ const pieceVertices = pooled(pieceCapacity * 16, BUFFER.STORAGE), pieceCoordinates = pooled(pieceCapacity * 32, BUFFER.STORAGE)
293
+ dispatch(pipelines.pieces, layouts.pieces, [...pieceInputs, writingWinding, eventStarts, events, leftWinding, polygonsWithWinding, pieceStarts, pieceVertices, pieceCoordinates], segmentCount)
294
+
295
+ // ---- link: each piece's successor at the vertex it ends in (the piece count lives on the GPU from here on)
296
+ const pieceCount = pooled(16, BUFFER.STORAGE | BUFFER.COPY_SRC)
297
+ copy(pieceStarts, segmentCount * 4, pieceCount, 0, 4)
298
+ const pieceCountUniform = uniform([0, 0, 0, 0])
299
+ copy(pieceCount, 0, pieceCountUniform, 0, 4)
300
+ const perPiece = dispatchArgumentsFrom(pieceCount)
301
+ const directions = pooled(pieceCapacity * 16, BUFFER.STORAGE)
302
+ dispatchIndirect(pipelines.directions, layouts.directions, [pieceCountUniform, pieceCoordinates, directions], perPiece)
303
+ const linkParameters = uniform([0, vertexCapacity, 0, 0])
304
+ copy(pieceCount, 0, linkParameters, 0, 4)
305
+ const vertexStarts = pooled((vertexCapacity + 1) * 4, BUFFER.STORAGE), vertexCursor = pooled((vertexCapacity + 1) * 4, BUFFER.STORAGE | BUFFER.COPY_SRC)
306
+ const vertexList = pooled(pieceCapacity * 4, BUFFER.STORAGE), successors = pooled(pieceCapacity * 4, BUFFER.STORAGE)
307
+ const labels = pooled(pieceCapacity * 8, BUFFER.STORAGE), labelsSpare = pooled(pieceCapacity * 8, BUFFER.STORAGE) // bound by the link kernels, not read here
308
+ clear(vertexCursor); clear(vertexStarts)
309
+ dispatchIndirect(pipelines.vertexCount, layouts.link, [linkParameters, pieceVertices, directions, vertexStarts, unused[2], vertexList, successors, labels, labelsSpare], perPiece)
310
+ scan(vertexStarts, vertexCapacity + 1)
311
+ checkRanges(vertexStarts, vertexCapacity, VERTEX_DEGREE_LIMIT, bad)
312
+ const gatedPieceCount = gate(bad, pieceCount, 0, linkParameters, 0) // 0 once any range was bad: the link kernels then do nothing
313
+ const perGatedPiece = dispatchArgumentsFrom(gatedPieceCount)
314
+ const link = [linkParameters, pieceVertices, directions, vertexCursor, vertexStarts, vertexList, successors, labels, labelsSpare]
315
+ dispatchIndirect(pipelines.vertexScatter, layouts.link, link, perGatedPiece)
316
+ dispatchIndirect(pipelines.successor, layouts.link, link, perGatedPiece)
317
+
318
+ // ---- what the kernel reads: 16 bytes a piece
319
+ const compact = pooled(pieceCapacity * 16, BUFFER.STORAGE | BUFFER.COPY_SRC)
320
+ dispatchIndirect(pipelines.compact, layouts.compact, [pieceCountUniform, pieceCoordinates, successors, pieceVertices, compact], perPiece)
321
+
322
+ // ---- one readback of every count the host needs
323
+ const summaryBuffer = pooled(32, BUFFER.STORAGE | BUFFER.COPY_SRC)
324
+ copy(pieceCount, 0, summaryBuffer, 0, 4); copy(eventStarts, segmentCount * 4, summaryBuffer, 4, 4); copy(flags, 0, summaryBuffer, 8, 4)
325
+ copy(ties, 0, summaryBuffer, 12, 4); copy(bad, 0, summaryBuffer, 16, 4); copy(vertexCursor, vertexCapacity * 4, summaryBuffer, 20, 4)
326
+ await submit()
327
+ const [pieces, eventCount, pairCount, tiedSegments, badRanges, overDegree] = await readWords(summaryBuffer, 6, words => Array.from(words))
328
+ const summary = { pieceCount: pieces, eventCount, pairCount, tiedSegments, eventsPerSegment }
329
+ if (eventCount > eventCapacity || pairCount > pairCapacity) {
330
+ release()
331
+ return { ok: false, overflow: true, reason: `${eventCount} crossings over the ${eventCapacity} the buffers held`, ...summary }
332
+ }
333
+ if (badRanges > 0 || overDegree > 0 || pieces > pieceCapacity) {
334
+ release()
335
+ return { ok: false, overflow: false, reason: `the pipeline stopped itself: ${badRanges} ranges out of bounds, ${overDegree} vertices over the degree limit`, ...summary }
336
+ }
337
+ const target = piecesTarget(pieces)
338
+ await readWords(compact, pieces * 4, words => target.set(new Int32Array(words.buffer, words.byteOffset, words.length)))
339
+ release()
340
+ return { ok: true, ms: performance.now() - started, ...summary }
341
+ }
342
+
343
+ /**
344
+ * The union of every layer of `input` (the kernel's contour_gpu_input(): counts plus views into its heap).
345
+ * @param {(pieceCount: number) => Int32Array} piecesTarget where the pieces go, 4 x i32 each (the kernel's
346
+ * contour_result_buffer)
347
+ * @returns {Promise<{ok: boolean, reason?: string, ms?: number, pieceCount?: number, eventCount?: number}>}
348
+ */
349
+ const union = async (input, piecesTarget) => {
350
+ if (input.segmentCount === 0 || input.polygonCount === 0) { piecesTarget(0); return { ok: true, ms: 0, pieceCount: 0, eventCount: 0, pairCount: 0, tiedSegments: 0 } }
351
+ let outcome = null
352
+ for (const eventsPerSegment of EVENTS_PER_SEGMENT) {
353
+ outcome = await run(input, eventsPerSegment, piecesTarget)
354
+ if (outcome.ok || !outcome.overflow) return outcome
355
+ }
356
+ return outcome
357
+ }
358
+
359
+ const front = await makeContourFront(device)
360
+ const dispose = () => {
361
+ release()
362
+ for (const buffers of pool.values()) for (const buffer of buffers) buffer.destroy()
363
+ pool.clear()
364
+ front.dispose()
365
+ }
366
+ return { union, front, dispose }
367
+ }