API

WebGPU Non Compatibility Mode

WebGPU can run in a non compatibility mode (NCM, also called fast path) that can provide better performance, depending on your scene.

The WebGPUEngine.compatibilityMode property is a switch that is true by default. In this mode, the WebGPU engine runs in full compatibility mode (CM) with WebGL, meaning all scenes that can be rendered with WebGL can also be rendered by WebGPU without any changes on your side.

Setting this property to false switches to non compatibility mode (NCM), allowing the engine to use another code path that may improve performance at the cost of some possible reorganization of your code.

Description

What this mode does is build an object called a bundle, which holds the commands needed to draw a mesh, more precisely a submesh, in a given context, and then reuses this bundle each time the mesh must be drawn in that context. Building this bundle has a cost, so the bundle must live long enough to make up for that cost. If the bundle is recreated every frame, using NCM will actually hurt performance.

A bundle must be recreated when a number of things happen: you change the material, you change some global context directly on the engine, you update some vertex data, and so on. You can see how many bundles were recreated and how many were reused during the last frame by looking at the engine.countersLastFrame property:

> engine.countersLastFrame
> {numEnableEffects: 0, numEnableDrawWrapper: 2, numBundleCreationNonCompatMode: 0, numBundleReuseNonCompatMode: 2}

What you are interested in are the two numBundleXXX counters:

  • numBundleReuseNonCompatMode is the number of bundles that have been reused in the last frame
  • numBundleCreationNonCompatMode is the number of bundles that have been created in the last frame

The lower numBundleCreationNonCompatMode is, the better. The best case is when this counter is 0. Of course, depending on what you are doing, it may not be possible to keep this counter at 0 all the time, but you should try to keep it as low as possible.

Note: if both counters are 0 it means you are not in NCM, you should do engine.compatibilityMode = false to switch to NCM.

See the last section of this page for things to watch for that can lead to bundle creation or recreation, and for ways to modify your code to avoid it.

Caveats

When in NCM mode, it is theoretically possible that some things may not work as expected, for example that changing a material would not work. In that case, calling scene.resetDrawCache() would fix the problem, or mesh.resetDrawCache() if the problem affects only a single mesh.

In practice, however, this should happen only very infrequently, as explained in the next section. If it does happen, please report it on the forum so we can analyze the case and see whether it can be made to work automatically without calling resetDrawCache.

Also, even if you are able to achieve low, or even zero, bundle recreations each frame, you may not see a performance improvement over compatibility mode. It depends on your scene, so you should benchmark to see whether NCM improves things for you.

Do/Don't in non compatibility mode (NCM)

  • If two or more cameras are rendering into the same RTT using the Camera.outputRenderTarget property, set rtt.renderPassId = undefined so that camera.renderPassId is used for each camera instead of rtt.renderPassId for both cameras. Otherwise, this leads to bundle recreation.
  • If you are using skeletons, make sure a material is not linked to several different skeletons. That means a material should not be used by several meshes unless they all use the same skeleton. In that case, clone the material as many times as necessary. The same applies to morph targets.
  • Material.needDepthPrePass works in NCM, but it always creates new bundles each frame.
  • If you update one of these material properties after switching to NCM, you must reset the draw cache for the changes to take effect by calling either mesh.resetDrawCache() or scene.resetDrawCache(): sideOrientation, disableDepthWrite, forceDepthWrite, depthFunction, disableColorWrite, zOffset, stencil
  • If you update the samples property of a post-process or RTT, you must call scene.resetDrawCache() afterward. Otherwise, rendering will use the new value together with the old bundles created with the old samples value, and you will get an error like Attachment state of renderBundles[0] ([RenderBundle]) is not compatible with attachment state of [RenderPassEncoder]..
  • Not working in NCM, meaning do not switch to NCM if you want to use them, or your program will not work as expected:
    • Occlusion queries. They do not work because the order of draws and queries must be preserved. That is not possible in NCM because all draws are postponed to the end of the frame when calling executeBundles(), whereas queries are executed at the point where they are called.
    • Material.separateCullingPass = true does not work because of the way it is currently implemented.

Case analysis

