UNPKG

rollup-plugin-glsl-optimize

Version:

Import GLSL source files as strings. Pre-processed, validated and optimized with Khronos Group SPIRV-Tools. Supports glslify.

954 lines (940 loc) 31.3 kB
/* eslint-disable */ // @ts-nocheck /* * DO NOT EDIT: Auto-generated bundle from sources in ./src * For easier debugging you can include ./src/index.js directly instead */ import { createFilter } from '@rollup/pluginutils'; import { platform, arch, EOL } from 'os'; import * as path from 'path'; import * as fsSync from 'fs'; import { spawn } from 'child_process'; import { TextDecoder } from 'util'; import { once } from 'events'; import envPaths from 'env-paths'; import { settings } from '../settings.js'; import 'url'; import 'https-proxy-agent'; import TFileCache from '@derhuerst/http-basic/lib/FileCache.js'; import '@derhuerst/http-basic'; import 'progress'; import 'adm-zip'; import * as crypto from 'crypto'; import MagicString from 'magic-string'; function* simpleParse(input) { yield* parser(lexer(input)); } const _T = (n) => (n); const TOK = Object.freeze({EOL: _T(1), EOF: _T(2), Line: _T(3), Comment: _T(4), Version: _T(5), Extension: _T(6), LineNo: _T(7), Directive: _T(8)}); Object.freeze({1: 'EOL', 2: 'EOF', 3: 'Line', 4: 'Comment', 5: 'Version', 6: 'Extension', 7: 'LineNo', 8: 'Directive'}); function* lexer(input) { let skipOne = false; let line = 1, col = 0; let afterLineContinuation = false, inCommentSingleLine = false, inCommentMultiLine = false; let curToken = undefined; let curText = undefined; const setTokenIf = (type) => { if (!curToken) { setToken(type); } }; const setToken = (type) => { curToken = {type, col, line, value: '', text: ''}; }; const emitToken = function* () { if (curToken.type === TOK.Line) { yield curToken; } else { yield curToken; } curToken = undefined; }; const emitTokenIf = function* () { if (curToken) { yield* emitToken(); } }; const appendTokenValue = () => { curToken.text += curText; curToken.value += curText; }; const appendToken = () => { curToken.text += curText; }; const handleEOL = function* () { if (afterLineContinuation) { appendToken(); afterLineContinuation = false; } else if (inCommentMultiLine) { appendToken(); curToken.value += '\n'; } else { yield* emitTokenIf(); inCommentSingleLine = false; yield {type: TOK.EOL, text: curText, col, line, value: '\n'}; } line++; col = 0; }; if (input.length > 0) { let next = input[0]; let cur; for (let i = 1; i <= input.length; i++) { cur = next; next = i < input.length ? input[i] : undefined; col++; if (skipOne) { skipOne = false; continue; } curText = cur; switch (cur) { case '\\': switch (next) { case '\r': case '\n': setTokenIf(TOK.Line); appendToken(); afterLineContinuation = true; break; default: setTokenIf(TOK.Line); appendTokenValue(); } break; case '\r': if (next === '\n') { curText += next; skipOne = true; } yield* handleEOL(); break; case '\n': if (next === '\r') { curText += next; skipOne = true; } yield* handleEOL(); break; default: if (inCommentSingleLine) { appendTokenValue(); } else if (inCommentMultiLine) { if (cur === '*' && next === '/') { curText += next; skipOne = true; appendToken(); yield* emitToken(); inCommentMultiLine = false; } else { appendTokenValue(); } } else { switch (cur) { case '/': switch (next) { case '/': curText += next; skipOne = true; yield* emitTokenIf(); setToken(TOK.Comment); appendToken(); inCommentSingleLine = true; break; case '*': curText += next; skipOne = true; yield* emitTokenIf(); setToken(TOK.Comment); appendToken(); inCommentMultiLine = true; break; default: setTokenIf(TOK.Line); appendTokenValue(); } break; default: setTokenIf(TOK.Line); appendTokenValue(); } } } } yield* emitTokenIf(); } yield {type: TOK.EOF, text: '', col, line, value: ''}; } function* parser(input) { let LineTokens = []; for (const token of input) { switch (token.type) { case TOK.Line: LineTokens.push(token); break; case TOK.Comment: if (LineTokens.length > 0) { LineTokens.push(token); } else { yield token; } break; case TOK.EOL: case TOK.EOF: if (LineTokens.length > 0) { const combinedToken = {...LineTokens[0], type: TOK.Line, value: LineTokens.map((token) => token.type === TOK.Comment ? ' ' : token.value).join(''), text: LineTokens.map((token) => token.text).join(''), }; const matchPreprocessor = /^[ \t]*#[ \t]*([^ \t].*)?$/u.exec(combinedToken.value); if (matchPreprocessor && matchPreprocessor.length === 2) { const directiveLine = matchPreprocessor[1]; if (directiveLine !== undefined) { const directiveParts = directiveLine.split(/[ \t]+/u); if (directiveParts.length > 0) { let [directive, ...body] = directiveParts; body = body.filter(Boolean); switch (directive.toLowerCase()) { case 'version': combinedToken.type = TOK.Version; combinedToken.Version = body.join(' '); break; case 'line': combinedToken.type = TOK.LineNo; break; case 'extension': { combinedToken.type = TOK.Extension; if (body.length === 3 && body[1] === ':') { combinedToken.ExtensionName = body[0]; const extensionBehavior = body[2].toLowerCase(); switch (extensionBehavior) { case 'require': case 'enable': case 'warn': case 'disable': combinedToken.ExtensionBehavior = extensionBehavior; break; default: combinedToken.ExtensionBehavior = body[2]; warnParse(`#extension directive: unknown behavior '${body[2]}'`, combinedToken); } } else { warnParse('#extension directive: parse error', combinedToken); } } break; default: combinedToken.type = TOK.Directive; break; } } } } yield combinedToken; LineTokens = []; } yield token; break; default: yield token; } } } const warnParse = (message, token) => console.error(`Warning: ${formatParseError(message, token)}`); const formatParseError = (message, token) => `${message}\nLine ${ token.line} col ${token.col}:\n${formatLine(token.text)}`; const formatLine = (line) => { let lineF = ''; for (let i = 0; i < line.length; i++) { switch (line[i]) { case '\r': lineF += '<CR>'; break; case '\n': lineF += '<EOL>'; break; case '\t': lineF += '<TAB>'; break; default: lineF += line[i]; } } return lineF; }; const GLSL_INCLUDE_EXT = 'GL_GOOGLE_include_directive'; const GLSL_LINE_EXT = 'GL_GOOGLE_cpp_style_line_directive'; function insertExtensionPreamble(code, filePath, versionReplacer = (v) => v, extraPreamble) { const tokens = [...(function* () { for (const token of simpleParse(code)) { if (token.type === TOK.Extension) { if (token?.ExtensionName === GLSL_INCLUDE_EXT || token?.ExtensionName === GLSL_LINE_EXT) { if (token?.ExtensionBehavior === 'enable' || token?.ExtensionBehavior === 'require') ; else { throw new Error(formatParseError(`Error: extension ${token.ExtensionName} cannot be disabled`, token)); } } } yield token; } })()]; return insertPreambleTokens(tokens, (fixupLineNo) => ({col: 0, line: fixupLineNo, type: TOK.Directive, value: '', text: `#extension ${GLSL_INCLUDE_EXT} : require${ extraPreamble ? `\n${extraPreamble}` : ''}\n#line ${fixupLineNo} "${filePath}"\n`}), versionReplacer); } function fixupDirectives(code, preserve = false, required = true, searchLineDirective = false, stripLineDirectives = false, versionReplacer = (v) => v) { const STRIP_EXT = searchLineDirective ? GLSL_LINE_EXT : GLSL_INCLUDE_EXT; return [...(function* () { let found = false; let skipNextEOL = false; nextToken: for (const token of simpleParse(code)) { if (skipNextEOL) { skipNextEOL = false; if (token.type === TOK.EOL) { continue nextToken; } } switch (token.type) { case TOK.Extension: if (token?.ExtensionName === STRIP_EXT) { if (token?.ExtensionBehavior === 'enable' || token?.ExtensionBehavior === 'require') { if (!found) { found = true; } if (preserve) { token.text = `#extension ${GLSL_LINE_EXT} : require`; } else { skipNextEOL = true; continue nextToken; } } else { console.warn(formatParseError(`Warning: extension ${STRIP_EXT} disabled`, token)); } } break; case TOK.Version: { const newVersion = versionReplacer(token.Version); token.Version = newVersion; token.text = `#version ${newVersion}`; break; } case TOK.LineNo: if (stripLineDirectives && token.type === TOK.LineNo) { skipNextEOL = true; continue nextToken; } break; } yield token; } if (required && !found) { console.warn(`Warning: couldn't find ${STRIP_EXT} directive`); return code; } })()].map((tok) => tok.text).join(''); } function insertPreambleTokens(tokens, preambleToken, versionReplacer = (v) => v) { const newVersionToken = function* (token) { const newVersion = versionReplacer(undefined); if (newVersion !== undefined) { yield {type: TOK.Version, Version: newVersion, col: token.col, line: token.line, text: `#version ${newVersion}`, value: ''}; yield {type: TOK.EOL, col: token.col, line: token.line, text: '\n', value: '\n'}; } }; return {code: [...( function* () { let insertNext = false, acceptVersion = true, foundVersion = false, didInsertion = false; const newVersionPreambleTokens = function* (token) { acceptVersion = false; didInsertion = true; yield* newVersionToken(token); yield preambleToken(token.line); }; for (const token of tokens) { if (insertNext) { insertNext = false; yield preambleToken(token.line); } switch (token.type) { case TOK.Comment: break; case TOK.EOF: if (acceptVersion) { yield* newVersionPreambleTokens(token); } else { if (!didInsertion) { didInsertion = true; yield {type: TOK.EOL, col: token.col, line: token.line, text: '\n', value: '\n'}; yield preambleToken(token.line + 1); } } break; case TOK.EOL: if (acceptVersion) { yield* newVersionPreambleTokens(token); } else { if (!didInsertion) { insertNext = true; didInsertion = true; } } break; case TOK.Version: if (acceptVersion) { acceptVersion = false; foundVersion = true; const newVersion = versionReplacer(token.Version); token.Version = newVersion; token.text = `#version ${newVersion}`; } else { throw new Error(formatParseError(`Parse error: #version directive must be on first line`, token)); } break; default: if (acceptVersion) { yield* newVersionPreambleTokens(token); } } yield token; } if (!foundVersion) { console.warn(`Warning: #version directive missing`); } })()].map((tok) => tok.text).join(''), didInsertion: true}; } function insertPreamble(code, preamble) { return insertPreambleTokens(simpleParse(code), (fixupLineNo) => ({col: 0, line: fixupLineNo, type: TOK.Comment, value: '', text: `${preamble}\n`})); } function chunkWriterAsync(outputStream) { outputStream.setDefaultEncoding('utf8'); outputStream.addListener('error', (err) => { throw new Error(`Output stream error: ${err?.message ?? ''}`); }); return { write: async (strChunk) => { if (!outputStream.write(strChunk, 'utf8')) { await once(outputStream, 'drain'); } }, done: async () => { outputStream.end(); await once(outputStream, 'finish'); }, }; } async function writeLines(stream, lines) { const chunkWriter = chunkWriterAsync(stream); await chunkWriter.write(lines); await chunkWriter.done(); } async function* parseLines(stream) { stream.addListener('error', (err) => { throw new Error(`Input stream error: ${err?.message ?? ''}`); }); const utf8Decoder = new TextDecoder('utf-8'); let outputBuffer = Buffer.from([]); let outputBufferPos = 0; for await (const chunk of stream) { outputBuffer = outputBuffer.length > 0 ? Buffer.concat([outputBuffer, chunk]) : chunk; while (outputBufferPos < outputBuffer.length) { if (outputBuffer[outputBufferPos] === 0xA) { const outputEndPos = (outputBufferPos > 0 && outputBuffer[outputBufferPos-1] === 0xD) ? outputBufferPos - 1 : outputBufferPos; const nextChunk = outputBuffer.slice(0, outputEndPos); outputBuffer = outputBuffer.slice(outputBufferPos+1); outputBufferPos = 0; const nextChunkString = utf8Decoder.decode(nextChunk, {stream: false}); yield nextChunkString; } else { outputBufferPos++; } } } if (outputBuffer.length > 0) { const nextChunkString = utf8Decoder.decode(outputBuffer, {stream: false}); yield nextChunkString; } } async function bufferLines(lines) { const output = []; for await (const line of lines) { output.push(line); } return output; } async function bufferAndOutLines(lines, prefix = '') { const output = []; for await (const line of lines) { output.push(line); console.log(`${prefix}${line}`); } return output; } async function bufferAndErrLines(lines, prefix = '') { const output = []; for await (const line of lines) { output.push(line); console.error(`${prefix}${line}`); } return output; } const binFolder = settings.BIN_PATH; const rootFolder = settings.PROJECT_ROOT; let _pkg; const getPkg = () => { if (!_pkg) { try { _pkg = loadJSON('package.json'); } catch (err) { _pkg = {name: 'unknown'}; } } return _pkg; }; const loadJSON = (file) => JSON.parse(fsSync.readFileSync( path.resolve(rootFolder, file), {encoding: 'utf8'})); const ToolConfig = { Validator: { name: 'glslangValidator', optionKey: 'glslangValidatorPath', envKey: 'GLSLANG_VALIDATOR', url: 'https://github.com/KhronosGroup/glslang', }, Optimizer: { name: 'spirv-opt', optionKey: 'glslangOptimizerPath', envKey: 'GLSLANG_OPTIMIZER', url: 'https://github.com/KhronosGroup/SPIRV-Tools', }, Cross: { name: 'spriv-cross', optionKey: 'glslangCrossPath', envKey: 'GLSLANG_CROSS', url: 'https://github.com/KhronosGroup/SPIRV-Cross', }, }; const ToolDistPaths = { win64: { Validator: `glslangValidator.exe`, Optimizer: `spirv-opt.exe`, Cross: `spirv-cross.exe`, }, ubuntu64: { Validator: `glslangValidator`, Optimizer: `spirv-opt`, Cross: `spirv-cross`, }, macos64: { Validator: `glslangValidator`, Optimizer: `spirv-opt`, Cross: `spirv-cross`, }, }; let _platTag = undefined; let _platConfigured = false; function getPlatTag() { if (!_platTag) { if (arch() === 'x64') { switch (platform()) { case 'win32': _platTag = 'win64'; break; case 'linux': _platTag = 'ubuntu64'; break; case 'darwin': _platTag = 'macos64'; break; } } } return _platTag; } function configurePlatformBinaries() { if (!_platConfigured) { _platConfigured = true; getPlatTag(); if (_platTag) { ((Object.entries(ToolDistPaths[_platTag]))) .forEach(([tool, file]) => ToolConfig[tool].distPath = `${_platTag}${path.sep}${file}`); } } return _platTag ? { folderPath: path.join(binFolder, _platTag), tag: _platTag, fileList: Object.values(ToolConfig).map((tool) => path.join(binFolder, tool.distPath) ?? ''), } : null; } function errorMissingTools(kinds) { let errMsg = `Khronos tool binaries could not be found:\n`; for (const kind of kinds) { const config = ToolConfig[kind]; errMsg += `${config.name} not found, searched path: '${config.path ?? ''}'\n` + toolInfo(config); } throw new Error(errMsg); } const toolInfo = (config) => `${config.name} : configure with the environment variable ${ config.envKey} (or the option ${config.optionKey})\n${config.url}\n`; function configureTools(options, required = (Object.keys(ToolConfig))) { configurePlatformBinaries(); const missingKinds = []; for (const kind of required) { const tool = ToolConfig[kind]; const toolPath = process.env[tool.envKey] || options[tool.optionKey] || tool.distPath; if (!toolPath) { console.warn(`Khronos ${tool.name} binary not shipped for this platform`); } else { tool.path = path.resolve(binFolder, toolPath); } if (!tool.path || !fsSync.existsSync(tool.path)) { missingKinds.push(kind); } } if (missingKinds.length) { errorMissingTools(missingKinds); } } function getToolPath(kind) { const validatorPath = ToolConfig[kind].path; if (!validatorPath) errorMissingTools([kind]); return validatorPath; } function launchTool(kind, workingDir, args) { const toolBin = getToolPath(kind); return launchToolPath(toolBin, workingDir, args); } function launchToolPath(path, workingDir, args) { const toolProcess = spawn(path, args, { cwd: workingDir, stdio: ['pipe', 'pipe', 'pipe'], shell: false, windowsVerbatimArguments: true, }, ); toolProcess.on('error', (err) => { throw new Error(`${path}: failed to launch${err?.message?` : ${err.message}`:''}`); }); const exitPromise = new Promise((resolve, reject) => { toolProcess.on('exit', (code, signal) => { resolve({code, signal}); }); }); return {toolProcess, exitPromise}; } async function waitForToolBuffered({toolProcess, exitPromise}, input = undefined, echo = false) { const stderrPromise = echo ? bufferAndErrLines(parseLines(toolProcess.stderr)) : bufferLines(parseLines(toolProcess.stderr)); const stdoutPromise = echo ? bufferAndOutLines(parseLines(toolProcess.stdout)) : bufferLines(parseLines(toolProcess.stdout)); if (input !== undefined) { await writeLines(toolProcess.stdin, input); } const exitStatus = await exitPromise; const outLines = await stdoutPromise; const errLines = await stderrPromise; return { error: exitStatus.signal !== null || (exitStatus.code && exitStatus.code !== 0), exitMessage: `exit status: ${exitStatus.code || 'n/a'} ${exitStatus.signal || ''}`, exitStatus, outLines, errLines, }; } function printToolDiagnostic(lines) { for (const line of lines) { if (line.length && line !== 'stdin') { console.error(line); } } } const argEscapeWindows = (pattern) => { const buf = []; for (const char of pattern) { switch (char) { case '"': buf.push('\\', '"'); break; default: buf.push(char); } } return buf.join(''); }; const argQuoteWindows = (val) => `"${argEscapeWindows(val)}"`.split(' '); const argVerbatim = (val) => [val]; const argQuote = platform() === 'win32' ? argQuoteWindows : argVerbatim; let _cachePath; const getCachePath = () => { if (!_cachePath) { _cachePath = envPaths(getPkg().name).cache; } return _cachePath; }; ( (TFileCache).default); function checkMakeFolder(path) { if (!fsSync.existsSync(path)) { fsSync.mkdirSync(path, {recursive: true}); } return true; } const rmDir = (path) => fsSync.existsSync(path) && fsSync.rmSync(path, {force: true, recursive: true}); function compressShader(code) { let needNewline = false; return code.replace(/\\(?:\r\n|\n\r|\n|\r)|\/\*.*?\*\/|\/\/(?:\\(?:\r\n|\n\r|\n|\r)|[^\n\r])*/g, '') .split(/\n+/).reduce((result, line) => { line = line.trim().replace(/\s{2,}|\t/, ' '); if (line.charAt(0) === '#') { if (needNewline) { result.push('\n'); } result.push(line, '\n'); needNewline = false; } else { result.push(line.replace(/\s*({|}|=|\*|,|\+|\/|>|<|&|\||\[|\]|\(|\)|-|!|;)\s*/g, '$1')); needNewline = true; } return result; }, []).join('').replace(/\n+/g, '\n'); } async function glslRunTool(kind, title, name, workingDir, input, params) { const result = await waitForToolBuffered(launchTool(kind, workingDir, params), input); if (result.error) { printToolDiagnostic(result.outLines); printToolDiagnostic(result.errLines); const errMsg = `${title}: ${name} failed, ${result.exitMessage}`; console.error(errMsg); throw new Error(errMsg); } return result.outLines ? result.outLines.join(EOL) : ''; } async function glslRunValidator(name, workingDir, stageName, input, params, extraParams) { return glslRunTool('Validator', 'Khronos glslangValidator', name, workingDir, input, [ '--stdin', '-C', '-t', '-S', stageName, ...params, ...extraParams, ]); } async function glslRunOptimizer(name, workingDir, inputFile, outputFile, input, preserveUnusedBindings = true, params, extraParams) { return glslRunTool('Optimizer', 'Khronos spirv-opt', name, workingDir, input, [ '-O', '--target-env=opengl4.0', ...(preserveUnusedBindings ? ['--preserve-bindings'] : []), ...params, ...extraParams, ...argQuote(inputFile), '-o', ...argQuote(outputFile), ]); } async function glslRunCross(name, workingDir, stageName, inputFile, input, emitLineInfo, params, extraParams) { return glslRunTool('Cross', 'Khronos spirv-cross', name, workingDir, input, [ ...argQuote(inputFile), ...(emitLineInfo ? ['--emit-line-directives'] : []), `--stage`, stageName, ...params, ...extraParams, ]); } function getBuildDir(id) { const sanitizeID = path.basename(id).replace(/([^a-z0-9]+)/gi, '-').toLowerCase(); const uniqID = ((Date.now()>>>0) + crypto.randomBytes(4).readUInt32LE())>>>0; const uniqIDHex = uniqID.toString(16).padStart(8, '0'); return path.join(getCachePath(), 'glslBuild', `${sanitizeID}-${uniqIDHex}`); } async function glslProcessSource(id, source, stageName, glslOptions = {}, warnLog = console.error) { const options = { sourceMap: true, compress: true, optimize: true, emitLineDirectives: false, suppressLineExtensionDirective: false, optimizerPreserveUnusedBindings: true, optimizerDebugSkipOptimizer: false, preamble: undefined, includePaths: [], extraValidatorParams: [], extraOptimizerParams: [], extraCrossParams: [], ...glslOptions, }; configureTools({}, options.optimize ? ['Validator', 'Optimizer', 'Cross'] : ['Validator']); let tempBuildDir; if (options.optimize) { tempBuildDir = getBuildDir(id); rmDir(tempBuildDir); checkMakeFolder(tempBuildDir); } const baseDir = path.dirname(id); const baseName = path.basename(id); let targetID = `./${baseName}`; let targetDir = baseDir; let outputFile = targetID; if (!fsSync.existsSync(targetDir)) { warnLog(`Error resolving path: '${id}' : Khronos glslangValidator may fail to find includes`); targetDir = process.cwd(); targetID = id; outputFile = `temp`; } let outputFileAbs; let optimizedFileAbs; let versionReplacer; let targetGlslVersion = 300; if (options.optimize) { outputFileAbs = path.join(tempBuildDir, `${outputFile}.spv`); optimizedFileAbs = path.join(tempBuildDir, `${outputFile}-opt.spv`); versionReplacer = (version) => { const versionParts = version && version.match(/^\s*(\d+)(?:\s+(es))?\s*$/i); if (versionParts && versionParts.length === 3) { targetGlslVersion = +versionParts[1]; } if (targetGlslVersion < 300) { throw new Error(`Only GLSL ES shaders version 300 (WebGL2) or higher can be optimized`); } return `${Math.max(targetGlslVersion, 310)} es`; }; } const {code, didInsertion} = insertExtensionPreamble(source, targetID, versionReplacer, options.preamble); const extraValidatorParams = [ ...options.includePaths.map((path) => `-I${path}`), ...options.extraValidatorParams, ]; let processedGLSL; if (options.optimize) { await glslRunValidator('Build spirv', targetDir, stageName, code, [ '-G', '-g', '--auto-map-locations', '--auto-map-bindings', '-o', ...argQuote(outputFileAbs), ], extraValidatorParams); if (!fsSync.existsSync(outputFileAbs)) { throw new Error(`Build spirv failed: no output file`); } if (!options.optimizerDebugSkipOptimizer) { await glslRunOptimizer('Optimize spirv', targetDir, outputFileAbs, optimizedFileAbs, undefined, options.optimizerPreserveUnusedBindings, [ ], options.extraOptimizerParams); if (!fsSync.existsSync(optimizedFileAbs)) { throw new Error(`Optimize spirv failed: no output file (${optimizedFileAbs})`); } } processedGLSL = await glslRunCross('Build spirv to GLSL', targetDir, stageName, options.optimizerDebugSkipOptimizer ? outputFileAbs : optimizedFileAbs, undefined, options.emitLineDirectives, [ '--es', '--version', `${targetGlslVersion}`, ], options.extraCrossParams); rmDir(tempBuildDir); } else { processedGLSL = await glslRunValidator('Preprocessing', targetDir, stageName, code, [ '-E', ], extraValidatorParams); await glslRunValidator('Validation', targetDir, stageName, processedGLSL, [], extraValidatorParams); } processedGLSL = fixupDirectives(processedGLSL, options.emitLineDirectives && !options.suppressLineExtensionDirective, didInsertion && (!options.optimize || options.emitLineDirectives), options.optimize, !options.emitLineDirectives, undefined); const outputCode = options.compress ? compressShader(processedGLSL) : processedGLSL; const result = { code: outputCode, map: {mappings: ''}, }; if (options.sourceMap) { const sourceMapSource = insertPreamble(processedGLSL, '/*\n' + `* Preprocessed${options.optimize?' + Optimized':''} from '${targetID}'\n` + (options.compress ? '* [Embedded string is compressed]\n':'') + '*/', ).code; const magicString = new MagicString(sourceMapSource); result.map = magicString.generateMap({ source: id, includeContent: true, hires: true, }); } return result; } let glslifyCompile; async function glslifyInit() { if (glslifyCompile) return; try { const glslify = await import('glslify'); if (glslify && glslify.compile && typeof glslify.compile === 'function') { glslifyCompile = glslify.compile; } } catch { } } async function glslifyProcessSource(id, source, options, failError, warnLog = console.error) { if (!glslifyCompile) { failError(`glslify could not be found. Install it with npm i -D glslify`); } let basedir = path.dirname(id); if (!fsSync.existsSync(basedir)) { warnLog(`Error resolving path: '${id}' : glslify may fail to find includes`); basedir = process.cwd(); } return glslifyCompile(source, ({basedir, ...options})); } const stageDefs = { 'vert': ['.vs', '.vert', '.vs.glsl', '.vert.glsl'], 'frag': ['.fs', '.frag', '.fs.glsl', '.frag.glsl'], 'geom': ['.geom', '.geom.glsl'], 'comp': ['.comp', '.comp.glsl'], 'tesc': ['.tesc', '.tesc.glsl'], 'tese': ['.tese', '.tese.glsl'], }; const extsIncludeDefault = [...Object.values(stageDefs).flatMap( (exts) => exts.map((ext) => `**/*${ext}`)), '**/*.glsl', ]; const stageRegexes = ( (Object.entries(stageDefs)) .map(([st, exts]) => [st, new RegExp(`(?:${exts.map((ext) => ext.replace('.', '\\.')).join('|')})$`, 'i'), ])); function generateCode(source) { return `export default ${JSON.stringify(source)}; // eslint-disable-line`; } function glslOptimize(userOptions = {}) { const pluginOptions = { include: extsIncludeDefault, exclude: [], glslify: false, glslifyOptions: {}, ...userOptions, }; const filter = createFilter(pluginOptions.include, pluginOptions.exclude); return { name: 'glsl-optimize', async options(options) { if (pluginOptions.glslify) { await glslifyInit(); } return options; }, async load(id) { if (!id || !filter(id) || !fsSync.existsSync(id)) return; let source; try { source = fsSync.readFileSync(id, {encoding: 'utf8'}); } catch (err) { this.warn(`Failed to load file '${id}' : ${err.message}`); return; } const stage = stageRegexes.find(([, regex]) => id.match(regex))?.[0]; if (!stage) { this.error({message: `File '${id}' : extension did not match a shader stage.`}); } if (pluginOptions.glslify) { try { source = await glslifyProcessSource(id, source, pluginOptions.glslifyOptions, (message) => this.error({message})); } catch (err) { this.error({message: `Error processing GLSL source with glslify:\n${err.message}`}); } } try { const result = await glslProcessSource(id, source, stage, pluginOptions); result.code = generateCode(result.code); return result; } catch (err) { this.error({message: `Error processing GLSL source:\n${err.message}`}); } }, }; } export { glslOptimize as default };