@gdgt/hlsl-loader
Version:
Webpack loader for using HLSL in WebGL 2.0
2 lines (1 loc) • 8.32 kB
JavaScript
;var e=require("crypto"),t=require("fs/promises"),n=require("child_process"),r=require("util"),o=require("path"),i=require("json-stable-stringify");function s(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var a=s(o),c=s(i),l="http://json-schema.org/draft-07/schema#",p={exports:{type:"object",additionalProperties:{type:"object",properties:{entry:{type:"string",description:"Name of the HLSL function to use as the entry point."},stage:{type:"string",description:'Shader model and -stage to compile to.\n\nUsually `"vs_6_7"` for vertex- and `"ps_6_7"` for pixel shaders.'}},required:["entry","stage"],additionalProperties:!1,description:"Object specifying an export, the key being the exports name."},description:"A list of exports per HLSL file.\n\nNon-existent entry points will be ignored."},includeDirectories:{type:"array",items:{type:"string"},description:"Additional directories to search for `#include`d files. By default, only the directory containing the imported file is considered."},mangle:{type:"boolean",description:"Whether to shorten internal variable names."},logGlsl:{type:"boolean",description:"Whether to log the compiled GLSL code to the console.\n\nMay be useful during development. This is automatically disabled for production builds."},generateDeclarations:{type:"boolean",description:"Whether to emit .d.ts files containing declarations for imported HLSL files.\n\nThis is automatically disabled for production builds."}},d={$schema:l,type:"object",properties:p,additionalProperties:false},u=Object.freeze({__proto__:null,$schema:l,type:"object",properties:p,additionalProperties:false,default:d});async function f(e,t){return new Promise((r=>{const o=n.exec(e,((e,t,n)=>{r({stderr:n,stdout:t})}));o.stdin.write(t),o.stdin.end()}))}const w=["-spirv","-fspv-reflect","-fspv-target-env=vulkan1.2","-Zpr","-fvk-use-gl-layout","-O3"],g=["-","--version 300","--es","--remove-unused-variables"],y="win32"===process.platform,m=new Map([["darwin","darwin/bin"],["win32","win32/bin"],["linux","linux/bin"]]);if(!m.has(process.platform))throw new Error(`Your current platform "${process.platform}" is not supported.`);const $=a.default.resolve(__dirname,"../bin",m.get(process.platform)),h=a.default.resolve($,"dxc"+(y?".exe":"")),b=a.default.resolve($,"spirv-cross"+(y?".exe":""));function v(e,t){const n=[];return e.forEach(((e,r)=>{n.push(t(e,r))})),n}const x=[["float",5126],["vec2",35664],["vec3",35665],["vec4",35666],["mat4",35676],["uint",5125],["uvec2",36294],["uvec3",36295],["uvec4",36296],["int",5124],["ivec2",35667],["ivec3",35668],["ivec4",35669],["sampler2D",35678],["sampler3D",35679],["samplerCube",35680]],_=new Map(x),E=new Map(x.map((e=>e.reverse())));function S(e){const t=_.get(e);if(!t)throw new Error(`Type currently not supported: ${e}`);return t}function j(e){return e.replace(/[^a-z0-9]/gi,"_")}function O(e,t,n=0){if(t in e.types){const r=e.types[t],o={};return r.members.forEach((t=>{const r=j(t.name);if(t.array){const i=t.array[0];let s=n+t.offset;const a=[];for(let n=0;n<i;n+=1)a.push(O(e,t.type,s)),s+=t.array_stride;o[r]=a}else o[r]=O(e,t.type,n+t.offset)})),o}return{"@type":S(t),"@offset":n}}class M extends Error{constructor(e,t,n=""){super(`The ${t} called "${e}" ${n?`(${n}) `:""}is unexpectedly missing after converting to GLES3.0. \nThis is likely due to a naming conflict.\nPlease make sure you're following symbol naming conventions. (avoid names starting with "gl_" or an underscore)\n`)}}const P=r.promisify(n.exec);module.exports=async function(){const{resourcePath:n,getOptions:r,getLogger:o,rootContext:i,utils:{contextify:s,absolutify:l},mode:p,addDependency:d,emitWarning:y}=this,m=o("[@gdgt/hlsl-loader]"),$={exports:{vertexShader:{entry:"vsMain",stage:"vs_6_7"},fragmentShader:{entry:"psMain",stage:"ps_6_7"}},includeDirectories:[],logGlsl:!1,mangle:!0,generateDeclarations:!0,...r(u)||{}};if(Object.keys($.exports).length<1)return y(new Error("\nNo exports configured – the loader will export nothing.")),"";const x=new Set,_=async e=>{if(x.has(e))return;d(e),x.add(e);const n=(await t.readFile(e,{encoding:"utf-8"})).matchAll(/^#include\s+"(.+)"/gim);await Promise.all(Array.from(n).map((async n=>{const r=n[1],o=[a.default.dirname(e),...$.includeDirectories];for(const e of o){const n=l(e,r);if(!await t.access(n).catch((e=>e)))return void await _(n)}})))},L=_(n),T=e.randomUUID();await t.mkdir(a.default.resolve("./.temp")).catch((e=>{if("EEXIST"!==e.code)throw e}));const k=$.includeDirectories.map((e=>`-I ${e}`)),q=new Map,D=new Set;await Promise.all(Object.entries($.exports).map((async([e,{entry:r,stage:o}],i)=>{const s=a.default.resolve(`./.temp/${T}-${i}.spv`),{stderr:c}=await P([h,n,...w,...k,`-E ${r}`,`-T ${o}`,`-Fo ${s}`].join(" ")).catch((e=>e));if(c){if(-1!==c.indexOf("missing entry point"))return;if(c.includes(": error:")||!c.includes(": warning: "))throw new Error(`dxc failed:\n${c}`);D.has(c)||(D.add(c),y(new Error(`\n${c.replace(": warning: ","\nwarning: ")}`)))}q.set(e,await t.readFile(s)),await t.rm(s)})));const B=new Map,G=new Map,N=new Map,U=new Map,C=new Map,I={};await Promise.all(v(q,(async(e,t)=>{const{stdout:r,stderr:o}=await f([b,...g,"--reflect"].join(" "),e);if(o)throw new Error(`SPIRV-Cross failed: ${o}`);const{entryPoints:[a],...c}=JSON.parse(r);C.set(t,{spirv:e,entry:a.name,mode:a.mode}),"vert"===a.mode?(c.inputs?.forEach((({type:e,name:t,location:n})=>{B.set($.mangle?`_a${n}`:`_a_${j(t)}`,{type:S(e),location:n})})),c.outputs?.forEach((({type:e,name:t,location:n})=>{G.set($.mangle?`_v${n}`:`_v_${j(t)}`,{type:S(e),location:n})}))):"frag"===a.mode&&c.outputs?.forEach((({location:e})=>{N.set(e,`_o${e}`)}));const l=new Set;c.ubos?.forEach((({type:e,name:t,block_size:r})=>{const o=16*Math.ceil(r/16);l.has(t)?y(new Error(`\nMultiple cbuffers are named "${t.replace(/^type\./,"")}".\n\tThis will result in incomplete introspection data for ${s(i,n)}`)):(l.add(t),I[t]={...O(c,e),"@blockSize":o})})),c.textures?.forEach((({type:e,name:t,binding:n})=>{U.set(t,{type:S(e),binding:n})}))})));const R=new Map;await Promise.all(v(C,(async({spirv:e,entry:t,mode:n},r)=>{const o="vert"===n,i=[...g,`--entry ${t}`,`--stage ${n}`];G.forEach((({location:e},t)=>{i.push(`--rename-interface-variable ${o?"out":"in"} ${e} ${t}`)})),o?B.forEach((({location:e},t)=>{i.push(`--rename-interface-variable in ${e} ${t}`)})):N.forEach(((e,t)=>{i.push(`--rename-interface-variable out ${t} ${e}`)}));const{stdout:s,stderr:a}=await f([b,...i].join(" "),e);if(a)throw new Error(`SPIRV-Cross Error: ${a}`);R.set(r,s)}))),R.size<1&&y(new Error(`\nNo entry points found in ${s(i,n)}.\n\tLooking for the following functions: ${Object.entries($.exports).map((e=>e[1].entry)).join(", ")}`));let W=0;Object.keys(I).forEach((e=>{if(e.startsWith("type.")){const t=j(e),n=j(e.replace(/^type\./g,""));let r=!1;if(R.forEach(((e,o)=>{const i=e.replace(new RegExp(`(?<!\\w)${n}(?!\\w)`,"gm"),$.mangle?`_u${W}`:`_u_${n}`).replace(new RegExp(`(?<!\\w)${t}(?!\\w)`,"gm"),n);i.includes(`uniform ${n}`)&&(r=!0),R.set(o,i)})),!r)throw new M(n,"UBO");const o=I[e];delete I[e],I[n]=o,W+=1}})),U.forEach((({binding:e,type:t},n)=>{let r=!1;if(R.forEach((e=>{e.includes(`${function(e){const t=E.get(e);if(!t)throw new Error(`Type currently not supported: ${e}`);return t}(t)} ${n};`)&&(r=!0)})),!r){if(n in I)throw new Error(`The texture called "${n}" (binding ${e}) conflicts with a UBO of the same name. Please make sure UBO and texture names are unique.`);throw new M(n,"texture",`binding ${e}`)}}));const z=[],A=["/* This file was auto-generated by @gdgt/hlsl-loader. Do not edit. */\n"],H=`export const introspection = ${c.default({ubos:I,textures:Object.fromEntries(U),attributes:Object.fromEntries(B)})};`;return z.push(H),A.push("/**"," * Object containing static introspection data,"," * including UBO buffer layouts, vertex attributes and textures."," */",H),[...R].sort(((e,t)=>String(e[0]).localeCompare(t[0]))).forEach((([e,t])=>{z.push(`export const ${e} = \`${t}\`;`),A.push(`\n/** GLES 3.0 source of \`${$.exports[e].entry}\` */`,`export const ${e}: string;`),$.logGlsl&&"production"!==p&&(m.clear(),m.info(`\n\n// export "${e}" (compiled from "${$.exports[e].entry}" in ${s(i,n)}):\n\n${t}`))})),$.generateDeclarations&&"production"!==p&&await t.writeFile(`${n}.d.ts`,A.join("\n")),await L,z.join("\n")};