From e1e61bee18442710eedc7393ca98a579bfbf9b2c Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:54:39 +0100 Subject: [PATCH 1/9] test: Add missing buffer binding tests --- packages/typegpu/tests/bufferBindings.test.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/typegpu/tests/bufferBindings.test.ts b/packages/typegpu/tests/bufferBindings.test.ts index 770bb81951..c094821544 100644 --- a/packages/typegpu/tests/bufferBindings.test.ts +++ b/packages/typegpu/tests/bufferBindings.test.ts @@ -230,6 +230,25 @@ describe('root.createMutable', () => { mappedBuffer.write([{ id: 1, values: d.vec3f() }]); }); }); + + it('should not require .$ in template resolve', ({ root }) => { + const buf = root.createMutable(d.u32); + + expect( + tgpu.resolve({ + externals: { buf }, + template: ` +fn helper() -> i32 { + return buf; +}`, + }), + ).toMatchInlineSnapshot(` + "@group(0) @binding(0) var buf: u32; + fn helper() -> i32 { + return buf; + }" + `); + }); }); describe('root.createReadonly', () => { @@ -498,6 +517,25 @@ describe('root.createReadonly', () => { mappedBuffer.write([{ id: 1, values: d.vec3f() }]); }); }); + + it('should not require .$ in template resolve', ({ root }) => { + const buf = root.createReadonly(d.u32); + + expect( + tgpu.resolve({ + externals: { buf }, + template: ` +fn helper() -> i32 { + return buf; +}`, + }), + ).toMatchInlineSnapshot(` + "@group(0) @binding(0) var buf: u32; + fn helper() -> i32 { + return buf; + }" + `); + }); }); describe('root.createUniform', () => { @@ -728,4 +766,23 @@ describe('root.createUniform', () => { mappedBuffer.write([{ id: 1, values: d.vec3f() }]); }); }); + + it('should not require .$ in template resolve', ({ root }) => { + const buf = root.createUniform(d.u32); + + expect( + tgpu.resolve({ + externals: { buf }, + template: ` +fn helper() -> i32 { + return buf; +}`, + }), + ).toMatchInlineSnapshot(` + "@group(0) @binding(0) var buf: u32; + fn helper() -> i32 { + return buf; + }" + `); + }); }); From 9a78c143de5b1c4a652de0f048165450affd448a Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:55:56 +0100 Subject: [PATCH 2/9] refactor: Use `makeResolvable` in tgpu.declare --- .../typegpu/src/core/declare/tgpuDeclare.ts | 41 ++++++++++--------- packages/typegpu/tests/declare.test.ts | 21 ++++++++++ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/packages/typegpu/src/core/declare/tgpuDeclare.ts b/packages/typegpu/src/core/declare/tgpuDeclare.ts index a105518138..30108de219 100644 --- a/packages/typegpu/src/core/declare/tgpuDeclare.ts +++ b/packages/typegpu/src/core/declare/tgpuDeclare.ts @@ -1,7 +1,7 @@ -import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; +import { makeResolvable } from 'typegpu/~internal'; +import { snip } from '../../data/snippet.ts'; import { Void } from '../../data/wgslTypes.ts'; -import { $internal, $resolve } from '../../shared/symbols.ts'; -import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; +import { $internal } from '../../shared/symbols.ts'; import { type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; // ---------- @@ -31,7 +31,7 @@ export function declare(declaration: string): TgpuDeclare { // Implementation // -------------- -export class TgpuDeclareImpl implements TgpuDeclare, SelfResolvable { +export class TgpuDeclareImpl implements TgpuDeclare { readonly [$internal] = true; #externals: ExternalMap | undefined; #declaration: string; @@ -40,6 +40,24 @@ export class TgpuDeclareImpl implements TgpuDeclare, SelfResolvable { this.#declaration = declaration; } + static { + makeResolvable(TgpuDeclareImpl.prototype, { + asString() { + return `declare: ${this.#declaration}`; + }, + resolve(ctx) { + const replacedDeclaration = replaceExternalsInWgsl( + ctx, + this.#externals ?? {}, + this.#declaration, + ); + + ctx.addDeclaration(replacedDeclaration); + return snip('', Void, /* origin */ 'constant'); + }, + }); + } + $uses(dependencyMap: Record): this { if (this.#externals !== undefined) { throw new Error( @@ -49,19 +67,4 @@ export class TgpuDeclareImpl implements TgpuDeclare, SelfResolvable { this.#externals = dependencyMap; return this; } - - [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const replacedDeclaration = replaceExternalsInWgsl( - ctx, - this.#externals ?? {}, - this.#declaration, - ); - - ctx.addDeclaration(replacedDeclaration); - return snip('', Void, /* origin */ 'constant'); - } - - toString() { - return `declare: ${this.#declaration}`; - } } diff --git a/packages/typegpu/tests/declare.test.ts b/packages/typegpu/tests/declare.test.ts index cd5d95679f..1aa08b5f4e 100644 --- a/packages/typegpu/tests/declare.test.ts +++ b/packages/typegpu/tests/declare.test.ts @@ -117,4 +117,25 @@ struct Output { `[Error: Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.]`, ); }); + + it('should not require .$ in template resolve', () => { + const declaration = tgpu['~unstable'].declare('const C = 4;'); + + expect( + tgpu.resolve({ + externals: { declaration }, + template: ` +declaration +fn helper() -> i32 { + return C; +}`, + }), + ).toMatchInlineSnapshot(` + "const C = 4; + + fn helper() -> i32 { + return C; + }" + `); + }); }); From fad56b7dfa5c31599d4862bad2dfc3d5af25a323 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:08:47 +0100 Subject: [PATCH 3/9] test: Add entry function types tests --- .../typegpu/tests/computePipeline.test.ts | 28 ++++++++++++++- packages/typegpu/tests/renderPipeline.test.ts | 36 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/typegpu/tests/computePipeline.test.ts b/packages/typegpu/tests/computePipeline.test.ts index 3d68551d45..fb9326cf7f 100644 --- a/packages/typegpu/tests/computePipeline.test.ts +++ b/packages/typegpu/tests/computePipeline.test.ts @@ -1,5 +1,12 @@ import { describe, expect, expectTypeOf, vi } from 'vitest'; -import { d, isBindGroup, MissingBindGroupsError, tgpu, type TgpuComputePipeline } from 'typegpu'; +import { + d, + isBindGroup, + MissingBindGroupsError, + tgpu, + type TgpuComputeFn, + type TgpuComputePipeline, +} from 'typegpu'; import { restoreResource, snapshotResource } from 'typegpu/~internal'; import { it } from 'typegpu-testing-utility'; import { extensionEnabled } from 'typegpu/std'; @@ -840,3 +847,22 @@ describe('pipe', () => { expect(pipeline.pipe((p) => p)).toBe(pipeline); }); }); + +describe('types', () => { + it('compute is assignable to a bare TgpuComputeFn', () => { + const computeMain = tgpu.computeFn({ + workgroupSize: [1], + })``; + + expectTypeOf(computeMain).toExtend(); + }); + + it('compute with in is assignable to a bare TgpuComputeFn', () => { + const computeMain = tgpu.computeFn({ + in: { gid: d.builtin.globalInvocationId }, + workgroupSize: [1], + })``; + + expectTypeOf(computeMain).toExtend(); + }); +}); diff --git a/packages/typegpu/tests/renderPipeline.test.ts b/packages/typegpu/tests/renderPipeline.test.ts index b9876c0009..5f454b374f 100644 --- a/packages/typegpu/tests/renderPipeline.test.ts +++ b/packages/typegpu/tests/renderPipeline.test.ts @@ -2460,3 +2460,39 @@ describe('pipe', () => { expectTypeOf(pipeline.pipe((p) => p)).toEqualTypeOf(pipeline); }); }); + +describe('types', () => { + it('fragment is assignable to a bare TgpuFragmentFn', () => { + const fragmentMain = tgpu.fragmentFn({ + out: d.vec4f, + })``; + + expectTypeOf(fragmentMain).toExtend(); + }); + + it('fragment with in is assignable to a bare TgpuFragmentFn', () => { + const fragmentMain = tgpu.fragmentFn({ + in: { coordinates: d.location(4, d.vec2i) }, + out: d.vec4f, + })``; + + expectTypeOf(fragmentMain).toExtend(); + }); + + it('vertex is assignable to a bare TgpuVertexFn', () => { + const vertexMain = tgpu.vertexFn({ + out: { pos: d.builtin.position }, + })``; + + expectTypeOf(vertexMain).toExtend(); + }); + + it('vertex with in is assignable to a bare TgpuVertexFn', () => { + const vertexMain = tgpu.vertexFn({ + in: { coordinates: d.location(4, d.vec2i) }, + out: { pos: d.builtin.position }, + })``; + + expectTypeOf(vertexMain).toExtend(); + }); +}); From 696a3b17c65669ff50d30e40d56bb5bbcdae22be Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:19:12 +0100 Subject: [PATCH 4/9] Fix entry types --- packages/typegpu/src/core/function/tgpuFragmentFn.ts | 2 +- packages/typegpu/src/core/function/tgpuVertexFn.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index e6d3b8091f..a909e4d733 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -107,7 +107,7 @@ export interface TgpuFragmentFnShell< export interface TgpuFragmentFn< // @ts-expect-error: We override the variance - in Varying extends TgpuFragmentFn.In = Record, + in Varying extends TgpuFragmentFn.In = never, out Output extends TgpuFragmentFn.Out = TgpuFragmentFn.Out, > extends TgpuNamable { readonly [$internal]: true; diff --git a/packages/typegpu/src/core/function/tgpuVertexFn.ts b/packages/typegpu/src/core/function/tgpuVertexFn.ts index e0d2d4c388..877a053bbc 100644 --- a/packages/typegpu/src/core/function/tgpuVertexFn.ts +++ b/packages/typegpu/src/core/function/tgpuVertexFn.ts @@ -78,7 +78,7 @@ export interface TgpuVertexFnShell< export interface TgpuVertexFn< // @ts-expect-error: We override the variance - in VertexIn extends TgpuVertexFn.In = Record, + in VertexIn extends TgpuVertexFn.In = never, out VertexOut extends TgpuVertexFn.Out = TgpuVertexFn.Out, > extends TgpuNamable { readonly [$internal]: true; From 241e8ba4ee54c25fd14ead9e0363546c4bb07af0 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:33:57 +0100 Subject: [PATCH 5/9] feat: Make declare dereferenceable --- .../typegpu/src/core/declare/tgpuDeclare.ts | 54 +++++++++++++------ packages/typegpu/tests/declare.test.ts | 14 ++++- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/packages/typegpu/src/core/declare/tgpuDeclare.ts b/packages/typegpu/src/core/declare/tgpuDeclare.ts index 30108de219..fccf67451e 100644 --- a/packages/typegpu/src/core/declare/tgpuDeclare.ts +++ b/packages/typegpu/src/core/declare/tgpuDeclare.ts @@ -1,8 +1,10 @@ -import { makeResolvable } from 'typegpu/~internal'; import { snip } from '../../data/snippet.ts'; import { Void } from '../../data/wgslTypes.ts'; import { $internal } from '../../shared/symbols.ts'; import { type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; +import type { InferGPU } from '../../data/index.ts'; +import { makeDereferenceable } from '../../tgsl/makeDereferenceable.ts'; +import { makeResolvable } from '../../tgsl/makeResolvable.ts'; // ---------- // Public API @@ -13,7 +15,9 @@ import { type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.t * when resolving objects that use it. */ export interface TgpuDeclare { + [$internal]: true; $uses(dependencyMap: Record): this; + $: InferGPU; } /** @@ -32,7 +36,6 @@ export function declare(declaration: string): TgpuDeclare { // -------------- export class TgpuDeclareImpl implements TgpuDeclare { - readonly [$internal] = true; #externals: ExternalMap | undefined; #declaration: string; @@ -40,22 +43,43 @@ export class TgpuDeclareImpl implements TgpuDeclare { this.#declaration = declaration; } + // prototype properties + declare [$internal]: true; + declare $: InferGPU; + static { - makeResolvable(TgpuDeclareImpl.prototype, { - asString() { - return `declare: ${this.#declaration}`; - }, - resolve(ctx) { - const replacedDeclaration = replaceExternalsInWgsl( - ctx, - this.#externals ?? {}, - this.#declaration, - ); + TgpuDeclareImpl.prototype[$internal] = true; + + makeDereferenceable( + makeResolvable(TgpuDeclareImpl.prototype, { + asString() { + return `declare: ${this.#declaration}`; + }, + resolve(ctx) { + const replacedDeclaration = replaceExternalsInWgsl( + ctx, + this.#externals ?? {}, + this.#declaration, + ); - ctx.addDeclaration(replacedDeclaration); - return snip('', Void, /* origin */ 'constant'); + ctx.addDeclaration(replacedDeclaration); + return snip('', Void, /* origin */ 'constant'); + }, + }), + { + codegenMode: { + getBaseSnippet(trackingProxy) { + return snip( + trackingProxy, + Void, + /* origin */ 'constant', + /* possibleSideEffects */ false, + ); + }, + }, + normalMode: { get() {} }, }, - }); + ); } $uses(dependencyMap: Record): this { diff --git a/packages/typegpu/tests/declare.test.ts b/packages/typegpu/tests/declare.test.ts index 1aa08b5f4e..0a674aa101 100644 --- a/packages/typegpu/tests/declare.test.ts +++ b/packages/typegpu/tests/declare.test.ts @@ -93,7 +93,7 @@ struct Output { [], d.f32, )(() => { - declaration; + declaration.$; return 2; }); @@ -106,6 +106,18 @@ struct Output { `); }); + it('is a noop in JS', () => { + const declaration = tgpu['~unstable'].declare('@group(0) @binding(0) var val: f32;'); + + const main = () => { + 'use gpu'; + declaration.$; + return 2 + 2; + }; + + expect(main()).toBe(4); + }); + it("throws when '$uses' is called multiple times", () => { const declaration = tgpu['~unstable'] .declare('@group(0) @binding(0) var val: f32;') From 71817e9a50df489a963c749b8d85b7db527ff309 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:47:52 +0100 Subject: [PATCH 6/9] Review fixes --- apps/typegpu-docs/src/content/docs/apis/utils.mdx | 10 +++++++++- packages/typegpu/src/core/declare/tgpuDeclare.ts | 4 ++-- packages/typegpu/tests/bufferBindings.test.ts | 6 +++--- packages/typegpu/tests/declare.test.ts | 8 ++++++++ 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/apps/typegpu-docs/src/content/docs/apis/utils.mdx b/apps/typegpu-docs/src/content/docs/apis/utils.mdx index 223406342d..89225ebb07 100644 --- a/apps/typegpu-docs/src/content/docs/apis/utils.mdx +++ b/apps/typegpu-docs/src/content/docs/apis/utils.mdx @@ -219,7 +219,9 @@ allowing functions to branch based on what language is being generated. `tgpu['~unstable'].declare(source)` adds a declaration to generated WGSL whenever a resolving object depends on it. It is an escape hatch for WGSL declarations that TypeGPU cannot yet express directly: -```ts +```ts twoslash +import { tgpu } from 'typegpu'; +// ---cut--- const diagnosticDirective = tgpu['~unstable'].declare( 'diagnostic(off, derivative_uniformity);', ); @@ -227,6 +229,12 @@ const diagnosticDirective = tgpu['~unstable'].declare( const helper = tgpu.fn([])`() { // ... }`.$uses({ diagnosticDirective }); + +const anotherHelper = () => { + 'use gpu'; + // The declaration will not affect the function's body. + diagnosticDirective.$; +} ``` Declarations can reference TypeGPU resources through one dependency map: diff --git a/packages/typegpu/src/core/declare/tgpuDeclare.ts b/packages/typegpu/src/core/declare/tgpuDeclare.ts index fccf67451e..927059254b 100644 --- a/packages/typegpu/src/core/declare/tgpuDeclare.ts +++ b/packages/typegpu/src/core/declare/tgpuDeclare.ts @@ -15,9 +15,9 @@ import { makeResolvable } from '../../tgsl/makeResolvable.ts'; * when resolving objects that use it. */ export interface TgpuDeclare { - [$internal]: true; + readonly [$internal]: true; $uses(dependencyMap: Record): this; - $: InferGPU; + readonly $: InferGPU; } /** diff --git a/packages/typegpu/tests/bufferBindings.test.ts b/packages/typegpu/tests/bufferBindings.test.ts index c094821544..e60321fcf9 100644 --- a/packages/typegpu/tests/bufferBindings.test.ts +++ b/packages/typegpu/tests/bufferBindings.test.ts @@ -232,7 +232,7 @@ describe('root.createMutable', () => { }); it('should not require .$ in template resolve', ({ root }) => { - const buf = root.createMutable(d.u32); + const buf = root.createMutable(d.i32); expect( tgpu.resolve({ @@ -519,7 +519,7 @@ describe('root.createReadonly', () => { }); it('should not require .$ in template resolve', ({ root }) => { - const buf = root.createReadonly(d.u32); + const buf = root.createReadonly(d.i32); expect( tgpu.resolve({ @@ -768,7 +768,7 @@ describe('root.createUniform', () => { }); it('should not require .$ in template resolve', ({ root }) => { - const buf = root.createUniform(d.u32); + const buf = root.createUniform(d.i32); expect( tgpu.resolve({ diff --git a/packages/typegpu/tests/declare.test.ts b/packages/typegpu/tests/declare.test.ts index 0a674aa101..3c616ab4c9 100644 --- a/packages/typegpu/tests/declare.test.ts +++ b/packages/typegpu/tests/declare.test.ts @@ -118,6 +118,14 @@ struct Output { expect(main()).toBe(4); }); + it('has a readable toString', () => { + const declaration = tgpu['~unstable'].declare('@group(0) @binding(0) var val: f32;'); + + expect(`${declaration}`).toMatchInlineSnapshot( + `"declare: @group(0) @binding(0) var val: f32;"`, + ); + }); + it("throws when '$uses' is called multiple times", () => { const declaration = tgpu['~unstable'] .declare('@group(0) @binding(0) var val: f32;') From 457c6ec456bf2ff18d8b20b0b493ae2e31fd4276 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:46:11 +0100 Subject: [PATCH 7/9] test: Fix snapshots --- packages/typegpu/tests/bufferBindings.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/typegpu/tests/bufferBindings.test.ts b/packages/typegpu/tests/bufferBindings.test.ts index e60321fcf9..ee9af57660 100644 --- a/packages/typegpu/tests/bufferBindings.test.ts +++ b/packages/typegpu/tests/bufferBindings.test.ts @@ -243,7 +243,7 @@ fn helper() -> i32 { }`, }), ).toMatchInlineSnapshot(` - "@group(0) @binding(0) var buf: u32; + "@group(0) @binding(0) var buf: i32; fn helper() -> i32 { return buf; }" @@ -530,7 +530,7 @@ fn helper() -> i32 { }`, }), ).toMatchInlineSnapshot(` - "@group(0) @binding(0) var buf: u32; + "@group(0) @binding(0) var buf: i32; fn helper() -> i32 { return buf; }" @@ -779,7 +779,7 @@ fn helper() -> i32 { }`, }), ).toMatchInlineSnapshot(` - "@group(0) @binding(0) var buf: u32; + "@group(0) @binding(0) var buf: i32; fn helper() -> i32 { return buf; }" From 7e35149d8fc68b2a88378feb8967f00392ef7328 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:53:03 +0100 Subject: [PATCH 8/9] test: Fix style --- packages/typegpu/tests/declare.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/typegpu/tests/declare.test.ts b/packages/typegpu/tests/declare.test.ts index 3c616ab4c9..64cb00b138 100644 --- a/packages/typegpu/tests/declare.test.ts +++ b/packages/typegpu/tests/declare.test.ts @@ -121,6 +121,7 @@ struct Output { it('has a readable toString', () => { const declaration = tgpu['~unstable'].declare('@group(0) @binding(0) var val: f32;'); + // oxlint-disable-next-line typescript/no-base-to-string expect(`${declaration}`).toMatchInlineSnapshot( `"declare: @group(0) @binding(0) var val: f32;"`, ); From 126dd206ecb172c4e281e54e4b7b5b9659f9d31f Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:34:36 +0100 Subject: [PATCH 9/9] Review fixes --- packages/typegpu/src/core/declare/tgpuDeclare.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/declare/tgpuDeclare.ts b/packages/typegpu/src/core/declare/tgpuDeclare.ts index 927059254b..a309314479 100644 --- a/packages/typegpu/src/core/declare/tgpuDeclare.ts +++ b/packages/typegpu/src/core/declare/tgpuDeclare.ts @@ -1,6 +1,6 @@ import { snip } from '../../data/snippet.ts'; import { Void } from '../../data/wgslTypes.ts'; -import { $internal } from '../../shared/symbols.ts'; +import { $gpuValueOf, $internal } from '../../shared/symbols.ts'; import { type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; import type { InferGPU } from '../../data/index.ts'; import { makeDereferenceable } from '../../tgsl/makeDereferenceable.ts'; @@ -18,6 +18,7 @@ export interface TgpuDeclare { readonly [$internal]: true; $uses(dependencyMap: Record): this; readonly $: InferGPU; + readonly [$gpuValueOf]: InferGPU; } /** @@ -46,6 +47,7 @@ export class TgpuDeclareImpl implements TgpuDeclare { // prototype properties declare [$internal]: true; declare $: InferGPU; + declare readonly [$gpuValueOf]: InferGPU; static { TgpuDeclareImpl.prototype[$internal] = true;