@forgeax/engine-codec 0.1.2

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 (122) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +268 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/basis-encode-fast-preset.unit.test.d.ts +2 -0
  5. package/dist/__tests__/basis-encode-fast-preset.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/basis-encode-lifecycle.unit.test.d.ts +2 -0
  7. package/dist/__tests__/basis-encode-lifecycle.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/basis-encode-mode-owner.test-d.d.ts +2 -0
  9. package/dist/__tests__/basis-encode-mode-owner.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/basis-transcoder.unit.test.d.ts +2 -0
  11. package/dist/__tests__/basis-transcoder.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/block-format.unit.test.d.ts +2 -0
  13. package/dist/__tests__/block-format.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/byte-identical-falsification.test.d.ts +2 -0
  15. package/dist/__tests__/byte-identical-falsification.test.d.ts.map +1 -0
  16. package/dist/__tests__/codec-errors.test-d.d.ts +2 -0
  17. package/dist/__tests__/codec-errors.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/codec-errors.unit.test.d.ts +2 -0
  19. package/dist/__tests__/codec-errors.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/compression-field.unit.test.d.ts +19 -0
  21. package/dist/__tests__/compression-field.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/encode-pixel-limit.unit.test.d.ts +2 -0
  23. package/dist/__tests__/encode-pixel-limit.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/encode-roundtrip.unit.test.d.ts +2 -0
  25. package/dist/__tests__/encode-roundtrip.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/ktx2-basis-capability-matrix.unit.test.d.ts +2 -0
  27. package/dist/__tests__/ktx2-basis-capability-matrix.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/ktx2-errors.test.d.ts +2 -0
  29. package/dist/__tests__/ktx2-errors.test.d.ts.map +1 -0
  30. package/dist/__tests__/ktx2-parse-scheme0.test.d.ts +2 -0
  31. package/dist/__tests__/ktx2-parse-scheme0.test.d.ts.map +1 -0
  32. package/dist/__tests__/ktx2-parse-scheme2.test.d.ts +2 -0
  33. package/dist/__tests__/ktx2-parse-scheme2.test.d.ts.map +1 -0
  34. package/dist/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.d.ts +2 -0
  35. package/dist/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/ktx2-to-texture.integration.test.d.ts +2 -0
  37. package/dist/__tests__/ktx2-to-texture.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/runtime-validation.unit.test.d.ts +2 -0
  39. package/dist/__tests__/runtime-validation.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/transcode-select-target.unit.test.d.ts +2 -0
  41. package/dist/__tests__/transcode-select-target.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/wasm-init-smoke.test.d.ts +2 -0
  43. package/dist/__tests__/wasm-init-smoke.test.d.ts.map +1 -0
  44. package/dist/__tests__/zstd-deterministic.unit.test.d.ts +2 -0
  45. package/dist/__tests__/zstd-deterministic.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/zstd-encode-singleton.unit.test.d.ts +2 -0
  47. package/dist/__tests__/zstd-encode-singleton.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/zstd-roundtrip.unit.test.d.ts +2 -0
  49. package/dist/__tests__/zstd-roundtrip.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/zstd-singleton.unit.test.d.ts +2 -0
  51. package/dist/__tests__/zstd-singleton.unit.test.d.ts.map +1 -0
  52. package/dist/basis-transcoder.d.ts +86 -0
  53. package/dist/basis-transcoder.d.ts.map +1 -0
  54. package/dist/block-format.d.ts +41 -0
  55. package/dist/block-format.d.ts.map +1 -0
  56. package/dist/encode/basis-encode.d.ts +68 -0
  57. package/dist/encode/basis-encode.d.ts.map +1 -0
  58. package/dist/encode/index.d.ts +10 -0
  59. package/dist/encode/index.d.ts.map +1 -0
  60. package/dist/encode/index.mjs +207 -0
  61. package/dist/encode/index.mjs.map +1 -0
  62. package/dist/encode-impl.d.ts +30 -0
  63. package/dist/encode-impl.d.ts.map +1 -0
  64. package/dist/errors.d.ts +61 -0
  65. package/dist/errors.d.ts.map +1 -0
  66. package/dist/index.d.ts +17 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.mjs +672 -0
  69. package/dist/index.mjs.map +1 -0
  70. package/dist/ktx2.d.ts +111 -0
  71. package/dist/ktx2.d.ts.map +1 -0
  72. package/dist/transcode.d.ts +55 -0
  73. package/dist/transcode.d.ts.map +1 -0
  74. package/dist/wasm/basis-types.d.ts +179 -0
  75. package/dist/wasm/basis-types.d.ts.map +1 -0
  76. package/dist/zstd.d.ts +28 -0
  77. package/dist/zstd.d.ts.map +1 -0
  78. package/package.json +71 -0
  79. package/pkg/basis_transcoder.mjs +2 -0
  80. package/pkg/basis_transcoder.wasm +0 -0
  81. package/pkg/encode/basis_encoder.mjs +2 -0
  82. package/pkg/encode/basis_encoder.wasm +0 -0
  83. package/scripts/build-wasm.mjs +370 -0
  84. package/scripts/content-key.mjs +85 -0
  85. package/scripts/ensure-wasm.mjs +38 -0
  86. package/scripts/fetch-basis.mjs +150 -0
  87. package/scripts/fetch-wasm.mjs +97 -0
  88. package/src/__tests__/basis-encode-fast-preset.unit.test.ts +204 -0
  89. package/src/__tests__/basis-encode-lifecycle.unit.test.ts +249 -0
  90. package/src/__tests__/basis-encode-mode-owner.test-d.ts +39 -0
  91. package/src/__tests__/basis-transcoder.unit.test.ts +502 -0
  92. package/src/__tests__/block-format.unit.test.ts +215 -0
  93. package/src/__tests__/byte-identical-falsification.test.ts +164 -0
  94. package/src/__tests__/codec-errors.test-d.ts +67 -0
  95. package/src/__tests__/codec-errors.unit.test.ts +128 -0
  96. package/src/__tests__/compression-field.unit.test.ts +245 -0
  97. package/src/__tests__/encode-pixel-limit.unit.test.ts +117 -0
  98. package/src/__tests__/encode-roundtrip.unit.test.ts +239 -0
  99. package/src/__tests__/ktx2-basis-capability-matrix.unit.test.ts +241 -0
  100. package/src/__tests__/ktx2-errors.test.ts +239 -0
  101. package/src/__tests__/ktx2-parse-scheme0.test.ts +263 -0
  102. package/src/__tests__/ktx2-parse-scheme2.test.ts +220 -0
  103. package/src/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.ts +178 -0
  104. package/src/__tests__/ktx2-to-texture.integration.test.ts +384 -0
  105. package/src/__tests__/runtime-validation.unit.test.ts +19 -0
  106. package/src/__tests__/transcode-select-target.unit.test.ts +167 -0
  107. package/src/__tests__/wasm-init-smoke.test.ts +102 -0
  108. package/src/__tests__/zstd-deterministic.unit.test.ts +49 -0
  109. package/src/__tests__/zstd-encode-singleton.unit.test.ts +63 -0
  110. package/src/__tests__/zstd-roundtrip.unit.test.ts +87 -0
  111. package/src/__tests__/zstd-singleton.unit.test.ts +105 -0
  112. package/src/basis-transcoder.ts +380 -0
  113. package/src/block-format.ts +128 -0
  114. package/src/encode/basis-encode.ts +264 -0
  115. package/src/encode/index.ts +19 -0
  116. package/src/encode-impl.ts +103 -0
  117. package/src/errors.ts +79 -0
  118. package/src/index.ts +38 -0
  119. package/src/ktx2.ts +469 -0
  120. package/src/transcode.ts +99 -0
  121. package/src/wasm/basis-types.ts +220 -0
  122. package/src/zstd.ts +85 -0
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env node
2
+ // build-wasm.mjs -- Compile the pinned basis_universal C++ source into two
3
+ // self-contained WASM modules via bare emcc (plan-strategy D-1, path B):
4
+ //
5
+ // 1. transcoder (slim) -> pkg/basis_transcoder.mjs + .wasm (runtime-safe)
6
+ // 2. encoder -> pkg/encode/basis_encoder.mjs + .wasm (build-time)
7
+ //
8
+ // Why bare emcc and not emcmake+cmake (D-1): CI has only the emsdk toolchain
9
+ // (emscripten-core/setup-emsdk@v16) and no cmake setup step; the fbx package
10
+ // already builds ufbx this way. The emcc flag set below is lifted verbatim from
11
+ // the upstream CMake build files and anchored to them in comments so a pin bump
12
+ // surfaces any flag drift at review time:
13
+ // transcoder: vendor/basis/webgl/transcoder/CMakeLists.txt
14
+ // encoder: vendor/basis/webgl/encoder/CMakeLists.txt
15
+ //
16
+ // Prerequisites:
17
+ // 1. emcc on PATH (emsdk 6.0.2; see .github/workflows/ci.yml build-artifacts)
18
+ // 2. node scripts/fetch-basis.mjs (vendors the pinned source)
19
+ //
20
+ // Usage: node packages/codec/scripts/build-wasm.mjs
21
+
22
+ import { execFileSync } from 'node:child_process';
23
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
24
+ import { dirname, join } from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const CODEC_ROOT = join(__dirname, '..');
29
+ const VENDOR = join(CODEC_ROOT, 'vendor', 'basis');
30
+ const PKG = join(CODEC_ROOT, 'pkg');
31
+ const PKG_ENCODE = join(PKG, 'encode');
32
+ const REPRODUCIBLE_PATH = `-ffile-prefix-map=${CODEC_ROOT}=/forgeax/codec`;
33
+
34
+ if (!existsSync(join(VENDOR, 'transcoder', 'basisu_transcoder.cpp'))) {
35
+ console.error('basis source not found. Run `node scripts/fetch-basis.mjs` first.');
36
+ process.exit(1);
37
+ }
38
+
39
+ // --- Compile source lists (mirror upstream CMakeLists SRC_LIST) --------------
40
+ // Paths relative to VENDOR. C++ sources compiled by emcc directly; the C zstd
41
+ // source is pre-compiled to an object first (C++ std flag must not touch it).
42
+
43
+ // Transcoder SRC_LIST -- webgl/transcoder/CMakeLists.txt.
44
+ const TRANSCODER_CPP = [
45
+ 'transcoder/basisu_transcoder.cpp',
46
+ 'webgl/transcoder/basis_wrappers.cpp',
47
+ ];
48
+ // zstddeclib.c (decompress-only) for the transcoder KTX2_ZSTANDARD path.
49
+ const TRANSCODER_C = 'zstd/zstddeclib.c';
50
+
51
+ // Encoder SRC_LIST -- webgl/encoder/CMakeLists.txt (non-threaded wasm32 target).
52
+ const ENCODER_CPP = [
53
+ 'webgl/transcoder/basis_wrappers.cpp',
54
+ 'transcoder/basisu_transcoder.cpp',
55
+ 'encoder/basisu_backend.cpp',
56
+ 'encoder/basisu_basis_file.cpp',
57
+ 'encoder/basisu_comp.cpp',
58
+ 'encoder/basisu_enc.cpp',
59
+ 'encoder/basisu_etc.cpp',
60
+ 'encoder/basisu_frontend.cpp',
61
+ 'encoder/basisu_gpu_texture.cpp',
62
+ 'encoder/basisu_pvrtc1_4.cpp',
63
+ 'encoder/basisu_resampler.cpp',
64
+ 'encoder/basisu_resample_filters.cpp',
65
+ 'encoder/basisu_ssim.cpp',
66
+ 'encoder/basisu_uastc_enc.cpp',
67
+ 'encoder/basisu_bc7e_scalar.cpp',
68
+ 'encoder/basisu_dds_export.cpp',
69
+ 'encoder/basisu_bc7enc.cpp',
70
+ 'encoder/basisu_kernels_sse.cpp',
71
+ 'encoder/basisu_opencl.cpp',
72
+ 'encoder/pvpngreader.cpp',
73
+ 'encoder/jpgd.cpp',
74
+ 'encoder/3rdparty/android_astc_decomp.cpp',
75
+ 'encoder/basisu_uastc_hdr_4x4_enc.cpp',
76
+ 'encoder/basisu_astc_hdr_6x6_enc.cpp',
77
+ 'encoder/basisu_astc_hdr_common.cpp',
78
+ 'encoder/basisu_astc_ldr_common.cpp',
79
+ 'encoder/basisu_astc_ldr_encode.cpp',
80
+ 'encoder/basisu_astc_ldr_fencode.cpp',
81
+ 'encoder/basisu_tinyexr.cpp',
82
+ 'encoder/basisu_xbc7_encode.cpp',
83
+ ];
84
+ // Encoder needs the full zstd (compress + decompress), not the declib.
85
+ const ENCODER_C = 'zstd/zstd.c';
86
+
87
+ // --- Drift guard (D-1) -------------------------------------------------------
88
+ // Every .cpp we name must exist in the vendored tree; and every .cpp present in
89
+ // encoder/ (the upstream compile universe) must be either named above or
90
+ // explicitly excluded here. This turns a pin bump that adds/removes/renames an
91
+ // encoder source into a hard build error instead of a silent miscompile.
92
+ const ENCODER_EXCLUDED = new Set([
93
+ // SSE kernels: BASISU_SUPPORT_SSE=0 for WASM (webgl/encoder/CMakeLists.txt).
94
+ 'encoder/basisu_bc15_spmd.cpp',
95
+ 'encoder/basisu_bc15_spmd_sse.cpp',
96
+ // Standalone WASI C-API path (encoder/basisu_wasm_api.*): a different build
97
+ // target from the webgl embind wrapper (basis_wrappers.cpp). Not in the
98
+ // upstream webgl/encoder CMake SRC_LIST -- excluded from the embind build.
99
+ 'encoder/basisu_wasm_api.cpp',
100
+ 'encoder/basisu_wasm_transcoder_api.cpp',
101
+ // OpenCL kernel source is header-embedded; basisu_opencl.cpp is the only unit.
102
+ ]);
103
+
104
+ function assertSourcesExist(list) {
105
+ for (const rel of list) {
106
+ if (!existsSync(join(VENDOR, rel))) {
107
+ throw new Error(`source drift: expected vendored file missing: ${rel}`);
108
+ }
109
+ }
110
+ }
111
+
112
+ function assertNoUnclaimedEncoderCpp() {
113
+ const encDir = join(VENDOR, 'encoder');
114
+ const named = new Set([...ENCODER_CPP.filter((p) => p.startsWith('encoder/')), ...ENCODER_EXCLUDED]);
115
+ const found = [];
116
+ for (const name of readdirSync(encDir)) {
117
+ if (!name.endsWith('.cpp')) continue;
118
+ found.push(`encoder/${name}`);
119
+ }
120
+ const unclaimed = found.filter((p) => !named.has(p));
121
+ if (unclaimed.length > 0) {
122
+ throw new Error(
123
+ `source drift: encoder/*.cpp present in vendored tree but not in build list ` +
124
+ `nor ENCODER_EXCLUDED: ${unclaimed.join(', ')}. Update build-wasm.mjs after a pin bump.`,
125
+ );
126
+ }
127
+ }
128
+
129
+ // --- Encoder source pixel-limit patch (CI-red fix, human decision) -----------
130
+ // The vendored basis_wrappers.cpp caps the encoder's total source texels for the
131
+ // non-wasm64 (wasm32) build at BASISU_ENCODER_MAX_SOURCE_IMAGE_PIXELS: default
132
+ // (1024*1024*4) = 4 Mpx, higher-limit (1024*1024*12) = 12 Mpx. A 4096x4096 game
133
+ // texture is 16.78 Mpx (= 1024*1024*16), above both, so encode() returns 0 bytes
134
+ // -> ktx2-encode-failed. Human decision: 4096^2 is a reasonable engine ceiling,
135
+ // so raise both non-wasm64 limits to (1024*1024*16), matching the wasm64 branch.
136
+ //
137
+ // vendor/basis is gitignored + re-fetched every build (fetch-basis.mjs), so the
138
+ // override must be applied at build time, not committed to the source file. A
139
+ // -D flag can NOT do this: the source uses `#define` (an unconditional macro
140
+ // definition), and a command-line -D of the same macro collides with the
141
+ // in-source #define (redefinition). So we deterministically rewrite the two
142
+ // #else-branch literals in place. Idempotent: after the first patch the old
143
+ // (1024*1024*4)/(1024*1024*12) literals no longer exist, so a re-run is a no-op.
144
+ const ENCODER_MAX_PIXELS_PATCH = [
145
+ {
146
+ from: '#define BASISU_ENCODER_MAX_SOURCE_IMAGE_PIXELS (1024*1024*4)',
147
+ to: '#define BASISU_ENCODER_MAX_SOURCE_IMAGE_PIXELS (1024*1024*16)',
148
+ },
149
+ {
150
+ from: '#define BASISU_ENCODER_MAX_SOURCE_IMAGE_PIXELS_HIGHER_LIMIT (1024*1024*12)',
151
+ to: '#define BASISU_ENCODER_MAX_SOURCE_IMAGE_PIXELS_HIGHER_LIMIT (1024*1024*16)',
152
+ },
153
+ ];
154
+
155
+ function patchEncoderPixelLimit() {
156
+ const wrappers = join(VENDOR, 'webgl', 'transcoder', 'basis_wrappers.cpp');
157
+ let src = readFileSync(wrappers, 'utf8');
158
+ let changed = false;
159
+ for (const { from, to } of ENCODER_MAX_PIXELS_PATCH) {
160
+ if (src.includes(from)) {
161
+ src = src.replace(from, to);
162
+ changed = true;
163
+ } else if (!src.includes(to)) {
164
+ // Neither the pre-patch literal nor the target is present: the upstream
165
+ // source shape drifted (pin bump). Fail loud rather than silently skip.
166
+ throw new Error(
167
+ `encoder pixel-limit patch anchor missing in basis_wrappers.cpp: ${from}. ` +
168
+ 'Re-check the non-wasm64 BASISU_ENCODER_MAX_SOURCE_IMAGE_PIXELS defines after a pin bump.',
169
+ );
170
+ }
171
+ }
172
+ if (changed) {
173
+ writeFileSync(wrappers, src);
174
+ console.log('[patch] raised non-wasm64 encoder pixel limits to 1024*1024*16 (4096^2)');
175
+ } else {
176
+ console.log('[patch] encoder pixel limits already at 1024*1024*16, skipping');
177
+ }
178
+ }
179
+
180
+ // --- emcc flag sets (anchored to upstream CMakeLists) ------------------------
181
+ const INCLUDE = ['-I', join(VENDOR, 'transcoder')];
182
+
183
+ // Transcoder compile defines -- webgl/transcoder/CMakeLists.txt
184
+ // target_compile_definitions. Slim: encoding off, HDR + BC7 + KTX2 + zstd on,
185
+ // ATC/PVRTC2/FXT1/EAC-RG11 off.
186
+ const TRANSCODER_DEFS = [
187
+ '-DNDEBUG=1',
188
+ '-DBASISD_SUPPORT_UASTC_HDR=1',
189
+ '-DBASISD_SUPPORT_UASTC=1',
190
+ '-DBASISD_SUPPORT_BC7=1',
191
+ '-DBASISD_SUPPORT_ATC=0',
192
+ '-DBASISD_SUPPORT_ASTC_HIGHER_OPAQUE_QUALITY=0',
193
+ '-DBASISD_SUPPORT_PVRTC2=0',
194
+ '-DBASISD_SUPPORT_FXT1=0',
195
+ '-DBASISD_SUPPORT_ETC2_EAC_RG11=0',
196
+ '-DBASISU_SUPPORT_ENCODING=0',
197
+ '-DBASISD_ENABLE_DEBUG_FLAGS=1',
198
+ '-DBASISD_SUPPORT_KTX2=1',
199
+ '-DBASISD_SUPPORT_KTX2_ZSTD=1',
200
+ ];
201
+
202
+ // Encoder compile defines -- webgl/encoder/CMakeLists.txt COMMON_DEFS.
203
+ // SSE off (no WASM SSE), encoding + XUASTC on. UASTC_HDR is implied on by the
204
+ // encoder unconditionally; KTX2 macros default on in the encoder path.
205
+ const ENCODER_DEFS = [
206
+ '-DNDEBUG=1',
207
+ '-DBASISD_SUPPORT_UASTC=1',
208
+ '-DBASISD_SUPPORT_BC7=1',
209
+ '-DBASISD_SUPPORT_ATC=0',
210
+ '-DBASISD_SUPPORT_ASTC_HIGHER_OPAQUE_QUALITY=0',
211
+ '-DBASISD_SUPPORT_PVRTC2=0',
212
+ '-DBASISD_SUPPORT_FXT1=0',
213
+ '-DBASISD_SUPPORT_ETC2_EAC_RG11=0',
214
+ '-DBASISU_SUPPORT_ENCODING=1',
215
+ '-DBASISU_SUPPORT_SSE=0',
216
+ '-DBASISD_SUPPORT_XUASTC=1',
217
+ '-DBASISD_SUPPORT_KTX2_ZSTD=1',
218
+ '-DBASISU_SUPPORT_ASTCENC=0',
219
+ ];
220
+
221
+ // Shared link flags. --bind (embind) + MODULARIZE factory named BASIS (upstream
222
+ // EXPORT_NAME) + ES6 output + web,node dual environment (fbx precedent).
223
+ // EXPORT_ES6 replaces the CMake .js suffix so node/vite consume a .mjs module.
224
+ const TRANSCODER_LINK = [
225
+ '--bind',
226
+ '-O3',
227
+ '-s', 'ALLOW_MEMORY_GROWTH=1',
228
+ '-s', 'MALLOC=emmalloc',
229
+ '-s', 'MODULARIZE=1',
230
+ '-s', 'EXPORT_NAME=BASIS',
231
+ '-s', 'EXPORT_ES6=1',
232
+ '-s', 'ENVIRONMENT=web,node',
233
+ '-s', 'ASSERTIONS=0',
234
+ '-s', "EXPORTED_RUNTIME_METHODS=['HEAP8']",
235
+ ];
236
+
237
+ // Encoder base link flags -- webgl/encoder/CMakeLists.txt LINK_BASE + Release
238
+ // CONFIG_LINK. Non-threaded target only (USE_PTHREADS omitted): determinism for
239
+ // byte-equal re-encode (R-11) and no worker infra needed. INITIAL_MEMORY 128MB
240
+ // + 2MB stack are upstream encoder requirements for real image sizes.
241
+ const ENCODER_LINK = [
242
+ '--bind',
243
+ '-O3',
244
+ '-s', 'ALLOW_MEMORY_GROWTH=1',
245
+ '-s', 'INITIAL_MEMORY=134217728',
246
+ '-s', 'STACK_SIZE=2097152',
247
+ '-s', 'MODULARIZE=1',
248
+ '-s', 'EXPORT_NAME=BASIS',
249
+ '-s', 'EXPORT_ES6=1',
250
+ '-s', 'ENVIRONMENT=web,node',
251
+ '-s', 'ASSERTIONS=0',
252
+ '-s', "EXPORTED_RUNTIME_METHODS=['HEAP8']",
253
+ ];
254
+
255
+ const COMMON_CFLAGS = ['-O3', REPRODUCIBLE_PATH, '-fno-strict-aliasing', '-std=c++17'];
256
+
257
+ function emcc(args, label) {
258
+ console.log(`\n[${label}] emcc ${args.length} args ...`);
259
+ execFileSync('emcc', args, { stdio: 'inherit', cwd: VENDOR });
260
+ }
261
+
262
+ function abs(list) {
263
+ return list.map((rel) => join(VENDOR, rel));
264
+ }
265
+
266
+ function sizeKB(p) {
267
+ return (statSync(p).size / 1024).toFixed(0);
268
+ }
269
+
270
+ function buildTranscoder() {
271
+ mkdirSync(PKG, { recursive: true });
272
+ // Compile the C zstd unit to an object first (C++ std flag excluded).
273
+ const zstdObj = join(PKG, 'zstddeclib.transcoder.o');
274
+ emcc(
275
+ [
276
+ '-O3',
277
+ REPRODUCIBLE_PATH,
278
+ '-fno-strict-aliasing',
279
+ '-DNDEBUG=1',
280
+ '-DBASISD_SUPPORT_KTX2_ZSTD=1',
281
+ '-c',
282
+ join(VENDOR, TRANSCODER_C),
283
+ '-o',
284
+ zstdObj,
285
+ ],
286
+ 'transcoder:zstd',
287
+ );
288
+ emcc(
289
+ [
290
+ ...COMMON_CFLAGS,
291
+ ...TRANSCODER_DEFS,
292
+ ...INCLUDE,
293
+ ...abs(TRANSCODER_CPP),
294
+ zstdObj,
295
+ ...TRANSCODER_LINK,
296
+ '-o', join(PKG, 'basis_transcoder.mjs'),
297
+ ],
298
+ 'transcoder:link',
299
+ );
300
+ const wasm = join(PKG, 'basis_transcoder.wasm');
301
+ console.log(` -> ${wasm} (${sizeKB(wasm)} KB)`);
302
+ }
303
+
304
+ function buildEncoder() {
305
+ mkdirSync(PKG_ENCODE, { recursive: true });
306
+ const zstdObj = join(PKG, 'zstd.encoder.o');
307
+ emcc(
308
+ [
309
+ '-O3',
310
+ REPRODUCIBLE_PATH,
311
+ '-fno-strict-aliasing',
312
+ '-DNDEBUG=1',
313
+ '-c',
314
+ join(VENDOR, ENCODER_C),
315
+ '-o',
316
+ zstdObj,
317
+ ],
318
+ 'encoder:zstd',
319
+ );
320
+ emcc(
321
+ [
322
+ ...COMMON_CFLAGS,
323
+ ...ENCODER_DEFS,
324
+ ...INCLUDE,
325
+ ...abs(ENCODER_CPP),
326
+ zstdObj,
327
+ ...ENCODER_LINK,
328
+ '-o', join(PKG_ENCODE, 'basis_encoder.mjs'),
329
+ ],
330
+ 'encoder:link',
331
+ );
332
+ const wasm = join(PKG_ENCODE, 'basis_encoder.wasm');
333
+ console.log(` -> ${wasm} (${sizeKB(wasm)} KB)`);
334
+ }
335
+
336
+ function main() {
337
+ assertSourcesExist(TRANSCODER_CPP);
338
+ assertSourcesExist(ENCODER_CPP);
339
+ assertSourcesExist([TRANSCODER_C, ENCODER_C]);
340
+ assertNoUnclaimedEncoderCpp();
341
+
342
+ // Raise the non-wasm64 encoder source-pixel ceiling to 4096^2 before the
343
+ // encoder compile so 4096x4096 game textures encode instead of returning 0.
344
+ patchEncoderPixelLimit();
345
+
346
+ buildTranscoder();
347
+ buildEncoder();
348
+
349
+ // Drop the intermediate emcc object files: they are recompiled from source on
350
+ // every run (no .o-keyed caching) and are NOT part of the shipped runtime. The
351
+ // release publisher packs pkg/ with `tar -C pkg .`, so leaving them here would
352
+ // bloat the content-keyed tarball with non-runtime bytes. pkg/ then holds only
353
+ // the four runtime files (basis_transcoder.{mjs,wasm} + encode/basis_encoder.
354
+ // {mjs,wasm}) that fetch-wasm.mjs extracts back.
355
+ for (const o of [join(PKG, 'zstddeclib.transcoder.o'), join(PKG, 'zstd.encoder.o')]) {
356
+ rmSync(o, { force: true });
357
+ }
358
+
359
+ console.log('\nBuild complete:');
360
+ console.log(` ${join(PKG, 'basis_transcoder.mjs')} + .wasm`);
361
+ console.log(` ${join(PKG_ENCODE, 'basis_encoder.mjs')} + .wasm`);
362
+ }
363
+
364
+ try {
365
+ main();
366
+ } catch (e) {
367
+ console.error(`\nbuild-wasm failed: ${e.message}`);
368
+ console.error('Is emcc on PATH? Install: https://emscripten.org/docs/getting_started/downloads.html');
369
+ process.exit(1);
370
+ }
@@ -0,0 +1,85 @@
1
+ // content-key.mjs — Single source of truth for the basis-wasm release asset key.
2
+ //
3
+ // The pkg/ bundle (basis_transcoder.{mjs,wasm} + encode/basis_encoder.{mjs,wasm})
4
+ // is an emcc build artifact — NOT committed to git (zero-binary invariant, see
5
+ // packages/codec/.gitignore). It is published to the `wasm-artifacts` GitHub
6
+ // Release, content-keyed so a bundle is uploaded once per unique source state
7
+ // and fetched deterministically.
8
+ //
9
+ // Like @forgeax/engine-wgpu-wasm (and unlike @forgeax/engine-fbx's lone .wasm),
10
+ // the codec pkg/ carries multiple files — two emscripten glue .mjs + two .wasm,
11
+ // nested one level (encode/) — so the release asset is the WHOLE pkg/ packed as
12
+ // a .tar.gz.
13
+ //
14
+ // What determines pkg/ output, and therefore the content key:
15
+ // - scripts/fetch-basis.mjs — pins the exact basis_universal commit
16
+ // (VERSION.commit) + the vendored subtree list. The upstream C++ source is
17
+ // itself gitignored (downloaded on demand), but this pin uniquely fixes it,
18
+ // so hashing the pin script is equivalent to hashing the source tree.
19
+ // - scripts/build-wasm.mjs — the emcc flag sets, compile/link source lists,
20
+ // defines, and the encoder pixel-limit patch. Anything here changes the
21
+ // emitted .wasm/.mjs bit-for-bit.
22
+ // Hashing exactly these two scripts (not the transient vendor/ tree) keeps the
23
+ // key stable across machines and CI while still shifting on any real change.
24
+ //
25
+ // Both scripts/fetch-wasm.mjs (consumer) and the CI publish job import this
26
+ // module so producer and consumer never disagree on the asset name (Pipeline
27
+ // Isolation + SSOT: architecture-principles #1, #4).
28
+
29
+ import { createHash } from 'node:crypto';
30
+ import { readFile } from 'node:fs/promises';
31
+ import { join, dirname, relative } from 'node:path';
32
+ import { fileURLToPath } from 'node:url';
33
+
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
35
+ export const PKG_ROOT = join(__dirname, '..');
36
+
37
+ export const RELEASE_TAG = 'wasm-artifacts';
38
+
39
+ /**
40
+ * Compute the full SHA-256 (hex) over every input that determines the pkg/
41
+ * output. Path is included in the hash so a rename alone shifts the key.
42
+ *
43
+ * Inputs are the two build scripts that fully determine the emitted bundle:
44
+ * fetch-basis.mjs (source pin) + build-wasm.mjs (emcc flags + patch).
45
+ */
46
+ export async function computeContentSha256() {
47
+ const inputs = [
48
+ join(PKG_ROOT, 'scripts', 'fetch-basis.mjs'),
49
+ join(PKG_ROOT, 'scripts', 'build-wasm.mjs'),
50
+ ];
51
+ const hash = createHash('sha256');
52
+ for (const path of inputs) {
53
+ const rel = relative(PKG_ROOT, path).split('\\').join('/');
54
+ const content = await readFile(path);
55
+ hash.update(rel);
56
+ hash.update('\0');
57
+ hash.update(content);
58
+ hash.update('\0');
59
+ }
60
+ return hash.digest('hex');
61
+ }
62
+
63
+ export function buildAssetName(sha8) {
64
+ return `basis-wasm-pkg-${sha8}.tar.gz`;
65
+ }
66
+
67
+ /** Convenience: returns { sha256, sha8, assetName }. */
68
+ export async function resolveAsset() {
69
+ const sha256 = await computeContentSha256();
70
+ const sha8 = sha256.slice(0, 8);
71
+ return { sha256, sha8, assetName: buildAssetName(sha8) };
72
+ }
73
+
74
+ // CLI: `node scripts/content-key.mjs` prints the asset name (used by CI shell).
75
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
76
+ resolveAsset()
77
+ .then(({ sha256, assetName }) => {
78
+ // Two lines: full sha then asset name, so shell can pick either.
79
+ process.stdout.write(`${sha256}\n${assetName}\n`);
80
+ })
81
+ .catch((e) => {
82
+ console.error('content-key: failed to compute:', e.message || e);
83
+ process.exit(1);
84
+ });
85
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ // ensure-wasm.mjs — postinstall best-effort hydration of the codec pkg/ WASM.
3
+ // Thin config over scripts/lib/ensure-wasm-lib.mjs (shared with -fbx, -wgpu-wasm).
4
+ //
5
+ // Why no compile fallback here (matches -wgpu-wasm + -fbx): the basis_universal
6
+ // encoder is a multi-minute -O3 emcc compile; doing it inside `bun install`
7
+ // would block every dependency install for minutes with no progress. The
8
+ // compile fallback lives in Studio's setup.ts (fetch → build:wasm, with
9
+ // progress). Bare install stays fast; a consumer with neither a release nor a
10
+ // setup.ts run gets the buildHint below.
11
+
12
+ import { existsSync } from 'node:fs';
13
+ import { dirname, join } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
17
+ const PKG = join(SCRIPT_DIR, '..', 'pkg');
18
+ const presenceMarkers = [
19
+ join(PKG, 'basis_transcoder.wasm'),
20
+ join(PKG, 'encode', 'basis_encoder.wasm'),
21
+ ];
22
+ if (presenceMarkers.every(existsSync)) {
23
+ process.stdout.write('[codec] pkg/ WASM already present -- skipping fetch.\n');
24
+ process.exit(0);
25
+ }
26
+ const { ensureWasm } = await import('../../../scripts/lib/ensure-wasm-lib.mjs');
27
+
28
+ process.exit(
29
+ ensureWasm({
30
+ pkgLabel: 'codec',
31
+ presenceMarkers,
32
+ fetchScript: join(SCRIPT_DIR, 'fetch-wasm.mjs'),
33
+ skipEnv: 'FORGEAX_SKIP_CODEC_WASM_FETCH',
34
+ buildHint:
35
+ 'run Studio `bun fx setup` (fetch → compile fallback), or build locally with\n' +
36
+ ' pnpm -F @forgeax/engine-codec build:wasm (needs Emscripten emcc)',
37
+ }),
38
+ );
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ // fetch-basis.mjs -- Download + pin the BinomialLLC/basis_universal C++ source
3
+ // tree needed to self-build the transcoder + encoder WASM (see build-wasm.mjs).
4
+ //
5
+ // Mirrors the fbx/ufbx precedent (packages/fbx/scripts/fetch-ufbx.mjs): a
6
+ // pinned VERSION constant + GitHub download + skip-if-exists idempotency. The
7
+ // vendored source lands in packages/codec/vendor/basis/ which is gitignored
8
+ // (zero-binary invariant, AC-12) -- never committed, always re-fetched from the
9
+ // pinned commit locally / in CI.
10
+ //
11
+ // Pin: plan-strategy D-2. The commit below is the exact checkout the
12
+ // knowledge-base source analyses (transcoder API / encoder modes / CMake
13
+ // flags) were produced against, so build-wasm.mjs's flag set stays in lockstep
14
+ // with the KB. `git describe` on this commit = v2_10_final_snapshot-62-g1b33fd5
15
+ // (basis lib version "2.50").
16
+ //
17
+ // Usage: node packages/codec/scripts/fetch-basis.mjs
18
+
19
+ import { execSync } from 'node:child_process';
20
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
21
+ import { tmpdir } from 'node:os';
22
+ import { dirname, join } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+ const CODEC_ROOT = join(__dirname, '..');
27
+ const VENDOR_DIR = join(CODEC_ROOT, 'vendor', 'basis');
28
+
29
+ // --- Pin (D-2) --------------------------------------------------------------
30
+ const VERSION = {
31
+ commit: '1b33fd5098c6e7b58324146b8f5518cbb4cdfb72',
32
+ describe: 'v2_10_final_snapshot-62-g1b33fd5',
33
+ lib: '2.50',
34
+ };
35
+
36
+ // Subtrees copied out of the upstream tree into VENDOR_DIR. Whole directories
37
+ // are copied (not a hand-picked .cpp list) so every relative #include of a
38
+ // sibling .h / .inc / .inl resolves without a second manifest to maintain --
39
+ // the .cpp compile list lives in build-wasm.mjs, guarded there against drift.
40
+ const SUBTREES = [
41
+ 'transcoder',
42
+ 'encoder',
43
+ 'zstd',
44
+ 'webgl/transcoder',
45
+ ];
46
+ const EXTRA_FILES = ['LICENSE'];
47
+
48
+ // Optional offline fallback: a pinned local clone materialised by the KB
49
+ // reference sync. Used only if the network fetch fails (e.g. LibreSSL TLS
50
+ // alert on some macOS toolchains). Must be checked out at the pinned commit.
51
+ const LOCAL_CLONE = join(
52
+ CODEC_ROOT,
53
+ '..',
54
+ '..',
55
+ '.forgeax-harness',
56
+ 'knowledge-base',
57
+ 'references',
58
+ 'repos',
59
+ 'basis-universal',
60
+ );
61
+
62
+ const VERSION_MARKER = join(VENDOR_DIR, '.basis-pin');
63
+
64
+ function isAlreadyFetched() {
65
+ if (!existsSync(VERSION_MARKER)) return false;
66
+ const marker = readFileSync(VERSION_MARKER, 'utf8').trim();
67
+ if (marker !== VERSION.commit) return false;
68
+ // Sanity: the two canonical entry sources must be present.
69
+ return (
70
+ existsSync(join(VENDOR_DIR, 'transcoder', 'basisu_transcoder.cpp')) &&
71
+ existsSync(join(VENDOR_DIR, 'encoder', 'basisu_comp.cpp'))
72
+ );
73
+ }
74
+
75
+ function copyFromRoot(srcRoot) {
76
+ if (existsSync(VENDOR_DIR)) rmSync(VENDOR_DIR, { recursive: true, force: true });
77
+ mkdirSync(VENDOR_DIR, { recursive: true });
78
+ for (const sub of SUBTREES) {
79
+ const from = join(srcRoot, sub);
80
+ if (!existsSync(from)) throw new Error(`missing subtree in source: ${sub}`);
81
+ cpSync(from, join(VENDOR_DIR, sub), { recursive: true });
82
+ }
83
+ for (const f of EXTRA_FILES) {
84
+ const from = join(srcRoot, f);
85
+ if (!existsSync(from)) throw new Error(`missing file in source: ${f}`);
86
+ cpSync(from, join(VENDOR_DIR, f));
87
+ }
88
+ writeFileSync(VERSION_MARKER, `${VERSION.commit}\n`);
89
+ }
90
+
91
+ async function fetchFromGitHub() {
92
+ const url = `https://codeload.github.com/BinomialLLC/basis_universal/tar.gz/${VERSION.commit}`;
93
+ const tmp = mkdtempSync(join(tmpdir(), 'basis-fetch-'));
94
+ const tarPath = join(tmp, 'basis.tar.gz');
95
+ try {
96
+ console.log(`Fetching ${url} ...`);
97
+ const resp = await fetch(url);
98
+ if (!resp.ok) throw new Error(`download failed: HTTP ${resp.status}`);
99
+ const buf = Buffer.from(await resp.arrayBuffer());
100
+ writeFileSync(tarPath, buf);
101
+ console.log(` downloaded ${(buf.length / 1024 / 1024).toFixed(1)} MB, extracting ...`);
102
+ const prefix = `basis_universal-${VERSION.commit}`;
103
+ const memberArgs = [...SUBTREES, ...EXTRA_FILES].map((p) => `${prefix}/${p}`).join(' ');
104
+ // Extract only the needed members, stripping the top-level prefix dir.
105
+ execSync(`tar xzf ${tarPath} -C ${tmp} ${memberArgs}`, { stdio: 'inherit' });
106
+ copyFromRoot(join(tmp, prefix));
107
+ } finally {
108
+ rmSync(tmp, { recursive: true, force: true });
109
+ }
110
+ }
111
+
112
+ function fetchFromLocalClone() {
113
+ if (!existsSync(LOCAL_CLONE)) return false;
114
+ let head = '';
115
+ try {
116
+ head = execSync('git rev-parse HEAD', { cwd: LOCAL_CLONE, encoding: 'utf8' }).trim();
117
+ } catch {
118
+ return false;
119
+ }
120
+ if (head !== VERSION.commit) {
121
+ console.warn(` local clone at ${head} != pinned ${VERSION.commit}, skipping fallback`);
122
+ return false;
123
+ }
124
+ console.log(` using pinned local clone: ${LOCAL_CLONE}`);
125
+ copyFromRoot(LOCAL_CLONE);
126
+ return true;
127
+ }
128
+
129
+ async function main() {
130
+ if (isAlreadyFetched()) {
131
+ console.log(`basis source already at pinned ${VERSION.commit}, skipping (delete vendor/basis to re-fetch).`);
132
+ return;
133
+ }
134
+ try {
135
+ await fetchFromGitHub();
136
+ } catch (e) {
137
+ console.warn(`Network fetch failed (${e.message}); trying local clone fallback ...`);
138
+ if (!fetchFromLocalClone()) {
139
+ throw new Error(
140
+ `Could not obtain basis source: network fetch failed and no pinned local clone at ${LOCAL_CLONE}`,
141
+ );
142
+ }
143
+ }
144
+ console.log(`Done. Vendored basis ${VERSION.describe} (lib ${VERSION.lib}) -> ${VENDOR_DIR}`);
145
+ }
146
+
147
+ main().catch((e) => {
148
+ console.error(e);
149
+ process.exit(1);
150
+ });