@playcanvas/splat-transform 2.0.5 → 2.0.6

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/dist/cli.mjs CHANGED
@@ -17494,11 +17494,11 @@ class CompressedChunk {
17494
17494
  /**
17495
17495
  * The splat-transform version (semver MAJOR.MINOR.PATCH).
17496
17496
  */
17497
- const version = '2.0.5';
17497
+ const version = '2.0.6';
17498
17498
  /**
17499
17499
  * The splat-transform revision (short Git hash of HEAD at build time).
17500
17500
  */
17501
- const revision = '1fc5209';
17501
+ const revision = 'aa3dbae';
17502
17502
 
17503
17503
  const generatedByString = `Generated by splat-transform ${version}`;
17504
17504
  const chunkProps = [
@@ -19486,13 +19486,17 @@ class GpuDilation {
19486
19486
  // Extract: clear bufferA, dispatch extract from sparse src into bufferA.
19487
19487
  this.dispatchClear(slot, slot.bufferA, numWords);
19488
19488
  this.dispatchExtract(slot, minBx, minBy, minBz, outerBx, outerBy, outerBz, numXWords);
19489
- // Tiny throwaway readback forces a queue submit between extract
19490
- // and dilate. WITHOUT this, atomicOr writes from the extract pass
19491
- // aren't reliably visible to the next dilate pass (apparent
19492
- // missing memory barrier in PlayCanvas/Dawn for cross-pass atomic
19493
- // writes). Promise is intentionally not awaitedonly the
19494
- // implicit submit+barrier matters. 16 bytes negligible PCIe.
19495
- slot.bufferA.read(0, 16, null, true).catch(() => { });
19489
+ // Force a queue submission boundary between the extract pass (which
19490
+ // writes bufferA via atomicOr bound as `storage, read_write` with
19491
+ // `array<atomic<u32>>`) and the dilate-X pass (which reads bufferA
19492
+ // bound as `storage, read` with `array<u32>`). Without this, the
19493
+ // atomic writes are not reliably visible to the next pass dilation
19494
+ // produces empty output. Other inter-pass transitions in this
19495
+ // pipeline (clear→extract, dilateX→Z→Y, Y→compact) rely on automatic
19496
+ // intra-encoder synchronization without issue. Verified raw Dawn
19497
+ // synchronizes this case correctly, so the bug is somewhere in
19498
+ // PlayCanvas's compute dispatch / bind-group path.
19499
+ this.device.submit();
19496
19500
  // X-pass: A → B
19497
19501
  this.dispatchX(slot, slot.bufferA, slot.bufferB, numXWords, outerNy, outerNz, halfExtentXZ);
19498
19502
  // Z-pass: B → A
@@ -20907,8 +20911,16 @@ const binIndices = (parent, lod) => {
20907
20911
  */
20908
20912
  const writeLod = async (options, fs) => {
20909
20913
  const { filename, iterations, createDevice, chunkCount, chunkExtent } = options;
20910
- const dataTable = convertToSpace(options.dataTable, Transform.IDENTITY, true);
20911
- const envDataTable = options.envDataTable ? convertToSpace(options.envDataTable, Transform.IDENTITY, true) : null;
20914
+ // Operate in PLY space so per-leaf bounds in tree.bound are in the same
20915
+ // coordinate frame as the SOG chunk data emitted by writeSog (which also
20916
+ // converts to Transform.PLY). Without this, view-dependent streaming
20917
+ // built on tree.bound picks the wrong chunks because the bounds are
20918
+ // 180°-Z-rotated relative to the splat positions inside them.
20919
+ // This intentionally mutates the input tables to avoid doubling peak
20920
+ // memory during LOD export. Callers should treat writeLod as consuming its
20921
+ // DataTable inputs.
20922
+ const dataTable = convertToSpace(options.dataTable, Transform.PLY, true);
20923
+ const envDataTable = options.envDataTable ? convertToSpace(options.envDataTable, Transform.PLY, true) : null;
20912
20924
  const outputDir = dirname(filename);
20913
20925
  // ensure top-level output folder exists
20914
20926
  await fs.mkdir(outputDir);
@@ -26420,8 +26432,14 @@ const voxelizeToBuffer = async (bvh, gpuVoxelization, gridBounds, voxelResolutio
26420
26432
  const buffer = new BlockMaskBuffer();
26421
26433
  const bStride = numBlocksX * numBlocksY;
26422
26434
  const batchSize = 16;
26423
- const MEGA_MAX_BATCHES = 512;
26424
- const MEGA_MAX_INDICES = 4 * 1024 * 1024;
26435
+ // Caps for one mega-flush. Sized to keep a single compute dispatch's
26436
+ // wall-clock comfortably under Windows D3D12 TDR (~2s watchdog) on slower
26437
+ // GPUs without materially affecting throughput on fast GPUs (the
26438
+ // double-buffered slots keep the GPU fed across the extra flushes).
26439
+ // Output is bit-identical regardless: each batch's full Gaussian list rides
26440
+ // with it, and slot buffers grow on demand to accommodate any oversize batch.
26441
+ const MEGA_MAX_BATCHES = 256;
26442
+ const MEGA_MAX_INDICES = 2 * 1024 * 1024;
26425
26443
  const maxBlocks = GpuVoxelization.MAX_BLOCKS_PER_BATCH;
26426
26444
  const numSlots = GpuVoxelization.NUM_SLOTS;
26427
26445
  const slotIndexArrays = [];
@@ -28227,21 +28245,32 @@ const cropToNavigable = (grid, gridBounds, voxelResolution) => {
28227
28245
  return { grid, gridBounds };
28228
28246
  }
28229
28247
  const { minBx, minBy, minBz, maxBx, maxBy, maxBz } = navBounds;
28230
- const cropMaxBx = maxBx + 1;
28231
- const cropMaxBy = maxBy + 1;
28232
- const cropMaxBz = maxBz + 1;
28233
- if (minBx === 0 && minBy === 0 && minBz === 0 &&
28248
+ // Pad by 1 block on each side so the cropped grid retains the solid wall
28249
+ // blocks immediately surrounding the navigable cavity. Matches the
28250
+ // MARGIN = 1 pattern used by carve() before this re-crop strips it. The
28251
+ // collision-mesh extractors treat out-of-grid as empty, so without this
28252
+ // padding the mesh has holes wherever the cavity reaches the cropped
28253
+ // boundary; with it, the mesh extractor sees a real SOLID→EMPTY
28254
+ // transition at the cavity edge and emits a sealed wall there.
28255
+ const MARGIN = 1;
28256
+ const cropMinBx = Math.max(0, minBx - MARGIN);
28257
+ const cropMinBy = Math.max(0, minBy - MARGIN);
28258
+ const cropMinBz = Math.max(0, minBz - MARGIN);
28259
+ const cropMaxBx = Math.min(nbx, maxBx + 1 + MARGIN);
28260
+ const cropMaxBy = Math.min(nby, maxBy + 1 + MARGIN);
28261
+ const cropMaxBz = Math.min(nbz, maxBz + 1 + MARGIN);
28262
+ if (cropMinBx === 0 && cropMinBy === 0 && cropMinBz === 0 &&
28234
28263
  cropMaxBx === nbx && cropMaxBy === nby && cropMaxBz === nbz) {
28235
28264
  return { grid, gridBounds };
28236
28265
  }
28237
28266
  const cropBar = logger.bar('Cropping grid', grid.types.length);
28238
- const croppedGrid = grid.cropTo(minBx, minBy, minBz, cropMaxBx, cropMaxBy, cropMaxBz, done => cropBar.update(done));
28267
+ const croppedGrid = grid.cropTo(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz, done => cropBar.update(done));
28239
28268
  cropBar.end();
28240
28269
  const blockSize = 4 * voxelResolution;
28241
- const croppedMin = new Vec3(gridBounds.min.x + minBx * blockSize, gridBounds.min.y + minBy * blockSize, gridBounds.min.z + minBz * blockSize);
28270
+ const croppedMin = new Vec3(gridBounds.min.x + cropMinBx * blockSize, gridBounds.min.y + cropMinBy * blockSize, gridBounds.min.z + cropMinBz * blockSize);
28242
28271
  const croppedBounds = {
28243
28272
  min: croppedMin,
28244
- max: new Vec3(croppedMin.x + (cropMaxBx - minBx) * blockSize, croppedMin.y + (cropMaxBy - minBy) * blockSize, croppedMin.z + (cropMaxBz - minBz) * blockSize)
28273
+ max: new Vec3(croppedMin.x + (cropMaxBx - cropMinBx) * blockSize, croppedMin.y + (cropMaxBy - cropMinBy) * blockSize, croppedMin.z + (cropMaxBz - cropMinBz) * blockSize)
28245
28274
  };
28246
28275
  return { grid: croppedGrid, gridBounds: croppedBounds };
28247
28276
  };
@@ -28368,13 +28397,27 @@ const writeVoxel = async (options, fs) => {
28368
28397
  gpuVoxelization = new GpuVoxelization(device);
28369
28398
  gpuVoxelization.uploadAllGaussians(pcDataTable, extentsResult.extents);
28370
28399
  // Align grid bounds to block boundaries BEFORE voxelization so the
28371
- // block coordinates used during voxelization match what the reader expects.
28372
- // When fillExterior runs, pad by halfExtent + 1 voxels per side so the
28373
- // boundary-face flood seeds survive the dilation (notably below the floor).
28400
+ // block coordinates used during voxelization match what the reader
28401
+ // expects. fillExterior and fillFloor both need a margin of empty
28402
+ // voxels outside the splat's tight 3-sigma extents to do their job:
28403
+ // fillExterior so the boundary-face flood seeds survive its dilation
28404
+ // (notably below the floor), fillFloor so its column walk has empty
28405
+ // XZ columns to convert into wall pillars and the dilation halo to
28406
+ // extend the floor footprint outward.
28407
+ //
28408
+ // Lateral pad combines both as `dilation_radius + 1` voxels per side.
28409
+ // Vertical pad is only contributed by exteriorPad — fillFloor's
28410
+ // dilation is XZ-only, and Y padding would extend the wall pillars
28411
+ // above the splat's natural ceiling and below its floor.
28374
28412
  const exteriorPad = hasFillExterior ?
28375
28413
  (Math.ceil(navExteriorRadius / voxelResolution) + 1) * voxelResolution :
28376
28414
  0;
28377
- let gridBounds = alignGridBounds(bounds.min.x - exteriorPad, bounds.min.y - exteriorPad, bounds.min.z - exteriorPad, bounds.max.x + exteriorPad, bounds.max.y + exteriorPad, bounds.max.z + exteriorPad, voxelResolution);
28415
+ const floorPad = hasFloorFill ?
28416
+ (Math.ceil(floorFillDilation / voxelResolution) + 1) * voxelResolution :
28417
+ 0;
28418
+ const padXZ = Math.max(exteriorPad, floorPad);
28419
+ const padY = exteriorPad;
28420
+ let gridBounds = alignGridBounds(bounds.min.x - padXZ, bounds.min.y - padY, bounds.min.z - padXZ, bounds.max.x + padXZ, bounds.max.y + padY, bounds.max.z + padXZ, voxelResolution);
28378
28421
  const buffer = await voxelizeToBuffer(bvh, gpuVoxelization, gridBounds, voxelResolution, opacityCutoff);
28379
28422
  bvh = null;
28380
28423
  pcDataTable = null;
@@ -29051,6 +29094,17 @@ const createDevice = async (adapterName) => {
29051
29094
  stencil: false
29052
29095
  });
29053
29096
  await graphicsDevice.createDevice();
29097
+ // Surface Dawn's device-lost reason directly. PlayCanvas logs this only via
29098
+ // Debug.warn (debug builds) and then attempts to recreate the device, which
29099
+ // can fail noisily (e.g. DXGI_ERROR_DEVICE_REMOVED on D3D12 TDR) and bury
29100
+ // the original cause. Skip the `destroyed` reason — that fires on intentional
29101
+ // device.destroy() during normal shutdown.
29102
+ // @ts-ignore - wgpu is private on WebgpuGraphicsDevice but exposed in practice
29103
+ graphicsDevice.wgpu?.lost?.then((info) => {
29104
+ if (info?.reason === 'destroyed')
29105
+ return;
29106
+ logger.error(`WebGPU device was lost: reason=${info.reason || 'unknown'}, message=${info.message || '(none)'}`);
29107
+ });
29054
29108
  return graphicsDevice;
29055
29109
  };
29056
29110