Skip to content

Commit 78d557c

Browse files
mvaligurskyMartin Valigursky
andauthored
Per-node block allocation and partial work buffer updates for GSplats (#8490)
* Per-node block allocation and partial work buffer updates for GSplats * updates * updates * updates * update --------- Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
1 parent fec14d1 commit 78d557c

22 files changed

+878
-278
lines changed

examples/src/examples/gaussian-splatting/lod-streaming.controls.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,12 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
8989
{ v: '2M', t: '2M' },
9090
{ v: '3M', t: '3M' },
9191
{ v: '4M', t: '4M' },
92-
{ v: '6M', t: '6M' }
92+
{ v: '5M', t: '5M' },
93+
{ v: '6M', t: '6M' },
94+
{ v: '7M', t: '7M' },
95+
{ v: '8M', t: '8M' },
96+
{ v: '9M', t: '9M' },
97+
{ v: '10M', t: '10M' }
9398
]
9499
})
95100
)

examples/src/examples/gaussian-splatting/lod-streaming.example.mjs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const config = {
8181
const LOD_PRESETS = {
8282
'desktop-max': {
8383
range: [0, 5],
84-
lodDistances: [10, 20, 40, 80, 120, 150, 200]
84+
lodDistances: [7, 20, 40, 80, 120, 150, 200]
8585
},
8686
'desktop': {
8787
range: [1, 5],
@@ -119,21 +119,29 @@ assetListLoader.load(() => {
119119
app.scene.skyboxMip = 1;
120120
app.scene.exposure = 1.5;
121121

122-
// Mini-Stats: add gsplats on top of default stats
122+
// Mini-Stats: insert gsplat stats after the default 'Frame' entry
123123
const msOptions = pc.MiniStats.getDefaultOptions();
124-
msOptions.stats.push({
124+
const frameIndex = msOptions.stats.findIndex(s => s.name === 'Frame');
125+
msOptions.stats.splice(frameIndex + 1, 0, {
125126
name: 'GSplats',
126127
stats: ['frame.gsplats'],
127128
decimalPlaces: 3,
128129
multiplier: 1 / 1000000,
129130
unitsName: 'M',
130131
watermark: 10
132+
}, {
133+
name: 'GsplatsCopy',
134+
stats: ['frame.gsplatBufferCopy'],
135+
decimalPlaces: 1,
136+
multiplier: 1,
137+
unitsName: '%',
138+
watermark: 100
131139
});
132140
const miniStats = new pc.MiniStats(app, msOptions); // eslint-disable-line no-unused-vars
133141

134142
// enable rotation-based LOD updates and behind-camera penalty
135143
app.scene.gsplat.lodUpdateAngle = 90;
136-
app.scene.gsplat.lodBehindPenalty = 5;
144+
app.scene.gsplat.lodBehindPenalty = 3;
137145
app.scene.gsplat.radialSorting = true;
138146
app.scene.gsplat.lodUpdateDistance = config.lodUpdateDistance;
139147
app.scene.gsplat.lodUnderfillLimit = config.lodUnderfillLimit;
@@ -156,7 +164,7 @@ assetListLoader.load(() => {
156164

157165
// initialize UI settings (must be after observer registration)
158166
data.set('gpuSorting', false);
159-
data.set('culling', false);
167+
data.set('culling', device.isWebGPU);
160168
data.set('compact', true);
161169
data.set('debugLod', false);
162170
data.set('lodPreset', pc.platform.mobile ? 'mobile' : 'desktop');
@@ -193,7 +201,12 @@ assetListLoader.load(() => {
193201
'2M': 2000000,
194202
'3M': 3000000,
195203
'4M': 4000000,
196-
'6M': 6000000
204+
'5M': 5000000,
205+
'6M': 6000000,
206+
'7M': 7000000,
207+
'8M': 8000000,
208+
'9M': 9000000,
209+
'10M': 10000000
197210
};
198211
// Global splat budget applies to all GSplats in the scene
199212
app.scene.gsplat.splatBudget = budgetMap[preset] || 0;

examples/src/examples/gaussian-splatting/viewer.example.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ assetListLoader.load(() => {
165165
// Initialize data values
166166
data.set('data', {
167167
skydome: false,
168-
compact: true,
168+
compact: false,
169169
orientation: 180,
170170
tonemapping: pc.TONEMAP_LINEAR,
171171
grading: {

src/core/block-allocator.js

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,17 @@ class BlockAllocator {
123123
*/
124124
_tailFree = null;
125125

126+
/**
127+
* Advisory starting point for the next free-block search. Avoids repeatedly scanning past
128+
* small fragments at the start of the free list during sequential allocations. Cleared
129+
* (set to null) on free() and defrag(), causing the next search to restart from _headFree
130+
* since those operations may create earlier gaps.
131+
*
132+
* @type {MemBlock|null}
133+
* @private
134+
*/
135+
_freeHint = null;
136+
126137
/**
127138
* Pool of recycled MemBlock objects.
128139
*
@@ -164,22 +175,23 @@ class BlockAllocator {
164175
_freeRegionCount = 0;
165176

166177
/**
167-
* Minimum growth increment used by {@link BlockAllocator#updateAllocation}.
178+
* Multiplicative growth factor used by {@link BlockAllocator#updateAllocation}.
179+
* When growing, the new capacity is at least `capacity * growMultiplier`.
168180
*
169181
* @type {number}
170182
* @private
171183
*/
172-
_growSize;
184+
_growMultiplier;
173185

174186
/**
175187
* Create a new BlockAllocator.
176188
*
177189
* @param {number} [capacity] - Initial address space capacity. Defaults to 0.
178-
* @param {number} [growSize] - Minimum growth increment for auto-grow in
179-
* {@link BlockAllocator#updateAllocation}. Defaults to 1024.
190+
* @param {number} [growMultiplier] - Multiplicative growth factor for auto-grow in
191+
* {@link BlockAllocator#updateAllocation}. Defaults to 1.1 (10% extra).
180192
*/
181-
constructor(capacity = 0, growSize = 1024) {
182-
this._growSize = growSize;
193+
constructor(capacity = 0, growMultiplier = 1.1) {
194+
this._growMultiplier = growMultiplier;
183195
if (capacity > 0) {
184196
this._capacity = capacity;
185197
this._freeSize = capacity;
@@ -188,6 +200,7 @@ class BlockAllocator {
188200
this._tailAll = block;
189201
this._headFree = block;
190202
this._tailFree = block;
203+
this._freeHint = block;
191204
this._freeRegionCount = 1;
192205
}
193206
}
@@ -339,6 +352,9 @@ class BlockAllocator {
339352
* @private
340353
*/
341354
_removeFromFreeList(block) {
355+
if (this._freeHint === block) {
356+
this._freeHint = block._nextFree;
357+
}
342358
if (block._prevFree) block._prevFree._nextFree = block._nextFree;
343359
else this._headFree = block._nextFree;
344360
if (block._nextFree) block._nextFree._prevFree = block._prevFree;
@@ -349,18 +365,36 @@ class BlockAllocator {
349365
}
350366

351367
/**
352-
* Scan the free list for the first block with size >= requested.
368+
* Scan the free list for the first block with size >= requested, starting from _freeHint
369+
* to skip past small fragments already examined by recent allocations.
353370
*
354371
* @param {number} size - Minimum size needed.
355372
* @returns {MemBlock|null} The first fitting free block, or null.
356373
* @private
357374
*/
358375
_findFreeBlock(size) {
359-
let block = this._headFree;
376+
const hint = this._freeHint;
377+
let block = hint ?? this._headFree;
360378
while (block) {
361-
if (block._size >= size) return block;
379+
if (block._size >= size) {
380+
this._freeHint = block;
381+
return block;
382+
}
362383
block = block._nextFree;
363384
}
385+
386+
// Hint skipped earlier blocks — retry from head up to the hint
387+
if (hint) {
388+
block = this._headFree;
389+
while (block !== hint) {
390+
if (block._size >= size) {
391+
this._freeHint = block;
392+
return block;
393+
}
394+
block = block._nextFree;
395+
}
396+
}
397+
364398
return null;
365399
}
366400

@@ -406,6 +440,7 @@ class BlockAllocator {
406440
block._free = true;
407441
this._usedSize -= block._size;
408442
this._freeSize += block._size;
443+
this._freeHint = null;
409444

410445
const prev = block._prev;
411446
const next = block._next;
@@ -492,6 +527,7 @@ class BlockAllocator {
492527
*/
493528
defrag(maxMoves = 0, result = new Set()) {
494529
result.clear();
530+
this._freeHint = null;
495531

496532
if (this._freeRegionCount === 0) return result;
497533

@@ -724,10 +760,11 @@ class BlockAllocator {
724760
totalRemaining += /** @type {number} */ (toAllocate[j]);
725761
}
726762

727-
// Grow if needed
763+
// Grow if needed, or if free space would be below headroom threshold
728764
const neededCapacity = this._usedSize + totalRemaining;
729-
if (neededCapacity > this._capacity) {
730-
this.grow(Math.max(this._capacity + this._growSize, neededCapacity));
765+
const headroomCapacity = Math.ceil(neededCapacity * this._growMultiplier);
766+
if (headroomCapacity > this._capacity) {
767+
this.grow(headroomCapacity);
731768
}
732769

733770
// Full defrag: compact everything

src/framework/app-base.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,7 @@ class AppBase extends EventHandler {
11341134
this.graphicsDevice._drawCallsPerFrame = 0;
11351135

11361136
stats.gsplats = this.renderer._gsplatCount;
1137+
stats.gsplatBufferCopy = this.renderer._gsplatBufferCopy ?? 0;
11371138
}
11381139

11391140
/** @private */

src/framework/stats.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class ApplicationStats {
4040
triangles: 0,
4141
gsplats: 0,
4242
gsplatSort: 0,
43+
gsplatBufferCopy: 0,
4344
otherPrimitives: 0,
4445
shaders: 0,
4546
materials: 0,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { NumericIds } from '../../core/numeric-ids.js';
2+
3+
/**
4+
* Centralized allocation ID generator for gsplat work buffer allocations.
5+
* Provides unique IDs for placements and octree nodes that need persistent
6+
* allocation tracking in the block allocator.
7+
*
8+
* @type {NumericIds}
9+
* @ignore
10+
*/
11+
const GsplatAllocId = new NumericIds();
12+
13+
export { GsplatAllocId };

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,8 @@ class GSplatDirector {
260260
});
261261

262262
let gsplatCount = 0;
263+
let bufferCopyUploaded = 0;
264+
let bufferCopyTotal = 0;
263265

264266
// for all cameras in the composition
265267
const camerasComponents = comp.cameras;
@@ -311,16 +313,22 @@ class GSplatDirector {
311313
for (const layerData of cameraData.layersMap.values()) {
312314
if (layerData.gsplatManager) {
313315
gsplatCount += layerData.gsplatManager.update();
316+
bufferCopyUploaded += layerData.gsplatManager.bufferCopyUploaded;
317+
bufferCopyTotal += layerData.gsplatManager.bufferCopyTotal;
314318
}
315319
if (layerData.gsplatManagerShadow) {
316320
gsplatCount += layerData.gsplatManagerShadow.update();
321+
bufferCopyUploaded += layerData.gsplatManagerShadow.bufferCopyUploaded;
322+
bufferCopyTotal += layerData.gsplatManagerShadow.bufferCopyTotal;
317323
}
318324
}
319325
}
320326
}
321327

322328
// update stats
323329
this.renderer._gsplatCount = gsplatCount;
330+
this.renderer._gsplatBufferCopy = bufferCopyTotal > 0 ?
331+
(bufferCopyUploaded / bufferCopyTotal * 100) : 0;
324332

325333
// clear dirty flags
326334
this.scene.gsplat.frameEnd();

0 commit comments

Comments
 (0)