@forgeax/engine-codec 0.0.0-dev.8d955ade1c79
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 +261 -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-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__/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 +610 -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-transcoder.unit.test.ts +151 -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__/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 +315 -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,263 @@
|
|
|
1
|
+
import { ktx2LevelsToRGBA, parseKtx2 } from '@forgeax/engine-codec';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* KTX2 2.0 identifier magic (12 bytes).
|
|
6
|
+
* spec section 1: «KTX 20»\r\n\x1A\n
|
|
7
|
+
*/
|
|
8
|
+
const KTX2_IDENTIFIER = new Uint8Array([
|
|
9
|
+
0xab, 0x4b, 0x54, 0x58, 0x20, 0x32, 0x30, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Write a u32 little-endian into `out` at `offset`.
|
|
14
|
+
*/
|
|
15
|
+
function writeU32(out: Uint8Array, offset: number, value: number): void {
|
|
16
|
+
new DataView(out.buffer, out.byteOffset + offset, 4).setUint32(0, value, true);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Write a u64 little-endian into `out` at `offset`.
|
|
21
|
+
* Uses two u32 writes because bitwise ops truncate to 32 bits.
|
|
22
|
+
*/
|
|
23
|
+
function writeU64(out: Uint8Array, offset: number, value: number): void {
|
|
24
|
+
const lo = value >>> 0;
|
|
25
|
+
const hi = (value - lo) / 0x100000000;
|
|
26
|
+
new DataView(out.buffer, out.byteOffset + offset, 8).setUint32(0, lo, true);
|
|
27
|
+
new DataView(out.buffer, out.byteOffset + offset + 4, 4).setUint32(0, hi, true);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Synthesize a minimal KTX2 file with scheme=0 (uncompressed RGBA8).
|
|
32
|
+
*
|
|
33
|
+
* Layout (per spec sections 1-7):
|
|
34
|
+
* [12] identifier
|
|
35
|
+
* [ 4] vkFormat = VK_FORMAT_R8G8B8A8_UNORM (37)
|
|
36
|
+
* [ 4] typeSize = 4 (4 bytes per pixel)
|
|
37
|
+
* [ 4] pixelWidth = 64
|
|
38
|
+
* [ 4] pixelHeight = 64
|
|
39
|
+
* [ 4] pixelDepth = 0
|
|
40
|
+
* [ 4] layerCount = 0
|
|
41
|
+
* [ 4] faceCount = 1
|
|
42
|
+
* [ 4] levelCount = 1
|
|
43
|
+
* [ 4] supercompressionScheme = 0
|
|
44
|
+
* [ 4] dfdByteOffset
|
|
45
|
+
* [ 4] dfdByteLength
|
|
46
|
+
* [ 4] kvdByteOffset
|
|
47
|
+
* [ 4] kvdByteLength
|
|
48
|
+
* [ 8] sgdByteOffset = 0
|
|
49
|
+
* [ 8] sgdByteLength = 0
|
|
50
|
+
* [24] levelIndex[0] { byteOffset, byteLength, uncompressedByteLength }
|
|
51
|
+
* ... DFD block
|
|
52
|
+
* ... KV data (empty)
|
|
53
|
+
* ... level 0 RGBA pixels
|
|
54
|
+
*
|
|
55
|
+
* Returns the raw bytes of the KTX2 file.
|
|
56
|
+
*/
|
|
57
|
+
function buildScheme0Fixture(): Uint8Array {
|
|
58
|
+
const W = 64;
|
|
59
|
+
const H = 64;
|
|
60
|
+
const pixelBytes = W * H * 4; // RGBA8 = 16384 bytes per level
|
|
61
|
+
|
|
62
|
+
// DFD descriptor: minimal RGBA8 descriptor
|
|
63
|
+
// descriptorBlockSize = 24 (header) + 16*1 (sample) = 40 bytes
|
|
64
|
+
// total dfd = 4 (dfdTotalSize) + 40 (descriptor block) = 44
|
|
65
|
+
const dfdBlockSize = 40; // 24 header + 16 sample
|
|
66
|
+
const dfdTotalSize = 4 + dfdBlockSize; // 44
|
|
67
|
+
|
|
68
|
+
// Offsets
|
|
69
|
+
const headerEnd = 12 + 9 * 4 + 4 * 4 + 2 * 8; // 12 + 36 + 16 + 16 = 80
|
|
70
|
+
const levelIndexEnd = headerEnd + 1 * 24; // 80 + 24 = 104
|
|
71
|
+
const dfdByteOffset = levelIndexEnd; // 104
|
|
72
|
+
const dfdByteLength = dfdTotalSize; // 44
|
|
73
|
+
const kvdByteOffset = dfdByteOffset + dfdByteLength; // 148
|
|
74
|
+
const kvdByteLength = 0; // no KV data
|
|
75
|
+
const levelPayloadOffset = kvdByteOffset + kvdByteLength; // 148
|
|
76
|
+
const levelPayloadSize = pixelBytes; // 16384
|
|
77
|
+
const totalSize = levelPayloadOffset + levelPayloadSize; // 16532
|
|
78
|
+
|
|
79
|
+
const buf = new Uint8Array(totalSize);
|
|
80
|
+
|
|
81
|
+
// --- identifier (offset 0, 12 bytes) ---
|
|
82
|
+
buf.set(KTX2_IDENTIFIER, 0);
|
|
83
|
+
|
|
84
|
+
// --- header (offset 12) ---
|
|
85
|
+
// vkFormat = VK_FORMAT_R8G8B8A8_UNORM = 37
|
|
86
|
+
writeU32(buf, 12, 37);
|
|
87
|
+
// typeSize = 4
|
|
88
|
+
writeU32(buf, 16, 4);
|
|
89
|
+
// pixelWidth = 64
|
|
90
|
+
writeU32(buf, 20, W);
|
|
91
|
+
// pixelHeight = 64
|
|
92
|
+
writeU32(buf, 24, H);
|
|
93
|
+
// pixelDepth = 0
|
|
94
|
+
writeU32(buf, 28, 0);
|
|
95
|
+
// layerCount = 0
|
|
96
|
+
writeU32(buf, 32, 0);
|
|
97
|
+
// faceCount = 1
|
|
98
|
+
writeU32(buf, 36, 1);
|
|
99
|
+
// levelCount = 1
|
|
100
|
+
writeU32(buf, 40, 1);
|
|
101
|
+
// supercompressionScheme = 0
|
|
102
|
+
writeU32(buf, 44, 0);
|
|
103
|
+
|
|
104
|
+
// --- index (offset 48) ---
|
|
105
|
+
writeU32(buf, 48, dfdByteOffset);
|
|
106
|
+
writeU32(buf, 52, dfdByteLength);
|
|
107
|
+
writeU32(buf, 56, kvdByteOffset);
|
|
108
|
+
writeU32(buf, 60, kvdByteLength);
|
|
109
|
+
writeU64(buf, 64, 0); // sgdByteOffset = 0
|
|
110
|
+
writeU64(buf, 72, 0); // sgdByteLength = 0
|
|
111
|
+
|
|
112
|
+
// --- level index (offset 80) ---
|
|
113
|
+
writeU64(buf, 80, levelPayloadOffset);
|
|
114
|
+
writeU64(buf, 88, levelPayloadSize);
|
|
115
|
+
writeU64(buf, 96, levelPayloadSize); // uncompressedByteLength == byteLength for scheme=0
|
|
116
|
+
|
|
117
|
+
// --- DFD (offset 104) ---
|
|
118
|
+
// dfdTotalSize (u32)
|
|
119
|
+
writeU32(buf, 104, dfdTotalSize);
|
|
120
|
+
// descriptor block word 0: vendorId(17)=0, descriptorType(15)=0
|
|
121
|
+
writeU32(buf, 108, 0);
|
|
122
|
+
// descriptor block word 1: versionNumber(16)=2, descriptorBlockSize(16)=40
|
|
123
|
+
writeU32(buf, 112, (40 << 16) | 2);
|
|
124
|
+
// descriptor block word 2: colorModel(8)=1 (RGBSDA), colorPrimaries(8)=1 (BT709/SRGB), transferFunction(8)=1 (LINEAR), flags(8)=0
|
|
125
|
+
writeU32(buf, 116, (1 << 16) | (1 << 8) | 1);
|
|
126
|
+
// descriptor block word 3: texelBlockDimension[0..3]=0 (uncompressed, block=1x1x1)
|
|
127
|
+
writeU32(buf, 120, 0);
|
|
128
|
+
// descriptor block words 4-5: bytesPlane[0..7] = 4 for plane 0 (RGBA8 = 4 bytes), rest 0
|
|
129
|
+
writeU32(buf, 124, 4); // bytesPlane0=4
|
|
130
|
+
writeU32(buf, 128, 0); // bytesPlane1..7 = 0
|
|
131
|
+
// sample 0 (16 bytes): R8G8B8A8
|
|
132
|
+
// word 0: bitOffset(8)=0, bitLength(12)=7 (actual 8 - 1), channelType(8)=0 (RED/RGB), qualifiers(4)=0
|
|
133
|
+
writeU32(buf, 132, 7 << 12); // bitLength=7, channelType=0 (RED — means RGBA combined sample)
|
|
134
|
+
// word 1: samplePosition[0..3] = 0
|
|
135
|
+
writeU32(buf, 136, 0);
|
|
136
|
+
// word 2: sampleLower = 0
|
|
137
|
+
writeU32(buf, 140, 0);
|
|
138
|
+
// word 3: sampleUpper = UINT32_MAX for UNORM
|
|
139
|
+
writeU32(buf, 144, 0xffffffff);
|
|
140
|
+
|
|
141
|
+
// --- KV data: empty (kvdByteLength=0) ---
|
|
142
|
+
|
|
143
|
+
// --- level 0 pixel data (offset 148) ---
|
|
144
|
+
// Fill with known pattern: pixel(x,y) = (r,g,b,a) based on position
|
|
145
|
+
for (let y = 0; y < H; y++) {
|
|
146
|
+
for (let x = 0; x < W; x++) {
|
|
147
|
+
const px = levelPayloadOffset + (y * W + x) * 4;
|
|
148
|
+
buf[px] = (x * 3 + y * 7) & 0xff; // R
|
|
149
|
+
buf[px + 1] = (x * 5 + y * 11) & 0xff; // G
|
|
150
|
+
buf[px + 2] = (x * 13 + y * 17) & 0xff; // B
|
|
151
|
+
buf[px + 3] = 255; // A
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return buf;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const scheme0Fixture = buildScheme0Fixture();
|
|
159
|
+
|
|
160
|
+
describe('KTX2 parse — scheme=0 uncompressed (w21)', () => {
|
|
161
|
+
it('parses header fields from scheme=0 fixture', async () => {
|
|
162
|
+
const result = await parseKtx2(scheme0Fixture);
|
|
163
|
+
expect(result.ok).toBe(true);
|
|
164
|
+
if (!result.ok) throw new Error('parse failed');
|
|
165
|
+
|
|
166
|
+
const h = result.value.header;
|
|
167
|
+
expect(h.vkFormat).toBe(37); // VK_FORMAT_R8G8B8A8_UNORM
|
|
168
|
+
expect(h.typeSize).toBe(4);
|
|
169
|
+
expect(h.pixelWidth).toBe(64);
|
|
170
|
+
expect(h.pixelHeight).toBe(64);
|
|
171
|
+
expect(h.pixelDepth).toBe(0);
|
|
172
|
+
expect(h.layerCount).toBe(0);
|
|
173
|
+
expect(h.faceCount).toBe(1);
|
|
174
|
+
expect(h.levelCount).toBe(1);
|
|
175
|
+
expect(h.supercompressionScheme).toBe(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('parses index fields (DFD/KVD/SGD offsets)', async () => {
|
|
179
|
+
const result = await parseKtx2(scheme0Fixture);
|
|
180
|
+
expect(result.ok).toBe(true);
|
|
181
|
+
if (!result.ok) throw new Error('parse failed');
|
|
182
|
+
|
|
183
|
+
const idx = result.value.index;
|
|
184
|
+
// dfdByteOffset should be after header + level index = 104
|
|
185
|
+
expect(idx.dfdByteOffset).toBe(104);
|
|
186
|
+
expect(idx.dfdByteLength).toBe(44);
|
|
187
|
+
expect(idx.kvdByteOffset).toBe(148);
|
|
188
|
+
expect(idx.kvdByteLength).toBe(0);
|
|
189
|
+
expect(idx.sgdByteOffset).toBe(0);
|
|
190
|
+
expect(idx.sgdByteLength).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('parses level index entries', async () => {
|
|
194
|
+
const result = await parseKtx2(scheme0Fixture);
|
|
195
|
+
expect(result.ok).toBe(true);
|
|
196
|
+
if (!result.ok) throw new Error('parse failed');
|
|
197
|
+
|
|
198
|
+
expect(result.value.levelIndex.length).toBe(1);
|
|
199
|
+
// biome-ignore lint/style/noNonNullAssertion: previous assertion ensures levelIndex[0] exists
|
|
200
|
+
const l0 = result.value.levelIndex[0]!;
|
|
201
|
+
expect(l0.byteOffset).toBe(148);
|
|
202
|
+
expect(l0.byteLength).toBe(16384); // 64*64*4
|
|
203
|
+
expect(l0.uncompressedByteLength).toBe(16384); // scheme=0: same as byteLength
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('parses DFD descriptor block', async () => {
|
|
207
|
+
const result = await parseKtx2(scheme0Fixture);
|
|
208
|
+
expect(result.ok).toBe(true);
|
|
209
|
+
if (!result.ok) throw new Error('parse failed');
|
|
210
|
+
|
|
211
|
+
const dfd = result.value.dfd;
|
|
212
|
+
expect(dfd).toBeDefined();
|
|
213
|
+
expect(dfd?.totalSize).toBe(44);
|
|
214
|
+
expect(dfd?.colorModel).toBe(1); // RGBSDA
|
|
215
|
+
expect(dfd?.colorPrimaries).toBe(1); // BT709/SRGB
|
|
216
|
+
expect(dfd?.transferFunction).toBe(1); // LINEAR
|
|
217
|
+
expect(dfd?.samples.length).toBe(1);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('ktx2LevelsToRGBA returns correct pixel bytes for scheme=0', async () => {
|
|
221
|
+
const parsed = await parseKtx2(scheme0Fixture);
|
|
222
|
+
expect(parsed.ok).toBe(true);
|
|
223
|
+
if (!parsed.ok) throw new Error('parse failed');
|
|
224
|
+
|
|
225
|
+
const rgba = await ktx2LevelsToRGBA(parsed.value, 0);
|
|
226
|
+
expect(rgba.ok).toBe(true);
|
|
227
|
+
if (!rgba.ok) throw new Error('levelsToRGBA failed');
|
|
228
|
+
|
|
229
|
+
const pixels = rgba.value;
|
|
230
|
+
expect(pixels.length).toBe(16384);
|
|
231
|
+
|
|
232
|
+
// Verify pixel at (0,0): R = (0*3 + 0*7) & 0xff = 0, G = 0, B = 0, A = 255
|
|
233
|
+
expect(pixels[0]).toBe(0);
|
|
234
|
+
expect(pixels[1]).toBe(0);
|
|
235
|
+
expect(pixels[2]).toBe(0);
|
|
236
|
+
expect(pixels[3]).toBe(255);
|
|
237
|
+
|
|
238
|
+
// Verify pixel at (63,63): R = (63*3 + 63*7) & 0xff = (189+441) & 0xff = 630 & 0xff = 118
|
|
239
|
+
const lastPx = 16384 - 4;
|
|
240
|
+
expect(pixels[lastPx]).toBe((63 * 3 + 63 * 7) & 0xff); // 118
|
|
241
|
+
expect(pixels[lastPx + 1]).toBe((63 * 5 + 63 * 11) & 0xff); // 252
|
|
242
|
+
expect(pixels[lastPx + 2]).toBe((63 * 13 + 63 * 17) & 0xff); // 118
|
|
243
|
+
expect(pixels[lastPx + 3]).toBe(255);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('parses empty KV metadata', async () => {
|
|
247
|
+
const result = await parseKtx2(scheme0Fixture);
|
|
248
|
+
expect(result.ok).toBe(true);
|
|
249
|
+
if (!result.ok) throw new Error('parse failed');
|
|
250
|
+
|
|
251
|
+
expect(result.value.kvEntries).toEqual([]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('SG descriptor is empty for scheme=0', async () => {
|
|
255
|
+
const result = await parseKtx2(scheme0Fixture);
|
|
256
|
+
expect(result.ok).toBe(true);
|
|
257
|
+
if (!result.ok) throw new Error('parse failed');
|
|
258
|
+
|
|
259
|
+
expect(result.value.sgd).toBeNull();
|
|
260
|
+
// sgdByteLength is 0 for scheme=0
|
|
261
|
+
expect(result.value.index.sgdByteLength).toBe(0);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { 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 (12 bytes).
|
|
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=2 KTX2 fixture: same RGBA content as scheme=0,
|
|
25
|
+
* but level payload is zstd-compressed.
|
|
26
|
+
*/
|
|
27
|
+
async function buildScheme2Fixture(): Promise<Uint8Array> {
|
|
28
|
+
const W = 64;
|
|
29
|
+
const H = 64;
|
|
30
|
+
const pixelBytes = W * H * 4; // 16384
|
|
31
|
+
|
|
32
|
+
// Generate raw RGBA pixels (same pattern as scheme=0)
|
|
33
|
+
const rawPixels = new Uint8Array(pixelBytes);
|
|
34
|
+
for (let y = 0; y < H; y++) {
|
|
35
|
+
for (let x = 0; x < W; x++) {
|
|
36
|
+
const px = (y * W + x) * 4;
|
|
37
|
+
rawPixels[px] = (x * 3 + y * 7) & 0xff;
|
|
38
|
+
rawPixels[px + 1] = (x * 5 + y * 11) & 0xff;
|
|
39
|
+
rawPixels[px + 2] = (x * 13 + y * 17) & 0xff;
|
|
40
|
+
rawPixels[px + 3] = 255;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Compress with zstd
|
|
45
|
+
const compResult = await compressZstd(rawPixels);
|
|
46
|
+
if (!compResult.ok) throw new Error(`compressZstd failed: ${compResult.error.code}`);
|
|
47
|
+
const compressedPayload = compResult.value;
|
|
48
|
+
|
|
49
|
+
// DFD descriptor: minimal RGBA8
|
|
50
|
+
const dfdBlockSize = 40;
|
|
51
|
+
const dfdTotalSize = 4 + dfdBlockSize; // 44
|
|
52
|
+
|
|
53
|
+
// Level payload offset is right after index + level index + DFD + empty KV + empty SGD
|
|
54
|
+
// Header: 80 bytes, levelIndex (1 entry): 24 bytes, total: 104
|
|
55
|
+
// DFD at 104, length 44, ends at 148
|
|
56
|
+
// KV: empty, so kvdByteOffset=148, kvdByteLength=0
|
|
57
|
+
// SGD: empty (scheme=2), so sgdByteOffset=0, sgdByteLength=0 (per spec §7: Zstandard has no global data)
|
|
58
|
+
// Level payload at 148 (mipPadding not needed for scheme!=0 per spec §8.1)
|
|
59
|
+
const levelPayloadOffset = 148;
|
|
60
|
+
const totalSize = levelPayloadOffset + compressedPayload.length;
|
|
61
|
+
|
|
62
|
+
const buf = new Uint8Array(totalSize);
|
|
63
|
+
|
|
64
|
+
// identifier
|
|
65
|
+
buf.set(KTX2_IDENTIFIER, 0);
|
|
66
|
+
|
|
67
|
+
// header
|
|
68
|
+
writeU32(buf, 12, 37); // vkFormat = VK_FORMAT_R8G8B8A8_UNORM
|
|
69
|
+
writeU32(buf, 16, 4); // typeSize
|
|
70
|
+
writeU32(buf, 20, W);
|
|
71
|
+
writeU32(buf, 24, H);
|
|
72
|
+
writeU32(buf, 28, 0);
|
|
73
|
+
writeU32(buf, 32, 0);
|
|
74
|
+
writeU32(buf, 36, 1);
|
|
75
|
+
writeU32(buf, 40, 1);
|
|
76
|
+
writeU32(buf, 44, 2); // supercompressionScheme = 2 (Zstandard)
|
|
77
|
+
|
|
78
|
+
// index
|
|
79
|
+
writeU32(buf, 48, 104); // dfdByteOffset
|
|
80
|
+
writeU32(buf, 52, dfdTotalSize);
|
|
81
|
+
writeU32(buf, 56, 148); // kvdByteOffset (after DFD)
|
|
82
|
+
writeU32(buf, 60, 0); // kvdByteLength = 0
|
|
83
|
+
writeU64(buf, 64, 0); // sgdByteOffset = 0
|
|
84
|
+
writeU64(buf, 72, 0); // sgdByteLength = 0 (spec §7: Zstandard has no global data)
|
|
85
|
+
|
|
86
|
+
// level index
|
|
87
|
+
writeU64(buf, 80, levelPayloadOffset);
|
|
88
|
+
writeU64(buf, 88, compressedPayload.length);
|
|
89
|
+
writeU64(buf, 96, pixelBytes); // uncompressedByteLength
|
|
90
|
+
|
|
91
|
+
// DFD
|
|
92
|
+
writeU32(buf, 104, dfdTotalSize);
|
|
93
|
+
writeU32(buf, 108, 0);
|
|
94
|
+
writeU32(buf, 112, (40 << 16) | 2);
|
|
95
|
+
writeU32(buf, 116, (1 << 16) | (1 << 8) | 1);
|
|
96
|
+
writeU32(buf, 120, 0);
|
|
97
|
+
writeU32(buf, 124, 4);
|
|
98
|
+
writeU32(buf, 128, 0);
|
|
99
|
+
writeU32(buf, 132, 7 << 12);
|
|
100
|
+
writeU32(buf, 136, 0);
|
|
101
|
+
writeU32(buf, 140, 0);
|
|
102
|
+
writeU32(buf, 144, 0xffffffff);
|
|
103
|
+
|
|
104
|
+
// level payload (zstd-compressed)
|
|
105
|
+
buf.set(compressedPayload, levelPayloadOffset);
|
|
106
|
+
|
|
107
|
+
return buf;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let scheme2Fixture: Uint8Array;
|
|
111
|
+
let expectedRawPixels: Uint8Array;
|
|
112
|
+
|
|
113
|
+
describe('KTX2 parse — scheme=2 zstd supercompression (w22)', () => {
|
|
114
|
+
beforeAll(async () => {
|
|
115
|
+
scheme2Fixture = await buildScheme2Fixture();
|
|
116
|
+
|
|
117
|
+
// Build expected raw pixels for comparison
|
|
118
|
+
const W = 64;
|
|
119
|
+
const H = 64;
|
|
120
|
+
expectedRawPixels = new Uint8Array(W * H * 4);
|
|
121
|
+
for (let y = 0; y < H; y++) {
|
|
122
|
+
for (let x = 0; x < W; x++) {
|
|
123
|
+
const px = (y * W + x) * 4;
|
|
124
|
+
expectedRawPixels[px] = (x * 3 + y * 7) & 0xff;
|
|
125
|
+
expectedRawPixels[px + 1] = (x * 5 + y * 11) & 0xff;
|
|
126
|
+
expectedRawPixels[px + 2] = (x * 13 + y * 17) & 0xff;
|
|
127
|
+
expectedRawPixels[px + 3] = 255;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('parses header with supercompressionScheme=2', async () => {
|
|
133
|
+
const result = await parseKtx2(scheme2Fixture);
|
|
134
|
+
expect(result.ok).toBe(true);
|
|
135
|
+
if (!result.ok) throw new Error('parse failed');
|
|
136
|
+
|
|
137
|
+
expect(result.value.header.supercompressionScheme).toBe(2);
|
|
138
|
+
expect(result.value.header.pixelWidth).toBe(64);
|
|
139
|
+
expect(result.value.header.pixelHeight).toBe(64);
|
|
140
|
+
expect(result.value.header.levelCount).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('sgdByteLength is 0 (Zstandard has no global data per spec §7)', async () => {
|
|
144
|
+
const result = await parseKtx2(scheme2Fixture);
|
|
145
|
+
expect(result.ok).toBe(true);
|
|
146
|
+
if (!result.ok) throw new Error('parse failed');
|
|
147
|
+
|
|
148
|
+
expect(result.value.index.sgdByteLength).toBe(0);
|
|
149
|
+
expect(result.value.sgd).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('level index has compressed byteLength < uncompressedByteLength', async () => {
|
|
153
|
+
const result = await parseKtx2(scheme2Fixture);
|
|
154
|
+
expect(result.ok).toBe(true);
|
|
155
|
+
if (!result.ok) throw new Error('parse failed');
|
|
156
|
+
|
|
157
|
+
// biome-ignore lint/style/noNonNullAssertion: previous assertion ensures levelIndex[0] exists
|
|
158
|
+
const l0 = result.value.levelIndex[0]!;
|
|
159
|
+
expect(l0.byteLength).toBeGreaterThan(0);
|
|
160
|
+
expect(l0.byteLength).toBeLessThan(l0.uncompressedByteLength);
|
|
161
|
+
expect(l0.uncompressedByteLength).toBe(16384);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('ktx2LevelsToRGBA decompresses scheme=2 and returns correct pixels', async () => {
|
|
165
|
+
const parsed = await parseKtx2(scheme2Fixture);
|
|
166
|
+
expect(parsed.ok).toBe(true);
|
|
167
|
+
if (!parsed.ok) throw new Error('parse failed');
|
|
168
|
+
|
|
169
|
+
const rgba = await ktx2LevelsToRGBA(parsed.value, 0);
|
|
170
|
+
expect(rgba.ok).toBe(true);
|
|
171
|
+
if (!rgba.ok) throw new Error('levelsToRGBA failed');
|
|
172
|
+
|
|
173
|
+
const pixels = rgba.value;
|
|
174
|
+
expect(pixels.length).toBe(16384);
|
|
175
|
+
|
|
176
|
+
// Compare all pixels byte-by-byte against expected
|
|
177
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
178
|
+
// biome-ignore lint/style/noNonNullAssertion: index within bounds of expectedRawPixels array verified above
|
|
179
|
+
expect(pixels[i]).toBe(expectedRawPixels[i]!);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('scheme=2 decompressed pixels match scheme=0 pixel pattern byte-identical', async () => {
|
|
184
|
+
// This asserts AC-04: same zstd decoder gives same result
|
|
185
|
+
// Compare just key pixels (first, middle, last) to keep test fast
|
|
186
|
+
const parsed = await parseKtx2(scheme2Fixture);
|
|
187
|
+
expect(parsed.ok).toBe(true);
|
|
188
|
+
if (!parsed.ok) throw new Error('parse failed');
|
|
189
|
+
|
|
190
|
+
const rgba = await ktx2LevelsToRGBA(parsed.value, 0);
|
|
191
|
+
expect(rgba.ok).toBe(true);
|
|
192
|
+
if (!rgba.ok) throw new Error('levelsToRGBA failed');
|
|
193
|
+
const pixels = rgba.value;
|
|
194
|
+
|
|
195
|
+
// First pixel (0,0)
|
|
196
|
+
expect(pixels[0]).toBe(0);
|
|
197
|
+
expect(pixels[1]).toBe(0);
|
|
198
|
+
expect(pixels[2]).toBe(0);
|
|
199
|
+
expect(pixels[3]).toBe(255);
|
|
200
|
+
|
|
201
|
+
// Last pixel (63,63)
|
|
202
|
+
const lastPx = 16384 - 4;
|
|
203
|
+
expect(pixels[lastPx]).toBe((63 * 3 + 63 * 7) & 0xff);
|
|
204
|
+
expect(pixels[lastPx + 1]).toBe((63 * 5 + 63 * 11) & 0xff);
|
|
205
|
+
expect(pixels[lastPx + 2]).toBe((63 * 13 + 63 * 17) & 0xff);
|
|
206
|
+
expect(pixels[lastPx + 3]).toBe(255);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('structural check: levels-to-RGBA decompresses fewer bytes than raw size', async () => {
|
|
210
|
+
// Verify that compression actually happened (zstd produced smaller data)
|
|
211
|
+
const parsed = await parseKtx2(scheme2Fixture);
|
|
212
|
+
expect(parsed.ok).toBe(true);
|
|
213
|
+
if (!parsed.ok) throw new Error('parse failed');
|
|
214
|
+
|
|
215
|
+
// biome-ignore lint/style/noNonNullAssertion: previous assertion ensures levelIndex[0] exists
|
|
216
|
+
const l0 = parsed.value.levelIndex[0]!;
|
|
217
|
+
// zstd should compress RGBA8 patterned data well below 16384 bytes
|
|
218
|
+
expect(l0.byteLength).toBeLessThan(16384);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { CodecErrorCode } from '@forgeax/engine-codec';
|
|
2
|
+
import { codecError, parseKtx2 } from '@forgeax/engine-codec';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* KTX2 scheme=1 pass-through + transcode-failed error unit tests (w10).
|
|
7
|
+
*
|
|
8
|
+
* (a) A byte-level scheme=1 (BasisLZ) KTX2 header parses successfully -- parseKtx2
|
|
9
|
+
* no longer rejects scheme=1 with ktx2-unsupported-scheme (F-1 single-point
|
|
10
|
+
* gate at ktx2.ts:308 opened for the Basis arm).
|
|
11
|
+
* (b) A scheme=3 (ZLIB) header still rejects with ktx2-unsupported-scheme (the
|
|
12
|
+
* old rejection is preserved -- contract seam AC-11(c): extend, do not
|
|
13
|
+
* rewrite the container parser).
|
|
14
|
+
* (c) The transcode-failed CodecError object carries .code / .detail (with the
|
|
15
|
+
* sourceFormat/targetFormat discriminant) / a non-empty actionable .hint
|
|
16
|
+
* (AC-10 add-only-minor union member).
|
|
17
|
+
*
|
|
18
|
+
* Does NOT execute real transcode (M5 integration test's job); this only builds
|
|
19
|
+
* byte-level headers and inspects error object shape.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const KTX2_IDENTIFIER = new Uint8Array([
|
|
23
|
+
0xab, 0x4b, 0x54, 0x58, 0x20, 0x32, 0x30, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function writeU32(out: Uint8Array, offset: number, value: number): void {
|
|
27
|
+
new DataView(out.buffer, out.byteOffset + offset, 4).setUint32(0, value, true);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeU64(out: Uint8Array, offset: number, value: number): void {
|
|
31
|
+
const lo = value >>> 0;
|
|
32
|
+
const hi = (value - lo) / 0x100000000;
|
|
33
|
+
new DataView(out.buffer, out.byteOffset + offset, 8).setUint32(0, lo, true);
|
|
34
|
+
new DataView(out.buffer, out.byteOffset + offset + 4, 4).setUint32(0, hi, true);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a minimal KTX2 header with the given supercompression scheme. The level
|
|
39
|
+
* payload is a small opaque blob (not real Basis data -- parseKtx2 does not
|
|
40
|
+
* interpret it). One level, no DFD / KV / SGD, so the parser exercises only the
|
|
41
|
+
* header + level index + scheme gate.
|
|
42
|
+
*/
|
|
43
|
+
function buildHeaderFixture(scheme: number): Uint8Array {
|
|
44
|
+
const W = 8;
|
|
45
|
+
const H = 8;
|
|
46
|
+
const payload = new Uint8Array(64);
|
|
47
|
+
for (let i = 0; i < payload.length; i++) payload[i] = (i * 17 + 3) & 0xff;
|
|
48
|
+
|
|
49
|
+
// header(80) + level index (1 entry = 24) = 104; payload at 104
|
|
50
|
+
const levelPayloadOffset = 104;
|
|
51
|
+
const totalSize = levelPayloadOffset + payload.length;
|
|
52
|
+
const buf = new Uint8Array(totalSize);
|
|
53
|
+
|
|
54
|
+
buf.set(KTX2_IDENTIFIER, 0);
|
|
55
|
+
writeU32(buf, 12, 0); // vkFormat = VK_FORMAT_UNDEFINED (Basis payload)
|
|
56
|
+
writeU32(buf, 16, 1); // typeSize
|
|
57
|
+
writeU32(buf, 20, W);
|
|
58
|
+
writeU32(buf, 24, H);
|
|
59
|
+
writeU32(buf, 28, 0);
|
|
60
|
+
writeU32(buf, 32, 0);
|
|
61
|
+
writeU32(buf, 36, 1);
|
|
62
|
+
writeU32(buf, 40, 1); // levelCount
|
|
63
|
+
writeU32(buf, 44, scheme); // supercompressionScheme
|
|
64
|
+
|
|
65
|
+
// index: no DFD / KV / SGD
|
|
66
|
+
writeU32(buf, 48, 0); // dfdByteOffset
|
|
67
|
+
writeU32(buf, 52, 0); // dfdByteLength
|
|
68
|
+
writeU32(buf, 56, 0); // kvdByteOffset
|
|
69
|
+
writeU32(buf, 60, 0); // kvdByteLength
|
|
70
|
+
writeU64(buf, 64, 0); // sgdByteOffset
|
|
71
|
+
writeU64(buf, 72, 0); // sgdByteLength
|
|
72
|
+
|
|
73
|
+
// level index (smallest-first; only 1 level)
|
|
74
|
+
writeU64(buf, 80, levelPayloadOffset);
|
|
75
|
+
writeU64(buf, 88, payload.length);
|
|
76
|
+
writeU64(buf, 96, payload.length); // uncompressedByteLength
|
|
77
|
+
|
|
78
|
+
buf.set(payload, levelPayloadOffset);
|
|
79
|
+
return buf;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('parseKtx2 — scheme=1 (BasisLZ) pass-through (w10)', () => {
|
|
83
|
+
it('scheme=1 parses successfully (no ktx2-unsupported-scheme)', async () => {
|
|
84
|
+
const result = await parseKtx2(buildHeaderFixture(1));
|
|
85
|
+
expect(result.ok).toBe(true);
|
|
86
|
+
if (!result.ok) throw new Error(`expected ok, got ${result.error.code}`);
|
|
87
|
+
expect(result.value.header.supercompressionScheme).toBe(1);
|
|
88
|
+
expect(result.value.header.pixelWidth).toBe(8);
|
|
89
|
+
expect(result.value.levelIndex.length).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('scheme=0 still parses (regression guard)', async () => {
|
|
93
|
+
const result = await parseKtx2(buildHeaderFixture(0));
|
|
94
|
+
expect(result.ok).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('scheme=2 still parses (regression guard)', async () => {
|
|
98
|
+
const result = await parseKtx2(buildHeaderFixture(2));
|
|
99
|
+
expect(result.ok).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('parseKtx2 — scheme=3 (ZLIB) still rejected (w10)', () => {
|
|
104
|
+
it('scheme=3 returns ktx2-unsupported-scheme echoing the scheme value', async () => {
|
|
105
|
+
const result = await parseKtx2(buildHeaderFixture(3));
|
|
106
|
+
expect(result.ok).toBe(false);
|
|
107
|
+
if (result.ok) throw new Error('expected rejection');
|
|
108
|
+
expect(result.error.code).toBe('ktx2-unsupported-scheme');
|
|
109
|
+
expect(result.error.detail).toEqual({ scheme: 3 });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('scheme=4 (arbitrary future scheme) also rejected', async () => {
|
|
113
|
+
const result = await parseKtx2(buildHeaderFixture(4));
|
|
114
|
+
expect(result.ok).toBe(false);
|
|
115
|
+
if (result.ok) throw new Error('expected rejection');
|
|
116
|
+
expect(result.error.code).toBe('ktx2-unsupported-scheme');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('transcode-failed error object shape (AC-10, w10)', () => {
|
|
121
|
+
it('code / detail (sourceFormat + targetFormat) / hint all present', () => {
|
|
122
|
+
const err = codecError('transcode-failed', {
|
|
123
|
+
sourceFormat: 'uastc-ldr',
|
|
124
|
+
targetFormat: 'bc7-rgba-unorm',
|
|
125
|
+
});
|
|
126
|
+
expect(err.ok).toBe(false);
|
|
127
|
+
expect(err.error.code).toBe('transcode-failed');
|
|
128
|
+
expect(err.error.detail).toEqual({
|
|
129
|
+
sourceFormat: 'uastc-ldr',
|
|
130
|
+
targetFormat: 'bc7-rgba-unorm',
|
|
131
|
+
});
|
|
132
|
+
expect(typeof err.error.hint).toBe('string');
|
|
133
|
+
expect(err.error.hint.length).toBeGreaterThan(0);
|
|
134
|
+
expect(typeof err.error.expected).toBe('string');
|
|
135
|
+
expect(err.error.expected.length).toBeGreaterThan(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('transcode-failed is a member of the CodecErrorCode union', () => {
|
|
139
|
+
const code: CodecErrorCode = 'transcode-failed';
|
|
140
|
+
expect(code).toBe('transcode-failed');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('pre-existing codes remain intact (add-only-minor, not a rewrite)', () => {
|
|
144
|
+
const stillThere: CodecErrorCode[] = [
|
|
145
|
+
'decompression-failed',
|
|
146
|
+
'codec-init-failed',
|
|
147
|
+
'ktx2-parse-failed',
|
|
148
|
+
'ktx2-unsupported-scheme',
|
|
149
|
+
];
|
|
150
|
+
for (const c of stillThere) {
|
|
151
|
+
const e = codecError(c, dummyDetailFor(c));
|
|
152
|
+
expect(e.error.code).toBe(c);
|
|
153
|
+
expect(e.error.hint.length).toBeGreaterThan(0);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/** Build a minimal valid detail payload for each pre-existing code. */
|
|
159
|
+
function dummyDetailFor(code: CodecErrorCode): never {
|
|
160
|
+
switch (code) {
|
|
161
|
+
case 'decompression-failed':
|
|
162
|
+
return { reason: 'test' } as never;
|
|
163
|
+
case 'codec-init-failed':
|
|
164
|
+
return { stage: 'test' } as never;
|
|
165
|
+
case 'ktx2-parse-failed':
|
|
166
|
+
return { reason: 'test' } as never;
|
|
167
|
+
case 'ktx2-unsupported-scheme':
|
|
168
|
+
return { scheme: 3 } as never;
|
|
169
|
+
case 'transcode-failed':
|
|
170
|
+
return { sourceFormat: 'uastc-ldr', targetFormat: 'bc7-rgba-unorm' } as never;
|
|
171
|
+
case 'ktx2-encode-failed':
|
|
172
|
+
return { mode: 'uastc-ldr', reason: 'test' } as never;
|
|
173
|
+
default: {
|
|
174
|
+
const _exhaustive: never = code;
|
|
175
|
+
return _exhaustive;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|