diff --git a/examples/files.json b/examples/files.json index 2f8a0d483c19e6..35dff8d5a55d1e 100644 --- a/examples/files.json +++ b/examples/files.json @@ -314,6 +314,7 @@ "webgpu_compute_particles_rain", "webgpu_compute_particles_snow", "webgpu_compute_points", + "webgpu_compute_sort_bitonic", "webgpu_compute_texture", "webgpu_compute_texture_pingpong", "webgpu_compute_water", diff --git a/examples/screenshots/webgpu_compute_sort_bitonic.jpg b/examples/screenshots/webgpu_compute_sort_bitonic.jpg new file mode 100644 index 00000000000000..a0a4d0c1c21099 Binary files /dev/null and b/examples/screenshots/webgpu_compute_sort_bitonic.jpg differ diff --git a/examples/tags.json b/examples/tags.json index 646c127b08886d..b103ba8298fd06 100644 --- a/examples/tags.json +++ b/examples/tags.json @@ -119,6 +119,7 @@ "webgpu_compute_particles_rain": [ "gpgpu" ], "webgpu_compute_particles_snow_external": [ "gpgpu" ], "webgpu_compute_points": [ "gpgpu" ], + "webgpu_compute_sort_bitonic": [ "gpgpu" ], "webgpu_compute_texture": [ "gpgpu" ], "webgpu_compute_texture_pingpong": [ "gpgpu" ], "webgpu_depth_texture": [ "renderTarget" ], diff --git a/examples/webgpu_compute_sort_bitonic.html b/examples/webgpu_compute_sort_bitonic.html new file mode 100644 index 00000000000000..495539eceac118 --- /dev/null +++ b/examples/webgpu_compute_sort_bitonic.html @@ -0,0 +1,565 @@ + + + three.js webgpu - storage pbo external element + + + + + + +
+ three.js +
This example demonstrates a bitonic sort running step by step in a compute shader. +
The left canvas swaps values within workgroup local arrays. The right swaps values within storage buffers. +
Reference implementation by Tim Gfrerer +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/examples/webgpu_storage_buffer.html b/examples/webgpu_storage_buffer.html index 42fd2300a9509e..98edd3a8da541f 100644 --- a/examples/webgpu_storage_buffer.html +++ b/examples/webgpu_storage_buffer.html @@ -9,8 +9,8 @@
three.js -
This example demonstrate the fetch of external element from a StorageBuffer. -
Left canvas is using WebGPU Backend, right canvas is WebGL Backend. +
This example demonstrates fetching an external element from a StorageBuffer. +
The left canvas uses the WebGPU Backend, while the right uses the WebGL Backend.
import * as THREE from 'three'; - import { storageObject, If, vec3, uv, uint, float, Fn, instanceIndex } from 'three/tsl'; + import { storageObject, If, vec3, uv, uint, float, Fn, instanceIndex, workgroupBarrier } from 'three/tsl'; const timestamps = { webgpu: document.getElementById( 'timestamps' ), @@ -107,7 +107,8 @@ for ( let i = 0; i < type.length; i ++ ) { - const invertIndex = arrayBufferNodes[ i ].element( uint( size - 1 ).sub( instanceIndex ) ); + const invertIndex = arrayBufferNodes[ i ].element( uint( size - 1 ).sub( instanceIndex ) ).toVar(); + workgroupBarrier(); arrayBufferNodes[ i ].element( instanceIndex ).assign( invertIndex ); } diff --git a/src/nodes/TSL.js b/src/nodes/TSL.js index ca0f523abe1b1c..3275e00e9a911c 100644 --- a/src/nodes/TSL.js +++ b/src/nodes/TSL.js @@ -142,6 +142,8 @@ export * from './geometry/RangeNode.js'; // gpgpu export * from './gpgpu/ComputeNode.js'; +export * from './gpgpu/BarrierNode.js'; +export * from './gpgpu/WorkgroupInfoNode.js'; // lighting export * from './lighting/LightNode.js'; diff --git a/src/nodes/gpgpu/BarrierNode.js b/src/nodes/gpgpu/BarrierNode.js new file mode 100644 index 00000000000000..683e8d6472666a --- /dev/null +++ b/src/nodes/gpgpu/BarrierNode.js @@ -0,0 +1,40 @@ +import Node from '../core/Node.js'; +import { nodeProxy } from '../tsl/TSLCore.js'; + +class BarrierNode extends Node { + + constructor( scope ) { + + super(); + + this.scope = scope; + + } + + generate( builder ) { + + const { scope } = this; + const { renderer } = builder; + + if ( renderer.backend.isWebGLBackend === true ) { + + builder.addFlowCode( `\t// ${scope}Barrier \n` ); + + } else { + + builder.addLineFlowCode( `${scope}Barrier()` ); + + } + + } + +} + +export default BarrierNode; + +const barrier = nodeProxy( BarrierNode ); + +export const workgroupBarrier = () => barrier( 'workgroup' ).append(); +export const storageBarrier = () => barrier( 'storage' ).append(); +export const textureBarrier = () => barrier( 'texture' ).append(); + diff --git a/src/nodes/gpgpu/WorkgroupInfoNode.js b/src/nodes/gpgpu/WorkgroupInfoNode.js new file mode 100644 index 00000000000000..2bbe3e8eafdb9a --- /dev/null +++ b/src/nodes/gpgpu/WorkgroupInfoNode.js @@ -0,0 +1,100 @@ +import ArrayElementNode from '../utils/ArrayElementNode.js'; +import { nodeObject } from '../tsl/TSLCore.js'; +import Node from '../core/Node.js'; + +class WorkgroupInfoElementNode extends ArrayElementNode { + + constructor( workgroupInfoNode, indexNode ) { + + super( workgroupInfoNode, indexNode ); + + this.isWorkgroupInfoElementNode = true; + + } + + generate( builder, output ) { + + let snippet; + + const isAssignContext = builder.context.assign; + snippet = super.generate( builder ); + + if ( isAssignContext !== true ) { + + const type = this.getNodeType( builder ); + + snippet = builder.format( snippet, type, output ); + + } + + // TODO: Possibly activate clip distance index on index access rather than from clipping context + + return snippet; + + } + +} + + +class WorkgroupInfoNode extends Node { + + constructor( scope, bufferType, bufferCount = 0 ) { + + super( bufferType ); + + this.bufferType = bufferType; + this.bufferCount = bufferCount; + + this.isWorkgroupInfoNode = true; + + this.scope = scope; + + } + + label( name ) { + + this.name = name; + + return this; + + } + + getHash() { + + return this.uuid; + + } + + setScope( scope ) { + + this.scope = scope; + + return this; + + } + + getInputType( /*builder*/ ) { + + return `${this.scope}Array`; + + } + + element( indexNode ) { + + return nodeObject( new WorkgroupInfoElementNode( this, indexNode ) ); + + } + + generate( builder ) { + + return builder.getScopedArray( this.name || `${this.scope}Array_${this.id}`, this.scope.toLowerCase(), this.bufferType, this.bufferCount ); + + } + +} + +export default WorkgroupInfoNode; + +export const workgroupArray = ( type, count ) => nodeObject( new WorkgroupInfoNode( 'Workgroup', type, count ) ); + + diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js index 68d3afd224367b..8bab3e7a5e288a 100644 --- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js @@ -171,6 +171,8 @@ class WGSLNodeBuilder extends NodeBuilder { this.directives = {}; + this.scopedArrays = new Map(); + } needsToWorkingColorSpace( texture ) { @@ -766,6 +768,45 @@ ${ flowData.code } } + getScopedArray( name, scope, bufferType, bufferCount ) { + + if ( this.scopedArrays.has( name ) === false ) { + + this.scopedArrays.set( name, { + name, + scope, + bufferType, + bufferCount + } ); + + } + + return name; + + } + + getScopedArrays( shaderStage ) { + + if ( shaderStage !== 'compute' ) { + + return; + + } + + const snippets = []; + + for ( const { name, scope, bufferType, bufferCount } of this.scopedArrays.values() ) { + + const type = this.getType( bufferType ); + + snippets.push( `var<${scope}> ${name}: array< ${type}, ${bufferCount} >;` ); + + } + + return snippets.join( '\n' ); + + } + getAttributes( shaderStage ) { const snippets = []; @@ -1065,6 +1106,7 @@ ${ flowData.code } stageData.vars = this.getVars( shaderStage ); stageData.codes = this.getCodes( shaderStage ); stageData.directives = this.getDirectives( shaderStage ); + stageData.scopedArrays = this.getScopedArrays( shaderStage ); // @@ -1291,6 +1333,9 @@ ${shaderData.directives} // system var instanceIndex : u32; +// locals +${shaderData.scopedArrays} + // uniforms ${shaderData.uniforms} diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index d13a933163a0a2..926954c6a40c13 100644 --- a/test/e2e/puppeteer.js +++ b/test/e2e/puppeteer.js @@ -124,6 +124,7 @@ const exceptionList = [ // Awaiting for WebGPU Backend support in Puppeteer 'webgpu_storage_buffer', + 'webgpu_compute_sort_bitonic', // WebGPURenderer: Unknown problem 'webgpu_camera_logarithmicdepthbuffer',