Parcourir la source

Merge pull request #9752 from Popov72/optimize-bindgroupcache

WebGPU: optimize render pipeline and bind group caching
David Catuhe il y a 4 ans
Parent
commit
5accff6a64

+ 14 - 0
.vscode/launch.json

@@ -120,6 +120,20 @@
                 "--enable-unsafe-es3-apis"
             ]
         },
+        {
+            "name": "Launch playground (Chrome Canary)",
+            "type": "chrome",
+            "request": "launch",
+            "url": "http://localhost:1338/Playground/index-local.html",
+            "webRoot": "${workspaceRoot}/",
+            "sourceMaps": true,
+            "preLaunchTask": "run",
+            "userDataDir": "${workspaceRoot}/.tempChromeCanaryProfileForDebug",
+            "runtimeExecutable": "C:/Users/alexis/AppData/Local/Google/Chrome SxS/Application/Chrome.exe",
+            "runtimeArgs": [
+                "--enable-unsafe-es3-apis"
+            ]
+        },
         { 
             "name": "Launch playground (Edge)",
             "type": "edge",

+ 1 - 2
serializers/src/glTF/2.0/glTFMaterialExporter.ts

@@ -1120,8 +1120,7 @@ export class _GLTFMaterialExporter {
     }
 
     private getPixelsFromTexture(babylonTexture: BaseTexture): Promise<Nullable<Uint8Array | Float32Array>> {
-        // TODO WEBGPU remove the as unknown cast once using the new babylonjs package to compile the glTF material exporter
-        const pixels = babylonTexture.textureType === Constants.TEXTURETYPE_UNSIGNED_INT ? babylonTexture.readPixels() as unknown as Promise<Uint8Array> : babylonTexture.readPixels() as unknown as Promise<Float32Array>;
+        const pixels = babylonTexture.textureType === Constants.TEXTURETYPE_UNSIGNED_INT ? babylonTexture.readPixels() as Promise<Uint8Array> : babylonTexture.readPixels() as Promise<Float32Array>;
         return pixels;
     }
 

+ 94 - 87
src/Engines/WebGPU/webgpuCacheRenderPipeline.ts

@@ -9,17 +9,17 @@ import { WebGPUHardwareTexture } from "./webgpuHardwareTexture";
 import { WebGPUPipelineContext } from "./webgpuPipelineContext";
 
 enum StatePosition {
-    ShaderStage = 0,
-    RasterizationState = 1,
-    //DepthBias = 2, // not used, so remove it to improve perf
-    //DepthBiasClamp = 3, // not used, so remove it to improve perf
+    //DepthBias = 0, // not used, so remove it to improve perf
+    //DepthBiasClamp = 1, // not used, so remove it to improve perf
+    StencilReadMask = 0,
+    StencilWriteMask = 1,
     DepthBiasSlopeScale = 2,
     MRTAttachments1 = 3,
     MRTAttachments2 = 4,
-    ColorStates = 5,
-    DepthStencilState = 6,
-    StencilReadMask = 7,
-    StencilWriteMask = 8,
+    DepthStencilState = 5,
+    RasterizationState = 6,
+    ColorStates = 7,
+    ShaderStage = 8,
     VertexState = 9, // vertex state will consume positions 9, 10, ... depending on the number of vertex inputs
 
     NumStates = 10
@@ -116,7 +116,7 @@ const stencilOpToIndex: { [name: number]: number } = {
 };
 
 /** @hidden */
-export class WebGPUCacheRenderPipeline {
+export abstract class WebGPUCacheRenderPipeline {
 
     public static NumCacheHitWithoutHash = 0;
     public static NumCacheHitWithHash = 0;
@@ -125,14 +125,16 @@ export class WebGPUCacheRenderPipeline {
 
     public disabled: boolean;
 
-    private static _Cache: { [hash: string]: GPURenderPipeline } = {};
     private static _NumPipelineCreationCurrentFrame = 0;
 
+    protected _states: number[];
+    protected _stateDirtyLowestIndex: number;
+    public lastStateDirtyLowestIndex: number; // for stats only
+
     private _device: GPUDevice;
-    private _states: string[];
     private _isDirty: boolean;
-    private _currentRenderPipeline: GPURenderPipeline;
     private _emptyVertexBuffer: VertexBuffer;
+    private _parameter: { token: any, pipeline: Nullable<GPURenderPipeline> };
 
     private _shaderId: number;
     private _alphaToCoverageEnabled: boolean;
@@ -175,8 +177,10 @@ export class WebGPUCacheRenderPipeline {
         this._device = device;
         this._states = [];
         this._states.length = StatePosition.NumStates;
+        this._stateDirtyLowestIndex = 0;
         this._emptyVertexBuffer = emptyVertexBuffer;
         this._mrtFormats = [];
+        this._parameter = { token: undefined, pipeline: null };
         this.disabled = false;
         this.reset();
     }
@@ -199,16 +203,19 @@ export class WebGPUCacheRenderPipeline {
         this.setBuffers(null, null, null);
     }
 
+    protected abstract _getRenderPipeline(param: { token: any, pipeline: Nullable<GPURenderPipeline> }): void;
+    protected abstract _setRenderPipeline(param: { token: any, pipeline: Nullable<GPURenderPipeline> }): void;
+
     public getRenderPipeline(fillMode: number, effect: Effect, sampleCount: number): GPURenderPipeline {
         if (this.disabled) {
             const topology = WebGPUCacheRenderPipeline._GetTopology(fillMode);
 
-            this._currentRenderPipeline = this._createRenderPipeline(effect, topology, sampleCount);
+            this._parameter.pipeline = this._createRenderPipeline(effect, topology, sampleCount);
 
             WebGPUCacheRenderPipeline.NumCacheMiss++;
             WebGPUCacheRenderPipeline._NumPipelineCreationCurrentFrame++;
 
-            return this._currentRenderPipeline;
+            return this._parameter.pipeline;
         }
 
         this._setShaderStage(effect.uniqueId);
@@ -217,31 +224,33 @@ export class WebGPUCacheRenderPipeline {
         this._setDepthStencilState();
         this._setVertexState(effect);
 
-        if (!this._isDirty && this._currentRenderPipeline) {
+        this.lastStateDirtyLowestIndex = this._stateDirtyLowestIndex;
+
+        if (!this._isDirty && this._parameter.pipeline) {
+            this._stateDirtyLowestIndex = this._states.length;
             WebGPUCacheRenderPipeline.NumCacheHitWithoutHash++;
-            return this._currentRenderPipeline;
+            return this._parameter.pipeline;
         }
 
-        this._isDirty = false;
+        this._getRenderPipeline(this._parameter);
 
-        let hash = this._states.join();
-        let pipeline = WebGPUCacheRenderPipeline._Cache[hash];
+        this._isDirty = false;
+        this._stateDirtyLowestIndex = this._states.length;
 
-        if (pipeline) {
-            this._currentRenderPipeline = pipeline;
+        if (this._parameter.pipeline) {
             WebGPUCacheRenderPipeline.NumCacheHitWithHash++;
-            return pipeline;
+            return this._parameter.pipeline;
         }
 
         const topology = WebGPUCacheRenderPipeline._GetTopology(fillMode);
 
-        this._currentRenderPipeline = this._createRenderPipeline(effect, topology, sampleCount);
-        WebGPUCacheRenderPipeline._Cache[hash] = this._currentRenderPipeline;
+        this._parameter.pipeline = this._createRenderPipeline(effect, topology, sampleCount);
+        this._setRenderPipeline(this._parameter);
 
         WebGPUCacheRenderPipeline.NumCacheMiss++;
         WebGPUCacheRenderPipeline._NumPipelineCreationCurrentFrame++;
 
-        return this._currentRenderPipeline;
+        return this._parameter.pipeline;
     }
 
     public endFrame(): void {
@@ -302,8 +311,9 @@ export class WebGPUCacheRenderPipeline {
     public setDepthBiasSlopeScale(depthBiasSlopeScale: number): void {
         if (this._depthBiasSlopeScale !== depthBiasSlopeScale) {
             this._depthBiasSlopeScale = depthBiasSlopeScale;
-            this._states[StatePosition.DepthBiasSlopeScale] = depthBiasSlopeScale.toString();
+            this._states[StatePosition.DepthBiasSlopeScale] = depthBiasSlopeScale;
             this._isDirty = true;
+            this._stateDirtyLowestIndex = Math.min(this._stateDirtyLowestIndex, StatePosition.DepthBiasSlopeScale);
         }
     }
 
@@ -340,9 +350,10 @@ export class WebGPUCacheRenderPipeline {
         if (this._mrtAttachments1 !== bits[0] || this._mrtAttachments2 !== bits[1]) {
             this._mrtAttachments1 = bits[0];
             this._mrtAttachments2 = bits[1];
-            this._states[StatePosition.MRTAttachments1] = bits[0].toString();
-            this._states[StatePosition.MRTAttachments2] = bits[1].toString();
+            this._states[StatePosition.MRTAttachments1] = bits[0];
+            this._states[StatePosition.MRTAttachments2] = bits[1];
             this._isDirty = true;
+            this._stateDirtyLowestIndex = Math.min(this._stateDirtyLowestIndex, StatePosition.MRTAttachments1);
         }
     }
 
@@ -399,16 +410,18 @@ export class WebGPUCacheRenderPipeline {
     public setStencilReadMask(mask: number): void {
         if (this._stencilReadMask !== mask) {
             this._stencilReadMask = mask;
-            this._states[StatePosition.StencilReadMask] = mask.toString();
+            this._states[StatePosition.StencilReadMask] = mask;
             this._isDirty = true;
+            this._stateDirtyLowestIndex = Math.min(this._stateDirtyLowestIndex, StatePosition.StencilReadMask);
         }
     }
 
     public setStencilWriteMask(mask: number): void {
         if (this._stencilWriteMask !== mask) {
             this._stencilWriteMask = mask;
-            this._states[StatePosition.StencilWriteMask] = mask.toString();
+            this._states[StatePosition.StencilWriteMask] = mask;
             this._isDirty = true;
+            this._stateDirtyLowestIndex = Math.min(this._stateDirtyLowestIndex, StatePosition.StencilWriteMask);
         }
     }
 
@@ -675,8 +688,9 @@ export class WebGPUCacheRenderPipeline {
     private _setShaderStage(id: number): void {
         if (this._shaderId !== id) {
             this._shaderId = id;
-            this._states[StatePosition.ShaderStage] = id.toString();
+            this._states[StatePosition.ShaderStage] = id;
             this._isDirty = true;
+            this._stateDirtyLowestIndex = Math.min(this._stateDirtyLowestIndex, StatePosition.ShaderStage);
         }
     }
 
@@ -695,13 +709,16 @@ export class WebGPUCacheRenderPipeline {
 
         if (this._rasterizationState !== rasterizationState) {
             this._rasterizationState = rasterizationState;
-            this._states[StatePosition.RasterizationState] = this._rasterizationState.toString();
+            this._states[StatePosition.RasterizationState] = this._rasterizationState;
             this._isDirty = true;
+            this._stateDirtyLowestIndex = Math.min(this._stateDirtyLowestIndex, StatePosition.RasterizationState);
         }
     }
 
     private _setColorStates(): void {
-        let colorStates = ((this._writeMask ? 1 : 0) << 22) + (this._colorFormat << 23);
+        let colorStates =
+            ((this._writeMask ? 1 : 0) << 22) + (this._colorFormat << 23) +
+            ((this._depthWriteEnabled ? 1 : 0) << 29); // this state has been moved from depthStencilState here because alpha and depth are related (generally when alpha is on, depth write is off and the other way around)
 
         if (this._alphaBlendEnabled) {
             colorStates +=
@@ -715,8 +732,9 @@ export class WebGPUCacheRenderPipeline {
 
         if (colorStates !== this._colorStates) {
             this._colorStates = colorStates;
-            this._states[StatePosition.ColorStates] = this._colorStates.toString();
+            this._states[StatePosition.ColorStates] = this._colorStates;
             this._isDirty = true;
+            this._stateDirtyLowestIndex = Math.min(this._stateDirtyLowestIndex, StatePosition.ColorStates);
         }
     }
 
@@ -727,14 +745,14 @@ export class WebGPUCacheRenderPipeline {
 
         const depthStencilState =
                 this._depthStencilFormat +
-                ((this._depthWriteEnabled ? 1 : 0) << 6) +
-                ((this._depthTestEnabled ? this._depthCompare : 7 /* ALWAYS */) << 7) +
+                ((this._depthTestEnabled ? this._depthCompare : 7 /* ALWAYS */) << 6) +
                 (stencilState << 10); // stencil front - stencil back is the same
 
         if (this._depthStencilState !== depthStencilState) {
             this._depthStencilState = depthStencilState;
-            this._states[StatePosition.DepthStencilState] = this._depthStencilState.toString();
+            this._states[StatePosition.DepthStencilState] = this._depthStencilState;
             this._isDirty = true;
+            this._stateDirtyLowestIndex = Math.min(this._stateDirtyLowestIndex, StatePosition.DepthStencilState);
         }
     }
 
@@ -742,39 +760,29 @@ export class WebGPUCacheRenderPipeline {
         const currStateLen = this._states.length;
         let newNumStates = StatePosition.VertexState;
 
-        const attributes = effect.getAttributesNames();
+        const webgpuPipelineContext = effect._pipelineContext as WebGPUPipelineContext;
+        const attributes = webgpuPipelineContext.shaderProcessingContext.attributeNamesFromEffect;
+        const locations = webgpuPipelineContext.shaderProcessingContext.attributeLocationsFromEffect;
         for (var index = 0; index < attributes.length; index++) {
-            const location = effect.getAttributeLocation(index);
-
-            if (location >= 0) {
-                let vertexBuffer = (this._overrideVertexBuffers && this._overrideVertexBuffers[attributes[index]]) ?? this._vertexBuffers![attributes[index]];
-                if (!vertexBuffer) {
-                    // In WebGL it's valid to not bind a vertex buffer to an attribute, but it's not valid in WebGPU
-                    // So we must bind a dummy buffer when we are not given one for a specific attribute
-                    vertexBuffer = this._emptyVertexBuffer;
-                }
-
-                const type = vertexBuffer.type - 5120;
-                const normalized = vertexBuffer.normalized ? 1 : 0;
-                const size = vertexBuffer.getSize();
-                const stepMode = vertexBuffer.getIsInstanced() ? 1 : 0;
-                const stride = vertexBuffer.byteStride;
-
-                const vid =
-                    ((type << 0) +
-                    (normalized << 3) +
-                    (size << 4) +
-                    (stepMode << 6) +
-                    (location << 7) +
-                    (stride << 12)).toString();
-
-                this._isDirty = this._isDirty || this._states[newNumStates] !== vid;
-                this._states[newNumStates++] = vid;
+            const location = locations[index];
+            let vertexBuffer = (this._overrideVertexBuffers && this._overrideVertexBuffers[attributes[index]]) ?? this._vertexBuffers![attributes[index]];
+            if (!vertexBuffer) {
+                // In WebGL it's valid to not bind a vertex buffer to an attribute, but it's not valid in WebGPU
+                // So we must bind a dummy buffer when we are not given one for a specific attribute
+                vertexBuffer = this._emptyVertexBuffer;
             }
+
+            const vid = vertexBuffer.hashCode + (location << 7);
+
+            this._isDirty = this._isDirty || this._states[newNumStates] !== vid;
+            this._states[newNumStates++] = vid;
         }
 
         this._states.length = newNumStates;
         this._isDirty = this._isDirty || newNumStates !== currStateLen;
+        if (this._isDirty) {
+            this._stateDirtyLowestIndex = Math.min(this._stateDirtyLowestIndex, StatePosition.VertexState);
+        }
     }
 
     private _createPipelineLayout(webgpuPipelineContext: WebGPUPipelineContext): GPUPipelineLayout {
@@ -848,33 +856,32 @@ export class WebGPUCacheRenderPipeline {
 
     private _getVertexInputDescriptor(effect: Effect, topology: GPUPrimitiveTopology): GPUVertexStateDescriptor {
         const descriptors: GPUVertexBufferLayoutDescriptor[] = [];
-        const attributes = effect.getAttributesNames();
+        const webgpuPipelineContext = effect._pipelineContext as WebGPUPipelineContext;
+        const attributes = webgpuPipelineContext.shaderProcessingContext.attributeNamesFromEffect;
+        const locations = webgpuPipelineContext.shaderProcessingContext.attributeLocationsFromEffect;
         for (var index = 0; index < attributes.length; index++) {
-            const location = effect.getAttributeLocation(index);
-
-            if (location >= 0) {
-                let vertexBuffer = (this._overrideVertexBuffers && this._overrideVertexBuffers[attributes[index]]) ?? this._vertexBuffers![attributes[index]];
-                if (!vertexBuffer) {
-                    // In WebGL it's valid to not bind a vertex buffer to an attribute, but it's not valid in WebGPU
-                    // So we must bind a dummy buffer when we are not given one for a specific attribute
-                    vertexBuffer = this._emptyVertexBuffer;
-                }
+            const location = locations[index];
+            let vertexBuffer = (this._overrideVertexBuffers && this._overrideVertexBuffers[attributes[index]]) ?? this._vertexBuffers![attributes[index]];
+            if (!vertexBuffer) {
+                // In WebGL it's valid to not bind a vertex buffer to an attribute, but it's not valid in WebGPU
+                // So we must bind a dummy buffer when we are not given one for a specific attribute
+                vertexBuffer = this._emptyVertexBuffer;
+            }
 
-                const attributeDescriptor: GPUVertexAttributeDescriptor = {
-                    shaderLocation: location,
-                    offset: 0, // not available in WebGL
-                    format: WebGPUCacheRenderPipeline._GetVertexInputDescriptorFormat(vertexBuffer),
-                };
+            const attributeDescriptor: GPUVertexAttributeDescriptor = {
+                shaderLocation: location,
+                offset: 0, // not available in WebGL
+                format: WebGPUCacheRenderPipeline._GetVertexInputDescriptorFormat(vertexBuffer),
+            };
 
-                // TODO WEBGPU. Factorize the one with the same underlying buffer.
-                const vertexBufferDescriptor: GPUVertexBufferLayoutDescriptor = {
-                    arrayStride: vertexBuffer.byteStride,
-                    stepMode: vertexBuffer.getIsInstanced() ? WebGPUConstants.InputStepMode.Instance : WebGPUConstants.InputStepMode.Vertex,
-                    attributes: [attributeDescriptor]
-                };
+            // TODO WEBGPU. Factorize the one with the same underlying buffer.
+            const vertexBufferDescriptor: GPUVertexBufferLayoutDescriptor = {
+                arrayStride: vertexBuffer.byteStride,
+                stepMode: vertexBuffer.getIsInstanced() ? WebGPUConstants.InputStepMode.Instance : WebGPUConstants.InputStepMode.Vertex,
+                attributes: [attributeDescriptor]
+            };
 
-               descriptors.push(vertexBufferDescriptor);
-            }
+            descriptors.push(vertexBufferDescriptor);
         }
 
         const inputStateDescriptor: GPUVertexStateDescriptor = {

+ 18 - 0
src/Engines/WebGPU/webgpuCacheRenderPipelineString.ts

@@ -0,0 +1,18 @@
+import { Nullable } from "../../types";
+import { WebGPUCacheRenderPipeline } from "./webgpuCacheRenderPipeline";
+
+/** @hidden */
+export class WebGPUCacheRenderPipelineString extends WebGPUCacheRenderPipeline {
+
+    private static _Cache: { [hash: string]: GPURenderPipeline } = {};
+
+    protected _getRenderPipeline(param: { token: any, pipeline: Nullable<GPURenderPipeline> }): void {
+        let hash = this._states.join();
+        param.token = hash;
+        param.pipeline = WebGPUCacheRenderPipelineString._Cache[hash];
+    }
+
+    protected _setRenderPipeline(param: { token: any, pipeline: Nullable<GPURenderPipeline> }): void {
+        WebGPUCacheRenderPipelineString._Cache[param.token] = param.pipeline!;
+    }
+}

+ 65 - 0
src/Engines/WebGPU/webgpuCacheRenderPipelineTree.ts

@@ -0,0 +1,65 @@
+import { VertexBuffer } from "../../Meshes/buffer";
+import { Nullable } from "../../types";
+import { WebGPUCacheRenderPipeline } from "./webgpuCacheRenderPipeline";
+
+/** @hidden */
+class NodeState {
+    public values: { [name: number]: NodeState };
+    public pipeline: GPURenderPipeline;
+
+    constructor() {
+        this.values = {};
+    }
+
+    public count(): [number, number] {
+        let countNode = 0, countPipeline = this.pipeline ? 1 : 0;
+        for (const value in this.values) {
+            const node = this.values[value];
+            const [childCountNodes, childCoundPipeline] = node!.count();
+            countNode += childCountNodes;
+            countPipeline += childCoundPipeline;
+            countNode++;
+        }
+        return [countNode, countPipeline];
+    }
+}
+
+/** @hidden */
+export class WebGPUCacheRenderPipelineTree extends WebGPUCacheRenderPipeline {
+
+    private static _Cache: NodeState = new NodeState();
+
+    private _nodeStack: NodeState[];
+
+    public static GetNodeCounts(): { nodeCount: number, pipelineCount: number } {
+        const counts = WebGPUCacheRenderPipelineTree._Cache.count();
+
+        return { nodeCount: counts[0], pipelineCount: counts[1] };
+    }
+
+    constructor(device: GPUDevice, emptyVertexBuffer: VertexBuffer) {
+        super(device, emptyVertexBuffer);
+        this._nodeStack = [];
+        this._nodeStack[0] = WebGPUCacheRenderPipelineTree._Cache;
+    }
+
+    protected _getRenderPipeline(param: { token: any, pipeline: Nullable<GPURenderPipeline> }): void {
+        let node = this._nodeStack[this._stateDirtyLowestIndex];
+        for (let i = this._stateDirtyLowestIndex; i < this._states.length; ++i) {
+            let nn: NodeState | undefined = node!.values[this._states[i]];
+            if (!nn) {
+                nn = new NodeState();
+                node!.values[this._states[i]] = nn;
+            }
+            node = nn;
+            this._nodeStack[i + 1] = node;
+        }
+
+        param.token = node;
+        param.pipeline = node.pipeline;
+    }
+
+    protected _setRenderPipeline(param: { token: NodeState, pipeline: Nullable<GPURenderPipeline> }): void {
+        param.token.pipeline = param.pipeline!;
+    }
+}

+ 15 - 3
src/Engines/WebGPU/webgpuCacheSampler.ts

@@ -75,12 +75,13 @@ export class WebGPUCacheSampler {
         return code;
     }
 
-    private static _GetSamplerFilterDescriptor(internalTexture: InternalTexture): {
+    private static _GetSamplerFilterDescriptor(internalTexture: InternalTexture, anisotropy: number): {
         magFilter: GPUFilterMode,
         minFilter: GPUFilterMode,
         mipmapFilter: GPUFilterMode,
         lodMinClamp?: number,
         lodMaxClamp?: number,
+        anisotropyEnabled?: boolean,
     } {
         let magFilter: GPUFilterMode, minFilter: GPUFilterMode, mipmapFilter: GPUFilterMode, lodMinClamp: number | undefined, lodMaxClamp: number | undefined;
         const useMipMaps = internalTexture.generateMipMaps;
@@ -192,6 +193,15 @@ export class WebGPUCacheSampler {
                 break;
         }
 
+        if (anisotropy > 1 && (lodMinClamp !== 0 || lodMaxClamp !== 0)) {
+            return {
+                magFilter: WebGPUConstants.FilterMode.Linear,
+                minFilter: WebGPUConstants.FilterMode.Linear,
+                mipmapFilter: WebGPUConstants.FilterMode.Linear,
+                anisotropyEnabled: true,
+            };
+        }
+
         return {
             magFilter,
             minFilter,
@@ -226,11 +236,13 @@ export class WebGPUCacheSampler {
     }
 
     private static _GetSamplerDescriptor(internalTexture: InternalTexture): GPUSamplerDescriptor {
+        const anisotropy = internalTexture.generateMipMaps ? (internalTexture._cachedAnisotropicFilteringLevel ?? 1) : 1;
+        const filterDescriptor = this._GetSamplerFilterDescriptor(internalTexture, anisotropy);
         return {
-            ...this._GetSamplerFilterDescriptor(internalTexture),
+            ...filterDescriptor,
             ...this._GetSamplerWrappingDescriptor(internalTexture),
             compare: internalTexture._comparisonFunction ? WebGPUTextureHelper.GetCompareFunction(internalTexture._comparisonFunction) : undefined,
-            maxAnisotropy: internalTexture._cachedAnisotropicFilteringLevel ?? 1,
+            maxAnisotropy: filterDescriptor.anisotropyEnabled ? anisotropy : 1,
         };
     }
 

+ 127 - 0
src/Engines/WebGPU/webgpuDepthCullingState.ts

@@ -0,0 +1,127 @@
+import { Nullable } from "../../types";
+import { WebGPUCacheRenderPipeline } from "./webgpuCacheRenderPipeline";
+import { DepthCullingState } from "../../States/depthCullingState";
+
+/**
+ * @hidden
+ **/
+export class WebGPUDepthCullingState extends DepthCullingState {
+
+    private _cache: WebGPUCacheRenderPipeline;
+
+    /**
+     * Initializes the state.
+     */
+    public constructor(cache: WebGPUCacheRenderPipeline) {
+        super(false);
+        this._cache = cache;
+        this.reset();
+    }
+
+    public get zOffset(): number {
+        return this._zOffset;
+    }
+
+    public set zOffset(value: number) {
+        if (this._zOffset === value) {
+            return;
+        }
+
+        this._zOffset = value;
+        this._isZOffsetDirty = true;
+        this._cache.setDepthBiasSlopeScale(value);
+    }
+
+    public get cullFace(): Nullable<number> {
+        return this._cullFace;
+    }
+
+    public set cullFace(value: Nullable<number>) {
+        if (this._cullFace === value) {
+            return;
+        }
+
+        this._cullFace = value;
+        this._isCullFaceDirty = true;
+        this._cache.setCullFace(value ?? 1);
+    }
+
+    public get cull(): Nullable<boolean> {
+        return this._cull;
+    }
+
+    public set cull(value: Nullable<boolean>) {
+        if (this._cull === value) {
+            return;
+        }
+
+        this._cull = value;
+        this._isCullDirty = true;
+        this._cache.setCullEnabled(!!value);
+    }
+
+    public get depthFunc(): Nullable<number> {
+        return this._depthFunc;
+    }
+
+    public set depthFunc(value: Nullable<number>) {
+        if (this._depthFunc === value) {
+            return;
+        }
+
+        this._depthFunc = value;
+        this._isDepthFuncDirty = true;
+        this._cache.setDepthCompare(value);
+    }
+
+    public get depthMask(): boolean {
+        return this._depthMask;
+    }
+
+    public set depthMask(value: boolean) {
+        if (this._depthMask === value) {
+            return;
+        }
+
+        this._depthMask = value;
+        this._isDepthMaskDirty = true;
+        this._cache.setDepthWriteEnabled(value);
+    }
+
+    public get depthTest(): boolean {
+        return this._depthTest;
+    }
+
+    public set depthTest(value: boolean) {
+        if (this._depthTest === value) {
+            return;
+        }
+
+        this._depthTest = value;
+        this._isDepthTestDirty = true;
+        this._cache.setDepthTestEnabled(value);
+    }
+
+    public get frontFace(): Nullable<number> {
+        return this._frontFace;
+    }
+
+    public set frontFace(value: Nullable<number>) {
+        if (this._frontFace === value) {
+            return;
+        }
+
+        this._frontFace = value;
+        this._isFrontFaceDirty = true;
+        this._cache.setFrontFace(value ?? 2);
+    }
+
+    public reset() {
+        super.reset();
+        this._cache.resetDepthCullingState();
+    }
+
+    public apply(gl: WebGLRenderingContext) {
+        // nothing to do
+    }
+}

+ 24 - 2
src/Engines/WebGPU/webgpuPipelineContext.ts

@@ -51,6 +51,16 @@ export interface IWebGPURenderPipelineStageDescriptor {
 }
 
 /** @hidden */
+export class WebGPUBindGroupCacheNode {
+    public values: { [id: number]: WebGPUBindGroupCacheNode };
+    public bindGroups: GPUBindGroup[];
+
+    constructor() {
+        this.values = {};
+    }
+}
+
+/** @hidden */
 export class WebGPUPipelineContext implements IPipelineContext {
     public engine: WebGPUEngine;
 
@@ -71,7 +81,7 @@ export class WebGPUPipelineContext implements IPipelineContext {
     public textures: { [name: string]: Nullable<IWebGPUPipelineContextTextureCache> } = { };
 
     public bindGroupLayouts: GPUBindGroupLayout[];
-    public bindGroupsCache: { [key: string]: GPUBindGroup[] };
+    public bindGroupsCache: WebGPUBindGroupCacheNode;
 
     /**
      * Stores the uniform buffer
@@ -101,7 +111,7 @@ export class WebGPUPipelineContext implements IPipelineContext {
         this.shaderProcessingContext = shaderProcessingContext;
         this.leftOverUniformsByName = {};
         this.engine = engine;
-        this.bindGroupsCache = {};
+        this.bindGroupsCache = new WebGPUBindGroupCacheNode();
     }
 
     public _handlesSpectorRebuildCallback(onCompiled: (program: any) => void): void {
@@ -144,6 +154,18 @@ export class WebGPUPipelineContext implements IPipelineContext {
 
         // Build the uniform layout for the left over uniforms.
         this.buildUniformLayout();
+
+        let attributeNamesFromEffect: string[] = [];
+        let attributeLocationsFromEffect: number[] = [];
+        for (index = 0; index < attributesNames.length; index++) {
+            const location = attributes[index];
+            if (location >= 0) {
+                attributeNamesFromEffect.push(attributesNames[index]);
+                attributeLocationsFromEffect.push(location);
+            }
+        }
+        this.shaderProcessingContext.attributeNamesFromEffect = attributeNamesFromEffect;
+        this.shaderProcessingContext.attributeLocationsFromEffect = attributeLocationsFromEffect;
     }
 
     /** @hidden */

+ 2 - 0
src/Engines/WebGPU/webgpuShaderProcessingContext.ts

@@ -57,6 +57,8 @@ export class WebGPUShaderProcessingContext implements ShaderProcessingContext {
     public orderedAttributes: string[];
     public orderedUBOsAndSamplers: WebGPUBindingDescription[][];
     public uniformBufferNames: string[];
+    public attributeNamesFromEffect: string[];
+    public attributeLocationsFromEffect: number[];
 
     private _attributeNextLocation: number;
     private _varyingNextLocation: number;

+ 136 - 0
src/Engines/WebGPU/webgpuStencilState.ts

@@ -0,0 +1,136 @@
+import { WebGPUCacheRenderPipeline } from "./webgpuCacheRenderPipeline";
+import { StencilState } from "../../States/stencilState";
+
+/**
+ * @hidden
+ **/
+export class WebGPUStencilState extends StencilState {
+
+    private _cache: WebGPUCacheRenderPipeline;
+
+    public constructor(cache: WebGPUCacheRenderPipeline) {
+        super(false);
+        this._cache = cache;
+        this.reset();
+    }
+
+    public get stencilFunc(): number {
+        return this._stencilFunc;
+    }
+
+    public set stencilFunc(value: number) {
+        if (this._stencilFunc === value) {
+            return;
+        }
+
+        this._stencilFunc = value;
+        this._isStencilFuncDirty = true;
+        this._cache.setStencilCompare(value);
+    }
+
+    public get stencilFuncRef(): number {
+        return this._stencilFuncRef;
+    }
+
+    public set stencilFuncRef(value: number) {
+        if (this._stencilFuncRef === value) {
+            return;
+        }
+
+        this._stencilFuncRef = value;
+        this._isStencilFuncDirty = true;
+    }
+
+    public get stencilFuncMask(): number {
+        return this._stencilFuncMask;
+    }
+
+    public set stencilFuncMask(value: number) {
+        if (this._stencilFuncMask === value) {
+            return;
+        }
+
+        this._stencilFuncMask = value;
+        this._isStencilFuncDirty = true;
+        this._cache.setStencilReadMask(value);
+    }
+
+    public get stencilOpStencilFail(): number {
+        return this._stencilOpStencilFail;
+    }
+
+    public set stencilOpStencilFail(value: number) {
+        if (this._stencilOpStencilFail === value) {
+            return;
+        }
+
+        this._stencilOpStencilFail = value;
+        this._isStencilOpDirty = true;
+        this._cache.setStencilFailOp(value);
+    }
+
+    public get stencilOpDepthFail(): number {
+        return this._stencilOpDepthFail;
+    }
+
+    public set stencilOpDepthFail(value: number) {
+        if (this._stencilOpDepthFail === value) {
+            return;
+        }
+
+        this._stencilOpDepthFail = value;
+        this._isStencilOpDirty = true;
+        this._cache.setStencilDepthFailOp(value);
+    }
+
+    public get stencilOpStencilDepthPass(): number {
+        return this._stencilOpStencilDepthPass;
+    }
+
+    public set stencilOpStencilDepthPass(value: number) {
+        if (this._stencilOpStencilDepthPass === value) {
+            return;
+        }
+
+        this._stencilOpStencilDepthPass = value;
+        this._isStencilOpDirty = true;
+        this._cache.setStencilPassOp(value);
+    }
+
+    public get stencilMask(): number {
+        return this._stencilMask;
+    }
+
+    public set stencilMask(value: number) {
+        if (this._stencilMask === value) {
+            return;
+        }
+
+        this._stencilMask = value;
+        this._isStencilMaskDirty = true;
+        this._cache.setStencilWriteMask(value);
+    }
+
+    public get stencilTest(): boolean {
+        return this._stencilTest;
+    }
+
+    public set stencilTest(value: boolean) {
+        if (this._stencilTest === value) {
+            return;
+        }
+
+        this._stencilTest = value;
+        this._isStencilTestDirty = true;
+        this._cache.setStencilEnabled(value);
+    }
+
+    public reset() {
+        super.reset();
+        this._cache.resetStencilState();
+    }
+
+    public apply(gl: WebGLRenderingContext) {
+        // nothing to do
+    }
+}

+ 4 - 4
src/Engines/engine.ts

@@ -952,28 +952,28 @@ export class Engine extends ThinEngine {
      * Sets the current depth function to GREATER
      */
     public setDepthFunctionToGreater(): void {
-        this._depthCullingState.depthFunc = this._gl.GREATER;
+        this._depthCullingState.depthFunc = Constants.GREATER;
     }
 
     /**
      * Sets the current depth function to GEQUAL
      */
     public setDepthFunctionToGreaterOrEqual(): void {
-        this._depthCullingState.depthFunc = this._gl.GEQUAL;
+        this._depthCullingState.depthFunc = Constants.GEQUAL;
     }
 
     /**
      * Sets the current depth function to LESS
      */
     public setDepthFunctionToLess(): void {
-        this._depthCullingState.depthFunc = this._gl.LESS;
+        this._depthCullingState.depthFunc = Constants.LESS;
     }
 
     /**
      * Sets the current depth function to LEQUAL
      */
     public setDepthFunctionToLessOrEqual(): void {
-        this._depthCullingState.depthFunc = this._gl.LEQUAL;
+        this._depthCullingState.depthFunc = Constants.LEQUAL;
     }
 
     private _cachedStencilBuffer: boolean;

+ 1 - 0
src/Engines/index.ts

@@ -11,6 +11,7 @@ export * from "./WebGL/webGLPipelineContext";
 export * from "./WebGPU/webgpuConstants";
 export * from "./webgpuEngine";
 export * from "./WebGPU/webgpuCacheRenderPipeline";
+export * from "./WebGPU/webgpuCacheRenderPipelineTree";
 export * from "./WebGL/webGL2ShaderProcessors";
 export * from "./nativeEngine";
 export * from "./Processors/shaderCodeInliner";

+ 39 - 151
src/Engines/webgpuEngine.ts

@@ -11,7 +11,7 @@ import { _TimeToken } from "../Instrumentation/timeToken";
 import { Constants } from "./constants";
 import * as WebGPUConstants from './WebGPU/webgpuConstants';
 import { VertexBuffer } from "../Meshes/buffer";
-import { WebGPUPipelineContext, IWebGPURenderPipelineStageDescriptor } from './WebGPU/webgpuPipelineContext';
+import { WebGPUPipelineContext, IWebGPURenderPipelineStageDescriptor, WebGPUBindGroupCacheNode } from './WebGPU/webgpuPipelineContext';
 import { IPipelineContext } from './IPipelineContext';
 import { DataBuffer } from '../Meshes/dataBuffer';
 import { WebGPUDataBuffer } from '../Meshes/WebGPU/webgpuDataBuffer';
@@ -36,6 +36,9 @@ import { IMultiRenderTargetOptions } from '../Materials/Textures/multiRenderTarg
 import { WebGPUCacheSampler } from "./WebGPU/webgpuCacheSampler";
 import { WebGPUShaderManager } from "./WebGPU/webgpuShaderManager";
 import { WebGPUCacheRenderPipeline } from "./WebGPU/webgpuCacheRenderPipeline";
+import { WebGPUCacheRenderPipelineTree } from "./WebGPU/webgpuCacheRenderPipelineTree";
+import { WebGPUStencilState } from "./WebGPU/webgpuStencilState";
+import { WebGPUDepthCullingState } from "./WebGPU/webgpuDepthCullingState";
 
 import "../Shaders/clearQuad.vertex";
 import "../Shaders/clearQuad.fragment";
@@ -369,10 +372,6 @@ export class WebGPUEngine extends Engine {
         this._mainPassSampleCount = options.antialiasing ? this._defaultSampleCount : 1;
         this._isStencilEnable = options.stencil;
 
-        this._depthCullingState.depthTest = true;
-        this._depthCullingState.depthFunc = Constants.LEQUAL;
-        this._depthCullingState.depthMask = true;
-
         this._sharedInit(canvas, !!options.doNotHandleTouchAction, options.audioEngine);
 
         this._shaderProcessor = this._getShaderProcessor();
@@ -445,10 +444,14 @@ export class WebGPUEngine extends Engine {
 
                 this._emptyVertexBuffer = new VertexBuffer(this, [0], "", false, false, 1, false, 0, 1);
 
-                this._cacheRenderPipeline = new WebGPUCacheRenderPipeline(this._device, this._emptyVertexBuffer);
+                this._cacheRenderPipeline = new WebGPUCacheRenderPipelineTree(this._device, this._emptyVertexBuffer);
+
+                this._depthCullingState = new WebGPUDepthCullingState(this._cacheRenderPipeline);
+                this._stencilState = new WebGPUStencilState(this._cacheRenderPipeline);
 
-                this._cacheRenderPipeline.setDepthCompare(this._depthCullingState.depthFunc);
-                //this._cacheRenderPipeline.disabled = true;
+                this._depthCullingState.depthTest = true;
+                this._depthCullingState.depthFunc = Constants.LEQUAL;
+                this._depthCullingState.depthMask = true;
 
                 this._textureHelper.setCommandEncoder(this._uploadEncoder);
 
@@ -715,12 +718,9 @@ export class WebGPUEngine extends Engine {
             this._currentProgram = null;
 
             this._stencilState.reset();
-            this._cacheRenderPipeline.resetStencilState();
 
             this._depthCullingState.reset();
             this._depthCullingState.depthFunc = Constants.LEQUAL;
-            this._cacheRenderPipeline.resetDepthCullingState();
-            this._cacheRenderPipeline.setDepthCompare(Constants.LEQUAL);
 
             this._alphaState.reset();
             this._alphaMode = Constants.ALPHA_ADD;
@@ -1805,7 +1805,7 @@ export class WebGPUEngine extends Engine {
 
             if (webgpuPipelineContext.textures[name]) {
                 if (webgpuPipelineContext.textures[name]!.texture !== internalTexture) {
-                    webgpuPipelineContext.bindGroupsCache = {}; // the bind groups need to be rebuilt (at least the bind group owning this texture, but it's easier to just have them all rebuilt)
+                    webgpuPipelineContext.bindGroupsCache.values = {}; // the bind groups need to be rebuilt (at least the bind group owning this texture, but it's easier to just have them all rebuilt)
                 }
                 webgpuPipelineContext.textures[name]!.texture = internalTexture!;
             }
@@ -1859,7 +1859,7 @@ export class WebGPUEngine extends Engine {
             const webgpuPipelineContext = this._currentEffect._pipelineContext as WebGPUPipelineContext;
             if (!texture) {
                 if (webgpuPipelineContext.textures[name] && webgpuPipelineContext.textures[name]!.texture) {
-                    webgpuPipelineContext.bindGroupsCache = {}; // the bind groups need to be rebuilt (at least the bind group owning this texture, but it's easier to just have them all rebuilt)
+                    webgpuPipelineContext.bindGroupsCache.values = {}; // the bind groups need to be rebuilt (at least the bind group owning this texture, but it's easier to just have them all rebuilt)
                 }
                 webgpuPipelineContext.textures[name] = null;
                 return false;
@@ -3143,10 +3143,16 @@ export class WebGPUEngine extends Engine {
         // TODO WEBGPU remove the assert debugging code
         assert(this._currentRenderTarget === null || (this._currentRenderTarget !== null && texture === this._currentRenderTarget), "unBindFramebuffer - the texture we want to unbind is not the same than the currentRenderTarget! texture=" + texture + ", this._currentRenderTarget=" + this._currentRenderTarget);
 
+        const saveCRT = this._currentRenderTarget;
+
+        this._currentRenderTarget = null; // to be iso with thinEngine, this._currentRenderTarget must be null when onBeforeUnbind is called
+
         if (onBeforeUnbind) {
             onBeforeUnbind();
         }
 
+        this._currentRenderTarget = saveCRT;
+
         if (this._currentRenderPass && this._currentRenderPass !== this._mainRenderPassWrapper.renderPass) {
             this._endRenderTargetRenderPass();
         }
@@ -3222,12 +3228,6 @@ export class WebGPUEngine extends Engine {
     //                              Render
     //------------------------------------------------------------------------------
 
-    public setZOffset(value: number): void {
-        if (value !== this._depthCullingState.zOffset) {
-            this._depthCullingState.zOffset = value;
-        }
-    }
-
     private _setColorFormat(wrapper: WebGPURenderPassWrapper): void {
         const format = wrapper.colorAttachmentGPUTextures[0].format;
         this._cacheRenderPipeline.setColorFormat(format);
@@ -3245,66 +3245,6 @@ export class WebGPUEngine extends Engine {
         this._depthTextureFormat = wrapper.depthTextureFormat;
     }
 
-    public setDepthBuffer(enable: boolean): void {
-        if (this._depthCullingState.depthTest !== enable) {
-            this._depthCullingState.depthTest = enable;
-        }
-    }
-
-    public setDepthWrite(enable: boolean): void {
-        if (this._depthCullingState.depthMask !== enable) {
-            this._depthCullingState.depthMask = enable;
-        }
-    }
-
-    public setStencilBuffer(enable: boolean): void {
-        if (this._stencilState.stencilTest !== enable) {
-            this._stencilState.stencilTest = enable;
-        }
-    }
-
-    public setStencilMask(mask: number): void {
-        if (this._stencilState.stencilMask !== mask) {
-            this._stencilState.stencilMask = mask;
-        }
-    }
-
-    public setStencilFunction(stencilFunc: number) {
-        if (this._stencilState.stencilFunc !== stencilFunc) {
-            this._stencilState.stencilFunc = stencilFunc;
-        }
-    }
-
-    public setStencilFunctionReference(reference: number) {
-        if (this._stencilState.stencilFuncRef !== reference) {
-            this._stencilState.stencilFuncRef = reference;
-        }
-    }
-
-    public setStencilFunctionMask(mask: number) {
-        if (this._stencilState.stencilFuncMask !== mask) {
-            this._stencilState.stencilFuncMask = mask;
-        }
-    }
-
-    public setStencilOperationFail(operation: number): void {
-        if (this._stencilState.stencilOpStencilFail !== operation) {
-            this._stencilState.stencilOpStencilFail = operation;
-        }
-    }
-
-    public setStencilOperationDepthFail(operation: number): void {
-        if (this._stencilState.stencilOpDepthFail !== operation) {
-            this._stencilState.stencilOpDepthFail = operation;
-        }
-    }
-
-    public setStencilOperationPass(operation: number): void {
-        if (this._stencilState.stencilOpStencilDepthPass !== operation) {
-            this._stencilState.stencilOpStencilDepthPass = operation;
-        }
-    }
-
     public setDitheringState(value: boolean): void {
         // Does not exist in WebGPU
     }
@@ -3313,36 +3253,6 @@ export class WebGPUEngine extends Engine {
         // Does not exist in WebGPU
     }
 
-    public setDepthFunction(depthFunc: number) {
-        if (this._depthCullingState.depthFunc !== depthFunc) {
-            this._depthCullingState.depthFunc = depthFunc;
-        }
-    }
-
-    public setDepthFunctionToGreater(): void {
-        if (this._depthCullingState.depthFunc !== Constants.GREATER) {
-            this._depthCullingState.depthFunc = Constants.GREATER;
-        }
-    }
-
-    public setDepthFunctionToGreaterOrEqual(): void {
-        if (this._depthCullingState.depthFunc !== Constants.GEQUAL) {
-            this._depthCullingState.depthFunc = Constants.GEQUAL;
-        }
-    }
-
-    public setDepthFunctionToLess(): void {
-        if (this._depthCullingState.depthFunc !== Constants.LESS) {
-            this._depthCullingState.depthFunc = Constants.LESS;
-        }
-    }
-
-    public setDepthFunctionToLessOrEqual(): void {
-        if (this._depthCullingState.depthFunc !== Constants.LEQUAL) {
-            this._depthCullingState.depthFunc = Constants.LEQUAL;
-        }
-    }
-
     /**
      * Set various states to the context
      * @param culling defines backface culling state
@@ -3467,23 +3377,26 @@ export class WebGPUEngine extends Engine {
             webgpuPipelineContext.uniformBuffer.update();
         }
 
-        let bufferKey = "";
+        let node: WebGPUBindGroupCacheNode = webgpuPipelineContext.bindGroupsCache;
         for (let i = 0; i < webgpuPipelineContext.shaderProcessingContext.uniformBufferNames.length; ++i) {
             const bufferName = webgpuPipelineContext.shaderProcessingContext.uniformBufferNames[i];
-            const dataBuffer = this._uniformsBuffers[bufferName];
-            if (dataBuffer) {
-                bufferKey += dataBuffer.uniqueId + "_";
+            const uboId = this._uniformsBuffers[bufferName].uniqueId;
+            let nextNode = node.values[uboId];
+            if (!nextNode) {
+                nextNode = new WebGPUBindGroupCacheNode();
+                node.values[uboId] = nextNode;
             }
+            node = nextNode;
         }
 
-        let bindGroups: GPUBindGroup[] = webgpuPipelineContext.bindGroupsCache[bufferKey];
+        let bindGroups: GPUBindGroup[] = node.bindGroups;
         if (bindGroups) {
             return bindGroups;
         }
 
         bindGroups = [];
 
-        webgpuPipelineContext.bindGroupsCache[bufferKey] = bindGroups;
+        node.bindGroups = bindGroups;
         this._counters.numBindGroupsCreation++;
 
         const bindGroupLayouts = webgpuPipelineContext.bindGroupLayouts;
@@ -3579,24 +3492,19 @@ export class WebGPUEngine extends Engine {
             renderPass.setIndexBuffer(this._currentIndexBuffer.underlyingResource, this._currentIndexBuffer!.is32Bits ? WebGPUConstants.IndexFormat.Uint32 : WebGPUConstants.IndexFormat.Uint16, 0);
         }
 
-        const effect = this._currentEffect!;
-        const attributes = effect.getAttributesNames();
-        let bufferIdx = 0;
+        const webgpuPipelineContext = this._currentEffect!._pipelineContext as WebGPUPipelineContext;
+        const attributes = webgpuPipelineContext.shaderProcessingContext.attributeNamesFromEffect;
         for (var index = 0; index < attributes.length; index++) {
-            const order = effect.getAttributeLocation(index);
-
-            if (order >= 0) {
-                let vertexBuffer = (this._currentOverrideVertexBuffers && this._currentOverrideVertexBuffers[attributes[index]]) ?? this._currentVertexBuffers![attributes[index]];
-                if (!vertexBuffer) {
-                    // In WebGL it's valid to not bind a vertex buffer to an attribute, but it's not valid in WebGPU
-                    // So we must bind a dummy buffer when we are not given one for a specific attribute
-                    vertexBuffer = this._emptyVertexBuffer;
-                }
+            let vertexBuffer = (this._currentOverrideVertexBuffers && this._currentOverrideVertexBuffers[attributes[index]]) ?? this._currentVertexBuffers![attributes[index]];
+            if (!vertexBuffer) {
+                // In WebGL it's valid to not bind a vertex buffer to an attribute, but it's not valid in WebGPU
+                // So we must bind a dummy buffer when we are not given one for a specific attribute
+                vertexBuffer = this._emptyVertexBuffer;
+            }
 
-                const buffer = vertexBuffer.getBuffer();
-                if (buffer) {
-                    renderPass.setVertexBuffer(bufferIdx++, buffer.underlyingResource, vertexBuffer.byteOffset);
-                }
+            const buffer = vertexBuffer.getBuffer();
+            if (buffer) {
+                renderPass.setVertexBuffer(index, buffer.underlyingResource, vertexBuffer.byteOffset);
             }
         }
     }
@@ -3612,26 +3520,6 @@ export class WebGPUEngine extends Engine {
     private _setRenderPipeline(fillMode: number): void {
         const renderPass = this._bundleEncoder || this._getCurrentRenderPass();
 
-        this._cacheRenderPipeline.setDepthCullingState(
-            !!this._depthCullingState.cull,
-            this._depthCullingState.frontFace ?? 2,
-            this._depthCullingState.cullFace ?? 1,
-            this._depthCullingState.zOffset,
-            this._depthCullingState.depthTest,
-            this._depthCullingState.depthMask,
-            this._depthCullingState.depthFunc
-        );
-
-        this._cacheRenderPipeline.setStencilState(
-            this._stencilState.stencilTest,
-            this._stencilState.stencilFunc,
-            this._stencilState.stencilOpDepthFail,
-            this._stencilState.stencilOpStencilDepthPass,
-            this._stencilState.stencilOpStencilFail,
-            this._stencilState.stencilFuncMask,
-            this._stencilState.stencilMask
-        );
-
         const pipeline = this._cacheRenderPipeline.getRenderPipeline(fillMode, this._currentEffect!, this._currentRenderTarget ? this._currentRenderTarget.samples : this._mainPassSampleCount);
         renderPass.setPipeline(pipeline);
 

+ 1 - 2
src/Materials/Textures/mirrorTexture.ts

@@ -171,8 +171,7 @@ export class MirrorTexture extends RenderTargetTexture {
             Matrix.ReflectionToRef(this.mirrorPlane, this._mirrorMatrix);
             this._mirrorMatrix.multiplyToRef(scene.getViewMatrix(), this._transformMatrix);
 
-            // Clone to not mark matrices as updated
-            scene.setTransformMatrix(this._transformMatrix.clone(), scene.getProjectionMatrix().clone());
+            scene.setTransformMatrix(this._transformMatrix, scene.getProjectionMatrix());
 
             saveClipPlane = scene.clipPlane;
             scene.clipPlane = this.mirrorPlane;

+ 20 - 0
src/Meshes/buffer.ts

@@ -261,6 +261,7 @@ export class VertexBuffer {
         } else {
             this._instanced = true;
         }
+        this._computeHashCode();
     }
 
     /**
@@ -289,6 +290,12 @@ export class VertexBuffer {
     public readonly uniqueId: number;
 
     /**
+     * Gets a hash code representing the format (type, normalized, size, instanced, stride) of this buffer
+     * All buffers with the same format will have the same hash code
+     */
+    public readonly hashCode: number;
+
+    /**
      * Constructor
      * @param engine the engine
      * @param data the data to use for this vertex buffer
@@ -353,6 +360,19 @@ export class VertexBuffer {
 
         this._instanced = instanced !== undefined ? instanced : false;
         this._instanceDivisor = instanced ? divisor : 0;
+
+        this._computeHashCode();
+    }
+
+    private _computeHashCode(): void {
+        // note: cast to any because the property is declared readonly
+        (this.hashCode as any) =
+            (((this.type - 5120) << 0) +
+            ((this.normalized ? 1 : 0) << 3) +
+            (this._size << 4) +
+            ((this._instanced ? 1 : 0) << 6) +
+            /* keep 5 bits free */
+            (this.byteStride << 12));
     }
 
     /** @hidden */

+ 19 - 17
src/States/depthCullingState.ts

@@ -4,27 +4,29 @@ import { Nullable } from "../types";
  * @hidden
  **/
 export class DepthCullingState {
-    private _isDepthTestDirty = false;
-    private _isDepthMaskDirty = false;
-    private _isDepthFuncDirty = false;
-    private _isCullFaceDirty = false;
-    private _isCullDirty = false;
-    private _isZOffsetDirty = false;
-    private _isFrontFaceDirty = false;
-
-    private _depthTest: boolean;
-    private _depthMask: boolean;
-    private _depthFunc: Nullable<number>;
-    private _cull: Nullable<boolean>;
-    private _cullFace: Nullable<number>;
-    private _zOffset: number;
-    private _frontFace: Nullable<number>;
+    protected _isDepthTestDirty = false;
+    protected _isDepthMaskDirty = false;
+    protected _isDepthFuncDirty = false;
+    protected _isCullFaceDirty = false;
+    protected _isCullDirty = false;
+    protected _isZOffsetDirty = false;
+    protected _isFrontFaceDirty = false;
+
+    protected _depthTest: boolean;
+    protected _depthMask: boolean;
+    protected _depthFunc: Nullable<number>;
+    protected _cull: Nullable<boolean>;
+    protected _cullFace: Nullable<number>;
+    protected _zOffset: number;
+    protected _frontFace: Nullable<number>;
 
     /**
      * Initializes the state.
      */
-    public constructor() {
-        this.reset();
+    public constructor(reset = true) {
+        if (reset) {
+            this.reset();
+        }
     }
 
     public get isDirty(): boolean {

+ 16 - 14
src/States/stencilState.ts

@@ -10,22 +10,22 @@ export class StencilState {
     /** Passed to stencilOperation to specify that stencil value must be replaced */
     public static readonly REPLACE = Constants.REPLACE;
 
-    private _isStencilTestDirty = false;
-    private _isStencilMaskDirty = false;
-    private _isStencilFuncDirty = false;
-    private _isStencilOpDirty = false;
+    protected _isStencilTestDirty = false;
+    protected _isStencilMaskDirty = false;
+    protected _isStencilFuncDirty = false;
+    protected _isStencilOpDirty = false;
 
-    private _stencilTest: boolean;
+    protected _stencilTest: boolean;
 
-    private _stencilMask: number;
+    protected _stencilMask: number;
 
-    private _stencilFunc: number;
-    private _stencilFuncRef: number;
-    private _stencilFuncMask: number;
+    protected _stencilFunc: number;
+    protected _stencilFuncRef: number;
+    protected _stencilFuncMask: number;
 
-    private _stencilOpStencilFail: number;
-    private _stencilOpDepthFail: number;
-    private _stencilOpStencilDepthPass: number;
+    protected _stencilOpStencilFail: number;
+    protected _stencilOpDepthFail: number;
+    protected _stencilOpStencilDepthPass: number;
 
     public get isDirty(): boolean {
         return this._isStencilTestDirty || this._isStencilMaskDirty || this._isStencilFuncDirty || this._isStencilOpDirty;
@@ -135,8 +135,10 @@ export class StencilState {
         this._isStencilTestDirty = true;
     }
 
-    public constructor() {
-        this.reset();
+    public constructor(reset = true) {
+        if (reset) {
+            this.reset();
+        }
     }
 
     public reset() {