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

Addons: Bitonic Sort (#31852)

* change bitonic sort examplke

* bitonic sort

* fix swap local set algo

* fix local sort

* strip out timestamp and unecessary buffers

* fix left side display, remove timestamps

* slow down sort

* add additional comments to bitonicSort class

* lint

* lint
Christian Helgeson 3 месяцев назад
Родитель
Сommit
48ecb7835c
2 измененных файлов с 984 добавлено и 281 удалено
  1. 662 0
      examples/jsm/gpgpu/BitonicSort.js
  2. 322 281
      examples/webgpu_compute_sort_bitonic.html

+ 662 - 0
examples/jsm/gpgpu/BitonicSort.js

@@ -0,0 +1,662 @@
+import { Fn, uvec2, If, instancedArray, instanceIndex, invocationLocalIndex, Loop, workgroupArray, workgroupBarrier, workgroupId, uint, select, min, max } from 'three/tsl';
+
+const StepType = {
+	NONE: 0,
+	// Swap all values within the local range of workgroupSize * 2
+	SWAP_LOCAL: 1,
+	DISPERSE_LOCAL: 2,
+	// Swap values within global data buffer.
+	FLIP_GLOBAL: 3,
+	DISPERSE_GLOBAL: 4,
+};
+
+
+/**
+ * Returns the indices that will be compared in a bitonic flip operation.
+ *
+ * @tsl
+ * @private
+ * @param {Node<uint>} index - The compute thread's invocation id.
+ * @param {Node<uint>} blockHeight - The height of the block within which elements are being swapped.
+ * @returns {Node<uvec2>} The indices of the elements in the data buffer being compared.
+ */
+export const getBitonicFlipIndices = /*@__PURE__*/ Fn( ( [ index, blockHeight ] ) => {
+
+	const blockOffset = ( index.mul( 2 ).div( blockHeight ) ).mul( blockHeight );
+	const halfHeight = blockHeight.div( 2 );
+	const idx = uvec2(
+		index.mod( halfHeight ),
+		blockHeight.sub( index.mod( halfHeight ) ).sub( 1 )
+	);
+	idx.x.addAssign( blockOffset );
+	idx.y.addAssign( blockOffset );
+
+	return idx;
+
+} ).setLayout( {
+	name: 'getBitonicFlipIndices',
+	type: 'uvec2',
+	inputs: [
+		{ name: 'index', type: 'uint' },
+		{ name: 'blockHeight', type: 'uint' }
+	]
+} );
+
+/**
+ * Returns the indices that will be compared in a bitonic sort's disperse operation.
+ *
+ * @tsl
+ * @private
+ * @param {Node<uint>} index - The compute thread's invocation id.
+ * @param {Node<uint>} swapSpan - The maximum span over which elements are being swapped.
+ * @returns {Node<uvec2>} The indices of the elements in the data buffer being compared.
+ */
+export const getBitonicDisperseIndices = /*@__PURE__*/ Fn( ( [ index, swapSpan ] ) => {
+
+	const blockOffset = ( ( index.mul( 2 ) ).div( swapSpan ) ).mul( swapSpan );
+	const halfHeight = swapSpan.div( 2 );
+	const idx = uvec2(
+		index.mod( halfHeight ),
+		( index.mod( halfHeight ) ).add( halfHeight )
+	);
+
+	idx.x.addAssign( blockOffset );
+	idx.y.addAssign( blockOffset );
+
+	return idx;
+
+} ).setLayout( {
+	name: 'getBitonicDisperseIndices',
+	type: 'uvec2',
+	inputs: [
+		{ name: 'index', type: 'uint' },
+		{ name: 'blockHeight', type: 'uint' }
+	]
+} );
+
+// TODO: Add parameters for computing a buffer larger than vec4
+export class BitonicSort {
+
+	/**
+	 * Constructs a new light probe helper.
+	 *
+	 * @param {Renderer} renderer - The current scene's renderer.
+	 * @param {StorageBufferNode} [size=1] - The size of the helper.
+	 * @param {Object} [options={}] - The size of the helper.
+	 */
+	constructor( renderer, dataBuffer, options = {} ) {
+
+		/**
+		 * A reference to the renderer.
+		 *
+		 * @type {Renderer}
+		 */
+		this.renderer = renderer;
+
+		/**
+		 * A reference to the StorageBufferNode holding the data that will be sorted  .
+		 *
+		 * @type {StorageBufferNode}
+		 */
+		this.dataBuffer = dataBuffer;
+
+		/**
+		 * The size of the data.
+		 *
+		 * @type {StorageBufferNode}
+		 */
+		this.count = dataBuffer.value.count;
+
+		/**
+		 *
+		 * The size of each compute dispatch.
+		 * @type {number}
+		 */
+
+		 this.dispatchSize = this.count / 2;
+
+		/**
+		 * The workgroup size of the compute shaders executed during the sort.
+		 *
+		 * @type {StorageBufferNode}
+		*/
+		this.workgroupSize = options.workgroupSize ? Math.min( this.dispatchSize, options.workgroupSize ) : Math.min( this.dispatchSize, 64 );
+
+		/**
+		 * A node representing a workgroup scoped buffer that holds locally sorted elements.
+		 *
+		 * @type {WorkgroupInfoNode}
+		*/
+		this.localStorage = workgroupArray( dataBuffer.nodeType, this.workgroupSize * 2 );
+
+		this._tempArray = new Uint32Array( this.count );
+		for ( let i = 0; i < this.count; i ++ ) {
+
+			this._tempArray[ i ] = 0;
+
+		}
+
+		/**
+		 * A node representing a storage buffer used for transfering the result of the global sort back to the original data buffer.
+		 *
+		 * @type {StorageBufferNode}
+		*/
+		this.tempBuffer = instancedArray( this.count, dataBuffer.nodeType ).setName( 'TempStorage' );
+
+		/**
+		 * A node containing the current algorithm type, the current swap span, and the highest swap span.
+		 *
+		 * @type {StorageBufferNode}
+		*/
+		this.infoStorage = instancedArray( new Uint32Array( [ 1, 2, 2 ] ), 'uint' ).setName( 'BitonicSortInfo' );
+
+
+		/**
+		 * The number of distinct swap operations ('flips' and 'disperses') executed in an in-place
+		 * bitonic sort of the current data buffer.
+		 *
+		 * @type {number}
+		*/
+		this.swapOpCount = this._getSwapOpCount();
+
+		/**
+		 * The number of steps (i.e prepping and/or executing a swap) needed to fully execute an in-place bitonic sort of the current data buffer.
+		 *
+		 * @type {number}
+		*/
+		this.stepCount = this._getStepCount();
+
+		/**
+		 * A compute shader that executes a 'flip' swap within a global address space on elements in the data buffer.
+		 *
+		 * @type {ComputeNode}
+		*/
+		this.flipGlobalFn = this._getFlipGlobal();
+
+		/**
+		 * A compute shader that executes a 'disperse' swap within a global address space on elements in the data buffer.
+		 *
+		 * @type {ComputeNode}
+		*/
+		this.disperseGlobalFn = this._getDisperseGlobal();
+
+		/**
+		 * A compute shader that executes a sequence of flip and disperse swaps within a local address space on elements in the data buffer.
+		 *
+		 * @type {ComputeNode}
+		*/
+		this.swapLocalFn = this._getSwapLocal();
+
+		/**
+		 * A compute shader that executes a sequence of disperse swaps within a local address space on elements in the data buffer.
+		 *
+		 * @type {ComputeNode}
+		*/
+		this.disperseLocalFn = this._getDisperseLocal();
+
+		// Utility functions
+
+		/**
+		 * A compute shader that sets up the algorithm and the swap span for the next swap operation.
+		 *
+		 * @type {ComputeNode}
+		*/
+		this.setAlgoFn = this._getSetAlgoFn();
+
+		/**
+		 * A compute shader that aligns the result of the global swap operation with the current buffer.
+		 *
+		 * @type {ComputeNode}
+		*/
+		this.alignFn = this._getAlignFn();
+
+
+		/**
+		 * A compute shader that resets the algorithm and swap span information.
+		 *
+		 * @type {ComputeNode}
+		*/
+		this.resetFn = this._getResetFn();
+
+
+		/**
+		 * The current compute shader dispatch within the list of dispatches needed to complete the sort.
+		 *
+		 * @type {number}
+		*/
+		this.currentDispatch = 0;
+
+		/**
+		 * The number of global swap operations that must be executed before the sort
+		 * can swap in local address space.
+		 *
+		 * @type {number}
+		*/
+		this.globalOpsRemaining = 0;
+
+		/**
+		 * The total number of global operations needed to sort elements within the current swap span.
+		 *
+		 * @type {number}
+		*/
+		this.globalOpsInSpan = 0;
+
+
+	}
+
+	/**
+	 * Get total number of distinct swaps that occur in a bitonic sort.
+	 *
+	 * @private
+	 */
+	_getSwapOpCount() {
+
+		const n = Math.log2( this.count );
+		return ( n * ( n + 1 ) ) / 2;
+
+	}
+
+	/**
+	 * Get the number of steps it takes to execute a complete bitonic sort.
+	 *
+	 * @private
+	 */
+	_getStepCount() {
+
+		const logElements = Math.log2( this.count );
+		const logSwapSpan = Math.log2( this.workgroupSize * 2 );
+
+		const numGlobalFlips = logElements - logSwapSpan;
+
+		// Start with 1 for initial sort over all local elements
+		let numSteps = 1;
+		let numGlobalDisperses = 0;
+
+		for ( let i = 1; i <= numGlobalFlips; i ++ ) {
+
+			// Increment by the global flip that starts each global block
+			numSteps += 1;
+			// Increment by number of global disperses following the global flip
+			numSteps += numGlobalDisperses;
+			// Increment by local disperse that occurs after all global swaps are finished
+			numSteps += 1;
+
+			// Number of global disperse increases as swapSpan increases by factor of 2
+			numGlobalDisperses += 1;
+
+		}
+
+		return numSteps;
+
+	}
+
+	/**
+	 * Compares and swaps two data points in the data buffer within the global address space.
+	 *
+	 * @private
+	 */
+	_globalCompareAndSwapTSL( idxBefore, idxAfter, dataBuffer, tempBuffer ) {
+
+		const data1 = dataBuffer.element( idxBefore );
+		const data2 = dataBuffer.element( idxAfter );
+
+		tempBuffer.element( idxBefore ).assign( min( data1, data2 ) );
+		tempBuffer.element( idxAfter ).assign( max( data1, data2 ) );
+
+	}
+
+	/**
+	 * Compares and swaps two data points in the data buffer within the local address space.
+	 *
+	 * @private
+	 */
+	_localCompareAndSwapTSL( idxBefore, idxAfter ) {
+
+		const { localStorage } = this;
+
+		const data1 = localStorage.element( idxBefore ).toVar();
+		const data2 = localStorage.element( idxAfter ).toVar();
+
+		localStorage.element( idxBefore ).assign( min( data1, data2 ) );
+		localStorage.element( idxAfter ).assign( max( data1, data2 ) );
+
+	}
+
+
+	/**
+	 * Create the compute shader that performs a global disperse swap on the data buffer.
+	 *
+	 * @private
+	 */
+	_getDisperseGlobal() {
+
+		const { infoStorage, tempBuffer, dataBuffer } = this;
+
+		const currentSwapSpan = infoStorage.element( 1 );
+
+		const fnDef = Fn( () => {
+
+			const idx = getBitonicDisperseIndices( instanceIndex, currentSwapSpan );
+			this._globalCompareAndSwapTSL( idx.x, idx.y, dataBuffer, tempBuffer );
+
+		} )().compute( this.dispatchSize, [ this.workgroupSize ] );
+
+		return fnDef;
+
+	}
+
+	/**
+	 * Create the compute shader that performs a global flip swap on the data buffer.
+	 *
+	 * @private
+	 */
+	_getFlipGlobal() {
+
+		const { infoStorage, tempBuffer, dataBuffer } = this;
+
+		const currentSwapSpan = infoStorage.element( 1 );
+
+		const fnDef = Fn( () => {
+
+			const idx = getBitonicFlipIndices( instanceIndex, currentSwapSpan );
+			this._globalCompareAndSwapTSL( idx.x, idx.y, dataBuffer, tempBuffer );
+
+		} )().compute( this.dispatchSize, [ this.workgroupSize ] );
+
+		return fnDef;
+
+	}
+
+
+	/**
+	 * Create the compute shader that performs a complete local swap on the data buffer.
+	 *
+	 * @private
+	 */
+	_getSwapLocal() {
+
+		const { localStorage, dataBuffer, workgroupSize } = this;
+
+		const fnDef = Fn( () => {
+
+			// Get ids of indices needed to populate workgroup local buffer.
+			// Use .toVar() to prevent these values from being recalculated multiple times.
+			const localOffset = uint( workgroupSize ).mul( 2 ).mul( workgroupId.x ).toVar();
+
+			const localID1 = invocationLocalIndex.mul( 2 );
+			const localID2 = invocationLocalIndex.mul( 2 ).add( 1 );
+
+			localStorage.element( localID1 ).assign( dataBuffer.element( localOffset.add( localID1 ) ) );
+			localStorage.element( localID2 ).assign( dataBuffer.element( localOffset.add( localID2 ) ) );
+
+			// Ensure that all local data has been populated
+			workgroupBarrier();
+
+			// Perform a chunk of the sort in a single pass that operates entirely in workgroup local space
+			// SWAP_LOCAL will always be first pass, so we start with known block height of 2
+			const flipBlockHeight = uint( 2 );
+
+			Loop( { start: uint( 2 ), end: uint( workgroupSize * 2 ), type: 'uint', condition: '<=', update: '<<= 1' }, () => {
+
+				// Ensure that last dispatch block executed
+				workgroupBarrier();
+
+				const flipIdx = getBitonicFlipIndices( invocationLocalIndex, flipBlockHeight );
+
+				this._localCompareAndSwapTSL( flipIdx.x, flipIdx.y );
+
+				const localBlockHeight = flipBlockHeight.div( 2 );
+
+				Loop( { start: localBlockHeight, end: uint( 1 ), type: 'uint', condition: '>', update: '>>= 1' }, () => {
+
+					// Ensure that last dispatch op executed
+					workgroupBarrier();
+
+					const disperseIdx = getBitonicDisperseIndices( invocationLocalIndex, localBlockHeight );
+					this._localCompareAndSwapTSL( disperseIdx.x, disperseIdx.y );
+
+					localBlockHeight.divAssign( 2 );
+
+				} );
+
+				// flipBlockHeight *= 2;
+				flipBlockHeight.shiftLeftAssign( 1 );
+
+			} );
+
+			// Ensure that all invocations have swapped their own regions of data
+			workgroupBarrier();
+
+			dataBuffer.element( localOffset.add( localID1 ) ).assign( localStorage.element( localID1 ) );
+			dataBuffer.element( localOffset.add( localID2 ) ).assign( localStorage.element( localID2 ) );
+
+		} )().compute( this.dispatchSize, [ this.workgroupSize ] );
+
+		return fnDef;
+
+	}
+
+	/**
+	 * Create the compute shader that performs a local disperse swap on the data buffer.
+	 *
+	 * @private
+	 */
+	_getDisperseLocal() {
+
+		const { localStorage, dataBuffer, workgroupSize } = this;
+
+		const fnDef = Fn( () => {
+
+			// Get ids of indices needed to populate workgroup local buffer.
+			// Use .toVar() to prevent these values from being recalculated multiple times.
+			const localOffset = uint( workgroupSize ).mul( 2 ).mul( workgroupId.x ).toVar();
+
+			const localID1 = invocationLocalIndex.mul( 2 );
+			const localID2 = invocationLocalIndex.mul( 2 ).add( 1 );
+
+			localStorage.element( localID1 ).assign( dataBuffer.element( localOffset.add( localID1 ) ) );
+			localStorage.element( localID2 ).assign( dataBuffer.element( localOffset.add( localID2 ) ) );
+
+			// Ensure that all local data has been populated
+			workgroupBarrier();
+
+			const localBlockHeight = uint( workgroupSize * 2 );
+
+			Loop( { start: localBlockHeight, end: uint( 1 ), type: 'uint', condition: '>', update: '>>= 1' }, () => {
+
+				// Ensure that last dispatch op executed
+				workgroupBarrier();
+
+				const disperseIdx = getBitonicDisperseIndices( invocationLocalIndex, localBlockHeight );
+				this._localCompareAndSwapTSL( disperseIdx.x, disperseIdx.y );
+
+				localBlockHeight.divAssign( 2 );
+
+			} );
+
+			// Ensure that all invocations have swapped their own regions of data
+			workgroupBarrier();
+
+			dataBuffer.element( localOffset.add( localID1 ) ).assign( localStorage.element( localID1 ) );
+			dataBuffer.element( localOffset.add( localID2 ) ).assign( localStorage.element( localID2 ) );
+
+		} )().compute( this.dispatchSize, [ this.workgroupSize ] );
+
+		return fnDef;
+
+	}
+
+	/**
+	 * Create the compute shader that resets the sort's algorithm information.
+	 *
+	 * @private
+	 */
+	_getResetFn() {
+
+		const fnDef = Fn( () => {
+
+			const { infoStorage } = this;
+
+			const currentAlgo = infoStorage.element( 0 );
+			const currentSwapSpan = infoStorage.element( 1 );
+			const maxSwapSpan = infoStorage.element( 2 );
+
+			currentAlgo.assign( StepType.SWAP_LOCAL );
+			currentSwapSpan.assign( 2 );
+			maxSwapSpan.assign( 2 );
+
+		} )().compute( 1 );
+
+		return fnDef;
+
+	}
+
+	/**
+	 * Create the compute shader that copies the state of the global swap to the data buffer.
+	 *
+	 * @private
+	 */
+	_getAlignFn() {
+
+		const { dataBuffer, tempBuffer } = this;
+
+		// TODO: Only do this in certain instances by ping-ponging which buffer gets sorted
+		// And only aligning if numDispatches % 2 === 1
+		const fnDef = Fn( () => {
+
+			dataBuffer.element( instanceIndex ).assign( tempBuffer.element( instanceIndex ) );
+
+		} )().compute( this.count, [ this.workgroupSize ] );
+
+		return fnDef;
+
+	}
+
+	/**
+	 * Create the compute shader that sets the algorithm's information.
+	 *
+	 * @private
+	 */
+	_getSetAlgoFn() {
+
+		const fnDef = Fn( () => {
+
+			const { infoStorage, workgroupSize } = this;
+
+			const currentAlgo = infoStorage.element( 0 );
+			const currentSwapSpan = infoStorage.element( 1 );
+			const maxSwapSpan = infoStorage.element( 2 );
+
+			If( currentAlgo.equal( StepType.SWAP_LOCAL ), () => {
+
+				const nextHighestSwapSpan = uint( workgroupSize * 4 );
+
+				currentAlgo.assign( StepType.FLIP_GLOBAL );
+				currentSwapSpan.assign( nextHighestSwapSpan );
+				maxSwapSpan.assign( nextHighestSwapSpan );
+
+			} ).ElseIf( currentAlgo.equal( StepType.DISPERSE_LOCAL ), () => {
+
+				currentAlgo.assign( StepType.FLIP_GLOBAL );
+
+				const nextHighestSwapSpan = maxSwapSpan.mul( 2 );
+
+				currentSwapSpan.assign( nextHighestSwapSpan );
+				maxSwapSpan.assign( nextHighestSwapSpan );
+
+			} ).Else( () => {
+
+				const nextSwapSpan = currentSwapSpan.div( 2 );
+				currentAlgo.assign(
+					select(
+						nextSwapSpan.lessThanEqual( uint( workgroupSize * 2 ) ),
+						StepType.DISPERSE_LOCAL,
+						StepType.DISPERSE_GLOBAL
+					).uniformFlow()
+				);
+				currentSwapSpan.assign( nextSwapSpan );
+
+			} );
+
+		} )().compute( 1 );
+
+		return fnDef;
+
+	}
+
+	/**
+	 * Executes a step of the bitonic sort operation.
+	 *
+	 * @param {Renderer} renderer - The current scene's renderer.
+	 */
+	async computeStep( renderer ) {
+
+		// Swap local only runs once
+		if ( this.currentDispatch === 0 ) {
+
+			await renderer.computeAsync( this.swapLocalFn );
+
+			this.globalOpsRemaining = 1;
+			this.globalOpsInSpan = 1;
+
+		} else if ( this.globalOpsRemaining > 0 ) {
+
+			const swapType = this.globalOpsRemaining === this.globalOpsInSpan ? 'Flip' : 'Disperse';
+
+			await renderer.computeAsync( swapType === 'Flip' ? this.flipGlobalFn : this.disperseGlobalFn );
+			await renderer.computeAsync( this.alignFn );
+
+			this.globalOpsRemaining -= 1;
+
+		} else {
+
+			// Then run local disperses when we've finished all global swaps
+			await renderer.computeAsync( this.disperseLocalFn );
+
+			const nextSpanGlobalOps = this.globalOpsInSpan + 1;
+			this.globalOpsInSpan = nextSpanGlobalOps;
+			this.globalOpsRemaining = nextSpanGlobalOps;
+
+		}
+
+
+		this.currentDispatch += 1;
+
+		if ( this.currentDispatch === this.stepCount ) {
+
+			// Just reset the algorithm information
+			await renderer.computeAsync( this.resetFn );
+
+			this.currentDispatch = 0;
+			this.globalOpsRemaining = 0;
+			this.globalOpsInSpan = 0;
+
+		} else {
+
+			// Otherwise, determine what next swap span is
+			await renderer.computeAsync( this.setAlgoFn );
+
+		}
+
+	}
+
+	/**
+	 * Executes a complete bitonic sort on the data buffer.
+	 *
+	 * @param {Renderer} renderer - The current scene's renderer.
+	 */
+	async compute( renderer ) {
+
+		this.globalOpsRemaining = 0;
+		this.globalOpsInSpan = 0;
+		this.currentDispatch = 0;
+
+		for ( let i = 0; i < this.stepCount; i ++ ) {
+
+			await this.computeStep( renderer );
+
+		}
+
+	}
+
+}

+ 322 - 281
examples/webgpu_compute_sort_bitonic.html

@@ -7,16 +7,10 @@
 	</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="
+		<style>
+			.swap_area {
 				position: absolute;
 				top: 150px;
-				left: 0;
 				padding: 10px;
 				background: rgba( 0, 0, 0, 0.5 );
 				color: #fff;
@@ -25,20 +19,18 @@
 				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>
+			}
+		
+		</style>
+
+		<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" class="swap_area" style="left: 0;"></div>
+			<div id="global_swap" class="swap_area" style="right: 0;"></div>
 		</div>
 
 		<script type="importmap">
@@ -55,7 +47,9 @@
 		<script type="module">
 
 			import * as THREE from 'three/webgpu';
-			import { storage, If, vec3, not, uniform, uv, uint, float, Fn, vec2, abs, int, invocationLocalIndex, workgroupArray, uvec2, floor, instanceIndex, workgroupBarrier, atomicAdd, atomicStore, workgroupId } from 'three/tsl';
+			import { storage, If, vec3, not, uniform, uv, uint, Fn, vec2, abs, int, uvec2, floor, instanceIndex } from 'three/tsl';
+
+			import { BitonicSort, getBitonicDisperseIndices, getBitonicFlipIndices } from 'three/addons/gpgpu/BitonicSort.js';
 
 			import WebGPU from 'three/addons/capabilities/WebGPU.js';
 
@@ -64,8 +58,8 @@
 			const StepType = {
 
 				NONE: 0,
-				// Swap values within workgroup local buffer.
-				FLIP_LOCAL: 1,
+				// Swap values within workgroup local values
+				SWAP_LOCAL: 1,
 				DISPERSE_LOCAL: 2,
 				// Swap values within global data buffer.
 				FLIP_GLOBAL: 3,
@@ -78,7 +72,6 @@
 				global_swap: document.getElementById( 'global_swap' )
 			};
 
-
 			const localColors = [ 'rgb(203, 64, 203)', 'rgb(0, 215, 215)' ];
 			const globalColors = [ 'rgb(1, 150, 1)', 'red' ];
 
@@ -95,13 +88,13 @@
 
 			// 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 ),
+				stepBitonic: true,
 				'Display Mode': 'Swap Zone Highlight'
 			};
 