Below is a list of all the playgrounds from our validation test suite that recreated one or more bundles each frame when we first tested them in non compatibility mode. We explain why they were creating bundles and how we fixed them, when that was possible. You can see the fixed code by appending the PG number, given inside the parentheses after the PG name, to the Playground URL (https://playground.babylonjs.com/).

WebGPU list (webgpu.json)

  • sphere with custom shader to display wireframe using glow layer (#Y05E2C#6)
    • why: a property of a material is changed two times in a frame, to change the color depending on where the mesh is drawn (the glow layer or the regular rendering)
    • solution: use two materials, one for each case
  • particle system matrix like (#WL44T7)
    • why: a new particle system is recreated to replace one that has reached its time to live
    • solution: none. New particle systems are recreated regularly as part of the normal operation of the PG, so it is expected that bundles are created regularly too

Default list (config.json)

  • Nested BBG, Chibi Rex, Yeti, Bones, GLTF Serializer Morph Target Animation Group (#ZG0C8B#5, #QATUCH#18, #QATUCH#19, #7EC27T#3, #T087A8#29)
    • why: use same material for different meshes that are using different skeletons
    • solution: use as many materials as the number of skeletons
  • Solid particle system (#WCDZS#92)
    • why: SPS mesh is pickable. When the mesh is pickable, the vertex attributes (position, normal) are updated by calling mesh.updateVerticesData which dirtifies the material and ends-up recreating the bundle used by the fast path code
    • solution: sets the mesh as not pickable
  • Ribbon morphing (#ACKC2#1)
    • why: updating the position attribute each frame
    • solution: none at the time, the CreateTube (and all functions of MeshBuilder) are using getVerticesData / updateVerticesData to update an existing instance, the latter method triggering a markAsAttributeDirty call on the material. We would need to update the GPU buffer directly without using updateVerticesData, but by doing so we would lose the baking CPU array which is necessary for the mesh builder methods to work (the Buffer.updateDirectly method is clearing the _data property but getVerticesData works only if this property is not null)
  • Custom render target (#TQCEBF#3)
    • why: changing the material used by a RenderTargetTexture in onBeforeRender and resetting it in onAfterRender
    • solution: use RenderTargetTexture.setMaterialForRendering instead of onBeforeRender / onAfterRender
  • Advanced shadows, Advanced shadows (right handed), Reverse depth buffer and shadows (#SLV8LW#3, #B48X7G#64, #WL4Q8J#20)
    • why: the same material is used for the eight floors. For Reverse depth buffer and shadows, the boxes, sphere, and knot reuse the same material. Each floor and box is lit by a specific light with its own shader generator. When a floor is drawn, the shadow sampler corresponding to that shadow generator is bound to the shader, and because all floors use the same material, setting a new shadow sampler resets the cache
    • solution: use a different (cloned) material for each floor
  • Motion Blur, Instances + GBR + motion blur (#E5YGEL#20, #YB006J#403)
    • why: the number of spheres (of trees in Instances + GBR + motion blur) visible each frame is never the same because they are moving. The LeftOver uniform buffer that stores the motion blur parameters for each sphere has a number of GPU buffers internally, one per sphere. When the fast path bundle is created, one buffer is associated to the bundle (buffer #0 for first sphere displayed, #1 for second sphere and so on). However, the next frame, the GPU buffer that will be used for a given sphere may be different because the buffers are reused starting from the first one: the first buffer is used for the first sphere displayed, second buffer is used for the second sphere displayed, etc. So, if the spheres are not displayed in the same order, the bundle(s) will be recreated
    • solution: none at the time. For the PG, we simply set alwaysSelectAsActiveMesh=true so that the number of spheres handled by the system does not vary
  • Thin instances + motion blur + manual (#HJGC2G#132)
    • why: thinInstanceSetBuffer was called each frame, which is (re)creating a vertex buffer, which in turn is flagging the material as "attribute dirty"
    • solution: use thinInstanceBufferUpdated instead
  • Multi cameras and output render target (#BCYE7J#31)
    • why: cameraRTT1 and cameraRTT2 are rendering into the same RTT (using their outputRenderTarget property), but when rendering in an RTT the render pass id which is used is RTT.renderPassId, meaning the meshes are rendered two times (because two cameras) with the same renderPassId, leading to bundle recreations (because the scene ubo is not the same in both cases as it stores the camera view/projection)
    • solution: remove the RTT.renderPassID property (RTT.renderPassID = undefined) => in that case, the camera.renderPassId value will be used instead
  • Order independent transparency (#1PLV5Z#104)
    • why: not using useRenderPasses = true on the depth peeling renderer
    • solution: set scene.depthPeelingRenderer.useRenderPasses = true;