Skip to content

Commit 3fa2f0b

Browse files
mvaligurskyMartin Valigursky
andauthored
Remove per-resource instance vertex buffer from GSplat rendering (#8513)
* Remove per-resource instance vertex buffer from GSplat rendering Replace vertex_id_attrib with attributeless instancing, deriving the splat base index from gl_InstanceID / pcInstanceIndex GPU builtins. This eliminates per-resource GPU memory allocation and dynamic reallocation for instance index vertex buffers. MeshInstance.setInstancing now accepts `true` for attributeless instancing (count-only, no per-instance VB). Made-with: Cursor * update --------- Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
1 parent 51ee94c commit 3fa2f0b

File tree

8 files changed

+45
-94
lines changed

8 files changed

+45
-94
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,6 @@ class GSplatManager {
832832
const textureSize = worldState.textureSize;
833833
if (textureSize !== this.workBuffer.textureSize) {
834834
this.workBuffer.resize(textureSize);
835-
this.renderer.setMaxNumSplats(textureSize * textureSize);
836835
}
837836

838837
// Bounds and transforms textures are needed for frustum culling.

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

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import { SEMANTIC_POSITION, SEMANTIC_ATTR13, CULLFACE_NONE, PIXELFORMAT_RGBA16U } from '../../platform/graphics/constants.js';
1+
import { SEMANTIC_POSITION, CULLFACE_NONE, PIXELFORMAT_RGBA16U } from '../../platform/graphics/constants.js';
22
import {
33
BLEND_NONE, BLEND_PREMULTIPLIED, BLEND_ADDITIVE, GSPLAT_FORWARD, GSPLAT_SHADOW,
44
SHADOWCAMERA_NAME
55
} from '../constants.js';
66
import { ShaderMaterial } from '../materials/shader-material.js';
77
import { GSplatResourceBase } from '../gsplat/gsplat-resource-base.js';
88
import { MeshInstance } from '../mesh-instance.js';
9-
import { math } from '../../core/math/math.js';
109

1110
/**
12-
* @import { VertexBuffer } from '../../platform/graphics/vertex-buffer.js'
1311
* @import { StorageBuffer } from '../../platform/graphics/storage-buffer.js'
1412
* @import { Layer } from '../layer.js'
1513
* @import { GraphNode } from '../graph-node.js'
@@ -29,12 +27,6 @@ class GSplatRenderer {
2927
/** @type {MeshInstance} */
3028
meshInstance;
3129

32-
/** @type {VertexBuffer|null} */
33-
instanceIndices = null;
34-
35-
/** @type {number} */
36-
instanceIndicesCount = 0;
37-
3830
/** @type {Layer} */
3931
layer;
4032

@@ -84,11 +76,12 @@ class GSplatRenderer {
8476
vertexWGSL: '#include "gsplatVS"',
8577
fragmentWGSL: '#include "gsplatPS"',
8678
attributes: {
87-
vertex_position: SEMANTIC_POSITION,
88-
vertex_id_attrib: SEMANTIC_ATTR13
79+
vertex_position: SEMANTIC_POSITION
8980
}
9081
});
9182

83+
this._material.setDefine('{GSPLAT_INSTANCE_SIZE}', GSplatResourceBase.instanceSize);
84+
9285
this.configureMaterial();
9386

9487
// Capture internal define names to protect them from being cleared
@@ -97,6 +90,7 @@ class GSplatRenderer {
9790
});
9891

