@gridspace/raster-path 1.0.6 ā 1.0.7
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/build/app.js +151 -12
- package/build/index.html +6 -2
- package/build/raster-path.js +118 -9
- package/build/raster-worker.js +421 -7
- package/package.json +4 -2
- package/src/core/path-radial.js +0 -1
- package/src/core/path-tracing.js +492 -0
- package/src/core/raster-config.js +13 -4
- package/src/core/raster-path.js +118 -9
- package/src/core/raster-worker.js +41 -0
- package/src/core/workload-calibrate.js +57 -3
- package/src/shaders/tracing-toolpath.wgsl +95 -0
- package/src/test/tracing-test.cjs +307 -0
- package/src/web/app.js +151 -12
- package/src/web/index.html +6 -2
package/src/core/raster-path.js
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
/**
|
|
46
46
|
* Configuration options for RasterPath
|
|
47
47
|
* @typedef {Object} RasterPathConfig
|
|
48
|
-
* @property {'planar'|'radial'} mode - Rasterization mode (default: 'planar')
|
|
48
|
+
* @property {'planar'|'radial'|'tracing'} mode - Rasterization mode (default: 'planar')
|
|
49
49
|
* @property {boolean} autoTiling - Automatically tile large datasets (default: true)
|
|
50
50
|
* @property {number} gpuMemorySafetyMargin - Safety margin as percentage (default: 0.8 = 80%)
|
|
51
51
|
* @property {number} maxGPUMemoryMB - Maximum GPU memory per tile (default: 256MB)
|
|
@@ -80,8 +80,8 @@ export class RasterPath {
|
|
|
80
80
|
|
|
81
81
|
// Validate mode
|
|
82
82
|
const mode = config.mode || 'planar';
|
|
83
|
-
if (mode !== 'planar' && mode !== 'radial') {
|
|
84
|
-
throw new Error(`Invalid mode: ${mode}. Must be 'planar' or '
|
|
83
|
+
if (mode !== 'planar' && mode !== 'radial' && mode !== 'tracing') {
|
|
84
|
+
throw new Error(`Invalid mode: ${mode}. Must be 'planar', 'radial', or 'tracing'`);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// Validate rotationStep for radial mode
|
|
@@ -221,8 +221,8 @@ export class RasterPath {
|
|
|
221
221
|
throw new Error('RasterPath not initialized. Call init() first.');
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
if (this.mode === 'planar') {
|
|
225
|
-
// Planar: rasterize and return
|
|
224
|
+
if (this.mode === 'planar' || this.mode === 'tracing') {
|
|
225
|
+
// Planar/Tracing: rasterize and return (tracing reuses planar terrain rasterization)
|
|
226
226
|
const terrainData = await this.#rasterizePlanar({ triangles, zFloor, boundsOverride, isForTool: false, onProgress });
|
|
227
227
|
this.terrainData = terrainData;
|
|
228
228
|
return terrainData;
|
|
@@ -269,7 +269,7 @@ export class RasterPath {
|
|
|
269
269
|
* @param {function} params.onProgress - Optional progress callback (progress: number, info?: string) => void
|
|
270
270
|
* @returns {Promise<object>} Planar: {pathData, width, height} | Radial: {strips[], numStrips, totalPoints}
|
|
271
271
|
*/
|
|
272
|
-
async generateToolpaths({ xStep, yStep, zFloor, onProgress }) {
|
|
272
|
+
async generateToolpaths({ xStep, yStep, zFloor, onProgress, paths, step }) {
|
|
273
273
|
if (!this.isInitialized) {
|
|
274
274
|
throw new Error('RasterPath not initialized. Call init() first.');
|
|
275
275
|
}
|
|
@@ -278,7 +278,7 @@ export class RasterPath {
|
|
|
278
278
|
throw new Error('Tool not loaded. Call loadTool() first.');
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
debug.log('gen.paths', { xStep, yStep, zFloor });
|
|
281
|
+
debug.log('gen.paths', { xStep, yStep, zFloor, paths: paths?.length, step });
|
|
282
282
|
|
|
283
283
|
if (this.mode === 'planar') {
|
|
284
284
|
if (!this.terrainData) {
|
|
@@ -292,7 +292,7 @@ export class RasterPath {
|
|
|
292
292
|
zFloor,
|
|
293
293
|
onProgress
|
|
294
294
|
});
|
|
295
|
-
} else {
|
|
295
|
+
} else if (this.mode === 'radial') {
|
|
296
296
|
// Radial mode: use stored triangles
|
|
297
297
|
if (!this.terrainTriangles) {
|
|
298
298
|
throw new Error('Terrain not loaded. Call loadTerrain() first.');
|
|
@@ -306,14 +306,83 @@ export class RasterPath {
|
|
|
306
306
|
zFloor: zFloor ?? this.terrainZFloor,
|
|
307
307
|
onProgress
|
|
308
308
|
});
|
|
309
|
+
} else if (this.mode === 'tracing') {
|
|
310
|
+
// Tracing mode: follow input paths
|
|
311
|
+
if (!this.terrainData) {
|
|
312
|
+
throw new Error('Terrain not loaded. Call loadTerrain() first.');
|
|
313
|
+
}
|
|
314
|
+
if (!paths || paths.length === 0) {
|
|
315
|
+
throw new Error('Tracing mode requires paths parameter (array of Float32Array XY coordinates)');
|
|
316
|
+
}
|
|
317
|
+
if (!step || step <= 0) {
|
|
318
|
+
throw new Error('Tracing mode requires step parameter (sampling resolution in world units)');
|
|
319
|
+
}
|
|
320
|
+
return this.#generateToolpathsTracing({
|
|
321
|
+
paths,
|
|
322
|
+
step,
|
|
323
|
+
zFloor,
|
|
324
|
+
onProgress
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Create reusable GPU buffers for tracing mode (optimization for iterative tracing)
|
|
331
|
+
* Call this after loadTerrain() and loadTool() to cache buffers across multiple trace calls
|
|
332
|
+
* @returns {Promise<void>}
|
|
333
|
+
*/
|
|
334
|
+
async createTracingBuffers() {
|
|
335
|
+
if (this.mode !== 'tracing') {
|
|
336
|
+
throw new Error('createTracingBuffers() only available in tracing mode');
|
|
309
337
|
}
|
|
338
|
+
if (!this.terrainData || !this.toolData) {
|
|
339
|
+
throw new Error('Must call loadTerrain() and loadTool() before createTracingBuffers()');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return new Promise((resolve, reject) => {
|
|
343
|
+
const handler = () => resolve();
|
|
344
|
+
this.#sendMessage(
|
|
345
|
+
'create-tracing-buffers',
|
|
346
|
+
{
|
|
347
|
+
terrainPositions: this.terrainData.positions,
|
|
348
|
+
toolPositions: this.toolData.positions
|
|
349
|
+
},
|
|
350
|
+
'tracing-buffers-created',
|
|
351
|
+
handler
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Destroy reusable tracing buffers
|
|
358
|
+
* @returns {Promise<void>}
|
|
359
|
+
*/
|
|
360
|
+
async destroyTracingBuffers() {
|
|
361
|
+
if (this.mode !== 'tracing') {
|
|
362
|
+
return; // No-op for non-tracing modes
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
const handler = () => resolve();
|
|
367
|
+
this.#sendMessage(
|
|
368
|
+
'destroy-tracing-buffers',
|
|
369
|
+
{},
|
|
370
|
+
'tracing-buffers-destroyed',
|
|
371
|
+
handler
|
|
372
|
+
);
|
|
373
|
+
});
|
|
310
374
|
}
|
|
311
375
|
|
|
312
376
|
/**
|
|
313
377
|
* Terminate worker and cleanup resources
|
|
314
378
|
*/
|
|
315
|
-
terminate() {
|
|
379
|
+
async terminate() {
|
|
316
380
|
if (this.worker) {
|
|
381
|
+
// Cleanup tracing buffers if in tracing mode
|
|
382
|
+
if (this.mode === 'tracing') {
|
|
383
|
+
await this.destroyTracingBuffers();
|
|
384
|
+
}
|
|
385
|
+
|
|
317
386
|
this.worker.terminate();
|
|
318
387
|
this.worker = null;
|
|
319
388
|
this.isInitialized = false;
|
|
@@ -388,6 +457,46 @@ export class RasterPath {
|
|
|
388
457
|
});
|
|
389
458
|
}
|
|
390
459
|
|
|
460
|
+
async #generateToolpathsTracing({ paths, step, zFloor, onProgress }) {
|
|
461
|
+
return new Promise((resolve, reject) => {
|
|
462
|
+
// Set up progress handler if callback provided
|
|
463
|
+
if (onProgress) {
|
|
464
|
+
const progressHandler = (data) => {
|
|
465
|
+
onProgress(data.percent, { current: data.current, total: data.total, pathIndex: data.pathIndex });
|
|
466
|
+
};
|
|
467
|
+
this.messageHandlers.set('tracing-progress', progressHandler);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const handler = (data) => {
|
|
471
|
+
// Clean up progress handler
|
|
472
|
+
if (onProgress) {
|
|
473
|
+
this.messageHandlers.delete('tracing-progress');
|
|
474
|
+
}
|
|
475
|
+
resolve(data);
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
this.#sendMessage(
|
|
479
|
+
'tracing-generate-toolpaths',
|
|
480
|
+
{
|
|
481
|
+
paths,
|
|
482
|
+
terrainPositions: this.terrainData.positions,
|
|
483
|
+
terrainData: {
|
|
484
|
+
width: this.terrainData.gridWidth,
|
|
485
|
+
height: this.terrainData.gridHeight,
|
|
486
|
+
bounds: this.terrainData.bounds
|
|
487
|
+
},
|
|
488
|
+
toolPositions: this.toolData.positions,
|
|
489
|
+
step,
|
|
490
|
+
gridStep: this.resolution,
|
|
491
|
+
terrainBounds: this.terrainData.bounds,
|
|
492
|
+
zFloor: zFloor ?? 0
|
|
493
|
+
},
|
|
494
|
+
'tracing-toolpaths-complete',
|
|
495
|
+
handler
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
391
500
|
async #generateToolpathsRadial({ triangles, bounds, toolData, xStep, yStep, zFloor, onProgress }) {
|
|
392
501
|
const maxRadius = this.#calculateMaxRadius(triangles);
|
|
393
502
|
|
|
@@ -54,6 +54,7 @@ import { initWebGPU, setConfig, updateConfig, deviceCapabilities, debug, device
|
|
|
54
54
|
import { rasterizeMesh } from './raster-planar.js';
|
|
55
55
|
import { generateToolpath } from './path-planar.js';
|
|
56
56
|
import { generateRadialToolpaths } from './path-radial.js';
|
|
57
|
+
import { generateTracingToolpaths, createReusableTracingBuffers, destroyReusableTracingBuffers } from './path-tracing.js';
|
|
57
58
|
import { calibrateGPU } from './workload-calibrate.js';
|
|
58
59
|
|
|
59
60
|
// Global error handler for uncaught errors in worker
|
|
@@ -127,6 +128,46 @@ self.onmessage = async function(e) {
|
|
|
127
128
|
}, toolpathTransferBuffers);
|
|
128
129
|
break;
|
|
129
130
|
|
|
131
|
+
case 'tracing-generate-toolpaths':
|
|
132
|
+
const tracingResult = await generateTracingToolpaths({
|
|
133
|
+
paths: data.paths,
|
|
134
|
+
terrainPositions: data.terrainPositions,
|
|
135
|
+
terrainData: data.terrainData,
|
|
136
|
+
toolPositions: data.toolPositions,
|
|
137
|
+
step: data.step,
|
|
138
|
+
gridStep: data.gridStep,
|
|
139
|
+
terrainBounds: data.terrainBounds,
|
|
140
|
+
zFloor: data.zFloor,
|
|
141
|
+
onProgress: (progressData) => {
|
|
142
|
+
self.postMessage({
|
|
143
|
+
type: 'tracing-progress',
|
|
144
|
+
data: progressData.data
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const tracingTransferBuffers = tracingResult.paths.map(p => p.buffer);
|
|
149
|
+
self.postMessage({
|
|
150
|
+
type: 'tracing-toolpaths-complete',
|
|
151
|
+
data: tracingResult
|
|
152
|
+
}, tracingTransferBuffers);
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case 'create-tracing-buffers':
|
|
156
|
+
createReusableTracingBuffers(data.terrainPositions, data.toolPositions);
|
|
157
|
+
self.postMessage({
|
|
158
|
+
type: 'tracing-buffers-created',
|
|
159
|
+
data: { success: true }
|
|
160
|
+
});
|
|
161
|
+
break;
|
|
162
|
+
|
|
163
|
+
case 'destroy-tracing-buffers':
|
|
164
|
+
destroyReusableTracingBuffers();
|
|
165
|
+
self.postMessage({
|
|
166
|
+
type: 'tracing-buffers-destroyed',
|
|
167
|
+
data: { success: true }
|
|
168
|
+
});
|
|
169
|
+
break;
|
|
170
|
+
|
|
130
171
|
case 'calibrate':
|
|
131
172
|
const calibrationResult = await calibrateGPU(device, data?.options || {});
|
|
132
173
|
self.postMessage({
|
|
@@ -114,18 +114,72 @@ async function testWorkloadDispatch(device, pipeline, workgroupSize, triangleTes
|
|
|
114
114
|
passEncoder.dispatchWorkgroups(dispatchX, dispatchY, 1);
|
|
115
115
|
passEncoder.end();
|
|
116
116
|
|
|
117
|
-
//
|
|
117
|
+
// Submit the compute work
|
|
118
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
119
|
+
|
|
120
|
+
// TEST: Queue progress checkpoints while GPU works
|
|
121
|
+
const numCheckpoints = 5;
|
|
122
|
+
const checkpointInterval = 100; // ms
|
|
123
|
+
const progressSnapshots = [];
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < numCheckpoints; i++) {
|
|
126
|
+
await new Promise(resolve => setTimeout(resolve, checkpointInterval));
|
|
127
|
+
|
|
128
|
+
const checkpointStart = performance.now();
|
|
129
|
+
|
|
130
|
+
// Queue a copy to read progress
|
|
131
|
+
const checkpointEncoder = device.createCommandEncoder();
|
|
132
|
+
const checkpointStaging = device.createBuffer({
|
|
133
|
+
size: totalThreads * 4,
|
|
134
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
checkpointEncoder.copyBufferToBuffer(completionBuffer, 0, checkpointStaging, 0, totalThreads * 4);
|
|
138
|
+
device.queue.submit([checkpointEncoder.finish()]);
|
|
139
|
+
|
|
140
|
+
// Wait for this checkpoint copy to complete
|
|
141
|
+
await checkpointStaging.mapAsync(GPUMapMode.READ);
|
|
142
|
+
const checkpointElapsed = performance.now() - checkpointStart;
|
|
143
|
+
|
|
144
|
+
// Count completed threads
|
|
145
|
+
const checkpointData = new Uint32Array(checkpointStaging.getMappedRange());
|
|
146
|
+
let completedThreads = 0;
|
|
147
|
+
for (let j = 0; j < totalThreads; j++) {
|
|
148
|
+
if (checkpointData[j] === 1) completedThreads++;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
progressSnapshots.push({
|
|
152
|
+
checkpoint: i + 1,
|
|
153
|
+
timeMs: Math.round(performance.now() - startTime),
|
|
154
|
+
completedThreads,
|
|
155
|
+
totalThreads,
|
|
156
|
+
percentComplete: Math.round((completedThreads / totalThreads) * 100),
|
|
157
|
+
checkpointLatencyMs: checkpointElapsed.toFixed(2)
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
checkpointStaging.unmap();
|
|
161
|
+
checkpointStaging.destroy();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Final readback
|
|
118
165
|
const stagingBuffer = device.createBuffer({
|
|
119
166
|
size: totalThreads * 4,
|
|
120
167
|
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
121
168
|
});
|
|
122
169
|
|
|
123
|
-
|
|
124
|
-
|
|
170
|
+
const finalEncoder = device.createCommandEncoder();
|
|
171
|
+
finalEncoder.copyBufferToBuffer(completionBuffer, 0, stagingBuffer, 0, totalThreads * 4);
|
|
172
|
+
device.queue.submit([finalEncoder.finish()]);
|
|
125
173
|
|
|
126
174
|
await device.queue.onSubmittedWorkDone();
|
|
127
175
|
const elapsed = performance.now() - startTime;
|
|
128
176
|
|
|
177
|
+
// Log progress snapshots
|
|
178
|
+
if (progressSnapshots.length > 0) {
|
|
179
|
+
console.log('\nš Progress Checkpoints:');
|
|
180
|
+
console.table(progressSnapshots);
|
|
181
|
+
}
|
|
182
|
+
|
|
129
183
|
await stagingBuffer.mapAsync(GPUMapMode.READ);
|
|
130
184
|
const completionData = new Uint32Array(stagingBuffer.getMappedRange());
|
|
131
185
|
const completionCopy = new Uint32Array(completionData);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Tracing toolpath generation
|
|
2
|
+
// Follows input polylines and calculates Z-depth at each sampled point
|
|
3
|
+
// Sentinel value for empty terrain cells (must match rasterize shader)
|
|
4
|
+
const EMPTY_CELL: f32 = -1e10;
|
|
5
|
+
const MAX_F32: f32 = 3.402823466e+38;
|
|
6
|
+
|
|
7
|
+
struct SparseToolPoint {
|
|
8
|
+
x_offset: i32,
|
|
9
|
+
y_offset: i32,
|
|
10
|
+
z_value: f32,
|
|
11
|
+
padding: f32,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
struct Uniforms {
|
|
15
|
+
terrain_width: u32,
|
|
16
|
+
terrain_height: u32,
|
|
17
|
+
tool_count: u32,
|
|
18
|
+
point_count: u32, // Number of sampled points to process
|
|
19
|
+
path_index: u32, // Index of current path being processed
|
|
20
|
+
terrain_min_x: f32, // Terrain bounding box (world coordinates)
|
|
21
|
+
terrain_min_y: f32,
|
|
22
|
+
grid_step: f32, // Resolution of terrain rasterization
|
|
23
|
+
oob_z: f32, // Z value for out-of-bounds points (zFloor)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@group(0) @binding(0) var<storage, read> terrain_map: array<f32>;
|
|
27
|
+
@group(0) @binding(1) var<storage, read> sparse_tool: array<SparseToolPoint>;
|
|
28
|
+
@group(0) @binding(2) var<storage, read> input_points: array<f32>; // XY pairs
|
|
29
|
+
@group(0) @binding(3) var<storage, read_write> output_depths: array<f32>; // Z values
|
|
30
|
+
@group(0) @binding(4) var<storage, read_write> max_z_buffer: array<atomic<i32>>; // Max Z per path (as bits)
|
|
31
|
+
@group(0) @binding(5) var<uniform> uniforms: Uniforms;
|
|
32
|
+
|
|
33
|
+
@compute @workgroup_size(64, 1, 1)
|
|
34
|
+
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
35
|
+
let point_idx = global_id.x;
|
|
36
|
+
|
|
37
|
+
if (point_idx >= uniforms.point_count) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Read input X,Y world coordinates
|
|
42
|
+
let world_x = input_points[point_idx * 2u + 0u];
|
|
43
|
+
let world_y = input_points[point_idx * 2u + 1u];
|
|
44
|
+
|
|
45
|
+
// Convert world coordinates to grid coordinates
|
|
46
|
+
let grid_x_f32 = (world_x - uniforms.terrain_min_x) / uniforms.grid_step;
|
|
47
|
+
let grid_y_f32 = (world_y - uniforms.terrain_min_y) / uniforms.grid_step;
|
|
48
|
+
let tool_center_x = i32(grid_x_f32);
|
|
49
|
+
let tool_center_y = i32(grid_y_f32);
|
|
50
|
+
|
|
51
|
+
// Check if tool center is outside terrain bounds
|
|
52
|
+
let center_oob = tool_center_x < 0 || tool_center_x >= i32(uniforms.terrain_width) ||
|
|
53
|
+
tool_center_y < 0 || tool_center_y >= i32(uniforms.terrain_height);
|
|
54
|
+
|
|
55
|
+
var max_collision_z = uniforms.oob_z;
|
|
56
|
+
var found_collision = false;
|
|
57
|
+
|
|
58
|
+
// Test each tool point for collision with terrain
|
|
59
|
+
for (var i = 0u; i < uniforms.tool_count; i++) {
|
|
60
|
+
let tool_point = sparse_tool[i];
|
|
61
|
+
let terrain_x = tool_center_x + tool_point.x_offset;
|
|
62
|
+
let terrain_y = tool_center_y + tool_point.y_offset;
|
|
63
|
+
|
|
64
|
+
// Bounds check: terrain sample must be within terrain grid
|
|
65
|
+
if (terrain_x < 0 || terrain_x >= i32(uniforms.terrain_width) ||
|
|
66
|
+
terrain_y < 0 || terrain_y >= i32(uniforms.terrain_height)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let terrain_idx = u32(terrain_y) * uniforms.terrain_width + u32(terrain_x);
|
|
71
|
+
let terrain_z = terrain_map[terrain_idx];
|
|
72
|
+
|
|
73
|
+
// Check if terrain cell has geometry (not empty sentinel value)
|
|
74
|
+
if (terrain_z > EMPTY_CELL + 1.0) {
|
|
75
|
+
// Tool z_value is positive offset from tip (tip=0, shaft=+50)
|
|
76
|
+
// Add to terrain height to find where tool center needs to be
|
|
77
|
+
let collision_z = terrain_z + tool_point.z_value;
|
|
78
|
+
max_collision_z = max(max_collision_z, collision_z);
|
|
79
|
+
found_collision = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If no collision found and center was in-bounds, use oob_z
|
|
84
|
+
var output_z = uniforms.oob_z;
|
|
85
|
+
if (found_collision) {
|
|
86
|
+
output_z = max_collision_z;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
output_depths[point_idx] = output_z;
|
|
90
|
+
|
|
91
|
+
// Update max Z for this path using atomic operation
|
|
92
|
+
// Convert float to int bits for atomic comparison
|
|
93
|
+
let z_bits = bitcast<i32>(output_z);
|
|
94
|
+
atomicMax(&max_z_buffer[uniforms.path_index], z_bits);
|
|
95
|
+
}
|