-
-
Notifications
You must be signed in to change notification settings - Fork 36
feat: tgpu.comptime, tgpu.rawCodeSnippet and this allowed in TypeGPU shader functions
#1917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c3c3953
a10f989
34212f4
f9bcd96
e07faa9
9ff9990
9d9c2fe
5f9a96f
ff5956a
3814b5f
3fe0be3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import type { DualFn } from '../../data/dualFn.ts'; | ||
| import type { MapValueToSnippet } from '../../data/snippet.ts'; | ||
| import { WgslTypeError } from '../../errors.ts'; | ||
| import { inCodegenMode } from '../../execMode.ts'; | ||
| import { setName, type TgpuNamable } from '../../shared/meta.ts'; | ||
| import { $getNameForward, $internal } from '../../shared/symbols.ts'; | ||
| import { coerceToSnippet } from '../../tgsl/generationHelpers.ts'; | ||
| import { isKnownAtComptime } from '../../types.ts'; | ||
|
|
||
| export type TgpuComptime<T extends (...args: never[]) => unknown> = | ||
| & DualFn<T> | ||
| & TgpuNamable | ||
| & { [$getNameForward]: unknown }; | ||
|
|
||
| /** | ||
| * Creates a version of `func` that can called safely in a TypeGPU function to | ||
| * precompute and inject a value into the final shader code. | ||
| * | ||
| * Note how the function passed into `comptime` doesn't have to be marked with | ||
| * 'use gpu'. That's because the function doesn't execute on the GPU, it gets | ||
| * executed before the shader code gets sent to the GPU. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const injectRand01 = tgpu['~unstable'] | ||
| * .comptime(() => Math.random()); | ||
| * | ||
| * const getColor = (diffuse: d.v3f) => { | ||
| * 'use gpu'; | ||
| * const albedo = hsvToRgb(injectRand01(), 1, 0.5); | ||
| * return albedo.mul(diffuse); | ||
| * }; | ||
| * ``` | ||
| */ | ||
| export function comptime<T extends (...args: never[]) => unknown>( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add tests for this... |
||
| func: T, | ||
| ): TgpuComptime<T> { | ||
| const gpuImpl = (...args: MapValueToSnippet<Parameters<T>>) => { | ||
| const argSnippets = args as MapValueToSnippet<Parameters<T>>; | ||
|
|
||
| if (!argSnippets.every((s) => isKnownAtComptime(s))) { | ||
| throw new WgslTypeError( | ||
| `Called comptime function with runtime-known values: ${ | ||
| argSnippets.filter((s) => !isKnownAtComptime(s)).map((s) => | ||
| `'${s.value}'` | ||
| ).join(', ') | ||
| }`, | ||
| ); | ||
| } | ||
|
|
||
| return coerceToSnippet(func(...argSnippets.map((s) => s.value) as never[])); | ||
| }; | ||
|
|
||
| const impl = ((...args: Parameters<T>) => { | ||
| if (inCodegenMode()) { | ||
| return gpuImpl(...args as MapValueToSnippet<Parameters<T>>); | ||
| } | ||
| return func(...args); | ||
| }) as TgpuComptime<T>; | ||
|
|
||
| impl.toString = () => 'comptime'; | ||
| impl[$getNameForward] = func; | ||
| impl.$name = (label: string) => { | ||
| setName(func, label); | ||
| return impl; | ||
| }; | ||
| Object.defineProperty(impl, $internal, { | ||
| value: { | ||
| jsImpl: func, | ||
| gpuImpl, | ||
| argConversionHint: 'keep', | ||
| }, | ||
| }); | ||
|
|
||
| return impl as TgpuComptime<T>; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -142,9 +142,13 @@ export function createFnCore( | |||||||||
| // get data generated by the plugin | ||||||||||
| const pluginData = getMetaData(implementation); | ||||||||||
|
|
||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. smol
Suggested change
|
||||||||||
| if (pluginData?.externals) { | ||||||||||
| const pluginExternals = typeof pluginData?.externals === 'function' | ||||||||||
| ? pluginData.externals() | ||||||||||
| : pluginData?.externals; | ||||||||||
|
|
||||||||||
| if (pluginExternals) { | ||||||||||
| const missing = Object.fromEntries( | ||||||||||
| Object.entries(pluginData.externals).filter( | ||||||||||
| Object.entries(pluginExternals).filter( | ||||||||||
| ([name]) => !(name in externalMap), | ||||||||||
| ), | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| import type { AnyData } from '../../data/dataTypes.ts'; | ||
| import { type Origin, type ResolvedSnippet, snip } from '../../data/snippet.ts'; | ||
| 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 { | ||
| applyExternals, | ||
| type ExternalMap, | ||
| replaceExternalsInWgsl, | ||
| } from '../resolve/externals.ts'; | ||
| import { valueProxyHandler } from '../valueProxyUtils.ts'; | ||
|
|
||
| // ---------- | ||
| // Public API | ||
| // ---------- | ||
|
|
||
| /** | ||
| * Extra declaration that shall be included in final WGSL code, | ||
| * when resolving objects that use it. | ||
| */ | ||
| export interface TgpuRawCodeSnippet<TDataType extends AnyData> { | ||
| $: InferGPU<TDataType>; | ||
| value: InferGPU<TDataType>; | ||
|
|
||
| $uses(dependencyMap: Record<string, unknown>): this; | ||
| } | ||
|
|
||
| // The origin 'function' refers to values passed in from the calling scope, which means | ||
| // we would have access to this value anyway. Same goes for 'argument' and 'this-function', | ||
| // the values literally exist in the function we're writing. | ||
| // | ||
| // 'constant-ref' was excluded because it's a special origin reserved for tgpu.const values. | ||
| export type RawCodeSnippetOrigin = Exclude< | ||
| Origin, | ||
| 'function' | 'this-function' | 'argument' | 'constant-ref' | ||
| >; | ||
|
|
||
| /** | ||
| * An advanced API that creates a typed shader expression which | ||
| * can be injected into the final shader bundle upon use. | ||
| * | ||
| * @param expression The code snippet that will be injected in place of `foo.$` | ||
| * @param type The type of the expression | ||
| * @param [origin='runtime'] Where the value originates from. | ||
| * | ||
| * **-- Which origin to choose?** | ||
| * | ||
| * Usually 'runtime' (the default) is a safe bet, but if you're sure that the expression or | ||
| * computation is constant (either a reference to a constant, a numeric literal, | ||
| * or an operation on constants), then pass 'constant' as it might lead to better | ||
| * optimisations. | ||
| * | ||
| * If what the expression is a direct reference to an existing value (e.g. a uniform, a | ||
| * storage binding, ...), then choose from 'uniform', 'mutable', 'readonly', 'workgroup', | ||
| * 'private' or 'handle' depending on the address space of the referred value. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // An identifier that we know will be in the | ||
| * // final shader bundle, but we cannot | ||
| * // refer to it in any other way. | ||
| * const existingGlobal = tgpu['~unstable'] | ||
| * .rawCodeSnippet('EXISTING_GLOBAL', d.f32, 'constant'); | ||
| * | ||
| * const foo = () => { | ||
| * 'use gpu'; | ||
| * return existingGlobal.$ * 2; | ||
| * }; | ||
| * | ||
| * const wgsl = tgpu.resolve([foo]); | ||
| * // fn foo() -> f32 { | ||
| * // return EXISTING_GLOBAL * 2; | ||
| * // } | ||
| * ``` | ||
| */ | ||
| export function rawCodeSnippet<TDataType extends AnyData>( | ||
| expression: string, | ||
| type: TDataType, | ||
| origin: RawCodeSnippetOrigin | undefined = 'runtime', | ||
| ): TgpuRawCodeSnippet<TDataType> { | ||
| return new TgpuRawCodeSnippetImpl(expression, type, origin); | ||
| } | ||
|
|
||
| // -------------- | ||
| // Implementation | ||
| // -------------- | ||
|
|
||
| class TgpuRawCodeSnippetImpl<TDataType extends AnyData> | ||
aleksanderkatan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| implements TgpuRawCodeSnippet<TDataType>, SelfResolvable { | ||
| readonly [$internal]: true; | ||
| readonly dataType: TDataType; | ||
| readonly origin: RawCodeSnippetOrigin; | ||
|
|
||
| #expression: string; | ||
| #externalsToApply: ExternalMap[]; | ||
|
|
||
| constructor( | ||
| expression: string, | ||
| type: TDataType, | ||
| origin: RawCodeSnippetOrigin, | ||
| ) { | ||
| this[$internal] = true; | ||
| this.dataType = type; | ||
| this.origin = origin; | ||
|
|
||
| this.#expression = expression; | ||
| this.#externalsToApply = []; | ||
| } | ||
|
|
||
| $uses(dependencyMap: Record<string, unknown>): this { | ||
| this.#externalsToApply.push(dependencyMap); | ||
| return this; | ||
| } | ||
|
|
||
| [$resolve](ctx: ResolutionCtx): ResolvedSnippet { | ||
| const externalMap: ExternalMap = {}; | ||
|
|
||
| for (const externals of this.#externalsToApply) { | ||
| applyExternals(externalMap, externals); | ||
| } | ||
|
|
||
| const replacedExpression = replaceExternalsInWgsl( | ||
| ctx, | ||
| externalMap, | ||
| this.#expression, | ||
| ); | ||
|
|
||
| return snip(replacedExpression, this.dataType, this.origin); | ||
| } | ||
|
|
||
| toString() { | ||
| return `raw(${String(this.dataType)}): "${this.#expression}"`; | ||
| } | ||
|
|
||
| get [$gpuValueOf](): InferGPU<TDataType> { | ||
| const dataType = this.dataType; | ||
| const origin = this.origin; | ||
|
|
||
| return new Proxy({ | ||
| [$internal]: true, | ||
| get [$ownSnippet]() { | ||
| return snip(this, dataType, origin); | ||
| }, | ||
| [$resolve]: (ctx) => ctx.resolve(this), | ||
| toString: () => `raw(${String(this.dataType)}): "${this.#expression}".$`, | ||
| }, valueProxyHandler) as InferGPU<TDataType>; | ||
| } | ||
|
|
||
| get $(): InferGPU<TDataType> { | ||
| if (!inCodegenMode()) { | ||
| throw new Error('Raw code snippets can only be used on the GPU.'); | ||
| } | ||
|
|
||
| return this[$gpuValueOf]; | ||
| } | ||
|
|
||
| get value(): InferGPU<TDataType> { | ||
| return this.$; | ||
| } | ||
|
Comment on lines
+163
to
+165
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we still plan to deprecate and remove the |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what exactly happens here, and whether this is relevant, but
thisis a reserved word and cannot be used to name variables, struct props, structs or functionsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true! Could be simplified to return just "this"