chore: Refactor GPU function call mechanism - #2919
Conversation
ae39d1b to
08937bf
Compare
Resolution Time Benchmark---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Random Branching (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.81, 1.66, 3.78, 6.06, 7.32, 10.26, 22.18, 25.58]
line [0.92, 1.94, 4.08, 6.22, 7.55, 11.47, 23.02, 25.44]
line [0.97, 1.88, 4.12, 5.63, 7.01, 10.66, 22.26, 24.62]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Linear Recursion (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.25, 0.44, 0.63, 0.74, 1.01, 1.00, 1.20, 1.36]
line [0.29, 0.50, 0.70, 0.85, 1.08, 1.16, 1.38, 1.51]
line [0.29, 0.53, 0.71, 0.85, 1.14, 1.20, 1.40, 1.56]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Full Tree (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.95, 2.17, 3.22, 7.83, 12.50, 25.45, 54.65, 107.45]
line [0.77, 2.11, 4.32, 7.14, 12.26, 25.43, 53.99, 110.55]
line [0.77, 2.18, 3.27, 7.60, 12.09, 25.07, 54.29, 109.96]
|
08937bf to
2c95c24
Compare
|
pkg.pr.new packages benchmark commit |
Bundle size comparison (
|
| 🟢 Decreased (max -0.02%) | ➖ Unchanged | 🔴 Increased (max 269.26%) | ❔ Unknown |
|---|---|---|---|
| 21 | 2 | 302 | 0 |
import * as ... in PR vs import * as ... in target (did bundle size increase?):
Click to reveal the results table (103 entries).
| Test | tsdown |
|---|---|
| d_struct.ts | 13.69 kB ( |
| d_Void.ts | 810 B ( |
| d_sampler.ts | 818 B ( |
| d_isPtr.ts | 828 B ( |
| d_comparisonSampler.ts | 829 B ( |
| d_isSizeAttrib.ts | 830 B ( |
| d_isWgslArray.ts | 830 B ( |
| d_isAlignAttrib.ts | 831 B ( |
| d_isAtomic.ts | 831 B ( |
| d_isWgslStruct.ts | 831 B ( |
| d_isBuiltinAttrib.ts | 833 B ( |
| d_isDecorated.ts | 834 B ( |
| d_isLocationAttrib.ts | 834 B ( |
| d_isInvariantAttrib.ts | 835 B ( |
| d_isInterpolateAttrib.ts | 837 B ( |
| d_atomic.ts | 855 B ( |
| d_ptrHandle.ts | 927 B ( |
| d_ptrUniform.ts | 928 B ( |
| d_ptrStorage.ts | 932 B ( |
| d_ptrPrivate.ts | 934 B ( |
| d_ptrFn.ts | 935 B ( |
| d_ptrWorkgroup.ts | 936 B ( |
| d_textureExternal.ts | 949 B ( |
| d_textureStorage1d.ts | 1.09 kB ( |
| d_textureStorage2d.ts | 1.09 kB ( |
| d_textureStorage3d.ts | 1.09 kB ( |
| d_textureStorage2dArray.ts | 1.10 kB ( |
| d_isDisarray.ts | 1.19 kB ( |
| d_isUnstruct.ts | 1.19 kB ( |
| d_isLooseDecorated.ts | 1.19 kB ( |
| d_isLooseData.ts | 1.23 kB ( |
| d_isWgslData.ts | 1.38 kB ( |
| d_arrayOf.ts | 28.95 kB ( |
| d_builtin.ts | 31.41 kB ( |
| d_unstruct.ts | 1.70 kB ( |
| common_fullScreenTriangle.ts | 43.55 kB ( |
| d_isData.ts | 1.88 kB ( |
| d_deepEqual.ts | 2.27 kB ( |
| STATIC_common.ts | 60.31 kB ( |
| STATIC_d.ts | 78.97 kB ( |
| d_ref.ts | 5.04 kB ( |
| std_dot.ts | 56.63 kB ( |
| std_length.ts | 56.63 kB ( |
| std_distance.ts | 56.63 kB ( |
| std_dot4U8Packed.ts | 56.63 kB ( |
| std_fract.ts | 56.63 kB ( |
| std_frexp.ts | 56.63 kB ( |
| std_mix.ts | 56.63 kB ( |
| std_modf.ts | 56.63 kB ( |
| std_abs.ts | 56.64 kB ( |
| std_acos.ts | 56.64 kB ( |
| std_acosh.ts | 56.64 kB ( |
| std_asin.ts | 56.64 kB ( |
| std_asinh.ts | 56.64 kB ( |
| std_atan.ts | 56.64 kB ( |
| std_atan2.ts | 56.64 kB ( |
| std_atanh.ts | 56.64 kB ( |
| std_ceil.ts | 56.64 kB ( |
| std_clamp.ts | 56.64 kB ( |
| std_cos.ts | 56.64 kB ( |
| std_cosh.ts | 56.64 kB ( |
| std_countLeadingZeros.ts | 56.64 kB ( |
| std_countOneBits.ts | 56.64 kB ( |
| std_countTrailingZeros.ts | 56.64 kB ( |
| std_cross.ts | 56.64 kB ( |
| std_degrees.ts | 56.64 kB ( |
| std_determinant.ts | 56.64 kB ( |
| std_dot4I8Packed.ts | 56.64 kB ( |
| std_exp.ts | 56.64 kB ( |
| std_exp2.ts | 56.64 kB ( |
| std_extractBits.ts | 56.64 kB ( |
| std_faceForward.ts | 56.64 kB ( |
| std_firstLeadingBit.ts | 56.64 kB ( |
| std_firstTrailingBit.ts | 56.64 kB ( |
| std_floor.ts | 56.64 kB ( |
| std_fma.ts | 56.64 kB ( |
| std_insertBits.ts | 56.64 kB ( |
| std_intdiv.ts | 56.64 kB ( |
| std_inverseSqrt.ts | 56.64 kB ( |
| std_ldexp.ts | 56.64 kB ( |
| std_log.ts | 56.64 kB ( |
| std_log2.ts | 56.64 kB ( |
| std_max.ts | 56.64 kB ( |
| std_min.ts | 56.64 kB ( |
| std_normalize.ts | 56.64 kB ( |
| std_pow.ts | 56.64 kB ( |
| std_quantizeToF16.ts | 56.64 kB ( |
| std_radians.ts | 56.64 kB ( |
| std_reflect.ts | 56.64 kB ( |
| std_refract.ts | 56.64 kB ( |
| std_reverseBits.ts | 56.64 kB ( |
| std_round.ts | 56.64 kB ( |
| std_saturate.ts | 56.64 kB ( |
| std_sign.ts | 56.64 kB ( |
| std_sin.ts | 56.64 kB ( |
| std_sinh.ts | 56.64 kB ( |
| std_smoothstep.ts | 56.64 kB ( |
| std_sqrt.ts | 56.64 kB ( |
| std_step.ts | 56.64 kB ( |
| std_tan.ts | 56.64 kB ( |
| std_tanh.ts | 56.64 kB ( |
| std_transpose.ts | 56.64 kB ( |
| std_trunc.ts | 56.64 kB ( |
import { ... } in PR vs import * as ... in PR (is the library tree-Shakeable?):
| Test | tsdown |
|---|---|
| tgpu_init.ts | 263.07 kB ( |
| tgpu_initFromDevice.ts | 262.53 kB ( |
| tgpu_resolve.ts | 161.72 kB ( |
| tgpu_resolveWithContext.ts | 161.66 kB ( |
| tgpu_bindGroupLayout.ts | 62.70 kB ( |
| tgpu_mutableAccessor.ts | 57.41 kB ( |
| tgpu_accessor.ts | 57.41 kB ( |
| tgpu_privateVar.ts | 56.11 kB ( |
| tgpu_workgroupVar.ts | 56.10 kB ( |
| tgpu_const.ts | 55.52 kB ( |
| tgpu_lazy.ts | 55.32 kB ( |
| tgpu_fragmentFn.ts | 39.98 kB ( |
| tgpu_vertexFn.ts | 39.79 kB ( |
| tgpu_fn.ts | 39.71 kB ( |
| tgpu_computeFn.ts | 39.50 kB ( |
| tgpu_vertexLayout.ts | 28.33 kB ( |
| tgpu_comptime.ts | 16.01 kB ( |
| tgpu_unroll.ts | 1.79 kB ( |
| tgpu_slot.ts | 1.75 kB ( |
If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu.
There was a problem hiding this comment.
Pull request overview
Refactors TypeGPU’s GPU-callable invocation model by moving call behavior onto schemas/functions themselves and separating strict signature metadata from the call implementation, with WGSL generation updated to route all calls through a unified ShaderGenerator.call(...) path.
Changes:
- Replace the
$gpuCallableobject-with-.call(...)pattern with a direct callable symbol function and introduce$gpuCallableStrictSignaturefor strict signature metadata. - Centralize call handling in
WgslGenerator.call(...), including strict-signature-based argument conversion and improved argument count diagnostics. - Update schemas (struct/array) and standard library helpers to implement the new calling convention; adjust tests to match updated error messages and typing behavior.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/typegpu/tests/tgsl/wgslGenerator.test.ts | Updates inline snapshot for new strict-signature argument count error message. |
| packages/typegpu/tests/struct.test.ts | Updates snapshot message and removes non-null assertions to match new call/type behavior. |
| packages/typegpu/tests/bindGroupLayout.test.ts | Removes non-null assertion in schema call usage. |
| packages/typegpu/tests/array.test.ts | Updates inline snapshot for new strict-signature argument count error message. |
| packages/typegpu/src/types.ts | Introduces GPUCallableStrictSignature, reshapes GPUCallable, and tightens generateLog args type to readonly. |
| packages/typegpu/src/tgsl/wgslGenerator.ts | Adds centralized call(...) method and refactors call-expression generation to use it. |
| packages/typegpu/src/tgsl/shaderGenerator.ts | Adds call(callee, args) to the ShaderGenerator interface. |
| packages/typegpu/src/tgsl/forOfUtils.ts | Updates arrayLength GPU-callable invocation to the new $gpuCallable(ctx, args) form. |
| packages/typegpu/src/std/numeric.ts | Adjusts variadic helper typing to accept readonly tuples. |
| packages/typegpu/src/std/matrix.ts | Updates std matrix wrappers to bind the new $gpuCallable function directly. |
| packages/typegpu/src/std/environment.ts | Converts $gpuCallable implementation to a direct function. |
| packages/typegpu/src/shared/symbols.ts | Adds $gpuCallableStrictSignature symbol. |
| packages/typegpu/src/data/wgslTypes.ts | Refactors WgslStruct/WgslArray types to extend DualFn for unified call typing. |
| packages/typegpu/src/data/vertexFormatData.ts | Converts $gpuCallable implementation to a direct function. |
| packages/typegpu/src/data/struct.ts | Moves struct schema call behavior into $gpuCallable and provides strict signature metadata. |
| packages/typegpu/src/data/snippet.ts | Makes MapValueToSnippet<T> readonly to better align with readonly argument flows. |
| packages/typegpu/src/data/schemaCallWrapper.ts | Updates schema GPU-call wrapper to use $gpuCallable(ctx, args) form. |
| packages/typegpu/src/data/ref.ts | Converts _ref GPU-callable implementation to a direct function. |
| packages/typegpu/src/data/array.ts | Moves array schema call behavior into $gpuCallable and provides strict signature metadata. |
| packages/typegpu/src/core/unroll/tgpuUnroll.ts | Converts unroll GPU-callable implementation to a direct function. |
| packages/typegpu/src/core/slot/accessor.ts | Updates GPU-callable accessors to use $gpuCallable(ctx, []). |
| packages/typegpu/src/core/function/dualImpl.ts | Splits strict signature metadata onto $gpuCallableStrictSignature and updates $gpuCallable to a direct function. |
| packages/typegpu/src/core/function/createCallableSchema.ts | Converts callable schema GPU-callable implementation to a direct function. |
| packages/typegpu/src/core/function/comptime.ts | Moves comptime GPU-callable behavior onto a shared prototype and updates internal metadata shape. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| pushBlockScope(): void; | ||
| popBlockScope(): void; | ||
| generateLog(op: SupportedLogOp, args: Snippet[]): Snippet; | ||
| generateLog(op: SupportedLogOp, args: readonly Snippet[]): Snippet; |
| /** | ||
| * Can be called on the GPU | ||
| */ | ||
| export const $gpuCallableStrictSignature = Symbol(`typegpu:${version}:$gpuCallableStrictSignature`); |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — single commit 2c95c245 ("chore: Refactor GPU function call mechanism"): struct and array schemas now own their call semantics through $gpuCallable, the $gpuCallable symbol changed shape (callable (ctx, args) => Snippet plus a separate $gpuCallableStrictSignature), and argument-snippet generation was split out of call resolution into a new WgslGenerator.call(callee, args) funnel that _expression and generators can share.
Verified end-to-end: the full packages/typegpu suite (176 files / 2362 tests) and @typegpu/gl (58 tests) pass, and test:types is green on both packages. The struct/array codegen that moved out of _expression into the schema impls maps onto typeInstantiation with identical output for the WGSL generator and the GLSL GlslGenerator override (verified by tracing the array/struct paths through typeInstantiation), so downstream behavior is unchanged. The rewritten over-arity error messages have their snapshots updated in all four touched tests. No leftover consumers of the old object-shaped $gpuCallable anywhere, and no other package implements the symbol.
ℹ️ Redundant second conversion of strict-signature arguments
On AST-backed calls to a strict-signature callable, argument snippets are converted twice: once in the _expression branch via _typedExpression(arg, argType) (which also sets ctx.expectedType, needed for literal typing) and again inside WgslGenerator.call at the isGPUCallable/strict-signature branch. The second pass is a type-preserving no-op (tryConvertSnippet short-circuits on target === dataType), and the schema impls re-run tryConvertSnippet a third time on the already-typed value, so the behavior is correct — just redundant. This includes the pre-existing arrangement, so it is not a regression, but since call() is now the documented funnel for generators, it is worth deciding where conversion is owned once.
Technical details
# Strict-signature argument conversion happens three times
## Affected sites
- packages/typegpu/src/tgsl/wgslGenerator.ts:819-836 (_expression strict-signature branch) — converts via `_typedExpression`
- packages/typegpu/src/tgsl/wgslGenerator.ts:461-471 (call()'s strict-signature branch) — converts via `tryConvertSnippet`
- packages/typegpu/src/data/array.ts:137 and packages/typegpu/src/data/struct.ts:100 — schema impls convert a third time
## Required outcome
- No behavior change; evaluate whether the second and third conversions can be removed without losing the `expectedType` hint that `_typedExpression` provides.ℹ️ Naming: call sits ambiguously next to emitCall
The new ShaderGenerator.call(callee, args) (returns a Snippet) reads confusingly against the existing emitCall(name, templateParams, args) (returns a string that emits the resolved call text), which std codegenImpls and GlslGenerator override. Since ShaderGenerator is already marked unstable, a more distinct name for the invoke-and-resolve path (e.g. callGpu/resolveCall) would spare downstream generator authors a footgun.
Technical details
# Naming ambiguity: `call` vs `emitCall`
## Affected sites
- packages/typegpu/src/tgsl/shaderGenerator.ts:126 — `call(callee: AnyFn, args: readonly Snippet[]): Snippet`
- packages/typegpu/src/tgsl/shaderGenerator.ts:127 — `emitCall(name: string, templateParams: readonly Snippet[], args: readonly Snippet[]): string`
## Required outcome
- Decide on a name for `call` that does not collide conceptually with `emitCall`; renames only touch the unstable interface and the typegpu-gl generator, which inherits from WgslGenerator.Also nice touch: dropping the now-unnecessary ! non-null assertions in the two struct/bind-group tests under the new DualFn-based WgslArray typing.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Changes:
tinyestnodes is now a seperate step from actually calling a function. This allows other parts of the generator to call helper functions without needing to reach into the deep internals via symbols (helpful in GLSL generation as well as the future matrix std function rewrites)