@galacean/engine-shader-compiler 2.0.0-alpha.52 → 2.0.0-alpha.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +40 -4
  2. package/bundler/cli.js +199 -104
  3. package/bundler/precompile.cjs.js +117 -47
  4. package/bundler/precompile.js +117 -47
  5. package/bundler/rollup.cjs.js +115 -45
  6. package/bundler/rollup.js +115 -45
  7. package/dist/browser.js +13421 -6555
  8. package/dist/browser.js.map +1 -1
  9. package/dist/browser.min.js +1 -1
  10. package/dist/browser.min.js.map +1 -1
  11. package/dist/main.js +1758 -8450
  12. package/dist/main.js.map +1 -1
  13. package/dist/module.js +1730 -8427
  14. package/dist/module.js.map +1 -1
  15. package/dist/precompile.main.js +2280 -0
  16. package/dist/precompile.main.js.map +1 -0
  17. package/dist/precompile.module.js +2276 -0
  18. package/dist/precompile.module.js.map +1 -0
  19. package/package.json +24 -13
  20. package/types/GLESBackend.d.ts +22 -0
  21. package/types/ShaderBackend.d.ts +15 -0
  22. package/types/ShaderCompilation.d.ts +40 -0
  23. package/types/ShaderCompiler.d.ts +42 -14
  24. package/types/ShaderInstructionEncoder.d.ts +46 -14
  25. package/types/ShaderPrecompiler.d.ts +31 -0
  26. package/types/bundler/precompile.d.ts +26 -3
  27. package/types/bundler/rollup.d.ts +23 -2
  28. package/types/codeGen/CodeGenVisitor.d.ts +11 -6
  29. package/types/codeGen/GLES100.d.ts +3 -5
  30. package/types/codeGen/GLES300.d.ts +14 -7
  31. package/types/codeGen/GLESVisitor.d.ts +55 -9
  32. package/types/codeGen/VisitorContext.d.ts +88 -29
  33. package/types/codeGen/types.d.ts +1 -1
  34. package/types/index.d.ts +4 -1
  35. package/types/precompile.d.ts +1 -0
  36. package/dist/browser.verbose.js +0 -9601
  37. package/dist/browser.verbose.js.map +0 -1
  38. package/dist/browser.verbose.min.js +0 -2
  39. package/dist/browser.verbose.min.js.map +0 -1
  40. package/dist/main.verbose.js +0 -9598
  41. package/dist/main.verbose.js.map +0 -1
  42. package/dist/module.verbose.js +0 -9591
  43. package/dist/module.verbose.js.map +0 -1
  44. package/types/GSError.d.ts +0 -16
  45. package/types/ParserUtils.d.ts +0 -43
  46. package/types/Preprocessor.d.ts +0 -30
  47. package/types/ShaderCompilerUtils.d.ts +0 -10
  48. package/types/common/BaseLexer.d.ts +0 -51
  49. package/types/common/BaseToken.d.ts +0 -35
  50. package/types/common/IBaseSymbol.d.ts +0 -5
  51. package/types/common/ShaderPosition.d.ts +0 -8
  52. package/types/common/ShaderRange.d.ts +0 -8
  53. package/types/common/SymbolTable.d.ts +0 -13
  54. package/types/common/SymbolTableStack.d.ts +0 -17
  55. package/types/common/enums/Keyword.d.ts +0 -124
  56. package/types/common/enums/ShaderStage.d.ts +0 -5
  57. package/types/common/index.d.ts +0 -5
  58. package/types/common/types.d.ts +0 -101
  59. package/types/lalr/CFG.d.ts +0 -5
  60. package/types/lalr/LALR1.d.ts +0 -27
  61. package/types/lalr/Production.d.ts +0 -9
  62. package/types/lalr/State.d.ts +0 -20
  63. package/types/lalr/StateItem.d.ts +0 -20
  64. package/types/lalr/Utils.d.ts +0 -22
  65. package/types/lalr/index.d.ts +0 -2
  66. package/types/lalr/types.d.ts +0 -14
  67. package/types/lexer/Lexer.d.ts +0 -143
  68. package/types/lexer/index.d.ts +0 -1
  69. package/types/parser/AST.d.ts +0 -502
  70. package/types/parser/Grammar.d.ts +0 -11
  71. package/types/parser/GrammarSymbol.d.ts +0 -128
  72. package/types/parser/SemanticAnalyzer.d.ts +0 -42
  73. package/types/parser/ShaderInfo.d.ts +0 -10
  74. package/types/parser/ShaderTargetParser.d.ts +0 -26
  75. package/types/parser/builtin/functions.d.ts +0 -34
  76. package/types/parser/builtin/index.d.ts +0 -2
  77. package/types/parser/builtin/variables.d.ts +0 -11
  78. package/types/parser/index.d.ts +0 -2
  79. package/types/parser/symbolTable/FnSymbol.d.ts +0 -6
  80. package/types/parser/symbolTable/StructSymbol.d.ts +0 -6
  81. package/types/parser/symbolTable/SymbolDataType.d.ts +0 -8
  82. package/types/parser/symbolTable/SymbolInfo.d.ts +0 -22
  83. package/types/parser/symbolTable/VarSymbol.d.ts +0 -8
  84. package/types/parser/symbolTable/index.d.ts +0 -6
  85. package/types/parser/types.d.ts +0 -24
  86. package/types/sourceParser/ShaderSourceFactory.d.ts +0 -8
  87. package/types/sourceParser/ShaderSourceParser.d.ts +0 -30
  88. package/types/sourceParser/ShaderSourceSymbol.d.ts +0 -10
  89. package/types/sourceParser/SourceLexer.d.ts +0 -19
  90. package/types/sourceParser/index.d.ts +0 -1
  91. package/verbose/package.json +0 -10
