Skip to content

feat: Obfuscate variables in plugin - #2755

Open
aleksanderkatan wants to merge 37 commits into
mainfrom
feat/minify-variables-in-plugin
Open

feat: Obfuscate variables in plugin#2755
aleksanderkatan wants to merge 37 commits into
mainfrom
feat/minify-variables-in-plugin

Conversation

@aleksanderkatan

@aleksanderkatan aleksanderkatan commented Jul 23, 2026

Copy link
Copy Markdown
Member

Blocked by #2788

image

We obfuscate variables, externals and parameters. Struct props are left as-is (at least for now).

Testing:

  • going through all examples with minification enabled (apps/typegpu-docs/astro.config.mjs:56),
  • skimming the diffs in updated example snapshots with minification enabled (apps/typegpu-docs/vitest.config.mts:12),
  • seeing all failing tests in typegpu/tests with minification enabled (packages/typegpu/vitest.config.mts:11).

Further work: #2783, #2786.

Also, helpers like fullScreenTriangle will remain as is, they are implemented in raw wgsl.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

pkg.pr.new

packages
Ready to be installed by your favorite package manager ⬇️

https://pkg.pr.new/software-mansion/TypeGPU/eslint-plugin-typegpu@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/tgpu-gen@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/tinyest-for-wgsl@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/typegpu@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/cli@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/color@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/gl@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/radiance-cascades@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/react@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/sdf@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/three@9cf09750a28134e57b7ada3718af41746b505ea0
https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@9cf09750a28134e57b7ada3718af41746b505ea0

benchmark
view benchmark

commit
view commit

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

Bundle size comparison (import * as ... in PR vs import * as ... in target):

🟢 Decreased ➖ Unchanged 🔴 Increased (max 0.01%) ❔ Unknown
0 301 21 0

import { ... } in PR vs import * as ... in PR (is the library tree-Shakeable?):

Test tsdown
tgpu_init.ts 260.81 kB ($${\color{green}-3.2\%}$$)
tgpu_initFromDevice.ts 260.28 kB ($${\color{green}-3.4\%}$$)
tgpu_resolve.ts 167.28 kB ($${\color{green}-37.9\%}$$)
tgpu_resolveWithContext.ts 167.22 kB ($${\color{green}-38.0\%}$$)
tgpu_bindGroupLayout.ts 71.66 kB ($${\color{green}-73.4\%}$$)
tgpu_mutableAccessor.ts 68.05 kB ($${\color{green}-74.8\%}$$)
tgpu_accessor.ts 68.05 kB ($${\color{green}-74.8\%}$$)
tgpu_privateVar.ts 66.79 kB ($${\color{green}-75.2\%}$$)
tgpu_workgroupVar.ts 66.79 kB ($${\color{green}-75.2\%}$$)
tgpu_const.ts 66.27 kB ($${\color{green}-75.4\%}$$)
tgpu_lazy.ts 66.15 kB ($${\color{green}-75.5\%}$$)
tgpu_fn.ts 38.59 kB ($${\color{green}-85.7\%}$$)
tgpu_fragmentFn.ts 38.58 kB ($${\color{green}-85.7\%}$$)
tgpu_vertexFn.ts 38.40 kB ($${\color{green}-85.8\%}$$)
tgpu_computeFn.ts 38.10 kB ($${\color{green}-85.9\%}$$)
tgpu_vertexLayout.ts 27.22 kB ($${\color{green}-89.9\%}$$)
tgpu_comptime.ts 14.91 kB ($${\color{green}-94.5\%}$$)
tgpu_unroll.ts 1.66 kB ($${\color{green}-99.4\%}$$)
tgpu_slot.ts 1.54 kB ($${\color{green}-99.4\%}$$)

If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

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.82, 1.64, 3.48, 5.64, 5.78, 9.48, 18.41, 20.09]
  line [0.77, 1.66, 3.51, 5.78, 6.51, 10.66, 19.27, 19.23]
  line [0.85, 1.63, 3.66, 5.46, 6.23, 9.33, 17.98, 20.95]
Loading
---
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.29, 0.50, 0.64, 0.77, 1.00, 1.05, 1.18, 1.34]
  line [0.27, 0.46, 0.62, 0.76, 0.98, 1.03, 1.17, 1.34]
  line [0.31, 0.49, 0.65, 0.71, 0.93, 1.05, 1.27, 1.40]
Loading
---
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.76, 1.88, 3.52, 5.64, 10.42, 21.43, 46.99, 94.29]
  line [0.84, 1.88, 3.37, 5.80, 10.13, 22.76, 46.31, 93.92]
  line [0.93, 1.87, 3.84, 5.67, 10.92, 23.33, 49.45, 98.36]
Loading

declaredNames: string[];
};

export type Externals = Set<string>;

@aleksanderkatan aleksanderkatan Aug 3, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Less work swapping it for a map here than in unplugin, and it needs to become a map eventually

