Repository navigation
Conversation
aa79f1e to
8aacd28
Compare
Shader generation runs during an app's first frame, on code the JS engine hasn't optimized yet, so every bit of unneeded work there delays the first frame. These changes remove work the generator did even when nothing goes wrong, without changing the generated WGSL. - **Reserved words:** each namespace copied ~500 reserved WGSL words into its own Set. `isIdentifierTaken` now checks the shared sets directly. - **Error messages:** `invariant` messages that stringify AST nodes (e.g. on every numeric literal) are passed as functions, so they're only built when the check fails. - **Externals:** each external is coerced to a snippet once per function instead of on every reference. Function externals aren't copied when there are no arguments or outputs to merge in. - **Conversion ranks:** cached per pair of schemas in a `WeakMap` (schemas are immutable once created; the invariant is documented next to the cache). - **Small helpers:** `stitch` uses plain loops instead of `filter`/`map`/`join`. The `var`/`let` placeholders in a function body are filled with one fixed regex instead of one built per function. Memoized items are matched against slot values without copying the slot map into an array (and the duplicated check became one method). - **Type guards and proxies:** the hottest guards (`isDecorated`, `isLooseDecorated`, `isPtr`) test the cheap `type` property first, and texture views reuse their value proxy. `tgpu.resolve` of the shaders of four docs examples in Node 22: median of interleaved runs in fresh processes, `main` and this branch built side by side. "First" is the first resolve in a process; "later" is the median of the following 29. | Example | First: main → this PR | Later: main → this PR | |---|---|---| | boids | 23.1 → 19.6 ms (−15%) | 2.72 → 2.02 ms (−26%) | | stable-fluid | 39.7 → 38.0 ms (−4%) | 11.18 → 9.54 ms (−15%) | | slime-mold-3d | 53.5 → 42.1 ms (−21%) | 12.78 → 10.80 ms (−15%) | | jelly-slider | 74.6 → 62.2 ms (−17%) | 21.50 → 16.54 ms (−23%) | 16 rounds per example, 30 for stable-fluid. Two identical builds measured the same way differ by ~1.5%. The generated WGSL is identical to `main`'s for all 22 shader modules. - `typegpu` test suite passes (192 files, 2771 tests), along with typecheck, oxlint and oxfmt.
8aacd28 to
43021ff
Compare
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This PR removes avoidable work from the shader-generation hot path without changing emitted WGSL. I traced each behavioral change and ran the affected suites locally: packages/typegpu (195 files / 2921 tests) and @typegpu/gl (134 tests) both pass.
- Reserved-word sets —
namespace.tsnow seedstakenGlobalIdentifiersempty;isIdentifierTakenchecksbannedTokens/builtins/templateEnumerantsdirectly. Those are the set's only readers (isIdentifierTakenand thereserveIdentifierwrite), so identifier allocation is unchanged. - External snippet caching —
FunctionScopeLayer.externalSnippetscachescoerceToSnippet(external)once per id. The layer is created fresh inpushFunctionScope, and block scopes are still scanned first, so shadowing is preserved.mergeFunctionExternalsnow returnsbasedirectly whenargs/outare both undefined; no consumer mutatesexternalMap(bothgetSnippetByIdandreplaceExternalsInWgslonly read it), and the invariant is documented on the early return. - Conversion-rank cache —
autoRankCache/implicitRankCacheare keyed by(src, dest)perallowImplicit; every consumer (findBestType,applyActionToSnippet) treats the returned info as read-only, as the JSDoc requires. - Hot-loop rewrites —
stitchloops, the fixed/#VAR_\d+#/gplaceholder regex (which matches the#VAR_${size}#placeholders emitted at the twoplaceholderForVariable.setsites), and the extracted#slotsHaveValuesare all equivalent to the code they replace. - Guard reordering —
isLooseDecorated/isPtr/isDecoratedtest.typebeforeisMarkedInternal; both are pure, so the outcome is unchanged for every input. - Texture value-proxy reuse — caching
#valueProxyper instance only changes proxy identity; naming and resolution stay dynamic (getName(this)/ctx.resolve(this)at resolve time).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
🟡 Changes recommended
External snippet caching can freeze getter-backed values and change generated shader constants.
1 open issue
What changed in this PR
This PR reduces shader-generation overhead to improve first-frame latency, aiming to preserve generated WGSL.
Changes:
- Caches conversion ranks, external snippets, and texture proxies.
- Avoids reserved-word copies and eagerly formatted diagnostics.
- Streamlines stitching, placeholder replacement, and memoization checks.
| File | Description |
|---|---|
| packages/typegpu/src/types.ts | Adds per-function external snippet storage. |
| packages/typegpu/src/tgsl/wgslGenerator.ts | Defers diagnostics and simplifies placeholder replacement. |
| packages/typegpu/src/tgsl/conversion.ts | Caches conversion ranks by schema pair. |
| packages/typegpu/src/resolutionCtx.ts | Caches externals and streamlines identifier and slot checks. |
| packages/typegpu/src/data/wgslTypes.ts | Reorders hot type-guard checks. |
| packages/typegpu/src/data/dataTypes.ts | Reorders the loose-decoration guard. |
| packages/typegpu/src/core/texture/texture.ts | Reuses texture-view proxies. |
| packages/typegpu/src/core/resolve/stitch.ts | Replaces array pipelines with loops. |
| packages/typegpu/src/core/resolve/namespace.ts | Avoids copying reserved identifiers. |
| packages/typegpu/src/core/resolve/externals.ts | Skips unnecessary external-map copies. |
Review effort: Balanced
| const cached = layer.externalSnippets.get(id); | ||
| if (cached) { | ||
| return cached; | ||
| } |
|
Moved to #3240 (branch now lives on the main repo instead of the fork). |


Shader generation runs during an app's first frame, on code the JS engine hasn't optimized yet, so every bit of unneeded work there delays the first frame. These changes remove work the generator did even when nothing goes wrong, without changing the generated WGSL.
isIdentifierTakennow checks the shared sets directly.invariantmessages that stringify AST nodes (e.g. on every numeric literal) are passed as functions, so they're only built when the check fails.WeakMap(schemas are immutable once created; the invariant is documented next to the cache).stitchuses plain loops instead offilter/map/join. Thevar/letplaceholders in a function body are filled with one fixed regex instead of one built per function. Memoized items are matched against slot values without copying the slot map into an array (and the duplicated check became one method).isDecorated,isLooseDecorated,isPtr) test the cheaptypeproperty first, and texture views reuse their value proxy.tgpu.resolveof the shaders of four docs examples in Node 22: median of interleaved runs in fresh processes,mainand this branch built side by side. "First" is the first resolve in a process; "later" is the median of the following 29.16 rounds per example, 30 for stable-fluid. Two identical builds measured the same way differ by ~1.5%. The generated WGSL is identical to
main's for all 22 shader modules.