@@ -119,7 +112,6 @@
 
 				}
 
-
 			} );
 
 			if ( WebGPU.isAvailable() === false ) {
@@ -130,125 +122,333 @@
 
 			}
 
-			// Allow Workgroup Array Swaps
-			init();
+			// Display utilities
 
-			// Global Swaps Only
-			init( true );
+			const getElementIndex = Fn( ( [ uvNode, gridWidth, gridHeight ] ) => {
 
+				const newUV = uvNode.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 );
 
-			// When forceGlobalSwap is true, force all valid local swaps to be global swaps.
-			async function init( forceGlobalSwap = false ) {
+				return elementIndex;
 
-				let currentStep = 0;
-				let nextStepGlobal = false;
+			}, {
+				uvNode: 'vec2',
+				gridWidth: 'uint',
+				gridHeight: 'uint',
+				return: 'uint'
+			} );
+
+			const getColor = Fn( ( [ colorChanger, gridWidth, gridHeight ] ) => {
+
+				const subtracter = colorChanger.div( gridWidth.mul( gridHeight ) );
+				return vec3( subtracter.oneMinus() ).toVar();
+
+			}, {
+				colorChanger: 'float',
+				gridWidth: 'float',
+				gridHeight: 'float',
+				return: 'vec3'
+			} );
+
+			const randomizeDataArray = ( array ) => {
+
+				let currentIndex = array.length;
+				while ( currentIndex !== 0 ) {
 
+					const randomIndex = Math.floor( Math.random() * currentIndex );
+					currentIndex -= 1;
+					[ array[ currentIndex ], array[ randomIndex ] ] = [
+						array[ randomIndex ],
+						array[ currentIndex ],
+					];
+
+				}
+
+			};
+
+			const windowResizeCallback = ( renderer, scene, camera ) => {
+
+				renderer.setSize( window.innerWidth / 2, window.innerHeight );
 				const aspect = ( window.innerWidth / 2 ) / window.innerHeight;
-				const camera = new THREE.OrthographicCamera( - aspect, aspect, 1, - 1, 0, 2 );
-				camera.position.z = 1;
+				const frustumHeight = camera.top - camera.bottom;
+				camera.left = - frustumHeight * aspect / 2;
+				camera.right = frustumHeight * aspect / 2;
+				camera.updateProjectionMatrix();
+				renderer.render( scene, camera );
 
-				const scene = new THREE.Scene();
+			};
 
