diff --git a/javascript/MaterialXView/source/helper.js b/javascript/MaterialXView/source/helper.js index 417512f666..a50153e7d3 100644 --- a/javascript/MaterialXView/source/helper.js +++ b/javascript/MaterialXView/source/helper.js @@ -21,7 +21,9 @@ export function prepareEnvTexture(texture, capabilities) { let newTexture = new THREE.DataTexture(texture.image.data, texture.image.width, texture.image.height, texture.format, texture.type); newTexture.wrapS = THREE.RepeatWrapping; - newTexture.anisotropy = capabilities.getMaxAnisotropy(); + // WebGLRenderer exposes getMaxAnisotropy(); guard for WebGPURenderer capabilities. + newTexture.anisotropy = (capabilities && typeof capabilities.getMaxAnisotropy === 'function') + ? capabilities.getMaxAnisotropy() : 1; newTexture.minFilter = THREE.LinearMipmapLinearFilter; newTexture.magFilter = THREE.LinearFilter; newTexture.generateMipmaps = true; @@ -270,7 +272,9 @@ export function getLightRotation() export function findLights(doc) { let lights = []; - for (let node of doc.getNodes()) + const nodes = doc.getNodes(); + if (!nodes) return lights; + for (let node of nodes) { if (node.getType() === "lightshader") lights.push(node); @@ -292,6 +296,7 @@ export function registerLights(mx, lights, genContext) const lightTypesBound = {}; const lightData = []; let lightId = 1; + if (!lights) return lightData; for (let light of lights) { let nodeDef = light.getNodeDef(); diff --git a/javascript/MaterialXView/source/index.js b/javascript/MaterialXView/source/index.js index 2840261a40..9998993147 100644 --- a/javascript/MaterialXView/source/index.js +++ b/javascript/MaterialXView/source/index.js @@ -8,7 +8,15 @@ import { Viewer } from './viewer.js' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { dropHandler, dragOverHandler, setLoadingCallback, setSceneLoadingCallback } from './dropHandling.js'; +// Rendering backend, selected at build time per bundle (see webpack.config.js). The WebGL +// bundle uses classic THREE.WebGLRenderer + RawShaderMaterial (ESSL); the WebGPU bundle +// aliases `three` to three/webgpu and uses WebGPURenderer + NodeMaterial (WGSL via the +// upstream WgslShaderGenerator). A toggle switches between the two HTML pages. +const BACKEND = (typeof __BACKEND__ !== 'undefined') ? __BACKEND__ : 'webgl'; +let TSL = null; // three/tsl namespace, loaded for the WebGPU backend only + let renderer, orbitControls; +let webgpuSupported = null; // cached result of navigator.gpu probe // FPS overlay state let fpsOverlay = null; @@ -50,8 +58,32 @@ function setFPSOverlayVisible(visible) { createFPSOverlay(); setFPSOverlayVisible(showFPS); -init(); -viewer.getEditor().updateProperties(0.9); +probeWebGPU().then(() => +{ + init(); + viewer.getEditor().updateProperties(0.9); +}); + +/** Probe WebGPU availability once; used by the backend toggle and init fallback. */ +async function probeWebGPU() +{ + if (webgpuSupported !== null) return webgpuSupported; + if (!navigator.gpu) + { + webgpuSupported = false; + return false; + } + try + { + const adapter = await navigator.gpu.requestAdapter(); + webgpuSupported = !!adapter; + } + catch + { + webgpuSupported = false; + } + return webgpuSupported; +} // Capture the current frame and save an image file. function captureFrame() @@ -94,11 +126,26 @@ function init() // Set up scene scene.initialize(); - // Set up renderer - renderer = new THREE.WebGLRenderer({ antialias: true, canvas }); + // Set up renderer for the selected backend. + if (BACKEND === 'webgpu') + { + if (!webgpuSupported) + { + showWebGPUFallbackBanner(); + return; + } + renderer = new THREE.WebGPURenderer({ antialias: true, canvas }); + } + else + { + renderer = new THREE.WebGLRenderer({ antialias: true, canvas }); + renderer.debug.checkShaderErrors = false; + } renderer.setSize(window.innerWidth, window.innerHeight); + // WebGL encodes sRGB in the pixel shader; WebGPU outputs linear and relies on this conversion. renderer.outputColorSpace = THREE.SRGBColorSpace; - renderer.debug.checkShaderErrors = false; + + addBackendToggle(webgpuSupported); window.addEventListener('resize', onWindowResize); @@ -134,8 +181,17 @@ function init() .then(({ default: MaterialX }) => MaterialX()) ]).then(async ([radianceTexture, irradianceTexture, lightRigXml, mxIn]) => { + // WebGPU: load the TSL namespace (used by the WGSL→NodeMaterial bridge) and + // initialize the device before the first render. + if (BACKEND === 'webgpu') + { + TSL = await import('three/tsl'); + await renderer.init(); + } + // Initialize viewer + lighting - await viewer.initialize(mxIn, renderer, radianceTexture, irradianceTexture, lightRigXml); + await viewer.initialize(mxIn, renderer, radianceTexture, irradianceTexture, lightRigXml, + { backend: BACKEND, TSL }); // Load geometry let scene = viewer.getScene(); @@ -154,7 +210,13 @@ function init() animate(); }).catch(err => { - console.error(Number.isInteger(err) ? this.getMx().getExceptionMessage(err) : err); + const mx = viewer.getMx(); + const message = (Number.isInteger(err) && mx) + ? mx.getExceptionMessage(err) + : (err && err.message ? err.message : err); + console.error(message, err); + if (BACKEND === 'webgpu') + showWebGPUFallbackBanner('WebGPU initialization failed. Use the WebGL view instead.'); }) // allow dropping files and directories @@ -187,6 +249,67 @@ function onWindowResize() renderer.setSize(window.innerWidth, window.innerHeight); } +// Floating toggle to switch rendering backend. Each backend is a separate bundle/page +// (different three build), so switching navigates between index.html and index-webgpu.html +// while preserving the current ?file/?geom query so the comparison stays on the same content. +function addBackendToggle(gpuAvailable) +{ + const search = window.location.search; + const wrap = document.createElement('div'); + // Bottom-center: clear of the material/geometry selectors (top-left), the property + // editor (top-right), and the FPS overlay (bottom-left). + wrap.style.cssText = 'position:fixed;bottom:10px;left:50%;transform:translateX(-50%);z-index:1000;' + + 'display:flex;gap:4px;flex-wrap:wrap;align-items:center;' + + 'font-family:sans-serif;font-size:12px;background:rgba(0,0,0,0.5);padding:4px 6px;border-radius:4px;max-width:90vw;'; + const label = document.createElement('span'); + label.textContent = 'Renderer:'; + label.style.cssText = 'color:#ccc;align-self:center;'; + wrap.appendChild(label); + + for (const [name, page] of [['WebGL', 'index.html'], ['WebGPU', 'index-webgpu.html']]) + { + const isWebGPU = (name === 'WebGPU'); + const active = (name.toLowerCase() === BACKEND); + const disabled = isWebGPU && gpuAvailable === false; + const btn = document.createElement('button'); + btn.textContent = name; + btn.title = disabled + ? 'WebGPU is not available in this browser' + : (isWebGPU + ? 'WebGPU uses MaterialX WGSL + TSL NodeMaterial (same light rig and IBL as WebGL)' + : 'WebGL uses full MaterialX light rig + GLSL ES shaders'); + btn.style.cssText = 'border:none;border-radius:3px;padding:3px 8px;' + + (active ? 'background:#4a9;color:#fff;font-weight:bold;' : + disabled ? 'background:#222;color:#666;cursor:not-allowed;' : + 'background:#333;color:#bbb;cursor:pointer;'); + if (!active && !disabled) + { + btn.addEventListener('click', () => + { + btn.textContent = '…'; + btn.disabled = true; + window.location.href = page + search; + }); + } + wrap.appendChild(btn); + } + document.body.appendChild(wrap); +} + +function showWebGPUFallbackBanner(message) +{ + const search = window.location.search; + const banner = document.createElement('div'); + banner.style.cssText = 'position:fixed;inset:0;display:flex;align-items:center;justify-content:center;' + + 'background:rgba(0,0,0,0.85);color:#eee;font-family:sans-serif;font-size:14px;z-index:2000;padding:24px;text-align:center;'; + const link = 'index.html' + search; + banner.innerHTML = '

' + + (message || 'WebGPU is not available in this browser.') + + '

Open WebGL view
'; + document.body.appendChild(banner); + addBackendToggle(false); +} + function animate() { requestAnimationFrame(animate); diff --git a/javascript/MaterialXView/source/mxtsladapter.js b/javascript/MaterialXView/source/mxtsladapter.js new file mode 100644 index 0000000000..c2cc08b570 --- /dev/null +++ b/javascript/MaterialXView/source/mxtsladapter.js @@ -0,0 +1,1190 @@ +/** + * mxtsladapter + * + * Adapts MaterialX-generated WGSL into Three.js TSL / WebGPU. + * + * The MaterialX `WgslShaderGenerator` emits a complete, standard WGSL module: + * helper structs + functions, a uniform block + texture/sampler bindings declared + * with `@group(N) @binding(M)`, and a single material entry function that returns + * the surface result. Three.js never ingests such a module directly — its only raw + * WGSL entry point is `wgslFn`, which accepts exactly one function whose resources + * arrive as *parameters* (TSL owns the bindings). + * + * This module performs that reshaping ("TSL-portable WGSL"): + * - `convertToTslPortable(wgsl, manifest)` rewrites the entry function so that the + * uniform-block members and texture/sampler bindings become explicit parameters, + * and strips the now-unused `@group/@binding` declarations and uniform struct. + * Everything else (helper structs/functions) is returned verbatim as `includes`. + * - `createMxWgslMaterial({ THREE, TSL, ... })` wires the converted entry up + * with `wgslFn`, binding each parameter to the matching TSL node + * (`uniform`/`texture`/`sampler`/builtin), and returns a NodeMaterial. + * + * The manifest is emitted alongside the WGSL by the generator and makes the + * conversion deterministic (no fragile parsing of binding layouts). + */ + +// ── WGSL text utilities ───────────────────────────────────────────────────── + +function skipWs( src, i ) { + + while ( i < src.length && /\s/.test( src[ i ] ) ) i ++; + return i; + +} + +// Escape a string for safe use as a literal inside a RegExp. The function/resource names +// here are generator-emitted WGSL identifiers, but escaping keeps the matching correct if +// that ever changes and documents the intent. +function escapeRegExp( s ) { + + return s.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ); + +} + +const WGSL_TYPE_ALIGN = { + i32: 4, u32: 4, f32: 4, bool: 4, + vec2f: 8, 'vec2': 8, + vec3f: 16, 'vec3': 16, + vec4f: 16, 'vec4': 16 +}; + +const WGSL_TYPE_SIZE = { + i32: 4, u32: 4, f32: 4, bool: 4, + vec2f: 8, 'vec2': 8, + vec3f: 12, 'vec3': 12, + vec4f: 16, 'vec4': 16 +}; + +/** WGSL uniform struct layout (WebGPU host-shareable rules). */ +function layoutUniformStruct( fields ) { + + let offset = 0; + let maxAlign = 4; + const placements = []; + for ( const field of fields ) { + + const align = WGSL_TYPE_ALIGN[ field.type ] || 4; + const size = WGSL_TYPE_SIZE[ field.type ] || 4; + offset = Math.ceil( offset / align ) * align; + placements.push( { name: field.name, type: field.type, offset } ); + offset += size; + maxAlign = Math.max( maxAlign, align ); + + } + const structSize = Math.ceil( offset / maxAlign ) * maxAlign; + return { placements, structSize, vec4Stride: structSize / 16 }; + +} + +/** Parse `array` from a manifest uniform type string. */ +function parseStructArrayType( typeStr, wgslSource, numLights, lightData, layout = null ) { + + const type = typeStr || ''; + const flatVec4 = type.match( /array/ ); + if ( flatVec4 ) { + + const flatCount = parseInt( flatVec4[ 1 ], 10 ); + const stride = layout?.vec4Stride || 1; + return { + structName: 'LightData', + count: Math.ceil( flatCount / stride ), + vec4Stride: stride, + flatCount + }; + + } + const numeric = type.match( /array<(\w+),\s*(\d+)>/ ); + if ( numeric ) { + + const count = parseInt( numeric[ 2 ], 10 ); + return { + structName: numeric[ 1 ], + count, + vec4Stride: layout?.vec4Stride || 1, + flatCount: layout ? count * layout.vec4Stride : count + }; + + } + const symbolic = type.match( /array<(\w+),\s*(\w+)>/ ); + const structName = symbolic ? symbolic[ 1 ] : 'LightData'; + let count = Math.max( numLights || 0, lightData?.length || 0, 1 ); + if ( symbolic && wgslSource ) { + + const constRe = new RegExp( `const\\s+${ escapeRegExp( symbolic[ 2 ] ) }:\\s*i32\\s*=\\s*(\\d+)` ); + const m = wgslSource.match( constRe ); + if ( m ) count = parseInt( m[ 1 ], 10 ); + + } + const vec4Stride = layout?.vec4Stride || 1; + return { structName, count, vec4Stride, flatCount: count * vec4Stride }; + +} + +/** Parse `struct LightData { ... }` from generated WGSL. */ +export function parseLightDataStruct( wgsl ) { + + const m = wgsl.match( /struct\s+LightData\s*\{([^}]+)\}/ ); + if ( ! m ) return null; + const fields = []; + for ( const line of m[ 1 ].split( '\n' ) ) { + + const trimmed = line.trim().replace( /,$/, '' ); + if ( ! trimmed ) continue; + const colon = trimmed.indexOf( ':' ); + if ( colon < 0 ) continue; + fields.push( { + name: trimmed.slice( 0, colon ).trim(), + type: trimmed.slice( colon + 1 ).trim() + } ); + + } + return fields.length ? layoutUniformStruct( fields ) : null; + +} + +/** + * TSL `uniformArray` only binds `array`. Flatten `array` to + * `array` and add an unpack helper for indexed struct access. + */ +function flattenLightDataUniform( wgsl ) { + + const layout = parseLightDataStruct( wgsl ); + if ( ! layout ) return { wgsl, layout: null }; + + const maxMatch = wgsl.match( /array/ ); + let maxCount = 1; + if ( maxMatch ) { + + const sym = maxMatch[ 1 ]; + const constM = wgsl.match( new RegExp( `const\\s+${ escapeRegExp( sym ) }:\\s*i32\\s*=\\s*(\\d+)` ) ); + maxCount = constM ? parseInt( constM[ 1 ], 10 ) : 1; + + } + const flatCount = maxCount * layout.vec4Stride; + const stride = layout.vec4Stride; + + const unpackLines = [ 'var out: LightData;' ]; + for ( const p of layout.placements ) { + + const vec4Index = Math.floor( p.offset / 16 ); + const comp = [ 'x', 'y', 'z', 'w' ][ ( p.offset % 16 ) / 4 ]; + if ( p.type === 'vec3f' || p.type === 'vec3' ) { + + unpackLines.push( `out.${ p.name } = data[base + ${ vec4Index }u].xyz;` ); + + } else if ( p.type === 'i32' ) { + + unpackLines.push( `out.${ p.name } = bitcast(data[base + ${ vec4Index }u].${ comp });` ); + + } else if ( p.type === 'f32' ) { + + unpackLines.push( `out.${ p.name } = data[base + ${ vec4Index }u].${ comp };` ); + + } + + } + const helper = [ + `fn mx_lightData_at(data: array, index: i32) -> LightData {`, + `\tlet base: u32 = u32(index) * ${ stride }u;`, + ...unpackLines.map( ( l ) => `\t${ l }` ), + '\treturn out;', + '}' + ].join( '\n' ); + + let out = wgsl.replace( /struct\s+LightData\s*\{/, `${ helper }\n\nstruct LightData {` ); + out = out.replace( /array]+>/g, `array` ); + out = out.replace( /u_lightData\[([^\]]+)\]/g, 'mx_lightData_at(u_lightData, $1)' ); + return { wgsl: out, layout }; + +} + +function vec3ToArray( v ) { + + if ( Array.isArray( v ) ) return v; + if ( v && v.x != null ) return [ v.x, v.y, v.z ]; + return [ 0, 0, 0 ]; + +} + +// Reused buffer for packing i32 fields into vec4f slots (WGSL unpack uses bitcast). +const _intBitsView = new DataView( new ArrayBuffer( 4 ) ); + +/** Store an i32 in a float32 uniform slot preserving bit pattern for WGSL bitcast. */ +function floatFromIntBits( i32 ) { + + _intBitsView.setInt32( 0, i32 | 0, true ); + return _intBitsView.getFloat32( 0, true ); + +} + +/** + * Pack light slots into a flat vec4 array matching WGSL uniform struct layout. + * Inactive slots are zeroed so the WGSL light loop can skip them via u_numActiveLightSources. + */ +export function packLightDataToVec4Array( lightData, maxCount, layout ) { + + const vec4Count = maxCount * layout.vec4Stride; + const floats = new Array( vec4Count * 4 ).fill( 0 ); + const floatStride = layout.vec4Stride * 4; + for ( let i = 0; i < maxCount; i ++ ) { + + const l = lightData && lightData[ i ]; + const base = i * floatStride; + if ( ! l ) continue; + for ( const p of layout.placements ) { + + const floatIndex = base + p.offset / 4; + if ( p.type === 'i32' || p.type === 'u32' ) { + + let intVal = 0; + if ( p.name === 'light_type' || p.name === 'type' ) { + + intVal = l.light_type != null ? l.light_type : ( l.type | 0 ); + + } else if ( l[ p.name ] != null ) { + + intVal = l[ p.name ] | 0; + + } + floats[ floatIndex ] = floatFromIntBits( intVal ); + + } else if ( p.name === 'direction' ) { + + const d = vec3ToArray( l.direction ); + floats[ floatIndex ] = d[ 0 ]; + floats[ floatIndex + 1 ] = d[ 1 ]; + floats[ floatIndex + 2 ] = d[ 2 ]; + + } else if ( p.name === 'color' ) { + + const c = vec3ToArray( l.color ); + floats[ floatIndex ] = c[ 0 ]; + floats[ floatIndex + 1 ] = c[ 1 ]; + floats[ floatIndex + 2 ] = c[ 2 ]; + + } else if ( p.name === 'intensity' ) { + + floats[ floatIndex ] = l.intensity != null ? l.intensity : 0; + + } + + } + + } + const out = []; + for ( let i = 0; i < vec4Count; i ++ ) { + + const b = i * 4; + out.push( { x: floats[ b ], y: floats[ b + 1 ], z: floats[ b + 2 ], w: floats[ b + 3 ] } ); + + } + return out; + +} + +/** + * Extract a top-level `fn ( ) -> { }` by name. + * Returns { start, end, params, ret, body } where [start,end) spans the whole + * definition in `src`. Throws if not found. + */ +function extractFunction( src, name ) { + + const re = new RegExp( `\\bfn\\s+${ escapeRegExp( name ) }\\s*\\(`, 'g' ); + const m = re.exec( src ); + if ( m === null ) throw new Error( `mxtsladapter: entry function '${ name }' not found` ); + + const start = m.index; + let i = m.index + m[ 0 ].length - 1; // at '(' + + // Match the parameter-list parentheses. + let depth = 0; + const paramStart = i + 1; + let paramEnd = - 1; + for ( ; i < src.length; i ++ ) { + + if ( src[ i ] === '(' ) depth ++; + else if ( src[ i ] === ')' ) { depth --; if ( depth === 0 ) { paramEnd = i; break; } } + + } + + if ( paramEnd < 0 ) throw new Error( `mxtsladapter: unterminated parameter list for '${ name }'` ); + + const params = src.slice( paramStart, paramEnd ); + + // Optional `-> type` then the body braces. + let j = skipWs( src, paramEnd + 1 ); + let ret = ''; + if ( src[ j ] === '-' && src[ j + 1 ] === '>' ) { + + j = skipWs( src, j + 2 ); + const retStart = j; + while ( j < src.length && src[ j ] !== '{' ) j ++; + ret = src.slice( retStart, j ).trim(); + + } + + if ( src[ j ] !== '{' ) throw new Error( `mxtsladapter: missing body for '${ name }'` ); + + const bodyStart = j; + depth = 0; + let bodyEnd = - 1; + for ( ; j < src.length; j ++ ) { + + if ( src[ j ] === '{' ) depth ++; + else if ( src[ j ] === '}' ) { depth --; if ( depth === 0 ) { bodyEnd = j + 1; break; } } + + } + + if ( bodyEnd < 0 ) throw new Error( `mxtsladapter: unterminated body for '${ name }'` ); + + return { + start, + end: bodyEnd, + params: params.trim(), + ret, + body: src.slice( bodyStart + 1, bodyEnd - 1 ) // inside the outer braces + }; + +} + +/** Remove every line that declares a `@group(...) @binding(...)` resource. */ +function removeBindingDecls( src ) { + + return src + .split( '\n' ) + .filter( ( line ) => ! /^\s*@group\s*\(/.test( line ) ) + .join( '\n' ); + +} + +/** Index of the `)` matching the `(` at `openIdx` in `src`. */ +function matchParen( src, openIdx ) { + + let depth = 0; + for ( let i = openIdx; i < src.length; i ++ ) { + + if ( src[ i ] === '(' ) depth ++; + else if ( src[ i ] === ')' ) { depth --; if ( depth === 0 ) return i; } + + } + return - 1; + +} + +/** + * Thread private/global HW resources through the call chain. + * + * The MaterialX library functions (IBL, transmission, lighting) reference the HW private + * uniforms + env maps as *module-scope globals* (`u_envMatrix`, `u_viewPosition`, + * `u_lightData`, the env textures, …). Three.js `wgslFn` forbids module-scope resources in + * `includes`, so any function that (transitively) touches one of them must receive it as a + * parameter, and every call site must forward it. + * + * Only these private/global resources are threaded. Public material inputs (semantic + * `uniform`) and material image textures (semantic `texture`/`sampler`) must NOT be threaded: + * they are part of the MaterialX node interface and are already passed explicitly down the + * node-graph call chain as named parameters (e.g. `NG_..._surfaceshader_100(base, base_color, …)`). + * Threading them would re-append them to functions that already declare them as parameters or + * use them as local variable names, producing WGSL "redeclaration of '…'" errors. + * + * The entry function is left unmodified — `convertToTslPortable` adds the resources to it as + * parameters from the manifest, and call sites in the entry body forward them. + * + * @param {string} wgsl - The full generated WGSL module. + * @param {Object} manifest - Generator manifest (uniforms + textures after normalization). + * @return {string} The rewritten module (unchanged if there are no private resources). + */ +export function threadEnvResources( wgsl, manifest ) { + + // The generator references geometry varyings through the MaterialX vertex-data instance + // `vd` (e.g. `vd.normalWorld`). The entry exposes the `VertexData` members as flat + // parameters (and the assembler binds them to TSL builtins), so flatten the struct access + // to bare member names module-wide. The varyings are then threaded like any other global + // below, so library helpers (e.g. the surface-shader node) that read them get them as + // parameters forwarded from the entry. `\bvd\.` only matches the vertex-data instance. + wgsl = wgsl.replace( /\bvd\./g, '' ); + + // A resource is "private/global" (referenced as a module-scope global inside library + // helpers, hence threaded) when it is an env map or a non-public uniform. Public material + // inputs (`uniform`) and material image textures (`texture`/`sampler`) flow through the + // node-graph parameter chain and are excluded. + const isPrivateTexture = ( t ) => ( t.semantic || '' ).startsWith( 'env:' ); + const isPrivateUniform = ( u ) => ( u.semantic || 'uniform' ) !== 'uniform'; + + // Ordered resource list (signature decls + call args), data-driven from the manifest. + const resourceParts = []; + + // Vertex-data varyings (now bare member names after the `vd.` flatten above). These are + // genuine module inputs that helpers reference globally, so they must be threaded too. + for ( const p of manifest.entryParams || [] ) { + + if ( ! ( p.semantic || '' ).startsWith( 'varying:' ) ) continue; + resourceParts.push( { decl: `${ p.name }: ${ p.type }`, arg: p.name } ); + + } + for ( const t of manifest.textures || [] ) { + + if ( ! isPrivateTexture( t ) ) continue; + resourceParts.push( { decl: `${ t.texture }: ${ t.wgslType }`, arg: t.texture } ); + resourceParts.push( { decl: `${ t.sampler }: sampler`, arg: t.sampler } ); + + } + for ( const u of manifest.uniforms || [] ) { + + if ( ! isPrivateUniform( u ) ) continue; + resourceParts.push( { decl: `${ u.name }: ${ u.type }`, arg: u.name } ); + + } + if ( resourceParts.length === 0 ) return wgsl; + + const resourceNames = resourceParts.map( ( p ) => p.arg ); + const paramsStr = resourceParts.map( ( p ) => p.decl ).join( ', ' ); + const argsStr = resourceParts.map( ( p ) => p.arg ).join( ', ' ); + + // Enumerate all top-level `fn name( params ) ... { body }` definitions. + const fnRe = /\bfn\s+([A-Za-z_]\w*)\s*\(/g; + const fns = []; + for ( let m = fnRe.exec( wgsl ); m !== null; m = fnRe.exec( wgsl ) ) { + + const openParen = m.index + m[ 0 ].length - 1; + const closeParen = matchParen( wgsl, openParen ); + const braceOpen = wgsl.indexOf( '{', closeParen ); + // Match the body braces to know where this function ends. + let depth = 0, braceEnd = - 1; + for ( let i = braceOpen; i < wgsl.length; i ++ ) { + + if ( wgsl[ i ] === '{' ) depth ++; + else if ( wgsl[ i ] === '}' ) { depth --; if ( depth === 0 ) { braceEnd = i; break; } } + + } + fns.push( { name: m[ 1 ], openParen, closeParen, bodyStart: braceOpen, bodyEnd: braceEnd } ); + + } + + const byName = Object.fromEntries( fns.map( ( f ) => [ f.name, f ] ) ); + const bodyOf = ( f ) => wgsl.slice( f.bodyStart, f.bodyEnd + 1 ); + + // Fixpoint: a function "needs" resources if its body references one directly, or + // calls another function that needs them. + const needs = new Set(); + const refsResource = ( body ) => resourceNames.some( ( n ) => new RegExp( `\\b${ escapeRegExp( n ) }\\b` ).test( body ) ); + for ( const f of fns ) if ( refsResource( bodyOf( f ) ) ) needs.add( f.name ); + + for ( let changed = true; changed; ) { + + changed = false; + for ( const f of fns ) { + + if ( needs.has( f.name ) ) continue; + const body = bodyOf( f ); + for ( const callee of needs ) { + + if ( new RegExp( `\\b${ escapeRegExp( callee ) }\\s*\\(` ).test( body ) ) { needs.add( f.name ); changed = true; break; } + + } + + } + + } + + if ( needs.size === 0 ) return wgsl; + + const entryName = manifest.entry; + + // Collect insertions (index -> text). Apply right-to-left so indices stay valid. + const inserts = []; + + // 1. Append params to the signature of each needing function (except the entry). + for ( const f of fns ) { + + if ( ! needs.has( f.name ) || f.name === entryName ) continue; + const hasParams = wgsl.slice( f.openParen + 1, f.closeParen ).trim().length > 0; + inserts.push( { at: f.closeParen, text: ( hasParams ? ', ' : '' ) + paramsStr } ); + + } + + // 2. Append args at every call site of a needing function (anywhere in the module). + for ( const callee of needs ) { + + const callRe = new RegExp( `\\b${ escapeRegExp( callee ) }\\s*\\(`, 'g' ); + for ( let m = callRe.exec( wgsl ); m !== null; m = callRe.exec( wgsl ) ) { + + // Skip the definition itself (`fn callee(`). + const before = wgsl.slice( Math.max( 0, m.index - 4 ), m.index ); + if ( /\bfn\s$/.test( before ) ) continue; + const open = m.index + m[ 0 ].length - 1; + const close = matchParen( wgsl, open ); + if ( close < 0 ) continue; + const hasArgs = wgsl.slice( open + 1, close ).trim().length > 0; + inserts.push( { at: close, text: ( hasArgs ? ', ' : '' ) + argsStr } ); + + } + + } + + inserts.sort( ( a, b ) => b.at - a.at ); + let out = wgsl; + for ( const ins of inserts ) out = out.slice( 0, ins.at ) + ins.text + out.slice( ins.at ); + return out; + +} + +// ── Reflection normalization (neutral C++ → TSL semantics) ───────────────── + +function varyingSemantic( name ) { + + if ( name.includes( 'normal' ) ) return 'varying:normalWorld'; + if ( name.includes( 'tangent' ) ) return 'varying:tangentWorld'; + if ( name.includes( 'position' ) ) return 'varying:positionWorld'; + return 'varying:uv'; + +} + +function buildTslSemantics( binding ) { + + const { name, role } = binding; + const HOST = { + u_viewPosition: 'camera:viewPosition', + u_numActiveLightSources: 'light:count', + u_lightData: 'light:data', + u_envMatrix: 'env:matrix', + u_envRadianceMips: 'env:radianceMips', + u_envRadianceSamples: 'env:radianceSamples', + u_envLightIntensity: 'env:lightIntensity' + }; + if ( HOST[ name ] ) return HOST[ name ]; + if ( name === 'u_lightDirection' ) return 'light:direction'; + if ( name === 'u_lightColor' ) return 'light:color'; + if ( role === 'lightData' ) return 'light:data'; + if ( role === 'uniform' ) return 'uniform'; + if ( role === 'texture' ) { + + if ( name.startsWith( 'u_envRadiance' ) ) return 'env:radiance'; + if ( name.startsWith( 'u_envIrradiance' ) ) return 'env:irradiance'; + return 'texture'; + + } + if ( role === 'sampler' ) { + + if ( name.startsWith( 'u_envRadiance' ) ) return 'env:radiance:sampler'; + if ( name.startsWith( 'u_envIrradiance' ) ) return 'env:irradiance:sampler'; + return 'sampler'; + + } + if ( role === 'host' ) return HOST[ name ] || 'host'; + return 'uniform'; + +} + +/** + * Normalize generator reflection into the legacy manifest shape expected by the + * TSL bridge. Accepts both the old manifest (entry string + uniforms/textures) + * and the new neutral format (entry object + bindings + struct entryParams). + * + * @param {Object} reflection - Generator reflection or legacy manifest. + * @return {Object} Manifest with TSL semantics on uniforms, textures, entryParams. + */ +export function normalizeReflection( reflection ) { + + if ( ! reflection ) return reflection; + + // Legacy manifests already carry TSL semantics — return unchanged. + if ( reflection.uniforms?.some( ( u ) => u.semantic ) || reflection.textures?.some( ( t ) => t.semantic ) ) + return reflection; + + if ( ! reflection.bindings ) return reflection; + + const entry = typeof reflection.entry === 'string' + ? reflection.entry + : reflection.entry?.pixel; + + const uniforms = []; + const textures = []; + const textureKeys = new Set(); + + for ( const b of reflection.bindings ) { + + const semantic = buildTslSemantics( b ); + if ( b.role === 'texture' ) { + + const key = b.key || b.name.replace( /_texture$/, '' ); + textures.push( { + key, + texture: b.name, + sampler: null, + wgslType: b.type, + semantic, + file: b.file + } ); + textureKeys.add( key ); + + } else if ( b.role === 'sampler' ) { + + const key = b.key || b.name.replace( /_sampler$/, '' ); + const tex = textures.find( ( t ) => t.key === key ); + if ( tex ) tex.sampler = b.name; + + } else { + + uniforms.push( { + name: b.name, + type: b.type, + semantic, + value: b.value + } ); + + } + + } + + for ( const t of textures ) { + + if ( ! t.sampler ) t.sampler = `${ t.key }_sampler`; + + } + + let entryParams = reflection.entryParams || []; + if ( entryParams.length === 1 && entryParams[ 0 ].members ) { + + entryParams = entryParams[ 0 ].members.map( ( m ) => ( { + name: m.name, + type: m.type, + semantic: varyingSemantic( m.name ) + } ) ); + + } else { + + entryParams = entryParams.map( ( p ) => ( { + ...p, + semantic: p.semantic || varyingSemantic( p.name ) + } ) ); + + } + + return { + entry, + output: reflection.output, + entryParams, + uniforms, + textures + }; + +} + +// ── Conversion ────────────────────────────────────────────────────────────── + +/** + * Convert a complete MaterialX WGSL module into a TSL-portable form. + * + * @param {string} wgsl - The generated WGSL module. + * @param {Object} manifest - Resource description emitted by the generator. + * @return {{ name:string, entry:string, includes:string, params:Array }} + */ +export function convertToTslPortable( wgsl, manifest ) { + + manifest = normalizeReflection( manifest ); + + // Thread module-scope resources through the call chain so `includes` receive them + // as parameters rather than globals (required by Three.js `wgslFn`). + wgsl = threadEnvResources( wgsl, manifest ); + + // Three.js uniformArray only supports primitive array types (vec4/mat4), not struct arrays. + const lightFlatten = flattenLightDataUniform( wgsl ); + wgsl = lightFlatten.wgsl; + const lightLayout = lightFlatten.layout; + + const entryName = manifest.entry; + const fn = extractFunction( wgsl, entryName ); + + const uniforms = manifest.uniforms || []; + const textures = manifest.textures || []; + const entryParams = manifest.entryParams || []; + + // The generated entry already takes the geometry varyings as parameters and + // references uniforms/textures by bare name as module-scope `@group/@binding` + // resources. Hoist those resources into the parameter list (bodies use bare + // names, so no in-body rewriting is needed) and strip the binding declarations. + const newParams = []; + const params = []; + + // 1. Existing entry parameters (varyings). Neutral reflection may pass vertex + // data as a single struct parameter — flatten it for wgslFn and rewrite the body. + const semanticByName = Object.fromEntries( entryParams.map( ( p ) => [ p.name, p.semantic ] ) ); + const flatParams = splitParams( fn.params ); + const isStructEntry = flatParams.length === 1 && entryParams.length > 0 && + entryParams[ 0 ].name !== flatParams[ 0 ].name; + let body = fn.body; + if ( isStructEntry ) { + + const structName = flatParams[ 0 ].name; + for ( const m of entryParams ) { + + newParams.push( `${ m.name }: ${ m.type }` ); + params.push( { name: m.name, type: m.type, semantic: m.semantic || varyingSemantic( m.name ) } ); + + } + body = body.replace( new RegExp( `\\b${ escapeRegExp( structName ) }\\.`, 'g' ), '' ); + + } else { + + for ( const p of flatParams ) { + + newParams.push( `${ p.name }: ${ p.type }` ); + params.push( { name: p.name, type: p.type, semantic: semanticByName[ p.name ] || 'varying:uv' } ); + + } + + } + + // 2. Uniforms become individual parameters (bare names already used in body). + // Most carry semantic 'uniform'; surface lighting inputs carry camera:/light:. + for ( const u of uniforms ) { + + let paramType = u.type; + if ( ( u.semantic || '' ) === 'light:data' && lightLayout ) { + + const { flatCount } = parseStructArrayType( u.type, wgsl, null, null, lightLayout ); + paramType = `array`; + + } + newParams.push( `${ u.name }: ${ paramType }` ); + params.push( { name: u.name, type: paramType, semantic: u.semantic || 'uniform', value: u.value } ); + + } + + // 3. Each texture contributes a texture + sampler parameter pair. Material image + // textures carry semantic 'texture'; env (IBL) maps carry 'env:radiance' / + // 'env:irradiance' so the assembler can bind them from the environment instead. + for ( const tex of textures ) { + + const texSemantic = tex.semantic && tex.semantic.startsWith( 'env:' ) ? tex.semantic : 'texture'; + const sampSemantic = texSemantic === 'texture' ? 'sampler' : texSemantic + ':sampler'; + newParams.push( `${ tex.texture }: ${ tex.wgslType }` ); + newParams.push( `${ tex.sampler }: sampler` ); + params.push( { name: tex.texture, type: tex.wgslType, semantic: texSemantic, key: tex.key } ); + params.push( { name: tex.sampler, type: 'sampler', semantic: sampSemantic, key: tex.key } ); + + } + + const retArrow = fn.ret ? ` -> ${ fn.ret }` : ''; + const entry = `fn ${ entryName }( ${ newParams.join( ', ' ) } )${ retArrow } {${ body }}`; + + // Includes: the original module minus the entry and the binding declarations. + let includes = wgsl.slice( 0, fn.start ) + wgsl.slice( fn.end ); + includes = removeBindingDecls( includes ).trim(); + + return { name: entryName, entry, includes, params }; + +} + +/** Split a WGSL parameter list ("a: T1, b: T2") into [{name,type}]. */ +function splitParams( params ) { + + const out = []; + let depth = 0, start = 0; + const pieces = []; + for ( let i = 0; i < params.length; i ++ ) { + + const c = params[ i ]; + if ( c === '<' || c === '(' ) depth ++; + else if ( c === '>' || c === ')' ) depth --; + else if ( c === ',' && depth === 0 ) { pieces.push( params.slice( start, i ) ); start = i + 1; } + + } + const tail = params.slice( start ).trim(); + if ( tail ) pieces.push( tail ); + + for ( const piece of pieces ) { + + const colon = piece.indexOf( ':' ); + if ( colon < 0 ) continue; + out.push( { name: piece.slice( 0, colon ).trim(), type: piece.slice( colon + 1 ).trim() } ); + + } + return out; + +} + +// ── Material assembly ──────────────────────────────────────────────────────── + +/** + * Build a Three.js NodeMaterial from MaterialX WGSL + manifest. + * + * @param {Object} options + * @param {Object} options.THREE - The `three/webgpu` namespace. + * @param {Object} options.TSL - The `three/tsl` namespace. + * @param {string} options.wgsl - Generated WGSL module. + * @param {Object} options.manifest - Generator manifest. + * @param {Object} [options.textures] - Map of texture key -> Texture. + * @param {Node} [options.uvNode] - Optional uv node (defaults to TSL `uv()`). + * @param {boolean} [options.useGeometryTangent] - Use TSL `tangentWorld` when geometry tangents exist. + * @return {NodeMaterial} + */ +export function createMxWgslMaterial( { THREE, TSL, wgsl, manifest, textures = {}, uvNode = null, light = null, lightData = null, numLights = null, environment = null, useGeometryTangent = false } ) { + + manifest = normalizeReflection( manifest ); + + const { wgslFn, wgsl: wgslCode, uniform, uniformArray, texture, sampler, uv, normalWorld, positionWorld, cameraPosition, tangentWorld: tangentWorldBuiltin, vec3, float, select } = TSL; + + // IBL environment: equirect radiance (mipped) + irradiance maps and FIS parameters. + // When the shader was generated with FIS but no environment is supplied, bind a 1×1 + // black fallback so the module still compiles (IBL simply contributes nothing). + const env = environment || {}; + const fallbackEnvTex = () => { + + const t = new THREE.DataTexture( new Uint8Array( [ 0, 0, 0, 255 ] ), 1, 1 ); + t.needsUpdate = true; + return t; + + }; + const envRadianceTex = env.radiance || fallbackEnvTex(); + const envIrradianceTex = env.irradiance || envRadianceTex; + // HDR env maps must stay linear (see viewer buildEnvironment). + envRadianceTex.colorSpace = THREE.NoColorSpace; + envIrradianceTex.colorSpace = THREE.NoColorSpace; + const envTexFor = ( semantic ) => semantic.startsWith( 'env:irradiance' ) ? envIrradianceTex : envRadianceTex; + const fallbackMatTex = () => { + + const t = new THREE.DataTexture( new Uint8Array( [ 128, 128, 128, 255 ] ), 1, 1 ); + // DataTexture defaults to Nearest+Nearest; Three.js treats that as "unfilterable" and + // omits nodeUniformN_sampler bindings while MaterialX WGSL still passes samplers. + t.magFilter = THREE.LinearFilter; + t.minFilter = THREE.LinearFilter; + t.needsUpdate = true; + return t; + + }; + + // Three.js only emits sampler bindings for filterable textures (see WebGPUNodeBuilder + // needsSampler). MaterialX WGSL always declares matching sampler parameters. + const ensureFilterableTexture = ( tex ) => { + + if ( ! tex ) return tex; + if ( tex.magFilter === THREE.NearestFilter ) tex.magFilter = THREE.LinearFilter; + if ( tex.minFilter === THREE.NearestFilter || tex.minFilter === THREE.NearestMipmapNearestFilter ) { + + tex.minFilter = tex.generateMipmaps !== false ? THREE.LinearMipmapLinearFilter : THREE.LinearFilter; + + } + return tex; + + }; + + // A robust world-space tangent. MaterialX BSDFs orthogonalize the tangent against + // the normal (`normalize(T - dot(T,N)*N)`), which yields NaN if T is zero or parallel + // to N — and most geometries (e.g. TorusKnotGeometry) carry no tangent attribute, so + // a raw `tangentWorld` accessor would be degenerate. Derive an arbitrary orthonormal + // tangent from the normal instead; for isotropic GGX the exact direction is irrelevant. + function derivedTangentWorld() { + + const n = normalWorld.normalize(); + // Reference axis least aligned with the normal, to avoid a parallel cross product. + const ref = select( n.y.abs().lessThan( float( 0.99 ) ), vec3( 0, 1, 0 ), vec3( 1, 0, 0 ) ); + return ref.cross( n ).normalize(); + + } + // MaterialXView computes tangents for indexed geometry; use them when available for + // anisotropic parity with the WebGL path. Fall back to a derived tangent otherwise. + const tangentWorld = useGeometryTangent ? tangentWorldBuiltin : derivedTangentWorld(); + + const converted = convertToTslPortable( wgsl, manifest ); + const wgslForConsts = converted.includes + '\n' + converted.entry; + + const includesNode = converted.includes.length ? [ wgslCode( converted.includes ) ] : []; + const entryFn = wgslFn( converted.entry, includesNode ); + + const builtin = { + 'varying:uv': () => ( uvNode || uv() ), + 'varying:normalWorld': () => normalWorld, + 'varying:positionWorld': () => positionWorld, + 'varying:tangentWorld': () => tangentWorld, + 'camera:viewPosition': () => cameraPosition + }; + + const args = {}; + // One TSL texture() node per manifest texture key. wgslFn passes texture and sampler as + // separate WGSL parameters; calling texture() twice (even with the same THREE.Texture) + // creates two binding slots (nodeUniformN + nodeUniformN+1_sampler) and breaks pairing. + const textureNodes = {}; + const textureNodeFor = ( key, threeTex ) => { + + if ( ! textureNodes[ key ] ) textureNodes[ key ] = texture( ensureFilterableTexture( threeTex ) ); + return textureNodes[ key ]; + + }; + + for ( const p of converted.params ) { + + if ( p.semantic === 'uniform' ) { + + args[ p.name ] = makeUniformNode( uniform, toUniformValue( THREE, p ), p.type ); + + } else if ( p.semantic === 'light:direction' ) { + + args[ p.name ] = uniform( ( light && light.direction ) || toUniformValue( THREE, p ) ); + + } else if ( p.semantic === 'light:color' ) { + + args[ p.name ] = uniform( ( light && light.color ) || toUniformValue( THREE, p ) ); + + } else if ( p.semantic === 'light:count' ) { + + args[ p.name ] = uniform( ( numLights != null ? numLights : p.value ) | 0, 'int' ); + + } else if ( p.semantic === 'light:data' ) { + + const layout = parseLightDataStruct( wgslForConsts ); + const { count, flatCount } = parseStructArrayType( p.type, wgslForConsts, null, lightData, layout ); + const packed = layout + ? packLightDataToVec4Array( lightData || p.value, count, layout ) + : new Array( flatCount ).fill( { x: 0, y: 0, z: 0, w: 0 } ); + args[ p.name ] = uniformArray( packed, 'vec4' ); + + } else if ( p.semantic === 'host' ) { + + args[ p.name ] = makeUniformNode( uniform, toUniformValue( THREE, p ), p.type ); + + } else if ( p.semantic === 'texture' ) { + + args[ p.name ] = textureNodeFor( p.key, textures[ p.key ] || fallbackMatTex() ); + + } else if ( p.semantic === 'sampler' ) { + + args[ p.name ] = sampler( textureNodeFor( p.key, textures[ p.key ] || fallbackMatTex() ) ); + + } else if ( p.semantic === 'env:radiance' || p.semantic === 'env:irradiance' ) { + + args[ p.name ] = textureNodeFor( p.key, envTexFor( p.semantic ) ); + + } else if ( p.semantic === 'env:radiance:sampler' || p.semantic === 'env:irradiance:sampler' ) { + + const envSemantic = p.semantic.replace( ':sampler', '' ); + args[ p.name ] = sampler( textureNodeFor( p.key, envTexFor( envSemantic ) ) ); + + } else if ( p.semantic === 'env:matrix' ) { + + args[ p.name ] = uniform( env.matrix || matrixFromArray( THREE, p.value ) ); + + } else if ( p.semantic === 'env:radianceSamples' ) { + + args[ p.name ] = uniform( ( env.samples != null ? env.samples : p.value ) | 0, 'int' ); + + } else if ( p.semantic === 'env:radianceMips' ) { + + args[ p.name ] = uniform( ( env.mips != null ? env.mips : p.value ) | 0, 'int' ); + + } else if ( p.semantic === 'env:lightIntensity' ) { + + args[ p.name ] = uniform( env.intensity != null ? env.intensity : p.value ); + + } else if ( p.semantic && ( p.semantic.startsWith( 'varying:' ) || p.semantic.startsWith( 'camera:' ) ) ) { + + const make = builtin[ p.semantic ]; + if ( ! make ) throw new Error( `mxtsladapter: unknown builtin semantic '${ p.semantic }'` ); + args[ p.name ] = make(); + + } else { + + throw new Error( `mxtsladapter: unhandled parameter semantic '${ p.semantic }' for '${ p.name }'` ); + + } + + } + + const material = new THREE.MeshBasicNodeMaterial(); + // MaterialX WGSL already includes direct lights + IBL; do not run Three.js scene lighting. + material.lights = false; + material.fog = false; + material.colorNode = entryFn( args ); + // Bound argument nodes, keyed by parameter name. `createMxWgslGUI` reads these to + // drive live uniform edits, so this is a functional binding handle, not debug state. + material.userData.mxArgs = args; + return material; + +} + +function toUniformValue( THREE, p ) { + + const v = p.value; + if ( v === undefined || v === null ) { + + // Sensible zero/identity defaults by type. + if ( p.type === 'f32' || p.type === 'i32' || p.type === 'u32' ) return 0; + if ( p.type === 'vec2f' ) return new THREE.Vector2(); + if ( p.type === 'vec4f' ) return new THREE.Vector4(); + return new THREE.Vector3(); + + } + + if ( typeof v === 'boolean' && ( p.type === 'i32' || p.type === 'u32' ) ) return v ? 1 : 0; + + if ( Array.isArray( v ) ) { + + if ( v.length === 2 ) return new THREE.Vector2( v[ 0 ], v[ 1 ] ); + if ( v.length === 3 ) return new THREE.Vector3( v[ 0 ], v[ 1 ], v[ 2 ] ); + if ( v.length === 4 ) return new THREE.Vector4( v[ 0 ], v[ 1 ], v[ 2 ], v[ 3 ] ); + if ( v.length === 16 ) return matrixFromArray( THREE, v ); + + } + + return v; + +} + +/** Bind a TSL uniform node with the correct scalar type for WGSL i32/u32 host inputs. */ +function makeUniformNode( uniformFn, value, wgslType ) { + + if ( wgslType === 'i32' || wgslType === 'u32' ) return uniformFn( value | 0, 'int' ); + return uniformFn( value ); + +} + +/** Build a THREE.Matrix4 from a 16-element column-major array (WGSL mat4x4f layout). */ +function matrixFromArray( THREE, v ) { + + const m = new THREE.Matrix4(); + if ( Array.isArray( v ) && v.length === 16 ) { + + // Three stores column-major internally; WGSL mat4x4f is also column-major, so the + // array maps directly into Matrix4.elements. + m.elements = v.slice(); + + } + return m; + +} + +// ── Live parameter editor ───────────────────────────────────────────────────── + +// Slider range heuristics for scalar surface inputs, keyed by name substring. +function scalarRange( name ) { + + if ( /IOR$/.test( name ) ) return [ 1, 3 ]; + if ( /thickness/.test( name ) ) return [ 0, 2000 ]; // thin-film, nanometres + if ( /roughness|metalness|anisotropy|rotation|affect|walled/.test( name ) ) return [ 0, 1 ]; + if ( /emission|scale|depth|dispersion/.test( name ) ) return [ 0, 5 ]; + return [ 0, 1 ]; + +} + +const GUI_CATEGORIES = [ 'base', 'diffuse', 'metalness', 'specular', 'transmission', + 'subsurface', 'sheen', 'coat', 'thin_film', 'thin_walled', 'emission', 'opacity' ]; + +function guiCategory( stripped ) { + + for ( const c of GUI_CATEGORIES ) if ( stripped === c || stripped.startsWith( c + '_' ) ) return c; + return 'other'; + +} + +/** + * Build a live lil-gui editor for a material created by `createMxWgslMaterial`. + * + * Drives the bound TSL uniform nodes (`material.userData.mxArgs`) directly, so edits take + * effect on the next render with no rebuild. Surface inputs are read from the manifest and + * grouped into folders by closure (base / specular / coat / …); the directional light and + * environment intensity get their own folder. + * + * @param {Object} options + * @param {Function} options.GUI - The lil-gui constructor. + * @param {Object} options.THREE - The `three/webgpu` namespace. + * @param {NodeMaterial} options.material - Material from `createMxWgslMaterial`. + * @param {Object} options.manifest - The generator manifest used to build the material. + * @param {GUI} [options.gui] - Existing gui to populate (otherwise a new one is created). + * @return {GUI} + */ +export function createMxWgslGUI( { GUI, THREE, material, manifest, gui = null } ) { + + manifest = normalizeReflection( manifest ); + gui = gui || new GUI(); + const args = material.userData.mxArgs || {}; + + const folders = {}; + const folderFor = ( parent, key, label ) => { + + const id = parent + '/' + key; + if ( ! folders[ id ] ) { folders[ id ] = ( parent === '' ? gui : folders[ parent ] ).addFolder( label ); folders[ id ].close(); } + return folders[ id ]; + + }; + + // One control bound to a uniform node's `.value`. + const addControl = ( folder, node, type, name, value, label, range ) => { + + if ( type === 'vec3f' && /color|scatter/.test( name ) ) { + + const proxy = { c: Array.isArray( value ) ? value.slice() : [ value.x, value.y, value.z ] }; + folder.addColor( proxy, 'c' ).name( label ).onChange( ( v ) => node.value.fromArray( v ) ); + + } else if ( type === 'vec3f' ) { + + const sub = folder.addFolder( label ); sub.close(); + const proxy = Array.isArray( value ) ? { x: value[ 0 ], y: value[ 1 ], z: value[ 2 ] } : { x: value.x, y: value.y, z: value.z }; + const lo = range ? range[ 0 ] : 0, hi = range ? range[ 1 ] : 2; + for ( const k of [ 'x', 'y', 'z' ] ) + sub.add( proxy, k, lo, hi ).onChange( () => node.value.set( proxy.x, proxy.y, proxy.z ) ); + + } else if ( type === 'bool' ) { + + const proxy = { v: !! value }; + folder.add( proxy, 'v' ).name( label ).onChange( ( v ) => { node.value = v; } ); + + } else if ( type === 'vec2f' ) { + + const sub = folder.addFolder( label ); sub.close(); + const proxy = Array.isArray( value ) ? { x: value[ 0 ], y: value[ 1 ] } : { x: value.x, y: value.y }; + const lo = range ? range[ 0 ] : 0, hi = range ? range[ 1 ] : 2; + for ( const k of [ 'x', 'y' ] ) + sub.add( proxy, k, lo, hi ).onChange( () => node.value.set( proxy.x, proxy.y ) ); + + } else if ( type === 'vec4f' ) { + + const sub = folder.addFolder( label ); sub.close(); + const proxy = Array.isArray( value ) + ? { x: value[ 0 ], y: value[ 1 ], z: value[ 2 ], w: value[ 3 ] } + : { x: value.x, y: value.y, z: value.z, w: value.w }; + const lo = range ? range[ 0 ] : 0, hi = range ? range[ 1 ] : 2; + for ( const k of [ 'x', 'y', 'z', 'w' ] ) + sub.add( proxy, k, lo, hi ).onChange( () => node.value.set( proxy.x, proxy.y, proxy.z, proxy.w ) ); + + } else if ( type === 'f32' || type === 'i32' || type === 'u32' ) { + + const [ lo, hi ] = range || scalarRange( name ); + const proxy = { v: Number( value ) || 0 }; + const ctrl = folder.add( proxy, 'v', lo, hi ).name( label ).onChange( ( v ) => { node.value = v; } ); + if ( type === 'i32' || type === 'u32' ) ctrl.step( 1 ); + + } + + }; + + // Surface inputs (semantic 'uniform'), grouped by closure category. + const surface = folderFor( '', 'surface', 'MaterialX surface' ); + surface.open(); + for ( const u of manifest.uniforms || [] ) { + + if ( u.semantic !== 'uniform' ) continue; + const node = args[ u.name ]; + if ( ! node || u.type === 'mat4x4f' ) continue; + const stripped = u.name.replace( /^[A-Za-z][A-Za-z0-9]*_/, '' ); // drop node prefix (e.g. 'ss_') + const cat = guiCategory( stripped ); + const folder = cat === 'other' ? surface : folderFor( '/surface', cat, cat.replace( /_/g, ' ' ) ); + addControl( folder, node, u.type, u.name, u.value, stripped, null ); + + } + + // Lighting + environment. + const lighting = folderFor( '', 'lighting', 'Lighting & environment' ); + for ( const u of manifest.uniforms || [] ) { + + const node = args[ u.name ]; + if ( ! node ) continue; + if ( u.semantic === 'light:direction' ) addControl( lighting, node, 'vec3f', 'dir_vec', u.value, 'light direction', [ - 1, 1 ] ); + else if ( u.semantic === 'light:color' ) addControl( lighting, node, 'vec3f', 'light_rgb', u.value, 'light color', [ 0, 10 ] ); + else if ( u.semantic === 'env:lightIntensity' ) addControl( lighting, node, 'f32', 'env_intensity', u.value, 'env intensity', [ 0, 3 ] ); + + } + + return gui; + +} diff --git a/javascript/MaterialXView/source/viewer.js b/javascript/MaterialXView/source/viewer.js index 5cfce0bcb3..2590043316 100644 --- a/javascript/MaterialXView/source/viewer.js +++ b/javascript/MaterialXView/source/viewer.js @@ -10,6 +10,8 @@ import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js'; import { prepareEnvTexture, getLightRotation, findLights, registerLights, getUniformValues } from './helper.js' import { Group } from 'three'; import GUI from 'lil-gui'; +import { createMxWgslMaterial, createMxWgslGUI, normalizeReflection } from './mxtsladapter.js'; +import { buildWgslManifest } from './wgslmanifest.js'; const ALL_GEOMETRY_SPECIFIER = "*"; const NO_GEOMETRY_SPECIFIER = ""; @@ -18,6 +20,96 @@ const DAG_PATH_SEPERATOR = "/"; // Logging toggle var logDetailedTime = false; +/** + * Configure genContext for WebGPU material generation. + * @returns {boolean} Whether the surface is transparent. + */ +function configureWebGPUGenContext(mx, gen, genContext, elem) +{ + // WebGPU: leave surface color linear; WebGPURenderer converts working space → sRGB for the canvas. + // WebGL RawShaderMaterial writes shader output directly, so that path keeps hwSrgbEncodeOutput=true. + genContext.getOptions().hwSrgbEncodeOutput = false; + const isTransparent = mx.isTransparentSurface(elem, gen.getTarget()); + genContext.getOptions().hwTransparency = isTransparent; + genContext.getOptions().shaderInterfaceType = mx.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE; + return isTransparent; +} + +/** + * Generate WGSL pixel shader and manifest for a renderable element. + */ +function generateWebGPUShader(mx, gen, genContext, elem) +{ + const shader = gen.generate(elem.getNamePath(), elem, genContext); + const wgsl = shader.getSourceCode('pixel'); + // The in-repo WgslShaderGenerator does not emit a manifest; reconstruct it in JS from + // the generated WGSL text + the Shader's uniform ports (see wgslmanifest.js). + const manifest = buildWgslManifest(shader, wgsl); + return { shader, wgsl, manifest }; +} + +/** + * Build the first directional light payload for the TSL bridge. + */ +function buildDirectionalLight(lightData) +{ + if (!lightData || !lightData.length) return null; + const l = lightData[0]; + return { + direction: l.direction.clone(), + color: l.color.clone().multiplyScalar(l.intensity) + }; +} + +/** + * Build IBL environment payload for the TSL bridge. + */ +function buildEnvironment(radianceTexture, irradianceTexture, matrix, THREE) +{ + // HDR env maps are linear radiance data; disable Three.js sRGB decode on the TSL path + // (same rationale as buildTextureMap for material textures). + radianceTexture.colorSpace = THREE.NoColorSpace; + irradianceTexture.colorSpace = THREE.NoColorSpace; + const mips = Math.trunc(Math.log2(Math.max(radianceTexture.image.width, radianceTexture.image.height))) + 1; + return { + radiance: radianceTexture, + irradiance: irradianceTexture, + samples: 16, + mips, + intensity: 1.0, + matrix + }; +} + +/** + * Map manifest texture keys to loaded THREE.Texture objects. + * + * MaterialXView registers no color-management system, so the generated shader performs no + * sRGB->linear input transform, and the WebGL RawShaderMaterial samples textures raw (the + * stored sRGB bytes are used directly). A TSL `texture()` node, by contrast, decodes to + * linear working space when `texture.colorSpace` is sRGB. To stay pixel-identical with the + * WebGL reference we therefore force `NoColorSpace` so WebGPU samples raw as well. (Proper + * color management would mean registering a CMS so BOTH backends convert in-shader; that is + * a separate change that would alter the existing WebGL appearance.) + */ +function buildTextureMap(manifest, uniforms, THREE) +{ + // C++ reflection uses { bindings: [...] }; normalize before reading textures[]. + manifest = normalizeReflection(manifest); + const textures = {}; + for (const tex of (manifest.textures || [])) + { + if (tex.semantic !== 'texture') continue; + const u = uniforms[tex.key]; + if (u && u.value) + { + u.value.colorSpace = THREE.NoColorSpace; + textures[tex.key] = u.value; + } + } + return textures; +} + /* Scene management */ @@ -243,11 +335,18 @@ export class Scene updateObjectUniforms(child, material, camera) { if (!child || !material || !camera) return; + + // The WebGPU NodeMaterial has no `material.uniforms` block: its per-object world + // transforms come from TSL accessors (positionWorld / normalWorld / cameraPosition) + // and editor edits drive the TSL uniform nodes directly, so this path is a no-op for + // it. The guards below keep the WebGL (RawShaderMaterial) updates working unchanged. const uniforms = material.uniforms; if (!uniforms) return; - uniforms.u_worldMatrix.value = child.matrixWorld; - uniforms.u_viewProjectionMatrix.value = this.#_viewProjMat.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + if (uniforms.u_worldMatrix) + uniforms.u_worldMatrix.value = child.matrixWorld; + if (uniforms.u_viewProjectionMatrix) + uniforms.u_viewProjectionMatrix.value = this.#_viewProjMat.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); if (uniforms.u_viewPosition) uniforms.u_viewPosition.value = camera.getWorldPosition(this.#_worldViewPos); @@ -900,6 +999,9 @@ export class Material // generateMaterial(matassign, viewer, searchPath, closeUI) { + if (viewer.getBackend() === 'webgpu') + return this.generateMaterialWebGPU(matassign, viewer, searchPath, closeUI); + var elem = matassign.getMaterial(); var startGenerateMat = performance.now(); @@ -985,6 +1087,68 @@ export class Material return newMaterial; } + // + // WebGPU material generation: emit WGSL with the upstream WgslShaderGenerator, reshape it + // into a TSL NodeMaterial via the bridge, and reuse the existing property Editor by + // exposing a RawShaderMaterial-style `uniforms` map plus a per-frame sync into the TSL + // nodes. Lighting reuses the rig's first directional light + the prepared IBL textures. + // + generateMaterialWebGPU(matassign, viewer, searchPath, closeUI) + { + const mx = viewer.getMx(); + const TSL = viewer.getTSL(); + const elem = matassign.getMaterial(); + const gen = viewer.getGenerator(); + const genContext = viewer.getGenContext(); + const textureLoader = new THREE.TextureLoader(); + + try + { + const isTransparent = configureWebGPUGenContext(mx, gen, genContext, elem); + const { shader, wgsl, manifest } = generateWebGPUShader(mx, gen, genContext, elem); + + const flipV = viewer.getScene().getFlipGeometryV(); + const uniforms = getUniformValues(shader.getStage('pixel'), textureLoader, searchPath, flipV); + const textures = buildTextureMap(manifest, uniforms, THREE); + + const environment = buildEnvironment( + viewer.getRadianceTexture(), + viewer.getIrradianceTexture(), + getLightRotation(), + THREE + ); + const light = buildDirectionalLight(viewer.getLightData()); + const lightData = viewer.getLightData(); + const numLights = viewer.getLights()?.length ?? 0; + + // Indexed geometry gets computed tangents in updateScene(); use them for anisotropic parity. + const material = createMxWgslMaterial({ + THREE, TSL, wgsl, manifest, light, lightData, numLights, environment, textures, + useGeometryTangent: true + }); + material.side = THREE.DoubleSide; + material.transparent = isTransparent; + material.name = elem.getName(); + + const gui = viewer.getEditor().getGUI(); + this.updateEditorWebGPU(matassign, material, manifest, gui, closeUI, viewer); + return material; + } + catch (error) + { + // WGSL generation or TSL assembly failed (e.g. an unsupported node). Surface it and + // return a visible magenta fallback so the viewer stays usable instead of crashing. + const name = elem ? elem.getName() : ''; + console.error(`WebGPU material generation failed for '${name}':`, + Number.isInteger(error) ? mx.getExceptionMessage(error) : error); + const fallback = new THREE.MeshBasicNodeMaterial(); + fallback.colorNode = TSL.vec3(1.0, 0.0, 1.0); + fallback.side = THREE.DoubleSide; + fallback.name = name; + return fallback; + } + } + clearSoloMaterialUI() { for (let i = 0; i < this._materials.length; ++i) @@ -1043,31 +1207,18 @@ export class Material // Update property editor for a given MaterialX element, it's shader, and // Three material // - updateEditor(matassign, shader, material, gui, closeUI, viewer) + setupMaterialFolder(matassign, elem, gui, closeUI, viewer) { - var elem = matassign.getMaterial(); - var materials = this._materials; - - const DEFAULT_MIN = 0; - const DEFAULT_MAX = 100; - - var startTime = performance.now(); - + const materials = this._materials; const elemPath = elem.getNamePath(); - - // Create and cache associated UI - var matUI = gui.addFolder(elemPath); + const matUI = gui.addFolder(elemPath); matassign.setMaterialUI(matUI); - let matTitle = matUI.domElement.getElementsByClassName('title')[0]; - // Add a icon to the title to allow for assigning the material to geometry - // Clicking on the icon will "solo" the material to the geometry. - // Clicking on the title will open/close the material folder. + const matTitle = matUI.domElement.getElementsByClassName('title')[0]; matTitle.innerHTML = "" + elem.getNamePath(); - let img = matTitle.getElementsByTagName('img')[0]; + const img = matTitle.getElementsByTagName('img')[0]; if (img) { - // Add event listener to icon to call updateSoloMaterial function img.addEventListener('click', function (event) { Material.updateSoloMaterial(viewer, elemPath, materials, event); @@ -1075,9 +1226,33 @@ export class Material } if (closeUI) - { matUI.close(); - } + + return matUI; + } + + // + // WebGPU property editor: manifest-driven TSL uniform nodes via createMxWgslGUI. + // + updateEditorWebGPU(matassign, material, manifest, gui, closeUI, viewer) + { + const elem = matassign.getMaterial(); + const matUI = this.setupMaterialFolder(matassign, elem, gui, closeUI, viewer); + createMxWgslGUI({ GUI, THREE, material, manifest, gui: matUI }); + } + + updateEditor(matassign, shader, material, gui, closeUI, viewer) + { + var elem = matassign.getMaterial(); + + const DEFAULT_MIN = 0; + const DEFAULT_MAX = 100; + + var startTime = performance.now(); + + var matUI = this.setupMaterialFolder(matassign, elem, gui, closeUI, viewer); + const elemPath = elem.getNamePath(); + const uniformBlocks = Object.values(shader.getStage('pixel').getUniformBlocks()); var uniformToUpdate; const ignoreList = ['u_envRadianceMips', 'u_envRadianceSamples', 'u_alphaThreshold']; @@ -1539,19 +1714,24 @@ export class Viewer // Create shader generator, generation context and "base" document which // contains the standard definition libraries and lighting elements. // - async initialize(mtlxIn, renderer, radianceTexture, irradianceTexture, lightRigXml) + async initialize(mtlxIn, renderer, radianceTexture, irradianceTexture, lightRigXml, opts = {}) { this.mx = mtlxIn; - - // Initialize base document - this.generator = this.mx.EsslShaderGenerator.create(); + this.backend = opts.backend || 'webgl'; + this.tsl = opts.TSL || null; + + // Initialize base document. The WebGPU backend uses the upstream WgslShaderGenerator + // (WGSL, consumed by the TSL bridge); WebGL uses the ESSL generator (GLSL). + this.generator = (this.backend === 'webgpu') + ? this.mx.WgslShaderGenerator.create() + : this.mx.EsslShaderGenerator.create(); this.genContext = new this.mx.GenContext(this.generator); this.document = this.mx.createDocument(); this.stdlib = this.mx.loadStandardLibraries(this.genContext); this.document.setDataLibrary(this.stdlib); - this.initializeLighting(renderer, radianceTexture, irradianceTexture, lightRigXml); + await this.initializeLighting(renderer, radianceTexture, irradianceTexture, lightRigXml); radianceTexture.mapping = THREE.EquirectangularReflectionMapping; this.getScene().setBackgroundTexture(radianceTexture); @@ -1626,6 +1806,16 @@ export class Viewer return this.mx; } + getBackend() + { + return this.backend; + } + + getTSL() + { + return this.tsl; + } + getGenerator() { return this.generator; diff --git a/javascript/MaterialXView/source/wgslmanifest.js b/javascript/MaterialXView/source/wgslmanifest.js new file mode 100644 index 0000000000..becffe0c71 --- /dev/null +++ b/javascript/MaterialXView/source/wgslmanifest.js @@ -0,0 +1,211 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +/** + * WGSL reflection manifest builder. + * + * The in-repo `WgslShaderGenerator` emits WGSL but does not emit the JSON reflection + * manifest that the TSL bridge (`mxtsladapter.js`) consumes. Rather than reintroduce + * manifest emission into the C++ generator, we reconstruct the manifest here in JS from + * two sources that are already available after `generate()`: + * + * 1. The generated WGSL text — supplies the `@group/@binding` layout, the final WGSL + * type spellings, the entry-function names, and the vertex-input struct (varyings). + * 2. The `Shader` object — supplies what is NOT present in the WGSL text: each uniform's + * default value (`ShaderPort.getValue()`) and the public-vs-host role (the uniform + * block name: `PublicUniforms` => editable "uniform", everything else => "host"). + * + * The output matches the "bindings" reflection shape accepted by + * `normalizeReflection()` in mxtsladapter.js, so no adapter changes are required. + */ + +// Uniform block names emitted by HwShaderGenerator (see source/MaterialXGenHw/HwConstants.cpp). +// The role split mirrors the original C++ manifest: PublicUniforms members are the +// user-editable surface inputs; all other blocks are engine/host-set. +const PUBLIC_UNIFORMS = 'PublicUniforms'; +const LIGHT_DATA = 'LightData'; + +// Entry-function names emitted by WgslShaderGenerator::emitPixelStage / emitVertexStage +// (see source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.cpp). The adapter is entry-name +// agnostic (it reads manifest.entry), so emitting the in-repo names is sufficient. +const PIXEL_ENTRY = 'FragmentMain'; +const VERTEX_ENTRY = 'VertexMain'; + +/** + * Format a MaterialX port value as a JS scalar / array / boolean. + * Mirrors the C++ `valueToJson` in WgslShaderGenerator.cpp so the produced object graph + * is identical to what `JSON.parse(getWgslGeneratedManifest(...))` used to yield. + * + * @param {Object} port - A bound mx.ShaderPort, or null. + * @return {(number|boolean|number[]|null)} The default value, or null when unset. + */ +function portValueToJson( port ) { + + const value = port && port.getValue && port.getValue(); + if ( ! value ) return null; + + const s = value.getValueString(); + if ( s.indexOf( ',' ) !== - 1 ) { + + // Vector / color / matrix: comma-separated numbers. + return s.split( ',' ) + .map( ( item ) => item.trim() ) + .filter( ( item ) => item.length > 0 ) + .map( ( item ) => parseFloat( item ) ); + + } + if ( s === 'true' ) return true; + if ( s === 'false' ) return false; + if ( s === '' ) return 0; + const n = Number( s ); + return Number.isNaN( n ) ? s : n; + +} + +/** + * Build a name -> { role, port } map from the pixel stage's uniform blocks. + * The block name determines the role; LightData members are not enumerated individually + * (the light-data array is exposed as a single `var` binding instead). + */ +function collectUniformPorts( shader ) { + + const map = new Map(); + let stage; + try { + + stage = shader.getStage( 'pixel' ); + + } catch ( e ) { + + return map; + + } + if ( ! stage ) return map; + + const blocks = stage.getUniformBlocks(); // JS object keyed by block name + for ( const blockName of Object.keys( blocks ) ) { + + if ( blockName === LIGHT_DATA ) continue; + const block = blocks[ blockName ]; + if ( ! block ) continue; + const role = blockName === PUBLIC_UNIFORMS ? 'uniform' : 'host'; + const count = block.size(); + for ( let i = 0; i < count; ++ i ) { + + const port = block.get( i ); + if ( ! port ) continue; + map.set( port.getVariable(), { role, port } ); + + } + + } + + return map; + +} + +// Matches `@group(N) @binding(M) var[] name: type;` lines emitted by +// WgslResourceBindingContext. `var` covers scalar/vector/matrix/array uniforms; +// bare `var` covers texture_2d and sampler bindings. +const BINDING_RE = /@group\(\s*(\d+)\s*\)\s*@binding\(\s*(\d+)\s*\)\s*var(?:<[^>]*>)?\s+([A-Za-z_]\w*)\s*:\s*([^;]+);/g; + +/** + * Parse the `@group/@binding` resource declarations out of the WGSL text into manifest + * bindings, attaching role + default value from the uniform-port map. + */ +function parseBindings( wgsl, portMap ) { + + const bindings = []; + BINDING_RE.lastIndex = 0; + for ( let m = BINDING_RE.exec( wgsl ); m !== null; m = BINDING_RE.exec( wgsl ) ) { + + const group = Number( m[ 1 ] ); + const binding = Number( m[ 2 ] ); + const name = m[ 3 ]; + const type = m[ 4 ].trim(); + + if ( name.endsWith( '_texture' ) || type.startsWith( 'texture_' ) ) { + + bindings.push( { stage: 'pixel', group, binding, name, type, role: 'texture', key: name.replace( /_texture$/, '' ) } ); + + } else if ( name.endsWith( '_sampler' ) || type === 'sampler' ) { + + bindings.push( { stage: 'pixel', group, binding, name, type, role: 'sampler', key: name.replace( /_sampler$/, '' ) } ); + + } else if ( type.startsWith( 'array<' ) ) { + + // Light-data array (struct array). The adapter re-parses the struct itself, + // so no per-member value is needed here. + bindings.push( { stage: 'pixel', group, binding, name, type, role: 'lightData' } ); + + } else { + + const info = portMap.get( name ); + const role = info ? info.role : 'host'; + const value = info ? portValueToJson( info.port ) : null; + bindings.push( { stage: 'pixel', group, binding, name, type, role, value } ); + + } + + } + + return bindings; + +} + +/** + * Parse the pixel entry signature `fn FragmentMain(: ) -> vec4f` plus the + * referenced `struct { name: type, ... }` into a single entryParam carrying the + * varying members. normalizeReflection() flattens this into the varying input list. + */ +function parseEntryParams( wgsl ) { + + const sig = new RegExp( `\\bfn\\s+${ PIXEL_ENTRY }\\s*\\(\\s*([A-Za-z_]\\w*)\\s*:\\s*([A-Za-z_]\\w*)\\s*\\)` ).exec( wgsl ); + if ( ! sig ) return []; + + const instName = sig[ 1 ]; + const structName = sig[ 2 ]; + + const structRe = new RegExp( `\\bstruct\\s+${ structName }\\s*\\{([^}]*)\\}` ); + const structMatch = structRe.exec( wgsl ); + const members = []; + if ( structMatch ) { + + for ( const rawLine of structMatch[ 1 ].split( '\n' ) ) { + + const line = rawLine.trim().replace( /,$/, '' ); + if ( ! line ) continue; + const colon = line.indexOf( ':' ); + if ( colon < 0 ) continue; + const name = line.slice( 0, colon ).trim(); + const type = line.slice( colon + 1 ).trim(); + if ( name && type ) members.push( { name, type } ); + + } + + } + + return [ { name: instName, type: structName, members } ]; + +} + +/** + * Build the WGSL reflection manifest (bindings format) from a generated Shader + its WGSL. + * + * @param {Object} shader - The mx.Shader returned by WgslShaderGenerator.generate(). + * @param {string} wgsl - The generated pixel-stage WGSL source. + * @return {Object} Manifest consumable by normalizeReflection() / convertToTslPortable(). + */ +export function buildWgslManifest( shader, wgsl ) { + + const portMap = collectUniformPorts( shader ); + return { + entry: { vertex: VERTEX_ENTRY, pixel: PIXEL_ENTRY }, + output: 'vec4f', + entryParams: parseEntryParams( wgsl ), + bindings: parseBindings( wgsl, portMap ) + }; + +} diff --git a/javascript/MaterialXView/webpack.config.js b/javascript/MaterialXView/webpack.config.js index b3f0e33f79..b00c0aef53 100644 --- a/javascript/MaterialXView/webpack.config.js +++ b/javascript/MaterialXView/webpack.config.js @@ -1,5 +1,6 @@ const path = require('path'); const fs = require('fs'); +const webpack = require('webpack'); const CopyPlugin = require("copy-webpack-plugin"); const HtmlWebpackPlugin = require('html-webpack-plugin') @@ -11,9 +12,9 @@ function processMaterialPath(materialPath, baseURL) { const dirent = fs.readdirSync(materialPath).filter( function (file) { if (file.lastIndexOf(".mtlx") > -1) return file; } ); - return dirent.map((fileName) => ({ - name: fileName, - value: `${baseURL}/${fileName}` + return dirent.map((fileName) => ({ + name: fileName, + value: `${baseURL}/${fileName}` })); } @@ -32,47 +33,58 @@ dirent = fs.readdirSync(geometryFiles).filter( let geometry = dirent .map((fileName) => ({ name: fileName, value: `${geometryFilesURL}/${fileName}` })); -module.exports = { - entry: './source/index.js', - output: { - filename: 'main.js', - path: path.resolve(__dirname, 'dist') - }, - mode: "development", - plugins: [ - new HtmlWebpackPlugin({ - templateParameters: { - materials, - geometry - }, - template: 'index.ejs' - }), - new CopyPlugin({ - patterns: [ - { - context: "../../resources/Images", - from: "*.*", - to: "Images", - }, - { - context: "../../resources/Geometry/", - from: "*.glb", - to: "Geometry", - }, - { from: "./public", to: 'public' }, - { context: "../../resources/Lights", from: "*.*", to: "Lights" }, - { context: "../../resources/Lights/irradiance", from: "*.*", to: "Lights/irradiance" }, - // Dynamically generate material copy patterns from configuration - ...materialConfig.materials.map(materialType => ({ - from: materialType.path, - to: materialType.baseURL - })), - { from: "../build/bin/JsMaterialXCore.wasm" }, - { from: "../build/bin/JsMaterialXCore.js" }, - { from: "../build/bin/JsMaterialXGenShader.wasm" }, - { from: "../build/bin/JsMaterialXGenShader.js" }, - { from: "../build/bin/JsMaterialXGenShader.data" }, - ], - }), - ] -}; +// Shared asset copy (WASM, libraries, textures, materials, geometry, lights). Applied to +// both backend configs so index-webgpu.html deploys standalone with all assets. +const copyPlugin = new CopyPlugin({ + patterns: [ + { context: "../../resources/Images", from: "*.*", to: "Images" }, + { context: "../../resources/Geometry/", from: "*.glb", to: "Geometry" }, + { from: "./public", to: 'public' }, + { context: "../../resources/Lights", from: "*.*", to: "Lights" }, + { context: "../../resources/Lights/irradiance", from: "*.*", to: "Lights/irradiance" }, + ...materialConfig.materials.map(materialType => ({ + from: materialType.path, + to: materialType.baseURL + })), + { from: "../build/bin/JsMaterialXCore.wasm" }, + { from: "../build/bin/JsMaterialXCore.js" }, + { from: "../build/bin/JsMaterialXGenShader.wasm" }, + { from: "../build/bin/JsMaterialXGenShader.js" }, + { from: "../build/bin/JsMaterialXGenShader.data" }, + ], +}); + +// Build one config per rendering backend. Both compile the SAME source; a `__BACKEND__` +// define + a per-backend `three` alias select the WebGL (classic THREE.WebGLRenderer + +// RawShaderMaterial / ESSL) or WebGPU (WebGPURenderer + NodeMaterial / WGSL) path. The two +// HTML pages let a toggle switch backends by navigation (avoids loading both three builds). +function makeConfig(backend) { + const isWebGPU = backend === 'webgpu'; + return { + name: backend, + entry: './source/index.js', + output: { + filename: isWebGPU ? 'main.webgpu.js' : 'main.js', + path: path.resolve(__dirname, 'dist'), + }, + mode: "development", + // Each bundle references only its backend's renderer at runtime, but both code paths + // exist in the shared source, so the unused one's renderer isn't in this build's three + // export set. That's expected — silence the "export not found in 'three'" warning. + ignoreWarnings: [ (w) => /was not found in 'three'/.test((w && w.message) || '') ], + // For WebGPU, resolve the bare `three` specifier to the WebGPU build (exact match + // via `three$`, so `three/tsl` and `three/webgpu` still resolve normally). + resolve: isWebGPU ? { alias: { 'three$': require.resolve('three/webgpu') } } : {}, + plugins: [ + new webpack.DefinePlugin({ __BACKEND__: JSON.stringify(backend) }), + new HtmlWebpackPlugin({ + filename: isWebGPU ? 'index-webgpu.html' : 'index.html', + template: 'index.ejs', + templateParameters: { materials, geometry, backend }, + }), + copyPlugin, + ], + }; +} + +module.exports = [makeConfig('webgl'), makeConfig('webgpu')]; diff --git a/python/Scripts/generateshader.py b/python/Scripts/generateshader.py index 46ff359043..e90a69cb43 100644 --- a/python/Scripts/generateshader.py +++ b/python/Scripts/generateshader.py @@ -126,12 +126,15 @@ def main(): genoptions.shaderInterfaceType = mx_gen_shader.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE print('- Set up CMS ...') - cms = mx_gen_shader.DefaultColorManagementSystem.create(shadergen.getTarget()) + cmsTarget = shadergen.getTarget() + if gentarget == 'wgsl' and cmsTarget not in ('genglsl', 'genmsl', 'genosl'): + cmsTarget = 'genglsl' + cms = mx_gen_shader.DefaultColorManagementSystem.create(cmsTarget) cms.loadLibrary(doc) shadergen.setColorManagementSystem(cms) print('- Set up Units ...') - unitsystem = mx_gen_shader.UnitSystem.create(shadergen.getTarget()) + unitsystem = mx_gen_shader.UnitSystem.create(cmsTarget) registry = mx.UnitConverterRegistry.create() distanceTypeDef = doc.getUnitTypeDef('distance') registry.addUnitConverter(distanceTypeDef, mx.LinearUnitConverter.create(distanceTypeDef)) diff --git a/source/JsMaterialX/JsMaterialXGenEssl/JsEsslShaderGenerator.cpp b/source/JsMaterialX/JsMaterialXGenEssl/JsEsslShaderGenerator.cpp index cac1d91e47..5b34c12323 100644 --- a/source/JsMaterialX/JsMaterialXGenEssl/JsEsslShaderGenerator.cpp +++ b/source/JsMaterialX/JsMaterialXGenEssl/JsEsslShaderGenerator.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include +#include #include #include diff --git a/source/JsMaterialX/JsMaterialXGenVk/JsVkShaderGenerator.cpp b/source/JsMaterialX/JsMaterialXGenVk/JsVkShaderGenerator.cpp index e55b64a397..1a5bf49676 100644 --- a/source/JsMaterialX/JsMaterialXGenVk/JsVkShaderGenerator.cpp +++ b/source/JsMaterialX/JsMaterialXGenVk/JsVkShaderGenerator.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include +#include #include #include diff --git a/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp b/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp index b0c05d3228..b54372bc5d 100644 --- a/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp +++ b/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include +#include #include #include diff --git a/source/MaterialXGenGlsl/CMakeLists.txt b/source/MaterialXGenGlsl/CMakeLists.txt index 8a9e4acdeb..838fc83d51 100644 --- a/source/MaterialXGenGlsl/CMakeLists.txt +++ b/source/MaterialXGenGlsl/CMakeLists.txt @@ -1,5 +1,6 @@ file(GLOB_RECURSE materialx_source "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") file(GLOB_RECURSE materialx_headers "${CMAKE_CURRENT_SOURCE_DIR}/*.h*") +list(FILTER materialx_headers EXCLUDE REGEX "wgsl/converter/") # internal WGSL helpers; not installed mx_add_library(MaterialXGenGlsl SOURCE_FILES diff --git a/source/MaterialXGenGlsl/WgslResourceBindingContext.cpp b/source/MaterialXGenGlsl/WgslResourceBindingContext.cpp deleted file mode 100644 index b9ceb270a4..0000000000 --- a/source/MaterialXGenGlsl/WgslResourceBindingContext.cpp +++ /dev/null @@ -1,105 +0,0 @@ -// -// Copyright Contributors to the MaterialX Project -// SPDX-License-Identifier: Apache-2.0 -// - -#include - -#include -#include - -MATERIALX_NAMESPACE_BEGIN - -// -// WgslResourceBindingContext methods -// - -WgslResourceBindingContext::WgslResourceBindingContext(size_t uniformBindingLocation) : - VkResourceBindingContext(uniformBindingLocation) -{ -} - -// Copied from VkResourceBindingContext::emitResourceBindings(). -// Modified the Type::FILENAME uniform codegen. -void WgslResourceBindingContext::emitResourceBindings(GenContext& context, const VariableBlock& uniforms, ShaderStage& stage) -{ - const ShaderGenerator& generator = context.getShaderGenerator(); - const Syntax& syntax = generator.getSyntax(); - - // First, emit all value uniforms in a block with single layout binding - bool hasValueUniforms = false; - for (auto uniform : uniforms.getVariableOrder()) - { - if (uniform->getType() != Type::FILENAME) - { - hasValueUniforms = true; - break; - } - } - if (hasValueUniforms) - { - generator.emitLine("layout (std140, binding=" + std::to_string(_hwUniformBindLocation++) + ") " + - syntax.getUniformQualifier() + " " + uniforms.getName() + "_" + stage.getName(), - stage, false); - generator.emitScopeBegin(stage); - for (auto uniform : uniforms.getVariableOrder()) - { - if (uniform->getType() != Type::FILENAME) - { - if ( uniform->getType() == Type::BOOLEAN ) - { - // Cannot have boolean uniforms in WGSL - std::cerr << "Warning: WGSL does not allow boolean types to be stored in uniform or storage address spaces." << std::endl; - - // Set uniform type to integer - uniform->setType( Type::INTEGER ); - - // Write declaration as normal - generator.emitLineBegin(stage); - generator.emitVariableDeclaration(uniform, EMPTY_STRING, context, stage, false); - generator.emitString(Syntax::SEMICOLON, stage); - generator.emitLineEnd(stage, false); - - // Add macro to treat any follow usages of this variable as a boolean - // eg. u_myUniformBool -> bool(u_myUniformBool) - generator.emitString("#define " + uniform->getVariable() + " bool(" + uniform->getVariable() + ")", stage); - generator.emitLineBreak(stage); - } - else - { - generator.emitLineBegin(stage); - generator.emitVariableDeclaration(uniform, EMPTY_STRING, context, stage, false); - generator.emitString(Syntax::SEMICOLON, stage); - generator.emitLineEnd(stage, false); - } - - } - } - generator.emitScopeEnd(stage, true); - } - - // Second, emit all sampler uniforms as separate uniforms with separate layout bindings - for (auto uniform : uniforms.getVariableOrder()) - { - if (uniform->getType() == Type::FILENAME) - { - // Bind separately as texture2D + sampler - // - // NOTE: the *_texture and *_sampler binding names method below expect that - // variables from HwShaderGenerator.cpp (HW::ENV_RADIANCE_SPLIT and HW::ENV_IRRADIANCE_SPLIT) - // use the same naming convention as here. - // - generator.emitString("layout (binding=" + std::to_string(_hwUniformBindLocation++) + ") " + syntax.getUniformQualifier() + " ", stage); - generator.emitString(string("texture2D ")+uniform->getVariable()+"_texture", stage); - generator.emitLineEnd(stage, true); - - generator.emitString("layout (binding=" + std::to_string(_hwUniformBindLocation++) + ") " + syntax.getUniformQualifier() + " ", stage); - generator.emitString(string("sampler ")+uniform->getVariable()+"_sampler", stage); - generator.emitLineEnd(stage, true); - } - } - - generator.emitLineBreak(stage); -} - -MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenGlsl/WgslShaderGenerator.cpp b/source/MaterialXGenGlsl/WgslShaderGenerator.cpp deleted file mode 100644 index d39a2498f4..0000000000 --- a/source/MaterialXGenGlsl/WgslShaderGenerator.cpp +++ /dev/null @@ -1,67 +0,0 @@ -// -// Copyright Contributors to the MaterialX Project -// SPDX-License-Identifier: Apache-2.0 -// - -#include -#include - -#include - -MATERIALX_NAMESPACE_BEGIN - -const string WgslShaderGenerator::LIGHTDATA_TYPEVAR_STRING = "light_type"; - -WgslShaderGenerator::WgslShaderGenerator(TypeSystemPtr typeSystem) : - VkShaderGenerator(typeSystem) -{ - _syntax = WgslSyntax::create(typeSystem); - - // Set binding context to handle resource binding layouts - _resourceBindingCtx = std::make_shared(0); - - // For functions described in ::emitSpecularEnvironment() - // override map value from HwShaderGenerator - _tokenSubstitutions[HW::T_ENV_RADIANCE] = HW::ENV_RADIANCE_SPLIT; - _tokenSubstitutions[HW::T_ENV_RADIANCE_SAMPLER2D] = HW::ENV_RADIANCE_SAMPLER2D_SPLIT; - _tokenSubstitutions[HW::T_ENV_IRRADIANCE] = HW::ENV_IRRADIANCE_SPLIT; - _tokenSubstitutions[HW::T_ENV_IRRADIANCE_SAMPLER2D] = HW::ENV_IRRADIANCE_SAMPLER2D_SPLIT; - _tokenSubstitutions[HW::T_TEX_SAMPLER_SAMPLER2D] = HW::TEX_SAMPLER_SAMPLER2D_SPLIT; - _tokenSubstitutions[HW::T_TEX_SAMPLER_SIGNATURE] = HW::TEX_SAMPLER_SIGNATURE_SPLIT; -} - -void WgslShaderGenerator::emitDirectives(GenContext& context, ShaderStage& stage) const -{ - VkShaderGenerator::emitDirectives(context, stage); - // Add additional directives and #define statements here - // Example: emitLine("#define HW_SEPARATE_SAMPLERS", stage, false); - emitLineBreak(stage); -} - -// Called by CompoundNode::emitFunctionDefinition() -void WgslShaderGenerator::emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext& context, ShaderStage& stage) const -{ - if (shaderPort->getType() == Type::FILENAME) - { - emitString("texture2D " + shaderPort->getVariable() + "_texture, sampler "+shaderPort->getVariable() + "_sampler", stage); - } - else - { - VkShaderGenerator::emitFunctionDefinitionParameter(shaderPort, isOutput, context, stage); - } -} - -// Called by SourceCodeNode::emitFunctionCall() -void WgslShaderGenerator::emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const -{ - if (input->getType() == Type::FILENAME) - { - emitString(getUpstreamResult(input, context)+"_texture, "+getUpstreamResult(input, context)+"_sampler", stage); - } - else - { - VkShaderGenerator::emitInput(input, context, stage); - } -} - -MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenGlsl/WgslShaderGenerator.h b/source/MaterialXGenGlsl/WgslShaderGenerator.h deleted file mode 100644 index af9141f440..0000000000 --- a/source/MaterialXGenGlsl/WgslShaderGenerator.h +++ /dev/null @@ -1,52 +0,0 @@ -// -// Copyright Contributors to the MaterialX Project -// SPDX-License-Identifier: Apache-2.0 -// - -#ifndef MATERIALX_WGSLSHADERGENERATOR_H -#define MATERIALX_WGSLSHADERGENERATOR_H - -/// @file -/// Vulkan GLSL shader generator flavor for WGSL - -#include -#include -#include - -MATERIALX_NAMESPACE_BEGIN - -using WgslShaderGeneratorPtr = shared_ptr; - -/// @class WgslShaderGenerator -/// WGSL Flavor of Vulkan GLSL shader generator -class MX_GENGLSL_API WgslShaderGenerator : public VkShaderGenerator -{ - public: - /// Constructor. - WgslShaderGenerator(TypeSystemPtr typeSystem); - - /// Creator function. - /// If a TypeSystem is not provided it will be created internally. - /// Optionally pass in an externally created TypeSystem here, - /// if you want to keep type descriptions alive after the lifetime - /// of the shader generator. - static ShaderGeneratorPtr create(TypeSystemPtr typeSystem = nullptr) - { - return std::make_shared(typeSystem ? typeSystem : TypeSystem::create()); - } - - void emitDirectives(GenContext& context, ShaderStage& stage) const override; - - const string& getLightDataTypevarString() const override { return LIGHTDATA_TYPEVAR_STRING; } - - void emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext& context, ShaderStage& stage) const override; - - void emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const override; - - protected: - static const string LIGHTDATA_TYPEVAR_STRING; -}; - -MATERIALX_NAMESPACE_END - -#endif diff --git a/source/MaterialXGenGlsl/EsslShaderGenerator.cpp b/source/MaterialXGenGlsl/essl/EsslShaderGenerator.cpp similarity index 97% rename from source/MaterialXGenGlsl/EsslShaderGenerator.cpp rename to source/MaterialXGenGlsl/essl/EsslShaderGenerator.cpp index 8c074ea555..45071fe7e7 100644 --- a/source/MaterialXGenGlsl/EsslShaderGenerator.cpp +++ b/source/MaterialXGenGlsl/essl/EsslShaderGenerator.cpp @@ -3,8 +3,8 @@ // SPDX-License-Identifier: Apache-2.0 // -#include -#include +#include +#include #include #include diff --git a/source/MaterialXGenGlsl/EsslShaderGenerator.h b/source/MaterialXGenGlsl/essl/EsslShaderGenerator.h similarity index 100% rename from source/MaterialXGenGlsl/EsslShaderGenerator.h rename to source/MaterialXGenGlsl/essl/EsslShaderGenerator.h diff --git a/source/MaterialXGenGlsl/EsslSyntax.cpp b/source/MaterialXGenGlsl/essl/EsslSyntax.cpp similarity index 87% rename from source/MaterialXGenGlsl/EsslSyntax.cpp rename to source/MaterialXGenGlsl/essl/EsslSyntax.cpp index fb391645af..7ab3d9e490 100644 --- a/source/MaterialXGenGlsl/EsslSyntax.cpp +++ b/source/MaterialXGenGlsl/essl/EsslSyntax.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include +#include #include #include diff --git a/source/MaterialXGenGlsl/EsslSyntax.h b/source/MaterialXGenGlsl/essl/EsslSyntax.h similarity index 100% rename from source/MaterialXGenGlsl/EsslSyntax.h rename to source/MaterialXGenGlsl/essl/EsslSyntax.h diff --git a/source/MaterialXGenGlsl/VkResourceBindingContext.cpp b/source/MaterialXGenGlsl/vk/VkResourceBindingContext.cpp similarity index 99% rename from source/MaterialXGenGlsl/VkResourceBindingContext.cpp rename to source/MaterialXGenGlsl/vk/VkResourceBindingContext.cpp index 692334f120..5a262061f8 100644 --- a/source/MaterialXGenGlsl/VkResourceBindingContext.cpp +++ b/source/MaterialXGenGlsl/vk/VkResourceBindingContext.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include +#include #include diff --git a/source/MaterialXGenGlsl/VkResourceBindingContext.h b/source/MaterialXGenGlsl/vk/VkResourceBindingContext.h similarity index 100% rename from source/MaterialXGenGlsl/VkResourceBindingContext.h rename to source/MaterialXGenGlsl/vk/VkResourceBindingContext.h diff --git a/source/MaterialXGenGlsl/VkShaderGenerator.cpp b/source/MaterialXGenGlsl/vk/VkShaderGenerator.cpp similarity index 97% rename from source/MaterialXGenGlsl/VkShaderGenerator.cpp rename to source/MaterialXGenGlsl/vk/VkShaderGenerator.cpp index 22ff1f85c2..0c4c461c05 100644 --- a/source/MaterialXGenGlsl/VkShaderGenerator.cpp +++ b/source/MaterialXGenGlsl/vk/VkShaderGenerator.cpp @@ -3,8 +3,8 @@ // SPDX-License-Identifier: Apache-2.0 // -#include -#include +#include +#include #include MATERIALX_NAMESPACE_BEGIN diff --git a/source/MaterialXGenGlsl/VkShaderGenerator.h b/source/MaterialXGenGlsl/vk/VkShaderGenerator.h similarity index 97% rename from source/MaterialXGenGlsl/VkShaderGenerator.h rename to source/MaterialXGenGlsl/vk/VkShaderGenerator.h index 45647af3e7..bec06fad8c 100644 --- a/source/MaterialXGenGlsl/VkShaderGenerator.h +++ b/source/MaterialXGenGlsl/vk/VkShaderGenerator.h @@ -10,7 +10,7 @@ /// Vulkan GLSL shader generator #include -#include +#include MATERIALX_NAMESPACE_BEGIN diff --git a/source/MaterialXGenGlsl/VkSyntax.cpp b/source/MaterialXGenGlsl/vk/VkSyntax.cpp similarity index 84% rename from source/MaterialXGenGlsl/VkSyntax.cpp rename to source/MaterialXGenGlsl/vk/VkSyntax.cpp index 563190ad51..35d2fcd188 100644 --- a/source/MaterialXGenGlsl/VkSyntax.cpp +++ b/source/MaterialXGenGlsl/vk/VkSyntax.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include +#include MATERIALX_NAMESPACE_BEGIN diff --git a/source/MaterialXGenGlsl/VkSyntax.h b/source/MaterialXGenGlsl/vk/VkSyntax.h similarity index 100% rename from source/MaterialXGenGlsl/VkSyntax.h rename to source/MaterialXGenGlsl/vk/VkSyntax.h diff --git a/source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.cpp b/source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.cpp new file mode 100644 index 0000000000..85496eab34 --- /dev/null +++ b/source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.cpp @@ -0,0 +1,114 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include + +#include + +MATERIALX_NAMESPACE_BEGIN + +// +// WgslResourceBindingContext methods +// + +WgslResourceBindingContext::WgslResourceBindingContext(size_t uniformBindingLocation) : + VkResourceBindingContext(uniformBindingLocation) +{ +} + +void WgslResourceBindingContext::emitResourceBindings(GenContext& context, const VariableBlock& uniforms, ShaderStage& stage) +{ + const ShaderGenerator& generator = context.getShaderGenerator(); + const Syntax& syntax = generator.getSyntax(); + + for (auto uniform : uniforms.getVariableOrder()) + { + const TypeDesc t = uniform->getType(); + if (t.isClosure() || t == Type::SURFACESHADER || t == Type::MATERIAL || + t == Type::DISPLACEMENTSHADER || t == Type::VOLUMESHADER || t == Type::LIGHTSHADER) + { + continue; + } + + if (uniform->getType() == Type::FILENAME) + { + const string& name = uniform->getVariable(); + // Bind separately as texture_2d + sampler. + // + // NOTE: the *_texture and *_sampler binding names below expect that + // variables from HwShaderGenerator.cpp (HW::ENV_RADIANCE_SPLIT and HW::ENV_IRRADIANCE_SPLIT) + // use the same naming convention as here. + // + generator.emitLine("@group(0) @binding(" + std::to_string(_hwUniformBindLocation++) + ") var " + name + + "_texture: texture_2d", + stage); + generator.emitLine("@group(0) @binding(" + std::to_string(_hwUniformBindLocation++) + ") var " + name + + "_sampler: sampler", + stage); + } + else + { + const string typeName = getWgslUniformType(uniform, syntax); + generator.emitLine("@group(0) @binding(" + std::to_string(_hwUniformBindLocation++) + ") var " + + uniform->getVariable() + ": " + typeName, + stage); + } + } + + generator.emitLineBreak(stage); +} + +// Emit a uniform block as a WGSL struct plus a single `var` binding for it. +// Used for the light-data array (a scalar suffix like "[4]" becomes a WGSL `array`). +void WgslResourceBindingContext::emitStructuredResourceBindings(GenContext& context, const VariableBlock& uniforms, + ShaderStage& stage, const std::string& structInstanceName, + const std::string& arraySuffix) +{ + const ShaderGenerator& generator = context.getShaderGenerator(); + const Syntax& syntax = generator.getSyntax(); + + // Emit a WGSL struct for the uniform block members. + generator.emitLine("struct " + uniforms.getName(), stage, false); + generator.emitScopeBegin(stage); + for (size_t i = 0; i < uniforms.size(); ++i) + { + const string typeName = getWgslUniformType(uniforms[i], syntax); + // WGSL struct members are comma-separated (not `;`-terminated like GLSL). + generator.emitLine(uniforms[i]->getVariable() + ": " + typeName + ",", stage, false); + } + generator.emitScopeEnd(stage, false); + + if (!arraySuffix.empty()) + { + // arraySuffix is e.g. "[4]" — extract count for WGSL array<> syntax. + const string count = arraySuffix.substr(1, arraySuffix.size() - 2); + generator.emitLine("@group(0) @binding(" + std::to_string(_hwUniformBindLocation++) + ") var " + + structInstanceName + ": array<" + uniforms.getName() + ", " + count + ">", + stage); + } + else + { + generator.emitLine("@group(0) @binding(" + std::to_string(_hwUniformBindLocation++) + ") var " + + structInstanceName + ": " + uniforms.getName(), + stage); + } + generator.emitLineBreak(stage); +} + +// Map a uniform port's type to its WGSL spelling: booleans become i32 (WGSL forbids bool +// in uniform/storage address spaces), and every other GLSL type name is converted to WGSL. +string WgslResourceBindingContext::getWgslUniformType(const ShaderPort* port, const Syntax& syntax) const +{ + if (port->getType() == Type::BOOLEAN) + { + // WGSL does not allow boolean types in uniform or storage address spaces. + return "i32"; + } + return GlslToWgsl::mapType(syntax.getTypeName(port->getType())); +} + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenGlsl/WgslResourceBindingContext.h b/source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.h similarity index 50% rename from source/MaterialXGenGlsl/WgslResourceBindingContext.h rename to source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.h index 7b39b65661..d03a570c30 100644 --- a/source/MaterialXGenGlsl/WgslResourceBindingContext.h +++ b/source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.h @@ -7,11 +7,14 @@ #define MATERIALX_WGSLRESOURCEBINDING_H /// @file -/// Vulkan GLSL resource binding context for WGSL +/// WebGPU WGSL resource binding context #include -#include +#include + +#include +#include MATERIALX_NAMESPACE_BEGIN @@ -19,7 +22,7 @@ MATERIALX_NAMESPACE_BEGIN using WgslResourceBindingContextPtr = shared_ptr; /// @class WgslResourceBindingContext -/// Class representing a resource binding for Vulkan Glsl shader resources. +/// Class representing a resource binding for WGSL shader resources. class MX_GENGLSL_API WgslResourceBindingContext : public VkResourceBindingContext { public: @@ -32,6 +35,19 @@ class MX_GENGLSL_API WgslResourceBindingContext : public VkResourceBindingContex // Emit uniforms with binding information void emitResourceBindings(GenContext& context, const VariableBlock& uniforms, ShaderStage& stage) override; + + // Emit structured uniforms with binding information + void emitStructuredResourceBindings(GenContext& context, const VariableBlock& uniforms, + ShaderStage& stage, const std::string& structInstanceName, + const std::string& arraySuffix) override; + + // Current @binding index (advanced by emitResourceBindings) + size_t getBindingLocation() const { return _hwUniformBindLocation; } + + void setBindingLocation(size_t location) { _hwUniformBindLocation = location; } + + // Emit a WGSL type name for a uniform port (maps bool to i32) + string getWgslUniformType(const ShaderPort* port, const Syntax& syntax) const; }; MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.cpp b/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.cpp new file mode 100644 index 0000000000..de17d18722 --- /dev/null +++ b/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.cpp @@ -0,0 +1,740 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +const string WgslShaderGenerator::TARGET = "genwgsl"; +const string WgslShaderGenerator::LIGHTDATA_TYPEVAR_STRING = "light_type"; + +namespace +{ +// Tracks include files (by basename) already expanded during a generation, so the +// genglsl headers (closure types, math) that many BSDF sources #include are emitted +// once rather than producing duplicate WGSL struct/const definitions. +class WgslIncludeSet : public GenUserData +{ + public: + std::set basenames; +}; +const string WGSL_INCLUDES = "WGSL_INCLUDES"; + +bool isIdentChar(char c) +{ + return std::isalnum(static_cast(c)) || c == '_'; +} + +string getWgslTypeName(const Syntax& syntax, const TypeDesc& type) +{ + if (type == Type::BOOLEAN) + return "i32"; + return GlslToWgsl::mapType(syntax.getTypeName(type)); +} + +string mapGlslValueExpr(string value) +{ + if (value == "true") + return "1"; + if (value == "false") + return "0"; + + struct CtorMap + { + const char* from; + const char* to; + }; + static const CtorMap CTORS[] = { + { "vec2(", "vec2f(" }, + { "vec3(", "vec3f(" }, + { "vec4(", "vec4f(" }, + { "mat2(", "mat2x2f(" }, + { "mat3(", "mat3x3f(" }, + { "mat4(", "mat4x4f(" }, + }; + for (const CtorMap& c : CTORS) + { + const string from = c.from; + size_t pos = 0; + while ((pos = value.find(from, pos)) != string::npos) + { + if (pos > 0 && isIdentChar(value[pos - 1])) + { + pos += from.size(); + continue; + } + value.replace(pos, from.size(), c.to); + pos += std::strlen(c.to); + } + } + return value; +} + +string getWgslValue(const Syntax& syntax, const TypeDesc& type, const Value& value) +{ + if (type == Type::BOOLEAN) + return value.getValueString() == "true" ? "1" : "0"; + return mapGlslValueExpr(syntax.getValue(type, value)); +} + +string getWgslDefaultValue(const Syntax& syntax, const TypeDesc& type) +{ + if (type == Type::BOOLEAN) + return "0"; + return mapGlslValueExpr(syntax.getDefaultValue(type)); +} +} // namespace + +WgslShaderGenerator::WgslShaderGenerator(TypeSystemPtr typeSystem) : + VkShaderGenerator(typeSystem) +{ + _syntax = WgslSyntax::create(typeSystem); + + // Set binding context to handle resource binding layouts + _resourceBindingCtx = std::make_shared(0); + + // For functions described in ::emitSpecularEnvironment() + // override map value from HwShaderGenerator + _tokenSubstitutions[HW::T_ENV_RADIANCE] = HW::ENV_RADIANCE_SPLIT; + _tokenSubstitutions[HW::T_ENV_RADIANCE_SAMPLER2D] = HW::ENV_RADIANCE_SAMPLER2D_SPLIT; + _tokenSubstitutions[HW::T_ENV_IRRADIANCE] = HW::ENV_IRRADIANCE_SPLIT; + _tokenSubstitutions[HW::T_ENV_IRRADIANCE_SAMPLER2D] = HW::ENV_IRRADIANCE_SAMPLER2D_SPLIT; + _tokenSubstitutions[HW::T_TEX_SAMPLER_SAMPLER2D] = HW::TEX_SAMPLER_SAMPLER2D_SPLIT; + _tokenSubstitutions[HW::T_TEX_SAMPLER_SIGNATURE] = HW::TEX_SAMPLER_SIGNATURE_SPLIT; +} + +ShaderNodeImplPtr WgslShaderGenerator::getImplementation(const NodeDef& nodedef, GenContext& context) const +{ + InterfaceElementPtr implElement = nodedef.getImplementation(TARGET); + if (!implElement) + { + // WGSL reuses genglsl node implementations until genwgsl-specific overrides exist. + implElement = nodedef.getImplementation(VkShaderGenerator::TARGET); + } + if (!implElement) + { + return nullptr; + } + + const string& name = implElement->getName(); + ShaderNodeImplPtr impl = context.findNodeImplementation(name); + if (impl) + { + return impl; + } + + if (implElement->isA()) + { + impl = createShaderNodeImplForNodeGraph(*implElement->asA()); + } + else if (implElement->isA()) + { + if (getColorManagementSystem() && getColorManagementSystem()->hasImplementation(name)) + { + impl = getColorManagementSystem()->createImplementation(name); + } + else + { + impl = _implFactory.create(name); + } + if (!impl) + { + impl = createShaderNodeImplForImplementation(*implElement->asA()); + } + } + if (!impl) + { + return nullptr; + } + + impl->initialize(*implElement, context); + context.addNodeImplementation(name, impl); + return impl; +} + +void WgslShaderGenerator::emitVariableDeclaration(const ShaderPort* variable, const string& qualifier, + GenContext&, ShaderStage& stage, bool assignValue) const +{ + if (variable->getType() == Type::FILENAME) + { + // Combined GLSL sampler2D becomes a WGSL texture (sampler emitted separately). + emitString("var " + variable->getVariable() + ": texture_2d", stage); + return; + } + + string typeName = getWgslTypeName(*_syntax, variable->getType()); + if (variable->getType().isArray() && variable->getValue()) + typeName += _syntax->getArrayVariableSuffix(variable->getType(), *variable->getValue()); + + const bool isConst = (qualifier == _syntax->getConstantQualifier()); + const string keyword = isConst ? "const " : "var "; + string line = keyword + variable->getVariable() + ": " + typeName; + + const bool isUniform = (qualifier == _syntax->getUniformQualifier()); + if (assignValue && !isUniform) + { + const string valueStr = variable->getValue() + ? getWgslValue(*_syntax, variable->getType(), *variable->getValue()) + : getWgslDefaultValue(*_syntax, variable->getType()); + if (!valueStr.empty()) + line += " = " + valueStr; + } + + emitString(line, stage); +} + +// Called by CompoundNode::emitFunctionDefinition() +void WgslShaderGenerator::emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext& /*context*/, ShaderStage& stage) const +{ + const string& name = shaderPort->getVariable(); + if (shaderPort->getType() == Type::FILENAME) + { + VkShaderGenerator::emitString(name + "_texture: texture_2d, " + name + "_sampler: sampler", stage); + return; + } + const string typeName = getWgslTypeName(*_syntax, shaderPort->getType()); + if (isOutput) + { + VkShaderGenerator::emitString(name + ": ptr", stage); + } + else + { + VkShaderGenerator::emitString(name + ": " + typeName, stage); + } +} + +// Called by SourceCodeNode::emitFunctionCall() +void WgslShaderGenerator::emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const +{ + if (input->getType() == Type::FILENAME) + { + emitString(getUpstreamResult(input, context) + "_texture, " + getUpstreamResult(input, context) + "_sampler", stage); + } + else + { + VkShaderGenerator::emitInput(input, context, stage); + } +} + +void WgslShaderGenerator::emitOutput(const ShaderOutput* output, bool includeType, bool assignValue, + GenContext& context, ShaderStage& stage) const +{ + if (includeType) + { + stage.addString("var " + output->getVariable() + ": " + getWgslTypeName(*_syntax, output->getType())); + } + else + { + // Function-call argument: WGSL out parameters are `ptr`, passed as `&var`. + stage.addString((assignValue ? "" : "&") + output->getVariable()); + } + + string suffix; + context.getOutputSuffix(output, suffix); + if (!suffix.empty()) + stage.addString(suffix); + + if (assignValue) + { + const string value = getWgslDefaultValue(*_syntax, output->getType()); + if (!value.empty()) + stage.addString(" = " + value); + } +} + +void WgslShaderGenerator::emitString(const string& str, ShaderStage& stage) const +{ + if (str.size() >= 5 && str.compare(0, 5, "void ") == 0) + { + string replaced = str; + replaced.replace(0, 5, "fn "); + VkShaderGenerator::emitString(replaced, stage); + return; + } + VkShaderGenerator::emitString(str, stage); +} + +void WgslShaderGenerator::emitLine(const string& str, ShaderStage& stage, bool semicolon) const +{ + // Split a run of statements onto separate lines, then apply the GLSL->WGSL rewrites. + const size_t firstNewline = str.find('\n'); + if (firstNewline != string::npos) + { + emitLine(str.substr(0, firstNewline), stage, true); + emitLine(str.substr(firstNewline + 1), stage, semicolon); + return; + } + VkShaderGenerator::emitLine(GlslToWgsl::rewriteAll(str), stage, semicolon); +} + +void WgslShaderGenerator::emitLineEnd(ShaderStage& stage, bool semicolon) const +{ + // Rewrite the line just built via emitString (e.g. by SourceCodeNode) to WGSL. + const string& code = stage.getSourceCode(); + const string& newlineStr = _syntax->getNewline(); + const size_t newlineLen = newlineStr.empty() ? 1 : newlineStr.size(); + const size_t lastNewline = code.rfind(newlineStr); + const string lastLine = (lastNewline == string::npos) ? code : code.substr(lastNewline + newlineLen); + if (!lastLine.empty()) + { + const string rewritten = GlslToWgsl::rewriteAll(lastLine); + if (rewritten != lastLine) + { + const string newCode = (lastNewline == string::npos) + ? rewritten + : code.substr(0, lastNewline + newlineLen) + rewritten; + stage.setSourceCode(newCode); + } + } + VkShaderGenerator::emitLineEnd(stage, semicolon); +} + +void WgslShaderGenerator::emitBlock(const string& str, const FilePath& sourceFilename, GenContext& context, ShaderStage& stage) const +{ + if (sourceFilename.getExtension() == "glsl" && !str.empty()) + { + GlslToWgsl::LineRewriter rewriter; + const string wgsl = expandAndRewriteGlsl(str, sourceFilename, context, rewriter); + stage.addBlock(wgsl, sourceFilename, context); + return; + } + VkShaderGenerator::emitBlock(str, sourceFilename, context, stage); +} + +void WgslShaderGenerator::emitLibraryInclude(const FilePath& filename, GenContext& context, ShaderStage& stage) const +{ + FilePath libraryPrefix = context.getOptions().libraryPrefix; + FilePath fullFilename = libraryPrefix.isEmpty() ? filename : libraryPrefix / filename; + FilePath resolvedFilename = context.resolveSourceFile(fullFilename, FilePath()); + if (resolvedFilename.exists()) + { + emitBlock(readFile(resolvedFilename), resolvedFilename, context, stage); + } +} + +void WgslShaderGenerator::emitTypeDefinitions(GenContext&, ShaderStage&) const +{ + // GLSL typedefs are `#define` directives (e.g. `#define EDF vec3`). WGSL has no + // preprocessor — surface/closure aliases are emitted by emitWgslSurfaceTypes(). +} + +void WgslShaderGenerator::emitVertexStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const +{ + emitDirectives(context, stage); + emitLineBreak(stage); + + emitConstants(context, stage); + emitUniforms(context, stage); + emitInputs(context, stage); + emitOutputs(context, stage); + + emitLibraryInclude("stdlib/genglsl/lib/mx_math.glsl", context, stage); + emitLineBreak(stage); + + emitFunctionDefinitions(graph, context, stage); + + const VariableBlock& vertexData = stage.getOutputBlock(HW::VERTEX_DATA); + const string vdInstance = vertexData.getInstance(); + + setFunctionName("VertexMain", stage); + emitLine("fn VertexMain(inputs: VertexInput) -> VertexOutput", stage, false); + emitFunctionBodyBegin(graph, context, stage); + + emitLine("var output: VertexOutput", stage); + emitLine("var " + vdInstance + ": " + vertexData.getName(), stage); + emitLine("let hPositionWorld = " + HW::T_WORLD_MATRIX + " * vec4f(inputs." + HW::T_IN_POSITION + ", 1.0)", stage); + emitLine("output.position = " + HW::T_VIEW_PROJECTION_MATRIX + " * hPositionWorld", stage); + + for (const ShaderNode* node : graph.getNodes()) + emitFunctionCall(*node, context, stage); + + emitLine("output." + vdInstance + " = " + vdInstance, stage); + emitLine("return output", stage); + emitFunctionBodyEnd(graph, context, stage); + + finalizeStageSource(stage); +} + +void WgslShaderGenerator::emitPixelStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const +{ + _tokenSubstitutions[ShaderGenerator::T_FILE_TRANSFORM_UV] = + context.getOptions().fileTextureVerticalFlip ? "mx_transform_uv_vflip.glsl" : "mx_transform_uv.glsl"; + + auto includeSet = std::make_shared(); + context.pushUserData(WGSL_INCLUDES, includeSet); + + emitDirectives(context, stage); + emitLineBreak(stage); + + const bool lighting = requiresLighting(graph); + const ShaderGraphOutputSocket* outputSocket = graph.getOutputSocket(); + const TypeDesc outSocketType = outputSocket->getType(); + const bool isSurface = lighting || outSocketType == Type::SURFACESHADER || outSocketType == Type::MATERIAL; + + if (isSurface) + emitWgslSurfaceTypes(context, stage); + + emitTypeDefinitions(context, stage); + emitConstants(context, stage); + + emitPixelUniforms(context, stage, lighting); + + const VariableBlock& vertexData = stage.getInputBlock(HW::VERTEX_DATA); + emitVertexDataStruct(vertexData, stage); + + emitLibraryInclude("stdlib/genglsl/lib/mx_math.glsl", context, stage); + emitLineBreak(stage); + + if (lighting || context.getOptions().hwWriteAlbedoTable || context.getOptions().hwWriteEnvPrefilter) + { + emitLine("const DIRECTIONAL_ALBEDO_METHOD: i32 = " + std::to_string(int(context.getOptions().hwDirectionalAlbedoMethod)), stage); + emitLineBreak(stage); + } + + emitLine("const AIRY_FRESNEL_ITERATIONS: i32 = " + std::to_string(context.getOptions().hwAiryFresnelIterations), stage); + emitLineBreak(stage); + + if (lighting) + { + if (context.getOptions().hwMaxActiveLightSources > 0) + { + const unsigned int maxLights = std::max(1u, context.getOptions().hwMaxActiveLightSources); + emitLine("const " + HW::LIGHT_DATA_MAX_LIGHT_SOURCES + ": i32 = " + std::to_string(maxLights), stage); + emitLineBreak(stage); + } + emitSpecularEnvironment(context, stage); + emitTransmissionRender(context, stage); + if (context.getOptions().hwMaxActiveLightSources > 0) + emitLightData(context, stage); + } + + emitLightFunctionDefinitions(graph, context, stage); + emitFunctionDefinitions(graph, context, stage); + emitLineBreak(stage); + + const string vdInstance = vertexData.getInstance(); + const string entryParam = vdInstance + ": " + vertexData.getName(); + setFunctionName("FragmentMain", stage); + emitLine("fn FragmentMain(" + entryParam + ") -> vec4f", stage, false); + emitScopeBegin(stage); + + if (graph.hasClassification(ShaderNode::Classification::SHADER | ShaderNode::Classification::SURFACE)) + { + emitFunctionCalls(graph, context, stage, ShaderNode::Classification::TEXTURE); + for (const ShaderGraphOutputSocket* socket : graph.getOutputSockets()) + { + if (socket->getConnection()) + { + const ShaderNode* upstream = socket->getConnection()->getNode(); + if (upstream->getParent() == &graph && + (upstream->hasClassification(ShaderNode::Classification::CLOSURE) || + upstream->hasClassification(ShaderNode::Classification::SHADER))) + { + emitFunctionCall(*upstream, context, stage); + } + } + } + } + else + { + emitFunctionCalls(graph, context, stage); + } + + const ShaderOutput* outputConnection = outputSocket->getConnection(); + string outValue = outputConnection ? outputConnection->getVariable() : getWgslDefaultValue(*_syntax, Type::COLOR3); + const TypeDesc outType = outputSocket->getType(); + string vec4Value; + if (isSurface && outputConnection) + { + string outColor = outValue + ".color"; + const string outTransparency = outValue + ".transparency"; + if (context.getOptions().hwSrgbEncodeOutput) + outColor = "mx_srgb_encode(" + outColor + ")"; + if (context.getOptions().hwTransparency) + { + emitLine("let outAlpha: f32 = clamp(1.0 - dot(" + outTransparency + ", vec3f(0.3333)), 0.0, 1.0)", stage); + vec4Value = "vec4f(" + outColor + ", outAlpha)"; + emitLine("if (outAlpha < " + HW::T_ALPHA_THRESHOLD + ")", stage, false); + emitScopeBegin(stage); + emitLine("discard", stage); + emitScopeEnd(stage); + } + else + { + vec4Value = "vec4f(" + outColor + ", 1.0)"; + } + } + else if (outType.isFloat4()) + { + vec4Value = outValue; + } + else if (outType.isFloat3()) + { + if (context.getOptions().hwSrgbEncodeOutput) + outValue = "mx_srgb_encode(" + outValue + ")"; + vec4Value = "vec4f(" + outValue + ", 1.0)"; + } + else + { + vec4Value = "vec4f(vec3f(" + outValue + "), 1.0)"; + } + emitLine("return " + vec4Value, stage); + emitScopeEnd(stage); + + finalizeStageSource(stage); +} + +void WgslShaderGenerator::emitDirectives(GenContext&, ShaderStage&) const +{ + // WGSL has no #version or preprocessor directives. +} + +void WgslShaderGenerator::emitWgslSurfaceTypes(GenContext&, ShaderStage& stage) const +{ + stage.addString(R"WGSL(// MaterialX closure result types (multi-line so the +// overload/broadcast type-inference pass can read their members). +struct BSDF { + response: vec3f, + throughput: vec3f, +} +struct VDF { + response: vec3f, + throughput: vec3f, +} +alias EDF = vec3; +struct surfaceshader { + color: vec3f, + transparency: vec3f, +} +struct volumeshader { + color: vec3f, + transparency: vec3f, +} +struct displacementshader { + offset: vec3f, + scale: f32, +} +alias material = surfaceshader; +struct lightshader { + direction: vec3f, + intensity: vec3f, +} +)WGSL"); + emitLineBreak(stage); +} + +void WgslShaderGenerator::emitPixelUniforms(GenContext& context, ShaderStage& stage, bool lighting) const +{ + HwResourceBindingContextPtr resourceBindingCtx = getResourceBindingContext(context); + if (!resourceBindingCtx) + return; + + auto wgslCtx = std::dynamic_pointer_cast(resourceBindingCtx); + wgslCtx->setBindingLocation(0); + + for (const auto& it : stage.getUniformBlocks()) + { + const VariableBlock& uniforms = *it.second; + if (uniforms.empty() || uniforms.getName() == HW::LIGHT_DATA) + continue; + + emitComment("Uniform block: " + uniforms.getName(), stage); + resourceBindingCtx->emitResourceBindings(context, uniforms, stage); + emitLineBreak(stage); + } + + if (lighting && context.getOptions().hwMaxActiveLightSources > 0) + { + const VariableBlock& lightData = stage.getUniformBlock(HW::LIGHT_DATA); + const string structArraySuffix = "[" + HW::LIGHT_DATA_MAX_LIGHT_SOURCES + "]"; + resourceBindingCtx->emitStructuredResourceBindings(context, lightData, stage, lightData.getInstance(), structArraySuffix); + } +} + +void WgslShaderGenerator::emitLightData(GenContext&, ShaderStage&) const +{ + // emitPixelUniforms() already emits the LightData struct plus u_lightData array. + // The GLSL base class would emit a second copy here. +} + +void WgslShaderGenerator::emitInputs(GenContext& /*context*/, ShaderStage& stage) const +{ + DEFINE_SHADER_STAGE(stage, Stage::VERTEX) + { + const VariableBlock& vertexInputs = stage.getInputBlock(HW::VERTEX_INPUTS); + if (!vertexInputs.empty()) + { + emitComment("Inputs block: " + vertexInputs.getName(), stage); + emitLine("struct VertexInput", stage, false); + emitScopeBegin(stage); + for (size_t i = 0; i < vertexInputs.size(); ++i) + { + const string name = replaceSubstrings(vertexInputs[i]->getVariable(), getTokenSubstitutions()); + emitLine("@location(" + std::to_string(i) + ") " + name + ": " + + getWgslTypeName(*_syntax, vertexInputs[i]->getType()), + stage); + } + emitScopeEnd(stage, false); + emitLineBreak(stage); + } + } +} + +void WgslShaderGenerator::emitVertexDataStruct(const VariableBlock& vertexData, ShaderStage& stage) const +{ + if (vertexData.empty()) + return; + emitLine("struct " + vertexData.getName(), stage, false); + emitScopeBegin(stage); + for (size_t i = 0; i < vertexData.size(); ++i) + { + const string name = replaceSubstrings(vertexData[i]->getVariable(), getTokenSubstitutions()); + // WGSL struct members are comma-separated (not `;`-terminated like GLSL). + emitLine(name + ": " + getWgslTypeName(*_syntax, vertexData[i]->getType()) + ",", stage, false); + } + emitScopeEnd(stage, false); + emitLineBreak(stage); +} + +void WgslShaderGenerator::emitOutputs(GenContext& /*context*/, ShaderStage& stage) const +{ + DEFINE_SHADER_STAGE(stage, Stage::VERTEX) + { + const VariableBlock& vertexData = stage.getOutputBlock(HW::VERTEX_DATA); + if (!vertexData.empty()) + { + emitVertexDataStruct(vertexData, stage); + emitLine("struct VertexOutput", stage, false); + emitScopeBegin(stage); + emitLine("@builtin(position) position: vec4f", stage); + emitLine("@location(" + std::to_string(vertexDataLocation) + ") " + vertexData.getInstance() + ": " + + vertexData.getName(), + stage); + emitScopeEnd(stage, false); + emitLineBreak(stage); + } + } +} + +void WgslShaderGenerator::finalizeStageSource(ShaderStage& stage) const +{ + string src = stage.getSourceCode(); + // HwNumLightsNode / HwLightSamplerNode emit GLSL function definitions that per-line + // rewriteAll() does not convert. Rewrite only those stubs (not the whole stage). + src = GlslToWgsl::rewriteResidualGlslFunctions(src); + src = GlslToWgsl::dedupDefinitions(src); + src = GlslToWgsl::derefPointerParams(src); + src = GlslToWgsl::resolveOverloads(src); + src = GlslToWgsl::coerceBoolCallSites(src); + src = GlslToWgsl::repairEmptyElseCommentBlocks(src); + src = GlslToWgsl::splitChainedAssignments(src); + stage.setSourceCode(src); + + for (const string& issue : GlslToWgsl::findResidualGlsl(src)) + { + std::cerr << "Warning: WgslShaderGenerator: " << issue << " in generated shader." << std::endl; + } +} + +string WgslShaderGenerator::expandAndRewriteGlsl(const string& source, const FilePath& sourceFilename, + GenContext& context, GlslToWgsl::LineRewriter& rewriter) const +{ + // Apply token substitutions ($fileTransformUv etc.) up front so include paths + // and sampler/texture rewrites see resolved text. Apply longest keys first: some + // tokens are prefixes of others (`$envRadiance` of `$envRadianceSamples`), and the + // stock GLSL substitution values mask this by accident (`u_envRadiance`+`Samples` == + // `u_envRadianceSamples`) — but the split sampler values used here do not, so an + // unordered pass would corrupt the longer token. + std::vector> subs(getTokenSubstitutions().begin(), + getTokenSubstitutions().end()); + std::sort(subs.begin(), subs.end(), + [](const std::pair& a, const std::pair& b) + { + return a.first.length() > b.first.length(); + }); + string substituted = source; + for (const auto& pair : subs) + { + if (pair.first.empty()) + continue; + size_t pos = 0; + while ((pos = substituted.find(pair.first, pos)) != string::npos) + { + substituted.replace(pos, pair.first.length(), pair.second); + pos += pair.second.length(); + } + } + + std::istringstream stream(substituted); + string line; + string out; + out.reserve(substituted.size() + substituted.size() / 8); + + while (std::getline(stream, line)) + { + const bool hadCr = !line.empty() && line.back() == '\r'; + if (hadCr) + line.pop_back(); + + const size_t firstNonSpace = line.find_first_not_of(" \t"); + const bool isInclude = (firstNonSpace != string::npos && + line.compare(firstNonSpace, 8, "#include") == 0); + if (isInclude) + { + const size_t q1 = line.find('"', firstNonSpace + 8); + const size_t q2 = (q1 == string::npos) ? string::npos : line.find('"', q1 + 1); + if (q1 == string::npos || q2 == string::npos || q2 <= q1 + 1) + continue; // malformed include: drop it + const string includePath = line.substr(q1 + 1, q2 - q1 - 1); + // Expand each header only once per generation (avoids duplicate WGSL defs). + auto seen = context.getUserData(WGSL_INCLUDES); + const string baseName = FilePath(includePath).getBaseName(); + if (seen && seen->basenames.count(baseName)) + continue; + if (seen) + seen->basenames.insert(baseName); + const FilePath resolved = context.resolveSourceFile(includePath, sourceFilename.getParentPath()); + const string content = readFile(resolved); + if (!content.empty()) + out += expandAndRewriteGlsl(content, resolved, context, rewriter); + continue; + } + + out += rewriter.rewrite(line); + if (hadCr) + out += '\r'; + out += '\n'; + } + return out; +} + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.h b/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.h new file mode 100644 index 0000000000..fa06a9e7c1 --- /dev/null +++ b/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.h @@ -0,0 +1,90 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_WGSLSHADERGENERATOR_H +#define MATERIALX_WGSLSHADERGENERATOR_H + +/// @file +/// WGSL shader generator + +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +// The GLSL->WGSL text utilities are internal to MaterialXGenGlsl (see wgsl/converter/GlslToWgsl.h); +// only the incomplete LineRewriter type is referenced here, so a forward declaration keeps +// that internal header out of the installed public API. +namespace GlslToWgsl +{ +class LineRewriter; +} + +using WgslShaderGeneratorPtr = shared_ptr; + +/// A WGSL (WebGPU Shading Language) shader generator. +/// Reuses the genglsl emit pipeline (via VkShaderGenerator) and converts emitted GLSL to WGSL +/// at emit time via the GlslToWgsl utilities. +class MX_GENGLSL_API WgslShaderGenerator : public VkShaderGenerator +{ + public: + /// Constructor. + WgslShaderGenerator(TypeSystemPtr typeSystem); + + /// Creator function. + /// If a TypeSystem is not provided it will be created internally. + /// Optionally pass in an externally created TypeSystem here, + /// if you want to keep type descriptions alive after the lifetime + /// of the shader generator. + static ShaderGeneratorPtr create(TypeSystemPtr typeSystem = nullptr) + { + return std::make_shared(typeSystem ? typeSystem : TypeSystem::create()); + } + + /// Return a unique identifier for the target this generator is for + const string& getTarget() const override { return TARGET; } + + const string& getLightDataTypevarString() const override { return LIGHTDATA_TYPEVAR_STRING; } + + ShaderNodeImplPtr getImplementation(const NodeDef& nodedef, GenContext& context) const override; + + /// Unique identifier for this generator target + static const string TARGET; + + /// Emit a shader variable. + void emitVariableDeclaration(const ShaderPort* variable, const string& qualifier, GenContext& context, ShaderStage& stage, bool assignValue = true) const override; + void emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext& context, ShaderStage& stage) const override; + void emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const override; + void emitOutput(const ShaderOutput* output, bool includeType, bool assignValue, GenContext& context, ShaderStage& stage) const override; + void emitString(const string& str, ShaderStage& stage) const override; + void emitLine(const string& str, ShaderStage& stage, bool semicolon = true) const override; + void emitLineEnd(ShaderStage& stage, bool semicolon = true) const override; + void emitBlock(const string& str, const FilePath& sourceFilename, GenContext& context, ShaderStage& stage) const override; + void emitLibraryInclude(const FilePath& filename, GenContext& context, ShaderStage& stage) const override; + void emitTypeDefinitions(GenContext& context, ShaderStage& stage) const override; + + protected: + void emitVertexStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const override; + void emitPixelStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const override; + + void emitDirectives(GenContext& context, ShaderStage& stage) const override; + void emitWgslSurfaceTypes(GenContext& context, ShaderStage& stage) const; + void emitPixelUniforms(GenContext& context, ShaderStage& stage, bool lighting) const; + void emitLightData(GenContext& context, ShaderStage& stage) const override; + void emitInputs(GenContext& context, ShaderStage& stage) const override; + void emitVertexDataStruct(const VariableBlock& vertexData, ShaderStage& stage) const; + void emitOutputs(GenContext& context, ShaderStage& stage) const override; + + void finalizeStageSource(ShaderStage& stage) const; + string expandAndRewriteGlsl(const string& source, const FilePath& sourceFilename, + GenContext& context, GlslToWgsl::LineRewriter& rewriter) const; + + static const string LIGHTDATA_TYPEVAR_STRING; +}; + +MATERIALX_NAMESPACE_END + +#endif diff --git a/source/MaterialXGenGlsl/WgslSyntax.cpp b/source/MaterialXGenGlsl/wgsl/WgslSyntax.cpp similarity index 98% rename from source/MaterialXGenGlsl/WgslSyntax.cpp rename to source/MaterialXGenGlsl/wgsl/WgslSyntax.cpp index d5d58dc014..63065deeea 100644 --- a/source/MaterialXGenGlsl/WgslSyntax.cpp +++ b/source/MaterialXGenGlsl/wgsl/WgslSyntax.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include +#include MATERIALX_NAMESPACE_BEGIN diff --git a/source/MaterialXGenGlsl/WgslSyntax.h b/source/MaterialXGenGlsl/wgsl/WgslSyntax.h similarity index 92% rename from source/MaterialXGenGlsl/WgslSyntax.h rename to source/MaterialXGenGlsl/wgsl/WgslSyntax.h index 0e16f221bd..f53e0f9503 100644 --- a/source/MaterialXGenGlsl/WgslSyntax.h +++ b/source/MaterialXGenGlsl/wgsl/WgslSyntax.h @@ -9,7 +9,7 @@ /// @file /// Vulkan GLSL syntax class for WGSL -#include +#include MATERIALX_NAMESPACE_BEGIN diff --git a/source/MaterialXGenGlsl/wgsl/converter/GlslToWgsl.cpp b/source/MaterialXGenGlsl/wgsl/converter/GlslToWgsl.cpp new file mode 100644 index 0000000000..c1c981174b --- /dev/null +++ b/source/MaterialXGenGlsl/wgsl/converter/GlslToWgsl.cpp @@ -0,0 +1,3829 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +namespace GlslToWgsl +{ + +namespace +{ + +using std::string_view; + +bool isIdentChar(char c) { return std::isalnum(static_cast(c)) || c == '_'; } + +// True for a non-empty run of identifier characters only (no spaces, operators or punctuation). +bool isIdentifier(string_view s) +{ + if (s.empty()) + return false; + for (char c : s) + { + if (!isIdentChar(c)) + return false; + } + return true; +} + +bool isHorizontalSpace(char c) { return c == ' ' || c == '\t'; } + +bool startsWith(string_view s, string_view prefix) +{ + return s.size() >= prefix.size() && s.compare(0, prefix.size(), prefix) == 0; +} + +// Strip carriage returns/newlines as well as spaces/tabs (see trim() below). +string_view trimView(string_view s) +{ + const size_t b = s.find_first_not_of(" \t\r\n"); + if (b == string_view::npos) + return {}; + const size_t e = s.find_last_not_of(" \t\r\n"); + return s.substr(b, e - b + 1); +} + +string trim(string_view s) +{ + const string_view t = trimView(s); + return string(t.data(), t.size()); +} + +// Read the identifier starting at `start` (skipping leading whitespace). +// Returns the identifier and advances `start` to one past its last character. +string readIdentifier(const string& s, size_t& start) +{ + while (start < s.size() && isHorizontalSpace(s[start])) + start++; + size_t end = start; + while (end < s.size() && isIdentChar(s[end])) + end++; + string ident = s.substr(start, end - start); + start = end; + return ident; +} + +int braceDelta(string_view s) +{ + int d = 0; + for (char c : s) + { + if (c == '{') + d++; + else if (c == '}') + d--; + } + return d; +} + +StringVec readLines(const string& text) +{ + StringVec lines; + std::istringstream stream(text); + string line; + while (std::getline(stream, line)) + { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + lines.push_back(std::move(line)); + } + return lines; +} + +string joinLines(const StringVec& lines) +{ + string result; + result.reserve(lines.size() * 32); + for (size_t i = 0; i < lines.size(); ++i) + { + result += lines[i]; + if (i + 1 < lines.size()) + result += '\n'; + } + return result; +} + +struct FnDef +{ + string name; + string params; +}; + +// Parse a `fn NAME(PARAMS)` definition line. Returns nullopt if not a definition. +std::optional parseFnDef(string_view lineIn) +{ + const string line(lineIn); + const size_t indentEnd = line.find_first_not_of(" \t"); + if (indentEnd == string::npos || !startsWith(string_view(line).substr(indentEnd), "fn ")) + return std::nullopt; + const size_t nameStart = indentEnd + 3; + const size_t paren = line.find('(', nameStart); + if (paren == string::npos) + return std::nullopt; + int depth = 0; + size_t close = string::npos; + for (size_t i = paren; i < line.size(); ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')') + { + depth--; + if (depth == 0) + { + close = i; + break; + } + } + } + if (close == string::npos) + return std::nullopt; + FnDef def; + def.name = trim(string_view(line).substr(nameStart, paren - nameStart)); + def.params = line.substr(paren + 1, close - paren - 1); + if (def.name.empty() || def.name.find_first_of(" \t") != string::npos) + return std::nullopt; + return def; +} + +// Split a parameter list (without the enclosing parentheses) on top-level +// commas, ignoring commas nested inside angle brackets or parens. +StringVec splitTopLevel(const string& params) +{ + StringVec out; + int angle = 0, paren = 0; + size_t start = 0; + for (size_t i = 0; i < params.size(); ++i) + { + char c = params[i]; + if (c == '<') + angle++; + else if (c == '>') + angle--; + else if (c == '(') + paren++; + else if (c == ')') + paren--; + else if (c == ',' && angle == 0 && paren == 0) + { + out.push_back(trim(params.substr(start, i - start))); + start = i + 1; + } + } + string last = trim(params.substr(start)); + if (!last.empty()) + out.push_back(last); + return out; +} + +bool isKnownReturnType(string_view t) +{ + static constexpr string_view SCALARS[] = { + "void", "float", "int", "uint", "bool", + }; + if (std::find(std::begin(SCALARS), std::end(SCALARS), t) != std::end(SCALARS)) + return true; + if (startsWith(t, "vec") || startsWith(t, "ivec") || startsWith(t, "uvec") || startsWith(t, "mat")) + return true; + static constexpr string_view STRUCTS[] = { + "ClosureData", "FresnelData", "BSDF", "EDF", "VDF", "surfaceshader", "volumeshader", + "displacementshader", "lightshader", "material", + }; + return std::find(std::begin(STRUCTS), std::end(STRUCTS), t) != std::end(STRUCTS); +} + +void countBraces(const string& line, int& depth, bool& seenOpen) +{ + for (char c : line) + { + if (c == '{') + { + depth++; + seenOpen = true; + } + else if (c == '}') + { + depth--; + } + } +} + +int netParens(const string& line) +{ + int net = 0; + for (char c : line) + { + if (c == '(') + net++; + else if (c == ')') + net--; + } + return net; +} + +// Net parenthesis balance of a line ignoring any trailing `//` line comment, whose +// parentheses are not code (e.g. `// spherical cap in (-V.z, 1]` has an unmatched `(`). +int netParensCode(const string& line) +{ + int net = 0; + for (size_t i = 0; i < line.size(); ++i) + { + if (line[i] == '/' && i + 1 < line.size() && line[i + 1] == '/') + break; // rest of the line is a comment + if (line[i] == '(') + net++; + else if (line[i] == ')') + net--; + } + return net; +} + +constexpr std::array, 19> TYPE_MAP = { { + { "float", "f32" }, + { "int", "i32" }, + { "uint", "u32" }, + { "bool", "bool" }, + { "vec2", "vec2f" }, + { "vec3", "vec3f" }, + { "vec4", "vec4f" }, + { "ivec2", "vec2" }, + { "ivec3", "vec3" }, + { "ivec4", "vec4" }, + { "uvec2", "vec2" }, + { "uvec3", "vec3" }, + { "uvec4", "vec4" }, + { "bvec2", "vec2" }, + { "bvec3", "vec3" }, + { "bvec4", "vec4" }, + { "mat2", "mat2x2f" }, + { "mat3", "mat3x3f" }, + { "mat4", "mat4x4f" }, +} }; + +string_view lookupWgslType(string_view glslType) +{ + for (const auto& entry : TYPE_MAP) + { + if (entry.first == glslType) + return entry.second; + } + return glslType; +} + +// A WGSL type that cannot be copied into a local `var` (handle/pointer types). +bool isHandleType(const string& wgslType) +{ + return wgslType == "sampler" || + wgslType.compare(0, 7, "texture") == 0 || + wgslType.compare(0, 3, "ptr") == 0; +} + +// Replace GLSL-style scalar casts with WGSL spellings (float() -> f32(), int() -> i32(), …). +string rewriteScalarCast(string line, string_view glslType, string_view wgslType) +{ + size_t pos = 0; + const string token = string(glslType) + "("; + const string repl = string(wgslType) + "("; + while ((pos = line.find(token, pos)) != string::npos) + { + if (pos > 0 && isIdentChar(line[pos - 1])) + { + pos += token.size(); + continue; + } + line.replace(pos, token.size(), repl); + pos += repl.size(); + } + return line; +} + +string rewriteScalarCasts(string line) +{ + static constexpr std::array, 3> SCALAR_CASTS = { { + { "float", "f32" }, + { "int", "i32" }, + { "uint", "u32" }, + } }; + for (const auto& cast : SCALAR_CASTS) + line = rewriteScalarCast(std::move(line), cast.first, cast.second); + return line; +} + +// Rewrite "const type name = ..." to "const name: type = ...". +string rewriteConst(const string& line) +{ + string result = line; + size_t pos = 0; + while ((pos = result.find("const ", pos)) != string::npos) + { + size_t constStart = pos; + size_t constEnd = pos + 6; + + size_t nameStart = constEnd; + while (nameStart < result.size() && isHorizontalSpace(result[nameStart])) + nameStart++; + if (nameStart >= result.size()) + { + pos = constEnd; + continue; + } + + // Only handle the GLSL form "const type name =" (no colon before '='). + size_t eqPos = result.find('=', nameStart); + size_t colonPos = result.find(':', nameStart); + if (eqPos == string::npos) + { + pos = constEnd; + continue; + } + if (colonPos != string::npos && colonPos < eqPos) + { + // Already in WGSL "name: type =" form (or unrelated colon) -- leave it. + pos = constEnd; + continue; + } + + size_t typeStart = nameStart; + size_t typeEnd = typeStart; + if (result[typeEnd] == 'v' || result[typeEnd] == 'm') + { + size_t angle = result.find('>', typeEnd); + if (angle != string::npos && angle < eqPos) + typeEnd = angle + 1; + else + typeEnd = result.find_first_of(" \t", typeStart); + } + else + typeEnd = result.find_first_of(" \t", typeStart); + if (typeEnd == string::npos || typeEnd > eqPos) + { + pos = constEnd; + continue; + } + + string typeStr = mapType(result.substr(typeStart, typeEnd - typeStart)); + while (typeEnd < result.size() && isHorizontalSpace(result[typeEnd])) + typeEnd++; + size_t nameStart2 = typeEnd; + size_t nameEnd2 = result.find_first_of(" \t=;", nameStart2); + if (nameEnd2 == string::npos || nameEnd2 > eqPos) + { + pos = constEnd; + continue; + } + string nameStr = result.substr(nameStart2, nameEnd2 - nameStart2); + string wgsl = "const " + nameStr + ": " + typeStr + " ="; + result.replace(constStart, eqPos - constStart + 1, wgsl); + pos = constStart + wgsl.size(); + } + return result; +} + +bool replaceOneTernary(string& line) +{ + size_t q = line.find('?'); + if (q == string::npos) + return false; + + size_t qBefore = q; + while (qBefore > 0 && isHorizontalSpace(line[qBefore - 1])) + qBefore--; + + size_t qAfter = q + 1; + while (qAfter < line.size() && isHorizontalSpace(line[qAfter])) + qAfter++; + + // Walk back to the start of the condition expression. Stop only at expression + // boundaries — an enclosing `(`, a top-level `,`/`;`/`{`/`}`, or an assignment + // `=` — NOT at spaces or comparison/logical operators, so multi-token conditions + // like `cosTheta < cosB` are captured whole. + size_t condStart = qBefore; + int paren = 0; + while (condStart > 0) + { + char ch = line[condStart - 1]; + if (ch == ')') + paren++; + else if (ch == '(') + { + paren--; + if (paren < 0) + break; + } + else if (paren == 0) + { + if (ch == ',' || ch == ';' || ch == '{' || ch == '}') + break; + if (ch == '=') + { + const char l = (condStart >= 2) ? line[condStart - 2] : '\0'; + const char r = (condStart < line.size()) ? line[condStart] : '\0'; + const bool opPart = (l == '<' || l == '>' || l == '!' || l == '=') || (r == '='); + if (!opPart) + break; // a true assignment, not ==/<=/>=/!= + } + } + condStart--; + } + while (condStart < qBefore && isHorizontalSpace(line[condStart])) + condStart++; + // A `return ? ...` leaves the keyword outside the condition. + if (line.compare(condStart, 7, "return ") == 0) + condStart += 7; + while (condStart < qBefore && isHorizontalSpace(line[condStart])) + condStart++; + + string cond = line.substr(condStart, qBefore - condStart); + + size_t colonPos = string::npos; + paren = 0; + for (size_t c = qAfter; c < line.size(); ++c) + { + char ch = line[c]; + if (ch == '(') + paren++; + else if (ch == ')') + paren--; + else if (ch == ':' && paren == 0) + { + colonPos = c; + break; + } + } + if (colonPos == string::npos) + return false; + + size_t tStart = qAfter; + while (tStart < colonPos && isHorizontalSpace(line[tStart])) + tStart++; + size_t tEnd = colonPos; + while (tEnd > tStart && isHorizontalSpace(line[tEnd - 1])) + tEnd--; + string tVal = trim(line.substr(tStart, tEnd - tStart)); + + size_t fStart = colonPos + 1; + while (fStart < line.size() && isHorizontalSpace(line[fStart])) + fStart++; + + size_t fEnd = fStart; + paren = 0; + for (size_t i = fStart; i < line.size(); ++i) + { + char ch = line[i]; + if (ch == '(') + paren++; + else if (ch == ')') + { + paren--; + if (paren < 0) + { + fEnd = i; + break; + } + } + else if (paren == 0 && (ch == ';' || ch == ',')) + { + fEnd = i; + break; + } + fEnd = i + 1; + } + string fVal = trim(line.substr(fStart, fEnd - fStart)); + + if (cond.empty() || tVal.empty() || fVal.empty()) + return false; + + string repl = "select(" + fVal + ", " + tVal + ", " + cond + ")"; + line.replace(condStart, fEnd - condStart, repl); + return true; +} + +string rewriteTernaries(string line) +{ + const size_t MAX_ITERATIONS = 64; + for (size_t iter = 0; iter < MAX_ITERATIONS; ++iter) + { + string prev = line; + if (!replaceOneTernary(line)) + break; + if (line == prev) + break; + } + return line; +} + +// Rewrite "type name = value" to "var name: wgslType = value" for known GLSL types. +// If a declarator name carries a GLSL array suffix (`name[N]`), strip it into `arraySize` +// and reduce `name` to the bare identifier. Returns true when an array suffix was present. +// (WGSL spells the type as `array`; the `[N]` never stays on the name.) +bool splitArrayName(string& name, string& arraySize) +{ + const size_t lb = name.find('['); + if (lb == string::npos || name.empty() || name.back() != ']') + return false; + arraySize = trim(name.substr(lb + 1, name.size() - lb - 2)); + name = trim(name.substr(0, lb)); + return true; +} + +string rewriteVariableDecl(string line) +{ + size_t lineStart = 0; + while (lineStart < line.size() && isHorizontalSpace(line[lineStart])) + lineStart++; + for (const auto& entry : TYPE_MAP) + { + const string_view glslType = entry.first; + const string_view wgslType = entry.second; + if (line.size() > lineStart + glslType.size() + 1 && + line.compare(lineStart, glslType.size(), glslType.data(), glslType.size()) == 0 && + isHorizontalSpace(line[lineStart + glslType.size()])) + { + size_t nameStart = lineStart + glslType.size(); + while (nameStart < line.size() && isHorizontalSpace(line[nameStart])) + nameStart++; + if (nameStart >= line.size()) + break; + size_t nameEnd = nameStart; + while (nameEnd < line.size() && isIdentChar(line[nameEnd])) + nameEnd++; + if (nameEnd <= nameStart) + break; + size_t afterName = nameEnd; + while (afterName < line.size() && isHorizontalSpace(line[afterName])) + afterName++; + const string varName = line.substr(nameStart, nameEnd - nameStart); + const string leading = line.substr(0, lineStart); + string wgsl = string(wgslType.data(), wgslType.size()); + + // Array declarator: `vec3 Ap[4];` -> `var Ap: array;`. + if (afterName < line.size() && line[afterName] == '[') + { + const size_t rb = line.find(']', afterName); + if (rb == string::npos) + break; + const string size = trim(line.substr(afterName + 1, rb - afterName - 1)); + wgsl = "array<" + wgsl + ", " + size + ">"; + size_t aft = rb + 1; + while (aft < line.size() && isHorizontalSpace(line[aft])) + aft++; + if (aft < line.size() && line[aft] == '=') + { + size_t rs = aft + 1; + while (rs < line.size() && isHorizontalSpace(line[rs])) + rs++; + return leading + "var " + varName + ": " + wgsl + " = " + line.substr(rs); + } + if (aft >= line.size() || line[aft] == ';') + return leading + "var " + varName + ": " + wgsl + ";"; + break; + } + if (afterName < line.size() && line[afterName] == '=') + { + size_t rs = afterName + 1; + while (rs < line.size() && isHorizontalSpace(line[rs])) + rs++; + return leading + "var " + varName + ": " + wgsl + " = " + line.substr(rs); + } + if (afterName < line.size() && line[afterName] == ';') + { + return leading + "var " + varName + ": " + wgsl + ";"; + } + break; + } + } + + // Also handle declarations already using WGSL type names but GLSL order + // (e.g. emitted source like `vec3f V = ...` -> `var V: vec3f = ...`). + static constexpr string_view WGSL_TYPES[] = { + "vec2f", "vec3f", "vec4f", "mat2x2f", "mat3x3f", "mat4x4f", "f32", "i32", "u32" + }; + for (const string_view wtype : WGSL_TYPES) + { + if (line.size() > lineStart + wtype.size() + 1 && + line.compare(lineStart, wtype.size(), wtype.data(), wtype.size()) == 0 && + isHorizontalSpace(line[lineStart + wtype.size()])) + { + size_t nameStart = lineStart + wtype.size(); + while (nameStart < line.size() && isHorizontalSpace(line[nameStart])) + nameStart++; + size_t nameEnd = nameStart; + while (nameEnd < line.size() && isIdentChar(line[nameEnd])) + nameEnd++; + if (nameEnd <= nameStart) + break; + size_t afterName = nameEnd; + while (afterName < line.size() && isHorizontalSpace(line[afterName])) + afterName++; + const string varName = line.substr(nameStart, nameEnd - nameStart); + const string wgsl = string(wtype.data(), wtype.size()); + if (afterName < line.size() && line[afterName] == '=') + { + size_t rs = afterName + 1; + while (rs < line.size() && isHorizontalSpace(line[rs])) + rs++; + return line.substr(0, lineStart) + "var " + varName + ": " + wgsl + " = " + line.substr(rs); + } + if (afterName < line.size() && line[afterName] == ';') + { + return line.substr(0, lineStart) + "var " + varName + ": " + wgsl + ";"; + } + break; + } + } + return line; +} + +const char* KNOWN_STRUCT_TYPES[] = { + "ClosureData", "FresnelData", "lightshader", "material", "BSDF", "EDF", "VDF", + "surfaceshader", "volumeshader", "displacementshader" +}; + +// Rewrite "StructType name [= value];" to "var name: StructType [= value];" for +// known MaterialX struct types. +string rewriteStructVariableDecl(string line) +{ + size_t lineStart = 0; + while (lineStart < line.size() && isHorizontalSpace(line[lineStart])) + lineStart++; + if (lineStart >= line.size()) + return line; + + size_t typeEnd = lineStart; + while (typeEnd < line.size() && isIdentChar(line[typeEnd])) + typeEnd++; + if (typeEnd <= lineStart || typeEnd >= line.size()) + return line; + if (!isHorizontalSpace(line[typeEnd])) + return line; + + string typeName = line.substr(lineStart, typeEnd - lineStart); + + bool isKnownStruct = false; + for (const char* t : KNOWN_STRUCT_TYPES) + { + if (typeName == t) + { + isKnownStruct = true; + break; + } + } + if (!isKnownStruct) + return line; + + size_t nameStart = typeEnd; + while (nameStart < line.size() && isHorizontalSpace(line[nameStart])) + nameStart++; + if (nameStart >= line.size()) + return line; + + size_t nameEnd = nameStart; + while (nameEnd < line.size() && isIdentChar(line[nameEnd])) + nameEnd++; + if (nameEnd <= nameStart) + return line; + + string varName = line.substr(nameStart, nameEnd - nameStart); + size_t afterName = nameEnd; + while (afterName < line.size() && isHorizontalSpace(line[afterName])) + afterName++; + + string leading = line.substr(0, lineStart); + + if (afterName < line.size() && line[afterName] == '=') + { + size_t rs = afterName + 1; + while (rs < line.size() && isHorizontalSpace(line[rs])) + rs++; + string rest = line.substr(rs); + return leading + "var " + varName + ": " + typeName + " = " + rest; + } + if (afterName >= line.size() || line[afterName] == ';' || + line[afterName] == '\n' || line[afterName] == '\r') + { + return leading + "var " + varName + ": " + typeName + ";"; + } + return line; +} + +// Rewrite GLSL component-wise comparison built-ins to WGSL operators: +// greaterThan(a, b) -> (a > b), lessThanEqual(a, b) -> (a <= b), etc. +string rewriteVectorCompare(string line) +{ + struct Cmp + { + const char* fn; + const char* op; + }; + // Longer names first so e.g. greaterThanEqual is matched before greaterThan. + static const Cmp CMPS[] = { + { "greaterThanEqual(", " >= " }, + { "lessThanEqual(", " <= " }, + { "greaterThan(", " > " }, + { "lessThan(", " < " }, + { "notEqual(", " != " }, + { "equal(", " == " }, + }; + for (const Cmp& c : CMPS) + { + const string tok = c.fn; + size_t pos = 0; + while ((pos = line.find(tok, pos)) != string::npos) + { + if (pos > 0 && isIdentChar(line[pos - 1])) + { + pos += tok.size(); + continue; + } + const size_t argStart = pos + tok.size(); + int depth = 1; + size_t i = argStart; + std::vector commas; + for (; i < line.size() && depth > 0; ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')') + { + depth--; + if (depth == 0) + break; + } + else if (line[i] == ',' && depth == 1) + commas.push_back(i); + } + if (depth != 0 || commas.size() != 1) + { + pos += tok.size(); + continue; + } + const string a = trim(line.substr(argStart, commas[0] - argStart)); + const string b = trim(line.substr(commas[0] + 1, i - 1 - commas[0] - 1)); + const string repl = "(" + a + c.op + b + ")"; + line.replace(pos, i - pos, repl); + pos += repl.size(); + } + } + return line; +} + +// Rewrite GLSL matrix constructors to WGSL: mat3(...) -> mat3x3f(...). (vec3(...) +// is left as-is; WGSL infers its component type.) +string rewriteMatrixCtors(string line) +{ + static constexpr std::array, 3> MATRIX_CTORS = { { + { "mat2(", "mat2x2f(" }, + { "mat3(", "mat3x3f(" }, + { "mat4(", "mat4x4f(" }, + } }; + for (const auto& m : MATRIX_CTORS) + { + const string from(m.first); + const string to(m.second); + size_t pos = 0; + while ((pos = line.find(from, pos)) != string::npos) + { + if (pos > 0 && isIdentChar(line[pos - 1])) + { + pos += from.size(); + continue; + } + line.replace(pos, from.size(), to); + pos += to.size(); + } + } + return line; +} + +// Rewrite genglsl math built-in alias calls (`mx_cos(` etc.) to WGSL built-ins. +string rewriteMathBuiltins(string line) +{ + // mx_atan maps to WGSL atan (1 arg) or atan2 (2 args). + { + const string tok = "mx_atan("; + size_t pos = 0; + while ((pos = line.find(tok, pos)) != string::npos) + { + if (pos > 0 && isIdentChar(line[pos - 1])) + { + pos += tok.size(); + continue; + } + int depth = 1; + size_t i = pos + tok.size(); + int commas = 0; + for (; i < line.size() && depth > 0; ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')') + depth--; + else if (line[i] == ',' && depth == 1) + commas++; + } + const string repl = (commas == 1) ? "atan2(" : "atan("; + line.replace(pos, tok.size(), repl); + pos += repl.size(); + } + } + + static constexpr std::array, 7> ALIASES = { { + { "mx_inversesqrt(", "inverseSqrt(" }, + { "mx_radians(", "radians(" }, + { "mx_asin(", "asin(" }, + { "mx_acos(", "acos(" }, + { "mx_sin(", "sin(" }, + { "mx_cos(", "cos(" }, + { "mx_tan(", "tan(" }, + } }; + for (const auto& alias : ALIASES) + { + const string from(alias.first); + const string to(alias.second); + size_t pos = 0; + while ((pos = line.find(from, pos)) != string::npos) + { + if (pos > 0 && isIdentChar(line[pos - 1])) + { + pos += from.size(); + continue; + } + line.replace(pos, from.size(), to); + pos += to.size(); + } + } + return line; +} + +// Add a `&` to the (out) result argument of the light-bridge call +// `sampleLightSource(light, position, result)` -> `(light, position, &result)`. +string rewriteSampleLightSource(string line) +{ + const string tok = "sampleLightSource("; + const size_t pos = line.find(tok); + if (pos == string::npos || (pos > 0 && isIdentChar(line[pos - 1]))) + return line; + // HwLightSamplerNode emits the GLSL definition `void sampleLightSource(...)`; only + // rewrite call sites (third argument needs `&`), not the definition signature. + { + const string head = trim(line.substr(0, pos)); + if (head == "void" || head.rfind("void ", 0) == 0) + return line; + } + const size_t argStart = pos + tok.size(); + int depth = 1; + std::vector commas; + size_t i = argStart; + for (; i < line.size() && depth > 0; ++i) + { + const char c = line[i]; + if (c == '(') + depth++; + else if (c == ')') + { + depth--; + if (depth == 0) + break; + } + else if (c == ',' && depth == 1) + commas.push_back(i); + } + if (depth != 0 || commas.size() != 2) + return line; + size_t a3 = commas[1] + 1; + while (a3 < line.size() && isHorizontalSpace(line[a3])) + a3++; + if (a3 < line.size() && line[a3] != '&') + line.insert(a3, "&"); + return line; +} + +// Rewrite a GLSL preprocessor `#define` to a WGSL `const` or type `alias`. +// #define M_PI 3.14159 -> const M_PI: f32 = 3.14159; +// #define CLOSURE_TYPE_X 1 -> const CLOSURE_TYPE_X: i32 = 1; +// #define EDF vec3 -> alias EDF = vec3f; +// #define material surfaceshader -> alias material = surfaceshader; +string rewriteDefine(const string& line) +{ + const size_t p = line.find("#define"); + string rest = trim(line.substr(p + 7)); + const size_t sp = rest.find_first_of(" \t"); + if (sp == string::npos) + return ""; // bare `#define NAME` guard — drop (WGSL has no preprocessor) + const string name = rest.substr(0, sp); + const string value = trim(rest.substr(sp)); + if (value.empty()) + return ""; + + // Drop genglsl math built-in aliases (`#define mx_cos cos`, `#define mx_float_bits_to_int + // floatBitsToInt`, ...) — a single-identifier value. Their call sites are rewritten to WGSL + // built-ins by rewriteMathBuiltins. + if (startsWith(name, "mx_") && isIdentifier(value)) + return ""; + + // A type-valued define becomes a WGSL type `alias`. mapType() converts a GLSL type spelling + // (`vec3` -> `vec3f`) and returns the value unchanged for the MaterialX closure structs, which + // are already valid WGSL type names -- so the alias target is just the mapped value. + const string mapped = mapType(value); + const bool isClosureType = value == "surfaceshader" || value == "material" || + value == "BSDF" || value == "EDF" || value == "VDF"; + if (isIdentifier(value) && (mapped != value || isClosureType)) + return "alias " + name + " = " + mapped + ";"; + + // Otherwise it is a numeric or expression define -> `const` (integer literal => i32, else f32). + const bool isInt = value.find_first_not_of("0123456789+-") == string::npos; + return "const " + name + ": " + (isInt ? "i32" : "f32") + " = " + value + ";"; +} + +// Rewrite the declaration in a `for ( i = ...; ...)` init clause to WGSL. +// for (int i = 0; i < n; i++) -> for (var i: i32 = 0; i < n; i += 1) +string rewriteForLoopInit(string line) +{ + size_t pos = 0; + while ((pos = line.find("for", pos)) != string::npos) + { + // Whole-word `for` followed by `(`. + const bool wordBefore = (pos > 0 && isIdentChar(line[pos - 1])); + size_t paren = pos + 3; + while (paren < line.size() && isHorizontalSpace(line[paren])) + paren++; + if (wordBefore || paren >= line.size() || line[paren] != '(') + { + pos += 3; + continue; + } + const size_t initStart = paren + 1; + const size_t semi = line.find(';', initStart); + if (semi == string::npos) + { + pos = paren + 1; + continue; + } + // The init clause is rewritten with the standard value-declaration pass. + const string init = line.substr(initStart, semi - initStart); + const string trimmedInit = trim(init); + const string rewritten = rewriteVariableDecl(trimmedInit); + if (rewritten != trimmedInit) + { + line.replace(initStart, semi - initStart, rewritten); + pos = line.find(';', initStart) + 1; + continue; + } + pos = semi + 1; + } + return line; +} + +// Rename GLSL identifiers that collide with WGSL keywords (e.g. a variable named +// `var` in the thin-film code). Runs before declarations introduce the `var` +// keyword, so every such token in the GLSL source is an identifier. +string rewriteReservedIdents(string line) +{ + static const char* RESERVED[] = { "var", "let", "loop" }; + for (const char* r : RESERVED) + { + const string word = r; + size_t pos = 0; + while ((pos = line.find(word, pos)) != string::npos) + { + const bool before = (pos > 0) && isIdentChar(line[pos - 1]); + const size_t after = pos + word.size(); + const bool afterIdent = (after < line.size()) && isIdentChar(line[after]); + // WGSL resource declarations use `var` / `var`; do not rename. + const bool wgslStorage = (word == "var" && after < line.size() && line[after] == '<'); + if (!before && !afterIdent && !wgslStorage) + { + line.insert(after, "_"); + pos = after + 1; + } + else + { + pos = after; + } + } + } + return line; +} + +// Split a line on top-level `;` (ignoring nested parens/brackets/braces). +StringVec splitTopLevelSemicolons(const string& line) +{ + StringVec out; + int angle = 0, paren = 0, brace = 0; + size_t start = 0; + for (size_t i = 0; i < line.size(); ++i) + { + const char c = line[i]; + if (c == '<') + angle++; + else if (c == '>') + angle--; + else if (c == '(') + paren++; + else if (c == ')') + paren--; + else if (c == '{') + brace++; + else if (c == '}') + brace--; + else if (c == ';' && angle == 0 && paren == 0 && brace == 0) + { + out.push_back(line.substr(start, i - start)); + start = i + 1; + } + } + out.push_back(line.substr(start)); + return out; +} + +bool looksLikeTypedDecl(const string& segIn) +{ + const string seg = trim(segIn); + if (seg.empty()) + return false; + size_t at = 0; + while (at < seg.size() && isHorizontalSpace(seg[at])) + at++; + for (const auto& entry : TYPE_MAP) + { + const string_view glslType = entry.first; + if (seg.size() >= at + glslType.size() && + seg.compare(at, glslType.size(), glslType.data(), glslType.size()) == 0 && + (at + glslType.size() >= seg.size() || isHorizontalSpace(seg[at + glslType.size()]))) + { + size_t nameStart = at + glslType.size(); + while (nameStart < seg.size() && isHorizontalSpace(seg[nameStart])) + nameStart++; + size_t nameEnd = nameStart; + while (nameEnd < seg.size() && isIdentChar(seg[nameEnd])) + nameEnd++; + if (nameEnd <= nameStart) + return false; + size_t after = nameEnd; + while (after < seg.size() && isHorizontalSpace(seg[after])) + after++; + return after >= seg.size() || seg[after] == '='; + } + } + return false; +} + +// Expand chained GLSL declarations on one line, e.g. +// `float h = hsv.x; float s = hsv.y; float v = hsv.z;` -> separate WGSL `var` decls. +string rewriteChainedDecls(string line) +{ + StringVec parts; + for (const string& p : splitTopLevelSemicolons(line)) + { + if (!trim(p).empty()) + parts.push_back(p); + } + if (parts.size() < 2) + return line; + for (const string& p : parts) + { + if (!looksLikeTypedDecl(p)) + return line; + } + const size_t ls = line.find_first_not_of(" \t"); + const string indent = (ls == string::npos) ? "" : line.substr(0, ls); + string out; + for (const string& p : parts) + { + const string seg = trim(p); + if (seg.empty()) + continue; + const string decl = rewriteVariableDecl(seg + ";"); + out += (out.empty() ? "" : " ") + trim(decl); + } + return indent + out; +} + +size_t findMatchingCloseParen(const string& s, size_t open) +{ + int depth = 0; + for (size_t i = open; i < s.size(); ++i) + { + if (s[i] == '(') + depth++; + else if (s[i] == ')') + { + depth--; + if (depth == 0) + return i; + } + } + return string::npos; +} + +// Wrap a single-statement control body on the same line in `{ }`. +// GLSL allows `else s = 0.0f;` and `else if (a) return x;`; WGSL requires blocks. +string rewriteBracelessControlSameLine(string line) +{ + const size_t ls = line.find_first_not_of(" \t"); + const string indent = (ls == string::npos) ? "" : line.substr(0, ls); + const string t = trim(line); + if (t.empty() || t.find('{') != string::npos) + return line; + + size_t headerEnd = string::npos; + + if ((t.compare(0, 8, "else if ") == 0) || (t.size() > 7 && t.compare(0, 7, "else if") == 0 && t[7] == '(')) + { + const size_t open = t.find('('); + if (open != string::npos) + { + const size_t close = findMatchingCloseParen(t, open); + if (close != string::npos) + headerEnd = close + 1; + } + } + else if (t.compare(0, 4, "else") == 0 && (t.size() == 4 || isHorizontalSpace(t[4]))) + { + size_t after = 4; + while (after < t.size() && isHorizontalSpace(t[after])) + after++; + if (after + 2 <= t.size() && t.compare(after, 2, "if") == 0 && + (after + 2 >= t.size() || !isIdentChar(t[after + 2]))) + return line; + // `else // comment` with a braced body on the following line — not a same-line stmt. + if (after >= t.size() || t.compare(after, 2, "//") == 0 || t.compare(after, 2, "/*") == 0) + return line; + headerEnd = after; + } + else + { + static constexpr string_view CONTROL_PREFIXES[] = { "if", "while", "for" }; + for (const string_view pfx : CONTROL_PREFIXES) + { + if (startsWith(t, pfx) && (t.size() == pfx.size() || isHorizontalSpace(t[pfx.size()]) || t[pfx.size()] == '(')) + { + const size_t open = t.find('('); + if (open != string::npos) + { + const size_t close = findMatchingCloseParen(t, open); + if (close != string::npos) + headerEnd = close + 1; + } + break; + } + } + } + + if (headerEnd == string::npos || headerEnd >= t.size()) + return line; + size_t bodyStart = headerEnd; + while (bodyStart < t.size() && isHorizontalSpace(t[bodyStart])) + bodyStart++; + if (bodyStart >= t.size() || t[bodyStart] == '{') + return line; + if (t.compare(bodyStart, 2, "//") == 0 || t.compare(bodyStart, 2, "/*") == 0) + return line; + + const string header = trim(t.substr(0, headerEnd)); + const string body = trim(t.substr(bodyStart)); + return indent + header + " { " + body + " }"; +} + +// Strip the GLSL `f` suffix from float literals (`0.0f` -> `0.0`). +string rewriteFloatLiteralSuffix(string line) +{ + for (size_t i = 0; i < line.size(); ++i) + { + const char c = line[i]; + if (c != 'f' && c != 'F') + continue; + if (i == 0) + continue; + const char prev = line[i - 1]; + if (!std::isdigit(static_cast(prev)) && prev != '.') + continue; + if (i + 1 < line.size() && isIdentChar(line[i + 1])) + continue; + // WGSL type names such as vec3f and mat4x4f end in `f` after a digit; do not treat those + // as GLSL float-literal suffixes (otherwise native WGSL struct/uniform declarations break). + size_t j = i - 1; + while (j > 0 && (std::isdigit(static_cast(line[j])) || line[j] == '.')) + --j; + if (j < i - 1 && isIdentChar(line[j])) + continue; + line.erase(i, 1); + if (i > 0) + --i; + } + return line; +} + +// Expand a GLSL multiple declaration `vec3 a, b;` into separate WGSL declarations +// `var a: vec3f; var b: vec3f;`. Only the no-initializer form is handled. +string rewriteMultiDecl(string line) +{ + size_t ls = 0; + while (ls < line.size() && isHorizontalSpace(line[ls])) + ls++; + size_t typeEnd = ls; + while (typeEnd < line.size() && (isalnum(static_cast(line[typeEnd])) || line[typeEnd] == '_')) + typeEnd++; + if (typeEnd == ls || typeEnd >= line.size() || !isHorizontalSpace(line[typeEnd])) + return line; + const string glslType = line.substr(ls, typeEnd - ls); + const string wtype = mapType(glslType); + if (wtype == glslType && glslType.compare(0, 3, "vec") != 0 && glslType.compare(0, 3, "mat") != 0 && + glslType != "float" && glslType != "int" && glslType != "bool") + return line; // not a known type + + const size_t semi = line.find(';', typeEnd); + if (semi == string::npos) + return line; + const string body = line.substr(typeEnd, semi - typeEnd); + if (body.find(',') == string::npos || body.find('=') != string::npos || body.find('(') != string::npos) + return line; // not a comma-separated declaration list + + StringVec names; + for (const string& n : splitTopLevel(body)) + { + const string nm = trim(n); + if (!isIdentifier(nm)) + return line; // not a plain identifier list + names.push_back(nm); + } + const string indent = line.substr(0, ls); + string out; + for (const string& nm : names) + out += (out.empty() ? "" : " ") + ("var " + nm + ": " + wtype + ";"); + return indent + out; +} + +// Replace ++/-- (prefix and postfix) with += 1 / -= 1. +string rewriteIncDec(string line) +{ + // Prefix: ++var -> var += 1, --var -> var -= 1 + const char* OPS[] = { "++", "--" }; + const char* DELTAS[] = { " += 1", " -= 1" }; + for (size_t k = 0; k < 2; ++k) + { + for (size_t pos = 0; (pos = line.find(OPS[k], pos)) != string::npos;) + { + size_t nameStart = pos + 2; + while (nameStart < line.size() && isHorizontalSpace(line[nameStart])) + nameStart++; + size_t nameEnd = nameStart; + while (nameEnd < line.size() && isIdentChar(line[nameEnd])) + nameEnd++; + if (nameEnd > nameStart) + { + string varName = line.substr(nameStart, nameEnd - nameStart); + line.replace(pos, nameEnd - pos, varName + DELTAS[k]); + pos = nameStart + varName.size(); + continue; + } + pos += 2; + } + } + // Postfix: var++ -> var += 1, var-- -> var -= 1 + for (size_t k = 0; k < 2; ++k) + { + size_t pos = 0; + while ((pos = line.find(OPS[k], pos)) != string::npos) + { + size_t nameEnd = pos; + while (nameEnd > 0 && isHorizontalSpace(line[nameEnd - 1])) + nameEnd--; + size_t nameStart = nameEnd; + while (nameStart > 0 && isIdentChar(line[nameStart - 1])) + nameStart--; + if (nameStart < nameEnd) + { + string varName = line.substr(nameStart, nameEnd - nameStart); + line.replace(nameStart, (pos + 2) - nameStart, varName + DELTAS[k]); + pos = nameStart + varName.size(); + continue; + } + pos += 2; + } + } + return line; +} + +// A value parameter renamed to make it mutable in WGSL: the signature takes +// `` and the body gets an injected `var = ;`. +const char* const ARG_SUFFIX = "_arg"; + +// Rewrite a single function-definition parameter to WGSL. Records out-parameter +// names (for body dereferencing) and copyable value parameters (for the mutable +// `var` copy injected at body start). Combined samplers are expanded into the +// split texture/sampler pair. +string rewriteParam(string p, StringVec& outNames, + std::vector>& valueParams) +{ + p = trim(p); + if (p.empty()) + return p; + + bool isOut = false; + if (p.compare(0, 4, "out ") == 0) + { + isOut = true; + p = trim(p.substr(4)); + } + else if (p.compare(0, 6, "inout ") == 0) + { + isOut = true; + p = trim(p.substr(6)); + } + else if (p.compare(0, 3, "in ") == 0) + { + p = trim(p.substr(3)); + } + + // Combined sampler: `sampler2D name` -> split texture + sampler (handle types). + if (p.compare(0, 10, "sampler2D ") == 0) + { + size_t s = 10; + const string name = readIdentifier(p, s); + return name + "_texture: texture_2d, " + name + "_sampler: sampler"; + } + + string name, type; + size_t colon = p.find(':'); + if (colon != string::npos) + { + // Already `name: type` (e.g. from token substitution); normalize the type. + name = trim(p.substr(0, colon)); + type = mapType(trim(p.substr(colon + 1))); + } + else + { + // GLSL `type name`. + size_t sp = p.find_first_of(" \t"); + if (sp == string::npos) + return p; // malformed; leave as-is + type = mapType(trim(p.substr(0, sp))); + name = trim(p.substr(sp + 1)); + } + + // Array parameter (`out vec3 Ap[4]` / `vec2 angles[4]`): fold the size into the WGSL type + // and keep `name` a bare identifier (out-name dereferencing indexes via `(*name)[i]`). + string arraySize; + if (splitArrayName(name, arraySize)) + type = "array<" + type + ", " + arraySize + ">"; + + if (isOut) + { + outNames.push_back(name); + return name + ": ptr"; + } + + // Value parameters are renamed and copied into a mutable local so bodies that + // assign to them compile under WGSL (parameters are immutable). Handle types + // (textures/samplers/pointers) cannot be copied and are passed through. + if (!isHandleType(type)) + { + valueParams.emplace_back(name, type); + return name + ARG_SUFFIX + ": " + type; + } + return name + ": " + type; +} + +struct RewrittenSignature +{ + string sig; + StringVec outNames; + std::vector> valueParams; +}; + +// Detect and rewrite a GLSL function-definition signature line. +std::optional rewriteSignature(const string& line) +{ + const size_t indentEnd = line.find_first_not_of(" \t"); + if (indentEnd == string::npos) + return std::nullopt; + const string indent = line.substr(0, indentEnd); + + const size_t paren = line.find('('); + if (paren == string::npos) + return std::nullopt; + + // Head must be exactly " " with no assignment. + const string head = trim(string_view(line).substr(indentEnd, paren - indentEnd)); + if (head.find('=') != string::npos) + return std::nullopt; + const size_t headSp = head.find_first_of(" \t"); + if (headSp == string::npos) + return std::nullopt; + const string retType = trim(head.substr(0, headSp)); + const string funcName = trim(head.substr(headSp + 1)); + if (funcName.empty() || funcName.find_first_of(" \t") != string::npos) + return std::nullopt; + if (!isKnownReturnType(retType)) + return std::nullopt; + + // Find the matching close paren for the parameter list. + int depth = 0; + size_t close = string::npos; + for (size_t i = paren; i < line.size(); ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')') + { + depth--; + if (depth == 0) + { + close = i; + break; + } + } + } + if (close == string::npos) + return std::nullopt; + + // After the parameter list only whitespace and an optional `{` may follow. + const string tail = trim(string_view(line).substr(close + 1)); + if (!tail.empty() && tail[0] != '{') + return std::nullopt; + + const string paramList = line.substr(paren + 1, close - paren - 1); + const StringVec params = splitTopLevel(paramList); + + RewrittenSignature rewritten; + string wgslParams; + for (const string& p : params) + { + const string wp = rewriteParam(p, rewritten.outNames, rewritten.valueParams); + if (wp.empty()) + continue; + if (!wgslParams.empty()) + wgslParams += ", "; + wgslParams += wp; + } + + string sig = indent + "fn " + funcName + "(" + wgslParams + ")"; + // Keep the WGSL return type for any non-void function (a function may have both + // a value return and `out` parameters); only `void` procedures have no return. + if (retType != "void") + sig += " -> " + mapType(retType); + sig += tail.empty() ? "" : " " + tail; + + rewritten.sig = std::move(sig); + return rewritten; +} + +// True when a line begins a function definition: ` (` +// with no assignment. Used to start multi-line signature accumulation. +bool looksLikeSignatureStart(const string& line) +{ + const size_t indentEnd = line.find_first_not_of(" \t"); + if (indentEnd == string::npos) + return false; + const size_t paren = line.find('('); + if (paren == string::npos) + return false; + const string head = trim(string_view(line).substr(indentEnd, paren - indentEnd)); + if (head.empty() || head.find('=') != string::npos) + return false; + const size_t sp = head.find_first_of(" \t"); + if (sp == string::npos) + return false; + const string retType = trim(head.substr(0, sp)); + const string name = trim(head.substr(sp + 1)); + if (name.empty() || name.find_first_of(" \t") != string::npos) + return false; + return isKnownReturnType(retType); +} + +// `(*name)` is only wrong as a lone function-call argument; undo that case so the +// callee still receives `ptr`. +// Identifier of the call that directly encloses the argument at `argPos` (i.e. the callee +// before the `(` containing it), or empty when the enclosing `(` is a grouping paren. +string enclosingCallee(const string& line, size_t argPos) +{ + int depth = 0; + size_t i = argPos; + while (i > 0) + { + --i; + const char c = line[i]; + if (c == ')') + depth++; + else if (c == '(') + { + if (depth == 0) + break; // `i` is the enclosing open paren + depth--; + } + } + if (i >= line.size() || line[i] != '(') + return {}; + size_t end = i; + while (end > 0 && isHorizontalSpace(line[end - 1])) + end--; + size_t start = end; + while (start > 0 && isIdentChar(line[start - 1])) + start--; + return line.substr(start, end - start); +} + +string undoDerefInCallArgs(string line, const StringVec& names, const StringSet& ptrFuncs) +{ + for (const string& name : names) + { + if (name.empty()) + continue; + const string deref = "(*" + name + ")"; + size_t pos = 0; + while ((pos = line.find(deref, pos)) != string::npos) + { + size_t before = pos; + while (before > 0 && isHorizontalSpace(line[before - 1])) + before--; + const bool okBefore = (before == 0) || line[before - 1] == '(' || line[before - 1] == ','; + + size_t after = pos + deref.size(); + while (after < line.size() && isHorizontalSpace(line[after])) + after++; + const bool okAfter = (after >= line.size()) || line[after] == ')' || line[after] == ','; + + // Only undo the dereference when the out parameter is forwarded to a function that + // actually takes a pointer (out/inout) argument at that position. Value calls and + // type constructors (`f32((*i))`, `mx_rotl32((*c), 4)`) read the value, so the + // dereference must be kept. + const bool forwardsPointer = okBefore && okAfter && ptrFuncs.count(enclosingCallee(line, pos)) > 0; + + if (forwardsPointer) + { + line.replace(pos, deref.size(), name); + pos += name.size(); + } + else + { + pos += deref.size(); + } + } + } + return line; +} + +// Dereference out-parameter names in expressions and assignments. References that forward the +// out parameter to another pointer-taking function (named in `ptrFuncs`) are restored to bare +// pointers by undoDerefInCallArgs. +string derefOutParams(string line, const StringVec& names, const StringSet& ptrFuncs) +{ + for (const string& name : names) + { + if (name.empty()) + continue; + size_t pos = 0; + const string repl = "(*" + name + ")"; + while ((pos = line.find(name, pos)) != string::npos) + { + const bool boundaryBefore = (pos == 0) || !isIdentChar(line[pos - 1]); + const size_t after = pos + name.size(); + const bool boundaryAfter = (after >= line.size()) || !isIdentChar(line[after]); + const bool alreadyDeref = (pos >= 2 && line[pos - 1] == '*' && line[pos - 2] == '('); + if (boundaryBefore && boundaryAfter && !alreadyDeref) + { + line.replace(pos, name.size(), repl); + pos += repl.size(); + } + else + { + pos = after; + } + } + } + return undoDerefInCallArgs(line, names, ptrFuncs); +} + +// A braceless control header `if (...)`, `else if (...)`, `for (...)`, `while (...)` +// or `else` with the single-statement body on the FOLLOWING line (GLSL allows this; +// WGSL requires a `{ }` block). The body-on-same-line case is left to the caller. +bool looksLikeControlHeader(const string& lineIn) +{ + const string t = trim(lineIn); + if (t.find('{') != string::npos) + return false; // already braced on this line + if (t.compare(0, 4, "else") == 0 && (t.size() == 4 || isHorizontalSpace(t[4]))) + { + size_t after = 4; + while (after < t.size() && isHorizontalSpace(t[after])) + after++; + if (after >= t.size()) + return true; // bare `else`, body on next line + if (t.compare(after, 2, "//") == 0 || t.compare(after, 2, "/*") == 0) + return true; // `else // comment`, braced body on next line + } + size_t paren = string::npos; + if (t.compare(0, 3, "if ") == 0 || t.compare(0, 3, "if(") == 0 || + t.compare(0, 8, "else if ") == 0 || t.compare(0, 7, "else if") == 0 || + t.compare(0, 4, "for ") == 0 || t.compare(0, 4, "for(") == 0 || + t.compare(0, 6, "while ") == 0 || t.compare(0, 6, "while(") == 0) + paren = t.find('('); + if (paren == string::npos) + return false; + int depth = 0; + size_t close = string::npos; + for (size_t i = paren; i < t.size(); ++i) + { + if (t[i] == '(') + depth++; + else if (t[i] == ')') + { + depth--; + if (depth == 0) + { + close = i; + break; + } + } + } + if (close == string::npos) + return false; + return trim(t.substr(close + 1)).empty(); // nothing after ')' -> body is next line +} + +// WGSL requires `if`/`while` conditions to be `bool`. MaterialX stores boolean HW uniforms +// (e.g. `u_refractionTwoSided`) as `i32`, because WGSL uniform buffers cannot hold `bool`. +// A GLSL boolean condition such as `if ($refractionTwoSided)` is substituted to +// `if (u_refractionTwoSided)`, which WGSL rejects ("if condition must be bool, got i32"). +// +// Rewrite a condition that is exactly a single `u_`-prefixed uniform reference to an explicit +// `!= 0` comparison. Only `u_` identifiers are touched, since those are precisely the +// bool-stored-as-i32 HW uniforms; genuine `bool` locals/params (never `u_`-prefixed) are left +// untouched so no valid `bool` condition is broken. +string rewriteBoolUniformCondition(string line) +{ + static const char* const KEYWORDS[] = { "if", "while" }; + for (const char* kw : KEYWORDS) + { + const string k = kw; + for (size_t pos = 0; (pos = line.find(k, pos)) != string::npos;) + { + const bool boundaryBefore = (pos == 0) || !isIdentChar(line[pos - 1]); + size_t i = pos + k.size(); + while (i < line.size() && isHorizontalSpace(line[i])) + i++; + if (!boundaryBefore || i >= line.size() || line[i] != '(') + { + pos += k.size(); + continue; + } + const size_t open = i; + size_t identStart = open + 1; + while (identStart < line.size() && isHorizontalSpace(line[identStart])) + identStart++; + if (identStart + 1 < line.size() && line[identStart] == 'u' && line[identStart + 1] == '_') + { + size_t identEnd = identStart; + while (identEnd < line.size() && isIdentChar(line[identEnd])) + identEnd++; + size_t afterIdent = identEnd; + while (afterIdent < line.size() && isHorizontalSpace(line[afterIdent])) + afterIdent++; + if (afterIdent < line.size() && line[afterIdent] == ')') + { + const string ident = line.substr(identStart, identEnd - identStart); + const string replacement = "(" + ident + " != 0)"; + line.replace(open, afterIdent - open + 1, replacement); + pos = open + replacement.size(); + continue; + } + } + pos += k.size(); + } + } + return line; +} + +// Split a GLSL `sampler2D tex` parameter into the WGSL texture + sampler pair. +string rewriteSamplerParams(string line) +{ + const string token = "sampler2D "; + size_t pos = 0; + while ((pos = line.find(token, pos)) != string::npos) + { + size_t nameStart = pos + token.size(); + const string name = readIdentifier(line, nameStart); + if (name.empty()) + { + pos += token.size(); + continue; + } + const string repl = name + "_texture: texture_2d, " + name + "_sampler: sampler"; + line.replace(pos, nameStart - pos, repl); + pos += repl.size(); + } + return line; +} + +// Rewrite a GLSL `texture(tex, uv)` read to the WGSL `mx_texture_sample(...)` helper. +string rewriteTextureSampling(string line) +{ + const string token = "texture("; + size_t pos = 0; + while ((pos = line.find(token, pos)) != string::npos) + { + if (pos > 0 && isIdentChar(line[pos - 1])) + { + pos += token.size(); + continue; + } + size_t argStart = pos + token.size(); + const string sampler = readIdentifier(line, argStart); + if (sampler.empty() || argStart >= line.size() || line[argStart] != ',') + { + pos += token.size(); + continue; + } + const string repl = "mx_texture_sample(" + sampler + "_texture, " + sampler + "_sampler,"; + line.replace(pos, (argStart + 1) - pos, repl); + pos += repl.size(); + } + return line; +} + +// Rewrite a whole multi-line GLSL block to WGSL, line by line. `#include` directives are +// left verbatim so the caller can resolve and rewrite them with path context. +// Collect the names of every function in `shader` that declares a pointer (out/inout) +// parameter, i.e. `name: ptr`. Signatures are single-line at conversion time. +StringSet collectPointerFunctions(const string& shader) +{ + StringSet fns; + std::istringstream scan(shader); + string sl; + while (std::getline(scan, sl)) + { + if (auto d = parseFnDef(sl)) + { + for (const string& p : splitTopLevel(d->params)) + { + const size_t colon = p.find(':'); + if (colon != string::npos && p.find("ptrname); + break; + } + } + } + } + return fns; +} + +string rewriteBlock(const string& block, const StringSet& ptrFuncs = {}) +{ + std::istringstream stream(block); + string line; + string result; + result.reserve(block.size() + block.size() / 8); + LineRewriter rewriter; + // Seed externally-defined pointer-taking functions (e.g. mx_directional_light) so that an + // out parameter forwarded to them is restored to a bare pointer rather than dereferenced. + rewriter.registerPointerFunctions(ptrFuncs); + while (std::getline(stream, line)) + { + bool hadCr = !line.empty() && line.back() == '\r'; + if (hadCr) + line.pop_back(); + + size_t firstNonSpace = line.find_first_not_of(" \t"); + bool isInclude = (firstNonSpace != string::npos && + line.compare(firstNonSpace, 8, "#include") == 0); + if (!isInclude) + line = rewriter.rewrite(line); + + result += line; + if (hadCr) + result += '\r'; + result += '\n'; + } + return result; +} + +// Rewrite GLSL combined-sampler reads to WGSL split form: +// texture(sampler2D(A, B), C) -> textureSample(A, B, C) +// textureLod(sampler2D(A, B), C, D) -> textureSampleLevel(A, B, C, D) +string rewriteCombinedTextureSampling(string line) +{ + struct Form + { + const char* glsl; + const char* wgsl; + }; + const Form forms[] = { + { "texture(sampler2D(", "textureSample(" }, + { "textureLod(sampler2D(", "textureSampleLevel(" }, + }; + for (const Form& f : forms) + { + const string token = f.glsl; + size_t pos = 0; + while ((pos = line.find(token, pos)) != string::npos) + { + if (pos > 0 && isIdentChar(line[pos - 1])) + { + pos += token.size(); + continue; + } + // Find the close of the inner sampler2D( ... ). + size_t inner = pos + token.size(); + int depth = 1; + size_t i = inner; + for (; i < line.size() && depth > 0; ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')') + depth--; + } + if (depth != 0) + { + pos += token.size(); + continue; + } + const size_t innerClose = i - 1; // index of the ')' closing sampler2D( + // Expect a following comma (the remaining args of the sample call). + size_t comma = innerClose + 1; + while (comma < line.size() && (line[comma] == ' ' || line[comma] == '\t')) + comma++; + if (comma >= line.size() || line[comma] != ',') + { + pos += token.size(); + continue; + } + const string innerArgs = line.substr(inner, innerClose - inner); // "A, B" + const string repl = string(f.wgsl) + innerArgs + ","; + line.replace(pos, (comma + 1) - pos, repl); + pos += repl.size(); + } + } + return line; +} + +// Ordinary (non-signature) line rewrite: shared GLSL→WGSL syntax conversions +// plus the WebGPU-specific sampler/texture deltas. +string rewriteNonSignatureLine(string line) +{ + // Reserved-word identifier rename runs only on raw library GLSL (this path), + // before declarations introduce the WGSL `var` keyword — never on the + // already-WGSL lines that reach rewriteAll via the generator's emitLine. + line = rewriteReservedIdents(std::move(line)); + line = rewriteSamplerParams(std::move(line)); + line = rewriteAll(std::move(line)); + line = rewriteCombinedTextureSampling(std::move(line)); + line = rewriteTextureSampling(std::move(line)); + return line; +} + +// Known compile-time macro values used to evaluate preprocessor conditionals. +// DIRECTIONAL_ALBEDO_METHOD 0 selects the self-contained analytic albedo path +// (no albedo-table texture). +int knownMacroValue(const string& name, bool& known) +{ + known = true; + if (name == "DIRECTIONAL_ALBEDO_METHOD") + return 0; + known = false; + return 0; +} + +// Evaluate a simple `#if`/`#elif` expression: `IDENT == INT`, `IDENT != INT`, +// `defined(IDENT)`, or a bare identifier/number. Unknown macros evaluate to 0. +bool evalPPExpr(const string& exprIn) +{ + string expr = trim(exprIn); + if (expr.compare(0, 8, "defined(") == 0) + return true; // treat referenced macros as defined + const size_t eq = expr.find("=="); + const size_t ne = expr.find("!="); + if (eq != string::npos || ne != string::npos) + { + const bool isEq = (eq != string::npos); + const size_t op = isEq ? eq : ne; + const string lhs = trim(expr.substr(0, op)); + const string rhs = trim(expr.substr(op + 2)); + bool known; + const int lval = knownMacroValue(lhs, known); + const int rval = std::atoi(rhs.c_str()); + return isEq ? (lval == rval) : (lval != rval); + } + // Bare identifier/number: nonzero is true. + bool known; + const int v = (expr.find_first_not_of("0123456789") == string::npos) ? std::atoi(expr.c_str()) : knownMacroValue(expr, known); + return v != 0; +} + +// Make a WGSL type name safe to embed in an identifier suffix. +string typeCode(const string& type) +{ + string t = type; + for (char& c : t) + if (!std::isalnum(static_cast(c))) + c = '_'; + return t; +} + +// Split a function parameter list into the ordered parameter type strings. +StringVec paramTypes(const string& params) +{ + StringVec types; + for (const string& p : splitTopLevel(params)) + { + const size_t colon = p.find(':'); + if (colon != string::npos) + types.push_back(trim(p.substr(colon + 1))); + } + return types; +} + +// Infer the WGSL type of an argument expression from the symbol tables. +string inferArgType(const string& argIn, + const std::map& locals, + const std::map& globals, + const std::map>& structs) +{ + string a = trim(argIn); + if (a.empty()) + return ""; + // Strip a fully-enclosing pair of parentheses: "(expr)" -> "expr". + while (a.size() >= 2 && a.front() == '(' && a.back() == ')') + { + int depth = 0; + bool wraps = true; + for (size_t i = 0; i < a.size(); ++i) + { + if (a[i] == '(') + depth++; + else if (a[i] == ')') + depth--; + if (depth == 0 && i + 1 < a.size()) + { + wraps = false; + break; + } + } + if (!wraps) + break; + a = trim(a.substr(1, a.size() - 2)); + if (a.empty()) + return ""; + } + if (a == "true" || a == "false") + return "bool"; + // Numeric literal — the WHOLE token must be numeric (else it is an expression). + if ((std::isdigit(static_cast(a[0])) || a[0] == '-' || a[0] == '+') && + a.find_first_not_of("0123456789.eE+-fF") == string::npos && + a.find_first_of("0123456789") != string::npos) + return (a.find('.') != string::npos || a.find('e') != string::npos || a.find('E') != string::npos || a.back() == 'f') ? "f32" : "i32"; + // Constructor expression. + static constexpr string_view CTORS[] = { + "vec2f", "vec3f", "vec4f", "mat2x2f", "mat3x3f", "mat4x4f", "f32", "i32", "u32" + }; + for (const string_view c : CTORS) + if (startsWith(a, c) && a.size() > c.size() && a[c.size()] == '(') + return string(c.data(), c.size()); + if (startsWith(a, "vec3<")) + return "vec3f"; + if (startsWith(a, "vec2<")) + return "vec2f"; + if (startsWith(a, "vec4<")) + return "vec4f"; + + // Type-preserving unary built-in call f(x): the result type equals x's type. + { + const size_t lp = a.find('('); + if (lp != string::npos && !a.empty() && a.back() == ')' && + a.substr(0, lp).find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") == string::npos) + { + const string fn = a.substr(0, lp); + static const char* PRESERVE[] = { + "sqrt", "abs", "normalize", "floor", "ceil", "fract", "exp", "exp2", + "log", "log2", "sin", "cos", "tan", "asin", "acos", "inverseSqrt", + "sign", "radians", "degrees", "saturate", "mx_square", "pow", "reflect" + }; + for (const char* p : PRESERVE) + if (fn == p) + { + const StringVec inner = splitTopLevel(a.substr(lp + 1, a.size() - lp - 2)); + return inner.empty() ? string() : inferArgType(inner[0], locals, globals, structs); + } + // User-function call: use its recorded return type (globals "fn:" prefix). + const auto fr = globals.find("fn:" + fn); + if (fr != globals.end() && fr->second != "void") + return fr->second; + } + } + + // Top-level binary operator (a OP b): the expression type is the vector operand + // if either is a float vector, else the first known operand type. + { + int paren = 0; + for (size_t i = a.size(); i-- > 0;) + { + const char c = a[i]; + if (c == ')' || c == ']' || c == '>') + paren++; + else if (c == '(' || c == '[' || c == '<') + paren--; + else if (paren == 0 && (c == '+' || c == '-' || c == '*' || c == '/')) + { + // Require a complete operand to the left (else it is unary / not an operator). + size_t j = i; + while (j > 0 && isHorizontalSpace(a[j - 1])) + j--; + if (j == 0) + continue; + const char prev = a[j - 1]; + if (!(isIdentChar(prev) || prev == ')' || prev == ']')) + continue; + const string lt = inferArgType(a.substr(0, i), locals, globals, structs); + const string rt = inferArgType(a.substr(i + 1), locals, globals, structs); + if (lt == "vec2f" || lt == "vec3f" || lt == "vec4f") + return lt; + if (rt == "vec2f" || rt == "vec3f" || rt == "vec4f") + return rt; + if (!lt.empty()) + return lt; + if (!rt.empty()) + return rt; + return ""; + } + } + } + + // Swizzle access base.{xyzw|rgba}: the component scalar follows the base vector's scalar + // type (uvec/ivec yield u32/i32, not f32). 1 component -> scalar, 2/3/4 -> vecN of it. + { + const size_t dot = a.rfind('.'); + if (dot != string::npos && dot + 1 < a.size()) + { + const string sw = a.substr(dot + 1); + if (sw.size() >= 1 && sw.size() <= 4 && sw.find_first_not_of("xyzwrgba") == string::npos) + { + string scalar = "f32"; + const string base = a.substr(0, dot); + if (base.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") == string::npos) + { + auto l = locals.find(base); + const string* vt = (l != locals.end()) ? &l->second : nullptr; + if (!vt) + { + auto g = globals.find(base); + if (g != globals.end()) + vt = &g->second; + } + if (vt) + { + if (vt->find("u32") != string::npos) + scalar = "u32"; + else if (vt->find("i32") != string::npos) + scalar = "i32"; + } + } + if (sw.size() == 1) + return scalar; + // Multi-component swizzle: only the float spelling (vecNf) is well-defined here; + // leave integer multi-swizzles unknown rather than guess a mismatching type. + return scalar == "f32" ? ("vec" + std::to_string(sw.size()) + "f") : string(); + } + } + } + + // Member access X.Y (single level). + const size_t dot = a.find('.'); + if (dot != string::npos && a.find('(') == string::npos) + { + const string base = a.substr(0, dot); + const string member = a.substr(dot + 1); + auto bt = locals.count(base) ? locals.find(base) : globals.find(base); + if (bt != (locals.count(base) ? locals.end() : globals.end())) + { + auto s = structs.find(bt->second); + if (s != structs.end()) + { + auto m = s->second.find(member); + if (m != s->second.end()) + return m->second; + } + } + return ""; + } + // Plain identifier. + if (a.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") == string::npos) + { + auto l = locals.find(a); + if (l != locals.end()) + return l->second; + auto g = globals.find(a); + if (g != globals.end()) + return g->second; + } + return ""; +} + +// Collect every `var/let/const NAME: TYPE` declaration on a line into `out` +// (a line may declare several, e.g. `var a: vec3f; var b: vec3f;`). +void collectVarDecl(const string& line, std::map& out) +{ + for (const char* kw : { "var ", "let ", "const " }) + { + const string kwStr = kw; + size_t k = 0; + while ((k = line.find(kwStr, k)) != string::npos) + { + // Whole-word keyword (preceded by a non-identifier char). + if (k > 0 && isIdentChar(line[k - 1])) + { + k += kwStr.size(); + continue; + } + size_t p = k + kwStr.size(); + while (p < line.size() && isHorizontalSpace(line[p])) + p++; + size_t nameEnd = p; + while (nameEnd < line.size() && isIdentChar(line[nameEnd])) + nameEnd++; + const string name = line.substr(p, nameEnd - p); + size_t c = nameEnd; + while (c < line.size() && isHorizontalSpace(line[c])) + c++; + if (c >= line.size() || line[c] != ':') + { + k = p; + continue; + } + size_t ts = c + 1; + while (ts < line.size() && isHorizontalSpace(line[ts])) + ts++; + size_t te = ts; + int angle = 0; + while (te < line.size()) + { + char ch = line[te]; + if (ch == '<') + angle++; + else if (ch == '>') + angle--; + else if (angle == 0 && (ch == ';' || ch == '=' || isHorizontalSpace(ch))) + break; + te++; + } + if (te > ts && !name.empty()) + out[name] = trim(line.substr(ts, te - ts)); + k = te; + } + } +} + +bool isScalarType(const string& t) +{ + return t == "f32" || t == "i32" || t == "u32" || t == "f16"; +} +bool isVecFloatType(const string& t) +{ + return t == "vec2f" || t == "vec3f" || t == "vec4f"; +} + +// GLSL broadcasts scalar arguments in component-wise built-ins (max(vec3, 0.0)); +// WGSL requires matching types. Promote scalar args to the call's vector type: +// max(v, 0.1) -> max(v, vec3f(0.1)). +string rewriteScalarBroadcast(string line, + const std::map& locals, + const std::map& globals, + const std::map>& structs) +{ + static const char* BUILTINS[] = { "max(", "min(", "pow(", "mod(", "step(", "clamp(", "smoothstep(" }; + for (const char* b : BUILTINS) + { + const string tok = b; + size_t pos = 0; + while ((pos = line.find(tok, pos)) != string::npos) + { + if (pos > 0 && isIdentChar(line[pos - 1])) + { + pos += tok.size(); + continue; + } + const size_t argStart = pos + tok.size(); + int depth = 1; + size_t i = argStart; + for (; i < line.size() && depth > 0; ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')') + depth--; + } + if (depth != 0) + break; // unterminated call on this line; nothing more to match + const size_t close = i - 1; + StringVec args = splitTopLevel(line.substr(argStart, close - argStart)); + + string vecType; + StringVec types; + for (const string& a : args) + { + const string t = inferArgType(a, locals, globals, structs); + types.push_back(t); + if (vecType.empty() && isVecFloatType(t)) + vecType = t; + } + bool changed = false; + if (!vecType.empty()) + { + for (size_t a = 0; a < args.size(); ++a) + if (isScalarType(types[a])) + { + args[a] = vecType + "(" + args[a] + ")"; + changed = true; + } + } + if (changed) + { + string rebuilt = tok; + for (size_t a = 0; a < args.size(); ++a) + rebuilt += (a ? ", " : "") + args[a]; + rebuilt += ")"; + line.replace(pos, (close + 1) - pos, rebuilt); + pos += rebuilt.size(); + } + else + { + pos += tok.size(); + } + } + } + return line; +} + +// GLSL `mix(x, y, boolMask)` has no WGSL equivalent (mix needs a float t); it maps +// to `select(x, y, boolMask)` (same argument order). Rewrite when the 3rd argument +// infers to a boolean type. +string rewriteMixToSelect(string line, + const std::map& locals, + const std::map& globals, + const std::map>& structs) +{ + const string tok = "mix("; + size_t pos = 0; + while ((pos = line.find(tok, pos)) != string::npos) + { + if (pos > 0 && isIdentChar(line[pos - 1])) + { + pos += tok.size(); + continue; + } + const size_t argStart = pos + tok.size(); + int depth = 1; + size_t i = argStart; + for (; i < line.size() && depth > 0; ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')') + depth--; + } + if (depth != 0) + { + pos += tok.size(); + break; + } + const StringVec args = splitTopLevel(line.substr(argStart, i - 1 - argStart)); + if (args.size() == 3) + { + const string t = inferArgType(args[2], locals, globals, structs); + if (t.find("bool") != string::npos) + { + line.replace(pos, 3, "select"); + pos += string("select").size(); + continue; + } + } + pos += tok.size(); + } + return line; +} + +// Insert `&` before out-arguments at call sites of functions with `ptr` +// parameters (GLSL passes out-args by bare name; WGSL needs the address). `fnPtr` +// maps function name -> list of per-overload ptr-position flag vectors. +string addAddressOfToOutArgs(string line, + const std::map>>& fnPtr, + const std::map& locals) +{ + for (const auto& entry : fnPtr) + { + const string token = entry.first + "("; + size_t pos = 0; + while ((pos = line.find(token, pos)) != string::npos) + { + const bool defHere = (pos >= 3 && line.compare(pos - 3, 3, "fn ") == 0); + if ((pos > 0 && isIdentChar(line[pos - 1])) || defHere) + { + pos += token.size(); + continue; + } + const size_t argStart = pos + token.size(); + int depth = 1; + size_t i = argStart; + for (; i < line.size() && depth > 0; ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')') + depth--; + } + if (depth != 0) + { + pos += token.size(); + break; + } + const size_t close = i - 1; + StringVec args = splitTopLevel(line.substr(argStart, close - argStart)); + + const std::vector* flags = nullptr; + for (const auto& f : entry.second) + if (f.size() == args.size()) + { + flags = &f; + break; + } + bool changed = false; + if (flags) + { + for (size_t a = 0; a < args.size(); ++a) + { + if (!(*flags)[a]) + continue; + const string arg = trim(args[a]); + if (arg.empty() || arg[0] == '&') + continue; + if (arg.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") != string::npos) + continue; + const auto it = locals.find(arg); + if (it != locals.end() && it->second.compare(0, 3, "ptr") == 0) + continue; // already a pointer + args[a] = "&" + arg; + changed = true; + } + } + if (changed) + { + string rebuilt = token; + for (size_t a = 0; a < args.size(); ++a) + rebuilt += (a ? ", " : "") + args[a]; + rebuilt += ")"; + line.replace(pos, (close + 1) - pos, rebuilt); + pos += rebuilt.size(); + } + else + { + pos += token.size(); + } + } + } + return line; +} + +// Rewrite overloaded call sites on a line using the symbol tables. +string rewriteOverloadCalls(string line, + const std::map>>& overloads, + const std::map& locals, + const std::map& globals, + const std::map>& structs) +{ + for (const auto& entry : overloads) + { + const string& name = entry.first; + const string token = name + "("; + size_t pos = 0; + while ((pos = line.find(token, pos)) != string::npos) + { + // Skip the definition itself and identifier-suffix false matches. + const bool defHere = (pos >= 3 && line.compare(pos - 3, 3, "fn ") == 0); + if ((pos > 0 && isIdentChar(line[pos - 1])) || defHere) + { + pos += token.size(); + continue; + } + + size_t argStart = pos + token.size(); + int depth = 1; + size_t i = argStart; + for (; i < line.size() && depth > 0; ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')') + depth--; + } + if (depth != 0) + { + pos += token.size(); + break; + } + const size_t close = i - 1; + const StringVec args = splitTopLevel(line.substr(argStart, close - argStart)); + + // Candidate overloads with the matching argument count. + string chosen; + int bestScore = -1; + for (const auto& ov : entry.second) + { + if (ov.first.size() != args.size()) + continue; + int score = 0; + bool mismatch = false; + for (size_t a = 0; a < args.size(); ++a) + { + const string t = inferArgType(args[a], locals, globals, structs); + if (t.empty()) + continue; // unknown: neither matches nor conflicts + if (t == ov.first[a]) + score++; + else + { + mismatch = true; + break; + } + } + if (!mismatch && score > bestScore) + { + bestScore = score; + chosen = ov.second; + } + } + if (!chosen.empty() && chosen != name) + { + line.replace(pos, name.size(), chosen); + pos += chosen.size() + 1; + } + else + { + pos += token.size(); + } + } + } + return line; +} + +// GLSL `vecN(...)` constructors are always float (ivec/uvec are the integer forms), and GLSL +// implicitly converts integer arguments. WGSL does neither: `vec2(intExpr, intExpr)` yields +// vec2. Emit the explicit float type and cast any integer scalar argument, e.g. +// `vec2(x+xoff, y+yoff)` -> `vec2f(f32(x+xoff), f32(y+yoff))`. Needs argument-type info, so it +// runs in the overload-resolution pass where locals/globals/structs are known. +string rewriteFloatVectorCtors(string line, + const std::map& locals, + const std::map& globals, + const std::map>& structs) +{ + for (int n = 2; n <= 4; ++n) + { + const string token = "vec" + std::to_string(n) + "("; + size_t pos = 0; + while ((pos = line.find(token, pos)) != string::npos) + { + // Skip ivec/uvec/bvec and identifier suffixes (`myvec2(` etc.). + if (pos > 0 && isIdentChar(line[pos - 1])) + { + pos += token.size(); + continue; + } + const size_t argStart = pos + token.size(); + int depth = 1; + size_t i = argStart; + for (; i < line.size() && depth > 0; ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')') + depth--; + } + if (depth != 0) + { + pos += token.size(); + break; + } + const size_t close = i - 1; + const StringVec args = splitTopLevel(line.substr(argStart, close - argStart)); + string rebuilt; + for (size_t a = 0; a < args.size(); ++a) + { + const string t = inferArgType(args[a], locals, globals, structs); + const bool isInt = (t == "i32" || t == "u32"); + if (a) + rebuilt += ", "; + rebuilt += isInt ? ("f32(" + args[a] + ")") : args[a]; + } + const string repl = "vec" + std::to_string(n) + "f(" + rebuilt + ")"; + line.replace(pos, (close + 1) - pos, repl); + pos += repl.size(); + } + } + return line; +} + +// WGSL requires the right operand of `<<` / `>>` to be u32, but GLSL allows a signed shift +// count (`x << k` with `int k`, `h >> 8`). Wrap the shift count in `u32(...)` (idempotent for +// operands already typed u32). Runs on GLSL-flavoured input where `<<` / `>>` are unambiguous +// shift operators; a non-value operand (e.g. a stray generic `>>` close) is left untouched. +string rewriteShiftCounts(string line) +{ + for (const char* op : { "<<", ">>" }) + { + size_t pos = 0; + while ((pos = line.find(op, pos)) != string::npos) + { + size_t rhs = pos + 2; + while (rhs < line.size() && isHorizontalSpace(line[rhs])) + rhs++; + if (rhs >= line.size() || line[rhs] == '=') // end of line or compound assign (<<=) + { + pos += 2; + continue; + } + + // Extract the shift count: a parenthesized group or a single identifier/number token. + size_t end; + if (line[rhs] == '(') + { + int depth = 0; + size_t i = rhs; + for (; i < line.size(); ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')' && --depth == 0) + { + ++i; + break; + } + } + end = i; + } + else if (std::isalnum(static_cast(line[rhs])) || line[rhs] == '_') + { + size_t i = rhs; + while (i < line.size() && (isIdentChar(line[i]) || line[i] == '.')) + i++; + end = i; + } + else + { + pos += 2; // not a value operand (punctuation / generic close) — leave it + continue; + } + + const string operand = line.substr(rhs, end - rhs); + if (operand.rfind("u32(", 0) == 0) // already wrapped + { + pos = end; + continue; + } + const string repl = "u32(" + operand + ")"; + line.replace(rhs, end - rhs, repl); + pos = rhs + repl.size(); + } + } + return line; +} + +// GLSL screen-space derivative built-ins are spelled differently in WGSL: dFdx -> dpdx, +// dFdy -> dpdy (fwidth is the same in both). Whole-word rename on library GLSL. +string rewriteDerivativeBuiltins(string line) +{ + static const std::pair RENAMES[] = { + { "dFdx", "dpdx" }, { "dFdy", "dpdy" } + }; + for (const auto& r : RENAMES) + { + const string from = r.first; + const string to = r.second; + size_t pos = 0; + while ((pos = line.find(from, pos)) != string::npos) + { + const bool boundaryBefore = (pos == 0) || !isIdentChar(line[pos - 1]); + const size_t after = pos + from.size(); + const bool boundaryAfter = (after >= line.size()) || !isIdentChar(line[after]); + if (boundaryBefore && boundaryAfter) + { + line.replace(pos, from.size(), to); + pos += to.size(); + } + else + { + pos = after; + } + } + } + return line; +} +} // namespace + +// ─── Public entry points ──────────────────────────────────────────────────── + +string mapType(const string& glslType) +{ + if (glslType == "texture2D") + return "texture_2d"; + const string_view mapped = lookupWgslType(glslType); + return string(mapped.data(), mapped.size()); +} + +string rewriteAll(string line) +{ + // Preprocessor defines become WGSL const/alias declarations. + const size_t firstNonSpace = line.find_first_not_of(" \t"); + if (firstNonSpace != string::npos && line.compare(firstNonSpace, 7, "#define") == 0) + return rewriteDefine(line); + + // Coerce shift counts to u32 first, while `<<`/`>>` are still unambiguous shift operators + // (before type conversion can introduce WGSL `<...>` generics). + line = rewriteShiftCounts(std::move(line)); + line = rewriteScalarCasts(std::move(line)); + line = rewriteConst(line); + line = rewriteTernaries(std::move(line)); + line = rewriteIncDec(std::move(line)); + line = rewriteBracelessControlSameLine(std::move(line)); + line = rewriteChainedDecls(std::move(line)); + line = rewriteMultiDecl(std::move(line)); + line = rewriteVariableDecl(std::move(line)); + line = rewriteStructVariableDecl(std::move(line)); + line = rewriteForLoopInit(std::move(line)); + line = rewriteVectorCompare(std::move(line)); + line = rewriteMatrixCtors(std::move(line)); + line = rewriteMathBuiltins(std::move(line)); + line = rewriteDerivativeBuiltins(std::move(line)); + line = rewriteSampleLightSource(std::move(line)); + line = rewriteBoolUniformCondition(std::move(line)); + line = rewriteFloatLiteralSuffix(std::move(line)); + // Collapse the occasional doubled statement terminator (WGSL has no empty statement). + for (size_t p; (p = line.find(";;")) != string::npos;) + line.replace(p, 2, ";"); + return line; +} + +// ─── LineRewriter ─────────────────────────────────────────────────────────── + +string LineRewriter::beginFunction(const string& sig, StringVec&& outNames, + const std::vector>& valueParams) +{ + _func.active = true; + _func.seenOpenBrace = false; + _func.braceDepth = 0; + _func.injected = false; + + // Record this function as taking a pointer (out/inout) parameter, so call sites that + // forward an out parameter to it are restored to bare pointers (undoDerefInCallArgs). + if (!outNames.empty()) + { + const size_t fnPos = sig.find("fn "); + if (fnPos != string::npos) + { + size_t s = fnPos + 3; + while (s < sig.size() && isHorizontalSpace(sig[s])) + s++; + size_t e = s; + while (e < sig.size() && isIdentChar(sig[e])) + e++; + if (e > s) + _ptrFuncs.insert(sig.substr(s, e - s)); + } + } + + _func.outParams = std::move(outNames); + + // Build the mutable `var` copies for value parameters. + _func.paramInjections.clear(); + for (const auto& vp : valueParams) + _func.paramInjections.push_back(" var " + vp.first + ": " + vp.second + " = " + vp.first + ARG_SUFFIX + ";"); + + string out = sig; + countBraces(sig, _func.braceDepth, _func.seenOpenBrace); + if (_func.seenOpenBrace) + { + // The signature line also opened the body brace (single-line function, or + // brace on the signature line): inject the var copies just after the `{` + // so they land inside the body, not after a single-line function's `}`. + const size_t bpos = out.find('{'); + if (bpos != string::npos && !_func.paramInjections.empty()) + { + string inj; + for (const string& i : _func.paramInjections) + inj += " " + trim(i); + out.insert(bpos + 1, inj); + } + _func.injected = true; + } + if (_func.seenOpenBrace && _func.braceDepth <= 0) + leaveFunctionScope(); + return out; +} + +void LineRewriter::leaveFunctionScope() +{ + _func.active = false; + _func.outParams.clear(); + _func.paramInjections.clear(); +} + +// Rewrite a GLSL struct member `type name;` to a WGSL member `name: type,`. +static string rewriteStructMember(const string& line) +{ + const size_t indentEnd = line.find_first_not_of(" \t"); + if (indentEnd == string::npos) + return line; + const string indent = line.substr(0, indentEnd); + string body = line.substr(indentEnd); + // Drop a trailing ';'. + const size_t semi = body.find(';'); + if (semi == string::npos) + return line; // not a simple member declaration + body = body.substr(0, semi); + const size_t sp = body.find_first_of(" \t"); + if (sp == string::npos) + return line; + const string type = mapType(body.substr(0, sp)); + string name = body.substr(body.find_first_not_of(" \t", sp)); + if (name.empty() || name.find_first_of(" \t") != string::npos) + return line; + // Array member (`vec2 coords[3];`) -> `coords: array,`. + string arraySize; + const string memberType = splitArrayName(name, arraySize) + ? ("array<" + type + ", " + arraySize + ">") + : type; + return indent + name + ": " + memberType + ","; +} + +bool LineRewriter::Preprocessor::active() const +{ + for (const Frame& f : stack) + if (!f.active) + return false; + return true; +} + +void LineRewriter::Preprocessor::update(const string& t) +{ + auto directive = [&](string_view d) { return startsWith(trimView(t), d); }; + if (directive("#ifdef") || directive("#ifndef") || directive("#if")) + { + const bool parentActive = active(); + bool cond; + if (directive("#ifdef")) + cond = true; // referenced macros treated as defined + else if (directive("#ifndef")) + cond = false; + else + cond = evalPPExpr(t.substr(3)); + const bool on = parentActive && cond; + stack.push_back({ on, on, parentActive }); + } + else if (directive("#elif")) + { + if (!stack.empty()) + { + Frame& f = stack.back(); + const bool cond = evalPPExpr(t.substr(5)); + f.active = f.parentActive && !f.taken && cond; + f.taken = f.taken || f.active; + } + } + else if (directive("#else")) + { + if (!stack.empty()) + { + Frame& f = stack.back(); + f.active = f.parentActive && !f.taken; + f.taken = true; + } + } + else if (directive("#endif")) + { + if (!stack.empty()) + stack.pop_back(); + } +} + +string LineRewriter::rewrite(const string& line) +{ + string out; + if (handlePreprocessor(line, out)) + return out; + if (handleStruct(line, out)) + return out; + if (_func.active) + return rewriteFunctionBodyLine(line); + if (handleSignature(line, out)) + return out; + return rewriteNonSignatureLine(line); +} + +// Minimal C preprocessor for `#if/#elif/#else/#endif`: evaluate conditionals (WGSL has +// none) and drop both the directive lines and any lines in inactive branches. +bool LineRewriter::handlePreprocessor(const string& line, string& out) +{ + const string t = trim(line); + if (!t.empty() && t[0] == '#' && + (startsWith(t, "#if") || startsWith(t, "#elif") || startsWith(t, "#else") || startsWith(t, "#endif"))) + { + _pp.update(t); + out.clear(); + return true; + } + if (!_pp.active()) + { + out.clear(); // inside an inactive preprocessor branch + return true; + } + return false; +} + +// Struct definition body: rewrite GLSL members to WGSL and drop the trailing semicolon +// on the closing brace. Entered by a `struct Name {` / `struct Name` header. +bool LineRewriter::handleStruct(const string& line, string& out) +{ + const string t = trim(line); + if (_inStruct) + { + if (t == "};" || t == "}") + { + _inStruct = false; + const size_t indentEnd = line.find_first_not_of(" \t"); + out = (indentEnd == string::npos ? "" : line.substr(0, indentEnd)) + "}"; + } + else if (t.empty() || t[0] == '/') + { + out = line; // blank or comment + } + else + { + out = rewriteStructMember(line); + } + return true; + } + // Enter struct mode for `struct Name {` or `struct Name` (brace on next line), + // but not a single-line `struct Name { ... };`. + if (startsWith(t, "struct ") && t.find('}') == string::npos) + { + _inStruct = true; + out = line; // valid WGSL as-is + return true; + } + return false; +} + +// A line inside a function body: resolve buffered braceless headers and multi-line +// ternaries, dereference out parameters, and inject value-parameter copies. +string LineRewriter::rewriteFunctionBodyLine(const string& line) +{ + // Resolve a buffered braceless control header now that we can see its body. + if (_pendingHeader.active) + { + const string bt = trim(line); + // Skip blank lines and comments between the header and its body. + if (bt.empty() || startsWith(bt, "//") || startsWith(bt, "/*")) + return ""; + _pendingHeader.active = false; + const string hdr = rewriteNonSignatureLine(derefOutParams(_pendingHeader.line, _func.outParams, _ptrFuncs)); + const string body = rewriteNonSignatureLine(derefOutParams(line, _func.outParams, _ptrFuncs)); + countBraces(line, _func.braceDepth, _func.seenOpenBrace); + if (_func.seenOpenBrace && _func.braceDepth <= 0) + leaveFunctionScope(); + if (!bt.empty() && bt[0] == '{') + return hdr + "\n" + body; // body is a normal `{` block + return hdr + " {\n" + body + "\n}"; // wrap the single-statement body + } + if (looksLikeControlHeader(line)) + { + _pendingHeader.active = true; + _pendingHeader.line = line; + return ""; + } + + // Accumulate a ternary expression that spans multiple lines (the per-line rewrite + // needs the whole `cond ? a : b` on one line). A continuation begins when a line + // ends with `?` and runs until the statement terminator `;`. + if (_ternary.active) + { + _ternary.buffer += " " + trim(line); + if (!trim(line).empty() && trim(line).back() == ';') + { + const string joined = _ternary.buffer; + _ternary.active = false; + _ternary.buffer.clear(); + const string done = rewriteNonSignatureLine(derefOutParams(joined, _func.outParams, _ptrFuncs)); + countBraces(joined, _func.braceDepth, _func.seenOpenBrace); + if (_func.seenOpenBrace && _func.braceDepth <= 0) + leaveFunctionScope(); + return done; + } + return ""; + } + if (!trim(line).empty() && trim(line).back() == '?') + { + _ternary.active = true; + _ternary.buffer = trim(line); + return ""; + } + + // Ordinary body line: dereference out parameters, then apply the shared syntax + // rewrites (body lines never match the signature heuristic). + string out = rewriteNonSignatureLine(derefOutParams(line, _func.outParams, _ptrFuncs)); + + const bool wasOpen = _func.seenOpenBrace; + countBraces(line, _func.braceDepth, _func.seenOpenBrace); + if (!_func.injected && !wasOpen && _func.seenOpenBrace) + { + // The body brace just opened on this line: inject the value-param copies. + for (const string& inj : _func.paramInjections) + out += "\n" + inj; + _func.injected = true; + } + if (_func.seenOpenBrace && _func.braceDepth <= 0) + leaveFunctionScope(); + return out; +} + +// A function signature: single-line, or a parameter list spanning several lines. +bool LineRewriter::handleSignature(const string& line, string& out) +{ + // Continuation of a signature whose parameter list spans multiple lines. + if (_sig.active) + { + _sig.buffer += " " + line; + _sig.parenDepth += netParens(line); + if (_sig.parenDepth > 0) + { + out.clear(); // still open: defer emission until the list closes + return true; + } + const string joined = _sig.buffer; + _sig.active = false; + _sig.buffer.clear(); + if (auto rewritten = rewriteSignature(joined)) + out = beginFunction(rewritten->sig, std::move(rewritten->outNames), std::move(rewritten->valueParams)); + else + out = rewriteNonSignatureLine(joined); // not a signature after all + return true; + } + + // Single-line signature. + if (auto rewritten = rewriteSignature(line)) + { + out = beginFunction(rewritten->sig, std::move(rewritten->outNames), std::move(rewritten->valueParams)); + return true; + } + + // Signature whose parameter list opens here but closes on a later line. + if (looksLikeSignatureStart(line) && netParens(line) > 0) + { + _sig.active = true; + _sig.buffer = line; + _sig.parenDepth = netParens(line); + out.clear(); + return true; + } + return false; +} + +// ─── Overload resolution ───────────────────────────────────────────────────── + +string derefPointerParams(const string& shader) +{ + std::istringstream in(shader); + std::ostringstream out; + string line; + + StringVec fnLines; // buffered current function + StringVec ptrParams; // its ptr parameter names + bool inFn = false, seenDeref = false; + int depth = 0; + + // Complete pointer-function set up front so the undo decision is independent of + // definition/use order (e.g. forwarding to a function defined later in the shader). + const StringSet ptrFuncs = collectPointerFunctions(shader); + + auto flush = [&]() + { + // Compound functions assign to out params by bare name (`out1 = ...`); library + // functions already use `(*out1)`. Only deref when no `(*` is present. + if (!ptrParams.empty() && !seenDeref) + for (size_t i = 1; i < fnLines.size(); ++i) + fnLines[i] = derefOutParams(fnLines[i], ptrParams, ptrFuncs); + for (const string& l : fnLines) + out << l << "\n"; + fnLines.clear(); + ptrParams.clear(); + inFn = false; + seenDeref = false; + depth = 0; + }; + + while (std::getline(in, line)) + { + if (!inFn) + { + if (auto def = parseFnDef(line)) + { + inFn = true; + seenDeref = (line.find("(*") != string::npos); + for (const string& p : splitTopLevel(def->params)) + { + const size_t colon = p.find(':'); + if (colon != string::npos && p.find("ptr 0) + { + skipping = true; + skipBase = depth; + depth += d; + } + continue; // drop opening (and block if multi-line) + } + } + else if (startsWith(t, "fn ")) + { + if (auto def = parseFnDef(line)) + { + string key = def->name + "("; + for (const string& ty : paramTypes(def->params)) + key += ty + ","; + if (!seenFn.insert(key).second) + { + const int d = braceDelta(line); + if (d > 0) + { + skipping = true; + skipBase = depth; + depth += d; + } + continue; // drop duplicate function (single- or multi-line) + } + } + } + } + + depth += braceDelta(line); + if (!drop) + out << line << "\n"; + } + return out.str(); +} + +// MaterialX booleans are represented as i32 in WGSL (uniform buffers cannot hold `bool`), but a +// handful of hand-written library functions shared with the GLSL backends declare boolean +// parameters with the native WGSL `bool` type (e.g. `retroreflective`, `energy_compensation`, +// `flip_g`). Passing a MaterialX boolean (an i32 value or integer literal) to such a parameter is +// rejected by WGSL ("cannot convert value of type 'abstract-int' to type 'bool'"). Wrap those +// integer arguments in an explicit `bool(...)` conversion. `fnBool` maps a function name to its +// per-definition flags marking which parameters are `bool`. +string coerceBoolArgs(string line, + const std::map>>& fnBool, + const std::map& locals, + const std::map& globals, + const std::map>& structs) +{ + for (const auto& entry : fnBool) + { + const string token = entry.first + "("; + for (size_t pos = 0; (pos = line.find(token, pos)) != string::npos;) + { + const bool defHere = (pos >= 3 && line.compare(pos - 3, 3, "fn ") == 0); + const bool boundaryBefore = (pos == 0) || !isIdentChar(line[pos - 1]); + if (!boundaryBefore || defHere) + { + pos += token.size(); + continue; + } + const size_t argStart = pos + token.size(); + int depth = 1; + size_t i = argStart; + for (; i < line.size() && depth != 0; ++i) + { + if (line[i] == '(') + depth++; + else if (line[i] == ')') + depth--; + } + if (depth != 0) + { + pos += token.size(); + continue; // unterminated call on this line + } + const size_t close = i - 1; + StringVec args = splitTopLevel(line.substr(argStart, close - argStart)); + + const std::vector* flags = nullptr; + for (const auto& f : entry.second) + if (f.size() == args.size()) + { + flags = &f; + break; + } + if (!flags) + { + pos += token.size(); + continue; + } + + bool changed = false; + for (size_t a = 0; a < args.size(); ++a) + { + if (!(*flags)[a]) + continue; + const string arg = trim(args[a]); + if (arg.empty() || arg.compare(0, 5, "bool(") == 0) + continue; + // MaterialX booleans are emitted as i32 (`0`/`1`). WGSL will not implicitly + // convert an integer literal (abstract-int) or i32 variable to `bool`. + const bool isIntLiteral = (arg == "0" || arg == "1"); + const string at = inferArgType(arg, locals, globals, structs); + if (!isIntLiteral && at != "i32" && at != "u32") + continue; + args[a] = "bool(" + arg + ")"; + changed = true; + } + if (!changed) + { + pos = close + 1; + continue; + } + string rebuilt; + for (size_t a = 0; a < args.size(); ++a) + rebuilt += (a ? ", " : "") + args[a]; + line.replace(argStart, close - argStart, rebuilt); + pos = argStart + rebuilt.size() + 1; + } + } + return line; +} + +string coerceBoolCallSites(const string& shader) +{ + std::istringstream pre(shader); + string line; + + std::map> structs; + std::map globals; + std::map>> fnBool; + string curStruct; + int braceDepth = 0; + + while (std::getline(pre, line)) + { + const string t = trim(line); + if (!curStruct.empty()) + { + if (t == "}" || t == "};") + curStruct.clear(); + else + { + const size_t colon = t.find(':'); + if (colon != string::npos) + { + string ty = trim(t.substr(colon + 1)); + if (!ty.empty() && ty.back() == ',') + ty.pop_back(); + structs[curStruct][trim(t.substr(0, colon))] = ty; + } + } + continue; + } + if (t.compare(0, 7, "struct ") == 0 && t.find('}') == string::npos) + { + size_t s = 7; + curStruct = readIdentifier(t, s); + continue; + } + + if (auto def = parseFnDef(line)) + { + braceDepth = 0; + const StringVec types = paramTypes(def->params); + std::vector boolFlags; + bool anyBool = false; + for (const string& ty : types) + { + const bool b = (ty == "bool"); + boolFlags.push_back(b); + anyBool = anyBool || b; + } + if (anyBool) + fnBool[def->name].push_back(boolFlags); + } + else if (braceDepth == 0) + { + collectVarDecl(line, globals); + } + for (char c : line) + { + if (c == '{') + braceDepth++; + else if (c == '}') + braceDepth--; + } + } + + if (fnBool.empty()) + return shader; + + std::istringstream in(shader); + std::ostringstream out; + std::map locals; + braceDepth = 0; + curStruct.clear(); + while (std::getline(in, line)) + { + const string t = trim(line); + if (!curStruct.empty()) + { + out << line << "\n"; + if (t == "}" || t == "};") + curStruct.clear(); + continue; + } + if (t.compare(0, 7, "struct ") == 0 && t.find('}') == string::npos) + { + curStruct = "x"; + out << line << "\n"; + continue; + } + + if (auto def = parseFnDef(line)) + { + braceDepth = 0; + locals.clear(); + for (const string& p : splitTopLevel(def->params)) + { + const size_t colon = p.find(':'); + if (colon != string::npos) + locals[trim(p.substr(0, colon))] = trim(p.substr(colon + 1)); + } + } + else + { + collectVarDecl(line, locals); + line = coerceBoolArgs(line, fnBool, locals, globals, structs); + } + + for (char c : line) + { + if (c == '{') + braceDepth++; + else if (c == '}') + braceDepth--; + } + out << line << "\n"; + } + return out.str(); +} + +string resolveOverloads(const string& shader) +{ + std::istringstream pre(shader); + string line; + + // Pass 1: struct member types, module-scope globals, and overload groups. + std::map> structs; + std::map globals; + std::map>> defs; // name -> [(types,_)] + std::map defRet; // name -> [return type per def] + std::map>> fnPtr; // name -> [per-overload ptr-flags] + std::map>> fnBool; // name -> [per-overload bool-flags] + string curStruct; + int braceDepth = 0; + while (std::getline(pre, line)) + { + const string t = trim(line); + if (!curStruct.empty()) + { + if (t == "}" || t == "};") + { + curStruct.clear(); + continue; + } + const size_t colon = t.find(':'); + if (colon != string::npos) + { + const string m = trim(t.substr(0, colon)); + string ty = trim(t.substr(colon + 1)); + if (!ty.empty() && ty.back() == ',') + ty.pop_back(); + structs[curStruct][m] = ty; + } + continue; + } + if (t.compare(0, 7, "struct ") == 0 && t.find('}') == string::npos) + { + size_t s = 7; + const string nm = readIdentifier(t, s); + curStruct = nm; + continue; + } + // Track brace depth to distinguish module-scope globals from locals. A function + // definition is always at module scope (WGSL has no nested functions), so use it + // to re-anchor braceDepth to 0 — this keeps definition collection robust even if + // an earlier line's brace count drifted (which otherwise silently drops later + // overloads and produces redeclarations). + if (auto def = parseFnDef(line)) + { + braceDepth = 0; + const StringVec types = paramTypes(def->params); + defs[def->name].push_back({ types, string() }); + std::vector ptrFlags; + std::vector boolFlags; + bool anyPtr = false; + bool anyBool = false; + for (const string& ty : types) + { + const bool p = ty.compare(0, 3, "ptr") == 0; + ptrFlags.push_back(p); + anyPtr = anyPtr || p; + const bool b = (ty == "bool"); + boolFlags.push_back(b); + anyBool = anyBool || b; + } + if (anyPtr) + fnPtr[def->name].push_back(ptrFlags); + if (anyBool) + fnBool[def->name].push_back(boolFlags); + // Capture the return type (keyed below by the function's final name). + string ret = "void"; + const size_t arrow = line.find("->"); + if (arrow != string::npos) + { + const size_t br = line.find('{', arrow); + ret = trim(string_view(line).substr(arrow + 2, (br == string::npos ? line.size() : br) - (arrow + 2))); + } + defRet[def->name].push_back(ret); + } + else if (braceDepth == 0) + { + collectVarDecl(line, globals); + // module-scope uniform: `@group(..) ... var NAME: TYPE` + const size_t v = line.find("var<"); + if (v != string::npos) + { + const size_t gt = line.find('>', v); + if (gt != string::npos) + { + string tail = line.substr(gt + 1); + collectVarDecl("var " + trim(tail), globals); + } + } + } + for (char c : line) + { + if (c == '{') + braceDepth++; + else if (c == '}') + braceDepth--; + } + } + + // Determine overloaded names and assign unique type-suffixed names. + std::map>> overloads; + for (auto& d : defs) + { + if (d.second.size() < 2) + continue; + for (auto& variant : d.second) + { + string suffix; + for (const string& ty : variant.first) + suffix += "_" + typeCode(ty); + variant.second = d.first + suffix; + overloads[d.first].push_back(variant); + } + } + // Record each function's final (possibly overload-suffixed) name -> return type + // in `globals` under a "fn:" prefix, so inferArgType can type call expressions. + for (const auto& d : defs) + { + for (size_t i = 0; i < d.second.size(); ++i) + { + string fn = d.first; + if (d.second.size() >= 2) + for (const string& ty : d.second[i].first) + fn += "_" + typeCode(ty); + if (i < defRet[d.first].size()) + globals["fn:" + fn] = defRet[d.first][i]; + } + // Also record under the original name when every overload shares one return type, so a + // call still written with the un-suffixed name (e.g. an argument being type-inferred + // before its own renaming) resolves to the right type. Overloads with differing return + // types stay ambiguous (unrecorded -> inferArgType yields "unknown"). + const auto& rets = defRet[d.first]; + if (!rets.empty()) + { + const bool allSame = std::all_of(rets.begin(), rets.end(), + [&](const string& r) + { + return r == rets.front(); + }); + if (allSame) + globals["fn:" + d.first] = rets.front(); + } + } + + if (overloads.empty() && fnPtr.empty() && fnBool.empty()) + return shader; + + // Pass 2: rewrite definitions and call sites, tracking per-function locals. + std::istringstream in(shader); + std::ostringstream out; + std::map locals; + braceDepth = 0; + curStruct.clear(); + while (std::getline(in, line)) + { + const string t = trim(line); + // Skip struct bodies unchanged. + if (!curStruct.empty()) + { + out << line << "\n"; + if (t == "}" || t == "};") + curStruct.clear(); + continue; + } + if (t.compare(0, 7, "struct ") == 0 && t.find('}') == string::npos) + { + curStruct = "x"; + out << line << "\n"; + continue; + } + + if (auto def = parseFnDef(line)) + { + braceDepth = 0; // re-anchor at each top-level definition (see pass 1) + locals.clear(); + // Seed locals with parameter names/types. + for (const string& p : splitTopLevel(def->params)) + { + const size_t colon = p.find(':'); + if (colon != string::npos) + locals[trim(p.substr(0, colon))] = trim(p.substr(colon + 1)); + } + // Rename this definition if its name is overloaded. + auto ov = overloads.find(def->name); + if (ov != overloads.end()) + { + const StringVec types = paramTypes(def->params); + for (const auto& variant : ov->second) + if (variant.first == types) + { + const size_t p = line.find("fn " + def->name + "("); + if (p != string::npos) + line.replace(p + 3, def->name.size(), variant.second); + break; + } + } + } + else + { + // Join a statement whose parentheses span several lines (e.g. a multi-line call + // such as `mx_bilerp(\n arg,\n ...\n)`) so the argument rewriters below see the + // whole argument list instead of bailing on the unbalanced opening line. Embedded + // newlines are tolerated by the rewriters (their tokenizers trim whitespace). + int net = netParensCode(line); + string cont; + while (net > 0 && std::getline(in, cont)) + { + line += "\n" + cont; + net += netParensCode(cont); + } + collectVarDecl(line, locals); + line = rewriteFloatVectorCtors(line, locals, globals, structs); + line = addAddressOfToOutArgs(line, fnPtr, locals); + line = coerceBoolArgs(line, fnBool, locals, globals, structs); + line = rewriteOverloadCalls(line, overloads, locals, globals, structs); + line = rewriteMixToSelect(line, locals, globals, structs); + line = rewriteScalarBroadcast(line, locals, globals, structs); + } + + for (char c : line) + { + if (c == '{') + braceDepth++; + else if (c == '}') + braceDepth--; + } + out << line << "\n"; + } + return out.str(); +} + +// Rewrite GLSL function definitions emitted inline by HwNumLightsNode / HwLightSamplerNode. +// Per-line rewriteAll() does not convert their signatures; rewriteBlock() on the full stage +// mangles native WGSL (`var` bindings). Convert only the known residual stubs. +string rewriteResidualGlslFunctions(const string& shader) +{ + static const char* GLSL_FUNCS[] = { + "int numActiveLightSources(", + "void sampleLightSource(", + }; + + // Functions with pointer parameters are defined outside the extracted stub blocks (e.g. the + // light shaders the sampler forwards to), so collect them from the whole shader up front. + const StringSet ptrFuncs = collectPointerFunctions(shader); + + string result = shader; + for (const char* prefix : GLSL_FUNCS) + { + const string sig = prefix; + size_t searchFrom = 0; + while (true) + { + const size_t pos = result.find(sig, searchFrom); + if (pos == string::npos) + break; + + const size_t lineStart = (pos == 0 || result[pos - 1] == '\n') ? pos : result.rfind('\n', pos); + const size_t blockStart = (lineStart == string::npos) ? 0 : (lineStart + (lineStart == pos ? 0 : 1)); + const size_t braceOpen = result.find('{', pos); + if (braceOpen == string::npos) + { + searchFrom = pos + sig.size(); + continue; + } + + int depth = 0; + size_t blockEnd = string::npos; + for (size_t i = braceOpen; i < result.size(); ++i) + { + if (result[i] == '{') + depth++; + else if (result[i] == '}') + { + depth--; + if (depth == 0) + { + blockEnd = i + 1; + break; + } + } + } + if (blockEnd == string::npos) + break; + + string block = result.substr(blockStart, blockEnd - blockStart); + // Undo mistaken `&out` on a definition line from an older rewriteSampleLightSource. + const size_t sigEnd = block.find('{'); + if (sigEnd != string::npos) + { + string sigLine = block.substr(0, sigEnd); + for (size_t p; (p = sigLine.find("&out ")) != string::npos;) + sigLine.replace(p, 5, "out "); + block = sigLine + block.substr(sigEnd); + } + + const string converted = rewriteBlock(block, ptrFuncs); + result.replace(blockStart, blockEnd - blockStart, converted); + searchFrom = blockStart + converted.size(); + } + } + return result; +} + +// Repair `else { // comment }` empty blocks left before a real `{` body. This pattern +// breaks brace balance and makes the next `fn` look like it is still inside a function. +string repairEmptyElseCommentBlocks(const string& shader) +{ + const StringVec lines = readLines(shader); + StringVec out; + for (size_t i = 0; i < lines.size(); ++i) + { + const string t = trim(lines[i]); + if (startsWith(t, "else {") && t.find("//") != string::npos) + { + const size_t close = t.rfind('}'); + const size_t comment = t.find("//"); + if (close != string::npos && comment != string::npos && comment < close) + { + size_t j = i + 1; + while (j < lines.size() && trim(lines[j]).empty()) + j++; + if (j < lines.size() && trim(lines[j]) == "{") + { + const size_t indent = lines[i].find_first_not_of(" \t"); + const string ind = (indent == string::npos) ? "" : lines[i].substr(0, indent); + out.push_back(ind + "else " + trim(t.substr(comment, close - comment))); + out.push_back(lines[j]); + i = j; + continue; + } + } + } + out.push_back(lines[i]); + } + + return joinLines(out); +} + +string splitChainedAssignments(const string& shader) +{ + // A chained assignment target must be a simple lvalue: an identifier with optional + // `.member` / `[index]` accessors. This is all the genglsl libraries emit, and it keeps + // the split from misfiring on arbitrary expressions. + auto isSimpleLvalue = [](const string& s) + { + if (s.empty() || !(std::isalpha(static_cast(s[0])) || s[0] == '_')) + return false; + for (char c : s) + { + if (!(isIdentChar(c) || c == '.' || c == '[' || c == ']')) + return false; + } + return true; + }; + + const StringVec lines = readLines(shader); + StringVec out; + for (const string& line : lines) + { + const size_t indentEnd = line.find_first_not_of(" \t"); + const string body = trim(line); + + // Only consider plain single-statement `...;` lines (no block braces, no trailing + // comment, no second statement) — everything else passes through untouched. + if (indentEnd == string::npos || body.empty() || body.back() != ';' || + body.find('{') != string::npos || body.find('}') != string::npos) + { + out.push_back(line); + continue; + } + const string stmt = trim(body.substr(0, body.size() - 1)); + if (stmt.find(';') != string::npos) + { + out.push_back(line); + continue; + } + + // Collect top-level (paren/bracket depth 0) plain `=` operators, skipping comparisons + // (`==`, `!=`, `<=`, `>=`) and compound assignments (`+=`, `<<=`, ...). + std::vector eqPositions; + int depth = 0; + for (size_t i = 0; i < stmt.size(); ++i) + { + const char c = stmt[i]; + if (c == '(' || c == '[') + depth++; + else if (c == ')' || c == ']') + depth--; + else if (c == '=' && depth == 0) + { + const char prev = i > 0 ? stmt[i - 1] : '\0'; + const char next = i + 1 < stmt.size() ? stmt[i + 1] : '\0'; + const bool comparisonOrCompound = + next == '=' || prev == '=' || prev == '!' || prev == '<' || prev == '>' || + prev == '+' || prev == '-' || prev == '*' || prev == '/' || prev == '%' || + prev == '&' || prev == '|' || prev == '^'; + if (!comparisonOrCompound) + eqPositions.push_back(i); + } + } + if (eqPositions.size() < 2) + { + out.push_back(line); + continue; + } + + // Segment the statement at the assignment operators: [target0, target1, ..., RHS]. + StringVec segments; + size_t start = 0; + for (size_t pos : eqPositions) + { + segments.push_back(trim(stmt.substr(start, pos - start))); + start = pos + 1; + } + segments.push_back(trim(stmt.substr(start))); + + bool simpleTargets = true; + for (size_t i = 0; i + 1 < segments.size(); ++i) + { + if (!isSimpleLvalue(segments[i])) + { + simpleTargets = false; + break; + } + } + if (!simpleTargets) + { + out.push_back(line); + continue; + } + + // Emit one assignment per target, right-to-left: `c = expr; b = c; a = b;`. + const string indent = line.substr(0, indentEnd); + for (size_t k = segments.size() - 1; k-- > 0;) + out.push_back(indent + segments[k] + " = " + segments[k + 1] + ";"); + } + + return joinLines(out); +} + +StringVec findResidualGlsl(const string& wgsl) +{ + // Tokens that should not appear in fully converted WGSL. The trailing space on the + // sampler/texture entries avoids matching the legitimate WGSL `texture_2d<...>` and + // `sampler` forms; we only want the GLSL `sampler2D x` / `texture2D x` declarations. + static constexpr std::array, 6> RESIDUAL_MARKERS = { { + { "#version", "GLSL #version directive" }, + { "#define", "GLSL preprocessor #define" }, + { "layout(", "GLSL layout() qualifier" }, + { "sampler2D ", "GLSL combined-sampler type" }, + { "texture2D ", "GLSL texture type" }, + { "gl_", "GLSL gl_* builtin" }, + } }; + StringVec findings; + for (const auto& [token, message] : RESIDUAL_MARKERS) + { + if (wgsl.find(token) != string::npos) + findings.push_back(string("residual GLSL token '") + string(token) + "' (" + string(message) + ")"); + } + return findings; +} + +} // namespace GlslToWgsl + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenGlsl/wgsl/converter/GlslToWgsl.h b/source/MaterialXGenGlsl/wgsl/converter/GlslToWgsl.h new file mode 100644 index 0000000000..97b4446fdd --- /dev/null +++ b/source/MaterialXGenGlsl/wgsl/converter/GlslToWgsl.h @@ -0,0 +1,187 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_GLSLTOWGSL_H +#define MATERIALX_GLSLTOWGSL_H + +/// @file +/// GLSL-to-WGSL string rewriting utilities for the WGSL shader generator. +/// +/// This header is internal to MaterialXGenGlsl (used only by WgslShaderGenerator) and is +/// not part of the installed public API. +/// +/// The MaterialX GLSL code path (HwSurfaceNode, CompoundNode, SourceCodeNode and the +/// genglsl `.glsl` library sources) emits GLSL-flavoured text. These utilities convert the +/// common GLSL patterns to valid WGSL at emit time, so the WGSL generator can reuse the +/// genglsl node implementations and library functions without a parallel set of `.wgsl` +/// sources: +/// - types: `float`/`vec3`/`mat4` -> `f32`/`vec3f`/`mat4x4f` +/// - const declarations: `const float x = 1.0` -> `const x: f32 = 1.0` +/// - variable decls: `vec3 v = ...` -> `var v: vec3f = ...` +/// - ternary operators: `a ? b : c` -> `select(c, b, a)` +/// - function signatures: `void f(float x, out int y)` -> `fn f(x: f32) -> i32` +/// - out parameters: body refs to `y` become `(*y)` with `y: ptr` +/// - arrays: `vec2 c[3]` -> `c: array` +/// - shift counts: `x << k` (signed `k`) -> `x << u32(k)` +/// - float vec ctors: `vec2(intX, intY)` -> `vec2f(f32(intX), f32(intY))` +/// +/// These are a *targeted* converter for the constrained, machine-generated GLSL the genglsl +/// node implementations and libraries produce -- not a general-purpose GLSL parser. They +/// assume single-statement-per-line output and well-formed input. As a safety net the +/// generator runs findResidualGlsl() on its output and warns if any unconverted GLSL +/// remains, so an incomplete rewrite is observable rather than silently invalid. + +#include + +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +namespace GlslToWgsl +{ + +/// Map a GLSL type keyword to its WGSL equivalent (`float`->`f32`, `vec3`->`vec3f`, ...). +/// Returns the input unchanged if it is already a WGSL type or unknown. +string mapType(const string& glslType); + +/// Apply the stateless, single-line GLSL->WGSL syntax conversions (type casts, const/var +/// declarations, ternaries, increment/decrement, function signatures). Out-parameter body +/// dereferencing is handled by LineRewriter, not here. +string rewriteAll(string line); + +// --- Whole-shader post-processing passes (run in order over the finished stage) --- + +/// Dereference `ptr` (out) parameters in compound function bodies, which assign +/// to them by bare name (`out1 = ...`). Library functions already use `(*out1)`. +string derefPointerParams(const string& shader); + +/// Drop duplicate module-scope definitions (the same `const`/`alias`/`struct`/`fn` emitted +/// by more than one library file), keeping the first; WGSL forbids redeclaration. +string dedupDefinitions(const string& shader); + +/// Wrap integer MaterialX booleans in `bool(...)` at call sites whose callee declares a +/// native WGSL `bool` parameter. Run after resolveOverloads. +string coerceBoolCallSites(const string& shader); + +/// Resolve GLSL function overloading (which WGSL forbids): give each same-named definition a +/// unique type-suffixed name and rewrite every call site by argument-type inference. +string resolveOverloads(const string& shader); + +/// Convert the HwNumLightsNode / HwLightSamplerNode GLSL function stubs that per-line +/// rewriting leaves behind. Does not touch native WGSL bindings. +string rewriteResidualGlslFunctions(const string& shader); + +/// Fix `else { // comment }` empty blocks that orphan the real `{ ... }` body and break +/// brace balance in generated WGSL. +string repairEmptyElseCommentBlocks(const string& shader); + +/// Split GLSL chained assignment `a = b = c = expr;` (legal GLSL, rejected by WGSL) into +/// separate right-to-left statements (`c = expr; b = c; a = b;`). Only fires when every +/// target is a simple lvalue, as in the mx_noise.glsl hash seed. +string splitChainedAssignments(const string& shader); + +/// Scan finished WGSL for residual GLSL tokens (`#version`, `layout(`, `sampler2D `, ...). +/// Returns one human-readable entry per distinct token found; empty when fully converted. +StringVec findResidualGlsl(const string& wgsl); + +/// Stateful, function-scope-aware GLSL-to-WGSL line rewriter. +/// +/// Feed it one line at a time in source order. It tracks the current function body (via +/// brace depth) so that: +/// - a signature `void f(..., out T r)` becomes `fn f(..., r: ptr)` (no +/// `-> T` return), and +/// - every reference to an `out` parameter `r` inside the body becomes `(*r)`. +/// Value-returning signatures (`vec3 g(...)`) gain a `-> vec3f` return type. +class LineRewriter +{ + public: + /// Rewrite one line (without trailing newline). Updates internal scope state. + string rewrite(const string& line); + + /// Seed the set of functions known to take a pointer (out/inout) parameter. Used when a + /// block is rewritten in isolation (see rewriteBlock) and its callees are defined elsewhere. + void registerPointerFunctions(const StringSet& fns) { _ptrFuncs.insert(fns.begin(), fns.end()); } + + private: + // Per-line handlers tried in order by rewrite(). Each returns true and fills `out` when + // it consumes the line; false means "not my case, try the next". + bool handlePreprocessor(const string& line, string& out); + bool handleStruct(const string& line, string& out); + bool handleSignature(const string& line, string& out); + string rewriteFunctionBodyLine(const string& line); + + // Enter a function body: record out-parameter names and the mutable `var` copies to + // inject for value parameters; return the rewritten signature line. + string beginFunction(const string& sig, StringVec&& outNames, + const std::vector>& valueParams); + // Leave the current function body and clear its per-function state. + void leaveFunctionScope(); + + // The function body currently being rewritten. + struct FunctionScope + { + bool active = false; // inside a function body (or its signature) + bool seenOpenBrace = false; // the body's opening brace has been seen + int braceDepth = 0; // net brace nesting within the function + bool injected = false; // value-parameter `var` copies already emitted + StringVec outParams; // out-parameter names to dereference + StringVec paramInjections; // `var name: T = name_arg;` lines + }; + + // A function signature whose parameter list spans several lines. + struct SignatureAccum + { + bool active = false; + string buffer; + int parenDepth = 0; + }; + + // A ternary expression `cond ? a : b` spanning several lines. + struct TernaryAccum + { + bool active = false; + string buffer; + }; + + // A braceless control header (`if(...)`/`for(...)`/`else`) awaiting its body. + struct PendingHeader + { + bool active = false; + string line; + }; + + // Minimal `#if/#elif/#else/#endif` evaluator (WGSL has no preprocessor). + struct Preprocessor + { + struct Frame + { + bool active; + bool taken; + bool parentActive; + }; + std::vector stack; + bool active() const; // all enclosing conditionals select the line + void update(const string& directive); + }; + + // Names of functions seen so far that take a pointer (out/inout) parameter. Used to decide + // whether an out-parameter forwarded as a call argument must stay a bare pointer. + StringSet _ptrFuncs; + + FunctionScope _func; + SignatureAccum _sig; + TernaryAccum _ternary; + PendingHeader _pendingHeader; + Preprocessor _pp; + bool _inStruct = false; // inside a `struct { ... }` definition body +}; + +} // namespace GlslToWgsl + +MATERIALX_NAMESPACE_END + +#endif // MATERIALX_GLSLTOWGSL_H diff --git a/source/MaterialXTest/MaterialXGenGlsl/GenGlsl.cpp b/source/MaterialXTest/MaterialXGenGlsl/GenGlsl.cpp index e99d83a452..be5caeacf0 100644 --- a/source/MaterialXTest/MaterialXGenGlsl/GenGlsl.cpp +++ b/source/MaterialXTest/MaterialXGenGlsl/GenGlsl.cpp @@ -13,13 +13,13 @@ // codebase #include -#include -#include +#include +#include #include #include #include -#include -#include +#include +#include #include namespace mx = MaterialX; diff --git a/source/MaterialXTest/MaterialXGenGlsl/GenWgsl.cpp b/source/MaterialXTest/MaterialXGenGlsl/GenWgsl.cpp new file mode 100644 index 0000000000..4f1b83109f --- /dev/null +++ b/source/MaterialXTest/MaterialXGenGlsl/GenWgsl.cpp @@ -0,0 +1,275 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace mx = MaterialX; + +namespace +{ +// Count non-overlapping occurrences of a substring. +int countOccurrences(const std::string& haystack, const std::string& needle) +{ + int n = 0; + for (size_t p = haystack.find(needle); p != std::string::npos; p = haystack.find(needle, p + needle.size())) + { + n++; + } + return n; +} + +// Verify that '{' and '}' are balanced and never close below zero. +bool bracesBalanced(const std::string& source) +{ + int depth = 0; + for (char c : source) + { + if (c == '{') + { + depth++; + } + else if (c == '}') + { + if (--depth < 0) + { + return false; + } + } + } + return depth == 0; +} + +// Assert the structural invariants every generated pixel shader must satisfy: it is valid-looking +// WGSL (no GLSL leftovers), exposes exactly one entry function, and has balanced braces. These are +// cheap, deterministic checks; full WGSL-syntax validation (naga/tint) runs as a separate CI step. +void checkWgslInvariants(const std::string& pixel, const std::string& label) +{ + INFO("material: " << label); + REQUIRE(pixel.length() > 0); + + // No GLSL leftovers from the GLSL->WGSL rewrite. + CHECK(pixel.find("#version") == std::string::npos); + CHECK(pixel.find("#define") == std::string::npos); + CHECK(pixel.find("layout(") == std::string::npos); + CHECK(pixel.find("texture2D ") == std::string::npos); // GLSL combined-sampler type + CHECK(pixel.find("sampler2D ") == std::string::npos); + CHECK(pixel.find("gl_") == std::string::npos); + + // Boolean HW uniforms are stored as i32 (WGSL uniform buffers cannot hold bool), so any + // GLSL boolean condition on one must be lowered to an explicit `!= 0` comparison; a bare + // `if (u_refractionTwoSided)` would be an invalid `if (i32)` in WGSL. + CHECK(pixel.find("if (u_refractionTwoSided)") == std::string::npos); + + // MaterialX booleans map to i32 everywhere (uniforms, node-function parameters, args) so + // the type agrees on both sides of a call. A node-graph parameter spelled `thin_walled: bool` + // would mismatch the i32 uniform argument passed to it. + CHECK(pixel.find("thin_walled: bool") == std::string::npos); + + // Library BSDF helpers keep native `bool` parameters; MaterialX boolean inputs passed to + // them must be wrapped in `bool(...)`. A bare integer literal is rejected by WGSL. + CHECK(pixel.find("roughness_vector_out, 0, 0.000000") == std::string::npos); + + // Light-bridge helpers must be WGSL `fn`s, not GLSL `int`/`void` definitions. + CHECK(pixel.find("int numActiveLightSources()") == std::string::npos); + CHECK(pixel.find("void sampleLightSource") == std::string::npos); + + // Volume shaders (Open PBR, glTF PBR, …) emit VDF helpers; the type must be declared in + // emitWgslSurfaceTypes() — GLSL struct typedefs are not copied into WGSL output. + if (pixel.find("mx_anisotropic_vdf") != std::string::npos) + CHECK(pixel.find("struct VDF") != std::string::npos); + + // mx_hsv.glsl uses chained `float` decls, braceless `else`, `0.0f` literals, and GLSL `int()` casts. + if (pixel.find("mx_rgbtohsv") != std::string::npos || pixel.find("mx_hsvtorgb") != std::string::npos) + { + CHECK(pixel.find("float s =") == std::string::npos); + CHECK(pixel.find("float g =") == std::string::npos); + CHECK(pixel.find("else s =") == std::string::npos); + CHECK(pixel.find("0.0f") == std::string::npos); + CHECK(pixel.find("int(trunc") == std::string::npos); + } + + // `else // comment` must not be lowered to an empty `else { }` block (breaks brace balance). + CHECK(pixel.find("else { // FRESNEL_MODEL_SCHLICK }") == std::string::npos); + + // Native WGSL struct/uniform members must keep `vec3f`/`mat4x4f` spellings (not bare GLSL `vec3`). + CHECK(pixel.find(": vec3,") == std::string::npos); + CHECK(pixel.find(": vec3;") == std::string::npos); + CHECK(pixel.find(": vec4,") == std::string::npos); + CHECK(pixel.find(": mat4x4,") == std::string::npos); + + // Exactly one WGSL entry function, and balanced scopes. + CHECK(countOccurrences(pixel, "fn FragmentMain(") == 1); + CHECK(bracesBalanced(pixel)); +} + +// Optionally write the generated shader for manual inspection, controlled by an environment +// variable so the test never depends on a hardcoded path. Set MATERIALX_WGSL_DUMP_DIR to enable. +void maybeDump(const mx::ShaderPtr& shader, const std::string& name) +{ + const std::string dumpDir = mx::getEnviron("MATERIALX_WGSL_DUMP_DIR"); + if (dumpDir.empty()) + { + return; + } + std::ofstream(dumpDir + "/" + name + "_pixel.wgsl") << shader->getSourceCode(mx::Stage::PIXEL); +} + +// A minimal standard_surface, kept inline so the smoke test is independent of example assets. +const std::string STANDARD_SURFACE_MTLX = R"( + + + + + + + + + + + + +)"; + +// Load the standard MaterialX data libraries used by all cases below. +mx::DocumentPtr loadStandardLibraries(const mx::FileSearchPath& searchPath) +{ + mx::DocumentPtr stdlib = mx::createDocument(); + mx::loadLibraries({ "libraries/targets", "libraries/stdlib", "libraries/pbrlib", "libraries/bxdf" }, + searchPath, stdlib); + return stdlib; +} +} // namespace + +TEST_CASE("WgslGen: syntax uses inherited GLSL type spellings", "[genwgsl]") +{ + const mx::ShaderGeneratorPtr generator = mx::WgslShaderGenerator::create(); + const mx::Syntax& syntax = generator->getSyntax(); + + // WgslSyntax inherits GLSL type spellings from GlslSyntax (matching upstream); the native + // WGSL type names are produced later, at emission time, by the generator. + CHECK(syntax.getTypeName(mx::Type::FLOAT) == "float"); + CHECK(syntax.getTypeName(mx::Type::BOOLEAN) == "bool"); + CHECK(syntax.getTypeName(mx::Type::VECTOR3) == "vec3"); + CHECK(syntax.getTypeName(mx::Type::COLOR3) == "vec3"); +} + +TEST_CASE("WgslGen: hwSrgbEncodeOutput encodes surface output", "[genwgsl]") +{ + mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath(); + mx::DocumentPtr stdlib = loadStandardLibraries(searchPath); + + mx::DocumentPtr doc = mx::createDocument(); + mx::readFromXmlString(doc, STANDARD_SURFACE_MTLX); + doc->setDataLibrary(stdlib); + + mx::ShaderGeneratorPtr generator = mx::WgslShaderGenerator::create(); + mx::GenContext context(generator); + context.registerSourceCodeSearchPath(searchPath); + context.getOptions().hwSrgbEncodeOutput = true; + + std::vector elements = mx::findRenderableElements(doc); + REQUIRE(!elements.empty()); + + mx::ShaderPtr shader = generator->generate(elements[0]->getNamePath(), elements[0], context); + REQUIRE(shader != nullptr); + + const std::string pixel = shader->getSourceCode(mx::Stage::PIXEL); + checkWgslInvariants(pixel, "standard_surface_srgb"); + CHECK(pixel.find("mx_srgb_encode(") != std::string::npos); + CHECK(pixel.find("return vec4f(mx_srgb_encode(") != std::string::npos); +} + +TEST_CASE("WgslGen: directional lights emit LightData binding", "[genwgsl]") +{ + mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath(); + mx::DocumentPtr stdlib = loadStandardLibraries(searchPath); + mx::loadLibraries({ "libraries/lights" }, searchPath, stdlib); + mx::NodeDefPtr directionalLight = stdlib->getNodeDef("ND_directional_light"); + REQUIRE(directionalLight != nullptr); + + mx::DocumentPtr doc = mx::createDocument(); + mx::readFromXmlString(doc, STANDARD_SURFACE_MTLX); + doc->setDataLibrary(stdlib); + + mx::ShaderGeneratorPtr generator = mx::WgslShaderGenerator::create(); + mx::GenContext context(generator); + context.registerSourceCodeSearchPath(searchPath); + context.getOptions().hwMaxActiveLightSources = 4; + mx::HwShaderGenerator::bindLightShader(*directionalLight, 1, context); + + std::vector elements = mx::findRenderableElements(doc); + REQUIRE(!elements.empty()); + + mx::ShaderPtr shader = generator->generate(elements[0]->getNamePath(), elements[0], context); + REQUIRE(shader != nullptr); + + const std::string pixel = shader->getSourceCode(mx::Stage::PIXEL); + checkWgslInvariants(pixel, "standard_surface_lights"); + CHECK(pixel.find("struct LightData") != std::string::npos); + CHECK(pixel.find("fn numActiveLightSources") != std::string::npos); + CHECK(pixel.find("fn sampleLightSource") != std::string::npos); + CHECK(pixel.find("mx_directional_light(light, position, (*result))") == std::string::npos); + CHECK(pixel.find("u_lightData") != std::string::npos); + CHECK(countOccurrences(pixel, "var u_lightData") == 1); + + maybeDump(shader, "standard_surface_lights"); +} + +TEST_CASE("WgslGen: example materials emit valid WGSL", "[genwgsl]") +{ + // Broad generation coverage across the full TestSuite + Examples corpus lives in + // GenGlsl.cpp's "GenShader: Wgsl GLSL Shader Generation" (a generation smoke test). This + // case adds the WGSL-specific invariant assertions on the few materials that hit + // branches the standard_surface feature tests above do not: brass_tiled drives the image-node + // / sampler-splitting path, and open_pbr exercises the volume (VDF) closure. + const mx::StringVec materials = { + "resources/Materials/Examples/StandardSurface/standard_surface_brass_tiled.mtlx", + "resources/Materials/Examples/OpenPbr/open_pbr_default.mtlx", + }; + + mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath(); + mx::DocumentPtr stdlib = loadStandardLibraries(searchPath); + + for (const std::string& relPath : materials) + { + const mx::FilePath resolved = searchPath.find(relPath); + INFO("material file: " << relPath); + REQUIRE(resolved.exists()); + + mx::DocumentPtr doc = mx::createDocument(); + mx::readFromXmlFile(doc, resolved); + doc->setDataLibrary(stdlib); + + // Each material gets a fresh generator so per-generation state cannot leak between cases. + mx::ShaderGeneratorPtr generator = mx::WgslShaderGenerator::create(); + mx::GenContext context(generator); + context.registerSourceCodeSearchPath(searchPath); + context.registerSourceCodeSearchPath(resolved.getParentPath()); + + std::vector elements = mx::findRenderableElements(doc); + REQUIRE(!elements.empty()); + + mx::ShaderPtr shader = generator->generate(elements[0]->getNamePath(), elements[0], context); + REQUIRE(shader != nullptr); + + const std::string name = resolved.getBaseName(); + checkWgslInvariants(shader->getSourceCode(mx::Stage::PIXEL), name); + + maybeDump(shader, name); + } +} diff --git a/source/MaterialXView/Viewer.cpp b/source/MaterialXView/Viewer.cpp index 3030574f8f..e9ee90e516 100644 --- a/source/MaterialXView/Viewer.cpp +++ b/source/MaterialXView/Viewer.cpp @@ -35,7 +35,7 @@ #if MATERIALX_BUILD_GEN_OSL #include #endif -#include +#include #include #include diff --git a/source/PyMaterialX/PyMaterialXGenGlsl/PyGlslShaderGenerator.cpp b/source/PyMaterialX/PyMaterialXGenGlsl/PyGlslShaderGenerator.cpp index 87726352ff..6fcb8bfc94 100644 --- a/source/PyMaterialX/PyMaterialXGenGlsl/PyGlslShaderGenerator.cpp +++ b/source/PyMaterialX/PyMaterialXGenGlsl/PyGlslShaderGenerator.cpp @@ -7,9 +7,9 @@ #include #include -#include -#include -#include +#include +#include +#include #include #include