@forgeax/engine-codec 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +268 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/basis-encode-fast-preset.unit.test.d.ts +2 -0
  5. package/dist/__tests__/basis-encode-fast-preset.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/basis-encode-lifecycle.unit.test.d.ts +2 -0
  7. package/dist/__tests__/basis-encode-lifecycle.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/basis-encode-mode-owner.test-d.d.ts +2 -0
  9. package/dist/__tests__/basis-encode-mode-owner.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/basis-transcoder.unit.test.d.ts +2 -0
  11. package/dist/__tests__/basis-transcoder.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/block-format.unit.test.d.ts +2 -0
  13. package/dist/__tests__/block-format.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/byte-identical-falsification.test.d.ts +2 -0
  15. package/dist/__tests__/byte-identical-falsification.test.d.ts.map +1 -0
  16. package/dist/__tests__/codec-errors.test-d.d.ts +2 -0
  17. package/dist/__tests__/codec-errors.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/codec-errors.unit.test.d.ts +2 -0
  19. package/dist/__tests__/codec-errors.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/compression-field.unit.test.d.ts +19 -0
  21. package/dist/__tests__/compression-field.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/encode-pixel-limit.unit.test.d.ts +2 -0
  23. package/dist/__tests__/encode-pixel-limit.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/encode-roundtrip.unit.test.d.ts +2 -0
  25. package/dist/__tests__/encode-roundtrip.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/ktx2-basis-capability-matrix.unit.test.d.ts +2 -0
  27. package/dist/__tests__/ktx2-basis-capability-matrix.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/ktx2-errors.test.d.ts +2 -0
  29. package/dist/__tests__/ktx2-errors.test.d.ts.map +1 -0
  30. package/dist/__tests__/ktx2-parse-scheme0.test.d.ts +2 -0
  31. package/dist/__tests__/ktx2-parse-scheme0.test.d.ts.map +1 -0
  32. package/dist/__tests__/ktx2-parse-scheme2.test.d.ts +2 -0
  33. package/dist/__tests__/ktx2-parse-scheme2.test.d.ts.map +1 -0
  34. package/dist/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.d.ts +2 -0
  35. package/dist/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/ktx2-to-texture.integration.test.d.ts +2 -0
  37. package/dist/__tests__/ktx2-to-texture.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/runtime-validation.unit.test.d.ts +2 -0
  39. package/dist/__tests__/runtime-validation.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/transcode-select-target.unit.test.d.ts +2 -0
  41. package/dist/__tests__/transcode-select-target.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/wasm-init-smoke.test.d.ts +2 -0
  43. package/dist/__tests__/wasm-init-smoke.test.d.ts.map +1 -0
  44. package/dist/__tests__/zstd-deterministic.unit.test.d.ts +2 -0
  45. package/dist/__tests__/zstd-deterministic.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/zstd-encode-singleton.unit.test.d.ts +2 -0
  47. package/dist/__tests__/zstd-encode-singleton.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/zstd-roundtrip.unit.test.d.ts +2 -0
  49. package/dist/__tests__/zstd-roundtrip.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/zstd-singleton.unit.test.d.ts +2 -0
  51. package/dist/__tests__/zstd-singleton.unit.test.d.ts.map +1 -0
  52. package/dist/basis-transcoder.d.ts +86 -0
  53. package/dist/basis-transcoder.d.ts.map +1 -0
  54. package/dist/block-format.d.ts +41 -0
  55. package/dist/block-format.d.ts.map +1 -0
  56. package/dist/encode/basis-encode.d.ts +68 -0
  57. package/dist/encode/basis-encode.d.ts.map +1 -0
  58. package/dist/encode/index.d.ts +10 -0
  59. package/dist/encode/index.d.ts.map +1 -0
  60. package/dist/encode/index.mjs +207 -0
  61. package/dist/encode/index.mjs.map +1 -0
  62. package/dist/encode-impl.d.ts +30 -0
  63. package/dist/encode-impl.d.ts.map +1 -0
  64. package/dist/errors.d.ts +61 -0
  65. package/dist/errors.d.ts.map +1 -0
  66. package/dist/index.d.ts +17 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.mjs +672 -0
  69. package/dist/index.mjs.map +1 -0
  70. package/dist/ktx2.d.ts +111 -0
  71. package/dist/ktx2.d.ts.map +1 -0
  72. package/dist/transcode.d.ts +55 -0
  73. package/dist/transcode.d.ts.map +1 -0
  74. package/dist/wasm/basis-types.d.ts +179 -0
  75. package/dist/wasm/basis-types.d.ts.map +1 -0
  76. package/dist/zstd.d.ts +28 -0
  77. package/dist/zstd.d.ts.map +1 -0
  78. package/package.json +71 -0
  79. package/pkg/basis_transcoder.mjs +2 -0
  80. package/pkg/basis_transcoder.wasm +0 -0
  81. package/pkg/encode/basis_encoder.mjs +2 -0
  82. package/pkg/encode/basis_encoder.wasm +0 -0
  83. package/scripts/build-wasm.mjs +370 -0
  84. package/scripts/content-key.mjs +85 -0
  85. package/scripts/ensure-wasm.mjs +38 -0
  86. package/scripts/fetch-basis.mjs +150 -0
  87. package/scripts/fetch-wasm.mjs +97 -0
  88. package/src/__tests__/basis-encode-fast-preset.unit.test.ts +204 -0
  89. package/src/__tests__/basis-encode-lifecycle.unit.test.ts +249 -0
  90. package/src/__tests__/basis-encode-mode-owner.test-d.ts +39 -0
  91. package/src/__tests__/basis-transcoder.unit.test.ts +502 -0
  92. package/src/__tests__/block-format.unit.test.ts +215 -0
  93. package/src/__tests__/byte-identical-falsification.test.ts +164 -0
  94. package/src/__tests__/codec-errors.test-d.ts +67 -0
  95. package/src/__tests__/codec-errors.unit.test.ts +128 -0
  96. package/src/__tests__/compression-field.unit.test.ts +245 -0
  97. package/src/__tests__/encode-pixel-limit.unit.test.ts +117 -0
  98. package/src/__tests__/encode-roundtrip.unit.test.ts +239 -0
  99. package/src/__tests__/ktx2-basis-capability-matrix.unit.test.ts +241 -0
  100. package/src/__tests__/ktx2-errors.test.ts +239 -0
  101. package/src/__tests__/ktx2-parse-scheme0.test.ts +263 -0
  102. package/src/__tests__/ktx2-parse-scheme2.test.ts +220 -0
  103. package/src/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.ts +178 -0
  104. package/src/__tests__/ktx2-to-texture.integration.test.ts +384 -0
  105. package/src/__tests__/runtime-validation.unit.test.ts +19 -0
  106. package/src/__tests__/transcode-select-target.unit.test.ts +167 -0
  107. package/src/__tests__/wasm-init-smoke.test.ts +102 -0
  108. package/src/__tests__/zstd-deterministic.unit.test.ts +49 -0
  109. package/src/__tests__/zstd-encode-singleton.unit.test.ts +63 -0
  110. package/src/__tests__/zstd-roundtrip.unit.test.ts +87 -0
  111. package/src/__tests__/zstd-singleton.unit.test.ts +105 -0
  112. package/src/basis-transcoder.ts +380 -0
  113. package/src/block-format.ts +128 -0
  114. package/src/encode/basis-encode.ts +264 -0
  115. package/src/encode/index.ts +19 -0
  116. package/src/encode-impl.ts +103 -0
  117. package/src/errors.ts +79 -0
  118. package/src/index.ts +38 -0
  119. package/src/ktx2.ts +469 -0
  120. package/src/transcode.ts +99 -0
  121. package/src/wasm/basis-types.ts +220 -0
  122. package/src/zstd.ts +85 -0
@@ -0,0 +1,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
+ }