Skip to content

Commit

Permalink
WebGPURenderer: StorageBufferNode Support reading external elements…
Browse files Browse the repository at this point in the history
… in the WebGL Backend (#27661)

* init pbo

* remove flag

* regenerate live example

* single buffer and alternate for example

* cleanup, no idea about circ dep

* test fix circular

* cleanup

* moved pbo management to GLSLNodeBuilder

* fix screenshots

* support more ranges

* format dynamically in arrayelementnode

* init vertex buffer allocation to prevent issues with some backends

* support different type size and update example

* fix files.json

* puppeteer need webgpu support

* unbind post pbo

* add TODO

* cleanup

* Move to StorageArrayElementNode

* cleanup

* rev

* add increaseUsage

* Update StorageArrayElementNode.js

* fixes optmization and revisions

* revision

* improved example for E2E tests

* WIP: Test (1)

* handle compute with non-PBO -> PBO case

* revert WIP

* TSL: add `storageObject`

* revision

* make sure attributeData gets pbo attached in GLSLNodeBuilder

* no need transfer anymore

* revert compute_texture

* remove copyBufferToSubBuffer

---------

Co-authored-by: sunag <sunagbrasil@gmail.com>
  • Loading branch information
RenaudRohlinger and sunag authored Feb 6, 2024
1 parent 85e5dda commit a2bf250
Show file tree
Hide file tree
Showing 13 changed files with 415 additions and 26 deletions.
3 changes: 2 additions & 1 deletion examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,8 @@
"webgpu_postprocessing_anamorphic",
"webgpu_mirror",
"webgpu_multisampled_renderbuffers",
"webgpu_materials_texture_anisotropy"
"webgpu_materials_texture_anisotropy",
"webgpu_storage_buffer"
],
"webaudio": [
"webaudio_orientation",
Expand Down
2 changes: 1 addition & 1 deletion examples/jsm/nodes/Nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export { default as ReferenceNode, reference, referenceIndex } from './accessors
export { default as ReflectVectorNode, reflectVector } from './accessors/ReflectVectorNode.js';
export { default as SkinningNode, skinning } from './accessors/SkinningNode.js';
export { default as SceneNode, backgroundBlurriness, backgroundIntensity } from './accessors/SceneNode.js';
export { default as StorageBufferNode, storage } from './accessors/StorageBufferNode.js';
export { default as StorageBufferNode, storage, storageObject } from './accessors/StorageBufferNode.js';
export { default as TangentNode, tangentGeometry, tangentLocal, tangentView, tangentWorld, transformedTangentView, transformedTangentWorld } from './accessors/TangentNode.js';
export { default as TextureNode, texture, textureLoad, /*textureLevel,*/ sampler } from './accessors/TextureNode.js';
export { default as TextureStoreNode, textureStore } from './accessors/TextureStoreNode.js';
Expand Down
11 changes: 11 additions & 0 deletions examples/jsm/nodes/accessors/StorageBufferNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class StorageBufferNode extends BufferNode {

this.isStorageBufferNode = true;

this.bufferObject = false;

this._attribute = null;
this._varying = null;

Expand All @@ -30,6 +32,14 @@ class StorageBufferNode extends BufferNode {

}

setBufferObject( value ) {

this.bufferObject = value;

return this;

}

generate( builder ) {

if ( builder.isAvailable( 'storageBuffer' ) ) return super.generate( builder );
Expand Down Expand Up @@ -57,5 +67,6 @@ class StorageBufferNode extends BufferNode {
export default StorageBufferNode;

export const storage = ( value, type, count ) => nodeObject( new StorageBufferNode( value, type, count ) );
export const storageObject = ( value, type, count ) => nodeObject( new StorageBufferNode( value, type, count ).setBufferObject( true ) );

addNodeClass( 'StorageBufferNode', StorageBufferNode );
5 changes: 2 additions & 3 deletions examples/jsm/nodes/core/AssignNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,11 @@ class AssignNode extends TempNode {

generate( builder, output ) {

const targetNode = this.targetNode;
const sourceNode = this.sourceNode;
const { targetNode, sourceNode } = this;

const targetType = targetNode.getNodeType( builder );

const target = targetNode.build( builder );
const target = targetNode.context( { assign: true } ).build( builder );
const source = sourceNode.build( builder, targetType );

const snippet = `${ target } = ${ source }`;
Expand Down
42 changes: 40 additions & 2 deletions examples/jsm/nodes/utils/StorageArrayElementNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,58 @@ class StorageArrayElementNode extends ArrayElementNode {

}

generate( builder ) {
setup( builder ) {

if ( builder.isAvailable( 'storageBuffer' ) === false ) {

if ( ! this.node.instanceIndex && this.node.bufferObject === true ) {

builder.setupPBO( this.node );

}

}

return super.setup( builder );

}

generate( builder, output ) {

let snippet;

const isAssignContext = builder.context.assign;

//

if ( builder.isAvailable( 'storageBuffer' ) === false ) {

snippet = this.node.build( builder );
const { node } = this;

if ( ! node.instanceIndex && this.node.bufferObject === true && isAssignContext !== true ) {

snippet = builder.generatePBO( this );

} else {

snippet = node.build( builder );

}

} else {

snippet = super.generate( builder );

}

if ( isAssignContext !== true ) {

const type = this.getNodeType( builder );

snippet = builder.format( snippet, type, output );

}

return snippet;

}
Expand Down
13 changes: 12 additions & 1 deletion examples/jsm/renderers/webgl/WebGLBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,9 +428,20 @@ class WebGLBackend extends Backend {
gl.bindTransformFeedback( gl.TRANSFORM_FEEDBACK, null );

// switch active buffers

for ( let i = 0; i < transformBuffers.length; i ++ ) {

transformBuffers[ i ].switchBuffers();
const dualAttributeData = transformBuffers[ i ];

if ( dualAttributeData.pbo ) {

this.textureUtils.copyBufferToTexture( dualAttributeData.transformBuffer, dualAttributeData.pbo );

} else {

dualAttributeData.switchBuffers();

}

}

Expand Down
130 changes: 128 additions & 2 deletions examples/jsm/renderers/webgl/nodes/GLSLNodeBuilder.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { MathNode, GLSLNodeParser, NodeBuilder } from '../../../nodes/Nodes.js';
import { MathNode, GLSLNodeParser, NodeBuilder, UniformNode, vectorComponents } from '../../../nodes/Nodes.js';

import UniformBuffer from '../../common/UniformBuffer.js';
import NodeUniformsGroup from '../../common/nodes/NodeUniformsGroup.js';

import { NodeSampledTexture, NodeSampledCubeTexture } from '../../common/nodes/NodeSampledTexture.js';

import { IntType } from 'three';

import { IntType, DataTexture, RGBAFormat, FloatType } from 'three';

const glslMethods = {
[ MathNode.ATAN2 ]: 'atan',
Expand Down Expand Up @@ -84,6 +85,131 @@ ${ flowData.code }

}

setupPBO( storageBufferNode ) {

const attribute = storageBufferNode.value;

if ( attribute.pbo === undefined ) {

const originalArray = attribute.array;
const numElements = attribute.count * attribute.itemSize;

const width = Math.pow( 2, Math.ceil( Math.log2( Math.sqrt( numElements / 4 ) ) ) );
let height = Math.ceil( ( numElements / 4 ) / width );
if ( width * height * 4 < numElements ) height ++; // Ensure enough space

const newSize = width * height * 4; // 4 floats per pixel due to RGBA format

const newArray = new Float32Array( newSize );

newArray.set( originalArray, 0 );

attribute.array = newArray;
attribute.count = newSize;

const pboTexture = new DataTexture( attribute.array, width, height, RGBAFormat, FloatType );
pboTexture.needsUpdate = true;
pboTexture.isPBOTexture = true;

const pbo = new UniformNode( pboTexture );
pbo.setPrecision( 'high' );

attribute.pboNode = pbo;
attribute.pbo = pbo.value;

this.getUniformFromNode( attribute.pboNode, 'texture', this.shaderStage, this.context.label );

}

}

generatePBO( storageArrayElementNode ) {

const { node, indexNode } = storageArrayElementNode;
const attribute = node.value;

if ( this.renderer.backend.has( attribute ) ) {

const attributeData = this.renderer.backend.get( attribute );
attributeData.pbo = attribute.pbo;

}


const nodeUniform = this.getUniformFromNode( attribute.pboNode, 'texture', this.shaderStage, this.context.label );
const textureName = this.getPropertyName( nodeUniform );

indexNode.increaseUsage( this ); // force cache generate to be used as index in x,y
const indexSnippet = indexNode.build( this, 'uint' );

const elementNodeData = this.getDataFromNode( storageArrayElementNode );

let propertyName = elementNodeData.propertyName;

if ( propertyName === undefined ) {

// property element

const nodeVar = this.getVarFromNode( storageArrayElementNode );

propertyName = this.getPropertyName( nodeVar );

// property size

const bufferNodeData = this.getDataFromNode( node );

let propertySizeName = bufferNodeData.propertySizeName;

if ( propertySizeName === undefined ) {

propertySizeName = propertyName + 'Size';

this.getVarFromNode( node, propertySizeName, 'uint' );

this.addLineFlowCode( `${ propertySizeName } = uint( textureSize( ${ textureName }, 0 ).x )` );

bufferNodeData.propertySizeName = propertySizeName;

}

//

let channel;
let padding;

const itemSize = attribute.itemSize;

if ( itemSize === 1 ) {

padding = 4;
channel = `[ ${indexSnippet} % uint( ${ padding } ) ]`;

} else {

padding = itemSize > 2 ? 1 : itemSize;
channel = '.' + vectorComponents.join( '' ).slice( 0, itemSize );

}

const uvSnippet = `ivec2(
${indexSnippet} / uint( ${ padding } ) % ${ propertySizeName },
${indexSnippet} / ( uint( ${ padding } ) * ${ propertySizeName } )
)`;

const snippet = this.generateTextureLoad( null, textureName, uvSnippet, null, '0' );

//

this.addLineFlowCode( `${ propertyName } = ${ snippet + channel }` );

elementNodeData.propertyName = propertyName;

}

return propertyName;

}

generateTextureLoad( texture, textureProperty, uvIndexSnippet, depthSnippet, levelSnippet = '0' ) {

if ( depthSnippet ) {
Expand Down
4 changes: 4 additions & 0 deletions examples/jsm/renderers/webgl/utils/WebGLAttributeUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ class DualAttributeData {

this.buffers = [ attributeData.bufferGPU, dualBuffer ];
this.type = attributeData.type;
this.pbo = attributeData.pbo;
this.byteLength = attributeData.byteLength;
this.bytesPerElement = attributeData.BYTES_PER_ELEMENT;
this.version = attributeData.version;
this.isInteger = attributeData.isInteger;
Expand Down Expand Up @@ -127,8 +129,10 @@ class WebGLAttributeUtils {
let attributeData = {
bufferGPU,
type,
byteLength: array.byteLength,
bytesPerElement: array.BYTES_PER_ELEMENT,
version: attribute.version,
pbo: attribute.pbo,
isInteger: type === gl.INT || type === gl.UNSIGNED_INT || attribute.gpuType === IntType,
id: _id ++
};
Expand Down
36 changes: 36 additions & 0 deletions examples/jsm/renderers/webgl/utils/WebGLTextureUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ class WebGLTextureUtils {
if ( texture.type === FloatType && extensions.has( 'OES_texture_float_linear' ) === false ) return; // verify extension for WebGL 1 and WebGL 2

if ( texture.anisotropy > 1 || currentAnisotropy !== texture.anisotropy ) {

const extension = extensions.get( 'EXT_texture_filter_anisotropic' );
gl.texParameterf( textureType, extension.TEXTURE_MAX_ANISOTROPY_EXT, Math.min( texture.anisotropy, backend.getMaxAnisotropy() ) );
backend.get( texture ).currentAnisotropy = texture.anisotropy;
Expand Down Expand Up @@ -289,6 +290,41 @@ class WebGLTextureUtils {

}

copyBufferToTexture( buffer, texture ) {

const { gl, backend } = this;

const { textureGPU, glTextureType, glFormat, glType } = backend.get( texture );

const { width, height } = texture.source.data;

gl.bindBuffer( gl.PIXEL_UNPACK_BUFFER, buffer );

backend.state.bindTexture( glTextureType, textureGPU );

gl.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, false );
gl.pixelStorei( gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false );
gl.texSubImage2D( glTextureType, 0, 0, 0, width, height, glFormat, glType, 0 );

gl.bindBuffer( gl.PIXEL_UNPACK_BUFFER, null );

backend.state.unbindTexture();
// debug
// const framebuffer = gl.createFramebuffer();
// gl.bindFramebuffer( gl.FRAMEBUFFER, framebuffer );
// gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, glTextureType, textureGPU, 0 );

// const readout = new Float32Array( width * height * 4 );

// const altFormat = gl.getParameter( gl.IMPLEMENTATION_COLOR_READ_FORMAT );
// const altType = gl.getParameter( gl.IMPLEMENTATION_COLOR_READ_TYPE );

// gl.readPixels( 0, 0, width, height, altFormat, altType, readout );
// gl.bindFramebuffer( gl.FRAMEBUFFER, null );
// console.log( readout );

}

updateTexture( texture, options ) {

const { gl } = this;
Expand Down
Binary file added examples/screenshots/webgpu_storage_buffer.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit a2bf250

Please sign in to comment.