diff --git a/packages/core/src/webgpu/shaders/attributes.ts b/packages/core/src/webgpu/shaders/attributes.ts new file mode 100644 index 00000000..0603ffcb --- /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..5e347859 --- /dev/null +++ b/packages/core/src/webgpu/shaders/declarations.ts @@ -0,0 +1,416 @@ +/** + * 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 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?: TypeIdentifier; + readonly initializer: unknown; + }; + +export type OverrideValueDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'value'; + readonly assignmentType: 'override'; + readonly attributes?: VariableOrValueAttribute[]; + } & ( + | { + readonly type: TypeIdentifier; + readonly initializer?: unknown; + } + | { + readonly type?: TypeIdentifier; + 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?: TypeIdentifier; + readonly initializer?: unknown; + }; + +export type WorkgroupVariableDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'workgroup'; + readonly type: TypeIdentifier; + }; + +// 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: TypeIdentifier; + 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: TypeIdentifier; + readonly accessMode?: 'read' | 'write' | 'read_write'; + readonly attributes?: VariableOrValueAttribute[]; + }; + +export type ResourceDeclaration = + | UniformVariableDeclaration + | TextureVariableDeclaration + | SamplerVariableDeclaration + | StorageVariableDeclaration; + +export type Declaration = ValueDeclaration | StructDeclaration | ResourceDeclaration | FunctionDeclaration; + +/// CONSTRUCTORS + +export function constant(name: string, initializer: unknown, type?: TypeIdentifier): ConstValueDeclaration { + return { + __identType: 'value', + assignmentType: 'const', + name, + ...(type !== undefined && { type }), + initializer, + __gen: () => `const ${name}${type !== undefined ? `: ${type}` : ''} = ${initializer}`, + }; +} + +export function override( + name: string, + type?: TypeIdentifier, + 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?: TypeIdentifier, 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: TypeIdentifier): WorkgroupVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'workgroup', + name, + type, + __gen: () => `var ${name}: ${type}`, + }; +} + +export function uniform( + name: string, + type: TypeIdentifier, + 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: TypeIdentifier, + 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: TypeIdentifier, + 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: TypeIdentifier, + attributes?: VariableOrValueAttribute[] +): FunctionParameterDeclaration { + return { + name, + type, + ...(attributes !== undefined && { attributes }), + __gen: () => `${renderAttrs(attributes)}${name}: ${type}`, + }; +} + +export function returns(type: TypeIdentifier, 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..47637d66 --- /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..4a8cc1a1 --- /dev/null +++ b/packages/core/src/webgpu/shaders/shader.ts @@ -0,0 +1,43 @@ +/** + * 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 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 +// 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); +} + +export function asSource(shader: WgslShader): string { + if (isWgslShader(shader)) { + return shader.declarations.map((d) => d.__gen()).join(';\n'); + } + throw new Error('Invalid shader object'); +} + +export function shader(declarations: Declaration[]): WgslShader { + return { declarations }; +} + +export function serializeShader(shader: WgslShader): string { + return JSON.stringify(shader); +} + +export function deserializeShader(serialized: string): WgslShader { + const parsed = JSON.parse(serialized); + if (isWgslShader(parsed)) { + return parsed; + } + throw new Error('Invalid serialized shader'); +}