@@ -5,7 +5,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var fs = require('node:fs');
6
6
  var path = require('node:path');
7
7
  var node_module = require('node:module');
8
- var node_url = require('node:url');
9
8
 
10
9
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
11
10
 
@@ -30,7 +29,11 @@ function _interopNamespace(e) {
30
29
  var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
31
30
  var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
32
31
 
33
- /** Normalize to forward slashes (Windows uses `\`; downstream regex/keys assume `/`). */ function normalizePath(p) {
32
+ /**
33
+ * Converts a filesystem path to the slash-separated form used by shader asset keys.
34
+ * @param p - Filesystem path.
35
+ * @returns Slash-separated path.
36
+ */ function normalizePath(p) {
34
37
  return p.split(path__default["default"].sep).join("/");
35
38
  }
36
39
  /** Recursively find all files with a given extension. */ function findFiles(dir, ext) {
@@ -67,7 +70,12 @@ var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
67
70
  dir = path__default["default"].dirname(dir);
68
71
  }
69
72
  }
70
- async function runFull(options) {
73
+ /**
74
+ * Precompiles the current shader source tree once.
75
+ * @param options - Source, output, cleanup, and artifact-generation options.
76
+ * @returns An object containing the number of shader sources that failed to compile.
77
+ * @throws Error when the input tree or an explicitly selected source is invalid.
78
+ */ async function runFull(options) {
71
79
  const inputDir = path__default["default"].resolve(options.input);
72
80
  const outputDir = path__default["default"].resolve(options.output);
73
81
  const platformTarget = options.platformTarget ?? 0;
@@ -82,22 +90,13 @@ async function runFull(options) {
82
90
  emitSources(inputDir);
83
91
  }
84
92
  const shaderCompiler = await loadShaderCompiler();
85
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
93
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
86
94
  let failed = 0;
87
95
  if (options.only) {
88
96
  const target = path__default["default"].resolve(options.only);
89
97
  if (!compileSingle(shaderCompiler, target, inputDir, outputDir, platformTarget)) failed++;
90
98
  } else {
91
- const shaderFiles = findFiles(inputDir, ".shader");
92
- console.log(`[shader-compiler-bundler] Precompiling ${shaderFiles.length} shader(s)...`);
93
- for (const file of shaderFiles){
94
- if (!compileSingle(shaderCompiler, file, inputDir, outputDir, platformTarget)) failed++;
95
- }
96
- if (options.clean) {
97
- const removed = cleanOrphanedBundles(shaderFiles, inputDir, outputDir);
98
- if (removed > 0) console.log(`[shader-compiler-bundler] Cleaned ${removed} orphaned .shaderc file(s).`);
99
- }
100
- if (failed > 0) console.warn(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
99
+ failed = compileAll(shaderCompiler, inputDir, outputDir, platformTarget, options.clean);
101
100
  }
102
101
  if (options.emitIndex) {
103
102
  emitIndex(outputDir);
@@ -106,12 +105,23 @@ async function runFull(options) {
106
105
  failed
107
106
  };
108
107
  }
109
- async function startWatcher(options) {
108
+ /**
109
+ * Watches one shader source tree and serializes incremental rebuilds.
110
+ * @param options - Source, output, cleanup, and artifact-generation options.
111
+ * @returns Once the filesystem watcher has been installed.
112
+ * @throws Error when the initial include registry cannot be loaded.
113
+ */ async function startWatcher(options) {
110
114
  const inputDir = path__default["default"].resolve(options.input);
111
115
  const outputDir = path__default["default"].resolve(options.output);
112
116
  const platformTarget = options.platformTarget ?? 0;
117
+ if (!fs__default["default"].existsSync(inputDir)) {
118
+ throw new Error(`Input directory does not exist: ${inputDir}`);
119
+ }
120
+ fs__default["default"].mkdirSync(outputDir, {
121
+ recursive: true
122
+ });
113
123
  const shaderCompiler = await loadShaderCompiler();
114
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
124
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
115
125
  // .glsl change → full recompile (can't cheaply track include graphs).
116
126
  // .shader change → single-file recompile.
117
127
  const handleInputChange = async (filename)=>{
@@ -120,8 +130,11 @@ async function startWatcher(options) {
120
130
  const fullPath = path__default["default"].join(inputDir, filename);
121
131
  if (norm.endsWith(".glsl")) {
122
132
  console.log(`[shader-compiler-bundler] Include changed (${norm}), full recompile...`);
123
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
124
- runFull(options).catch((e)=>console.error(e));
133
+ if (options.emitSources) emitSources(inputDir);
134
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
135
+ const failed = compileAll(shaderCompiler, inputDir, outputDir, platformTarget, options.clean);
136
+ if (options.emitIndex) emitIndex(outputDir);
137
+ if (failed > 0) console.error(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
125
138
  return;
126
139
  }
127
140
  if (!norm.endsWith(".shader")) return;
@@ -134,68 +147,92 @@ async function startWatcher(options) {
134
147
  if (options.emitIndex) emitIndex(outputDir);
135
148
  };
136
149
  console.log(`[shader-compiler-bundler] Watching ${inputDir} ...`);
150
+ let changeQueue = Promise.resolve();
137
151
  fs__default["default"].watch(inputDir, {
138
152
  recursive: true
139
153
  }, (_event, filename)=>{
140
- handleInputChange(filename).catch((e)=>console.error(e));
154
+ changeQueue = changeQueue.then(()=>handleInputChange(filename)).catch((e)=>console.error(e));
141
155
  });
142
156
  }
157
+ function compileAll(shaderCompiler, inputDir, outputDir, platformTarget, clean) {
158
+ const shaderFiles = findFiles(inputDir, ".shader");
159
+ console.log(`[shader-compiler-bundler] Precompiling ${shaderFiles.length} shader(s)...`);
160
+ let failed = 0;
161
+ for (const file of shaderFiles){
162
+ if (!compileSingle(shaderCompiler, file, inputDir, outputDir, platformTarget)) failed++;
163
+ }
164
+ if (clean) {
165
+ const removed = cleanOrphanedBundles(shaderFiles, inputDir, outputDir);
166
+ if (removed > 0) console.log(`[shader-compiler-bundler] Cleaned ${removed} orphaned .shaderc file(s).`);
167
+ }
168
+ if (failed > 0) console.warn(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
169
+ return failed;
170
+ }
143
171
  async function loadShaderCompiler() {
144
- // @ts-ignore — `../dist/main.js` is the compiled runtime entry; no .ts source.
145
- // Bundler ships at `<pkg>/bundler/` and the runtime at `<pkg>/dist/`, so this
146
- // resolves to the sibling `dist/` from `bundler/cli.js` at runtime.
147
- const mod = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('../dist/main.js')); });
148
- const instance = new mod.ShaderCompiler();
149
- if (typeof instance._precompile !== "function") {
150
- throw new Error("ShaderCompiler._precompile is not available; rebuild @galacean/engine-shader-compiler first.");
172
+ const mod = await (process.env.GALACEAN_SHADER_COMPILER_BOOTSTRAP === "true" ? importBootstrapPrecompiler() : importReleasePrecompiler());
173
+ const instance = new mod.ShaderPrecompiler();
174
+ if (typeof instance.precompile !== "function") {
175
+ throw new Error("ShaderPrecompiler is unavailable; rebuild @galacean/engine-shader-compiler first.");
151
176
  }
152
177
  return instance;
153
178
  }
179
+ async function importBootstrapPrecompiler() {
180
+ // @ts-expect-error generated repo-local runtime has no declaration file
181
+ return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('../.bootstrap/main.js')); });
182
+ }
183
+ async function importReleasePrecompiler() {
184
+ // @ts-expect-error generated Node-only runtime has no declaration file
185
+ return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('../dist/precompile.main.js')); });
186
+ }
154
187
  // Local .glsl come from inputDir; standard library comes from
155
188
  // `@galacean/engine-shader/sources` with a sibling-dir fallback for engine self-build cold-start.
156
189
  async function collectIncludeMap(inputDir) {
157
- const map = {};
190
+ const map = Object.create(null);
158
191
  for (const file of findFiles(inputDir, ".glsl")){
159
192
  const relPath = normalizePath(path__default["default"].relative(inputDir, file));
160
- map[relPath] = fs__default["default"].readFileSync(file, "utf-8");
193
+ addIncludeSource(map, relPath, fs__default["default"].readFileSync(file, "utf-8"));
161
194
  }
162
- let usedRelease = false;
163
195
  // Resolve from inputDir's node_modules so consumers without transitive linkage to shader-compiler can still find it.
196
+ const requireFromInput = node_module.createRequire(path__default["default"].join(inputDir, "package.json"));
197
+ let sourcesPath;
164
198
  try {
165
- const requireFromInput = node_module.createRequire(path__default["default"].join(inputDir, "package.json"));
166
- const sourcesPath = requireFromInput.resolve("@galacean/engine-shader/sources");
167
- const { shaderLibrary } = await (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })(node_url.pathToFileURL(sourcesPath).href);
199
+ sourcesPath = requireFromInput.resolve("@galacean/engine-shader/sources");
200
+ } catch (error) {
201
+ if (error.code !== "MODULE_NOT_FOUND") throw error;
202
+ }
203
+ if (sourcesPath) {
204
+ const { shaderLibrary } = requireFromInput(sourcesPath);
168
205
  for (const { source, path: chunkPath } of shaderLibrary){
169
- map[chunkPath] = source;
206
+ addIncludeSource(map, chunkPath, source);
170
207
  }
171
- usedRelease = true;
172
- } catch {
173
- // engine self-build cold-start: dist/sources.* not yet built; use sibling source.
174
- }
175
- if (!usedRelease) {
208
+ } else {
209
+ // Engine self-build cold-start has no generated sources entry yet.
176
210
  const siblingLibrary = path__default["default"].join(path__default["default"].dirname(inputDir), "ShaderLibrary");
177
211
  if (fs__default["default"].existsSync(siblingLibrary)) {
178
212
  for (const file of findFiles(siblingLibrary, ".glsl")){
179
213
  const rel = normalizePath(path__default["default"].relative(siblingLibrary, file));
180
- map[`ShaderLibrary/${rel}`] = fs__default["default"].readFileSync(file, "utf-8");
214
+ addIncludeSource(map, `ShaderLibrary/${rel}`, fs__default["default"].readFileSync(file, "utf-8"));
181
215
  }
182
216
  }
183
217
  }
184
218
  return map;
185
219
  }
