From 4134f5de233293d4ce575f975df9b6cde4a1d2c8 Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Thu, 26 Feb 2026 14:37:42 +0000 Subject: [PATCH] fix: Prevent setLayout from corrupting interval compaction data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setLayout synthesized a full-range interval [0, activeSplats) directly into this.intervals when no intervals existed (fully-loaded octree). This mutation leaked into GSplatIntervalCompaction.uploadIntervals, which saw intervals.length > 0 and fell into the wrong branch — mapping all splats to a single boundsIndex and losing per-node culling granularity. The result was incorrect GPU frustum culling when all octree nodes were loaded. Move the synthesis into updateSubDraws as a local variable so this.intervals is never mutated and the interval compaction correctly falls back to placementIntervals for per-node bounds mapping. Fixes regression from #8480. Made-with: Cursor --- src/scene/gsplat-unified/gsplat-info.js | 33 ++++++++++--------- .../gsplat-unified/gsplat-world-state.js | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/scene/gsplat-unified/gsplat-info.js b/src/scene/gsplat-unified/gsplat-info.js index 72c66e0776e..e7d4c6dc1e9 100644 --- a/src/scene/gsplat-unified/gsplat-info.js +++ b/src/scene/gsplat-unified/gsplat-info.js @@ -21,6 +21,9 @@ const tmpSize = new Vec2(); // Reusable buffer for sub-draw data (only grows, never shrinks) let subDrawDataArray = new Uint32Array(0); +// Temporary full-range interval used by updateSubDraws when this.intervals is empty +const _fullRangeInterval = [0, 0]; + /** * Represents a snapshot of gsplat state for rendering. This class captures all necessary data * at a point in time and should not hold references back to the source placement. All required @@ -209,17 +212,9 @@ class GSplatInfo { * * @param {number} pixelOffset - Starting pixel offset in the work buffer. * @param {number} textureSize - The work buffer texture width. - * @param {number} activeSplats - Number of active splats. */ - setLayout(pixelOffset, textureSize, activeSplats) { + setLayout(pixelOffset, textureSize) { this.pixelOffset = pixelOffset; - - // Synthesize a full-range interval when none exist, so all paths use sub-draws - if (this.intervals.length === 0) { - this.intervals[0] = 0; - this.intervals[1] = activeSplats; - } - this.updateSubDraws(textureSize); } @@ -276,16 +271,25 @@ class GSplatInfo { } /** - * Builds the sub-draw data texture from the current intervals. Each interval is split at - * row boundaries of the work buffer texture to produce axis-aligned rectangles. The result - * is a small RGBA32U texture where each texel stores the parameters for one instanced quad. - * Called once from setLayout when the work buffer texture width is known. + * Builds the sub-draw data texture from the current intervals (or a synthetic full-range + * interval when none exist). Each interval is split at row boundaries of the work buffer + * texture to produce axis-aligned rectangles stored as a small RGBA32U texture. * * @param {number} textureWidth - The work buffer texture width. */ updateSubDraws(textureWidth) { - const numIntervals = this.intervals.length / 2; + // Use a local full-range interval when none exist, so the instanced draw path + // always has sub-draws. This must NOT mutate this.intervals because the GPU + // interval compaction reads this.intervals separately for per-node culling. + let intervals = this.intervals; + let numIntervals = intervals.length / 2; + if (numIntervals === 0) { + _fullRangeInterval[0] = 0; + _fullRangeInterval[1] = this.activeSplats; + intervals = _fullRangeInterval; + numIntervals = 1; + } // Split intervals at row boundaries. Each interval produces at most 3 sub-draws: // partial first row, full middle rows, partial last row. @@ -296,7 +300,6 @@ class GSplatInfo { subDrawDataArray = new Uint32Array(requiredSize); } const subDrawData = subDrawDataArray; - const intervals = this.intervals; let subDrawCount = 0; let targetOffset = this.pixelOffset; // absolute pixel position in work buffer diff --git a/src/scene/gsplat-unified/gsplat-world-state.js b/src/scene/gsplat-unified/gsplat-world-state.js index 3b4350e5219..b314d752c68 100644 --- a/src/scene/gsplat-unified/gsplat-world-state.js +++ b/src/scene/gsplat-unified/gsplat-world-state.js @@ -106,7 +106,7 @@ class GSplatWorldState { } else { totalIntervals += 1; } - splat.setLayout(pixelOffset, this.textureSize, splat.activeSplats); + splat.setLayout(pixelOffset, this.textureSize); pixelOffset += splat.activeSplats; }