Skip to content

Commit a22dd50

Browse files
committed
feat(@typegpu/gl): Generate GLSL function signatures
1 parent 8517cba commit a22dd50

9 files changed

Lines changed: 337 additions & 64 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import glslGenerator from './glslGenerator.ts';
2+
3+
export function glOptions() {
4+
return {
5+
unstable_shaderGenerator: glslGenerator,
6+
};
7+
}

packages/typegpu-gl/src/glslGenerator.ts

Lines changed: 177 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
import { d, ShaderGenerator, WgslGenerator } from 'typegpu';
1+
import { NodeTypeCatalog as NODE } from 'tinyest';
2+
import type { Return } from 'tinyest';
3+
import tgpu, { d, ShaderGenerator, WgslGenerator } from 'typegpu';
4+
5+
type ResolutionCtx = ShaderGenerator.ResolutionCtx;
6+
7+
const UnknownData: typeof ShaderGenerator.UnknownData = ShaderGenerator.UnknownData;
28

39
// ----------
410
// WGSL → GLSL type name mapping
511
// ----------
612

713
const WGSL_TO_GLSL_TYPE: Record<string, string> = {
14+
void: 'void',
815
f32: 'float',
916
u32: 'uint',
1017
i32: 'int',
@@ -37,13 +44,43 @@ export function translateWgslTypeToGlsl(wgslType: string): string {
3744
return WGSL_TO_GLSL_TYPE[wgslType] ?? wgslType;
3845
}
3946

47+
/**
48+
* Resolves a struct and adds its declaration to the resolution context.
49+
* @param ctx - The resolution context.
50+
* @param struct - The struct to resolve.
51+
*
52+
* @returns The resolved struct name.
53+
*/
54+
function resolveStruct(ctx: ResolutionCtx, struct: d.WgslStruct) {
55+
const id = ctx.makeUniqueIdentifier(ShaderGenerator.getName(struct), 'global');
56+
57+
ctx.addDeclaration(`\
58+
struct ${id} {
59+
${Object.entries(struct.propTypes)
60+
.map(([prop, type]) => ` ${ctx.resolve(type).value} ${prop};\n`)
61+
.join('')}\
62+
};`);
63+
64+
return id;
65+
}
66+
67+
const gl_PositionSnippet = tgpu['~unstable'].rawCodeSnippet('gl_Position', d.vec4f, 'private');
68+
69+
interface EntryFnState {
70+
structPropToVarMap: Record<string, string>;
71+
outVars: { varName: string; propName: string }[];
72+
}
73+
4074
/**
4175
* A GLSL ES 3.0 shader generator that extends WgslGenerator.
4276
* Overrides `dataType` to emit GLSL type names instead of WGSL ones,
4377
* and overrides variable declaration emission to use `type name = rhs` syntax.
4478
*/
4579
export class GlslGenerator extends WgslGenerator {
46-
public override typeAnnotation(data: d.BaseData): string {
80+
#functionType: ShaderGenerator.TgpuShaderStage | 'normal' | undefined;
81+
#entryFnState: EntryFnState | undefined;
82+
83+
override typeAnnotation(data: d.BaseData): string {
4784
// For WGSL identity types (scalars, vectors, common matrices), map to GLSL directly.
4885
if (!d.isLooseData(data)) {
4986
const glslName = WGSL_TO_GLSL_TYPE[data.type];
@@ -52,20 +89,153 @@ export class GlslGenerator extends WgslGenerator {
5289
}
5390
}
5491

92+
if (d.isWgslStruct(data)) {
93+
return resolveStruct(this.ctx, data);
94+
}
95+
5596
// For all other types (structs, arrays, etc.) delegate to WGSL resolution.
5697
return super.typeAnnotation(data);
5798
}
5899

59-
protected override emitVarDecl(
60-
pre: string,
100+
override _emitVarDecl(
61101
_keyword: 'var' | 'let' | 'const',
62102
name: string,
63103
dataType: d.BaseData | ShaderGenerator.UnknownData,
64104
rhsStr: string,
65105
): string {
66-
const glslTypeName =
67-
dataType !== ShaderGenerator.UnknownData ? this.typeAnnotation(dataType) : 'auto';
68-
return `${pre}${glslTypeName} ${name} = ${rhsStr};`;
106+
const glslTypeName = dataType !== UnknownData ? this.ctx.resolve(dataType).value : 'auto';
107+
return `${this.ctx.pre}${glslTypeName} ${name} = ${rhsStr};`;
108+
}
109+
110+
override _return(statement: Return): string {
111+
const exprNode = statement[1];
112+
113+
if (exprNode === undefined) {
114+
// Default behavior
115+
return super._return(statement);
116+
}
117+
118+
if (this.#functionType !== 'normal') {
119+
// oxlint-disable-next-line no-non-null-assertion
120+
const entryFnState = this.#entryFnState!;
121+
const expectedReturnType = this.ctx.topFunctionReturnType;
122+
123+
if (typeof exprNode === 'object' && exprNode[0] === NODE.objectExpr) {
124+
const transformed = Object.entries(exprNode[1]).map(([prop, rhsNode]) => {
125+
let name: string | undefined = entryFnState.structPropToVarMap[prop];
126+
if (name === undefined) {
127+
if (
128+
prop === '$position' ||
129+
(expectedReturnType &&
130+
d.isWgslStruct(expectedReturnType) &&
131+
expectedReturnType.propTypes[prop] === d.builtin.position)
132+
) {
133+
name = 'gl_Position';
134+
} else {
135+
name = this.ctx.makeUniqueIdentifier(prop, 'global');
136+
entryFnState.outVars.push({ varName: name, propName: prop });
137+
}
138+
entryFnState.structPropToVarMap[prop] = name;
139+
}
140+
const rhsExpr = this._expression(rhsNode);
141+
const type = rhsExpr.dataType as d.BaseData;
142+
143+
const snippet = tgpu['~unstable'].rawCodeSnippet(name, type as d.AnyData, 'private');
144+
145+
return {
146+
name,
147+
snippet,
148+
assignment: [NODE.assignmentExpr, name, '=', rhsNode],
149+
} as const;
150+
});
151+
152+
const block = super._block(
153+
[NODE.block, [...transformed.map((t) => t.assignment), [NODE.return]]],
154+
Object.fromEntries(
155+
transformed.map(({ name, snippet }) => {
156+
return [name, snippet.$] as const;
157+
}),
158+
),
159+
);
160+
161+
return `${this.ctx.pre}${block}`;
162+
} else {
163+
// Resolving the expression to inspect it's type
164+
// We will resolve it again as part of the modifed statement
165+
const expr = expectedReturnType
166+
? this._typedExpression(exprNode, expectedReturnType)
167+
: this._expression(exprNode);
168+
169+
if (expr.dataType === UnknownData) {
170+
// Unknown data type, don't know what to do
171+
return super._return(statement);
172+
}
173+
174+
if (expr.dataType.type.startsWith('vec')) {
175+
const block = super._block(
176+
[NODE.block, [[NODE.assignmentExpr, 'gl_Position', '=', exprNode], [NODE.return]]],
177+
{ gl_Position: gl_PositionSnippet.$ },
178+
);
179+
180+
return `${this.ctx.pre}${block}`;
181+
}
182+
}
183+
}
184+
185+
return super._return(statement);
186+
}
187+
188+
override functionDefinition(options: ShaderGenerator.FunctionDefinitionOptions): string {
189+
if (options.functionType !== 'normal') {
190+
this.ctx.reserveIdentifier('gl_Position', 'global');
191+
}
192+
193+
// Function body
194+
let lastFunctionType = this.#functionType;
195+
this.#functionType = options.functionType;
196+
if (options.functionType !== 'normal') {
197+
if (this.#entryFnState) {
198+
throw new Error('Cannot nest entry functions');
199+
}
200+
this.#entryFnState = { structPropToVarMap: {}, outVars: [] };
201+
}
202+
203+
try {
204+
const body = this._block(options.body);
205+
206+
// Only after generating the body can we determine the return type
207+
const returnType = options.determineReturnType();
208+
209+
if (options.functionType !== 'normal') {
210+
// oxlint-disable-next-line no-non-null-assertion
211+
const entryFnState = this.#entryFnState!;
212+
if (d.isWgslStruct(returnType)) {
213+
for (const { varName, propName } of entryFnState.outVars) {
214+
const dataType = returnType.propTypes[propName];
215+
if (dataType && d.isDecorated(dataType)) {
216+
const location = (dataType.attribs as d.AnyAttribute[]).find(
217+
(a) => a.type === '@location',
218+
)?.params[0];
219+
this.ctx.addDeclaration(`layout(location = ${location}) out ${varName};`);
220+
}
221+
}
222+
}
223+
return `void main() ${body}`;
224+
}
225+
226+
const argList = options.args
227+
// Stripping out unused arguments in entry functions
228+
.filter((arg) => arg.used || options.functionType === 'normal')
229+
.map((arg) => {
230+
return `${this.ctx.resolve(arg.decoratedType).value} ${arg.name}`;
231+
})
232+
.join(', ');
233+
234+
return `${this.ctx.resolve(returnType).value} ${options.name}(${argList}) ${body}`;
235+
} finally {
236+
this.#functionType = lastFunctionType;
237+
this.#entryFnState = undefined;
238+
}
69239
}
70240
}
71241

packages/typegpu-gl/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { initWithGL } from './initWithGL.ts';
22
export { initWithGLFallback } from './initWithGLFallback.ts';
3+
export { glOptions } from './glOptions.ts';

0 commit comments

Comments
 (0)