@forgeax/engine-codec 0.1.3 → 0.1.6
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.
- package/README.md +4 -11
- package/dist/.tsbuildinfo +1 -1
- package/dist/basis-transcoder.d.ts.map +1 -1
- package/dist/index.mjs +57 -119
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/basis-transcoder.unit.test.ts +1 -352
- package/src/basis-transcoder.ts +60 -125
- package/dist/__tests__/basis-encode-mode-owner.test-d.d.ts +0 -2
- package/dist/__tests__/basis-encode-mode-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/runtime-validation.unit.test.d.ts +0 -2
- package/dist/__tests__/runtime-validation.unit.test.d.ts.map +0 -1
- package/src/__tests__/basis-encode-mode-owner.test-d.ts +0 -39
- package/src/__tests__/runtime-validation.unit.test.ts +0 -19
|
@@ -5,16 +5,10 @@ import {
|
|
|
5
5
|
_basisTranscoderInitCount,
|
|
6
6
|
_setBasisTranscoderImporter,
|
|
7
7
|
initBasisTranscoder,
|
|
8
|
-
transcodeBasis,
|
|
9
8
|
transcodeKtx2,
|
|
10
9
|
} from '../basis-transcoder.js';
|
|
11
|
-
import type { Ktx2Parsed } from '../ktx2.js';
|
|
12
10
|
import { parseKtx2 } from '../ktx2.js';
|
|
13
|
-
import type {
|
|
14
|
-
BasisEncoderModule,
|
|
15
|
-
BasisModuleFactory,
|
|
16
|
-
BasisTranscoderModule,
|
|
17
|
-
} from '../wasm/basis-types.js';
|
|
11
|
+
import type { BasisEncoderModule, BasisModuleFactory } from '../wasm/basis-types.js';
|
|
18
12
|
|
|
19
13
|
/**
|
|
20
14
|
* basis-transcoder glue unit tests (w14).
|
|
@@ -117,351 +111,6 @@ describe('initBasisTranscoder — lazy-init singleton (D-10, w14)', () => {
|
|
|
117
111
|
});
|
|
118
112
|
});
|
|
119
113
|
|
|
120
|
-
const TEST_TARGET = 'bc7-rgba-unorm' as const;
|
|
121
|
-
const TEST_SENTINEL = 'm112-controlled-wasm-throw';
|
|
122
|
-
const TEST_CLOSE_SENTINEL = 'm119-controlled-close-throw';
|
|
123
|
-
const TEST_FORMAT = { cTFBC7_RGBA: { value: 7 } };
|
|
124
|
-
|
|
125
|
-
function fakeParsed(rawBytes: Uint8Array): Ktx2Parsed {
|
|
126
|
-
return { rawBytes, dfd: { colorModel: 166 } } as unknown as Ktx2Parsed;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function createKtx2OperationModule() {
|
|
130
|
-
let shouldThrow = true;
|
|
131
|
-
let shouldCloseThrow = false;
|
|
132
|
-
const state = {
|
|
133
|
-
closeCount: 0,
|
|
134
|
-
inputBytes: [] as Uint8Array[],
|
|
135
|
-
targetEnums: [] as number[],
|
|
136
|
-
transcodeLevels: [] as number[],
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
class FakeKtx2File {
|
|
140
|
-
constructor(bytes: Uint8Array) {
|
|
141
|
-
state.inputBytes.push(bytes);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
close(): void {
|
|
145
|
-
state.closeCount++;
|
|
146
|
-
if (shouldCloseThrow) throw TEST_CLOSE_SENTINEL;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
isValid(): boolean {
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
startTranscoding(): number {
|
|
154
|
-
return 1;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
getLevels(): number {
|
|
158
|
-
return 2;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
getImageLevelInfo(
|
|
162
|
-
level: number,
|
|
163
|
-
_layer: number,
|
|
164
|
-
_face: number,
|
|
165
|
-
): { origWidth: number; origHeight: number } {
|
|
166
|
-
return level === 0 ? { origWidth: 8, origHeight: 4 } : { origWidth: 4, origHeight: 2 };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
getImageTranscodedSizeInBytes(
|
|
170
|
-
level: number,
|
|
171
|
-
_layer: number,
|
|
172
|
-
_face: number,
|
|
173
|
-
format: number,
|
|
174
|
-
): number {
|
|
175
|
-
state.targetEnums.push(format);
|
|
176
|
-
return level === 0 ? 16 : 4;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
transcodeImage(
|
|
180
|
-
dst: Uint8Array,
|
|
181
|
-
level: number,
|
|
182
|
-
_layer: number,
|
|
183
|
-
_face: number,
|
|
184
|
-
_format: number,
|
|
185
|
-
_getAlphaForOpaqueFormats: number,
|
|
186
|
-
_channel0: number,
|
|
187
|
-
_channel1: number,
|
|
188
|
-
): number {
|
|
189
|
-
state.transcodeLevels.push(level);
|
|
190
|
-
if (shouldThrow && level === 1) throw TEST_SENTINEL;
|
|
191
|
-
dst.fill(level + 1);
|
|
192
|
-
return 1;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
getWidth(): number {
|
|
196
|
-
return 8;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
getHeight(): number {
|
|
200
|
-
return 4;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const module = {
|
|
205
|
-
transcoder_texture_format: TEST_FORMAT,
|
|
206
|
-
KTX2File: FakeKtx2File,
|
|
207
|
-
} as unknown as BasisTranscoderModule;
|
|
208
|
-
return {
|
|
209
|
-
module,
|
|
210
|
-
state,
|
|
211
|
-
repair(): void {
|
|
212
|
-
shouldThrow = false;
|
|
213
|
-
},
|
|
214
|
-
refuseClose(): void {
|
|
215
|
-
shouldCloseThrow = true;
|
|
216
|
-
},
|
|
217
|
-
repairClose(): void {
|
|
218
|
-
shouldCloseThrow = false;
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function createBasisOperationModule() {
|
|
224
|
-
let shouldThrow = true;
|
|
225
|
-
let shouldCloseThrow = false;
|
|
226
|
-
const state = {
|
|
227
|
-
closeCount: 0,
|
|
228
|
-
inputBytes: [] as Uint8Array[],
|
|
229
|
-
targetEnums: [] as number[],
|
|
230
|
-
transcodeLevels: [] as number[],
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
class FakeBasisFile {
|
|
234
|
-
constructor(bytes: Uint8Array) {
|
|
235
|
-
state.inputBytes.push(bytes);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
close(): void {
|
|
239
|
-
state.closeCount++;
|
|
240
|
-
if (shouldCloseThrow) throw TEST_CLOSE_SENTINEL;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
getNumImages(): number {
|
|
244
|
-
return 1;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
getNumLevels(_imageIndex: number): number {
|
|
248
|
-
return 2;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
getImageWidth(_imageIndex: number, level: number): number {
|
|
252
|
-
return level === 0 ? 8 : 4;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
getImageHeight(_imageIndex: number, level: number): number {
|
|
256
|
-
return level === 0 ? 4 : 2;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
startTranscoding(): number {
|
|
260
|
-
return 1;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
getImageTranscodedSizeInBytes(_imageIndex: number, level: number, format: number): number {
|
|
264
|
-
state.targetEnums.push(format);
|
|
265
|
-
return level === 0 ? 16 : 4;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
transcodeImage(
|
|
269
|
-
dst: Uint8Array,
|
|
270
|
-
_imageIndex: number,
|
|
271
|
-
level: number,
|
|
272
|
-
_format: number,
|
|
273
|
-
_decodeFlags: number,
|
|
274
|
-
): number {
|
|
275
|
-
state.transcodeLevels.push(level);
|
|
276
|
-
if (shouldThrow && level === 1) throw TEST_SENTINEL;
|
|
277
|
-
dst.fill(level + 1);
|
|
278
|
-
return 1;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const module = {
|
|
283
|
-
transcoder_texture_format: TEST_FORMAT,
|
|
284
|
-
KTX2File: class {},
|
|
285
|
-
BasisFile: FakeBasisFile,
|
|
286
|
-
} as unknown as BasisTranscoderModule;
|
|
287
|
-
return {
|
|
288
|
-
module,
|
|
289
|
-
state,
|
|
290
|
-
repair(): void {
|
|
291
|
-
shouldThrow = false;
|
|
292
|
-
},
|
|
293
|
-
refuseClose(): void {
|
|
294
|
-
shouldCloseThrow = true;
|
|
295
|
-
},
|
|
296
|
-
repairClose(): void {
|
|
297
|
-
shouldCloseThrow = false;
|
|
298
|
-
},
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function createConstructorThrowingModule(): BasisTranscoderModule {
|
|
303
|
-
class ThrowingKtx2File {
|
|
304
|
-
constructor(_bytes: Uint8Array) {
|
|
305
|
-
throw TEST_SENTINEL;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
class ThrowingBasisFile {
|
|
310
|
-
constructor(_bytes: Uint8Array) {
|
|
311
|
-
throw TEST_SENTINEL;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
transcoder_texture_format: TEST_FORMAT,
|
|
317
|
-
KTX2File: ThrowingKtx2File,
|
|
318
|
-
BasisFile: ThrowingBasisFile,
|
|
319
|
-
} as unknown as BasisTranscoderModule;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
describe('transcode throw containment and same-module retry (M112)', () => {
|
|
323
|
-
it('contains KTX2 operation throws, closes once, and retries the same input/module deterministically', async () => {
|
|
324
|
-
const rawBytes = new Uint8Array([1, 2, 3]);
|
|
325
|
-
const parsed = fakeParsed(rawBytes);
|
|
326
|
-
const fixture = createKtx2OperationModule();
|
|
327
|
-
let importerCalls = 0;
|
|
328
|
-
_setBasisTranscoderImporter(() => {
|
|
329
|
-
importerCalls++;
|
|
330
|
-
return Promise.resolve(fixture.module);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
fixture.refuseClose();
|
|
334
|
-
const failed = await transcodeKtx2(parsed, TEST_TARGET);
|
|
335
|
-
expect(failed.ok).toBe(false);
|
|
336
|
-
if (failed.ok) throw new Error('expected operation throw to return a CodecError');
|
|
337
|
-
expect(failed.error.code).toBe('transcode-failed');
|
|
338
|
-
expect(failed.error.detail).toEqual({
|
|
339
|
-
sourceFormat: 'dfd-model-166',
|
|
340
|
-
targetFormat: TEST_TARGET,
|
|
341
|
-
});
|
|
342
|
-
expect(JSON.stringify(failed)).not.toContain(TEST_SENTINEL);
|
|
343
|
-
expect(JSON.stringify(failed)).not.toContain(TEST_CLOSE_SENTINEL);
|
|
344
|
-
expect(fixture.state.closeCount).toBe(1);
|
|
345
|
-
expect(fixture.state.transcodeLevels).toEqual([0, 1]);
|
|
346
|
-
|
|
347
|
-
fixture.repair();
|
|
348
|
-
const cleanupFailed = await transcodeKtx2(parsed, TEST_TARGET);
|
|
349
|
-
expect(cleanupFailed.ok).toBe(false);
|
|
350
|
-
if (cleanupFailed.ok) throw new Error('expected close throw to return a CodecError');
|
|
351
|
-
expect(cleanupFailed.error.code).toBe('transcode-failed');
|
|
352
|
-
expect(cleanupFailed.error.detail).toEqual({
|
|
353
|
-
sourceFormat: 'dfd-model-166',
|
|
354
|
-
targetFormat: TEST_TARGET,
|
|
355
|
-
});
|
|
356
|
-
expect(JSON.stringify(cleanupFailed)).not.toContain(TEST_CLOSE_SENTINEL);
|
|
357
|
-
expect(fixture.state.closeCount).toBe(2);
|
|
358
|
-
|
|
359
|
-
fixture.repairClose();
|
|
360
|
-
const repaired = await transcodeKtx2(parsed, TEST_TARGET);
|
|
361
|
-
expect(repaired.ok).toBe(true);
|
|
362
|
-
if (!repaired.ok) throw new Error('expected repaired KTX2 transcode to succeed');
|
|
363
|
-
expect(repaired.value.format).toBe(TEST_TARGET);
|
|
364
|
-
expect(repaired.value.width).toBe(8);
|
|
365
|
-
expect(repaired.value.height).toBe(4);
|
|
366
|
-
expect(
|
|
367
|
-
repaired.value.mips.map(({ level, width, height, data }) => ({
|
|
368
|
-
level,
|
|
369
|
-
width,
|
|
370
|
-
height,
|
|
371
|
-
data: [...data],
|
|
372
|
-
})),
|
|
373
|
-
).toEqual([
|
|
374
|
-
{ level: 0, width: 8, height: 4, data: new Array(16).fill(1) },
|
|
375
|
-
{ level: 1, width: 4, height: 2, data: new Array(4).fill(2) },
|
|
376
|
-
]);
|
|
377
|
-
|
|
378
|
-
const third = await transcodeKtx2(parsed, TEST_TARGET);
|
|
379
|
-
expect(third).toEqual(repaired);
|
|
380
|
-
expect(importerCalls).toBe(1);
|
|
381
|
-
expect(_basisTranscoderInitCount()).toBe(1);
|
|
382
|
-
expect(fixture.state.inputBytes).toHaveLength(4);
|
|
383
|
-
expect(fixture.state.inputBytes.every((bytes) => bytes === rawBytes)).toBe(true);
|
|
384
|
-
expect(fixture.state.targetEnums).toEqual([7, 7, 7, 7, 7, 7, 7, 7]);
|
|
385
|
-
expect(fixture.state.closeCount).toBe(4);
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it('contains raw Basis operation throws and retries the same bytes/module deterministically', async () => {
|
|
389
|
-
const rawBytes = new Uint8Array([4, 5, 6]);
|
|
390
|
-
const fixture = createBasisOperationModule();
|
|
391
|
-
let importerCalls = 0;
|
|
392
|
-
_setBasisTranscoderImporter(() => {
|
|
393
|
-
importerCalls++;
|
|
394
|
-
return Promise.resolve(fixture.module);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
fixture.refuseClose();
|
|
398
|
-
const failed = await transcodeBasis(rawBytes, TEST_TARGET);
|
|
399
|
-
expect(failed.ok).toBe(false);
|
|
400
|
-
if (failed.ok) throw new Error('expected raw Basis operation throw to return a CodecError');
|
|
401
|
-
expect(failed.error.code).toBe('transcode-failed');
|
|
402
|
-
expect(failed.error.detail).toEqual({ sourceFormat: 'raw-basis', targetFormat: TEST_TARGET });
|
|
403
|
-
expect(JSON.stringify(failed)).not.toContain(TEST_SENTINEL);
|
|
404
|
-
expect(JSON.stringify(failed)).not.toContain(TEST_CLOSE_SENTINEL);
|
|
405
|
-
expect(fixture.state.closeCount).toBe(1);
|
|
406
|
-
expect(fixture.state.transcodeLevels).toEqual([0, 1]);
|
|
407
|
-
|
|
408
|
-
fixture.repair();
|
|
409
|
-
const cleanupFailed = await transcodeBasis(rawBytes, TEST_TARGET);
|
|
410
|
-
expect(cleanupFailed.ok).toBe(false);
|
|
411
|
-
if (cleanupFailed.ok) throw new Error('expected close throw to return a CodecError');
|
|
412
|
-
expect(cleanupFailed.error.code).toBe('transcode-failed');
|
|
413
|
-
expect(cleanupFailed.error.detail).toEqual({
|
|
414
|
-
sourceFormat: 'raw-basis',
|
|
415
|
-
targetFormat: TEST_TARGET,
|
|
416
|
-
});
|
|
417
|
-
expect(JSON.stringify(cleanupFailed)).not.toContain(TEST_CLOSE_SENTINEL);
|
|
418
|
-
expect(fixture.state.closeCount).toBe(2);
|
|
419
|
-
|
|
420
|
-
fixture.repairClose();
|
|
421
|
-
const repaired = await transcodeBasis(rawBytes, TEST_TARGET);
|
|
422
|
-
expect(repaired.ok).toBe(true);
|
|
423
|
-
if (!repaired.ok) throw new Error('expected repaired raw Basis transcode to succeed');
|
|
424
|
-
expect(
|
|
425
|
-
repaired.value.mips.map(({ level, width, height, data }) => ({
|
|
426
|
-
level,
|
|
427
|
-
width,
|
|
428
|
-
height,
|
|
429
|
-
data: [...data],
|
|
430
|
-
})),
|
|
431
|
-
).toEqual([
|
|
432
|
-
{ level: 0, width: 8, height: 4, data: new Array(16).fill(1) },
|
|
433
|
-
{ level: 1, width: 4, height: 2, data: new Array(4).fill(2) },
|
|
434
|
-
]);
|
|
435
|
-
|
|
436
|
-
const third = await transcodeBasis(rawBytes, TEST_TARGET);
|
|
437
|
-
expect(third).toEqual(repaired);
|
|
438
|
-
expect(importerCalls).toBe(1);
|
|
439
|
-
expect(_basisTranscoderInitCount()).toBe(1);
|
|
440
|
-
expect(fixture.state.inputBytes).toHaveLength(4);
|
|
441
|
-
expect(fixture.state.inputBytes.every((bytes) => bytes === rawBytes)).toBe(true);
|
|
442
|
-
expect(fixture.state.targetEnums).toEqual([7, 7, 7, 7, 7, 7, 7, 7]);
|
|
443
|
-
expect(fixture.state.closeCount).toBe(4);
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('contains KTX2 and raw Basis constructor throws without attempting close', async () => {
|
|
447
|
-
const module = createConstructorThrowingModule();
|
|
448
|
-
_setBasisTranscoderImporter(() => Promise.resolve(module));
|
|
449
|
-
|
|
450
|
-
const ktx2 = await transcodeKtx2(fakeParsed(new Uint8Array([7])), TEST_TARGET);
|
|
451
|
-
expect(ktx2.ok).toBe(false);
|
|
452
|
-
if (ktx2.ok) throw new Error('expected KTX2 constructor throw to return a CodecError');
|
|
453
|
-
expect(ktx2.error.code).toBe('transcode-failed');
|
|
454
|
-
expect(ktx2.error.detail).toEqual({ sourceFormat: 'dfd-model-166', targetFormat: TEST_TARGET });
|
|
455
|
-
|
|
456
|
-
const basis = await transcodeBasis(new Uint8Array([8]), TEST_TARGET);
|
|
457
|
-
expect(basis.ok).toBe(false);
|
|
458
|
-
if (basis.ok) throw new Error('expected Basis constructor throw to return a CodecError');
|
|
459
|
-
expect(basis.error.code).toBe('transcode-failed');
|
|
460
|
-
expect(basis.error.detail).toEqual({ sourceFormat: 'raw-basis', targetFormat: TEST_TARGET });
|
|
461
|
-
expect(JSON.stringify({ ktx2, basis })).not.toContain(TEST_SENTINEL);
|
|
462
|
-
});
|
|
463
|
-
});
|
|
464
|
-
|
|
465
114
|
describe.skipIf(!pkgBuilt)('transcodeKtx2 — real UASTC -> BC7 (w14)', () => {
|
|
466
115
|
let uastcKtx2: Uint8Array;
|
|
467
116
|
|
package/src/basis-transcoder.ts
CHANGED
|
@@ -18,12 +18,7 @@
|
|
|
18
18
|
import type { CodecResult } from './errors.js';
|
|
19
19
|
import { codecError } from './errors.js';
|
|
20
20
|
import type { Ktx2Parsed } from './ktx2.js';
|
|
21
|
-
import type {
|
|
22
|
-
BasisFile,
|
|
23
|
-
BasisModuleFactory,
|
|
24
|
-
BasisTranscoderModule,
|
|
25
|
-
KTX2File,
|
|
26
|
-
} from './wasm/basis-types.js';
|
|
21
|
+
import type { BasisFile, BasisModuleFactory, BasisTranscoderModule } from './wasm/basis-types.js';
|
|
27
22
|
|
|
28
23
|
/** How the transcoder WASM module is loaded. Overridable in tests. */
|
|
29
24
|
type TranscoderImporter = () => Promise<BasisTranscoderModule>;
|
|
@@ -217,78 +212,49 @@ export async function transcodeKtx2(
|
|
|
217
212
|
});
|
|
218
213
|
}
|
|
219
214
|
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
file = new mod.KTX2File(parsed.rawBytes);
|
|
223
|
-
} catch {
|
|
224
|
-
return codecError('transcode-failed', {
|
|
225
|
-
sourceFormat: `dfd-model-${parsed.dfd?.colorModel ?? 'unknown'}`,
|
|
226
|
-
targetFormat,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
let result: CodecResult<TranscodedTexture> = codecError('transcode-failed', {
|
|
230
|
-
sourceFormat: `dfd-model-${parsed.dfd?.colorModel ?? 'unknown'}`,
|
|
231
|
-
targetFormat,
|
|
232
|
-
});
|
|
215
|
+
const file = new mod.KTX2File(parsed.rawBytes);
|
|
233
216
|
try {
|
|
234
217
|
if (!file.isValid()) {
|
|
235
|
-
|
|
218
|
+
return codecError('transcode-failed', {
|
|
236
219
|
sourceFormat: 'invalid-ktx2-file',
|
|
237
220
|
targetFormat,
|
|
238
221
|
});
|
|
239
|
-
}
|
|
240
|
-
|
|
222
|
+
}
|
|
223
|
+
if (file.startTranscoding() === 0) {
|
|
224
|
+
return codecError('transcode-failed', {
|
|
241
225
|
sourceFormat: 'start-transcoding-failed',
|
|
242
226
|
targetFormat,
|
|
243
227
|
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
failed = true;
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
mips.push({ level, width: info.origWidth, height: info.origHeight, data: dst });
|
|
262
|
-
}
|
|
263
|
-
if (!failed) {
|
|
264
|
-
result = {
|
|
265
|
-
ok: true,
|
|
266
|
-
value: {
|
|
267
|
-
format: targetFormat,
|
|
268
|
-
width: file.getWidth(),
|
|
269
|
-
height: file.getHeight(),
|
|
270
|
-
mips,
|
|
271
|
-
},
|
|
272
|
-
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const levels = file.getLevels();
|
|
231
|
+
const mips: TranscodedMip[] = [];
|
|
232
|
+
for (let level = 0; level < levels; level++) {
|
|
233
|
+
const info = file.getImageLevelInfo(level, 0, 0);
|
|
234
|
+
const size = file.getImageTranscodedSizeInBytes(level, 0, 0, targetEnum);
|
|
235
|
+
const dst = new Uint8Array(size);
|
|
236
|
+
const ok = file.transcodeImage(dst, level, 0, 0, targetEnum, 0, -1, -1);
|
|
237
|
+
if (ok === 0) {
|
|
238
|
+
return codecError('transcode-failed', {
|
|
239
|
+
sourceFormat: `dfd-model-${parsed.dfd?.colorModel ?? 'unknown'}-level-${level}`,
|
|
240
|
+
targetFormat,
|
|
241
|
+
});
|
|
273
242
|
}
|
|
243
|
+
mips.push({ level, width: info.origWidth, height: info.origHeight, data: dst });
|
|
274
244
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
value: {
|
|
249
|
+
format: targetFormat,
|
|
250
|
+
width: file.getWidth(),
|
|
251
|
+
height: file.getHeight(),
|
|
252
|
+
mips,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
} finally {
|
|
282
256
|
file.close();
|
|
283
|
-
} catch {
|
|
284
|
-
if (result.ok) {
|
|
285
|
-
result = codecError('transcode-failed', {
|
|
286
|
-
sourceFormat: `dfd-model-${parsed.dfd?.colorModel ?? 'unknown'}`,
|
|
287
|
-
targetFormat,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
257
|
}
|
|
291
|
-
return result;
|
|
292
258
|
}
|
|
293
259
|
|
|
294
260
|
/** Transcode a raw `.basis` file; source color remains a Meta author fact. */
|
|
@@ -309,72 +275,41 @@ export async function transcodeBasis(
|
|
|
309
275
|
targetFormat,
|
|
310
276
|
});
|
|
311
277
|
}
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
file = new mod.BasisFile(bytes);
|
|
315
|
-
} catch {
|
|
316
|
-
return codecError('transcode-failed', {
|
|
317
|
-
sourceFormat: 'raw-basis',
|
|
318
|
-
targetFormat,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
let result: CodecResult<TranscodedBasisTexture> = codecError('transcode-failed', {
|
|
322
|
-
sourceFormat: 'raw-basis',
|
|
323
|
-
targetFormat,
|
|
324
|
-
});
|
|
278
|
+
const file = new mod.BasisFile(bytes);
|
|
325
279
|
try {
|
|
326
280
|
if (file.getNumImages() <= 0 || file.getNumLevels(0) <= 0) {
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
281
|
+
return codecError('transcode-failed', { sourceFormat: 'invalid-basis-file', targetFormat });
|
|
282
|
+
}
|
|
283
|
+
if (file.startTranscoding() === 0) {
|
|
284
|
+
return codecError('transcode-failed', {
|
|
330
285
|
sourceFormat: 'start-transcoding-failed',
|
|
331
286
|
targetFormat,
|
|
332
287
|
});
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
});
|
|
346
|
-
failed = true;
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
mips.push({ level, width, height, data });
|
|
350
|
-
}
|
|
351
|
-
if (!failed) {
|
|
352
|
-
result = {
|
|
353
|
-
ok: true,
|
|
354
|
-
value: {
|
|
355
|
-
format: targetFormat,
|
|
356
|
-
width: mips[0]?.width ?? 0,
|
|
357
|
-
height: mips[0]?.height ?? 0,
|
|
358
|
-
mips,
|
|
359
|
-
},
|
|
360
|
-
};
|
|
288
|
+
}
|
|
289
|
+
const mips: TranscodedMip[] = [];
|
|
290
|
+
for (let level = 0; level < file.getNumLevels(0); level++) {
|
|
291
|
+
const width = file.getImageWidth(0, level);
|
|
292
|
+
const height = file.getImageHeight(0, level);
|
|
293
|
+
const size = file.getImageTranscodedSizeInBytes(0, level, targetEnum);
|
|
294
|
+
const data = new Uint8Array(size);
|
|
295
|
+
if (file.transcodeImage(data, 0, level, targetEnum, 0) === 0) {
|
|
296
|
+
return codecError('transcode-failed', {
|
|
297
|
+
sourceFormat: `raw-basis-level-${level}`,
|
|
298
|
+
targetFormat,
|
|
299
|
+
});
|
|
361
300
|
}
|
|
301
|
+
mips.push({ level, width, height, data });
|
|
362
302
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
303
|
+
return {
|
|
304
|
+
ok: true,
|
|
305
|
+
value: {
|
|
306
|
+
format: targetFormat,
|
|
307
|
+
width: mips[0]?.width ?? 0,
|
|
308
|
+
height: mips[0]?.height ?? 0,
|
|
309
|
+
mips,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
} finally {
|
|
370
313
|
file.close();
|
|
371
|
-
} catch {
|
|
372
|
-
if (result.ok) {
|
|
373
|
-
result = codecError('transcode-failed', {
|
|
374
|
-
sourceFormat: 'raw-basis',
|
|
375
|
-
targetFormat,
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
314
|
}
|
|
379
|
-
return result;
|
|
380
315
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"basis-encode-mode-owner.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/basis-encode-mode-owner.test-d.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-validation.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/runtime-validation.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
5
|
-
import type { BasisEncodeMode } from '../encode/basis-encode.js';
|
|
6
|
-
import type { TranscodeModel } from '../transcode.js';
|
|
7
|
-
|
|
8
|
-
const basisEncodeSource = readFileSync(
|
|
9
|
-
new URL('../encode/basis-encode.ts', import.meta.url),
|
|
10
|
-
'utf8',
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
describe('BasisEncodeMode owner', () => {
|
|
14
|
-
it('keeps the encode and transcode vocabularies bilaterally equal', () => {
|
|
15
|
-
expectTypeOf<BasisEncodeMode>().toEqualTypeOf<TranscodeModel>();
|
|
16
|
-
expectTypeOf<TranscodeModel>().toEqualTypeOf<BasisEncodeMode>();
|
|
17
|
-
|
|
18
|
-
const acceptsEncodeMode = (mode: BasisEncodeMode): BasisEncodeMode => mode;
|
|
19
|
-
const acceptsTranscodeModel = (model: TranscodeModel): TranscodeModel => model;
|
|
20
|
-
acceptsEncodeMode('etc1s');
|
|
21
|
-
acceptsEncodeMode('uastc-ldr');
|
|
22
|
-
acceptsEncodeMode('uastc-hdr');
|
|
23
|
-
acceptsTranscodeModel('etc1s');
|
|
24
|
-
acceptsTranscodeModel('uastc-ldr');
|
|
25
|
-
acceptsTranscodeModel('uastc-hdr');
|
|
26
|
-
// @ts-expect-error unknown Basis encode modes remain outside the closed owner union.
|
|
27
|
-
acceptsEncodeMode('basis-mode-not-real');
|
|
28
|
-
// @ts-expect-error unknown transcode models remain outside the closed owner union.
|
|
29
|
-
acceptsTranscodeModel('basis-mode-not-real');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('keeps BasisEncodeMode derived from the public transcode owner', () => {
|
|
33
|
-
expect(basisEncodeSource).toContain("import type { TranscodeModel } from '../transcode.js';");
|
|
34
|
-
expect(basisEncodeSource).toContain('export type BasisEncodeMode = TranscodeModel;');
|
|
35
|
-
expect(basisEncodeSource).not.toContain(
|
|
36
|
-
"export type BasisEncodeMode = 'etc1s' | 'uastc-ldr' | 'uastc-hdr';",
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { KTX2_IDENTIFIER, parseKtx2 } from '../index';
|
|
3
|
-
|
|
4
|
-
describe('codec runtime validation owner', () => {
|
|
5
|
-
it('rejects a non-KTX2 artifact with the codec-owned error', async () => {
|
|
6
|
-
const result = await parseKtx2(new Uint8Array([0, 1, 2, 3]));
|
|
7
|
-
|
|
8
|
-
expect(result.ok).toBe(false);
|
|
9
|
-
if (result.ok) return;
|
|
10
|
-
expect(result.error.code).toBe('ktx2-parse-failed');
|
|
11
|
-
expect(result.error.expected).toContain('valid compressed data');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('keeps the KTX2 signature as the codec validation boundary', () => {
|
|
15
|
-
expect(KTX2_IDENTIFIER).toEqual(
|
|
16
|
-
new Uint8Array([0xab, 0x4b, 0x54, 0x58, 0x20, 0x32, 0x30, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
17
|
-
);
|
|
18
|
-
});
|
|
19
|
-
});
|