Просмотр исходного кода

WebGPURenderer: Workgroup Arrays and Barrier Support (#29192)

* init

* barrier, private array, workgroup array support

* clean

* Implement Renaud suggestions

* fix

* fix storage buffer example with workgroupBarrier()

* add tags and other info

* add bitonic sort example

* update

* Rebase branch

* try to fix bitonic sort shader

* simplify

* fix

* bitonic sort now works but local swap is slower than global swap :

* cleanup

* fix rebase issues

* Change display and html to make difference between global and local swap clearer. May want to improve the performance of the fragment shader by writing nextAlgo and nextBlockHeight to uniforms on the CPU side

* update (ugly?) screenshot

* cleanup

---------
Christian Helgeson 1 год назад
Родитель
Сommit
1174d07040

+ 1 - 0
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",

BIN
examples/screenshots/webgpu_compute_sort_bitonic.jpg


+ 1 - 0
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" ],

+ 565 - 0
examples/webgpu_compute_sort_bitonic.html

@@ -0,0 +1,565 @@
+<html lang="en">
+	<head>
+		<title>three.js webgpu - storage pbo external element</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a>
+			<br /> This example demonstrates a bitonic sort running step by step in a compute shader.
+			<br /> The left canvas swaps values within workgroup local arrays. The right swaps values within storage buffers.
+			<br /> Reference implementation by <a href="https://poniesandlight.co.uk/reflect/bitonic_merge_sort/">Tim Gfrerer</a>
+			<br /> 
+			<div id="local_swap" style="
+				position: absolute;
+				top: 150px;
+				left: 0;
+				padding: 10px;
+				background: rgba( 0, 0, 0, 0.5 );
+				color: #fff;
+				font-family: monospace;
+				font-size: 12px;
+				line-height: 1.5;
+				pointer-events: none;
+				text-align: left;
+			"></div>
+			<div id="global_swap" style="
+			position: absolute;
+			top: 150px;
+			right: 0;
+			padding: 10px;
+			background: rgba( 0, 0, 0, 0.5 );
+			color: #fff;
+			font-family: monospace;
+			font-size: 12px;
+			line-height: 1.5;
+			pointer-events: none;
+			text-align: left;
+		"></div>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.webgpu.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { storageObject, If, vec3, not, uniform, uv, uint, float, Fn, vec2, abs, int, invocationLocalIndex, workgroupArray, uvec2, floor, instanceIndex, workgroupBarrier } from 'three/tsl';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			const StepType = {
+
+				NONE: 0,
+				// Swap values within workgroup local buffer.
+				FLIP_LOCAL: 1,
+				DISPERSE_LOCAL: 2,
+				// Swap values within global data buffer.
+				FLIP_GLOBAL: 3,
+				DISPERSE_GLOBAL: 4,
+
+			};
+
+			const timestamps = {
+				local_swap: document.getElementById( 'local_swap' ),
+				global_swap: document.getElementById( 'global_swap' )
+			};
+
+
+			const localColors = [ 'rgb(203, 64, 203)', 'rgb(0, 215, 215)' ];
+			const globalColors = [ 'rgb(1, 150, 1)', 'red' ];
+
+			// Total number of elements and the dimensions of the display grid.
+			const size = 16384;
+			const gridDim = Math.sqrt( size );
+
+			const getNumSteps = () => {
+
+				const n = Math.log2( size );
+				return ( n * ( n + 1 ) ) / 2;
+
+			};
+
+			// Total number of steps in a bitonic sort with 'size' elements.
+			const MAX_STEPS = getNumSteps();
+			const WORKGROUP_SIZE = [ 64 ];
+
+			const effectController = {
+				// Sqr root of 16834
+				gridWidth: uniform( gridDim ),
+				gridHeight: uniform( gridDim ),
+				highlight: uniform( 1 ),
+				'Display Mode': 'Swap Zone Highlight'
+			};
+
+			const gui = new GUI();
+			gui.add( effectController, 'Display Mode', [ 'Elements', 'Swap Zone Highlight' ] ).onChange( () => {
+
+				if ( effectController[ 'Display Mode' ] === 'Elements' ) {
+
+					effectController.highlight.value = 0;
+
+			
+				} else {
+
+					effectController.highlight.value = 1;
+			
+				}
+
+			
+			} );
+
+			// Allow Workgroup Array Swaps
+			init();
+
+			// Global Swaps Only
+			init( true );
+
+
+			// When forceGlobalSwap is true, force all valid local swaps to be global swaps.
+			async function init( forceGlobalSwap = false ) {
+
+				let currentStep = 0;
+				let nextStepGlobal = false;
+
+				const aspect = ( window.innerWidth / 2 ) / window.innerHeight;
+				const camera = new THREE.OrthographicCamera( - aspect, aspect, 1, - 1, 0, 2 );
+				camera.position.z = 1;
+
+				const scene = new THREE.Scene();
+
+				const nextAlgoBuffer = new THREE.StorageInstancedBufferAttribute( new Uint32Array( 1 ).fill( forceGlobalSwap ? StepType.FLIP_GLOBAL : StepType.FLIP_LOCAL ), 1 );
+
+				const nextAlgoStorage = storageObject( nextAlgoBuffer, 'uint', nextAlgoBuffer.count ).label( 'NextAlgo' );
+
+				const nextBlockHeightBuffer = new THREE.StorageInstancedBufferAttribute( new Uint32Array( 1 ).fill( 2 ), 1 );
+				const nextBlockHeightStorage = storageObject( nextBlockHeightBuffer, 'uint', nextBlockHeightBuffer.count ).label( 'NextBlockHeight' );
+				const nextBlockHeightRead = storageObject( nextBlockHeightBuffer, 'uint', nextBlockHeightBuffer.count ).label( 'NextBlockHeight' ).toReadOnly();
+
+				const highestBlockHeightBuffer = new THREE.StorageInstancedBufferAttribute( new Uint32Array( 1 ).fill( 2 ), 1 );
+				const highestBlockHeightStorage = storageObject( highestBlockHeightBuffer, 'uint', highestBlockHeightBuffer.count ).label( 'HighestBlockHeight' );
+
+				const array = new Uint32Array( Array.from( { length: size }, ( _, i ) => {
+
+					return i;
+
+				} ) );
+
+
+				const randomizeDataArray = () => {
+
+					let currentIndex = array.length;
+					while ( currentIndex !== 0 ) {
+
+						const randomIndex = Math.floor( Math.random() * currentIndex );
+						currentIndex -= 1;
+						[ array[ currentIndex ], array[ randomIndex ] ] = [
+							array[ randomIndex ],
+							array[ currentIndex ],
+						];
+
+					}
+
+				};
+
+				randomizeDataArray();
+
+				const currentElementsBuffer = new THREE.StorageInstancedBufferAttribute( array, 1 );
+				const currentElementsStorage = storageObject( currentElementsBuffer, 'uint', size ).label( 'Elements' );
+				const tempBuffer = new THREE.StorageInstancedBufferAttribute( array, 1 );
+				const tempStorage = storageObject( tempBuffer, 'uint', size ).label( 'Temp' );
+				const randomizedElementsBuffer = new THREE.StorageInstancedBufferAttribute( size, 1 );
+				const randomizedElementsStorage = storageObject( randomizedElementsBuffer, 'uint', size ).label( 'RandomizedElements' );
+
+				const getFlipIndices = ( index, blockHeight ) => {
+
+					const blockOffset = ( index.mul( 2 ).div( blockHeight ) ).mul( blockHeight );
+					const halfHeight = blockHeight.div( 2 );
+					const idx = uvec2(
+						index.modInt( halfHeight ),
+						blockHeight.sub( index.modInt( halfHeight ) ).sub( 1 )
+					);
+					idx.x.addAssign( blockOffset );
+					idx.y.addAssign( blockOffset );
+
+					return idx;
+
+				};
+
+				const getDisperseIndices = ( index, blockHeight ) => {
+
+					const blockOffset = ( ( index.mul( 2 ) ).div( blockHeight ) ).mul( blockHeight );
+					const halfHeight = blockHeight.div( 2 );
+					const idx = uvec2(
+						index.modInt( halfHeight ),
+						( index.modInt( halfHeight ) ).add( halfHeight )
+					);
+
+					idx.x.addAssign( blockOffset );
+					idx.y.addAssign( blockOffset );
+
+					return idx;
+
+				};
+
+				const localStorage = workgroupArray( 'uint', 64 * 2 );
+
+				// Swap the elements in local storage
+				const localCompareAndSwap = ( idxBefore, idxAfter ) => {
+
+					If( localStorage.element( idxAfter ).lessThan( localStorage.element( idxBefore ) ), () => {
+
+						const temp = localStorage.element( idxBefore ).toVar();
+						localStorage.element( idxBefore ).assign( localStorage.element( idxAfter ) );
+						localStorage.element( idxAfter ).assign( temp );
+
+					} );
+
+				};
+
+				const globalCompareAndSwap = ( idxBefore, idxAfter ) => {
+
+					// If the later element is less than the current element
+					If( currentElementsStorage.element( idxAfter ).lessThan( currentElementsStorage.element( idxBefore ) ), () => {
+
+						// Apply the swapped values to temporary storage.
+						tempStorage.element( idxBefore ).assign( currentElementsStorage.element( idxAfter ) );
+						tempStorage.element( idxAfter ).assign( currentElementsStorage.element( idxBefore ) );
+
+					} ).Else( () => {
+
+						// Otherwise apply the existing values to temporary storage.
+						tempStorage.element( idxBefore ).assign( currentElementsStorage.element( idxBefore ) );
+						tempStorage.element( idxAfter ).assign( currentElementsStorage.element( idxAfter ) );
+
+					} );
+
+				};
+
+				const computeInitFn = Fn( () => {
+
+					randomizedElementsStorage.element( instanceIndex ).assign( currentElementsStorage.element( instanceIndex ) );
+
+				} );
+
+				const computeBitonicStepFn = Fn( () => {
+
+					const nextBlockHeight = nextBlockHeightStorage.element( 0 ).toVar();
+					const nextAlgo = nextAlgoStorage.element( 0 ).toVar();
+
+					// Get ids of indices needed to populate workgroup local buffer.
+					// Use .toVar() to prevent these values from being recalculated multiple times.
+					const workgroupId = instanceIndex.div( WORKGROUP_SIZE[ 0 ] ).toVar();
+					const localOffset = uint( WORKGROUP_SIZE[ 0 ] ).mul( 2 ).mul( workgroupId ).toVar();
+			
+					const localID1 = invocationLocalIndex.mul( 2 );
+					const localID2 = invocationLocalIndex.mul( 2 ).add( 1 );
+
+					// If we will perform a local swap, then populate the local data
+					If( nextAlgo.lessThanEqual( uint( StepType.DISPERSE_LOCAL ) ), () => {
+
+						localStorage.element( localID1 ).assign( currentElementsStorage.element( localOffset.add( localID1 ) ) );
+						localStorage.element( localID2 ).assign( currentElementsStorage.element( localOffset.add( localID2 ) ) );
+
+					} );
+
+					workgroupBarrier();
+
+					// TODO: Convert to switch block.
+					If( nextAlgo.equal( uint( StepType.FLIP_LOCAL ) ), () => {
+
+						const idx = getFlipIndices( invocationLocalIndex, nextBlockHeight );
+						localCompareAndSwap( idx.x, idx.y );
+
+					} ).ElseIf( nextAlgo.equal( uint( StepType.DISPERSE_LOCAL ) ), () => {
+
+						const idx = getDisperseIndices( invocationLocalIndex, nextBlockHeight );
+						localCompareAndSwap( idx.x, idx.y );
+
+					} ).ElseIf( nextAlgo.equal( uint( StepType.FLIP_GLOBAL ) ), () => {
+
+						const idx = getFlipIndices( instanceIndex, nextBlockHeight );
+						globalCompareAndSwap( idx.x, idx.y );
+
+					} ).ElseIf( nextAlgo.equal( uint( StepType.DISPERSE_GLOBAL ) ), () => {
+
+						const idx = getDisperseIndices( instanceIndex, nextBlockHeight );
+						globalCompareAndSwap( idx.x, idx.y );
+
+					} );
+
+					// Ensure that all invocations have swapped their own regions of data
+					workgroupBarrier();
+
+					// Populate output data with the results from our swaps
+					If( nextAlgo.lessThanEqual( uint( StepType.DISPERSE_LOCAL ) ), () => {
+
+						currentElementsStorage.element( localOffset.add( localID1 ) ).assign( localStorage.element( localID1 ) );
+						currentElementsStorage.element( localOffset.add( localID2 ) ).assign( localStorage.element( localID2 ) );
+
+					} );
+
+					// If the previous algorithm was global, we execute an additional compute step to sync the current buffer with the output buffer.
+
+				} );
+
+				const computeSetAlgoFn = Fn( () => {
+
+					const nextBlockHeight = nextBlockHeightStorage.element( 0 ).toVar();
+					const nextAlgo = nextAlgoStorage.element( 0 );
+					const highestBlockHeight = highestBlockHeightStorage.element( 0 ).toVar();
+
+					nextBlockHeight.divAssign( 2 );
+
+					If( nextBlockHeight.equal( 1 ), () => {
+
+						highestBlockHeight.mulAssign( 2 );
+
+						if ( forceGlobalSwap ) {
+
+							If( highestBlockHeight.equal( size * 2 ), () => {
+			
+								nextAlgo.assign( StepType.NONE );
+								nextBlockHeight.assign( 0 );
+
+							} ).Else( () => {
+
+								nextAlgo.assign( StepType.FLIP_GLOBAL );
+								nextBlockHeight.assign( highestBlockHeight );
+
+							} );
+
+						} else {
+
+							If( highestBlockHeight.equal( size * 2 ), () => {
+			
+								nextAlgo.assign( StepType.NONE );
+								nextBlockHeight.assign( 0 );
+
+							} ).ElseIf( highestBlockHeight.greaterThan( WORKGROUP_SIZE[ 0 ] * 2 ), () => {
+
+								nextAlgo.assign( StepType.FLIP_GLOBAL );
+								nextBlockHeight.assign( highestBlockHeight );
+
+							} ).Else( () => {
+
+								nextAlgo.assign( forceGlobalSwap ? StepType.FLIP_GLOBAL : StepType.FLIP_LOCAL );
+								nextBlockHeight.assign( highestBlockHeight );
+
+							} );
+
+						}
+
+					} ).Else( () => {
+
+						if ( forceGlobalSwap ) {
+
+							nextAlgo.assign( StepType.DISPERSE_GLOBAL );
+
+						} else {
+
+							nextAlgo.assign( nextBlockHeight.greaterThan( WORKGROUP_SIZE[ 0 ] * 2 ).select( StepType.DISPERSE_GLOBAL, StepType.DISPERSE_LOCAL ) );
+
+						}
+
+					} );
+
+					nextBlockHeightStorage.element( 0 ).assign( nextBlockHeight );
+					highestBlockHeightStorage.element( 0 ).assign( highestBlockHeight );
+
+				} );
+
+				const computeAlignCurrentFn = Fn( () => {
+
+					currentElementsStorage.element( instanceIndex ).assign( tempStorage.element( instanceIndex ) );
+
+				} );
+
+				const computeResetBuffersFn = Fn( () => {
+
+					currentElementsStorage.element( instanceIndex ).assign( randomizedElementsStorage.element( instanceIndex ) );
+			
+				} );
+
+				const computeResetAlgoFn = Fn( () => {
+
+					nextAlgoStorage.element( 0 ).assign( forceGlobalSwap ? StepType.FLIP_GLOBAL : StepType.FLIP_LOCAL );
+					nextBlockHeightStorage.element( 0 ).assign( 2 );
+					highestBlockHeightStorage.element( 0 ).assign( 2 );
+
+				} );
+
+				// Initialize each value in the elements buffer.
+				const computeInit = computeInitFn().compute( size );
+				// Swap a pair of elements in the elements buffer.
+				const computeBitonicStep = computeBitonicStepFn().compute( size / 2 );
+				// Set the conditions for the next swap.
+				const computeSetAlgo = computeSetAlgoFn().compute( 1 );
+				// Align the current buffer with the temp buffer if the previous sort was executed in a global scope.
+				const computeAlignCurrent = computeAlignCurrentFn().compute( size );
+				// Reset the buffers and algorithm information after a full bitonic sort has been completed.
+				const computeResetBuffers = computeResetBuffersFn().compute( size );
+				const computeResetAlgo = computeResetAlgoFn().compute( 1 );
+
+				const material = new THREE.MeshBasicNodeMaterial( { color: 0x00ff00 } );
+
+				const display = Fn( () => {
+
+					const { gridWidth, gridHeight, highlight } = effectController;
+
+					const newUV = uv().mul( vec2( gridWidth, gridHeight ) );
+
+					const pixel = uvec2( uint( floor( newUV.x ) ), uint( floor( newUV.y ) ) );
+
+					const elementIndex = uint( gridWidth ).mul( pixel.y ).add( pixel.x );
+
+					const colorChanger = currentElementsStorage.element( elementIndex );
+
+					const subtracter = float( colorChanger ).div( gridWidth.mul( gridHeight ) );
+
+					const color = vec3( subtracter.oneMinus() ).toVar();
+
+					If( highlight.equal( 1 ).and( not( nextAlgoStorage.element( 0 ).equal( StepType.NONE ) ) ), () => {
+
+						const boolCheck = int( elementIndex.modInt( nextBlockHeightRead.element( 0 ) ).lessThan( nextBlockHeightRead.element( 0 ).div( 2 ) ) );
+						color.z.assign( nextAlgoStorage.element( 0 ).lessThanEqual( StepType.DISPERSE_LOCAL ) );
+						color.x.mulAssign( boolCheck );
+						color.y.mulAssign( abs( boolCheck.sub( 1 ) ) );
+
+					} );
+
+					return color;
+
+				} );
+
+				material.colorNode = display();
+
+				const plane = new THREE.Mesh( new THREE.PlaneGeometry( 1, 1 ), material );
+				scene.add( plane );
+
+				const renderer = new THREE.WebGPURenderer( { antialias: false, trackTimestamp: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth / 2, window.innerHeight );
+
+				const animate = () => {
+
+					renderer.render( scene, camera );
+
+				};
+
+				renderer.setAnimationLoop( animate );
+
+				document.body.appendChild( renderer.domElement );
+				renderer.domElement.style.position = 'absolute';
+				renderer.domElement.style.top = '0';
+				renderer.domElement.style.left = '0';
+				renderer.domElement.style.width = '50%';
+				renderer.domElement.style.height = '100%';
+
+				if ( forceGlobalSwap ) {
+
+					renderer.domElement.style.left = '50%';
+
+					scene.background = new THREE.Color( 0x212121 );
+			
+				} else {
+
+					scene.background = new THREE.Color( 0x313131 );
+
+				}
+
+				await renderer.computeAsync( computeInit );
+
+				renderer.info.autoReset = false;
+
+				 const stepAnimation = async function () {
+
+					renderer.info.reset();
+
+					if ( currentStep !== MAX_STEPS ) {
+
+						renderer.compute( computeBitonicStep );
+
+						if ( nextStepGlobal ) {
+
+							renderer.compute( computeAlignCurrent );
+			
+						}
+			
+						renderer.compute( computeSetAlgo );
+
+						currentStep ++;
+
+					} else {
+
+						renderer.compute( computeResetBuffers );
+						renderer.compute( computeResetAlgo );
+
+						currentStep = 0;
+			
+					}
+
+					const algo = new Uint32Array( await renderer.getArrayBufferAsync( nextAlgoBuffer ) );
+					algo > StepType.DISPERSE_LOCAL ? ( nextStepGlobal = true ) : ( nextStepGlobal = false );
+			
+					renderer.render( scene, camera );
+
+					timestamps[ forceGlobalSwap ? 'global_swap' : 'local_swap' ].innerHTML = `
+
+							Compute ${forceGlobalSwap ? 'Global' : 'Local'}: ${renderer.info.compute.frameCalls} pass in ${renderer.info.compute.timestamp.toFixed( 6 )}ms<br>
+								<div style="display: flex; flex-direction:row; justify-content: center; align-items: center;">
+									${forceGlobalSwap ? 'Global Swaps' : 'Local Swaps'} Compare Region&nbsp;
+									<div style="background-color: ${ forceGlobalSwap ? globalColors[ 0 ] : localColors[ 0 ]}; width:12.5px; height: 1em; border-radius: 20%;"></div>
+									&nbsp;to Region&nbsp;
+									<div style="background-color: ${ forceGlobalSwap ? globalColors[ 1 ] : localColors[ 1 ]}; width:12.5px; height: 1em; border-radius: 20%;"></div>
+								</div>`;
+			
+
+					if ( currentStep === MAX_STEPS ) {
+
+						setTimeout( stepAnimation, 1000 );
+
+			
+					} else {
+
+						setTimeout( stepAnimation, 50 );
+			
+					}
+
+				};
+
+				stepAnimation();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				function onWindowResize() {
+
+					renderer.setSize( window.innerWidth / 2, window.innerHeight );
+
+					const aspect = ( window.innerWidth / 2 ) / window.innerHeight;
+
+					const frustumHeight = camera.top - camera.bottom;
+
+					camera.left = - frustumHeight * aspect / 2;
+					camera.right = frustumHeight * aspect / 2;
+
+					camera.updateProjectionMatrix();
+
+					renderer.render( scene, camera );
+
+				}
+
+			}
+		</script>
+	</body>
+</html>

+ 5 - 4
examples/webgpu_storage_buffer.html

@@ -9,8 +9,8 @@
 
 		<div id="info">
 			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a>
-			<br />This example demonstrate the fetch of external element from a StorageBuffer.
-			<br /> Left canvas is using WebGPU Backend, right canvas is WebGL Backend.
+			<br />This example demonstrates fetching an external element from a StorageBuffer.
+			<br />The left canvas uses the WebGPU Backend, while the right uses the WebGL Backend.
 			<div id="timestamps" style="
 				position: absolute;
 				top: 60px;
@@ -52,7 +52,7 @@
 		<script type="module">
 
 			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 );
 
 					}

+ 2 - 0
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';

+ 40 - 0
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();
+

+ 100 - 0
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 ) );
+
+

+ 45 - 0
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<private> instanceIndex : u32;
 
+// locals
+${shaderData.scopedArrays}
+
 // uniforms
 ${shaderData.uniforms}
 

+ 1 - 0
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',

粤ICP备19079148号