diff --git a/apps/typegpu-docs/package.json b/apps/typegpu-docs/package.json index 2b8273217d..fae0ab7e9f 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/sdf": "workspace:*", "@typegpu/sort": "workspace:*", diff --git a/apps/typegpu-docs/src/components/stackblitz/openInStackBlitz.ts b/apps/typegpu-docs/src/components/stackblitz/openInStackBlitz.ts index 971bff0581..3597dad794 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' }; @@ -120,6 +121,7 @@ ${example.htmlFile.content} "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}" } 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/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 e98c8d92f5..b58d9ef9bd 100644 --- a/apps/typegpu-docs/src/utils/examples/sandboxModules.ts +++ b/apps/typegpu-docs/src/utils/examples/sandboxModules.ts @@ -133,6 +133,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/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/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 4c549bdac7..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 @@ -44,15 +47,8 @@ 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'); + const id = ctx.makeUniqueIdentifier(getName(struct), 'global'); ctx.addDeclaration(`\ struct ${id} { @@ -66,9 +62,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; } /** @@ -77,11 +119,10 @@ interface EntryFnState { * 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 { - // 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,14 +134,13 @@ 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); } 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'; @@ -110,88 +150,151 @@ 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.type.startsWith('vec')) { - const block = super._block( - [NODE.block, [[NODE.assignmentExpr, 'gl_Position', '=', exprNode], [NODE.return]]], - { gl_Position: gl_PositionSnippet.$ }, - ); + if (expr.dataType === UnknownData) { + return super._return(statement); + } + + 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}`; + } - 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); + } + + #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 { + override functionDefinition(options: FunctionDefinitionOptions): string { if (options.functionType !== 'normal') { 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 +305,116 @@ 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 + // 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; + + // 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 completeStruct = autoStruct.completeStruct; + const structTypeName = this.ctx.resolve(completeStruct).value; + const initArgs: string[] = []; + for (const [prop, propType] of Object.entries(completeStruct.propTypes)) { + initArgs.push(resolveInputForField(prop, propType)); } + 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};`); + } + } + + // 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 +424,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/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/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 c010352abe..4a24035252 100644 --- a/packages/typegpu-gl/src/tgpuRootWebGL.ts +++ b/packages/typegpu-gl/src/tgpuRootWebGL.ts @@ -6,8 +6,27 @@ * Compute operations, storage buffers, textures, etc. throw WebGLFallbackUnsupportedError. */ -import tgpu, { d, ShaderGenerator, type TgpuFragmentFn, type TgpuVertexFn } from 'typegpu'; -import glslGenerator, { translateWgslTypeToGlsl } from './glslGenerator.ts'; +import { BufferWriter, BufferReader } from 'typed-binary'; +import tgpu, { + d, + type BufferInitialData, + type BufferWriteOptions, + type TgpuBuffer, + type TgpuFragmentFn, + type TgpuVertexFn, +} from 'typegpu'; +import { + readData, + writeData, + AutoFragmentFn, + AutoVertexFn, + makeDereferencable, + makeResolvable, + type ShaderGenerator, +} from 'typegpu/~internals'; + +import glslGenerator from './glslGenerator.ts'; +import { matchUpVaryingLocations } from './matchUpVaryingLocations.ts'; // ---------- // Public API @@ -30,243 +49,88 @@ 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; - /** @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 ArrayBuffer representation of the written data */ + readonly buffer: ArrayBuffer; } // ---------- // 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.`); - } +const GLSL_HEADER = `#version 300 es +precision highp float; +precision highp int; - // 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++; - } +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); } - 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'); -} - -/** - * 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; +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 + // 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]'); + + // 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} = `; } - 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};`); - } - } - } - } - - 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 { @@ -309,23 +173,65 @@ function linkProgram( return program; } +interface UniformBinding { + uniform: WebGLUniform; + location: WebGLUniformLocation; + setter: (gl: WebGL2RenderingContext, loc: WebGLUniformLocation, data: ArrayBuffer) => void; +} + +function uniformSetterFor( + schema: d.AnyWgslData, +): (gl: WebGL2RenderingContext, loc: WebGLUniformLocation, dataView: ArrayBuffer) => void { + const typeName = (schema as { type: string }).type; + 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 () => {}; +} + class TgpuWebGLRenderPipelineImpl implements TgpuWebGLRenderPipeline { #gl: WebGL2RenderingContext; #program: WebGLProgram; - #uniforms: Array; + #uniformBindings: UniformBinding[]; #renderCtx: WebGLRenderContext | 'screen' | null = null; #offscreen: OffscreenCanvas; + #vao: WebGLVertexArrayObject; constructor( gl: WebGL2RenderingContext, program: WebGLProgram, - uniforms: Array, + uniforms: WebGLUniform[], offscreen: OffscreenCanvas, ) { 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.dataType) }); + } + } + this.#uniformBindings = bindings; } withColorAttachment(attachment: { view: WebGLRenderContext | 'screen' }): this { @@ -336,13 +242,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,22 +254,18 @@ 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++) { - 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.buffer); } 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'); @@ -377,65 +277,134 @@ 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 glslName: string; - #gl: WebGL2RenderingContext; - #schema: TData; + readonly #gl: WebGL2RenderingContext; + readonly #initial: BufferInitialData | undefined; + + readonly dataType: TData; + + buffer: ArrayBuffer; + webglBuffer: WebGLBuffer | undefined; - constructor(gl: WebGL2RenderingContext, schema: TData) { + declare readonly $: d.InferGPU; + + constructor(gl: WebGL2RenderingContext, dataType: TData, initial?: BufferInitialData) { this.#gl = gl; - this.#schema = schema; - this.bindingIndex = _uniformBindingCounter++; + this.dataType = dataType; + this.#initial = initial; + this.glslName = `_u${_uniformCounter++}`; + this.buffer = new ArrayBuffer(d.sizeOf(dataType)); + } + + 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; + this.#populateBackingBuffer(initialData); + + gl.bufferData(gl.UNIFORM_BUFFER, this.buffer, gl.DYNAMIC_DRAW); + gl.bindBufferBase(gl.UNIFORM_BUFFER, bindingPoint, null); + } + return this.webglBuffer; + } + + #populateBackingBuffer(data: d.InferInput, options?: BufferWriteOptions) { + const startOffset = options?.startOffset ?? 0; + const endOffset = options?.endOffset ?? this.buffer.byteLength; + + // Fast path: raw byte copy, user guarantees the padded layout + if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { + const src = + data instanceof ArrayBuffer + ? new Uint8Array(data) + : new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + const regionSize = endOffset - startOffset; + if (src.byteLength !== regionSize) { + console.warn( + `Buffer size mismatch: expected ${regionSize} bytes, got ${src.byteLength}. ` + + (src.byteLength < regionSize ? 'Data truncated.' : 'Excess ignored.'), + ); + } + const copyLen = Math.min(src.byteLength, regionSize); + new Uint8Array(this.buffer).set(src.subarray(0, copyLen), startOffset); + return; + } - const buffer = gl.createBuffer(); - if (!buffer) throw new Error('Failed to create WebGL buffer'); - this.glBuffer = buffer; + const writer = new BufferWriter(this.buffer); + writer.seekTo(startOffset); + writeData(writer, this.dataType, data as d.Infer); } - get schema(): TData { - return this.#schema; + write(data: d.InferInput, options?: BufferWriteOptions): void { + this.#populateBackingBuffer(data, options); } - write(data: d.Infer): 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); - gl.bindBuffer(gl.UNIFORM_BUFFER, null); + public patch(_data: d.InferPatch): void { + // TODO(#2410): A lower-level patching API would be helpful here + throw new WebGLFallbackUnsupportedError('.patch()'); } -} -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); + public clear(): void { + new Uint8Array(this.buffer).fill(0); } - 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); + + copyFrom(_srcBuffer: TgpuBuffer>): void { + throw new WebGLFallbackUnsupportedError('.copyFrom()'); + } + + read(): Promise> { + return Promise.resolve(readData(new BufferReader(this.buffer), this.dataType)); + } + + 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}`; + } } -export interface CreateRenderPipelineWebGLOptions { - vertex: TgpuVertexFn; - fragment: TgpuFragmentFn; +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; + +function isShellFn(value: unknown): value is TgpuVertexFn | TgpuFragmentFn { + return typeof value === 'object' && value !== null && 'shell' in value; } export class TgpuRootWebGL { @@ -443,7 +412,7 @@ export class TgpuRootWebGL { #gl: WebGL2RenderingContext; #offscreen: OffscreenCanvas; - #uniforms: Array> = []; + #uniforms: WebGLUniformImpl[] = []; #buffers: WebGLBuffer[] = []; constructor(gl: WebGL2RenderingContext) { @@ -457,13 +426,10 @@ export class TgpuRootWebGL { createUniform( typeSchema: TData, - _initial?: d.Infer, + initial?: BufferInitialData, ): WebGLUniform { - const uniform = new WebGLUniformImpl(this.#gl, 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; } @@ -534,41 +500,60 @@ 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 vertexShell = isShellFn(vertex) ? vertex : undefined; + const fragmentShell = isShellFn(fragment) ? fragment : undefined; - // Resolve both functions using the GLSL generator - const vertexCode = tgpu.resolve([vertex], { - unstable_shaderGenerator: this.#shaderGenerator, - names: vertexNamespace, - }); + const locations = matchUpVaryingLocations( + vertexShell?.shell?.out, + fragmentShell?.shell?.in, + '', + '', + ); - const fragmentCode = tgpu.resolve([fragment], { - unstable_shaderGenerator: this.#shaderGenerator, - names: fragmentNamespace, - }); + 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]) => !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, - 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 +566,6 @@ export class TgpuRootWebGL { } with(_slot: unknown, _value: unknown): this { - // TODO: Implement slot binding return this; } @@ -596,22 +580,23 @@ 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) { this.#gl.deleteBuffer(buf); } + this.#buffers = []; for (const uniform of this.#uniforms) { - this.#gl.deleteBuffer(uniform.glBuffer); + uniform.destroy(); } - this.#buffers = []; this.#uniforms = []; } } + +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..b76d42fc80 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 { describe, expect } from 'vitest'; import tgpu, { d } from 'typegpu'; import { glOptions } from '@typegpu/gl'; import { translateWgslTypeToGlsl } from '../src/glslGenerator.ts'; +import { _resetUniformCounter } from '../src/tgpuRootWebGL.ts'; +import { it } 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,10 +238,12 @@ 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; } }" 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 96fb1978b4..d77074e761 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 f21cff1bf3..f26509614b 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'; // types @@ -59,6 +57,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..3b6ea860cc --- /dev/null +++ b/packages/typegpu/src/internals.ts @@ -0,0 +1,16 @@ +// 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'; +// TODO(#2410): Required for @typegpu/gl, but should be replaced with a proper API +export { writeData, readData } from './data/dataIO.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 ced6abe106..2a1a0a2a1e 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 { isValidIdentifier, sanitizePrimer } from './nameUtils.ts'; /** diff --git a/packages/typegpu/src/tgsl/shaderGenerator.ts b/packages/typegpu/src/tgsl/shaderGenerator.ts index adde48bf00..a6294947a1 100644 --- a/packages/typegpu/src/tgsl/shaderGenerator.ts +++ b/packages/typegpu/src/tgsl/shaderGenerator.ts @@ -1,7 +1,18 @@ +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 } from './shaderGenerator_members.ts'; +import type { 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; +} /** * **NOTE: This is an unstable API and may change in the future.** @@ -16,5 +27,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/shaderGenerator_members.ts b/packages/typegpu/src/tgsl/shaderGenerator_members.ts deleted file mode 100644 index 5fa67ce0c6..0000000000 --- a/packages/typegpu/src/tgsl/shaderGenerator_members.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Block } from 'tinyest'; -import type { BaseData } from '../data/wgslTypes.ts'; -import type { FunctionArgument, TgpuShaderStage } from '../types.ts'; - -export { UnknownData } from '../data/dataTypes.ts'; -export { getName } from '../shared/meta.ts'; -export { makeDereferencable } from './makeDereferencable.ts'; -export { makeResolvable } from './makeResolvable.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; -} diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index fbd00724db..84f4fef5eb 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -38,7 +38,7 @@ import { } from './generationHelpers.ts'; import { accessIndex } from './accessIndex.ts'; import { accessProp } from './accessProp.ts'; -import type { ShaderGenerator } from './shaderGenerator.ts'; +import type { ShaderGenerator, FunctionDefinitionOptions } from './shaderGenerator.ts'; import { resolveData } from '../core/resolve/resolveData.ts'; import { createPtrFromOrigin, implicitFrom, ptrFn } from '../data/ptr.ts'; import { RefOperator } from '../data/ref.ts'; @@ -51,7 +51,6 @@ import { mathToStd } from './math.ts'; import type { ExternalMap } from '../core/resolve/externals.ts'; import * as forOfUtils from './forOfUtils.ts'; import { isTgpuRange } from '../std/range.ts'; -import type { FunctionDefinitionOptions } from './shaderGenerator_members.ts'; import { getAttributesString } from '../data/attributes.ts'; const { NodeTypeCatalog: NODE } = tinyest; diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index 9b39c20138..95ef864440 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 bae32d359d..21a07a4592 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,7 +130,7 @@ importers: devDependencies: '@types/bun': specifier: latest - version: 1.3.12 + version: 1.3.13 apps/infra-benchmarks: devDependencies: @@ -244,6 +244,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 @@ -629,6 +632,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:* @@ -3539,6 +3546,9 @@ packages: '@types/bun@1.3.12': resolution: {integrity: sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A==} + '@types/bun@1.3.13': + resolution: {integrity: sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -4195,6 +4205,9 @@ packages: bun-types@1.3.12: resolution: {integrity: sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA==} + bun-types@1.3.13: + resolution: {integrity: sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA==} + bun@1.3.10: resolution: {integrity: sha512-S/CXaXXIyA4CMjdMkYQ4T2YMqnAn4s0ysD3mlsY4bUiOCqGlv28zck4Wd4H4kpvbekx15S9mUeLQ7Uxd0tYTLA==} cpu: [arm64, x64] @@ -10623,6 +10636,10 @@ snapshots: dependencies: bun-types: 1.3.12 + '@types/bun@1.3.13': + dependencies: + bun-types: 1.3.13 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -11543,6 +11560,10 @@ snapshots: dependencies: '@types/node': 24.10.0 + bun-types@1.3.13: + dependencies: + '@types/node': 24.10.0 + bun@1.3.10: optionalDependencies: '@oven/bun-darwin-aarch64': 1.3.10