-				const nextAlgoBuffer = new THREE.StorageInstancedBufferAttribute( new Uint32Array( 1 ).fill( forceGlobalSwap ? StepType.FLIP_GLOBAL : StepType.FLIP_LOCAL ), 1 );
+			const constructInnerHTML = ( isGlobal, colorsArr ) => {
 
-				const nextAlgoStorage = storage( nextAlgoBuffer, 'uint', nextAlgoBuffer.count ).setPBO( true ).setName( 'NextAlgo' );
+				return `
 
-				const nextBlockHeightBuffer = new THREE.StorageInstancedBufferAttribute( new Uint32Array( 1 ).fill( 2 ), 1 );
-				const nextBlockHeightStorage = storage( nextBlockHeightBuffer, 'uint', nextBlockHeightBuffer.count ).setPBO( true ).setName( 'NextBlockHeight' );
-				const nextBlockHeightRead = storage( nextBlockHeightBuffer, 'uint', nextBlockHeightBuffer.count ).setPBO( true ).setName( 'NextBlockHeight' ).toReadOnly();
+				Compute ${isGlobal ? 'Global' : 'Local'}:
+				<div style="display: flex; flex-direction:row; justify-content: center; align-items: center;">
+					${isGlobal ? 'Global Swaps' : 'Local Swaps'} Compare Region&nbsp;
+					<div style="background-color: ${ colorsArr[ 0 ]}; width:12.5px; height: 1em; border-radius: 20%;"></div>
+					&nbsp;to Region&nbsp;
+					<div style="background-color: ${ colorsArr[ 1 ] }; width:12.5px; height: 1em; border-radius: 20%;"></div>
+				</div>`;
 
-				const highestBlockHeightBuffer = new THREE.StorageInstancedBufferAttribute( new Uint32Array( 1 ).fill( 2 ), 1 );
-				const highestBlockHeightStorage = storage( highestBlockHeightBuffer, 'uint', highestBlockHeightBuffer.count ).setPBO( true ).setName( 'HighestBlockHeight' );
+			};
 
