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