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)
This commit is contained in:
shensi.zxd
2026-03-27 00:33:21 +08:00
parent 7b18aef47c
commit 80e6e3ebb3
5 changed files with 157 additions and 15 deletions

View File

@@ -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(<string>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(<string>fullType.type, ESymbolType.STRUCT);
}
return `uniform ${this.defaultCodeGen(children)}`;
}

View File

@@ -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<string>();
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<string, "varying" | "attribute" | "mrt">,
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);
}
}

View File

@@ -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 =

View File

@@ -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);
});
});

View File

@@ -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;
}
}
}
}