UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

817 lines (811 loc) 40.4 kB
import { Debug } from '../../../core/debug.js'; import { uniformTypeToNameMapWGSL, UNUSED_UNIFORM_NAME, UNIFORMTYPE_FLOAT, BINDGROUP_MESH_UB, SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT, BINDGROUP_MESH, bindGroupNames, uniformTypeToNameWGSL, SAMPLETYPE_DEPTH, semanticToLocation, TYPE_FLOAT32, TYPE_FLOAT16, TEXTUREDIMENSION_CUBE_ARRAY, TEXTUREDIMENSION_CUBE, TEXTUREDIMENSION_2D_ARRAY, TEXTUREDIMENSION_2D, TEXTUREDIMENSION_3D, TEXTUREDIMENSION_1D, SAMPLETYPE_INT, SAMPLETYPE_UINT, SAMPLETYPE_UNFILTERABLE_FLOAT, SAMPLETYPE_FLOAT, TYPE_INT8, TYPE_INT16, TYPE_INT32 } from '../constants.js'; import { UniformFormat, UniformBufferFormat } from '../uniform-buffer-format.js'; import { BindTextureFormat, BindStorageBufferFormat, BindGroupFormat } from '../bind-group-format.js'; /** * @import { GraphicsDevice } from '../graphics-device.js' * @import { ShaderProcessorOptions } from '../shader-processor-options.js' * @import { Shader } from '../shader.js' */ // matches lines where the keyword is the first non-whitespace content, followed by a whitespace const KEYWORD = /^[ \t]*(attribute|varying|uniform)[\t ]+/gm; // match 'attribute' and anything else till ';' // eslint-disable-next-line const KEYWORD_LINE = /^[ \t]*(attribute|varying|uniform)[ \t]*([^;]+)(;+)/gm; // match global variables // branch A matches: var<storage,...> // branch B matches: texture, storage buffer, storage texture or external texture // eslint-disable-next-line const KEYWORD_RESOURCE = /^[ \t]*var\s*(?:(<storage,[^>]*>)\s*([\w\d_]+)\s*:\s*(.*?)\s*;|(<(?!storage,)[^>]*>)?\s*([\w\d_]+)\s*:\s*(texture_.*|storage_texture_.*|storage\w.*|external_texture|sampler(?:_comparison)?)\s*;)\s*$/gm; // match varying name from string like: '@interpolate(perspective, centroid) smoothColor : vec3f;' // eslint-disable-next-line const VARYING = /(?:@interpolate\([^)]*\)\s*)?([\w]+)\s*:/; // marker for a place in the source code to be replaced by code const MARKER = '@@@'; // matches vertex of fragment entry function, extracts the input name. Ends at the start of the function body '{'. const ENTRY_FUNCTION = /(@vertex|@fragment)\s*fn\s+\w+\s*\(\s*(\w+)\s*:[\s\S]*?\{/; const textureBaseInfo = { 'texture_1d': { viewDimension: TEXTUREDIMENSION_1D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_2d': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_2d_array': { viewDimension: TEXTUREDIMENSION_2D_ARRAY, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_3d': { viewDimension: TEXTUREDIMENSION_3D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_cube': { viewDimension: TEXTUREDIMENSION_CUBE, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_cube_array': { viewDimension: TEXTUREDIMENSION_CUBE_ARRAY, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_multisampled_2d': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_depth_2d': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_depth_2d_array': { viewDimension: TEXTUREDIMENSION_2D_ARRAY, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_depth_cube': { viewDimension: TEXTUREDIMENSION_CUBE, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_depth_cube_array': { viewDimension: TEXTUREDIMENSION_CUBE_ARRAY, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_external': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_UNFILTERABLE_FLOAT } }; // get the view dimension and sample type for a given texture type // example: texture_2d_array<u32> -> 2d_array & uint const getTextureInfo = (baseType, componentType)=>{ const baseInfo = textureBaseInfo[baseType]; Debug.assert(baseInfo); let finalSampleType = baseInfo.baseSampleType; if (baseInfo.baseSampleType === SAMPLETYPE_FLOAT && baseType !== 'texture_multisampled_2d') { switch(componentType){ case 'u32': finalSampleType = SAMPLETYPE_UINT; break; case 'i32': finalSampleType = SAMPLETYPE_INT; break; case 'f32': finalSampleType = SAMPLETYPE_FLOAT; break; // custom 'uff' type for unfilterable float, allowing us to create correct bind, which is automatically generated based on the shader case 'uff': finalSampleType = SAMPLETYPE_UNFILTERABLE_FLOAT; break; } } return { viewDimension: baseInfo.viewDimension, sampleType: finalSampleType }; }; // reverse to getTextureInfo, convert view dimension and sample type to texture declaration // example: 2d_array & float -> texture_2d_array<f32> const getTextureDeclarationType = (viewDimension, sampleType)=>{ // types without template specifiers if (sampleType === SAMPLETYPE_DEPTH) { switch(viewDimension){ case TEXTUREDIMENSION_2D: return 'texture_depth_2d'; case TEXTUREDIMENSION_2D_ARRAY: return 'texture_depth_2d_array'; case TEXTUREDIMENSION_CUBE: return 'texture_depth_cube'; case TEXTUREDIMENSION_CUBE_ARRAY: return 'texture_depth_cube_array'; default: Debug.assert(false); } } // the base texture type string based on dimension let baseTypeString; switch(viewDimension){ case TEXTUREDIMENSION_1D: baseTypeString = 'texture_1d'; break; case TEXTUREDIMENSION_2D: baseTypeString = 'texture_2d'; break; case TEXTUREDIMENSION_2D_ARRAY: baseTypeString = 'texture_2d_array'; break; case TEXTUREDIMENSION_3D: baseTypeString = 'texture_3d'; break; case TEXTUREDIMENSION_CUBE: baseTypeString = 'texture_cube'; break; case TEXTUREDIMENSION_CUBE_ARRAY: baseTypeString = 'texture_cube_array'; break; default: Debug.assert(false); } // component format string ('f32', 'u32', 'i32') let coreFormatString; switch(sampleType){ case SAMPLETYPE_FLOAT: case SAMPLETYPE_UNFILTERABLE_FLOAT: coreFormatString = 'f32'; break; case SAMPLETYPE_UINT: coreFormatString = 'u32'; break; case SAMPLETYPE_INT: coreFormatString = 'i32'; break; default: Debug.assert(false); } // final type return `${baseTypeString}<${coreFormatString}>`; }; const wrappedArrayTypes = { 'f32': 'WrappedF32', 'i32': 'WrappedI32', 'u32': 'WrappedU32', 'vec2f': 'WrappedVec2F', 'vec2i': 'WrappedVec2I', 'vec2u': 'WrappedVec2U' }; const splitToWords = (line)=>{ // remove any double spaces line = line.replace(/\s+/g, ' ').trim(); // Split by spaces or ':' symbol return line.split(/[\s:]+/); }; // matches: array<f32, 4>; // eslint-disable-next-line const UNIFORM_ARRAY_REGEX = /array<([^,]+),\s*([^>]+)>/; class UniformLine { constructor(line, shader){ /** * A name of the ub buffer which this uniform is assigned to. * * @type {string|null} */ this.ubName = null; this.arraySize = 0; // Save the raw line this.line = line; // Use splitToWords to split the line into parts const parts = splitToWords(line); if (parts.length < 2) { Debug.error(`Invalid uniform line format: ${line}`, shader); shader.failed = true; return; } // Extract the name and type this.name = parts[0]; this.type = parts.slice(1).join(' '); // array of uniforms (e.g. array<f32, 5>) if (this.type.includes('array<')) { const match = UNIFORM_ARRAY_REGEX.exec(this.type); Debug.assert(match, `Array type on line [${line}] is not supported.`); // array type this.type = match[1].trim(); this.arraySize = Number(match[2]); if (isNaN(this.arraySize)) { shader.failed = true; Debug.error(`Only numerically specified uniform array sizes are supported, this uniform is not supported: '${line}'`, shader); } } } } // regex constants for resource lines, for example: // var diffuseTexture : texture_2d<f32>; // var diffuseTextures : texture_2d_array<f32>; // var shadowMap : texture_depth_2d; // var diffuseSampler : sampler; // var<storage, read> particles: array<Particle>; // var<storage, read_write> storageBuffer : Buffer; // var storageTexture : texture_storage_2d<rgba8unorm, write>; // var videoTexture : texture_external; const TEXTURE_REGEX = /^\s*var\s+(\w+)\s*:\s*(texture_\w+)(?:<(\w+)>)?;\s*$/; // eslint-disable-next-line const STORAGE_TEXTURE_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*(texture_storage_2d|texture_storage_2d_array)<([\w\d_]+),\s*(\w+)>\s*;\s*$/; // eslint-disable-next-line const STORAGE_BUFFER_REGEX = /^\s*var\s*<storage,\s*(read|write)?>\s*([\w\d_]+)\s*:\s*(.*)\s*;\s*$/; // eslint-disable-next-line const EXTERNAL_TEXTURE_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*texture_external;\s*$/; // eslint-disable-next-line const SAMPLER_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*(sampler|sampler_comparison)\s*;\s*$/; // ResourceLine class to parse the resource declarations class ResourceLine { constructor(line, shader){ // save the raw line this.originalLine = line; this.line = line; // defaults this.isTexture = false; this.isSampler = false; this.isStorageTexture = false; this.isStorageBuffer = false; this.isExternalTexture = false; this.type = ''; this.matchedElements = []; // handle texture type const textureMatch = this.line.match(TEXTURE_REGEX); if (textureMatch) { this.name = textureMatch[1]; this.type = textureMatch[2]; // texture type (e.g., texture_2d or texture_cube_array) this.textureFormat = textureMatch[3]; // texture format (e.g., f32) this.isTexture = true; this.matchedElements.push(...textureMatch); // get dimension and sample type const info = getTextureInfo(this.type, this.textureFormat); Debug.assert(info); this.textureDimension = info.viewDimension; this.sampleType = info.sampleType; } // storage texture (e.g., texture_storage_2d<rgba8unorm, write>) const storageTextureMatch = this.line.match(STORAGE_TEXTURE_REGEX); if (storageTextureMatch) { this.isStorageTexture = true; this.name = storageTextureMatch[1]; this.textureType = storageTextureMatch[2]; // texture_storage_2d or texture_storage_2d_array this.format = storageTextureMatch[3]; // format (e.g., rgba8unorm) this.access = storageTextureMatch[4]; // access mode (e.g., write) this.matchedElements.push(...storageTextureMatch); } // storage buffer (e.g., <storage, read> particles: array<Particle>;) const storageBufferMatch = this.line.match(STORAGE_BUFFER_REGEX); if (storageBufferMatch) { this.isStorageBuffer = true; this.accessMode = storageBufferMatch[1] || 'none'; // Default to 'none' if no access mode this.name = storageBufferMatch[2]; this.type = storageBufferMatch[3]; // Everything after ':' (e.g., array<Particle>) this.matchedElements.push(...storageBufferMatch); } // external texture (e.g., texture_external) const externalTextureMatch = this.line.match(EXTERNAL_TEXTURE_REGEX); if (externalTextureMatch) { this.name = externalTextureMatch[1]; this.isExternalTexture = true; this.matchedElements.push(...storageBufferMatch); } // sampler const samplerMatch = this.line.match(SAMPLER_REGEX); if (samplerMatch) { this.name = samplerMatch[1]; this.samplerType = samplerMatch[2]; // sampler type (e.g., sampler or sampler_comparison) this.isSampler = true; this.matchedElements.push(...samplerMatch); } if (this.matchedElements.length === 0) { Debug.error(`Invalid / unsupported resource line format: ${line}`, shader); shader.failed = true; } } equals(other) { if (this.name !== other.name) return false; if (this.type !== other.type) return false; if (this.isTexture !== other.isTexture) return false; if (this.isSampler !== other.isSampler) return false; if (this.isStorageTexture !== other.isStorageTexture) return false; if (this.isStorageBuffer !== other.isStorageBuffer) return false; if (this.isExternalTexture !== other.isExternalTexture) return false; if (this.textureFormat !== other.textureFormat) return false; if (this.textureDimension !== other.textureDimension) return false; if (this.sampleType !== other.sampleType) return false; if (this.textureType !== other.textureType) return false; if (this.format !== other.format) return false; if (this.access !== other.access) return false; if (this.accessMode !== other.accessMode) return false; if (this.samplerType !== other.samplerType) return false; return true; } } /** * Pure static class implementing processing of WGSL shaders. It allocates fixed locations for * attributes, and handles conversion of uniforms to uniform buffers. */ class WebgpuShaderProcessorWGSL { /** * Process the shader. * * @param {GraphicsDevice} device - The graphics device. * @param {object} shaderDefinition - The shader definition. * @param {Shader} shader - The shader. * @returns {object} - The processed shader data. */ static run(device, shaderDefinition, shader) { /** @type {Map<string, number>} */ const varyingMap = new Map(); // extract lines of interests from both shaders const vertexExtracted = WebgpuShaderProcessorWGSL.extract(shaderDefinition.vshader); const fragmentExtracted = WebgpuShaderProcessorWGSL.extract(shaderDefinition.fshader); // VS - convert a list of attributes to a shader block with fixed locations const attributesMap = new Map(); const attributesBlock = WebgpuShaderProcessorWGSL.processAttributes(vertexExtracted.attributes, shaderDefinition.attributes, attributesMap, shaderDefinition.processingOptions, shader); // VS - convert a list of varyings to a shader block const vertexVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(vertexExtracted.varyings, varyingMap, true); // FS - convert a list of varyings to a shader block const fragmentVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(fragmentExtracted.varyings, varyingMap, false); // uniforms - merge vertex and fragment uniforms, and create shared uniform buffers // Note that as both vertex and fragment can declare the same uniform, we need to remove duplicates const concatUniforms = vertexExtracted.uniforms.concat(fragmentExtracted.uniforms); const uniforms = Array.from(new Set(concatUniforms)); // parse uniform lines const parsedUniforms = uniforms.map((line)=>new UniformLine(line, shader)); // validation - as uniforms go to a shared uniform buffer, vertex and fragment versions need to match Debug.call(()=>{ const map = new Map(); parsedUniforms.forEach((uni)=>{ const existing = map.get(uni.name); Debug.assert(!existing, `Vertex and fragment shaders cannot use the same uniform name with different types: '${existing}' and '${uni.line}'`, shader); map.set(uni.name, uni.line); }); }); const uniformsData = WebgpuShaderProcessorWGSL.processUniforms(device, parsedUniforms, shaderDefinition.processingOptions, shader); // rename references to uniforms to match the uniform buffer vertexExtracted.src = WebgpuShaderProcessorWGSL.renameUniformAccess(vertexExtracted.src, parsedUniforms); fragmentExtracted.src = WebgpuShaderProcessorWGSL.renameUniformAccess(fragmentExtracted.src, parsedUniforms); // parse resource lines const parsedResources = WebgpuShaderProcessorWGSL.mergeResources(vertexExtracted.resources, fragmentExtracted.resources, shader); const resourcesData = WebgpuShaderProcessorWGSL.processResources(device, parsedResources, shaderDefinition.processingOptions, shader); // generate fragment output struct const fOutput = WebgpuShaderProcessorWGSL.generateFragmentOutputStruct(fragmentExtracted.src, device.maxColorAttachments); // inject the call to the function which copies the shader input globals vertexExtracted.src = WebgpuShaderProcessorWGSL.copyInputs(vertexExtracted.src, shader); fragmentExtracted.src = WebgpuShaderProcessorWGSL.copyInputs(fragmentExtracted.src, shader); // VS - insert the blocks to the source const vBlock = `${attributesBlock}\n${vertexVaryingsBlock}\n${uniformsData.code}\n${resourcesData.code}\n`; const vshader = vertexExtracted.src.replace(MARKER, vBlock); // FS - insert the blocks to the source const fBlock = `${fragmentVaryingsBlock}\n${fOutput}\n${uniformsData.code}\n${resourcesData.code}\n`; const fshader = fragmentExtracted.src.replace(MARKER, fBlock); return { vshader: vshader, fshader: fshader, attributes: attributesMap, meshUniformBufferFormat: uniformsData.meshUniformBufferFormat, meshBindGroupFormat: resourcesData.meshBindGroupFormat }; } // Extract required information from the shader source code. static extract(src) { // collected data const attributes = []; const varyings = []; const uniforms = []; const resources = []; // replacement marker - mark a first replacement place let replacement = `${MARKER}\n`; let match; // Extract uniforms, attributes, and varyings while((match = KEYWORD.exec(src)) !== null){ const keyword = match[1]; KEYWORD_LINE.lastIndex = match.index; const lineMatch = KEYWORD_LINE.exec(src); if (keyword === 'attribute') { attributes.push(lineMatch[2]); } else if (keyword === 'varying') { varyings.push(lineMatch[2]); } else if (keyword === 'uniform') { uniforms.push(lineMatch[2]); } // Remove the matched line from source src = WebgpuShaderProcessorWGSL.cutOut(src, match.index, KEYWORD_LINE.lastIndex, replacement); KEYWORD.lastIndex = match.index + replacement.length; replacement = ''; // Only place a single replacement marker } // Extract resource declarations while((match = KEYWORD_RESOURCE.exec(src)) !== null){ resources.push(match[0]); // Store the full line // Remove the matched line from source src = WebgpuShaderProcessorWGSL.cutOut(src, match.index, KEYWORD_RESOURCE.lastIndex, replacement); KEYWORD_RESOURCE.lastIndex = match.index + replacement.length; replacement = ''; } return { src, attributes, varyings, uniforms, resources }; } /** * Process the lines with uniforms. The function receives the lines containing all numerical * uniforms. The function also receives the format of uniform buffers for view and material * level. All uniforms that match any of those are ignored, as those would be supplied by view / * material level buffers. All leftover uniforms create uniform buffer and bind group for the * mesh itself, containing uniforms that change on the level of the mesh. * * @param {GraphicsDevice} device - The graphics device. * @param {Array<UniformLine>} uniforms - Lines containing uniforms. * @param {ShaderProcessorOptions} processingOptions - Uniform formats. * @param {Shader} shader - The shader definition. * @returns {object} - The uniform data. Returns a shader code block containing uniforms, to be * inserted into the shader, as well as generated uniform format structures for the mesh level. */ static processUniforms(device, uniforms, processingOptions, shader) { // build mesh uniform buffer format const meshUniforms = []; uniforms.forEach((uniform)=>{ // uniforms not already in supplied uniform buffers go to the mesh buffer if (!processingOptions.hasUniform(uniform.name)) { uniform.ubName = 'ub_mesh_ub'; // Find the uniform type index in uniformTypeToNameWGSL const uniformType = uniformTypeToNameMapWGSL.get(uniform.type); Debug.assert(uniformType !== undefined, `Uniform type ${uniform.type} is not recognised on line [${uniform.line}]`); const uniformFormat = new UniformFormat(uniform.name, uniformType, uniform.arraySize); meshUniforms.push(uniformFormat); } else { // TODO: when we add material ub, this name will need to be updated uniform.ubName = 'ub_view'; // Validate types here if needed Debug.assert(true, `Uniform ${uniform.name} already processed, skipping additional validation.`); } }); // if we don't have any uniform, add a dummy uniform to avoid empty uniform buffer - WebGPU rendering does not // support rendering will NULL bind group as binding a null buffer changes placement of other bindings if (meshUniforms.length === 0) { meshUniforms.push(new UniformFormat(UNUSED_UNIFORM_NAME, UNIFORMTYPE_FLOAT)); } const meshUniformBufferFormat = new UniformBufferFormat(device, meshUniforms); // generate code for uniform buffers, starts on the slot 0 let code = ''; processingOptions.uniformFormats.forEach((format, bindGroupIndex)=>{ if (format) { code += WebgpuShaderProcessorWGSL.getUniformShaderDeclaration(format, bindGroupIndex, 0); } }); // and also for generated mesh uniform format, which is at the slot 0 of the bind group if (meshUniformBufferFormat) { code += WebgpuShaderProcessorWGSL.getUniformShaderDeclaration(meshUniformBufferFormat, BINDGROUP_MESH_UB, 0); } return { code, meshUniformBufferFormat }; } /** * Source code references uniforms as `uniform.name`, but swap those to reference the actual uniform buffer * the uniform was assigned to, for example `ub_view.name`. * * @param {string} source - The source code. * @param {Array<UniformLine>} uniforms - Lines containing uniforms. * @returns {string} - The source code with updated uniform references. */ static renameUniformAccess(source, uniforms) { uniforms.forEach((uniform)=>{ const srcName = `uniform.${uniform.name}`; const dstName = `${uniform.ubName}.${uniform.name}`; // Use a regular expression to match `uniform.name` as a whole word. const regex = new RegExp(`\\b${srcName}\\b`, 'g'); source = source.replace(regex, dstName); }); return source; } static mergeResources(vertex, fragment, shader) { const resources = vertex.map((line)=>new ResourceLine(line, shader)); const fragmentResources = fragment.map((line)=>new ResourceLine(line, shader)); // merge fragment list to resources, removing exact duplicates fragmentResources.forEach((fragmentResource)=>{ const existing = resources.find((resource)=>resource.name === fragmentResource.name); if (existing) { // if the resource is already in the list, check if it matches if (!existing.equals(fragmentResource)) { Debug.error(`Resource '${fragmentResource.name}' is declared with different types in vertex and fragment shaders.`, { vertexLine: existing.line, fragmentLine: fragmentResource.line, shader, vertexResource: existing, fragmentResource }); shader.failed = true; } } else { resources.push(fragmentResource); } }); return resources; } static processResources(device, resources, processingOptions, shader) { // build mesh bind group format - this contains the textures, but not the uniform buffer as that is a separate binding const textureFormats = []; for(let i = 0; i < resources.length; i++){ const resource = resources[i]; if (resource.isTexture) { // followed by optional sampler uniform const sampler = resources[i + 1]; const hasSampler = sampler?.isSampler; // TODO: handle external, and storage types const sampleType = resource.sampleType; const dimension = resource.textureDimension; // TODO: we could optimize visibility to only stages that use any of the data textureFormats.push(new BindTextureFormat(resource.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, dimension, sampleType, hasSampler, hasSampler ? sampler.name : null)); // following sampler was already handled if (hasSampler) i++; } if (resource.isStorageBuffer) { const readOnly = resource.accessMode !== 'read_write'; const bufferFormat = new BindStorageBufferFormat(resource.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, readOnly); bufferFormat.format = resource.type; textureFormats.push(bufferFormat); } Debug.assert(!resource.isSampler, `Sampler uniform needs to follow a texture uniform, but does not on line [${resource.originalLine}]`); Debug.assert(!resource.isStorageTexture, 'TODO: add support for storage textures here'); Debug.assert(!resource.externalTexture, 'TODO: add support for external textures here'); } const meshBindGroupFormat = new BindGroupFormat(device, textureFormats); // generate code for textures let code = ''; processingOptions.bindGroupFormats.forEach((format, bindGroupIndex)=>{ if (format) { code += WebgpuShaderProcessorWGSL.getTextureShaderDeclaration(format, bindGroupIndex, 1); } }); // and also for generated mesh format code += WebgpuShaderProcessorWGSL.getTextureShaderDeclaration(meshBindGroupFormat, BINDGROUP_MESH, 0); return { code, meshBindGroupFormat }; } /** * Generates a shader code for a uniform buffer, something like: * ``` * struct ub_view { matrix_viewProjection : mat4x4f } * @group(0) @binding(0) var<uniform> ubView : ub_view; * ``` * * @param {UniformBufferFormat} ubFormat - Format of the uniform buffer. * @param {number} bindGroup - The bind group index. * @param {number} bindIndex - The bind index. * @returns {string} - The shader code for the uniform buffer. * @private */ static getUniformShaderDeclaration(ubFormat, bindGroup, bindIndex) { const name = bindGroupNames[bindGroup]; const structName = `struct_ub_${name}`; let code = `struct ${structName} {\n`; ubFormat.uniforms.forEach((uniform)=>{ let typeString = uniformTypeToNameWGSL[uniform.type][0]; Debug.assert(typeString !== undefined, `Uniform type ${uniform.type} is not handled.`); // array uniforms if (uniform.count > 0) { // if the type is one of the ones that are not by default 16byte aligned, which is // a requirement for uniform buffers, we need to wrap them in a struct // for example: array<f32, 5> becomes array<WrappedF32, 5> if (wrappedArrayTypes.hasOwnProperty(typeString)) { typeString = wrappedArrayTypes[typeString]; } code += ` ${uniform.shortName}: array<${typeString}, ${uniform.count}>,\n`; } else { code += ` ${uniform.shortName}: ${typeString},\n`; } }); code += '};\n'; code += `@group(${bindGroup}) @binding(${bindIndex}) var<uniform> ub_${name} : ${structName};\n\n`; return code; } /** * Generates a shader code for a bind group, something like: * ``` * @group(0) @binding(0) var diffuseTexture: texture_2d<f32>; * @group(0) @binding(1) var diffuseTexture_sampler: sampler; // optional * ``` * @param {BindGroupFormat} format - The format of the bind group. * @param {number} bindGroup - The bind group index. * @param {number} startBindIndex - The starting bind index. * @returns {string} - The shader code for the bind group. */ static getTextureShaderDeclaration(format, bindGroup, startBindIndex) { let code = ''; let bindIndex = startBindIndex; format.textureFormats.forEach((format)=>{ const textureTypeName = getTextureDeclarationType(format.textureDimension, format.sampleType); code += `@group(${bindGroup}) @binding(${bindIndex}) var ${format.name}: ${textureTypeName};\n`; bindIndex++; if (format.hasSampler) { const samplerName = format.sampleType === SAMPLETYPE_DEPTH ? 'sampler_comparison' : 'sampler'; code += `@group(${bindGroup}) @binding(${bindIndex}) var ${format.samplerName}: ${samplerName};\n`; bindIndex++; } }); format.storageBufferFormats.forEach((format)=>{ const access = format.readOnly ? 'read' : 'read_write'; code += `@group(${bindGroup}) @binding(${bindIndex}) var<storage, ${access}> ${format.name} : ${format.format};\n`; bindIndex++; }); Debug.assert(format.storageTextureFormats.length === 0, 'Implement support for storage textures here'); // TODO: also add external texture support here return code; } static processVaryings(varyingLines, varyingMap, isVertex) { let block = ''; let blockPrivates = ''; let blockCopy = ''; varyingLines.forEach((line, index)=>{ const match = line.match(VARYING); Debug.assert(match, `Varying line is not valid: ${line}`); if (match) { const name = match[1]; if (isVertex) { // store it in the map varyingMap.set(name, index); } else { Debug.assert(varyingMap.has(name), `Fragment shader requires varying [${name}] but vertex shader does not generate it.`); index = varyingMap.get(name); } // generates: `@location(0) @interpolate(perspective, centroid) smoothColor : vec3f` block += ` @location(${index}) ${line},\n`; // fragment shader inputs (varyings) if (!isVertex) { // private global variable for fragment varying blockPrivates += ` var<private> ${line};\n`; // copy input variable to the private variable blockCopy += ` ${name} = input.${name};\n`; } } }); // add built-in varyings if (isVertex) { block += ' @builtin(position) position : vec4f,\n'; // output position } else { block += ' @builtin(position) position : vec4f,\n'; // interpolated fragment position block += ' @builtin(front_facing) frontFacing : bool,\n'; // front-facing block += ' @builtin(sample_index) sampleIndex : u32\n'; // sample index for MSAA } // global variables for build-in input into fragment shader const fragmentGlobals = isVertex ? '' : ` var<private> pcPosition: vec4f; var<private> pcFrontFacing: bool; var<private> pcSampleIndex: u32; ${blockPrivates} // function to copy inputs (varyings) to private global variables fn _pcCopyInputs(input: FragmentInput) { ${blockCopy} pcPosition = input.position; pcFrontFacing = input.frontFacing; pcSampleIndex = input.sampleIndex; } `; const structName = isVertex ? 'VertexOutput' : 'FragmentInput'; return ` struct ${structName} { ${block} }; ${fragmentGlobals} `; } static generateFragmentOutputStruct(src, numRenderTargets) { let structCode = 'struct FragmentOutput {\n'; for(let i = 0; i < numRenderTargets; i++){ structCode += ` @location(${i}) color${i > 0 ? i : ''} : pcOutType${i},\n`; } // find if the src contains `.fragDepth =`, ignoring whitespace before = sign const needsFragDepth = src.search(/\.fragDepth\s*=/) !== -1; if (needsFragDepth) { structCode += ' @builtin(frag_depth) fragDepth : f32\n'; } return `${structCode}};\n`; } // convert a float attribute type to matching signed or unsigned int type // for example: vec4f -> vec4u, f32 -> u32 static floatAttributeToInt(type, signed) { // convert any long-form type to short-form const longToShortMap = { 'f32': 'f32', 'vec2<f32>': 'vec2f', 'vec3<f32>': 'vec3f', 'vec4<f32>': 'vec4f' }; const shortType = longToShortMap[type] || type; // map from float short type to int short type const floatToIntShort = { 'f32': signed ? 'i32' : 'u32', 'vec2f': signed ? 'vec2i' : 'vec2u', 'vec3f': signed ? 'vec3i' : 'vec3u', 'vec4f': signed ? 'vec4i' : 'vec4u' }; return floatToIntShort[shortType] || null; } static processAttributes(attributeLines, shaderDefinitionAttributes = {}, attributesMap, processingOptions, shader) { let blockAttributes = ''; let blockPrivates = ''; let blockCopy = ''; const usedLocations = {}; attributeLines.forEach((line)=>{ const words = splitToWords(line); const name = words[0]; let type = words[1]; const originalType = type; if (shaderDefinitionAttributes.hasOwnProperty(name)) { const semantic = shaderDefinitionAttributes[name]; const location = semanticToLocation[semantic]; Debug.assert(location !== undefined, `Semantic ${semantic} used by the attribute ${name} is not known - make sure it's one of the supported semantics.`); Debug.assert(!usedLocations.hasOwnProperty(location), `WARNING: Two vertex attributes are mapped to the same location in a shader: ${usedLocations[location]} and ${semantic}`); usedLocations[location] = semantic; // build a map of used attributes attributesMap.set(location, name); // if vertex format for this attribute is not of a float type, but shader specifies float type, convert the shader type // to match the vertex format type, for example: vec4f -> vec4u // Note that we skip normalized elements, as shader receives them as floats already. const element = processingOptions.getVertexElement(semantic); if (element) { const dataType = element.dataType; if (dataType !== TYPE_FLOAT32 && dataType !== TYPE_FLOAT16 && !element.normalize && !element.asInt) { // new attribute type, based on the vertex format element type const isSignedType = dataType === TYPE_INT8 || dataType === TYPE_INT16 || dataType === TYPE_INT32; type = WebgpuShaderProcessorWGSL.floatAttributeToInt(type, isSignedType); Debug.assert(type !== null, `Attribute ${name} has a type that cannot be converted to int: ${dataType}`); } } // generates: @location(0) position : vec4f blockAttributes += ` @location(${location}) ${name}: ${type},\n`; // private global variable - this uses the original type blockPrivates += ` var<private> ${line};\n`; // copy input variable to the private variable - convert type if needed blockCopy += ` ${name} = ${originalType}(input.${name});\n`; } else { Debug.error(`Attribute ${name} is specified in the shader source, but is not defined in the shader definition, and so will be removed from the shader, as it cannot be used without a known semantic.`, { shaderDefinitionAttributes, shader }); } }); return ` struct VertexInput { ${blockAttributes} @builtin(vertex_index) vertexIndex : u32, // built-in vertex index @builtin(instance_index) instanceIndex : u32 // built-in instance index }; ${blockPrivates} var<private> pcVertexIndex: u32; var<private> pcInstanceIndex: u32; fn _pcCopyInputs(input: VertexInput) { ${blockCopy} pcVertexIndex = input.vertexIndex; pcInstanceIndex = input.instanceIndex; } `; } /** * Injects a call to _pcCopyInputs with the function's input parameter right after the opening * brace of a WGSL function marked with `@vertex` or `@fragment`. * * @param {string} src - The source string containing the WGSL code. * @param {Shader} shader - The shader. * @returns {string} - The modified source string. */ static copyInputs(src, shader) { // find @vertex or @fragment followed by the function signature and capture the input parameter name const match = src.match(ENTRY_FUNCTION); // check if match exists AND the parameter name (Group 2) was captured if (!match || !match[2]) { Debug.warn('No entry function found or input parameter name not captured.', { shader, src }); return src; } const inputName = match[2]; const braceIndex = match.index + match[0].length - 1; // Calculate the index of the '{' // inject the line right after the opening brace const beginning = src.slice(0, braceIndex + 1); const end = src.slice(braceIndex + 1); const lineToInject = `\n _pcCopyInputs(${inputName});`; return beginning + lineToInject + end; } static cutOut(src, start, end, replacement) { return src.substring(0, start) + replacement + src.substring(end); } } export { WebgpuShaderProcessorWGSL };