You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We could have the plugin and runtime library detect whether it's being run in production or in development mode, and choose to minify in production by default. In other scenarios, devs can use the following APIs to control minification:
// vite.config.mjsimport{defineConfig}from'vite';importtypegpufrom'unplugin-typegpu/vite';exportdefaultdefineConfig({// letting the plugin know that it can optimize the code in a way that doesn't have// to preserve the original names.plugins: [typegpu({minify: true})],});
// Letting the shader generator know that it's supposed to minify things like whitespaceconstroot=awaittgpu.init({minify: true});constcode=tgpu.resolve([foo],{minify: true});
We could also inject some sort of config so that the root and tgpu.resolve APIs automatically conform to the setting used by the plugin, but then we wouldn't have the ability to minify some snippets, and not minify others.
Reasoning and implementation notes
During bundling, unplugin-typegpu currently emits the following (I highlighted the parts that have an effect on the names we use in the shaders):
After the bundler minifies the code for production, those name strings will stay the same, meaning they will still contribute to generating the same shader code as they did during development, with their proper names. This is great for debugging, but if minifying is used a form of obfuscation, then the original names would still be retrievable.
Renaming definitions
With minification turned on, each time we auto-name a definition (constant, variable, struct, function, ...), we can generate a random 1 character name for them. We don't have to worry about clashes, because our naming system will ensure unique names across the program.
External reference names do not directly contribute to what shader code gets generated, but they could be used to easily reverse-engineer original names of values from the outer scope. We rename these names with the same mechanism as we do for definitions.
Note how we don't rename the locals in the JS functions itself, only in the tinyest representation, as that's what going to be used to generate the shader. The JS function will be minified by the bundler, outside of our minification process.
The tinyest representation of source code doesn't encode any whitespace, so the whitespace seen in the resulting WGSL shader above is recreated by the shader generator (WgslGenerator by default). This means the simplest solution would be to omit whitespace based on a minify: boolean parameter accepted by tgpu.init() and tgpu.resolve() APIs.
With this setup, granularly opting out of renaming for specific definitions is very straight-forward and doesn't require any new APIs. We simply respect the names given explicitly by devs with .$name(...).
Note
This issue assumes that #2019 and #2414 are implemented, and builds upon them in certain sections
Let's consider the following code:
When calling
tgpu.resolve([getAlignment]), the following gets generates:We would like to have the option to generate the following instead (new lines added for clarity):
Proposed API
We could have the plugin and runtime library detect whether it's being run in production or in development mode, and choose to minify in production by default. In other scenarios, devs can use the following APIs to control minification:
We could also inject some sort of config so that the
rootandtgpu.resolveAPIs automatically conform to the setting used by the plugin, but then we wouldn't have the ability to minify some snippets, and not minify others.Reasoning and implementation notes
During bundling,
unplugin-typegpucurrently emits the following (I highlighted the parts that have an effect on the names we use in the shaders):const Boid = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(d.struct({ pos: d.vec3f, dir: d.vec3f + }), "Boid")); const boids = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))( root.createReadonly(d.arrayOf(Boid, 100)), + "boids" )); const getAlignment = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (() => { "use gpu"; const firstBoid = boids.$[0]; const alignment = std.dot(firstBoid.dir, std.normalize(d.vec3f(.1, .4, 1))); return alignment; }), { v: 2, + name: "getAlignment", + ast: {"params":[],"body":[0,[[13,"firstBoid",[8,"boids.$",[5,"0"]]],[13,"alignment",[6,"std.dot",[[7,"firstBoid","dir"],[6,"std.normalize",[[6,"d.vec3f",[[5,"0.1"],[5,"0.4"],[5,"1"]]]]]]]],[10,"alignment"]]]}, + ref: { + 'boids.$': () => boids.$, + 'std.dot': () => std.dot, + 'std.normalize': () => std.normalize, + 'd.vec3f': () => d.vec3f, + }, }) && $.f)({}));After the bundler minifies the code for production, those name strings will stay the same, meaning they will still contribute to generating the same shader code as they did during development, with their proper names. This is great for debugging, but if minifying is used a form of obfuscation, then the original names would still be retrievable.
Renaming definitions
With minification turned on, each time we auto-name a definition (constant, variable, struct, function, ...), we can generate a random 1 character name for them. We don't have to worry about clashes, because our naming system will ensure unique names across the program.
const Boid = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(d.struct({ pos: d.vec3f, dir: d.vec3f - }), "Boid")); + }), "z")); const boids = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))( root.createReadonly(d.arrayOf(Boid, 100)), - "boids" + "y" )); const getAlignment = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (() => { "use gpu"; const firstBoid = boids.$[0]; const alignment = std.dot(firstBoid.dir, std.normalize(d.vec3f(.1, .4, 1))); return alignment; }), { v: 2, - name: "getAlignment", + name: "x", ast: {"params":[],"body":[0,[[13,"firstBoid",[8,"boids.$",[5,"0"]]],[13,"alignment",[6,"std.dot",[[7,"firstBoid","dir"],[6,"std.normalize",[[6,"d.vec3f",[[5,"0.1"],[5,"0.4"],[5,"1"]]]]]]]],[10,"alignment"]]]}, ref: { 'boids.$': () => boids.$, 'std.dot': () => std.dot, 'std.normalize': () => std.normalize, 'd.vec3f': () => d.vec3f, }, }) && $.f)({}));Renaming external references
External reference names do not directly contribute to what shader code gets generated, but they could be used to easily reverse-engineer original names of values from the outer scope. We rename these names with the same mechanism as we do for definitions.
const getAlignment = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (() => { "use gpu"; const firstBoid = boids.$[0]; const alignment = std.dot(firstBoid.dir, std.normalize(d.vec3f(.1, .4, 1))); return alignment; }), { v: 2, name: "x", - ast: {"params":[],"body":[0,[[13,"firstBoid",[8,"boids.$",[5,"0"]]],[13,"alignment",[6,"std.dot",[[7,"firstBoid","dir"],[6,"std.normalize",[[6,"d.vec3f",[[5,"0.1"],[5,"0.4"],[5,"1"]]]]]]]],[10,"alignment"]]]}, + ast: {"params":[],"body":[0,[[13,"firstBoid",[8,"a" ,[5,"0"]]],[13,"alignment",[6,"b" ,[[7,"firstBoid","dir"],[6,"c" ,[[6,"d" ,[[5,"0.1"],[5,"0.4"],[5,"1"]]]]]]]],[10,"alignment"]]]}, ref: { - 'boids.$': () => boids.$, - 'std.dot': () => std.dot, - 'std.normalize': () => std.normalize, - 'd.vec3f': () => d.vec3f, + 'a': () => boids.$, + 'b': () => std.dot, + 'c': () => std.normalize, + 'd': () => d.vec3f, }, }) && $.f)({}));Renaming local definitions
Local definitions can be renamed with the same mechanism as all of the above.
const getAlignment = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (() => { "use gpu"; const firstBoid = boids.$[0]; const alignment = std.dot(firstBoid.dir, std.normalize(d.vec3f(.1, .4, 1))); return alignment; }), { v: 2, name: "x", - ast: {"params":[],"body":[0,[[13,"firstBoid",[8,"a",[5,"0"]]],[13,"alignment",[6,"b",[[7,"firstBoid","dir"],[6,"c",[[6,"d",[[5,"0.1"],[5,"0.4"],[5,"1"]]]]]]]],[10,"alignment"]]]}, + ast: {"params":[],"body":[0,[[13,"e" ,[8,"a",[5,"0"]]],[13,"f" ,[6,"b",[[7,"e" ,"dir"],[6,"c",[[6,"d",[[5,"0.1"],[5,"0.4"],[5,"1"]]]]]]]],[10,"f" ]]]}, ref: { 'a': () => boids.$, 'b': () => std.dot, 'c': () => std.normalize, 'd': () => d.vec3f, }, }) && $.f)({}));Note how we don't rename the locals in the JS functions itself, only in the tinyest representation, as that's what going to be used to generate the shader. The JS function will be minified by the bundler, outside of our minification process.
Resulting shader
Reducing whitespace
The tinyest representation of source code doesn't encode any whitespace, so the whitespace seen in the resulting WGSL shader above is recreated by the shader generator (
WgslGeneratorby default). This means the simplest solution would be to omit whitespace based on aminify: booleanparameter accepted bytgpu.init()andtgpu.resolve()APIs.Granularly opting out of renaming
With this setup, granularly opting out of renaming for specific definitions is very straight-forward and doesn't require any new APIs. We simply respect the names given explicitly by devs with
.$name(...).This is helpful when using TypeGPU with a framework that expects functions or other definitions to be named very specifically.