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

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,8 +70,12 @@ var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
67
70
  dir = path__default["default"].dirname(dir);
68
71
  }
69
72
  }
70
- // One-shot mode exits non-zero on failure so CI breaks; watch mode logs and keeps running.
71
- async function precompile(options) {
73
+ /**
74
+ * Precompiles shaders once or starts incremental watch mode.
75
+ * @param options - Source, output, cleanup, watch, and artifact-generation options.
76
+ * @returns After a one-shot compile finishes or a watch has been installed.
77
+ * @throws Error when a one-shot compile has failures or the source tree is invalid.
78
+ */ async function precompile(options) {
72
79
  const { failed } = await runFull(options);
73
80
  if (options.watch) {
74
81
  await startWatcher(options);
@@ -76,10 +83,15 @@ async function precompile(options) {
76
83
  }
77
84
  console.log("[shader-compiler-bundler] Done.");
78
85
  if (failed > 0) {
79
- process.exit(1);
86
+ throw new Error(`${failed} shader(s) failed to precompile.`);
80
87
  }
81
88
  }
82
- async function runFull(options) {
89
+ /**
90
+ * Precompiles the current shader source tree once.
91
+ * @param options - Source, output, cleanup, and artifact-generation options.
92
+ * @returns An object containing the number of shader sources that failed to compile.
93
+ * @throws Error when the input tree or an explicitly selected source is invalid.
94
+ */ async function runFull(options) {
83
95
  const inputDir = path__default["default"].resolve(options.input);
84
96
  const outputDir = path__default["default"].resolve(options.output);
85
97
  const platformTarget = options.platformTarget ?? 0;
@@ -94,22 +106,13 @@ async function runFull(options) {
94
106
  emitSources(inputDir);
95
107
  }
96
108
  const shaderCompiler = await loadShaderCompiler();
97
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
109
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
98
110
  let failed = 0;
99
111
  if (options.only) {
100
112
  const target = path__default["default"].resolve(options.only);
101
113
  if (!compileSingle(shaderCompiler, target, inputDir, outputDir, platformTarget)) failed++;
102
114
  } else {
103
- const shaderFiles = findFiles(inputDir, ".shader");
104
- console.log(`[shader-compiler-bundler] Precompiling ${shaderFiles.length} shader(s)...`);
105
- for (const file of shaderFiles){
106
- if (!compileSingle(shaderCompiler, file, inputDir, outputDir, platformTarget)) failed++;
107
- }
108
- if (options.clean) {
109
- const removed = cleanOrphanedBundles(shaderFiles, inputDir, outputDir);
110
- if (removed > 0) console.log(`[shader-compiler-bundler] Cleaned ${removed} orphaned .shaderc file(s).`);
111
- }
112
- if (failed > 0) console.warn(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
115
+ failed = compileAll(shaderCompiler, inputDir, outputDir, platformTarget, options.clean);
113
116
  }
114
117
  if (options.emitIndex) {
115
118
  emitIndex(outputDir);
@@ -118,12 +121,23 @@ async function runFull(options) {
118
121
  failed
119
122
  };
120
123
  }
121
- async function startWatcher(options) {
124
+ /**
125
+ * Watches one shader source tree and serializes incremental rebuilds.
126
+ * @param options - Source, output, cleanup, and artifact-generation options.
127
+ * @returns Once the filesystem watcher has been installed.
128
+ * @throws Error when the initial include registry cannot be loaded.
129
+ */ async function startWatcher(options) {
122
130
  const inputDir = path__default["default"].resolve(options.input);
123
131
  const outputDir = path__default["default"].resolve(options.output);
124
132
  const platformTarget = options.platformTarget ?? 0;
133
+ if (!fs__default["default"].existsSync(inputDir)) {
134
+ throw new Error(`Input directory does not exist: ${inputDir}`);
135
+ }
136
+ fs__default["default"].mkdirSync(outputDir, {
137
+ recursive: true
138
+ });
125
139
  const shaderCompiler = await loadShaderCompiler();
126
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
140
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
127
141
  // .glsl change → full recompile (can't cheaply track include graphs).
128
142
  // .shader change → single-file recompile.
129
143
  const handleInputChange = async (filename)=>{
@@ -132,8 +146,11 @@ async function startWatcher(options) {
132
146
  const fullPath = path__default["default"].join(inputDir, filename);
133
147
  if (norm.endsWith(".glsl")) {
134
148
  console.log(`[shader-compiler-bundler] Include changed (${norm}), full recompile...`);
135
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
136
- runFull(options).catch((e)=>console.error(e));
149
+ if (options.emitSources) emitSources(inputDir);
150
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
151
+ const failed = compileAll(shaderCompiler, inputDir, outputDir, platformTarget, options.clean);
152
+ if (options.emitIndex) emitIndex(outputDir);
153
+ if (failed > 0) console.error(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
137
154
  return;
138
155
  }
139
156
  if (!norm.endsWith(".shader")) return;
@@ -146,68 +163,92 @@ async function startWatcher(options) {
146
163
  if (options.emitIndex) emitIndex(outputDir);
147
164
  };
148
165
  console.log(`[shader-compiler-bundler] Watching ${inputDir} ...`);
166
+ let changeQueue = Promise.resolve();
149
167
  fs__default["default"].watch(inputDir, {
150
168
  recursive: true
151
169
  }, (_event, filename)=>{
152
- handleInputChange(filename).catch((e)=>console.error(e));
170
+ changeQueue = changeQueue.then(()=>handleInputChange(filename)).catch((e)=>console.error(e));
153
171
  });
154
172
  }
173
+ function compileAll(shaderCompiler, inputDir, outputDir, platformTarget, clean) {
174
+ const shaderFiles = findFiles(inputDir, ".shader");
175
+ console.log(`[shader-compiler-bundler] Precompiling ${shaderFiles.length} shader(s)...`);
176
+ let failed = 0;
177
+ for (const file of shaderFiles){
178
+ if (!compileSingle(shaderCompiler, file, inputDir, outputDir, platformTarget)) failed++;
179
+ }
180
+ if (clean) {
181
+ const removed = cleanOrphanedBundles(shaderFiles, inputDir, outputDir);
182
+ if (removed > 0) console.log(`[shader-compiler-bundler] Cleaned ${removed} orphaned .shaderc file(s).`);
183
+ }
184
+ if (failed > 0) console.warn(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
185
+ return failed;
186
+ }
155
187
  async function loadShaderCompiler() {
156
- // @ts-ignore — `../dist/main.js` is the compiled runtime entry; no .ts source.
157
- // Bundler ships at `<pkg>/bundler/` and the runtime at `<pkg>/dist/`, so this
158
- // resolves to the sibling `dist/` from `bundler/cli.js` at runtime.
159
- const mod = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('../dist/main.js')); });
160
- const instance = new mod.ShaderCompiler();
161
- if (typeof instance._precompile !== "function") {
162
- throw new Error("ShaderCompiler._precompile is not available; rebuild @galacean/engine-shader-compiler first.");
188
+ const mod = await (process.env.GALACEAN_SHADER_COMPILER_BOOTSTRAP === "true" ? importBootstrapPrecompiler() : importReleasePrecompiler());
189
+ const instance = new mod.ShaderPrecompiler();
190
+ if (typeof instance.precompile !== "function") {
191
+ throw new Error("ShaderPrecompiler is unavailable; rebuild @galacean/engine-shader-compiler first.");
163
192
  }
164
193
  return instance;
165
194
  }
195
+ async function importBootstrapPrecompiler() {
196
+ // @ts-expect-error generated repo-local runtime has no declaration file
197
+ return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('../.bootstrap/main.js')); });
198
+ }
199
+ async function importReleasePrecompiler() {
200
+ // @ts-expect-error generated Node-only runtime has no declaration file
201
+ return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('../dist/precompile.main.js')); });
202
+ }
166
203
  // Local .glsl come from inputDir; standard library comes from
167
204
  // `@galacean/engine-shader/sources` with a sibling-dir fallback for engine self-build cold-start.
168
205
  async function collectIncludeMap(inputDir) {
169
- const map = {};
206
+ const map = Object.create(null);
170
207
  for (const file of findFiles(inputDir, ".glsl")){
171
208
  const relPath = normalizePath(path__default["default"].relative(inputDir, file));
172
- map[relPath] = fs__default["default"].readFileSync(file, "utf-8");
209
+ addIncludeSource(map, relPath, fs__default["default"].readFileSync(file, "utf-8"));
173
210
  }
174
- let usedRelease = false;
175
211
  // Resolve from inputDir's node_modules so consumers without transitive linkage to shader-compiler can still find it.
212
+ const requireFromInput = node_module.createRequire(path__default["default"].join(inputDir, "package.json"));
213
+ let sourcesPath;
176
214
  try {
177
- const requireFromInput = node_module.createRequire(path__default["default"].join(inputDir, "package.json"));
178
- const sourcesPath = requireFromInput.resolve("@galacean/engine-shader/sources");
179
- const { shaderLibrary } = await (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })(node_url.pathToFileURL(sourcesPath).href);
215
+ sourcesPath = requireFromInput.resolve("@galacean/engine-shader/sources");
216
+ } catch (error) {
217
+ if (error.code !== "MODULE_NOT_FOUND") throw error;
218
+ }
219
+ if (sourcesPath) {
220
+ const { shaderLibrary } = requireFromInput(sourcesPath);
180
221
  for (const { source, path: chunkPath } of shaderLibrary){
181
- map[chunkPath] = source;
222
+ addIncludeSource(map, chunkPath, source);
182
223
  }
183
- usedRelease = true;
184
- } catch {
185
- // engine self-build cold-start: dist/sources.* not yet built; use sibling source.
186
- }
187
- if (!usedRelease) {
224
+ } else {
225
+ // Engine self-build cold-start has no generated sources entry yet.
188
226
  const siblingLibrary = path__default["default"].join(path__default["default"].dirname(inputDir), "ShaderLibrary");
189
227
  if (fs__default["default"].existsSync(siblingLibrary)) {
190
228
  for (const file of findFiles(siblingLibrary, ".glsl")){
191
229
  const rel = normalizePath(path__default["default"].relative(siblingLibrary, file));
192
- map[`ShaderLibrary/${rel}`] = fs__default["default"].readFileSync(file, "utf-8");
230
+ addIncludeSource(map, `ShaderLibrary/${rel}`, fs__default["default"].readFileSync(file, "utf-8"));
193
231
  }
194
232
  }
195
233
  }
196
234
  return map;
197
235
  }
198
- // Mirrors `ShaderPass._shaderRootPath` in engine; inlined to keep bundler engine-free.
199
- const SHADER_ROOT_PATH = "shaders://root/";
236
+ function addIncludeSource(map, key, source) {
237
+ if (Object.prototype.hasOwnProperty.call(map, key)) {
238
+ throw new Error(`Shader include "${key}" is registered more than once.`);
239
+ }
240
+ map[key] = source;
241
+ }
200
242
  function compileSingle(shaderCompiler, shaderPath, inputDir, outputDir, platformTarget) {
243
+ const relativePath = shaderSourceRelativePath(shaderPath, inputDir);
201
244
  const source = fs__default["default"].readFileSync(shaderPath, "utf-8");
202
- const relativePath = normalizePath(path__default["default"].relative(inputDir, shaderPath));
203
245
  const bundleRelative = relativePath.replace(/\.shader$/, ".shaderc");
204
246
  const bundlePath = path__default["default"].join(outputDir, bundleRelative);
205
- const basePathForIncludeKey = new URL(relativePath, SHADER_ROOT_PATH).href;
206
247
  fs__default["default"].mkdirSync(path__default["default"].dirname(bundlePath), {
207
248
  recursive: true
208
249
  });
209
250
  try {
210
- const precompiled = shaderCompiler._precompile(source, platformTarget, basePathForIncludeKey);
251
+ const precompiled = shaderCompiler.precompile(source, platformTarget, relativePath);
211
252
  fs__default["default"].writeFileSync(bundlePath, JSON.stringify(precompiled));
212
253
  console.log(` ${relativePath} -> ${bundleRelative}`);
213
254
  return true;
@@ -217,6 +258,13 @@ function compileSingle(shaderCompiler, shaderPath, inputDir, outputDir, platform
217
258
  return false;
218
259
  }
219
260
  }
261
+ function shaderSourceRelativePath(shaderPath, inputDir) {
262
+ const relativePath = path__default["default"].relative(inputDir, shaderPath);
263
+ if (relativePath === ".." || relativePath.startsWith(`..${path__default["default"].sep}`) || path__default["default"].isAbsolute(relativePath) || path__default["default"].extname(relativePath) !== ".shader") {
264
+ throw new Error(`Shader source must be a .shader file inside the input directory: ${shaderPath}`);
265
+ }
266
+ return normalizePath(relativePath);
267
+ }
220
268
  function cleanOrphanedBundles(shaderFiles, inputDir, outputDir) {
221
269
  const aliveSet = new Set(shaderFiles.map((f)=>normalizePath(path__default["default"].relative(inputDir, f)).replace(/\.shader$/, "")));
222
270
  const bundleFiles = findFiles(outputDir, ".shaderc");
@@ -241,6 +289,10 @@ function emitIndex(outputDir) {
241
289
  importPath: `./${rel}`
242
290
  };
243
291
  }).sort((a, b)=>a.varName.localeCompare(b.varName));
292
+ assertUniqueIdentifiers(entries.map(({ varName, importPath })=>({
293
+ identifier: varName,
294
+ path: importPath
295
+ })));
244
296
  const imports = entries.map((e)=>`import ${e.varName}Data from "${e.importPath}";`).join("\n");
245
297
  const exports = entries.map((e)=>`export const ${e.varName}: IPrecompiledShader = ${e.varName}Data;`).join("\n");
246
298
  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`;
@@ -267,6 +319,10 @@ function emitShaderSourceIndex(rootDir) {
267
319
  const importPaths = files.map((f)=>normalizePath(path__default["default"].relative(rootDir, f)));
268
320
  const paths = files.map((f)=>normalizePath(path__default["default"].relative(srcDir, f)));
269
321
  const ids = importPaths.map((p)=>pathToIdentifier(p, ".shader"));
322
+ assertUniqueIdentifiers(ids.map((identifier, index)=>({
323
+ identifier,
324
+ path: importPaths[index]
325
+ })));
270
326
  const lines = [];
271
327
  lines.push("// Auto-generated by shader-compiler-precompile --emit-sources — do not edit.");
272
328
  lines.push("");
@@ -292,6 +348,10 @@ function emitLibrarySourceIndex(rootDir) {
292
348
  const importPaths = files.map((f)=>normalizePath(path__default["default"].relative(rootDir, f)));
293
349
  const paths = files.map((f)=>normalizePath(path__default["default"].relative(srcDir, f)));
294
350
  const ids = importPaths.map((p)=>pathToIdentifier(p, ".glsl"));
351
+ assertUniqueIdentifiers(ids.map((identifier, index)=>({
352
+ identifier,
353
+ path: importPaths[index]
354
+ })));
295
355
  const lines = [];
296
356
  lines.push("// Auto-generated by shader-compiler-precompile --emit-sources — do not edit.");
297
357
  lines.push("");
@@ -307,6 +367,16 @@ function emitLibrarySourceIndex(rootDir) {
307
367
  fs__default["default"].writeFileSync(out, lines.join("\n"), "utf-8");
308
368
  console.log(` Emitted library index ${path__default["default"].relative(process.cwd(), out)} (${files.length})`);
309
369
  }
370
+ function assertUniqueIdentifiers(entries) {
371
+ const pathsByIdentifier = new Map();
372
+ for (const { identifier, path: sourcePath } of entries){
373
+ const existingPath = pathsByIdentifier.get(identifier);
374
+ if (existingPath !== undefined) {
375
+ throw new Error(`Shader asset paths "${existingPath}" and "${sourcePath}" both emit identifier "${identifier}".`);
376
+ }
377
+ pathsByIdentifier.set(identifier, sourcePath);
378
+ }
379
+ }
310
380
  function removeBundleFor(shaderPath, inputDir, outputDir) {
311
381
  const rel = normalizePath(path__default["default"].relative(inputDir, shaderPath)).replace(/\.shader$/, ".shaderc");
312
382
  const bundlePath = path__default["default"].join(outputDir, rel);
@@ -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,8 +43,12 @@ import { pathToFileURL } from 'node:url';
40
43
  dir = path.dirname(dir);
41
44
  }
42
45
  }
43
- // One-shot mode exits non-zero on failure so CI breaks; watch mode logs and keeps running.
44
- async function precompile(options) {
46
+ /**
47
+ * Precompiles shaders once or starts incremental watch mode.
48
+ * @param options - Source, output, cleanup, watch, and artifact-generation options.
49
+ * @returns After a one-shot compile finishes or a watch has been installed.
50
+ * @throws Error when a one-shot compile has failures or the source tree is invalid.
51
+ */ async function precompile(options) {
45
52
  const { failed } = await runFull(options);
46
53
  if (options.watch) {
47
54
  await startWatcher(options);
@@ -49,10 +56,15 @@ async function precompile(options) {
49
56
  }
50
57
  console.log("[shader-compiler-bundler] Done.");
51
58
  if (failed > 0) {
52
- process.exit(1);
59
+ throw new Error(`${failed} shader(s) failed to precompile.`);
53
60
  }
54
61
  }
55
- async function runFull(options) {
62
+ /**
63
+ * Precompiles the current shader source tree once.
64
+ * @param options - Source, output, cleanup, and artifact-generation options.
65
+ * @returns An object containing the number of shader sources that failed to compile.
66
+ * @throws Error when the input tree or an explicitly selected source is invalid.
67
+ */ async function runFull(options) {
56
68
  const inputDir = path.resolve(options.input);
57
69
  const outputDir = path.resolve(options.output);
58
70
  const platformTarget = options.platformTarget ?? 0;
@@ -67,22 +79,13 @@ async function runFull(options) {
67
79
  emitSources(inputDir);
68
80
  }
69
81
  const shaderCompiler = await loadShaderCompiler();
70
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
82
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
71
83
  let failed = 0;
72
84
  if (options.only) {
73
85
  const target = path.resolve(options.only);
74
86
  if (!compileSingle(shaderCompiler, target, inputDir, outputDir, platformTarget)) failed++;
75
87
  } else {
76
- const shaderFiles = findFiles(inputDir, ".shader");
77
- console.log(`[shader-compiler-bundler] Precompiling ${shaderFiles.length} shader(s)...`);
78
- for (const file of shaderFiles){
79
- if (!compileSingle(shaderCompiler, file, inputDir, outputDir, platformTarget)) failed++;
80
- }
81
- if (options.clean) {
82
- const removed = cleanOrphanedBundles(shaderFiles, inputDir, outputDir);
83
- if (removed > 0) console.log(`[shader-compiler-bundler] Cleaned ${removed} orphaned .shaderc file(s).`);
84
- }
85
- if (failed > 0) console.warn(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
88
+ failed = compileAll(shaderCompiler, inputDir, outputDir, platformTarget, options.clean);
86
89
  }
87
90
  if (options.emitIndex) {
88
91
  emitIndex(outputDir);
@@ -91,12 +94,23 @@ async function runFull(options) {
91
94
  failed
92
95
  };
93
96
  }
94
- async function startWatcher(options) {
97
+ /**
98
+ * Watches one shader source tree and serializes incremental rebuilds.
99
+ * @param options - Source, output, cleanup, and artifact-generation options.
100
+ * @returns Once the filesystem watcher has been installed.
101
+ * @throws Error when the initial include registry cannot be loaded.
102
+ */ async function startWatcher(options) {
95
103
  const inputDir = path.resolve(options.input);
96
104
  const outputDir = path.resolve(options.output);
97
105
  const platformTarget = options.platformTarget ?? 0;
106
+ if (!fs.existsSync(inputDir)) {
107
+ throw new Error(`Input directory does not exist: ${inputDir}`);
108
+ }
109
+ fs.mkdirSync(outputDir, {
110
+ recursive: true
111
+ });
98
112
  const shaderCompiler = await loadShaderCompiler();
99
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
113
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
100
114
  // .glsl change → full recompile (can't cheaply track include graphs).
101
115
  // .shader change → single-file recompile.
102
116
  const handleInputChange = async (filename)=>{
@@ -105,8 +119,11 @@ async function startWatcher(options) {
105
119
  const fullPath = path.join(inputDir, filename);
106
120
  if (norm.endsWith(".glsl")) {
107
121
  console.log(`[shader-compiler-bundler] Include changed (${norm}), full recompile...`);
108
- shaderCompiler._setIncludeMap(await collectIncludeMap(inputDir));
109
- runFull(options).catch((e)=>console.error(e));
122
+ if (options.emitSources) emitSources(inputDir);
123
+ shaderCompiler.setIncludeMap(await collectIncludeMap(inputDir));
124
+ const failed = compileAll(shaderCompiler, inputDir, outputDir, platformTarget, options.clean);
125
+ if (options.emitIndex) emitIndex(outputDir);
126
+ if (failed > 0) console.error(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
110
127
  return;
111
128
  }
112
129
  if (!norm.endsWith(".shader")) return;
@@ -119,68 +136,92 @@ async function startWatcher(options) {
119
136
  if (options.emitIndex) emitIndex(outputDir);
120
137
  };
121
138
  console.log(`[shader-compiler-bundler] Watching ${inputDir} ...`);
139
+ let changeQueue = Promise.resolve();
122
140
  fs.watch(inputDir, {
123
141
  recursive: true
124
142
  }, (_event, filename)=>{
125
- handleInputChange(filename).catch((e)=>console.error(e));
143
+ changeQueue = changeQueue.then(()=>handleInputChange(filename)).catch((e)=>console.error(e));
126
144
  });
127
145
  }
146
+ function compileAll(shaderCompiler, inputDir, outputDir, platformTarget, clean) {
147
+ const shaderFiles = findFiles(inputDir, ".shader");
148
+ console.log(`[shader-compiler-bundler] Precompiling ${shaderFiles.length} shader(s)...`);
149
+ let failed = 0;
150
+ for (const file of shaderFiles){
151
+ if (!compileSingle(shaderCompiler, file, inputDir, outputDir, platformTarget)) failed++;
152
+ }
153
+ if (clean) {
154
+ const removed = cleanOrphanedBundles(shaderFiles, inputDir, outputDir);
155
+ if (removed > 0) console.log(`[shader-compiler-bundler] Cleaned ${removed} orphaned .shaderc file(s).`);
156
+ }
157
+ if (failed > 0) console.warn(`[shader-compiler-bundler] ${failed} shader(s) failed to precompile.`);
158
+ return failed;
159
+ }
128
160
  async function loadShaderCompiler() {
129
- // @ts-ignore — `../dist/main.js` is the compiled runtime entry; no .ts source.
130
- // Bundler ships at `<pkg>/bundler/` and the runtime at `<pkg>/dist/`, so this
131
- // resolves to the sibling `dist/` from `bundler/cli.js` at runtime.
132
- const mod = await import('../dist/main.js');
133
- const instance = new mod.ShaderCompiler();
134
- if (typeof instance._precompile !== "function") {
135
- throw new Error("ShaderCompiler._precompile is not available; rebuild @galacean/engine-shader-compiler first.");
161
+ const mod = await (process.env.GALACEAN_SHADER_COMPILER_BOOTSTRAP === "true" ? importBootstrapPrecompiler() : importReleasePrecompiler());
162
+ const instance = new mod.ShaderPrecompiler();
163
+ if (typeof instance.precompile !== "function") {
164
+ throw new Error("ShaderPrecompiler is unavailable; rebuild @galacean/engine-shader-compiler first.");
136
165
  }
137
166
  return instance;
138
167
  }
168
+ async function importBootstrapPrecompiler() {
169
+ // @ts-expect-error generated repo-local runtime has no declaration file
170
+ return import('../.bootstrap/main.js');
171
+ }
172
+ async function importReleasePrecompiler() {
173
+ // @ts-expect-error generated Node-only runtime has no declaration file
174
+ return import('../dist/precompile.main.js');
175
+ }
139
176
  // Local .glsl come from inputDir; standard library comes from
140
177
  // `@galacean/engine-shader/sources` with a sibling-dir fallback for engine self-build cold-start.
141
178
  async function collectIncludeMap(inputDir) {
142
- const map = {};
179
+ const map = Object.create(null);
143
180
  for (const file of findFiles(inputDir, ".glsl")){
144
181
  const relPath = normalizePath(path.relative(inputDir, file));
145
- map[relPath] = fs.readFileSync(file, "utf-8");
182
+ addIncludeSource(map, relPath, fs.readFileSync(file, "utf-8"));
146
183
  }
147
- let usedRelease = false;
148
184
  // Resolve from inputDir's node_modules so consumers without transitive linkage to shader-compiler can still find it.
185
+ const requireFromInput = createRequire(path.join(inputDir, "package.json"));
186
+ let sourcesPath;
149
187
  try {
150
- const requireFromInput = createRequire(path.join(inputDir, "package.json"));
151
- const sourcesPath = requireFromInput.resolve("@galacean/engine-shader/sources");
152
- const { shaderLibrary } = await import(pathToFileURL(sourcesPath).href);
188
+ sourcesPath = requireFromInput.resolve("@galacean/engine-shader/sources");
189
+ } catch (error) {
190
+ if (error.code !== "MODULE_NOT_FOUND") throw error;
191
+ }
192
+ if (sourcesPath) {
193
+ const { shaderLibrary } = requireFromInput(sourcesPath);
153
194
  for (const { source, path: chunkPath } of shaderLibrary){
154
- map[chunkPath] = source;
195
+ addIncludeSource(map, chunkPath, source);
155
196
  }
156
- usedRelease = true;
157
- } catch {
158
- // engine self-build cold-start: dist/sources.* not yet built; use sibling source.
159
- }
160
- if (!usedRelease) {
197
+ } else {
198
+ // Engine self-build cold-start has no generated sources entry yet.
161
199
  const siblingLibrary = path.join(path.dirname(inputDir), "ShaderLibrary");
162
200
  if (fs.existsSync(siblingLibrary)) {
163
201
  for (const file of findFiles(siblingLibrary, ".glsl")){
164
202
  const rel = normalizePath(path.relative(siblingLibrary, file));
165
- map[`ShaderLibrary/${rel}`] = fs.readFileSync(file, "utf-8");
203
+ addIncludeSource(map, `ShaderLibrary/${rel}`, fs.readFileSync(file, "utf-8"));
166
204
  }
167
205
  }
168
206
  }
169
207
  return map;
170
208
  }
171
- // Mirrors `ShaderPass._shaderRootPath` in engine; inlined to keep bundler engine-free.
172
- const SHADER_ROOT_PATH = "shaders://root/";
209
+ function addIncludeSource(map, key, source) {
210
+ if (Object.prototype.hasOwnProperty.call(map, key)) {
211
+ throw new Error(`Shader include "${key}" is registered more than once.`);
212
+ }
213
+ map[key] = source;
214
+ }
173
215
  function compileSingle(shaderCompiler, shaderPath, inputDir, outputDir, platformTarget) {
216
+ const relativePath = shaderSourceRelativePath(shaderPath, inputDir);
174
217
  const source = fs.readFileSync(shaderPath, "utf-8");
175
- const relativePath = normalizePath(path.relative(inputDir, shaderPath));
176
218
  const bundleRelative = relativePath.replace(/\.shader$/, ".shaderc");
177
219
  const bundlePath = path.join(outputDir, bundleRelative);
178
- const basePathForIncludeKey = new URL(relativePath, SHADER_ROOT_PATH).href;
179
220
  fs.mkdirSync(path.dirname(bundlePath), {
180
221
  recursive: true
181
222
  });
182
223
  try {
183
- const precompiled = shaderCompiler._precompile(source, platformTarget, basePathForIncludeKey);
224
+ const precompiled = shaderCompiler.precompile(source, platformTarget, relativePath);
184
225
  fs.writeFileSync(bundlePath, JSON.stringify(precompiled));
185
226
  console.log(` ${relativePath} -> ${bundleRelative}`);
186
227
  return true;
@@ -190,6 +231,13 @@ function compileSingle(shaderCompiler, shaderPath, inputDir, outputDir, platform
190
231
  return false;
191
232
  }
192
233
  }
234
+ function shaderSourceRelativePath(shaderPath, inputDir) {
235
+ const relativePath = path.relative(inputDir, shaderPath);
236
+ if (relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath) || path.extname(relativePath) !== ".shader") {
237
+ throw new Error(`Shader source must be a .shader file inside the input directory: ${shaderPath}`);
238
+ }
239
+ return normalizePath(relativePath);
240
+ }
193
241
  function cleanOrphanedBundles(shaderFiles, inputDir, outputDir) {
194
242
  const aliveSet = new Set(shaderFiles.map((f)=>normalizePath(path.relative(inputDir, f)).replace(/\.shader$/, "")));
195
243
  const bundleFiles = findFiles(outputDir, ".shaderc");
@@ -214,6 +262,10 @@ function emitIndex(outputDir) {
214
262
  importPath: `./${rel}`
215
263
  };
216
264
  }).sort((a, b)=>a.varName.localeCompare(b.varName));
265
+ assertUniqueIdentifiers(entries.map(({ varName, importPath })=>({
266
+ identifier: varName,
267
+ path: importPath
268
+ })));
217
269
  const imports = entries.map((e)=>`import ${e.varName}Data from "${e.importPath}";`).join("\n");
218
270
  const exports = entries.map((e)=>`export const ${e.varName}: IPrecompiledShader = ${e.varName}Data;`).join("\n");
219
271
  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`;
@@ -240,6 +292,10 @@ function emitShaderSourceIndex(rootDir) {
240
292
  const importPaths = files.map((f)=>normalizePath(path.relative(rootDir, f)));
241
293
  const paths = files.map((f)=>normalizePath(path.relative(srcDir, f)));
242
294
  const ids = importPaths.map((p)=>pathToIdentifier(p, ".shader"));
295
+ assertUniqueIdentifiers(ids.map((identifier, index)=>({
296
+ identifier,
297
+ path: importPaths[index]
298
+ })));
243
299
  const lines = [];
244
300
  lines.push("// Auto-generated by shader-compiler-precompile --emit-sources — do not edit.");
245
301
  lines.push("");
@@ -265,6 +321,10 @@ function emitLibrarySourceIndex(rootDir) {
265
321
  const importPaths = files.map((f)=>normalizePath(path.relative(rootDir, f)));
266
322
  const paths = files.map((f)=>normalizePath(path.relative(srcDir, f)));
267
323
  const ids = importPaths.map((p)=>pathToIdentifier(p, ".glsl"));
324
+ assertUniqueIdentifiers(ids.map((identifier, index)=>({
325
+ identifier,
326
+ path: importPaths[index]
327
+ })));
268
328
  const lines = [];
269
329
  lines.push("// Auto-generated by shader-compiler-precompile --emit-sources — do not edit.");
270
330
  lines.push("");
@@ -280,6 +340,16 @@ function emitLibrarySourceIndex(rootDir) {
280
340
  fs.writeFileSync(out, lines.join("\n"), "utf-8");
281
341
  console.log(` Emitted library index ${path.relative(process.cwd(), out)} (${files.length})`);
282
342
  }
343
+ function assertUniqueIdentifiers(entries) {
344
+ const pathsByIdentifier = new Map();
345
+ for (const { identifier, path: sourcePath } of entries){
346
+ const existingPath = pathsByIdentifier.get(identifier);
347
+ if (existingPath !== undefined) {
348
+ throw new Error(`Shader asset paths "${existingPath}" and "${sourcePath}" both emit identifier "${identifier}".`);
349
+ }
350
+ pathsByIdentifier.set(identifier, sourcePath);
351
+ }
352
+ }
283
353
  function removeBundleFor(shaderPath, inputDir, outputDir) {
284
354
  const rel = normalizePath(path.relative(inputDir, shaderPath)).replace(/\.shader$/, ".shaderc");
285
355
  const bundlePath = path.join(outputDir, rel);