@luma.gl/shadertools
Version:
Shader module system for luma.gl
228 lines (209 loc) • 6.5 kB
text/typescript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {WGSL_BINDABLE_VARIABLE_PATTERN, maskWGSLComments} from './wgsl-binding-scan';
type ShaderBindingAssignment = {
moduleName: string;
name: string;
group: number;
location: number;
};
const WGSL_BINDING_DEBUG_REGEXES = [
new RegExp(
`@binding\\(\\s*(\\d+)\\s*\\)\\s*@group\\(\\s*(\\d+)\\s*\\)\\s*${WGSL_BINDABLE_VARIABLE_PATTERN}\\s*:\\s*([^;]+);`,
'g'
),
new RegExp(
`@group\\(\\s*(\\d+)\\s*\\)\\s*@binding\\(\\s*(\\d+)\\s*\\)\\s*${WGSL_BINDABLE_VARIABLE_PATTERN}\\s*:\\s*([^;]+);`,
'g'
)
] as const;
/** One debug row describing a WGSL binding in the assembled shader source. */
export type ShaderBindingDebugRow = {
/** Binding name as declared in WGSL. */
name: string;
/** Bind-group index. */
group: number;
/** Binding slot within the bind group. */
binding: number;
/** Resource kind inferred from the WGSL declaration. */
kind:
| 'uniform'
| 'storage'
| 'read-only-storage'
| 'texture'
| 'sampler'
| 'storage-texture'
| 'unknown';
/** Whether the binding came from application WGSL or a shader module. */
owner: 'application' | 'module';
/** Shader module name when the binding was contributed by a module. */
moduleName?: string;
/** Full WGSL resource type text from the declaration. */
resourceType?: string;
/** WGSL access mode when cheaply available. */
access?: string;
/** Texture view dimension when cheaply available. */
viewDimension?: string;
/** Texture sample type when cheaply available. */
sampleType?: string;
/** Sampler kind when cheaply available. */
samplerKind?: string;
/** Whether the texture is multisampled when cheaply available. */
multisampled?: boolean;
};
/** Builds a stable, table-friendly binding summary from assembled WGSL source. */
export function getShaderBindingDebugRowsFromWGSL(
source: string,
bindingAssignments: ShaderBindingAssignment[] = []
): ShaderBindingDebugRow[] {
const maskedSource = maskWGSLComments(source);
const assignmentMap = new Map<string, string>();
for (const bindingAssignment of bindingAssignments) {
assignmentMap.set(
getBindingAssignmentKey(
bindingAssignment.name,
bindingAssignment.group,
bindingAssignment.location
),
bindingAssignment.moduleName
);
}
const rows: ShaderBindingDebugRow[] = [];
for (const regex of WGSL_BINDING_DEBUG_REGEXES) {
regex.lastIndex = 0;
let match: RegExpExecArray | null;
match = regex.exec(maskedSource);
while (match) {
const isBindingFirst = regex === WGSL_BINDING_DEBUG_REGEXES[0];
const binding = Number(match[isBindingFirst ? 1 : 2]);
const group = Number(match[isBindingFirst ? 2 : 1]);
const accessDeclaration = match[3]?.trim();
const name = match[4];
const resourceType = match[5].trim();
const moduleName = assignmentMap.get(getBindingAssignmentKey(name, group, binding));
rows.push(
normalizeShaderBindingDebugRow({
name,
group,
binding,
owner: moduleName ? 'module' : 'application',
moduleName,
accessDeclaration,
resourceType
})
);
match = regex.exec(maskedSource);
}
}
return rows.sort((left, right) => {
if (left.group !== right.group) {
return left.group - right.group;
}
if (left.binding !== right.binding) {
return left.binding - right.binding;
}
return left.name.localeCompare(right.name);
});
}
function normalizeShaderBindingDebugRow(row: {
name: string;
group: number;
binding: number;
owner: 'application' | 'module';
moduleName?: string;
accessDeclaration?: string;
resourceType: string;
}): ShaderBindingDebugRow {
const baseRow: ShaderBindingDebugRow = {
name: row.name,
group: row.group,
binding: row.binding,
owner: row.owner,
kind: 'unknown',
moduleName: row.moduleName,
resourceType: row.resourceType
};
if (row.accessDeclaration) {
const access = row.accessDeclaration.split(',').map(value => value.trim());
if (access[0] === 'uniform') {
return {...baseRow, kind: 'uniform', access: 'uniform'};
}
if (access[0] === 'storage') {
const storageAccess = access[1] || 'read_write';
return {
...baseRow,
kind: storageAccess === 'read' ? 'read-only-storage' : 'storage',
access: storageAccess
};
}
}
if (row.resourceType === 'sampler' || row.resourceType === 'sampler_comparison') {
return {
...baseRow,
kind: 'sampler',
samplerKind: row.resourceType === 'sampler_comparison' ? 'comparison' : 'filtering'
};
}
if (row.resourceType.startsWith('texture_storage_')) {
return {
...baseRow,
kind: 'storage-texture',
access: getStorageTextureAccess(row.resourceType),
viewDimension: getTextureViewDimension(row.resourceType)
};
}
if (row.resourceType.startsWith('texture_')) {
return {
...baseRow,
kind: 'texture',
viewDimension: getTextureViewDimension(row.resourceType),
sampleType: getTextureSampleType(row.resourceType),
multisampled: row.resourceType.startsWith('texture_multisampled_')
};
}
return baseRow;
}
function getBindingAssignmentKey(name: string, group: number, binding: number): string {
return `${group}:${binding}:${name}`;
}
function getTextureViewDimension(resourceType: string): string | undefined {
if (resourceType.includes('cube_array')) {
return 'cube-array';
}
if (resourceType.includes('2d_array')) {
return '2d-array';
}
if (resourceType.includes('cube')) {
return 'cube';
}
if (resourceType.includes('3d')) {
return '3d';
}
if (resourceType.includes('2d')) {
return '2d';
}
if (resourceType.includes('1d')) {
return '1d';
}
return undefined;
}
function getTextureSampleType(resourceType: string): string | undefined {
if (resourceType.startsWith('texture_depth_')) {
return 'depth';
}
if (resourceType.includes('<i32>')) {
return 'sint';
}
if (resourceType.includes('<u32>')) {
return 'uint';
}
if (resourceType.includes('<f32>')) {
return 'float';
}
return undefined;
}
function getStorageTextureAccess(resourceType: string): string | undefined {
const match = /,\s*([A-Za-z_][A-Za-z0-9_]*)\s*>$/.exec(resourceType);
return match?.[1];
}