@luma.gl/shadertools
Version:
Shader module system for luma.gl
1,063 lines (934 loc) • 33.2 kB
text/typescript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {getShaderModuleDependencies} from '../shader-module/shader-module-dependencies';
import {PlatformInfo} from './platform-info';
import {getPlatformShaderDefines} from './platform-defines';
import {injectShader, DECLARATION_INJECT_MARKER} from './shader-injections';
import {transpileGLSLShader} from '../shader-transpiler/transpile-glsl-shader';
import {checkShaderModuleDeprecations} from '../shader-module/shader-module';
import {
validateShaderModuleUniformLayout,
warnIfGLSLUniformBlocksAreNotStd140
} from '../shader-module/shader-module-uniform-layout';
import type {ShaderInjection} from './shader-injections';
import type {ShaderModule} from '../shader-module/shader-module';
import {ShaderHook, normalizeShaderHooks, getShaderHooks} from './shader-hooks';
import {assert} from '../utils/assert';
import {getShaderInfo} from '../glsl-utils/get-shader-info';
import {getShaderBindingDebugRowsFromWGSL, type ShaderBindingDebugRow} from './wgsl-binding-debug';
import {
MODULE_WGSL_BINDING_DECLARATION_REGEXES,
WGSL_BINDING_DECLARATION_REGEXES,
WGSL_EXPLICIT_BINDING_DECLARATION_REGEXES,
getFirstWGSLAutoBindingDeclarationMatch,
getWGSLBindingDeclarationMatches,
hasWGSLAutoBinding,
replaceWGSLBindingDeclarationMatches,
type WGSLBindingDeclarationMatch
} from './wgsl-binding-scan';
const INJECT_SHADER_DECLARATIONS = `\n\n${DECLARATION_INJECT_MARKER}\n`;
const RESERVED_APPLICATION_GROUP_0_BINDING_LIMIT = 100;
/**
* Precision prologue to inject before functions are injected in shader
* TODO - extract any existing prologue in the fragment source and move it up...
*/
const FRAGMENT_SHADER_PROLOGUE = /* glsl */ `\
precision highp float;
`;
/**
* Options for `ShaderAssembler.assembleShaders()`
*/
export type AssembleShaderProps = AssembleShaderOptions & {
platformInfo: PlatformInfo;
/** WGSL: single shader source. */
source?: string | null;
/** GLSL vertex shader source. */
vs?: string | null;
/** GLSL fragment shader source. */
fs?: string | null;
};
export type AssembleShaderOptions = {
/** information about the platform (which shader language & version, extensions etc.) */
platformInfo: PlatformInfo;
/** Inject shader id #defines */
id?: string;
/** Modules to be injected */
modules?: ShaderModule[];
/** Defines to be injected */
defines?: Record<string, boolean>;
/** GLSL only: Overrides to be injected. In WGSL these are supplied during Pipeline creation time */
constants?: Record<string, number>;
/** Hook functions */
hookFunctions?: (ShaderHook | string)[];
/** Code injections */
inject?: Record<string, string | ShaderInjection>;
/** Whether to inject prologue */
prologue?: boolean;
/** logger object */
log?: any;
};
type AssembleStageOptions = {
/** Inject shader id #defines */
id?: string;
/** Vertex shader */
source: string;
stage: 'vertex' | 'fragment';
/** Modules to be injected */
modules: any[];
/** Defines to be injected */
defines?: Record<string, boolean>;
/** GLSL only: Overrides to be injected. In WGSL these are supplied during Pipeline creation time */
constants?: Record<string, number>;
/** Hook functions */
hookFunctions?: (ShaderHook | string)[];
/** Code injections */
inject?: Record<string, string | ShaderInjection>;
/** Whether to inject prologue */
prologue?: boolean;
/** logger object */
log?: any;
/** @internal Stable per-assembler WGSL binding assignments. */
_bindingRegistry?: Map<string, number>;
};
export type HookFunction = {hook: string; header: string; footer: string; signature?: string};
/**
* getUniforms function returned from the shader module system
*/
export type GetUniformsFunc = (opts: Record<string, any>) => Record<string, any>;
/**
* Inject a list of shader modules into a single shader source for WGSL
*/
export function assembleWGSLShader(
options: AssembleShaderOptions & {
/** Single WGSL shader */
source: string;
/** @internal Stable per-assembler WGSL binding assignments. */
_bindingRegistry?: Map<string, number>;
}
): {
source: string;
getUniforms: GetUniformsFunc;
bindingAssignments: {moduleName: string; name: string; group: number; location: number}[];
bindingTable: ShaderBindingDebugRow[];
} {
const modules = getShaderModuleDependencies(options.modules || []);
const {source, bindingAssignments} = assembleShaderWGSL(options.platformInfo, {
...options,
source: options.source,
stage: 'vertex',
modules
});
return {
source,
getUniforms: assembleGetUniforms(modules),
bindingAssignments,
bindingTable: getShaderBindingDebugRowsFromWGSL(source, bindingAssignments)
};
}
/**
* Injects dependent shader module sources into pair of main vertex/fragment shader sources for GLSL
*/
export function assembleGLSLShaderPair(
options: AssembleShaderOptions & {
/** Vertex shader */
vs: string;
/** Fragment shader */
fs?: string;
}
): {
vs: string;
fs: string;
getUniforms: GetUniformsFunc;
} {
const {vs, fs} = options;
const modules = getShaderModuleDependencies(options.modules || []);
return {
vs: assembleShaderGLSL(options.platformInfo, {
...options,
source: vs,
stage: 'vertex',
modules
}),
fs: assembleShaderGLSL(options.platformInfo, {
...options,
// @ts-expect-error
source: fs,
stage: 'fragment',
modules
}),
getUniforms: assembleGetUniforms(modules)
};
}
/**
* Pulls together complete source code for either a vertex or a fragment shader
* adding prologues, requested module chunks, and any final injections.
* @param gl
* @param options
* @returns
*/
export function assembleShaderWGSL(
platformInfo: PlatformInfo,
options: AssembleStageOptions
): {source: string; bindingAssignments: WGSLBindingAssignment[]} {
const {
// id,
source,
stage,
modules,
// defines = {},
hookFunctions = [],
inject = {},
log
} = options;
assert(typeof source === 'string', 'shader source must be a string');
// const isVertex = type === 'vs';
// const sourceLines = source.split('\n');
const coreSource = source;
// Combine Module and Application Defines
// const allDefines = {};
// modules.forEach(module => {
// Object.assign(allDefines, module.getDefines());
// });
// Object.assign(allDefines, defines);
// Add platform defines (use these to work around platform-specific bugs and limitations)
// Add common defines (GLSL version compatibility, feature detection)
// Add precision declaration for fragment shaders
let assembledSource = '';
// prologue
// ? `\
// ${getShaderNameDefine({id, source, type})}
// ${getShaderType(type)}
// ${getPlatformShaderDefines(platformInfo)}
// ${getApplicationDefines(allDefines)}
// ${isVertex ? '' : FRAGMENT_SHADER_PROLOGUE}
// `
// `;
const hookFunctionMap = normalizeShaderHooks(hookFunctions);
// Add source of dependent modules in resolved order
const hookInjections: Record<string, ShaderInjection[]> = {};
const declInjections: Record<string, ShaderInjection[]> = {};
const mainInjections: Record<string, ShaderInjection[]> = {};
for (const key in inject) {
const injection =
typeof inject[key] === 'string' ? {injection: inject[key], order: 0} : inject[key];
const match = /^(v|f)s:(#)?([\w-]+)$/.exec(key);
if (match) {
const hash = match[2];
const name = match[3];
if (hash) {
if (name === 'decl') {
declInjections[key] = [injection as any];
} else {
mainInjections[key] = [injection as any];
}
} else {
hookInjections[key] = [injection as any];
}
} else {
// Regex injection
mainInjections[key] = [injection as any];
}
}
// TODO - hack until shadertool modules support WebGPU
const modulesToInject = modules;
const applicationRelocation = relocateWGSLApplicationBindings(coreSource);
const usedBindingsByGroup = getUsedBindingsByGroupFromApplicationWGSL(
applicationRelocation.source
);
const reservedBindingKeysByGroup = reserveRegisteredModuleBindings(
modulesToInject,
options._bindingRegistry,
usedBindingsByGroup
);
const bindingAssignments: WGSLBindingAssignment[] = [];
for (const module of modulesToInject) {
if (log) {
checkShaderModuleDeprecations(module, coreSource, log);
}
const relocation = relocateWGSLModuleBindings(
getShaderModuleSource(module, 'wgsl', log),
module,
{
usedBindingsByGroup,
bindingRegistry: options._bindingRegistry,
reservedBindingKeysByGroup
}
);
bindingAssignments.push(...relocation.bindingAssignments);
const moduleSource = relocation.source;
// Add the module source, and a #define that declares it presence
assembledSource += moduleSource;
const injections = module.injections?.[stage] || {};
for (const key in injections) {
const match = /^(v|f)s:#([\w-]+)$/.exec(key);
if (match) {
const name = match[2];
const injectionType = name === 'decl' ? declInjections : mainInjections;
injectionType[key] = injectionType[key] || [];
injectionType[key].push(injections[key]);
} else {
hookInjections[key] = hookInjections[key] || [];
hookInjections[key].push(injections[key]);
}
}
}
// For injectShader
assembledSource += INJECT_SHADER_DECLARATIONS;
assembledSource = injectShader(assembledSource, stage, declInjections);
assembledSource += getShaderHooks(hookFunctionMap[stage], hookInjections);
assembledSource += formatWGSLBindingAssignmentComments(bindingAssignments);
// Add the version directive and actual source of this shader
assembledSource += applicationRelocation.source;
// Apply any requested shader injections
assembledSource = injectShader(assembledSource, stage, mainInjections);
assertNoUnresolvedAutoBindings(assembledSource);
return {source: assembledSource, bindingAssignments};
}
/**
* Pulls together complete source code for either a vertex or a fragment shader
* adding prologues, requested module chunks, and any final injections.
* @param gl
* @param options
* @returns
*/
function assembleShaderGLSL(
platformInfo: PlatformInfo,
options: {
id?: string;
source: string;
language?: 'glsl' | 'wgsl';
stage: 'vertex' | 'fragment';
modules: ShaderModule[];
defines?: Record<string, boolean>;
hookFunctions?: any[];
inject?: Record<string, string | ShaderInjection>;
prologue?: boolean;
log?: any;
}
) {
const {
source,
stage,
language = 'glsl',
modules,
defines = {},
hookFunctions = [],
inject = {},
prologue = true,
log
} = options;
assert(typeof source === 'string', 'shader source must be a string');
const sourceVersion = language === 'glsl' ? getShaderInfo(source).version : -1;
const targetVersion = platformInfo.shaderLanguageVersion;
const sourceVersionDirective = sourceVersion === 100 ? '#version 100' : '#version 300 es';
const sourceLines = source.split('\n');
// TODO : keep all pre-processor statements at the beginning of the shader.
const coreSource = sourceLines.slice(1).join('\n');
// Combine Module and Application Defines
const allDefines = {};
modules.forEach(module => {
Object.assign(allDefines, module.defines);
});
Object.assign(allDefines, defines);
// Add platform defines (use these to work around platform-specific bugs and limitations)
// Add common defines (GLSL version compatibility, feature detection)
// Add precision declaration for fragment shaders
let assembledSource = '';
switch (language) {
case 'wgsl':
break;
case 'glsl':
assembledSource = prologue
? `\
${sourceVersionDirective}
// ----- PROLOGUE -------------------------
${`#define SHADER_TYPE_${stage.toUpperCase()}`}
${getPlatformShaderDefines(platformInfo)}
${stage === 'fragment' ? FRAGMENT_SHADER_PROLOGUE : ''}
// ----- APPLICATION DEFINES -------------------------
${getApplicationDefines(allDefines)}
`
: `${sourceVersionDirective}
`;
break;
}
const hookFunctionMap = normalizeShaderHooks(hookFunctions);
// Add source of dependent modules in resolved order
const hookInjections: Record<string, ShaderInjection[]> = {};
const declInjections: Record<string, ShaderInjection[]> = {};
const mainInjections: Record<string, ShaderInjection[]> = {};
for (const key in inject) {
const injection: ShaderInjection =
typeof inject[key] === 'string' ? {injection: inject[key], order: 0} : inject[key];
const match = /^(v|f)s:(#)?([\w-]+)$/.exec(key);
if (match) {
const hash = match[2];
const name = match[3];
if (hash) {
if (name === 'decl') {
declInjections[key] = [injection];
} else {
mainInjections[key] = [injection];
}
} else {
hookInjections[key] = [injection];
}
} else {
// Regex injection
mainInjections[key] = [injection];
}
}
for (const module of modules) {
if (log) {
checkShaderModuleDeprecations(module, coreSource, log);
}
const moduleSource = getShaderModuleSource(module, stage, log);
// Add the module source, and a #define that declares it presence
assembledSource += moduleSource;
const injections = module.instance?.normalizedInjections[stage] || {};
for (const key in injections) {
const match = /^(v|f)s:#([\w-]+)$/.exec(key);
if (match) {
const name = match[2];
const injectionType = name === 'decl' ? declInjections : mainInjections;
injectionType[key] = injectionType[key] || [];
injectionType[key].push(injections[key]);
} else {
hookInjections[key] = hookInjections[key] || [];
hookInjections[key].push(injections[key]);
}
}
}
assembledSource += '// ----- MAIN SHADER SOURCE -------------------------';
// For injectShader
assembledSource += INJECT_SHADER_DECLARATIONS;
assembledSource = injectShader(assembledSource, stage, declInjections);
assembledSource += getShaderHooks(hookFunctionMap[stage], hookInjections);
// Add the version directive and actual source of this shader
assembledSource += coreSource;
// Apply any requested shader injections
assembledSource = injectShader(assembledSource, stage, mainInjections);
if (language === 'glsl' && sourceVersion !== targetVersion) {
assembledSource = transpileGLSLShader(assembledSource, stage);
}
if (language === 'glsl') {
warnIfGLSLUniformBlocksAreNotStd140(assembledSource, stage, log);
}
return assembledSource.trim();
}
/**
* Returns a combined `getUniforms` covering the options for all the modules,
* the created function will pass on options to the inidividual `getUniforms`
* function of each shader module and combine the results into one object that
* can be passed to setUniforms.
* @param modules
* @returns
*/
export function assembleGetUniforms(modules: ShaderModule[]) {
return function getUniforms(opts: Record<string, any>): Record<string, any> {
const uniforms = {};
for (const module of modules) {
// `modules` is already sorted by dependency level. This guarantees that
// modules have access to the uniforms that are generated by their dependencies.
const moduleUniforms = module.getUniforms?.(opts, uniforms);
Object.assign(uniforms, moduleUniforms);
}
return uniforms;
};
}
/**
* NOTE: Removed as id injection defeated caching of shaders
*
* Generate "glslify-compatible" SHADER_NAME defines
* These are understood by the GLSL error parsing function
* If id is provided and no SHADER_NAME constant is present in source, create one
unction getShaderNameDefine(options: {
id?: string;
source: string;
stage: 'vertex' | 'fragment';
}): string {
const {id, source, stage} = options;
const injectShaderName = id && source.indexOf('SHADER_NAME') === -1;
return injectShaderName
? `
#define SHADER_NAME ${id}_${stage}`
: '';
}
*/
/** Generates application defines from an object of key value pairs */
function getApplicationDefines(defines: Record<string, boolean> = {}): string {
let sourceText = '';
for (const define in defines) {
const value = defines[define];
if (value || Number.isFinite(value)) {
sourceText += `#define ${define.toUpperCase()} ${defines[define]}\n`;
}
}
return sourceText;
}
/** Extracts the source code chunk for the specified shader type from the named shader module */
export function getShaderModuleSource(
module: ShaderModule,
stage: 'vertex' | 'fragment' | 'wgsl',
log?: any
): string {
let moduleSource;
switch (stage) {
case 'vertex':
moduleSource = module.vs || '';
break;
case 'fragment':
moduleSource = module.fs || '';
break;
case 'wgsl':
moduleSource = module.source || '';
break;
default:
assert(false);
}
if (!module.name) {
throw new Error('Shader module must have a name');
}
validateShaderModuleUniformLayout(module, stage, {log});
const moduleName = module.name.toUpperCase().replace(/[^0-9a-z]/gi, '_');
let source = `\
// ----- MODULE ${module.name} ---------------
`;
if (stage !== 'wgsl') {
source += `#define MODULE_${moduleName}\n`;
}
source += `${moduleSource}\n`;
return source;
}
type BindingRelocationContext = {
usedBindingsByGroup: Map<number, Set<number>>;
bindingRegistry?: Map<string, number>;
reservedBindingKeysByGroup: Map<number, Map<number, string>>;
};
type WGSLBindingAssignment = {
moduleName: string;
name: string;
group: number;
location: number;
};
type WGSLApplicationRelocationState = {
sawSupportedBindingDeclaration: boolean;
};
type WGSLRelocationState = {
sawSupportedBindingDeclaration: boolean;
nextHintedBindingLocation: number | null;
};
type WGSLRelocationParams = {
module: ShaderModule;
context: BindingRelocationContext;
bindingAssignments: WGSLBindingAssignment[];
relocationState: WGSLRelocationState;
};
function getUsedBindingsByGroupFromApplicationWGSL(source: string): Map<number, Set<number>> {
const usedBindingsByGroup = new Map<number, Set<number>>();
for (const match of getWGSLBindingDeclarationMatches(
source,
WGSL_EXPLICIT_BINDING_DECLARATION_REGEXES
)) {
const location = Number(match.bindingToken);
const group = Number(match.groupToken);
validateApplicationWGSLBinding(group, location, match.name);
registerUsedBindingLocation(
usedBindingsByGroup,
group,
location,
`application binding "${match.name}"`
);
}
return usedBindingsByGroup;
}
function relocateWGSLApplicationBindings(source: string): {source: string} {
const declarationMatches = getWGSLBindingDeclarationMatches(
source,
WGSL_BINDING_DECLARATION_REGEXES
);
const usedBindingsByGroup = new Map<number, Set<number>>();
for (const declarationMatch of declarationMatches) {
if (declarationMatch.bindingToken === 'auto') {
continue;
}
const location = Number(declarationMatch.bindingToken);
const group = Number(declarationMatch.groupToken);
validateApplicationWGSLBinding(group, location, declarationMatch.name);
registerUsedBindingLocation(
usedBindingsByGroup,
group,
location,
`application binding "${declarationMatch.name}"`
);
}
const relocationState: WGSLApplicationRelocationState = {
sawSupportedBindingDeclaration: declarationMatches.length > 0
};
const relocatedSource = replaceWGSLBindingDeclarationMatches(
source,
WGSL_BINDING_DECLARATION_REGEXES,
declarationMatch =>
relocateWGSLApplicationBindingMatch(declarationMatch, usedBindingsByGroup, relocationState)
);
if (hasWGSLAutoBinding(source) && !relocationState.sawSupportedBindingDeclaration) {
throw new Error(
'Unsupported @binding(auto) declaration form in application WGSL. ' +
'Use adjacent "@group(N)" and "@binding(auto)" decorators followed by a bindable "var" declaration.'
);
}
return {source: relocatedSource};
}
function relocateWGSLModuleBindings(
moduleSource: string,
module: ShaderModule,
context: BindingRelocationContext
): {source: string; bindingAssignments: WGSLBindingAssignment[]} {
const bindingAssignments: WGSLBindingAssignment[] = [];
const declarationMatches = getWGSLBindingDeclarationMatches(
moduleSource,
MODULE_WGSL_BINDING_DECLARATION_REGEXES
);
const relocationState: WGSLRelocationState = {
sawSupportedBindingDeclaration: declarationMatches.length > 0,
nextHintedBindingLocation:
typeof module.firstBindingSlot === 'number' ? module.firstBindingSlot : null
};
const relocatedSource = replaceWGSLBindingDeclarationMatches(
moduleSource,
MODULE_WGSL_BINDING_DECLARATION_REGEXES,
declarationMatch =>
relocateWGSLModuleBindingMatch(declarationMatch, {
module,
context,
bindingAssignments,
relocationState
})
);
if (hasWGSLAutoBinding(moduleSource) && !relocationState.sawSupportedBindingDeclaration) {
throw new Error(
`Unsupported @binding(auto) declaration form in module "${module.name}". ` +
'Use adjacent "@group(N)" and "@binding(auto)" decorators followed by a bindable "var" declaration.'
);
}
return {source: relocatedSource, bindingAssignments};
}
function relocateWGSLModuleBindingMatch(
declarationMatch: WGSLBindingDeclarationMatch,
params: WGSLRelocationParams
): string {
const {module, context, bindingAssignments, relocationState} = params;
const {match, bindingToken, groupToken, name} = declarationMatch;
const group = Number(groupToken);
if (bindingToken === 'auto') {
const registryKey = getBindingRegistryKey(group, module.name, name);
const registryLocation = context.bindingRegistry?.get(registryKey);
const location =
registryLocation !== undefined
? registryLocation
: relocationState.nextHintedBindingLocation === null
? allocateAutoBindingLocation(group, context.usedBindingsByGroup)
: allocateAutoBindingLocation(
group,
context.usedBindingsByGroup,
relocationState.nextHintedBindingLocation
);
validateModuleWGSLBinding(module.name, group, location, name);
if (
registryLocation !== undefined &&
claimReservedBindingLocation(context.reservedBindingKeysByGroup, group, location, registryKey)
) {
bindingAssignments.push({moduleName: module.name, name, group, location});
return match.replace(/@binding\(\s*auto\s*\)/, `@binding(${location})`);
}
registerUsedBindingLocation(
context.usedBindingsByGroup,
group,
location,
`module "${module.name}" binding "${name}"`
);
context.bindingRegistry?.set(registryKey, location);
bindingAssignments.push({moduleName: module.name, name, group, location});
if (relocationState.nextHintedBindingLocation !== null && registryLocation === undefined) {
relocationState.nextHintedBindingLocation = location + 1;
}
return match.replace(/@binding\(\s*auto\s*\)/, `@binding(${location})`);
}
const location = Number(bindingToken);
validateModuleWGSLBinding(module.name, group, location, name);
registerUsedBindingLocation(
context.usedBindingsByGroup,
group,
location,
`module "${module.name}" binding "${name}"`
);
bindingAssignments.push({moduleName: module.name, name, group, location});
return match;
}
function relocateWGSLApplicationBindingMatch(
declarationMatch: WGSLBindingDeclarationMatch,
usedBindingsByGroup: Map<number, Set<number>>,
relocationState: WGSLApplicationRelocationState
): string {
const {match, bindingToken, groupToken, name} = declarationMatch;
const group = Number(groupToken);
if (bindingToken === 'auto') {
const location = allocateApplicationAutoBindingLocation(group, usedBindingsByGroup);
validateApplicationWGSLBinding(group, location, name);
registerUsedBindingLocation(
usedBindingsByGroup,
group,
location,
`application binding "${name}"`
);
return match.replace(/@binding\(\s*auto\s*\)/, `@binding(${location})`);
}
relocationState.sawSupportedBindingDeclaration = true;
return match;
}
function reserveRegisteredModuleBindings(
modules: ShaderModule[],
bindingRegistry: Map<string, number> | undefined,
usedBindingsByGroup: Map<number, Set<number>>
): Map<number, Map<number, string>> {
const reservedBindingKeysByGroup = new Map<number, Map<number, string>>();
if (!bindingRegistry) {
return reservedBindingKeysByGroup;
}
for (const module of modules) {
for (const binding of getModuleWGSLBindingDeclarations(module)) {
const registryKey = getBindingRegistryKey(binding.group, module.name, binding.name);
const location = bindingRegistry.get(registryKey);
if (location !== undefined) {
const reservedBindingKeys =
reservedBindingKeysByGroup.get(binding.group) || new Map<number, string>();
const existingReservation = reservedBindingKeys.get(location);
if (existingReservation && existingReservation !== registryKey) {
throw new Error(
`Duplicate WGSL binding reservation for modules "${existingReservation}" and "${registryKey}": group ${binding.group}, binding ${location}.`
);
}
registerUsedBindingLocation(
usedBindingsByGroup,
binding.group,
location,
`registered module binding "${registryKey}"`
);
reservedBindingKeys.set(location, registryKey);
reservedBindingKeysByGroup.set(binding.group, reservedBindingKeys);
}
}
}
return reservedBindingKeysByGroup;
}
function claimReservedBindingLocation(
reservedBindingKeysByGroup: Map<number, Map<number, string>>,
group: number,
location: number,
registryKey: string
): boolean {
const reservedBindingKeys = reservedBindingKeysByGroup.get(group);
if (!reservedBindingKeys) {
return false;
}
const reservedKey = reservedBindingKeys.get(location);
if (!reservedKey) {
return false;
}
if (reservedKey !== registryKey) {
throw new Error(
`Registered module binding "${registryKey}" collided with "${reservedKey}": group ${group}, binding ${location}.`
);
}
return true;
}
function getModuleWGSLBindingDeclarations(module: ShaderModule): {name: string; group: number}[] {
const declarations: {name: string; group: number}[] = [];
const moduleSource = module.source || '';
for (const match of getWGSLBindingDeclarationMatches(
moduleSource,
MODULE_WGSL_BINDING_DECLARATION_REGEXES
)) {
declarations.push({
name: match.name,
group: Number(match.groupToken)
});
}
return declarations;
}
function validateApplicationWGSLBinding(group: number, location: number, name: string): void {
if (group === 0 && location >= RESERVED_APPLICATION_GROUP_0_BINDING_LIMIT) {
throw new Error(
`Application binding "${name}" in group 0 uses reserved binding ${location}. ` +
`Application-owned explicit group-0 bindings must stay below ${RESERVED_APPLICATION_GROUP_0_BINDING_LIMIT}.`
);
}
}
function validateModuleWGSLBinding(
moduleName: string,
group: number,
location: number,
name: string
): void {
if (group === 0 && location < RESERVED_APPLICATION_GROUP_0_BINDING_LIMIT) {
throw new Error(
`Module "${moduleName}" binding "${name}" in group 0 uses reserved application binding ${location}. ` +
`Module-owned explicit group-0 bindings must be ${RESERVED_APPLICATION_GROUP_0_BINDING_LIMIT} or higher.`
);
}
}
function registerUsedBindingLocation(
usedBindingsByGroup: Map<number, Set<number>>,
group: number,
location: number,
label: string
): void {
const usedBindings = usedBindingsByGroup.get(group) || new Set<number>();
if (usedBindings.has(location)) {
throw new Error(
`Duplicate WGSL binding assignment for ${label}: group ${group}, binding ${location}.`
);
}
usedBindings.add(location);
usedBindingsByGroup.set(group, usedBindings);
}
function allocateAutoBindingLocation(
group: number,
usedBindingsByGroup: Map<number, Set<number>>,
preferredBindingLocation?: number
): number {
const usedBindings = usedBindingsByGroup.get(group) || new Set<number>();
let nextBinding =
preferredBindingLocation ??
(group === 0
? RESERVED_APPLICATION_GROUP_0_BINDING_LIMIT
: usedBindings.size > 0
? Math.max(...usedBindings) + 1
: 0);
while (usedBindings.has(nextBinding)) {
nextBinding++;
}
return nextBinding;
}
function allocateApplicationAutoBindingLocation(
group: number,
usedBindingsByGroup: Map<number, Set<number>>
): number {
const usedBindings = usedBindingsByGroup.get(group) || new Set<number>();
let nextBinding = 0;
while (usedBindings.has(nextBinding)) {
nextBinding++;
}
return nextBinding;
}
function assertNoUnresolvedAutoBindings(source: string): void {
const unresolvedBinding = getFirstWGSLAutoBindingDeclarationMatch(
source,
MODULE_WGSL_BINDING_DECLARATION_REGEXES
);
if (!unresolvedBinding) {
return;
}
const moduleName = getWGSLModuleNameAtIndex(source, unresolvedBinding.index);
if (moduleName) {
throw new Error(
`Unresolved @binding(auto) for module "${moduleName}" binding "${unresolvedBinding.name}" remained in assembled WGSL source.`
);
}
if (isInApplicationWGSLSection(source, unresolvedBinding.index)) {
throw new Error(
`Unresolved @binding(auto) for application binding "${unresolvedBinding.name}" remained in assembled WGSL source.`
);
}
throw new Error(
`Unresolved @binding(auto) remained in assembled WGSL source near "${formatWGSLSourceSnippet(unresolvedBinding.match)}".`
);
}
function formatWGSLBindingAssignmentComments(bindingAssignments: WGSLBindingAssignment[]): string {
if (bindingAssignments.length === 0) {
return '';
}
let source = '// ----- MODULE WGSL BINDING ASSIGNMENTS ---------------\n';
for (const bindingAssignment of bindingAssignments) {
source += `// ${bindingAssignment.moduleName}.${bindingAssignment.name} -> @group(${bindingAssignment.group}) @binding(${bindingAssignment.location})\n`;
}
source += '\n';
return source;
}
function getBindingRegistryKey(group: number, moduleName: string, bindingName: string): string {
return `${group}:${moduleName}:${bindingName}`;
}
function getWGSLModuleNameAtIndex(source: string, index: number): string | undefined {
const moduleHeaderRegex = /^\/\/ ----- MODULE ([^\n]+) ---------------$/gm;
let moduleName: string | undefined;
let match: RegExpExecArray | null;
match = moduleHeaderRegex.exec(source);
while (match && match.index <= index) {
moduleName = match[1];
match = moduleHeaderRegex.exec(source);
}
return moduleName;
}
function isInApplicationWGSLSection(source: string, index: number): boolean {
const injectionMarkerIndex = source.indexOf(INJECT_SHADER_DECLARATIONS);
return injectionMarkerIndex >= 0 ? index > injectionMarkerIndex : true;
}
function formatWGSLSourceSnippet(source: string): string {
return source.replace(/\s+/g, ' ').trim();
}
/*
function getHookFunctions(
hookFunctions: Record<string, HookFunction>,
hookInjections: Record<string, Injection[]>
): string {
let result = '';
for (const hookName in hookFunctions) {
const hookFunction = hookFunctions[hookName];
result += `void ${hookFunction.signature} {\n`;
if (hookFunction.header) {
result += ` ${hookFunction.header}`;
}
if (hookInjections[hookName]) {
const injections = hookInjections[hookName];
injections.sort((a: {order: number}, b: {order: number}): number => a.order - b.order);
for (const injection of injections) {
result += ` ${injection.injection}\n`;
}
}
if (hookFunction.footer) {
result += ` ${hookFunction.footer}`;
}
result += '}\n';
}
return result;
}
function normalizeHookFunctions(hookFunctions: (string | HookFunction)[]): {
vs: Record<string, HookFunction>;
fs: Record<string, HookFunction>;
} {
const result: {vs: Record<string, any>; fs: Record<string, any>} = {
vs: {},
fs: {}
};
hookFunctions.forEach((hookFunction: string | HookFunction) => {
let opts: HookFunction;
let hook: string;
if (typeof hookFunction !== 'string') {
opts = hookFunction;
hook = opts.hook;
} else {
opts = {} as HookFunction;
hook = hookFunction;
}
hook = hook.trim();
const [stage, signature] = hook.split(':');
const name = hook.replace(/\(.+/, '');
if (stage !== 'vs' && stage !== 'fs') {
throw new Error(stage);
}
result[stage][name] = Object.assign(opts, {signature});
});
return result;
}
*/