@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.
- package/LICENSE +202 -0
- package/README.md +268 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/basis-encode-fast-preset.unit.test.d.ts +2 -0
- package/dist/__tests__/basis-encode-fast-preset.unit.test.d.ts.map +1 -0
- package/dist/__tests__/basis-encode-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/basis-encode-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/basis-encode-mode-owner.test-d.d.ts +2 -0
- package/dist/__tests__/basis-encode-mode-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/basis-transcoder.unit.test.d.ts +2 -0
- package/dist/__tests__/basis-transcoder.unit.test.d.ts.map +1 -0
- package/dist/__tests__/block-format.unit.test.d.ts +2 -0
- package/dist/__tests__/block-format.unit.test.d.ts.map +1 -0
- package/dist/__tests__/byte-identical-falsification.test.d.ts +2 -0
- package/dist/__tests__/byte-identical-falsification.test.d.ts.map +1 -0
- package/dist/__tests__/codec-errors.test-d.d.ts +2 -0
- package/dist/__tests__/codec-errors.test-d.d.ts.map +1 -0
- package/dist/__tests__/codec-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/codec-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/compression-field.unit.test.d.ts +19 -0
- package/dist/__tests__/compression-field.unit.test.d.ts.map +1 -0
- package/dist/__tests__/encode-pixel-limit.unit.test.d.ts +2 -0
- package/dist/__tests__/encode-pixel-limit.unit.test.d.ts.map +1 -0
- package/dist/__tests__/encode-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/encode-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-basis-capability-matrix.unit.test.d.ts +2 -0
- package/dist/__tests__/ktx2-basis-capability-matrix.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-errors.test.d.ts +2 -0
- package/dist/__tests__/ktx2-errors.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-parse-scheme0.test.d.ts +2 -0
- package/dist/__tests__/ktx2-parse-scheme0.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-parse-scheme2.test.d.ts +2 -0
- package/dist/__tests__/ktx2-parse-scheme2.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-to-texture.integration.test.d.ts +2 -0
- package/dist/__tests__/ktx2-to-texture.integration.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-validation.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-validation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/transcode-select-target.unit.test.d.ts +2 -0
- package/dist/__tests__/transcode-select-target.unit.test.d.ts.map +1 -0
- package/dist/__tests__/wasm-init-smoke.test.d.ts +2 -0
- package/dist/__tests__/wasm-init-smoke.test.d.ts.map +1 -0
- package/dist/__tests__/zstd-deterministic.unit.test.d.ts +2 -0
- package/dist/__tests__/zstd-deterministic.unit.test.d.ts.map +1 -0
- package/dist/__tests__/zstd-encode-singleton.unit.test.d.ts +2 -0
- package/dist/__tests__/zstd-encode-singleton.unit.test.d.ts.map +1 -0
- package/dist/__tests__/zstd-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/zstd-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/zstd-singleton.unit.test.d.ts +2 -0
- package/dist/__tests__/zstd-singleton.unit.test.d.ts.map +1 -0
- package/dist/basis-transcoder.d.ts +86 -0
- package/dist/basis-transcoder.d.ts.map +1 -0
- package/dist/block-format.d.ts +41 -0
- package/dist/block-format.d.ts.map +1 -0
- package/dist/encode/basis-encode.d.ts +68 -0
- package/dist/encode/basis-encode.d.ts.map +1 -0
- package/dist/encode/index.d.ts +10 -0
- package/dist/encode/index.d.ts.map +1 -0
- package/dist/encode/index.mjs +207 -0
- package/dist/encode/index.mjs.map +1 -0
- package/dist/encode-impl.d.ts +30 -0
- package/dist/encode-impl.d.ts.map +1 -0
- package/dist/errors.d.ts +61 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +672 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ktx2.d.ts +111 -0
- package/dist/ktx2.d.ts.map +1 -0
- package/dist/transcode.d.ts +55 -0
- package/dist/transcode.d.ts.map +1 -0
- package/dist/wasm/basis-types.d.ts +179 -0
- package/dist/wasm/basis-types.d.ts.map +1 -0
- package/dist/zstd.d.ts +28 -0
- package/dist/zstd.d.ts.map +1 -0
- package/package.json +71 -0
- package/pkg/basis_transcoder.mjs +2 -0
- package/pkg/basis_transcoder.wasm +0 -0
- package/pkg/encode/basis_encoder.mjs +2 -0
- package/pkg/encode/basis_encoder.wasm +0 -0
- package/scripts/build-wasm.mjs +370 -0
- package/scripts/content-key.mjs +85 -0
- package/scripts/ensure-wasm.mjs +38 -0
- package/scripts/fetch-basis.mjs +150 -0
- package/scripts/fetch-wasm.mjs +97 -0
- package/src/__tests__/basis-encode-fast-preset.unit.test.ts +204 -0
- package/src/__tests__/basis-encode-lifecycle.unit.test.ts +249 -0
- package/src/__tests__/basis-encode-mode-owner.test-d.ts +39 -0
- package/src/__tests__/basis-transcoder.unit.test.ts +502 -0
- package/src/__tests__/block-format.unit.test.ts +215 -0
- package/src/__tests__/byte-identical-falsification.test.ts +164 -0
- package/src/__tests__/codec-errors.test-d.ts +67 -0
- package/src/__tests__/codec-errors.unit.test.ts +128 -0
- package/src/__tests__/compression-field.unit.test.ts +245 -0
- package/src/__tests__/encode-pixel-limit.unit.test.ts +117 -0
- package/src/__tests__/encode-roundtrip.unit.test.ts +239 -0
- package/src/__tests__/ktx2-basis-capability-matrix.unit.test.ts +241 -0
- package/src/__tests__/ktx2-errors.test.ts +239 -0
- package/src/__tests__/ktx2-parse-scheme0.test.ts +263 -0
- package/src/__tests__/ktx2-parse-scheme2.test.ts +220 -0
- package/src/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.ts +178 -0
- package/src/__tests__/ktx2-to-texture.integration.test.ts +384 -0
- package/src/__tests__/runtime-validation.unit.test.ts +19 -0
- package/src/__tests__/transcode-select-target.unit.test.ts +167 -0
- package/src/__tests__/wasm-init-smoke.test.ts +102 -0
- package/src/__tests__/zstd-deterministic.unit.test.ts +49 -0
- package/src/__tests__/zstd-encode-singleton.unit.test.ts +63 -0
- package/src/__tests__/zstd-roundtrip.unit.test.ts +87 -0
- package/src/__tests__/zstd-singleton.unit.test.ts +105 -0
- package/src/basis-transcoder.ts +380 -0
- package/src/block-format.ts +128 -0
- package/src/encode/basis-encode.ts +264 -0
- package/src/encode/index.ts +19 -0
- package/src/encode-impl.ts +103 -0
- package/src/errors.ts +79 -0
- package/src/index.ts +38 -0
- package/src/ktx2.ts +469 -0
- package/src/transcode.ts +99 -0
- package/src/wasm/basis-types.ts +220 -0
- package/src/zstd.ts +85 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { decompressZstd, ktx2LevelsToRGBA, parseKtx2 } from '@forgeax/engine-codec';
|
|
2
|
+
import { compressZstd } from '@forgeax/engine-codec/encode';
|
|
3
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* KTX2 2.0 identifier magic.
|
|
7
|
+
*/
|
|
8
|
+
const KTX2_IDENTIFIER = new Uint8Array([
|
|
9
|
+
0xab, 0x4b, 0x54, 0x58, 0x20, 0x32, 0x30, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function writeU32(out: Uint8Array, offset: number, value: number): void {
|
|
13
|
+
new DataView(out.buffer, out.byteOffset + offset, 4).setUint32(0, value, true);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeU64(out: Uint8Array, offset: number, value: number): void {
|
|
17
|
+
const lo = value >>> 0;
|
|
18
|
+
const hi = (value - lo) / 0x100000000;
|
|
19
|
+
new DataView(out.buffer, out.byteOffset + offset, 8).setUint32(0, lo, true);
|
|
20
|
+
new DataView(out.buffer, out.byteOffset + offset + 4, 4).setUint32(0, hi, true);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build a scheme=0 KTX2 fixture with the given texture dimensions and pixel pattern.
|
|
25
|
+
* Returns the raw bytes.
|
|
26
|
+
*/
|
|
27
|
+
function buildKtx2Scheme0Bytes(
|
|
28
|
+
width: number,
|
|
29
|
+
height: number,
|
|
30
|
+
pixelFn: (x: number, y: number, px: number) => void,
|
|
31
|
+
): Uint8Array {
|
|
32
|
+
const pixelBytes = width * height * 4;
|
|
33
|
+
const dfdBlockSize = 40;
|
|
34
|
+
const dfdTotalSize = 4 + dfdBlockSize;
|
|
35
|
+
|
|
36
|
+
const headerEnd = 12 + 9 * 4 + 4 * 4 + 2 * 8; // 80
|
|
37
|
+
const levelIndexEnd = headerEnd + 1 * 24; // 104
|
|
38
|
+
const dfdByteOffset = levelIndexEnd;
|
|
39
|
+
const dfdByteLength = dfdTotalSize;
|
|
40
|
+
const kvdByteOffset = dfdByteOffset + dfdByteLength;
|
|
41
|
+
const kvdByteLength = 0;
|
|
42
|
+
const levelPayloadOffset = kvdByteOffset + kvdByteLength;
|
|
43
|
+
const totalSize = levelPayloadOffset + pixelBytes;
|
|
44
|
+
|
|
45
|
+
const buf = new Uint8Array(totalSize);
|
|
46
|
+
buf.set(KTX2_IDENTIFIER, 0);
|
|
47
|
+
|
|
48
|
+
writeU32(buf, 12, 37); // VK_FORMAT_R8G8B8A8_UNORM
|
|
49
|
+
writeU32(buf, 16, 4);
|
|
50
|
+
writeU32(buf, 20, width);
|
|
51
|
+
writeU32(buf, 24, height);
|
|
52
|
+
writeU32(buf, 28, 0);
|
|
53
|
+
writeU32(buf, 32, 0);
|
|
54
|
+
writeU32(buf, 36, 1);
|
|
55
|
+
writeU32(buf, 40, 1);
|
|
56
|
+
writeU32(buf, 44, 0); // scheme=0
|
|
57
|
+
|
|
58
|
+
writeU32(buf, 48, dfdByteOffset);
|
|
59
|
+
writeU32(buf, 52, dfdByteLength);
|
|
60
|
+
writeU32(buf, 56, kvdByteOffset);
|
|
61
|
+
writeU32(buf, 60, kvdByteLength);
|
|
62
|
+
writeU64(buf, 64, 0);
|
|
63
|
+
writeU64(buf, 72, 0);
|
|
64
|
+
|
|
65
|
+
writeU64(buf, 80, levelPayloadOffset);
|
|
66
|
+
writeU64(buf, 88, pixelBytes);
|
|
67
|
+
writeU64(buf, 96, pixelBytes);
|
|
68
|
+
|
|
69
|
+
// DFD
|
|
70
|
+
writeU32(buf, dfdByteOffset, dfdTotalSize);
|
|
71
|
+
writeU32(buf, dfdByteOffset + 4, 0);
|
|
72
|
+
writeU32(buf, dfdByteOffset + 8, (40 << 16) | 2);
|
|
73
|
+
writeU32(buf, dfdByteOffset + 12, (1 << 16) | (1 << 8) | 1); // RGBSDA, BT709, LINEAR
|
|
74
|
+
writeU32(buf, dfdByteOffset + 16, 0);
|
|
75
|
+
writeU32(buf, dfdByteOffset + 20, 4);
|
|
76
|
+
writeU32(buf, dfdByteOffset + 24, 0);
|
|
77
|
+
writeU32(buf, dfdByteOffset + 28, 7 << 12);
|
|
78
|
+
writeU32(buf, dfdByteOffset + 32, 0);
|
|
79
|
+
writeU32(buf, dfdByteOffset + 36, 0);
|
|
80
|
+
writeU32(buf, dfdByteOffset + 40, 0xffffffff);
|
|
81
|
+
|
|
82
|
+
// Pixels
|
|
83
|
+
for (let y = 0; y < height; y++) {
|
|
84
|
+
for (let x = 0; x < width; x++) {
|
|
85
|
+
const px = levelPayloadOffset + (y * width + x) * 4;
|
|
86
|
+
pixelFn(x, y, px);
|
|
87
|
+
// Apply to buffer
|
|
88
|
+
// pixelFn sets buf[px..px+3]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Apply pixel function after buffer creation
|
|
93
|
+
for (let y = 0; y < height; y++) {
|
|
94
|
+
for (let x = 0; x < width; x++) {
|
|
95
|
+
const px = levelPayloadOffset + (y * width + x) * 4;
|
|
96
|
+
pixelFn(x, y, px);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return buf;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe('KTX2→TextureAsset loading + AC-04 structural check (w24)', () => {
|
|
104
|
+
// Build scheme=0 fixture for 64x64
|
|
105
|
+
const W = 64;
|
|
106
|
+
const H = 64;
|
|
107
|
+
const pixelBytes = W * H * 4;
|
|
108
|
+
let scheme0Bytes: Uint8Array;
|
|
109
|
+
let expectedRaw: Uint8Array;
|
|
110
|
+
let scheme2Bytes: Uint8Array;
|
|
111
|
+
|
|
112
|
+
beforeAll(async () => {
|
|
113
|
+
// Build scheme=0
|
|
114
|
+
const buf0 = buildKtx2Scheme0Bytes(W, H, (_x: number, _y: number, _px: number) => {
|
|
115
|
+
// filled by loop below
|
|
116
|
+
});
|
|
117
|
+
for (let y = 0; y < H; y++) {
|
|
118
|
+
for (let x = 0; x < W; x++) {
|
|
119
|
+
const pxOffset = 148 + (y * W + x) * 4;
|
|
120
|
+
buf0[pxOffset] = (x * 3 + y * 7) & 0xff;
|
|
121
|
+
buf0[pxOffset + 1] = (x * 5 + y * 11) & 0xff;
|
|
122
|
+
buf0[pxOffset + 2] = (x * 13 + y * 17) & 0xff;
|
|
123
|
+
buf0[pxOffset + 3] = 255;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
scheme0Bytes = buf0;
|
|
127
|
+
|
|
128
|
+
expectedRaw = new Uint8Array(pixelBytes);
|
|
129
|
+
for (let y = 0; y < H; y++) {
|
|
130
|
+
for (let x = 0; x < W; x++) {
|
|
131
|
+
const px = (y * W + x) * 4;
|
|
132
|
+
expectedRaw[px] = (x * 3 + y * 7) & 0xff;
|
|
133
|
+
expectedRaw[px + 1] = (x * 5 + y * 11) & 0xff;
|
|
134
|
+
expectedRaw[px + 2] = (x * 13 + y * 17) & 0xff;
|
|
135
|
+
expectedRaw[px + 3] = 255;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Build scheme=2: compress expectedRaw with zstd
|
|
140
|
+
const compResult = await compressZstd(expectedRaw);
|
|
141
|
+
if (!compResult.ok) throw new Error('compress failed');
|
|
142
|
+
|
|
143
|
+
const compressedPayload = compResult.value;
|
|
144
|
+
const scheme2TotalSize = 148 + compressedPayload.length;
|
|
145
|
+
const buf2 = new Uint8Array(scheme2TotalSize);
|
|
146
|
+
buf2.set(KTX2_IDENTIFIER, 0);
|
|
147
|
+
|
|
148
|
+
writeU32(buf2, 12, 37);
|
|
149
|
+
writeU32(buf2, 16, 4);
|
|
150
|
+
writeU32(buf2, 20, W);
|
|
151
|
+
writeU32(buf2, 24, H);
|
|
152
|
+
writeU32(buf2, 28, 0);
|
|
153
|
+
writeU32(buf2, 32, 0);
|
|
154
|
+
writeU32(buf2, 36, 1);
|
|
155
|
+
writeU32(buf2, 40, 1);
|
|
156
|
+
writeU32(buf2, 44, 2); // scheme=2
|
|
157
|
+
|
|
158
|
+
const dfdBlockSize = 40;
|
|
159
|
+
const dfdTotalSize = 4 + dfdBlockSize;
|
|
160
|
+
writeU32(buf2, 48, 104);
|
|
161
|
+
writeU32(buf2, 52, dfdTotalSize);
|
|
162
|
+
writeU32(buf2, 56, 148);
|
|
163
|
+
writeU32(buf2, 60, 0);
|
|
164
|
+
writeU64(buf2, 64, 0);
|
|
165
|
+
writeU64(buf2, 72, 0);
|
|
166
|
+
|
|
167
|
+
writeU64(buf2, 80, 148);
|
|
168
|
+
writeU64(buf2, 88, compressedPayload.length);
|
|
169
|
+
writeU64(buf2, 96, pixelBytes);
|
|
170
|
+
|
|
171
|
+
writeU32(buf2, 104, dfdTotalSize);
|
|
172
|
+
writeU32(buf2, 108, 0);
|
|
173
|
+
writeU32(buf2, 112, (40 << 16) | 2);
|
|
174
|
+
writeU32(buf2, 116, (1 << 16) | (1 << 8) | 1);
|
|
175
|
+
writeU32(buf2, 120, 0);
|
|
176
|
+
writeU32(buf2, 124, 4);
|
|
177
|
+
writeU32(buf2, 128, 0);
|
|
178
|
+
writeU32(buf2, 132, 7 << 12);
|
|
179
|
+
writeU32(buf2, 136, 0);
|
|
180
|
+
writeU32(buf2, 140, 0);
|
|
181
|
+
writeU32(buf2, 144, 0xffffffff);
|
|
182
|
+
|
|
183
|
+
buf2.set(compressedPayload, 148);
|
|
184
|
+
scheme2Bytes = buf2;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('ktx2LevelsToRGBA', () => {
|
|
188
|
+
it('returns full RGBA byte array for scheme=0, level=0', async () => {
|
|
189
|
+
const parsed = await parseKtx2(scheme0Bytes);
|
|
190
|
+
expect(parsed.ok).toBe(true);
|
|
191
|
+
if (!parsed.ok) throw new Error('parse failed');
|
|
192
|
+
|
|
193
|
+
const rgba = await ktx2LevelsToRGBA(parsed.value, 0);
|
|
194
|
+
expect(rgba.ok).toBe(true);
|
|
195
|
+
if (!rgba.ok) throw new Error('levelsToRGBA failed');
|
|
196
|
+
|
|
197
|
+
expect(rgba.value.length).toBe(pixelBytes);
|
|
198
|
+
// Verify all bytes match
|
|
199
|
+
for (let i = 0; i < pixelBytes; i++) {
|
|
200
|
+
// biome-ignore lint/style/noNonNullAssertion: index within bounds verified by length check above
|
|
201
|
+
expect(rgba.value[i]).toBe(expectedRaw[i]!);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('returns decompressed RGBA for scheme=2, level=0', async () => {
|
|
206
|
+
const parsed = await parseKtx2(scheme2Bytes);
|
|
207
|
+
expect(parsed.ok).toBe(true);
|
|
208
|
+
if (!parsed.ok) throw new Error('parse failed');
|
|
209
|
+
|
|
210
|
+
const rgba = await ktx2LevelsToRGBA(parsed.value, 0);
|
|
211
|
+
expect(rgba.ok).toBe(true);
|
|
212
|
+
if (!rgba.ok) throw new Error('levelsToRGBA failed');
|
|
213
|
+
|
|
214
|
+
expect(rgba.value.length).toBe(pixelBytes);
|
|
215
|
+
for (let i = 0; i < pixelBytes; i++) {
|
|
216
|
+
// biome-ignore lint/style/noNonNullAssertion: index within bounds verified by length check above
|
|
217
|
+
expect(rgba.value[i]).toBe(expectedRaw[i]!);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('scheme=0 and scheme=2 produce byte-identical output', async () => {
|
|
222
|
+
const p0 = await parseKtx2(scheme0Bytes);
|
|
223
|
+
expect(p0.ok).toBe(true);
|
|
224
|
+
const p2 = await parseKtx2(scheme2Bytes);
|
|
225
|
+
expect(p2.ok).toBe(true);
|
|
226
|
+
|
|
227
|
+
if (!p0.ok || !p2.ok) throw new Error('parse failed');
|
|
228
|
+
|
|
229
|
+
const r0 = await ktx2LevelsToRGBA(p0.value, 0);
|
|
230
|
+
const r2 = await ktx2LevelsToRGBA(p2.value, 0);
|
|
231
|
+
expect(r0.ok).toBe(true);
|
|
232
|
+
expect(r2.ok).toBe(true);
|
|
233
|
+
if (!r0.ok || !r2.ok) throw new Error('levelsToRGBA failed');
|
|
234
|
+
|
|
235
|
+
expect(r0.value.length).toBe(r2.value.length);
|
|
236
|
+
for (let i = 0; i < r0.value.length; i++) {
|
|
237
|
+
// biome-ignore lint/style/noNonNullAssertion: index within bounds verified by length check above
|
|
238
|
+
expect(r0.value[i]).toBe(r2.value[i]!);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('textureLoader KTX2 dispatch simulation', () => {
|
|
244
|
+
/**
|
|
245
|
+
* Simulates the textureLoader KTX2 dispatch branch:
|
|
246
|
+
* 1. Check first 12 bytes against KTX2 magic
|
|
247
|
+
* 2. Call parseKtx2
|
|
248
|
+
* 3. Call ktx2LevelsToRGBA to get RGBA bytes
|
|
249
|
+
* 4. Assemble a TextureAsset POD (no KTX2-specific fields)
|
|
250
|
+
*/
|
|
251
|
+
function isKtx2(bytes: Uint8Array): boolean {
|
|
252
|
+
if (bytes.length < 12) return false;
|
|
253
|
+
for (let i = 0; i < 12; i++) {
|
|
254
|
+
const b = KTX2_IDENTIFIER[i];
|
|
255
|
+
if (b === undefined || bytes[i] !== b) return false;
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface TextureAsset {
|
|
261
|
+
kind: 'texture';
|
|
262
|
+
width: number;
|
|
263
|
+
height: number;
|
|
264
|
+
format: string;
|
|
265
|
+
data: Uint8Array;
|
|
266
|
+
mipLevelCount: number;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
it('detects KTX2 magic and produces TextureAsset from scheme=0 bytes', async () => {
|
|
270
|
+
expect(isKtx2(scheme0Bytes)).toBe(true);
|
|
271
|
+
|
|
272
|
+
const parsed = await parseKtx2(scheme0Bytes);
|
|
273
|
+
expect(parsed.ok).toBe(true);
|
|
274
|
+
if (!parsed.ok) throw new Error('parse failed');
|
|
275
|
+
|
|
276
|
+
const rgba = await ktx2LevelsToRGBA(parsed.value, 0);
|
|
277
|
+
expect(rgba.ok).toBe(true);
|
|
278
|
+
if (!rgba.ok) throw new Error('levelsToRGBA failed');
|
|
279
|
+
|
|
280
|
+
const tex: TextureAsset = {
|
|
281
|
+
kind: 'texture',
|
|
282
|
+
width: parsed.value.header.pixelWidth,
|
|
283
|
+
height: parsed.value.header.pixelHeight,
|
|
284
|
+
format: 'rgba8unorm',
|
|
285
|
+
data: rgba.value,
|
|
286
|
+
mipLevelCount: parsed.value.header.levelCount,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
expect(tex.kind).toBe('texture');
|
|
290
|
+
expect(tex.width).toBe(W);
|
|
291
|
+
expect(tex.height).toBe(H);
|
|
292
|
+
expect(tex.format).toBe('rgba8unorm');
|
|
293
|
+
expect(tex.data.length).toBe(pixelBytes);
|
|
294
|
+
expect(tex.mipLevelCount).toBe(1);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('produces TextureAsset with zero KTX2-specific fields', async () => {
|
|
298
|
+
const parsed = await parseKtx2(scheme0Bytes);
|
|
299
|
+
expect(parsed.ok).toBe(true);
|
|
300
|
+
if (!parsed.ok) throw new Error('parse failed');
|
|
301
|
+
|
|
302
|
+
const rgba = await ktx2LevelsToRGBA(parsed.value, 0);
|
|
303
|
+
expect(rgba.ok).toBe(true);
|
|
304
|
+
if (!rgba.ok) throw new Error('levelsToRGBA failed');
|
|
305
|
+
|
|
306
|
+
const tex: TextureAsset = {
|
|
307
|
+
kind: 'texture',
|
|
308
|
+
width: parsed.value.header.pixelWidth,
|
|
309
|
+
height: parsed.value.header.pixelHeight,
|
|
310
|
+
format: 'rgba8unorm',
|
|
311
|
+
data: rgba.value,
|
|
312
|
+
mipLevelCount: parsed.value.header.levelCount,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Verify no KTX2-specific fields exist on the POD
|
|
316
|
+
const keys = Object.keys(tex).sort();
|
|
317
|
+
expect(keys).toEqual(['data', 'format', 'height', 'kind', 'mipLevelCount', 'width']);
|
|
318
|
+
// No 'ktx2', 'supercompression', 'dfd', etc.
|
|
319
|
+
expect(keys).not.toContain('ktx2');
|
|
320
|
+
expect(keys).not.toContain('supercompression');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('KTX2 detection rejects non-KTX2 bytes (backward compat path)', async () => {
|
|
324
|
+
const pngLike = new Uint8Array(20);
|
|
325
|
+
pngLike[0] = 0x89;
|
|
326
|
+
pngLike[1] = 0x50; // "PNG" like
|
|
327
|
+
expect(isKtx2(pngLike)).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('KTX2 detection rejects bytes shorter than 12', async () => {
|
|
331
|
+
const short = new Uint8Array(11);
|
|
332
|
+
expect(isKtx2(short)).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('AC-04 structural: single zstd decode implementation', () => {
|
|
337
|
+
it('decompressZstd import references same function used by both asset-layer and KTX2', () => {
|
|
338
|
+
// AC-04: ktx2.ts imports decompressZstd from codec's zstd.ts.
|
|
339
|
+
// Both asset-layer (fetchBinary gate) and KTX2 (ktx2LevelsToRGBA)
|
|
340
|
+
// use the SAME function. This is verified by code-level import:
|
|
341
|
+
// - ktx2.ts imports from `./zstd.js`
|
|
342
|
+
// - asset-registry.ts imports via codec package main entry
|
|
343
|
+
//
|
|
344
|
+
// Since we can't runtime-inspect import sources easily in vitest,
|
|
345
|
+
// this test verifies the function exists and is callable (both paths
|
|
346
|
+
// work). The structural check (single source) is enforced by:
|
|
347
|
+
// 1. `grep 'decompressZstd' packages/codec/src/ktx2.ts` shows import
|
|
348
|
+
// from `./zstd.js` (same module as asset-layer)
|
|
349
|
+
// 2. Codec main entry re-exports from zstd.ts (single export source)
|
|
350
|
+
expect(typeof decompressZstd).toBe('function');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('decompressZstd successfully round-trips through KTX2 path', async () => {
|
|
354
|
+
// Prove that the codec's decompressZstd (which KTX2 uses internally)
|
|
355
|
+
// works — this implicitly verifies AC-04 because ktx2LevelsToRGBA
|
|
356
|
+
// calls the same decompressZstd used by the asset layer.
|
|
357
|
+
const parsed = await parseKtx2(scheme2Bytes);
|
|
358
|
+
expect(parsed.ok).toBe(true);
|
|
359
|
+
if (!parsed.ok) throw new Error('parse failed');
|
|
360
|
+
|
|
361
|
+
const rgba = await ktx2LevelsToRGBA(parsed.value, 0);
|
|
362
|
+
expect(rgba.ok).toBe(true);
|
|
363
|
+
if (!rgba.ok) throw new Error('levelsToRGBA failed');
|
|
364
|
+
|
|
365
|
+
// Now independently decompress the level payload
|
|
366
|
+
const l0 = parsed.value.levelIndex[0];
|
|
367
|
+
if (!l0) throw new Error('levelIndex[0] missing');
|
|
368
|
+
const compressedPayload = scheme2Bytes.slice(
|
|
369
|
+
Number(l0.byteOffset),
|
|
370
|
+
Number(l0.byteOffset) + Number(l0.byteLength),
|
|
371
|
+
);
|
|
372
|
+
const directDecomp = await decompressZstd(compressedPayload);
|
|
373
|
+
expect(directDecomp.ok).toBe(true);
|
|
374
|
+
if (!directDecomp.ok) throw new Error('decompress failed');
|
|
375
|
+
|
|
376
|
+
// Both paths should produce identical output
|
|
377
|
+
expect(directDecomp.value.length).toBe(rgba.value.length);
|
|
378
|
+
for (let i = 0; i < directDecomp.value.length; i++) {
|
|
379
|
+
// biome-ignore lint/style/noNonNullAssertion: index within bounds verified by length check above
|
|
380
|
+
expect(directDecomp.value[i]).toBe(rgba.value[i]!);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
selectTranscodeTarget,
|
|
3
|
+
type TranscodeCaps,
|
|
4
|
+
type TranscodeSource,
|
|
5
|
+
} from '@forgeax/engine-codec';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* selectTranscodeTarget priority-chain unit tests (w8).
|
|
10
|
+
*
|
|
11
|
+
* AC-04 application point: node imports the pure function directly and asserts
|
|
12
|
+
* the returned GPUTextureFormat for each caps combination. No DOM / node:path
|
|
13
|
+
* dependency is imported here -- that absence is the necessary evidence that the
|
|
14
|
+
* function is browser+node dual-safe (the isolation gate proves the source side).
|
|
15
|
+
*
|
|
16
|
+
* The priority chains (plan-strategy §2 D-4 + requirements §range table):
|
|
17
|
+
* RGBA : ASTC-4x4 -> BC7 -> ETC2 -> RGBA8 (D-4: bc present => BC7 first)
|
|
18
|
+
* RG : BC5 -> EAC-RG11 -> RG8 (D-4: bc present => BC5 first)
|
|
19
|
+
* R : BC4 -> EAC-R11 -> R8 (D-4: bc present => BC4 first)
|
|
20
|
+
* HDR : BC6H -> rgba16float
|
|
21
|
+
*
|
|
22
|
+
* srgb/linear only varies the RGBA color arm (data channels R/RG and HDR carry
|
|
23
|
+
* no sRGB variant in WebGPU). The srgb flag is derived upstream from the DFD
|
|
24
|
+
* transfer function; this function receives it as an explicit boolean.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const BC_ONLY: TranscodeCaps = { bc: true, etc2: false, astc: false };
|
|
28
|
+
const ETC2_ONLY: TranscodeCaps = { bc: false, etc2: true, astc: false };
|
|
29
|
+
const ASTC_ONLY: TranscodeCaps = { bc: false, etc2: false, astc: true };
|
|
30
|
+
const NO_CAP: TranscodeCaps = { bc: false, etc2: false, astc: false };
|
|
31
|
+
const BC_AND_ETC2: TranscodeCaps = { bc: true, etc2: true, astc: false };
|
|
32
|
+
const BC_AND_ASTC: TranscodeCaps = { bc: true, etc2: false, astc: true };
|
|
33
|
+
const ASTC_AND_ETC2: TranscodeCaps = { bc: false, etc2: true, astc: true };
|
|
34
|
+
|
|
35
|
+
function ldrRgba(srgb: boolean): TranscodeSource {
|
|
36
|
+
return { model: 'uastc-ldr', srgb, channels: 'rgba' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('selectTranscodeTarget — LDR RGBA arm (w8)', () => {
|
|
40
|
+
it('bc-only + srgb => bc7-rgba-unorm-srgb', () => {
|
|
41
|
+
expect(selectTranscodeTarget(ldrRgba(true), BC_ONLY)).toBe('bc7-rgba-unorm-srgb');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('bc-only + linear => bc7-rgba-unorm', () => {
|
|
45
|
+
expect(selectTranscodeTarget(ldrRgba(false), BC_ONLY)).toBe('bc7-rgba-unorm');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('etc2-only + srgb => etc2-rgba8unorm-srgb', () => {
|
|
49
|
+
expect(selectTranscodeTarget(ldrRgba(true), ETC2_ONLY)).toBe('etc2-rgba8unorm-srgb');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('etc2-only + linear => etc2-rgba8unorm', () => {
|
|
53
|
+
expect(selectTranscodeTarget(ldrRgba(false), ETC2_ONLY)).toBe('etc2-rgba8unorm');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('astc-only + srgb => astc-4x4-unorm-srgb', () => {
|
|
57
|
+
expect(selectTranscodeTarget(ldrRgba(true), ASTC_ONLY)).toBe('astc-4x4-unorm-srgb');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('astc-only + linear => astc-4x4-unorm', () => {
|
|
61
|
+
expect(selectTranscodeTarget(ldrRgba(false), ASTC_ONLY)).toBe('astc-4x4-unorm');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('no cap + srgb => rgba8unorm-srgb (fallback is a format, not an error)', () => {
|
|
65
|
+
expect(selectTranscodeTarget(ldrRgba(true), NO_CAP)).toBe('rgba8unorm-srgb');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('no cap + linear => rgba8unorm', () => {
|
|
69
|
+
expect(selectTranscodeTarget(ldrRgba(false), NO_CAP)).toBe('rgba8unorm');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('selectTranscodeTarget — D-4 Mesa guard: BC present => prefer BC (w8)', () => {
|
|
74
|
+
it('bc+etc2 (Mesa software-decode trap) => bc7, NOT etc2', () => {
|
|
75
|
+
const target = selectTranscodeTarget(ldrRgba(true), BC_AND_ETC2);
|
|
76
|
+
expect(target).toBe('bc7-rgba-unorm-srgb');
|
|
77
|
+
expect(target).not.toBe('etc2-rgba8unorm-srgb');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('bc+astc (Apple Silicon dual hardware) => bc7, NOT astc', () => {
|
|
81
|
+
const target = selectTranscodeTarget(ldrRgba(false), BC_AND_ASTC);
|
|
82
|
+
expect(target).toBe('bc7-rgba-unorm');
|
|
83
|
+
expect(target).not.toBe('astc-4x4-unorm');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('astc+etc2 (no bc) follows table order ASTC before ETC2', () => {
|
|
87
|
+
expect(selectTranscodeTarget(ldrRgba(true), ASTC_AND_ETC2)).toBe('astc-4x4-unorm-srgb');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('selectTranscodeTarget — single/dual-channel arms (w8)', () => {
|
|
92
|
+
const rg = (srgb: boolean): TranscodeSource => ({ model: 'uastc-ldr', srgb, channels: 'rg' });
|
|
93
|
+
const r = (srgb: boolean): TranscodeSource => ({ model: 'uastc-ldr', srgb, channels: 'r' });
|
|
94
|
+
|
|
95
|
+
it('RG bc-only => bc5-rg-unorm (no srgb variant even when srgb=true)', () => {
|
|
96
|
+
expect(selectTranscodeTarget(rg(true), BC_ONLY)).toBe('bc5-rg-unorm');
|
|
97
|
+
expect(selectTranscodeTarget(rg(false), BC_ONLY)).toBe('bc5-rg-unorm');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('RG etc2-only => eac-rg11unorm', () => {
|
|
101
|
+
expect(selectTranscodeTarget(rg(false), ETC2_ONLY)).toBe('eac-rg11unorm');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('RG astc-only => rg8unorm (ASTC not in the RG chain)', () => {
|
|
105
|
+
expect(selectTranscodeTarget(rg(false), ASTC_ONLY)).toBe('rg8unorm');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('RG no cap => rg8unorm', () => {
|
|
109
|
+
expect(selectTranscodeTarget(rg(false), NO_CAP)).toBe('rg8unorm');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('RG bc+etc2 => bc5-rg-unorm (D-4 BC first)', () => {
|
|
113
|
+
expect(selectTranscodeTarget(rg(false), BC_AND_ETC2)).toBe('bc5-rg-unorm');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('R bc-only => bc4-r-unorm', () => {
|
|
117
|
+
expect(selectTranscodeTarget(r(false), BC_ONLY)).toBe('bc4-r-unorm');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('R etc2-only => eac-r11unorm', () => {
|
|
121
|
+
expect(selectTranscodeTarget(r(false), ETC2_ONLY)).toBe('eac-r11unorm');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('R astc-only => r8unorm (ASTC not in the R chain)', () => {
|
|
125
|
+
expect(selectTranscodeTarget(r(false), ASTC_ONLY)).toBe('r8unorm');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('R no cap => r8unorm', () => {
|
|
129
|
+
expect(selectTranscodeTarget(r(false), NO_CAP)).toBe('r8unorm');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('R bc+etc2 => bc4-r-unorm (D-4 BC first)', () => {
|
|
133
|
+
expect(selectTranscodeTarget(r(false), BC_AND_ETC2)).toBe('bc4-r-unorm');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('selectTranscodeTarget — HDR arm (w8)', () => {
|
|
138
|
+
const hdr: TranscodeSource = { model: 'uastc-hdr', srgb: false, channels: 'rgba' };
|
|
139
|
+
|
|
140
|
+
it('HDR bc-only => bc6h-rgb-ufloat', () => {
|
|
141
|
+
expect(selectTranscodeTarget(hdr, BC_ONLY)).toBe('bc6h-rgb-ufloat');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('HDR bc+etc2 => bc6h-rgb-ufloat (D-4 BC first)', () => {
|
|
145
|
+
expect(selectTranscodeTarget(hdr, BC_AND_ETC2)).toBe('bc6h-rgb-ufloat');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('HDR etc2-only => rgba16float (no bc; ASTC-HDR is OOS)', () => {
|
|
149
|
+
expect(selectTranscodeTarget(hdr, ETC2_ONLY)).toBe('rgba16float');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('HDR astc-only => rgba16float (no bc)', () => {
|
|
153
|
+
expect(selectTranscodeTarget(hdr, ASTC_ONLY)).toBe('rgba16float');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('HDR no cap => rgba16float', () => {
|
|
157
|
+
expect(selectTranscodeTarget(hdr, NO_CAP)).toBe('rgba16float');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('selectTranscodeTarget — ETC1S and UASTC-LDR share the LDR chain (w8)', () => {
|
|
162
|
+
it('etc1s model routes through the same LDR RGBA arm as uastc-ldr', () => {
|
|
163
|
+
const etc1s: TranscodeSource = { model: 'etc1s', srgb: true, channels: 'rgba' };
|
|
164
|
+
expect(selectTranscodeTarget(etc1s, BC_ONLY)).toBe('bc7-rgba-unorm-srgb');
|
|
165
|
+
expect(selectTranscodeTarget(etc1s, NO_CAP)).toBe('rgba8unorm-srgb');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import type {
|
|
5
|
+
BasisEncoderModule,
|
|
6
|
+
BasisModuleFactory,
|
|
7
|
+
BasisTranscoderModule,
|
|
8
|
+
} from '../wasm/basis-types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* M1 closing red-green gate: the two self-built basis WASM modules (transcoder +
|
|
12
|
+
* encoder) init in node, and the encoder produces a KTX2 container whose first
|
|
13
|
+
* bytes are the KTX2 identifier magic. This validates the whole M1 WASM
|
|
14
|
+
* self-build base end to end without touching M2/M3 code.
|
|
15
|
+
*
|
|
16
|
+
* pkg/ is a gitignored emcc build artifact (zero-binary, AC-12). When it has
|
|
17
|
+
* not been built (contributor without emsdk), skip rather than false-fail --
|
|
18
|
+
* CI builds it in the build-artifacts job (w6) so the gate runs there.
|
|
19
|
+
*/
|
|
20
|
+
const TRANSCODER_GLUE = new URL('../../pkg/basis_transcoder.mjs', import.meta.url);
|
|
21
|
+
const ENCODER_GLUE = new URL('../../pkg/encode/basis_encoder.mjs', import.meta.url);
|
|
22
|
+
|
|
23
|
+
const pkgBuilt =
|
|
24
|
+
existsSync(fileURLToPath(TRANSCODER_GLUE)) && existsSync(fileURLToPath(ENCODER_GLUE));
|
|
25
|
+
|
|
26
|
+
/** KTX2 2.0 identifier: «KTX 20»\r\n\x1A\n (spec section 1). */
|
|
27
|
+
const KTX2_IDENTIFIER = [0xab, 0x4b, 0x54, 0x58, 0x20, 0x32, 0x30, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
28
|
+
|
|
29
|
+
async function loadTranscoder(): Promise<BasisTranscoderModule> {
|
|
30
|
+
const factory = (
|
|
31
|
+
(await import(/* @vite-ignore */ TRANSCODER_GLUE.href)) as {
|
|
32
|
+
default: BasisModuleFactory<BasisTranscoderModule>;
|
|
33
|
+
}
|
|
34
|
+
).default;
|
|
35
|
+
const mod = await factory({
|
|
36
|
+
locateFile: () => new URL('../../pkg/basis_transcoder.wasm', import.meta.url).href,
|
|
37
|
+
});
|
|
38
|
+
mod.initializeBasis();
|
|
39
|
+
return mod;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function loadEncoder(): Promise<BasisEncoderModule> {
|
|
43
|
+
const factory = (
|
|
44
|
+
(await import(/* @vite-ignore */ ENCODER_GLUE.href)) as {
|
|
45
|
+
default: BasisModuleFactory<BasisEncoderModule>;
|
|
46
|
+
}
|
|
47
|
+
).default;
|
|
48
|
+
const mod = await factory({
|
|
49
|
+
locateFile: () => new URL('../../pkg/encode/basis_encoder.wasm', import.meta.url).href,
|
|
50
|
+
});
|
|
51
|
+
mod.initializeBasis();
|
|
52
|
+
return mod;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A deterministic 8x8 RGBA test image. */
|
|
56
|
+
function makeRgba8x8(): Uint8Array {
|
|
57
|
+
const w = 8;
|
|
58
|
+
const h = 8;
|
|
59
|
+
const img = new Uint8Array(w * h * 4);
|
|
60
|
+
for (let i = 0; i < w * h; i++) {
|
|
61
|
+
img[i * 4] = (i * 7) & 255;
|
|
62
|
+
img[i * 4 + 1] = (i * 13) & 255;
|
|
63
|
+
img[i * 4 + 2] = (i * 29) & 255;
|
|
64
|
+
img[i * 4 + 3] = 255;
|
|
65
|
+
}
|
|
66
|
+
return img;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe.skipIf(!pkgBuilt)('basis WASM init smoke (M1)', () => {
|
|
70
|
+
it('transcoder module inits with KTX2File + format enum', async () => {
|
|
71
|
+
const tc = await loadTranscoder();
|
|
72
|
+
expect(tc).not.toBeNull();
|
|
73
|
+
expect(typeof tc.KTX2File).toBe('function');
|
|
74
|
+
expect(typeof tc.transcoder_texture_format.cTFBC7_RGBA.value).toBe('number');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('encoder module inits with BasisEncoder + format enum', async () => {
|
|
78
|
+
const enc = await loadEncoder();
|
|
79
|
+
expect(enc).not.toBeNull();
|
|
80
|
+
expect(typeof enc.BasisEncoder).toBe('function');
|
|
81
|
+
expect(typeof enc.basis_tex_format.cUASTC_LDR_4x4.value).toBe('number');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('encoder produces a KTX2 container with the identifier magic', async () => {
|
|
85
|
+
const enc = await loadEncoder();
|
|
86
|
+
const encoder = new enc.BasisEncoder();
|
|
87
|
+
const img = makeRgba8x8();
|
|
88
|
+
encoder.setSliceSourceImage(0, img, 8, 8, 0);
|
|
89
|
+
encoder.setCreateKTX2File(true);
|
|
90
|
+
encoder.setKTX2UASTCSupercompression(true);
|
|
91
|
+
encoder.setFormatMode(enc.basis_tex_format.cUASTC_LDR_4x4.value);
|
|
92
|
+
encoder.setPerceptual(true);
|
|
93
|
+
encoder.setMipGen(false);
|
|
94
|
+
const out = new Uint8Array(1 << 20);
|
|
95
|
+
const n = encoder.encode(out);
|
|
96
|
+
encoder.delete();
|
|
97
|
+
|
|
98
|
+
expect(n).toBeGreaterThan(KTX2_IDENTIFIER.length);
|
|
99
|
+
const head = Array.from(out.slice(0, KTX2_IDENTIFIER.length));
|
|
100
|
+
expect(head).toEqual(KTX2_IDENTIFIER);
|
|
101
|
+
});
|
|
102
|
+
});
|