186
- // Mirrors `ShaderPass._shaderRootPath` in engine; inlined to keep bundler engine-free.
187
- const SHADER_ROOT_PATH = "shaders://root/";
220
+ function addIncludeSource(map, key, source) {
221
+ if (Object.prototype.hasOwnProperty.call(map, key)) {
222
+ throw new Error(`Shader include "${key}" is registered more than once.`);
223
+ }
224
+ map[key] = source;
225
+ }
188
226
  function compileSingle(shaderCompiler, shaderPath, inputDir, outputDir, platformTarget) {
227
+ const relativePath = shaderSourceRelativePath(shaderPath, inputDir);
189
228
  const source = fs__default["default"].readFileSync(shaderPath, "utf-8");
190
- const relativePath = normalizePath(path__default["default"].relative(inputDir, shaderPath));
191
229
  const bundleRelative = relativePath.replace(/\.shader$/, ".shaderc");
192
230
  const bundlePath = path__default["default"].join(outputDir, bundleRelative);
193
- const basePathForIncludeKey = new URL(relativePath, SHADER_ROOT_PATH).href;
194
231
  fs__default["default"].mkdirSync(path__default["default"].dirname(bundlePath), {
195
232
  recursive: true
196
233
  });
197
234
  try {
198
- const precompiled = shaderCompiler._precompile(source, platformTarget, basePathForIncludeKey);
235
+ const precompiled = shaderCompiler.precompile(source, platformTarget, relativePath);
199
236
  fs__default["default"].writeFileSync(bundlePath, JSON.stringify(precompiled));
200
237
  console.log(` ${relativePath} -> ${bundleRelative}`);
201
238
  return true;
@@ -205,6 +242,13 @@ function compileSingle(shaderCompiler, shaderPath, inputDir, outputDir, platform
205
242
  return false;
206
243
  }
207
244
  }
245
+ function shaderSourceRelativePath(shaderPath, inputDir) {
246
+ const relativePath = path__default["default"].relative(inputDir, shaderPath);
247
+ if (relativePath === ".." || relativePath.startsWith(`..${path__default["default"].sep}`) || path__default["default"].isAbsolute(relativePath) || path__default["default"].extname(relativePath) !== ".shader") {
248
+ throw new Error(`Shader source must be a .shader file inside the input directory: ${shaderPath}`);
249
+ }
250
+ return normalizePath(relativePath);
251
+ }
208
252
  function cleanOrphanedBundles(shaderFiles, inputDir, outputDir) {
209
253
  const aliveSet = new Set(shaderFiles.map((f)=>normalizePath(path__default["default"].relative(inputDir, f)).replace(/\.shader$/, "")));
210
254
  const bundleFiles = findFiles(outputDir, ".shaderc");
@@ -229,6 +273,10 @@ function emitIndex(outputDir) {
229
273
  importPath: `./${rel}`
230
274
  };
231
275
  }).sort((a, b)=>a.varName.localeCompare(b.varName));
