UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

1,035 lines 53.6 kB
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { Compute } from "../../platform/graphics/compute.js";
import { Shader } from "../../platform/graphics/shader.js";
import { StorageBuffer } from "../../platform/graphics/storage-buffer.js";
import { BindGroupFormat, BindStorageBufferFormat, BindUniformBufferFormat } from "../../platform/graphics/bind-group-format.js";
import { UniformBufferFormat, UniformFormat } from "../../platform/graphics/uniform-buffer-format.js";
import {
  BUFFERUSAGE_COPY_DST,
  BUFFERUSAGE_COPY_SRC,
  BUFFERUSAGE_INDIRECT,
  PIXELFORMAT_RGBA16U,
  SHADERLANGUAGE_WGSL,
  SHADERSTAGE_COMPUTE,
  UNIFORMTYPE_FLOAT,
  UNIFORMTYPE_MAT4,
  UNIFORMTYPE_UINT
} from "../../platform/graphics/constants.js";
import { GSPLAT_FORWARD, PROJECTION_ORTHOGRAPHIC, FOG_NONE, GSPLAT_DEBUG_HEATMAP } from "../constants.js";
import { Debug, DebugHelper } from "../../core/debug.js";
import { Color } from "../../core/math/color.js";
import { Mat4 } from "../../core/math/mat4.js";
import { Camera } from "../camera.js";
import { GSplatRenderer } from "./gsplat-renderer.js";
import { FramePassGSplatComputeLocal } from "./frame-pass-gsplat-compute-local.js";
import { computeGsplatLocalDispatchPrepSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-dispatch-prep.js";
import { computeGsplatLocalDispatchPrepLargeSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-dispatch-prep-large.js";
import { computeGsplatLocalTileCountSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-tile-count.js";
import { computeGsplatLocalTileCountLargeSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-tile-count-large.js";
import { computeGsplatLocalPlaceEntriesSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-place-entries.js";
import { computeGsplatLocalPlaceEntriesLargeSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-place-entries-large.js";
import { computeGsplatLocalTileSortSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-tile-sort.js";
import { computeGsplatLocalClassifySource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-classify.js";
import { computeGsplatLocalBucketSortSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-bucket-sort.js";
import { computeGsplatLocalChunkSortSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-chunk-sort.js";
import { computeGsplatLocalCopySource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-copy.js";
import { computeGsplatLocalBitonicSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-bitonic.js";
import { computeGsplatProjectCommonSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-project-common.js";
import { computeGsplatCommonSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-common.js";
import { computeGsplatTileIntersectSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-tile-intersect.js";
import { GSplatTileComposite } from "./gsplat-tile-composite.js";
import { GSplatLocalDispatchSet } from "./gsplat-local-dispatch-set.js";
import { ALPHA_VISIBILITY_THRESHOLD, CACHE_STRIDE } from "./constants.js";
import computeSplatSource from "../shader-lib/wgsl/chunks/gsplat/vert/gsplatComputeSplat.js";
const INITIAL_TILE_ENTRY_MULTIPLIER = 2.5;
const ENTRY_HEADROOM_MULTIPLIER = 2;
const SHRINK_THRESHOLD = 200;
const INITIAL_LARGE_SPLAT_CAPACITY = 16384;
const TILE_SIZE = 16;
const MAX_TILES = 65535;
const MAX_CHUNKS_PER_TILE = 8;
const _viewProjMat = new Mat4();
const _viewProjData = new Float32Array(16);
const _viewData = new Float32Array(16);
const _shaderProjMat = new Mat4();
const _fogColorLinear = new Color();
const _fogColorArray = new Float32Array(3);
class GSplatComputeLocalRenderer extends GSplatRenderer {
  /**
   * @param {GraphicsDevice} device - The graphics device.
   * @param {GraphNode} node - The graph node.
   * @param {GraphNode} cameraNode - The camera node.
   * @param {Layer} layer - The layer to add mesh instances to.
   * @param {GSplatWorkBuffer} workBuffer - The work buffer containing splat data.
   */
  constructor(device, node, cameraNode, layer, workBuffer) {
    super(device, node, cameraNode, layer, workBuffer);
    /** @type {GSplatLocalDispatchSet} */
    __publicField(this, "_mainSet");
    /** @type {GSplatLocalDispatchSet|null} */
    __publicField(this, "_pickSet", null);
    /** @type {FramePassGSplatComputeLocal} */
    __publicField(this, "framePass");
    /** @type {GSplatTileComposite} */
    __publicField(this, "tileComposite");
    /** @type {boolean} */
    __publicField(this, "_needsFramePassRegister", false);
    /** @type {number} */
    __publicField(this, "_textureSize", 0);
    /** @type {number} */
    __publicField(this, "_minPixelSize", 2);
    /** @type {number} */
    __publicField(this, "_minContribution", 3);
    /** @type {number} */
    __publicField(this, "_alphaClip", 0.3);
    /** @type {number} */
    __publicField(this, "_alphaClipForward", ALPHA_VISIBILITY_THRESHOLD);
    /** @type {number} */
    __publicField(this, "_exposure", 1);
    /** @type {number} */
    __publicField(this, "_numSplats", 0);
    /** @type {StorageBuffer|null} */
    __publicField(this, "_compactedSplatIds", null);
    /** @type {StorageBuffer|null} */
    __publicField(this, "_sortElementCountBuffer", null);
    // --- Shared splat/entry-dependent buffers ---
    /** @type {StorageBuffer|null} */
    __publicField(this, "_projCacheBuffer", null);
    /** @type {StorageBuffer|null} */
    __publicField(this, "_tileEntriesBuffer", null);
    // Pair buffer for scatter-free tile binning: stores packed (tileIdx << 16 | localOffset)
    // per splat-tile intersection. Same size as tileEntries (one u32 per pair).
    /** @type {StorageBuffer|null} */
    __publicField(this, "_pairBuffer", null);
    /**
     * Packed atomic counters: [0] = global pair counter, [1] = large splat count.
     *
     * @type {StorageBuffer|null}
     */
    __publicField(this, "_countersBuffer", null);
    /** @type {StorageBuffer|null} */
    __publicField(this, "_splatPairStartBuffer", null);
    /** @type {StorageBuffer|null} */
    __publicField(this, "_splatPairCountBuffer", null);
    // --- Large-splat deferred processing buffers ---
    /** @type {StorageBuffer|null} */
    __publicField(this, "_largeSplatIdsBuffer", null);
    /** @type {number} */
    __publicField(this, "_largeSplatIdsCapacity", INITIAL_LARGE_SPLAT_CAPACITY);
    /** @type {number} */
    __publicField(this, "_allocatedLargeSplatCapacity", 0);
    /** @type {number} */
    __publicField(this, "_allocatedSplatCapacity", 0);
    /** @type {number} */
    __publicField(this, "_allocatedEntryCapacity", 0);
    /** @type {number} */
    __publicField(this, "_tileEntryMultiplier", INITIAL_TILE_ENTRY_MULTIPLIER);
    /** @type {number} Last device.submitVersion when shared buffers were resized */
    __publicField(this, "_lastBufferSubmitVersion", -1);
    /** @type {number} Last readback entry count (-1 = consumed / no fresh data) */
    __publicField(this, "_lastReadbackEntryCount", -1);
    /** @type {number} Consecutive frames where usage < half capacity */
    __publicField(this, "_shrinkFrameCount", 0);
    /** @type {number} */
    __publicField(this, "_fisheye", 0);
    /**
     * Active data source providing format and texture access. When set via {@link setDataSource},
     * the renderer reads format and textures from this object instead of the inherited workBuffer.
     * Defaults to the workBuffer passed to the constructor.
     *
     * @type {{ format: GSplatFormat, getTexture: (name: string) => Texture }}
     * @private
     */
    __publicField(this, "_dataSource");
    /** @type {Shader} */
    __publicField(this, "_placeEntriesShader");
    /** @type {BindGroupFormat} */
    __publicField(this, "_placeEntriesBindGroupFormat");
    /** @type {Shader} */
    __publicField(this, "_placeEntryPrepShader");
    /** @type {BindGroupFormat} */
    __publicField(this, "_placeEntryPrepBindGroupFormat");
    /** @type {StorageBuffer|null} */
    __publicField(this, "_placeEntryPrepDispatchBuffer", null);
    /** @type {Compute|null} */
    __publicField(this, "_placeEntryPrepCompute", null);
    /** @type {Shader} */
    __publicField(this, "_largeSplatShader");
    /** @type {BindGroupFormat} */
    __publicField(this, "_largeSplatBindGroupFormat");
    /** @type {Shader} */
    __publicField(this, "_largeSplatPrepShader");
    /** @type {BindGroupFormat} */
    __publicField(this, "_largeSplatPrepBindGroupFormat");
    /** @type {StorageBuffer|null} */
    __publicField(this, "_largeSplatDispatchBuffer", null);
    /** @type {Compute|null} */
    __publicField(this, "_largeSplatPrepCompute", null);
    /** @type {Shader} */
    __publicField(this, "_largePlaceEntriesShader");
    /** @type {BindGroupFormat} */
    __publicField(this, "_largePlaceEntriesBindGroupFormat");
    /** @type {Shader} */
    __publicField(this, "_classifyShader");
    /** @type {BindGroupFormat} */
    __publicField(this, "_classifyBindGroupFormat");
    /** @type {Shader} */
    __publicField(this, "_sortShader");
    /** @type {BindGroupFormat} */
    __publicField(this, "_sortBindGroupFormat");
    /** @type {Shader} */
    __publicField(this, "_bucketSortShader");
    /** @type {BindGroupFormat} */
    __publicField(this, "_bucketSortBindGroupFormat");
    /** @type {Shader} */
    __publicField(this, "_copyShader");
    /** @type {BindGroupFormat} */
    __publicField(this, "_copyBindGroupFormat");
    /** @type {Shader} */
    __publicField(this, "_chunkSortShader");
    /** @type {BindGroupFormat} */
    __publicField(this, "_chunkSortBindGroupFormat");
    /** @type {number} */
    __publicField(this, "_lastDrawSlot", 0);
    /** @type {number} */
    __publicField(this, "_lastNumTilesX", 0);
    this._dataSource = workBuffer;
    this._createSharedShaders();
    this._mainSet = this._createDispatchSet(false);
    this.framePass = new FramePassGSplatComputeLocal(this);
    const thisCamera = cameraNode.camera;
    this.tileComposite = new GSplatTileComposite(device, node, (camera) => {
      const renderMode = this.renderMode ?? 0;
      return thisCamera.camera === camera && (renderMode & GSPLAT_FORWARD) !== 0 && this._mainSet._rasterizeTileListBuffer !== null;
    });
  }
  /**
   * Sets the data source for format and texture access, decoupling this renderer from
   * the work buffer. The source object must provide:
   *
   * - `format` — a {@link GSplatFormat} describing the texture streams and shader read code.
   * - `getTexture(name)` — a function returning a {@link Texture} for a given stream name.
   *
   * @param {object} source - The data source.
   */
  setDataSource(source) {
    this._dataSource = source;
  }
  destroy() {
    this._unregisterFramePass();
    if (this.renderMode) {
      if (this.renderMode & GSPLAT_FORWARD) {
        this.layer.removeMeshInstances([this.tileComposite.meshInstance], true);
      }
    }
    this._mainSet.destroy();
    this._pickSet?.destroy();
    this._placeEntriesShader.destroy();
    this._placeEntriesBindGroupFormat.destroy();
    this._placeEntryPrepShader.destroy();
    this._placeEntryPrepBindGroupFormat.destroy();
    this._placeEntryPrepDispatchBuffer?.destroy();
    this._largeSplatShader.destroy();
    this._largeSplatBindGroupFormat.destroy();
    this._largeSplatPrepShader.destroy();
    this._largeSplatPrepBindGroupFormat.destroy();
    this._largeSplatDispatchBuffer?.destroy();
    this._largePlaceEntriesShader.destroy();
    this._largePlaceEntriesBindGroupFormat.destroy();
    this._classifyShader.destroy();
    this._classifyBindGroupFormat.destroy();
    this._sortShader.destroy();
    this._sortBindGroupFormat.destroy();
    this._bucketSortShader.destroy();
    this._bucketSortBindGroupFormat.destroy();
    this._copyShader.destroy();
    this._copyBindGroupFormat.destroy();
    this._chunkSortShader.destroy();
    this._chunkSortBindGroupFormat.destroy();
    this._projCacheBuffer?.destroy();
    this._depthBuffer?.destroy();
    this._tileEntriesBuffer?.destroy();
    this._pairBuffer?.destroy();
    this._countersBuffer?.destroy();
    this._splatPairStartBuffer?.destroy();
    this._splatPairCountBuffer?.destroy();
    this._largeSplatIdsBuffer?.destroy();
    this.tileComposite.destroy();
    super.destroy();
  }
  get material() {
    return this.tileComposite.material;
  }
  setRenderMode(renderMode) {
    const oldRenderMode = this.renderMode ?? 0;
    const wasForward = (oldRenderMode & GSPLAT_FORWARD) !== 0;
    const isForward = (renderMode & GSPLAT_FORWARD) !== 0;
    if (!wasForward && isForward) {
      this.layer.addMeshInstances([this.tileComposite.meshInstance], true);
      this._registerFramePass();
    }
    if (wasForward && !isForward) {
      this.layer.removeMeshInstances([this.tileComposite.meshInstance], true);
      this._unregisterFramePass();
    }
    super.setRenderMode(renderMode);
  }
  frameUpdate(gsplat, exposure, fogParams) {
    if (this._needsFramePassRegister) {
      this._registerFramePass();
    }
    this._minPixelSize = gsplat.minPixelSize;
    this._minContribution = gsplat.minContribution;
    this._alphaClip = gsplat.alphaClip;
    this._alphaClipForward = gsplat.alphaClipForward;
    this._exposure = exposure ?? 1;
    this._fisheye = gsplat.fisheye;
    this._fogParams = fogParams ?? null;
    this._debugMode = gsplat.debug;
    const formatHash = this._dataSource.format.hash;
    if (formatHash !== this._formatHash) {
      this._formatHash = formatHash;
      this._invalidateCountCompute();
    }
  }
  /**
   * @param {StorageBuffer} compactedSplatIds - Dense buffer of visible splat IDs.
   * @param {StorageBuffer} sortElementCountBuffer - Single-u32 buffer with visible count.
   * @param {number} textureSize - The work buffer texture size.
   * @param {number} numSplats - Upper bound on visible splats.
   */
  setCompactedData(compactedSplatIds, sortElementCountBuffer, textureSize, numSplats) {
    this._compactedSplatIds = compactedSplatIds;
    this._sortElementCountBuffer = sortElementCountBuffer;
    this._textureSize = textureSize;
    this._numSplats = numSplats;
  }
  /** @private */
  _registerFramePass() {
    const camera = this.cameraNode.camera?.camera;
    if (camera) {
      if (!camera.beforePasses.includes(this.framePass)) {
        camera.beforePasses.push(this.framePass);
      }
      this._needsFramePassRegister = false;
    } else {
      this._needsFramePassRegister = true;
    }
  }
  /** @private */
  _unregisterFramePass() {
    this._needsFramePassRegister = false;
    const camera = this.cameraNode.camera?.camera;
    if (camera) {
      const idx = camera.beforePasses.indexOf(this.framePass);
      if (idx !== -1) {
        camera.beforePasses.splice(idx, 1);
      }
    }
  }
  resizeOutputTexture(width, height) {
    this._mainSet.resizeOutputTextures(width, height);
  }
  /**
   * Ensure shared splat/entry buffers are large enough. Only resizes when
   * device.submitVersion has changed (preventing mid-encoder buffer destruction).
   *
   * @param {number} numSplats - Upper bound on visible splats.
   * @private
   */
  _ensureSharedBuffers(numSplats) {
    const device = this.device;
    const canResize = device.submitVersion !== this._lastBufferSubmitVersion;
    const readbackValue = this._lastReadbackEntryCount;
    if (readbackValue !== -1) {
      this._lastReadbackEntryCount = -1;
      const currentCapacity = this._allocatedEntryCapacity;
      if (currentCapacity > 0 && readbackValue < currentCapacity / 2) {
        this._shrinkFrameCount++;
      } else {
        this._shrinkFrameCount = 0;
      }
      if (this._shrinkFrameCount >= SHRINK_THRESHOLD && numSplats > 0) {
        const target = Math.max(INITIAL_TILE_ENTRY_MULTIPLIER, readbackValue / numSplats * ENTRY_HEADROOM_MULTIPLIER);
        this._tileEntryMultiplier = Math.max(target, this._tileEntryMultiplier * 0.9);
        this._shrinkFrameCount = 0;
      }
    }
    if (!canResize) return;
    if (numSplats > this._allocatedSplatCapacity) {
      this._projCacheBuffer?.destroy();
      this._depthBuffer?.destroy();
      this._splatPairStartBuffer?.destroy();
      this._splatPairCountBuffer?.destroy();
      this._allocatedSplatCapacity = numSplats;
      this._projCacheBuffer = new StorageBuffer(device, numSplats * CACHE_STRIDE * 4);
      this._depthBuffer = new StorageBuffer(device, numSplats * 4);
      this._splatPairStartBuffer = new StorageBuffer(device, numSplats * 4);
      this._splatPairCountBuffer = new StorageBuffer(device, numSplats * 4);
      DebugHelper.setName(this._projCacheBuffer, "GsplatComputeLocalRenderer.projCache");
      DebugHelper.setName(this._depthBuffer, "GsplatComputeLocalRenderer.depth");
      DebugHelper.setName(this._splatPairStartBuffer, "GsplatComputeLocalRenderer.splatPairStart");
      DebugHelper.setName(this._splatPairCountBuffer, "GsplatComputeLocalRenderer.splatPairCount");
    }
    if (!this._countersBuffer) {
      this._countersBuffer = new StorageBuffer(device, 8, BUFFERUSAGE_COPY_DST | BUFFERUSAGE_COPY_SRC);
      DebugHelper.setName(this._countersBuffer, "GsplatComputeLocalRenderer.counters");
    }
    if (this._largeSplatIdsCapacity > this._allocatedLargeSplatCapacity) {
      this._largeSplatIdsBuffer?.destroy();
      this._allocatedLargeSplatCapacity = this._largeSplatIdsCapacity;
      this._largeSplatIdsBuffer = new StorageBuffer(device, this._largeSplatIdsCapacity * 4);
      DebugHelper.setName(this._largeSplatIdsBuffer, "GsplatComputeLocalRenderer.largeSplatIds");
    }
    const requiredEntryCapacity = Math.ceil(numSplats * this._tileEntryMultiplier);
    const needsGrow = requiredEntryCapacity > this._allocatedEntryCapacity;
    const needsShrink = this._allocatedEntryCapacity > 0 && requiredEntryCapacity * 2 < this._allocatedEntryCapacity;
    if (needsGrow || needsShrink) {
      this._tileEntriesBuffer?.destroy();
      this._pairBuffer?.destroy();
      this._allocatedEntryCapacity = requiredEntryCapacity;
      this._tileEntriesBuffer = new StorageBuffer(device, requiredEntryCapacity * 4, BUFFERUSAGE_COPY_DST);
      this._pairBuffer = new StorageBuffer(device, requiredEntryCapacity * 4);
      DebugHelper.setName(this._tileEntriesBuffer, "GsplatComputeLocalRenderer.tileEntries");
      DebugHelper.setName(this._pairBuffer, "GsplatComputeLocalRenderer.pair");
    }
    this._lastBufferSubmitVersion = device.submitVersion;
  }
  /**
   * Main color dispatch.
   */
  dispatch() {
    const set = this._mainSet;
    const outputTex = set.outputTexture;
    if (!outputTex) return;
    const width = outputTex.width;
    const height = outputTex.height;
    this._dispatchPipeline(set, width, height, false);
    this.tileComposite.update(
      this._lastDrawSlot,
      outputTex,
      set._rasterizeTileListBuffer,
      this._lastNumTilesX,
      width,
      height
    );
  }
  /**
   * Pick dispatch: runs the compute pick pipeline and returns the configured pick mesh instance.
   *
   * @param {object} cam - The camera.
   * @param {number} width - Pick target width.
   * @param {number} height - Pick target height.
   * @returns {MeshInstance|null} The pick mesh instance ready for the picker's render list.
   */
  dispatchPick(cam, width, height) {
    if (!this._pickSet) {
      this._pickSet = this._createDispatchSet(true);
    }
    const set = this._pickSet;
    set.resizeOutputTextures(width, height);
    this._dispatchPipeline(set, width, height, true);
    return this.tileComposite.prepareForPicking(
      this._lastDrawSlot,
      set.pickIdTexture,
      set.pickDepthTexture,
      set._rasterizeTileListBuffer,
      this._lastNumTilesX,
      width,
      height
    );
  }
  /**
   * Unified dispatch pipeline used by both color and pick paths.
   *
   * @param {GSplatLocalDispatchSet} set - The dispatch set to use.
   * @param {number} width - Render target width.
   * @param {number} height - Render target height.
   * @param {boolean} pickMode - Whether this is a pick dispatch.
   * @private
   */
  _dispatchPipeline(set, width, height, pickMode) {
    const numSplats = this._numSplats;
    if (!this._compactedSplatIds || !this._sortElementCountBuffer || numSplats === 0) return;
    const device = this.device;
    let numTilesX = Math.ceil(width / TILE_SIZE);
    let numTilesY = Math.ceil(height / TILE_SIZE);
    if (numTilesX * numTilesY > MAX_TILES) {
      Debug.warnOnce("GSplatComputeLocalRenderer: render target exceeds maximum supported tile count (65535). Tile coverage will be clamped, causing rendering artifacts at screen edges.");
      const scale = Math.sqrt(MAX_TILES / (numTilesX * numTilesY));
      numTilesX = Math.max(1, Math.floor(numTilesX * scale));
      numTilesY = Math.max(1, Math.floor(numTilesY * scale));
    }
    const numTiles = numTilesX * numTilesY;
    this._ensureSharedBuffers(numSplats);
    set.ensureTileBuffers(numTiles);
    const maxEntries = this._allocatedEntryCapacity;
    const ds = this._dataSource;
    const camera = this.cameraNode.camera;
    const cam = camera.camera;
    const view = cam.viewMatrix;
    const flipY = !!camera.renderTarget?.flipY;
    const webgpu = device.isWebGPU;
    _viewProjMat.mul2(Camera.applyShaderProjectionTransform(cam.projectionMatrix, _shaderProjMat, flipY, webgpu), view);
    _viewProjData.set(_viewProjMat.data);
    _viewData.set(view.data);
    const focal = width * _shaderProjMat.data[0];
    const alphaClip = pickMode ? this._alphaClip : Math.max(ALPHA_VISIBILITY_THRESHOLD, this._alphaClipForward);
    this.fisheyeProj.update(this._fisheye, camera.fov, cam.projectionMatrix);
    const fisheyeEnabled = this.fisheyeProj.enabled;
    const createCountShader = (pick, fisheye) => this._createCountShaderAndFormat(pick, fisheye);
    const countCompute = set.getCountCompute(fisheyeEnabled, createCountShader);
    this._placeEntryPrepCompute.setParameter("sortElementCount", this._sortElementCountBuffer);
    this._placeEntryPrepCompute.setParameter("dispatchArgs", this._placeEntryPrepDispatchBuffer);
    this._placeEntryPrepCompute.setupDispatch(1, 1, 1);
    device.computeDispatch([this._placeEntryPrepCompute], "GSplatLocalDispatchPrep");
    set._tileSplatCountsBuffer.clear();
    this._countersBuffer.clear();
    countCompute.setParameter("compactedSplatIds", this._compactedSplatIds);
    countCompute.setParameter("sortElementCount", this._sortElementCountBuffer);
    countCompute.setParameter("projCache", this._projCacheBuffer);
    countCompute.setParameter("tileSplatCounts", set._tileSplatCountsBuffer);
    countCompute.setParameter("pairBuffer", this._pairBuffer);
    countCompute.setParameter("countersBuffer", this._countersBuffer);
    countCompute.setParameter("splatPairStart", this._splatPairStartBuffer);
    countCompute.setParameter("splatPairCount", this._splatPairCountBuffer);
    countCompute.setParameter("largeSplatIds", this._largeSplatIdsBuffer);
    countCompute.setParameter("depthBuffer", this._depthBuffer);
    for (const stream of ds.format.streams) {
      countCompute.setParameter(stream.name, ds.getTexture(stream.name));
    }
    for (const stream of ds.format.extraStreams) {
      countCompute.setParameter(stream.name, ds.getTexture(stream.name));
    }
    countCompute.setParameter("splatTextureSize", this._textureSize);
    countCompute.setParameter("numTilesX", numTilesX);
    countCompute.setParameter("numTilesY", numTilesY);
    countCompute.setParameter("viewProj", _viewProjData);
    countCompute.setParameter("viewMatrix", _viewData);
    countCompute.setParameter("focal", focal);
    countCompute.setParameter("viewportWidth", width);
    countCompute.setParameter("viewportHeight", height);
    countCompute.setParameter("nearClip", cam.nearClip);
    countCompute.setParameter("farClip", cam.farClip);
    countCompute.setParameter("minPixelSize", this._minPixelSize * 0.5);
    countCompute.setParameter("isOrtho", cam.projection === PROJECTION_ORTHOGRAPHIC ? 1 : 0);
    countCompute.setParameter("exposure", this._exposure);
    countCompute.setParameter("alphaClip", alphaClip);
    countCompute.setParameter("minContribution", this._minContribution);
    if (fisheyeEnabled) {
      const fp = this.fisheyeProj;
      countCompute.setParameter("fisheye_k", fp.k);
      countCompute.setParameter("fisheye_inv_k", fp.invK);
      countCompute.setParameter("fisheye_projMat00", fp.projMat00);
      countCompute.setParameter("fisheye_projMat11", fp.projMat11);
    }
    countCompute.setupIndirectDispatch(0, this._placeEntryPrepDispatchBuffer);
    device.computeDispatch([countCompute], pickMode ? "GSplatPickTileCount" : "GSplatLocalTileCount");
    this._largeSplatPrepCompute.setParameter("countersBuffer", this._countersBuffer);
    this._largeSplatPrepCompute.setParameter("dispatchArgs", this._largeSplatDispatchBuffer);
    this._largeSplatPrepCompute.setParameter("largeSplatIds", this._largeSplatIdsBuffer);
    this._largeSplatPrepCompute.setupDispatch(1, 1, 1);
    device.computeDispatch([this._largeSplatPrepCompute], pickMode ? "GSplatPickLargeSplatPrep" : "GSplatLocalLargeSplatPrep");
    set.largeSplatCompute.setParameter("projCache", this._projCacheBuffer);
    set.largeSplatCompute.setParameter("tileSplatCounts", set._tileSplatCountsBuffer);
    set.largeSplatCompute.setParameter("pairBuffer", this._pairBuffer);
    set.largeSplatCompute.setParameter("countersBuffer", this._countersBuffer);
    set.largeSplatCompute.setParameter("splatPairStart", this._splatPairStartBuffer);
    set.largeSplatCompute.setParameter("splatPairCount", this._splatPairCountBuffer);
    set.largeSplatCompute.setParameter("largeSplatIds", this._largeSplatIdsBuffer);
    set.largeSplatCompute.setParameter("numTilesX", numTilesX);
    set.largeSplatCompute.setParameter("numTilesY", numTilesY);
    set.largeSplatCompute.setParameter("viewportWidth", width);
    set.largeSplatCompute.setParameter("viewportHeight", height);
    set.largeSplatCompute.setParameter("alphaClip", alphaClip);
    set.largeSplatCompute.setupIndirectDispatch(0, this._largeSplatDispatchBuffer);
    device.computeDispatch([set.largeSplatCompute], pickMode ? "GSplatPickLargeTileCount" : "GSplatLocalLargeTileCount");
    set.prefixSumKernel.resize(set._tileSplatCountsBuffer, numTiles + 1);
    set.prefixSumKernel.dispatch(device);
    set.placeEntriesCompute.setParameter("pairBuffer", this._pairBuffer);
    set.placeEntriesCompute.setParameter("splatPairStart", this._splatPairStartBuffer);
    set.placeEntriesCompute.setParameter("splatPairCount", this._splatPairCountBuffer);
    set.placeEntriesCompute.setParameter("tileSplatCounts", set._tileSplatCountsBuffer);
    set.placeEntriesCompute.setParameter("tileEntries", this._tileEntriesBuffer);
    set.placeEntriesCompute.setParameter("sortElementCount", this._sortElementCountBuffer);
    set.placeEntriesCompute.setupIndirectDispatch(1, this._placeEntryPrepDispatchBuffer);
    device.computeDispatch([set.placeEntriesCompute], pickMode ? "GSplatPickPlaceEntries" : "GSplatLocalPlaceEntries");
    set.largePlaceEntriesCompute.setParameter("pairBuffer", this._pairBuffer);
    set.largePlaceEntriesCompute.setParameter("splatPairStart", this._splatPairStartBuffer);
    set.largePlaceEntriesCompute.setParameter("splatPairCount", this._splatPairCountBuffer);
    set.largePlaceEntriesCompute.setParameter("tileSplatCounts", set._tileSplatCountsBuffer);
    set.largePlaceEntriesCompute.setParameter("tileEntries", this._tileEntriesBuffer);
    set.largePlaceEntriesCompute.setParameter("largeSplatIds", this._largeSplatIdsBuffer);
    set.largePlaceEntriesCompute.setParameter("countersBuffer", this._countersBuffer);
    set.largePlaceEntriesCompute.setupIndirectDispatch(0, this._largeSplatDispatchBuffer);
    device.computeDispatch([set.largePlaceEntriesCompute], pickMode ? "GSplatPickLargePlaceEntries" : "GSplatLocalLargePlaceEntries");
    set._tileListCountsBuffer.clear();
    set._totalChunksBuffer.clear();
    set._chunkSortIndirectBuffer.clear();
    const indirectSlot = device.getIndirectDispatchSlot(3);
    const drawSlot = device.getIndirectDrawSlot(1);
    set.classifyCompute.setParameter("tileSplatCounts", set._tileSplatCountsBuffer);
    set.classifyCompute.setParameter("smallTileList", set._smallTileListBuffer);
    set.classifyCompute.setParameter("largeTileList", set._largeTileListBuffer);
    set.classifyCompute.setParameter("rasterizeTileList", set._rasterizeTileListBuffer);
    set.classifyCompute.setParameter("tileListCounts", set._tileListCountsBuffer);
    set.classifyCompute.setParameter("indirectDispatchArgs", device.indirectDispatchBuffer);
    set.classifyCompute.setParameter("largeTileOverflowBases", set._largeTileOverflowBasesBuffer);
    set.classifyCompute.setParameter("indirectDrawArgs", device.indirectDrawBuffer);
    set.classifyCompute.setParameter("numTiles", numTiles);
    set.classifyCompute.setParameter("dispatchSlotOffset", indirectSlot * 3);
    set.classifyCompute.setParameter("bufferCapacity", maxEntries);
    set.classifyCompute.setParameter("maxWorkgroupsPerDim", device.limits.maxComputeWorkgroupsPerDimension || 65535);
    set.classifyCompute.setParameter("drawSlot", drawSlot);
    set.classifyCompute.setupDispatch(1, 1, 1);
    device.computeDispatch([set.classifyCompute], pickMode ? "GSplatPickClassify" : "GSplatLocalClassify");
    set.bucketSortCompute.setParameter("tileEntries", this._tileEntriesBuffer);
    set.bucketSortCompute.setParameter("largeTileOverflowBases", set._largeTileOverflowBasesBuffer);
    set.bucketSortCompute.setParameter("tileSplatCounts", set._tileSplatCountsBuffer);
    set.bucketSortCompute.setParameter("depthBuffer", this._depthBuffer);
    set.bucketSortCompute.setParameter("largeTileList", set._largeTileListBuffer);
    set.bucketSortCompute.setParameter("chunkRanges", set._chunkRangesBuffer);
    set.bucketSortCompute.setParameter("totalChunks", set._totalChunksBuffer);
    set.bucketSortCompute.setParameter("tileListCounts", set._tileListCountsBuffer);
    set.bucketSortCompute.setParameter("bufferCapacity", maxEntries);
    set.bucketSortCompute.setParameter("maxChunks", numTiles * MAX_CHUNKS_PER_TILE);
    set.bucketSortCompute.setupIndirectDispatch(indirectSlot + 1);
    device.computeDispatch([set.bucketSortCompute], pickMode ? "GSplatPickBucketSort" : "GSplatLocalBucketSort");
    set.copyCompute.setParameter("totalChunks", set._totalChunksBuffer);
    set.copyCompute.setParameter("chunkSortIndirect", set._chunkSortIndirectBuffer);
    set.copyCompute.setParameter("maxChunks", numTiles * MAX_CHUNKS_PER_TILE);
    set.copyCompute.setParameter("maxWorkgroupsPerDim", device.limits.maxComputeWorkgroupsPerDimension || 65535);
    set.copyCompute.setupDispatch(1, 1, 1);
    device.computeDispatch([set.copyCompute], pickMode ? "GSplatPickCopy" : "GSplatLocalCopy");
    set.sortCompute.setParameter("tileEntries", this._tileEntriesBuffer);
    set.sortCompute.setParameter("tileSplatCounts", set._tileSplatCountsBuffer);
    set.sortCompute.setParameter("depthBuffer", this._depthBuffer);
    set.sortCompute.setParameter("smallTileList", set._smallTileListBuffer);
    set.sortCompute.setParameter("tileListCounts", set._tileListCountsBuffer);
    set.sortCompute.setupIndirectDispatch(indirectSlot);
    device.computeDispatch([set.sortCompute], pickMode ? "GSplatPickTileSort" : "GSplatLocalTileSort");
    set.chunkSortCompute.setParameter("tileEntries", this._tileEntriesBuffer);
    set.chunkSortCompute.setParameter("depthBuffer", this._depthBuffer);
    set.chunkSortCompute.setParameter("chunkRanges", set._chunkRangesBuffer);
    set.chunkSortCompute.setParameter("totalChunks", set._totalChunksBuffer);
    set.chunkSortCompute.setParameter("maxChunks", numTiles * MAX_CHUNKS_PER_TILE);
    set.chunkSortCompute.setupIndirectDispatch(0, set._chunkSortIndirectBuffer);
    device.computeDispatch([set.chunkSortCompute], pickMode ? "GSplatPickChunkSort" : "GSplatLocalChunkSort");
    const hasLinearDepth = cam.shaderParams.sceneDepthMapLinear;
    const sceneDepthMap = hasLinearDepth ? device.scope.resolve("uSceneDepthMap").value : null;
    const useDepth = !pickMode && sceneDepthMap;
    const fogParams = this._fogParams;
    const fogType = fogParams && fogParams.type !== FOG_NONE ? fogParams.type : "none";
    const heatmap = !pickMode && this._debugMode === GSPLAT_DEBUG_HEATMAP;
    const rasterizeCompute = set.getRasterizeCompute(pickMode, useDepth, fogType, heatmap);
    rasterizeCompute.setParameter("screenWidth", width);
    rasterizeCompute.setParameter("screenHeight", height);
    rasterizeCompute.setParameter("numTilesX", numTilesX);
    rasterizeCompute.setParameter("nearClip", cam.nearClip);
    rasterizeCompute.setParameter("farClip", cam.farClip);
    rasterizeCompute.setParameter("alphaClip", alphaClip);
    if (fogType !== "none") {
      _fogColorLinear.linear(fogParams.color);
      _fogColorArray[0] = _fogColorLinear.r;
      _fogColorArray[1] = _fogColorLinear.g;
      _fogColorArray[2] = _fogColorLinear.b;
      rasterizeCompute.setParameter("fog_color", _fogColorArray);
      rasterizeCompute.setParameter("fog_start", fogParams.start);
      rasterizeCompute.setParameter("fog_end", fogParams.end);
      rasterizeCompute.setParameter("fog_density", fogParams.density);
    }
    rasterizeCompute.setParameter("tileEntries", this._tileEntriesBuffer);
    rasterizeCompute.setParameter("tileSplatCounts", set._tileSplatCountsBuffer);
    rasterizeCompute.setParameter("projCache", this._projCacheBuffer);
    rasterizeCompute.setParameter("rasterizeTileList", set._rasterizeTileListBuffer);
    rasterizeCompute.setParameter("tileListCounts", set._tileListCountsBuffer);
    rasterizeCompute.setParameter("depthBuffer", this._depthBuffer);
    if (pickMode) {
      rasterizeCompute.setParameter("pickIdTexture", set.pickIdTexture);
      rasterizeCompute.setParameter("pickDepthTexture", set.pickDepthTexture);
    } else {
      rasterizeCompute.setParameter("outputTexture", set.outputTexture);
      if (useDepth) {
        rasterizeCompute.setParameter("sceneDepthMap", sceneDepthMap);
      }
    }
    rasterizeCompute.setupIndirectDispatch(indirectSlot + 2);
    device.computeDispatch([rasterizeCompute], pickMode ? "GSplatPickRasterize" : "GSplatLocalRasterize");
    this._lastDrawSlot = drawSlot;
    this._lastNumTilesX = numTilesX;
    this._scheduleReadback(numSplats, numTiles, set);
  }
  /**
   * @param {number} numSplats - Splat count at dispatch time.
   * @param {number} numTiles - Tile count at dispatch time.
   * @param {GSplatLocalDispatchSet} set - The dispatch set to readback from.
   * @private
   */
  _scheduleReadback(numSplats, numTiles, set) {
    if (numSplats === 0) return;
    const capturedNumSplats = numSplats;
    const readback1 = set._tileSplatCountsBuffer.read(numTiles * 4, 4);
    const readback2 = set._tileListCountsBuffer.read(3 * 4, 4);
    const readback3 = this._countersBuffer.read(4, 4);
    Promise.all([readback1, readback2, readback3]).then(([r1, r2, r3]) => {
      const totalEntries = new Uint32Array(r1.buffer, r1.byteOffset, 1)[0];
      const totalOverflow = new Uint32Array(r2.buffer, r2.byteOffset, 1)[0];
      const needed = totalEntries + totalOverflow;
      if (capturedNumSplats > 0) {
        this._tileEntryMultiplier = Math.max(this._tileEntryMultiplier, needed / capturedNumSplats * ENTRY_HEADROOM_MULTIPLIER);
        this._tileEntryMultiplier = Math.max(this._tileEntryMultiplier, INITIAL_TILE_ENTRY_MULTIPLIER);
      }
      this._lastReadbackEntryCount = needed;
      const largeSplatDemand = new Uint32Array(r3.buffer, r3.byteOffset, 1)[0];
      if (largeSplatDemand > this._largeSplatIdsCapacity) {
        this._largeSplatIdsCapacity = Math.ceil(largeSplatDemand * 1.2);
      }
    }).catch(() => {
    });
  }
  /**
   * Invalidates all cached count resources on both dispatch sets. Called when the
   * work buffer format changes, making all compiled count shaders invalid.
   *
   * @private
   */
  _invalidateCountCompute() {
    this._mainSet.destroyCountResources();
    this._pickSet?.destroyCountResources();
  }
  // ---- Shader / BindGroupFormat creation ----
  /** @private */
  _createCommonIncludes() {
    const cincludes = /* @__PURE__ */ new Map();
    cincludes.set("gsplatCommonCS", computeGsplatCommonSource);
    cincludes.set("gsplatTileIntersectCS", computeGsplatTileIntersectSource);
    return cincludes;
  }
  /** @private */
  _createBitonicIncludes() {
    const cincludes = /* @__PURE__ */ new Map();
    cincludes.set("gsplatLocalBitonicCS", computeGsplatLocalBitonicSource);
    return cincludes;
  }
  /**
   * Creates all shared shaders and bind group formats (called once in constructor).
   *
   * @private
   */
  _createSharedShaders() {
    const device = this.device;
    this._placeEntriesBindGroupFormat = new BindGroupFormat(device, [
      new BindStorageBufferFormat("pairBuffer", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("splatPairStart", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("splatPairCount", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("tileSplatCounts", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("tileEntries", SHADERSTAGE_COMPUTE),
      new BindStorageBufferFormat("sortElementCount", SHADERSTAGE_COMPUTE, true)
    ]);
    this._placeEntriesShader = new Shader(device, {
      name: "GSplatLocalPlaceEntries",
      shaderLanguage: SHADERLANGUAGE_WGSL,
      cshader: computeGsplatLocalPlaceEntriesSource,
      computeBindGroupFormat: this._placeEntriesBindGroupFormat
    });
    {
      const maxDim = device.limits.maxComputeWorkgroupsPerDimension || 65535;
      const prepDefines = /* @__PURE__ */ new Map();
      prepDefines.set("{MAX_DIM}", maxDim.toString());
      prepDefines.set("{SPLATS_PER_WG}", "256");
      prepDefines.set("{SPLATS_PER_WG_MINUS_1}", "255");
      this._placeEntryPrepBindGroupFormat = new BindGroupFormat(device, [
        new BindStorageBufferFormat("sortElementCount", SHADERSTAGE_COMPUTE, true),
        new BindStorageBufferFormat("dispatchArgs", SHADERSTAGE_COMPUTE)
      ]);
      this._placeEntryPrepShader = new Shader(device, {
        name: "GSplatLocalPlaceEntryPrep",
        shaderLanguage: SHADERLANGUAGE_WGSL,
        cshader: computeGsplatLocalDispatchPrepSource,
        cdefines: prepDefines,
        computeBindGroupFormat: this._placeEntryPrepBindGroupFormat
      });
      this._placeEntryPrepDispatchBuffer = new StorageBuffer(device, 6 * 4, BUFFERUSAGE_INDIRECT);
      DebugHelper.setName(this._placeEntryPrepDispatchBuffer, "GsplatComputeLocalRenderer.placeEntryPrepDispatch");
      this._placeEntryPrepCompute = new Compute(device, this._placeEntryPrepShader);
    }
    {
      const cincludes = this._createCommonIncludes();
      const ubf = new UniformBufferFormat(device, [
        new UniformFormat("numTilesX", UNIFORMTYPE_UINT),
        new UniformFormat("numTilesY", UNIFORMTYPE_UINT),
        new UniformFormat("viewportWidth", UNIFORMTYPE_FLOAT),
        new UniformFormat("viewportHeight", UNIFORMTYPE_FLOAT),
        new UniformFormat("alphaClip", UNIFORMTYPE_FLOAT)
      ]);
      this._largeSplatBindGroupFormat = new BindGroupFormat(device, [
        new BindStorageBufferFormat("projCache", SHADERSTAGE_COMPUTE, true),
        new BindStorageBufferFormat("tileSplatCounts", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("pairBuffer", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("countersBuffer", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("splatPairStart", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("splatPairCount", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("largeSplatIds", SHADERSTAGE_COMPUTE, true),
        new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE)
      ]);
      const cdefines = /* @__PURE__ */ new Map();
      cdefines.set("{CACHE_STRIDE}", CACHE_STRIDE.toString());
      this._largeSplatShader = new Shader(device, {
        name: "GSplatLocalTileCountLarge",
        shaderLanguage: SHADERLANGUAGE_WGSL,
        cshader: computeGsplatLocalTileCountLargeSource,
        cincludes,
        cdefines,
        computeBindGroupFormat: this._largeSplatBindGroupFormat,
        computeUniformBufferFormats: { uniforms: ubf }
      });
    }
    {
      const maxDim = device.limits.maxComputeWorkgroupsPerDimension || 65535;
      const largePrepDefines = /* @__PURE__ */ new Map();
      largePrepDefines.set("{MAX_DIM}", maxDim.toString());
      this._largeSplatPrepBindGroupFormat = new BindGroupFormat(device, [
        new BindStorageBufferFormat("countersBuffer", SHADERSTAGE_COMPUTE, true),
        new BindStorageBufferFormat("dispatchArgs", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("largeSplatIds", SHADERSTAGE_COMPUTE, true)
      ]);
      this._largeSplatPrepShader = new Shader(device, {
        name: "GSplatLocalLargeSplatPrep",
        shaderLanguage: SHADERLANGUAGE_WGSL,
        cshader: computeGsplatLocalDispatchPrepLargeSource,
        cdefines: largePrepDefines,
        computeBindGroupFormat: this._largeSplatPrepBindGroupFormat
      });
      this._largeSplatDispatchBuffer = new StorageBuffer(device, 3 * 4, BUFFERUSAGE_INDIRECT);
      DebugHelper.setName(this._largeSplatDispatchBuffer, "GsplatComputeLocalRenderer.largeSplatDispatch");
      this._largeSplatPrepCompute = new Compute(device, this._largeSplatPrepShader);
    }
    this._largePlaceEntriesBindGroupFormat = new BindGroupFormat(device, [
      new BindStorageBufferFormat("pairBuffer", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("splatPairStart", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("splatPairCount", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("tileSplatCounts", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("tileEntries", SHADERSTAGE_COMPUTE),
      new BindStorageBufferFormat("largeSplatIds", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("countersBuffer", SHADERSTAGE_COMPUTE, true)
    ]);
    this._largePlaceEntriesShader = new Shader(device, {
      name: "GSplatLocalPlaceEntriesLarge",
      shaderLanguage: SHADERLANGUAGE_WGSL,
      cshader: computeGsplatLocalPlaceEntriesLargeSource,
      computeBindGroupFormat: this._largePlaceEntriesBindGroupFormat
    });
    {
      const ubf = new UniformBufferFormat(device, [
        new UniformFormat("numTiles", UNIFORMTYPE_UINT),
        new UniformFormat("dispatchSlotOffset", UNIFORMTYPE_UINT),
        new UniformFormat("bufferCapacity", UNIFORMTYPE_UINT),
        new UniformFormat("maxWorkgroupsPerDim", UNIFORMTYPE_UINT),
        new UniformFormat("drawSlot", UNIFORMTYPE_UINT)
      ]);
      this._classifyBindGroupFormat = new BindGroupFormat(device, [
        new BindStorageBufferFormat("tileSplatCounts", SHADERSTAGE_COMPUTE, true),
        new BindStorageBufferFormat("smallTileList", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("largeTileList", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("rasterizeTileList", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("tileListCounts", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("indirectDispatchArgs", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("largeTileOverflowBases", SHADERSTAGE_COMPUTE),
        new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("indirectDrawArgs", SHADERSTAGE_COMPUTE)
      ]);
      this._classifyShader = new Shader(device, {
        name: "GSplatLocalClassify",
        shaderLanguage: SHADERLANGUAGE_WGSL,
        cshader: computeGsplatLocalClassifySource,
        computeBindGroupFormat: this._classifyBindGroupFormat,
        computeUniformBufferFormats: { uniforms: ubf }
      });
    }
    this._sortBindGroupFormat = new BindGroupFormat(device, [
      new BindStorageBufferFormat("tileEntries", SHADERSTAGE_COMPUTE),
      new BindStorageBufferFormat("tileSplatCounts", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("depthBuffer", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("smallTileList", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("tileListCounts", SHADERSTAGE_COMPUTE, true)
    ]);
    this._sortShader = new Shader(device, {
      name: "GSplatLocalTileSort",
      shaderLanguage: SHADERLANGUAGE_WGSL,
      cshader: computeGsplatLocalTileSortSource,
      cincludes: this._createBitonicIncludes(),
      computeBindGroupFormat: this._sortBindGroupFormat
    });
    {
      const ubf = new UniformBufferFormat(device, [
        new UniformFormat("bufferCapacity", UNIFORMTYPE_UINT),
        new UniformFormat("maxChunks", UNIFORMTYPE_UINT)
      ]);
      this._bucketSortBindGroupFormat = new BindGroupFormat(device, [
        new BindStorageBufferFormat("tileEntries", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("largeTileOverflowBases", SHADERSTAGE_COMPUTE, true),
        new BindStorageBufferFormat("tileSplatCounts", SHADERSTAGE_COMPUTE, true),
        new BindStorageBufferFormat("depthBuffer", SHADERSTAGE_COMPUTE, true),
        new BindStorageBufferFormat("largeTileList", SHADERSTAGE_COMPUTE, true),
        new BindStorageBufferFormat("chunkRanges", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("totalChunks", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("tileListCounts", SHADERSTAGE_COMPUTE, true),
        new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE)
      ]);
      this._bucketSortShader = new Shader(device, {
        name: "GSplatLocalBucketSort",
        shaderLanguage: SHADERLANGUAGE_WGSL,
        cshader: computeGsplatLocalBucketSortSource,
        computeBindGroupFormat: this._bucketSortBindGroupFormat,
        computeUniformBufferFormats: { uniforms: ubf }
      });
    }
    {
      const ubf = new UniformBufferFormat(device, [
        new UniformFormat("maxChunks", UNIFORMTYPE_UINT),
        new UniformFormat("maxWorkgroupsPerDim", UNIFORMTYPE_UINT)
      ]);
      this._copyBindGroupFormat = new BindGroupFormat(device, [
        new BindStorageBufferFormat("totalChunks", SHADERSTAGE_COMPUTE, true),
        new BindStorageBufferFormat("chunkSortIndirect", SHADERSTAGE_COMPUTE),
        new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE)
      ]);
      this._copyShader = new Shader(device, {
        name: "GSplatLocalCopy",
        shaderLanguage: SHADERLANGUAGE_WGSL,
        cshader: computeGsplatLocalCopySource,
        computeBindGroupFormat: this._copyBindGroupFormat,
        computeUniformBufferFormats: { uniforms: ubf }
      });
    }
    {
      const ubf = new UniformBufferFormat(device, [
        new UniformFormat("maxChunks", UNIFORMTYPE_UINT)
      ]);
      this._chunkSortBindGroupFormat = new BindGroupFormat(device, [
        new BindStorageBufferFormat("tileEntries", SHADERSTAGE_COMPUTE),
        new BindStorageBufferFormat("depthBuffer", SHADERSTAGE_COMPUTE, true),
        new BindStorageBufferFormat("chunkRanges", SHADERSTAGE_COMPUTE, true),
        new BindStorageBufferFormat("totalChunks", SHADERSTAGE_COMPUTE, true),
        new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE)
      ]);
      this._chunkSortShader = new Shader(device, {
        name: "GSplatLocalChunkSort",
        shaderLanguage: SHADERLANGUAGE_WGSL,
        cshader: computeGsplatLocalChunkSortSource,
        cincludes: this._createBitonicIncludes(),
        computeBindGroupFormat: this._chunkSortBindGroupFormat,
        computeUniformBufferFormats: { uniforms: ubf }
      });
    }
  }
  /**
   * Creates the count shader + shared bind group format.
   *
   * @param {boolean} pickMode - Whether to create the pick variant.
   * @param {boolean} fisheyeEnabled - Whether to include the GSPLAT_FISHEYE define and fisheye uniforms.
   * @returns {{ shader: Shader, bindGroupFormat: BindGroupFormat }} The shader and format.
   * @private
   */
  _createCountShaderAndFormat(pickMode, fisheyeEnabled) {
    const device = this.device;
    const uniforms = [
      new UniformFormat("splatTextureSize", UNIFORMTYPE_UINT),
      new UniformFormat("numTilesX", UNIFORMTYPE_UINT),
      new UniformFormat("numTilesY", UNIFORMTYPE_UINT),
      new UniformFormat("viewProj", UNIFORMTYPE_MAT4),
      new UniformFormat("viewMatrix", UNIFORMTYPE_MAT4),
      new UniformFormat("focal", UNIFORMTYPE_FLOAT),
      new UniformFormat("viewportWidth", UNIFORMTYPE_FLOAT),
      new UniformFormat("viewportHeight", UNIFORMTYPE_FLOAT),
      new UniformFormat("nearClip", UNIFORMTYPE_FLOAT),
      new UniformFormat("farClip", UNIFORMTYPE_FLOAT),
      new UniformFormat("minPixelSize", UNIFORMTYPE_FLOAT),
      new UniformFormat("isOrtho", UNIFORMTYPE_UINT),
      new UniformFormat("exposure", UNIFORMTYPE_FLOAT),
      new UniformFormat("alphaClip", UNIFORMTYPE_FLOAT),
      new UniformFormat("minContribution", UNIFORMTYPE_FLOAT)
    ];
    if (fisheyeEnabled) {
      uniforms.push(
        new UniformFormat("fisheye_k", UNIFORMTYPE_FLOAT),
        new UniformFormat("fisheye_inv_k", UNIFORMTYPE_FLOAT),
        new UniformFormat("fisheye_projMat00", UNIFORMTYPE_FLOAT),
        new UniformFormat("fisheye_projMat11", UNIFORMTYPE_FLOAT)
      );
    }
    const uniformBufferFormat = new UniformBufferFormat(device, uniforms);
    const wbFormat = this._dataSource.format;
    const fixedBindings = [
      new BindStorageBufferFormat("compactedSplatIds", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("sortElementCount", SHADERSTAGE_COMPUTE, true),
      new BindStorageBufferFormat("projCache", SHADERSTAGE_COMPUTE),
      new BindStorageBufferFormat("tileSplatCounts", SHADERSTAGE_COMPUTE),
      new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE),
      new BindStorageBufferFormat("pairBuffer", SHADERSTAGE_COMPUTE),
      new BindStorageBufferFormat("countersBuffer", SHADERSTAGE_COMPUTE),
      new BindStorageBufferFormat("splatPairStart", SHADERSTAGE_COMPUTE),
      new BindStorageBufferFormat("splatPairCount", SHADERSTAGE_COMPUTE),
      new BindStorageBufferFormat("largeSplatIds", SHADERSTAGE_COMPUTE),
      new BindStorageBufferFormat("depthBuffer", SHADERSTAGE_COMPUTE)
    ];
    const bindGroupFormat = new BindGroupFormat(device, [
      ...fixedBindings,
      ...wbFormat.getComputeBindFormats()
    ]);
    const cincludes = this._createCommonIncludes();
    cincludes.set("gsplatComputeSplatCS", computeSplatSource);
    cincludes.set("gsplatFormatDeclCS", wbFormat.getComputeInputDeclarations(fixedBindings.length));
    cincludes.set("gsplatFormatReadCS", wbFormat.getReadCode());
    cincludes.set("gsplatProjectCommonCS", computeGsplatProjectCommonSource);
    const cdefines = /* @__PURE__ */ new Map();
    cdefines.set("{CACHE_STRIDE}", CACHE_STRIDE.toString());
    if (fisheyeEnabled) {
      cdefines.set("GSPLAT_FISHEYE", "");
    }
    if (pickMode) {
      cdefines.set("PICK_MODE", "");
    }
    const colorStream = wbFormat.getStream("dataColor");
    if (colorStream && colorStream.format !== PIXELFORMAT_RGBA16U) {
      cdefines.set("GSPLAT_COLOR_FLOAT", "");
    }
    const suffix = fisheyeEnabled ? "Fisheye" : "";
    const shader = new Shader(device, {
      name: pickMode ? `GSplatLocalTileCountPick${suffix}` : `GSplatLocalTileCount${suffix}`,
      shaderLanguage: SHADERLANGUAGE_WGSL,
      cshader: computeGsplatLocalTileCountSource,
      cincludes,
      cdefines,
      computeBindGroupFormat: bindGroupFormat,
      computeUniformBufferFormats: { uniforms: uniformBufferFormat }
    });
    return { shader, bindGroupFormat };
  }
  /**
   * Creates a dispatch set with its 8 Compute instances.
   *
   * @param {boolean} pickMode - Whether this set is for picking.
   * @returns {GSplatLocalDispatchSet} The populated dispatch set.
   * @private
   */
  _createDispatchSet(pickMode) {
    const device = this.device;
    const set = new GSplatLocalDispatchSet(device, pickMode);
    set.placeEntriesCompute = new Compute(device, this._placeEntriesShader, pickMode ? "GSplatPickPlaceEntries" : "GSplatLocalPlaceEntries");
    set.largeSplatCompute = new Compute(device, this._largeSplatShader, pickMode ? "GSplatPickLargeTileCount" : "GSplatLocalLargeTileCount");
    set.largePlaceEntriesCompute = new Compute(device, this._largePlaceEntriesShader, pickMode ? "GSplatPickLargePlaceEntries" : "GSplatLocalLargePlaceEntries");
    set.classifyCompute = new Compute(device, this._classifyShader, pickMode ? "GSplatPickClassify" : "GSplatLocalClassify");
    set.sortCompute = new Compute(device, this._sortShader, pickMode ? "GSplatPickTileSort" : "GSplatLocalTileSort");
    set.bucketSortCompute = new Compute(device, this._bucketSortShader, pickMode ? "GSplatPickBucketSort" : "GSplatLocalBucketSort");
    set.copyCompute = new Compute(device, this._copyShader, pickMode ? "GSplatPickCopy" : "GSplatLocalCopy");
    set.chunkSortCompute = new Compute(device, this._chunkSortShader, pickMode ? "GSplatPickChunkSort" : "GSplatLocalChunkSort");
    return set;
  }
}
export {
  GSplatComputeLocalRenderer
};