Skip to content

Commit aff948d

Browse files
mvaligurskyMartin Valigursky
andauthored
fix: Prevent setLayout from corrupting interval compaction data (#8485)
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 Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
1 parent 5421804 commit aff948d

File tree

2 files changed

+19
-16
lines changed

2 files changed

+19
-16
lines changed

src/scene/gsplat-unified/gsplat-info.js

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const tmpSize = new Vec2();
2121
// Reusable buffer for sub-draw data (only grows, never shrinks)
2222
let subDrawDataArray = new Uint32Array(0);
2323

24+
// Temporary full-range interval used by updateSubDraws when this.intervals is empty
25+
const _fullRangeInterval = [0, 0];
26+
2427
/**
2528
* Represents a snapshot of gsplat state for rendering. This class captures all necessary data
2629
* at a point in time and should not hold references back to the source placement. All required
@@ -209,17 +212,9 @@ class GSplatInfo {
209212
*
210213
* @param {number} pixelOffset - Starting pixel offset in the work buffer.
211214
* @param {number} textureSize - The work buffer texture width.
212-
* @param {number} activeSplats - Number of active splats.
213215
*/
214-
setLayout(pixelOffset, textureSize, activeSplats) {
216+
setLayout(pixelOffset, textureSize) {
215217
this.pixelOffset = pixelOffset;
216-
217-
// Synthesize a full-range interval when none exist, so all paths use sub-draws
218-
if (this.intervals.length === 0) {
219-
this.intervals[0] = 0;
220-
this.intervals[1] = activeSplats;
221-
}
222-
223218
this.updateSubDraws(textureSize);
224219
}
225220

@@ -276,16 +271,25 @@ class GSplatInfo {
276271
}
277272

278273
/**
279-
* Builds the sub-draw data texture from the current intervals. Each interval is split at
280-
* row boundaries of the work buffer texture to produce axis-aligned rectangles. The result
281-
* is a small RGBA32U texture where each texel stores the parameters for one instanced quad.
282-
* Called once from setLayout when the work buffer texture width is known.
274+
* Builds the sub-draw data texture from the current intervals (or a synthetic full-range
275+
* interval when none exist). Each interval is split at row boundaries of the work buffer
276+
* texture to produce axis-aligned rectangles stored as a small RGBA32U texture.
283277
*
284278
* @param {number} textureWidth - The work buffer texture width.
285279
*/
286280
updateSubDraws(textureWidth) {
287281

288-
const numIntervals = this.intervals.length / 2;
282+
// Use a local full-range interval when none exist, so the instanced draw path
283+
// always has sub-draws. This must NOT mutate this.intervals because the GPU
284+
// interval compaction reads this.intervals separately for per-node culling.
285+
let intervals = this.intervals;
286+
let numIntervals = intervals.length / 2;
287+
if (numIntervals === 0) {
288+
_fullRangeInterval[0] = 0;
289+
_fullRangeInterval[1] = this.activeSplats;
290+
intervals = _fullRangeInterval;
291+
numIntervals = 1;
292+
}
289293

290294
// Split intervals at row boundaries. Each interval produces at most 3 sub-draws:
291295
// partial first row, full middle rows, partial last row.
@@ -296,7 +300,6 @@ class GSplatInfo {
296300
subDrawDataArray = new Uint32Array(requiredSize);
297301
}
298302
const subDrawData = subDrawDataArray;
299-
const intervals = this.intervals;
300303
let subDrawCount = 0;
301304
let targetOffset = this.pixelOffset; // absolute pixel position in work buffer
302305

src/scene/gsplat-unified/gsplat-world-state.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class GSplatWorldState {
106106
} else {
107107
totalIntervals += 1;
108108
}
109-
splat.setLayout(pixelOffset, this.textureSize, splat.activeSplats);
109+
splat.setLayout(pixelOffset, this.textureSize);
110110
pixelOffset += splat.activeSplats;
111111
}
112112

0 commit comments

Comments
 (0)