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.
- package/CHANGELOG.md +171 -0
- package/README.md +6 -0
- package/data/filaments.js +1 -1
- package/data/printers.json +1 -1
- package/data/processes.js +1 -1
- package/engine/PARAMS.md +37 -7
- 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 +22 -12
- package/engine/src/slicer.worker.js +100 -45
- 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 +10 -3
- package/types/data/processes.js.d.ts +6 -1
- package/types/settings.d.ts +2 -0
- package/types/worker.d.ts +4 -1
package/engine/PARAMS.md
CHANGED
|
@@ -8,8 +8,8 @@ cannot drift from what `slice()` actually honours. Regenerate with `node types/g
|
|
|
8
8
|
|
|
9
9
|
<!-- KERNEL-PARAMS:BEGIN — generated by types/gen_kernel_params.mjs, do not edit by hand -->
|
|
10
10
|
|
|
11
|
-
The kernel accepts **
|
|
12
|
-
OrcaSlicer settings map; the remaining
|
|
11
|
+
The kernel accepts **189 parameters**. `deriveKernelParams()` produces 160 of them from the
|
|
12
|
+
OrcaSlicer settings map; the remaining 29 have no schema key behind them and can only be set by writing
|
|
13
13
|
the params object directly — those rows show `—` in the *From setting* column.
|
|
14
14
|
|
|
15
15
|
A parameter left out of the object keeps the default listed here, so sending a partial object is normal and is
|
|
@@ -46,11 +46,13 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
|
|
|
46
46
|
| `bottom_shell_layers` | `number` | `3` | `bottom_shell_layers` | top_shell_layers/bottom_shell_layers |
|
|
47
47
|
| `infill_angle` | `number` | `45.0` | `infill_direction` | infill_direction |
|
|
48
48
|
| `infill_density` | `number` | `0.15` | `sparse_infill_density` | |
|
|
49
|
+
| `infill_wall_overlap` | `number` | `15.0` | `infill_wall_overlap` | infill_wall_overlap (percent) |
|
|
49
50
|
| `ironing_flow` | `number` | `10.0` | `ironing_flow` | ironing flow (%) |
|
|
50
51
|
| `ironing_spacing` | `number` | `0.1` | `ironing_spacing` | ironing line spacing (mm) |
|
|
51
52
|
| `ironing_speed` | `number` | `30.0` | `ironing_speed` | ironing speed (mm/s) |
|
|
52
53
|
| `ironing_type` | `string` | `'none'` | `ironing_type` | none\|top\|topmost\|solid (the top family = on) |
|
|
53
54
|
| `sparse_infill_filament_id` | `number` | `0` | `sparse_infill_filament_id` | |
|
|
55
|
+
| `sparse_infill_flow_ratio` | `number` | `1.0` | `sparse_infill_flow_ratio` | |
|
|
54
56
|
| `sparse_infill_pattern` | `string` | `'rectilinear'` | `sparse_infill_pattern` | rectilinear\|grid\|triangles\|zigzag\|gyroid |
|
|
55
57
|
| `top_shell_layers` | `number` | `4` | `top_shell_layers` | top_shell_layers/bottom_shell_layers |
|
|
56
58
|
|
|
@@ -103,6 +105,7 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
|
|
|
103
105
|
|
|
104
106
|
| Kernel param | Type | Default | From setting | Notes |
|
|
105
107
|
| --- | --- | --- | --- | --- |
|
|
108
|
+
| `brim_flow_ratio` | `?` | — | `brim_flow_ratio` | undefined |
|
|
106
109
|
| `brim_object_gap` | `number` | `0.0` | `brim_object_gap` | brim_object_gap (mm): gap between brim and object |
|
|
107
110
|
| `brim_width` | `number` | `0.0` | `brim_width` | skirt_distance / brim_width |
|
|
108
111
|
| `raft_contact_distance` | `number` | `0.1` | `raft_contact_distance` | raft_contact_distance (mm) |
|
|
@@ -127,9 +130,11 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
|
|
|
127
130
|
| `support_density` | `number` | `0.15` | — | density of the support body |
|
|
128
131
|
| `support_expansion` | `number` | `0.0` | `support_expansion` | support_expansion (mm): expands the overhang region |
|
|
129
132
|
| `support_filament` | `number` | `0` | `support_filament` | filament for the support base (and, upstream, the raft) |
|
|
133
|
+
| `support_flow_ratio` | `?` | — | `support_flow_ratio` | undefined |
|
|
130
134
|
| `support_grid_snap` | `boolean` | `true` | — | equivalent of the upstream SupportGridPattern (default behavior of the grid style) |
|
|
131
135
|
| `support_interface_bottom_layers` | `number` | `0` | `support_interface_bottom_layers` | support_interface_bottom_layers (0 = none) |
|
|
132
136
|
| `support_interface_filament` | `number` | `0` | `support_interface_filament` | filament for the support interface |
|
|
137
|
+
| `support_interface_flow_ratio` | `?` | — | `support_interface_flow_ratio` | undefined |
|
|
133
138
|
| `support_interface_pattern` | `string` | `'auto'` | `support_interface_pattern` | support_interface_pattern: auto\|rectilinear\|concentric\|rectilinear_interlaced\|grid |
|
|
134
139
|
| `support_interface_spacing` | `number` | `0.5` | `support_interface_spacing` | support_interface_spacing (mm, 0=solid). Upstream default 0.5 |
|
|
135
140
|
| `support_interface_top_layers` | `number` | `2` | `support_interface_top_layers` | support_interface_top_layers |
|
|
@@ -144,7 +149,7 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
|
|
|
144
149
|
| `support_xy_distance` | `number` | `0.35` | `support_object_xy_distance` | support_object_xy_distance |
|
|
145
150
|
| `tree_lite_min_radius` | `number` | `1.5` | — | tree_lite taper constants (our own approximation — no matching upstream key) |
|
|
146
151
|
| `tree_lite_shrink` | `number` | `0.5` | — | tree_lite taper constants (our own approximation — no matching upstream key) |
|
|
147
|
-
| `tree_style` | `string` | `'organic'` |
|
|
152
|
+
| `tree_style` | `string` | `'organic'` | `support_style` | organic\|slim\|strong\|hybrid (upstream support_style smsTree*) |
|
|
148
153
|
| `tree_support_angle_slow` | `number` | `25.0` | `tree_support_angle_slow` | tree_support_angle_slow (deg) |
|
|
149
154
|
| `tree_support_branch_angle` | `number` | `40.0` | `tree_support_branch_angle` | fallback spelling — `tree_support_branch_angle_organic` wins when both are sent |
|
|
150
155
|
| `tree_support_branch_angle_organic` | `number` | `40.0` | `tree_support_branch_angle_organic` | tree_support_branch_angle_organic (deg) |
|
|
@@ -190,7 +195,11 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
|
|
|
190
195
|
|
|
191
196
|
| Kernel param | Type | Default | From setting | Notes |
|
|
192
197
|
| --- | --- | --- | --- | --- |
|
|
198
|
+
| `alternate_extra_wall` | `boolean` | `false` | `alternate_extra_wall` | alternate_extra_wall |
|
|
193
199
|
| `arachne_dump` | `boolean` | `false` | — | temporary diagnostic: dump the PASS1 arachne input polygons to stderr |
|
|
200
|
+
| `bottom_solid_infill_flow_ratio` | `number` | `1.0` | `bottom_solid_infill_flow_ratio` | |
|
|
201
|
+
| `bridge_flow` | `number` | `1.0` | `bridge_flow` | |
|
|
202
|
+
| `detect_thin_wall` | `boolean` | `false` | `detect_thin_wall` | detect_thin_wall: medial-axis thin walls where one loop does not fit |
|
|
194
203
|
| `economy` | `boolean` | `false` | — | |
|
|
195
204
|
| `emit_pe_tags` | `boolean` | `false` | — | emit PE tags on extrusion runs (enabled automatically when the real PE is used). Default false (backwards compatible) |
|
|
196
205
|
| `extruder_count` | `number` | `1` | — | multi-material: how many extruders are used (1\|2) |
|
|
@@ -201,27 +210,48 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
|
|
|
201
210
|
| `extruder_retract_speed` | `?` | `[]` | `filament_retraction_speed` | undefined |
|
|
202
211
|
| `extruder_z_hop` | `?` | `[]` | `filament_z_hop` | undefined |
|
|
203
212
|
| `extrusion_rate_slope_segment_length` | `number` | `1.0` | — | segment split length for the real PE (mm) |
|
|
213
|
+
| `filament_max_volumetric_speed` | `number[]` | `[]` | `filament_max_volumetric_speed` | mm³/s, per filament; empty = no cap (GW::capped_feed) |
|
|
214
|
+
| `filter_out_gap_fill` | `number` | `0.0` | `filter_out_gap_fill` | filter_out_gap_fill (mm): gap fill shorter than this is dropped |
|
|
215
|
+
| `first_layer_flow_ratio` | `?` | — | `first_layer_flow_ratio` | undefined |
|
|
204
216
|
| `flush_into_infill` | `boolean` | `false` | `flush_into_infill` | |
|
|
205
217
|
| `flush_multiplier` | `number` | `1.0` | `flush_multiplier` | upstream scales the whole table by this |
|
|
206
218
|
| `flush_volumes_matrix` | `number[]` | `[]` | `flush_volumes_matrix` | |
|
|
219
|
+
| `gap_fill_flow_ratio` | `?` | — | `gap_fill_flow_ratio` | undefined |
|
|
220
|
+
| `gap_infill_speed` | `number[]` | `[]` | `gap_infill_speed` | gap_infill_speed (mm/s, per extruder): a first entry of 0 turns gap fill off (upstream has_gap_fill); empty = the schema's 30 |
|
|
207
221
|
| `gcode_config_block` | `boolean` | `false` | — | ; CONFIG_BLOCK_START … the parameters this slice ran with … END |
|
|
222
|
+
| `gcode_role_tags` | `boolean` | `false` | — | ;TYPE:<role> on every role change (upstream GCodeProcessor tag) |
|
|
208
223
|
| `gcode_stats_block` | `boolean` | `false` | — | ; filament used [mm]/[cm3]/[g], cost, total filament change |
|
|
209
224
|
| `independent_support_layer_height` | `boolean` | `false` | `independent_support_layer_height` | false by default because of the kernel z grid constraint (gap quantized to layers) |
|
|
225
|
+
| `inner_wall_flow_ratio` | `number` | `1.0` | `inner_wall_flow_ratio` | |
|
|
226
|
+
| `internal_solid_infill_flow_ratio` | `?` | — | `internal_solid_infill_flow_ratio` | undefined |
|
|
210
227
|
| `keep_stages` | `boolean` | `false` | — | keep the stages cached after slicing (skips the early release — a memory trade-off) |
|
|
211
228
|
| `mm_group_split` | `number` | `0` | — | triangle group boundary index ([0,split)=T0, [split,N)=T1) |
|
|
212
229
|
| `mm_group_splits` | `number[]` | `[]` | — | |
|
|
213
230
|
| `mm_group_tools` | `number[]` | `[]` | — | |
|
|
231
|
+
| `only_one_wall_first_layer` | `boolean` | `false` | `only_one_wall_first_layer` | only_one_wall_first_layer |
|
|
232
|
+
| `outer_wall_flow_ratio` | `number` | `1.0` | `outer_wall_flow_ratio` | |
|
|
214
233
|
| `pe_external_perimeter_only` | `boolean` | `false` | — | real PE: smooth outer walls only |
|
|
215
234
|
| `pe_lite` | `boolean` | `true` | — | true = the effective PE-lite (default), false = the ported real PE (tagged g-code) |
|
|
216
235
|
| `pe_strip_tags` | `boolean` | `true` | — | strip the tags from the final output after real PE post-processing |
|
|
236
|
+
| `placeholder_config` | `string` | — | `filament_end_gcode`, `machine_end_gcode`, `machine_start_gcode` | |
|
|
237
|
+
| `precise_outer_wall` | `boolean` | `true` | `precise_outer_wall` | precise_outer_wall |
|
|
217
238
|
| `prime_tower_ring_size` | `number` | `15.0` | — | side length of the fallback square ring (mm) |
|
|
218
239
|
| `prime_tower_width` | `number` | `30.0` | `prime_tower_width` | width of the real WipeTower (mm). Separate from the square ring width (15). |
|
|
219
240
|
| `prime_tower_x` | `number` | `10.0` | `wipe_tower_x` | |
|
|
220
241
|
| `prime_tower_y` | `number` | `10.0` | `wipe_tower_y` | |
|
|
221
242
|
| `prime_volume` | `number` | `0.0` | `prime_volume` | extra volume primed on every change, on top of the pair's |
|
|
243
|
+
| `print_flow_ratio` | `number` | `1.0` | `print_flow_ratio` | |
|
|
222
244
|
| `reduce_crossing_wall` | `boolean` | `false` | `reduce_crossing_wall` | wall-avoiding travel |
|
|
223
245
|
| `reuse_stages` | `number` | `0` | — | |
|
|
246
|
+
| `scarf_joint_flow_ratio` | `?` | — | `scarf_joint_flow_ratio` | undefined |
|
|
247
|
+
| `set_other_flow_ratios` | `boolean` | `false` | `set_other_flow_ratios` | |
|
|
248
|
+
| `single_tool` | `number` | `0` | — | |
|
|
249
|
+
| `thick_bridges` | `boolean` | `false` | `thick_bridges` | |
|
|
224
250
|
| `time_engine` | `string` | `'full'` | — | |
|
|
251
|
+
| `top_bottom_infill_wall_overlap` | `number` | `25.0` | `top_bottom_infill_wall_overlap` | top_bottom_infill_wall_overlap (percent, first and topmost layer) |
|
|
252
|
+
| `top_solid_infill_flow_ratio` | `number` | `1.0` | `top_solid_infill_flow_ratio` | |
|
|
253
|
+
| `wall_direction` | `string` | `'ccw'` | `wall_direction` | wall_direction (ccw \| cw) |
|
|
254
|
+
| `wall_sequence` | `string` | `'inner wall/outer wall'` | `wall_sequence` | wall_sequence (inner wall/outer wall \| outer wall/inner wall \| inner-outer-inner wall) |
|
|
225
255
|
|
|
226
256
|
<!-- KERNEL-PARAMS:END -->
|
|
227
257
|
|
|
@@ -231,14 +261,14 @@ A `—` in *From setting* means there is no OrcaSlicer option behind that parame
|
|
|
231
261
|
in the params object yourself. They are not one kind of thing, and the difference matters before you reach for one:
|
|
232
262
|
|
|
233
263
|
- **Host runtime flags** — `economy`, `keep_stages`, `reuse_stages`, `time_engine`, `wipe_tower_real`,
|
|
234
|
-
`gcode_stats_block`, `gcode_config_block`. Decisions the *caller* makes about a particular run, not properties of
|
|
264
|
+
`gcode_stats_block`, `gcode_config_block`, `gcode_role_tags`. Decisions the *caller* makes about a particular run, not properties of
|
|
235
265
|
the print. `economy` is the last rung of an out-of-memory retry: it streams G-code and skips the preview
|
|
236
266
|
toolpaths and the time estimate, so a model that cannot finish any other way still finishes.
|
|
237
267
|
- **Values the viewer computes** — `extruder_count`, `mm_group_split`, `mm_group_splits`, `mm_group_tools`,
|
|
238
|
-
`auto_center`. These come from the scene (how many extruders are loaded, which triangle ranges belong
|
|
239
|
-
tool), so there is nothing for a settings map to hold. `<Viewport/>` fills them in at slice time.
|
|
268
|
+
`single_tool`, `auto_center`. These come from the scene (how many extruders are loaded, which triangle ranges belong
|
|
269
|
+
to which tool, the one filament a plate's objects all print with), so there is nothing for a settings map to hold. `<Viewport/>` fills them in at slice time.
|
|
240
270
|
- **Kernel-only tuning** — `support_density`, `support_auto`, `support_grid_snap`, `support_overhang_min_area`,
|
|
241
|
-
`
|
|
271
|
+
`tree_lite_shrink`, `tree_lite_min_radius`, `scarf_length`, `prime_tower_ring_size`,
|
|
242
272
|
`raft_first_layer_height`, `extrusion_rate_slope_segment_length`. Real knobs with no upstream option of the same
|
|
243
273
|
name — some are this kernel's own approximations (the `tree_lite_*` taper), others are upstream behaviour that
|
|
244
274
|
upstream never exposed as a setting.
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// The front of PASS1 on the GPU: the seated triangles and the layer planes in (the kernel's MESH phase,
|
|
2
|
+
// wasm-core/contour_phase.h), contour_gpu.js's union input out, in GPU buffers, so the contours never leave the GPU
|
|
3
|
+
// between the cut and the union.
|
|
4
|
+
//
|
|
5
|
+
// What runs here (WGSL: contour_front_shaders.js), every layer at once:
|
|
6
|
+
// cut per triangle, the layers it spans (exact z compares), each crossing in double-float, oriented as tri_plane
|
|
7
|
+
// cancel segments coinciding exactly in opposite directions removed in pairs (cancel_coincident), by a hash on place
|
|
8
|
+
// chain each end continues into the smallest unused segment starting at its 1e-3 mm key (chain_polys' greedy rule)
|
|
9
|
+
// loops pointer jumping for each loop's head and each segment's rank; tails that lead into a cycle stay out
|
|
10
|
+
// layout loops sorted by (layer, head), laid out as chain_polys' n + 1 points, write_polygon's duplicates dropped
|
|
11
|
+
// tables layer origins and grids, polygon ranges, the cell-list size
|
|
12
|
+
//
|
|
13
|
+
// Measured (design report gpu-full-port-trial-2026-10-01.md section 3): 3.0M triangles, 420 layers in 30 ms (the kernel's
|
|
14
|
+
// own capture: 410 ms st, 71 ms mt); 0.24 % of the edges differ from the kernel's by its double rounding a shared
|
|
15
|
+
// mesh-edge crossing to 1e-6 mm two ways, which the 48-bit double-float does not reproduce.
|
|
16
|
+
//
|
|
17
|
+
// A layer with a chain that does not close is marked (openLayers): the kernel cuts, chains and unions that layer itself
|
|
18
|
+
// from the mesh it handed over (contour_phase::assemble), so the slice never depends on the GPU closing every loop.
|
|
19
|
+
import {
|
|
20
|
+
CUT_SHADER, CANCEL_SHADER, CHAIN_SHADER, JUMP_SHADER, CYCLES_SHADER, LOOPS_SHADER, OUTPUT_SHADER, WRITE_SHADER,
|
|
21
|
+
TABLES_SHADER, ENTRIES_SHADER, OPEN_LAYERS_SHADER,
|
|
22
|
+
} from './contour_front_shaders.js'
|
|
23
|
+
import { SCAN_SHADER } from './contour_gpu_shaders.js'
|
|
24
|
+
|
|
25
|
+
// Numeric WebGPU constants: the named globals exist in a browser worker but not on a bare node globalThis.
|
|
26
|
+
const BUFFER = { MAP_READ: 0x1, COPY_SRC: 0x4, COPY_DST: 0x8, UNIFORM: 0x40, STORAGE: 0x80 }
|
|
27
|
+
const SHADER_STAGE_COMPUTE = 0x4
|
|
28
|
+
const MAP_MODE_READ = 0x1
|
|
29
|
+
const WORKGROUP_SIZE = 256
|
|
30
|
+
const MAX_WORKGROUPS_PER_DIMENSION = 65535
|
|
31
|
+
const CELL_SIZE = 2_000_000 // the union's grid cell in kernel units; contour_gpu.js and contour_phase.cpp use the same
|
|
32
|
+
const INT32_MAX = 0x7fffffff
|
|
33
|
+
const INT32_MIN = -0x80000000
|
|
34
|
+
|
|
35
|
+
const smallestPowerOfTwo = (count) => 2 ** Math.ceil(Math.log2(Math.max(2, count)))
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {GPUDevice} device the device contour_gpu.js runs on (its buffers feed the union directly)
|
|
39
|
+
* @returns {Promise<{ cut(triangles: Float32Array, planes: Float64Array): Promise<object>, dispose(): void }>}
|
|
40
|
+
*/
|
|
41
|
+
export async function makeContourFront(device) {
|
|
42
|
+
const BINDING_TYPE = { u: 'uniform', r: 'read-only-storage', w: 'storage' }
|
|
43
|
+
const layouts = new Map(), modules = new Map(), pipelines = new Map(), buffers = new Map()
|
|
44
|
+
let encoder = null, transient = []
|
|
45
|
+
|
|
46
|
+
const layoutOf = (kinds) => {
|
|
47
|
+
if (!layouts.has(kinds)) layouts.set(kinds, device.createBindGroupLayout({ entries: [...kinds].map((kind, binding) => (
|
|
48
|
+
{ binding, visibility: SHADER_STAGE_COMPUTE, buffer: { type: BINDING_TYPE[kind] } })) }))
|
|
49
|
+
return layouts.get(kinds)
|
|
50
|
+
}
|
|
51
|
+
const pipelineOf = (code, entryPoint, kinds) => {
|
|
52
|
+
const key = `${entryPoint}\u0000${kinds}\u0000${code}`
|
|
53
|
+
if (!pipelines.has(key)) {
|
|
54
|
+
if (!modules.has(code)) modules.set(code, device.createShaderModule({ code }))
|
|
55
|
+
pipelines.set(key, device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [layoutOf(kinds)] }),
|
|
56
|
+
compute: { module: modules.get(code), entryPoint } }))
|
|
57
|
+
}
|
|
58
|
+
return pipelines.get(key)
|
|
59
|
+
}
|
|
60
|
+
// compile every shader up front, so a WGSL error surfaces as a refusal instead of an empty result
|
|
61
|
+
for (const code of [CUT_SHADER, CANCEL_SHADER, CHAIN_SHADER, JUMP_SHADER, CYCLES_SHADER, LOOPS_SHADER, OUTPUT_SHADER, WRITE_SHADER, TABLES_SHADER, ENTRIES_SHADER, OPEN_LAYERS_SHADER, SCAN_SHADER]) {
|
|
62
|
+
const module = device.createShaderModule({ code })
|
|
63
|
+
modules.set(code, module)
|
|
64
|
+
const failure = (await module.getCompilationInfo()).messages.find(message => message.type === 'error')
|
|
65
|
+
if (failure) throw new Error(`contour front shader: line ${failure.lineNum}: ${failure.message}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// a storage buffer kept across slices by name; a regrown one is destroyed after the submit that last used it
|
|
69
|
+
const buffer = (name, bytes) => {
|
|
70
|
+
const size = Math.max(16, Math.ceil(bytes / 16) * 16)
|
|
71
|
+
const kept = buffers.get(name)
|
|
72
|
+
if (kept && kept.size >= size) return kept
|
|
73
|
+
if (kept) transient.push(kept)
|
|
74
|
+
const made = device.createBuffer({ size, usage: BUFFER.STORAGE | BUFFER.COPY_SRC | BUFFER.COPY_DST })
|
|
75
|
+
buffers.set(name, made)
|
|
76
|
+
return made
|
|
77
|
+
}
|
|
78
|
+
const encoderOf = () => { if (!encoder) encoder = device.createCommandEncoder(); return encoder }
|
|
79
|
+
const uniform = (values) => {
|
|
80
|
+
const made = device.createBuffer({ size: Math.max(16, values.length * 4), usage: BUFFER.UNIFORM | BUFFER.COPY_DST })
|
|
81
|
+
device.queue.writeBuffer(made, 0, new Uint32Array(values))
|
|
82
|
+
transient.push(made)
|
|
83
|
+
return made
|
|
84
|
+
}
|
|
85
|
+
const zero = device.createBuffer({ size: 16, usage: BUFFER.UNIFORM | BUFFER.COPY_DST }) // the double-float's `dfZero`
|
|
86
|
+
device.queue.writeBuffer(zero, 0, new Uint32Array(4))
|
|
87
|
+
const run = (code, entryPoint, kinds, bound, invocations) => {
|
|
88
|
+
if (invocations <= 0) return
|
|
89
|
+
let groupsX = Math.ceil(invocations / WORKGROUP_SIZE), groupsY = 1
|
|
90
|
+
if (groupsX > MAX_WORKGROUPS_PER_DIMENSION) { groupsY = Math.ceil(groupsX / MAX_WORKGROUPS_PER_DIMENSION); groupsX = MAX_WORKGROUPS_PER_DIMENSION }
|
|
91
|
+
const group = device.createBindGroup({ layout: layoutOf(kinds), entries: bound.map((target, binding) => ({ binding, resource: { buffer: target } })) })
|
|
92
|
+
const pass = encoderOf().beginComputePass()
|
|
93
|
+
pass.setPipeline(pipelineOf(code, entryPoint, kinds)); pass.setBindGroup(0, group); pass.dispatchWorkgroups(groupsX, groupsY); pass.end()
|
|
94
|
+
}
|
|
95
|
+
const clear = (target, bytes) => encoderOf().clearBuffer(target, 0, Math.ceil(bytes / 4) * 4)
|
|
96
|
+
const copy = (source, sourceOffset, target, targetOffset, bytes) => encoderOf().copyBufferToBuffer(source, sourceOffset, target, targetOffset, bytes)
|
|
97
|
+
const submit = async () => {
|
|
98
|
+
if (encoder) { device.queue.submit([encoder.finish()]); encoder = null }
|
|
99
|
+
await device.queue.onSubmittedWorkDone()
|
|
100
|
+
for (const item of transient) item.destroy()
|
|
101
|
+
transient = []
|
|
102
|
+
}
|
|
103
|
+
// `wordCount` 32-bit words from `offsetWords` on, copied out of the staging buffer
|
|
104
|
+
const readWords = async (source, wordCount, offsetWords = 0, Type = Uint32Array) => {
|
|
105
|
+
const bytes = Math.max(4, wordCount * 4)
|
|
106
|
+
const staging = device.createBuffer({ size: Math.ceil(bytes / 16) * 16, usage: BUFFER.MAP_READ | BUFFER.COPY_DST })
|
|
107
|
+
encoderOf().copyBufferToBuffer(source, offsetWords * 4, staging, 0, Math.ceil(bytes / 4) * 4)
|
|
108
|
+
await submit()
|
|
109
|
+
await staging.mapAsync(MAP_MODE_READ)
|
|
110
|
+
const words = new Type(staging.getMappedRange(0, wordCount * 4).slice(0))
|
|
111
|
+
staging.unmap(); staging.destroy()
|
|
112
|
+
return words
|
|
113
|
+
}
|
|
114
|
+
// in-place exclusive scan of `count` u32 (contour_gpu_shaders.js' scan)
|
|
115
|
+
const scan = (data, count, depth = 0) => {
|
|
116
|
+
const blocks = Math.ceil(count / WORKGROUP_SIZE)
|
|
117
|
+
const sums = buffer(`scan.${depth}`, (blocks + 1) * 4)
|
|
118
|
+
const parameters = uniform([count, 0, 0, 0])
|
|
119
|
+
run(SCAN_SHADER, 'scanBlock', 'uww', [parameters, data, sums], count)
|
|
120
|
+
if (blocks === 1) return
|
|
121
|
+
scan(sums, blocks, depth + 1)
|
|
122
|
+
run(SCAN_SHADER, 'addOff', 'uww', [parameters, data, sums], count)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// layers, triangles -> segments (with their chain keys), cancelled pairs, successors
|
|
126
|
+
async function segmentsOf(triangles, planes) {
|
|
127
|
+
const triangleCount = triangles.length / 9, layerCount = planes.length
|
|
128
|
+
const planePairs = new Float32Array(layerCount * 2) // each plane's z as hi + lo, for exact compares with f32 vertices
|
|
129
|
+
for (let layer = 0; layer < layerCount; layer++) {
|
|
130
|
+
const high = Math.fround(planes[layer])
|
|
131
|
+
planePairs[layer * 2] = high; planePairs[layer * 2 + 1] = Math.fround(planes[layer] - high)
|
|
132
|
+
}
|
|
133
|
+
const triangleBuffer = buffer('triangles', triangles.byteLength), planeBuffer = buffer('planes', planePairs.byteLength)
|
|
134
|
+
device.queue.writeBuffer(triangleBuffer, 0, triangles); device.queue.writeBuffer(planeBuffer, 0, planePairs)
|
|
135
|
+
const counts = buffer('counts', (triangleCount + 1) * 4)
|
|
136
|
+
clear(counts, (triangleCount + 1) * 4)
|
|
137
|
+
const unused = [1, 2, 3].map(index => buffer(`unused${index}`, 16))
|
|
138
|
+
run(CUT_SHADER, 'main', 'urrwwwwu', [uniform([triangleCount, layerCount, 0, 0]), triangleBuffer, planeBuffer, counts, ...unused, zero], triangleCount)
|
|
139
|
+
scan(counts, triangleCount + 1)
|
|
140
|
+
const [segmentCount] = await readWords(counts, 1, triangleCount) // the segment count sizes everything after
|
|
141
|
+
const segmentPoints = buffer('segmentPoints', segmentCount * 16), segmentKeys = buffer('segmentKeys', segmentCount * 16)
|
|
142
|
+
const segmentLayer = buffer('segmentLayer', segmentCount * 4)
|
|
143
|
+
run(CUT_SHADER, 'main', 'urrwwwwu', [uniform([triangleCount, layerCount, 1, 0]), triangleBuffer, planeBuffer, counts, segmentPoints, segmentKeys, segmentLayer, zero], triangleCount)
|
|
144
|
+
|
|
145
|
+
const tableSize = smallestPowerOfTwo(segmentCount * 2), mask = tableSize - 1
|
|
146
|
+
const dead = buffer('dead', segmentCount * 4)
|
|
147
|
+
const owner = buffer('owner', tableSize * 4), headA = buffer('headA', tableSize * 4), headB = buffer('headB', tableSize * 4)
|
|
148
|
+
const nextA = buffer('nextA', segmentCount * 4), nextB = buffer('nextB', segmentCount * 4)
|
|
149
|
+
clear(dead, segmentCount * 4); clear(owner, tableSize * 4); clear(headA, tableSize * 4); clear(headB, tableSize * 4)
|
|
150
|
+
const cancelBound = [uniform([segmentCount, mask, 0, 0]), segmentPoints, segmentLayer, owner, headA, headB, nextA, dead]
|
|
151
|
+
run(CANCEL_SHADER, 'insert', 'urrwwwww', cancelBound, segmentCount)
|
|
152
|
+
run(CANCEL_SHADER, 'pair', 'urrwwwww', cancelBound, tableSize)
|
|
153
|
+
clear(owner, tableSize * 4); clear(headA, tableSize * 4); clear(headB, tableSize * 4)
|
|
154
|
+
const successor = buffer('successor', (segmentCount + 2) * 4)
|
|
155
|
+
clear(successor, (segmentCount + 2) * 4)
|
|
156
|
+
const chainBound = [uniform([segmentCount, mask, 0, 0]), segmentKeys, segmentLayer, dead, owner, headA, headB, nextA, nextB, successor]
|
|
157
|
+
run(CHAIN_SHADER, 'insertStarts', 'urrrwwwwww', chainBound, segmentCount)
|
|
158
|
+
run(CHAIN_SHADER, 'insertEnds', 'urrrwwwwww', chainBound, segmentCount)
|
|
159
|
+
run(CHAIN_SHADER, 'pairEnds', 'urrrwwwwww', chainBound, tableSize)
|
|
160
|
+
return { layerCount, segmentCount, segmentPoints, segmentLayer, successor, dead }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// segments with successors -> the union's input (contour_gpu.js' layout), still on the GPU
|
|
164
|
+
async function loopsOf(segments) {
|
|
165
|
+
const { layerCount, segmentCount, segmentPoints, segmentLayer, successor, dead } = segments
|
|
166
|
+
// labels (each segment's loop head), then ranks (its distance to the loop's last segment); ping-pong buffers
|
|
167
|
+
const rounds = Math.ceil(Math.log2(Math.max(2, segmentCount))) + 1
|
|
168
|
+
const valueA = buffer('valueA', segmentCount * 4), nextOfA = buffer('nextOfA', segmentCount * 4)
|
|
169
|
+
const valueB = buffer('valueB', segmentCount * 4), nextOfB = buffer('nextOfB', segmentCount * 4)
|
|
170
|
+
const label = buffer('label', segmentCount * 4), rank = buffer('rank', segmentCount * 4), reach = buffer('reach', segmentCount * 4)
|
|
171
|
+
const jump = (mode, input, output) => run(JUMP_SHADER, 'main', 'urrwwwwrr',
|
|
172
|
+
[uniform([segmentCount, mode, 0, 0]), successor, dead, input[0], input[1], output[0], output[1], label, reach], segmentCount)
|
|
173
|
+
const jumpRounds = (mode, finalValue, finalNext) => {
|
|
174
|
+
let from = [valueA, nextOfA], to = [valueB, nextOfB]
|
|
175
|
+
for (let round = 0; round < rounds; round++) { jump(mode, from, to); [from, to] = [to, from] }
|
|
176
|
+
copy(from[0], 0, finalValue, 0, segmentCount * 4)
|
|
177
|
+
if (finalNext) copy(from[1], 0, finalNext, 0, segmentCount * 4)
|
|
178
|
+
}
|
|
179
|
+
jump(0, [valueB, nextOfB], [valueA, nextOfA]); jumpRounds(1, label, reach)
|
|
180
|
+
const hit = buffer('hit', segmentCount * 4)
|
|
181
|
+
clear(hit, segmentCount * 4)
|
|
182
|
+
run(CYCLES_SHADER, 'main', 'uww', [uniform([segmentCount, 0, 0, 0]), reach, hit], segmentCount)
|
|
183
|
+
run(CYCLES_SHADER, 'main', 'uww', [uniform([segmentCount, 1, 0, 0]), reach, hit], segmentCount)
|
|
184
|
+
const openLayer = buffer('openLayer', layerCount * 4)
|
|
185
|
+
clear(openLayer, layerCount * 4)
|
|
186
|
+
run(OPEN_LAYERS_SHADER, 'main', 'urrrw', [uniform([segmentCount, 0, 0, 0]), dead, reach, segmentLayer, openLayer], segmentCount)
|
|
187
|
+
jump(2, [valueB, nextOfB], [valueA, nextOfA]); jumpRounds(3, rank)
|
|
188
|
+
|
|
189
|
+
// heads, sorted by (layer, head) with a bitonic sort
|
|
190
|
+
const headFlag = buffer('headFlag', (segmentCount + 1) * 4)
|
|
191
|
+
clear(headFlag, (segmentCount + 1) * 4)
|
|
192
|
+
const loopBound = (parameters, heads, polygonOf, loopStart, order) => [parameters, successor, label, rank, segmentLayer, headFlag, heads, polygonOf, loopStart, order, reach]
|
|
193
|
+
const unused = [4, 5, 6, 7].map(index => buffer(`unused${index}`, 16))
|
|
194
|
+
run(LOOPS_SHADER, 'flagHeads', 'urrrrwwwwwr', loopBound(uniform([segmentCount, 0, layerCount, 0]), ...unused), segmentCount)
|
|
195
|
+
scan(headFlag, segmentCount + 1)
|
|
196
|
+
const [headCount] = await readWords(headFlag, 1, segmentCount) // the loop count sizes the sort
|
|
197
|
+
const padded = smallestPowerOfTwo(headCount)
|
|
198
|
+
const heads = buffer('heads', padded * 8), polygonOf = buffer('polygonOf', segmentCount * 4)
|
|
199
|
+
const loopStart = buffer('loopStart', (headCount + 1) * 4), order = buffer('order', (segmentCount + headCount) * 4)
|
|
200
|
+
const bound = (parameters) => loopBound(parameters, heads, polygonOf, loopStart, order)
|
|
201
|
+
run(LOOPS_SHADER, 'gatherHeads', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, 0])), segmentCount)
|
|
202
|
+
run(LOOPS_SHADER, 'padHeads', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, headCount])), padded - headCount)
|
|
203
|
+
for (let block = 2; block <= padded; block *= 2) for (let distance = block / 2; distance >= 1; distance /= 2)
|
|
204
|
+
run(LOOPS_SHADER, 'bitonic', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, distance, block])), padded)
|
|
205
|
+
clear(loopStart, (headCount + 1) * 4)
|
|
206
|
+
run(LOOPS_SHADER, 'lengths', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, headCount])), headCount)
|
|
207
|
+
scan(loopStart, headCount + 1)
|
|
208
|
+
const [slotCount] = await readWords(loopStart, 1, headCount) // points of the closed loops, chain_polys' n + 1 each
|
|
209
|
+
run(LOOPS_SHADER, 'place', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, headCount])), segmentCount)
|
|
210
|
+
|
|
211
|
+
// the points write_polygon keeps, the polygons chain_polys keeps, the layer boxes
|
|
212
|
+
const kept = buffer('kept', (slotCount + 1) * 4), polygonKept = buffer('polygonKept', (headCount + 1) * 4)
|
|
213
|
+
const layerBox = buffer('layerBox', layerCount * 32), slotPolygon = buffer('slotPolygon', slotCount * 4)
|
|
214
|
+
const boxStart = new Int32Array(layerCount * 8)
|
|
215
|
+
for (let layer = 0; layer < layerCount; layer++) boxStart.set([INT32_MAX, INT32_MAX, INT32_MIN, INT32_MIN], layer * 8)
|
|
216
|
+
device.queue.writeBuffer(layerBox, 0, boxStart)
|
|
217
|
+
clear(kept, (slotCount + 1) * 4)
|
|
218
|
+
const outputBound = [uniform([slotCount, headCount, layerCount, 0]), order, segmentPoints, segmentLayer, successor, loopStart, kept, polygonKept, layerBox, slotPolygon]
|
|
219
|
+
run(OUTPUT_SHADER, 'slotPolygons', 'urrrrrwwww', outputBound, headCount)
|
|
220
|
+
run(OUTPUT_SHADER, 'keep', 'urrrrrwwww', outputBound, slotCount)
|
|
221
|
+
run(OUTPUT_SHADER, 'polygonCounts', 'urrrrrwwww', outputBound, headCount)
|
|
222
|
+
run(OUTPUT_SHADER, 'keepValid', 'urrrrrwwww', outputBound, slotCount)
|
|
223
|
+
const polygonValidAt = buffer('polygonValidAt', (headCount + 1) * 4)
|
|
224
|
+
clear(polygonValidAt, (headCount + 1) * 4)
|
|
225
|
+
copy(polygonKept, 0, polygonValidAt, 0, headCount * 4)
|
|
226
|
+
scan(polygonValidAt, headCount + 1)
|
|
227
|
+
const keptAt = buffer('keptAt', (slotCount + 1) * 4)
|
|
228
|
+
clear(keptAt, (slotCount + 1) * 4)
|
|
229
|
+
copy(kept, 0, keptAt, 0, slotCount * 4)
|
|
230
|
+
scan(keptAt, slotCount + 1)
|
|
231
|
+
const [pointCount] = await readWords(keptAt, 1, slotCount)
|
|
232
|
+
const [polygonCount] = await readWords(polygonValidAt, 1, headCount)
|
|
233
|
+
const points = buffer('points', pointCount * 8), layerStart = buffer('layerStart', (layerCount + 1) * 4)
|
|
234
|
+
clear(layerStart, (layerCount + 1) * 4)
|
|
235
|
+
run(WRITE_SHADER, 'main', 'urrrrrrww', [uniform([slotCount, headCount, layerCount, 0]), order, segmentPoints, segmentLayer, kept, keptAt, layerBox, points, layerStart], slotCount)
|
|
236
|
+
scan(layerStart, layerCount + 1)
|
|
237
|
+
const layerInfo = buffer('layerInfo', (layerCount + 1) * 16), polygonInfo = buffer('polygonInfo', polygonCount * 20 + 16)
|
|
238
|
+
const tableBound = [uniform([slotCount, headCount, layerCount, CELL_SIZE]), layerBox, layerStart, layerInfo, polygonValidAt, loopStart, keptAt, order, segmentLayer, polygonInfo]
|
|
239
|
+
run(TABLES_SHADER, 'cells', 'urwwrrrrrw', tableBound, 1)
|
|
240
|
+
run(TABLES_SHADER, 'polygons', 'urwwrrrrrw', tableBound, headCount)
|
|
241
|
+
const entries = buffer('entries', 16)
|
|
242
|
+
clear(entries, 16)
|
|
243
|
+
run(ENTRIES_SHADER, 'main', 'urrw', [uniform([polygonCount, CELL_SIZE, 0, 0]), polygonInfo, points, entries], polygonCount)
|
|
244
|
+
const [cellCount] = await readWords(layerInfo, 1, layerCount * 4)
|
|
245
|
+
const [cellEntryCount] = await readWords(entries, 1)
|
|
246
|
+
const boxes = await readWords(layerBox, layerCount * 8, 0, Int32Array)
|
|
247
|
+
const openLayers = await readWords(openLayer, layerCount)
|
|
248
|
+
// the origin the kernel adds back to every piece; a layer with no loop has none, and the kernel's own is 0 there
|
|
249
|
+
const origins = new Int32Array(layerCount * 2)
|
|
250
|
+
for (let layer = 0; layer < layerCount; layer++) {
|
|
251
|
+
if (boxes[layer * 8 + 2] < boxes[layer * 8]) continue
|
|
252
|
+
origins[layer * 2] = boxes[layer * 8]; origins[layer * 2 + 1] = boxes[layer * 8 + 1]
|
|
253
|
+
}
|
|
254
|
+
return { layerCount, segmentCount: pointCount, polygonCount, cellCount, cellEntryCount, headCount, slotCount, origins, openLayers,
|
|
255
|
+
points, layerInfo, polygonInfo }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The union input of every layer of a mesh.
|
|
260
|
+
* @param {Float32Array} triangles 9 floats per triangle, seated (the kernel's contour_mesh().tris)
|
|
261
|
+
* @param {Float64Array} planes the z of every layer plane (contour_mesh().planes)
|
|
262
|
+
* @returns {Promise<object>} counts, `origins` (min x, min y per layer, kernel units), `openLayers` (1 per layer with a
|
|
263
|
+
* segment left out of every closed loop: that layer's input is incomplete and the kernel rebuilds it), `openSegments`
|
|
264
|
+
* and `input`, contour_gpu.js' union input with its arrays as GPU buffers
|
|
265
|
+
*/
|
|
266
|
+
const cut = async (triangles, planes) => {
|
|
267
|
+
const started = performance.now()
|
|
268
|
+
const segments = await segmentsOf(triangles, planes)
|
|
269
|
+
const [matcherOverflow] = await readWords(segments.successor, 1, segments.segmentCount)
|
|
270
|
+
const deadFlags = await readWords(segments.dead, segments.segmentCount)
|
|
271
|
+
let cancelled = 0
|
|
272
|
+
for (const flag of deadFlags) cancelled += flag
|
|
273
|
+
const laid = await loopsOf(segments)
|
|
274
|
+
const loopSegments = laid.slotCount - laid.headCount
|
|
275
|
+
return {
|
|
276
|
+
ms: performance.now() - started, triangleCount: triangles.length / 9, rawSegments: segments.segmentCount, cancelled,
|
|
277
|
+
openSegments: segments.segmentCount - cancelled - loopSegments, matcherOverflow, origins: laid.origins, openLayers: laid.openLayers,
|
|
278
|
+
input: { layerCount: laid.layerCount, segmentCount: laid.segmentCount, polygonCount: laid.polygonCount, cellCount: laid.cellCount,
|
|
279
|
+
cellEntryCount: laid.cellEntryCount, points: { gpuBuffer: laid.points }, layerInfo: { gpuBuffer: laid.layerInfo },
|
|
280
|
+
polyInfo: { gpuBuffer: laid.polygonInfo }, polyLayer: { gpuBuffer: laid.polygonInfo, offset: laid.polygonCount * 16 } },
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const dispose = () => {
|
|
284
|
+
for (const kept of buffers.values()) kept.destroy()
|
|
285
|
+
buffers.clear()
|
|
286
|
+
for (const item of transient) item.destroy()
|
|
287
|
+
transient = []
|
|
288
|
+
zero.destroy()
|
|
289
|
+
}
|
|
290
|
+
return { cut, dispose }
|
|
291
|
+
}
|