-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Infer storage buffer element type from the typed array passed in #9083
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
aashu2006
wants to merge
4
commits into
processing:main
Choose a base branch
from
aashu2006:storage-element-types
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+261
−29
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1fc7b88
Infer storage buffer element type from the typed array passed in
aashu2006 62ed6b9
docs: update storage buffer JSDoc types and examples
aashu2006 e0a49c3
cache storage element type check and clarify docs
aashu2006 cfc7cf7
fix storage element type test after caching refactor
aashu2006 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,13 +43,33 @@ function rendererWebGPU(p5, fn) { | |
| const { Renderer3D, Shader, Texture, MipmapTexture, Image, Camera, RGBA } = | ||
| p5; | ||
|
|
||
| // Maps a WGSL storage element type to the typed array that reads it back | ||
| // correctly. Used to check what a buffer was created with against what the | ||
| // shader declares. | ||
| const STORAGE_ARRAY_TYPES = { | ||
| f32: Float32Array, | ||
| u32: Uint32Array, | ||
| i32: Int32Array | ||
| }; | ||
|
|
||
| class StorageBuffer { | ||
| constructor(buffer, size, renderer, schema = null) { | ||
| constructor( | ||
| buffer, | ||
| size, | ||
| renderer, | ||
| schema = null, | ||
| arrayType = Float32Array | ||
| ) { | ||
| this._isStorageBuffer = true; | ||
| this.buffer = buffer; | ||
| this.size = size; | ||
| this._renderer = renderer; | ||
| this._schema = schema; | ||
| // Struct buffers are always packed as floats | ||
| this._arrayType = schema !== null ? Float32Array : arrayType; | ||
| // The element type this buffer has already been checked against, so | ||
| // the check is skipped on every later frame | ||
| this._checkedArrayType = undefined; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -116,7 +136,7 @@ function rendererWebGPU(p5, fn) { | |
| * @beta | ||
| * @webgpu | ||
| * @webgpuOnly | ||
| * @param {Number[]|Float32Array|Object[]} data The new data to write into the buffer. | ||
| * @param {Number[]|Float32Array|Uint32Array|Int32Array|Object[]} data The new data to write into the buffer. | ||
| */ | ||
| update(data) { | ||
| const device = this._renderer.device; | ||
|
|
@@ -151,33 +171,35 @@ function rendererWebGPU(p5, fn) { | |
| } | ||
| device.queue.writeBuffer(this.buffer, 0, packed); | ||
| } else { | ||
| // Buffer was created with a float array | ||
| let floatData; | ||
| if (data instanceof Float32Array) { | ||
| floatData = data; | ||
| // Buffer was created with a number array | ||
| const ArrayType = this._arrayType; | ||
| let typedData; | ||
| if (data instanceof ArrayType) { | ||
| typedData = data; | ||
| } else if (Array.isArray(data)) { | ||
| floatData = new Float32Array(data); | ||
| typedData = new ArrayType(data); | ||
| } else { | ||
| throw new Error( | ||
| 'update() expects a Float32Array or array of numbers for this buffer' | ||
| `update() expects a ${ArrayType.name} or array of numbers for this buffer` | ||
| ); | ||
| } | ||
|
|
||
| if (floatData.byteLength > this.size) { | ||
| if (typedData.byteLength > this.size) { | ||
| throw new Error( | ||
| `update() data (${floatData.byteLength} bytes) exceeds buffer size (${this.size} bytes)` | ||
| `update() data (${typedData.byteLength} bytes) exceeds buffer size (${this.size} bytes)` | ||
| ); | ||
| } | ||
| device.queue.writeBuffer(this.buffer, 0, floatData); | ||
| device.queue.writeBuffer(this.buffer, 0, typedData); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Reads data from a storage buffer back into JavaScript. | ||
| * | ||
| * Copies data from the GPU to the CPU using a temporary buffer, | ||
| * so it must be awaited. Returns a `Float32Array` for number | ||
| * buffers, or an array of plain objects for struct buffers. | ||
| * so it must be awaited. Returns a typed array (such as `Float32Array` or | ||
| * `Uint32Array`) for number buffers, or an array of plain objects for | ||
| * struct buffers. | ||
| * | ||
| * Note: This is a GPU -> CPU read, so calling it often (like every frame) | ||
| * can be slow. | ||
|
|
@@ -208,12 +230,47 @@ function rendererWebGPU(p5, fn) { | |
| * } | ||
| * ``` | ||
| * | ||
| * While p5.strands is still growing, there may be WGSL features it doesn't | ||
| * cover yet. You can still reach for them by writing WGSL directly, and the | ||
| * buffer will read back as the right kind of typed array. Atomics are one | ||
| * example: WGSL only allows them on `u32` and `i32`, so a buffer used as | ||
| * `array<atomic<u32>>` should be created with a `Uint32Array`. | ||
| * | ||
| * ```js example | ||
| * let data; | ||
| * let computeShader; | ||
| * | ||
| * async function setup() { | ||
| * await createCanvas(100, 100, WEBGPU); | ||
| * | ||
| * data = createStorage(new Uint32Array([10, 20, 30, 40])); | ||
| * computeShader = buildComputeShader({ | ||
| * computeDeclarations: ` | ||
| * @group(0) @binding(1) var<storage, read_write> counts: array<atomic<u32>>; | ||
| * `, | ||
| * 'void iteration': `(index: vec3<i32>) { | ||
| * let idx = index.x; | ||
| * atomicAdd(&counts[idx], 5u); | ||
| * }` | ||
| * }); | ||
| * computeShader.setUniform('counts', data); | ||
| * compute(computeShader, 4); | ||
| * | ||
| * let result = await data.read(); | ||
| * // result is Uint32Array [15, 25, 35, 45] | ||
| * for (let i = 0; i < result.length; i++) { | ||
| * print(result[i]); | ||
| * } | ||
| * describe('Prints the values 15, 25, 35, 45 to the console.'); | ||
| * } | ||
| * ``` | ||
| * | ||
| * @method read | ||
| * @for p5.StorageBuffer | ||
| * @beta | ||
| * @webgpu | ||
| * @webgpuOnly | ||
| * @returns {Promise<Float32Array|Object[]>} | ||
| * @returns {Promise<Float32Array|Uint32Array|Int32Array|Object[]>} | ||
| */ | ||
| async read() { | ||
| const device = this._renderer.device; | ||
|
|
@@ -238,8 +295,11 @@ function rendererWebGPU(p5, fn) { | |
| const mappedRange = stagingBuffer.getMappedRange(0, this.size); | ||
|
|
||
| // Copy before unmapping because mapped memory becomes invalid after unmap | ||
| const rawCopy = new Float32Array(mappedRange.byteLength / 4); | ||
| rawCopy.set(new Float32Array(mappedRange)); | ||
| const ArrayType = this._arrayType; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice! |
||
| const rawCopy = new ArrayType( | ||
| mappedRange.byteLength / ArrayType.BYTES_PER_ELEMENT | ||
| ); | ||
| rawCopy.set(new ArrayType(mappedRange)); | ||
|
|
||
| stagingBuffer.unmap(); | ||
| stagingBuffer.destroy(); | ||
|
|
@@ -2134,6 +2194,7 @@ function rendererWebGPU(p5, fn) { | |
| `Use shader.setUniform("${entry.storage.name}", storageBuffer)` | ||
| ); | ||
| } | ||
| this._checkStorageElementType(entry.storage, uniform._cachedData); | ||
| bgEntries.push({ | ||
| binding: entry.binding, | ||
| resource: { buffer: uniform._cachedData.buffer } | ||
|
|
@@ -2464,7 +2525,7 @@ function rendererWebGPU(p5, fn) { | |
| // Extract storage buffers | ||
| const storageBuffers = {}; | ||
| const storageRegex = | ||
| /@group\((\d+)\)\s*@binding\((\d+)\)\s*var<storage,\s*(read|read_write)>\s+(\w+)\s*:\s*array<\w+>/g; | ||
| /@group\((\d+)\)\s*@binding\((\d+)\)\s*var<storage,\s*(read|read_write)>\s+(\w+)\s*:\s*array<(\w+|atomic<\w+>)>/g; | ||
|
|
||
| // Track which bindings are taken by the struct properties we've parsed | ||
| // (the rest should be textures/samplers) | ||
|
|
@@ -2517,7 +2578,7 @@ function rendererWebGPU(p5, fn) { | |
|
|
||
| // Parse storage buffers | ||
| while ((match = storageRegex.exec(src)) !== null) { | ||
| const [_, group, binding, accessMode, name] = match; | ||
| const [_, group, binding, accessMode, name, elementType] = match; | ||
| const groupIndex = parseInt(group); | ||
| const bindingIndex = parseInt(binding); | ||
|
|
||
|
|
@@ -2536,13 +2597,21 @@ function rendererWebGPU(p5, fn) { | |
| name, | ||
| accessMode: finalAccessMode, // 'read' or 'read_write' | ||
| isStorage: true, | ||
| type: 'storage' | ||
| type: 'storage', | ||
| elementType, // e.g. 'f32', 'u32', 'atomic<u32>' | ||
| // Resolved here so the per-frame check is just a comparison | ||
| expectedArrayType: this._storageArrayTypeFor(elementType) | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| // Store storage buffers on shader for later use | ||
| // Store storage buffers on shader for later use, keyed by name too so | ||
| // that setUniform() can look one up without scanning the whole list | ||
| shader._storageBuffers = Object.values(storageBuffers); | ||
| shader._storageBuffersByName = {}; | ||
| for (const storage of shader._storageBuffers) { | ||
| shader._storageBuffersByName[storage.name] = storage; | ||
| } | ||
|
|
||
| return [ | ||
| ...Object.values(allUniforms).sort((a, b) => a.index - b.index), | ||
|
|
@@ -2583,6 +2652,13 @@ function rendererWebGPU(p5, fn) { | |
| uniform, | ||
| uniform._cachedData | ||
| ); | ||
| } else if (shader._storageBuffersByName) { | ||
| // The shader has been parsed, so we know what element type it | ||
| // declares for this buffer and can check it early | ||
| this._checkStorageElementType( | ||
| shader._storageBuffersByName[uniform.name], | ||
| data | ||
| ); | ||
| } | ||
| shader.buffersDirty.add(uniform.group * 1000 + uniform.binding); | ||
| } | ||
|
|
@@ -3906,6 +3982,48 @@ ${hookUniformFields}} | |
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the WGSL element type a shader declares for a storage buffer | ||
| * into the typed array that reads it back correctly, unwrapping | ||
| * `atomic<T>` to `T`. Returns undefined for types we can't check. | ||
| * @private | ||
| */ | ||
| _storageArrayTypeFor(elementType) { | ||
| if (!elementType) return undefined; | ||
| return STORAGE_ARRAY_TYPES[elementType.replace(/^atomic<(\w+)>$/, '$1')]; | ||
| } | ||
|
|
||
| /** | ||
| * Warns when the typed array a storage buffer was created with doesn't | ||
| * match the element type the shader declares for it, since the bytes | ||
| * would otherwise be silently reinterpreted. | ||
| * | ||
| * Both call sites run every frame, so the result is cached on the buffer: | ||
| * after the first check this costs a property read and a comparison, and | ||
| * any warning is only ever logged once. | ||
| * @private | ||
| */ | ||
| _checkStorageElementType(parsedStorage, storageBuffer) { | ||
| if (p5.disableFriendlyErrors) return; | ||
| // Resolved once when the shader was parsed | ||
| const expected = parsedStorage?.expectedArrayType; | ||
| if (!expected) return; | ||
| if (storageBuffer._checkedArrayType === expected) return; | ||
| storageBuffer._checkedArrayType = expected; | ||
|
|
||
| // Struct buffers are always packed as floats | ||
| if (storageBuffer._schema !== null) return; | ||
| if (storageBuffer._arrayType === expected) return; | ||
|
|
||
| p5._friendlyError( | ||
| `The storage buffer "${parsedStorage.name}" is declared as ` + | ||
| `array<${parsedStorage.elementType}> in the shader, but it was created ` + | ||
| `with a ${storageBuffer._arrayType.name}. Create it with a ` + | ||
| `${expected.name} instead so the values are read back correctly.`, | ||
| 'createStorage' | ||
| ); | ||
| } | ||
|
|
||
| createStorage(dataOrCount) { | ||
| const device = this.device; | ||
|
|
||
|
|
@@ -3970,22 +4088,25 @@ ${hookUniformFields}} | |
| // Determine buffer size and initial data | ||
| let size, initialData; | ||
| if (typeof dataOrCount === 'number') { | ||
| // createStorage(count) - zero-initialized | ||
| // createStorage(count) - zero-initialized, nothing to infer a type from | ||
| size = dataOrCount * 4; // floats are 4 bytes | ||
| initialData = new Float32Array(dataOrCount); | ||
| } else { | ||
| // createStorage(array) - from data | ||
| if (dataOrCount instanceof Float32Array) { | ||
| if ( | ||
| ArrayBuffer.isView(dataOrCount) && | ||
| !(dataOrCount instanceof DataView) | ||
| ) { | ||
| initialData = dataOrCount; | ||
| } else if (Array.isArray(dataOrCount)) { | ||
| // Plain arrays default to floats for back compat | ||
| initialData = new Float32Array(dataOrCount); | ||
| } else { | ||
| throw new Error( | ||
| 'createStorage expects a number or array/Float32Array' | ||
| ); | ||
| throw new Error('createStorage expects a number or array/TypedArray'); | ||
| } | ||
| size = initialData.byteLength; | ||
| } | ||
| const ArrayType = initialData.constructor; | ||
|
|
||
| // Align to 16 bytes (WGSL storage buffer alignment requirement) | ||
| size = Math.ceil(size / 16) * 16; | ||
|
|
@@ -4002,12 +4123,18 @@ ${hookUniformFields}} | |
|
|
||
| // Write initial data if provided | ||
| if (initialData.length > 0) { | ||
| const mapping = new Float32Array(buffer.getMappedRange()); | ||
| const mapping = new ArrayType(buffer.getMappedRange()); | ||
| mapping.set(initialData); | ||
| buffer.unmap(); | ||
| } | ||
|
|
||
| const storageBuffer = new StorageBuffer(buffer, size, this); | ||
| const storageBuffer = new StorageBuffer( | ||
| buffer, | ||
| size, | ||
| this, | ||
| null, | ||
| ArrayType | ||
| ); | ||
|
|
||
| // Track for cleanup | ||
| this._storageBuffers.add(storageBuffer); | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a fairly advanced example so we should probably add some text above it explaining why you'd use it this way. The pitch is probably something like, while we're developing p5.strands, you might want to reach for features in wgsl that we haven't added yet, and here's how you might use atomics in compute shaders?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added an intro along those lines - while p5.strands is still growing you may want WGSL features it doesn't cover yet, with atomics as the example since WGSL only allows them on
u32/i32.