From baaf6e16e1fe79b17c999094aa1231bb2e0f0012 Mon Sep 17 00:00:00 2001 From: Mike Bond Date: Fri, 27 Jun 2025 13:27:55 -0700 Subject: [PATCH 1/4] WIP support for vertex pulling --- .../src/Engines/WebGPU/webgpuCacheRenderPipeline.ts | 10 +++++----- packages/dev/core/src/Engines/abstractEngine.ts | 3 +++ packages/dev/core/src/Engines/webgpuEngine.ts | 13 ++++++++++++- packages/dev/core/src/Meshes/abstractMesh.ts | 3 +++ packages/dev/core/src/Meshes/mesh.ts | 5 +++++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/dev/core/src/Engines/WebGPU/webgpuCacheRenderPipeline.ts b/packages/dev/core/src/Engines/WebGPU/webgpuCacheRenderPipeline.ts index ac1c5dba40d..23bdd88e5e6 100644 --- a/packages/dev/core/src/Engines/WebGPU/webgpuCacheRenderPipeline.ts +++ b/packages/dev/core/src/Engines/WebGPU/webgpuCacheRenderPipeline.ts @@ -194,7 +194,7 @@ export abstract class WebGPUCacheRenderPipeline { public readonly mrtTextureArray: InternalTexture[]; public readonly mrtTextureCount: number = 0; - public getRenderPipeline(fillMode: number, effect: Effect, sampleCount: number, textureState = 0): GPURenderPipeline { + public getRenderPipeline(fillMode: number, effect: Effect, sampleCount: number, textureState = 0, useVertexPulling: boolean = false): GPURenderPipeline { sampleCount = WebGPUTextureHelper.GetSample(sampleCount); if (this.disabled) { @@ -215,7 +215,7 @@ export abstract class WebGPUCacheRenderPipeline { this._setRasterizationState(fillMode, sampleCount); this._setColorStates(); this._setDepthStencilState(); - this._setVertexState(effect); + this._setVertexState(effect, useVertexPulling); this._setTextureState(textureState); this.lastStateDirtyLowestIndex = this._stateDirtyLowestIndex; @@ -853,13 +853,13 @@ export abstract class WebGPUCacheRenderPipeline { } } - private _setVertexState(effect: Effect): void { + private _setVertexState(effect: Effect, useVertexPulling: boolean = false): void { const currStateLen = this._statesLength; let newNumStates = StatePosition.VertexState; const webgpuPipelineContext = effect._pipelineContext as WebGPUPipelineContext; - const attributes = webgpuPipelineContext.shaderProcessingContext.attributeNamesFromEffect; - const locations = webgpuPipelineContext.shaderProcessingContext.attributeLocationsFromEffect; + const attributes = useVertexPulling ? [] : webgpuPipelineContext.shaderProcessingContext.attributeNamesFromEffect; + const locations = useVertexPulling ? [] : webgpuPipelineContext.shaderProcessingContext.attributeLocationsFromEffect; let currentGPUBuffer; let numVertexBuffers = 0; diff --git a/packages/dev/core/src/Engines/abstractEngine.ts b/packages/dev/core/src/Engines/abstractEngine.ts index 53fd8bfed93..fe482c5f5a3 100644 --- a/packages/dev/core/src/Engines/abstractEngine.ts +++ b/packages/dev/core/src/Engines/abstractEngine.ts @@ -225,6 +225,9 @@ export abstract class AbstractEngine { /** @internal */ public _videoTextureSupported: boolean; + /** @internal */ + public _useVertexPulling: boolean = false; + protected _compatibilityMode = true; /** @internal */ public _pointerLockRequested: boolean; diff --git a/packages/dev/core/src/Engines/webgpuEngine.ts b/packages/dev/core/src/Engines/webgpuEngine.ts index 14bf08a80d1..0123763586b 100644 --- a/packages/dev/core/src/Engines/webgpuEngine.ts +++ b/packages/dev/core/src/Engines/webgpuEngine.ts @@ -320,6 +320,8 @@ export class WebGPUEngine extends ThinWebGPUEngine { private _commandBuffers: GPUCommandBuffer[] = [null as any, null as any]; + private _dummyVertexBuffer: DataBuffer; + // Frame Buffer Life Cycle (recreated for each render target pass) private _mainRenderPassWrapper: IWebGPURenderPassWrapper = { @@ -3692,7 +3694,9 @@ export class WebGPUEngine extends ThinWebGPUEngine { this._currentMaterialContext.textureState = textureState; - const pipeline = this._cacheRenderPipeline.getRenderPipeline(fillMode, this._currentEffect!, this.currentSampleCount, textureState); + // If vertex pulling, get a cached pipeline with empty vertex layout + // Pass a boolean here to getRenderPipeline that will cause the vertex layout to be empty + const pipeline = this._cacheRenderPipeline.getRenderPipeline(fillMode, this._currentEffect!, this.currentSampleCount, textureState, this._useVertexPulling); const bindGroups = this._cacheBindGroups.getBindGroups(webgpuPipelineContext, this._currentDrawContext, this._currentMaterialContext); if (!this._snapshotRendering.record) { @@ -3719,6 +3723,13 @@ export class WebGPUEngine extends ThinWebGPUEngine { ); } + // If vertex pulling, bind a cached empty vertex buffer + if (this._useVertexPulling) { + if (!this._dummyVertexBuffer) { + this._dummyVertexBuffer = this.createVertexBuffer(new Float32Array(0), false, "DummyVertexPullingBuffer"); + } + renderPass2.setVertexBuffer(0, this._dummyVertexBuffer.underlyingResource, 0, 0); + } const vertexBuffers = this._cacheRenderPipeline.vertexBuffers; for (let index = 0; index < vertexBuffers.length; index++) { const vertexBuffer = vertexBuffers[index]; diff --git a/packages/dev/core/src/Meshes/abstractMesh.ts b/packages/dev/core/src/Meshes/abstractMesh.ts index 3095e2786ac..f68e0e92e36 100644 --- a/packages/dev/core/src/Meshes/abstractMesh.ts +++ b/packages/dev/core/src/Meshes/abstractMesh.ts @@ -956,6 +956,9 @@ export abstract class AbstractMesh extends TransformNode implements IDisposable, /** @internal */ public _unIndexed = false; + /** @internal */ + public _useVertexPulling = false; + /** @internal */ public _lightSources = new Array(); diff --git a/packages/dev/core/src/Meshes/mesh.ts b/packages/dev/core/src/Meshes/mesh.ts index 5024fabf7fa..f4f8becf58a 100644 --- a/packages/dev/core/src/Meshes/mesh.ts +++ b/packages/dev/core/src/Meshes/mesh.ts @@ -2061,6 +2061,11 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData { } else if (fillMode == Material.WireFrameFillMode) { // Triangles as wireframe engine.drawElementsType(fillMode, 0, subMesh._linesIndexCount, this.forcedInstanceCount || instancesCount); + } else if (this._useVertexPulling) { + // We're rendering the number of indices in the index buffer but the vertex shader is handling the data itself. + engine._useVertexPulling = true; + engine.drawArraysType(fillMode, subMesh.indexStart, subMesh.indexCount, this.forcedInstanceCount || instancesCount); + engine._useVertexPulling = false; } else { engine.drawElementsType(fillMode, subMesh.indexStart, subMesh.indexCount, this.forcedInstanceCount || instancesCount); } From 12d9fcfdd9794b03d321140cd980ad1217f3e852 Mon Sep 17 00:00:00 2001 From: Mike Bond Date: Fri, 27 Jun 2025 13:52:00 -0700 Subject: [PATCH 2/4] Undo some unnecessary logic for vertex pulling --- .../src/Engines/WebGPU/webgpuCacheRenderPipeline.ts | 10 +++++----- packages/dev/core/src/Engines/abstractEngine.ts | 3 --- packages/dev/core/src/Engines/webgpuEngine.ts | 13 +------------ packages/dev/core/src/Meshes/mesh.ts | 2 -- 4 files changed, 6 insertions(+), 22 deletions(-) diff --git a/packages/dev/core/src/Engines/WebGPU/webgpuCacheRenderPipeline.ts b/packages/dev/core/src/Engines/WebGPU/webgpuCacheRenderPipeline.ts index 23bdd88e5e6..ac1c5dba40d 100644 --- a/packages/dev/core/src/Engines/WebGPU/webgpuCacheRenderPipeline.ts +++ b/packages/dev/core/src/Engines/WebGPU/webgpuCacheRenderPipeline.ts @@ -194,7 +194,7 @@ export abstract class WebGPUCacheRenderPipeline { public readonly mrtTextureArray: InternalTexture[]; public readonly mrtTextureCount: number = 0; - public getRenderPipeline(fillMode: number, effect: Effect, sampleCount: number, textureState = 0, useVertexPulling: boolean = false): GPURenderPipeline { + public getRenderPipeline(fillMode: number, effect: Effect, sampleCount: number, textureState = 0): GPURenderPipeline { sampleCount = WebGPUTextureHelper.GetSample(sampleCount); if (this.disabled) { @@ -215,7 +215,7 @@ export abstract class WebGPUCacheRenderPipeline { this._setRasterizationState(fillMode, sampleCount); this._setColorStates(); this._setDepthStencilState(); - this._setVertexState(effect, useVertexPulling); + this._setVertexState(effect); this._setTextureState(textureState); this.lastStateDirtyLowestIndex = this._stateDirtyLowestIndex; @@ -853,13 +853,13 @@ export abstract class WebGPUCacheRenderPipeline { } } - private _setVertexState(effect: Effect, useVertexPulling: boolean = false): void { + private _setVertexState(effect: Effect): void { const currStateLen = this._statesLength; let newNumStates = StatePosition.VertexState; const webgpuPipelineContext = effect._pipelineContext as WebGPUPipelineContext; - const attributes = useVertexPulling ? [] : webgpuPipelineContext.shaderProcessingContext.attributeNamesFromEffect; - const locations = useVertexPulling ? [] : webgpuPipelineContext.shaderProcessingContext.attributeLocationsFromEffect; + const attributes = webgpuPipelineContext.shaderProcessingContext.attributeNamesFromEffect; + const locations = webgpuPipelineContext.shaderProcessingContext.attributeLocationsFromEffect; let currentGPUBuffer; let numVertexBuffers = 0; diff --git a/packages/dev/core/src/Engines/abstractEngine.ts b/packages/dev/core/src/Engines/abstractEngine.ts index fe482c5f5a3..53fd8bfed93 100644 --- a/packages/dev/core/src/Engines/abstractEngine.ts +++ b/packages/dev/core/src/Engines/abstractEngine.ts @@ -225,9 +225,6 @@ export abstract class AbstractEngine { /** @internal */ public _videoTextureSupported: boolean; - /** @internal */ - public _useVertexPulling: boolean = false; - protected _compatibilityMode = true; /** @internal */ public _pointerLockRequested: boolean; diff --git a/packages/dev/core/src/Engines/webgpuEngine.ts b/packages/dev/core/src/Engines/webgpuEngine.ts index 0123763586b..14bf08a80d1 100644 --- a/packages/dev/core/src/Engines/webgpuEngine.ts +++ b/packages/dev/core/src/Engines/webgpuEngine.ts @@ -320,8 +320,6 @@ export class WebGPUEngine extends ThinWebGPUEngine { private _commandBuffers: GPUCommandBuffer[] = [null as any, null as any]; - private _dummyVertexBuffer: DataBuffer; - // Frame Buffer Life Cycle (recreated for each render target pass) private _mainRenderPassWrapper: IWebGPURenderPassWrapper = { @@ -3694,9 +3692,7 @@ export class WebGPUEngine extends ThinWebGPUEngine { this._currentMaterialContext.textureState = textureState; - // If vertex pulling, get a cached pipeline with empty vertex layout - // Pass a boolean here to getRenderPipeline that will cause the vertex layout to be empty - const pipeline = this._cacheRenderPipeline.getRenderPipeline(fillMode, this._currentEffect!, this.currentSampleCount, textureState, this._useVertexPulling); + const pipeline = this._cacheRenderPipeline.getRenderPipeline(fillMode, this._currentEffect!, this.currentSampleCount, textureState); const bindGroups = this._cacheBindGroups.getBindGroups(webgpuPipelineContext, this._currentDrawContext, this._currentMaterialContext); if (!this._snapshotRendering.record) { @@ -3723,13 +3719,6 @@ export class WebGPUEngine extends ThinWebGPUEngine { ); } - // If vertex pulling, bind a cached empty vertex buffer - if (this._useVertexPulling) { - if (!this._dummyVertexBuffer) { - this._dummyVertexBuffer = this.createVertexBuffer(new Float32Array(0), false, "DummyVertexPullingBuffer"); - } - renderPass2.setVertexBuffer(0, this._dummyVertexBuffer.underlyingResource, 0, 0); - } const vertexBuffers = this._cacheRenderPipeline.vertexBuffers; for (let index = 0; index < vertexBuffers.length; index++) { const vertexBuffer = vertexBuffers[index]; diff --git a/packages/dev/core/src/Meshes/mesh.ts b/packages/dev/core/src/Meshes/mesh.ts index f4f8becf58a..178ee91d42a 100644 --- a/packages/dev/core/src/Meshes/mesh.ts +++ b/packages/dev/core/src/Meshes/mesh.ts @@ -2063,9 +2063,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData { engine.drawElementsType(fillMode, 0, subMesh._linesIndexCount, this.forcedInstanceCount || instancesCount); } else if (this._useVertexPulling) { // We're rendering the number of indices in the index buffer but the vertex shader is handling the data itself. - engine._useVertexPulling = true; engine.drawArraysType(fillMode, subMesh.indexStart, subMesh.indexCount, this.forcedInstanceCount || instancesCount); - engine._useVertexPulling = false; } else { engine.drawElementsType(fillMode, subMesh.indexStart, subMesh.indexCount, this.forcedInstanceCount || instancesCount); } From 8e92eedb1440db6d0c889877f812a25ed2e3c309 Mon Sep 17 00:00:00 2001 From: Mike Bond Date: Fri, 11 Jul 2025 11:34:29 -0700 Subject: [PATCH 3/4] Move vertex pulling flag to material --- packages/dev/core/src/Materials/material.ts | 6 ++++++ packages/dev/core/src/Meshes/abstractMesh.ts | 3 --- packages/dev/core/src/Meshes/mesh.ts | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/dev/core/src/Materials/material.ts b/packages/dev/core/src/Materials/material.ts index e45c8ae8e49..3b21e560bc8 100644 --- a/packages/dev/core/src/Materials/material.ts +++ b/packages/dev/core/src/Materials/material.ts @@ -239,6 +239,12 @@ export class Material implements IAnimatable, IClipPlanesHolder { protected _forceGLSL = false; + /** + * Tells the engine to draw geometry using vertex pulling instead of index drawing. This will automatically + * set the vertex buffers as storage buffers and make them accessible to the vertex shader. + */ + public useVertexPulling = false; + /** @internal */ public get _supportGlowLayer() { return false; diff --git a/packages/dev/core/src/Meshes/abstractMesh.ts b/packages/dev/core/src/Meshes/abstractMesh.ts index f68e0e92e36..3095e2786ac 100644 --- a/packages/dev/core/src/Meshes/abstractMesh.ts +++ b/packages/dev/core/src/Meshes/abstractMesh.ts @@ -956,9 +956,6 @@ export abstract class AbstractMesh extends TransformNode implements IDisposable, /** @internal */ public _unIndexed = false; - /** @internal */ - public _useVertexPulling = false; - /** @internal */ public _lightSources = new Array(); diff --git a/packages/dev/core/src/Meshes/mesh.ts b/packages/dev/core/src/Meshes/mesh.ts index 178ee91d42a..949512d3b70 100644 --- a/packages/dev/core/src/Meshes/mesh.ts +++ b/packages/dev/core/src/Meshes/mesh.ts @@ -2054,6 +2054,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData { const scene = this.getScene(); const engine = scene.getEngine(); + const material = subMesh.getMaterial(); if ((this._unIndexed && fillMode !== Material.WireFrameFillMode) || fillMode == Material.PointFillMode) { // or triangles as points @@ -2061,7 +2062,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData { } else if (fillMode == Material.WireFrameFillMode) { // Triangles as wireframe engine.drawElementsType(fillMode, 0, subMesh._linesIndexCount, this.forcedInstanceCount || instancesCount); - } else if (this._useVertexPulling) { + } else if (material && material.useVertexPulling) { // We're rendering the number of indices in the index buffer but the vertex shader is handling the data itself. engine.drawArraysType(fillMode, subMesh.indexStart, subMesh.indexCount, this.forcedInstanceCount || instancesCount); } else { From bc30fcee07e40118830085cd99bb17dfd9095630 Mon Sep 17 00:00:00 2001 From: Mike Bond Date: Fri, 11 Jul 2025 18:33:49 -0700 Subject: [PATCH 4/4] Bind vertex buffers for vertex pulling automatically --- packages/dev/core/src/Engines/webgpuEngine.ts | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/dev/core/src/Engines/webgpuEngine.ts b/packages/dev/core/src/Engines/webgpuEngine.ts index 14bf08a80d1..32d14825b87 100644 --- a/packages/dev/core/src/Engines/webgpuEngine.ts +++ b/packages/dev/core/src/Engines/webgpuEngine.ts @@ -1613,7 +1613,11 @@ export class WebGPUEngine extends ThinWebGPUEngine { view = data; } - const dataBuffer = this._bufferManager.createBuffer(view, WebGPUConstants.BufferUsage.Vertex | WebGPUConstants.BufferUsage.CopyDst, label); + const dataBuffer = this._bufferManager.createBuffer( + view, + WebGPUConstants.BufferUsage.Vertex | WebGPUConstants.BufferUsage.CopyDst | WebGPUConstants.BufferUsage.Storage, + label + ); return dataBuffer; } @@ -3693,6 +3697,35 @@ export class WebGPUEngine extends ThinWebGPUEngine { this._currentMaterialContext.textureState = textureState; const pipeline = this._cacheRenderPipeline.getRenderPipeline(fillMode, this._currentEffect!, this.currentSampleCount, textureState); + + // Compare the vertex buffers that we have to the ones that are bound to the pipeline. + // If there are vertex buffers that are not bound to the pipeline, AND they're used + // by the shader, we will bind them to the current draw context. + const availableVertexBuffers: { [key: string]: VertexBuffer } = (this._cacheRenderPipeline as any)._vertexBuffers; + const appliedVertexBuffers = this._cacheRenderPipeline.vertexBuffers; + const vertexBufferNames = Object.keys(availableVertexBuffers); + if (Object.keys(availableVertexBuffers).length !== appliedVertexBuffers.length) { + const unboundVertexBuffers: { [key: string]: VertexBuffer } = {}; + for (let i = 0; i < vertexBufferNames.length; i++) { + const name = vertexBufferNames[i]; + if (appliedVertexBuffers.findIndex((v) => v === availableVertexBuffers[name]) === -1) { + unboundVertexBuffers[name] = availableVertexBuffers[name]; + } + } + if (Object.keys(unboundVertexBuffers).length > 0) { + for (const unboundVertexBufferName of Object.keys(unboundVertexBuffers)) { + if (webgpuPipelineContext.shaderProcessingContext.bufferNames.findIndex((name) => name === unboundVertexBufferName) !== -1) { + this._currentDrawContext.buffers[unboundVertexBufferName] = unboundVertexBuffers[unboundVertexBufferName].effectiveBuffer as WebGPUDataBuffer; + } + } + } + // TODO - handle binding index buffer. + // if (webgpuPipelineContext.shaderProcessingContext.bufferNames.findIndex((name) => name === "indices") !== -1) { + // const indexBuffer = this._currentIndexBuffer ? this._currentIndexBuffer : (this._cacheRenderPipeline as any)._indexBuffer; + // this._currentDrawContext.buffers["indices"] = indexBuffer as WebGPUDataBuffer; + // } + } + const bindGroups = this._cacheBindGroups.getBindGroups(webgpuPipelineContext, this._currentDrawContext, this._currentMaterialContext); if (!this._snapshotRendering.record) {