diff --git a/editor/js/Loader.js b/editor/js/Loader.js index 5a5515388d02f5..2733ff8baf69f0 100644 --- a/editor/js/Loader.js +++ b/editor/js/Loader.js @@ -33,18 +33,104 @@ function Loader( editor ) { filesMap = filesMap || LoaderUtils.createFilesMap( files ); + const normalizeLookupPath = function ( path ) { + + let normalized = String( path || '' ).replace( /\\/g, '/' ); + const queryIndex = normalized.indexOf( '?' ); + if ( queryIndex !== - 1 ) normalized = normalized.slice( 0, queryIndex ); + const hashIndex = normalized.indexOf( '#' ); + if ( hashIndex !== - 1 ) normalized = normalized.slice( 0, hashIndex ); + + while ( normalized.startsWith( './' ) ) normalized = normalized.slice( 2 ); + while ( normalized.startsWith( '../' ) ) normalized = normalized.slice( 3 ); + while ( normalized.startsWith( '/' ) ) normalized = normalized.slice( 1 ); + + return normalized; + + }; + + const createFileFinder = function ( map ) { + + const suffixMap = {}; + const warnedAmbiguous = new Set(); + + const addCandidate = function ( suffix, candidate ) { + + if ( ! suffixMap[ suffix ] ) suffixMap[ suffix ] = []; + suffixMap[ suffix ].push( candidate ); + + }; + + for ( const rawKey in map ) { + + const key = normalizeLookupPath( rawKey ); + const file = map[ rawKey ]; + if ( key === '' || ! file ) continue; + + const parts = key.split( '/' ); + + for ( let i = 0; i < parts.length; i ++ ) { + + const suffix = parts.slice( i ).join( '/' ); + if ( suffix !== '' ) addCandidate( suffix, { key, file } ); + + } + + } + + for ( const suffix in suffixMap ) { + + suffixMap[ suffix ].sort( function ( a, b ) { + + if ( a.key.length !== b.key.length ) return a.key.length - b.key.length; + if ( a.key < b.key ) return - 1; + if ( a.key > b.key ) return 1; + return 0; + + } ); + + } + + return function findFile( url ) { + + const lookup = normalizeLookupPath( url ); + if ( lookup === '' ) return null; + + const candidates = suffixMap[ lookup ]; + if ( ! candidates || candidates.length === 0 ) return null; + if ( candidates.length === 1 ) return candidates[ 0 ]; + + for ( let i = 0; i < candidates.length; i ++ ) { + + if ( candidates[ i ].key === lookup ) return candidates[ i ]; + + } + + if ( ! warnedAmbiguous.has( lookup ) ) { + + console.warn( 'Loader: Ambiguous file reference "' + lookup + '". Using "' + candidates[ 0 ].key + '".' ); + warnedAmbiguous.add( lookup ); + + } + + return candidates[ 0 ]; + + }; + + }; + + const findFile = createFileFinder( filesMap ); + const manager = new THREE.LoadingManager(); manager.setURLModifier( function ( url ) { - url = url.replace( /^(\.?\/)/, '' ); // remove './' - - const file = filesMap[ url ]; + const resolved = findFile( url ); - if ( file ) { + if ( resolved ) { console.log( 'Loading', url ); - return URL.createObjectURL( file ); + return URL.createObjectURL( resolved.file ); } diff --git a/examples/files.json b/examples/files.json index b7b5f00e34375a..023ca2c07e02e8 100644 --- a/examples/files.json +++ b/examples/files.json @@ -308,6 +308,7 @@ "webgpu_centroid_sampling", "webgpu_clearcoat", "webgpu_clipping", + "webgpu_compile_async", "webgpu_compute_audio", "webgpu_compute_birds", "webgpu_compute_cloth", diff --git a/examples/jsm/loaders/usd/USDCParser.js b/examples/jsm/loaders/usd/USDCParser.js index 98425b668f7cc7..e78348d7565012 100644 --- a/examples/jsm/loaders/usd/USDCParser.js +++ b/examples/jsm/loaders/usd/USDCParser.js @@ -1092,6 +1092,19 @@ class USDCParser { // Seek to payload offset and read value const offset = valueRep.payload; + if ( offset === 0 && isArray ) { + + // Spec 16.3.9.3: Array payload 0 is an explicit empty-array sentinel. + return []; + + } + + if ( offset < 0 || offset >= this.buffer.byteLength ) { + + throw new RangeError( 'USDCParser: Invalid payload offset ' + offset + ' for type ' + type + '.' ); + + } + const savedOffset = this.reader.tell(); this.reader.seek( offset ); @@ -1569,6 +1582,19 @@ class USDCParser { } + if ( ! Number.isSafeInteger( size ) || size < 0 ) { + + throw new RangeError( 'USDCParser: Invalid array size ' + size + ' for type ' + type + '.' ); + + } + + if ( size > 0x7FFFFFFF ) { + + // Crate stores counts as uint64, but JS typed arrays cannot represent all such sizes. + throw new RangeError( 'USDCParser: Array size ' + size + ' exceeds implementation limits.' ); + + } + if ( size === 0 ) return []; // Handle compressed arrays diff --git a/examples/jsm/loaders/usd/USDComposer.js b/examples/jsm/loaders/usd/USDComposer.js index fcfb21430cc4a4..fcc3dfc2683ba2 100644 --- a/examples/jsm/loaders/usd/USDComposer.js +++ b/examples/jsm/loaders/usd/USDComposer.js @@ -653,12 +653,13 @@ class USDComposer { if ( obj ) { parent.add( obj ); + this._buildHierarchy( obj, path ); } - } else if ( typeName === 'Material' || typeName === 'Shader' ) { + } else if ( typeName === 'Material' || typeName === 'Shader' || typeName === 'GeomSubset' ) { - // Skip materials/shaders, they're referenced by meshes + // Skip materials/shaders/subsets, they're referenced by meshes } else { @@ -1112,13 +1113,13 @@ class USDComposer { } const displayOpacity = attrs[ 'primvars:displayOpacity' ]; - if ( displayOpacity && displayOpacity.length >= 1 ) { + if ( displayOpacity && displayOpacity.length === 1 && geomSubsets.length === 0 ) { const opacity = displayOpacity[ 0 ]; const applyDisplayOpacity = ( mat ) => { - if ( opacity < 1 ) { + if ( opacity < 1 && mat.opacity === 1 && mat.transparent === false ) { mat.opacity = opacity; mat.transparent = true; diff --git a/examples/screenshots/webgpu_compile_async.jpg b/examples/screenshots/webgpu_compile_async.jpg new file mode 100644 index 00000000000000..ff3afe1088211e Binary files /dev/null and b/examples/screenshots/webgpu_compile_async.jpg differ diff --git a/examples/webgpu_compile_async.html b/examples/webgpu_compile_async.html new file mode 100644 index 00000000000000..ea42ab7784107d --- /dev/null +++ b/examples/webgpu_compile_async.html @@ -0,0 +1,355 @@ + + + + three.js webgpu - async node compilation + + + + + + + +
+ + +
+ three.jsAsync Node Compilation +
+ + NodeMaterial shaders compile asynchronously without blocking the render loop. Animation stays smooth while 256 unique TSL materials build in the background. +
+ +
+
Longest frame: -
+
Meshes added: 0 / 256
+
Mode: -
+
+ + + + + + + diff --git a/src/nodes/core/NodeBuilder.js b/src/nodes/core/NodeBuilder.js index 237096e9f89b7e..360c4f5a58a01e 100644 --- a/src/nodes/core/NodeBuilder.js +++ b/src/nodes/core/NodeBuilder.js @@ -30,7 +30,7 @@ import { Vector2 } from '../../math/Vector2.js'; import { Vector3 } from '../../math/Vector3.js'; import { Vector4 } from '../../math/Vector4.js'; import { Float16BufferAttribute } from '../../core/BufferAttribute.js'; -import { warn, error } from '../../utils.js'; +import { warn, error, yieldToMain } from '../../utils.js'; let _id = 0; @@ -3019,6 +3019,89 @@ class NodeBuilder { } + /** + * Async version of build() that yields to main thread between shader stages. + * Use this in compileAsync() to prevent blocking the main thread. + * + * @return {Promise} A promise that resolves to this node builder. + */ + async buildAsync() { + + const { object, material, renderer } = this; + + if ( material !== null ) { + + let nodeMaterial = renderer.library.fromMaterial( material ); + + if ( nodeMaterial === null ) { + + error( `NodeMaterial: Material "${ material.type }" is not compatible.` ); + + nodeMaterial = new NodeMaterial(); + + } + + nodeMaterial.build( this ); + + } else { + + this.addFlow( 'compute', object ); + + } + + // setup() -> stage 1: create possible new nodes and/or return an output reference node + // analyze() -> stage 2: analyze nodes to possible optimization and validation + // generate() -> stage 3: generate shader + + for ( const buildStage of defaultBuildStages ) { + + this.setBuildStage( buildStage ); + + if ( this.context.position && this.context.position.isNode ) { + + this.flowNodeFromShaderStage( 'vertex', this.context.position ); + + } + + for ( const shaderStage of shaderStages ) { + + this.setShaderStage( shaderStage ); + + const flowNodes = this.flowNodes[ shaderStage ]; + + for ( const node of flowNodes ) { + + if ( buildStage === 'generate' ) { + + this.flowNode( node ); + + } else { + + node.build( this ); + + } + + } + + // Yield to main thread after each shader stage to prevent blocking + await yieldToMain(); + + } + + } + + this.setBuildStage( null ); + this.setShaderStage( null ); + + // stage 4: build code for a specific output + + this.buildCode(); + this.buildUpdateNodes(); + + return this; + + } + /** * Returns shared data object for the given node. * diff --git a/src/renderers/common/Pipelines.js b/src/renderers/common/Pipelines.js index d0adbea0795dfa..a965abe9a11219 100644 --- a/src/renderers/common/Pipelines.js +++ b/src/renderers/common/Pipelines.js @@ -234,6 +234,26 @@ class Pipelines extends DataMap { } + /** + * Checks if the render pipeline for the given render object is ready for drawing. + * Returns false if the GPU pipeline is still being compiled asynchronously. + * + * @param {RenderObject} renderObject - The render object. + * @return {boolean} True if the pipeline is ready for drawing. + */ + isReady( renderObject ) { + + const data = this.get( renderObject ); + const pipeline = data.pipeline; + + if ( pipeline === undefined ) return false; + + const pipelineData = this.backend.get( pipeline ); + + return pipelineData.pipeline !== undefined && pipelineData.pipeline !== null; + + } + /** * Deletes the pipeline for the given render object. * diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index 5b6175c8ecf486..756a2b4ed5f905 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -36,7 +36,7 @@ import { float, vec3, vec4, Fn } from '../../nodes/tsl/TSLCore.js'; import { reference } from '../../nodes/accessors/ReferenceNode.js'; import { highpModelNormalViewMatrix, highpModelViewMatrix } from '../../nodes/accessors/ModelNode.js'; import { context } from '../../nodes/core/ContextNode.js'; -import { error, warn, warnOnce } from '../../utils.js'; +import { error, warn, warnOnce, yieldToMain } from '../../utils.js'; const _scene = /*@__PURE__*/ new Scene(); const _drawingBufferSize = /*@__PURE__*/ new Vector2(); @@ -877,11 +877,15 @@ class Renderer { // - const sceneRef = ( scene.isScene === true ) ? scene : _scene; - if ( targetScene === null ) targetScene = scene; - const renderTarget = this._renderTarget; + // Use the actual scene for caching when compiling individual objects + // This ensures cache keys match between compileAsync and render + const sceneRef = ( scene.isScene === true ) ? scene : ( targetScene.isScene === true ) ? targetScene : _scene; + + // Match render()'s logic: use frameBufferTarget when needsFrameBufferTarget is true + const useFrameBufferTarget = this.needsFrameBufferTarget && this._renderTarget === null; + const renderTarget = useFrameBufferTarget ? this._getFrameBufferTarget() : ( this._renderTarget || this._outputRenderTarget ); const renderContext = this._renderContexts.get( renderTarget, this._mrt ); const activeMipmapLevel = this._activeMipmapLevel; @@ -914,7 +918,8 @@ class Renderer { // - const renderList = this._renderLists.get( scene, camera ); + // Use sceneRef for render list to ensure lightsNode matches between compileAsync and render + const renderList = this._renderLists.get( sceneRef, camera ); renderList.begin(); this._projectObject( scene, camera, 0, renderList, renderContext.clippingContext ); @@ -966,7 +971,7 @@ class Renderer { } - // process render lists + // process render lists - _createObjectPipeline will push async promises to _compilationPromises const opaqueObjects = renderList.opaque; const transparentObjects = renderList.transparent; @@ -985,9 +990,39 @@ class Renderer { this._handleObjectFunction = previousHandleObjectFunction; this._compilationPromises = previousCompilationPromises; - // wait for all promises setup by backends awaiting compilation/linking/pipeline creation to complete + // Process compilation work items sequentially to avoid freezing + // Yields between objects to keep animation smooth + + for ( const item of compilationPromises ) { + + const renderObject = this._objects.get( item.object, item.material, item.scene, item.camera, item.lightsNode, item.renderContext, item.clippingContext, item.passId ); + renderObject.drawRange = item.object.geometry.drawRange; + renderObject.group = item.group; + + this._geometries.updateForRender( renderObject ); + + // Use async node building to yield to main thread + await this._nodes.getForRenderAsync( renderObject ); + + this._nodes.updateBefore( renderObject ); + this._nodes.updateForRender( renderObject ); + this._bindings.updateForRender( renderObject ); - await Promise.all( compilationPromises ); + // Wait for pipeline creation + const pipelinePromises = []; + this._pipelines.getForRender( renderObject, pipelinePromises ); + if ( pipelinePromises.length > 0 ) { + + await Promise.all( pipelinePromises ); + + } + + this._nodes.updateAfter( renderObject ); + + // Yield between objects to allow animation frames + await yieldToMain(); + + } } @@ -3395,9 +3430,13 @@ class Renderer { // - this.backend.draw( renderObject, this.info ); + if ( this._pipelines.isReady( renderObject ) ) { + + this.backend.draw( renderObject, this.info ); - if ( needsRefresh ) this._nodes.updateAfter( renderObject ); + if ( needsRefresh ) this._nodes.updateAfter( renderObject ); + + } } @@ -3417,6 +3456,27 @@ class Renderer { */ _createObjectPipeline( object, material, scene, camera, lightsNode, group, clippingContext, passId ) { + // If in async compilation mode, queue the work for sequential execution + if ( this._compilationPromises !== null ) { + + // Store work items instead of promises - will be processed sequentially + this._compilationPromises.push( { + object, + material, + scene, + camera, + lightsNode, + group, + clippingContext, + passId, + renderContext: this._currentRenderContext + } ); + + return; + + } + + // Sync path const renderObject = this._objects.get( object, material, scene, camera, lightsNode, this._currentRenderContext, clippingContext, passId ); renderObject.drawRange = object.geometry.drawRange; renderObject.group = group; diff --git a/src/renderers/common/nodes/NodeManager.js b/src/renderers/common/nodes/NodeManager.js index 8d9eadd158d728..82eef1c9e3673f 100644 --- a/src/renderers/common/nodes/NodeManager.js +++ b/src/renderers/common/nodes/NodeManager.js @@ -76,6 +76,22 @@ class NodeManager extends DataMap { */ this.groupsData = new ChainMap(); + /** + * Queue for pending async builds to limit concurrent compilation. + * + * @private + * @type {Array} + */ + this._buildQueue = []; + + /** + * Whether an async build is currently in progress. + * + * @private + * @type {boolean} + */ + this._buildInProgress = false; + /** * A cache for managing node objects of * scene properties like fog or environments. @@ -174,13 +190,44 @@ class NodeManager extends DataMap { } + /** + * Creates a node builder configured for the given render object and material. + * + * @private + * @param {RenderObject} renderObject - The render object. + * @param {Material} material - The material to use. + * @return {NodeBuilder} The configured node builder. + */ + _createNodeBuilder( renderObject, material ) { + + const nodeBuilder = this.backend.createNodeBuilder( renderObject.object, this.renderer ); + nodeBuilder.scene = renderObject.scene; + nodeBuilder.material = material; + nodeBuilder.camera = renderObject.camera; + nodeBuilder.context.material = material; + nodeBuilder.lightsNode = renderObject.lightsNode; + nodeBuilder.environmentNode = this.getEnvironmentNode( renderObject.scene ); + nodeBuilder.fogNode = this.getFogNode( renderObject.scene ); + nodeBuilder.clippingContext = renderObject.clippingContext; + + if ( this.renderer.getOutputRenderTarget() ? this.renderer.getOutputRenderTarget().multiview : false ) { + + nodeBuilder.enableMultiview(); + + } + + return nodeBuilder; + + } + /** * Returns a node builder state for the given render object. * * @param {RenderObject} renderObject - The render object. - * @return {NodeBuilderState} The node builder state. + * @param {boolean} [useAsync=false] - Whether to use async build with yielding. + * @return {NodeBuilderState|Promise} The node builder state (or Promise if async). */ - getForRender( renderObject ) { + getForRender( renderObject, useAsync = false ) { const renderObjectData = this.get( renderObject ); @@ -196,20 +243,37 @@ class NodeManager extends DataMap { if ( nodeBuilderState === undefined ) { - const createNodeBuilder = ( material ) => { + const buildNodeBuilder = async () => { + + let nodeBuilder = this._createNodeBuilder( renderObject, renderObject.material ); - const nodeBuilder = this.backend.createNodeBuilder( renderObject.object, this.renderer ); - nodeBuilder.scene = renderObject.scene; - nodeBuilder.material = material; - nodeBuilder.camera = renderObject.camera; - nodeBuilder.context.material = material; - nodeBuilder.lightsNode = renderObject.lightsNode; - nodeBuilder.environmentNode = this.getEnvironmentNode( renderObject.scene ); - nodeBuilder.fogNode = this.getFogNode( renderObject.scene ); - nodeBuilder.clippingContext = renderObject.clippingContext; - if ( this.renderer.getOutputRenderTarget() ? this.renderer.getOutputRenderTarget().multiview : false ) { + try { - nodeBuilder.enableMultiview(); + if ( useAsync ) { + + await nodeBuilder.buildAsync(); + + } else { + + nodeBuilder.build(); + + } + + } catch ( e ) { + + nodeBuilder = this._createNodeBuilder( renderObject, new NodeMaterial() ); + + if ( useAsync ) { + + await nodeBuilder.buildAsync(); + + } else { + + nodeBuilder.build(); + + } + + error( 'TSL: ' + e ); } @@ -217,34 +281,52 @@ class NodeManager extends DataMap { }; - let nodeBuilder = createNodeBuilder( renderObject.material ); + if ( useAsync ) { - try { + return buildNodeBuilder().then( ( nodeBuilder ) => { - nodeBuilder.build(); + nodeBuilderState = this._createNodeBuilderState( nodeBuilder ); + nodeBuilderCache.set( cacheKey, nodeBuilderState ); + nodeBuilderState.usedTimes ++; + renderObjectData.nodeBuilderState = nodeBuilderState; - } catch ( e ) { + return nodeBuilderState; - nodeBuilder = createNodeBuilder( new NodeMaterial() ); - nodeBuilder.build(); + } ); - let stackTrace = e.stackTrace; + } else { - if ( ! stackTrace && e.stack ) { + // Synchronous path - call buildNodeBuilder but don't await + let nodeBuilder = this._createNodeBuilder( renderObject, renderObject.material ); - // Capture stack trace for JavaScript errors + try { - stackTrace = new StackTrace( e.stack ); + nodeBuilder.build(); - } + } catch ( e ) { - error( 'TSL: ' + e, stackTrace ); + nodeBuilder = this._createNodeBuilder( renderObject, new NodeMaterial() ); + nodeBuilder.build(); - } + let stackTrace = e.stackTrace; + + if ( ! stackTrace && e.stack ) { - nodeBuilderState = this._createNodeBuilderState( nodeBuilder ); + // Capture stack trace for JavaScript errors - nodeBuilderCache.set( cacheKey, nodeBuilderState ); + stackTrace = new StackTrace( e.stack ); + + } + + error( 'TSL: ' + e, stackTrace ); + + } + + nodeBuilderState = this._createNodeBuilderState( nodeBuilder ); + + nodeBuilderCache.set( cacheKey, nodeBuilderState ); + + } } @@ -258,6 +340,114 @@ class NodeManager extends DataMap { } + /** + * Async version of getForRender() that yields to main thread during build. + * Use this in compileAsync() to prevent blocking the main thread. + * + * @param {RenderObject} renderObject - The render object. + * @return {Promise} A promise that resolves to the node builder state. + */ + getForRenderAsync( renderObject ) { + + const result = this.getForRender( renderObject, true ); + + // Ensure we always return a Promise (cache hit returns nodeBuilderState directly) + if ( result.then ) { + + return result; + + } + + return Promise.resolve( result ); + + } + + /** + * Returns nodeBuilderState if ready, null if pending async build. + * Queues async build on first call for cache miss. + * Use this in render() path to enable non-blocking compilation. + * + * @param {RenderObject} renderObject - The render object. + * @return {?NodeBuilderState} The node builder state, or null if still building. + */ + getForRenderDeferred( renderObject ) { + + const renderObjectData = this.get( renderObject ); + + // Already built for this renderObject + if ( renderObjectData.nodeBuilderState !== undefined ) { + + return renderObjectData.nodeBuilderState; + + } + + // Check cache with stable key + const cacheKey = this.getForRenderCacheKey( renderObject ); + const nodeBuilderState = this.nodeBuilderCache.get( cacheKey ); + + if ( nodeBuilderState !== undefined ) { + + // Cache hit - use it + nodeBuilderState.usedTimes ++; + renderObjectData.nodeBuilderState = nodeBuilderState; + return nodeBuilderState; + + } + + // Cache miss - check if async build already queued + if ( renderObjectData.pendingBuild !== true ) { + + // Mark as pending and add to build queue + renderObjectData.pendingBuild = true; + + this._buildQueue.push( () => { + + return this.getForRenderAsync( renderObject ).then( () => { + + renderObjectData.pendingBuild = false; + + } ); + + } ); + + // Start processing queue if not already running + this._processBuildQueue(); + + } + + return null; // Not ready + + } + + /** + * Processes the build queue one item at a time. + * This ensures builds don't all run simultaneously and freeze the main thread. + * + * @private + */ + _processBuildQueue() { + + if ( this._buildInProgress || this._buildQueue.length === 0 ) { + + return; + + } + + this._buildInProgress = true; + + const buildFn = this._buildQueue.shift(); + + buildFn().then( () => { + + this._buildInProgress = false; + + // Process next item in queue + this._processBuildQueue(); + + } ); + + } + /** * Deletes the given object from the internal data map * diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js index 0c13695e476339..b00231b5ea2d13 100644 --- a/src/renderers/webgl-fallback/WebGLBackend.js +++ b/src/renderers/webgl-fallback/WebGLBackend.js @@ -1618,7 +1618,8 @@ class WebGLBackend extends Backend { // this.set( pipeline, { - programGPU + programGPU, + pipeline: programGPU } ); } diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 3d6f8042127dd9..b23217b0d0a412 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -1498,6 +1498,7 @@ class WebGPUBackend extends Backend { const pipelineData = this.get( pipeline ); const pipelineGPU = pipelineData.pipeline; + // Skip if pipeline has error if ( pipelineData.error === true ) return; const index = renderObject.getIndex(); diff --git a/src/utils.js b/src/utils.js index 40ed0bb310a346..829e88ec623cea 100644 --- a/src/utils.js +++ b/src/utils.js @@ -347,6 +347,28 @@ function warnOnce( ...params ) { } +/** + * Yields execution to the main thread to allow rendering and other tasks. + * Uses scheduler.yield() when available (Chrome 115+), falls back to requestAnimationFrame. + * + * @return {Promise} + */ +function yieldToMain() { + + if ( typeof self !== 'undefined' && typeof self.scheduler !== 'undefined' && typeof self.scheduler.yield !== 'undefined' ) { + + return self.scheduler.yield(); + + } + + return new Promise( resolve => { + + requestAnimationFrame( resolve ); + + } ); + +} + /** * Asynchronously probes for WebGL sync object completion. * @@ -468,4 +490,4 @@ const ReversedDepthFuncs = { [ GreaterEqualDepth ]: LessEqualDepth, }; -export { arrayMin, arrayMax, arrayNeedsUint32, getTypedArray, createElementNS, createCanvasElement, setConsoleFunction, getConsoleFunction, log, warn, error, warnOnce, probeAsync, toNormalizedProjectionMatrix, toReversedProjectionMatrix, isTypedArray, ReversedDepthFuncs }; +export { arrayMin, arrayMax, arrayNeedsUint32, getTypedArray, createElementNS, createCanvasElement, setConsoleFunction, getConsoleFunction, log, warn, error, warnOnce, probeAsync, yieldToMain, toNormalizedProjectionMatrix, toReversedProjectionMatrix, isTypedArray, ReversedDepthFuncs }; diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index b2aff0101c0114..49ab0bf6a2dec1 100644 --- a/test/e2e/puppeteer.js +++ b/test/e2e/puppeteer.js @@ -63,6 +63,7 @@ const exceptionList = [ 'webgpu_shadowmap', // WebGPU needed + 'webgpu_compile_async', 'webgpu_compute_audio', 'webgpu_compute_birds', 'webgpu_compute_cloth',