Skip to content

Support vertex pulling #16826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/dev/core/src/Engines/IMaterialContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @internal */
export interface IMaterialContext {
uniqueId: number;
useVertexPulling: boolean;

reset(): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ export abstract class WebGPUCacheRenderPipeline {
private _depthStencilState: number;
private _vertexBuffers: Nullable<{ [key: string]: Nullable<VertexBuffer> }>;
private _overrideVertexBuffers: Nullable<{ [key: string]: Nullable<VertexBuffer> }>;
private _indexBuffer: Nullable<DataBuffer>;
private _textureState: number;
private _useTextureStage: boolean;

Expand Down Expand Up @@ -185,6 +184,7 @@ export abstract class WebGPUCacheRenderPipeline {
protected abstract _setRenderPipeline(param: { token: any; pipeline: Nullable<GPURenderPipeline> }): void;

public readonly vertexBuffers: VertexBuffer[];
public readonly indexBuffer: Nullable<DataBuffer>;

public get colorFormats(): (GPUTextureFormat | null)[] {
return this._mrtAttachments > 0 ? this._mrtFormats : this._webgpuColorFormat;
Expand Down Expand Up @@ -500,7 +500,7 @@ export abstract class WebGPUCacheRenderPipeline {
): void {
this._vertexBuffers = vertexBuffers;
this._overrideVertexBuffers = overrideVertexBuffers;
this._indexBuffer = indexBuffer;
(this.indexBuffer as Nullable<DataBuffer>) = indexBuffer;
}

private static _GetTopology(fillMode: number): GPUPrimitiveTopology {
Expand Down Expand Up @@ -1116,7 +1116,7 @@ export abstract class WebGPUCacheRenderPipeline {

let stripIndexFormat: GPUIndexFormat | undefined = undefined;
if (topology === WebGPUConstants.PrimitiveTopology.LineStrip || topology === WebGPUConstants.PrimitiveTopology.TriangleStrip) {
stripIndexFormat = !this._indexBuffer || this._indexBuffer.is32Bits ? WebGPUConstants.IndexFormat.Uint32 : WebGPUConstants.IndexFormat.Uint16;
stripIndexFormat = !this.indexBuffer || this.indexBuffer.is32Bits ? WebGPUConstants.IndexFormat.Uint32 : WebGPUConstants.IndexFormat.Uint16;
}

const depthStencilFormatHasStencil = this._webgpuDepthStencilFormat ? WebGPUTextureHelper.HasStencilAspect(this._webgpuDepthStencilFormat) : false;
Expand Down
62 changes: 61 additions & 1 deletion packages/dev/core/src/Engines/WebGPU/webgpuDrawContext.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { VertexBuffer } from "../../Buffers/buffer";
import type { DataBuffer } from "../../Buffers/dataBuffer";
import type { WebGPUDataBuffer } from "../../Meshes/WebGPU/webgpuDataBuffer";
import type { Nullable } from "../../types";
import type { IDrawContext } from "../IDrawContext";
import type { WebGPUBufferManager } from "./webgpuBufferManager";
import type { WebGPUPipelineContext } from "./webgpuPipelineContext";
import * as WebGPUConstants from "./webgpuConstants";

/**
Expand Down Expand Up @@ -36,6 +39,7 @@ export class WebGPUDrawContext implements IDrawContext {
private _currentInstanceCount: number;
private _isDirty: boolean;
private _enableIndirectDraw: boolean;
private _vertexPullingEnabled: boolean;

/**
* Checks if the draw context is dirty.
Expand Down Expand Up @@ -101,13 +105,19 @@ export class WebGPUDrawContext implements IDrawContext {
/**
* Creates a new WebGPUDrawContext.
* @param bufferManager The buffer manager used to manage WebGPU buffers.
* @param _dummyIndexBuffer A dummy index buffer to be bound as the "indices"
* storage buffer when no index buffer is provided.
*/
constructor(bufferManager: WebGPUBufferManager) {
constructor(
bufferManager: WebGPUBufferManager,
private _dummyIndexBuffer: WebGPUDataBuffer
) {
this._bufferManager = bufferManager;
this.uniqueId = WebGPUDrawContext._Counter++;
this._useInstancing = false;
this._currentInstanceCount = 0;
this._enableIndirectDraw = false;
this._vertexPullingEnabled = false;
this.reset();
}

Expand All @@ -117,6 +127,7 @@ export class WebGPUDrawContext implements IDrawContext {
this._materialContextUpdateId = 0;
this.fastBundle = undefined;
this.bindGroups = undefined;
this._vertexPullingEnabled = false;
}

/**
Expand Down Expand Up @@ -146,6 +157,55 @@ export class WebGPUDrawContext implements IDrawContext {
this._bufferManager.setRawData(this.indirectDrawBuffer, 0, this._indirectDrawData, 0, 20);
}

public setVertexPulling(
useVertexPulling: boolean,
webgpuPipelineContext: WebGPUPipelineContext,
vertexBuffers: { [key: string]: Nullable<VertexBuffer> },
indexBuffer: Nullable<DataBuffer>,
overrideVertexBuffers: Nullable<{ [kind: string]: Nullable<VertexBuffer> }>
): void {
if (this._vertexPullingEnabled === useVertexPulling) {
return;
}

this._vertexPullingEnabled = useVertexPulling;
this._isDirty = true;

const bufferNames = webgpuPipelineContext.shaderProcessingContext.bufferNames;

if (overrideVertexBuffers) {
for (const attributeName in overrideVertexBuffers) {
const vertexBuffer = overrideVertexBuffers[attributeName];
if (!vertexBuffer || bufferNames.indexOf(attributeName) === -1) {
continue;
}

const buffer = vertexBuffer.effectiveBuffer as Nullable<WebGPUDataBuffer>;

this.setBuffer(attributeName, useVertexPulling ? buffer : null);
}
}

for (const attributeName in vertexBuffers) {
if (overrideVertexBuffers && attributeName in overrideVertexBuffers) {
continue;
}

const vertexBuffer = vertexBuffers[attributeName];
if (!vertexBuffer || bufferNames.indexOf(attributeName) === -1) {
continue;
}

const buffer = vertexBuffer.effectiveBuffer as Nullable<WebGPUDataBuffer>;

this.setBuffer(attributeName, useVertexPulling ? buffer : null);
}

if (bufferNames.indexOf("indices") !== -1) {
this.setBuffer("indices", !useVertexPulling ? null : ((indexBuffer as WebGPUDataBuffer) ?? this._dummyIndexBuffer));
}
}

public dispose(): void {
if (this.indirectDrawBuffer) {
this._bufferManager.releaseBuffer(this.indirectDrawBuffer);
Expand Down
2 changes: 2 additions & 0 deletions packages/dev/core/src/Engines/WebGPU/webgpuMaterialContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export class WebGPUMaterialContext implements IMaterialContext {
// There's the same problem with depth textures, where "float" filtering is not supported either.
public textureState: number;

public useVertexPulling = false;

public get forceBindGroupCreation() {
// If there is at least one external texture to bind, we must recreate the bind groups each time
// because we need to retrieve a new texture each frame (by calling device.importExternalTexture)
Expand Down
2 changes: 2 additions & 0 deletions packages/dev/core/src/Engines/abstractEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ export abstract class AbstractEngine {
protected _cachedViewport: Nullable<IViewportLike>;
/** @internal */
public _currentDrawContext: IDrawContext;
/** @internal */
public _currentMaterialContext: IMaterialContext;

/** @internal */
protected _boundTexturesCache: { [key: string]: Nullable<InternalTexture> } = {};
Expand Down
2 changes: 0 additions & 2 deletions packages/dev/core/src/Engines/thinEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,6 @@ export class ThinEngine extends AbstractEngine {

// Cache

/** @internal */
public _currentMaterialContext: IMaterialContext;
/** @internal */
protected _currentProgram: Nullable<WebGLProgram>;
private _vertexAttribArraysEnabled: boolean[] = [];
Expand Down
40 changes: 33 additions & 7 deletions packages/dev/core/src/Engines/webgpuEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,11 @@ export class WebGPUEngine extends ThinWebGPUEngine {
/** @internal */
public override _currentDrawContext: WebGPUDrawContext;
/** @internal */
public _currentMaterialContext: WebGPUMaterialContext;
public override _currentMaterialContext: WebGPUMaterialContext;
private _currentVertexBuffers: { [key: string]: Nullable<VertexBuffer> } = {};
private _currentOverrideVertexBuffers: Nullable<{ [key: string]: Nullable<VertexBuffer> }> = null;
private _currentIndexBuffer: Nullable<DataBuffer> = null;
private _dummyIndexBuffer: WebGPUDataBuffer;
private _colorWriteLocal = true;
private _forceEnableEffect = false;

Expand Down Expand Up @@ -785,6 +787,12 @@ export class WebGPUEngine extends ThinWebGPUEngine {
label: "EmptyVertexBuffer",
});

this._dummyIndexBuffer = this._bufferManager.createBuffer(
new Uint16Array([0, 0, 0, 0]),
WebGPUConstants.BufferUsage.Storage | WebGPUConstants.BufferUsage.CopyDst,
"DummyIndices"
);

this._cacheRenderPipeline = new WebGPUCacheRenderPipelineTree(this._device, this._emptyVertexBuffer);

this._depthCullingState = new WebGPUDepthCullingState(this._cacheRenderPipeline);
Expand Down Expand Up @@ -1618,7 +1626,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;
}

Expand Down Expand Up @@ -1662,7 +1674,11 @@ export class WebGPUEngine extends ThinWebGPUEngine {
}
}

const dataBuffer = this._bufferManager.createBuffer(view, WebGPUConstants.BufferUsage.Index | WebGPUConstants.BufferUsage.CopyDst, label);
const dataBuffer = this._bufferManager.createBuffer(
view,
WebGPUConstants.BufferUsage.Index | WebGPUConstants.BufferUsage.CopyDst | WebGPUConstants.BufferUsage.Storage,
label
);
dataBuffer.is32Bits = is32Bits;
return dataBuffer;
}
Expand Down Expand Up @@ -1789,18 +1805,19 @@ export class WebGPUEngine extends ThinWebGPUEngine {
* Bind a list of vertex buffers with the engine
* @param vertexBuffers defines the list of vertex buffers to bind
* @param indexBuffer defines the index buffer to bind
* @param effect defines the effect associated with the vertex buffers
* @param _effect defines the effect associated with the vertex buffers
* @param overrideVertexBuffers defines optional list of avertex buffers that overrides the entries in vertexBuffers
*/
public bindBuffers(
vertexBuffers: { [key: string]: Nullable<VertexBuffer> },
indexBuffer: Nullable<DataBuffer>,
effect: Effect,
_effect: Effect,
overrideVertexBuffers?: { [kind: string]: Nullable<VertexBuffer> }
): void {
this._currentVertexBuffers = vertexBuffers;
this._currentIndexBuffer = indexBuffer;
this._currentOverrideVertexBuffers = overrideVertexBuffers ?? null;
this._cacheRenderPipeline.setBuffers(vertexBuffers, indexBuffer, this._currentOverrideVertexBuffers);
this._cacheRenderPipeline.setBuffers(this._currentVertexBuffers, this._currentIndexBuffer, this._currentOverrideVertexBuffers);
}

/**
Expand Down Expand Up @@ -2098,7 +2115,7 @@ export class WebGPUEngine extends ThinWebGPUEngine {
* @returns the new context
*/
public createDrawContext(): WebGPUDrawContext | undefined {
return new WebGPUDrawContext(this._bufferManager);
return new WebGPUDrawContext(this._bufferManager, this._dummyIndexBuffer);
}

/**
Expand Down Expand Up @@ -3649,6 +3666,14 @@ export class WebGPUEngine extends ThinWebGPUEngine {

this.bindUniformBufferBase(this._currentRenderTarget ? this._ubInvertY : this._ubDontInvertY, 0, WebGPUShaderProcessor.InternalsUBOName);

this._currentDrawContext.setVertexPulling(
this._currentMaterialContext.useVertexPulling,
webgpuPipelineContext,
this._currentVertexBuffers,
this._cacheRenderPipeline.indexBuffer, // don't use this._currentIndexBuffer, it will have been set to null by _drawArraysType!
this._currentOverrideVertexBuffers
);

if (webgpuPipelineContext.uniformBuffer) {
webgpuPipelineContext.uniformBuffer.update();
this.bindUniformBufferBase(webgpuPipelineContext.uniformBuffer.getBuffer()!, 0, WebGPUShaderProcessor.LeftOvertUBOName);
Expand Down Expand Up @@ -3702,6 +3727,7 @@ export class WebGPUEngine extends ThinWebGPUEngine {
this._currentMaterialContext.textureState = textureState;

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) {
Expand Down
21 changes: 13 additions & 8 deletions packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export class PBRMaterialDefines extends MaterialDefines implements IImageProcess
public DECAL_AFTER_DETAIL = false;

public DEBUGMODE = 0;
public USE_VERTEX_PULLING = false;

/**
* Initializes the PBR Material defines.
Expand Down Expand Up @@ -1275,7 +1276,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {

const previousEffect = subMesh.effect;
const lightDisposed = defines._areLightsDisposed;
let effect = this._prepareEffect(mesh, defines, this.onCompiled, this.onError, useInstances, null, subMesh.getRenderingMesh().hasThinInstances);
let effect = this._prepareEffect(mesh, subMesh.getRenderingMesh(), defines, this.onCompiled, this.onError, useInstances, null);

let forceWasNotReadyPreviously = false;

Expand Down Expand Up @@ -1331,14 +1332,14 @@ export abstract class PBRBaseMaterial extends PushMaterial {

private _prepareEffect(
mesh: AbstractMesh,
renderingMesh: AbstractMesh,
defines: PBRMaterialDefines,
onCompiled: Nullable<(effect: Effect) => void> = null,
onError: Nullable<(effect: Effect, errors: string) => void> = null,
useInstances: Nullable<boolean> = null,
useClipPlane: Nullable<boolean> = null,
useThinInstances: boolean
useClipPlane: Nullable<boolean> = null
): Nullable<Effect> {
this._prepareDefines(mesh, defines, useInstances, useClipPlane, useThinInstances);
this._prepareDefines(mesh, renderingMesh, defines, useInstances, useClipPlane);

if (!defines.isDirty) {
return null;
Expand Down Expand Up @@ -1646,11 +1647,13 @@ export abstract class PBRBaseMaterial extends PushMaterial {

private _prepareDefines(
mesh: AbstractMesh,
renderingMesh: AbstractMesh,
defines: PBRMaterialDefines,
useInstances: Nullable<boolean> = null,
useClipPlane: Nullable<boolean> = null,
useThinInstances: boolean = false
useClipPlane: Nullable<boolean> = null
): void {
const useThinInstances = renderingMesh.hasThinInstances;

const scene = this.getScene();
const engine = scene.getEngine();

Expand Down Expand Up @@ -2005,7 +2008,9 @@ export abstract class PBRBaseMaterial extends PushMaterial {
this.fogEnabled,
this.needAlphaTestingForMesh(mesh),
defines,
this._applyDecalMapAfterDetailMap
this._applyDecalMapAfterDetailMap,
this._useVertexPulling,
renderingMesh
);
defines.UNLIT = this._unlit || ((this.pointsCloud || this.wireframe) && !mesh.isVerticesDataPresent(VertexBuffer.NormalKind));
defines.DEBUGMODE = this._debugMode;
Expand Down Expand Up @@ -2049,7 +2054,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
return;
}
const defines = new PBRMaterialDefines(this._eventInfo.defineNames);
const effect = this._prepareEffect(mesh, defines, undefined, undefined, localOptions.useInstances, localOptions.clipPlane, mesh.hasThinInstances)!;
const effect = this._prepareEffect(mesh, mesh, defines, undefined, undefined, localOptions.useInstances, localOptions.clipPlane)!;
if (this._onEffectCreatedObservable) {
onCreatedEffectParameters.effect = effect;
onCreatedEffectParameters.subMesh = null;
Expand Down
27 changes: 26 additions & 1 deletion packages/dev/core/src/Materials/material.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { BindSceneUniformBuffer } from "./materialHelper.functions";
import { SerializationHelper } from "../Misc/decorators.serialization";
import { ShaderLanguage } from "./shaderLanguage";
import type { IAssetContainer } from "core/IAssetContainer";
import { IsWrapper } from "./drawWrapper.functions";

declare let BABYLON: any;

Expand Down Expand Up @@ -239,6 +240,24 @@ export class Material implements IAnimatable, IClipPlanesHolder {

protected _forceGLSL = false;

protected _useVertexPulling = 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 (WebGPU only).
*/
public get useVertexPulling() {
return this._useVertexPulling;
}

public set useVertexPulling(value: boolean) {
if (this._useVertexPulling === value) {
return;
}

this._useVertexPulling = value;
this.markAsDirty(Material.MiscDirtyFlag);
}

/** @internal */
public get _supportGlowLayer() {
return false;
Expand Down Expand Up @@ -1292,7 +1311,13 @@ export class Material implements IAnimatable, IClipPlanesHolder {
const orientation = overrideOrientation == null ? this.sideOrientation : overrideOrientation;
const reverse = orientation === Material.ClockWiseSideOrientation;

engine.enableEffect(effect ? effect : this._getDrawWrapper());
const effectiveDrawWrapper = effect ? effect : this._getDrawWrapper();

if (IsWrapper(effectiveDrawWrapper) && effectiveDrawWrapper.materialContext) {
effectiveDrawWrapper.materialContext.useVertexPulling = this.useVertexPulling;
}

engine.enableEffect(effectiveDrawWrapper);
engine.setState(
this.backFaceCulling,
this.zOffset,
Expand Down
Loading