@aleksanderkatan aleksanderkatan changed the title feat: Minify variables in plugin feat: Obfuscate variables in plugin Aug 4, 2026
@aleksanderkatan

Copy link
Copy Markdown
Member Author

@pullfrog review

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The externalNames iteration in the obfuscator swaps the map's key and value. This works today only because tinyest-for-wgsl stores identical strings for both; it will silently produce incorrect WGSL if that invariant ever changes.

Reviewed changes — PR #2755 adds an experimental plugin-time obfuscation pass that renames parameters, local variables, and external bindings in the transpiled tinyest AST, plus the EXPERIMENTAL_obfuscate option across the Babel/Rollup/Bun plugin entry points, a new docs page, and supporting test updates.

  • Added packages/unplugin-typegpu/src/core/obfuscate.ts — AST-to-AST obfuscation of params, body, and externalNames.
  • Changed Externals from Set<string> to Map<string, string> in packages/tinyest-for-wgsl/src/types.ts and updated all consumers/test snapshots.
  • Wired EXPERIMENTAL_obfuscate into unplugin-typegpu plugin options, with a guard that rejects enabling it alongside autoNamingEnabled.
  • Added docs entry (apps/typegpu-docs/.../minifying-shaders.mdx) and sidebar link.
  • Updated/added tests in packages/tinyest-for-wgsl/tests/parsers.test.ts, packages/unplugin-typegpu/test/obfuscation.test.ts, and several typegpu snapshot tests.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

Comment thread packages/unplugin-typegpu/src/core/obfuscate.ts Outdated
@aleksanderkatan
aleksanderkatan changed the base branch from main to fix/name-collision August 6, 2026 08:37
@aleksanderkatan

Copy link
Copy Markdown
Member Author

@pullfrog review

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — minor suggestions inline.

Reviewed changes

This PR introduces an EXPERIMENTAL_obfuscate option for unplugin-typegpu that renames TGSL parameters, local variables, and external identifiers to short generated names before WGSL code generation. It also adds the supporting infrastructure: changing externalNames from a Set to a Map to carry original accessor chains, adding validation that prevents obfuscation and auto-naming from being enabled together, updating metadata serialization in the Babel and unplugin factories, and adding documentation and tests.

  • Added packages/unplugin-typegpu/src/core/obfuscate.ts with a tinyest-to-tinyest AST transformer that renames identifiers.
  • Added EXPERIMENTAL_obfuscate plugin option with a conflict check against autoNamingEnabled.
  • Changed Externals from Set<string> to Map<string, string> so obfuscated keys can map back to original accessor chains.
  • Updated packages/typegpu/src/resolutionCtx.ts to resolve undefined/Infinity/NaN externals correctly.
  • Added packages/unplugin-typegpu/test/obfuscation.test.ts covering AST-level obfuscation for variables, parameters, externals, destructuring, shadowing, and name exhaustion.
  • Added docs page for shader minification/obfuscation.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

Comment thread packages/unplugin-typegpu/src/core/obfuscate.ts Outdated
Comment thread packages/unplugin-typegpu/src/core/obfuscate.ts
import { bunPlugin, rollupPlugin } from '../src/index.ts';
import { defaultOptions } from '../src/core/common.ts';

