UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

643 lines (637 loc) 24 kB
import { BINDGROUP_MESH, semanticToLocation, SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT, SAMPLETYPE_FLOAT, TEXTUREDIMENSION_2D, TEXTUREDIMENSION_2D_ARRAY, TEXTUREDIMENSION_CUBE, TEXTUREDIMENSION_3D, TEXTUREDIMENSION_1D, TEXTUREDIMENSION_CUBE_ARRAY, SAMPLETYPE_INT, SAMPLETYPE_UINT, SAMPLETYPE_DEPTH, SAMPLETYPE_UNFILTERABLE_FLOAT, BINDGROUP_MESH_UB, uniformTypeToNameWGSL, uniformTypeToNameMapWGSL, bindGroupNames, UNIFORMTYPE_FLOAT, UNUSED_UNIFORM_NAME, TYPE_FLOAT32, TYPE_FLOAT16, TYPE_INT8, TYPE_INT16, TYPE_INT32 } from "../constants.js"; import { UniformFormat, UniformBufferFormat } from "../uniform-buffer-format.js"; import { BindGroupFormat, BindStorageBufferFormat, BindTextureFormat } from "../bind-group-format.js"; const KEYWORD = /^[ \t]*(attribute|varying|uniform)[\t ]+/gm; const KEYWORD_LINE = /^[ \t]*(attribute|varying|uniform)[ \t]*([^;]+)(;+)/gm; 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; const VARYING = /(?:@interpolate\([^)]*\)\s*)?([\w]+)\s*:\s*([\w<>]+)/; const MARKER = "@@@"; const ENTRY_FUNCTION = /(@vertex|@fragment)\s*fn\s+\w+\s*\(\s*(\w+)\s*:[\s\S]*?\{/; const FRAGMENT_BUILTINS = [ { wgslName: "position", wgslType: "vec4f", wgslBuiltin: "position", pcName: "pcPosition", isFallback: true }, { wgslName: "frontFacing", wgslType: "bool", wgslBuiltin: "front_facing", pcName: "pcFrontFacing" }, { wgslName: "sampleIndex", wgslType: "u32", wgslBuiltin: "sample_index", pcName: "pcSampleIndex" }, { wgslName: "primitiveIndex", wgslType: "u32", wgslBuiltin: "primitive_index", pcName: "pcPrimitiveIndex", requiresFeature: "supportsPrimitiveIndex" } ]; const VERTEX_BUILTINS = [ { wgslName: "vertexIndex", wgslType: "u32", wgslBuiltin: "vertex_index", pcName: "pcVertexIndex", isFallback: true }, { wgslName: "instanceIndex", wgslType: "u32", wgslBuiltin: "instance_index", pcName: "pcInstanceIndex" } ]; const detectUsedBuiltins = (builtins, source, entryInputName, device) => { return builtins.filter((b) => { if (b.requiresFeature && !device[b.requiresFeature]) return false; return new RegExp(`\\b(?:${b.pcName}|${entryInputName}\\.${b.wgslName})\\b`).test(source); }); }; const ensureNonEmptyStruct = (used, builtins, device, otherFieldsPresent) => { if (used.length === 0 && !otherFieldsPresent) { const fallback = builtins.find((b) => b.isFallback && (!b.requiresFeature || device[b.requiresFeature])); if (fallback) return [fallback]; } return used; }; const renderBuiltinStructFields = (used) => used.map((b) => ` @builtin(${b.wgslBuiltin}) ${b.wgslName} : ${b.wgslType}, `).join(""); const renderBuiltinPrivates = (used) => used.map((b) => ` var<private> ${b.pcName}: ${b.wgslType}; `).join(""); const renderBuiltinCopies = (used) => used.map((b) => ` ${b.pcName} = input.${b.wgslName}; `).join(""); 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 } }; const getTextureInfo = (baseType, componentType) => { const baseInfo = textureBaseInfo[baseType]; 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 }; }; const getTextureDeclarationType = (viewDimension, sampleType) => { 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: } } 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: } 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: } return `${baseTypeString}<${coreFormatString}>`; }; const wrappedArrayTypes = { "f32": "WrappedF32", "i32": "WrappedI32", "u32": "WrappedU32", "vec2f": "WrappedVec2F", "vec2i": "WrappedVec2I", "vec2u": "WrappedVec2U" }; const splitToWords = (line) => { line = line.replace(/\s+/g, " ").trim(); return line.split(/[\s:]+/); }; const UNIFORM_ARRAY_REGEX = /array<([^,]+),\s*([^>]+)>/; class UniformLine { ubName = null; arraySize = 0; constructor(line, shader) { this.line = line; const parts = splitToWords(line); if (parts.length < 2) { shader.failed = true; return; } this.name = parts[0]; this.type = parts.slice(1).join(" "); if (this.type.includes("array<")) { const match = UNIFORM_ARRAY_REGEX.exec(this.type); this.type = match[1].trim(); this.arraySize = Number(match[2]); if (isNaN(this.arraySize)) { shader.failed = true; } } } } const TEXTURE_REGEX = /^\s*var\s+(\w+)\s*:\s*(texture_\w+)(?:<(\w+)>)?;\s*$/; const STORAGE_TEXTURE_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*(texture_storage_2d|texture_storage_2d_array)<([\w\d_]+),\s*(\w+)>\s*;\s*$/; const STORAGE_BUFFER_REGEX = /^\s*var\s*<storage,\s*(read|write)?>\s*([\w\d_]+)\s*:\s*(.*)\s*;\s*$/; const EXTERNAL_TEXTURE_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*texture_external;\s*$/; const SAMPLER_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*(sampler|sampler_comparison)\s*;\s*$/; class ResourceLine { constructor(line, shader) { this.originalLine = line; this.line = line; this.isTexture = false; this.isSampler = false; this.isStorageTexture = false; this.isStorageBuffer = false; this.isExternalTexture = false; this.type = ""; this.matchedElements = []; const textureMatch = this.line.match(TEXTURE_REGEX); if (textureMatch) { this.name = textureMatch[1]; this.type = textureMatch[2]; this.textureFormat = textureMatch[3]; this.isTexture = true; this.matchedElements.push(...textureMatch); const info = getTextureInfo(this.type, this.textureFormat); this.textureDimension = info.viewDimension; this.sampleType = info.sampleType; } const storageTextureMatch = this.line.match(STORAGE_TEXTURE_REGEX); if (storageTextureMatch) { this.isStorageTexture = true; this.name = storageTextureMatch[1]; this.textureType = storageTextureMatch[2]; this.format = storageTextureMatch[3]; this.access = storageTextureMatch[4]; this.matchedElements.push(...storageTextureMatch); } const storageBufferMatch = this.line.match(STORAGE_BUFFER_REGEX); if (storageBufferMatch) { this.isStorageBuffer = true; this.accessMode = storageBufferMatch[1] || "none"; this.name = storageBufferMatch[2]; this.type = storageBufferMatch[3]; this.matchedElements.push(...storageBufferMatch); } const externalTextureMatch = this.line.match(EXTERNAL_TEXTURE_REGEX); if (externalTextureMatch) { this.name = externalTextureMatch[1]; this.isExternalTexture = true; this.matchedElements.push(...storageBufferMatch); } const samplerMatch = this.line.match(SAMPLER_REGEX); if (samplerMatch) { this.name = samplerMatch[1]; this.samplerType = samplerMatch[2]; this.isSampler = true; this.matchedElements.push(...samplerMatch); } if (this.matchedElements.length === 0) { 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; } } class WebgpuShaderProcessorWGSL { static run(device, shaderDefinition, shader) { const varyingMap = /* @__PURE__ */ new Map(); const vertexExtracted = WebgpuShaderProcessorWGSL.extract(shaderDefinition.vshader); const fragmentExtracted = WebgpuShaderProcessorWGSL.extract(shaderDefinition.fshader); const vertexInputName = vertexExtracted.src.match(ENTRY_FUNCTION)?.[2] ?? ""; const fragmentInputName = fragmentExtracted.src.match(ENTRY_FUNCTION)?.[2] ?? ""; const attributesMap = /* @__PURE__ */ new Map(); const attributesBlock = WebgpuShaderProcessorWGSL.processAttributes(vertexExtracted.attributes, shaderDefinition.attributes, attributesMap, shaderDefinition.processingOptions, shader, device, vertexExtracted.src, vertexInputName); const vertexVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(vertexExtracted.varyings, varyingMap, true, device); const fragmentVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(fragmentExtracted.varyings, varyingMap, false, device, fragmentExtracted.src, fragmentInputName); const concatUniforms = vertexExtracted.uniforms.concat(fragmentExtracted.uniforms); const uniforms = Array.from(new Set(concatUniforms)); const parsedUniforms = uniforms.map((line) => new UniformLine(line, shader)); const uniformsData = WebgpuShaderProcessorWGSL.processUniforms(device, parsedUniforms, shaderDefinition.processingOptions, shader); vertexExtracted.src = WebgpuShaderProcessorWGSL.renameUniformAccess(vertexExtracted.src, parsedUniforms); fragmentExtracted.src = WebgpuShaderProcessorWGSL.renameUniformAccess(fragmentExtracted.src, parsedUniforms); const parsedResources = WebgpuShaderProcessorWGSL.mergeResources(vertexExtracted.resources, fragmentExtracted.resources, shader); const resourcesData = WebgpuShaderProcessorWGSL.processResources(device, parsedResources, shaderDefinition.processingOptions, shader); const fOutput = WebgpuShaderProcessorWGSL.generateFragmentOutputStruct(fragmentExtracted.src, device.maxColorAttachments); vertexExtracted.src = WebgpuShaderProcessorWGSL.copyInputs(vertexExtracted.src, shader); fragmentExtracted.src = WebgpuShaderProcessorWGSL.copyInputs(fragmentExtracted.src, shader); const vBlock = `${attributesBlock} ${vertexVaryingsBlock} ${uniformsData.code} ${resourcesData.code} `; const vshader = vertexExtracted.src.replace(MARKER, vBlock); const fBlock = `${fragmentVaryingsBlock} ${fOutput} ${uniformsData.code} ${resourcesData.code} `; const fshader = fragmentExtracted.src.replace(MARKER, fBlock); return { vshader, fshader, attributes: attributesMap, meshUniformBufferFormat: uniformsData.meshUniformBufferFormat, meshBindGroupFormat: resourcesData.meshBindGroupFormat }; } // Extract required information from the shader source code. static extract(src) { const attributes = []; const varyings = []; const uniforms = []; const resources = []; let replacement = `${MARKER} `; let match; 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]); } src = WebgpuShaderProcessorWGSL.cutOut(src, match.index, KEYWORD_LINE.lastIndex, replacement); KEYWORD.lastIndex = match.index + replacement.length; replacement = ""; } while ((match = KEYWORD_RESOURCE.exec(src)) !== null) { resources.push(match[0]); src = WebgpuShaderProcessorWGSL.cutOut(src, match.index, KEYWORD_RESOURCE.lastIndex, replacement); KEYWORD_RESOURCE.lastIndex = match.index + replacement.length; replacement = ""; } return { src, attributes, varyings, uniforms, resources }; } static processUniforms(device, uniforms, processingOptions, shader) { const meshUniforms = []; uniforms.forEach((uniform) => { if (!processingOptions.hasUniform(uniform.name)) { uniform.ubName = "ub_mesh_ub"; const uniformType = uniformTypeToNameMapWGSL.get(uniform.type); const uniformFormat = new UniformFormat(uniform.name, uniformType, uniform.arraySize); meshUniforms.push(uniformFormat); } else { uniform.ubName = "ub_view"; } }); if (meshUniforms.length === 0) { meshUniforms.push(new UniformFormat(UNUSED_UNIFORM_NAME, UNIFORMTYPE_FLOAT)); } const meshUniformBufferFormat = new UniformBufferFormat(device, meshUniforms); let code = ""; processingOptions.uniformFormats.forEach((format, bindGroupIndex) => { if (format) { code += WebgpuShaderProcessorWGSL.getUniformShaderDeclaration(format, bindGroupIndex, 0); } }); if (meshUniformBufferFormat) { code += WebgpuShaderProcessorWGSL.getUniformShaderDeclaration(meshUniformBufferFormat, BINDGROUP_MESH_UB, 0); } return { code, meshUniformBufferFormat }; } static renameUniformAccess(source, uniforms) { uniforms.forEach((uniform) => { const srcName = `uniform.${uniform.name}`; const dstName = `${uniform.ubName}.${uniform.name}`; 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)); fragmentResources.forEach((fragmentResource) => { const existing = resources.find((resource) => resource.name === fragmentResource.name); if (existing) { if (!existing.equals(fragmentResource)) { shader.failed = true; } } else { resources.push(fragmentResource); } }); return resources; } static processResources(device, resources, processingOptions, shader) { const textureFormats = []; for (let i = 0; i < resources.length; i++) { const resource = resources[i]; if (resource.isTexture) { const sampler = resources[i + 1]; const hasSampler = sampler?.isSampler; const sampleType = resource.sampleType; const dimension = resource.textureDimension; textureFormats.push(new BindTextureFormat(resource.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, dimension, sampleType, hasSampler, hasSampler ? sampler.name : null)); 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); } } const meshBindGroupFormat = new BindGroupFormat(device, textureFormats); let code = ""; processingOptions.bindGroupFormats.forEach((format, bindGroupIndex) => { if (format) { code += WebgpuShaderProcessorWGSL.getTextureShaderDeclaration(format, bindGroupIndex); } }); code += WebgpuShaderProcessorWGSL.getTextureShaderDeclaration(meshBindGroupFormat, BINDGROUP_MESH); return { code, meshBindGroupFormat }; } static getUniformShaderDeclaration(ubFormat, bindGroup, bindIndex) { const name = bindGroupNames[bindGroup]; const structName = `struct_ub_${name}`; let code = `struct ${structName} { `; ubFormat.uniforms.forEach((uniform) => { let typeString = uniformTypeToNameWGSL[uniform.type][0]; if (uniform.count > 0) { if (wrappedArrayTypes.hasOwnProperty(typeString)) { typeString = wrappedArrayTypes[typeString]; } code += ` ${uniform.shortName}: array<${typeString}, ${uniform.count}>, `; } else { code += ` ${uniform.shortName}: ${typeString}, `; } }); code += "};\n"; code += `@group(${bindGroup}) @binding(${bindIndex}) var<uniform> ub_${name} : ${structName}; `; return code; } static getTextureShaderDeclaration(format, bindGroup) { let code = ""; format.textureFormats.forEach((format2) => { const textureTypeName = getTextureDeclarationType(format2.textureDimension, format2.sampleType); code += `@group(${bindGroup}) @binding(${format2.slot}) var ${format2.name}: ${textureTypeName}; `; if (format2.hasSampler) { const samplerName = format2.sampleType === SAMPLETYPE_DEPTH ? "sampler_comparison" : "sampler"; code += `@group(${bindGroup}) @binding(${format2.slot + 1}) var ${format2.samplerName}: ${samplerName}; `; } }); format.storageBufferFormats.forEach((format2) => { const access = format2.readOnly ? "read" : "read_write"; code += `@group(${bindGroup}) @binding(${format2.slot}) var<storage, ${access}> ${format2.name} : ${format2.format}; `; }); return code; } static processVaryings(varyingLines, varyingMap, isVertex, device, source = "", entryInputName = "") { let block = ""; let blockPrivates = ""; let blockCopy = ""; varyingLines.forEach((line, index) => { const match = line.match(VARYING); if (match) { const name = match[1]; const type = match[2]; if (isVertex) { varyingMap.set(name, index); } else { index = varyingMap.get(name); } block += ` @location(${index}) ${line}, `; if (!isVertex) { blockPrivates += ` var<private> ${name}: ${type}; `; blockCopy += ` ${name} = input.${name}; `; } } }); if (isVertex) { block += " @builtin(position) position : vec4f,\n"; return ` struct VertexOutput { ${block} }; `; } const usedBuiltins = ensureNonEmptyStruct( detectUsedBuiltins(FRAGMENT_BUILTINS, source, entryInputName, device), FRAGMENT_BUILTINS, device, block.length > 0 ); block += renderBuiltinStructFields(usedBuiltins); return ` struct FragmentInput { ${block} }; ${renderBuiltinPrivates(usedBuiltins)} ${blockPrivates} // function to copy inputs (varyings) to private global variables fn _pcCopyInputs(input: FragmentInput) { ${blockCopy} ${renderBuiltinCopies(usedBuiltins)} } `; } static generateFragmentOutputStruct(src, numRenderTargets) { let structCode = "struct FragmentOutput {\n"; const colorName = (i) => `color${i > 0 ? i : ""}`; for (let i = 0; i < numRenderTargets; i++) { const name = colorName(i); if (src.search(new RegExp(`\\.${name}\\s*=`)) !== -1) { structCode += ` @location(${i}) ${name} : pcOutType${i}, `; } } const needsFragDepth = src.search(/\.fragDepth\s*=/) !== -1; if (needsFragDepth) { structCode += " @builtin(frag_depth) fragDepth : f32\n"; } return `${structCode}}; `; } // convert a float attribute type to matching signed or unsigned int type // for example: vec4f -> vec4u, f32 -> u32 static floatAttributeToInt(type, signed) { const longToShortMap = { "f32": "f32", "vec2<f32>": "vec2f", "vec3<f32>": "vec3f", "vec4<f32>": "vec4f" }; const shortType = longToShortMap[type] || 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, device, source = "", entryInputName = "") { 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]; usedLocations[location] = semantic; attributesMap.set(location, name); const element = processingOptions.getVertexElement(semantic); if (element) { const dataType = element.dataType; if (dataType !== TYPE_FLOAT32 && dataType !== TYPE_FLOAT16 && !element.normalize && !element.asInt) { const isSignedType = dataType === TYPE_INT8 || dataType === TYPE_INT16 || dataType === TYPE_INT32; type = WebgpuShaderProcessorWGSL.floatAttributeToInt(type, isSignedType); } } blockAttributes += ` @location(${location}) ${name}: ${type}, `; blockPrivates += ` var<private> ${line}; `; blockCopy += ` ${name} = ${originalType}(input.${name}); `; } else { } }); const usedBuiltins = ensureNonEmptyStruct( detectUsedBuiltins(VERTEX_BUILTINS, source, entryInputName, device), VERTEX_BUILTINS, device, blockAttributes.length > 0 ); return ` struct VertexInput { ${blockAttributes} ${renderBuiltinStructFields(usedBuiltins)} }; ${blockPrivates} ${renderBuiltinPrivates(usedBuiltins)} fn _pcCopyInputs(input: VertexInput) { ${blockCopy} ${renderBuiltinCopies(usedBuiltins)} } `; } static copyInputs(src, shader) { const match = src.match(ENTRY_FUNCTION); if (!match || !match[2]) { return src; } const inputName = match[2]; const braceIndex = match.index + match[0].length - 1; const beginning = src.slice(0, braceIndex + 1); const end = src.slice(braceIndex + 1); const lineToInject = ` _pcCopyInputs(${inputName});`; return beginning + lineToInject + end; } static cutOut(src, start, end, replacement) { return src.substring(0, start) + replacement + src.substring(end); } } export { WebgpuShaderProcessorWGSL };