From 24cb637a1812663f3f88ebc72cfdf9307fe568e0 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Fri, 17 Apr 2026 20:21:01 +0200 Subject: [PATCH 01/14] feat: Make "Triangle" example work with the WebGL fallback --- apps/typegpu-docs/package.json | 1 + .../components/stackblitz/openInStackBlitz.ts | 48 +-- .../examples/simple/triangle-gl/index.html | 1 + .../src/examples/simple/triangle-gl/index.ts | 58 +++ .../src/examples/simple/triangle-gl/meta.json | 6 + .../src/utils/examples/sandboxModules.ts | 4 + packages/typegpu-gl/src/glslGenerator.ts | 334 +++++++++++----- packages/typegpu-gl/src/tgpuRootWebGL.ts | 357 ++++++------------ packages/typegpu/src/tgpuUnstable.ts | 3 + pnpm-lock.yaml | 3 + 10 files changed, 450 insertions(+), 365 deletions(-) create mode 100644 apps/typegpu-docs/src/examples/simple/triangle-gl/index.html create mode 100644 apps/typegpu-docs/src/examples/simple/triangle-gl/index.ts create mode 100644 apps/typegpu-docs/src/examples/simple/triangle-gl/meta.json diff --git a/apps/typegpu-docs/package.json b/apps/typegpu-docs/package.json index b81cd000d9..99e67e2059 100644 --- a/apps/typegpu-docs/package.json +++ b/apps/typegpu-docs/package.json @@ -30,6 +30,7 @@ "@tailwindcss/vite": "^4.1.18", "@typegpu/color": "workspace:*", "@typegpu/geometry": "workspace:*", + "@typegpu/gl": "workspace:*", "@typegpu/noise": "workspace:*", "@typegpu/radiance-cascades": "workspace:*", "@typegpu/react": "workspace:*", diff --git a/apps/typegpu-docs/src/components/stackblitz/openInStackBlitz.ts b/apps/typegpu-docs/src/components/stackblitz/openInStackBlitz.ts index 2eb5d83870..995ae2c099 100644 --- a/apps/typegpu-docs/src/components/stackblitz/openInStackBlitz.ts +++ b/apps/typegpu-docs/src/components/stackblitz/openInStackBlitz.ts @@ -2,6 +2,7 @@ import StackBlitzSDK from '@stackblitz/sdk'; import { parse } from 'yaml'; import { type } from 'arktype'; import typegpuColorPackageJson from '@typegpu/color/package.json' with { type: 'json' }; +import typegpuGlPackageJson from '@typegpu/gl/package.json' with { type: 'json' }; import typegpuNoisePackageJson from '@typegpu/noise/package.json' with { type: 'json' }; import typegpuSdfPackageJson from '@typegpu/sdf/package.json' with { type: 'json' }; import typegpuThreePackageJson from '@typegpu/three/package.json' with { type: 'json' }; @@ -118,29 +119,30 @@ ${example.htmlFile.content} "@tailwindcss/vite": "^4.1.18" }, "dependencies": ${JSON.stringify( - { - typegpu: `^${typegpuPackageJson.version}`, - 'unplugin-typegpu': `^${unpluginPackageJson.version}`, - 'wgpu-matrix': pnpmWorkspaceYaml.catalogs.example['wgpu-matrix'], - '@loaders.gl/core': typegpuDocsPackageJson.dependencies['@loaders.gl/core'], - '@loaders.gl/obj': typegpuDocsPackageJson.dependencies['@loaders.gl/obj'], - '@loaders.gl/gltf': typegpuDocsPackageJson.dependencies['@loaders.gl/gltf'], - three: pnpmWorkspaceYaml.catalogs.example.three, - '@typegpu/noise': typegpuNoisePackageJson.version, - '@typegpu/color': typegpuColorPackageJson.version, - '@typegpu/sdf': typegpuSdfPackageJson.version, - '@typegpu/three': typegpuThreePackageJson.version, - ...(example.usedApis.includes('@typegpu/react') - ? { - '@typegpu/react': typegpuReactPackageJson.version, - react: '^19.2.0', - 'react-dom': '^19.2.0', - } - : {}), - }, - undefined, - 2, - ).replaceAll('\n', '\n ')} + { + typegpu: `^${typegpuPackageJson.version}`, + 'unplugin-typegpu': `^${unpluginPackageJson.version}`, + 'wgpu-matrix': pnpmWorkspaceYaml.catalogs.example['wgpu-matrix'], + '@loaders.gl/core': typegpuDocsPackageJson.dependencies['@loaders.gl/core'], + '@loaders.gl/obj': typegpuDocsPackageJson.dependencies['@loaders.gl/obj'], + '@loaders.gl/gltf': typegpuDocsPackageJson.dependencies['@loaders.gl/gltf'], + three: pnpmWorkspaceYaml.catalogs.example.three, + '@typegpu/noise': typegpuNoisePackageJson.version, + '@typegpu/color': typegpuColorPackageJson.version, + '@typegpu/gl': typegpuGlPackageJson.version, + '@typegpu/sdf': typegpuSdfPackageJson.version, + '@typegpu/three': typegpuThreePackageJson.version, + ...(example.usedApis.includes('@typegpu/react') + ? { + '@typegpu/react': typegpuReactPackageJson.version, + react: '^19.2.0', + 'react-dom': '^19.2.0', + } + : {}), + }, + undefined, + 2, + ).replaceAll('\n', '\n ')} }`, 'vite.config.js': `\ import { defineConfig } from 'vite'; diff --git a/apps/typegpu-docs/src/examples/simple/triangle-gl/index.html b/apps/typegpu-docs/src/examples/simple/triangle-gl/index.html new file mode 100644 index 0000000000..aa8cc321b3 --- /dev/null +++ b/apps/typegpu-docs/src/examples/simple/triangle-gl/index.html @@ -0,0 +1 @@ + diff --git a/apps/typegpu-docs/src/examples/simple/triangle-gl/index.ts b/apps/typegpu-docs/src/examples/simple/triangle-gl/index.ts new file mode 100644 index 0000000000..b8e469074f --- /dev/null +++ b/apps/typegpu-docs/src/examples/simple/triangle-gl/index.ts @@ -0,0 +1,58 @@ +import tgpu, { d, std } from 'typegpu'; +import { initWithGL } from '@typegpu/gl'; + +// Constants and helper functions + +const purple = d.vec4f(0.769, 0.392, 1, 1); +const blue = d.vec4f(0.114, 0.447, 0.941, 1); + +const getGradientColor = (ratio: number) => { + 'use gpu'; + return std.mix(purple, blue, ratio); +}; + +const pos = tgpu.const(d.arrayOf(d.vec2f, 3), [ + d.vec2f(0.0, 0.5), + d.vec2f(-0.5, -0.5), + d.vec2f(0.5, -0.5), +]); + +const uv = tgpu.const(d.arrayOf(d.vec2f, 3), [ + d.vec2f(0.5, 1.0), + d.vec2f(0.0, 0.0), + d.vec2f(1.0, 0.0), +]); + +// Render pipeline — forces the WebGL 2 fallback backend. + +const root = initWithGL(); +const pipeline = root.createRenderPipeline({ + vertex: ({ $vertexIndex: vid }) => { + 'use gpu'; + return { + $position: d.vec4f(pos.$[vid], 0, 1), + uv: uv.$[vid], + }; + }, + fragment: ({ uv }) => { + 'use gpu'; + return getGradientColor((uv.x + uv.y) / 2); + }, +}); + +// Setting up the canvas and drawing to it + +const context = root.configureContext({ + canvas: document.querySelector('canvas') as HTMLCanvasElement, + alphaMode: 'premultiplied', +}); + +pipeline.withColorAttachment({ view: context }).draw(3); + +// #region Cleanup + +export function onCleanup() { + root.destroy(); +} + +// #endregion diff --git a/apps/typegpu-docs/src/examples/simple/triangle-gl/meta.json b/apps/typegpu-docs/src/examples/simple/triangle-gl/meta.json new file mode 100644 index 0000000000..dcd1ea967b --- /dev/null +++ b/apps/typegpu-docs/src/examples/simple/triangle-gl/meta.json @@ -0,0 +1,6 @@ +{ + "title": "Triangle (WebGL fallback)", + "category": "simple", + "tags": ["basics", "primitives", "webgl"], + "coolFactor": 3 +} diff --git a/apps/typegpu-docs/src/utils/examples/sandboxModules.ts b/apps/typegpu-docs/src/utils/examples/sandboxModules.ts index 10b7fe8d2b..2bd4a41a3c 100644 --- a/apps/typegpu-docs/src/utils/examples/sandboxModules.ts +++ b/apps/typegpu-docs/src/utils/examples/sandboxModules.ts @@ -149,6 +149,10 @@ export const SANDBOX_MODULES: Record = { import: { reroute: 'typegpu-noise/src/index.ts' }, typeDef: { reroute: 'typegpu-noise/src/index.ts' }, }, + '@typegpu/gl': { + import: { reroute: 'typegpu-gl/src/index.ts' }, + typeDef: { reroute: 'typegpu-gl/src/index.ts' }, + }, '@typegpu/color': { import: { reroute: 'typegpu-color/src/index.ts' }, typeDef: { reroute: 'typegpu-color/src/index.ts' }, diff --git a/packages/typegpu-gl/src/glslGenerator.ts b/packages/typegpu-gl/src/glslGenerator.ts index 4c549bdac7..4e6082b07d 100644 --- a/packages/typegpu-gl/src/glslGenerator.ts +++ b/packages/typegpu-gl/src/glslGenerator.ts @@ -44,21 +44,14 @@ export function translateWgslTypeToGlsl(wgslType: string): string { return WGSL_TO_GLSL_TYPE[wgslType] ?? wgslType; } -/** - * Resolves a struct and adds its declaration to the resolution context. - * @param ctx - The resolution context. - * @param struct - The struct to resolve. - * - * @returns The resolved struct name. - */ function resolveStruct(ctx: ResolutionCtx, struct: d.WgslStruct) { const id = ctx.makeUniqueIdentifier(ShaderGenerator.getName(struct), 'global'); ctx.addDeclaration(`\ struct ${id} { ${Object.entries(struct.propTypes) - .map(([prop, type]) => ` ${ctx.resolve(type).value} ${prop};\n`) - .join('')}\ + .map(([prop, type]) => ` ${ctx.resolve(type).value} ${prop};\n`) + .join('')}\ };`); return id; @@ -66,9 +59,55 @@ ${Object.entries(struct.propTypes) const gl_PositionSnippet = tgpu['~unstable'].rawCodeSnippet('gl_Position', d.vec4f, 'private'); +interface OutVarInfo { + varName: string; + propName: string; + dataType: d.BaseData; +} + interface EntryFnState { structPropToVarMap: Record; - outVars: { varName: string; propName: string }[]; + outVars: OutVarInfo[]; + /** The first-fragment-color output name, if allocated. */ + fragColorName?: string; + /** The auto-output struct (populated as the body resolves). */ + autoOutStruct?: { + completeStruct: d.WgslStruct; + accessProp(key: string): { prop: string; type: d.BaseData } | undefined; + provideProp(key: string, type: d.BaseData): { prop: string; type: d.BaseData }; + }; +} + +function undecorateDataType(t: d.BaseData): d.BaseData { + return d.isDecorated(t) ? (t.inner as d.BaseData) : t; +} + +function getLocationFromDecorated(type: d.BaseData): number | undefined { + if (!d.isDecorated(type)) return undefined; + const attr = (type.attribs as d.AnyAttribute[]).find((a) => a.type === '@location'); + return attr ? (attr.params[0] as number) : undefined; +} + +function getBuiltinKindFromDecorated(type: d.BaseData): string | undefined { + if (!d.isDecorated(type)) return undefined; + const attr = (type.attribs as d.AnyAttribute[]).find((a) => a.type === '@builtin'); + return attr ? (attr.params[0] as string) : undefined; +} + +function glslInputForBuiltin( + builtinKind: string, + functionType: 'vertex' | 'fragment' | 'compute', +): string | undefined { + if (functionType === 'vertex') { + if (builtinKind === 'vertex_index') return 'uint(gl_VertexID)'; + if (builtinKind === 'instance_index') return 'uint(gl_InstanceID)'; + } else if (functionType === 'fragment') { + if (builtinKind === 'position') return 'gl_FragCoord'; + if (builtinKind === 'front_facing') return 'gl_FrontFacing'; + if (builtinKind === 'sample_index') return 'uint(gl_SampleID)'; + if (builtinKind === 'sample_mask') return 'uint(gl_SampleMaskIn[0])'; + } + return undefined; } /** @@ -81,7 +120,6 @@ export class GlslGenerator extends WgslGenerator { #entryFnState: EntryFnState | undefined; override typeAnnotation(data: d.BaseData): string { - // For WGSL identity types (scalars, vectors, common matrices), map to GLSL directly. if (!d.isLooseData(data)) { const glslName = WGSL_TO_GLSL_TYPE[data.type]; if (glslName !== undefined) { @@ -93,7 +131,6 @@ export class GlslGenerator extends WgslGenerator { return resolveStruct(this.ctx, data); } - // For all other types (structs, arrays, etc.) delegate to WGSL resolution. return super.typeAnnotation(data); } @@ -110,79 +147,124 @@ export class GlslGenerator extends WgslGenerator { override _return(statement: Return): string { const exprNode = statement[1]; - if (exprNode === undefined) { + if (exprNode === undefined || this.#functionType === 'normal' || this.#functionType === undefined) { // Default behavior return super._return(statement); } - if (this.#functionType !== 'normal') { - // oxlint-disable-next-line no-non-null-assertion - const entryFnState = this.#entryFnState!; - const expectedReturnType = this.ctx.topFunctionReturnType; - - if (typeof exprNode === 'object' && exprNode[0] === NODE.objectExpr) { - const transformed = Object.entries(exprNode[1]).map(([prop, rhsNode]) => { - let name: string | undefined = entryFnState.structPropToVarMap[prop]; - if (name === undefined) { - if ( - prop === '$position' || - (expectedReturnType && - d.isWgslStruct(expectedReturnType) && - expectedReturnType.propTypes[prop] === d.builtin.position) - ) { - name = 'gl_Position'; - } else { - name = this.ctx.makeUniqueIdentifier(prop, 'global'); - entryFnState.outVars.push({ varName: name, propName: prop }); - } - entryFnState.structPropToVarMap[prop] = name; - } - const rhsExpr = this._expression(rhsNode); - const type = rhsExpr.dataType as d.BaseData; - - const snippet = tgpu['~unstable'].rawCodeSnippet(name, type as d.AnyData, 'private'); - - return { - name, - snippet, - assignment: [NODE.assignmentExpr, name, '=', rhsNode], - } as const; - }); - - const block = super._block( - [NODE.block, [...transformed.map((t) => t.assignment), [NODE.return]]], - Object.fromEntries( - transformed.map(({ name, snippet }) => { - return [name, snippet.$] as const; - }), - ), - ); - - return `${this.ctx.pre}${block}`; - } else { - // Resolving the expression to inspect it's type - // We will resolve it again as part of the modifed statement - const expr = expectedReturnType - ? this._typedExpression(exprNode, expectedReturnType) - : this._expression(exprNode); - - if (expr.dataType === UnknownData) { - // Unknown data type, don't know what to do - return super._return(statement); - } + const entryFnState = this.#entryFnState as EntryFnState; + const expectedReturnType = this.ctx.topFunctionReturnType; + + // Case 1: Object literal return like `return { $position: ..., uv: ... }`. + if (typeof exprNode === 'object' && exprNode[0] === NODE.objectExpr) { + return this.#handleStructReturn( + exprNode as unknown as [number, Record], + expectedReturnType, + entryFnState, + ); + } + + // Non-literal return: inspect type to decide how to assign. + const expr = expectedReturnType + ? this._typedExpression(exprNode, expectedReturnType) + : this._expression(exprNode); + + if (expr.dataType === UnknownData) { + return super._return(statement); + } - if (expr.dataType.type.startsWith('vec')) { - const block = super._block( - [NODE.block, [[NODE.assignmentExpr, 'gl_Position', '=', exprNode], [NODE.return]]], - { gl_Position: gl_PositionSnippet.$ }, - ); + const exprType = (expr.dataType as d.BaseData).type; + + if (this.#functionType === 'fragment' && typeof exprType === 'string' && exprType.startsWith('vec')) { + // Fragment returning a vec directly (typically vec4). Assign to frag color output. + const name = entryFnState.fragColorName ?? this.ctx.makeUniqueIdentifier('_fragColor', 'global'); + entryFnState.fragColorName = name; + const colorSnippet = tgpu['~unstable'].rawCodeSnippet(name, expr.dataType as d.AnyData, 'private'); + const block = super._block( + [NODE.block, [[NODE.assignmentExpr, name, '=', exprNode], [NODE.return]]], + { [name]: colorSnippet.$ }, + ); + return `${this.ctx.pre}${block}`; + } + + if (this.#functionType === 'vertex' && typeof exprType === 'string' && exprType.startsWith('vec')) { + // Vertex returning a vec directly -> gl_Position. + const block = super._block( + [NODE.block, [[NODE.assignmentExpr, 'gl_Position', '=', exprNode], [NODE.return]]], + { gl_Position: gl_PositionSnippet.$ }, + ); + return `${this.ctx.pre}${block}`; + } + + return super._return(statement); + } - return `${this.ctx.pre}${block}`; + #handleStructReturn( + exprNode: [number, Record], + expectedReturnType: d.BaseData | undefined, + entryFnState: EntryFnState, + ): string { + // Is this an auto-detected output struct? If so, register each prop so the + // output struct's propTypes reflects what the body actually returns. + const isAutoStruct = expectedReturnType !== undefined && + (expectedReturnType as { type?: string }).type === 'auto-struct'; + const autoStruct = isAutoStruct + ? (expectedReturnType as unknown as { + completeStruct: d.WgslStruct; + accessProp(key: string): { prop: string; type: d.BaseData } | undefined; + provideProp(key: string, type: d.BaseData): { prop: string; type: d.BaseData }; + }) + : undefined; + if (autoStruct) { + entryFnState.autoOutStruct = autoStruct; + } + + // Resolve each RHS first so module-level references get reserved (and types become + // available) before we allocate our LHS output identifiers. + const resolved = Object.entries(exprNode[1]).map(([prop, rhsNode]) => { + // oxlint-disable-next-line typescript/no-explicit-any + const rhsExpr = this._expression(rhsNode as any); + const dataType = rhsExpr.dataType as d.BaseData; + const rhsStr = this.ctx.resolve(rhsExpr.value, dataType).value; + // Register the prop on the auto-struct so the caller's completeStruct picks it up. + if (autoStruct) { + const existing = autoStruct.accessProp(prop); + if (!existing) { + autoStruct.provideProp(prop, dataType); } } + return { prop, rhsStr, dataType }; + }); + + const lines: string[] = []; + for (const { prop, rhsStr, dataType } of resolved) { + let name: string | undefined = entryFnState.structPropToVarMap[prop]; + if (name === undefined) { + const isPosition = + prop === '$position' || + (expectedReturnType && + d.isWgslStruct(expectedReturnType) && + expectedReturnType.propTypes[prop] === d.builtin.position); + if (isPosition) { + name = 'gl_Position'; + } else { + // Name varyings consistently between vertex out / fragment in so the GLSL + // ES 3.00 linker can match them by name. + const wgslKey = prop.replaceAll('$', ''); + name = this.ctx.makeUniqueIdentifier(`vary_${wgslKey}`, 'global'); + entryFnState.outVars.push({ varName: name, propName: prop, dataType }); + } + entryFnState.structPropToVarMap[prop] = name; + } + + // Copy-wrap the RHS in its type constructor so references get turned into values. + const glslType = this.ctx.resolve(undecorateDataType(dataType)).value; + lines.push(`${this.ctx.pre} ${name} = ${glslType}(${rhsStr});`); } - return super._return(statement); + lines.push(`${this.ctx.pre} return;`); + + return `${this.ctx.pre}{\n${lines.join('\n')}\n${this.ctx.pre}}`; } override functionDefinition(options: ShaderGenerator.FunctionDefinitionOptions): string { @@ -190,8 +272,8 @@ export class GlslGenerator extends WgslGenerator { this.ctx.reserveIdentifier('gl_Position', 'global'); } - // Function body - let lastFunctionType = this.#functionType; + const lastFunctionType = this.#functionType; + const lastEntryFnState = this.#entryFnState; this.#functionType = options.functionType; if (options.functionType !== 'normal') { if (this.#entryFnState) { @@ -202,29 +284,99 @@ export class GlslGenerator extends WgslGenerator { try { const body = this._block(options.body); - - // Only after generating the body can we determine the return type const returnType = options.determineReturnType(); if (options.functionType !== 'normal') { - // oxlint-disable-next-line no-non-null-assertion - const entryFnState = this.#entryFnState!; - if (d.isWgslStruct(returnType)) { - for (const { varName, propName } of entryFnState.outVars) { - const dataType = returnType.propTypes[propName]; - if (dataType && d.isDecorated(dataType)) { - const location = (dataType.attribs as d.AnyAttribute[]).find( - (a) => a.type === '@location', - )?.params[0]; - this.ctx.addDeclaration(`layout(location = ${location}) out ${varName};`); + const entryFnState = this.#entryFnState as EntryFnState; + + // --- Emit output declarations (layout(location=N) out TYPE NAME;) --- + // Prefer the auto-output struct if we collected one during body resolution; + // it carries @location attributes computed via withVaryingLocations. + const outStructForDecls = entryFnState.autoOutStruct + ? entryFnState.autoOutStruct.completeStruct + : d.isWgslStruct(returnType) + ? returnType + : undefined; + if (outStructForDecls) { + for (const { varName, dataType } of entryFnState.outVars) { + // Varyings (vertex -> fragment) in GLSL ES 3.00 are matched by name, + // so we don't emit layout(location=N) qualifiers here. + const glslType = this.ctx.resolve(undecorateDataType(dataType)).value; + if (options.functionType === 'fragment') { + // Fragment color outputs keep location=N since they target draw buffers. + this.ctx.addDeclaration( + `layout(location = 0) out ${glslType} ${varName};`, + ); + } else { + this.ctx.addDeclaration(`out ${glslType} ${varName};`); } } } + // Fragment color output + if (entryFnState.fragColorName) { + this.ctx.addDeclaration(`layout(location = 0) out vec4 ${entryFnState.fragColorName};`); + } + + // --- Emit input-side setup: declare layout(location) in vars, and initialize _arg_N structs --- + const prelude: string[] = []; + for (const arg of options.args) { + if (!arg.used) continue; + const argType = arg.decoratedType as d.BaseData; + // AutoStruct args (entry fn auto-detected inputs): + // Identified via `type === 'auto-struct'`. + if ((argType as { type?: string }).type === 'auto-struct') { + const autoStruct = argType as unknown as { + completeStruct: d.WgslStruct; + }; + const completeStruct = autoStruct.completeStruct; + const structTypeName = this.ctx.resolve(completeStruct).value; + const initArgs: string[] = []; + for (const [prop, propType] of Object.entries(completeStruct.propTypes)) { + const builtinKind = getBuiltinKindFromDecorated(propType); + if (builtinKind) { + const mapped = glslInputForBuiltin( + builtinKind, + options.functionType as 'vertex' | 'fragment' | 'compute', + ); + if (mapped === undefined) { + throw new Error( + `Unsupported builtin for ${options.functionType} shader: ${builtinKind}`, + ); + } + initArgs.push(mapped); + } else { + const location = getLocationFromDecorated(propType); + const glslType = this.ctx.resolve(undecorateDataType(propType)).value; + if (options.functionType === 'vertex') { + // Vertex attribute input — keep layout(location=N); safe in ES 3.00. + const inName = this.ctx.makeUniqueIdentifier(`_in_${prop}`, 'global'); + this.ctx.addDeclaration( + `layout(location = ${location ?? 0}) in ${glslType} ${inName};`, + ); + initArgs.push(inName); + } else { + // Fragment varying input — matched by name to the vertex output. + const inName = this.ctx.makeUniqueIdentifier(`vary_${prop}`, 'global'); + this.ctx.addDeclaration(`in ${glslType} ${inName};`); + initArgs.push(inName); + } + } + } + prelude.push(` ${structTypeName} ${arg.name} = ${structTypeName}(${initArgs.join(', ')});`); + } + } + + // Inject prelude into the body: body looks like "{\n\n}" — we insert after the opening brace. + if (prelude.length > 0) { + const firstNewlineIdx = body.indexOf('\n'); + const before = body.slice(0, firstNewlineIdx + 1); + const after = body.slice(firstNewlineIdx + 1); + return `void main() ${before}${prelude.join('\n')}\n${after}`; + } return `void main() ${body}`; } const argList = options.args - // Stripping out unused arguments in entry functions .filter((arg) => arg.used || options.functionType === 'normal') .map((arg) => { return `${this.ctx.resolve(arg.decoratedType).value} ${arg.name}`; @@ -234,7 +386,7 @@ export class GlslGenerator extends WgslGenerator { return `${this.ctx.resolve(returnType).value} ${options.name}(${argList}) ${body}`; } finally { this.#functionType = lastFunctionType; - this.#entryFnState = undefined; + this.#entryFnState = lastEntryFnState; } } } diff --git a/packages/typegpu-gl/src/tgpuRootWebGL.ts b/packages/typegpu-gl/src/tgpuRootWebGL.ts index c010352abe..c510ff0e1a 100644 --- a/packages/typegpu-gl/src/tgpuRootWebGL.ts +++ b/packages/typegpu-gl/src/tgpuRootWebGL.ts @@ -7,7 +7,7 @@ */ import tgpu, { d, ShaderGenerator, type TgpuFragmentFn, type TgpuVertexFn } from 'typegpu'; -import glslGenerator, { translateWgslTypeToGlsl } from './glslGenerator.ts'; +import glslGenerator from './glslGenerator.ts'; // ---------- // Public API @@ -48,225 +48,47 @@ interface WebGLUniform { // Implementation // ---------- -/** - * Translates a WGSL function body (output from `tgpu.resolve`) to a minimal - * GLSL ES 3.0 vertex shader. - * - * The full WGSL resolve output looks like: - * @vertex fn fnName(input: FnName_Input) -> FnName_Output { ... } - * - * We strip the header and translate the body to GLSL. - */ -function extractFunctionBody(resolvedCode: string, fnName: string): string { - // Find the function definition by its name - const fnPattern = new RegExp(`fn\\s+${fnName}\\s*\\([^)]*\\)[^{]*\\{`); - const match = fnPattern.exec(resolvedCode); - if (!match) { - throw new Error(`Could not find function '${fnName}' in resolved WGSL code.`); - } - - // Extract the body between matching braces - const startIdx = match.index + match[0].length; - let depth = 1; - let i = startIdx; - while (i < resolvedCode.length && depth > 0) { - if (resolvedCode[i] === '{') depth++; - else if (resolvedCode[i] === '}') depth--; - i++; - } - - return resolvedCode.slice(startIdx, i - 1).trim(); -} - -/** - * Gets the attribute annotation string for a GLSL I/O variable declaration. - * Returns the location number if it has a @location attribute, or undefined for builtins. - */ -function getLocationFromField( - fieldData: d.BaseData, -): { location: number } | { builtin: string } | null { - if (d.isDecorated(fieldData)) { - for (const attrib of fieldData.attribs) { - const a = attrib as { type: string; params: unknown[] }; - if (a.type === '@location') { - return { location: a.params[0] as number }; - } - if (a.type === '@builtin') { - return { builtin: a.params[0] as string }; - } - } - } - return null; -} - -/** - * Translates a WGSL type name to GLSL. For structs / arrays, returns the name as-is. - */ -function wgslTypeToGlsl(dataType: d.BaseData): string { - if (d.isDecorated(dataType)) { - return wgslTypeToGlsl(dataType.inner); - } - const wgslName = dataType.toString(); - return translateWgslTypeToGlsl(wgslName); -} - -/** - * Generate complete GLSL ES 3.0 vertex shader source. - */ -function generateVertexShader( - vertexFn: TgpuVertexFn, - resolvedCode: string, - fnName: string, -): string { - const lines: string[] = ['#version 300 es', 'precision highp float;', '']; - - const shell = vertexFn.shell; - - // Declare inputs (from shell.in) - if (shell.in) { - let locationIdx = 0; - for (const [_propName, fieldData] of Object.entries(shell.in as Record)) { - const attr = getLocationFromField(fieldData); - if (attr && 'builtin' in attr) { - // builtins like vertex_index → gl_VertexID, skip declaration - continue; - } - const loc = attr ? attr.location : locationIdx++; - const glslType = wgslTypeToGlsl(fieldData); - lines.push(`layout(location = ${loc}) in ${glslType} _in_${_propName};`); - } - } - - // Declare outputs (from shell.out) - skip builtins (position → gl_Position) - { - let locationIdx = 0; - for (const [propName, fieldData] of Object.entries(shell.out as Record)) { - const attr = getLocationFromField(fieldData); - if (attr && 'builtin' in attr && attr.builtin === 'position') { - // position → gl_Position, skip declaration - continue; - } - const loc = attr && 'location' in attr ? attr.location : locationIdx++; - const glslType = wgslTypeToGlsl(fieldData); - lines.push(`layout(location = ${loc}) out ${glslType} _out_${propName};`); - } - } - - lines.push(''); - - // Extract function body from resolved GLSL code - const body = extractFunctionBody(resolvedCode, fnName); - - // Wrap in void main() - lines.push('void main() {'); - // Provide input variables from built-in GLSL inputs - if (shell.in) { - for (const [propName, fieldData] of Object.entries(shell.in as Record)) { - const attr = getLocationFromField(fieldData); - if (attr && 'builtin' in attr) { - const builtinName = attr.builtin; - let glBuiltin = ''; - if (builtinName === 'vertex_index') glBuiltin = 'uint(gl_VertexID)'; - else if (builtinName === 'instance_index') glBuiltin = 'uint(gl_InstanceID)'; - if (glBuiltin) { - const glslType = wgslTypeToGlsl(fieldData); - lines.push(` ${glslType} _in_${propName} = ${glBuiltin};`); - } - } - } - } - - // Emit translated body indented - for (const line of body.split('\n')) { - lines.push(` ${line}`); - } - - // Map output variables back to GLSL outputs - // The body uses WGSL return with struct constructor. We need to translate this. - // For simplicity, we replace Output struct construction with assignments. - // This is handled by post-processing the body lines above. - - lines.push('}'); - - return lines.join('\n'); -} +const GLSL_HEADER = '#version 300 es\nprecision highp float;\nprecision highp int;\n\n'; /** - * Generate complete GLSL ES 3.0 fragment shader source. + * Applies post-processing fixups to WGSL-like output produced by the resolution + * pipeline so it becomes valid GLSL ES 3.0. + * + * Some resolutions (like `tgpu.const`) emit WGSL syntax (e.g. `const x: T = ...;`) + * that we can't cleanly intercept from a generator alone; we rewrite those here. */ -function generateFragmentShader( - fragmentFn: TgpuFragmentFn, - resolvedCode: string, - fnName: string, -): string { - const lines: string[] = ['#version 300 es', 'precision highp float;', '']; - - const shell = fragmentFn.shell; - - // Declare inputs (varyings from vertex shader) - if (shell.in) { - let locationIdx = 0; - for (const [propName, fieldData] of Object.entries(shell.in as Record)) { - const attr = getLocationFromField(fieldData); - if (attr && 'builtin' in attr) { - // builtins like position → gl_FragCoord - continue; - } - const loc = attr && 'location' in attr ? attr.location : locationIdx++; - const glslType = wgslTypeToGlsl(fieldData); - lines.push(`layout(location = ${loc}) in ${glslType} _in_${propName};`); - } - } - - // Declare outputs - const outSchema = shell.out; - if (outSchema && typeof outSchema === 'object' && !d.isDecorated(outSchema as d.BaseData)) { - // Struct output - let locationIdx = 0; - for (const [propName, fieldData] of Object.entries(outSchema as Record)) { - const attr = getLocationFromField(fieldData); - if (attr && 'builtin' in attr) continue; - const loc = attr && 'location' in attr ? attr.location : locationIdx++; - const glslType = wgslTypeToGlsl(fieldData); - lines.push(`layout(location = ${loc}) out ${glslType} _out_${propName};`); - } - } else if (outSchema) { - // Single value or decorated output - const fieldData = outSchema as d.BaseData; - const attr = getLocationFromField(fieldData); - const loc = attr && 'location' in attr ? attr.location : 0; - const glslType = wgslTypeToGlsl(fieldData); - lines.push(`layout(location = ${loc}) out ${glslType} _fragColor;`); - } - - lines.push(''); - - const body = extractFunctionBody(resolvedCode, fnName); - - lines.push('void main() {'); - // Provide input variables from builtins - if (shell.in) { - for (const [propName, fieldData] of Object.entries(shell.in as Record)) { - const attr = getLocationFromField(fieldData); - if (attr && 'builtin' in attr) { - const builtinName = attr.builtin; - let glBuiltin = ''; - if (builtinName === 'position') glBuiltin = 'gl_FragCoord'; - if (glBuiltin) { - const glslType = wgslTypeToGlsl(fieldData); - lines.push(` ${glslType} _in_${propName} = ${glBuiltin};`); - } +function wgslToGlslFixups(code: string): string { + let out = code; + + // WGSL integer literal suffix: `5i` -> `5`, `5u` -> `5` (GLSL happily accepts bare ints). + out = out.replaceAll(/(\d+)[iu]\b/g, '$1'); + + // WGSL f32 literal suffix -> GLSL float literal. An f-suffixed literal is always a float, + // but GLSL requires a decimal point to disambiguate from an int. So `2f` -> `2.0`, + // `2.5f` -> `2.5`. + out = out.replaceAll(/(\d+\.\d+)f\b/g, '$1'); + out = out.replaceAll(/(\d+)f\b/g, '$1.0'); + + // WGSL array type in expressions `array(...)` -> `T[N](...)` + out = out.replaceAll(/array<([^,<>]+?),\s*(\d+)>/g, '$1[$2]'); + + // WGSL const decls: `const NAME: TYPE = VALUE;` -> GLSL style. + // TYPE can include brackets if it started as `array` (already rewritten to `T[N]`). + // For GLSL arrays, the brackets go AFTER the identifier: `const T NAME[N] = ...`. + out = out.replaceAll( + /\bconst\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([A-Za-z_][A-Za-z0-9_]*)(\[[^\]]+\])?\s*=\s*/g, + (_m, name, baseType, arraySuffix) => { + if (arraySuffix) { + return `const ${baseType} ${name}${arraySuffix} = `; } - } - } - - for (const line of body.split('\n')) { - lines.push(` ${line}`); - } + return `const ${baseType} ${name} = `; + }, + ); - lines.push('}'); + // Empty vector constructors `vecN()` are illegal in GLSL; default to zero. + out = out.replaceAll(/\b(vec[234]|ivec[234]|uvec[234]|bvec[234])\s*\(\s*\)/g, '$1(0)'); - return lines.join('\n'); + return out; } function compileShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader { @@ -315,6 +137,7 @@ class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { #uniforms: Array; #renderCtx: WebGLRenderContext | 'screen' | null = null; #offscreen: OffscreenCanvas; + #vao: WebGLVertexArrayObject; constructor( gl: WebGL2RenderingContext, @@ -326,6 +149,9 @@ class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { this.#program = program; this.#uniforms = uniforms; this.#offscreen = offscreen; + const vao = gl.createVertexArray(); + if (!vao) throw new Error('Failed to create VAO'); + this.#vao = vao; } withColorAttachment(attachment: { view: WebGLRenderContext | 'screen' }): this { @@ -336,13 +162,11 @@ class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { draw(vertexCount: number, _instanceCount = 1, firstVertex = 0): void { const gl = this.#gl; - // Resize offscreen canvas if needed + // Resize offscreen canvas to match the target if (this.#renderCtx && this.#renderCtx !== 'screen') { const canvas = this.#renderCtx.canvas; - const w = canvas.width; - const h = canvas.height; - this.#offscreen.width = w; - this.#offscreen.height = h; + this.#offscreen.width = canvas.width; + this.#offscreen.height = canvas.height; } gl.viewport(0, 0, this.#offscreen.width, this.#offscreen.height); @@ -350,6 +174,7 @@ class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(this.#program); + gl.bindVertexArray(this.#vao); // Bind UBOs for (let i = 0; i < this.#uniforms.length; i++) { @@ -365,7 +190,9 @@ class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { gl.drawArrays(gl.TRIANGLES, firstVertex, vertexCount); - // Blit to user canvas if configured + gl.bindVertexArray(null); + + // Blit offscreen to the user-provided canvas if (this.#renderCtx && this.#renderCtx !== 'screen') { const canvas = this.#renderCtx.canvas as HTMLCanvasElement; const bitmapCtx = canvas.getContext('bitmaprenderer'); @@ -403,7 +230,6 @@ class WebGLUniformImpl implements WebGLUniform): void { const gl = this.#gl; - // Convert data to Float32Array for the UBO const floatData = flattenToFloat32(data); gl.bindBuffer(gl.UNIFORM_BUFFER, this.glBuffer); gl.bufferData(gl.UNIFORM_BUFFER, floatData, gl.DYNAMIC_DRAW); @@ -433,9 +259,11 @@ function flattenToFloat32(data: unknown): Float32Array { return new Float32Array([0]); } -export interface CreateRenderPipelineWebGLOptions { - vertex: TgpuVertexFn; - fragment: TgpuFragmentFn; +// oxlint-disable-next-line typescript/no-explicit-any +type AnyFn = (...args: any[]) => any; + +function isShellFn(value: unknown): value is TgpuVertexFn | TgpuFragmentFn { + return typeof value === 'object' && value !== null && 'shell' in value; } export class TgpuRootWebGL { @@ -534,41 +362,65 @@ export class TgpuRootWebGL { } createRenderPipeline(descriptor: { - vertex: TgpuVertexFn; - fragment: TgpuFragmentFn; + vertex: TgpuVertexFn | AnyFn; + fragment: TgpuFragmentFn | AnyFn; }): TgpuWebGLRenderPipeline { const { vertex, fragment } = descriptor; - const vertexNamespace = tgpu['~unstable'].namespace(); - const fragmentNamespace = tgpu['~unstable'].namespace(); + const { AutoVertexFn, AutoFragmentFn, matchUpVaryingLocations } = tgpu['~unstable']; - // Resolve both functions using the GLSL generator - const vertexCode = tgpu.resolve([vertex], { - unstable_shaderGenerator: this.#shaderGenerator, - names: vertexNamespace, - }); + const vertexShell = isShellFn(vertex) ? vertex : undefined; + const fragmentShell = isShellFn(fragment) ? fragment : undefined; - const fragmentCode = tgpu.resolve([fragment], { - unstable_shaderGenerator: this.#shaderGenerator, - names: fragmentNamespace, - }); + const locations = matchUpVaryingLocations( + vertexShell?.shell?.out, + fragmentShell?.shell?.in, + '', + '', + ); + + const vertexResolvable = vertexShell ?? new AutoVertexFn(vertex as AnyFn, {}, locations); + const fragmentFromAuto = fragmentShell === undefined; - // Get the function names from the resolved code - const vertexFnName = tgpu.resolve({ - template: '$$name$$', + const vertexNamespace = tgpu['~unstable'].namespace(); + const vertexCode = tgpu.resolve([vertexResolvable], { unstable_shaderGenerator: this.#shaderGenerator, names: vertexNamespace, - externals: { $$name$$: vertex }, }); - const fragmentFnName = tgpu.resolve({ - template: '$$name$$', + + // For the fragment, we want to know the vertex output varyings to route them as inputs. + // When using an auto-vertex-fn, we need the completed struct. + let varyings: Record = {}; + if (vertexResolvable instanceof AutoVertexFn) { + const outStruct = vertexResolvable.autoOut.completeStruct; + varyings = Object.fromEntries( + Object.entries(outStruct.propTypes).filter(([, type]) => !isBuiltinType(type)), + ); + } else if (vertexShell?.shell?.out) { + varyings = Object.fromEntries( + Object.entries(vertexShell.shell.out as Record).filter( + ([, type]) => !isBuiltinType(type), + ), + ); + } + + const fragmentResolvable = fragmentShell ?? new AutoFragmentFn( + fragment as AnyFn, + varyings, + locations, + ); + + const fragmentNamespace = tgpu['~unstable'].namespace(); + const fragmentCode = tgpu.resolve([fragmentResolvable], { unstable_shaderGenerator: this.#shaderGenerator, names: fragmentNamespace, - externals: { $$name$$: fragment }, }); - const vertexGlsl = generateVertexShader(vertex, vertexCode, vertexFnName); - const fragmentGlsl = generateFragmentShader(fragment, fragmentCode, fragmentFnName); + const vertexGlsl = GLSL_HEADER + wgslToGlslFixups(vertexCode); + const fragmentGlsl = GLSL_HEADER + wgslToGlslFixups(fragmentCode); + + // Silence unused variable lints for the shell-only fallback + void fragmentFromAuto; const program = linkProgram(this.#gl, vertexGlsl, fragmentGlsl); @@ -581,7 +433,6 @@ export class TgpuRootWebGL { } with(_slot: unknown, _value: unknown): this { - // TODO: Implement slot binding return this; } @@ -596,13 +447,10 @@ export class TgpuRootWebGL { } pipe(): this { - // TODO: Implement slot binding return this; } - flush(): void { - // No-op - } + flush(): void {} destroy(): void { for (const buf of this.#buffers) { @@ -615,3 +463,10 @@ export class TgpuRootWebGL { this.#uniforms = []; } } + +function isBuiltinType(type: d.BaseData): boolean { + if (d.isDecorated(type)) { + return (type.attribs as d.AnyAttribute[]).some((a) => a.type === '@builtin'); + } + return false; +} diff --git a/packages/typegpu/src/tgpuUnstable.ts b/packages/typegpu/src/tgpuUnstable.ts index 717ab231b5..3f9d036736 100644 --- a/packages/typegpu/src/tgpuUnstable.ts +++ b/packages/typegpu/src/tgpuUnstable.ts @@ -4,6 +4,9 @@ export { declare } from './core/declare/tgpuDeclare.ts'; export { rawCodeSnippet } from './core/rawCodeSnippet/tgpuRawCodeSnippet.ts'; export { namespace } from './core/resolve/namespace.ts'; export { simulate } from './core/simulate/tgpuSimulate.ts'; +// TODO: Export from shaderGenerator_members.ts instead +export { AutoVertexFn, AutoFragmentFn } from './core/function/autoIO.ts'; +export { matchUpVaryingLocations } from './core/pipeline/renderPipeline.ts'; // DEPRECATED diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b88277cf4..abca5966f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -250,6 +250,9 @@ importers: '@typegpu/geometry': specifier: workspace:* version: link:../../packages/typegpu-geometry + '@typegpu/gl': + specifier: workspace:* + version: link:../../packages/typegpu-gl '@typegpu/noise': specifier: workspace:* version: link:../../packages/typegpu-noise From d91b4e944282c40f4891b356b09142a328cba458 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sat, 18 Apr 2026 13:48:30 +0200 Subject: [PATCH 02/14] More updates to make Caustics work --- .../examples/rendering/caustics-gl/index.html | 1 + .../examples/rendering/caustics-gl/index.ts | 169 +++++++++++++++ .../examples/rendering/caustics-gl/meta.json | 6 + packages/typegpu-gl/ARCHITECTURE.md | 17 ++ packages/typegpu-gl/src/glslGenerator.ts | 119 +++++++---- packages/typegpu-gl/src/index.ts | 1 + packages/typegpu-gl/src/tgpuRootWebGL.ts | 193 +++++++++++++----- .../typegpu-gl/tests/glslGenerator.test.ts | 103 +++++++++- packages/typegpu/src/tgpuUnstable.ts | 3 - .../src/tgsl/shaderGenerator_members.ts | 6 + 10 files changed, 511 insertions(+), 107 deletions(-) create mode 100644 apps/typegpu-docs/src/examples/rendering/caustics-gl/index.html create mode 100644 apps/typegpu-docs/src/examples/rendering/caustics-gl/index.ts create mode 100644 apps/typegpu-docs/src/examples/rendering/caustics-gl/meta.json create mode 100644 packages/typegpu-gl/ARCHITECTURE.md diff --git a/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.html b/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.html new file mode 100644 index 0000000000..aa8cc321b3 --- /dev/null +++ b/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.html @@ -0,0 +1 @@ + diff --git a/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.ts b/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.ts new file mode 100644 index 0000000000..823c19e294 --- /dev/null +++ b/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.ts @@ -0,0 +1,169 @@ +import { perlin3d } from '@typegpu/noise'; +import tgpu, { d, std } from 'typegpu'; +import { initWithGL } from '@typegpu/gl'; +import { defineControls } from '../../common/defineControls.ts'; + +const mainVertex = tgpu.vertexFn({ + in: { vertexIndex: d.builtin.vertexIndex }, + out: { pos: d.builtin.position, uv: d.vec2f }, +})(({ vertexIndex }) => { + const pos = [d.vec2f(0, 0.8), d.vec2f(-0.8, -0.8), d.vec2f(0.8, -0.8)]; + const uv = [d.vec2f(0.5, 1), d.vec2f(0, 0), d.vec2f(1, 0)]; + + return { + pos: d.vec4f(pos[vertexIndex], 0, 1), + uv: uv[vertexIndex], + }; +}); + +/** + * Given a coordinate, it returns a grayscale floor tile pattern at that + * location. + */ +const tilePattern = (uv: d.v2f): number => { + 'use gpu'; + const tiledUv = std.fract(uv); + const proximity = std.abs(tiledUv * 2 - 1); + const maxProximity = std.max(proximity.x, proximity.y); + return std.saturate((1 - maxProximity) ** 0.6 * 5); +}; + +const caustics = (uv: d.v2f, time: number, profile: d.v3f): d.v3f => { + 'use gpu'; + const distortion = perlin3d.sample(d.vec3f(uv * 0.5, time * 0.2)); + // Distorting UV coordinates + const uv2 = uv + distortion; + const noise = std.abs(perlin3d.sample(d.vec3f(uv2 * 5, time))); + return std.pow(d.vec3f(1 - noise), profile); +}; + +/** + * Returns a transformation matrix that represents an `angle` rotation + * in the XY plane (around the imaginary Z axis) + */ +const rotateXY = (angle: number): d.m2x2f => { + 'use gpu'; + return d.mat2x2f( + /* right */ d.vec2f(std.cos(angle), std.sin(angle)), + /* up */ d.vec2f(-std.sin(angle), std.cos(angle)), + ); +}; + +const root = initWithGL(); + +/** Seconds passed since the start of the example, wrapped to the range [0, 1000) */ +const time = root.createUniform(d.f32); +/** Controls the angle of rotation for the pool tile texture */ +const angle = 0.2; +/** The bigger the number, the denser the pool tile texture is */ +const tileDensity = root.createUniform(d.f32); +/** The scene fades into this color at a distance */ +const fogColor = d.vec3f(0.05, 0.2, 0.7); +/** The ambient light color */ +const ambientColor = d.vec3f(0.2, 0.5, 1); + +const mainFragment = tgpu.fragmentFn({ + in: { uv: d.vec2f }, + out: d.vec4f, +})(({ uv }) => { + 'use gpu'; + /** + * A transformation matrix that skews the perspective a bit + * when applied to UV coordinates + */ + const skewMat = d.mat2x2f( + d.vec2f(std.cos(angle), std.sin(angle)), + d.vec2f(-std.sin(angle) * 10 + uv.x * 3, std.cos(angle) * 5), + ); + const skewedUv = skewMat * uv; + const tile = tilePattern(skewedUv * tileDensity.$); + const albedo = std.mix(d.vec3f(0.1), d.vec3f(1), tile); + + // Transforming coordinates to simulate perspective squash + const cuv = d.vec2f( + uv.x * (std.pow(uv.y * 1.5, 3) + 0.1) * 5, + std.pow((uv.y * 1.5 + 0.1) * 1.5, 3) * 1, + ); + // Generating two layers of caustics (large scale, and small scale) + const c1 = + caustics(cuv, time.$ * 0.2, /* profile */ d.vec3f(4, 4, 1)) * + // Tinting + d.vec3f(0.4, 0.65, 1); + const c2 = + caustics(cuv * 2, time.$ * 0.4, /* profile */ d.vec3f(16, 1, 4)) * + // Tinting + d.vec3f(0.18, 0.3, 0.5); + + // -- BLEND -- + + const blendCoord = d.vec3f(uv * d.vec2f(5, 10), time.$ * 0.2 + 5); + // A smooth blending factor, so that caustics only appear at certain spots + const blend = std.saturate(perlin3d.sample(blendCoord) + 0.3); + + // -- FOG -- + + const noFogColor = albedo * std.mix(ambientColor, c1 + c2, blend); + // Fog blending factor, based on the height of the pixels + const fog = std.min(uv.y ** 0.5 * 1.2, 1); + + // -- GOD RAYS -- + + const godRayUv = rotateXY(-0.3) * uv * d.vec2f(15, 3); + const godRayFactor = uv.y; + const godRay1 = + (perlin3d.sample(d.vec3f(godRayUv, time.$ * 0.5)) + 1) * + // Tinting + d.vec3f(0.18, 0.3, 0.5) * + godRayFactor; + const godRay2 = + (perlin3d.sample(d.vec3f(godRayUv * 2, time.$ * 0.3)) + 1) * + // Tinting + d.vec3f(0.18, 0.3, 0.5) * + godRayFactor * + 0.4; + const godRays = godRay1 + godRay2; + + return d.vec4f(std.mix(noFogColor, fogColor, fog) + godRays, 1); +}); + +const canvas = document.querySelector('canvas') as HTMLCanvasElement; +const context = root.configureContext({ canvas, alphaMode: 'premultiplied' }); + +const pipeline = root.createRenderPipeline({ + vertex: mainVertex, + fragment: mainFragment, +}); + +let isRunning = true; + +function draw(timestamp: number) { + if (!isRunning) return; + + time.write((timestamp * 0.001) % 1000); + + pipeline.withColorAttachment({ view: context }).draw(3); + + requestAnimationFrame(draw); +} +requestAnimationFrame(draw); + +// #region Example controls and cleanup + +export const controls = defineControls({ + 'tile density': { + initial: 10, + min: 5, + max: 20, + step: 1, + onSliderChange: (density) => { + tileDensity.write(density); + }, + }, +}); + +export function onCleanup() { + isRunning = false; + root.destroy(); +} + +// #endregion diff --git a/apps/typegpu-docs/src/examples/rendering/caustics-gl/meta.json b/apps/typegpu-docs/src/examples/rendering/caustics-gl/meta.json new file mode 100644 index 0000000000..9a9b0cd6a8 --- /dev/null +++ b/apps/typegpu-docs/src/examples/rendering/caustics-gl/meta.json @@ -0,0 +1,6 @@ +{ + "title": "Caustics (WebGL fallback)", + "category": "rendering", + "tags": ["ecosystem", "lighting", "noise", "webgl"], + "coolFactor": 8 +} diff --git a/packages/typegpu-gl/ARCHITECTURE.md b/packages/typegpu-gl/ARCHITECTURE.md new file mode 100644 index 0000000000..d0e8b9965a --- /dev/null +++ b/packages/typegpu-gl/ARCHITECTURE.md @@ -0,0 +1,17 @@ +## Resource management + +I (@iwoplaza) was initially planning to defer buffer creation to the root, but its unclear how to replicate full `TgpuBuffer` behavior anyways. It's way more straightforward to allow just buffer shorthands (`root.createUniform`, etc.) and return simplified implementations. For apps that want to optimize the non-fallback +path, it's very easy to do with accessors for example. + +```ts +const root = await initWithGLFallback(); +const isGL = isGLRoot(root); + +const Positions = d.arrayOf(d.vec3f, 64); +const positions = isGL ? root.createUniform(Positions) : root.createReadonly(Positions); + +function updatePosition(index: number) { + 'use gpu'; + positions.$[index] += d.vec3f(0, 1, 0); +} +``` diff --git a/packages/typegpu-gl/src/glslGenerator.ts b/packages/typegpu-gl/src/glslGenerator.ts index 4e6082b07d..e66b0ca816 100644 --- a/packages/typegpu-gl/src/glslGenerator.ts +++ b/packages/typegpu-gl/src/glslGenerator.ts @@ -175,11 +175,20 @@ export class GlslGenerator extends WgslGenerator { const exprType = (expr.dataType as d.BaseData).type; - if (this.#functionType === 'fragment' && typeof exprType === 'string' && exprType.startsWith('vec')) { + if ( + this.#functionType === 'fragment' && + typeof exprType === 'string' && + exprType.startsWith('vec') + ) { // Fragment returning a vec directly (typically vec4). Assign to frag color output. - const name = entryFnState.fragColorName ?? this.ctx.makeUniqueIdentifier('_fragColor', 'global'); + const name = + entryFnState.fragColorName ?? this.ctx.makeUniqueIdentifier('_fragColor', 'global'); entryFnState.fragColorName = name; - const colorSnippet = tgpu['~unstable'].rawCodeSnippet(name, expr.dataType as d.AnyData, 'private'); + const colorSnippet = tgpu['~unstable'].rawCodeSnippet( + name, + expr.dataType as d.AnyData, + 'private', + ); const block = super._block( [NODE.block, [[NODE.assignmentExpr, name, '=', exprNode], [NODE.return]]], { [name]: colorSnippet.$ }, @@ -187,7 +196,11 @@ export class GlslGenerator extends WgslGenerator { return `${this.ctx.pre}${block}`; } - if (this.#functionType === 'vertex' && typeof exprType === 'string' && exprType.startsWith('vec')) { + if ( + this.#functionType === 'vertex' && + typeof exprType === 'string' && + exprType.startsWith('vec') + ) { // Vertex returning a vec directly -> gl_Position. const block = super._block( [NODE.block, [[NODE.assignmentExpr, 'gl_Position', '=', exprNode], [NODE.return]]], @@ -206,7 +219,8 @@ export class GlslGenerator extends WgslGenerator { ): string { // Is this an auto-detected output struct? If so, register each prop so the // output struct's propTypes reflects what the body actually returns. - const isAutoStruct = expectedReturnType !== undefined && + const isAutoStruct = + expectedReturnType !== undefined && (expectedReturnType as { type?: string }).type === 'auto-struct'; const autoStruct = isAutoStruct ? (expectedReturnType as unknown as { @@ -304,9 +318,7 @@ export class GlslGenerator extends WgslGenerator { const glslType = this.ctx.resolve(undecorateDataType(dataType)).value; if (options.functionType === 'fragment') { // Fragment color outputs keep location=N since they target draw buffers. - this.ctx.addDeclaration( - `layout(location = 0) out ${glslType} ${varName};`, - ); + this.ctx.addDeclaration(`layout(location = 0) out ${glslType} ${varName};`); } else { this.ctx.addDeclaration(`out ${glslType} ${varName};`); } @@ -317,52 +329,71 @@ export class GlslGenerator extends WgslGenerator { this.ctx.addDeclaration(`layout(location = 0) out vec4 ${entryFnState.fragColorName};`); } - // --- Emit input-side setup: declare layout(location) in vars, and initialize _arg_N structs --- + // --- Emit input-side setup: declare layout(location) in vars, and initialize + // struct-shaped or scalar-shaped arg variables used by the body --- const prelude: string[] = []; + const stage = options.functionType as 'vertex' | 'fragment' | 'compute'; + const resolveInputForField = (prop: string, propType: d.BaseData): string => { + const builtinKind = getBuiltinKindFromDecorated(propType); + if (builtinKind) { + const mapped = glslInputForBuiltin(builtinKind, stage); + if (mapped === undefined) { + throw new Error(`Unsupported builtin for ${stage} shader: ${builtinKind}`); + } + return mapped; + } + const location = getLocationFromDecorated(propType); + const glslType = this.ctx.resolve(undecorateDataType(propType)).value; + if (stage === 'vertex') { + const inName = this.ctx.makeUniqueIdentifier(`_in_${prop}`, 'global'); + this.ctx.addDeclaration( + `layout(location = ${location ?? 0}) in ${glslType} ${inName};`, + ); + return inName; + } + const inName = this.ctx.makeUniqueIdentifier(`vary_${prop}`, 'global'); + this.ctx.addDeclaration(`in ${glslType} ${inName};`); + return inName; + }; + for (const arg of options.args) { if (!arg.used) continue; const argType = arg.decoratedType as d.BaseData; - // AutoStruct args (entry fn auto-detected inputs): - // Identified via `type === 'auto-struct'`. + + // Auto-detected IO struct (plain-function entry fns) if ((argType as { type?: string }).type === 'auto-struct') { - const autoStruct = argType as unknown as { - completeStruct: d.WgslStruct; - }; + const autoStruct = argType as unknown as { completeStruct: d.WgslStruct }; const completeStruct = autoStruct.completeStruct; const structTypeName = this.ctx.resolve(completeStruct).value; const initArgs: string[] = []; for (const [prop, propType] of Object.entries(completeStruct.propTypes)) { - const builtinKind = getBuiltinKindFromDecorated(propType); - if (builtinKind) { - const mapped = glslInputForBuiltin( - builtinKind, - options.functionType as 'vertex' | 'fragment' | 'compute', - ); - if (mapped === undefined) { - throw new Error( - `Unsupported builtin for ${options.functionType} shader: ${builtinKind}`, - ); - } - initArgs.push(mapped); - } else { - const location = getLocationFromDecorated(propType); - const glslType = this.ctx.resolve(undecorateDataType(propType)).value; - if (options.functionType === 'vertex') { - // Vertex attribute input — keep layout(location=N); safe in ES 3.00. - const inName = this.ctx.makeUniqueIdentifier(`_in_${prop}`, 'global'); - this.ctx.addDeclaration( - `layout(location = ${location ?? 0}) in ${glslType} ${inName};`, - ); - initArgs.push(inName); - } else { - // Fragment varying input — matched by name to the vertex output. - const inName = this.ctx.makeUniqueIdentifier(`vary_${prop}`, 'global'); - this.ctx.addDeclaration(`in ${glslType} ${inName};`); - initArgs.push(inName); - } - } + initArgs.push(resolveInputForField(prop, propType)); } - prelude.push(` ${structTypeName} ${arg.name} = ${structTypeName}(${initArgs.join(', ')});`); + prelude.push( + ` ${structTypeName} ${arg.name} = ${structTypeName}(${initArgs.join(', ')});`, + ); + continue; + } + + // Shell entry-fn IO struct (created from `in: {...}`): a regular WgslStruct with + // @builtin / @location decorated fields. + if (d.isWgslStruct(argType)) { + const structTypeName = this.ctx.resolve(argType).value; + const initArgs: string[] = []; + for (const [prop, propType] of Object.entries(argType.propTypes)) { + initArgs.push(resolveInputForField(prop, propType)); + } + prelude.push( + ` ${structTypeName} ${arg.name} = ${structTypeName}(${initArgs.join(', ')});`, + ); + continue; + } + + // Shell entry-fn positional arg: a single decorated scalar/vector (builtin or varying). + if (d.isDecorated(argType)) { + const inputExpr = resolveInputForField(arg.name, argType); + const glslType = this.ctx.resolve(undecorateDataType(argType)).value; + prelude.push(` ${glslType} ${arg.name} = ${inputExpr};`); } } diff --git a/packages/typegpu-gl/src/index.ts b/packages/typegpu-gl/src/index.ts index 5a3dbb29e2..3c1501f270 100644 --- a/packages/typegpu-gl/src/index.ts +++ b/packages/typegpu-gl/src/index.ts @@ -1,3 +1,4 @@ export { initWithGL } from './initWithGL.ts'; export { initWithGLFallback } from './initWithGLFallback.ts'; export { glOptions } from './glOptions.ts'; +export { isGLRoot } from './tgpuRootWebGL.ts'; diff --git a/packages/typegpu-gl/src/tgpuRootWebGL.ts b/packages/typegpu-gl/src/tgpuRootWebGL.ts index c510ff0e1a..2f946181f2 100644 --- a/packages/typegpu-gl/src/tgpuRootWebGL.ts +++ b/packages/typegpu-gl/src/tgpuRootWebGL.ts @@ -9,6 +9,9 @@ import tgpu, { d, ShaderGenerator, type TgpuFragmentFn, type TgpuVertexFn } from 'typegpu'; import glslGenerator from './glslGenerator.ts'; +const { $gpuValueOf, $internal, $ownSnippet, $resolve, snip, valueProxyHandler, inCodegenMode } = + ShaderGenerator; + // ---------- // Public API // ---------- @@ -38,17 +41,27 @@ interface WebGLUniform { readonly resourceType: 'uniform'; readonly schema: TData; write(data: d.Infer): void; - /** @internal The WebGL UBO index used when binding */ - readonly bindingIndex: number; - /** @internal The raw WebGL buffer */ - readonly glBuffer: WebGLBuffer; + readonly $: d.InferGPU; + /** @internal The stable GLSL identifier for this uniform */ + readonly glslName: string; + /** @internal The latest Float32Array representation of the written data */ + readonly latestData: Float32Array; } // ---------- // Implementation // ---------- -const GLSL_HEADER = '#version 300 es\nprecision highp float;\nprecision highp int;\n\n'; +const GLSL_HEADER = `#version 300 es +precision highp float; +precision highp int; + +float saturate(float x) { return clamp(x, 0.0, 1.0); } +vec2 saturate(vec2 x) { return clamp(x, 0.0, 1.0); } +vec3 saturate(vec3 x) { return clamp(x, 0.0, 1.0); } +vec4 saturate(vec4 x) { return clamp(x, 0.0, 1.0); } + +`; /** * Applies post-processing fixups to WGSL-like output produced by the resolution @@ -63,12 +76,22 @@ function wgslToGlslFixups(code: string): string { // WGSL integer literal suffix: `5i` -> `5`, `5u` -> `5` (GLSL happily accepts bare ints). out = out.replaceAll(/(\d+)[iu]\b/g, '$1'); - // WGSL f32 literal suffix -> GLSL float literal. An f-suffixed literal is always a float, - // but GLSL requires a decimal point to disambiguate from an int. So `2f` -> `2.0`, - // `2.5f` -> `2.5`. + // WGSL f32 literal suffixes -> GLSL float literals. A trailing `f` always marks a float, + // but GLSL requires a decimal point to disambiguate floats from ints. + // Handle scientific notation first (`1e-3f` -> `1e-3`), so the plain-int rule below doesn't + // mistakenly turn the exponent's digits into `1e-3.0`. + out = out.replaceAll(/(\d+(?:\.\d+)?[eE][+-]?\d+)f\b/g, '$1'); out = out.replaceAll(/(\d+\.\d+)f\b/g, '$1'); out = out.replaceAll(/(\d+)f\b/g, '$1.0'); + // WGSL private module var -> GLSL global var. + out = out.replaceAll(/\bvar\s+([A-Za-z_]\w*)\s*:\s*([^;=]+?)\s*;/g, '$2 $1;'); + out = out.replaceAll(/\bvar\s+([A-Za-z_]\w*)\s*:\s*([^;=]+?)\s*=\s*/g, '$2 $1 = '); + + // `sample` is a reserved word in GLSL ES (for multisample interpolation qualifiers), + // so rename any identifier `sample` used as a function or variable name. + out = out.replaceAll(/\bsample\b/g, 'sample_'); + // WGSL array type in expressions `array(...)` -> `T[N](...)` out = out.replaceAll(/array<([^,<>]+?),\s*(\d+)>/g, '$1[$2]'); @@ -131,10 +154,32 @@ function linkProgram( return program; } +interface UniformBinding { + uniform: WebGLUniform; + location: WebGLUniformLocation; + setter: (gl: WebGL2RenderingContext, loc: WebGLUniformLocation, data: Float32Array) => void; +} + +function uniformSetterFor( + schema: d.AnyWgslData, +): (gl: WebGL2RenderingContext, loc: WebGLUniformLocation, data: Float32Array) => void { + const typeName = (schema as { type: string }).type; + if (typeName === 'f32') return (gl, loc, data) => gl.uniform1f(loc, data[0] ?? 0); + if (typeName === 'u32') return (gl, loc, data) => gl.uniform1ui(loc, data[0] ?? 0); + if (typeName === 'i32') return (gl, loc, data) => gl.uniform1i(loc, data[0] ?? 0); + if (typeName === 'vec2f') return (gl, loc, data) => gl.uniform2fv(loc, data); + if (typeName === 'vec3f') return (gl, loc, data) => gl.uniform3fv(loc, data.subarray(0, 3)); + if (typeName === 'vec4f') return (gl, loc, data) => gl.uniform4fv(loc, data); + if (typeName === 'mat2x2f') return (gl, loc, data) => gl.uniformMatrix2fv(loc, false, data); + if (typeName === 'mat3x3f') return (gl, loc, data) => gl.uniformMatrix3fv(loc, false, data); + if (typeName === 'mat4x4f') return (gl, loc, data) => gl.uniformMatrix4fv(loc, false, data); + return () => {}; +} + class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { #gl: WebGL2RenderingContext; #program: WebGLProgram; - #uniforms: Array; + #uniformBindings: UniformBinding[]; #renderCtx: WebGLRenderContext | 'screen' | null = null; #offscreen: OffscreenCanvas; #vao: WebGLVertexArrayObject; @@ -147,11 +192,20 @@ class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { ) { this.#gl = gl; this.#program = program; - this.#uniforms = uniforms; this.#offscreen = offscreen; const vao = gl.createVertexArray(); if (!vao) throw new Error('Failed to create VAO'); this.#vao = vao; + + // Query uniform locations once; skip uniforms that weren't actually used by the shaders. + const bindings: UniformBinding[] = []; + for (const uniform of uniforms) { + const location = gl.getUniformLocation(program, uniform.glslName); + if (location !== null) { + bindings.push({ uniform, location, setter: uniformSetterFor(uniform.schema) }); + } + } + this.#uniformBindings = bindings; } withColorAttachment(attachment: { view: WebGLRenderContext | 'screen' }): this { @@ -176,16 +230,9 @@ class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { gl.useProgram(this.#program); gl.bindVertexArray(this.#vao); - // Bind UBOs - for (let i = 0; i < this.#uniforms.length; i++) { - const uniform = this.#uniforms[i]; - if (uniform) { - gl.bindBufferBase(gl.UNIFORM_BUFFER, uniform.bindingIndex, uniform.glBuffer); - const blockIdx = gl.getUniformBlockIndex(this.#program, `_uniform_block_${i}`); - if (blockIdx !== gl.INVALID_INDEX) { - gl.uniformBlockBinding(this.#program, blockIdx, uniform.bindingIndex); - } - } + // Upload current uniform values + for (const b of this.#uniformBindings) { + b.setter(gl, b.location, b.uniform.latestData); } gl.drawArrays(gl.TRIANGLES, firstVertex, vertexCount); @@ -204,24 +251,25 @@ class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { } } -let _uniformBindingCounter = 0; +let _uniformCounter = 0; + +/** @internal Reset the uniform name counter. For use in tests only. */ +export function _resetUniformCounter(): void { + _uniformCounter = 0; +} class WebGLUniformImpl implements WebGLUniform { readonly resourceType = 'uniform' as const; - readonly bindingIndex: number; - readonly glBuffer: WebGLBuffer; + readonly [$internal] = { dataType: undefined as TData | undefined }; + readonly glslName: string; + latestData: Float32Array; - #gl: WebGL2RenderingContext; #schema: TData; - constructor(gl: WebGL2RenderingContext, schema: TData) { - this.#gl = gl; + constructor(schema: TData) { this.#schema = schema; - this.bindingIndex = _uniformBindingCounter++; - - const buffer = gl.createBuffer(); - if (!buffer) throw new Error('Failed to create WebGL buffer'); - this.glBuffer = buffer; + this.glslName = `_u${_uniformCounter++}`; + this.latestData = new Float32Array(schemaFloatCount(schema)); } get schema(): TData { @@ -229,12 +277,60 @@ class WebGLUniformImpl implements WebGLUniform): void { - const gl = this.#gl; - const floatData = flattenToFloat32(data); - gl.bindBuffer(gl.UNIFORM_BUFFER, this.glBuffer); - gl.bufferData(gl.UNIFORM_BUFFER, floatData, gl.DYNAMIC_DRAW); - gl.bindBuffer(gl.UNIFORM_BUFFER, null); + this.latestData = flattenToFloat32(data); } + + toString(): string { + return `uniform:${this.glslName}`; + } + + // oxlint-disable-next-line typescript/no-explicit-any + [$resolve](ctx: any) { + const dataType = this.#schema; + const glslType = ctx.resolve(dataType).value; + ctx.addDeclaration(`uniform ${glslType} ${this.glslName};`); + return snip(this.glslName, dataType as d.BaseData, 'uniform'); + } + + get [$gpuValueOf](): d.InferGPU { + const self = this; + const dataType = this.#schema; + return new Proxy( + { + [$internal]: true, + get [$ownSnippet]() { + return snip(this, dataType as d.BaseData, 'uniform'); + }, + // oxlint-disable-next-line typescript/no-explicit-any + [$resolve]: (ctx: any) => ctx.resolve(self), + toString: () => `uniform:${self.glslName}.$`, + }, + valueProxyHandler, + ) as d.InferGPU; + } + + get $(): d.InferGPU { + if (inCodegenMode()) { + return this[$gpuValueOf]; + } + throw new Error( + 'Cannot read WebGL uniform outside of shader code. Use `.write()` to update it.', + ); + } +} + +function schemaFloatCount(schema: d.AnyWgslData): number { + const typeName = (schema as { type: string }).type; + if (typeName === 'f32' || typeName === 'u32' || typeName === 'i32' || typeName === 'bool') { + return 1; + } + if (typeName?.startsWith('vec2')) return 2; + if (typeName?.startsWith('vec3')) return 4; // vec3 is typically stored as vec4 aligned + if (typeName?.startsWith('vec4')) return 4; + if (typeName?.startsWith('mat2x2')) return 4; + if (typeName?.startsWith('mat3x3')) return 9; + if (typeName?.startsWith('mat4x4')) return 16; + return 1; } function flattenToFloat32(data: unknown): Float32Array { @@ -287,7 +383,7 @@ export class TgpuRootWebGL { typeSchema: TData, _initial?: d.Infer, ): WebGLUniform { - const uniform = new WebGLUniformImpl(this.#gl, typeSchema); + const uniform = new WebGLUniformImpl(typeSchema); this.#uniforms.push(uniform as WebGLUniformImpl); if (_initial !== undefined) { uniform.write(_initial); @@ -367,7 +463,7 @@ export class TgpuRootWebGL { }): TgpuWebGLRenderPipeline { const { vertex, fragment } = descriptor; - const { AutoVertexFn, AutoFragmentFn, matchUpVaryingLocations } = tgpu['~unstable']; + const { AutoVertexFn, AutoFragmentFn, matchUpVaryingLocations } = ShaderGenerator; const vertexShell = isShellFn(vertex) ? vertex : undefined; const fragmentShell = isShellFn(fragment) ? fragment : undefined; @@ -394,21 +490,18 @@ export class TgpuRootWebGL { if (vertexResolvable instanceof AutoVertexFn) { const outStruct = vertexResolvable.autoOut.completeStruct; varyings = Object.fromEntries( - Object.entries(outStruct.propTypes).filter(([, type]) => !isBuiltinType(type)), + Object.entries(outStruct.propTypes).filter(([, type]) => !d.isBuiltin(type)), ); } else if (vertexShell?.shell?.out) { varyings = Object.fromEntries( Object.entries(vertexShell.shell.out as Record).filter( - ([, type]) => !isBuiltinType(type), + ([, type]) => !d.isBuiltin(type), ), ); } - const fragmentResolvable = fragmentShell ?? new AutoFragmentFn( - fragment as AnyFn, - varyings, - locations, - ); + const fragmentResolvable = + fragmentShell ?? new AutoFragmentFn(fragment as AnyFn, varyings, locations); const fragmentNamespace = tgpu['~unstable'].namespace(); const fragmentCode = tgpu.resolve([fragmentResolvable], { @@ -456,17 +549,11 @@ export class TgpuRootWebGL { for (const buf of this.#buffers) { this.#gl.deleteBuffer(buf); } - for (const uniform of this.#uniforms) { - this.#gl.deleteBuffer(uniform.glBuffer); - } this.#buffers = []; this.#uniforms = []; } } -function isBuiltinType(type: d.BaseData): boolean { - if (d.isDecorated(type)) { - return (type.attribs as d.AnyAttribute[]).some((a) => a.type === '@builtin'); - } - return false; +export function isGLRoot(value: unknown): value is TgpuRootWebGL { + return value instanceof TgpuRootWebGL; } diff --git a/packages/typegpu-gl/tests/glslGenerator.test.ts b/packages/typegpu-gl/tests/glslGenerator.test.ts index a396d3df94..2185ae6201 100644 --- a/packages/typegpu-gl/tests/glslGenerator.test.ts +++ b/packages/typegpu-gl/tests/glslGenerator.test.ts @@ -1,7 +1,9 @@ -import { describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; import tgpu, { d } from 'typegpu'; -import { glOptions } from '@typegpu/gl'; +import { glOptions, initWithGL } from '@typegpu/gl'; import { translateWgslTypeToGlsl } from '../src/glslGenerator.ts'; +import { _resetUniformCounter } from '../src/tgpuRootWebGL.ts'; +import { it as glIt } from './utils/extendedTest.ts'; describe('translateWgslTypeToGlsl', () => { it('translates scalar types', () => { @@ -205,14 +207,14 @@ describe('GlslGenerator - entry point generation with JS functions', () => { const result = tgpu.resolve([vertFn], glOptions()); expect(result).toMatchInlineSnapshot(` - "layout(location = 0) out uv_1; + "out vec2 vary_uv; void main() { vec4 position = vec4(); vec2 uv = vec2(); { - gl_Position = position; - uv_1 = uv; + gl_Position = vec4(position); + vary_uv = vec2(uv); return; } }" @@ -236,13 +238,100 @@ describe('GlslGenerator - entry point generation with JS functions', () => { expect(result.code).not.toMatch(/\bvec4f\s*\(/); expect(result.code).toMatchInlineSnapshot(` - "void main() { + "layout(location = 0) out vec4 _fragColor; + + void main() { int gl_Position_1 = 1; { - gl_Position = vec4(1, 0, 0, 1); + _fragColor = vec4(1, 0, 0, 1); return; } }" `); }); }); + +describe('GlslGenerator - uniform resolution', () => { + beforeEach(() => { + _resetUniformCounter(); + }); + + glIt('emits a uniform declaration and references the name in shader body', ({ gl }) => { + const root = initWithGL({ gl }); + const time = root.createUniform(d.f32); + + const fn = () => { + 'use gpu'; + return time.$; + }; + + const result = tgpu.resolve([fn], GLOptions()); + expect(result).toMatchInlineSnapshot(` + "uniform float _u0; + + float fn() { + return _u0; + }" + `); + }); + + glIt('emits a vec3f uniform as vec3', ({ gl }) => { + const root = initWithGL({ gl }); + const color = root.createUniform(d.vec3f); + + const fn = () => { + 'use gpu'; + return color.$; + }; + + const result = tgpu.resolve([fn], GLOptions()); + expect(result).toMatchInlineSnapshot(` + "uniform vec3 _u0; + + vec3 fn() { + return _u0; + }" + `); + }); + + glIt('emits multiple uniforms with sequential names', ({ gl }) => { + const root = initWithGL({ gl }); + const time = root.createUniform(d.f32); + const scale = root.createUniform(d.f32); + + const fn = () => { + 'use gpu'; + return time.$ * scale.$; + }; + + const result = tgpu.resolve([fn], GLOptions()); + expect(result).toMatchInlineSnapshot(` + "uniform float _u0; + + uniform float _u1; + + float fn_1() { + return (_u0 * _u1); + }" + `); + }); + + glIt('emits a mat2x2f uniform as mat2', ({ gl }) => { + const root = initWithGL({ gl }); + const transform = root.createUniform(d.mat2x2f); + + const fn = (v: d.v2f) => { + 'use gpu'; + return transform.$ * v; + }; + + const result = tgpu.resolve([fn], GLOptions()); + expect(result).toMatchInlineSnapshot(` + "uniform mat2 _u0; + + vec2 fn(vec2 v) { + return (_u0 * v); + }" + `); + }); +}); diff --git a/packages/typegpu/src/tgpuUnstable.ts b/packages/typegpu/src/tgpuUnstable.ts index 3f9d036736..717ab231b5 100644 --- a/packages/typegpu/src/tgpuUnstable.ts +++ b/packages/typegpu/src/tgpuUnstable.ts @@ -4,9 +4,6 @@ export { declare } from './core/declare/tgpuDeclare.ts'; export { rawCodeSnippet } from './core/rawCodeSnippet/tgpuRawCodeSnippet.ts'; export { namespace } from './core/resolve/namespace.ts'; export { simulate } from './core/simulate/tgpuSimulate.ts'; -// TODO: Export from shaderGenerator_members.ts instead -export { AutoVertexFn, AutoFragmentFn } from './core/function/autoIO.ts'; -export { matchUpVaryingLocations } from './core/pipeline/renderPipeline.ts'; // DEPRECATED diff --git a/packages/typegpu/src/tgsl/shaderGenerator_members.ts b/packages/typegpu/src/tgsl/shaderGenerator_members.ts index 768e09b6ae..f37694a1de 100644 --- a/packages/typegpu/src/tgsl/shaderGenerator_members.ts +++ b/packages/typegpu/src/tgsl/shaderGenerator_members.ts @@ -8,6 +8,12 @@ export { UnknownData } from '../data/dataTypes.ts'; export { getName } from '../shared/meta.ts'; export { makeDereferencable } from './makeDereferencable.ts'; export { makeResolvable } from './makeResolvable.ts'; +export { AutoFragmentFn, AutoVertexFn } from '../core/function/autoIO.ts'; +export { matchUpVaryingLocations } from '../core/pipeline/renderPipeline.ts'; +export { valueProxyHandler } from '../core/valueProxyUtils.ts'; +export { inCodegenMode } from '../execMode.ts'; +export { $gpuValueOf, $internal, $ownSnippet, $resolve } from '../shared/symbols.ts'; +export { snip } from '../data/snippet.ts'; // types export type { ResolutionCtx, FunctionArgument, TgpuShaderStage } from '../types.ts'; From 552972750778ee427afa33c9e0a58a2404403e20 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Fri, 24 Apr 2026 21:36:49 +0200 Subject: [PATCH 03/14] Unslopify --- packages/typegpu-gl/package.json | 3 + packages/typegpu-gl/src/glslGenerator.ts | 39 +-- .../typegpu-gl/src/matchUpVaryingLocations.ts | 65 +++++ packages/typegpu-gl/src/tgpuRootWebGL.ts | 225 ++++++++++-------- .../typegpu-gl/tests/glslGenerator.test.ts | 91 +------ packages/typegpu-gl/tests/uniforms.test.ts | 129 ++++++++++ .../typegpu-gl/tests/utils/extendedTest.ts | 10 + .../typegpu-gl/tests/webglFallback.test.ts | 48 +--- packages/typegpu/package.json | 5 + packages/typegpu/src/data/index.ts | 9 +- packages/typegpu/src/indexNamedExports.ts | 5 +- packages/typegpu/src/internals.ts | 14 ++ packages/typegpu/src/resolutionCtx.ts | 6 +- packages/typegpu/src/tgsl/shaderGenerator.ts | 28 ++- packages/typegpu/src/tgsl/wgslGenerator.ts | 10 +- packages/typegpu/src/types.ts | 2 +- packages/typegpu/tests/utils/parseResolved.ts | 15 +- pnpm-lock.yaml | 4 + 18 files changed, 434 insertions(+), 274 deletions(-) create mode 100644 packages/typegpu-gl/src/matchUpVaryingLocations.ts create mode 100644 packages/typegpu-gl/tests/uniforms.test.ts create mode 100644 packages/typegpu/src/internals.ts diff --git a/packages/typegpu-gl/package.json b/packages/typegpu-gl/package.json index 73069dcd47..af4b464282 100644 --- a/packages/typegpu-gl/package.json +++ b/packages/typegpu-gl/package.json @@ -49,5 +49,8 @@ }, "peerDependencies": { "typegpu": "workspace:^" + }, + "dependencies": { + "typed-binary": "^4.3.3" } } diff --git a/packages/typegpu-gl/src/glslGenerator.ts b/packages/typegpu-gl/src/glslGenerator.ts index e66b0ca816..8d4a718930 100644 --- a/packages/typegpu-gl/src/glslGenerator.ts +++ b/packages/typegpu-gl/src/glslGenerator.ts @@ -1,10 +1,13 @@ import { NodeTypeCatalog as NODE } from 'tinyest'; import type { Return } from 'tinyest'; -import tgpu, { d, ShaderGenerator, WgslGenerator } from 'typegpu'; - -type ResolutionCtx = ShaderGenerator.ResolutionCtx; - -const UnknownData: typeof ShaderGenerator.UnknownData = ShaderGenerator.UnknownData; +import tgpu, { d } from 'typegpu'; +import { getName, UnknownData, WgslGenerator } from 'typegpu/~internals'; +import type { + ShaderGenerator, + ResolutionCtx, + TgpuShaderStage, + FunctionDefinitionOptions, +} from 'typegpu/~internals'; // ---------- // WGSL → GLSL type name mapping @@ -45,13 +48,13 @@ export function translateWgslTypeToGlsl(wgslType: string): string { } function resolveStruct(ctx: ResolutionCtx, struct: d.WgslStruct) { - const id = ctx.makeUniqueIdentifier(ShaderGenerator.getName(struct), 'global'); + const id = ctx.makeUniqueIdentifier(getName(struct), 'global'); ctx.addDeclaration(`\ struct ${id} { ${Object.entries(struct.propTypes) - .map(([prop, type]) => ` ${ctx.resolve(type).value} ${prop};\n`) - .join('')}\ + .map(([prop, type]) => ` ${ctx.resolve(type).value} ${prop};\n`) + .join('')}\ };`); return id; @@ -116,7 +119,7 @@ function glslInputForBuiltin( * and overrides variable declaration emission to use `type name = rhs` syntax. */ export class GlslGenerator extends WgslGenerator { - #functionType: ShaderGenerator.TgpuShaderStage | 'normal' | undefined; + #functionType: TgpuShaderStage | 'normal' | undefined; #entryFnState: EntryFnState | undefined; override typeAnnotation(data: d.BaseData): string { @@ -137,7 +140,7 @@ export class GlslGenerator extends WgslGenerator { override _emitVarDecl( _keyword: 'var' | 'let' | 'const', name: string, - dataType: d.BaseData | ShaderGenerator.UnknownData, + dataType: d.BaseData | UnknownData, rhsStr: string, ): string { const glslTypeName = dataType !== UnknownData ? this.ctx.resolve(dataType).value : 'auto'; @@ -147,7 +150,11 @@ export class GlslGenerator extends WgslGenerator { override _return(statement: Return): string { const exprNode = statement[1]; - if (exprNode === undefined || this.#functionType === 'normal' || this.#functionType === undefined) { + if ( + exprNode === undefined || + this.#functionType === 'normal' || + this.#functionType === undefined + ) { // Default behavior return super._return(statement); } @@ -224,10 +231,10 @@ export class GlslGenerator extends WgslGenerator { (expectedReturnType as { type?: string }).type === 'auto-struct'; const autoStruct = isAutoStruct ? (expectedReturnType as unknown as { - completeStruct: d.WgslStruct; - accessProp(key: string): { prop: string; type: d.BaseData } | undefined; - provideProp(key: string, type: d.BaseData): { prop: string; type: d.BaseData }; - }) + completeStruct: d.WgslStruct; + accessProp(key: string): { prop: string; type: d.BaseData } | undefined; + provideProp(key: string, type: d.BaseData): { prop: string; type: d.BaseData }; + }) : undefined; if (autoStruct) { entryFnState.autoOutStruct = autoStruct; @@ -281,7 +288,7 @@ export class GlslGenerator extends WgslGenerator { return `${this.ctx.pre}{\n${lines.join('\n')}\n${this.ctx.pre}}`; } - override functionDefinition(options: ShaderGenerator.FunctionDefinitionOptions): string { + override functionDefinition(options: FunctionDefinitionOptions): string { if (options.functionType !== 'normal') { this.ctx.reserveIdentifier('gl_Position', 'global'); } diff --git a/packages/typegpu-gl/src/matchUpVaryingLocations.ts b/packages/typegpu-gl/src/matchUpVaryingLocations.ts new file mode 100644 index 0000000000..d3f979e5eb --- /dev/null +++ b/packages/typegpu-gl/src/matchUpVaryingLocations.ts @@ -0,0 +1,65 @@ +import { d, type TgpuFragmentFn, type TgpuVertexFn } from 'typegpu'; + +export function getCustomLocation(data: d.BaseData): number | undefined { + return (data as unknown as d.Decorated | d.LooseDecorated).attribs?.find(d.isLocationAttrib) + ?.params[0]; +} +/** + * Assumes vertexOut and fragmentIn are matching when it comes to the keys, that is fragmentIn's keyset is a subset of vertexOut's + * Logs a warning, when they don't match in terms of custom locations + */ +export function matchUpVaryingLocations( + vertexOut: TgpuVertexFn.Out | undefined = {}, + fragmentIn: TgpuFragmentFn.In | undefined = {}, + vertexFnName: string, + fragmentFnName: string, +) { + const locations: Record = {}; + const usedLocations = new Set(); + + function saveLocation(key: string, location: number) { + locations[key] = location; + usedLocations.add(location); + } + + // respect custom locations and pair up vertex and fragment varying with the same key + for (const [key, value] of Object.entries(vertexOut)) { + const customLocation = getCustomLocation(value); + if (customLocation !== undefined) { + saveLocation(key, customLocation); + } + } + + for (const [key, value] of Object.entries(fragmentIn)) { + const customLocation = getCustomLocation(value); + if (customLocation === undefined) { + continue; + } + + if (locations[key] === undefined) { + saveLocation(key, customLocation); + } else if (locations[key] !== customLocation) { + console.warn( + `Mismatched location between vertexFn (${vertexFnName}) output (${ + locations[key] + }) and fragmentFn (${fragmentFnName}) input (${customLocation}) for the key "${key}", using the location set on vertex output.`, + ); + } + } + + // automatically assign remaining locations to the rest + let nextLocation = 0; + for (const key of Object.keys(vertexOut ?? {})) { + if (d.isBuiltin(vertexOut[key]) || locations[key] !== undefined) { + continue; + } + + while (usedLocations.has(nextLocation)) { + nextLocation++; + } + + saveLocation(key, nextLocation); + } + + return locations; +} diff --git a/packages/typegpu-gl/src/tgpuRootWebGL.ts b/packages/typegpu-gl/src/tgpuRootWebGL.ts index 2f946181f2..00dbfcb3b6 100644 --- a/packages/typegpu-gl/src/tgpuRootWebGL.ts +++ b/packages/typegpu-gl/src/tgpuRootWebGL.ts @@ -6,11 +6,27 @@ * Compute operations, storage buffers, textures, etc. throw WebGLFallbackUnsupportedError. */ -import tgpu, { d, ShaderGenerator, type TgpuFragmentFn, type TgpuVertexFn } from 'typegpu'; -import glslGenerator from './glslGenerator.ts'; +import tgpu, { + d, + patchArrayBuffer, + readFromArrayBuffer, + writeToArrayBuffer, + type BufferInitialData, + type BufferWriteOptions, + type TgpuBuffer, + type TgpuFragmentFn, + type TgpuVertexFn, +} from 'typegpu'; +import { + AutoFragmentFn, + AutoVertexFn, + makeDereferencable, + makeResolvable, + type ShaderGenerator, +} from 'typegpu/~internals'; -const { $gpuValueOf, $internal, $ownSnippet, $resolve, snip, valueProxyHandler, inCodegenMode } = - ShaderGenerator; +import glslGenerator from './glslGenerator.ts'; +import { matchUpVaryingLocations } from './matchUpVaryingLocations.ts'; // ---------- // Public API @@ -33,19 +49,22 @@ export interface WebGLRenderContext { } export interface TgpuWebGLRenderPipeline { + // TODO: Is 'screen' necessary? withColorAttachment(attachment: { view: WebGLRenderContext | 'screen' }): this; draw(vertexCount: number, instanceCount?: number, firstVertex?: number): void; } interface WebGLUniform { readonly resourceType: 'uniform'; - readonly schema: TData; + readonly dataType: TData; write(data: d.Infer): void; + readonly $: d.InferGPU; + /** @internal The stable GLSL identifier for this uniform */ readonly glslName: string; - /** @internal The latest Float32Array representation of the written data */ - readonly latestData: Float32Array; + /** @internal The latest ArrayBuffer representation of the written data */ + readonly buffer: ArrayBuffer; } // ---------- @@ -157,22 +176,29 @@ function linkProgram( interface UniformBinding { uniform: WebGLUniform; location: WebGLUniformLocation; - setter: (gl: WebGL2RenderingContext, loc: WebGLUniformLocation, data: Float32Array) => void; + setter: (gl: WebGL2RenderingContext, loc: WebGLUniformLocation, data: ArrayBuffer) => void; } function uniformSetterFor( schema: d.AnyWgslData, -): (gl: WebGL2RenderingContext, loc: WebGLUniformLocation, data: Float32Array) => void { +): (gl: WebGL2RenderingContext, loc: WebGLUniformLocation, dataView: ArrayBuffer) => void { const typeName = (schema as { type: string }).type; - if (typeName === 'f32') return (gl, loc, data) => gl.uniform1f(loc, data[0] ?? 0); - if (typeName === 'u32') return (gl, loc, data) => gl.uniform1ui(loc, data[0] ?? 0); - if (typeName === 'i32') return (gl, loc, data) => gl.uniform1i(loc, data[0] ?? 0); - if (typeName === 'vec2f') return (gl, loc, data) => gl.uniform2fv(loc, data); - if (typeName === 'vec3f') return (gl, loc, data) => gl.uniform3fv(loc, data.subarray(0, 3)); - if (typeName === 'vec4f') return (gl, loc, data) => gl.uniform4fv(loc, data); - if (typeName === 'mat2x2f') return (gl, loc, data) => gl.uniformMatrix2fv(loc, false, data); - if (typeName === 'mat3x3f') return (gl, loc, data) => gl.uniformMatrix3fv(loc, false, data); - if (typeName === 'mat4x4f') return (gl, loc, data) => gl.uniformMatrix4fv(loc, false, data); + if (typeName === 'f32') + return (gl, loc, data) => gl.uniform1f(loc, new Float32Array(data)[0] ?? 0); + if (typeName === 'u32') + return (gl, loc, data) => gl.uniform1ui(loc, new Float32Array(data)[0] ?? 0); + if (typeName === 'i32') + return (gl, loc, data) => gl.uniform1i(loc, new Float32Array(data)[0] ?? 0); + if (typeName === 'vec2f') return (gl, loc, data) => gl.uniform2fv(loc, new Float32Array(data)); + if (typeName === 'vec3f') + return (gl, loc, data) => gl.uniform3fv(loc, new Float32Array(data).subarray(0, 3)); + if (typeName === 'vec4f') return (gl, loc, data) => gl.uniform4fv(loc, new Float32Array(data)); + if (typeName === 'mat2x2f') + return (gl, loc, data) => gl.uniformMatrix2fv(loc, false, new Float32Array(data)); + if (typeName === 'mat3x3f') + return (gl, loc, data) => gl.uniformMatrix3fv(loc, false, new Float32Array(data)); + if (typeName === 'mat4x4f') + return (gl, loc, data) => gl.uniformMatrix4fv(loc, false, new Float32Array(data)); return () => {}; } @@ -187,7 +213,7 @@ class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { constructor( gl: WebGL2RenderingContext, program: WebGLProgram, - uniforms: Array, + uniforms: WebGLUniform[], offscreen: OffscreenCanvas, ) { this.#gl = gl; @@ -202,7 +228,7 @@ class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { for (const uniform of uniforms) { const location = gl.getUniformLocation(program, uniform.glslName); if (location !== null) { - bindings.push({ uniform, location, setter: uniformSetterFor(uniform.schema) }); + bindings.push({ uniform, location, setter: uniformSetterFor(uniform.dataType) }); } } this.#uniformBindings = bindings; @@ -232,7 +258,7 @@ class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { // Upload current uniform values for (const b of this.#uniformBindings) { - b.setter(gl, b.location, b.uniform.latestData); + b.setter(gl, b.location, b.uniform.buffer); } gl.drawArrays(gl.TRIANGLES, firstVertex, vertexCount); @@ -260,101 +286,92 @@ export function _resetUniformCounter(): void { class WebGLUniformImpl implements WebGLUniform { readonly resourceType = 'uniform' as const; - readonly [$internal] = { dataType: undefined as TData | undefined }; readonly glslName: string; - latestData: Float32Array; - #schema: TData; + readonly #gl: WebGL2RenderingContext; + readonly #initial: BufferInitialData | undefined; - constructor(schema: TData) { - this.#schema = schema; + readonly dataType: TData; + + buffer: ArrayBuffer; + webglBuffer: WebGLBuffer | undefined; + + declare readonly $: d.InferGPU; + + constructor(gl: WebGL2RenderingContext, dataType: TData, initial?: BufferInitialData) { + this.#gl = gl; + this.dataType = dataType; + this.#initial = initial; this.glslName = `_u${_uniformCounter++}`; - this.latestData = new Float32Array(schemaFloatCount(schema)); + this.buffer = new ArrayBuffer(d.sizeOf(dataType)); } - get schema(): TData { - return this.#schema; + unwrap() { + if (!this.webglBuffer) { + const gl = this.#gl; + this.webglBuffer = gl.createBuffer(); + const bindingPoint = 0; + gl.bindBufferBase(gl.UNIFORM_BUFFER, bindingPoint, this.webglBuffer); + const initialData = + typeof this.#initial === 'function' ? (this.#initial as any)(this) : undefined; + writeToArrayBuffer(this.buffer, this.dataType, initialData); + + gl.bufferData(gl.UNIFORM_BUFFER, this.buffer, gl.DYNAMIC_DRAW); + gl.bindBufferBase(gl.UNIFORM_BUFFER, bindingPoint, null); + } + return this.webglBuffer; } - write(data: d.Infer): void { - this.latestData = flattenToFloat32(data); + write(data: d.InferInput, options?: BufferWriteOptions): void { + writeToArrayBuffer(this.buffer, this.dataType, data, options); } - toString(): string { - return `uniform:${this.glslName}`; + public patch(data: d.InferPatch): void { + patchArrayBuffer(this.buffer, this.dataType, data); } - // oxlint-disable-next-line typescript/no-explicit-any - [$resolve](ctx: any) { - const dataType = this.#schema; - const glslType = ctx.resolve(dataType).value; - ctx.addDeclaration(`uniform ${glslType} ${this.glslName};`); - return snip(this.glslName, dataType as d.BaseData, 'uniform'); - } - - get [$gpuValueOf](): d.InferGPU { - const self = this; - const dataType = this.#schema; - return new Proxy( - { - [$internal]: true, - get [$ownSnippet]() { - return snip(this, dataType as d.BaseData, 'uniform'); - }, - // oxlint-disable-next-line typescript/no-explicit-any - [$resolve]: (ctx: any) => ctx.resolve(self), - toString: () => `uniform:${self.glslName}.$`, - }, - valueProxyHandler, - ) as d.InferGPU; - } - - get $(): d.InferGPU { - if (inCodegenMode()) { - return this[$gpuValueOf]; - } - throw new Error( - 'Cannot read WebGL uniform outside of shader code. Use `.write()` to update it.', - ); + public clear(): void { + new Uint8Array(this.buffer).fill(0); } -} -function schemaFloatCount(schema: d.AnyWgslData): number { - const typeName = (schema as { type: string }).type; - if (typeName === 'f32' || typeName === 'u32' || typeName === 'i32' || typeName === 'bool') { - return 1; - } - if (typeName?.startsWith('vec2')) return 2; - if (typeName?.startsWith('vec3')) return 4; // vec3 is typically stored as vec4 aligned - if (typeName?.startsWith('vec4')) return 4; - if (typeName?.startsWith('mat2x2')) return 4; - if (typeName?.startsWith('mat3x3')) return 9; - if (typeName?.startsWith('mat4x4')) return 16; - return 1; -} + copyFrom(_srcBuffer: TgpuBuffer>): void { + throw new WebGLFallbackUnsupportedError('.copyFrom()'); + } -function flattenToFloat32(data: unknown): Float32Array { - if (data instanceof Float32Array) return data; - if (typeof data === 'number') return new Float32Array([data]); - if (Array.isArray(data)) { - const arr: number[] = []; - for (const item of data) { - const sub = flattenToFloat32(item); - for (const v of sub) arr.push(v); - } - return new Float32Array(arr); + read(): Promise> { + return Promise.resolve(readFromArrayBuffer(this.buffer, this.dataType)); } - if (data !== null && typeof data === 'object') { - const arr: number[] = []; - for (const val of Object.values(data as Record)) { - const sub = flattenToFloat32(val); - for (const v of sub) arr.push(v); + + destroy() { + if (this.webglBuffer) { + this.#gl.deleteBuffer(this.webglBuffer); + this.webglBuffer = undefined; } - return new Float32Array(arr); } - return new Float32Array([0]); + + toString(): string { + return `uniform:${this.glslName}`; + } } +makeDereferencable( + makeResolvable(WebGLUniformImpl.prototype, { + resolve(ctx) { + const glslType = ctx.resolve(this.dataType).value; + ctx.addDeclaration(`uniform ${glslType} ${this.glslName};`); + return { value: this.glslName, dataType: this.dataType, origin: 'uniform' }; + }, + asString() { + return `uniform:${this.glslName}`; + }, + }), + { + getDataTypeAndOrigin(): [dataType: d.BaseData, origin: 'uniform'] { + return [this.dataType, 'uniform']; + }, + }, +); + // oxlint-disable-next-line typescript/no-explicit-any type AnyFn = (...args: any[]) => any; @@ -367,7 +384,7 @@ export class TgpuRootWebGL { #gl: WebGL2RenderingContext; #offscreen: OffscreenCanvas; - #uniforms: Array> = []; + #uniforms: WebGLUniformImpl[] = []; #buffers: WebGLBuffer[] = []; constructor(gl: WebGL2RenderingContext) { @@ -381,13 +398,10 @@ export class TgpuRootWebGL { createUniform( typeSchema: TData, - _initial?: d.Infer, + initial?: BufferInitialData, ): WebGLUniform { - const uniform = new WebGLUniformImpl(typeSchema); - this.#uniforms.push(uniform as WebGLUniformImpl); - if (_initial !== undefined) { - uniform.write(_initial); - } + const uniform = new WebGLUniformImpl(this.#gl, typeSchema, initial); + this.#uniforms.push(uniform); return uniform; } @@ -463,8 +477,6 @@ export class TgpuRootWebGL { }): TgpuWebGLRenderPipeline { const { vertex, fragment } = descriptor; - const { AutoVertexFn, AutoFragmentFn, matchUpVaryingLocations } = ShaderGenerator; - const vertexShell = isShellFn(vertex) ? vertex : undefined; const fragmentShell = isShellFn(fragment) ? fragment : undefined; @@ -550,6 +562,9 @@ export class TgpuRootWebGL { this.#gl.deleteBuffer(buf); } this.#buffers = []; + for (const uniform of this.#uniforms) { + uniform.destroy(); + } this.#uniforms = []; } } diff --git a/packages/typegpu-gl/tests/glslGenerator.test.ts b/packages/typegpu-gl/tests/glslGenerator.test.ts index 2185ae6201..b76d42fc80 100644 --- a/packages/typegpu-gl/tests/glslGenerator.test.ts +++ b/packages/typegpu-gl/tests/glslGenerator.test.ts @@ -1,9 +1,9 @@ -import { beforeEach, describe, expect, it } from 'vitest'; +import { describe, expect } from 'vitest'; import tgpu, { d } from 'typegpu'; -import { glOptions, initWithGL } from '@typegpu/gl'; +import { glOptions } from '@typegpu/gl'; import { translateWgslTypeToGlsl } from '../src/glslGenerator.ts'; import { _resetUniformCounter } from '../src/tgpuRootWebGL.ts'; -import { it as glIt } from './utils/extendedTest.ts'; +import { it } from './utils/extendedTest.ts'; describe('translateWgslTypeToGlsl', () => { it('translates scalar types', () => { @@ -250,88 +250,3 @@ describe('GlslGenerator - entry point generation with JS functions', () => { `); }); }); - -describe('GlslGenerator - uniform resolution', () => { - beforeEach(() => { - _resetUniformCounter(); - }); - - glIt('emits a uniform declaration and references the name in shader body', ({ gl }) => { - const root = initWithGL({ gl }); - const time = root.createUniform(d.f32); - - const fn = () => { - 'use gpu'; - return time.$; - }; - - const result = tgpu.resolve([fn], GLOptions()); - expect(result).toMatchInlineSnapshot(` - "uniform float _u0; - - float fn() { - return _u0; - }" - `); - }); - - glIt('emits a vec3f uniform as vec3', ({ gl }) => { - const root = initWithGL({ gl }); - const color = root.createUniform(d.vec3f); - - const fn = () => { - 'use gpu'; - return color.$; - }; - - const result = tgpu.resolve([fn], GLOptions()); - expect(result).toMatchInlineSnapshot(` - "uniform vec3 _u0; - - vec3 fn() { - return _u0; - }" - `); - }); - - glIt('emits multiple uniforms with sequential names', ({ gl }) => { - const root = initWithGL({ gl }); - const time = root.createUniform(d.f32); - const scale = root.createUniform(d.f32); - - const fn = () => { - 'use gpu'; - return time.$ * scale.$; - }; - - const result = tgpu.resolve([fn], GLOptions()); - expect(result).toMatchInlineSnapshot(` - "uniform float _u0; - - uniform float _u1; - - float fn_1() { - return (_u0 * _u1); - }" - `); - }); - - glIt('emits a mat2x2f uniform as mat2', ({ gl }) => { - const root = initWithGL({ gl }); - const transform = root.createUniform(d.mat2x2f); - - const fn = (v: d.v2f) => { - 'use gpu'; - return transform.$ * v; - }; - - const result = tgpu.resolve([fn], GLOptions()); - expect(result).toMatchInlineSnapshot(` - "uniform mat2 _u0; - - vec2 fn(vec2 v) { - return (_u0 * v); - }" - `); - }); -}); diff --git a/packages/typegpu-gl/tests/uniforms.test.ts b/packages/typegpu-gl/tests/uniforms.test.ts new file mode 100644 index 0000000000..db5593010c --- /dev/null +++ b/packages/typegpu-gl/tests/uniforms.test.ts @@ -0,0 +1,129 @@ +import { describe, beforeEach, expect } from 'vitest'; +import tgpu, { d } from 'typegpu'; +import { glOptions, initWithGL } from '@typegpu/gl'; +import { _resetUniformCounter } from '../src/tgpuRootWebGL.ts'; +import { it } from './utils/extendedTest.ts'; + +describe('TgpuRootWebGL - createUniform', () => { + it('creates a WebGL UBO-backed uniform', ({ gl }) => { + const root = initWithGL({ gl }); + + const uniform = root.createUniform(d.vec4f); + expect(uniform).toBeDefined(); + expect(uniform.resourceType).toBe('uniform'); + + expect(gl.createBuffer).toHaveBeenCalled(); + }); + + it('creates a uniform with an initial value', ({ gl }) => { + const root = initWithGL({ gl }); + + const uniform = root.createUniform(d.f32, 42); + expect(uniform).toBeDefined(); + // Should have called bufferData to set initial value + expect(gl.bufferData).toHaveBeenCalled(); + }); + + it('allows writing to the uniform', async ({ gl }) => { + const root = initWithGL({ gl }); + + const uniform = root.createUniform(d.f32); + uniform.write(1.0); + + expect(await uniform.read()).toBe(1.0); + }); +}); + +describe('GlslGenerator - uniform resolution', () => { + beforeEach(() => { + _resetUniformCounter(); + }); + + it('emits a uniform declaration and references the name in shader body', ({ gl }) => { + const root = initWithGL({ gl }); + const time = root.createUniform(d.f32); + + const fn = () => { + 'use gpu'; + return d.f32(time.$); + }; + + const result = tgpu.resolve([fn], glOptions()); + expect(result).toMatchInlineSnapshot(` + "uniform float _u0; + + float fn_1() { + return _u0; + }" + `); + }); + + it('emits a vec3f uniform as vec3', ({ gl }) => { + const root = initWithGL({ gl }); + const color = root.createUniform(d.vec3f); + + const fn = () => { + 'use gpu'; + return d.vec3f(color.$); + }; + + const result = tgpu.resolve([fn], glOptions()); + expect(result).toMatchInlineSnapshot(` + "uniform vec3 _u0; + + vec3 fn_1() { + return _u0; + }" + `); + }); + + it('emits multiple uniforms with sequential names', ({ gl }) => { + const root = initWithGL({ gl }); + const time = root.createUniform(d.f32); + const scale = root.createUniform(d.f32); + + const fn = () => { + 'use gpu'; + return time.$ * scale.$; + }; + + const result = tgpu.resolve([fn], glOptions()); + expect(result).toMatchInlineSnapshot(` + "uniform float _u0; + + uniform float _u1; + + float fn_1() { + return (_u0 * _u1); + }" + `); + }); + + it('emits a mat2x2f uniform as mat2', ({ gl }) => { + const root = initWithGL({ gl }); + const transform = root.createUniform(d.mat2x2f); + + function fn(v: d.v2f) { + 'use gpu'; + return transform.$ * v; + } + + function main() { + 'use gpu'; + return fn(d.vec2f(1, 2)); + } + + const result = tgpu.resolve([main], glOptions()); + expect(result).toMatchInlineSnapshot(` + "uniform mat2 _u0; + + vec2 fn_1(vec2 v) { + return (_u0 * v); + } + + vec2 main() { + return fn_1(vec2(1, 2)); + }" + `); + }); +}); diff --git a/packages/typegpu-gl/tests/utils/extendedTest.ts b/packages/typegpu-gl/tests/utils/extendedTest.ts index 56e0f540ce..e30dc85aee 100644 --- a/packages/typegpu-gl/tests/utils/extendedTest.ts +++ b/packages/typegpu-gl/tests/utils/extendedTest.ts @@ -61,6 +61,12 @@ function createMockWebGL2(canvas: OffscreenCanvas) { return b as unknown as WebGLBuffer; }; + const mockVertexArray = () => { + const va = { _type: 'vertexArray' }; + + return va as unknown as WebGLVertexArrayObject; + }; + const gl = { canvas, @@ -91,6 +97,10 @@ function createMockWebGL2(canvas: OffscreenCanvas) { bindBufferBase: vi.fn(), bufferData: vi.fn(), + createVertexArray: vi.fn(mockVertexArray), + deleteVertexArray: vi.fn(), + bindVertexArray: vi.fn(), + createShader: vi.fn((_type: number) => mockShader()), shaderSource: vi.fn(), compileShader: vi.fn(), diff --git a/packages/typegpu-gl/tests/webglFallback.test.ts b/packages/typegpu-gl/tests/webglFallback.test.ts index 6bf5da8552..d7cf372f16 100644 --- a/packages/typegpu-gl/tests/webglFallback.test.ts +++ b/packages/typegpu-gl/tests/webglFallback.test.ts @@ -49,36 +49,6 @@ describe('TgpuRootWebGL - unsupported operations throw', () => { }); }); -describe('TgpuRootWebGL - createUniform', () => { - it('creates a WebGL UBO-backed uniform', ({ gl }) => { - const root = initWithGL({ gl }); - - const uniform = root.createUniform(d.vec4f); - expect(uniform).toBeDefined(); - expect(uniform.resourceType).toBe('uniform'); - expect(gl.createBuffer).toHaveBeenCalled(); - }); - - it('creates a uniform with an initial value', ({ gl }) => { - const root = initWithGL({ gl }); - - const uniform = root.createUniform(d.f32, 42); - expect(uniform).toBeDefined(); - // Should have called bufferData to set initial value - expect(gl.bufferData).toHaveBeenCalled(); - }); - - it('allows writing to the uniform', ({ gl }) => { - const root = initWithGL({ gl }); - - const uniform = root.createUniform(d.f32); - uniform.write(1.0); - - expect(gl.bindBuffer).toHaveBeenCalled(); - expect(gl.bufferData).toHaveBeenCalled(); - }); -}); - describe('TgpuRootWebGL - configureContext', () => { it('returns a WebGLRenderContext with the provided canvas', ({ gl }) => { const root = initWithGL({ gl }); @@ -158,15 +128,13 @@ describe('TgpuRootWebGL - createRenderPipeline', () => { }); }); -describe('TgpuRootWebGL - destroy', () => { - it('destroys uniforms and buffers on destroy()', ({ gl }) => { - const root = initWithGL({ gl }); - - const foo1 = root.createUniform(d.f32); - const foo2 = root.createUniform(d.vec4f); +// TODO: Track destroying buffers once buffers can be created +// describe('TgpuRootWebGL - destroy', () => { +// it('destroys buffers on destroy()', ({ gl }) => { +// const root = initWithGL({ gl }); - root.destroy(); +// root.destroy(); - expect(gl.deleteBuffer).toHaveBeenCalledTimes(2); - }); -}); +// expect(gl.deleteBuffer).toHaveBeenCalledTimes(2); +// }); +// }); diff --git a/packages/typegpu/package.json b/packages/typegpu/package.json index bc35bc6750..79ec7ee4c3 100644 --- a/packages/typegpu/package.json +++ b/packages/typegpu/package.json @@ -33,6 +33,7 @@ "./data": "./src/data/index.ts", "./std": "./src/std/index.ts", "./common": "./src/common/index.ts", + "./~internals": "./src/internals.ts", "./$built$": { "types": "./dist/index.d.ts", "default": "./dist/index.js" @@ -70,6 +71,10 @@ "./common": { "types": "./dist/common/index.d.ts", "default": "./dist/common/index.js" + }, + "./~internals": { + "types": "./dist/internals.d.ts", + "default": "./dist/internals.js" } }, "linkDirectory": false, diff --git a/packages/typegpu/src/data/index.ts b/packages/typegpu/src/data/index.ts index 5eebe77415..b39ca25e1c 100644 --- a/packages/typegpu/src/data/index.ts +++ b/packages/typegpu/src/data/index.ts @@ -234,4 +234,11 @@ export type { BuiltinVertexIndex, BuiltinWorkgroupId, } from '../builtin.ts'; -export type { Infer, InferGPU, InferInput, InferPartial, InferPatch } from '../shared/repr.ts'; +export type { + Infer, + InferGPU, + InferInput, + InferPartial, + InferPatch, + MemIdentity, +} from '../shared/repr.ts'; diff --git a/packages/typegpu/src/indexNamedExports.ts b/packages/typegpu/src/indexNamedExports.ts index b53fb3a690..dba03e15ef 100644 --- a/packages/typegpu/src/indexNamedExports.ts +++ b/packages/typegpu/src/indexNamedExports.ts @@ -25,8 +25,6 @@ export { isTgpuFragmentFn } from './core/function/tgpuFragmentFn.ts'; export { isTgpuVertexFn } from './core/function/tgpuVertexFn.ts'; export { isTgpuComputeFn } from './core/function/tgpuComputeFn.ts'; export { isVariable } from './core/variable/tgpuVariable.ts'; -export { ShaderGenerator } from './tgsl/shaderGenerator.ts'; -export { WgslGenerator } from './tgsl/wgslGenerator.ts'; export { readFromArrayBuffer, writeToArrayBuffer } from './data/dataIO.ts'; export { patchArrayBuffer } from './data/partialIO.ts'; @@ -61,6 +59,9 @@ export type { ValidUsagesFor, Vertex, VertexFlag, + BufferWriteOptions, + BufferInitCallback, + BufferInitialData, } from './core/buffer/buffer.ts'; export type { TgpuBufferMutable, diff --git a/packages/typegpu/src/internals.ts b/packages/typegpu/src/internals.ts new file mode 100644 index 0000000000..9b42a399d6 --- /dev/null +++ b/packages/typegpu/src/internals.ts @@ -0,0 +1,14 @@ +// Each export here is available as a member on the 'typegpu/~internals` import. + +export { UnknownData } from './data/dataTypes.ts'; +export { getName } from './shared/meta.ts'; +export { makeDereferencable } from './tgsl/makeDereferencable.ts'; +export { makeResolvable } from './tgsl/makeResolvable.ts'; +export { AutoFragmentFn, AutoVertexFn } from './core/function/autoIO.ts'; +export { WgslGenerator } from './tgsl/wgslGenerator.ts'; + +// types +export type { ResolutionCtx, FunctionArgument, TgpuShaderStage } from './types.ts'; +export type { Snippet, Origin } from './data/snippet.ts'; + +export type { ShaderGenerator, FunctionDefinitionOptions } from './tgsl/shaderGenerator.ts'; diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index 6ffdc2d10e..50a1ed452f 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -50,7 +50,7 @@ import type { TgpuShaderStage, Wgsl, } from './types.ts'; -import { CodegenState, isSelfResolvable, NormalState } from './types.ts'; +import { CodegenState, isSelfResolvable, NormalState, type FunctionArgument } from './types.ts'; import type { WgslExtension } from './wgslExtensions.ts'; import { getName, hasTinyestMetadata, setName } from './shared/meta.ts'; import { FuncParameterType } from 'tinyest'; @@ -59,7 +59,6 @@ import { createIoSchema } from './core/function/ioSchema.ts'; import type { IOData } from './core/function/fnTypes.ts'; import { AutoStruct } from './data/autoStruct.ts'; import { EntryInputRouter } from './core/function/entryInputRouter.ts'; -import type { FunctionArgument } from './tgsl/shaderGenerator_members.ts'; import { validateIdentifier, sanitizePrimer } from './nameUtils.ts'; /** @@ -1052,8 +1051,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { } throw new WgslTypeError( - `Value ${safeStringify(item)} is not resolvable${ - schema ? ` to type ${safeStringify(schema)}` : '' + `Value ${safeStringify(item)} is not resolvable${schema ? ` to type ${safeStringify(schema)}` : '' }`, ); } diff --git a/packages/typegpu/src/tgsl/shaderGenerator.ts b/packages/typegpu/src/tgsl/shaderGenerator.ts index 635e955a49..c085cd2f80 100644 --- a/packages/typegpu/src/tgsl/shaderGenerator.ts +++ b/packages/typegpu/src/tgsl/shaderGenerator.ts @@ -1,10 +1,28 @@ +import type { Block } from 'tinyest'; import type { BaseData } from '../data/wgslTypes.ts'; import type { GenerationCtx } from './generationHelpers.ts'; import type { ResolvedSnippet, Snippet } from '../data/snippet.ts'; -import type { - FunctionDefinitionOptions, - VariableDefinitionOptions, -} from './shaderGenerator_members.ts'; +import type { VariableScope } from '../core/variable/tgpuVariable.ts'; +import type { BindableBufferUsage, FunctionArgument, TgpuShaderStage } from '../types.ts'; + +export interface FunctionDefinitionOptions { + readonly functionType: 'normal' | TgpuShaderStage; + readonly name: string; + readonly workgroupSize?: readonly number[] | undefined; + readonly args: readonly FunctionArgument[]; + readonly body: Block; + + determineReturnType(): BaseData; +} + +export interface VariableDefinitionOptions { + readonly scope: VariableScope | BindableBufferUsage; + readonly name: string; + readonly dataType: BaseData; + readonly init: Snippet | undefined; + readonly group?: string | undefined; + readonly binding?: number | undefined; +} /** * **NOTE: This is an unstable API and may change in the future.** @@ -22,5 +40,3 @@ export interface ShaderGenerator { typeInstantiation(schema: BaseData, args: readonly Snippet[]): ResolvedSnippet; typeAnnotation(schema: BaseData): string; } - -export * as ShaderGenerator from './shaderGenerator_members.ts'; diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index a1915fb450..189a3409cf 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -36,7 +36,11 @@ import { } from './generationHelpers.ts'; import { accessIndex } from './accessIndex.ts'; import { accessProp } from './accessProp.ts'; -import type { ShaderGenerator } from './shaderGenerator.ts'; +import type { + ShaderGenerator, + FunctionDefinitionOptions, + VariableDefinitionOptions, +} from './shaderGenerator.ts'; import { resolveData } from '../core/resolve/resolveData.ts'; import { createPtrFromOrigin, implicitFrom, ptrFn } from '../data/ptr.ts'; import { _ref, RefOperator } from '../data/ref.ts'; @@ -50,10 +54,6 @@ import type { ExternalMap } from '../core/resolve/externals.ts'; import * as forOfUtils from './forOfUtils.ts'; import { isTgpuRange } from '../std/range.ts'; import { stringifyNode } from '../shared/tseynit.ts'; -import type { - FunctionDefinitionOptions, - VariableDefinitionOptions, -} from './shaderGenerator_members.ts'; import { getAttributesString } from '../data/attributes.ts'; import type { VariableScope } from '../core/variable/tgpuVariable.ts'; diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index 0a2cdbb016..1e7b078a54 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -47,7 +47,7 @@ import { import type { TgpuBindGroupLayout, TgpuLayoutEntry } from './tgpuBindGroupLayout.ts'; import type { WgslExtension } from './wgslExtensions.ts'; import type { Infer } from './shared/repr.ts'; -import { ShaderGenerator } from './tgsl/shaderGenerator.ts'; +import type { ShaderGenerator } from './tgsl/shaderGenerator.ts'; export type ResolvableObject = | SelfResolvable diff --git a/packages/typegpu/tests/utils/parseResolved.ts b/packages/typegpu/tests/utils/parseResolved.ts index f6dffc8bdd..51c757594b 100644 --- a/packages/typegpu/tests/utils/parseResolved.ts +++ b/packages/typegpu/tests/utils/parseResolved.ts @@ -1,11 +1,14 @@ import type * as tinyest from 'tinyest'; import { NodeTypeCatalog as NODE } from 'tinyest'; import { type Assertion, expect } from 'vitest'; -import tgpu, { d, ShaderGenerator, WgslGenerator } from 'typegpu'; - -type Snippet = ShaderGenerator.Snippet; -type UnknownData = ShaderGenerator.UnknownData; -type Origin = ShaderGenerator.Origin; +import tgpu, { d } from 'typegpu'; +import { + type Snippet, + UnknownData, + type Origin, + WgslGenerator, + type FunctionDefinitionOptions, +} from 'typegpu/~internals'; class ExtractingGenerator extends WgslGenerator { #fnDepth: number; @@ -17,7 +20,7 @@ class ExtractingGenerator extends WgslGenerator { this.#fnDepth = 0; } - public functionDefinition(options: ShaderGenerator.FunctionDefinitionOptions): string { + public functionDefinition(options: FunctionDefinitionOptions): string { this.#fnDepth++; try { return super.functionDefinition(options); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abca5966f5..1b0ca5aa57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -644,6 +644,10 @@ importers: publishDirectory: dist packages/typegpu-gl: + dependencies: + typed-binary: + specifier: ^4.3.3 + version: 4.3.3 devDependencies: '@typegpu/tgpu-dev-cli': specifier: workspace:* From 73c6b9557ecc789910103077e8dfbb17375700ed Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 17 May 2026 11:00:21 +0200 Subject: [PATCH 04/14] Generate vertex and fragment from shell-less entry functions using only the public APIs --- packages/typegpu-gl/src/glOptions.ts | 14 +++- packages/typegpu-gl/src/glslGenerator.ts | 40 +++++++--- .../typegpu-gl/src/matchUpVaryingLocations.ts | 65 ---------------- packages/typegpu-gl/src/tgpuRootWebGL.ts | 78 +++---------------- .../typegpu-gl/tests/glslGenerator.test.ts | 16 ++-- packages/typegpu-gl/tests/uniforms.test.ts | 14 ++-- packages/typegpu/src/internals.ts | 1 - 7 files changed, 67 insertions(+), 161 deletions(-) delete mode 100644 packages/typegpu-gl/src/matchUpVaryingLocations.ts diff --git a/packages/typegpu-gl/src/glOptions.ts b/packages/typegpu-gl/src/glOptions.ts index 1e65a593db..bd2d044e60 100644 --- a/packages/typegpu-gl/src/glOptions.ts +++ b/packages/typegpu-gl/src/glOptions.ts @@ -1,7 +1,15 @@ -import glslGenerator from './glslGenerator.ts'; +import { fragmentGlslGenerator, glslGenerator, vertexGlslGenerator } from './glslGenerator.ts'; -export function glOptions() { +export interface GLOptionsParams { + shaderStage: 'none' | 'vertex' | 'fragment'; +} + +export function glOptions(params: GLOptionsParams) { return { - unstable_shaderGenerator: glslGenerator, + unstable_shaderGenerator: { + none: glslGenerator, + vertex: vertexGlslGenerator, + fragment: fragmentGlslGenerator, + }[params.shaderStage], }; } diff --git a/packages/typegpu-gl/src/glslGenerator.ts b/packages/typegpu-gl/src/glslGenerator.ts index 8d4a718930..dec1000197 100644 --- a/packages/typegpu-gl/src/glslGenerator.ts +++ b/packages/typegpu-gl/src/glslGenerator.ts @@ -2,12 +2,7 @@ import { NodeTypeCatalog as NODE } from 'tinyest'; import type { Return } from 'tinyest'; import tgpu, { d } from 'typegpu'; import { getName, UnknownData, WgslGenerator } from 'typegpu/~internals'; -import type { - ShaderGenerator, - ResolutionCtx, - TgpuShaderStage, - FunctionDefinitionOptions, -} from 'typegpu/~internals'; +import type { ResolutionCtx, TgpuShaderStage, FunctionDefinitionOptions } from 'typegpu/~internals'; // ---------- // WGSL → GLSL type name mapping @@ -121,6 +116,18 @@ function glslInputForBuiltin( export class GlslGenerator extends WgslGenerator { #functionType: TgpuShaderStage | 'normal' | undefined; #entryFnState: EntryFnState | undefined; + #vertexOutPropToVarMap: Record; + #shaderStageToEmit: 'none' | 'vertex' | 'fragment'; + + constructor(shaderStageToEmit: 'none' | 'vertex' | 'fragment') { + super(); + this.#shaderStageToEmit = shaderStageToEmit; + } + + override initGenerator(ctx: ResolutionCtx) { + super.initGenerator(ctx as any); + this.#vertexOutPropToVarMap = {}; + } override typeAnnotation(data: d.BaseData): string { if (!d.isLooseData(data)) { @@ -271,11 +278,13 @@ export class GlslGenerator extends WgslGenerator { } else { // Name varyings consistently between vertex out / fragment in so the GLSL // ES 3.00 linker can match them by name. - const wgslKey = prop.replaceAll('$', ''); - name = this.ctx.makeUniqueIdentifier(`vary_${wgslKey}`, 'global'); + name = this.ctx.makeUniqueIdentifier(`vary_${prop}`, 'global'); entryFnState.outVars.push({ varName: name, propName: prop, dataType }); } entryFnState.structPropToVarMap[prop] = name; + if (this.#functionType === 'vertex') { + this.#vertexOutPropToVarMap[prop] = name; + } } // Copy-wrap the RHS in its type constructor so references get turned into values. @@ -308,6 +317,14 @@ export class GlslGenerator extends WgslGenerator { const returnType = options.determineReturnType(); if (options.functionType !== 'normal') { + if ( + this.#shaderStageToEmit === 'none' || + this.#shaderStageToEmit !== options.functionType + ) { + // Not the entry function this generation is supposed to generate + return ''; + } + const entryFnState = this.#entryFnState as EntryFnState; // --- Emit output declarations (layout(location=N) out TYPE NAME;) --- @@ -358,7 +375,7 @@ export class GlslGenerator extends WgslGenerator { ); return inName; } - const inName = this.ctx.makeUniqueIdentifier(`vary_${prop}`, 'global'); + const inName = this.#vertexOutPropToVarMap[prop]!; this.ctx.addDeclaration(`in ${glslType} ${inName};`); return inName; }; @@ -429,5 +446,6 @@ export class GlslGenerator extends WgslGenerator { } } -const glslGenerator: GlslGenerator = new GlslGenerator(); -export default glslGenerator; +export const glslGenerator: GlslGenerator = new GlslGenerator('none'); +export const vertexGlslGenerator: GlslGenerator = new GlslGenerator('vertex'); +export const fragmentGlslGenerator: GlslGenerator = new GlslGenerator('fragment'); diff --git a/packages/typegpu-gl/src/matchUpVaryingLocations.ts b/packages/typegpu-gl/src/matchUpVaryingLocations.ts deleted file mode 100644 index d3f979e5eb..0000000000 --- a/packages/typegpu-gl/src/matchUpVaryingLocations.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { d, type TgpuFragmentFn, type TgpuVertexFn } from 'typegpu'; - -export function getCustomLocation(data: d.BaseData): number | undefined { - return (data as unknown as d.Decorated | d.LooseDecorated).attribs?.find(d.isLocationAttrib) - ?.params[0]; -} -/** - * Assumes vertexOut and fragmentIn are matching when it comes to the keys, that is fragmentIn's keyset is a subset of vertexOut's - * Logs a warning, when they don't match in terms of custom locations - */ -export function matchUpVaryingLocations( - vertexOut: TgpuVertexFn.Out | undefined = {}, - fragmentIn: TgpuFragmentFn.In | undefined = {}, - vertexFnName: string, - fragmentFnName: string, -) { - const locations: Record = {}; - const usedLocations = new Set(); - - function saveLocation(key: string, location: number) { - locations[key] = location; - usedLocations.add(location); - } - - // respect custom locations and pair up vertex and fragment varying with the same key - for (const [key, value] of Object.entries(vertexOut)) { - const customLocation = getCustomLocation(value); - if (customLocation !== undefined) { - saveLocation(key, customLocation); - } - } - - for (const [key, value] of Object.entries(fragmentIn)) { - const customLocation = getCustomLocation(value); - if (customLocation === undefined) { - continue; - } - - if (locations[key] === undefined) { - saveLocation(key, customLocation); - } else if (locations[key] !== customLocation) { - console.warn( - `Mismatched location between vertexFn (${vertexFnName}) output (${ - locations[key] - }) and fragmentFn (${fragmentFnName}) input (${customLocation}) for the key "${key}", using the location set on vertex output.`, - ); - } - } - - // automatically assign remaining locations to the rest - let nextLocation = 0; - for (const key of Object.keys(vertexOut ?? {})) { - if (d.isBuiltin(vertexOut[key]) || locations[key] !== undefined) { - continue; - } - - while (usedLocations.has(nextLocation)) { - nextLocation++; - } - - saveLocation(key, nextLocation); - } - - return locations; -} diff --git a/packages/typegpu-gl/src/tgpuRootWebGL.ts b/packages/typegpu-gl/src/tgpuRootWebGL.ts index 00dbfcb3b6..637b12b66b 100644 --- a/packages/typegpu-gl/src/tgpuRootWebGL.ts +++ b/packages/typegpu-gl/src/tgpuRootWebGL.ts @@ -14,19 +14,12 @@ import tgpu, { type BufferInitialData, type BufferWriteOptions, type TgpuBuffer, - type TgpuFragmentFn, + type TgpuRenderPipeline, type TgpuVertexFn, } from 'typegpu'; -import { - AutoFragmentFn, - AutoVertexFn, - makeDereferencable, - makeResolvable, - type ShaderGenerator, -} from 'typegpu/~internals'; +import { makeDereferencable, makeResolvable } from 'typegpu/~internals'; -import glslGenerator from './glslGenerator.ts'; -import { matchUpVaryingLocations } from './matchUpVaryingLocations.ts'; +import { fragmentGlslGenerator, vertexGlslGenerator } from './glslGenerator.ts'; // ---------- // Public API @@ -372,16 +365,7 @@ makeDereferencable( }, ); -// oxlint-disable-next-line typescript/no-explicit-any -type AnyFn = (...args: any[]) => any; - -function isShellFn(value: unknown): value is TgpuVertexFn | TgpuFragmentFn { - return typeof value === 'object' && value !== null && 'shell' in value; -} - export class TgpuRootWebGL { - readonly #shaderGenerator: ShaderGenerator = glslGenerator; - #gl: WebGL2RenderingContext; #offscreen: OffscreenCanvas; #uniforms: WebGLUniformImpl[] = []; @@ -471,62 +455,22 @@ export class TgpuRootWebGL { }; } - createRenderPipeline(descriptor: { - vertex: TgpuVertexFn | AnyFn; - fragment: TgpuFragmentFn | AnyFn; - }): TgpuWebGLRenderPipeline { - const { vertex, fragment } = descriptor; - - const vertexShell = isShellFn(vertex) ? vertex : undefined; - const fragmentShell = isShellFn(fragment) ? fragment : undefined; - - const locations = matchUpVaryingLocations( - vertexShell?.shell?.out, - fragmentShell?.shell?.in, - '', - '', - ); - - const vertexResolvable = vertexShell ?? new AutoVertexFn(vertex as AnyFn, {}, locations); - const fragmentFromAuto = fragmentShell === undefined; + createRenderPipeline(descriptor: TgpuRenderPipeline.Descriptor): TgpuWebGLRenderPipeline { + const fakeRoot = tgpu.initFromDevice({ device: {} as GPUDevice }); + // oxlint-disable-next-line typescript/no-explicit-any + const fakePipeline = fakeRoot.createRenderPipeline(descriptor as any); - const vertexNamespace = tgpu['~unstable'].namespace(); - const vertexCode = tgpu.resolve([vertexResolvable], { - unstable_shaderGenerator: this.#shaderGenerator, - names: vertexNamespace, + const vertexCode = tgpu.resolve([fakePipeline], { + unstable_shaderGenerator: vertexGlslGenerator, }); - // For the fragment, we want to know the vertex output varyings to route them as inputs. - // When using an auto-vertex-fn, we need the completed struct. - let varyings: Record = {}; - if (vertexResolvable instanceof AutoVertexFn) { - const outStruct = vertexResolvable.autoOut.completeStruct; - varyings = Object.fromEntries( - Object.entries(outStruct.propTypes).filter(([, type]) => !d.isBuiltin(type)), - ); - } else if (vertexShell?.shell?.out) { - varyings = Object.fromEntries( - Object.entries(vertexShell.shell.out as Record).filter( - ([, type]) => !d.isBuiltin(type), - ), - ); - } - - const fragmentResolvable = - fragmentShell ?? new AutoFragmentFn(fragment as AnyFn, varyings, locations); - - const fragmentNamespace = tgpu['~unstable'].namespace(); - const fragmentCode = tgpu.resolve([fragmentResolvable], { - unstable_shaderGenerator: this.#shaderGenerator, - names: fragmentNamespace, + const fragmentCode = tgpu.resolve([fakePipeline], { + unstable_shaderGenerator: fragmentGlslGenerator, }); const vertexGlsl = GLSL_HEADER + wgslToGlslFixups(vertexCode); const fragmentGlsl = GLSL_HEADER + wgslToGlslFixups(fragmentCode); - // Silence unused variable lints for the shell-only fallback - void fragmentFromAuto; - const program = linkProgram(this.#gl, vertexGlsl, fragmentGlsl); return new TgpuWebGLRenderPipelineImpl( diff --git a/packages/typegpu-gl/tests/glslGenerator.test.ts b/packages/typegpu-gl/tests/glslGenerator.test.ts index b76d42fc80..4b637deafc 100644 --- a/packages/typegpu-gl/tests/glslGenerator.test.ts +++ b/packages/typegpu-gl/tests/glslGenerator.test.ts @@ -58,7 +58,7 @@ describe('GlslGenerator - variable declarations', () => { return color; }; - const result = tgpu.resolveWithContext([main], glOptions()); + const result = tgpu.resolveWithContext([main], glOptions({ shaderStage: 'none' })); // Should contain the resolved function code expect(result.code).toBeDefined(); expect(result.code.length).toBeGreaterThan(0); @@ -77,7 +77,7 @@ describe('GlslGenerator - variable declarations', () => { return d.vec4f(x, 0, 0, 1); }); - const result = tgpu.resolveWithContext([fragFn], glOptions()); + const result = tgpu.resolveWithContext([fragFn], glOptions({ shaderStage: 'fragment' })); expect(result.code).toBeDefined(); // Variable declaration for f32 should be `float` expect(result.code).toContain('float '); @@ -96,7 +96,7 @@ describe('GlslGenerator - function definitions', () => { return add(1.5, 1.2); } - const result = tgpu.resolveWithContext([main], glOptions()); + const result = tgpu.resolveWithContext([main], glOptions({ shaderStage: 'none' })); expect(result.code).toMatchInlineSnapshot(` "float add(float a, float b) { @@ -118,7 +118,7 @@ describe('GlslGenerator - function definitions', () => { return d.vec4f(color[0], color[1], color[2], 1.0); }); - const result = tgpu.resolveWithContext([fragFn], glOptions()); + const result = tgpu.resolveWithContext([fragFn], glOptions({ shaderStage: 'fragment' })); expect(result.code).toContain('vec3('); expect(result.code).not.toMatch(/\bvec3f\s*\(/); expect(result.code).toContain('vec4('); @@ -140,7 +140,7 @@ describe('GlslGenerator - function definitions', () => { const boid = createBoid(); } - const result = tgpu.resolve([main], glOptions()); + const result = tgpu.resolve([main], glOptions({ shaderStage: 'none' })); expect(result).toMatchInlineSnapshot(` "struct Boid { vec3 pos; @@ -167,7 +167,7 @@ describe('GlslGenerator - entry point generation with JS functions', () => { return Out({ pos: d.vec4f(0.0, 0.0, 0.0, 1.0) }); }); - const result = tgpu.resolveWithContext([vertFn], glOptions()); + const result = tgpu.resolveWithContext([vertFn], glOptions({ shaderStage: 'vertex' })); expect(result.code).toBeDefined(); expect(result.code.length).toBeGreaterThan(0); // The body should have translated type names @@ -204,7 +204,7 @@ describe('GlslGenerator - entry point generation with JS functions', () => { }; }); - const result = tgpu.resolve([vertFn], glOptions()); + const result = tgpu.resolve([vertFn], glOptions({ shaderStage: 'vertex' })); expect(result).toMatchInlineSnapshot(` "out vec2 vary_uv; @@ -232,7 +232,7 @@ describe('GlslGenerator - entry point generation with JS functions', () => { return d.vec4f(1.0, 0.0, 0.0, 1.0); }); - const result = tgpu.resolveWithContext([fragFn], glOptions()); + const result = tgpu.resolveWithContext([fragFn], glOptions({ shaderStage: 'fragment' })); expect(result.code).toBeDefined(); expect(result.code).toContain('vec4('); expect(result.code).not.toMatch(/\bvec4f\s*\(/); diff --git a/packages/typegpu-gl/tests/uniforms.test.ts b/packages/typegpu-gl/tests/uniforms.test.ts index db5593010c..b8ec317d30 100644 --- a/packages/typegpu-gl/tests/uniforms.test.ts +++ b/packages/typegpu-gl/tests/uniforms.test.ts @@ -5,7 +5,8 @@ import { _resetUniformCounter } from '../src/tgpuRootWebGL.ts'; import { it } from './utils/extendedTest.ts'; describe('TgpuRootWebGL - createUniform', () => { - it('creates a WebGL UBO-backed uniform', ({ gl }) => { + // TODO(#2510): Unskip this if uniforms are backed by UBOs + it.skip('creates a WebGL UBO-backed uniform', ({ gl }) => { const root = initWithGL({ gl }); const uniform = root.createUniform(d.vec4f); @@ -15,7 +16,8 @@ describe('TgpuRootWebGL - createUniform', () => { expect(gl.createBuffer).toHaveBeenCalled(); }); - it('creates a uniform with an initial value', ({ gl }) => { + // TODO(#2510): Unskip this if uniforms are backed by UBOs + it.skip('creates a uniform with an initial value', ({ gl }) => { const root = initWithGL({ gl }); const uniform = root.createUniform(d.f32, 42); @@ -48,7 +50,7 @@ describe('GlslGenerator - uniform resolution', () => { return d.f32(time.$); }; - const result = tgpu.resolve([fn], glOptions()); + const result = tgpu.resolve([fn], glOptions({ shaderStage: 'none' })); expect(result).toMatchInlineSnapshot(` "uniform float _u0; @@ -67,7 +69,7 @@ describe('GlslGenerator - uniform resolution', () => { return d.vec3f(color.$); }; - const result = tgpu.resolve([fn], glOptions()); + const result = tgpu.resolve([fn], glOptions({ shaderStage: 'none' })); expect(result).toMatchInlineSnapshot(` "uniform vec3 _u0; @@ -87,7 +89,7 @@ describe('GlslGenerator - uniform resolution', () => { return time.$ * scale.$; }; - const result = tgpu.resolve([fn], glOptions()); + const result = tgpu.resolve([fn], glOptions({ shaderStage: 'none' })); expect(result).toMatchInlineSnapshot(` "uniform float _u0; @@ -113,7 +115,7 @@ describe('GlslGenerator - uniform resolution', () => { return fn(d.vec2f(1, 2)); } - const result = tgpu.resolve([main], glOptions()); + const result = tgpu.resolve([main], glOptions({ shaderStage: 'none' })); expect(result).toMatchInlineSnapshot(` "uniform mat2 _u0; diff --git a/packages/typegpu/src/internals.ts b/packages/typegpu/src/internals.ts index 9b42a399d6..7f3f062e94 100644 --- a/packages/typegpu/src/internals.ts +++ b/packages/typegpu/src/internals.ts @@ -4,7 +4,6 @@ export { UnknownData } from './data/dataTypes.ts'; export { getName } from './shared/meta.ts'; export { makeDereferencable } from './tgsl/makeDereferencable.ts'; export { makeResolvable } from './tgsl/makeResolvable.ts'; -export { AutoFragmentFn, AutoVertexFn } from './core/function/autoIO.ts'; export { WgslGenerator } from './tgsl/wgslGenerator.ts'; // types From 1dd58436d4247352feea2ac695f2f6682d9e5061 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 17 May 2026 11:17:16 +0200 Subject: [PATCH 05/14] Remove unnecessary casting when returning from entry function --- packages/typegpu-gl/src/glslGenerator.ts | 57 +++++-------------- .../typegpu-gl/tests/glslGenerator.test.ts | 6 +- 2 files changed, 17 insertions(+), 46 deletions(-) diff --git a/packages/typegpu-gl/src/glslGenerator.ts b/packages/typegpu-gl/src/glslGenerator.ts index dec1000197..f16859f902 100644 --- a/packages/typegpu-gl/src/glslGenerator.ts +++ b/packages/typegpu-gl/src/glslGenerator.ts @@ -68,12 +68,6 @@ interface EntryFnState { outVars: OutVarInfo[]; /** The first-fragment-color output name, if allocated. */ fragColorName?: string; - /** The auto-output struct (populated as the body resolves). */ - autoOutStruct?: { - completeStruct: d.WgslStruct; - accessProp(key: string): { prop: string; type: d.BaseData } | undefined; - provideProp(key: string, type: d.BaseData): { prop: string; type: d.BaseData }; - }; } function undecorateDataType(t: d.BaseData): d.BaseData { @@ -116,7 +110,7 @@ function glslInputForBuiltin( export class GlslGenerator extends WgslGenerator { #functionType: TgpuShaderStage | 'normal' | undefined; #entryFnState: EntryFnState | undefined; - #vertexOutPropToVarMap: Record; + #vertexOutPropToVarMap: Record = {}; #shaderStageToEmit: 'none' | 'vertex' | 'fragment'; constructor(shaderStageToEmit: 'none' | 'vertex' | 'fragment') { @@ -233,9 +227,7 @@ export class GlslGenerator extends WgslGenerator { ): string { // Is this an auto-detected output struct? If so, register each prop so the // output struct's propTypes reflects what the body actually returns. - const isAutoStruct = - expectedReturnType !== undefined && - (expectedReturnType as { type?: string }).type === 'auto-struct'; + const isAutoStruct = expectedReturnType?.type === 'auto-struct'; const autoStruct = isAutoStruct ? (expectedReturnType as unknown as { completeStruct: d.WgslStruct; @@ -243,9 +235,6 @@ export class GlslGenerator extends WgslGenerator { provideProp(key: string, type: d.BaseData): { prop: string; type: d.BaseData }; }) : undefined; - if (autoStruct) { - entryFnState.autoOutStruct = autoStruct; - } // Resolve each RHS first so module-level references get reserved (and types become // available) before we allocate our LHS output identifiers. @@ -276,8 +265,6 @@ export class GlslGenerator extends WgslGenerator { if (isPosition) { name = 'gl_Position'; } else { - // Name varyings consistently between vertex out / fragment in so the GLSL - // ES 3.00 linker can match them by name. name = this.ctx.makeUniqueIdentifier(`vary_${prop}`, 'global'); entryFnState.outVars.push({ varName: name, propName: prop, dataType }); } @@ -287,9 +274,7 @@ export class GlslGenerator extends WgslGenerator { } } - // Copy-wrap the RHS in its type constructor so references get turned into values. - const glslType = this.ctx.resolve(undecorateDataType(dataType)).value; - lines.push(`${this.ctx.pre} ${name} = ${glslType}(${rhsStr});`); + lines.push(`${this.ctx.pre} ${name} = ${rhsStr};`); } lines.push(`${this.ctx.pre} return;`); @@ -327,30 +312,21 @@ export class GlslGenerator extends WgslGenerator { const entryFnState = this.#entryFnState as EntryFnState; - // --- Emit output declarations (layout(location=N) out TYPE NAME;) --- - // Prefer the auto-output struct if we collected one during body resolution; - // it carries @location attributes computed via withVaryingLocations. - const outStructForDecls = entryFnState.autoOutStruct - ? entryFnState.autoOutStruct.completeStruct - : d.isWgslStruct(returnType) - ? returnType - : undefined; - if (outStructForDecls) { - for (const { varName, dataType } of entryFnState.outVars) { + for (const { varName, dataType } of entryFnState.outVars) { + const glslType = this.ctx.resolve(undecorateDataType(dataType)).value; + if (options.functionType === 'fragment') { + // Fragment color outputs keep location=N since they target draw buffers. + this.ctx.addDeclaration(`layout(location=0) out ${glslType} ${varName};`); + } else { // Varyings (vertex -> fragment) in GLSL ES 3.00 are matched by name, // so we don't emit layout(location=N) qualifiers here. - const glslType = this.ctx.resolve(undecorateDataType(dataType)).value; - if (options.functionType === 'fragment') { - // Fragment color outputs keep location=N since they target draw buffers. - this.ctx.addDeclaration(`layout(location = 0) out ${glslType} ${varName};`); - } else { - this.ctx.addDeclaration(`out ${glslType} ${varName};`); - } + this.ctx.addDeclaration(`out ${glslType} ${varName};`); } } + // Fragment color output if (entryFnState.fragColorName) { - this.ctx.addDeclaration(`layout(location = 0) out vec4 ${entryFnState.fragColorName};`); + this.ctx.addDeclaration(`layout(location=0) out vec4 ${entryFnState.fragColorName};`); } // --- Emit input-side setup: declare layout(location) in vars, and initialize @@ -370,9 +346,7 @@ export class GlslGenerator extends WgslGenerator { const glslType = this.ctx.resolve(undecorateDataType(propType)).value; if (stage === 'vertex') { const inName = this.ctx.makeUniqueIdentifier(`_in_${prop}`, 'global'); - this.ctx.addDeclaration( - `layout(location = ${location ?? 0}) in ${glslType} ${inName};`, - ); + this.ctx.addDeclaration(`layout(location=${location ?? 0}) in ${glslType} ${inName};`); return inName; } const inName = this.#vertexOutPropToVarMap[prop]!; @@ -432,10 +406,7 @@ export class GlslGenerator extends WgslGenerator { } const argList = options.args - .filter((arg) => arg.used || options.functionType === 'normal') - .map((arg) => { - return `${this.ctx.resolve(arg.decoratedType).value} ${arg.name}`; - }) + .map((arg) => `${this.ctx.resolve(arg.decoratedType).value} ${arg.name}`) .join(', '); return `${this.ctx.resolve(returnType).value} ${options.name}(${argList}) ${body}`; diff --git a/packages/typegpu-gl/tests/glslGenerator.test.ts b/packages/typegpu-gl/tests/glslGenerator.test.ts index 4b637deafc..2e92d3ec1e 100644 --- a/packages/typegpu-gl/tests/glslGenerator.test.ts +++ b/packages/typegpu-gl/tests/glslGenerator.test.ts @@ -213,8 +213,8 @@ describe('GlslGenerator - entry point generation with JS functions', () => { vec4 position = vec4(); vec2 uv = vec2(); { - gl_Position = vec4(position); - vary_uv = vec2(uv); + gl_Position = position; + vary_uv = uv; return; } }" @@ -238,7 +238,7 @@ describe('GlslGenerator - entry point generation with JS functions', () => { expect(result.code).not.toMatch(/\bvec4f\s*\(/); expect(result.code).toMatchInlineSnapshot(` - "layout(location = 0) out vec4 _fragColor; + "layout(location=0) out vec4 _fragColor; void main() { int gl_Position_1 = 1; From 022532855dfbfbf250d832b0a6ed1121ade9bb52 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 17 May 2026 11:30:33 +0200 Subject: [PATCH 06/14] Generating non-empty vector constructors, without a hacky solution --- packages/typegpu-gl/src/glslGenerator.ts | 19 ++++++++++++++-- packages/typegpu-gl/src/tgpuRootWebGL.ts | 3 --- .../typegpu-gl/tests/glslGenerator.test.ts | 6 ++--- packages/typegpu-gl/tests/vector.test.ts | 22 +++++++++++++++++++ packages/typegpu/src/internals.ts | 4 +++- 5 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 packages/typegpu-gl/tests/vector.test.ts diff --git a/packages/typegpu-gl/src/glslGenerator.ts b/packages/typegpu-gl/src/glslGenerator.ts index f16859f902..0fcff24bf3 100644 --- a/packages/typegpu-gl/src/glslGenerator.ts +++ b/packages/typegpu-gl/src/glslGenerator.ts @@ -1,8 +1,14 @@ import { NodeTypeCatalog as NODE } from 'tinyest'; import type { Return } from 'tinyest'; import tgpu, { d } from 'typegpu'; -import { getName, UnknownData, WgslGenerator } from 'typegpu/~internals'; -import type { ResolutionCtx, TgpuShaderStage, FunctionDefinitionOptions } from 'typegpu/~internals'; +import { abstractInt, getName, snip, UnknownData, WgslGenerator } from 'typegpu/~internals'; +import type { + ResolutionCtx, + TgpuShaderStage, + FunctionDefinitionOptions, + Snippet, + ResolvedSnippet, +} from 'typegpu/~internals'; // ---------- // WGSL → GLSL type name mapping @@ -138,6 +144,15 @@ export class GlslGenerator extends WgslGenerator { return super.typeAnnotation(data); } + override typeInstantiation(schema: d.BaseData, args: Snippet[]): ResolvedSnippet { + // Empty vector constructors `vecN()` are illegal in GLSL; replacing with vecN(0). + if (schema.type.startsWith('vec') && args.length === 0) { + return super.typeInstantiation(schema, [snip(0, abstractInt, 'constant')]); + } + + return super.typeInstantiation(schema, args); + } + override _emitVarDecl( _keyword: 'var' | 'let' | 'const', name: string, diff --git a/packages/typegpu-gl/src/tgpuRootWebGL.ts b/packages/typegpu-gl/src/tgpuRootWebGL.ts index 637b12b66b..1a33800590 100644 --- a/packages/typegpu-gl/src/tgpuRootWebGL.ts +++ b/packages/typegpu-gl/src/tgpuRootWebGL.ts @@ -120,9 +120,6 @@ function wgslToGlslFixups(code: string): string { }, ); - // Empty vector constructors `vecN()` are illegal in GLSL; default to zero. - out = out.replaceAll(/\b(vec[234]|ivec[234]|uvec[234]|bvec[234])\s*\(\s*\)/g, '$1(0)'); - return out; } diff --git a/packages/typegpu-gl/tests/glslGenerator.test.ts b/packages/typegpu-gl/tests/glslGenerator.test.ts index 2e92d3ec1e..c5921e6dda 100644 --- a/packages/typegpu-gl/tests/glslGenerator.test.ts +++ b/packages/typegpu-gl/tests/glslGenerator.test.ts @@ -148,7 +148,7 @@ describe('GlslGenerator - function definitions', () => { }; Boid createBoid() { - return Boid(vec3(), vec3(0, 1, 0)); + return Boid(vec3(0), vec3(0, 1, 0)); } void main() { @@ -210,8 +210,8 @@ describe('GlslGenerator - entry point generation with JS functions', () => { "out vec2 vary_uv; void main() { - vec4 position = vec4(); - vec2 uv = vec2(); + vec4 position = vec4(0); + vec2 uv = vec2(0); { gl_Position = position; vary_uv = uv; diff --git a/packages/typegpu-gl/tests/vector.test.ts b/packages/typegpu-gl/tests/vector.test.ts new file mode 100644 index 0000000000..7c26f9da87 --- /dev/null +++ b/packages/typegpu-gl/tests/vector.test.ts @@ -0,0 +1,22 @@ +import tgpu, { d } from 'typegpu'; +import { glOptions } from '@typegpu/gl'; +import { test } from './utils/extendedTest.ts'; +import { expect } from 'vitest'; + +test('empty vec3f constructor', () => { + function foo() { + 'use gpu'; + return d.vec3f(); + } + + const code = tgpu.resolve([foo], glOptions({ shaderStage: 'none' })); + + // Empty vector constructors `vecN()` are illegal in GLSL + expect(code).toContain('vec3(0)'); + + expect(code).toMatchInlineSnapshot(` + "vec3 foo() { + return vec3(0); + }" + `); +}); diff --git a/packages/typegpu/src/internals.ts b/packages/typegpu/src/internals.ts index 7f3f062e94..3b108906fd 100644 --- a/packages/typegpu/src/internals.ts +++ b/packages/typegpu/src/internals.ts @@ -1,13 +1,15 @@ // Each export here is available as a member on the 'typegpu/~internals` import. +export { abstractInt, abstractFloat } from './data/numeric.ts'; export { UnknownData } from './data/dataTypes.ts'; export { getName } from './shared/meta.ts'; export { makeDereferencable } from './tgsl/makeDereferencable.ts'; export { makeResolvable } from './tgsl/makeResolvable.ts'; export { WgslGenerator } from './tgsl/wgslGenerator.ts'; +export { snip } from './data/snippet.ts'; // types export type { ResolutionCtx, FunctionArgument, TgpuShaderStage } from './types.ts'; -export type { Snippet, Origin } from './data/snippet.ts'; +export type { Snippet, ResolvedSnippet, Origin } from './data/snippet.ts'; export type { ShaderGenerator, FunctionDefinitionOptions } from './tgsl/shaderGenerator.ts'; From 0f45048735aefd69571a3938e2af6c5271347cdc Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 17 May 2026 17:20:48 +0200 Subject: [PATCH 07/14] Tweaks and fixes --- .../components/stackblitz/openInStackBlitz.ts | 48 +++++++++---------- packages/typegpu-gl/package.json | 7 ++- packages/typegpu-gl/src/glslGenerator.ts | 14 ++++-- packages/typegpu-gl/src/tgpuRootWebGL.ts | 8 ++-- packages/typegpu/src/resolutionCtx.ts | 3 +- 5 files changed, 43 insertions(+), 37 deletions(-) diff --git a/apps/typegpu-docs/src/components/stackblitz/openInStackBlitz.ts b/apps/typegpu-docs/src/components/stackblitz/openInStackBlitz.ts index 995ae2c099..e727a6d882 100644 --- a/apps/typegpu-docs/src/components/stackblitz/openInStackBlitz.ts +++ b/apps/typegpu-docs/src/components/stackblitz/openInStackBlitz.ts @@ -119,30 +119,30 @@ ${example.htmlFile.content} "@tailwindcss/vite": "^4.1.18" }, "dependencies": ${JSON.stringify( - { - typegpu: `^${typegpuPackageJson.version}`, - 'unplugin-typegpu': `^${unpluginPackageJson.version}`, - 'wgpu-matrix': pnpmWorkspaceYaml.catalogs.example['wgpu-matrix'], - '@loaders.gl/core': typegpuDocsPackageJson.dependencies['@loaders.gl/core'], - '@loaders.gl/obj': typegpuDocsPackageJson.dependencies['@loaders.gl/obj'], - '@loaders.gl/gltf': typegpuDocsPackageJson.dependencies['@loaders.gl/gltf'], - three: pnpmWorkspaceYaml.catalogs.example.three, - '@typegpu/noise': typegpuNoisePackageJson.version, - '@typegpu/color': typegpuColorPackageJson.version, - '@typegpu/gl': typegpuGlPackageJson.version, - '@typegpu/sdf': typegpuSdfPackageJson.version, - '@typegpu/three': typegpuThreePackageJson.version, - ...(example.usedApis.includes('@typegpu/react') - ? { - '@typegpu/react': typegpuReactPackageJson.version, - react: '^19.2.0', - 'react-dom': '^19.2.0', - } - : {}), - }, - undefined, - 2, - ).replaceAll('\n', '\n ')} + { + typegpu: `^${typegpuPackageJson.version}`, + 'unplugin-typegpu': `^${unpluginPackageJson.version}`, + 'wgpu-matrix': pnpmWorkspaceYaml.catalogs.example['wgpu-matrix'], + '@loaders.gl/core': typegpuDocsPackageJson.dependencies['@loaders.gl/core'], + '@loaders.gl/obj': typegpuDocsPackageJson.dependencies['@loaders.gl/obj'], + '@loaders.gl/gltf': typegpuDocsPackageJson.dependencies['@loaders.gl/gltf'], + three: pnpmWorkspaceYaml.catalogs.example.three, + '@typegpu/noise': typegpuNoisePackageJson.version, + '@typegpu/color': typegpuColorPackageJson.version, + '@typegpu/gl': typegpuGlPackageJson.version, + '@typegpu/sdf': typegpuSdfPackageJson.version, + '@typegpu/three': typegpuThreePackageJson.version, + ...(example.usedApis.includes('@typegpu/react') + ? { + '@typegpu/react': typegpuReactPackageJson.version, + react: '^19.2.0', + 'react-dom': '^19.2.0', + } + : {}), + }, + undefined, + 2, + ).replaceAll('\n', '\n ')} }`, 'vite.config.js': `\ import { defineConfig } from 'vite'; diff --git a/packages/typegpu-gl/package.json b/packages/typegpu-gl/package.json index af4b464282..dc73056893 100644 --- a/packages/typegpu-gl/package.json +++ b/packages/typegpu-gl/package.json @@ -1,7 +1,6 @@ { "name": "@typegpu/gl", "version": "0.11.0-alpha.1", - "private": true, "description": "WebGL and GLSL utilities for TypeGPU", "keywords": [], "license": "MIT", @@ -36,6 +35,9 @@ "test:types": "pnpm tsc --p ./tsconfig.json --noEmit && pnpm tsc --p ./tsconfig.test.json --noEmit", "prepublishOnly": "tgpu-dev-cli prepack" }, + "dependencies": { + "typed-binary": "^4.3.3" + }, "devDependencies": { "@typegpu/tgpu-dev-cli": "workspace:*", "@webgpu/types": "catalog:types", @@ -49,8 +51,5 @@ }, "peerDependencies": { "typegpu": "workspace:^" - }, - "dependencies": { - "typed-binary": "^4.3.3" } } diff --git a/packages/typegpu-gl/src/glslGenerator.ts b/packages/typegpu-gl/src/glslGenerator.ts index 0fcff24bf3..4e305a53f4 100644 --- a/packages/typegpu-gl/src/glslGenerator.ts +++ b/packages/typegpu-gl/src/glslGenerator.ts @@ -77,13 +77,13 @@ interface EntryFnState { } function undecorateDataType(t: d.BaseData): d.BaseData { - return d.isDecorated(t) ? (t.inner as d.BaseData) : t; + return d.isDecorated(t) ? t.inner : t; } function getLocationFromDecorated(type: d.BaseData): number | undefined { if (!d.isDecorated(type)) return undefined; const attr = (type.attribs as d.AnyAttribute[]).find((a) => a.type === '@location'); - return attr ? (attr.params[0] as number) : undefined; + return attr ? attr.params[0] : undefined; } function getBuiltinKindFromDecorated(type: d.BaseData): string | undefined { @@ -125,6 +125,7 @@ export class GlslGenerator extends WgslGenerator { } override initGenerator(ctx: ResolutionCtx) { + // oxlint-disable-next-line typescript/no-explicit-any -- the GenerationCtx tpye is not exported super.initGenerator(ctx as any); this.#vertexOutPropToVarMap = {}; } @@ -196,7 +197,7 @@ export class GlslGenerator extends WgslGenerator { return super._return(statement); } - const exprType = (expr.dataType as d.BaseData).type; + const exprType = expr.dataType.type; if ( this.#functionType === 'fragment' && @@ -364,14 +365,17 @@ export class GlslGenerator extends WgslGenerator { this.ctx.addDeclaration(`layout(location=${location ?? 0}) in ${glslType} ${inName};`); return inName; } - const inName = this.#vertexOutPropToVarMap[prop]!; + const inName = this.#vertexOutPropToVarMap[prop]; + if (!inName) { + throw new Error(`Unknown varying: ${prop}`); + } this.ctx.addDeclaration(`in ${glslType} ${inName};`); return inName; }; for (const arg of options.args) { if (!arg.used) continue; - const argType = arg.decoratedType as d.BaseData; + const argType = arg.decoratedType; // Auto-detected IO struct (plain-function entry fns) if ((argType as { type?: string }).type === 'auto-struct') { diff --git a/packages/typegpu-gl/src/tgpuRootWebGL.ts b/packages/typegpu-gl/src/tgpuRootWebGL.ts index 1a33800590..f9a1e32eb9 100644 --- a/packages/typegpu-gl/src/tgpuRootWebGL.ts +++ b/packages/typegpu-gl/src/tgpuRootWebGL.ts @@ -303,7 +303,9 @@ class WebGLUniformImpl implements WebGLUniform d.InferInput)(this) + : (this.#initial as d.InferInput); writeToArrayBuffer(this.buffer, this.dataType, initialData); gl.bufferData(gl.UNIFORM_BUFFER, this.buffer, gl.DYNAMIC_DRAW); @@ -365,7 +367,7 @@ makeDereferencable( export class TgpuRootWebGL { #gl: WebGL2RenderingContext; #offscreen: OffscreenCanvas; - #uniforms: WebGLUniformImpl[] = []; + #uniforms: WebGLUniformImpl[] = []; #buffers: WebGLBuffer[] = []; constructor(gl: WebGL2RenderingContext) { @@ -382,7 +384,7 @@ export class TgpuRootWebGL { initial?: BufferInitialData, ): WebGLUniform { const uniform = new WebGLUniformImpl(this.#gl, typeSchema, initial); - this.#uniforms.push(uniform); + this.#uniforms.push(uniform as unknown as WebGLUniformImpl); return uniform; } diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index 50a1ed452f..f40ac4b82a 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -1051,7 +1051,8 @@ export class ResolutionCtxImpl implements ResolutionCtx { } throw new WgslTypeError( - `Value ${safeStringify(item)} is not resolvable${schema ? ` to type ${safeStringify(schema)}` : '' + `Value ${safeStringify(item)} is not resolvable${ + schema ? ` to type ${safeStringify(schema)}` : '' }`, ); } From 70f84b4f3957b327310a94417c5b1f8da966e78c Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 17 May 2026 17:29:25 +0200 Subject: [PATCH 08/14] Making tinyest a dep, and typed-binary not a dep --- packages/typegpu-gl/package.json | 3 +-- pnpm-lock.yaml | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/typegpu-gl/package.json b/packages/typegpu-gl/package.json index dc73056893..2df807ab9d 100644 --- a/packages/typegpu-gl/package.json +++ b/packages/typegpu-gl/package.json @@ -36,12 +36,11 @@ "prepublishOnly": "tgpu-dev-cli prepack" }, "dependencies": { - "typed-binary": "^4.3.3" + "tinyest": "workspace:^0.3.0" }, "devDependencies": { "@typegpu/tgpu-dev-cli": "workspace:*", "@webgpu/types": "catalog:types", - "tinyest": "workspace:^0.3.0", "tsdown": "catalog:build", "typegpu": "workspace:*", "typegpu-testing-utility": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b0ca5aa57..0d180eca1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -645,9 +645,9 @@ importers: packages/typegpu-gl: dependencies: - typed-binary: - specifier: ^4.3.3 - version: 4.3.3 + tinyest: + specifier: workspace:^0.3.0 + version: link:../tinyest devDependencies: '@typegpu/tgpu-dev-cli': specifier: workspace:* @@ -655,9 +655,6 @@ importers: '@webgpu/types': specifier: catalog:types version: 0.1.66 - tinyest: - specifier: workspace:^0.3.0 - version: link:../tinyest tsdown: specifier: catalog:build version: 0.15.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(tsover@5.9.11)(unrun@0.2.34(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)) From 591457a8bdd6c14ab21d4dfb36e245c6c78546c6 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 17 May 2026 17:43:28 +0200 Subject: [PATCH 09/14] Fix exports and alpha bump --- packages/typegpu-gl/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/typegpu-gl/package.json b/packages/typegpu-gl/package.json index 2df807ab9d..096aa458d3 100644 --- a/packages/typegpu-gl/package.json +++ b/packages/typegpu-gl/package.json @@ -1,6 +1,6 @@ { "name": "@typegpu/gl", - "version": "0.11.0-alpha.1", + "version": "0.11.0-alpha.2", "description": "WebGL and GLSL utilities for TypeGPU", "keywords": [], "license": "MIT", @@ -22,12 +22,12 @@ "./package.json": "./dist/package.json", ".": { "types": "./dist/index.d.ts", - "module": "./dist/index.mjs", - "import": "./dist/index.mjs" + "module": "./dist/index.js", + "import": "./dist/index.js" } }, "linkDirectory": false, - "main": "./dist/index.mjs", + "main": "./dist/index.js", "types": "./dist/index.d.ts" }, "scripts": { From 21915542d98bed2ebb7c1f60c4bd4d13233820db Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 17 May 2026 17:50:46 +0200 Subject: [PATCH 10/14] Build `internals` entrypoint --- packages/typegpu/tsdown.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/typegpu/tsdown.config.ts b/packages/typegpu/tsdown.config.ts index 1a3c9334e7..bd68e36a1c 100644 --- a/packages/typegpu/tsdown.config.ts +++ b/packages/typegpu/tsdown.config.ts @@ -6,6 +6,7 @@ const entry = [ 'src/data/index.ts', 'src/std/index.ts', 'src/common/index.ts', + 'src/internals.ts', ]; export default defineConfig({ From 26d22e20e73650bf51b90e93b104b100b46a5e1d Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 17 May 2026 17:59:22 +0200 Subject: [PATCH 11/14] Nightly publish of typegpu-gl --- .github/workflows/pkg-pr.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pkg-pr.yml b/.github/workflows/pkg-pr.yml index b8cabf72c3..0cc8ba24d9 100644 --- a/.github/workflows/pkg-pr.yml +++ b/.github/workflows/pkg-pr.yml @@ -37,7 +37,7 @@ jobs: run: pnpm nightly-build - name: Publish (pkg.pr.new) - run: pnpm exec pkg-pr-new publish './packages/typegpu' './packages/typegpu-noise' './packages/unplugin-typegpu' --json output.json --comment=off --pnpm --no-compact + run: pnpm exec pkg-pr-new publish './packages/typegpu' './packages/typegpu-noise' './packages/typegpu-gl' './packages/unplugin-typegpu' --json output.json --comment=off --pnpm --no-compact - name: Post or update comment uses: actions/github-script@v6 with: diff --git a/package.json b/package.json index 9dd5762786..05b70fd8a6 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test:browser": "vitest run --browser.enabled --project browser", "test:browser:watch": "vitest --browser.enabled --project browser", "test:coverage": "vitest --coverage run", - "nightly-build": "SKIP_TESTS=true pnpm --filter typegpu --filter @typegpu/noise --filter unplugin-typegpu prepublishOnly --skip-publish-tag-check", + "nightly-build": "SKIP_TESTS=true pnpm --filter typegpu --filter @typegpu/noise --filter @typegpu/gl --filter unplugin-typegpu prepublishOnly --skip-publish-tag-check", "changes": "tgpu-dev-cli changes" }, "devDependencies": { From 8914c6802f4b3b29397d881439f1b0e1ae190111 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 17 May 2026 21:51:10 +0200 Subject: [PATCH 12/14] Unhackify constants and variables --- packages/typegpu-gl/src/glslGenerator.ts | 29 +++++++++++++- packages/typegpu-gl/src/tgpuRootWebGL.ts | 24 ----------- packages/typegpu-gl/tests/constant.test.ts | 21 ++++++++++ packages/typegpu-gl/tests/variable.test.ts | 21 ++++++++++ packages/typegpu/src/internals.ts | 6 ++- .../src/tgsl/shaderGenerator_members.ts | 40 ------------------- 6 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 packages/typegpu-gl/tests/constant.test.ts create mode 100644 packages/typegpu-gl/tests/variable.test.ts delete mode 100644 packages/typegpu/src/tgsl/shaderGenerator_members.ts diff --git a/packages/typegpu-gl/src/glslGenerator.ts b/packages/typegpu-gl/src/glslGenerator.ts index 4e305a53f4..ca5338b49a 100644 --- a/packages/typegpu-gl/src/glslGenerator.ts +++ b/packages/typegpu-gl/src/glslGenerator.ts @@ -6,6 +6,7 @@ import type { ResolutionCtx, TgpuShaderStage, FunctionDefinitionOptions, + VariableDefinitionOptions, Snippet, ResolvedSnippet, } from 'typegpu/~internals'; @@ -125,11 +126,35 @@ export class GlslGenerator extends WgslGenerator { } override initGenerator(ctx: ResolutionCtx) { - // oxlint-disable-next-line typescript/no-explicit-any -- the GenerationCtx tpye is not exported + // oxlint-disable-next-line typescript/no-explicit-any -- the GenerationCtx type is not exported super.initGenerator(ctx as any); this.#vertexOutPropToVarMap = {}; } + override globalConstDefinition(id: string, schema: d.BaseData, init: Snippet): string { + const resolvedDataType = this.ctx.resolve(schema).value; + const resolvedValue = this.ctx.resolveSnippet(init).value; + + return `const ${resolvedDataType} ${id} = ${resolvedValue};`; + } + + override globalVarDefinition(options: VariableDefinitionOptions): string { + let pre = `${this.ctx.resolve(options.dataType).value} ${options.name}`; + + if (options.scope === 'private') { + // Nothing to add + } else if (options.scope === 'uniform') { + pre = 'uniform ' + pre; + } else { + throw new Error(`Cannot define ${options.scope} variables when generating GLSL.`); + } + + if (options.init) { + return `${pre} = ${this.ctx.resolveSnippet(options.init).value};`; + } + return `${pre};`; + } + override typeAnnotation(data: d.BaseData): string { if (!d.isLooseData(data)) { const glslName = WGSL_TO_GLSL_TYPE[data.type]; @@ -300,7 +325,9 @@ export class GlslGenerator extends WgslGenerator { override functionDefinition(options: FunctionDefinitionOptions): string { if (options.functionType !== 'normal') { + // Reserving GLSL keywords this.ctx.reserveIdentifier('gl_Position', 'global'); + this.ctx.reserveIdentifier('sample', 'global'); // `sample` is a reserved word in GLSL ES (for multisample interpolation qualifiers), } const lastFunctionType = this.#functionType; diff --git a/packages/typegpu-gl/src/tgpuRootWebGL.ts b/packages/typegpu-gl/src/tgpuRootWebGL.ts index f9a1e32eb9..0841336abc 100644 --- a/packages/typegpu-gl/src/tgpuRootWebGL.ts +++ b/packages/typegpu-gl/src/tgpuRootWebGL.ts @@ -85,9 +85,6 @@ vec4 saturate(vec4 x) { return clamp(x, 0.0, 1.0); } function wgslToGlslFixups(code: string): string { let out = code; - // WGSL integer literal suffix: `5i` -> `5`, `5u` -> `5` (GLSL happily accepts bare ints). - out = out.replaceAll(/(\d+)[iu]\b/g, '$1'); - // WGSL f32 literal suffixes -> GLSL float literals. A trailing `f` always marks a float, // but GLSL requires a decimal point to disambiguate floats from ints. // Handle scientific notation first (`1e-3f` -> `1e-3`), so the plain-int rule below doesn't @@ -96,30 +93,9 @@ function wgslToGlslFixups(code: string): string { out = out.replaceAll(/(\d+\.\d+)f\b/g, '$1'); out = out.replaceAll(/(\d+)f\b/g, '$1.0'); - // WGSL private module var -> GLSL global var. - out = out.replaceAll(/\bvar\s+([A-Za-z_]\w*)\s*:\s*([^;=]+?)\s*;/g, '$2 $1;'); - out = out.replaceAll(/\bvar\s+([A-Za-z_]\w*)\s*:\s*([^;=]+?)\s*=\s*/g, '$2 $1 = '); - - // `sample` is a reserved word in GLSL ES (for multisample interpolation qualifiers), - // so rename any identifier `sample` used as a function or variable name. - out = out.replaceAll(/\bsample\b/g, 'sample_'); - // WGSL array type in expressions `array(...)` -> `T[N](...)` out = out.replaceAll(/array<([^,<>]+?),\s*(\d+)>/g, '$1[$2]'); - // WGSL const decls: `const NAME: TYPE = VALUE;` -> GLSL style. - // TYPE can include brackets if it started as `array` (already rewritten to `T[N]`). - // For GLSL arrays, the brackets go AFTER the identifier: `const T NAME[N] = ...`. - out = out.replaceAll( - /\bconst\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([A-Za-z_][A-Za-z0-9_]*)(\[[^\]]+\])?\s*=\s*/g, - (_m, name, baseType, arraySuffix) => { - if (arraySuffix) { - return `const ${baseType} ${name}${arraySuffix} = `; - } - return `const ${baseType} ${name} = `; - }, - ); - return out; } diff --git a/packages/typegpu-gl/tests/constant.test.ts b/packages/typegpu-gl/tests/constant.test.ts new file mode 100644 index 0000000000..fa36d672f8 --- /dev/null +++ b/packages/typegpu-gl/tests/constant.test.ts @@ -0,0 +1,21 @@ +import { expect } from 'vitest'; +import tgpu, { d } from 'typegpu'; +import { glOptions } from '@typegpu/gl'; +import { test } from './utils/extendedTest.ts'; + +test('constant with a scalar value', () => { + const FOO = tgpu.const(d.f32, 123.5); + + function main() { + 'use gpu'; + return FOO.$; + } + + expect(tgpu.resolve([main], glOptions({ shaderStage: 'none' }))).toMatchInlineSnapshot(` + "const float FOO = 123.5f; + + float main() { + return FOO; + }" + `); +}); diff --git a/packages/typegpu-gl/tests/variable.test.ts b/packages/typegpu-gl/tests/variable.test.ts new file mode 100644 index 0000000000..efdfceb2ee --- /dev/null +++ b/packages/typegpu-gl/tests/variable.test.ts @@ -0,0 +1,21 @@ +import { expect } from 'vitest'; +import tgpu, { d } from 'typegpu'; +import { glOptions } from '@typegpu/gl'; +import { test } from './utils/extendedTest.ts'; + +test('private variable with a scalar value', () => { + const FOO = tgpu.privateVar(d.f32, 123.5); + + function main() { + 'use gpu'; + return FOO.$; + } + + expect(tgpu.resolve([main], glOptions({ shaderStage: 'none' }))).toMatchInlineSnapshot(` + "float FOO = 123.5f; + + float main() { + return FOO; + }" + `); +}); diff --git a/packages/typegpu/src/internals.ts b/packages/typegpu/src/internals.ts index 3b108906fd..908a797924 100644 --- a/packages/typegpu/src/internals.ts +++ b/packages/typegpu/src/internals.ts @@ -12,4 +12,8 @@ export { snip } from './data/snippet.ts'; export type { ResolutionCtx, FunctionArgument, TgpuShaderStage } from './types.ts'; export type { Snippet, ResolvedSnippet, Origin } from './data/snippet.ts'; -export type { ShaderGenerator, FunctionDefinitionOptions } from './tgsl/shaderGenerator.ts'; +export type { + ShaderGenerator, + FunctionDefinitionOptions, + VariableDefinitionOptions, +} from './tgsl/shaderGenerator.ts'; diff --git a/packages/typegpu/src/tgsl/shaderGenerator_members.ts b/packages/typegpu/src/tgsl/shaderGenerator_members.ts deleted file mode 100644 index f37694a1de..0000000000 --- a/packages/typegpu/src/tgsl/shaderGenerator_members.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Block } from 'tinyest'; -import type { BaseData } from '../data/wgslTypes.ts'; -import type { BindableBufferUsage, FunctionArgument, TgpuShaderStage } from '../types.ts'; -import type { VariableScope } from '../core/variable/tgpuVariable.ts'; -import type { Snippet } from '../data/snippet.ts'; - -export { UnknownData } from '../data/dataTypes.ts'; -export { getName } from '../shared/meta.ts'; -export { makeDereferencable } from './makeDereferencable.ts'; -export { makeResolvable } from './makeResolvable.ts'; -export { AutoFragmentFn, AutoVertexFn } from '../core/function/autoIO.ts'; -export { matchUpVaryingLocations } from '../core/pipeline/renderPipeline.ts'; -export { valueProxyHandler } from '../core/valueProxyUtils.ts'; -export { inCodegenMode } from '../execMode.ts'; -export { $gpuValueOf, $internal, $ownSnippet, $resolve } from '../shared/symbols.ts'; -export { snip } from '../data/snippet.ts'; - -// types -export type { ResolutionCtx, FunctionArgument, TgpuShaderStage } from '../types.ts'; -export type { Snippet } from '../data/snippet.ts'; -export type { Origin } from '../data/snippet.ts'; - -export interface FunctionDefinitionOptions { - readonly functionType: 'normal' | TgpuShaderStage; - readonly name: string; - readonly workgroupSize?: readonly number[] | undefined; - readonly args: readonly FunctionArgument[]; - readonly body: Block; - - determineReturnType(): BaseData; -} - -export interface VariableDefinitionOptions { - readonly scope: VariableScope | BindableBufferUsage; - readonly name: string; - readonly dataType: BaseData; - readonly init: Snippet | undefined; - readonly group?: string | undefined; - readonly binding?: number | undefined; -} From 207bcf7be1f39febdb981afdfa172f39bcf78931 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 17 May 2026 22:01:16 +0200 Subject: [PATCH 13/14] Better array handling --- packages/typegpu-gl/src/glslGenerator.ts | 10 +++++++--- packages/typegpu-gl/tests/constant.test.ts | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/typegpu-gl/src/glslGenerator.ts b/packages/typegpu-gl/src/glslGenerator.ts index ca5338b49a..7a74df7493 100644 --- a/packages/typegpu-gl/src/glslGenerator.ts +++ b/packages/typegpu-gl/src/glslGenerator.ts @@ -132,10 +132,14 @@ export class GlslGenerator extends WgslGenerator { } override globalConstDefinition(id: string, schema: d.BaseData, init: Snippet): string { - const resolvedDataType = this.ctx.resolve(schema).value; - const resolvedValue = this.ctx.resolveSnippet(init).value; + const initStr = this.ctx.resolveSnippet(init).value; - return `const ${resolvedDataType} ${id} = ${resolvedValue};`; + if (d.isWgslArray(schema)) { + const elemTypeStr = this.ctx.resolve(schema.elementType).value; + return `const ${elemTypeStr} ${id}[] = ${initStr};`; + } + const typeStr = this.ctx.resolve(schema).value; + return `const ${typeStr} ${id} = ${initStr};`; } override globalVarDefinition(options: VariableDefinitionOptions): string { diff --git a/packages/typegpu-gl/tests/constant.test.ts b/packages/typegpu-gl/tests/constant.test.ts index fa36d672f8..0d4ba2ed18 100644 --- a/packages/typegpu-gl/tests/constant.test.ts +++ b/packages/typegpu-gl/tests/constant.test.ts @@ -19,3 +19,23 @@ test('constant with a scalar value', () => { }" `); }); + +// TODO: Emit a proper array initialization +test('constant with an array value', () => { + const FOO = tgpu.const(d.arrayOf(d.f32), [0.5, 0.2, 1.6]); + + function main() { + 'use gpu'; + const arr = FOO.$; + return arr[0]; + } + + expect(tgpu.resolve([main], glOptions({ shaderStage: 'none' }))).toMatchInlineSnapshot(` + "const float FOO[] = array(0.5f, 0.2f, 1.6f); + + float main() { + array arr = FOO; + return arr[0i]; + }" + `); +}); From 8b9dcf3076c7a1555a61c392588bb61aec0602c7 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 17 May 2026 22:03:49 +0200 Subject: [PATCH 14/14] Export workaround temporarily --- packages/typegpu-gl/src/index.ts | 2 +- packages/typegpu-gl/src/tgpuRootWebGL.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/typegpu-gl/src/index.ts b/packages/typegpu-gl/src/index.ts index 3c1501f270..ab140b0587 100644 --- a/packages/typegpu-gl/src/index.ts +++ b/packages/typegpu-gl/src/index.ts @@ -1,4 +1,4 @@ export { initWithGL } from './initWithGL.ts'; export { initWithGLFallback } from './initWithGLFallback.ts'; export { glOptions } from './glOptions.ts'; -export { isGLRoot } from './tgpuRootWebGL.ts'; +export { isGLRoot, WORKAROUND_wgslToGlslFixups } from './tgpuRootWebGL.ts'; diff --git a/packages/typegpu-gl/src/tgpuRootWebGL.ts b/packages/typegpu-gl/src/tgpuRootWebGL.ts index 0841336abc..697f59b092 100644 --- a/packages/typegpu-gl/src/tgpuRootWebGL.ts +++ b/packages/typegpu-gl/src/tgpuRootWebGL.ts @@ -79,10 +79,9 @@ vec4 saturate(vec4 x) { return clamp(x, 0.0, 1.0); } * Applies post-processing fixups to WGSL-like output produced by the resolution * pipeline so it becomes valid GLSL ES 3.0. * - * Some resolutions (like `tgpu.const`) emit WGSL syntax (e.g. `const x: T = ...;`) - * that we can't cleanly intercept from a generator alone; we rewrite those here. + * This should be removed as soon as the other systems allow for sufficient customization. */ -function wgslToGlslFixups(code: string): string { +export function WORKAROUND_wgslToGlslFixups(code: string): string { let out = code; // WGSL f32 literal suffixes -> GLSL float literals. A trailing `f` always marks a float, @@ -443,8 +442,8 @@ export class TgpuRootWebGL { unstable_shaderGenerator: fragmentGlslGenerator, }); - const vertexGlsl = GLSL_HEADER + wgslToGlslFixups(vertexCode); - const fragmentGlsl = GLSL_HEADER + wgslToGlslFixups(fragmentCode); + const vertexGlsl = GLSL_HEADER + WORKAROUND_wgslToGlslFixups(vertexCode); + const fragmentGlsl = GLSL_HEADER + WORKAROUND_wgslToGlslFixups(fragmentCode); const program = linkProgram(this.#gl, vertexGlsl, fragmentGlsl);