describe('plugin obfuscation', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests cover the tinyest-to-tinyest transformation well, but there is no end-to-end coverage that runs tgpu.resolve on an obfuscated function and verifies the emitted WGSL. Since shadowing and reserved-word collisions are handled later by the resolver, at least one integration test would guard against regressions in that interaction.

@aleksanderkatan aleksanderkatan Aug 6, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to configure them in typegpu package but vitest won again, maintainers let me know if you think we need e2e tests.

Base automatically changed from fix/name-collision to main August 6, 2026 08:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental shader obfuscation/minification pathway by obfuscating identifiers captured in unplugin-typegpu metadata, supported by updates to the tinyest transpiler externals representation and expanded test/docs coverage.

Changes:

  • Introduces EXPERIMENTAL_obfuscate option to obfuscate parameters/locals/externals stored in plugin metadata.
  • Changes tinyest-for-wgsl “externals” from Set<string> to Map<string, string> to support renaming/aliasing of external chains.
  • Adds/updates tests and documentation for obfuscation behavior and collision/shadowing scenarios.

Reviewed changes

Copilot reviewed 15 out of 18 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/unplugin-typegpu/test/obfuscation.test.ts New plugin + core obfuscation test coverage and option-conflict checks.
packages/unplugin-typegpu/src/core/obfuscate.ts New obfuscation pass over tinyest AST + externals mapping.
packages/unplugin-typegpu/src/core/common.ts Plumbs EXPERIMENTAL_obfuscate through transpilation and adds option validation.
packages/unplugin-typegpu/src/core/factory.ts Updates externals serialization and validates options in unplugin factory.
packages/unplugin-typegpu/src/babel.ts Updates Babel plugin option validation and externals emission.
packages/unplugin-typegpu/src/bun.ts Validates options for Bun plugin entrypoint.
packages/typegpu/src/resolutionCtx.ts Adjusts external lookup to distinguish “missing” vs “present-but-undefined”.
packages/typegpu/src/internal.ts Exposes stringifyNode via internal exports for tests/tooling.
packages/typegpu/tests/tgslFn.test.ts Adds regression coverage for std shadowing behavior.
packages/typegpu/tests/tgsl/wgslGenerator.test.ts Snapshot formatting updates.
packages/typegpu/tests/std/boolean/not.test.ts Snapshot formatting updates.
packages/typegpu/tests/mutabilityTracking.test.ts Snapshot formatting updates.
packages/tinyest-for-wgsl/src/types.ts Switches Externals to Map<string, string>.
packages/tinyest-for-wgsl/src/parsers.ts Emits externals into a Map instead of a Set.
packages/tinyest-for-wgsl/tests/helpers.ts New shared helpers for dual-parser tests.
packages/tinyest-for-wgsl/tests/parsers.test.ts Updates expectations for externals now being a Map.
apps/typegpu-docs/src/content/docs/advanced/minifying-shaders.mdx New documentation page for obfuscation/minification.
apps/typegpu-docs/astro.config.mjs Adds docs nav entry for the new page.
Suppressed comments (1)

packages/tinyest-for-wgsl/src/types.ts:15

  • The Context.externalNames doc still says it “Holds a set…”, but Externals is now a Map<string, string>. Updating the comment will prevent confusion for future maintainers.
export type Context = {
  /** Holds a set of all identifiers that were used in code, but were not declared in code. */
  externalNames: Externals;
  /** Used to signal to identifiers that they should not treat their resolution as possible external uses. */

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +23 to 27
Array.from(externals, ([key, value]) => {
const chain = value.split('.');
if (!chain[0]) {
throw new Error('Internal error, expected chain to not be empty');
}
Comment on lines +79 to +80
const externalNames = new Map();
fn.externalNames.forEach((value, key) => externalNames.set(ctx.obfuscator.obfuscate(key), value));
Comment on lines +32 to +53
class Obfuscator {
#nameMap: Map<string, string> = new Map();
#nameGenerator: Generator<string> = nameGenerator();

#generateFreshName(): string {
return this.#nameGenerator.next().value;
}

/**
* If `name` wasn't obfuscated before, give it a new obfuscated name.
* Then, returns the obfuscated version of `name`.
*/
obfuscate(name: string): string {
let obfuscatedName = this.#nameMap.get(name);
if (!obfuscatedName) {
obfuscatedName = this.#generateFreshName();
this.#nameMap.set(name, obfuscatedName);
}

return obfuscatedName;
}
}
Comment on lines +16 to +17
To reduce both the shader size and readability, it is advised to disable the plugin auto-naming by setting `{ autoNamingEnabled: false }`.
This way, only resources given name via `.$name()` will be named in the resulting shader.
}

if (external !== undefined && external !== null) {
if (id in layer.externalMap) {

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The obfuscation name generator can emit WGSL keywords, which will produce invalid shader code once enough identifiers are present. See the inline comment on packages/unplugin-typegpu/src/core/obfuscate.ts.

Reviewed changes

  • Added an EXPERIMENTAL_obfuscate plugin option that renames parameters, variables, and captured externals to short lowercase identifiers at build time.
  • Changed tinyest-for-wgsl externalNames from Set<string> to Map<string, string> so obfuscated AST keys can still resolve to original external access chains at runtime.
  • Updated resolutionCtx.ts to resolve externals by map membership (id in externalMap) rather than by truthiness, preserving null/undefined external values.
  • Added stringifyNode to the internal TypeGPU exports for test assertions.
  • Added docs page and parser/unplugin tests.

ℹ️ Nitpicks

  • The options reference in apps/typegpu-docs/src/content/docs/tooling/unplugin-typegpu.mdx was not updated to list EXPERIMENTAL_obfuscate; consider adding it there (marked experimental) to keep the two docs pages consistent.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

/**
* Generates all strings consisting of lowercase letters.
*/
function* nameGenerator(): Generator<string> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generator emits a, b, …, z, aa, ab, … without skipping WGSL keywords or builtins. I verified locally that 220 variables produces the obfuscated name fn, which is invalid WGSL. Reuse the existing bannedTokens/builtins sets in packages/typegpu/src/nameUtils.ts here.

*/
obfuscate(name: string): string {
let obfuscatedName = this.#nameMap.get(name);
if (!obfuscatedName) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!obfuscatedName) will regenerate a name for the empty string. Since the generator never yields '', this is harmless, but obfuscatedName === undefined would express the intent more precisely.

## Plugin obfuscation

:::note
To reduce both the shader size and readability, it is advised to disable the plugin auto-naming by setting `{ autoNamingEnabled: false }`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a trailing space inside the inline-code backticks: { autoNamingEnabled: false }. . Remove the space before the closing backtick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants