From 5c202f331adbc17b04bf942469549779802b6610 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 14 May 2026 12:38:16 -0700 Subject: [PATCH 1/4] Baseline ccode for the shader builder for WebGPU --- .../core/src/webgpu/shaders/attributes.ts | 338 +++++++++++++++ .../core/src/webgpu/shaders/declarations.ts | 405 ++++++++++++++++++ packages/core/src/webgpu/shaders/index.ts | 80 ++++ packages/core/src/webgpu/shaders/shader.ts | 34 ++ 4 files changed, 857 insertions(+) create mode 100644 packages/core/src/webgpu/shaders/attributes.ts create mode 100644 packages/core/src/webgpu/shaders/declarations.ts create mode 100644 packages/core/src/webgpu/shaders/index.ts create mode 100644 packages/core/src/webgpu/shaders/shader.ts diff --git a/packages/core/src/webgpu/shaders/attributes.ts b/packages/core/src/webgpu/shaders/attributes.ts new file mode 100644 index 00000000..7900c7fe --- /dev/null +++ b/packages/core/src/webgpu/shaders/attributes.ts @@ -0,0 +1,338 @@ +/** + * This file defines TypeScript types and helper functions for representing WGSL shader attributes in a type-safe way. + * Each attribute is represented as an object with a specific shape, and includes a __gen method that generates the + * corresponding WGSL syntax for that attribute. The file also includes type guards for each attribute type, as well as + * constructors that validate input and create the attribute objects. + * + * Summary of Attributes: + * - align(x: i32 | u32 that is a power of 2 > 0) [can only be applied to a member of a struct] + * - binding(num >= 0) [can only be applied to a Resource variable] + * - blend_src(0 | 1) [only valid in specific feature-triggered scenarios; must be on a struct member with @location] + * - builtin(builtin-name) [only valid on a struct member, entrypoint argument, or entrypoint return type] + * - const [only allowed on non-user-defined functions; not relevant to our use case] + * - diagnostic(ShaderSeverityControlName, string) + * - group(num >= 0) [can only be applied to a Resource variable] + * - id(num >= 0) [can only be applied to an override variable with a scalar type] + * - interpolate(ShaderIntroplationType, ShaderInterpolationSamplingType?) [can only be applied to declarations with a @location attribute] + * - invariant [can only be applied to a @builtin(position) declaration; only has effect if applied to vertex position output] + * - location(num >= 0) [structure members or entrypoint inputs/outputs only; numeric scalar or vector declarations only; not allowed in compute shaders] + * - must_use [function declarations with return types only] + * - size(num >= 1) [only applicable to struct members with a size known at shader creation time] + * - workgroup_size(x: u32 >= 1, [y?: u32 >= 1, [z?: u32 >= 1]]) [only on compute shader entry points] + * + * Shader Stage indicator attributes: + * - vertex + * - fragment + * - compute + */ + +/// TYPES + +type ShaderSeverityControlName = 'error' | 'warning' | 'info' | 'off'; +const SHADER_SEVERITY_CONTROL_NAMES: ShaderSeverityControlName[] = ['error', 'warning', 'info', 'off']; + +type ShaderIntroplationType = 'perspective' | 'linear' | 'flat'; +const SHADER_INTERPOLATION_TYPES: ShaderIntroplationType[] = ['perspective', 'linear', 'flat']; + +type ShaderInterpolationSamplingType = 'center' | 'centroid' | 'sample' | 'first' | 'either'; +const SHADER_INTERPOLATION_SAMPLING_TYPES: ShaderInterpolationSamplingType[] = [ + 'center', + 'centroid', + 'sample', + 'first', + 'either', +]; + +type ShaderBuiltins = + | 'clip_distances' + | 'frag_depth' + | 'front_facing' + | 'global_invocation_id' + | 'global_invocation_index' + | 'instance_index' + | 'local_invocation_id' + | 'local_invocation_index' + | 'num_workgroups' + | 'position' + | 'primitive_index' + | 'sample_index' + | 'sample_mask' + | 'vertex_index' + | 'workgroup_id' + | 'workgroup_index' + | 'subgroup_invocation_id' + | 'subgroup_size' + | 'subgroup_id' + | 'num_subgroups'; + +const SHADER_BUILTINS: ShaderBuiltins[] = [ + 'clip_distances', + 'frag_depth', + 'front_facing', + 'global_invocation_id', + 'global_invocation_index', + 'instance_index', + 'local_invocation_id', + 'local_invocation_index', + 'num_workgroups', + 'position', + 'primitive_index', + 'sample_index', + 'sample_mask', + 'vertex_index', + 'workgroup_id', + 'workgroup_index', + 'subgroup_invocation_id', + 'subgroup_size', + 'subgroup_id', + 'num_subgroups', +]; + +export type DeclarationAttribute = { + __gen: () => string; +}; + +export type AlignAttribute = DeclarationAttribute & { + align: number; +}; + +/* + * NOTE: The `binding` attribute is intentionally omitted from the public API for now, + * as its usage is handled by setting the `binding` property on a Resource variable + * declaration. + **/ +// export type BindingAttribute = DeclarationAttribute & { +// binding: number; +// }; + +export type BlendSrcAttribute = DeclarationAttribute & { + blend_src: 0 | 1; +}; + +export type BuiltinAttribute = DeclarationAttribute & { + builtin: ShaderBuiltins; +}; + +export type ConstAttribute = DeclarationAttribute & { + const: true; +}; + +export type DiagnosticAttribute = DeclarationAttribute & { + diagnostic: [ShaderSeverityControlName, string]; +}; + +/* + * NOTE: The `group` attribute is intentionally omitted from the public API for now, + * as its usage is handled by setting the `group` property on a Resource variable + * declaration. + **/ +// export type GroupAttribute = DeclarationAttribute & { +// group: number; +// }; + +export type IdAttribute = DeclarationAttribute & { + id: number; +}; + +export type InterpolateAttribute = DeclarationAttribute & { + interpolate: [ShaderIntroplationType, ShaderInterpolationSamplingType?]; +}; + +export type InvariantAttribute = DeclarationAttribute & { + invariant: true; +}; + +export type LocationAttribute = DeclarationAttribute & { + location: number; +}; + +export type MustUseAttribute = DeclarationAttribute & { + must_use: true; +}; + +export type SizeAttribute = DeclarationAttribute & { + size: number; +}; + +export type WorkgroupSizeAttribute = DeclarationAttribute & { + workgroup_size: [number] | [number, number] | [number, number, number]; +}; + +export type VertexAttribute = DeclarationAttribute & { + vertex: true; +}; + +export type FragmentAttribute = DeclarationAttribute & { + fragment: true; +}; + +export type ComputeAttribute = DeclarationAttribute & { + compute: true; +}; + +export type VariableOrValueAttribute = + | AlignAttribute + | BlendSrcAttribute + | BuiltinAttribute + | DiagnosticAttribute + | IdAttribute + | InterpolateAttribute + | InvariantAttribute + | LocationAttribute + | SizeAttribute + | WorkgroupSizeAttribute; + +export type FunctionAttribute = + | ConstAttribute + | MustUseAttribute + | VertexAttribute + | FragmentAttribute + | ComputeAttribute; + +/// CONSTRUCTORS + +export function align(n: number): AlignAttribute { + if (n <= 0 || (n & (n - 1)) !== 0) { + throw new Error('Alignment must be a positive power of 2'); + } + return { align: n, __gen: () => `@align(${n})` }; +} + +/* + * NOTE: The `binding` attribute is intentionally omitted from the public API for now, + * as its usage is handled by setting the `binding` property on a Resource variable + * declaration. + **/ +// export function binding(n: number): BindingAttribute { +// if (n < 0) { +// throw new Error('Binding number must be a non-negative integer'); +// } +// return { binding: n, __gen: () => `@binding(${n})` }; +// } + +export function blendSrc(value: 0 | 1): BlendSrcAttribute { + if (value !== 0 && value !== 1) { + throw new Error('blend_src value must be either 0 or 1'); + } + return { blend_src: value, __gen: () => `@blend_src(${value})` }; +} + +export function builtin(name: ShaderBuiltins): BuiltinAttribute { + if (!SHADER_BUILTINS.includes(name)) { + throw new Error(`Invalid builtin name: ${name}`); + } + return { builtin: name, __gen: () => `@builtin(${name})` }; +} + +export function constAttr(): ConstAttribute { + return { const: true, __gen: () => `@const` }; +} + +export function diagnostic(severity: ShaderSeverityControlName, message: string): DiagnosticAttribute { + if (!SHADER_SEVERITY_CONTROL_NAMES.includes(severity)) { + throw new Error(`Invalid shader severity control name: ${severity}`); + } + if (typeof message !== 'string' || message.length === 0) { + throw new Error('Diagnostic message must be a non-empty string'); + } + return { diagnostic: [severity, message], __gen: () => `@diagnostic(${severity}, "${message}")` }; +} + +/* + * NOTE: The `group` attribute is intentionally omitted from the public API for now, + * as its usage is handled by setting the `group` property on a Resource variable + * declaration. + **/ +// export function group(n: number): GroupAttribute { +// if (n < 0) { +// throw new Error('Group number must be a non-negative integer'); +// } +// return { group: n, __gen: () => `@group(${n})` }; +// } + +export function id(n: number): IdAttribute { + if (n < 0) { + throw new Error('ID number must be a non-negative integer'); + } + return { id: n, __gen: () => `@id(${n})` }; +} + +export function interpolate( + type: ShaderIntroplationType, + samplingType?: ShaderInterpolationSamplingType +): InterpolateAttribute { + if (!SHADER_INTERPOLATION_TYPES.includes(type)) { + throw new Error(`Invalid interpolation type: ${type}`); + } + if (samplingType !== undefined && !SHADER_INTERPOLATION_SAMPLING_TYPES.includes(samplingType)) { + throw new Error(`Invalid interpolation sampling type: ${samplingType}`); + } + return { + interpolate: samplingType !== undefined ? [type, samplingType] : [type], + __gen: () => `@interpolate(${type}${samplingType !== undefined ? `, ${samplingType}` : ''})`, + }; +} + +export function invariant(): InvariantAttribute { + return { invariant: true, __gen: () => `@invariant` }; +} + +export function location(n: number): LocationAttribute { + if (n < 0) { + throw new Error('Location number must be a non-negative integer'); + } + return { location: n, __gen: () => `@location(${n})` }; +} + +export function mustUse(): MustUseAttribute { + return { must_use: true, __gen: () => `@must_use` }; +} + +export function size(n: number): SizeAttribute { + if (n <= 0) { + throw new Error('Size must be a positive number'); + } + return { size: n, __gen: () => `@size(${n})` }; +} + +export function workgroupSize( + ...sizes: [number] | [number, number] | [number, number, number] +): WorkgroupSizeAttribute { + if (sizes.length < 1 || sizes.length > 3) { + throw new Error('Workgroup size must have 1 to 3 dimensions'); + } + if (!sizes.every((n) => typeof n === 'number' && n > 0)) { + throw new Error('Workgroup size dimensions must be positive numbers'); + } + return { workgroup_size: sizes, __gen: () => `@workgroup_size(${sizes.join(', ')})` }; +} + +export function vertex(): VertexAttribute { + return { vertex: true, __gen: () => `@vertex` }; +} + +export function fragment(): FragmentAttribute { + return { fragment: true, __gen: () => `@fragment` }; +} + +export function compute(): ComputeAttribute { + return { compute: true, __gen: () => `@compute` }; +} + +export const constructors = { + align, + blendSrc, + builtin, + constant: constAttr, + diagnostic, + id, + interpolate, + invariant, + location, + mustUse, + size, + workgroupSize, + vertex, + fragment, + compute, +}; diff --git a/packages/core/src/webgpu/shaders/declarations.ts b/packages/core/src/webgpu/shaders/declarations.ts new file mode 100644 index 00000000..147da2cd --- /dev/null +++ b/packages/core/src/webgpu/shaders/declarations.ts @@ -0,0 +1,405 @@ +/** + * This file defines the various types of declarations that can be used in our shader generation system, + * including variables, constants, structs, and functions. Each declaration type includes a __gen method + * that generates the corresponding WGSL code for that declaration. + */ + +import { compute, fragment, vertex, type DeclarationAttribute, type FunctionAttribute, type VariableOrValueAttribute } from './attributes'; + +function renderAttrs(attrs: DeclarationAttribute[] | undefined): string { + return attrs && attrs.length > 0 ? attrs.map((attr) => `${attr.__gen()}`).join(' ') + ' ' : ''; +} + +/// TYPES + +export type DeclarationGenerator = { + __gen: () => string; +}; + +export type IdentifierDeclaration = { + readonly name: string; +}; + +export type ConstValueDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'value'; + readonly assignmentType: 'const'; + readonly type?: string; + readonly initializer: unknown; + }; + +export type OverrideValueDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'value'; + readonly assignmentType: 'override'; + readonly attributes?: VariableOrValueAttribute[]; + } & ( + | { + readonly type: string; + readonly initializer?: unknown; + } + | { + readonly type?: string; + readonly initializer: unknown; + } + ); + +// NOTE: skipping function-scoped vars because those are handled entirely within a function body and have no direct +// relationship to the resource interface of a shader, nor are they defined outside of function bodies so we don't +// need to include them for the sake of generating the shader itself +export type ValueDeclaration = ConstValueDeclaration | OverrideValueDeclaration; + +export type PrivateVariableDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'private'; + readonly type?: string; + readonly initializer?: unknown; + }; + +export type WorkgroupVariableDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'workgroup'; + readonly type: string; + }; + +// NOTE: currently, these "Resource Interface" declarations hard-code their group and binding, but +// at least in theory these could also be specified in the "attributes" array, which is not ideal. +// Need to revisit this at some point to reduce the duplication. +export type ResourceIdentifierDeclaration = { + group: number; + binding: number; +}; + +export type UniformVariableDeclaration = IdentifierDeclaration & + ResourceIdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'uniform'; + readonly type: string; + readonly attributes?: VariableOrValueAttribute[]; + }; + +export type TextureVariableDeclaration = IdentifierDeclaration & + ResourceIdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'texture'; + readonly type: `texture_${string}`; + readonly attributes?: VariableOrValueAttribute[]; + }; + +export type SamplerVariableDeclaration = IdentifierDeclaration & + ResourceIdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'sampler'; + readonly type: 'sampler' | 'sampler_comparison'; + readonly attributes?: VariableOrValueAttribute[]; + }; + +export type StorageVariableDeclaration = IdentifierDeclaration & + ResourceIdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'storage'; + readonly type: string; + readonly accessMode?: 'read' | 'write' | 'read_write'; + readonly attributes?: VariableOrValueAttribute[]; + }; + +export type ResourceDeclaration = + | UniformVariableDeclaration + | TextureVariableDeclaration + | SamplerVariableDeclaration + | StorageVariableDeclaration; + +export type StructMemberDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + type: string; + attributes?: VariableOrValueAttribute[]; + }; + +export type StructDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'struct'; + name: string; + fields: StructMemberDeclaration[]; + }; + +export type FunctionParameterDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + type: string; + attributes?: VariableOrValueAttribute[]; + }; + +export type FunctionReturnTypeDeclaration = DeclarationGenerator & { + type: string; + attributes?: VariableOrValueAttribute[]; +}; + +export type FunctionDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'function'; + parameters: FunctionParameterDeclaration[]; + body: string; + returnType?: FunctionReturnTypeDeclaration; + attributes?: FunctionAttribute[]; + }; + +export type Declaration = ValueDeclaration | StructDeclaration | ResourceDeclaration | FunctionDeclaration; + +/// CONSTRUCTORS + +export function constant(name: string, initializer: unknown, type?: string): ConstValueDeclaration { + return { + __identType: 'value', + assignmentType: 'const', + name, + ...(type !== undefined && { type }), + initializer, + __gen: () => `const ${name}${type !== undefined ? `: ${type}` : ''} = ${initializer}`, + }; +} + +export function override( + name: string, + type?: string, + initializer?: unknown, + attributes?: VariableOrValueAttribute[] +): OverrideValueDeclaration { + if (type === undefined && initializer === undefined) { + throw new Error('Override declaration must have at least a type or an initializer'); + } + const __gen = () => `${renderAttrs(attributes)}var ${name}${type !== undefined ? `: ${type}` : ''}${initializer !== undefined ? ` = ${initializer}` : ''}`; + if (type === undefined) { + return { + __identType: 'value', + assignmentType: 'override', + name, + initializer, + ...(attributes !== undefined && { attributes }), + __gen, + }; + } + return { + __identType: 'value' as const, + assignmentType: 'override' as const, + name, + type, + ...(initializer !== undefined && { initializer }), + ...(attributes !== undefined && { attributes }), + __gen, + }; +} + +export function privateVar(name: string, type?: string, initializer?: unknown): PrivateVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'private', + name, + ...(type !== undefined && { type }), + ...(initializer !== undefined && { initializer }), + __gen: () => + `var ${name}${type !== undefined ? `: ${type}` : ''}${initializer !== undefined ? ` = ${initializer}` : ''}`, + }; +} + +export function workgroupVar(name: string, type: string): WorkgroupVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'workgroup', + name, + type, + __gen: () => `var ${name}: ${type}`, + }; +} + +export function uniform( + name: string, + type: string, + group: number, + binding: number, + attributes?: VariableOrValueAttribute[] +): UniformVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'uniform', + name, + type, + group, + binding, + ...(attributes !== undefined && { attributes }), + __gen: () => + `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, + }; +} + +export function texture( + name: string, + type: `texture_${string}`, + group: number, + binding: number, + attributes?: VariableOrValueAttribute[] +): TextureVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'texture', + name, + type, + group, + binding, + ...(attributes !== undefined && { attributes }), + __gen: () => + `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, + }; +} + +export function sampler( + name: string, + type: 'sampler' | 'sampler_comparison', + group: number, + binding: number, + attributes?: VariableOrValueAttribute[] +): SamplerVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'sampler', + name, + type, + group, + binding, + __gen: () => + `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, + ...(attributes !== undefined && { attributes }), + }; +} + +export function storage( + name: string, + type: string, + group: number, + binding: number, + accessMode?: 'read' | 'write' | 'read_write', + attributes?: VariableOrValueAttribute[] +): StorageVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'storage', + name, + type, + group, + binding, + ...(accessMode !== undefined && { accessMode }), + ...(attributes !== undefined && { attributes }), + __gen: () => + `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, + }; +} + +export function member(name: string, type: string, attributes?: VariableOrValueAttribute[]): StructMemberDeclaration { + return { + name, + type, + ...(attributes !== undefined && { attributes }), + __gen: () => `${renderAttrs(attributes)}${name}: ${type}`, + }; +} + +export function struct(name: string, fields: StructMemberDeclaration[]): StructDeclaration { + return { + __identType: 'struct', + name, + fields, + __gen: () => `struct ${name} { ${fields.map((f) => f.__gen()).join(', ')} }`, + }; +} + +export function param( + name: string, + type: string, + attributes?: VariableOrValueAttribute[] +): FunctionParameterDeclaration { + return { + name, + type, + ...(attributes !== undefined && { attributes }), + __gen: () => `${renderAttrs(attributes)}${name}: ${type}`, + }; +} + +export function returns(type: string, attributes?: VariableOrValueAttribute[]): FunctionReturnTypeDeclaration { + return { + type, + ...(attributes !== undefined && { attributes }), + __gen: () => `${renderAttrs(attributes)}${type}`, + }; +} + +export function func( + name: string, + parameters: FunctionParameterDeclaration[], + body: string, + returnType?: FunctionReturnTypeDeclaration, + attributes?: FunctionAttribute[] +): FunctionDeclaration { + return { + __identType: 'function', + name, + parameters, + body, + ...(returnType !== undefined && { returnType }), + ...(attributes !== undefined && { attributes }), + __gen: () => { + const params = parameters.map((p) => p.__gen()).join(', '); + const ret = returnType ? ` -> ${returnType.__gen()}` : ''; + return `${renderAttrs(attributes)}fn ${name}(${params})${ret} { ${body} }`; + }, + }; +} + +export function vertexEntry( + name: string, + parameters: FunctionParameterDeclaration[], + body: string, + returnType?: FunctionReturnTypeDeclaration +): FunctionDeclaration { + return func(name, parameters, body, returnType, [vertex()]); +} + +export function fragmentEntry( + name: string, + parameters: FunctionParameterDeclaration[], + body: string, + returnType?: FunctionReturnTypeDeclaration +): FunctionDeclaration { + return func(name, parameters, body, returnType, [fragment()]); +} + +export function computeEntry( + name: string, + parameters: FunctionParameterDeclaration[], + body: string, + returnType?: FunctionReturnTypeDeclaration +): FunctionDeclaration { + return func(name, parameters, body, returnType, [compute()]); +} + +export const constructors = { + constant, + override, + privateVar, + workgroupVar, + uniform, + texture, + sampler, + member, + struct, + param, + returns, + func, + vertexEntry, + fragmentEntry, + computeEntry, +}; diff --git a/packages/core/src/webgpu/shaders/index.ts b/packages/core/src/webgpu/shaders/index.ts new file mode 100644 index 00000000..af156809 --- /dev/null +++ b/packages/core/src/webgpu/shaders/index.ts @@ -0,0 +1,80 @@ +export type { + AlignAttribute, + BlendSrcAttribute, + BuiltinAttribute, + ConstAttribute, + DiagnosticAttribute, + IdAttribute, + InterpolateAttribute, + InvariantAttribute, + LocationAttribute, + MustUseAttribute, + SizeAttribute, + WorkgroupSizeAttribute, + VertexAttribute, + FragmentAttribute, + ComputeAttribute, + VariableOrValueAttribute, + FunctionAttribute, +} from './attributes'; + +export type { + IdentifierDeclaration, + ConstValueDeclaration, + OverrideValueDeclaration, + ValueDeclaration, + PrivateVariableDeclaration, + WorkgroupVariableDeclaration, + ResourceIdentifierDeclaration, + UniformVariableDeclaration, + TextureVariableDeclaration, + SamplerVariableDeclaration, + StorageVariableDeclaration, + ResourceDeclaration, + StructMemberDeclaration, + StructDeclaration, + FunctionParameterDeclaration, + FunctionReturnTypeDeclaration, + FunctionDeclaration, + Declaration, +} from './declarations'; + +export { + align, + blendSrc, + builtin, + constAttr, + diagnostic, + id, + interpolate, + invariant, + location, + mustUse, + size, + vertex, + fragment, + compute, + workgroupSize, + constructors as $a, +} from './attributes'; + +export { + constant, + override, + privateVar, + workgroupVar, + uniform, + texture, + sampler, + member, + struct, + param, + returns, + func, + vertexEntry, + fragmentEntry, + computeEntry, + constructors as $s +} from './declarations'; + +export { WGSLShader, shader } from './shader'; diff --git a/packages/core/src/webgpu/shaders/shader.ts b/packages/core/src/webgpu/shaders/shader.ts new file mode 100644 index 00000000..61367cea --- /dev/null +++ b/packages/core/src/webgpu/shaders/shader.ts @@ -0,0 +1,34 @@ +/** + * This file defines the `WGSLShader` class, which represents a shader program in WGSL. + * It includes methods for serializing and deserializing the shader definition, as well + * as generating the WGSL source code from the defined declarations. The `shader` + * function is a simple helper function for creating a new `WGSLShader` instance from an + * array of declarations. + */ + +import type { Declaration } from "./declarations"; + +export class WGSLShader { + #definition: { declarations: Declaration[] }; + + constructor(declarations: Declaration[]) { + this.#definition = { declarations }; + } + + static deserialize(serialized: string): WGSLShader { + const definition = JSON.parse(serialized); + return new WGSLShader(definition.declarations); + } + + serialize(): string { + return JSON.stringify(this.#definition); + } + + asSource(): string { + return this.#definition.declarations.map((d) => d.__gen()).join(';\n'); + } +} + +export function shader(declarations: Declaration[]): WGSLShader { + return new WGSLShader(declarations); +} From 234aa9ddef9f830dc92e95f7247f74f84b2b77ff Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 14 May 2026 15:26:58 -0700 Subject: [PATCH 2/4] Further updates from feedback on the previous PR --- .../core/src/webgpu/shaders/declarations.ts | 100 +++++++++--------- packages/core/src/webgpu/shaders/shader.ts | 53 ++++++---- 2 files changed, 83 insertions(+), 70 deletions(-) diff --git a/packages/core/src/webgpu/shaders/declarations.ts b/packages/core/src/webgpu/shaders/declarations.ts index 147da2cd..1a131242 100644 --- a/packages/core/src/webgpu/shaders/declarations.ts +++ b/packages/core/src/webgpu/shaders/declarations.ts @@ -20,11 +20,46 @@ export type IdentifierDeclaration = { readonly name: string; }; +export type StructMemberDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + type: TypeIdentifier; + attributes?: VariableOrValueAttribute[]; + }; + +export type StructDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'struct'; + name: string; + fields: StructMemberDeclaration[]; + }; + +export type TypeIdentifier = string | StructDeclaration; + +export type FunctionParameterDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + type: TypeIdentifier; + attributes?: VariableOrValueAttribute[]; + }; + +export type FunctionReturnTypeDeclaration = DeclarationGenerator & { + type: TypeIdentifier; + attributes?: VariableOrValueAttribute[]; +}; + +export type FunctionDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'function'; + parameters: FunctionParameterDeclaration[]; + body: string; + returnType?: FunctionReturnTypeDeclaration; + attributes?: FunctionAttribute[]; + }; + export type ConstValueDeclaration = IdentifierDeclaration & DeclarationGenerator & { __identType: 'value'; readonly assignmentType: 'const'; - readonly type?: string; + readonly type?: TypeIdentifier; readonly initializer: unknown; }; @@ -35,11 +70,11 @@ export type OverrideValueDeclaration = IdentifierDeclaration & readonly attributes?: VariableOrValueAttribute[]; } & ( | { - readonly type: string; + readonly type: TypeIdentifier; readonly initializer?: unknown; } | { - readonly type?: string; + readonly type?: TypeIdentifier; readonly initializer: unknown; } ); @@ -53,7 +88,7 @@ export type PrivateVariableDeclaration = IdentifierDeclaration & DeclarationGenerator & { __identType: 'variable'; readonly assignmentType: 'private'; - readonly type?: string; + readonly type?: TypeIdentifier; readonly initializer?: unknown; }; @@ -61,7 +96,7 @@ export type WorkgroupVariableDeclaration = IdentifierDeclaration & DeclarationGenerator & { __identType: 'variable'; readonly assignmentType: 'workgroup'; - readonly type: string; + readonly type: TypeIdentifier; }; // NOTE: currently, these "Resource Interface" declarations hard-code their group and binding, but @@ -77,7 +112,7 @@ export type UniformVariableDeclaration = IdentifierDeclaration & DeclarationGenerator & { __identType: 'variable'; readonly assignmentType: 'uniform'; - readonly type: string; + readonly type: TypeIdentifier; readonly attributes?: VariableOrValueAttribute[]; }; @@ -104,7 +139,7 @@ export type StorageVariableDeclaration = IdentifierDeclaration & DeclarationGenerator & { __identType: 'variable'; readonly assignmentType: 'storage'; - readonly type: string; + readonly type: TypeIdentifier; readonly accessMode?: 'read' | 'write' | 'read_write'; readonly attributes?: VariableOrValueAttribute[]; }; @@ -115,44 +150,11 @@ export type ResourceDeclaration = | SamplerVariableDeclaration | StorageVariableDeclaration; -export type StructMemberDeclaration = IdentifierDeclaration & - DeclarationGenerator & { - type: string; - attributes?: VariableOrValueAttribute[]; - }; - -export type StructDeclaration = IdentifierDeclaration & - DeclarationGenerator & { - __identType: 'struct'; - name: string; - fields: StructMemberDeclaration[]; - }; - -export type FunctionParameterDeclaration = IdentifierDeclaration & - DeclarationGenerator & { - type: string; - attributes?: VariableOrValueAttribute[]; - }; - -export type FunctionReturnTypeDeclaration = DeclarationGenerator & { - type: string; - attributes?: VariableOrValueAttribute[]; -}; - -export type FunctionDeclaration = IdentifierDeclaration & - DeclarationGenerator & { - __identType: 'function'; - parameters: FunctionParameterDeclaration[]; - body: string; - returnType?: FunctionReturnTypeDeclaration; - attributes?: FunctionAttribute[]; - }; - export type Declaration = ValueDeclaration | StructDeclaration | ResourceDeclaration | FunctionDeclaration; /// CONSTRUCTORS -export function constant(name: string, initializer: unknown, type?: string): ConstValueDeclaration { +export function constant(name: string, initializer: unknown, type?: TypeIdentifier): ConstValueDeclaration { return { __identType: 'value', assignmentType: 'const', @@ -165,7 +167,7 @@ export function constant(name: string, initializer: unknown, type?: string): Con export function override( name: string, - type?: string, + type?: TypeIdentifier, initializer?: unknown, attributes?: VariableOrValueAttribute[] ): OverrideValueDeclaration { @@ -194,7 +196,7 @@ export function override( }; } -export function privateVar(name: string, type?: string, initializer?: unknown): PrivateVariableDeclaration { +export function privateVar(name: string, type?: TypeIdentifier, initializer?: unknown): PrivateVariableDeclaration { return { __identType: 'variable', assignmentType: 'private', @@ -206,7 +208,7 @@ export function privateVar(name: string, type?: string, initializer?: unknown): }; } -export function workgroupVar(name: string, type: string): WorkgroupVariableDeclaration { +export function workgroupVar(name: string, type: TypeIdentifier): WorkgroupVariableDeclaration { return { __identType: 'variable', assignmentType: 'workgroup', @@ -218,7 +220,7 @@ export function workgroupVar(name: string, type: string): WorkgroupVariableDecla export function uniform( name: string, - type: string, + type: TypeIdentifier, group: number, binding: number, attributes?: VariableOrValueAttribute[] @@ -278,7 +280,7 @@ export function sampler( export function storage( name: string, - type: string, + type: TypeIdentifier, group: number, binding: number, accessMode?: 'read' | 'write' | 'read_write', @@ -298,7 +300,7 @@ export function storage( }; } -export function member(name: string, type: string, attributes?: VariableOrValueAttribute[]): StructMemberDeclaration { +export function member(name: string, type: TypeIdentifier, attributes?: VariableOrValueAttribute[]): StructMemberDeclaration { return { name, type, @@ -318,7 +320,7 @@ export function struct(name: string, fields: StructMemberDeclaration[]): StructD export function param( name: string, - type: string, + type: TypeIdentifier, attributes?: VariableOrValueAttribute[] ): FunctionParameterDeclaration { return { @@ -329,7 +331,7 @@ export function param( }; } -export function returns(type: string, attributes?: VariableOrValueAttribute[]): FunctionReturnTypeDeclaration { +export function returns(type: TypeIdentifier, attributes?: VariableOrValueAttribute[]): FunctionReturnTypeDeclaration { return { type, ...(attributes !== undefined && { attributes }), diff --git a/packages/core/src/webgpu/shaders/shader.ts b/packages/core/src/webgpu/shaders/shader.ts index 61367cea..005369ee 100644 --- a/packages/core/src/webgpu/shaders/shader.ts +++ b/packages/core/src/webgpu/shaders/shader.ts @@ -1,34 +1,45 @@ /** - * This file defines the `WGSLShader` class, which represents a shader program in WGSL. - * It includes methods for serializing and deserializing the shader definition, as well - * as generating the WGSL source code from the defined declarations. The `shader` - * function is a simple helper function for creating a new `WGSLShader` instance from an - * array of declarations. + * This file defines the `WgslShader` type, which represents a shader program in WGSL. + * Also included are methods for serializing and deserializing the shader definition, as + * well as generating the WGSL source code from a `WgslShader` object and its declarations. + * The `shader` function is a simple helper function for creating a new `WgslShader` + * object from an array of declarations. */ import type { Declaration } from "./declarations"; -export class WGSLShader { - #definition: { declarations: Declaration[] }; +export type WgslShader = { + declarations: Declaration[]; +} - constructor(declarations: Declaration[]) { - this.#definition = { declarations }; - } +// NOTE: In the future, we may want to add further typeguards for the different declarations +// so that we can confirm the structure of the whole shader; for now, this is sufficient for +// some basic type safety for shader string rendering, deserialization, etc. +export function isWgslShader(value: unknown): value is WgslShader { + return typeof value === 'object' && value !== null && 'declarations' in value && Array.isArray(value.declarations); +} - static deserialize(serialized: string): WGSLShader { - const definition = JSON.parse(serialized); - return new WGSLShader(definition.declarations); +export function asSource(shader: WgslShader): string { + if (isWgslShader(shader)) { + return shader.declarations.map((d) => d.__gen()).join(';\n'); + } else { + throw new Error('Invalid shader object'); } +} - serialize(): string { - return JSON.stringify(this.#definition); - } +export function shader(declarations: Declaration[]): WgslShader { + return { declarations }; +} - asSource(): string { - return this.#definition.declarations.map((d) => d.__gen()).join(';\n'); - } +export function serializeShader(shader: WgslShader): string { + return JSON.stringify(shader); } -export function shader(declarations: Declaration[]): WGSLShader { - return new WGSLShader(declarations); +export function deserializeShader(serialized: string): WgslShader { + const parsed = JSON.parse(serialized); + if (isWgslShader(parsed)) { + return parsed; + } else { + throw new Error('Invalid serialized shader'); + } } From ae61002c1df56ca396e4969a1ba2e1576549e483 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 14 May 2026 15:49:55 -0700 Subject: [PATCH 3/4] Fmt fixes --- .../core/src/webgpu/shaders/declarations.ts | 31 ++++++++++++------- packages/core/src/webgpu/shaders/index.ts | 2 +- packages/core/src/webgpu/shaders/shader.ts | 10 +++--- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/core/src/webgpu/shaders/declarations.ts b/packages/core/src/webgpu/shaders/declarations.ts index 1a131242..5e347859 100644 --- a/packages/core/src/webgpu/shaders/declarations.ts +++ b/packages/core/src/webgpu/shaders/declarations.ts @@ -1,10 +1,17 @@ /** - * This file defines the various types of declarations that can be used in our shader generation system, - * including variables, constants, structs, and functions. Each declaration type includes a __gen method + * This file defines the various types of declarations that can be used in our shader generation system, + * including variables, constants, structs, and functions. Each declaration type includes a __gen method * that generates the corresponding WGSL code for that declaration. */ -import { compute, fragment, vertex, type DeclarationAttribute, type FunctionAttribute, type VariableOrValueAttribute } from './attributes'; +import { + compute, + fragment, + vertex, + type DeclarationAttribute, + type FunctionAttribute, + type VariableOrValueAttribute, +} from './attributes'; function renderAttrs(attrs: DeclarationAttribute[] | undefined): string { return attrs && attrs.length > 0 ? attrs.map((attr) => `${attr.__gen()}`).join(' ') + ' ' : ''; @@ -174,7 +181,8 @@ export function override( if (type === undefined && initializer === undefined) { throw new Error('Override declaration must have at least a type or an initializer'); } - const __gen = () => `${renderAttrs(attributes)}var ${name}${type !== undefined ? `: ${type}` : ''}${initializer !== undefined ? ` = ${initializer}` : ''}`; + const __gen = () => + `${renderAttrs(attributes)}var ${name}${type !== undefined ? `: ${type}` : ''}${initializer !== undefined ? ` = ${initializer}` : ''}`; if (type === undefined) { return { __identType: 'value', @@ -233,8 +241,7 @@ export function uniform( group, binding, ...(attributes !== undefined && { attributes }), - __gen: () => - `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, + __gen: () => `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, }; } @@ -253,8 +260,7 @@ export function texture( group, binding, ...(attributes !== undefined && { attributes }), - __gen: () => - `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, + __gen: () => `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, }; } @@ -272,8 +278,7 @@ export function sampler( type, group, binding, - __gen: () => - `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, + __gen: () => `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, ...(attributes !== undefined && { attributes }), }; } @@ -300,7 +305,11 @@ export function storage( }; } -export function member(name: string, type: TypeIdentifier, attributes?: VariableOrValueAttribute[]): StructMemberDeclaration { +export function member( + name: string, + type: TypeIdentifier, + attributes?: VariableOrValueAttribute[] +): StructMemberDeclaration { return { name, type, diff --git a/packages/core/src/webgpu/shaders/index.ts b/packages/core/src/webgpu/shaders/index.ts index af156809..47637d66 100644 --- a/packages/core/src/webgpu/shaders/index.ts +++ b/packages/core/src/webgpu/shaders/index.ts @@ -74,7 +74,7 @@ export { vertexEntry, fragmentEntry, computeEntry, - constructors as $s + constructors as $s, } from './declarations'; export { WGSLShader, shader } from './shader'; diff --git a/packages/core/src/webgpu/shaders/shader.ts b/packages/core/src/webgpu/shaders/shader.ts index 005369ee..d604d176 100644 --- a/packages/core/src/webgpu/shaders/shader.ts +++ b/packages/core/src/webgpu/shaders/shader.ts @@ -1,16 +1,16 @@ /** * This file defines the `WgslShader` type, which represents a shader program in WGSL. - * Also included are methods for serializing and deserializing the shader definition, as - * well as generating the WGSL source code from a `WgslShader` object and its declarations. - * The `shader` function is a simple helper function for creating a new `WgslShader` + * Also included are methods for serializing and deserializing the shader definition, as + * well as generating the WGSL source code from a `WgslShader` object and its declarations. + * The `shader` function is a simple helper function for creating a new `WgslShader` * object from an array of declarations. */ -import type { Declaration } from "./declarations"; +import type { Declaration } from './declarations'; export type WgslShader = { declarations: Declaration[]; -} +}; // NOTE: In the future, we may want to add further typeguards for the different declarations // so that we can confirm the structure of the whole shader; for now, this is sufficient for From 6ed9c1241f6e1780ac8ffcefc95e59689083a075 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 14 May 2026 15:52:30 -0700 Subject: [PATCH 4/4] Lint fixes --- packages/core/src/webgpu/shaders/attributes.ts | 12 ++++++------ packages/core/src/webgpu/shaders/shader.ts | 6 ++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/core/src/webgpu/shaders/attributes.ts b/packages/core/src/webgpu/shaders/attributes.ts index 7900c7fe..0603ffcb 100644 --- a/packages/core/src/webgpu/shaders/attributes.ts +++ b/packages/core/src/webgpu/shaders/attributes.ts @@ -225,7 +225,7 @@ export function builtin(name: ShaderBuiltins): BuiltinAttribute { } export function constAttr(): ConstAttribute { - return { const: true, __gen: () => `@const` }; + return { const: true, __gen: () => '@const' }; } export function diagnostic(severity: ShaderSeverityControlName, message: string): DiagnosticAttribute { @@ -274,7 +274,7 @@ export function interpolate( } export function invariant(): InvariantAttribute { - return { invariant: true, __gen: () => `@invariant` }; + return { invariant: true, __gen: () => '@invariant' }; } export function location(n: number): LocationAttribute { @@ -285,7 +285,7 @@ export function location(n: number): LocationAttribute { } export function mustUse(): MustUseAttribute { - return { must_use: true, __gen: () => `@must_use` }; + return { must_use: true, __gen: () => '@must_use' }; } export function size(n: number): SizeAttribute { @@ -308,15 +308,15 @@ export function workgroupSize( } export function vertex(): VertexAttribute { - return { vertex: true, __gen: () => `@vertex` }; + return { vertex: true, __gen: () => '@vertex' }; } export function fragment(): FragmentAttribute { - return { fragment: true, __gen: () => `@fragment` }; + return { fragment: true, __gen: () => '@fragment' }; } export function compute(): ComputeAttribute { - return { compute: true, __gen: () => `@compute` }; + return { compute: true, __gen: () => '@compute' }; } export const constructors = { diff --git a/packages/core/src/webgpu/shaders/shader.ts b/packages/core/src/webgpu/shaders/shader.ts index d604d176..4a8cc1a1 100644 --- a/packages/core/src/webgpu/shaders/shader.ts +++ b/packages/core/src/webgpu/shaders/shader.ts @@ -22,9 +22,8 @@ export function isWgslShader(value: unknown): value is WgslShader { export function asSource(shader: WgslShader): string { if (isWgslShader(shader)) { return shader.declarations.map((d) => d.__gen()).join(';\n'); - } else { - throw new Error('Invalid shader object'); } + throw new Error('Invalid shader object'); } export function shader(declarations: Declaration[]): WgslShader { @@ -39,7 +38,6 @@ export function deserializeShader(serialized: string): WgslShader { const parsed = JSON.parse(serialized); if (isWgslShader(parsed)) { return parsed; - } else { - throw new Error('Invalid serialized shader'); } + throw new Error('Invalid serialized shader'); }