From 29567302c4d2366df826387f79f00f04d6e681f4 Mon Sep 17 00:00:00 2001 From: "chenmo.gl" Date: Thu, 23 Apr 2026 20:37:08 +0800 Subject: [PATCH] fix(shader): scan instance uniforms with macro awareness for raw GLSL _scanInstanceUniforms regex-matches uniform declarations without understanding #ifdef blocks. For raw GLSL paths the source still contains preprocessor directives at scan time, so uniforms inside inactive branches (e.g. renderer_JointMatrix under #ifdef RENDERER_HAS_SKIN) get matched even when they won't compile. This caused "GPU Instancing does not support array uniform" errors for plain MeshRenderer batching whenever a SkinnedMeshRenderer had previously registered renderer_JointMatrix under ShaderDataGroup.Renderer. Add _scanInstanceUniformsWithMacros that walks the source line-by-line with a branch stack for #ifdef/#ifndef/#else/#endif, delegating active lines to the original scanner. compilePlatformSource passes its active macro set; the ShaderLab path keeps using the plain scanner since ShaderMacroProcessor.evaluate already expands directives there. Also change the array-uniform fallback from deletion to keeping the declaration as a regular uniform, so stray matches never directly fail shader compilation. --- packages/core/src/shaderlib/ShaderFactory.ts | 73 ++++++++++++++++++-- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/packages/core/src/shaderlib/ShaderFactory.ts b/packages/core/src/shaderlib/ShaderFactory.ts index 907d91832..678eb1b55 100644 --- a/packages/core/src/shaderlib/ShaderFactory.ts +++ b/packages/core/src/shaderlib/ShaderFactory.ts @@ -164,7 +164,9 @@ export class ShaderFactory { let instanceLayout: InstanceBufferLayout | null = null; if (isGPUInstance) { - const injected = ShaderFactory.injectInstanceUBO(engine, noIncludeVertex, noIncludeFrag); + const activeMacros = new Set(); + for (let i = 0, len = shaderMacroList.length; i < len; i++) activeMacros.add(shaderMacroList[i].name); + const injected = ShaderFactory.injectInstanceUBO(engine, noIncludeVertex, noIncludeFrag, activeMacros); noIncludeVertex = injected.vertexSource; noIncludeFrag = injected.fragmentSource; instanceLayout = injected.instanceLayout; @@ -192,12 +194,18 @@ export class ShaderFactory { static injectInstanceUBO( engine: Engine, vertexSource: string, - fragmentSource: string + fragmentSource: string, + activeMacros?: Set ): { vertexSource: string; fragmentSource: string; instanceLayout: InstanceBufferLayout | null } { // 1. Scan & strip renderer uniforms from both stages, collect into fieldMap const fieldMap: Record = Object.create(null); - vertexSource = ShaderFactory._scanInstanceUniforms(vertexSource, fieldMap); - fragmentSource = ShaderFactory._scanInstanceUniforms(fragmentSource, fieldMap); + if (activeMacros) { + vertexSource = ShaderFactory._scanInstanceUniformsWithMacros(vertexSource, fieldMap, activeMacros); + fragmentSource = ShaderFactory._scanInstanceUniformsWithMacros(fragmentSource, fieldMap, activeMacros); + } else { + vertexSource = ShaderFactory._scanInstanceUniforms(vertexSource, fieldMap); + fragmentSource = ShaderFactory._scanInstanceUniforms(fragmentSource, fieldMap); + } // Fast empty check without allocating an array let hasField = false; @@ -299,10 +307,10 @@ export class ShaderFactory { if (isDerived === undefined && ShaderProperty._getShaderPropertyGroup(name) !== ShaderDataGroup.Renderer) return match; if (isDerived) return ""; - // Array uniforms not supported in instancing UBO, remove to fail shader compilation + // Array uniforms not supported in instancing UBO, keep as regular uniform if (arraySize) { Logger.error(`GPU Instancing does not support array uniform "${name}${arraySize}"`); - return ""; + return match; } // ModelMat is affine, store as mat3x4 (3 columns) to save 16 bytes per instance fieldMap[ShaderProperty.getByName(name)._uniqueId] = @@ -311,6 +319,59 @@ export class ShaderFactory { }); } + // Matches preprocessor directives at line start. Only `#ifdef / #ifndef / #else / #endif` are + // supported; `#if` with expressions is not recognized (such blocks are treated as always-active). + private static readonly _ifdefRegex = /^[ \t]*#ifdef\s+(\w+)/; + private static readonly _ifndefRegex = /^[ \t]*#ifndef\s+(\w+)/; + private static readonly _elseRegex = /^[ \t]*#else\b/; + private static readonly _endifRegex = /^[ \t]*#endif\b/; + + /** + * Scan with preprocessor awareness, for raw GLSL paths where `#ifdef` blocks are not yet + * expanded. Uniforms inside inactive branches are skipped. + */ + private static _scanInstanceUniformsWithMacros( + source: string, + fieldMap: Record, + activeMacros: Set + ): string { + // Preprocessor branch stack: each frame tracks whether current branch is active + const branchStack: boolean[] = [true]; + const lines = source.split("\n"); + + for (let i = 0, n = lines.length; i < n; i++) { + const line = lines[i]; + + let m = line.match(ShaderFactory._ifdefRegex); + if (m) { + const parentActive = branchStack[branchStack.length - 1]; + branchStack.push(parentActive && activeMacros.has(m[1])); + continue; + } + m = line.match(ShaderFactory._ifndefRegex); + if (m) { + const parentActive = branchStack[branchStack.length - 1]; + branchStack.push(parentActive && !activeMacros.has(m[1])); + continue; + } + if (ShaderFactory._elseRegex.test(line)) { + const parentActive = branchStack.length >= 2 ? branchStack[branchStack.length - 2] : true; + const currentActive = branchStack[branchStack.length - 1]; + branchStack[branchStack.length - 1] = parentActive && !currentActive; + continue; + } + if (ShaderFactory._endifRegex.test(line)) { + if (branchStack.length > 1) branchStack.pop(); + continue; + } + if (!branchStack[branchStack.length - 1]) continue; + + lines[i] = ShaderFactory._scanInstanceUniforms(line, fieldMap); + } + + return lines.join("\n"); + } + private static _buildLayout(engine: Engine, fieldMap: Record): InstanceBufferLayout { const maxUBOSize = engine._hardwareRenderer.maxUniformBlockSize; const std140Map = ShaderFactory._std140TypeInfoMap;