9992
// Also protect defines that may be added dynamically
93+
this._internalDefines.add('{GSPLAT_INSTANCE_SIZE}');
10094
this._internalDefines.add('GSPLAT_UNIFIED_ID');
10195
this._internalDefines.add('PICK_CUSTOM_ID');
10296
this._internalDefines.add('GSPLAT_INDIRECT_DRAW');
@@ -248,6 +242,12 @@ class GSplatRenderer {
248242
updateIndirect(textureSize) {
249243
this._material.setParameter('splatTextureSize', textureSize);
250244
this.meshInstance.visible = true;
245+
246+
// Ensure instancingCount is non-zero so the forward/shadow renderers don't
247+
// skip this draw call. The actual instance count is GPU-driven via indirect args.
248+
if (this.meshInstance.instancingCount <= 0) {
249+
this.meshInstance.instancingCount = 1;
250+
}
251251
}
252252

253253
/**
@@ -420,34 +420,12 @@ class GSplatRenderer {
420420
}
421421
}
422422

423-
setMaxNumSplats(numSplats) {
424-
425-
// round up to the nearest multiple of instanceSize (same as createInstanceIndices does internally)
426-
const roundedNumSplats = math.roundUp(numSplats, GSplatResourceBase.instanceSize);
427-
428-
if (this.instanceIndicesCount < roundedNumSplats) {
429-
this.instanceIndicesCount = roundedNumSplats;
430-
431-
// destroy old instance indices
432-
this.instanceIndices?.destroy();
433-
434-
// create new instance indices
435-
this.instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, numSplats);
436-
this.meshInstance.setInstancing(this.instanceIndices, true);
437-
438-
// update texture size uniform
439-
this._material.setParameter('splatTextureSize', this.workBuffer.textureSize);
440-
}
441-
}
442-
443423
createMeshInstance() {
444424

445425
const mesh = GSplatResourceBase.createMesh(this.device);
446-
const textureSize = this.workBuffer.textureSize;
447-
const instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, textureSize * textureSize);
448426
const meshInstance = new MeshInstance(mesh, this._material);
449427
meshInstance.node = this.node;
450-
meshInstance.setInstancing(instanceIndices, true);
428+
meshInstance.setInstancing(true, true);
451429

452430
// only start rendering the splat after we've received the splat order data
453431
meshInstance.instancingCount = 0;

src/scene/gsplat/gsplat-instance.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Debug } from '../../core/debug.js';
22
import { Mat4 } from '../../core/math/mat4.js';
33
import { Vec3 } from '../../core/math/vec3.js';
4-
import { BUFFERUSAGE_COPY_DST, CULLFACE_NONE, SEMANTIC_ATTR13, SEMANTIC_POSITION, PIXELFORMAT_R32U } from '../../platform/graphics/constants.js';
4+
import { BUFFERUSAGE_COPY_DST, CULLFACE_NONE, SEMANTIC_POSITION, PIXELFORMAT_R32U } from '../../platform/graphics/constants.js';
55
import { StorageBuffer } from '../../platform/graphics/storage-buffer.js';
66
import { MeshInstance } from '../mesh-instance.js';
77
import { GSplatResolveSH } from './gsplat-resolve-sh.js';
@@ -16,7 +16,6 @@ import { BLEND_NONE, BLEND_PREMULTIPLIED } from '../constants.js';
1616
* @import { GraphNode } from '../graph-node.js'
1717
* @import { Mesh } from '../mesh.js'
1818
* @import { Texture } from '../../platform/graphics/texture.js'
19-
* @import { VertexBuffer } from '../../platform/graphics/vertex-buffer.js'
2019
*/
2120

2221
const mat = new Mat4();
@@ -88,6 +87,7 @@ class GSplatInstance {
8887

8988
if (options.material) {
9089
this._material = options.material;
90+
this._material.setDefine('{GSPLAT_INSTANCE_SIZE}', String(GSplatResourceBase.instanceSize));
9191
this.setMaterialOrderData(this._material);
9292
} else {
9393
this._material = new ShaderMaterial({
@@ -97,8 +97,7 @@ class GSplatInstance {
9797
vertexWGSL: '#include "gsplatVS"',
9898
fragmentWGSL: '#include "gsplatPS"',
9999
attributes: {
100-
vertex_position: SEMANTIC_POSITION,
101-
vertex_id_attrib: SEMANTIC_ATTR13
100+
vertex_position: SEMANTIC_POSITION
102101
}
103102
});
104103

@@ -108,7 +107,7 @@ class GSplatInstance {
108107

109108
resource.ensureMesh();
110109
this.meshInstance = new MeshInstance(/** @type {Mesh} */ (resource.mesh), this._material);
111-
this.meshInstance.setInstancing(/** @type {VertexBuffer} */ (resource.instanceIndices), true);
110+
this.meshInstance.setInstancing(true, true);
112111
this.meshInstance.gsplatInstance = this;
113112

114113
// only start rendering the splat after we've received the splat order data
@@ -154,6 +153,7 @@ class GSplatInstance {
154153
set material(value) {
155154
if (this._material !== value) {
156155
this._material = value;
156+
this._material.setDefine('{GSPLAT_INSTANCE_SIZE}', String(GSplatResourceBase.instanceSize));
157157
this.setMaterialOrderData(this._material);
158158

159159
if (this.meshInstance) {
@@ -176,6 +176,7 @@ class GSplatInstance {
176176
configureMaterial(material, options = {}) {
177177
this.resource.configureMaterial(material, null, this.resource.format.getInputDeclarations());
178178

179+
material.setDefine('{GSPLAT_INSTANCE_SIZE}', GSplatResourceBase.instanceSize);
179180
material.setParameter('numSplats', 0);
180181
this.setMaterialOrderData(material);
181182
material.setParameter('alphaClip', 0.3);

src/scene/gsplat/gsplat-material.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {
2-
CULLFACE_NONE, SEMANTIC_ATTR13, SEMANTIC_POSITION, SHADERLANGUAGE_GLSL, SHADERLANGUAGE_WGSL
2+
CULLFACE_NONE, SEMANTIC_POSITION, SHADERLANGUAGE_GLSL, SHADERLANGUAGE_WGSL
33
} from '../../platform/graphics/constants.js';
44

55
import { BLEND_NONE, BLEND_PREMULTIPLIED, DITHER_NONE } from '../constants.js';
66
import { ShaderMaterial } from '../materials/shader-material.js';
77
import { ShaderChunks } from '../shader-lib/shader-chunks.js';
8+
import { GSplatResourceBase } from './gsplat-resource-base.js';
89

910
/**
1011
* @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js'
@@ -38,11 +39,11 @@ const createGSplatMaterial = (device, options = {}) => {
3839
vertexWGSL: options.vertex ? '' : ShaderChunks.get(device, SHADERLANGUAGE_WGSL).get('gsplatVS'),
3940
fragmentWGSL: options.vertex ? '' : ShaderChunks.get(device, SHADERLANGUAGE_WGSL).get('gsplatPS'),
4041
attributes: {
41-
vertex_position: SEMANTIC_POSITION,
42-
vertex_id_attrib: SEMANTIC_ATTR13
42+
vertex_position: SEMANTIC_POSITION
4343
}
4444
});
4545

46+
material.setDefine('{GSPLAT_INSTANCE_SIZE}', GSplatResourceBase.instanceSize);
4647
material.setDefine(`DITHER_${ditherEnum.toUpperCase()}`, '');
4748

4849
material.cull = CULLFACE_NONE;

src/scene/gsplat/gsplat-resource-base.js

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { Debug } from '../../core/debug.js';
22
import { BoundingBox } from '../../core/shape/bounding-box.js';
3-
import { BUFFER_STATIC, SEMANTIC_ATTR13, TYPE_UINT32 } from '../../platform/graphics/constants.js';
4-
import { VertexFormat } from '../../platform/graphics/vertex-format.js';
5-
import { VertexBuffer } from '../../platform/graphics/vertex-buffer.js';
63
import { Mesh } from '../mesh.js';
74
import { ShaderMaterial } from '../materials/shader-material.js';
85
import { WorkBufferRenderInfo } from '../gsplat-unified/gsplat-work-buffer.js';
@@ -61,12 +58,6 @@ class GSplatResourceBase {
6158
*/
6259
mesh = null;
6360

64-
/**
65-
* @type {VertexBuffer|null}
66-
* @ignore
67-
*/
68-
instanceIndices = null;
69-
7061
/**
7162
* @type {number}
7263
* @ignore
@@ -154,7 +145,6 @@ class GSplatResourceBase {
154145
_actualDestroy() {
155146
this.streams.destroy();
156147
this.mesh?.destroy();
157-
this.instanceIndices?.destroy();
158148
this.workBufferRenderInfos.forEach(info => info.destroy());
159149
this.workBufferRenderInfos.clear();
160150
}
@@ -203,7 +193,6 @@ class GSplatResourceBase {
203193
if (!this.mesh) {
204194
this.mesh = GSplatResourceBase.createMesh(this.device);
205195
this.mesh.aabb.copy(this.aabb);
206-
this.instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, this.gsplatData.numSplats);
207196
}
208197
this._meshRefCount++;
209198
}
@@ -218,8 +207,6 @@ class GSplatResourceBase {
218207
this._meshRefCount--;
219208
if (this._meshRefCount < 1) {
220209
this.mesh = null; // mesh instances destroy mesh when their refCount reaches zero
221-
this.instanceIndices?.destroy();
222-
this.instanceIndices = null;
223210
}
224211
}
225212

@@ -323,28 +310,6 @@ class GSplatResourceBase {
323310
return mesh;
324311
}
325312

326-
static createInstanceIndices(device, splatCount) {
327-
const splatInstanceSize = GSplatResourceBase.instanceSize;
328-
const numSplats = Math.ceil(splatCount / splatInstanceSize) * splatInstanceSize;
329-
const numSplatInstances = numSplats / splatInstanceSize;
330-
331-
const indexData = new Uint32Array(numSplatInstances);
332-
for (let i = 0; i < numSplatInstances; ++i) {
333-
indexData[i] = i * splatInstanceSize;
334-
}
335-
336-
const vertexFormat = new VertexFormat(device, [
337-
{ semantic: SEMANTIC_ATTR13, components: 1, type: TYPE_UINT32, asInt: true }
338-
]);
339-
340-
const instanceIndices = new VertexBuffer(device, vertexFormat, numSplatInstances, {
341-
usage: BUFFER_STATIC,
342-
data: indexData.buffer
343-
});
344-
345-
return instanceIndices;
346-
}
347-
348313
static get instanceSize() {
349314
return 128; // number of splats per instance
350315
}

src/scene/mesh-instance.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BoundingBox } from '../core/shape/bounding-box.js';
33
import { BoundingSphere } from '../core/shape/bounding-sphere.js';
44
import { BindGroup } from '../platform/graphics/bind-group.js';
55
import { UniformBuffer } from '../platform/graphics/uniform-buffer.js';
6+
import { VertexBuffer } from '../platform/graphics/vertex-buffer.js';
67
import { DrawCommands } from '../platform/graphics/draw-commands.js';
78
import { indexFormatByteSize } from '../platform/graphics/constants.js';
89
import {
@@ -39,7 +40,6 @@ import { PickerId } from './picker-id.js';
3940
* @import { Texture } from '../platform/graphics/texture.js'
4041
* @import { UniformBufferFormat } from '../platform/graphics/uniform-buffer-format.js'
4142
* @import { Vec3 } from '../core/math/vec3.js'
42-
* @import { VertexBuffer } from '../platform/graphics/vertex-buffer.js'
4343
* @import { CameraComponent } from '../framework/components/camera/component.js';
4444
*/
4545

@@ -1129,20 +1129,27 @@ class MeshInstance {
11291129
* Note that {@link instancingCount} is automatically set to the number of vertices of the
11301130
* vertex buffer when it is provided.
11311131
*
1132-
* @param {VertexBuffer|null} vertexBuffer - Vertex buffer to hold per-instance vertex data
1133-
* (usually world matrices). Pass null to turn off hardware instancing.
1132+
* @param {VertexBuffer|true|null} vertexBuffer - Vertex buffer to hold per-instance vertex data
1133+
* (usually world matrices). Pass `true` to enable attributeless instancing where the instance
1134+
* index is derived from `gl_InstanceID` / `instance_index` builtins rather than a vertex
1135+
* buffer attribute — the caller must set {@link instancingCount} manually. Pass null to turn
1136+
* off hardware instancing.
11341137
* @param {boolean} cull - Whether to perform frustum culling on this instance. If true, the whole
11351138
* instance will be culled by the camera frustum. This often involves setting
11361139
* {@link RenderComponent#customAabb} containing all instances. Defaults to false, which means
11371140
* the whole instance is always rendered.
11381141
*/
11391142
setInstancing(vertexBuffer, cull = false) {
11401143
if (vertexBuffer) {
1141-
this.instancingData = new InstancingData(vertexBuffer.numVertices);
1142-
this.instancingData.vertexBuffer = vertexBuffer;
1144+
if (vertexBuffer === true) {
1145+
this.instancingData = new InstancingData(0);
1146+
} else {
1147+
this.instancingData = new InstancingData(vertexBuffer.numVertices);
1148+
this.instancingData.vertexBuffer = vertexBuffer;
11431149

1144-
// mark vertex buffer as instancing data
1145-
vertexBuffer.format.instancing = true;
1150+
// mark vertex buffer as instancing data
1151+
vertexBuffer.format.instancing = true;
1152+
}
11461153

11471154
// set up culling
11481155
this.cull = cull;
@@ -1151,7 +1158,9 @@ class MeshInstance {
11511158
this.cull = true;
11521159
}
11531160

1154-
this._updateShaderDefs(vertexBuffer ? (this._shaderDefs | SHADERDEF_INSTANCING) : (this._shaderDefs & ~SHADERDEF_INSTANCING));
1161+
this._updateShaderDefs(vertexBuffer instanceof VertexBuffer ?
1162+
(this._shaderDefs | SHADERDEF_INSTANCING) :
1163+
(this._shaderDefs & ~SHADERDEF_INSTANCING));
11551164
}
11561165

11571166
/**

src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatSource.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
export default /* glsl */`
2-
attribute vec3 vertex_position; // xy: cornerUV, z: render order offset
3-
attribute uint vertex_id_attrib; // render order base
2+
attribute vec3 vertex_position; // xy: cornerUV, z: render order offset within instance
43
54
uniform uint numSplats; // total number of splats
65
uniform highp usampler2D splatOrder; // per-splat index to source gaussian
76
87
// initialize the splat source structure and global splat
98
bool initSource(out SplatSource source) {
10-
// calculate splat order
11-
source.order = vertex_id_attrib + uint(vertex_position.z);
9+
// calculate splat order from instance index and vertex position offset
10+
source.order = uint(gl_InstanceID) * {GSPLAT_INSTANCE_SIZE}u + uint(vertex_position.z);
1211
1312
// return if out of range (since the last block of splats may be partially full)
1413
if (source.order >= numSplats) {

src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatSource.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export default /* wgsl */`
2-
attribute vertex_position: vec3f; // xy: cornerUV, z: render order offset
3-
attribute vertex_id_attrib: u32; // render order base
2+
attribute vertex_position: vec3f; // xy: cornerUV, z: render order offset within instance
43
54
#ifdef GSPLAT_INDIRECT_DRAW
65
// When using indirect draw with compaction, numSplats is written by the
@@ -16,8 +15,8 @@ attribute vertex_id_attrib: u32; // render order base
1615
1716
// initialize the splat source structure
1817
fn initSource(source: ptr<function, SplatSource>) -> bool {
19-
// calculate splat order
20-
source.order = vertex_id_attrib + u32(vertex_position.z);
18+
// calculate splat order from instance index and vertex position offset
19+
source.order = pcInstanceIndex * {GSPLAT_INSTANCE_SIZE}u + u32(vertex_position.z);
2120
2221
// return if out of range (since the last block of splats may be partially full)
2322
#ifdef GSPLAT_INDIRECT_DRAW

0 commit comments

Comments
 (0)