// 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(); 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('')) { return 'sint'; } if (resourceType.includes('')) { return 'uint'; } if (resourceType.includes('')) { 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]; }