three-slicer 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -0
- package/README.md +6 -0
- package/data/config-schema-lean.json +1 -1
- package/data/config-schema.json +6 -34
- package/data/filaments.js +1 -1
- package/data/printers.json +1 -1
- package/data/processes.js +1 -1
- package/engine/PARAMS.md +86 -8
- package/engine/src/contour_front_gpu.js +291 -0
- package/engine/src/contour_front_shaders.js +449 -0
- package/engine/src/contour_gpu.js +367 -0
- package/engine/src/contour_gpu_shaders.js +450 -0
- package/engine/src/contour_slice.js +131 -0
- package/engine/src/settings.js +25 -13
- package/engine/src/slicer.worker.js +50 -4
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/engine/src/slicer_core.mt.wasm +0 -0
- package/package.json +2 -2
- package/types/data/filaments.js.d.ts +6 -1
- package/types/data/printers.json.d.ts +13 -3
- package/types/data/processes.js.d.ts +6 -1
- package/types/settings.d.ts +16 -1
- package/types/worker.d.ts +4 -1
|
@@ -0,0 +1,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
|
+
}
|