From 489908dbdd3ccd80cca7bfecf1775c3279d87f26 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 5 Feb 2026 20:02:18 -0500 Subject: [PATCH 01/10] Factor uniforms into multiple bindings --- src/webgpu/p5.RendererWebGPU.js | 388 ++++++++++++++++++++++++------ src/webgpu/shaders/color.js | 45 ++-- src/webgpu/shaders/font.js | 69 +++--- src/webgpu/shaders/line.js | 79 +++--- src/webgpu/shaders/material.js | 152 ++++++------ src/webgpu/strands_wgslBackend.js | 7 +- 6 files changed, 511 insertions(+), 229 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 46db2b3925..38b2288660 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -496,39 +496,46 @@ function rendererWebGPU(p5, fn) { } _finalizeShader(shader) { - const rawSize = Math.max( - 0, - ...Object.values(shader.uniforms).filter(u => !u.isSampler).map(u => u.offsetEnd) - ); - const alignedSize = Math.ceil(rawSize / 16) * 16; - shader._uniformData = new Float32Array(alignedSize / 4); - shader._uniformDataView = new DataView(shader._uniformData.buffer); - - // Create pools for uniform buffers (both GPU buffers and data arrays.) This - // is so that we can queue up multiple things to be able to be drawn and have - // the GPU go through them as fast as possible. If we're overwriting the same - // data again and again, we would have to wait for the GPU after each primitive - // that we draw. - shader._uniformBufferPool = []; - shader._uniformBuffersInUse = []; - shader._uniformBufferSize = alignedSize; - - // Create the first buffer for the pool - const firstGPUBuffer = this.device.createBuffer({ - size: alignedSize, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - const firstData = new Float32Array(alignedSize / 4); - const firstDataView = new DataView(firstData.buffer); + // Create per-group buffer pools instead of a single pool + shader._uniformBufferGroups = []; + + for (const group of shader._uniformGroups) { + // Calculate the size needed for this group's uniforms + const groupUniforms = Object.values(group.uniforms); + const rawSize = Math.max( + 0, + ...groupUniforms.map(u => u.offsetEnd) + ); + const alignedSize = Math.ceil(rawSize / 16) * 16; - shader._uniformBufferPool.push({ - buffer: firstGPUBuffer, - data: firstData, - dataView: firstDataView - }); + // Create staging data arrays for this group + const groupData = new Float32Array(alignedSize / 4); + const groupDataView = new DataView(groupData.buffer); - // Keep backward compatibility reference - shader._uniformBuffer = firstGPUBuffer; + // Create GPU buffer pool for this group + const firstGPUBuffer = this.device.createBuffer({ + size: alignedSize, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + const firstData = new Float32Array(alignedSize / 4); + const firstDataView = new DataView(firstData.buffer); + + shader._uniformBufferGroups.push({ + binding: group.binding, + varName: group.varName, + structType: group.structType, + uniforms: groupUniforms, + size: alignedSize, + bufferPool: [{ + buffer: firstGPUBuffer, + data: firstData, + dataView: firstDataView + }], + buffersInUse: [], + currentBuffer: null, // For caching + cachedData: null // For caching comparison + }); + } // Register this shader in our registry for pool cleanup this._shadersWithPools.push(shader); @@ -536,12 +543,17 @@ function rendererWebGPU(p5, fn) { const bindGroupLayouts = new Map(); // group index -> bindGroupLayout const groupEntries = new Map(); // group index -> array of entries - // We're enforcing that every shader have a single uniform struct in binding 0 - groupEntries.set(0, [{ - binding: 0, - visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, - buffer: { type: 'uniform' }, - }]); + // Add all uniform group bindings to group 0 + const group0Entries = []; + for (const bufferGroup of shader._uniformBufferGroups) { + group0Entries.push({ + binding: bufferGroup.binding, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: 'uniform' }, + }); + } + group0Entries.sort((a, b) => a.binding - b.binding); + groupEntries.set(0, group0Entries); // Add the variable amount of samplers and texture bindings that can come after for (const sampler of shader.samplers) { @@ -563,6 +575,7 @@ function rendererWebGPU(p5, fn) { uniform: sampler, }); + entries.sort((a, b) => a.binding - b.binding); groupEntries.set(group, entries); } @@ -1072,6 +1085,7 @@ function rendererWebGPU(p5, fn) { // Uniform buffer pool management ////////////////////////////////////////////// + // TODO(dave): delete? _getUniformBufferFromPool(shader) { // Try to get a buffer from the pool if (shader._uniformBufferPool.length > 0) { @@ -1101,17 +1115,24 @@ function rendererWebGPU(p5, fn) { _returnUniformBuffersToPool() { // Return all used buffers back to their pools for all registered shaders for (const shader of this._shadersWithPools) { - if (shader._uniformBuffersInUse && shader._uniformBuffersInUse.length > 0) { - this._returnShaderBuffersToPool(shader); - } + this._returnShaderBuffersToPool(shader); } } _returnShaderBuffersToPool(shader) { - // Move all buffers from inUse back to pool - while (shader._uniformBuffersInUse.length > 0) { - const bufferInfo = shader._uniformBuffersInUse.pop(); - shader._uniformBufferPool.push(bufferInfo); + if (shader._uniformBufferGroups) { + for (const bufferGroup of shader._uniformBufferGroups) { + const seen = new Set(); + while (bufferGroup.buffersInUse.length > 0) { + const bufferInfo = bufferGroup.buffersInUse.pop(); + const prevSeen = seen.has(bufferInfo); + seen.add(bufferInfo); + + if (!prevSeen) { + bufferGroup.bufferPool.push(bufferInfo); + } + } + } } } @@ -1297,33 +1318,103 @@ function rendererWebGPU(p5, fn) { const gpuBuffer = buffers[buffer.dst]; passEncoder.setVertexBuffer(location, gpuBuffer, 0); } - // Bind uniforms - get a buffer from the pool - const uniformBufferInfo = this._getUniformBufferFromPool(currentShader); - this._packUniforms(currentShader, uniformBufferInfo); - this.device.queue.writeBuffer( - uniformBufferInfo.buffer, - 0, - uniformBufferInfo.data.buffer, - uniformBufferInfo.data.byteOffset, - uniformBufferInfo.data.byteLength - ); - // Bind sampler/texture uniforms + const uniformBuffersForBinding = []; + + for (const bufferGroup of currentShader._uniformBufferGroups) { + let bufferInfo; + const dataChanged = this._hasGroupDataChanged(currentShader, bufferGroup); + + if (!dataChanged && bufferGroup.currentBuffer) { + // Reuse the cached buffer - no need to pack or write + bufferInfo = bufferGroup.currentBuffer; + // Still need to track it in buffersInUse for proper cleanup + bufferGroup.buffersInUse.push(bufferInfo); + } else { + // Data changed - get a buffer from the pool + if (bufferGroup.bufferPool.length > 0) { + bufferInfo = bufferGroup.bufferPool.pop(); + } else { + // Create a new buffer + const gpuBuffer = this.device.createBuffer({ + size: bufferGroup.size, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + bufferInfo = { + buffer: gpuBuffer, + data: new Float32Array(bufferGroup.size / 4), + dataView: new DataView(new Float32Array(bufferGroup.size / 4).buffer) + }; + } + + // Pack and write the data + this._packUniformGroup(currentShader, bufferGroup.uniforms, bufferInfo); + this.device.queue.writeBuffer( + bufferInfo.buffer, + 0, + bufferInfo.data.buffer, + bufferInfo.data.byteOffset, + bufferInfo.data.byteLength + ); + + // Cache this buffer and data for next frame + bufferGroup.buffersInUse.push(bufferInfo); + bufferGroup.currentBuffer = bufferInfo; + + // Store cached data for comparison + if (!bufferGroup.cachedData) { + bufferGroup.cachedData = new Float32Array(bufferGroup.size / 4); + bufferGroup.cachedDataView = new DataView(bufferGroup.cachedData.buffer); + } + bufferGroup.cachedData.set(bufferInfo.data); + } + + uniformBuffersForBinding.push({ + binding: bufferGroup.binding, + buffer: bufferInfo.buffer + }); + } + + // Bind sampler/texture uniforms and uniform buffers for (const [group, entries] of currentShader._groupEntries) { const bgEntries = entries.map(entry => { - if (group === 0 && entry.binding === 0) { + // Check if this is a uniform buffer binding + const uniformBuffer = uniformBuffersForBinding.find(ub => ub.binding === entry.binding); + if (uniformBuffer) { return { - binding: 0, - resource: { buffer: uniformBufferInfo.buffer }, + binding: entry.binding, + resource: { buffer: uniformBuffer.buffer }, }; } + // This must be a texture/sampler entry + if (!entry.uniform) { + console.error('Entry missing uniform field:', entry, 'uniformBuffersForBinding:', uniformBuffersForBinding); + throw new Error( + `Bind group entry at binding ${entry.binding} has no uniform field and is not a uniform buffer!` + ); + } + if (!entry.uniform.isSampler) { + console.error('Non-sampler uniform not handled:', entry.uniform); throw new Error( 'All non-texture/sampler uniforms should be in the uniform struct!' ); } + const texture = entry.uniform.type === 'sampler' + ? entry.uniform.textureSource?.texture + : entry.uniform.texture; + + if (!texture && entry.uniform.type !== 'sampler') { + console.warn(`Texture uniform ${entry.uniform.name} at binding ${entry.binding} has no texture! Using empty texture.`, { + uniform: entry.uniform, + type: entry.uniform.type, + hasTexture: !!entry.uniform.texture, + texture: entry.uniform.texture + }); + } + return { binding: entry.binding, resource: entry.uniform.type === 'sampler' @@ -1375,6 +1466,92 @@ function rendererWebGPU(p5, fn) { // SHADER ////////////////////////////////////////////// + _packUniformGroup(shader, groupUniforms, bufferInfo) { + // Pack a single group's uniforms into a buffer + const data = bufferInfo.data; + const dataView = bufferInfo.dataView; + + for (const uniform of groupUniforms) { + const fullUniform = shader.uniforms[uniform.name]; + if (!fullUniform || fullUniform.isSampler) continue; + + if (fullUniform.baseType === 'u32') { + if (fullUniform.size === 4) { + dataView.setUint32(fullUniform.offset, fullUniform._cachedData, true); + } else { + const uniformData = fullUniform._cachedData; + for (let i = 0; i < uniformData.length; i++) { + dataView.setUint32(fullUniform.offset + i * 4, uniformData[i], true); + } + } + } else if (fullUniform.baseType === 'i32') { + if (fullUniform.size === 4) { + dataView.setInt32(fullUniform.offset, fullUniform._cachedData, true); + } else { + const uniformData = fullUniform._cachedData; + for (let i = 0; i < uniformData.length; i++) { + dataView.setInt32(fullUniform.offset + i * 4, uniformData[i], true); + } + } + } else if (fullUniform.size === 4) { + data.set([fullUniform._cachedData], fullUniform.offset / 4); + } else if (fullUniform._cachedData !== undefined) { + data.set(fullUniform._cachedData, fullUniform.offset / 4); + } + } + } + + _hasGroupDataChanged(shader, bufferGroup) { + return true + // Check if any uniform in this group has changed since last pack + if (!bufferGroup.cachedData) { + return true; // First time + } + + for (const uniform of bufferGroup.uniforms) { + const fullUniform = shader.uniforms[uniform.name]; + if (!fullUniform || fullUniform.isSampler) continue; + + // Compare typed arrays bytewise + const currentData = fullUniform._cachedData; + const cachedOffset = fullUniform.offset; + const size = fullUniform.size; + + if (fullUniform.baseType === 'u32' || fullUniform.baseType === 'i32') { + if (fullUniform.size === 4) { + // Single value + if (bufferGroup.cachedDataView.getUint32(cachedOffset, true) !== currentData) { + return true; + } + } else { + // Array + for (let i = 0; i < currentData.length; i++) { + if (bufferGroup.cachedDataView.getUint32(cachedOffset + i * 4, true) !== currentData[i]) { + return true; + } + } + } + } else { + if (fullUniform.size === 4) { + // Single float + if (bufferGroup.cachedData[cachedOffset / 4] !== currentData) { + return true; + } + } else if (currentData !== undefined) { + // Float array + const floatOffset = cachedOffset / 4; + for (let i = 0; i < currentData.length; i++) { + if (bufferGroup.cachedData[floatOffset + i] !== currentData[i]) { + return true; + } + } + } + } + } + + return false; // No changes detected + } + _packUniforms(shader, bufferInfo) { const data = bufferInfo.data; const dataView = bufferInfo.dataView; @@ -1542,22 +1719,54 @@ function rendererWebGPU(p5, fn) { } getUniformMetadata(shader) { - // Currently, for ease of parsing, we enforce that the first bind group is a - // struct, which contains all non-sampler uniforms. Then, any subsequent - // groups contain samplers. - - // Extract the struct name from the uniform variable declaration - const uniformVarRegex = /@group\(0\)\s+@binding\(0\)\s+var\s+(\w+)\s*:\s*(\w+);/; - const uniformVarMatch = uniformVarRegex.exec(shader.vertSrc()); - if (!uniformVarMatch) { - throw new Error('Expected a uniform struct bound to @group(0) @binding(0)'); - } - const structType = uniformVarMatch[2]; - const uniforms = this._parseStruct(shader.vertSrc(), structType); + // Parse all uniform struct bindings in group 0 + // Each binding represents a logical group of uniforms + const uniformGroups = []; + const uniformVarRegex = /@group\(0\)\s+@binding\((\d+)\)\s+var\s+(\w+)\s*:\s*(\w+);/g; + + let match; + while ((match = uniformVarRegex.exec(shader.vertSrc())) !== null) { + const [_, binding, varName, structType] = match; + const bindingIndex = parseInt(binding); + const uniforms = this._parseStruct(shader.vertSrc(), structType); + + uniformGroups.push({ + binding: bindingIndex, + varName, + structType, + uniforms + }); + } + + if (uniformGroups.length === 0) { + throw new Error('Expected at least one uniform struct bound to @group(0)'); + } + + // Flatten all uniforms for backward compatibility, but keep track of their groups + const allUniforms = {}; + for (const group of uniformGroups) { + for (const [uniformName, uniformData] of Object.entries(group.uniforms)) { + allUniforms[uniformName] = { + ...uniformData, + group: 0, + binding: group.binding, + varName: group.varName + }; + } + } + + const uniforms = allUniforms; + // Store uniform groups for buffer pooling + shader._uniformGroups = uniformGroups; + // Extract samplers from group bindings const samplers = {}; // TODO: support other texture types const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d|sampler);/g; + + // Track which bindings in group 0 are taken by uniforms + const group0UniformBindings = new Set(uniformGroups.map(g => g.binding)); + for (const [src, visibility] of [ [shader.vertSrc(), GPUShaderStage.VERTEX], [shader.fragSrc(), GPUShaderStage.FRAGMENT] @@ -1567,10 +1776,8 @@ function rendererWebGPU(p5, fn) { const [_, group, binding, name, type] = match; const groupIndex = parseInt(group); const bindingIndex = parseInt(binding); - // We're currently reserving group 0 for non-sampler stuff, which we parse - // above, so we can skip it here while we grab the remaining sampler - // uniforms - if (groupIndex === 0 && bindingIndex === 0) continue; + // Skip uniform bindings in group 0 which we've already parsed + if (groupIndex === 0 && group0UniformBindings.has(bindingIndex)) continue; const key = `${groupIndex},${bindingIndex}`; samplers[key] = { @@ -1602,14 +1809,14 @@ function rendererWebGPU(p5, fn) { return [...Object.values(uniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)]; } - getNextBindingIndex(shader, group = 0) { + getNextBindingIndex({ vert, frag }, group = 0) { // Get the highest binding index in the specified group and return the next available const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d|sampler|uniform)/g; let maxBindingIndex = -1; for (const [src, visibility] of [ - [shader.vertSrc(), GPUShaderStage.VERTEX], - [shader.fragSrc(), GPUShaderStage.FRAGMENT] + [vert, GPUShaderStage.VERTEX], + [frag, GPUShaderStage.FRAGMENT] ]) { let match; while ((match = samplerRegex.exec(src)) !== null) { @@ -1947,12 +2154,33 @@ function rendererWebGPU(p5, fn) { } } - let uniforms = ''; + // Inject hook uniforms as a separate struct at a new binding + let hookUniformFields = ''; for (const key in shader.hooks.uniforms) { // WGSL format: "name: type" - uniforms += `${key},\n`; + hookUniformFields += ` ${key},\n`; + } + + if (hookUniformFields) { + // Find the next available binding in group 0 + // Use the source we're currently building (preMain) so we can see texture bindings + // added by strands, and use the original source for the other shader type + const nextBinding = this.getNextBindingIndex({ + vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc, + frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc, + }, 0); + + // Create HookUniforms struct and binding + const hookUniformsDecl = ` +// Hook Uniforms (from .modify()) +struct HookUniforms { +${hookUniformFields}} + +@group(0) @binding(${nextBinding}) var hooks: HookUniforms; +`; + // Insert before the first @group binding + preMain = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`); } - preMain = preMain.replace(/struct\s+Uniforms\s+\{/, `$&\n${uniforms}`); // Handle varying variables by injecting them into VertexOutput and FragmentInput structs if (shader.hooks.varyingVariables && shader.hooks.varyingVariables.length > 0) { diff --git a/src/webgpu/shaders/color.js b/src/webgpu/shaders/color.js index dae7ab4e8d..b86be812d4 100644 --- a/src/webgpu/shaders/color.js +++ b/src/webgpu/shaders/color.js @@ -1,8 +1,16 @@ const uniforms = ` -struct Uniforms { +// Group 1: Camera and Projection +struct CameraUniforms { + uProjectionMatrix: mat4x4, // @p5 ifdef Vertex getWorldInputs - uModelMatrix: mat4x4, uViewMatrix: mat4x4, +// @p5 endif +} + +// Group 2: Model Transform +struct ModelUniforms { +// @p5 ifdef Vertex getWorldInputs + uModelMatrix: mat4x4, uModelNormalMatrix: mat3x3, uCameraNormalMatrix: mat3x3, // @p5 endif @@ -10,10 +18,13 @@ struct Uniforms { uModelViewMatrix: mat4x4, uNormalMatrix: mat3x3, // @p5 endif - uProjectionMatrix: mat4x4, +} + +// Group 3: Material Properties +struct MaterialUniforms { uMaterialColor: vec4, uUseVertexColor: u32, -}; +} `; export const colorVertexShader = ` @@ -32,7 +43,9 @@ struct VertexOutput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var material: MaterialUniforms; struct Vertex { position: vec3, @@ -46,12 +59,12 @@ fn main(input: VertexInput) -> VertexOutput { HOOK_beforeVertex(); var output: VertexOutput; - let useVertexColor = (uniforms.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0); + let useVertexColor = (material.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0); var inputs = Vertex( input.aPosition, input.aNormal, input.aTexCoord, - select(uniforms.uMaterialColor, input.aVertexColor, useVertexColor) + select(material.uMaterialColor, input.aVertexColor, useVertexColor) ); // @p5 ifdef Vertex getObjectInputs @@ -59,20 +72,20 @@ fn main(input: VertexInput) -> VertexOutput { // @p5 endif // @p5 ifdef Vertex getWorldInputs - inputs.position = (uniforms.uModelMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uModelNormalMatrix * inputs.normal; + inputs.position = (model.uModelMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = model.uModelNormalMatrix * inputs.normal; inputs = HOOK_getWorldInputs(inputs); // @p5 endif // @p5 ifdef Vertex getWorldInputs // Already multiplied by the model matrix, just apply view - inputs.position = (uniforms.uViewMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uCameraNormalMatrix * inputs.normal; + inputs.position = (camera.uViewMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = model.uCameraNormalMatrix * inputs.normal; // @p5 endif // @p5 ifndef Vertex getWorldInputs // Apply both at once - inputs.position = (uniforms.uModelViewMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uNormalMatrix * inputs.normal; + inputs.position = (model.uModelViewMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = model.uNormalMatrix * inputs.normal; // @p5 endif // @p5 ifdef Vertex getCameraInputs @@ -83,7 +96,7 @@ fn main(input: VertexInput) -> VertexOutput { output.vVertexNormal = normalize(inputs.normal); output.vColor = inputs.color; - output.Position = uniforms.uProjectionMatrix * vec4(inputs.position, 1.0); + output.Position = camera.uProjectionMatrix * vec4(inputs.position, 1.0); HOOK_afterVertex(); return output; @@ -98,7 +111,9 @@ struct FragmentInput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var material: MaterialUniforms; @fragment diff --git a/src/webgpu/shaders/font.js b/src/webgpu/shaders/font.js index 7cd92c6bff..b78550607e 100644 --- a/src/webgpu/shaders/font.js +++ b/src/webgpu/shaders/font.js @@ -1,7 +1,16 @@ const uniforms = ` -struct Uniforms { - uModelViewMatrix: mat4x4, +// Group 1: Camera and Projection +struct CameraUniforms { uProjectionMatrix: mat4x4, +} + +// Group 2: Model Transform +struct ModelUniforms { + uModelViewMatrix: mat4x4, +} + +// Group 3: Font Properties +struct FontUniforms { uStrokeImageSize: vec2, uCellsImageSize: vec2, uGridImageSize: vec2, @@ -10,7 +19,7 @@ struct Uniforms { uGlyphRect: vec4, uGlyphOffset: f32, uMaterialColor: vec4, -}; +} `; export const fontVertexShader = ` @@ -25,7 +34,9 @@ struct VertexOutput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var font: FontUniforms; @vertex fn main(input: VertexInput) -> VertexOutput { @@ -33,35 +44,35 @@ fn main(input: VertexInput) -> VertexOutput { var positionVec4 = vec4(input.aPosition, 1.0); // scale by the size of the glyph's rectangle - positionVec4.x = positionVec4.x * (uniforms.uGlyphRect.z - uniforms.uGlyphRect.x); - positionVec4.y = positionVec4.y * (uniforms.uGlyphRect.w - uniforms.uGlyphRect.y); + positionVec4.x = positionVec4.x * (font.uGlyphRect.z - font.uGlyphRect.x); + positionVec4.y = positionVec4.y * (font.uGlyphRect.w - font.uGlyphRect.y); // Expand glyph bounding boxes by 1px on each side to give a bit of room // for antialiasing - let newOrigin = (uniforms.uModelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz; - let newDX = (uniforms.uModelViewMatrix * vec4(1.0, 0.0, 0.0, 1.0)).xyz; - let newDY = (uniforms.uModelViewMatrix * vec4(0.0, 1.0, 0.0, 1.0)).xyz; + let newOrigin = (model.uModelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz; + let newDX = (model.uModelViewMatrix * vec4(1.0, 0.0, 0.0, 1.0)).xyz; + let newDY = (model.uModelViewMatrix * vec4(0.0, 1.0, 0.0, 1.0)).xyz; let pixelScale = vec2( 1.0 / length(newOrigin - newDX), 1.0 / length(newOrigin - newDY) ); let offset = pixelScale * normalize(input.aTexCoord - vec2(0.5, 0.5)); let textureOffset = offset * (1.0 / vec2( - uniforms.uGlyphRect.z - uniforms.uGlyphRect.x, - uniforms.uGlyphRect.w - uniforms.uGlyphRect.y + font.uGlyphRect.z - font.uGlyphRect.x, + font.uGlyphRect.w - font.uGlyphRect.y )); // move to the corner of the glyph - positionVec4.x = positionVec4.x + uniforms.uGlyphRect.x; - positionVec4.y = positionVec4.y + uniforms.uGlyphRect.y; + positionVec4.x = positionVec4.x + font.uGlyphRect.x; + positionVec4.y = positionVec4.y + font.uGlyphRect.y; // move to the letter's line offset - positionVec4.x = positionVec4.x + uniforms.uGlyphOffset; + positionVec4.x = positionVec4.x + font.uGlyphOffset; positionVec4.x = positionVec4.x + offset.x; positionVec4.y = positionVec4.y + offset.y; - output.Position = uniforms.uProjectionMatrix * uniforms.uModelViewMatrix * positionVec4; + output.Position = camera.uProjectionMatrix * model.uModelViewMatrix * positionVec4; output.vTexCoord = input.aTexCoord + textureOffset; return output; @@ -74,7 +85,9 @@ struct FragmentInput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var font: FontUniforms; @group(1) @binding(0) var uSamplerStrokes: texture_2d; @group(1) @binding(1) var uSamplerStrokes_sampler: sampler; @@ -217,14 +230,14 @@ fn main(input: FragmentInput) -> @location(0) vec4 { let pixelScale = hardness / fwidth(input.vTexCoord); // which grid cell is this pixel in? - let gridCoord = vec2(floor(input.vTexCoord * vec2(uniforms.uGridSize))); + let gridCoord = vec2(floor(input.vTexCoord * vec2(font.uGridSize))); // intersect curves in this row { // the index into the row info bitmap - let rowIndex = gridCoord.y + uniforms.uGridOffset.y; + let rowIndex = gridCoord.y + font.uGridOffset.y; // fetch the info texel - let rowInfo = getTexel(uSamplerRows, uSamplerRows_sampler, rowIndex, uniforms.uGridImageSize); + let rowInfo = getTexel(uSamplerRows, uSamplerRows_sampler, rowIndex, font.uGridImageSize); // unpack the rowInfo let rowStrokeIndex = getInt16(rowInfo.xy); let rowStrokeCount = getInt16(rowInfo.zw); @@ -237,14 +250,14 @@ fn main(input: FragmentInput) -> @location(0) vec4 { // each stroke is made up of 3 points: the start and control point // and the start of the next curve. // fetch the indices of this pair of strokes: - let strokeIndices = getTexel(uSamplerRowStrokes, uSamplerRowStrokes_sampler, rowStrokeIndex + iRowStroke, uniforms.uCellsImageSize); + let strokeIndices = getTexel(uSamplerRowStrokes, uSamplerRowStrokes_sampler, rowStrokeIndex + iRowStroke, font.uCellsImageSize); // unpack the stroke index let strokePos = getInt16(strokeIndices.xy); // fetch the two strokes - let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, uniforms.uStrokeImageSize); - let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, uniforms.uStrokeImageSize); + let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, font.uStrokeImageSize); + let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, font.uStrokeImageSize); // calculate the coverage coverageX(stroke0.xy, stroke0.zw, stroke1.xy, input.vTexCoord, pixelScale, &coverage, &weight); @@ -253,8 +266,8 @@ fn main(input: FragmentInput) -> @location(0) vec4 { // intersect curves in this column { - let colIndex = gridCoord.x + uniforms.uGridOffset.x; - let colInfo = getTexel(uSamplerCols, uSamplerCols_sampler, colIndex, uniforms.uGridImageSize); + let colIndex = gridCoord.x + font.uGridOffset.x; + let colInfo = getTexel(uSamplerCols, uSamplerCols_sampler, colIndex, font.uGridImageSize); let colStrokeIndex = getInt16(colInfo.xy); let colStrokeCount = getInt16(colInfo.zw); @@ -263,11 +276,11 @@ fn main(input: FragmentInput) -> @location(0) vec4 { break; } - let strokeIndices = getTexel(uSamplerColStrokes, uSamplerColStrokes_sampler, colStrokeIndex + iColStroke, uniforms.uCellsImageSize); + let strokeIndices = getTexel(uSamplerColStrokes, uSamplerColStrokes_sampler, colStrokeIndex + iColStroke, font.uCellsImageSize); let strokePos = getInt16(strokeIndices.xy); - let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, uniforms.uStrokeImageSize); - let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, uniforms.uStrokeImageSize); + let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, font.uStrokeImageSize); + let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, font.uStrokeImageSize); coverageY(stroke0.xy, stroke0.zw, stroke1.xy, input.vTexCoord, pixelScale, &coverage, &weight); } } @@ -276,7 +289,7 @@ fn main(input: FragmentInput) -> @location(0) vec4 { let distance = max(weight.x + weight.y, minDistance); // manhattan approx. let antialias = abs(dot(coverage, weight) / distance); let cover = min(abs(coverage.x), abs(coverage.y)); - var outColor = vec4(uniforms.uMaterialColor.rgb, 1.0) * uniforms.uMaterialColor.a; + var outColor = vec4(font.uMaterialColor.rgb, 1.0) * font.uMaterialColor.a; outColor = outColor * saturate_f32(max(antialias, cover)); return outColor; } diff --git a/src/webgpu/shaders/line.js b/src/webgpu/shaders/line.js index 5562d6775c..ac2f474f77 100644 --- a/src/webgpu/shaders/line.js +++ b/src/webgpu/shaders/line.js @@ -1,5 +1,13 @@ const uniforms = ` -struct Uniforms { +// Group 1: Camera and Projection +struct CameraUniforms { + uProjectionMatrix: mat4x4, + uViewport: vec4, + uPerspective: u32, +} + +// Group 2: Model Transform +struct ModelUniforms { // @p5 ifdef StrokeVertex getWorldInputs uModelMatrix: mat4x4, uViewMatrix: mat4x4, @@ -7,13 +15,14 @@ struct Uniforms { // @p5 ifndef StrokeVertex getWorldInputs uModelViewMatrix: mat4x4, // @p5 endif +} + +// Group 3: Stroke Properties +struct StrokeUniforms { uMaterialColor: vec4, - uProjectionMatrix: mat4x4, uStrokeWeight: f32, uUseLineColor: f32, uSimpleLines: f32, - uViewport: vec4, - uPerspective: u32, uStrokeCap: u32, uStrokeJoin: u32, }`; @@ -40,7 +49,9 @@ struct StrokeVertexOutput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var stroke: StrokeUniforms; struct StrokeVertex { position: vec3, @@ -73,7 +84,7 @@ fn lineIntersection(aPoint: vec2f, aDir: vec2f, bPoint: vec2f, bDir: vec2f) -> v fn main(input: StrokeVertexInput) -> StrokeVertexOutput { HOOK_beforeVertex(); var output: StrokeVertexOutput; - let simpleLines = (uniforms.uSimpleLines != 0.); + let simpleLines = (stroke.uSimpleLines != 0.); if (!simpleLines) { if (all(input.aTangentIn == vec3()) != all(input.aTangentOut == vec3())) { output.vCap = 1.; @@ -90,17 +101,17 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { } } var lineColor: vec4; - if (uniforms.uUseLineColor != 0.) { + if (stroke.uUseLineColor != 0.) { lineColor = input.aVertexColor; } else { - lineColor = uniforms.uMaterialColor; + lineColor = stroke.uMaterialColor; } var inputs = StrokeVertex( input.aPosition.xyz, input.aTangentIn, input.aTangentOut, lineColor, - uniforms.uStrokeWeight + stroke.uStrokeWeight ); // @p5 ifdef StrokeVertex getObjectInputs @@ -108,23 +119,23 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { // @p5 endif // @p5 ifdef StrokeVertex getWorldInputs - inputs.position = (uniforms.uModelMatrix * vec4(inputs.position, 1.)).xyz; - inputs.tangentIn = (uniforms.uModelMatrix * vec4(input.aTangentIn, 1.)).xyz; - inputs.tangentOut = (uniforms.uModelMatrix * vec4(input.aTangentOut, 1.)).xyz; + inputs.position = (model.uModelMatrix * vec4(inputs.position, 1.)).xyz; + inputs.tangentIn = (model.uModelMatrix * vec4(input.aTangentIn, 1.)).xyz; + inputs.tangentOut = (model.uModelMatrix * vec4(input.aTangentOut, 1.)).xyz; inputs = HOOK_getWorldInputs(inputs); // @p5 endif // @p5 ifdef StrokeVertex getWorldInputs // Already multiplied by the model matrix, just apply view - inputs.position = (uniforms.uViewMatrix * vec4(inputs.position, 1.)).xyz; - inputs.tangentIn = (uniforms.uViewMatrix * vec4(input.aTangentIn, 0.)).xyz; - inputs.tangentOut = (uniforms.uViewMatrix * vec4(input.aTangentOut, 0.)).xyz; + inputs.position = (model.uViewMatrix * vec4(inputs.position, 1.)).xyz; + inputs.tangentIn = (model.uViewMatrix * vec4(input.aTangentIn, 0.)).xyz; + inputs.tangentOut = (model.uViewMatrix * vec4(input.aTangentOut, 0.)).xyz; // @p5 endif // @p5 ifndef StrokeVertex getWorldInputs // Apply both at once - inputs.position = (uniforms.uModelViewMatrix * vec4(inputs.position, 1.)).xyz; - inputs.tangentIn = (uniforms.uModelViewMatrix * vec4(input.aTangentIn, 0.)).xyz; - inputs.tangentOut = (uniforms.uModelViewMatrix * vec4(input.aTangentOut, 0.)).xyz; + inputs.position = (model.uModelViewMatrix * vec4(inputs.position, 1.)).xyz; + inputs.tangentIn = (model.uModelViewMatrix * vec4(input.aTangentIn, 0.)).xyz; + inputs.tangentOut = (model.uModelViewMatrix * vec4(input.aTangentOut, 0.)).xyz; // @p5 endif // @p5 ifdef StrokeVertex getCameraInputs inputs = HOOK_getCameraInputs(inputs); @@ -174,27 +185,27 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { posqIn.z -= dynamicZAdjustment; posqOut.z -= dynamicZAdjustment; - var p = uniforms.uProjectionMatrix * posp; - var qIn = uniforms.uProjectionMatrix * posqIn; - var qOut = uniforms.uProjectionMatrix * posqOut; + var p = camera.uProjectionMatrix * posp; + var qIn = camera.uProjectionMatrix * posqIn; + var qOut = camera.uProjectionMatrix * posqOut; - var tangentIn = normalize((qIn.xy * p.w - p.xy * qIn.w) * uniforms.uViewport.zw); - var tangentOut = normalize((qOut.xy * p.w - p.xy * qOut.w) * uniforms.uViewport.zw); + var tangentIn = normalize((qIn.xy * p.w - p.xy * qIn.w) * camera.uViewport.zw); + var tangentOut = normalize((qOut.xy * p.w - p.xy * qOut.w) * camera.uViewport.zw); var curPerspScale = vec2(); - if (uniforms.uPerspective == 1) { + if (camera.uPerspective == 1) { // Perspective --- // convert from world to clip by multiplying with projection scaling factor // to get the right thickness (see https://github.com/processing/processing/issues/5182) // The y value of the projection matrix may be flipped if rendering to a Framebuffer. // Multiplying again by its sign here negates the flip to get just the scale. - curPerspScale = (uniforms.uProjectionMatrix * vec4(1., sign(uniforms.uProjectionMatrix[1][1]), 0., 0.)).xy; + curPerspScale = (camera.uProjectionMatrix * vec4(1., sign(camera.uProjectionMatrix[1][1]), 0., 0.)).xy; } else { // No Perspective --- // multiply by W (to cancel out division by W later in the pipeline) and // convert from screen to clip (derived from clip to screen above) - curPerspScale = p.w / (0.5 * uniforms.uViewport.zw); + curPerspScale = p.w / (0.5 * camera.uViewport.zw); } var offset = vec2(); @@ -217,7 +228,7 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { if (sideEnum == 2.) { // Calculate the position + tangent on either side of the join, and // find where the lines intersect to find the elbow of the join - var c = (posp.xy / posp.w + vec2(1.)) * 0.5 * uniforms.uViewport.zw; + var c = (posp.xy / posp.w + vec2(1.)) * 0.5 * camera.uViewport.zw; var intersection = lineIntersection( c + (side * normalIn * inputs.weight / 2.), @@ -243,7 +254,7 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { offset = side * normalOut * inputs.weight / 2.; } } - if (uniforms.uStrokeJoin == 2) { + if (stroke.uStrokeJoin == 2) { var avgNormal = vec2(-output.vTangent.y, output.vTangent.x); output.vMaxDist = abs(dot(avgNormal, normalIn * inputs.weight / 2.)); } else { @@ -292,7 +303,9 @@ struct StrokeFragmentInput { } ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var stroke: StrokeUniforms; fn distSquared(a: vec2, b: vec2) -> f32 { @@ -321,12 +334,12 @@ fn main(input: StrokeFragmentInput) -> @location(0) vec4 { if (input.vCap > 0.) { if ( - uniforms.uStrokeCap == STROKE_CAP_ROUND && + stroke.uStrokeCap == STROKE_CAP_ROUND && HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25) ) { discard; } else if ( - uniforms.uStrokeCap == STROKE_CAP_SQUARE && + stroke.uStrokeCap == STROKE_CAP_SQUARE && HOOK_shouldDiscard(dot(inputs.position - inputs.center, inputs.tangent) > 0.) ) { discard; @@ -335,11 +348,11 @@ fn main(input: StrokeFragmentInput) -> @location(0) vec4 { } } else if (input.vJoin > 0.) { if ( - uniforms.uStrokeJoin == STROKE_JOIN_ROUND && + stroke.uStrokeJoin == STROKE_JOIN_ROUND && HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25) ) { discard; - } else if (uniforms.uStrokeJoin == STROKE_JOIN_BEVEL) { + } else if (stroke.uStrokeJoin == STROKE_JOIN_BEVEL) { let normal = vec2(-inputs.tangent.y, -inputs.tangent.x); if (HOOK_shouldDiscard(abs(dot(inputs.position - inputs.center, normal)) > input.vMaxDist)) { discard; diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index aec9a1d292..c2218e4012 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -1,5 +1,13 @@ const uniforms = ` -struct Uniforms { +// Group 1: Camera and Projection +struct CameraUniforms { + uViewMatrix: mat4x4, + uProjectionMatrix: mat4x4, + uCameraRotation: mat3x3, +} + +// Group 2: Model Transform +struct ModelUniforms { // @p5 ifdef Vertex getWorldInputs uModelMatrix: mat4x4, uModelNormalMatrix: mat3x3, @@ -9,32 +17,34 @@ struct Uniforms { uModelViewMatrix: mat4x4, uNormalMatrix: mat3x3, // @p5 endif - uViewMatrix: mat4x4, - uProjectionMatrix: mat4x4, +} + +// Group 3: Material Properties +struct MaterialUniforms { uMaterialColor: vec4, uUseVertexColor: u32, - uHasSetAmbient: u32, uAmbientColor: vec3, uSpecularMatColor: vec4, uAmbientMatColor: vec4, uEmissiveMatColor: vec4, - uTint: vec4, isTexture: u32, + uSpecular: u32, + uShininess: f32, + uMetallic: f32, +} - uCameraRotation: mat3x3, - +// Group 4: Lighting +struct LightingUniforms { uDirectionalLightCount: i32, uLightingDirection: array, 5>, uDirectionalDiffuseColors: array, 5>, uDirectionalSpecularColors: array, 5>, - uPointLightCount: i32, uPointLightLocation: array, 5>, uPointLightDiffuseColors: array, 5>, uPointLightSpecularColors: array, 5>, - uSpotLightCount: i32, uSpotLightAngle: vec4, uSpotLightConc: vec4, @@ -42,18 +52,12 @@ struct Uniforms { uSpotLightSpecularColors: array, 4>, uSpotLightLocation: array, 4>, uSpotLightDirection: array, 4>, - - uSpecular: u32, - uShininess: f32, - uMetallic: f32, - uConstantAttenuation: f32, uLinearAttenuation: f32, uQuadraticAttenuation: f32, - uUseImageLight: u32, uUseLighting: u32, -}; +} `; export const materialVertexShader = ` @@ -73,7 +77,10 @@ struct VertexOutput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var material: MaterialUniforms; +@group(0) @binding(3) var lighting: LightingUniforms; struct Vertex { position: vec3, @@ -87,12 +94,12 @@ fn main(input: VertexInput) -> VertexOutput { HOOK_beforeVertex(); var output: VertexOutput; - let useVertexColor = (uniforms.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0); + let useVertexColor = (material.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0); var inputs = Vertex( input.aPosition, input.aNormal, input.aTexCoord, - select(uniforms.uMaterialColor, input.aVertexColor, useVertexColor) + select(material.uMaterialColor, input.aVertexColor, useVertexColor) ); // @p5 ifdef Vertex getObjectInputs @@ -100,20 +107,20 @@ fn main(input: VertexInput) -> VertexOutput { // @p5 endif // @p5 ifdef Vertex getWorldInputs - inputs.position = (uniforms.uModelMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uModelNormalMatrix * inputs.normal; + inputs.position = (model.uModelMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = model.uModelNormalMatrix * inputs.normal; inputs = HOOK_getWorldInputs(inputs); // @p5 endif // @p5 ifdef Vertex getWorldInputs // Already multiplied by the model matrix, just apply view - inputs.position = (uniforms.uViewMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uCameraNormalMatrix * inputs.normal; + inputs.position = (camera.uViewMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = model.uCameraNormalMatrix * inputs.normal; // @p5 endif // @p5 ifndef Vertex getWorldInputs // Apply both at once - inputs.position = (uniforms.uModelViewMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uNormalMatrix * inputs.normal; + inputs.position = (model.uModelViewMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = model.uNormalMatrix * inputs.normal; // @p5 endif // @p5 ifdef Vertex getCameraInputs @@ -125,7 +132,7 @@ fn main(input: VertexInput) -> VertexOutput { output.vNormal = normalize(inputs.normal); output.vColor = inputs.color; - output.Position = uniforms.uProjectionMatrix * vec4(inputs.position, 1.0); + output.Position = camera.uProjectionMatrix * vec4(inputs.position, 1.0); HOOK_afterVertex(); return output; @@ -141,15 +148,18 @@ struct FragmentInput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var material: MaterialUniforms; +@group(0) @binding(3) var lighting: LightingUniforms; -@group(0) @binding(1) var uSampler: texture_2d; -@group(0) @binding(2) var uSampler_sampler: sampler; +@group(0) @binding(4) var uSampler: texture_2d; +@group(0) @binding(5) var uSampler_sampler: sampler; -@group(0) @binding(3) var environmentMapDiffused: texture_2d; -@group(0) @binding(4) var environmentMapDiffused_sampler: sampler; -@group(0) @binding(5) var environmentMapSpecular: texture_2d; -@group(0) @binding(6) var environmentMapSpecular_sampler: sampler; +@group(0) @binding(6) var environmentMapDiffused: texture_2d; +@group(0) @binding(7) var environmentMapDiffused_sampler: sampler; +@group(0) @binding(8) var environmentMapSpecular: texture_2d; +@group(0) @binding(9) var environmentMapSpecular_sampler: sampler; struct ColorComponents { baseColor: vec3, @@ -212,7 +222,7 @@ fn mapTextureToNormal(v: vec3) -> vec2 { fn calculateImageDiffuse(vNormal: vec3, vViewPosition: vec3, metallic: f32) -> vec3 { // make 2 seperate builds let worldCameraPosition = vec3(0.0, 0.0, 0.0); // hardcoded world camera position - let worldNormal = normalize(vNormal * uniforms.uCameraRotation); + let worldNormal = normalize(vNormal * camera.uCameraRotation); let newTexCoord = mapTextureToNormal(worldNormal); let texture = textureSample(environmentMapDiffused, environmentMapDiffused_sampler, newTexCoord); // this is to make the darker sections more dark @@ -224,7 +234,7 @@ fn calculateImageSpecular(vNormal: vec3, vViewPosition: vec3, shinines let worldCameraPosition = vec3(0.0, 0.0, 0.0); let worldNormal = normalize(vNormal); let lightDirection = normalize(vViewPosition - worldCameraPosition); - let R = reflect(lightDirection, worldNormal) * uniforms.uCameraRotation; + let R = reflect(lightDirection, worldNormal) * camera.uCameraRotation; let newTexCoord = mapTextureToNormal(R); // In p5js the range of shininess is >= 1, @@ -273,7 +283,7 @@ fn singleLight( let specular = select( 0., phongSpecular(lightDir, viewDirection, normal, shininess) * specularIntensity, - uniforms.uSpecular == 1 + material.uSpecular == 1 ); return LightIntensityResult(diffuse, specular); } @@ -287,69 +297,69 @@ fn totalLight( var totalSpecular = vec3(0.0, 0.0, 0.0); var totalDiffuse = vec3(0.0, 0.0, 0.0); - if (uniforms.uUseLighting == 0) { + if (lighting.uUseLighting == 0) { return LightResult(vec3(1.0, 1.0, 1.0), totalSpecular); } let viewDirection = normalize(-modelPosition); for (var j = 0; j < 5; j++) { - if (j < uniforms.uDirectionalLightCount) { - let lightVector = (uniforms.uViewMatrix * vec4( - uniforms.uLightingDirection[j], + if (j < lighting.uDirectionalLightCount) { + let lightVector = (camera.uViewMatrix * vec4( + lighting.uLightingDirection[j], 0.0 )).xyz; - let lightColor = uniforms.uDirectionalDiffuseColors[j]; - let specularColor = uniforms.uDirectionalSpecularColors[j]; + let lightColor = lighting.uDirectionalDiffuseColors[j]; + let specularColor = lighting.uDirectionalSpecularColors[j]; let result = singleLight(viewDirection, normal, lightVector, shininess, metallic); totalDiffuse += result.diffuse * lightColor; totalSpecular += result.specular * specularColor; } - if (j < uniforms.uPointLightCount) { - let lightPosition = (uniforms.uViewMatrix * vec4( - uniforms.uPointLightLocation[j], + if (j < lighting.uPointLightCount) { + let lightPosition = (camera.uViewMatrix * vec4( + lighting.uPointLightLocation[j], 1.0 )).xyz; let lightVector = modelPosition - lightPosition; let lightDistance = length(lightVector); let lightFalloff = 1.0 / ( - uniforms.uConstantAttenuation + - lightDistance * uniforms.uLinearAttenuation + - lightDistance * lightDistance * uniforms.uQuadraticAttenuation + lighting.uConstantAttenuation + + lightDistance * lighting.uLinearAttenuation + + lightDistance * lightDistance * lighting.uQuadraticAttenuation ); - let lightColor = uniforms.uPointLightDiffuseColors[j] * lightFalloff; - let specularColor = uniforms.uPointLightSpecularColors[j] * lightFalloff; + let lightColor = lighting.uPointLightDiffuseColors[j] * lightFalloff; + let specularColor = lighting.uPointLightSpecularColors[j] * lightFalloff; let result = singleLight(viewDirection, normal, lightVector, shininess, metallic); totalDiffuse += result.diffuse * lightColor; totalSpecular += result.specular * specularColor; } - if (j < uniforms.uSpotLightCount) { - let lightPosition = (uniforms.uViewMatrix * vec4( - uniforms.uSpotLightLocation[j], + if (j < lighting.uSpotLightCount) { + let lightPosition = (camera.uViewMatrix * vec4( + lighting.uSpotLightLocation[j], 1.0 )).xyz; let lightVector = modelPosition - lightPosition; let lightDistance = length(lightVector); var lightFalloff = 1.0 / ( - uniforms.uConstantAttenuation + - lightDistance * uniforms.uLinearAttenuation + - lightDistance * lightDistance * uniforms.uQuadraticAttenuation + lighting.uConstantAttenuation + + lightDistance * lighting.uLinearAttenuation + + lightDistance * lightDistance * lighting.uQuadraticAttenuation ); - let lightDirection = (uniforms.uViewMatrix * vec4( - uniforms.uSpotLightDirection[j], + let lightDirection = (camera.uViewMatrix * vec4( + lighting.uSpotLightDirection[j], 0.0 )).xyz; let spotDot = dot(normalize(lightVector), normalize(lightDirection)); let spotFalloff = select( 0.0, - pow(spotDot, uniforms.uSpotLightConc[j]), - spotDot < uniforms.uSpotLightAngle[j] + pow(spotDot, lighting.uSpotLightConc[j]), + spotDot < lighting.uSpotLightAngle[j] ); lightFalloff *= spotFalloff; - let lightColor = uniforms.uSpotLightDiffuseColors[j]; - let specularColor = uniforms.uSpotLightSpecularColors[j]; + let lightColor = lighting.uSpotLightDiffuseColors[j]; + let specularColor = lighting.uSpotLightSpecularColors[j]; let result = singleLight(viewDirection, normal, lightVector, shininess, metallic); totalDiffuse += result.diffuse * lightColor; totalSpecular += result.specular * specularColor; @@ -357,7 +367,7 @@ fn totalLight( } // Image light contribution - if (uniforms.uUseImageLight != 0) { + if (lighting.uUseImageLight != 0) { totalDiffuse += calculateImageDiffuse(normal, modelPosition, metallic); totalSpecular += calculateImageSpecular(normal, modelPosition, shininess, metallic); } @@ -374,19 +384,19 @@ fn main(input: FragmentInput) -> @location(0) vec4 { let color = select( input.vColor, - textureSample(uSampler, uSampler_sampler, input.vTexCoord) * (uniforms.uTint/255.0), - uniforms.isTexture == 1 + textureSample(uSampler, uSampler_sampler, input.vTexCoord) * (material.uTint/255.0), + material.isTexture == 1 ); // TODO: check isTexture and apply tint var inputs = Inputs( normalize(input.vNormal), input.vTexCoord, - uniforms.uAmbientColor, - select(color.rgb, uniforms.uAmbientMatColor.rgb, uniforms.uHasSetAmbient == 1), - uniforms.uSpecularMatColor.rgb, - uniforms.uEmissiveMatColor.rgb, + material.uAmbientColor, + select(color.rgb, material.uAmbientMatColor.rgb, material.uHasSetAmbient == 1), + material.uSpecularMatColor.rgb, + material.uEmissiveMatColor.rgb, color, - uniforms.uShininess, - uniforms.uMetallic + material.uShininess, + material.uMetallic ); inputs = HOOK_getPixelInputs(inputs); diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index a2e432948c..22ae1d62d7 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -196,7 +196,10 @@ export const wgslBackend = { if (!strandsContext.renderer || !strandsContext.baseShader) return; // Get the next available binding index from the renderer - let bindingIndex = strandsContext.renderer.getNextBindingIndex(strandsContext.baseShader); + let bindingIndex = strandsContext.renderer.getNextBindingIndex({ + vert: strandsContext.baseShader.vertSrc(), + frag: strandsContext.baseShader.fragSrc(), + }); for (const {name, typeInfo} of strandsContext.uniforms) { if (typeInfo.baseType === 'sampler2D') { @@ -365,7 +368,7 @@ export const wgslBackend = { // Check if this is a uniform variable (but not a texture) const uniform = generationContext.strandsContext?.uniforms?.find(uniform => uniform.name === node.identifier); if (uniform && uniform.typeInfo.baseType !== 'sampler2D') { - return `uniforms.${node.identifier}`; + return `hooks.${node.identifier}`; } return node.identifier; From 6daedad9dc5e4ad40499153b9142c1afdbc624d8 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 5 Feb 2026 21:16:21 -0500 Subject: [PATCH 02/10] Try to optimize buffer writing --- src/webgl/p5.Shader.js | 4 + src/webgpu/p5.RendererWebGPU.js | 127 +++++++++++++++----------------- 2 files changed, 63 insertions(+), 68 deletions(-) diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 98556410fb..e9db76a25b 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -1046,8 +1046,12 @@ class Shader { return; } else { if (Array.isArray(data)) { + if (uniform._cachedData && this._renderer._arraysEqual(uniform._cachedData, data)) { + return; + } uniform._cachedData = data.slice(0); } else { + if (uniform._cachedData === data) return; uniform._cachedData = data; } } diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 38b2288660..e721f0b583 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -509,16 +509,16 @@ function rendererWebGPU(p5, fn) { const alignedSize = Math.ceil(rawSize / 16) * 16; // Create staging data arrays for this group - const groupData = new Float32Array(alignedSize / 4); - const groupDataView = new DataView(groupData.buffer); + // const groupData = new Float32Array(alignedSize / 4); + // const groupDataView = new DataView(groupData.buffer); - // Create GPU buffer pool for this group - const firstGPUBuffer = this.device.createBuffer({ - size: alignedSize, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - const firstData = new Float32Array(alignedSize / 4); - const firstDataView = new DataView(firstData.buffer); + // // Create GPU buffer pool for this group + // const firstGPUBuffer = this.device.createBuffer({ + // size: alignedSize, + // usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + // }); + // const firstData = new Float32Array(alignedSize / 4); + // const firstDataView = new DataView(firstData.buffer); shader._uniformBufferGroups.push({ binding: group.binding, @@ -526,12 +526,13 @@ function rendererWebGPU(p5, fn) { structType: group.structType, uniforms: groupUniforms, size: alignedSize, - bufferPool: [{ - buffer: firstGPUBuffer, - data: firstData, - dataView: firstDataView - }], - buffersInUse: [], + bufferPool: [], + // bufferPool: [{ + // buffer: firstGPUBuffer, + // data: firstData, + // dataView: firstDataView + // }], + buffersInUse: new Set(), currentBuffer: null, // For caching cachedData: null // For caching comparison }); @@ -1086,20 +1087,20 @@ function rendererWebGPU(p5, fn) { ////////////////////////////////////////////// // TODO(dave): delete? - _getUniformBufferFromPool(shader) { + _getUniformBufferFromPool(bufferGroup) { // Try to get a buffer from the pool - if (shader._uniformBufferPool.length > 0) { - const bufferInfo = shader._uniformBufferPool.pop(); - shader._uniformBuffersInUse.push(bufferInfo); + if (bufferGroup.bufferPool.length > 0) { + const bufferInfo = bufferGroup.bufferPool.pop(); + bufferGroup.buffersInUse.add(bufferInfo); return bufferInfo; } // No buffers available, create a new one const newBuffer = this.device.createBuffer({ - size: shader._uniformBufferSize, + size: bufferGroup.size, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); - const newData = new Float32Array(shader._uniformBufferSize / 4); + const newData = new Float32Array(bufferGroup.size / 4); const newDataView = new DataView(newData.buffer); const bufferInfo = { @@ -1108,7 +1109,7 @@ function rendererWebGPU(p5, fn) { dataView: newDataView }; - shader._uniformBuffersInUse.push(bufferInfo); + bufferGroup.buffersInUse.add(bufferInfo); return bufferInfo; } @@ -1122,16 +1123,11 @@ function rendererWebGPU(p5, fn) { _returnShaderBuffersToPool(shader) { if (shader._uniformBufferGroups) { for (const bufferGroup of shader._uniformBufferGroups) { - const seen = new Set(); - while (bufferGroup.buffersInUse.length > 0) { - const bufferInfo = bufferGroup.buffersInUse.pop(); - const prevSeen = seen.has(bufferInfo); - seen.add(bufferInfo); - - if (!prevSeen) { - bufferGroup.bufferPool.push(bufferInfo); - } + for (const bufferInfo of bufferGroup.buffersInUse.keys()) { + bufferGroup.bufferPool.push(bufferInfo); } + bufferGroup.currentBuffer = null; + bufferGroup.buffersInUse.clear(); } } } @@ -1224,11 +1220,14 @@ function rendererWebGPU(p5, fn) { this._returnVertexBuffersToPool(); // Destroy all retired buffers - for (const buffer of this._retiredBuffers) { - if (buffer && buffer.destroy) { - buffer.destroy(); + const retired = this._retiredBuffers; + this._postSubmitCallbacks.push(() => { + for (const buffer of retired) { + if (buffer && buffer.destroy) { + buffer.destroy(); + } } - } + }); this._retiredBuffers = []; if (this._frameState === FRAME_STATE.PROMOTED) { @@ -1329,23 +1328,10 @@ function rendererWebGPU(p5, fn) { // Reuse the cached buffer - no need to pack or write bufferInfo = bufferGroup.currentBuffer; // Still need to track it in buffersInUse for proper cleanup - bufferGroup.buffersInUse.push(bufferInfo); + bufferGroup.buffersInUse.add(bufferInfo); } else { // Data changed - get a buffer from the pool - if (bufferGroup.bufferPool.length > 0) { - bufferInfo = bufferGroup.bufferPool.pop(); - } else { - // Create a new buffer - const gpuBuffer = this.device.createBuffer({ - size: bufferGroup.size, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - bufferInfo = { - buffer: gpuBuffer, - data: new Float32Array(bufferGroup.size / 4), - dataView: new DataView(new Float32Array(bufferGroup.size / 4).buffer) - }; - } + bufferInfo = this._getUniformBufferFromPool(bufferGroup); // Pack and write the data this._packUniformGroup(currentShader, bufferGroup.uniforms, bufferInfo); @@ -1357,16 +1343,15 @@ function rendererWebGPU(p5, fn) { bufferInfo.data.byteLength ); + for (const uniform of bufferGroup.uniforms) { + const fullUniform = currentShader.uniforms[uniform.name]; + if (fullUniform) { + fullUniform.dirty = false; + } + } + // Cache this buffer and data for next frame - bufferGroup.buffersInUse.push(bufferInfo); bufferGroup.currentBuffer = bufferInfo; - - // Store cached data for comparison - if (!bufferGroup.cachedData) { - bufferGroup.cachedData = new Float32Array(bufferGroup.size / 4); - bufferGroup.cachedDataView = new DataView(bufferGroup.cachedData.buffer); - } - bufferGroup.cachedData.set(bufferInfo.data); } uniformBuffersForBinding.push({ @@ -1460,6 +1445,10 @@ function rendererWebGPU(p5, fn) { // Mark that we have pending draws that need submission this._hasPendingDraws = true; + + if (this._pendingCommandEncoders.length > 50) { + this.flushDraw(); + } } ////////////////////////////////////////////// @@ -1502,31 +1491,31 @@ function rendererWebGPU(p5, fn) { } _hasGroupDataChanged(shader, bufferGroup) { - return true - // Check if any uniform in this group has changed since last pack - if (!bufferGroup.cachedData) { - return true; // First time - } + // First time + if (!bufferGroup.currentBuffer) return true; + const cachedData = bufferGroup.currentBuffer.data; + const cachedDataView = bufferGroup.currentBuffer.dataView; for (const uniform of bufferGroup.uniforms) { const fullUniform = shader.uniforms[uniform.name]; if (!fullUniform || fullUniform.isSampler) continue; + if (fullUniform.dirty) return true; + // continue; // Compare typed arrays bytewise const currentData = fullUniform._cachedData; const cachedOffset = fullUniform.offset; - const size = fullUniform.size; if (fullUniform.baseType === 'u32' || fullUniform.baseType === 'i32') { if (fullUniform.size === 4) { // Single value - if (bufferGroup.cachedDataView.getUint32(cachedOffset, true) !== currentData) { + if (cachedDataView.getUint32(cachedOffset, true) !== currentData) { return true; } } else { // Array for (let i = 0; i < currentData.length; i++) { - if (bufferGroup.cachedDataView.getUint32(cachedOffset + i * 4, true) !== currentData[i]) { + if (cachedDataView.getUint32(cachedOffset + i * 4, true) !== currentData[i]) { return true; } } @@ -1534,14 +1523,14 @@ function rendererWebGPU(p5, fn) { } else { if (fullUniform.size === 4) { // Single float - if (bufferGroup.cachedData[cachedOffset / 4] !== currentData) { + if (cachedData[cachedOffset / 4] !== currentData) { return true; } } else if (currentData !== undefined) { // Float array const floatOffset = cachedOffset / 4; for (let i = 0; i < currentData.length; i++) { - if (bufferGroup.cachedData[floatOffset + i] !== currentData[i]) { + if (cachedData[floatOffset + i] !== currentData[i]) { return true; } } @@ -1834,6 +1823,8 @@ function rendererWebGPU(p5, fn) { if (uniform.isSampler) { uniform.texture = data instanceof Texture ? data : this.getTexture(data); + } else { + uniform.dirty = true; } } From b63af7150beddbe6decbc67de1b50f70a294ca00 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 5 Feb 2026 21:32:41 -0500 Subject: [PATCH 03/10] Fix comparisons when checking for different buffer values --- src/webgpu/p5.RendererWebGPU.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index e721f0b583..392947c400 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1126,7 +1126,7 @@ function rendererWebGPU(p5, fn) { for (const bufferInfo of bufferGroup.buffersInUse.keys()) { bufferGroup.bufferPool.push(bufferInfo); } - bufferGroup.currentBuffer = null; + // bufferGroup.currentBuffer = null; bufferGroup.buffersInUse.clear(); } } @@ -1506,16 +1506,17 @@ function rendererWebGPU(p5, fn) { const currentData = fullUniform._cachedData; const cachedOffset = fullUniform.offset; + // Note: intentionally using == instead of === below if (fullUniform.baseType === 'u32' || fullUniform.baseType === 'i32') { if (fullUniform.size === 4) { // Single value - if (cachedDataView.getUint32(cachedOffset, true) !== currentData) { + if (cachedDataView.getUint32(cachedOffset, true) != currentData) { return true; } } else { // Array for (let i = 0; i < currentData.length; i++) { - if (cachedDataView.getUint32(cachedOffset + i * 4, true) !== currentData[i]) { + if (cachedDataView.getUint32(cachedOffset + i * 4, true) != currentData[i]) { return true; } } @@ -1523,14 +1524,14 @@ function rendererWebGPU(p5, fn) { } else { if (fullUniform.size === 4) { // Single float - if (cachedData[cachedOffset / 4] !== currentData) { + if (cachedData[cachedOffset / 4] != currentData) { return true; } } else if (currentData !== undefined) { // Float array const floatOffset = cachedOffset / 4; for (let i = 0; i < currentData.length; i++) { - if (cachedData[floatOffset + i] !== currentData[i]) { + if (cachedData[floatOffset + i] != currentData[i]) { return true; } } From e53d5d114111755ab3ffe7c8287e12a6d8be6ee8 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 5 Feb 2026 23:36:28 -0500 Subject: [PATCH 04/10] Reuse the same render pass --- src/webgpu/p5.RendererWebGPU.js | 124 +++++++++++++++++++------------- 1 file changed, 73 insertions(+), 51 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 392947c400..b31b05e447 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -37,7 +37,8 @@ function rendererWebGPU(p5, fn) { constructor(pInst, w, h, isMainCanvas, elt) { super(pInst, w, h, isMainCanvas, elt) - this.renderPass = {}; + this.activeRenderPass = null; + this.activeRenderPassEncoder = null; this.samplers = new Map(); @@ -204,6 +205,66 @@ function rendererWebGPU(p5, fn) { return this.currentCanvasColorTextureView; } + _beginActiveRenderPass() { + if (this.activeRenderPass) return; + + // Use framebuffer texture if active, otherwise use canvas texture + const activeFramebuffer = this.activeFramebuffer(); + + const colorAttachment = { + view: activeFramebuffer + ? (activeFramebuffer.aaColorTexture + ? activeFramebuffer.aaColorTextureView + : activeFramebuffer.colorTextureView) + : this._getCanvasColorTextureView(), + loadOp: "load", + storeOp: "store", + // If using multisampled texture, resolve to non-multisampled texture + resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture + ? activeFramebuffer.colorTextureView + : undefined, + }; + + // Use framebuffer depth texture if active, otherwise use canvas depth texture + const depthTextureView = activeFramebuffer + ? (activeFramebuffer.aaDepthTexture + ? activeFramebuffer.aaDepthTextureView + : activeFramebuffer.depthTextureView) + : this.depthTextureView; + const renderPassDescriptor = { + colorAttachments: [colorAttachment], + depthStencilAttachment: depthTextureView + ? { + view: depthTextureView, + depthLoadOp: "load", + depthStoreOp: "store", + depthClearValue: 1.0, + stencilLoadOp: "load", + stencilStoreOp: "store", + depthReadOnly: false, + stencilReadOnly: false, + } + : undefined, + }; + const commandEncoder = this.device.createCommandEncoder(); + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + this.activeRenderPassEncoder = commandEncoder; + this.activeRenderPass = passEncoder; + } + + _finishActiveRenderPass() { + if (!this.activeRenderPass) return; + + const commandEncoder = this.activeRenderPassEncoder; + const passEncoder = this.activeRenderPass; + passEncoder.end(); + + // Store the command encoder for later submission + this._pendingCommandEncoders.push(commandEncoder.finish()); + this.activeRenderPassEncoder = null; + this.activeRenderPass = null; + } + clear(...args) { const _r = args[0] || 0; const _g = args[1] || 0; @@ -215,6 +276,8 @@ function rendererWebGPU(p5, fn) { this._frameState = FRAME_STATE.UNPROMOTED; } + this._finishActiveRenderPass(); + const commandEncoder = this.device.createCommandEncoder(); // Use framebuffer texture if active, otherwise use canvas texture @@ -269,6 +332,7 @@ function rendererWebGPU(p5, fn) { * occlude anything subsequently drawn. */ clearDepth(depth = 1) { + this._finishActiveRenderPass(); const commandEncoder = this.device.createCommandEncoder(); // Use framebuffer texture if active, otherwise use canvas texture @@ -359,7 +423,6 @@ function rendererWebGPU(p5, fn) { const loc = attr.location; if (!this.registerEnabled.has(loc)) { // TODO - // this.renderPass.setVertexBuffer(loc, buffer); this.registerEnabled.add(loc); } } @@ -833,6 +896,7 @@ function rendererWebGPU(p5, fn) { } _resetBuffersBeforeDraw() { + this._finishActiveRenderPass(); // Set state to PENDING - we'll decide on first draw this._frameState = FRAME_STATE.PENDING; @@ -1133,6 +1197,7 @@ function rendererWebGPU(p5, fn) { } flushDraw() { + this._finishActiveRenderPass(); // Only submit if we actually had any draws if (this._hasPendingDraws) { // Create a copy of pending command encoders @@ -1255,48 +1320,8 @@ function rendererWebGPU(p5, fn) { this._promoteToFramebufferWithoutCopy(); } - const commandEncoder = this.device.createCommandEncoder(); - - // Use framebuffer texture if active, otherwise use canvas texture - const activeFramebuffer = this.activeFramebuffer(); - - const colorAttachment = { - view: activeFramebuffer - ? (activeFramebuffer.aaColorTexture - ? activeFramebuffer.aaColorTextureView - : activeFramebuffer.colorTextureView) - : this._getCanvasColorTextureView(), - loadOp: "load", - storeOp: "store", - // If using multisampled texture, resolve to non-multisampled texture - resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture - ? activeFramebuffer.colorTextureView - : undefined, - }; - - // Use framebuffer depth texture if active, otherwise use canvas depth texture - const depthTextureView = activeFramebuffer - ? (activeFramebuffer.aaDepthTexture - ? activeFramebuffer.aaDepthTextureView - : activeFramebuffer.depthTextureView) - : this.depthTextureView; - const renderPassDescriptor = { - colorAttachments: [colorAttachment], - depthStencilAttachment: depthTextureView - ? { - view: depthTextureView, - depthLoadOp: "load", - depthStoreOp: "store", - depthClearValue: 1.0, - stencilLoadOp: "load", - stencilStoreOp: "store", - depthReadOnly: false, - stencilReadOnly: false, - } - : undefined, - }; - - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + this._beginActiveRenderPass(); + const passEncoder = this.activeRenderPass; const currentShader = this._curShader; passEncoder.setPipeline(currentShader.getPipeline(this._shaderOptions({ mode }))); @@ -1438,17 +1463,12 @@ function rendererWebGPU(p5, fn) { passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0); } - passEncoder.end(); - - // Store the command encoder for later submission - this._pendingCommandEncoders.push(commandEncoder.finish()); - // Mark that we have pending draws that need submission this._hasPendingDraws = true; - if (this._pendingCommandEncoders.length > 50) { + /*if (this._pendingCommandEncoders.length > 50) { this.flushDraw(); - } + }*/ } ////////////////////////////////////////////// @@ -2063,6 +2083,7 @@ function rendererWebGPU(p5, fn) { } _clearClipBuffer() { + this._finishActiveRenderPass(); const commandEncoder = this.device.createCommandEncoder(); const activeFramebuffer = this.activeFramebuffer(); @@ -2643,6 +2664,7 @@ ${hookUniformFields}} } _clearFramebufferTextures(framebuffer) { + this._finishActiveRenderPass(); const commandEncoder = this.device.createCommandEncoder(); // Clear the color texture (and multisampled texture if it exists) From b17e40fdfd6fe947df0aceb980bca54584aa9a5c Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 6 Feb 2026 16:34:09 -0500 Subject: [PATCH 05/10] Move what group the normal matrix is in, dedup uniform --- src/core/p5.Renderer3D.js | 4 ---- src/webgl/shaders/lighting.glsl | 5 ++--- src/webgpu/shaders/color.js | 4 ++-- src/webgpu/shaders/material.js | 9 ++++----- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 84cc88e7a7..b1ae468930 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1456,10 +1456,6 @@ export class Renderer3D extends Renderer { this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); shader.setUniform("uCameraNormalMatrix", this.scratchMat3.mat3); } - if (shader.uniforms.uCameraRotation) { - this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); - shader.setUniform("uCameraRotation", this.scratchMat3.mat3); - } shader.setUniform("uViewport", this._viewport); } diff --git a/src/webgl/shaders/lighting.glsl b/src/webgl/shaders/lighting.glsl index 85a4c79684..69bc277d38 100644 --- a/src/webgl/shaders/lighting.glsl +++ b/src/webgl/shaders/lighting.glsl @@ -7,7 +7,6 @@ uniform mat4 uViewMatrix; uniform bool uUseLighting; -uniform mat3 uCameraRotation; uniform int uDirectionalLightCount; uniform vec3 uLightingDirection[5]; uniform vec3 uDirectionalDiffuseColors[5]; @@ -108,7 +107,7 @@ vec2 mapTextureToNormal( vec3 v ){ vec3 calculateImageDiffuse(vec3 vNormal, vec3 vViewPosition, float metallic){ // make 2 seperate builds vec3 worldCameraPosition = vec3(0.0, 0.0, 0.0); // hardcoded world camera position - vec3 worldNormal = normalize(vNormal * uCameraRotation); + vec3 worldNormal = normalize(vNormal * uCameraNormalMatrix); vec2 newTexCoor = mapTextureToNormal( worldNormal ); vec4 texture = TEXTURE( environmentMapDiffused, newTexCoor ); // this is to make the darker sections more dark @@ -120,7 +119,7 @@ vec3 calculateImageSpecular(vec3 vNormal, vec3 vViewPosition, float shininess, f vec3 worldCameraPosition = vec3(0.0, 0.0, 0.0); vec3 worldNormal = normalize(vNormal); vec3 lightDirection = normalize( vViewPosition - worldCameraPosition ); - vec3 R = reflect(lightDirection, worldNormal) * uCameraRotation; + vec3 R = reflect(lightDirection, worldNormal) * uCameraNormalMatrix; vec2 newTexCoor = mapTextureToNormal( R ); #ifdef WEBGL2 // In p5js the range of shininess is >= 1, diff --git a/src/webgpu/shaders/color.js b/src/webgpu/shaders/color.js index b86be812d4..5ef30243f4 100644 --- a/src/webgpu/shaders/color.js +++ b/src/webgpu/shaders/color.js @@ -5,6 +5,7 @@ struct CameraUniforms { // @p5 ifdef Vertex getWorldInputs uViewMatrix: mat4x4, // @p5 endif + uCameraNormalMatrix: mat3x3, } // Group 2: Model Transform @@ -12,7 +13,6 @@ struct ModelUniforms { // @p5 ifdef Vertex getWorldInputs uModelMatrix: mat4x4, uModelNormalMatrix: mat3x3, - uCameraNormalMatrix: mat3x3, // @p5 endif // @p5 ifndef Vertex getWorldInputs uModelViewMatrix: mat4x4, @@ -80,7 +80,7 @@ fn main(input: VertexInput) -> VertexOutput { // @p5 ifdef Vertex getWorldInputs // Already multiplied by the model matrix, just apply view inputs.position = (camera.uViewMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = model.uCameraNormalMatrix * inputs.normal; + inputs.normal = camera.uCameraNormalMatrix * inputs.normal; // @p5 endif // @p5 ifndef Vertex getWorldInputs // Apply both at once diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index c2218e4012..50e65aedce 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -3,7 +3,7 @@ const uniforms = ` struct CameraUniforms { uViewMatrix: mat4x4, uProjectionMatrix: mat4x4, - uCameraRotation: mat3x3, + uCameraNormalMatrix: mat3x3, } // Group 2: Model Transform @@ -11,7 +11,6 @@ struct ModelUniforms { // @p5 ifdef Vertex getWorldInputs uModelMatrix: mat4x4, uModelNormalMatrix: mat3x3, - uCameraNormalMatrix: mat3x3, // @p5 endif // @p5 ifndef Vertex getWorldInputs uModelViewMatrix: mat4x4, @@ -115,7 +114,7 @@ fn main(input: VertexInput) -> VertexOutput { // @p5 ifdef Vertex getWorldInputs // Already multiplied by the model matrix, just apply view inputs.position = (camera.uViewMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = model.uCameraNormalMatrix * inputs.normal; + inputs.normal = camera.uCameraNormalMatrix * inputs.normal; // @p5 endif // @p5 ifndef Vertex getWorldInputs // Apply both at once @@ -222,7 +221,7 @@ fn mapTextureToNormal(v: vec3) -> vec2 { fn calculateImageDiffuse(vNormal: vec3, vViewPosition: vec3, metallic: f32) -> vec3 { // make 2 seperate builds let worldCameraPosition = vec3(0.0, 0.0, 0.0); // hardcoded world camera position - let worldNormal = normalize(vNormal * camera.uCameraRotation); + let worldNormal = normalize(vNormal * camera.uCameraNormalMatrix); let newTexCoord = mapTextureToNormal(worldNormal); let texture = textureSample(environmentMapDiffused, environmentMapDiffused_sampler, newTexCoord); // this is to make the darker sections more dark @@ -234,7 +233,7 @@ fn calculateImageSpecular(vNormal: vec3, vViewPosition: vec3, shinines let worldCameraPosition = vec3(0.0, 0.0, 0.0); let worldNormal = normalize(vNormal); let lightDirection = normalize(vViewPosition - worldCameraPosition); - let R = reflect(lightDirection, worldNormal) * camera.uCameraRotation; + let R = reflect(lightDirection, worldNormal) * camera.uCameraNormalMatrix; let newTexCoord = mapTextureToNormal(R); // In p5js the range of shininess is >= 1, From bbfc6d3e10ec43f0d4c6419eba0c20887a7aa5ba Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 6 Feb 2026 16:34:21 -0500 Subject: [PATCH 06/10] Experiment: use separate buffers for the next frame --- src/webgpu/p5.RendererWebGPU.js | 41 +++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index b31b05e447..ade6c7af8d 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -58,6 +58,9 @@ function rendererWebGPU(p5, fn) { // Registry to track geometries with buffer pools this._geometriesWithPools = []; + // Reusable Map for uniform buffer bindings to avoid GC + this._uniformBuffersForBinding = new Map(); + // Flag to track if any draws have happened that need queue submission this._hasPendingDraws = false; this._pendingCommandEncoders = []; @@ -584,12 +587,14 @@ function rendererWebGPU(p5, fn) { // const firstDataView = new DataView(firstData.buffer); shader._uniformBufferGroups.push({ + group: group.group, binding: group.binding, varName: group.varName, structType: group.structType, uniforms: groupUniforms, size: alignedSize, bufferPool: [], + nextBufferPool: [], // bufferPool: [{ // buffer: firstGPUBuffer, // data: firstData, @@ -1187,8 +1192,11 @@ function rendererWebGPU(p5, fn) { _returnShaderBuffersToPool(shader) { if (shader._uniformBufferGroups) { for (const bufferGroup of shader._uniformBufferGroups) { + while (bufferGroup.nextBufferPool.length > 0) { + bufferGroup.bufferPool.push(bufferGroup.nextBufferPool.pop()); + } for (const bufferInfo of bufferGroup.buffersInUse.keys()) { - bufferGroup.bufferPool.push(bufferInfo); + bufferGroup.nextBufferPool.push(bufferInfo); } // bufferGroup.currentBuffer = null; bufferGroup.buffersInUse.clear(); @@ -1343,7 +1351,8 @@ function rendererWebGPU(p5, fn) { passEncoder.setVertexBuffer(location, gpuBuffer, 0); } - const uniformBuffersForBinding = []; + // Clear and reuse the map to avoid GC + this._uniformBuffersForBinding.clear(); for (const bufferGroup of currentShader._uniformBufferGroups) { let bufferInfo; @@ -1368,38 +1377,37 @@ function rendererWebGPU(p5, fn) { bufferInfo.data.byteLength ); - for (const uniform of bufferGroup.uniforms) { + currentShader.buffersDirty = currentShader.buffersDirty || {}; + currentShader.buffersDirty[bufferGroup.group + ',' + bufferGroup.binding] = false; + /*for (const uniform of bufferGroup.uniforms) { const fullUniform = currentShader.uniforms[uniform.name]; if (fullUniform) { fullUniform.dirty = false; } - } + }*/ // Cache this buffer and data for next frame bufferGroup.currentBuffer = bufferInfo; } - uniformBuffersForBinding.push({ - binding: bufferGroup.binding, - buffer: bufferInfo.buffer - }); + this._uniformBuffersForBinding.set(bufferGroup.binding, bufferInfo.buffer); } // Bind sampler/texture uniforms and uniform buffers for (const [group, entries] of currentShader._groupEntries) { const bgEntries = entries.map(entry => { // Check if this is a uniform buffer binding - const uniformBuffer = uniformBuffersForBinding.find(ub => ub.binding === entry.binding); + const uniformBuffer = this._uniformBuffersForBinding.get(entry.binding); if (uniformBuffer) { return { binding: entry.binding, - resource: { buffer: uniformBuffer.buffer }, + resource: { buffer: uniformBuffer }, }; } // This must be a texture/sampler entry if (!entry.uniform) { - console.error('Entry missing uniform field:', entry, 'uniformBuffersForBinding:', uniformBuffersForBinding); + console.error('Entry missing uniform field:', entry, 'uniformBuffersForBinding:', this._uniformBuffersForBinding); throw new Error( `Bind group entry at binding ${entry.binding} has no uniform field and is not a uniform buffer!` ); @@ -1513,6 +1521,7 @@ function rendererWebGPU(p5, fn) { _hasGroupDataChanged(shader, bufferGroup) { // First time if (!bufferGroup.currentBuffer) return true; + return shader.buffersDirty?.[bufferGroup.group + ',' + bufferGroup.binding]; const cachedData = bufferGroup.currentBuffer.data; const cachedDataView = bufferGroup.currentBuffer.dataView; @@ -1732,15 +1741,16 @@ function rendererWebGPU(p5, fn) { // Parse all uniform struct bindings in group 0 // Each binding represents a logical group of uniforms const uniformGroups = []; - const uniformVarRegex = /@group\(0\)\s+@binding\((\d+)\)\s+var\s+(\w+)\s*:\s*(\w+);/g; + const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var\s+(\w+)\s*:\s*(\w+);/g; let match; while ((match = uniformVarRegex.exec(shader.vertSrc())) !== null) { - const [_, binding, varName, structType] = match; + const [_, groupNum, binding, varName, structType] = match; const bindingIndex = parseInt(binding); const uniforms = this._parseStruct(shader.vertSrc(), structType); uniformGroups.push({ + group: parseInt(groupNum), binding: bindingIndex, varName, structType, @@ -1840,12 +1850,13 @@ function rendererWebGPU(p5, fn) { return maxBindingIndex + 1; } - updateUniformValue(_shader, uniform, data) { + updateUniformValue(shader, uniform, data) { if (uniform.isSampler) { uniform.texture = data instanceof Texture ? data : this.getTexture(data); } else { - uniform.dirty = true; + shader.buffersDirty = shader.buffersDirty || {}; + shader.buffersDirty[uniform.group + ',' + uniform.binding] = true; } } From 0120c42795243f9e16f9ac632c1bfc284d29ce1b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 7 Feb 2026 10:58:50 -0500 Subject: [PATCH 07/10] Try using dynamic mapping --- preview/index.html | 4 +- src/webgpu/p5.RendererWebGPU.js | 209 ++++++++++++++++++++------------ 2 files changed, 134 insertions(+), 79 deletions(-) diff --git a/preview/index.html b/preview/index.html index ed76b913f5..6c75a9afbe 100644 --- a/preview/index.html +++ b/preview/index.html @@ -42,14 +42,14 @@ instance = p.buildGeometry(() => p.sphere(5)); - redFilter = p.baseFilterShader().modify(() => { + /*redFilter = p.baseFilterShader().modify(() => { p.getColor((inputs, canvasContent) => { let col = p.getTexture(canvasContent, inputs.texCoord); col.g = col.r; col.b = col.r; return col; }) - }, { p }) + }, { p })*/ tex = p.createImage(100, 100); tex.loadPixels(); diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index ade6c7af8d..695bd59942 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -42,6 +42,11 @@ function rendererWebGPU(p5, fn) { this.samplers = new Map(); + this.uniformBufferAlignment = 256; + this.uniformBufferPool = []; + this.activeUniformBuffers = []; + this.currentUniformBuffer = undefined; + // Cache for current frame's canvas texture view this.currentCanvasColorTexture = null; this.currentCanvasColorTextureView = null; @@ -600,6 +605,7 @@ function rendererWebGPU(p5, fn) { // data: firstData, // dataView: firstDataView // }], + dynamic: groupUniforms.some(u => u.name.startsWith('uModel')), buffersInUse: new Set(), currentBuffer: null, // For caching cachedData: null // For caching comparison @@ -616,9 +622,10 @@ function rendererWebGPU(p5, fn) { const group0Entries = []; for (const bufferGroup of shader._uniformBufferGroups) { group0Entries.push({ + bufferGroup, binding: bufferGroup.binding, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, - buffer: { type: 'uniform' }, + buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic }, }); } group0Entries.sort((a, b) => a.binding - b.binding); @@ -1155,7 +1162,6 @@ function rendererWebGPU(p5, fn) { // Uniform buffer pool management ////////////////////////////////////////////// - // TODO(dave): delete? _getUniformBufferFromPool(bufferGroup) { // Try to get a buffer from the pool if (bufferGroup.bufferPool.length > 0) { @@ -1171,7 +1177,6 @@ function rendererWebGPU(p5, fn) { }); const newData = new Float32Array(bufferGroup.size / 4); const newDataView = new DataView(newData.buffer); - const bufferInfo = { buffer: newBuffer, data: newData, @@ -1182,6 +1187,39 @@ function rendererWebGPU(p5, fn) { return bufferInfo; } + _getDynamicUniformBufferFromPool(bufferGroup) { + let buffer; + if (this.currentUniformBuffer && this.currentUniformBuffer.offset + bufferGroup.size < this.currentUniformBuffer.size) { + buffer = this.currentUniformBuffer; + } else if (this.uniformBufferPool.length > 0) { + buffer = this.uniformBufferPool.pop(); + this.activeUniformBuffers.push(buffer); + } else { + const size = 256 * 10 * 4; + buffer = { + dynamic: true, + lastOffset: 0, + offset: 0, + size, + buffer: this.device.createBuffer({ + size: size, + usage: GPUBufferUsage.UNIFORM, + mappedAtCreation: true, + }), + } + this.activeUniformBuffers.push(buffer); + } + + if (!buffer.data) { + buffer.data = new Float32Array(buffer.buffer.getMappedRange()); + buffer.dataView = new DataView(buffer.data.buffer); + } + + this.currentUniformBuffer = buffer; + + return buffer; + } + _returnUniformBuffersToPool() { // Return all used buffers back to their pools for all registered shaders for (const shader of this._shadersWithPools) { @@ -1213,9 +1251,16 @@ function rendererWebGPU(p5, fn) { this._pendingCommandEncoders = []; this._hasPendingDraws = false; + for (const bufferInfo of this.activeUniformBuffers) { + bufferInfo.buffer.unmap(); + } + // Submit the commands this.queue.submit(commandsToSubmit); + this.activeUniformBuffers = []; + this.currentUniformBuffer = undefined; + // Execute post-submit callbacks after GPU work completes if (this._postSubmitCallbacks.length > 0) { const callbacks = this._postSubmitCallbacks; @@ -1355,83 +1400,83 @@ function rendererWebGPU(p5, fn) { this._uniformBuffersForBinding.clear(); for (const bufferGroup of currentShader._uniformBufferGroups) { - let bufferInfo; - const dataChanged = this._hasGroupDataChanged(currentShader, bufferGroup); - - if (!dataChanged && bufferGroup.currentBuffer) { - // Reuse the cached buffer - no need to pack or write - bufferInfo = bufferGroup.currentBuffer; - // Still need to track it in buffersInUse for proper cleanup - bufferGroup.buffersInUse.add(bufferInfo); + if (bufferGroup.dynamic) { + // Bind uniforms - get a buffer from the pool + const uniformBufferInfo = this._getDynamicUniformBufferFromPool(bufferGroup); + this._packUniformGroup(currentShader, bufferGroup.uniforms, uniformBufferInfo); + // this._packUniforms(currentShader, uniformBufferInfo); + uniformBufferInfo.lastOffset = uniformBufferInfo.offset; + uniformBufferInfo.offset += Math.ceil(bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment; + /*this.device.queue.writeBuffer( + uniformBufferInfo.buffer, + 0, + uniformBufferInfo.data.buffer, + uniformBufferInfo.data.byteOffset, + uniformBufferInfo.data.byteLength + );*/ + // Make a shallow copy so that we keep track of the last offset for this uniform + this._uniformBuffersForBinding.set(bufferGroup.binding, { ...uniformBufferInfo }); } else { - // Data changed - get a buffer from the pool - bufferInfo = this._getUniformBufferFromPool(bufferGroup); + let bufferInfo; + const dataChanged = this._hasGroupDataChanged(currentShader, bufferGroup); + + if (!dataChanged && bufferGroup.currentBuffer) { + // Reuse the cached buffer - no need to pack or write + bufferInfo = bufferGroup.currentBuffer; + // Still need to track it in buffersInUse for proper cleanup + bufferGroup.buffersInUse.add(bufferInfo); + } else { + // Data changed - get a buffer from the pool + bufferInfo = this._getUniformBufferFromPool(bufferGroup); - // Pack and write the data - this._packUniformGroup(currentShader, bufferGroup.uniforms, bufferInfo); - this.device.queue.writeBuffer( - bufferInfo.buffer, - 0, - bufferInfo.data.buffer, - bufferInfo.data.byteOffset, - bufferInfo.data.byteLength - ); + // Pack and write the data + this._packUniformGroup(currentShader, bufferGroup.uniforms, bufferInfo); + this.device.queue.writeBuffer( + bufferInfo.buffer, + 0, + bufferInfo.data.buffer, + bufferInfo.data.byteOffset, + bufferInfo.data.byteLength + ); - currentShader.buffersDirty = currentShader.buffersDirty || {}; - currentShader.buffersDirty[bufferGroup.group + ',' + bufferGroup.binding] = false; - /*for (const uniform of bufferGroup.uniforms) { - const fullUniform = currentShader.uniforms[uniform.name]; - if (fullUniform) { - fullUniform.dirty = false; - } - }*/ + currentShader.buffersDirty = currentShader.buffersDirty || {}; + currentShader.buffersDirty[bufferGroup.group + ',' + bufferGroup.binding] = false; + /*for (const uniform of bufferGroup.uniforms) { + const fullUniform = currentShader.uniforms[uniform.name]; + if (fullUniform) { + fullUniform.dirty = false; + } + }*/ - // Cache this buffer and data for next frame - bufferGroup.currentBuffer = bufferInfo; - } + // Cache this buffer and data for next frame + bufferGroup.currentBuffer = bufferInfo; + } - this._uniformBuffersForBinding.set(bufferGroup.binding, bufferInfo.buffer); + this._uniformBuffersForBinding.set(bufferGroup.binding, bufferInfo); + } } // Bind sampler/texture uniforms and uniform buffers for (const [group, entries] of currentShader._groupEntries) { const bgEntries = entries.map(entry => { // Check if this is a uniform buffer binding - const uniformBuffer = this._uniformBuffersForBinding.get(entry.binding); - if (uniformBuffer) { + const uniformBufferInfo = this._uniformBuffersForBinding.get(entry.binding); + if (uniformBufferInfo) { return { binding: entry.binding, - resource: { buffer: uniformBuffer }, + resource: entry.bufferGroup.dynamic + ? { + buffer: uniformBufferInfo.buffer, + offset: 0, + size: Math.ceil(entry.bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment, + } + : { buffer: uniformBufferInfo.buffer }, }; } - // This must be a texture/sampler entry - if (!entry.uniform) { - console.error('Entry missing uniform field:', entry, 'uniformBuffersForBinding:', this._uniformBuffersForBinding); - throw new Error( - `Bind group entry at binding ${entry.binding} has no uniform field and is not a uniform buffer!` - ); - } - - if (!entry.uniform.isSampler) { - console.error('Non-sampler uniform not handled:', entry.uniform); - throw new Error( - 'All non-texture/sampler uniforms should be in the uniform struct!' - ); - } - - const texture = entry.uniform.type === 'sampler' - ? entry.uniform.textureSource?.texture - : entry.uniform.texture; - - if (!texture && entry.uniform.type !== 'sampler') { - console.warn(`Texture uniform ${entry.uniform.name} at binding ${entry.binding} has no texture! Using empty texture.`, { - uniform: entry.uniform, - type: entry.uniform.type, - hasTexture: !!entry.uniform.texture, - texture: entry.uniform.texture - }); - } + // const texture = entry.uniform.type === 'sampler' + // ? entry.uniform.textureSource?.texture + // : entry.uniform.texture; return { binding: entry.binding, @@ -1446,7 +1491,13 @@ function rendererWebGPU(p5, fn) { layout, entries: bgEntries, }); - passEncoder.setBindGroup(group, bindGroup); + passEncoder.setBindGroup( + group, + bindGroup, + entries.map(e => this._uniformBuffersForBinding.get(e.binding)) + .filter(b => b?.dynamic) + .map(b => b.lastOffset) + ); } if (currentShader.shaderType === "fill") { @@ -1488,32 +1539,33 @@ function rendererWebGPU(p5, fn) { const data = bufferInfo.data; const dataView = bufferInfo.dataView; + const offset = bufferInfo.offset || 0; for (const uniform of groupUniforms) { const fullUniform = shader.uniforms[uniform.name]; if (!fullUniform || fullUniform.isSampler) continue; if (fullUniform.baseType === 'u32') { if (fullUniform.size === 4) { - dataView.setUint32(fullUniform.offset, fullUniform._cachedData, true); + dataView.setUint32(offset + fullUniform.offset, fullUniform._cachedData, true); } else { const uniformData = fullUniform._cachedData; for (let i = 0; i < uniformData.length; i++) { - dataView.setUint32(fullUniform.offset + i * 4, uniformData[i], true); + dataView.setUint32(offset + fullUniform.offset + i * 4, uniformData[i], true); } } } else if (fullUniform.baseType === 'i32') { if (fullUniform.size === 4) { - dataView.setInt32(fullUniform.offset, fullUniform._cachedData, true); + dataView.setInt32(offset + fullUniform.offset, fullUniform._cachedData, true); } else { const uniformData = fullUniform._cachedData; for (let i = 0; i < uniformData.length; i++) { - dataView.setInt32(fullUniform.offset + i * 4, uniformData[i], true); + dataView.setInt32(offset + fullUniform.offset + i * 4, uniformData[i], true); } } } else if (fullUniform.size === 4) { - data.set([fullUniform._cachedData], fullUniform.offset / 4); + data.set([fullUniform._cachedData], (offset + fullUniform.offset) / 4); } else if (fullUniform._cachedData !== undefined) { - data.set(fullUniform._cachedData, fullUniform.offset / 4); + data.set(fullUniform._cachedData, (offset + fullUniform.offset) / 4); } } } @@ -1571,10 +1623,12 @@ function rendererWebGPU(p5, fn) { return false; // No changes detected } + // TODO: delete _packUniforms(shader, bufferInfo) { const data = bufferInfo.data; const dataView = bufferInfo.dataView; + const offset = bufferInfo.offset; for (const name in shader.uniforms) { const uniform = shader.uniforms[name]; if (uniform.isSampler) continue; @@ -1582,31 +1636,31 @@ function rendererWebGPU(p5, fn) { if (uniform.baseType === 'u32') { if (uniform.size === 4) { // Single u32 - dataView.setUint32(uniform.offset, uniform._cachedData, true); + dataView.setUint32(offset + uniform.offset, uniform._cachedData, true); } else { // Vector of u32s const uniformData = uniform._cachedData; for (let i = 0; i < uniformData.length; i++) { - dataView.setUint32(uniform.offset + i * 4, uniformData[i], true); + dataView.setUint32(offset + uniform.offset + i * 4, uniformData[i], true); } } } else if (uniform.baseType === 'i32') { if (uniform.size === 4) { // Single i32 - dataView.setInt32(uniform.offset, uniform._cachedData, true); + dataView.setInt32(offset + uniform.offset, uniform._cachedData, true); } else { // Vector of i32s const uniformData = uniform._cachedData; for (let i = 0; i < uniformData.length; i++) { - dataView.setInt32(uniform.offset + i * 4, uniformData[i], true); + dataView.setInt32(offset + uniform.offset + i * 4, uniformData[i], true); } } } else if (uniform.size === 4) { // Single float value - data.set([uniform._cachedData], uniform.offset / 4); + data.set([uniform._cachedData], (offset + uniform.offset) / 4); } else if (uniform._cachedData !== undefined) { // Float array (including vec2, vec3, vec4, mat4x4) - data.set(uniform._cachedData, uniform.offset / 4); + data.set(uniform._cachedData, (offset + uniform.offset) / 4); } } } @@ -3116,6 +3170,7 @@ ${hookUniformFields}} * Copy framebuffer content directly to WebGPU texture mip level */ _accumulateMipLevel(framebuffer, mipmapData, mipLevel, width, height) { + this.flushDraw(); // Copy from framebuffer texture to the mip level const commandEncoder = this.device.createCommandEncoder(); From b6d4c56b1a711cf5040c1326a3b0f8198a3482b1 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 7 Feb 2026 12:41:26 -0500 Subject: [PATCH 08/10] Group color with position because it can change a lot --- src/webgpu/shaders/color.js | 4 ++-- src/webgpu/shaders/line.js | 4 ++-- src/webgpu/shaders/material.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/webgpu/shaders/color.js b/src/webgpu/shaders/color.js index 5ef30243f4..fc9feace5e 100644 --- a/src/webgpu/shaders/color.js +++ b/src/webgpu/shaders/color.js @@ -18,11 +18,11 @@ struct ModelUniforms { uModelViewMatrix: mat4x4, uNormalMatrix: mat3x3, // @p5 endif + uMaterialColor: vec4, } // Group 3: Material Properties struct MaterialUniforms { - uMaterialColor: vec4, uUseVertexColor: u32, } `; @@ -64,7 +64,7 @@ fn main(input: VertexInput) -> VertexOutput { input.aPosition, input.aNormal, input.aTexCoord, - select(material.uMaterialColor, input.aVertexColor, useVertexColor) + select(model.uMaterialColor, input.aVertexColor, useVertexColor) ); // @p5 ifdef Vertex getObjectInputs diff --git a/src/webgpu/shaders/line.js b/src/webgpu/shaders/line.js index ac2f474f77..3b53b0a6e7 100644 --- a/src/webgpu/shaders/line.js +++ b/src/webgpu/shaders/line.js @@ -15,11 +15,11 @@ struct ModelUniforms { // @p5 ifndef StrokeVertex getWorldInputs uModelViewMatrix: mat4x4, // @p5 endif + uMaterialColor: vec4, } // Group 3: Stroke Properties struct StrokeUniforms { - uMaterialColor: vec4, uStrokeWeight: f32, uUseLineColor: f32, uSimpleLines: f32, @@ -104,7 +104,7 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { if (stroke.uUseLineColor != 0.) { lineColor = input.aVertexColor; } else { - lineColor = stroke.uMaterialColor; + lineColor = model.uMaterialColor; } var inputs = StrokeVertex( input.aPosition.xyz, diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index 50e65aedce..bac69aa4ff 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -16,11 +16,11 @@ struct ModelUniforms { uModelViewMatrix: mat4x4, uNormalMatrix: mat3x3, // @p5 endif + uMaterialColor: vec4, } // Group 3: Material Properties struct MaterialUniforms { - uMaterialColor: vec4, uUseVertexColor: u32, uHasSetAmbient: u32, uAmbientColor: vec3, @@ -98,7 +98,7 @@ fn main(input: VertexInput) -> VertexOutput { input.aPosition, input.aNormal, input.aTexCoord, - select(material.uMaterialColor, input.aVertexColor, useVertexColor) + select(model.uMaterialColor, input.aVertexColor, useVertexColor) ); // @p5 ifdef Vertex getObjectInputs From d1051eb4d13fb9391b15814ccc9a5e22856f2855 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 7 Feb 2026 13:47:20 -0500 Subject: [PATCH 09/10] Slight perf updates, fix text --- src/webgl/shaders/lighting.glsl | 1 + src/webgpu/p5.RendererWebGPU.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/webgl/shaders/lighting.glsl b/src/webgl/shaders/lighting.glsl index 69bc277d38..43343d07b4 100644 --- a/src/webgl/shaders/lighting.glsl +++ b/src/webgl/shaders/lighting.glsl @@ -4,6 +4,7 @@ precision highp float; precision highp int; uniform mat4 uViewMatrix; +uniform mat3 uCameraNormalMatrix; uniform bool uUseLighting; diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 695bd59942..3a0affc01a 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1461,7 +1461,7 @@ function rendererWebGPU(p5, fn) { const bgEntries = entries.map(entry => { // Check if this is a uniform buffer binding const uniformBufferInfo = this._uniformBuffersForBinding.get(entry.binding); - if (uniformBufferInfo) { + if (uniformBufferInfo && entry.bufferGroup) { return { binding: entry.binding, resource: entry.bufferGroup.dynamic @@ -1494,7 +1494,7 @@ function rendererWebGPU(p5, fn) { passEncoder.setBindGroup( group, bindGroup, - entries.map(e => this._uniformBuffersForBinding.get(e.binding)) + entries.map(e => e.bufferGroup && this._uniformBuffersForBinding.get(e.binding)) .filter(b => b?.dynamic) .map(b => b.lastOffset) ); From eda27254e5bcaca0b7b36bff29bc4c5ad7530f21 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 7 Feb 2026 14:40:59 -0500 Subject: [PATCH 10/10] Clean up + comment more --- preview/index.html | 4 +- src/webgpu/p5.RendererWebGPU.js | 213 +++++++++----------------------- 2 files changed, 57 insertions(+), 160 deletions(-) diff --git a/preview/index.html b/preview/index.html index 6c75a9afbe..ed76b913f5 100644 --- a/preview/index.html +++ b/preview/index.html @@ -42,14 +42,14 @@ instance = p.buildGeometry(() => p.sphere(5)); - /*redFilter = p.baseFilterShader().modify(() => { + redFilter = p.baseFilterShader().modify(() => { p.getColor((inputs, canvasContent) => { let col = p.getTexture(canvasContent, inputs.texCoord); col.g = col.r; col.b = col.r; return col; }) - }, { p })*/ + }, { p }) tex = p.createImage(100, 100); tex.loadPixels(); diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 3a0affc01a..389afe9b27 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -37,13 +37,18 @@ function rendererWebGPU(p5, fn) { constructor(pInst, w, h, isMainCanvas, elt) { super(pInst, w, h, isMainCanvas, elt) + // Used to group draws into one big render pass this.activeRenderPass = null; this.activeRenderPassEncoder = null; this.samplers = new Map(); + // Some uniforms update every frame, like model matrices and sometimes colors. + // The fastest way to handle these is to use mapped memory. We'll batch those + // into bigger buffers with dynamic offsets, separate from the usual system + // where bind groups have their own little buffers that get cached when they + // are unchanged this.uniformBufferAlignment = 256; - this.uniformBufferPool = []; this.activeUniformBuffers = []; this.currentUniformBuffer = undefined; @@ -567,7 +572,10 @@ function rendererWebGPU(p5, fn) { } _finalizeShader(shader) { - // Create per-group buffer pools instead of a single pool + // Per-group buffer pools. We will pull from these when we draw multiple + // times using the shader in a render pass. These are per group instead of + // global so that we can reuse the last used buffer when uniform values + // don't change. shader._uniformBufferGroups = []; for (const group of shader._uniformGroups) { @@ -579,18 +587,6 @@ function rendererWebGPU(p5, fn) { ); const alignedSize = Math.ceil(rawSize / 16) * 16; - // Create staging data arrays for this group - // const groupData = new Float32Array(alignedSize / 4); - // const groupDataView = new DataView(groupData.buffer); - - // // Create GPU buffer pool for this group - // const firstGPUBuffer = this.device.createBuffer({ - // size: alignedSize, - // usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - // }); - // const firstData = new Float32Array(alignedSize / 4); - // const firstDataView = new DataView(firstData.buffer); - shader._uniformBufferGroups.push({ group: group.group, binding: group.binding, @@ -598,17 +594,13 @@ function rendererWebGPU(p5, fn) { structType: group.structType, uniforms: groupUniforms, size: alignedSize, + bufferPool: [], nextBufferPool: [], - // bufferPool: [{ - // buffer: firstGPUBuffer, - // data: firstData, - // dataView: firstDataView - // }], + dynamic: groupUniforms.some(u => u.name.startsWith('uModel')), buffersInUse: new Set(), - currentBuffer: null, // For caching - cachedData: null // For caching comparison + currentBuffer: null, // For caching }); } @@ -1188,14 +1180,22 @@ function rendererWebGPU(p5, fn) { } _getDynamicUniformBufferFromPool(bufferGroup) { + // let buffer; - if (this.currentUniformBuffer && this.currentUniformBuffer.offset + bufferGroup.size < this.currentUniformBuffer.size) { + if ( + this.currentUniformBuffer && + this.currentUniformBuffer.offset + bufferGroup.size < this.currentUniformBuffer.size + ) { + // We can fit this next block of uniforms into the current active memory chunk buffer = this.currentUniformBuffer; - } else if (this.uniformBufferPool.length > 0) { - buffer = this.uniformBufferPool.pop(); - this.activeUniformBuffers.push(buffer); } else { - const size = 256 * 10 * 4; + // Kinda arbitrary. Each dynamic offset has to be in groups of 256, but then + // we can choose how many things we want to be able to fit into a block. + // There's some overhead to each block so if we're drawing a lot of stuff, + // bigger is better. But it's also a lot of wasted memory if we AREN'T drawing + // a lot of stuff. So.... right now it's 40. Feel free to update this if + // a better balance can be achieved. + const size = 256 * 40; buffer = { dynamic: true, lastOffset: 0, @@ -1207,12 +1207,11 @@ function rendererWebGPU(p5, fn) { mappedAtCreation: true, }), } - this.activeUniformBuffers.push(buffer); - } - if (!buffer.data) { buffer.data = new Float32Array(buffer.buffer.getMappedRange()); buffer.dataView = new DataView(buffer.data.buffer); + + this.activeUniformBuffers.push(buffer); } this.currentUniformBuffer = buffer; @@ -1236,7 +1235,7 @@ function rendererWebGPU(p5, fn) { for (const bufferInfo of bufferGroup.buffersInUse.keys()) { bufferGroup.nextBufferPool.push(bufferInfo); } - // bufferGroup.currentBuffer = null; + bufferGroup.currentBuffer = null; bufferGroup.buffersInUse.clear(); } } @@ -1401,35 +1400,27 @@ function rendererWebGPU(p5, fn) { for (const bufferGroup of currentShader._uniformBufferGroups) { if (bufferGroup.dynamic) { - // Bind uniforms - get a buffer from the pool + // Bind uniforms into a part of a big dynamic memory block because + // the group changes often const uniformBufferInfo = this._getDynamicUniformBufferFromPool(bufferGroup); this._packUniformGroup(currentShader, bufferGroup.uniforms, uniformBufferInfo); - // this._packUniforms(currentShader, uniformBufferInfo); uniformBufferInfo.lastOffset = uniformBufferInfo.offset; uniformBufferInfo.offset += Math.ceil(bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment; - /*this.device.queue.writeBuffer( - uniformBufferInfo.buffer, - 0, - uniformBufferInfo.data.buffer, - uniformBufferInfo.data.byteOffset, - uniformBufferInfo.data.byteLength - );*/ + // Make a shallow copy so that we keep track of the last offset for this uniform this._uniformBuffersForBinding.set(bufferGroup.binding, { ...uniformBufferInfo }); } else { + // Bind uniforms to a binding-specific buffer, which may be cached for performance let bufferInfo; const dataChanged = this._hasGroupDataChanged(currentShader, bufferGroup); if (!dataChanged && bufferGroup.currentBuffer) { // Reuse the cached buffer - no need to pack or write bufferInfo = bufferGroup.currentBuffer; - // Still need to track it in buffersInUse for proper cleanup bufferGroup.buffersInUse.add(bufferInfo); } else { - // Data changed - get a buffer from the pool + // Data changed - get a new buffer and write to it bufferInfo = this._getUniformBufferFromPool(bufferGroup); - - // Pack and write the data this._packUniformGroup(currentShader, bufferGroup.uniforms, bufferInfo); this.device.queue.writeBuffer( bufferInfo.buffer, @@ -1441,12 +1432,6 @@ function rendererWebGPU(p5, fn) { currentShader.buffersDirty = currentShader.buffersDirty || {}; currentShader.buffersDirty[bufferGroup.group + ',' + bufferGroup.binding] = false; - /*for (const uniform of bufferGroup.uniforms) { - const fullUniform = currentShader.uniforms[uniform.name]; - if (fullUniform) { - fullUniform.dirty = false; - } - }*/ // Cache this buffer and data for next frame bufferGroup.currentBuffer = bufferInfo; @@ -1474,10 +1459,6 @@ function rendererWebGPU(p5, fn) { }; } - // const texture = entry.uniform.type === 'sampler' - // ? entry.uniform.textureSource?.texture - // : entry.uniform.texture; - return { binding: entry.binding, resource: entry.uniform.type === 'sampler' @@ -1524,10 +1505,6 @@ function rendererWebGPU(p5, fn) { // Mark that we have pending draws that need submission this._hasPendingDraws = true; - - /*if (this._pendingCommandEncoders.length > 50) { - this.flushDraw(); - }*/ } ////////////////////////////////////////////// @@ -1574,95 +1551,6 @@ function rendererWebGPU(p5, fn) { // First time if (!bufferGroup.currentBuffer) return true; return shader.buffersDirty?.[bufferGroup.group + ',' + bufferGroup.binding]; - const cachedData = bufferGroup.currentBuffer.data; - const cachedDataView = bufferGroup.currentBuffer.dataView; - - for (const uniform of bufferGroup.uniforms) { - const fullUniform = shader.uniforms[uniform.name]; - if (!fullUniform || fullUniform.isSampler) continue; - if (fullUniform.dirty) return true; - // continue; - - // Compare typed arrays bytewise - const currentData = fullUniform._cachedData; - const cachedOffset = fullUniform.offset; - - // Note: intentionally using == instead of === below - if (fullUniform.baseType === 'u32' || fullUniform.baseType === 'i32') { - if (fullUniform.size === 4) { - // Single value - if (cachedDataView.getUint32(cachedOffset, true) != currentData) { - return true; - } - } else { - // Array - for (let i = 0; i < currentData.length; i++) { - if (cachedDataView.getUint32(cachedOffset + i * 4, true) != currentData[i]) { - return true; - } - } - } - } else { - if (fullUniform.size === 4) { - // Single float - if (cachedData[cachedOffset / 4] != currentData) { - return true; - } - } else if (currentData !== undefined) { - // Float array - const floatOffset = cachedOffset / 4; - for (let i = 0; i < currentData.length; i++) { - if (cachedData[floatOffset + i] != currentData[i]) { - return true; - } - } - } - } - } - - return false; // No changes detected - } - - // TODO: delete - _packUniforms(shader, bufferInfo) { - const data = bufferInfo.data; - const dataView = bufferInfo.dataView; - - const offset = bufferInfo.offset; - for (const name in shader.uniforms) { - const uniform = shader.uniforms[name]; - if (uniform.isSampler) continue; - - if (uniform.baseType === 'u32') { - if (uniform.size === 4) { - // Single u32 - dataView.setUint32(offset + uniform.offset, uniform._cachedData, true); - } else { - // Vector of u32s - const uniformData = uniform._cachedData; - for (let i = 0; i < uniformData.length; i++) { - dataView.setUint32(offset + uniform.offset + i * 4, uniformData[i], true); - } - } - } else if (uniform.baseType === 'i32') { - if (uniform.size === 4) { - // Single i32 - dataView.setInt32(offset + uniform.offset, uniform._cachedData, true); - } else { - // Vector of i32s - const uniformData = uniform._cachedData; - for (let i = 0; i < uniformData.length; i++) { - dataView.setInt32(offset + uniform.offset + i * 4, uniformData[i], true); - } - } - } else if (uniform.size === 4) { - // Single float value - data.set([uniform._cachedData], (offset + uniform.offset) / 4); - } else if (uniform._cachedData !== undefined) { - // Float array (including vec2, vec3, vec4, mat4x4) - data.set(uniform._cachedData, (offset + uniform.offset) / 4); - } - } } _parseStruct(shaderSource, structName) { @@ -1792,8 +1680,12 @@ function rendererWebGPU(p5, fn) { } getUniformMetadata(shader) { - // Parse all uniform struct bindings in group 0 - // Each binding represents a logical group of uniforms + // Parse all uniform struct bindings in group 0. + // TODO: support non-sampler uniforms being in other groups + + // Each binding represents a logical group of uniforms, since they get + // updated or cached all at once. + const uniformGroups = []; const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var\s+(\w+)\s*:\s*(\w+);/g; @@ -1816,20 +1708,21 @@ function rendererWebGPU(p5, fn) { throw new Error('Expected at least one uniform struct bound to @group(0)'); } - // Flatten all uniforms for backward compatibility, but keep track of their groups + // While we're also keeping track of the groups, the API we expose + // to users of p5 is just a flat list of uniforms (which can be the + // individual struct items in the group.) const allUniforms = {}; for (const group of uniformGroups) { for (const [uniformName, uniformData] of Object.entries(group.uniforms)) { allUniforms[uniformName] = { ...uniformData, - group: 0, + group: group.group, binding: group.binding, varName: group.varName }; } } - const uniforms = allUniforms; // Store uniform groups for buffer pooling shader._uniformGroups = uniformGroups; @@ -1838,8 +1731,12 @@ function rendererWebGPU(p5, fn) { // TODO: support other texture types const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d|sampler);/g; - // Track which bindings in group 0 are taken by uniforms - const group0UniformBindings = new Set(uniformGroups.map(g => g.binding)); + // Track which bindings are taken by the struct properties we've parsed + // (the rest should be textures/samplers) + const structUniformBindings = {}; + for (const g of uniformGroups) { + structUniformBindings[g.group + ',' + g.binding] = true; + } for (const [src, visibility] of [ [shader.vertSrc(), GPUShaderStage.VERTEX], @@ -1850,8 +1747,8 @@ function rendererWebGPU(p5, fn) { const [_, group, binding, name, type] = match; const groupIndex = parseInt(group); const bindingIndex = parseInt(binding); - // Skip uniform bindings in group 0 which we've already parsed - if (groupIndex === 0 && group0UniformBindings.has(bindingIndex)) continue; + // Skip struct uniform bindings which we've already parsed + if (structUniformBindings[groupIndex + ',' + bindingIndex]) continue; const key = `${groupIndex},${bindingIndex}`; samplers[key] = { @@ -1880,7 +1777,7 @@ function rendererWebGPU(p5, fn) { } } } - return [...Object.values(uniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)]; + return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)]; } getNextBindingIndex({ vert, frag }, group = 0) { @@ -2241,8 +2138,8 @@ function rendererWebGPU(p5, fn) { if (hookUniformFields) { // Find the next available binding in group 0 - // Use the source we're currently building (preMain) so we can see texture bindings - // added by strands, and use the original source for the other shader type + // Use the source we're currently building (preMain) which has texture bindings. We can't call `fragSrc()` + // or `vertSrc()` because we may be in one of those calls already, and might infinite loop const nextBinding = this.getNextBindingIndex({ vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc, frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,