276
+ assertUniqueIdentifiers(entries.map(({ varName, importPath })=>({
277
+ identifier: varName,
278
+ path: importPath
279
+ })));
232
280
  const imports = entries.map((e)=>`import ${e.varName}Data from "${e.importPath}";`).join("\n");
233
281
  const exports = entries.map((e)=>`export const ${e.varName}: IPrecompiledShader = ${e.varName}Data;`).join("\n");
234
282
  const content = `// Auto-generated by shader-compiler-precompile --emit-index — do not edit.\nimport type { IPrecompiledShader } from "@galacean/engine-design";\n${imports}\n\n${exports}\n`;
@@ -255,6 +303,10 @@ function emitShaderSourceIndex(rootDir) {
255
303
  const importPaths = files.map((f)=>normalizePath(path__default["default"].relative(rootDir, f)));
256
304
  const paths = files.map((f)=>normalizePath(path__default["default"].relative(srcDir, f)));
257
305
  const ids = importPaths.map((p)=>pathToIdentifier(p, ".shader"));
306
+ assertUniqueIdentifiers(ids.map((identifier, index)=>({
307
+ identifier,
308
+ path: importPaths[index]
309
+ })));
258
310
  const lines = [];
259
311
  lines.push("// Auto-generated by shader-compiler-precompile --emit-sources — do not edit.");
260
312
  lines.push("");
@@ -280,6 +332,10 @@ function emitLibrarySourceIndex(rootDir) {
280
332
  const importPaths = files.map((f)=>normalizePath(path__default["default"].relative(rootDir, f)));
281
333
  const paths = files.map((f)=>normalizePath(path__default["default"].relative(srcDir, f)));
282
334
  const ids = importPaths.map((p)=>pathToIdentifier(p, ".glsl"));
335
+ assertUniqueIdentifiers(ids.map((identifier, index)=>({
336
+ identifier,
337
+ path: importPaths[index]
338
+ })));
283
339
  const lines = [];
284
340
  lines.push("// Auto-generated by shader-compiler-precompile --emit-sources — do not edit.");
285
341
  lines.push("");
@@ -295,6 +351,16 @@ function emitLibrarySourceIndex(rootDir) {
295
351
  fs__default["default"].writeFileSync(out, lines.join("\n"), "utf-8");
296
352
  console.log(` Emitted library index ${path__default["default"].relative(process.cwd(), out)} (${files.length})`);
297
353
  }
354
+ function assertUniqueIdentifiers(entries) {
355
+ const pathsByIdentifier = new Map();
356
+ for (const { identifier, path: sourcePath } of entries){
357
+ const existingPath = pathsByIdentifier.get(identifier);
358
+ if (existingPath !== undefined) {
359
+ throw new Error(`Shader asset paths "${existingPath}" and "${sourcePath}" both emit identifier "${identifier}".`);
360
+ }
361
+ pathsByIdentifier.set(identifier, sourcePath);
362
+ }
363
+ }
298
364
  function removeBundleFor(shaderPath, inputDir, outputDir) {
299
365
  const rel = normalizePath(path__default["default"].relative(inputDir, shaderPath)).replace(/\.shader$/, ".shaderc");
300
366
  const bundlePath = path__default["default"].join(outputDir, rel);
@@ -333,6 +399,9 @@ function removeBundleFor(shaderPath, inputDir, outputDir) {
333
399
  * - When `precompile` option is set: runs a full precompile in buildStart and,
334
400
  * in watch mode, starts a background watcher that mirrors `.shader` source
335
401
  * changes into `.shaderc` outputs and refreshes the aggregated index.
402
+ * @param options - Transform filter and optional offline precompile configuration.
403
+ * @returns Rollup plugin for shader source and artifact modules.
404
+ * @throws Error during `buildStart` when offline precompilation reports failures.
336
405
  */ function shaderCompiler(options = {}) {
337
406
  const { filter, precompile } = options;
338
407
  const precompileOptions = precompile && {
@@ -347,7 +416,8 @@ function removeBundleFor(shaderPath, inputDir, outputDir) {
347
416
  async buildStart () {
348
417
  if (!precompileOptions || initialDone) return;
349
418
  initialDone = true;
350
- await runFull(precompileOptions);
419
+ const { failed } = await runFull(precompileOptions);
420
+ if (failed > 0) this.error(`${failed} shader(s) failed to precompile.`);
351
421
  if (this.meta.watchMode && !watcherStarted) {
352
422
  watcherStarted = true;
353
423
  startWatcher(precompileOptions).catch((e)=>this.warn(`[shader-compiler] watcher failed: ${e.message}`));
package/bundler/rollup.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
- import { pathToFileURL } from 'node:url';
5
4
 
6
- /** Normalize to forward slashes (Windows uses `\`; downstream regex/keys assume `/`). */ function normalizePath(p) {
5
+ /**
6
+ * Converts a filesystem path to the slash-separated form used by shader asset keys.
7
+ * @param p - Filesystem path.
8
+ * @returns Slash-separated path.
9
+ */ function normalizePath(p) {
7
10
  return p.split(path.sep).join("/");
8
11
  }
9
12
  /** Recursively find all files with a given extension. */ function findFiles(dir, ext) {
@@ -40,7 +43,12 @@ import { pathToFileURL } from 'node:url';
40
43
  dir = path.dirname(dir);
41
44
  }
42
45
  }
43
- async function runFull(options) {
46
+ /**
47
+ * Precompiles the current shader source tree once.
48
+ * @param options - Source, output, cleanup, and artifact-generation options.
49
+ * @returns An object containing the number of shader sources that failed to compile.
50
+ * @throws Error when the input tree or an explicitly selected source is invalid.
51
+ */ async function runFull(options) {
44
52
  const inputDir = path.resolve(options.input);
45
53
  const outputDir = path.resolve(options.output);
46
54
  const platformTarget = options.platformTarget ?? 0;
@@ -55,22 +63,13 @@ async function runFull(options) {
55
63
  emitSources(inputDir);
56
64
  }
57
65
  const shaderCompiler = await loadShaderCompiler();
58
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
66
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
59
67
  let failed = 0;
60
68
  if (options.only) {
61
69
  const target = path.resolve(options.only);
62
70
  if (!compileSingle(shaderCompiler, target, inputDir, outputDir, platformTarget)) failed++;
63
71
  } else {
64
- const shaderFiles = findFiles(inputDir, ".shader");
65
- console.log(`[shader-compiler-bundler] Precompiling ${shaderFiles.length} shader(s)...`);
66
- for (const file of shaderFiles){
67
- if (!compileSingle(shaderCompiler, file, inputDir, outputDir, platformTarget)) failed++;
68
- }
69
- if (options.clean) {
70
- const removed = cleanOrphanedBundles(shaderFiles, inputDir, outputDir);
71
- if (removed > 0) console.log(`[shader-compiler-bundler] Cleaned ${removed} orphaned .shaderc file(s).`);
72
- }
73
- if (failed > 0) console.warn(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
72
+ failed = compileAll(shaderCompiler, inputDir, outputDir, platformTarget, options.clean);
74
73
  }
75
74
  if (options.emitIndex) {
76
75
  emitIndex(outputDir);
@@ -79,12 +78,23 @@ async function runFull(options) {
79
78
  failed
80
79
  };
81
80
  }
82
- async function startWatcher(options) {
81
+ /**
82
+ * Watches one shader source tree and serializes incremental rebuilds.
83
+ * @param options - Source, output, cleanup, and artifact-generation options.
84
+ * @returns Once the filesystem watcher has been installed.
85
+ * @throws Error when the initial include registry cannot be loaded.
86
+ */ async function startWatcher(options) {
83
87
  const inputDir = path.resolve(options.input);
84
88
  const outputDir = path.resolve(options.output);
85
89
  const platformTarget = options.platformTarget ?? 0;
90
+ if (!fs.existsSync(inputDir)) {
91
+ throw new Error(`Input directory does not exist: ${inputDir}`);
92
+ }
93
+ fs.mkdirSync(outputDir, {
94
+ recursive: true
95
+ });
86
96
  const shaderCompiler = await loadShaderCompiler();
87
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
97
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
88
98
  // .glsl change → full recompile (can't cheaply track include graphs).
89
99
  // .shader change → single-file recompile.
90
100
  const handleInputChange = async (filename)=>{
@@ -93,8 +103,11 @@ async function startWatcher(options) {
93
103
  const fullPath = path.join(inputDir, filename);
94
104
  if (norm.endsWith(".glsl")) {
95
105
  console.log(`[shader-compiler-bundler] Include changed (${norm}), full recompile...`);
96
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
97
- runFull(options).catch((e)=>console.error(e));
106
+ if (options.emitSources) emitSources(inputDir);
107
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
108
+ const failed = compileAll(shaderCompiler, inputDir, outputDir, platformTarget, options.clean);
109
+ if (options.emitIndex) emitIndex(outputDir);
110
+ if (failed > 0) console.error(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
98
111
  return;
99
112
  }
100
113
  if (!norm.endsWith(".shader")) return;
@@ -107,68 +120,92 @@ async function startWatcher(options) {
107
120
  if (options.emitIndex) emitIndex(outputDir);
108
121
  };
109
122
  console.log(`[shader-compiler-bundler] Watching ${inputDir} ...`);
123
+ let changeQueue = Promise.resolve();
110
124
  fs.watch(inputDir, {
111
125
  recursive: true
112
126
  }, (_event, filename)=>{
113
- handleInputChange(filename).catch((e)=>console.error(e));
127
+ changeQueue = changeQueue.then(()=>handleInputChange(filename)).catch((e)=>console.error(e));
114
128
  });
115
129
  }
130
+ function compileAll(shaderCompiler, inputDir, outputDir, platformTarget, clean) {
131
+ const shaderFiles = findFiles(inputDir, ".shader");
132
+ console.log(`[shader-compiler-bundler] Precompiling ${shaderFiles.length} shader(s)...`);
133
+ let failed = 0;
134
+ for (const file of shaderFiles){
135
+ if (!compileSingle(shaderCompiler, file, inputDir, outputDir, platformTarget)) failed++;
136
+ }
137
+ if (clean) {
138
+ const removed = cleanOrphanedBundles(shaderFiles, inputDir, outputDir);
139
+ if (removed > 0) console.log(`[shader-compiler-bundler] Cleaned ${removed} orphaned .shaderc file(s).`);
140
+ }
141
+ if (failed > 0) console.warn(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
142
+ return failed;
143
+ }
116
144
  async function loadShaderCompiler() {
117
- // @ts-ignore — `../dist/main.js` is the compiled runtime entry; no .ts source.
118
- // Bundler ships at `<pkg>/bundler/` and the runtime at `<pkg>/dist/`, so this
119
- // resolves to the sibling `dist/` from `bundler/cli.js` at runtime.
120
- const mod = await import('../dist/main.js');
121
- const instance = new mod.ShaderCompiler();
122
- if (typeof instance._precompile !== "function") {
123
- throw new Error("ShaderCompiler._precompile is not available; rebuild @galacean/engine-shader-compiler first.");
145
+ const mod = await (process.env.GALACEAN_SHADER_COMPILER_BOOTSTRAP === "true" ? importBootstrapPrecompiler() : importReleasePrecompiler());
146
+ const instance = new mod.ShaderPrecompiler();
147
+ if (typeof instance.precompile !== "function") {
148
+ throw new Error("ShaderPrecompiler is unavailable; rebuild @galacean/engine-shader-compiler first.");
124
149
  }
125
150
  return instance;
126
151
  }
152
+ async function importBootstrapPrecompiler() {
153
+ // @ts-expect-error generated repo-local runtime has no declaration file
154
+ return import('../.bootstrap/main.js');
155
+ }
156
+ async function importReleasePrecompiler() {
157
+ // @ts-expect-error generated Node-only runtime has no declaration file
158
+ return import('../dist/precompile.main.js');
159
+ }
127
160
  // Local .glsl come from inputDir; standard library comes from
128
161
  // `@galacean/engine-shader/sources` with a sibling-dir fallback for engine self-build cold-start.
129
162
  async function collectIncludeMap(inputDir) {
130
- const map = {};
163
+ const map = Object.create(null);
131
164
  for (const file of findFiles(inputDir, ".glsl")){
132
165
  const relPath = normalizePath(path.relative(inputDir, file));
133
- map[relPath] = fs.readFileSync(file, "utf-8");
166
+ addIncludeSource(map, relPath, fs.readFileSync(file, "utf-8"));
134
167
  }
135
- let usedRelease = false;
136
168
  // Resolve from inputDir's node_modules so consumers without transitive linkage to shader-compiler can still find it.
169
+ const requireFromInput = createRequire(path.join(inputDir, "package.json"));
170
+ let sourcesPath;
137
171
  try {
138
- const requireFromInput = createRequire(path.join(inputDir, "package.json"));
139
- const sourcesPath = requireFromInput.resolve("@galacean/engine-shader/sources");
140
- const { shaderLibrary } = await import(pathToFileURL(sourcesPath).href);
172
+ sourcesPath = requireFromInput.resolve("@galacean/engine-shader/sources");
173
+ } catch (error) {
174
+ if (error.code !== "MODULE_NOT_FOUND") throw error;
175
+ }
176
+ if (sourcesPath) {
177
+ const { shaderLibrary } = requireFromInput(sourcesPath);
141
178
  for (const { source, path: chunkPath } of shaderLibrary){
142
- map[chunkPath] = source;
179
+ addIncludeSource(map, chunkPath, source);
143
180
  }
144
- usedRelease = true;
145
- } catch {
146
- // engine self-build cold-start: dist/sources.* not yet built; use sibling source.
147
- }
148
- if (!usedRelease) {
181
+ } else {
182
+ // Engine self-build cold-start has no generated sources entry yet.
149
183
  const siblingLibrary = path.join(path.dirname(inputDir), "ShaderLibrary");
150
184
  if (fs.existsSync(siblingLibrary)) {
151
185
  for (const file of findFiles(siblingLibrary, ".glsl")){
152
186
  const rel = normalizePath(path.relative(siblingLibrary, file));
153
- map[`ShaderLibrary/${rel}`] = fs.readFileSync(file, "utf-8");
187
+ addIncludeSource(map, `ShaderLibrary/${rel}`, fs.readFileSync(file, "utf-8"));
154
188
  }
155
189
  }
156
190
  }
157
191
  return map;
158
192
  }
159
- // Mirrors `ShaderPass._shaderRootPath` in engine; inlined to keep bundler engine-free.
160
- const SHADER_ROOT_PATH = "shaders://root/";
193
+ function addIncludeSource(map, key, source) {
194
+ if (Object.prototype.hasOwnProperty.call(map, key)) {
195
+ throw new Error(`Shader include "${key}" is registered more than once.`);
196
+ }
197
+ map[key] = source;
198
+ }
161
199
  function compileSingle(shaderCompiler, shaderPath, inputDir, outputDir, platformTarget) {
200
+ const relativePath = shaderSourceRelativePath(shaderPath, inputDir);
162
201
  const source = fs.readFileSync(shaderPath, "utf-8");
163
- const relativePath = normalizePath(path.relative(inputDir, shaderPath));
164
202
  const bundleRelative = relativePath.replace(/\.shader$/, ".shaderc");
165
203
  const bundlePath = path.join(outputDir, bundleRelative);
166
- const basePathForIncludeKey = new URL(relativePath, SHADER_ROOT_PATH).href;
167
204
  fs.mkdirSync(path.dirname(bundlePath), {
168
205
  recursive: true
169
206
  });
170
207
  try {
171
- const precompiled = shaderCompiler._precompile(source, platformTarget, basePathForIncludeKey);
208
+ const precompiled = shaderCompiler.precompile(source, platformTarget, relativePath);
172
209
  fs.writeFileSync(bundlePath, JSON.stringify(precompiled));
173
210
  console.log(` ${relativePath} -> ${bundleRelative}`);
174
211
  return true;
@@ -178,6 +215,13 @@ function compileSingle(shaderCompiler, shaderPath, inputDir, outputDir, platform
178
215
  return false;
179
216
  }
180
217
  }
218
+ function shaderSourceRelativePath(shaderPath, inputDir) {
219
+ const relativePath = path.relative(inputDir, shaderPath);
220
+ if (relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath) || path.extname(relativePath) !== ".shader") {
221
+ throw new Error(`Shader source must be a .shader file inside the input directory: ${shaderPath}`);
222
+ }
223
+ return normalizePath(relativePath);
224
+ }
181
225
  function cleanOrphanedBundles(shaderFiles, inputDir, outputDir) {
182
226
  const aliveSet = new Set(shaderFiles.map((f)=>normalizePath(path.relative(inputDir, f)).replace(/\.shader$/, "")));
183
227
  const bundleFiles = findFiles(outputDir, ".shaderc");
@@ -202,6 +246,10 @@ function emitIndex(outputDir) {
202
246
  importPath: `./${rel}`
203
247
  };
204
248
  }).sort((a, b)=>a.varName.localeCompare(b.varName));
249
+ assertUniqueIdentifiers(entries.map(({ varName, importPath })=>({
250
+ identifier: varName,
251
+ path: importPath
252
+ })));
205
253
  const imports = entries.map((e)=>`import ${e.varName}Data from "${e.importPath}";`).join("\n");
206
254
  const exports = entries.map((e)=>`export const ${e.varName}: IPrecompiledShader = ${e.varName}Data;`).join("\n");
207
255
  const content = `// Auto-generated by shader-compiler-precompile --emit-index — do not edit.\nimport type { IPrecompiledShader } from "@galacean/engine-design";\n${imports}\n\n${exports}\n`;
@@ -228,6 +276,10 @@ function emitShaderSourceIndex(rootDir) {
228
276
  const importPaths = files.map((f)=>normalizePath(path.relative(rootDir, f)));
229
277
  const paths = files.map((f)=>normalizePath(path.relative(srcDir, f)));
230
278
  const ids = importPaths.map((p)=>pathToIdentifier(p, ".shader"));
279
+ assertUniqueIdentifiers(ids.map((identifier, index)=>({
280
+ identifier,
281
+ path: importPaths[index]
282
+ })));
231
283
  const lines = [];
232
284
  lines.push("// Auto-generated by shader-compiler-precompile --emit-sources — do not edit.");
233
285
  lines.push("");
@@ -253,6 +305,10 @@ function emitLibrarySourceIndex(rootDir) {
253
305
  const importPaths = files.map((f)=>normalizePath(path.relative(rootDir, f)));
254
306
  const paths = files.map((f)=>normalizePath(path.relative(srcDir, f)));
255
307
  const ids = importPaths.map((p)=>pathToIdentifier(p, ".glsl"));
308
+ assertUniqueIdentifiers(ids.map((identifier, index)=>({
309
+ identifier,
310
+ path: importPaths[index]
311
+ })));
256
312
  const lines = [];
257
313
  lines.push("// Auto-generated by shader-compiler-precompile --emit-sources — do not edit.");
258
314
  lines.push("");
@@ -268,6 +324,16 @@ function emitLibrarySourceIndex(rootDir) {
268
324
  fs.writeFileSync(out, lines.join("\n"), "utf-8");
269
325
  console.log(` Emitted library index ${path.relative(process.cwd(), out)} (${files.length})`);
270
326
  }
327
+ function assertUniqueIdentifiers(entries) {
328
+ const pathsByIdentifier = new Map();
329
+ for (const { identifier, path: sourcePath } of entries){
330
+ const existingPath = pathsByIdentifier.get(identifier);
331
+ if (existingPath !== undefined) {
332
+ throw new Error(`Shader asset paths "${existingPath}" and "${sourcePath}" both emit identifier "${identifier}".`);
333
+ }
334
+ pathsByIdentifier.set(identifier, sourcePath);
335
+ }
336
+ }
271
337
  function removeBundleFor(shaderPath, inputDir, outputDir) {
272
338
  const rel = normalizePath(path.relative(inputDir, shaderPath)).replace(/\.shader$/, ".shaderc");
273
339
  const bundlePath = path.join(outputDir, rel);
@@ -306,6 +372,9 @@ function removeBundleFor(shaderPath, inputDir, outputDir) {
306
372
  * - When `precompile` option is set: runs a full precompile in buildStart and,
307
373
  * in watch mode, starts a background watcher that mirrors `.shader` source
308
374
  * changes into `.shaderc` outputs and refreshes the aggregated index.
375
+ * @param options - Transform filter and optional offline precompile configuration.
376
+ * @returns Rollup plugin for shader source and artifact modules.
377
+ * @throws Error during `buildStart` when offline precompilation reports failures.
309
378
  */ function shaderCompiler(options = {}) {
310
379
  const { filter, precompile } = options;
311
380
  const precompileOptions = precompile && {
@@ -320,7 +389,8 @@ function removeBundleFor(shaderPath, inputDir, outputDir) {
320
389
  async buildStart () {
321
390
  if (!precompileOptions || initialDone) return;
322
391
  initialDone = true;
323
- await runFull(precompileOptions);
392
+ const { failed } = await runFull(precompileOptions);
393
+ if (failed > 0) this.error(`${failed} shader(s) failed to precompile.`);
324
394
  if (this.meta.watchMode && !watcherStarted) {
325
395
  watcherStarted = true;
326
396
  startWatcher(precompileOptions).catch((e)=>this.warn(`[shader-compiler] watcher failed: ${e.message}`));