From 669e47f2a4301c028b87bc02b4450f48155c0bdb Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:35:50 +0200 Subject: [PATCH 01/15] Remove setName from mergeExternals --- .../individual-example-tests/shifting-gradient.test.ts | 8 ++++---- .../individual-example-tests/spinning-triangle.test.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts b/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts index c848d13d86..1c4373cfaf 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts @@ -34,7 +34,7 @@ describe('react/shifting-gradient example', () => { return fullScreenTriangle_Output(vec4f(pos[vertexIndex], 0, 1), uv[vertexIndex]); } - @group(0) @binding(0) var time: f32; + @group(0) @binding(0) var item: f32; fn computeMaxSaturation(a: f32, b: f32) -> f32 { var k0 = 0f; @@ -218,11 +218,11 @@ describe('react/shifting-gradient example', () => { @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f { let fromStart = vec3f(0.6279553771018982, 0.22486300766468048, 0.1258462816476822); let fromEnd = vec3f(0.45201370120048523, -0.03245693817734718, -0.31152817606925964); - let from_1 = mix(fromStart, fromEnd, ((sin(time) * 0.5f) + 0.5f)); + let from_1 = mix(fromStart, fromEnd, ((sin(item) * 0.5f) + 0.5f)); let toStart = vec3f(0.8664395809173584, -0.2338874489068985, 0.17949843406677246); let toEnd = vec3f(0.7016738653182983, 0.27456632256507874, -0.16915608942508698); - let to = mix(toStart, toEnd, ((cos((time * 1.5f)) * 0.5f) + 0.5f)); - let mixed = mix(from_1, to, ((((_arg_0.uv.x * 2f) - 1f) * 0.5f) + (sin((time + (_arg_0.uv.y * 3f))) * 0.5f))); + let to = mix(toStart, toEnd, ((cos((item * 1.5f)) * 0.5f) + 0.5f)); + let mixed = mix(from_1, to, ((((_arg_0.uv.x * 2f) - 1f) * 0.5f) + (sin((item + (_arg_0.uv.y * 3f))) * 0.5f))); return vec4f(oklabToRgb(mixed), 1f); }" `); diff --git a/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts b/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts index b6d81fc47a..6f21c2472d 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts @@ -24,7 +24,7 @@ describe('react/spinning-triangle example', () => { expect(shaderCodes).toMatchInlineSnapshot(` "const vertices: array = array(vec2f(0, 1), vec2f(-0.8660253882408142, -0.5), vec2f(0.8660253882408142, -0.5)); - @group(0) @binding(0) var time: f32; + @group(0) @binding(0) var item: f32; fn rotate(v: vec2f, angle: f32) -> vec2f { let pos = vec2f(((v.x * cos(angle)) - (v.y * sin(angle))), ((v.x * sin(angle)) + (v.y * cos(angle)))); @@ -44,7 +44,7 @@ describe('react/spinning-triangle example', () => { @vertex fn vertex(_arg_0: VertexIn) -> VertexOut { let local = vertices[_arg_0.vertexIndex]; - let rotated = rotate(local, (time * 0.1f)); + let rotated = rotate(local, (item * 0.1f)); return VertexOut(vec4f((rotated * 0.7f), 0f, 1f), length((local - vertices[0i])), length((local - vertices[1i])), length((local - vertices[2i]))); } @@ -235,7 +235,7 @@ describe('react/spinning-triangle example', () => { @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f { let dist = (1f / (1.4f - min(min(_arg_0.dist0, _arg_0.dist1), _arg_0.dist2))); - let albedo = getGradientColor((((fract(((dist * 2f) - time)) * 2f) - 1f) + cos(time))); + let albedo = getGradientColor((((fract(((dist * 2f) - item)) * 2f) - 1f) + cos(item))); return vec4f(albedo, 1f); }" `); From 692ac90136fa6ec90f307e45f37407c10cad6335 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:43:46 +0200 Subject: [PATCH 02/15] Name unnamed externals in propAccess and getById --- .../individual-example-tests/shifting-gradient.test.ts | 8 ++++---- .../individual-example-tests/spinning-triangle.test.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts b/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts index 1c4373cfaf..c848d13d86 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/shifting-gradient.test.ts @@ -34,7 +34,7 @@ describe('react/shifting-gradient example', () => { return fullScreenTriangle_Output(vec4f(pos[vertexIndex], 0, 1), uv[vertexIndex]); } - @group(0) @binding(0) var item: f32; + @group(0) @binding(0) var time: f32; fn computeMaxSaturation(a: f32, b: f32) -> f32 { var k0 = 0f; @@ -218,11 +218,11 @@ describe('react/shifting-gradient example', () => { @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f { let fromStart = vec3f(0.6279553771018982, 0.22486300766468048, 0.1258462816476822); let fromEnd = vec3f(0.45201370120048523, -0.03245693817734718, -0.31152817606925964); - let from_1 = mix(fromStart, fromEnd, ((sin(item) * 0.5f) + 0.5f)); + let from_1 = mix(fromStart, fromEnd, ((sin(time) * 0.5f) + 0.5f)); let toStart = vec3f(0.8664395809173584, -0.2338874489068985, 0.17949843406677246); let toEnd = vec3f(0.7016738653182983, 0.27456632256507874, -0.16915608942508698); - let to = mix(toStart, toEnd, ((cos((item * 1.5f)) * 0.5f) + 0.5f)); - let mixed = mix(from_1, to, ((((_arg_0.uv.x * 2f) - 1f) * 0.5f) + (sin((item + (_arg_0.uv.y * 3f))) * 0.5f))); + let to = mix(toStart, toEnd, ((cos((time * 1.5f)) * 0.5f) + 0.5f)); + let mixed = mix(from_1, to, ((((_arg_0.uv.x * 2f) - 1f) * 0.5f) + (sin((time + (_arg_0.uv.y * 3f))) * 0.5f))); return vec4f(oklabToRgb(mixed), 1f); }" `); diff --git a/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts b/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts index 6f21c2472d..b6d81fc47a 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/spinning-triangle.test.ts @@ -24,7 +24,7 @@ describe('react/spinning-triangle example', () => { expect(shaderCodes).toMatchInlineSnapshot(` "const vertices: array = array(vec2f(0, 1), vec2f(-0.8660253882408142, -0.5), vec2f(0.8660253882408142, -0.5)); - @group(0) @binding(0) var item: f32; + @group(0) @binding(0) var time: f32; fn rotate(v: vec2f, angle: f32) -> vec2f { let pos = vec2f(((v.x * cos(angle)) - (v.y * sin(angle))), ((v.x * sin(angle)) + (v.y * cos(angle)))); @@ -44,7 +44,7 @@ describe('react/spinning-triangle example', () => { @vertex fn vertex(_arg_0: VertexIn) -> VertexOut { let local = vertices[_arg_0.vertexIndex]; - let rotated = rotate(local, (item * 0.1f)); + let rotated = rotate(local, (time * 0.1f)); return VertexOut(vec4f((rotated * 0.7f), 0f, 1f), length((local - vertices[0i])), length((local - vertices[1i])), length((local - vertices[2i]))); } @@ -235,7 +235,7 @@ describe('react/spinning-triangle example', () => { @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f { let dist = (1f / (1.4f - min(min(_arg_0.dist0, _arg_0.dist1), _arg_0.dist2))); - let albedo = getGradientColor((((fract(((dist * 2f) - item)) * 2f) - 1f) + cos(item))); + let albedo = getGradientColor((((fract(((dist * 2f) - time)) * 2f) - 1f) + cos(time))); return vec4f(albedo, 1f); }" `); From a944c600397cef1fb31414649334a0c007fe5233 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:06:21 +0200 Subject: [PATCH 03/15] Limit tgpuDeclare to one external --- .../typegpu/src/core/declare/tgpuDeclare.ts | 23 +++++++++++-------- packages/typegpu/tests/declare.test.ts | 10 ++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/typegpu/src/core/declare/tgpuDeclare.ts b/packages/typegpu/src/core/declare/tgpuDeclare.ts index f30cd357e0..c1a3c2466c 100644 --- a/packages/typegpu/src/core/declare/tgpuDeclare.ts +++ b/packages/typegpu/src/core/declare/tgpuDeclare.ts @@ -2,7 +2,7 @@ import { type ResolvedSnippet, 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 { mergeExternals, type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; +import { type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; // ---------- // Public API @@ -33,7 +33,7 @@ export function declare(declaration: string): TgpuDeclare { class TgpuDeclareImpl implements TgpuDeclare, SelfResolvable { readonly [$internal] = true; - #externalsToApply: ExternalMap[] = []; + #externals: ExternalMap | undefined; #declaration: string; constructor(declaration: string) { @@ -41,18 +41,21 @@ class TgpuDeclareImpl implements TgpuDeclare, SelfResolvable { } $uses(dependencyMap: Record): this { - this.#externalsToApply.push(dependencyMap); + if (this.#externals !== undefined) { + throw new Error( + "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", + ); + } + this.#externals = dependencyMap; return this; } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const externalMap: ExternalMap = {}; - - for (const externals of this.#externalsToApply) { - mergeExternals(externalMap, externals); - } - - const replacedDeclaration = replaceExternalsInWgsl(ctx, externalMap, this.#declaration); + const replacedDeclaration = replaceExternalsInWgsl( + ctx, + this.#externals ?? {}, + this.#declaration, + ); ctx.addDeclaration(replacedDeclaration); return snip('', Void, /* origin */ 'constant'); diff --git a/packages/typegpu/tests/declare.test.ts b/packages/typegpu/tests/declare.test.ts index a4247ba832..8883c5e3fd 100644 --- a/packages/typegpu/tests/declare.test.ts +++ b/packages/typegpu/tests/declare.test.ts @@ -105,4 +105,14 @@ struct Output { }" `); }); + + it("throws when '$uses' is called multiple times", () => { + const declaration = tgpu['~unstable'] + .declare('@group(0) @binding(0) var val: f32;') + .$uses({ myStruct: d.struct({ p: d.u32 }) }); + + expect(() => + declaration.$uses({ myStruct: d.struct({ p: d.u32 }) }), + ).toThrowErrorMatchingInlineSnapshot(`[Error: Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.]`); + }); }); From 754eb92a5abe03528555ba7ef47f7f2d0274ed11 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:10:46 +0200 Subject: [PATCH 04/15] Remove unnecessary merge from tgpuResolve --- packages/typegpu/src/core/resolve/tgpuResolve.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/typegpu/src/core/resolve/tgpuResolve.ts b/packages/typegpu/src/core/resolve/tgpuResolve.ts index 710fe14c33..77d95a9403 100644 --- a/packages/typegpu/src/core/resolve/tgpuResolve.ts +++ b/packages/typegpu/src/core/resolve/tgpuResolve.ts @@ -195,14 +195,11 @@ function resolveFromTemplate(options: TgpuExtendedResolveOptions): ResolutionRes ); } - const dependencies = {} as Record; - mergeExternals(dependencies, externals ?? {}); - const resolutionObj: SelfResolvable = { [$internal]: true, [$resolve](ctx): ResolvedSnippet { return snip( - replaceExternalsInWgsl(ctx, dependencies, template ?? ''), + replaceExternalsInWgsl(ctx, externals, template ?? ''), Void, /* origin */ 'runtime', ); From a2179404efe51014847ffd5d03746f05ecf347a2 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:14:36 +0200 Subject: [PATCH 05/15] Limit rawCodeSnippet to one $uses --- .../core/rawCodeSnippet/tgpuRawCodeSnippet.ts | 24 ++++++++++--------- .../typegpu/tests/tgsl/rawCodeSnippet.test.ts | 12 ++++++++++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts index 7c0af44152..1f27ea9737 100644 --- a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts +++ b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts @@ -5,7 +5,7 @@ import { inCodegenMode } from '../../execMode.ts'; import type { InferGPU } from '../../shared/repr.ts'; import { $gpuValueOf, $internal, $ownSnippet, $resolve } from '../../shared/symbols.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; -import { mergeExternals, type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; +import { type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; // ---------- @@ -92,7 +92,7 @@ class TgpuRawCodeSnippetImpl readonly origin: RawCodeSnippetOrigin; #expression: string; - #externalsToApply: ExternalMap[]; + #externals: ExternalMap | undefined; constructor(expression: string, type: TDataType, origin: RawCodeSnippetOrigin) { this[$internal] = true; @@ -100,22 +100,24 @@ class TgpuRawCodeSnippetImpl this.origin = origin; this.#expression = expression; - this.#externalsToApply = []; } $uses(dependencyMap: Record): this { - this.#externalsToApply.push(dependencyMap); + if (this.#externals !== undefined) { + throw new Error( + "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", + ); + } + this.#externals = dependencyMap; return this; } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const externalMap: ExternalMap = {}; - - for (const externals of this.#externalsToApply) { - mergeExternals(externalMap, externals); - } - - const replacedExpression = replaceExternalsInWgsl(ctx, externalMap, this.#expression); + const replacedExpression = replaceExternalsInWgsl( + ctx, + this.#externals ?? {}, + this.#expression, + ); return snip(replacedExpression, this.dataType, this.origin); } diff --git a/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts b/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts index b0dcd5f70b..d4f71be7a5 100644 --- a/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts +++ b/packages/typegpu/tests/tgsl/rawCodeSnippet.test.ts @@ -124,6 +124,18 @@ describe('rawCodeSnippet', () => { `); }); + it("throws when '$uses' is called multiple times", ({ root }) => { + const myBuffer = root.createUniform(d.u32, 7); + + const rawSnippet = tgpu['~unstable'] + .rawCodeSnippet('myBuffer', d.u32, 'uniform') + .$uses({ myBuffer }); + + expect(() => rawSnippet.$uses({ myBuffer })).toThrowErrorMatchingInlineSnapshot( + `[Error: Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.]`, + ); + }); + it('should be accessed transitively through a slot', () => { const exprSlot = tgpu.slot(tgpu['~unstable'].rawCodeSnippet('0.5 + 0.2', d.f32, 'constant')); From 1f8bccbde6a2e99eb26db2cb32ca75565cc30dca Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:43:07 +0200 Subject: [PATCH 06/15] Add FnExternals, and throw if $uses is called multiple times --- packages/typegpu/src/core/function/fnCore.ts | 20 ++++ .../src/core/function/tgpuComputeFn.ts | 1 + packages/typegpu/src/core/function/tgpuFn.ts | 1 + .../src/core/function/tgpuFragmentFn.ts | 9 +- .../typegpu/src/core/function/tgpuVertexFn.ts | 2 + .../typegpu/src/core/resolve/tgpuResolve.ts | 2 +- packages/typegpu/tests/function.test.ts | 107 +++--------------- 7 files changed, 46 insertions(+), 96 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index d1dfe4249c..27ee0c501d 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -10,8 +10,16 @@ import { mergeExternals, type ExternalMap, replaceExternalsInWgsl } from '../res import { extractArgs } from './extractArgs.ts'; import type { Implementation, SeparatedEntryArgs } from './fnTypes.ts'; +export type FnExternals = { + userProvided?: ExternalMap; + pluginProvided?: ExternalMap; + args?: ExternalMap; + out?: ExternalMap; +}; + export interface FnCore { applyExternals: (newExternals: ExternalMap) => void; + setExternals: (key: keyof FnExternals, newExternal: ExternalMap) => void; resolve( ctx: ResolutionCtx, /** @@ -44,6 +52,7 @@ export function createFnCore( * entry fn). */ const externalsToApply: ExternalMap[] = []; + const externals: FnExternals = {}; const core = { // Making the implementation the holder of the name, as long as it's @@ -53,6 +62,17 @@ export function createFnCore( externalsToApply.push(newExternals); }, + setExternals(key: keyof typeof externals, newExternal: ExternalMap): void { + if (key in externals) { + if (key === 'userProvided') { + throw new Error( + "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", + ); + } + } + externals[key] = newExternal; + }, + resolve( ctx: ResolutionCtx, argTypes: BaseData[], diff --git a/packages/typegpu/src/core/function/tgpuComputeFn.ts b/packages/typegpu/src/core/function/tgpuComputeFn.ts index 9cef2d49cc..06a33cfc58 100644 --- a/packages/typegpu/src/core/function/tgpuComputeFn.ts +++ b/packages/typegpu/src/core/function/tgpuComputeFn.ts @@ -121,6 +121,7 @@ function createComputeFn>( $uses(newExternals) { core.applyExternals(newExternals); + core.setExternals('userProvided', newExternals); return this; }, diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index d7e316e092..c5dd9421fc 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -182,6 +182,7 @@ function createFn( $uses(newExternals: Record) { core.applyExternals(newExternals); + core.setExternals('userProvided', newExternals); return this; }, diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index cb3b4c990f..d987fbfa89 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -186,9 +186,10 @@ function createFragmentFn( const core = createFnCore(implementation, 'fragment'); const outputType = shell.returnType; if (typeof implementation === 'string') { - addReturnTypeToExternals(implementation, outputType, (externals) => - core.applyExternals(externals), - ); + addReturnTypeToExternals(implementation, outputType, (externals) => { + core.applyExternals(externals); + core.setExternals('out', externals); + }); } const result: This = { @@ -197,6 +198,7 @@ function createFragmentFn( $uses(newExternals) { core.applyExternals(newExternals); + core.setExternals('userProvided', newExternals); return this; }, @@ -217,6 +219,7 @@ function createFragmentFn( entryInput.dataSchema.$name(`${getName(this) ?? ''}_Input`); } core.applyExternals({ Out: outputType }); + core.setExternals('out', { Out: outputType }); return ctx.withSlots([[shaderStageSlot, 'fragment']], () => core.resolve(ctx, [], shell.returnType, entryInput), diff --git a/packages/typegpu/src/core/function/tgpuVertexFn.ts b/packages/typegpu/src/core/function/tgpuVertexFn.ts index 8c5b230891..66839245be 100644 --- a/packages/typegpu/src/core/function/tgpuVertexFn.ts +++ b/packages/typegpu/src/core/function/tgpuVertexFn.ts @@ -166,6 +166,7 @@ function createVertexFn( $uses(newExternals) { core.applyExternals(newExternals); + core.setExternals('userProvided', newExternals); return this; }, @@ -183,6 +184,7 @@ function createVertexFn( if (typeof implementation === 'string') { core.applyExternals({ Out: outputWithLocation }); + core.setExternals('out', { Out: outputWithLocation }); } return ctx.withSlots([[shaderStageSlot, 'vertex']], () => diff --git a/packages/typegpu/src/core/resolve/tgpuResolve.ts b/packages/typegpu/src/core/resolve/tgpuResolve.ts index 77d95a9403..71ff0487f6 100644 --- a/packages/typegpu/src/core/resolve/tgpuResolve.ts +++ b/packages/typegpu/src/core/resolve/tgpuResolve.ts @@ -8,7 +8,7 @@ import type { ResolvableObject, SelfResolvable, Wgsl } from '../../types.ts'; import type { WgslEnableExtension } from '../../wgslExtensions.ts'; import { isPipeline } from '../pipeline/typeGuards.ts'; import type { Configurable, ExperimentalTgpuRoot } from '../root/rootTypes.ts'; -import { mergeExternals, replaceExternalsInWgsl } from './externals.ts'; +import { replaceExternalsInWgsl } from './externals.ts'; import { type Namespace, namespace } from './namespace.ts'; export interface TgpuResolveOptions { diff --git a/packages/typegpu/tests/function.test.ts b/packages/typegpu/tests/function.test.ts index e9ae56ecc6..f44c3c9dd0 100644 --- a/packages/typegpu/tests/function.test.ts +++ b/packages/typegpu/tests/function.test.ts @@ -84,99 +84,22 @@ describe('tgpu.fn', () => { >(); }); - it('applies multiple externals', () => { - const fn = tgpu.fn([])`() { - let a = X; - let b = Y; - let c = Z; -}` - .$uses({ X: 1 }) - .$uses({ Y: 2 }) - .$uses({ Z: 3 }); - - expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { - let a = 1; - let b = 2; - let c = 3; - }" - `); - }); - - it('applies multiple externals in correct order', () => { - const fn = tgpu.fn([])`() { - let a = X; - let b = Y; - let c = Z; -}` - .$uses({ X: 'Y' }) - .$uses({ Y: 'Z' }) - .$uses({ Z: 3 }); - - expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { - let a = 3; - let b = 3; - let c = 3; - }" - `); - }); - - it('applies multiple nested externals', () => { - const fn = tgpu.fn([])`() { - let a = EXT.X; - let b = EXT.Y; - let c = EXT.Z; -}` - .$uses({ EXT: { X: 1 } }) - .$uses({ EXT: { Y: 2 } }) - .$uses({ EXT: { Z: 3 } }); - - expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { - let a = 1; - let b = 2; - let c = 3; - }" - `); - }); - - it('does not mutate original externals', () => { - const SHARED_EXT = { X: 1 }; - - const fn = tgpu.fn([])`() { - let a = X; - let b = Y; -}` - .$uses(SHARED_EXT) - .$uses({ Y: 2 }); - - expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { - let a = 1; - let b = 2; - }" - `); - expect(SHARED_EXT).toStrictEqual({ X: 1 }); - }); - it('does not mutate values of original externals', () => { - const SHARED_EXT = { EXT: { X: 1 } }; - - const fn = tgpu.fn([])`() { - let a = EXT.X; - let b = EXT.Y; -}` - .$uses(SHARED_EXT) - .$uses({ EXT: { Y: 2 } }); - - expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { - let a = 1; - let b = 2; - }" - `); - expect(SHARED_EXT).toStrictEqual({ EXT: { X: 1 } }); + // TODO: check out/in keys for entry points + // const SHARED_EXT = { EXT: { X: 1 } }; + // const fn = tgpu.fn([])`() { + // let a = EXT.X; + // let b = EXT.Y; + // }` + // .$uses(SHARED_EXT) + // .$uses({ EXT: { Y: 2 } }); + // expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + // "fn fn_1() { + // let a = 1; + // let b = 2; + // }" + // `); + // expect(SHARED_EXT).toStrictEqual({ EXT: { X: 1 } }); }); it('does not break when an unused unresolvable external is passed', () => { From b0ec4478b294c8772a60738377dc45309ebd36d0 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:15:29 +0200 Subject: [PATCH 07/15] Prepare string-implemented functions for external swap --- packages/typegpu/src/core/function/fnCore.ts | 22 ++++++- packages/typegpu/src/core/function/tgpuFn.ts | 4 +- .../src/core/function/tgpuFragmentFn.ts | 5 +- .../typegpu/src/core/resolve/externals.ts | 56 ++++++++++++----- packages/typegpu/tests/externals.test.ts | 61 ++++++++++--------- 5 files changed, 96 insertions(+), 52 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 27ee0c501d..0ea141fc37 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -6,7 +6,12 @@ import { validateIdentifier } from '../../nameUtils.ts'; import { getFunctionMetadata, getName } from '../../shared/meta.ts'; import { $getNameForward } from '../../shared/symbols.ts'; import type { ResolutionCtx, TgpuShaderStage } from '../../types.ts'; -import { mergeExternals, type ExternalMap, replaceExternalsInWgsl } from '../resolve/externals.ts'; +import { + mergeExternals, + type ExternalMap, + replaceExternalsInWgsl, + mergeFunctionExternals, +} from '../resolve/externals.ts'; import { extractArgs } from './extractArgs.ts'; import type { Implementation, SeparatedEntryArgs } from './fnTypes.ts'; @@ -62,7 +67,7 @@ export function createFnCore( externalsToApply.push(newExternals); }, - setExternals(key: keyof typeof externals, newExternal: ExternalMap): void { + setExternals(key: keyof FnExternals, newExternal: ExternalMap): void { if (key in externals) { if (key === 'userProvided') { throw new Error( @@ -121,9 +126,22 @@ export function createFnCore( entryInput.positionalArgs.map((a) => [a.schemaKey, a.schemaKey]), ), }); + this.setExternals('args', { + in: Object.fromEntries( + entryInput.positionalArgs.map((a) => [a.schemaKey, a.schemaKey]), + ), + }); } + console.log(externalsToApply); + console.log(externalMap); + console.log(externals); const replacedImpl = replaceExternalsInWgsl(ctx, externalMap, implementation); + // const replacedImpl = replaceExternalsInWgsl( + // ctx, + // mergeFunctionExternals(externals), + // implementation, + // ); let header = ''; let body = ''; diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index c5dd9421fc..2c500fc45f 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -204,8 +204,8 @@ function createFn( [$resolve](ctx: ResolutionCtx): ResolvedSnippet { if (typeof implementation === 'string') { - addArgTypesToExternals(implementation, shell.argTypes, core.applyExternals); - addReturnTypeToExternals(implementation, shell.returnType, core.applyExternals); + addArgTypesToExternals(implementation, shell.argTypes, core); + addReturnTypeToExternals(implementation, shell.returnType, core); } return core.resolve(ctx, shell.argTypes, shell.returnType); diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index d987fbfa89..e2862fb939 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -186,10 +186,7 @@ function createFragmentFn( const core = createFnCore(implementation, 'fragment'); const outputType = shell.returnType; if (typeof implementation === 'string') { - addReturnTypeToExternals(implementation, outputType, (externals) => { - core.applyExternals(externals); - core.setExternals('out', externals); - }); + addReturnTypeToExternals(implementation, outputType, core); } const result: This = { diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index ab3c885d0c..2082799ad7 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -2,6 +2,7 @@ import { isLooseData } from '../../data/dataTypes.ts'; import { isWgslStruct } from '../../data/wgslTypes.ts'; import { getName, hasTinyestMetadata, isNamable, setName } from '../../shared/meta.ts'; import { isWgsl, type ResolutionCtx } from '../../types.ts'; +import type { FnCore, FnExternals } from '../function/fnCore.ts'; /** * A key-value mapping where keys represent identifiers within shader code, @@ -13,6 +14,35 @@ function isResolvable(value: unknown) { return isWgsl(value) || isLooseData(value) || hasTinyestMetadata(value); } +/** + * Merges function externals into one map. + * Assumes that there is at most one map with non-trivial structure. + */ +export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { + console.log('MERGING'); + console.log(fnExternals); + + if (fnExternals.pluginProvided !== undefined && fnExternals.userProvided !== undefined) { + throw new Error( + "Cannot call '$uses' on functions whose metadata was provided by unplugin-typegpu.", + ); + } + const base = fnExternals.pluginProvided ?? fnExternals.userProvided ?? {}; + // avoid calling any of the getters + const result: ExternalMap = Object.defineProperties({}, Object.getOwnPropertyDescriptors(base)); + for (const flatExternal of [fnExternals.args, fnExternals.out].filter((e) => e !== undefined)) { + for (const [key, value] of Object.entries(flatExternal)) { + if (key in result) { + throw new Error( + `Key '${key}' appears in externals while being reserved for internals. Please rename this external.`, + ); + } + result[key] = value; + } + } + return result; +} + /** * Merges two external maps into one. * If the external value is a namable object, it is given a name if it does not already have one. @@ -45,35 +75,33 @@ export function mergeExternals(existing: ExternalMap, newExternals: ExternalMap) } } -export function addArgTypesToExternals( - implementation: string, - argTypes: unknown[], - applyExternals: (externals: ExternalMap) => void, -) { +export function addArgTypesToExternals(implementation: string, argTypes: unknown[], core: FnCore) { const argTypeNames = [...implementation.matchAll(/:\s*(?.*?)\s*[,)]/g)].map((found) => found ? found[1] : undefined, ); - applyExternals( - Object.fromEntries( - argTypes.flatMap((argType, i) => { - const argTypeName = argTypeNames ? argTypeNames[i] : undefined; - return isWgslStruct(argType) && argTypeName !== undefined ? [[argTypeName, argType]] : []; - }), - ), + const args = Object.fromEntries( + argTypes.flatMap((argType, i) => { + const argTypeName = argTypeNames ? argTypeNames[i] : undefined; + return isWgslStruct(argType) && argTypeName !== undefined ? [[argTypeName, argType]] : []; + }), ); + + core.applyExternals(args); + core.setExternals('args', args); } export function addReturnTypeToExternals( implementation: string, returnType: unknown, - applyExternals: (externals: ExternalMap) => void, + core: FnCore, ) { const matched = implementation.match(/->\s(?[\w\d_]+)\s{/); const outputName = matched ? matched[1]?.trim() : undefined; if (isWgslStruct(returnType) && outputName && !/\s/g.test(outputName)) { - applyExternals({ [outputName]: returnType }); + core.applyExternals({ [outputName]: returnType }); + core.setExternals('out', { [outputName]: returnType }); } } diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index f35c22cd93..841d5c7a33 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { addArgTypesToExternals, type ExternalMap } from '../src/core/resolve/externals.ts'; import * as d from '../src/data/index.ts'; +// TODO: fix these tests describe('addArgTypesToExternals', () => { const Particle = d.struct({ position: d.vec3f, @@ -15,47 +16,47 @@ describe('addArgTypesToExternals', () => { it('extracts struct argument types with their names', () => { const externals: ExternalMap[] = []; - addArgTypesToExternals( - '(a: vec4f, b: Particle, c: Light) {}', - [d.vec4f, Particle, Light], - (result) => externals.push(result), - ); - expect(externals).toStrictEqual([{ Particle, Light }]); + // addArgTypesToExternals( + // '(a: vec4f, b: Particle, c: Light) {}', + // [d.vec4f, Particle, Light], + // (result) => externals.push(result), + // ); + // expect(externals).toStrictEqual([{ Particle, Light }]); }); it('gets the names from argument list in WGSL implementation', () => { const externals: ExternalMap[] = []; - addArgTypesToExternals('(b: P, a: vec4f, c: L) -> L {}', [Particle, d.vec4f, Light], (result) => - externals.push(result), - ); - expect(externals).toStrictEqual([{ P: Particle, L: Light }]); + // addArgTypesToExternals('(b: P, a: vec4f, c: L) -> L {}', [Particle, d.vec4f, Light], (result) => + // externals.push(result), + // ); + // expect(externals).toStrictEqual([{ P: Particle, L: Light }]); }); it('works when builtins are present', () => { const externals: ExternalMap[] = []; - addArgTypesToExternals( - '(@builtin(workgroup_id) WorkGroupID : vec3u, a: vec4f, b: Particle, c: Light) {}', - [d.vec3u, d.vec4f, Particle, Light], - (result) => externals.push(result), - ); - expect(externals).toStrictEqual([{ Particle, Light }]); + // addArgTypesToExternals( + // '(@builtin(workgroup_id) WorkGroupID : vec3u, a: vec4f, b: Particle, c: Light) {}', + // [d.vec3u, d.vec4f, Particle, Light], + // (result) => externals.push(result), + // ); + // expect(externals).toStrictEqual([{ Particle, Light }]); }); it('works with unusual whitespace', () => { const externals: ExternalMap[] = []; - addArgTypesToExternals( - ` WorkGroupID : vec3u - , - a : A , - (@builtin(workgroup_id) b - - : B, - - c: C - ) -> vec4f {}`, - [d.vec3u, Particle, Particle, Particle], - (result) => externals.push(result), - ); - expect(externals).toStrictEqual([{ A: Particle, B: Particle, C: Particle }]); + // addArgTypesToExternals( + // ` WorkGroupID : vec3u + // , + // a : A , + // (@builtin(workgroup_id) b + + // : B, + + // c: C + // ) -> vec4f {}`, + // [d.vec3u, Particle, Particle, Particle], + // (result) => externals.push(result), + // ); + // expect(externals).toStrictEqual([{ A: Particle, B: Particle, C: Particle }]); }); }); From 236075cd62ca35ed9ddc32a072834775d52e4b38 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:23:11 +0200 Subject: [PATCH 08/15] Finish replacing old externals with new externals && cleanup --- packages/typegpu/src/core/function/fnCore.ts | 51 +++++-------------- .../src/core/function/tgpuComputeFn.ts | 1 - packages/typegpu/src/core/function/tgpuFn.ts | 1 - .../src/core/function/tgpuFragmentFn.ts | 2 - .../typegpu/src/core/function/tgpuVertexFn.ts | 2 - .../typegpu/src/core/resolve/externals.ts | 2 - .../typegpu/tests/computePipeline.test.ts | 8 ++- packages/typegpu/tests/tgslFn.test.ts | 50 ++++++++---------- 8 files changed, 38 insertions(+), 79 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 0ea141fc37..696983cde7 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -7,7 +7,6 @@ import { getFunctionMetadata, getName } from '../../shared/meta.ts'; import { $getNameForward } from '../../shared/symbols.ts'; import type { ResolutionCtx, TgpuShaderStage } from '../../types.ts'; import { - mergeExternals, type ExternalMap, replaceExternalsInWgsl, mergeFunctionExternals, @@ -56,24 +55,19 @@ export function createFnCore( * initialized yet (like when accessing the Output struct of a vertex * entry fn). */ - const externalsToApply: ExternalMap[] = []; const externals: FnExternals = {}; const core = { // Making the implementation the holder of the name, as long as it's // a function (and not a string implementation) [$getNameForward]: typeof implementation === 'function' ? implementation : undefined, - applyExternals(newExternals: ExternalMap): void { - externalsToApply.push(newExternals); - }, setExternals(key: keyof FnExternals, newExternal: ExternalMap): void { - if (key in externals) { - if (key === 'userProvided') { - throw new Error( - "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", - ); - } + if (key === 'userProvided' && key in externals) { + // other external keys may be set multiple times by multiple resolves + throw new Error( + "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", + ); } externals[key] = newExternal; }, @@ -84,8 +78,6 @@ export function createFnCore( returnType: BaseData | undefined, entryInput?: SeparatedEntryArgs, ): ResolvedSnippet { - const externalMap: ExternalMap = {}; - let attributes = ''; if (functionType === 'compute') { attributes = `@compute @workgroup_size(${workgroupSize?.join(', ')}) `; @@ -95,10 +87,6 @@ export function createFnCore( attributes = `@fragment `; } - for (const externals of externalsToApply) { - mergeExternals(externalMap, externals); - } - const id = ctx.makeUniqueIdentifier(getName(this), 'global'); if (typeof implementation === 'string') { @@ -121,11 +109,6 @@ export function createFnCore( } } - mergeExternals(externalMap, { - in: Object.fromEntries( - entryInput.positionalArgs.map((a) => [a.schemaKey, a.schemaKey]), - ), - }); this.setExternals('args', { in: Object.fromEntries( entryInput.positionalArgs.map((a) => [a.schemaKey, a.schemaKey]), @@ -133,15 +116,11 @@ export function createFnCore( }); } - console.log(externalsToApply); - console.log(externalMap); - console.log(externals); - const replacedImpl = replaceExternalsInWgsl(ctx, externalMap, implementation); - // const replacedImpl = replaceExternalsInWgsl( - // ctx, - // mergeFunctionExternals(externals), - // implementation, - // ); + const replacedImpl = replaceExternalsInWgsl( + ctx, + mergeFunctionExternals(externals), + implementation, + ); let header = ''; let body = ''; @@ -213,11 +192,7 @@ export function createFnCore( const pluginExternals = pluginData?.externals(); if (pluginExternals) { - const missing = Object.fromEntries( - Object.entries(pluginExternals).filter(([name]) => !(name in externalMap)), - ); - - mergeExternals(externalMap, missing); + this.setExternals('pluginProvided', pluginExternals); } const ast = pluginData?.ast; @@ -231,7 +206,7 @@ export function createFnCore( // We look at the identifier chosen by the user and add it to externals. const maybeSecondArg = ast.params[1]; if (maybeSecondArg && maybeSecondArg.type === 'i' && functionType !== 'normal') { - mergeExternals(externalMap, { + this.setExternals('out', { // oxlint-disable-next-line typescript/no-non-null-assertion -- entry functions cannot be shellless [maybeSecondArg.name]: undecorate(returnType!), }); @@ -248,7 +223,7 @@ export function createFnCore( params: ast.params, returnType, body: ast.body, - externalMap, + externalMap: mergeFunctionExternals(externals), }); ctx.addDeclaration(code); diff --git a/packages/typegpu/src/core/function/tgpuComputeFn.ts b/packages/typegpu/src/core/function/tgpuComputeFn.ts index 06a33cfc58..f7118526a6 100644 --- a/packages/typegpu/src/core/function/tgpuComputeFn.ts +++ b/packages/typegpu/src/core/function/tgpuComputeFn.ts @@ -120,7 +120,6 @@ function createComputeFn>( shell, $uses(newExternals) { - core.applyExternals(newExternals); core.setExternals('userProvided', newExternals); return this; }, diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index 2c500fc45f..a9840c57cb 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -181,7 +181,6 @@ function createFn( [$internal]: { implementation }, $uses(newExternals: Record) { - core.applyExternals(newExternals); core.setExternals('userProvided', newExternals); return this; }, diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index e2862fb939..676ad2828b 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -194,7 +194,6 @@ function createFragmentFn( outputType, $uses(newExternals) { - core.applyExternals(newExternals); core.setExternals('userProvided', newExternals); return this; }, @@ -215,7 +214,6 @@ function createFragmentFn( if (entryInput.dataSchema && isNamable(entryInput.dataSchema)) { entryInput.dataSchema.$name(`${getName(this) ?? ''}_Input`); } - core.applyExternals({ Out: outputType }); core.setExternals('out', { Out: outputType }); return ctx.withSlots([[shaderStageSlot, 'fragment']], () => diff --git a/packages/typegpu/src/core/function/tgpuVertexFn.ts b/packages/typegpu/src/core/function/tgpuVertexFn.ts index 66839245be..68b7bee7d3 100644 --- a/packages/typegpu/src/core/function/tgpuVertexFn.ts +++ b/packages/typegpu/src/core/function/tgpuVertexFn.ts @@ -165,7 +165,6 @@ function createVertexFn( shell, $uses(newExternals) { - core.applyExternals(newExternals); core.setExternals('userProvided', newExternals); return this; }, @@ -183,7 +182,6 @@ function createVertexFn( ); if (typeof implementation === 'string') { - core.applyExternals({ Out: outputWithLocation }); core.setExternals('out', { Out: outputWithLocation }); } diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 2082799ad7..303cc7d378 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -87,7 +87,6 @@ export function addArgTypesToExternals(implementation: string, argTypes: unknown }), ); - core.applyExternals(args); core.setExternals('args', args); } @@ -100,7 +99,6 @@ export function addReturnTypeToExternals( const outputName = matched ? matched[1]?.trim() : undefined; if (isWgslStruct(returnType) && outputName && !/\s/g.test(outputName)) { - core.applyExternals({ [outputName]: returnType }); core.setExternals('out', { [outputName]: returnType }); } } diff --git a/packages/typegpu/tests/computePipeline.test.ts b/packages/typegpu/tests/computePipeline.test.ts index 3cbea14d08..091a6fece0 100644 --- a/packages/typegpu/tests/computePipeline.test.ts +++ b/packages/typegpu/tests/computePipeline.test.ts @@ -280,11 +280,9 @@ describe('TgpuComputePipeline', () => { data: buffer, }); - const entryFn = tgpu - .computeFn({ workgroupSize: [1] })(() => { - layout.$.data; - }) - .$uses({ layout }); + const entryFn = tgpu.computeFn({ workgroupSize: [1] })(() => { + layout.$.data; + }); const querySet = root.createQuerySet('timestamp', 4); diff --git a/packages/typegpu/tests/tgslFn.test.ts b/packages/typegpu/tests/tgslFn.test.ts index 6b6be0d13c..d44b94d87a 100644 --- a/packages/typegpu/tests/tgslFn.test.ts +++ b/packages/typegpu/tests/tgslFn.test.ts @@ -28,36 +28,30 @@ describe('TGSL tgpu.fn function', () => { }); it('resolves externals', () => { - const getColor = tgpu - .fn( - [], - d.vec3f, - )(() => { - const color = d.vec3f(); - const color2 = d.vec3f(1, 2, 3); - return color; - }) - .$uses({ v: d.vec3f }); + const getColor = tgpu.fn( + [], + d.vec3f, + )(() => { + const color = d.vec3f(); + const color2 = d.vec3f(1, 2, 3); + return color; + }); - const getX = tgpu - .fn( - [], - d.f32, - )(() => { - const color = getColor(); - return 3; - }) - .$uses({ getColor }); + const getX = tgpu.fn( + [], + d.f32, + )(() => { + const color = getColor(); + return 3; + }); - const getY = tgpu - .fn( - [], - d.f32, - )(() => { - const c = getColor(); - return getX(); - }) - .$uses({ getX, getColor }); + const getY = tgpu.fn( + [], + d.f32, + )(() => { + const c = getColor(); + return getX(); + }); expect(tgpu.resolve([getY])).toMatchInlineSnapshot(` "fn getColor() -> vec3f { From 7c7faad51ff717ccf1098d8ed24b3cbd46c28c17 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:27:17 +0200 Subject: [PATCH 09/15] More cleanup --- packages/typegpu/src/core/function/fnCore.ts | 1 - .../core/rawCodeSnippet/tgpuRawCodeSnippet.ts | 6 +--- .../typegpu/src/core/resolve/externals.ts | 35 ------------------- packages/typegpu/tests/declare.test.ts | 4 ++- 4 files changed, 4 insertions(+), 42 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 696983cde7..4e73230be5 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -22,7 +22,6 @@ export type FnExternals = { }; export interface FnCore { - applyExternals: (newExternals: ExternalMap) => void; setExternals: (key: keyof FnExternals, newExternal: ExternalMap) => void; resolve( ctx: ResolutionCtx, diff --git a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts index 1f27ea9737..b07010e7f3 100644 --- a/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts +++ b/packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts @@ -113,11 +113,7 @@ class TgpuRawCodeSnippetImpl } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const replacedExpression = replaceExternalsInWgsl( - ctx, - this.#externals ?? {}, - this.#expression, - ); + const replacedExpression = replaceExternalsInWgsl(ctx, this.#externals ?? {}, this.#expression); return snip(replacedExpression, this.dataType, this.origin); } diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 303cc7d378..1698e5a92b 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -19,9 +19,6 @@ function isResolvable(value: unknown) { * Assumes that there is at most one map with non-trivial structure. */ export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { - console.log('MERGING'); - console.log(fnExternals); - if (fnExternals.pluginProvided !== undefined && fnExternals.userProvided !== undefined) { throw new Error( "Cannot call '$uses' on functions whose metadata was provided by unplugin-typegpu.", @@ -43,38 +40,6 @@ export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { return result; } -/** - * Merges two external maps into one. - * If the external value is a namable object, it is given a name if it does not already have one. - * @param existing - The existing external map. - * @param newExternals - The new external map. - * - * NOTE: - * This function attempts to avoid accidental reference modification - * by performing a shallow copy before each modification, - * but it cannot avoid `existing` modification. - * Make sure that `existing` is created internally, instead of being passed in by users. - */ -export function mergeExternals(existing: ExternalMap, newExternals: ExternalMap) { - for (const [key, value] of Object.entries(newExternals)) { - const existingValue = existing[key]; - if ( - existingValue !== null && - typeof existingValue === 'object' && - value !== null && - typeof value === 'object' && - !isResolvable(existingValue) && - !isResolvable(value) - ) { - const copiedValue = { ...(existingValue as ExternalMap) }; - mergeExternals(copiedValue, value as ExternalMap); - existing[key] = copiedValue; - } else { - existing[key] = value; - } - } -} - export function addArgTypesToExternals(implementation: string, argTypes: unknown[], core: FnCore) { const argTypeNames = [...implementation.matchAll(/:\s*(?.*?)\s*[,)]/g)].map((found) => found ? found[1] : undefined, diff --git a/packages/typegpu/tests/declare.test.ts b/packages/typegpu/tests/declare.test.ts index 8883c5e3fd..2b018c3192 100644 --- a/packages/typegpu/tests/declare.test.ts +++ b/packages/typegpu/tests/declare.test.ts @@ -113,6 +113,8 @@ struct Output { expect(() => declaration.$uses({ myStruct: d.struct({ p: d.u32 }) }), - ).toThrowErrorMatchingInlineSnapshot(`[Error: Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.]`); + ).toThrowErrorMatchingInlineSnapshot( + `[Error: Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.]`, + ); }); }); From a03b83fb22efc41e8cf596fffb2ca5f9c22a6f25 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:58:50 +0200 Subject: [PATCH 10/15] Detect $uses on function with plugin metadata earlier --- packages/typegpu/src/core/function/fnCore.ts | 17 ++++++++++++----- packages/typegpu/src/core/resolve/externals.ts | 5 ----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 4e73230be5..b2bd340c15 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -62,11 +62,18 @@ export function createFnCore( [$getNameForward]: typeof implementation === 'function' ? implementation : undefined, setExternals(key: keyof FnExternals, newExternal: ExternalMap): void { - if (key === 'userProvided' && key in externals) { - // other external keys may be set multiple times by multiple resolves - throw new Error( - "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", - ); + if (key === 'userProvided') { + if ('userProvided' in externals) { + // other external keys may be set multiple times by multiple resolves + throw new Error( + "Cannot call '$uses' multiple times. If you wish to override dependencies, use slots or accessors instead.", + ); + } + if ('pluginProvided' in externals) { + throw new Error( + "Cannot call '$uses' on functions whose metadata was provided by unplugin-typegpu.", + ); + } } externals[key] = newExternal; }, diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 1698e5a92b..a1accaacdb 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -19,11 +19,6 @@ function isResolvable(value: unknown) { * Assumes that there is at most one map with non-trivial structure. */ export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { - if (fnExternals.pluginProvided !== undefined && fnExternals.userProvided !== undefined) { - throw new Error( - "Cannot call '$uses' on functions whose metadata was provided by unplugin-typegpu.", - ); - } const base = fnExternals.pluginProvided ?? fnExternals.userProvided ?? {}; // avoid calling any of the getters const result: ExternalMap = Object.defineProperties({}, Object.getOwnPropertyDescriptors(base)); From 1a16a8b3daa5de1a2ec63c789d55eec2904f4bd9 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:57:06 +0200 Subject: [PATCH 11/15] Add FnExternals docs --- packages/typegpu/src/core/function/fnCore.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index b2bd340c15..9988a549f7 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -15,9 +15,25 @@ import { extractArgs } from './extractArgs.ts'; import type { Implementation, SeparatedEntryArgs } from './fnTypes.ts'; export type FnExternals = { + /** + * Externals provided by calling `$uses()`. + * May be nested. + */ userProvided?: ExternalMap; + /** + * Externals provided by unplugin-typegpu via function metadata. + * May be nested. + */ pluginProvided?: ExternalMap; + /** + * Function arguments, for example `{ S: Schema }` in `tgpu.fn([Schema])('(arg: S) => {}')`. + * Must be flat (every value must be resolvable). + */ args?: ExternalMap; + /** + * Function return type, for example `{ Out: ... }` in rawWgsl entrypoint functions. + * Must be flat (every value must be resolvable). + */ out?: ExternalMap; }; From 9f336913e5c405ad72ee9b300863937f3dd75871 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:57:26 +0200 Subject: [PATCH 12/15] Add external name collision tests --- packages/typegpu/tests/externals.test.ts | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index 841d5c7a33..15d8afea65 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { addArgTypesToExternals, type ExternalMap } from '../src/core/resolve/externals.ts'; import * as d from '../src/data/index.ts'; +import tgpu from '../src/index.js'; // TODO: fix these tests describe('addArgTypesToExternals', () => { @@ -60,3 +61,78 @@ describe('addArgTypesToExternals', () => { // expect(externals).toStrictEqual([{ A: Particle, B: Particle, C: Particle }]); }); }); + +describe('external name collisions', () => { + it("throws when rawWgsl fn has an 'Out' external", () => { + const vertexFn = tgpu.vertexFn({ + out: { position: d.builtin.position }, + })`{ return Out(); }`.$uses({ Out: d.struct({ prop: d.u32 }).$name('myOut') }); + + expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - vertexFn:vertexFn: Key 'Out' appears in externals while being reserved for internals. Please rename this external.] + `); + }); + + it("allows an 'Out' external in TGSL implemented functions", () => { + const Out = d.struct({ prop: d.u32 }); + const vertexFn = tgpu.vertexFn({ + out: { position: d.builtin.position }, + })(() => { + 'use gpu'; + const out = Out(); + return { position: d.vec4f() }; + }); + + expect(tgpu.resolve([vertexFn])).toMatchInlineSnapshot(` + "struct Out { + prop: u32, + } + + struct vertexFn_Output { + @builtin(position) position: vec4f, + } + + @vertex fn vertexFn() -> vertexFn_Output { + let out = Out(); + return vertexFn_Output(vec4f()); + }" + `); + }); + + it("throws when rawWgsl fn has an 'in' external", () => { + const vertexFn = tgpu.vertexFn({ + in: { vId: d.builtin.vertexIndex }, + out: { position: d.builtin.position }, + })`{ return d.vec4f(in); }`.$uses({ in: 1 }); + + expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - vertexFn:vertexFn: Key 'in' appears in externals while being reserved for internals. Please rename this external.] + `); + }); + + it("allows an 'in' external in TGSL implemented functions", () => { + const EXT = { in: 1 }; + const vertexFn = tgpu.vertexFn({ + out: { position: d.builtin.position }, + })(() => { + 'use gpu'; + const x = EXT.in; + return { position: d.vec4f() }; + }); + + expect(tgpu.resolve([vertexFn])).toMatchInlineSnapshot(` + "struct vertexFn_Output { + @builtin(position) position: vec4f, + } + + @vertex fn vertexFn() -> vertexFn_Output { + const x = 1; + return vertexFn_Output(vec4f()); + }" + `); + }); +}); From 052f36b39bd07ec7ef1a2389107db03a204925ba Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:13:52 +0200 Subject: [PATCH 13/15] Add more tests & fix fragment out external --- .../src/core/function/tgpuFragmentFn.ts | 4 +- packages/typegpu/tests/externals.test.ts | 51 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index 676ad2828b..7409cbdbb5 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -214,7 +214,9 @@ function createFragmentFn( if (entryInput.dataSchema && isNamable(entryInput.dataSchema)) { entryInput.dataSchema.$name(`${getName(this) ?? ''}_Input`); } - core.setExternals('out', { Out: outputType }); + if (typeof implementation === 'string') { + core.setExternals('out', { Out: outputType }); + } return ctx.withSlots([[shaderStageSlot, 'fragment']], () => core.resolve(ctx, [], shell.returnType, entryInput), diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index 15d8afea65..8beb2abf4a 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -66,13 +66,22 @@ describe('external name collisions', () => { it("throws when rawWgsl fn has an 'Out' external", () => { const vertexFn = tgpu.vertexFn({ out: { position: d.builtin.position }, - })`{ return Out(); }`.$uses({ Out: d.struct({ prop: d.u32 }).$name('myOut') }); + })`{ return Out(); }`.$uses({ Out: d.struct({ prop: d.u32 }) }); + const fragmentFn = tgpu.fragmentFn({ + out: { color: d.location(0, d.vec4f) }, + })`{ return Out(); }`.$uses({ Out: d.struct({ prop: d.u32 }) }); expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - vertexFn:vertexFn: Key 'Out' appears in externals while being reserved for internals. Please rename this external.] `); + + expect(() => tgpu.resolve([fragmentFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fragmentFn:fragmentFn: Key 'Out' appears in externals while being reserved for internals. Please rename this external.] + `); }); it("allows an 'Out' external in TGSL implemented functions", () => { @@ -84,6 +93,13 @@ describe('external name collisions', () => { const out = Out(); return { position: d.vec4f() }; }); + const fragmentFn = tgpu.fragmentFn({ + out: d.vec4f, + })(() => { + 'use gpu'; + const out = Out(); + return d.vec4f(); + }); expect(tgpu.resolve([vertexFn])).toMatchInlineSnapshot(` "struct Out { @@ -99,6 +115,17 @@ describe('external name collisions', () => { return vertexFn_Output(vec4f()); }" `); + + expect(tgpu.resolve([fragmentFn])).toMatchInlineSnapshot(` + "struct Out { + prop: u32, + } + + @fragment fn fragmentFn() -> @location(0) vec4f { + let out = Out(); + return vec4f(); + }" + `); }); it("throws when rawWgsl fn has an 'in' external", () => { @@ -106,12 +133,21 @@ describe('external name collisions', () => { in: { vId: d.builtin.vertexIndex }, out: { position: d.builtin.position }, })`{ return d.vec4f(in); }`.$uses({ in: 1 }); + const fragmentFn = tgpu.fragmentFn({ + in: { uv: d.vec2f }, + out: d.vec4f, + })`{ return d.vec4f(in); }`.$uses({ in: 1 }); expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - vertexFn:vertexFn: Key 'in' appears in externals while being reserved for internals. Please rename this external.] `); + expect(() => tgpu.resolve([fragmentFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fragmentFn:fragmentFn: Key 'in' appears in externals while being reserved for internals. Please rename this external.] + `); }); it("allows an 'in' external in TGSL implemented functions", () => { @@ -123,6 +159,13 @@ describe('external name collisions', () => { const x = EXT.in; return { position: d.vec4f() }; }); + const fragmentFn = tgpu.fragmentFn({ + out: d.vec4f, + })(() => { + 'use gpu'; + const x = EXT.in; + return d.vec4f(); + }); expect(tgpu.resolve([vertexFn])).toMatchInlineSnapshot(` "struct vertexFn_Output { @@ -134,5 +177,11 @@ describe('external name collisions', () => { return vertexFn_Output(vec4f()); }" `); + expect(tgpu.resolve([fragmentFn])).toMatchInlineSnapshot(` + "@fragment fn fragmentFn() -> @location(0) vec4f { + const x = 1; + return vec4f(); + }" + `); }); }); From 5ec7052d1a8874c58f6c0b553128578058d3359b Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:28:11 +0200 Subject: [PATCH 14/15] More tests && allow redundant external --- .../typegpu/src/core/resolve/externals.ts | 4 +- packages/typegpu/tests/externals.test.ts | 64 ++++++++++++++++--- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index a1accaacdb..882ff83d84 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -24,9 +24,9 @@ export function mergeFunctionExternals(fnExternals: FnExternals): ExternalMap { const result: ExternalMap = Object.defineProperties({}, Object.getOwnPropertyDescriptors(base)); for (const flatExternal of [fnExternals.args, fnExternals.out].filter((e) => e !== undefined)) { for (const [key, value] of Object.entries(flatExternal)) { - if (key in result) { + if (key in result && result[key] !== value) { throw new Error( - `Key '${key}' appears in externals while being reserved for internals. Please rename this external.`, + `Key '${key}' appears in externals despite already being used for argument/return type. Please rename this external.`, ); } result[key] = value; diff --git a/packages/typegpu/tests/externals.test.ts b/packages/typegpu/tests/externals.test.ts index 8beb2abf4a..2f0e273800 100644 --- a/packages/typegpu/tests/externals.test.ts +++ b/packages/typegpu/tests/externals.test.ts @@ -63,7 +63,7 @@ describe('addArgTypesToExternals', () => { }); describe('external name collisions', () => { - it("throws when rawWgsl fn has an 'Out' external", () => { + it("throws when rawWgsl entrypoint has an 'Out' external", () => { const vertexFn = tgpu.vertexFn({ out: { position: d.builtin.position }, })`{ return Out(); }`.$uses({ Out: d.struct({ prop: d.u32 }) }); @@ -74,17 +74,17 @@ describe('external name collisions', () => { expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - vertexFn:vertexFn: Key 'Out' appears in externals while being reserved for internals. Please rename this external.] + - vertexFn:vertexFn: Key 'Out' appears in externals despite already being used for argument/return type. Please rename this external.] `); expect(() => tgpu.resolve([fragmentFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fragmentFn:fragmentFn: Key 'Out' appears in externals while being reserved for internals. Please rename this external.] + - fragmentFn:fragmentFn: Key 'Out' appears in externals despite already being used for argument/return type. Please rename this external.] `); }); - it("allows an 'Out' external in TGSL implemented functions", () => { + it("allows an 'Out' external in TGSL implemented entrypoints", () => { const Out = d.struct({ prop: d.u32 }); const vertexFn = tgpu.vertexFn({ out: { position: d.builtin.position }, @@ -128,7 +128,7 @@ describe('external name collisions', () => { `); }); - it("throws when rawWgsl fn has an 'in' external", () => { + it("throws when rawWgsl entrypoint has an 'in' external", () => { const vertexFn = tgpu.vertexFn({ in: { vId: d.builtin.vertexIndex }, out: { position: d.builtin.position }, @@ -141,16 +141,16 @@ describe('external name collisions', () => { expect(() => tgpu.resolve([vertexFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - vertexFn:vertexFn: Key 'in' appears in externals while being reserved for internals. Please rename this external.] + - vertexFn:vertexFn: Key 'in' appears in externals despite already being used for argument/return type. Please rename this external.] `); expect(() => tgpu.resolve([fragmentFn])).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: - - - fragmentFn:fragmentFn: Key 'in' appears in externals while being reserved for internals. Please rename this external.] + - fragmentFn:fragmentFn: Key 'in' appears in externals despite already being used for argument/return type. Please rename this external.] `); }); - it("allows an 'in' external in TGSL implemented functions", () => { + it("allows an 'in' external in TGSL implemented entrypoints", () => { const EXT = { in: 1 }; const vertexFn = tgpu.vertexFn({ out: { position: d.builtin.position }, @@ -184,4 +184,52 @@ describe('external name collisions', () => { }" `); }); + + it('throws when rawWgsl fn has an external colliding with argument type', () => { + const Schema = d.struct({ p: d.u32 }); + const myFn = tgpu.fn([Schema])`(a: S) { let b = S(); }`.$uses({ S: 1 }); + + expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:myFn: Key 'S' appears in externals despite already being used for argument/return type. Please rename this external.] + `); + }); + + it('allows redundant external colliding with argument type', () => { + const Schema = d.struct({ p: d.u32 }); + const myFn = tgpu.fn([Schema])`(a: S) { let b = S(); }`.$uses({ S: Schema }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "struct Schema { + p: u32, + } + + fn myFn(a: Schema) { let b = Schema(); }" + `); + }); + + it('throws when rawWgsl fn has an external colliding with return type', () => { + const Schema = d.struct({ p: d.u32 }); + const myFn = tgpu.fn([], Schema)`() -> S { let a = S(); }`.$uses({ S: 1 }); + + expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn:myFn: Key 'S' appears in externals despite already being used for argument/return type. Please rename this external.] + `); + }); + + it('allows redundant external colliding with return type', () => { + const Schema = d.struct({ p: d.u32 }); + const myFn = tgpu.fn([], Schema)`() -> S { let a = S(); }`.$uses({ S: Schema }); + + expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(` + "struct Schema { + p: u32, + } + + fn myFn() -> Schema { let a = Schema(); }" + `); + }); }); From 54b8b27bc41ab9c63dd7e046af42ed97e8fc96ad Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:39:11 +0200 Subject: [PATCH 15/15] Update docs --- packages/typegpu/src/core/function/fnCore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/function/fnCore.ts b/packages/typegpu/src/core/function/fnCore.ts index 9988a549f7..5ad24a8ac1 100644 --- a/packages/typegpu/src/core/function/fnCore.ts +++ b/packages/typegpu/src/core/function/fnCore.ts @@ -31,7 +31,7 @@ export type FnExternals = { */ args?: ExternalMap; /** - * Function return type, for example `{ Out: ... }` in rawWgsl entrypoint functions. + * Function return type, for example `{ Out: ... }` in both rawWgsl entrypoint functions and `vertexFnShell(in, Out)`. * Must be flat (every value must be resolvable). */ out?: ExternalMap;