From 80e6e3ebb37511350b61e004ce3a3be69db4e180 Mon Sep 17 00:00:00 2001 From: "shensi.zxd" Date: Fri, 27 Mar 2026 00:33:21 +0800 Subject: [PATCH] fix(shader-lab): handle global struct-typed variables and simplify macro scanning - Suppress `uniform` output for global struct-typed variables (e.g. `Varyings o;`) - Register global struct vars in both per-function and cross-stage maps - Unify macro member access scanning into callback-based _forEachMacroMemberAccess - Add registerStructVar() encapsulation in VisitorContext - Add Cocos VSOutput pattern test (global-varying-var) --- .../shader-lab/src/codeGen/CodeGenVisitor.ts | 14 +++- .../shader-lab/src/codeGen/GLESVisitor.ts | 79 +++++++++++++++---- .../shader-lab/src/codeGen/VisitorContext.ts | 5 ++ tests/src/shader-lab/ShaderLab.test.ts | 32 ++++++++ .../shaders/global-varying-var.shader | 42 ++++++++++ 5 files changed, 157 insertions(+), 15 deletions(-) create mode 100644 tests/src/shader-lab/shaders/global-varying-var.shader diff --git a/packages/shader-lab/src/codeGen/CodeGenVisitor.ts b/packages/shader-lab/src/codeGen/CodeGenVisitor.ts index 1870a0e88..f4d560c98 100644 --- a/packages/shader-lab/src/codeGen/CodeGenVisitor.ts +++ b/packages/shader-lab/src/codeGen/CodeGenVisitor.ts @@ -244,7 +244,19 @@ export abstract class CodeGenVisitor { const children = node.children; const fullType = children[0]; if (fullType instanceof ASTNode.FullySpecifiedType && fullType.typeSpecifier.isCustom) { - VisitorContext.context.referenceGlobal(fullType.type, ESymbolType.STRUCT); + const context = VisitorContext.context; + const typeLexeme = fullType.typeSpecifier.lexeme; + const role = context.getStructRole(typeLexeme); + if (role) { + // Global variable of a varying/attribute/mrt struct type (e.g. "Varyings o;"). + // Don't output as uniform; register the variable in struct var maps instead. + const ident = children[1]; + if (ident instanceof BaseToken) { + context.registerStructVar(ident.lexeme, role); + } + return ""; + } + context.referenceGlobal(fullType.type, ESymbolType.STRUCT); } return `uniform ${this.defaultCodeGen(children)}`; } diff --git a/packages/shader-lab/src/codeGen/GLESVisitor.ts b/packages/shader-lab/src/codeGen/GLESVisitor.ts index a530a06f8..d7596ae9a 100644 --- a/packages/shader-lab/src/codeGen/GLESVisitor.ts +++ b/packages/shader-lab/src/codeGen/GLESVisitor.ts @@ -45,7 +45,7 @@ export abstract class GLESVisitor extends CodeGenVisitor { // Build combined _globalStructVarMap from both entry functions before per-stage processing. // This must happen here because vertex runs first and doesn't yet know fragment's variables. - this._buildGlobalStructVarMap(vertexEntry, fragmentEntry, shaderData, context); + this._buildGlobalStructVarMap(vertexEntry, fragmentEntry, shaderData, outerGlobalMacroDeclarations, context); return { vertex: this._vertexMain(vertexEntry, shaderData, outerGlobalMacroDeclarations), @@ -215,6 +215,7 @@ export abstract class GLESVisitor extends CodeGenVisitor { vertexEntry: string, fragmentEntry: string, data: ShaderData, + globalMacros: ASTNode.GlobalDeclaration[], context: VisitorContext ): void { const map = context._globalStructVarMap; @@ -266,6 +267,33 @@ export abstract class GLESVisitor extends CodeGenVisitor { } this._collectStructVarsFromBody(fnNode.statements, structTypeRoles, map); } + + // Also scan global macros for root variable names that might be global struct variables. + // e.g. #define VSOutput_worldPos o.v_worldPos → root "o" → look up in symbol table + let hasRoles = false; + for (const _ in structTypeRoles) { + hasRoles = true; + break; + } + if (hasRoles) { + const checked = new Set(); + const symOut: SymbolInfo[] = []; + this._forEachMacroMemberAccess(globalMacros, (rootName) => { + if (map[rootName] || checked.has(rootName)) return; + checked.add(rootName); + lookupSymbol.set(rootName, ESymbolType.VAR); + symbolTable.getSymbols(lookupSymbol, true, symOut); + for (const sym of symOut) { + if (sym.dataType) { + const role = structTypeRoles[sym.dataType.typeLexeme]; + if (role) { + map[rootName] = role; + break; + } + } + } + }); + } } private _collectStructVarsFromBody( @@ -296,18 +324,36 @@ export abstract class GLESVisitor extends CodeGenVisitor { */ private _registerGlobalMacroReferences(globalMacros: ASTNode.GlobalDeclaration[], context: VisitorContext): void { const map = context._globalStructVarMap; - if (!Object.keys(map).length) return; + let hasEntries = false; + for (const _ in map) { + hasEntries = true; + break; + } + if (!hasEntries) return; + this._forEachMacroMemberAccess(globalMacros, (rootName, propName) => { + const role = map[rootName]; + if (role) context.referenceStructPropByName(role, propName); + }); + } + + /** + * Traverse global macro declarations, extracting member access patterns (e.g. "o.v_uv") + * and invoking the callback with (rootName, propName) for each match. + */ + private _forEachMacroMemberAccess( + macros: ASTNode.GlobalDeclaration[], + callback: (rootName: string, propName: string) => void + ): void { const reg = CodeGenVisitor._memberAccessReg; - for (const macro of globalMacros) { - this._scanMacroMemberAccess(macro.children, reg, map, context); + for (const macro of macros) { + this._walkMacroChildren(macro.children, reg, callback); } } - private _scanMacroMemberAccess( + private _walkMacroChildren( children: NodeChild[], reg: RegExp, - map: Record, - context: VisitorContext + callback: (rootName: string, propName: string) => void ): void { for (const child of children) { if (child instanceof BaseToken && child.type === Keyword.MACRO_DEFINE_EXPRESSION) { @@ -317,19 +363,20 @@ export abstract class GLESVisitor extends CodeGenVisitor { reg.lastIndex = 0; let match: RegExpExecArray | null; while ((match = reg.exec(value)) !== null) { - const role = map[match[1]]; - if (role) context.referenceStructPropByName(role, match[2]); + callback(match[1], match[2]); } } else if (child instanceof TreeNode) { - this._scanMacroMemberAccess(child.children, reg, map, context); + this._walkMacroChildren(child.children, reg, callback); } } } private _getGlobalSymbol(out: ICodeSegment[]): void { - const { _referencedGlobals } = VisitorContext.context; + const context = VisitorContext.context; + const { _referencedGlobals } = context; - const lastLength = Object.keys(_referencedGlobals).length; + let lastLength = 0; + for (const _ in _referencedGlobals) lastLength++; if (lastLength === 0) return; for (const ident in _referencedGlobals) { @@ -339,7 +386,9 @@ export abstract class GLESVisitor extends CodeGenVisitor { const symbols = _referencedGlobals[ident]; for (let i = 0; i < symbols.length; i++) { const sm = symbols[i]; - const text = sm.astNode.codeGen(this) + (sm.type === ESymbolType.VAR ? ";" : ""); + const codeGenResult = sm.astNode.codeGen(this); + if (!codeGenResult) continue; + const text = codeGenResult + (sm.type === ESymbolType.VAR ? ";" : ""); if (!sm.isInMacroBranch) { out.push({ text, @@ -349,7 +398,9 @@ export abstract class GLESVisitor extends CodeGenVisitor { } } - if (Object.keys(_referencedGlobals).length !== lastLength) { + let newLength = 0; + for (const _ in _referencedGlobals) newLength++; + if (newLength !== lastLength) { this._getGlobalSymbol(out); } } diff --git a/packages/shader-lab/src/codeGen/VisitorContext.ts b/packages/shader-lab/src/codeGen/VisitorContext.ts index 3ac2a3eb3..1c1972c0d 100644 --- a/packages/shader-lab/src/codeGen/VisitorContext.ts +++ b/packages/shader-lab/src/codeGen/VisitorContext.ts @@ -72,6 +72,11 @@ export class VisitorContext { if (this.isMRTStruct(typeLexeme)) return "mrt"; } + registerStructVar(varName: string, role: "varying" | "attribute" | "mrt"): void { + this._structVarMap[varName] = role; + this._globalStructVarMap[varName] = role; + } + referenceStructPropByName(role: "varying" | "attribute" | "mrt", propName: string): void { const list = role === "varying" ? this.varyingList : role === "attribute" ? this.attributeList : this.mrtList; const refList = diff --git a/tests/src/shader-lab/ShaderLab.test.ts b/tests/src/shader-lab/ShaderLab.test.ts index d3d431a4d..e68a9c3c0 100644 --- a/tests/src/shader-lab/ShaderLab.test.ts +++ b/tests/src/shader-lab/ShaderLab.test.ts @@ -327,4 +327,36 @@ describe("ShaderLab", async () => { expect(fragment).to.contain("dot"); expect(fragment).to.contain("texture2D"); }); + + it("global-varying-var (Cocos VSOutput pattern: global Varyings var with #define macros)", async () => { + const shaderSource = await readFile("./shaders/global-varying-var.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + + // Verify verbose mode: global "Varyings o;" should not produce "uniform Varyings o;" + // and should not duplicate varying declarations. + const shader = shaderLabVerbose._parseShaderSource(shaderSource); + const passSource = shader.subShaders[0].passes[0]; + const { vertex, fragment } = shaderLabVerbose._parseShaderPass( + passSource.contents, + passSource.vertexEntry, + passSource.fragmentEntry, + 0, + "" + )!; + + expect(vertex).to.be.a("string").and.not.empty; + expect(fragment).to.be.a("string").and.not.empty; + + // No "uniform Varyings o;" in output + expect(vertex).to.not.contain("uniform Varyings"); + expect(fragment).to.not.contain("uniform Varyings"); + + // Macros should be transformed: "o.v_worldPos" → "v_worldPos" + expect(vertex).to.contain("#define VSOutput_worldPos v_worldPos"); + expect(vertex).to.contain("#define VSOutput_worldNormal v_normal.xyz"); + + // No duplicate varying declarations + const varyingMatches = vertex.match(/varying vec3 v_worldPos/g); + expect(varyingMatches).to.have.lengthOf(1); + }); }); diff --git a/tests/src/shader-lab/shaders/global-varying-var.shader b/tests/src/shader-lab/shaders/global-varying-var.shader new file mode 100644 index 000000000..bd6a5cf47 --- /dev/null +++ b/tests/src/shader-lab/shaders/global-varying-var.shader @@ -0,0 +1,42 @@ +Shader "global-varying-var-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec3 POSITION; vec3 NORMAL; vec2 TEXCOORD_0; }; + struct Varyings { vec3 v_worldPos; vec4 v_normal; vec2 v_uv; vec4 v_shadowBiasAndProbeId; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + vec3 u_lightDir; + + #define VSOutput_worldPos o.v_worldPos + #define VSOutput_worldNormal o.v_normal.xyz + #define VSOutput_faceSideSign o.v_normal.w + #define VSOutput_texcoord o.v_uv + #define VSOutput_shadowBias o.v_shadowBiasAndProbeId.xy + + Varyings o; + + Varyings vert(Attributes input) { + mat4 matWorld = renderer_MVPMat; + vec4 pos = matWorld * vec4(input.POSITION, 1.0); + VSOutput_worldPos = pos.xyz; + VSOutput_worldNormal = input.NORMAL; + VSOutput_faceSideSign = 1.0; + VSOutput_texcoord = input.TEXCOORD_0; + VSOutput_shadowBias = vec2(0.0, 0.0); + gl_Position = pos; + return o; + } + + void frag(Varyings v) { + vec3 N = normalize(v.v_normal.xyz); + float NdotL = dot(N, u_lightDir); + gl_FragColor = texture2D(u_texture, v.v_uv) * NdotL; + } + } + } +}