-				const counterBuffer = new THREE.StorageBufferAttribute( 1, 1 );
-				const counterStorage = storage( counterBuffer, 'uint', counterBuffer.count ).setPBO( true ).toAtomic().setName( 'Counter' );
+			const createDisplayMesh = ( elementsStorage, algoStorage = null, blockHeightStorage = null ) => {
 
-				const array = new Uint32Array( Array.from( { length: size }, ( _, i ) => {
+				const material = new THREE.MeshBasicNodeMaterial( { color: 0x00ff00 } );
 
-					return i;
+				const display = Fn( () => {
 
-				} ) );
+					const { gridWidth, gridHeight, highlight } = effectController;
 
+					const elementIndex = getElementIndex( uv(), gridWidth, gridHeight );
+					const color = getColor( elementsStorage.element( elementIndex ), gridWidth, gridHeight ).toVar();
 
-				const randomizeDataArray = () => {
+					if ( algoStorage !== null && blockHeightStorage !== null ) {
 
-					let currentIndex = array.length;
-					while ( currentIndex !== 0 ) {
+						If( highlight.equal( 1 ).and( not( algoStorage.element( 0 ).equal( StepType.NONE ) ) ), () => {
 
-						const randomIndex = Math.floor( Math.random() * currentIndex );
-						currentIndex -= 1;
-						[ array[ currentIndex ], array[ randomIndex ] ] = [
-							array[ randomIndex ],
-							array[ currentIndex ],
-						];
+							const boolCheck = int( elementIndex.mod( blockHeightStorage.element( 0 ) ).lessThan( blockHeightStorage.element( 0 ).div( 2 ) ) );
+							color.z.assign( algoStorage.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 );
+				return plane;
+
+			};
+
+			const createDisplayMesh2 = ( elementsStorage, infoStorage ) => {
+
+				const material = new THREE.MeshBasicNodeMaterial( { color: 0x00ff00 } );
+
+				const display = Fn( () => {
+
+					const { gridWidth, gridHeight, highlight } = effectController;
+
+					const elementIndex = getElementIndex( uv(), gridWidth, gridHeight );
+					const color = getColor( elementsStorage.element( elementIndex ), gridWidth, gridHeight ).toVar();
+
 
-				randomizeDataArray();
+					If( highlight.equal( 1 ).and( not( infoStorage.element( 0 ).equal( StepType.SWAP_LOCAL ) ) ), () => {
+
+						const boolCheck = int( elementIndex.mod( infoStorage.element( 1 ) ).lessThan( infoStorage.element( 1 ).div( 2 ) ) );
+						color.z.assign( infoStorage.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 );
+				return plane;
+
+			};
+			
+			const setupDomElement = ( renderer ) => {
+
+				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%';
+
+			};
+
+			async function initBitonicSort() {
+
+				let currentStep = 0;
+
+				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 array = new Uint32Array( Array.from( { length: size }, ( _, i ) => {
+
+					return i;
+
+				} ) );
+
+
+				randomizeDataArray( array );
 
 				const currentElementsBuffer = new THREE.StorageInstancedBufferAttribute( array, 1 );
 				const currentElementsStorage = storage( currentElementsBuffer, 'uint', size ).setPBO( true ).setName( 'Elements' );
-				const tempBuffer = new THREE.StorageInstancedBufferAttribute( array, 1 );
-				const tempStorage = storage( tempBuffer, 'uint', size ).setPBO( true ).setName( 'Temp' );
 				const randomizedElementsBuffer = new THREE.StorageInstancedBufferAttribute( size, 1 );
 				const randomizedElementsStorage = storage( randomizedElementsBuffer, 'uint', size ).setPBO( true ).setName( 'RandomizedElements' );
 
-				const getFlipIndices = ( index, blockHeight ) => {
+				const computeInitFn = Fn( () => {
 
-					const blockOffset = ( index.mul( 2 ).div( blockHeight ) ).mul( blockHeight );
-					const halfHeight = blockHeight.div( 2 );
-					const idx = uvec2(
-						index.mod( halfHeight ),
-						blockHeight.sub( index.mod( halfHeight ) ).sub( 1 )
-					);
-					idx.x.addAssign( blockOffset );
-					idx.y.addAssign( blockOffset );
+					randomizedElementsStorage.element( instanceIndex ).assign( currentElementsStorage.element( instanceIndex ) );
 
-					return idx;
+				} );
 
-				};
+				const computeResetBuffersFn = Fn( () => {
 
-				const getDisperseIndices = ( index, blockHeight ) => {
+					currentElementsStorage.element( instanceIndex ).assign( randomizedElementsStorage.element( instanceIndex ) );
 
-					const blockOffset = ( ( index.mul( 2 ) ).div( blockHeight ) ).mul( blockHeight );
-					const halfHeight = blockHeight.div( 2 );
-					const idx = uvec2(
-						index.mod( halfHeight ),
-						( index.mod( halfHeight ) ).add( halfHeight )
-					);
+				} );
+
+				const renderer = new THREE.WebGPURenderer( { antialias: false } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth / 2, window.innerHeight );
 
-					idx.x.addAssign( blockOffset );
-					idx.y.addAssign( blockOffset );
+				const animate = () => {
 
-					return idx;
+					renderer.render( scene, camera );
 
 				};
 
-				const localStorage = workgroupArray( 'uint', 64 * 2 );
+				renderer.setAnimationLoop( animate );
+				setupDomElement( renderer );
+				scene.background = new THREE.Color( 0x313131 );
+
+				const bitonicSortModule = new BitonicSort(
+					renderer,
+					currentElementsStorage,
+					{
+						workgroupSize: 64,
+						sideEffectBuffers: [ 2, 3 ]
+					}
+				);
 
-				// Swap the elements in local storage
-				const localCompareAndSwap = ( idxBefore, idxAfter ) => {
+				scene.add( createDisplayMesh2( currentElementsStorage, bitonicSortModule.infoStorage ) );
 
-					If( localStorage.element( idxAfter ).lessThan( localStorage.element( idxBefore ) ), () => {
+				// Initialize each value in the elements buffer.
+				const computeInit = computeInitFn().compute( size );
+				const computeReset = computeResetBuffersFn().compute( size );
 
-						atomicAdd( counterStorage.element( 0 ), 1 );
-						const temp = localStorage.element( idxBefore ).toVar();
-						localStorage.element( idxBefore ).assign( localStorage.element( idxAfter ) );
-						localStorage.element( idxAfter ).assign( temp );
+				await renderer.computeAsync( computeInit );
 
-					} );
+				gui.add( effectController, 'stepBitonic' ).onChange( async () => {
+
+					if ( currentStep < bitonicSortModule.stepCount ) {
+
+						await bitonicSortModule.computeStep( renderer );
+
+						currentStep ++;
+
+					} else {
+
+						await renderer.computeAsync( computeReset );
+
+						currentStep = 0;
+
+					}
+
+					timestamps[ 'local_swap' ].innerHTML = constructInnerHTML( false, localColors );
+
+				} );
+
+
+
+				 const stepAnimation = async function () {
+
+					renderer.info.reset();
+
+					if ( currentStep < bitonicSortModule.stepCount ) {
+
+						bitonicSortModule.computeStep( renderer );
+
+						currentStep ++;
+
+					} else {
+
+						renderer.compute( computeReset );
+
+						currentStep = 0;
+
+					}
+
+					timestamps[ 'local_swap' ].innerHTML = constructInnerHTML( false, localColors );
+
+					if ( currentStep === bitonicSortModule.stepCount ) {
+
+						setTimeout( stepAnimation, 1000 );
+
+					} else {
+
+						setTimeout( stepAnimation, 100 );
+
+					}
 
 				};
 
+				stepAnimation();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				function onWindowResize() {
+
+					windowResizeCallback( renderer, scene, camera );
+
+				}
+
+			}
+
+			initBitonicSort();
+
+			// Global Swaps Only
+			initGlobalSwapOnly();
+
+			// When forceGlobalSwap is true, force all valid local swaps to be global swaps.
+			async function initGlobalSwapOnly() {
+
+				let currentStep = 0;
+
+				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 infoArray = new Uint32Array( 3, 2, 2 );
+				const infoBuffer = new THREE.StorageInstancedBufferAttribute( infoArray, 1 );
+				const infoStorage = storage( infoBuffer, 'uint', infoBuffer.count ).setPBO( true ).setName( 'TheInfo' );
+
+				const nextBlockHeightBuffer = new THREE.StorageInstancedBufferAttribute( new Uint32Array( 1 ).fill( 2 ), 1 );
+				const nextBlockHeightStorage = storage( nextBlockHeightBuffer, 'uint', nextBlockHeightBuffer.count ).setPBO( true ).setName( 'NextBlockHeight' );
+				const nextBlockHeightRead = storage( nextBlockHeightBuffer, 'uint', nextBlockHeightBuffer.count ).setPBO( true ).setName( 'NextBlockHeight' ).toReadOnly();
+
+				const array = new Uint32Array( Array.from( { length: size }, ( _, i ) => {
+
+					return i;
+
+				} ) );
+
+				randomizeDataArray( array );
+
+				const currentElementsBuffer = new THREE.StorageInstancedBufferAttribute( array, 1 );
+				const currentElementsStorage = storage( currentElementsBuffer, 'uint', size ).setPBO( true ).setName( 'Elements' );
+				const tempBuffer = new THREE.StorageInstancedBufferAttribute( array, 1 );
+				const tempStorage = storage( tempBuffer, 'uint', size ).setPBO( true ).setName( 'Temp' );
+				const randomizedElementsBuffer = new THREE.StorageInstancedBufferAttribute( size, 1 );
+				const randomizedElementsStorage = storage( randomizedElementsBuffer, 'uint', size ).setPBO( true ).setName( 'RandomizedElements' );
+
+				// Swap the elements in local storage
 				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.
-						atomicAdd( counterStorage.element( 0 ), 1 );
 						tempStorage.element( idxBefore ).assign( currentElementsStorage.element( idxAfter ) );
 						tempStorage.element( idxAfter ).assign( currentElementsStorage.element( idxBefore ) );
 
@@ -271,68 +471,30 @@
 				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 localOffset = uint( WORKGROUP_SIZE[ 0 ] ).mul( 2 ).mul( workgroupId.x ).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();
+					const nextAlgo = infoStorage.element( 0 ).toVar();
 
 					// 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 ) ), () => {
+					If( nextAlgo.equal( uint( StepType.FLIP_GLOBAL ) ), () => {
 
-						const idx = getDisperseIndices( invocationLocalIndex, nextBlockHeight );
-						localCompareAndSwap( idx.x, idx.y );
-
-					} ).ElseIf( nextAlgo.equal( uint( StepType.FLIP_GLOBAL ) ), () => {
-
-						const idx = getFlipIndices( instanceIndex, nextBlockHeight );
+						const idx = getBitonicFlipIndices( instanceIndex, nextBlockHeight );
 						globalCompareAndSwap( idx.x, idx.y );
 
 					} ).ElseIf( nextAlgo.equal( uint( StepType.DISPERSE_GLOBAL ) ), () => {
 
-						const idx = getDisperseIndices( instanceIndex, nextBlockHeight );
+						const idx = getBitonicDisperseIndices( 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.
+					// Since this algorithm is global only, 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();
+					const nextAlgo = infoStorage.element( 0 );
+					const highestBlockHeight = infoStorage.element( 2 ).toVar();
 
 					nextBlockHeight.divAssign( 2 );
 
@@ -340,57 +502,27 @@
 
 						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 ), () => {
+						If( highestBlockHeight.equal( size * 2 ), () => {
 
-								nextAlgo.assign( StepType.NONE );
-								nextBlockHeight.assign( 0 );
+							nextAlgo.assign( StepType.NONE );
+							nextBlockHeight.assign( 0 );
 
-							} ).ElseIf( highestBlockHeight.greaterThan( WORKGROUP_SIZE[ 0 ] * 2 ), () => {
+						} ).Else( () => {
 
-								nextAlgo.assign( StepType.FLIP_GLOBAL );
-								nextBlockHeight.assign( highestBlockHeight );
+							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 ).uniformFlow() );
-
-						}
+						nextAlgo.assign( StepType.DISPERSE_GLOBAL );
 
 					} );
 
 					nextBlockHeightStorage.element( 0 ).assign( nextBlockHeight );
-					highestBlockHeightStorage.element( 0 ).assign( highestBlockHeight );
+					infoStorage.element( 2 ).assign( highestBlockHeight );
 
 				} );
 
@@ -408,10 +540,9 @@
 
 				const computeResetAlgoFn = Fn( () => {
 
-					nextAlgoStorage.element( 0 ).assign( forceGlobalSwap ? StepType.FLIP_GLOBAL : StepType.FLIP_LOCAL );
+					infoStorage.element( 0 ).assign( StepType.FLIP_GLOBAL );
 					nextBlockHeightStorage.element( 0 ).assign( 2 );
-					highestBlockHeightStorage.element( 0 ).assign( 2 );
-					atomicStore( counterStorage.element( 0 ), 0 );
+					infoStorage.element( 2 ).assign( 2 );
 
 				} );
 
@@ -427,43 +558,9 @@
 				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 );
+				scene.add( createDisplayMesh( currentElementsStorage, infoStorage, nextBlockHeightRead ) );
 
-					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.mod( 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 } );
+				const renderer = new THREE.WebGPURenderer( { antialias: false } );
 				renderer.setPixelRatio( window.devicePixelRatio );
 				renderer.setSize( window.innerWidth / 2, window.innerHeight );
 
@@ -474,43 +571,19 @@
 				};
 
 				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 );
-
-				}
+				setupDomElement( renderer );
+				renderer.domElement.style.left = '50%';
+				scene.background = new THREE.Color( 0x212121 );
 
 				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( computeAlignCurrent );
 
 						renderer.compute( computeSetAlgo );
 
@@ -525,36 +598,15 @@
 
 					}
 
-					renderer.resolveTimestampsAsync( THREE.TimestampQuery.COMPUTE );
-
-
-					const algo = new Uint32Array( await renderer.getArrayBufferAsync( nextAlgoBuffer ) );
-					nextStepGlobal = algo[ 0 ] > StepType.DISPERSE_LOCAL;
-					const totalSwaps = new Uint32Array( await renderer.getArrayBufferAsync( counterBuffer ) );
-
-					renderer.render( scene, camera );
-					renderer.resolveTimestampsAsync( THREE.TimestampQuery.RENDER );
-
-					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>
-							Total Swaps: ${totalSwaps}<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>`;
-
+					timestamps[ 'global_swap' ].innerHTML = constructInnerHTML( true, globalColors );
 
 					if ( currentStep === MAX_STEPS ) {
 
 						setTimeout( stepAnimation, 1000 );
 
-
 					} else {
 
-						setTimeout( stepAnimation, 50 );
+						setTimeout( stepAnimation, 100 );
 
 					}
 
@@ -566,18 +618,7 @@
 
 				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 );
+					windowResizeCallback( renderer, scene, camera );
 
 				}
 

粤ICP备19079148号