diff --git a/examples/jsm/webxr/WebGLXRFallback.js b/examples/jsm/webxr/WebGLXRFallback.js index 269b3d77c4d224..645319c3e6112c 100644 --- a/examples/jsm/webxr/WebGLXRFallback.js +++ b/examples/jsm/webxr/WebGLXRFallback.js @@ -26,6 +26,12 @@ function setupWebGLXRFallback( renderer, createFallbackRenderer, onFallback = () } + if ( session !== null && renderer.backend.isWebGPUBackend === true && session.enabledFeatures.includes( 'webgpu' ) === false ) { + + return switchToFallbackRenderer( session, renderer ); + + } + try { return await setSession( session ); @@ -38,29 +44,41 @@ function setupWebGLXRFallback( renderer, createFallbackRenderer, onFallback = () } - const fallbackRenderer = createFallbackRenderer( renderer ); + return switchToFallbackRenderer( session, renderer ); - if ( fallbackRenderer.backend.isWebGLBackend !== true ) { + } - throw new Error( 'WebGLXRFallback: createFallbackRenderer() must return a renderer with a WebGL backend.' ); + }; - } + } - fallbackRenderer.xr.enabled = renderer.xr.enabled; - fallbackRenderer.xr.cameraAutoUpdate = renderer.xr.cameraAutoUpdate; - fallbackRenderer.xr.setFramebufferScaleFactor( renderer.xr.getFramebufferScaleFactor() ); - fallbackRenderer.xr.setReferenceSpaceType( renderer.xr.getReferenceSpaceType() ); + async function switchToFallbackRenderer( session, renderer ) { - await onFallback( fallbackRenderer, renderer ); + if ( renderer.initialized === false ) await renderer.init(); - currentRenderer = fallbackRenderer; - patchRenderer( fallbackRenderer ); + const fallbackRenderer = createFallbackRenderer( renderer ); - return fallbackRenderer.xr.setSession( session ); + if ( fallbackRenderer.backend.isWebGLBackend !== true ) { - } + throw new Error( 'WebGLXRFallback: createFallbackRenderer() must return a renderer with a WebGL backend.' ); - }; + } + + const animationLoop = renderer.getAnimationLoop(); + + fallbackRenderer.xr.enabled = renderer.xr.enabled; + fallbackRenderer.xr.cameraAutoUpdate = renderer.xr.cameraAutoUpdate; + fallbackRenderer.xr.setFramebufferScaleFactor( renderer.xr.getFramebufferScaleFactor() ); + fallbackRenderer.xr.setReferenceSpaceType( renderer.xr.getReferenceSpaceType() ); + + if ( animationLoop !== null ) await fallbackRenderer.setAnimationLoop( animationLoop ); + + await onFallback( fallbackRenderer, renderer ); + + currentRenderer = fallbackRenderer; + patchRenderer( fallbackRenderer ); + + return fallbackRenderer.xr.setSession( session ); } diff --git a/examples/webgpu_xr_cubes.html b/examples/webgpu_xr_cubes.html index 603cc1b5f3d9c5..44e78e833af689 100644 --- a/examples/webgpu_xr_cubes.html +++ b/examples/webgpu_xr_cubes.html @@ -115,7 +115,7 @@ // - document.body.appendChild( XRButton.createButton( renderer ) ); + document.body.appendChild( XRButton.createButton( renderer, { optionalFeatures: [ 'webgpu' ] } ) ); } diff --git a/examples/webgpu_xr_rollercoaster.html b/examples/webgpu_xr_rollercoaster.html index 71e87b378872d4..fe488cf0ae1752 100644 --- a/examples/webgpu_xr_rollercoaster.html +++ b/examples/webgpu_xr_rollercoaster.html @@ -44,7 +44,7 @@ setRenderer( renderer ); setupWebGLXRFallback( renderer, () => createRenderer( true ), setRenderer ); - document.body.appendChild( VRButton.createButton( renderer ) ); + document.body.appendChild( VRButton.createButton( renderer, { optionalFeatures: [ 'webgpu' ] } ) ); // diff --git a/src/nodes/lighting/PointShadowNode.js b/src/nodes/lighting/PointShadowNode.js index 9afe9ae265e7ea..01d6909fea7fc5 100644 --- a/src/nodes/lighting/PointShadowNode.js +++ b/src/nodes/lighting/PointShadowNode.js @@ -12,7 +12,7 @@ import { CubeDepthTexture } from '../../textures/CubeDepthTexture.js'; import { screenCoordinate } from '../display/ScreenNode.js'; import { interleavedGradientNoise, vogelDiskSample } from '../utils/PostProcessingUtils.js'; import { abs, normalize, cross } from '../math/MathNode.js'; -import { viewZToPerspectiveDepth, viewZToReversedPerspectiveDepth } from '../display/ViewportDepthNode.js'; +import { viewZToLogarithmicDepth, viewZToPerspectiveDepth, viewZToReversedPerspectiveDepth } from '../display/ViewportDepthNode.js'; const _clearColor = /*@__PURE__*/ new Color(); const _projScreenMatrix = /*@__PURE__*/ new Matrix4(); @@ -117,6 +117,11 @@ const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCo dp = viewZToReversedPerspectiveDepth( viewZ.negate(), shadowCameraNear, shadowCameraFar ); dp.subAssign( bias ); + } else if ( builder.renderer.logarithmicDepthBuffer ) { + + dp = viewZToLogarithmicDepth( viewZ.negate(), shadowCameraNear, shadowCameraFar ); + dp.addAssign( bias ); + } else { dp = viewZToPerspectiveDepth( viewZ.negate(), shadowCameraNear, shadowCameraFar ); diff --git a/src/renderers/common/Backend.js b/src/renderers/common/Backend.js index fb09e3259eaa0d..548f2103a634d7 100644 --- a/src/renderers/common/Backend.js +++ b/src/renderers/common/Backend.js @@ -124,6 +124,15 @@ class Backend { */ finishRender( /*renderContext*/ ) {} + /** + * Sets the XR rendering destination. + * + * Backends that render directly into XR framebuffers can override this hook. + * + * @param {?Object} xrTarget - The XR rendering destination. + */ + setXRTarget( /*xrTarget*/ ) {} + /** * This method is executed at the beginning of a compute call and * can be used by the backend to prepare the state for upcoming diff --git a/src/renderers/common/XRManager.js b/src/renderers/common/XRManager.js index a0e93f78742e09..0665dddf9a4ce4 100644 --- a/src/renderers/common/XRManager.js +++ b/src/renderers/common/XRManager.js @@ -7,7 +7,7 @@ import { Vector2 } from '../../math/Vector2.js'; import { Vector3 } from '../../math/Vector3.js'; import { Vector4 } from '../../math/Vector4.js'; import { WebXRController } from '../webxr/WebXRController.js'; -import { AddEquation, BackSide, CustomBlending, DepthFormat, DepthStencilFormat, FrontSide, RGBAFormat, UnsignedByteType, UnsignedInt248Type, UnsignedIntType, ZeroFactor } from '../../constants.js'; +import { AddEquation, BackSide, CustomBlending, DepthFormat, DepthStencilFormat, FrontSide, RGBAFormat, UnsignedByteType, UnsignedInt248Type, UnsignedIntType, ZeroFactor, LinearFilter } from '../../constants.js'; import { DepthTexture } from '../../textures/DepthTexture.js'; import { XRRenderTarget } from './XRRenderTarget.js'; import { CylinderGeometry } from '../../geometries/CylinderGeometry.js'; @@ -16,6 +16,7 @@ import { MeshBasicMaterial } from '../../materials/MeshBasicMaterial.js'; import { Mesh } from '../../objects/Mesh.js'; import { warn, warnOnce } from '../../utils.js'; import { renderOutput } from '../../nodes/display/RenderOutputNode.js'; +import { RenderTarget } from '../../core/RenderTarget.js'; const _cameraLPos = /*@__PURE__*/ new Vector3(); const _cameraRPos = /*@__PURE__*/ new Vector3(); @@ -24,9 +25,7 @@ const _contextNodeLib = /*@__PURE__*/ new WeakMap(); /** * The XR manager is built on top of the WebXR Device API to - * manage XR sessions with `WebGPURenderer`. - * - * XR is currently only supported with a WebGL 2 backend. + * manage XR sessions with renderer backends. * * @augments EventDispatcher */ @@ -85,6 +84,7 @@ class XRManager extends EventDispatcher { */ this._cameraL = new PerspectiveCamera(); this._cameraL.viewport = new Vector4(); + this._cameraL.matrixWorldAutoUpdate = false; /** * Represents the camera for the right eye. @@ -94,6 +94,7 @@ class XRManager extends EventDispatcher { */ this._cameraR = new PerspectiveCamera(); this._cameraR.viewport = new Vector4(); + this._cameraR.matrixWorldAutoUpdate = false; /** * A list of cameras used for rendering the XR views. @@ -182,6 +183,7 @@ class XRManager extends EventDispatcher { * @readonly */ this._supportsGlBinding = typeof XRWebGLBinding !== 'undefined'; + this._supportsWebGPUBinding = typeof globalThis.XRGPUBinding !== 'undefined'; /** * Helper function to create native WebXR Layer. @@ -227,6 +229,15 @@ class XRManager extends EventDispatcher { */ this._currentPixelRatio = null; + /** + * The renderer's sample count before XR temporarily overrides it. + * + * @private + * @type {?number} + * @default null + */ + this._currentSamples = null; + /** * The current size of the renderer's canvas * in logical pixel unit. @@ -342,6 +353,16 @@ class XRManager extends EventDispatcher { */ this._glBinding = null; + /** + * A reference to the current XR WebGPU binding. + * + * @private + * @type {?XRGPUBinding} + * @default null + */ + + this._webgpuBinding = null; + /** * A reference to the current XR projection layer. * @@ -445,16 +466,10 @@ class XRManager extends EventDispatcher { /** * Returns the foveation value. * - * @return {number|undefined} The foveation value. Returns `undefined` if no base or projection layer is defined. + * @return {number|undefined} The foveation value. */ getFoveation() { - if ( this._glProjLayer === null && this._glBaseLayer === null ) { - - return undefined; - - } - return this._foveation; } @@ -681,6 +696,203 @@ class XRManager extends EventDispatcher { } + /** + * Returns the current XR WebGPU binding. + * + * Creates a new binding if needed and the browser is + * capable of doing so. + * + * @return {?XRGPUBinding} The XR WebGPU binding. Returns `null` if one cannot be created. + */ + getWebGPUBinding() { + + if ( this._webgpuBinding === null && this._supportsWebGPUBinding ) { + + this._webgpuBinding = new globalThis.XRGPUBinding( this._session, this._renderer.backend.device ); + + } + + return this._webgpuBinding; + + } + + /** + * Returns whether the current XR session is using WebGPU. + * + * @private + * @return {boolean} Whether the current session uses the WebGPU backend and the `webgpu` session feature. + */ + _isWebGPUSession() { + + return this._renderer.backend.isWebGPUBackend === true && + this._session !== null && + this._session.enabledFeatures.includes( 'webgpu' ); + + } + + /** + * Validates the current WebGPU XR session requirements. + * + * @private + */ + _validateWebGPUSession() { + + const renderer = this._renderer; + + if ( renderer.backend.isWebGPUBackend !== true ) return; + + if ( this._session.enabledFeatures.includes( 'webgpu' ) === false ) { + + throw new Error( 'THREE.XRManager: WebGPU XR sessions require the "webgpu" session feature. Use VRButtonGPU/XRButton with "webgpu" enabled or use a WebGL backend.' ); + + } + + if ( renderer.samples > 0 ) { + + warnOnce( 'THREE.XRManager: WebGPU XR does not support MSAA yet. Disabling MSAA for this XR session.' ); + + if ( this._currentSamples === null ) this._currentSamples = renderer.samples; + renderer._samples = 0; + + } + + } + + /** + * Initializes the WebGPU XR projection layer and render target. + * + * @private + * @async + * @param {XRSession} session - The XR session. + * @return {Promise} + */ + async _initWebGPUSession( session ) { + + const webgpuBinding = this.getWebGPUBinding(); + const glProjLayer = webgpuBinding.createProjectionLayer( { + colorFormat: webgpuBinding.getPreferredColorFormat(), + depthStencilFormat: 'depth24plus' + } ); + + this._glProjLayer = glProjLayer; + + session.updateRenderState( { layers: [ glProjLayer ] } ); + + this._referenceSpace = await session.requestReferenceSpace( this.getReferenceSpaceType() ); + + this._xrRenderTarget = new RenderTarget( glProjLayer.textureWidth, glProjLayer.textureHeight, { + depth: 2, + minFilter: LinearFilter, + magFilter: LinearFilter, + depthBuffer: true, + multiview: false, + useArrayDepthTexture: true, + samples: 0 + } ); + + this._xrRenderTarget.texture.isArrayTexture = true; + + if ( this._useMultiviewIfPossible === true ) { + + warnOnce( 'THREE.XRManager: WebGPU XR does not support multiview yet. Disabling multiview for this XR session.' ); + + } + + this._useMultiview = false; + + } + + /** + * Releases WebGPU XR resources associated with the current session. + * + * @private + */ + _disposeWebGPUSession() { + + const renderer = this._renderer; + const xrRenderTarget = this._xrRenderTarget; + + if ( xrRenderTarget === null || renderer.backend.isWebGPUBackend !== true ) return; + + // XR textures are external (from XRGPUBinding), so clear cached state before disposal. + const backend = renderer.backend; + const texturesModule = renderer._textures; + + const renderTargetData = backend.get ? backend.get( xrRenderTarget ) : null; + if ( renderTargetData ) { + + renderTargetData.descriptors = undefined; + + } + + const deleteResource = ( resource ) => { + + if ( resource === null || resource === undefined ) return; + + if ( backend.delete ) backend.delete( resource ); + if ( texturesModule.delete ) texturesModule.delete( resource ); + + }; + + for ( let i = 0; i < xrRenderTarget.textures.length; i ++ ) { + + deleteResource( xrRenderTarget.textures[ i ] ); + + } + + deleteResource( xrRenderTarget.depthTexture ); + deleteResource( xrRenderTarget ); + + if ( renderer._renderContexts && renderer._renderContexts.dispose ) { + + renderer._renderContexts.dispose(); + + } + + xrRenderTarget.dispose(); + + } + + /** + * Collects WebGPU XR sub-image data for the current frame. + * + * @private + * @param {Array} views - The XR views for the current pose. + * @return {{colorTexture:?GPUTexture, viewDescriptors:Array, viewports:Array}} The WebGPU XR view data. + */ + _getWebGPUViewData( views ) { + + const webgpuBinding = this.getWebGPUBinding(); + const viewData = { + colorTexture: null, + viewDescriptors: [], + viewports: [] + }; + + for ( let i = 0; i < views.length; i ++ ) { + + const gpuSubImage = webgpuBinding.getViewSubImage( this._glProjLayer, views[ i ] ); + + if ( viewData.colorTexture === null ) { + + viewData.colorTexture = gpuSubImage.colorTexture; + + } + + viewData.viewports.push( gpuSubImage.viewport ); + + if ( gpuSubImage.getViewDescriptor ) { + + viewData.viewDescriptors.push( gpuSubImage.getViewDescriptor() ); + + } + + } + + return viewData; + + } + /** * Returns the current XR frame. * @@ -994,22 +1206,16 @@ class XRManager extends EventDispatcher { async setSession( session ) { const renderer = this._renderer; - const backend = renderer.backend; - if ( session !== null && backend.isWebGPUBackend === true ) { + if ( renderer.initialized === false ) await renderer.init(); - throw new Error( 'THREE.XRManager: XR is currently not supported with a WebGPU backend. Use WebGL by passing "{ forceWebGL: true }" to the constructor of the renderer.' ); - - } + this._gl = renderer.getContext(); + const gl = this._gl; this._session = session; if ( session !== null ) { - this._gl = renderer.getContext(); - const gl = this._gl; - const attributes = gl.getContextAttributes(); - session.addEventListener( 'select', this._onSessionEvent ); session.addEventListener( 'selectstart', this._onSessionEvent ); session.addEventListener( 'selectend', this._onSessionEvent ); @@ -1019,7 +1225,7 @@ class XRManager extends EventDispatcher { session.addEventListener( 'end', this._onSessionEnd ); session.addEventListener( 'inputsourceschange', this._onInputSourcesChange ); - await backend.makeXRCompatible(); + this._validateWebGPUSession(); this._currentPixelRatio = renderer.getPixelRatio(); renderer.getSize( this._currentSize ); @@ -1030,7 +1236,11 @@ class XRManager extends EventDispatcher { // - if ( this._supportsLayers === true ) { + if ( this._isWebGPUSession() ) { + + await this._initWebGPUSession( session ); + + } else if ( this._supportsLayers === true ) { // default path using XRProjectionLayer @@ -1038,6 +1248,10 @@ class XRManager extends EventDispatcher { let depthType = null; let glDepthFormat = null; + const attributes = gl.getContextAttributes(); + await renderer.backend.makeXRCompatible(); + this.setFoveation( this.getFoveation() ); + if ( renderer.depth ) { glDepthFormat = renderer.stencil ? gl.DEPTH24_STENCIL8 : gl.DEPTH_COMPONENT24; @@ -1120,6 +1334,8 @@ class XRManager extends EventDispatcher { } else { // fallback to XRWebGLLayer + await renderer.backend.makeXRCompatible(); + this.setFoveation( this.getFoveation() ); const layerInit = { antialias: renderer.currentSamples > 0, @@ -1157,8 +1373,6 @@ class XRManager extends EventDispatcher { // - this.setFoveation( this.getFoveation() ); - renderer._animation.setAnimationLoop( this._onAnimationFrame ); renderer._animation.setContext( session ); renderer._animation.start(); @@ -1456,13 +1670,23 @@ function onSessionEnd() { this._currentDepthNear = null; this._currentDepthFar = null; + if ( this._currentSamples !== null ) { + + renderer._samples = this._currentSamples; + this._currentSamples = null; + + } + // restore framebuffer/rendering state renderer._resetXRState(); + this._disposeWebGPUSession(); + this._session = null; this._xrRenderTarget = null; this._glBinding = null; + this._webgpuBinding = null; this._glBaseLayer = null; this._glProjLayer = null; @@ -1646,7 +1870,9 @@ function onAnimationFrame( time, frame ) { const views = pose.views; - if ( this._glBaseLayer !== null ) { + const webgpuViewData = this._isWebGPUSession() ? this._getWebGPUViewData( views ) : null; + + if ( this._glBaseLayer !== null && webgpuViewData === null ) { backend.setXRTarget( glBaseLayer.framebuffer ); @@ -1669,8 +1895,13 @@ function onAnimationFrame( time, frame ) { let viewport; - if ( this._supportsLayers === true ) { + if ( webgpuViewData !== null ) { + + viewport = webgpuViewData.viewports[ i ]; + + } else if ( this._supportsLayers === true ) { + // WebGL path: Use XRWebGLBinding const glSubImage = this._glBinding.getViewSubImage( this._glProjLayer, view ); viewport = glSubImage.viewport; @@ -1698,6 +1929,7 @@ function onAnimationFrame( time, frame ) { camera = new PerspectiveCamera(); camera.layers.enable( i ); camera.viewport = new Vector4(); + camera.matrixWorldAutoUpdate = false; this._cameras[ i ] = camera; } @@ -1723,6 +1955,16 @@ function onAnimationFrame( time, frame ) { } + if ( webgpuViewData !== null && webgpuViewData.colorTexture !== null ) { + + backend.setXRRenderTargetTextures( + this._xrRenderTarget, + webgpuViewData.colorTexture, + webgpuViewData.viewDescriptors + ); + + } + renderer.setOutputRenderTarget( this._xrRenderTarget ); const frameBufferTarget = renderer._getFrameBufferTarget(); diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 013d29e0099567..83849b5e3a2b9d 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -210,7 +210,8 @@ class WebGPUBackend extends Backend { const adapterOptions = { powerPreference: parameters.powerPreference, - featureLevel: 'compatibility' + featureLevel: 'compatibility', + xrCompatible: renderer.xr.enabled }; const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null; @@ -296,6 +297,25 @@ class WebGPUBackend extends Backend { } + /** + * Registers external GPU textures from `XRGPUBinding` for use in rendering. + * + * @param {RenderTarget} renderTarget - The render target to register the textures for. + * @param {GPUTexture} colorTexture - The shared XR color GPUTexture. + * @param {?Array} [viewDescriptors=null] - Optional view descriptors, one per XR view. + */ + setXRRenderTargetTextures( renderTarget, colorTexture, viewDescriptors = null ) { + + this.set( renderTarget.texture, { + texture: colorTexture, + format: colorTexture.format, + externalTexture: true, + xrViewDescriptors: viewDescriptors, + initialized: true + } ); + + } + /** * A reference to the context. * @@ -471,6 +491,74 @@ class WebGPUBackend extends Backend { } + /** + * Returns whether the current render context references external textures. + * + * External textures can change every frame, so their descriptors must not be cached. + * + * @private + * @param {RenderContext} renderContext - The render context. + * @return {boolean} Whether the render context uses external textures. + */ + _hasExternalTexture( renderContext ) { + + const textures = renderContext.textures; + + if ( textures === null ) return false; + + for ( let i = 0; i < textures.length; i ++ ) { + + if ( this.get( textures[ i ] ).externalTexture === true ) return true; + + } + + return false; + + } + + /** + * Creates attachment views for an external texture render target. + * + * @private + * @param {RenderContext} renderContext - The render context. + * @param {Object} textureData - The backend data for the texture. + * @return {Array} The attachment view descriptors. + */ + _createExternalTextureViews( renderContext, textureData ) { + + const textureViews = []; + const camera = renderContext.camera; + + if ( textureData.xrViewDescriptors && camera !== null && camera.isArrayCamera === true ) { + + for ( let i = 0; i < textureData.xrViewDescriptors.length; i ++ ) { + + textureViews.push( { + view: textureData.texture.createView( textureData.xrViewDescriptors[ i ] ), + resolveTarget: undefined, + depthSlice: undefined + } ); + + } + + } else { + + textureViews.push( { + view: textureData.texture.createView( { + dimension: GPUTextureViewDimension.TwoD, + baseArrayLayer: renderContext.activeCubeFace, + arrayLayerCount: 1 + } ), + resolveTarget: undefined, + depthSlice: undefined + } ); + + } + + return textureViews; + + } + /** * Returns the render pass descriptor for the given render context. * @@ -483,13 +571,15 @@ class WebGPUBackend extends Backend { const renderTarget = renderContext.renderTarget; const renderTargetData = this.get( renderTarget ); + const hasExternalTexture = this._hasExternalTexture( renderContext ); let descriptors = renderTargetData.descriptors; if ( descriptors === undefined || renderTargetData.width !== renderTarget.width || renderTargetData.height !== renderTarget.height || - renderTargetData.samples !== renderTarget.samples + renderTargetData.samples !== renderTarget.samples || + hasExternalTexture ) { descriptors = {}; @@ -501,7 +591,7 @@ class WebGPUBackend extends Backend { const cacheKey = renderContext.getCacheKey(); let descriptorBase = descriptors[ cacheKey ]; - if ( descriptorBase === undefined ) { + if ( descriptorBase === undefined || hasExternalTexture ) { const textures = renderContext.textures; const textureViews = []; @@ -514,6 +604,13 @@ class WebGPUBackend extends Backend { const textureData = this.get( textures[ i ] ); + if ( textureData.externalTexture === true ) { + + textureViews.push( ...this._createExternalTextureViews( renderContext, textureData ) ); + continue; + + } + _viewDescriptor.label = `colorAttachment_${ i }`; _viewDescriptor.baseMipLevel = renderContext.activeMipmapLevel; _viewDescriptor.mipLevelCount = 1; diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js index 1fbfcb7431ccff..0758a19d380e83 100644 --- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js @@ -276,6 +276,13 @@ class WebGPUTextureUtils { if ( textureData.initialized ) { + // Skip creation for external XR textures - they are already set up + if ( textureData.externalTexture === true ) { + + return; + + } + throw new Error( 'WebGPUTextureUtils: Texture already initialized.' ); } diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index eb67d1768da404..0b9e93d620981d 100644 --- a/test/e2e/puppeteer.js +++ b/test/e2e/puppeteer.js @@ -36,6 +36,7 @@ const exceptionList = [ 'webaudio_visualizer', 'webgpu_compute_audio', 'webgpu_compute_cloth', + 'webgpu_compute_particles_fluid', 'webgpu_compute_sort_bitonic', 'webgpu_storage_buffer', 'webgpu_tsl_editor',