@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,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
blockParamsForFormat,
|
|
3
|
+
bytesPerRow,
|
|
4
|
+
isCompressedFormat,
|
|
5
|
+
rowsPerImage,
|
|
6
|
+
} from '@forgeax/engine-codec';
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* block-format lookup + byte-ratio unit tests (w9).
|
|
11
|
+
*
|
|
12
|
+
* AC-17 VRAM ratio: block-compressed TextureAsset.data (tight-packed, mip 0
|
|
13
|
+
* only here) is >=4x smaller than same-size RGBA8 for BC7/ASTC-4x4 (both 16
|
|
14
|
+
* bytes / 16 texels = 1 bpp block vs 4 bpp RGBA8 => 4x), and >=8x for
|
|
15
|
+
* ETC1S->BC1/ETC1 (8 bytes / 16 texels = 0.5 bpp => 8x).
|
|
16
|
+
*
|
|
17
|
+
* AC-08 non-4-multiple sizes: bytesPerRow = ceil(width/blockW) * bytesPerBlock,
|
|
18
|
+
* rowsPerImage = ceil(height/blockH). Tail blocks (1x1 / 2x2 mip) pad up to a
|
|
19
|
+
* full block.
|
|
20
|
+
*
|
|
21
|
+
* The block table is the shared SSOT (D-6) consumed by both the texture upload
|
|
22
|
+
* path and the equirect upload path.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
describe('blockParamsForFormat — compressed format dimensions (w9)', () => {
|
|
26
|
+
it('bc7-rgba-unorm => 4x4 block, 16 bytes', () => {
|
|
27
|
+
expect(blockParamsForFormat('bc7-rgba-unorm')).toEqual({
|
|
28
|
+
blockW: 4,
|
|
29
|
+
blockH: 4,
|
|
30
|
+
bytesPerBlock: 16,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('bc7-rgba-unorm-srgb => 4x4 block, 16 bytes', () => {
|
|
35
|
+
expect(blockParamsForFormat('bc7-rgba-unorm-srgb')).toEqual({
|
|
36
|
+
blockW: 4,
|
|
37
|
+
blockH: 4,
|
|
38
|
+
bytesPerBlock: 16,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('bc1-rgba-unorm => 4x4 block, 8 bytes', () => {
|
|
43
|
+
expect(blockParamsForFormat('bc1-rgba-unorm')).toEqual({
|
|
44
|
+
blockW: 4,
|
|
45
|
+
blockH: 4,
|
|
46
|
+
bytesPerBlock: 8,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('bc4-r-unorm => 4x4 block, 8 bytes', () => {
|
|
51
|
+
expect(blockParamsForFormat('bc4-r-unorm')).toEqual({
|
|
52
|
+
blockW: 4,
|
|
53
|
+
blockH: 4,
|
|
54
|
+
bytesPerBlock: 8,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('bc5-rg-unorm => 4x4 block, 16 bytes', () => {
|
|
59
|
+
expect(blockParamsForFormat('bc5-rg-unorm')).toEqual({
|
|
60
|
+
blockW: 4,
|
|
61
|
+
blockH: 4,
|
|
62
|
+
bytesPerBlock: 16,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('bc6h-rgb-ufloat => 4x4 block, 16 bytes', () => {
|
|
67
|
+
expect(blockParamsForFormat('bc6h-rgb-ufloat')).toEqual({
|
|
68
|
+
blockW: 4,
|
|
69
|
+
blockH: 4,
|
|
70
|
+
bytesPerBlock: 16,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('etc2-rgba8unorm => 4x4 block, 16 bytes', () => {
|
|
75
|
+
expect(blockParamsForFormat('etc2-rgba8unorm')).toEqual({
|
|
76
|
+
blockW: 4,
|
|
77
|
+
blockH: 4,
|
|
78
|
+
bytesPerBlock: 16,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('etc2-rgb8unorm => 4x4 block, 8 bytes', () => {
|
|
83
|
+
expect(blockParamsForFormat('etc2-rgb8unorm')).toEqual({
|
|
84
|
+
blockW: 4,
|
|
85
|
+
blockH: 4,
|
|
86
|
+
bytesPerBlock: 8,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('eac-r11unorm => 4x4 block, 8 bytes', () => {
|
|
91
|
+
expect(blockParamsForFormat('eac-r11unorm')).toEqual({
|
|
92
|
+
blockW: 4,
|
|
93
|
+
blockH: 4,
|
|
94
|
+
bytesPerBlock: 8,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('eac-rg11unorm => 4x4 block, 16 bytes', () => {
|
|
99
|
+
expect(blockParamsForFormat('eac-rg11unorm')).toEqual({
|
|
100
|
+
blockW: 4,
|
|
101
|
+
blockH: 4,
|
|
102
|
+
bytesPerBlock: 16,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('astc-4x4-unorm => 4x4 block, 16 bytes', () => {
|
|
107
|
+
expect(blockParamsForFormat('astc-4x4-unorm')).toEqual({
|
|
108
|
+
blockW: 4,
|
|
109
|
+
blockH: 4,
|
|
110
|
+
bytesPerBlock: 16,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('astc-4x4-unorm-srgb => 4x4 block, 16 bytes', () => {
|
|
115
|
+
expect(blockParamsForFormat('astc-4x4-unorm-srgb')).toEqual({
|
|
116
|
+
blockW: 4,
|
|
117
|
+
blockH: 4,
|
|
118
|
+
bytesPerBlock: 16,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('uncompressed format (rgba8unorm) returns null block params', () => {
|
|
123
|
+
expect(blockParamsForFormat('rgba8unorm')).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('uncompressed HDR format (rgba16float) returns null block params', () => {
|
|
127
|
+
expect(blockParamsForFormat('rgba16float')).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('isCompressedFormat — guard (w9)', () => {
|
|
132
|
+
it('true for block-compressed formats', () => {
|
|
133
|
+
expect(isCompressedFormat('bc7-rgba-unorm')).toBe(true);
|
|
134
|
+
expect(isCompressedFormat('bc1-rgba-unorm')).toBe(true);
|
|
135
|
+
expect(isCompressedFormat('etc2-rgba8unorm')).toBe(true);
|
|
136
|
+
expect(isCompressedFormat('astc-4x4-unorm-srgb')).toBe(true);
|
|
137
|
+
expect(isCompressedFormat('bc6h-rgb-ufloat')).toBe(true);
|
|
138
|
+
expect(isCompressedFormat('eac-rg11unorm')).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('false for uncompressed formats', () => {
|
|
142
|
+
expect(isCompressedFormat('rgba8unorm')).toBe(false);
|
|
143
|
+
expect(isCompressedFormat('rgba8unorm-srgb')).toBe(false);
|
|
144
|
+
expect(isCompressedFormat('rgba16float')).toBe(false);
|
|
145
|
+
expect(isCompressedFormat('r8unorm')).toBe(false);
|
|
146
|
+
expect(isCompressedFormat('rg8unorm')).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('bytesPerRow / rowsPerImage — ceil logic (w9)', () => {
|
|
151
|
+
it('BC7 64x64: 16 blocks/row * 16 bytes = 256 bytes/row, 16 rows', () => {
|
|
152
|
+
expect(bytesPerRow('bc7-rgba-unorm', 64)).toBe(256);
|
|
153
|
+
expect(rowsPerImage('bc7-rgba-unorm', 64)).toBe(16);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('BC7 65x65 (non-4-multiple): ceil(65/4)=17 blocks => 272 bytes/row, 17 rows', () => {
|
|
157
|
+
expect(bytesPerRow('bc7-rgba-unorm', 65)).toBe(17 * 16);
|
|
158
|
+
expect(rowsPerImage('bc7-rgba-unorm', 65)).toBe(17);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('BC1 63x63: ceil(63/4)=16 blocks => 128 bytes/row, 16 rows', () => {
|
|
162
|
+
expect(bytesPerRow('bc1-rgba-unorm', 63)).toBe(16 * 8);
|
|
163
|
+
expect(rowsPerImage('bc1-rgba-unorm', 63)).toBe(16);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('BC7 1x1 mip tail block pads to a full 4x4 block', () => {
|
|
167
|
+
expect(bytesPerRow('bc7-rgba-unorm', 1)).toBe(16);
|
|
168
|
+
expect(rowsPerImage('bc7-rgba-unorm', 1)).toBe(1);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('BC7 2x2 mip tail block pads to a full 4x4 block', () => {
|
|
172
|
+
expect(bytesPerRow('bc7-rgba-unorm', 2)).toBe(16);
|
|
173
|
+
expect(rowsPerImage('bc7-rgba-unorm', 2)).toBe(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('bytesPerRow throws / returns 0 semantics documented for uncompressed via null', () => {
|
|
177
|
+
// uncompressed formats have no block table entry; callers must branch on
|
|
178
|
+
// isCompressedFormat first. bytesPerRow returns null for these.
|
|
179
|
+
expect(bytesPerRow('rgba8unorm', 64)).toBeNull();
|
|
180
|
+
expect(rowsPerImage('rgba8unorm', 64)).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('byte-ratio assertions vs RGBA8 tight-pack (AC-17, w9)', () => {
|
|
185
|
+
function rgba8Bytes(w: number, h: number): number {
|
|
186
|
+
return w * h * 4;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function compressedBytes(format: GPUTextureFormat, w: number, h: number): number {
|
|
190
|
+
const bpr = bytesPerRow(format, w);
|
|
191
|
+
const rows = rowsPerImage(format, h);
|
|
192
|
+
if (bpr === null || rows === null) throw new Error(`no block params for ${format}`);
|
|
193
|
+
return bpr * rows;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
it('BC7 256x256 is >=4x smaller than RGBA8 tight-pack', () => {
|
|
197
|
+
const ratio = rgba8Bytes(256, 256) / compressedBytes('bc7-rgba-unorm', 256, 256);
|
|
198
|
+
expect(ratio).toBeGreaterThanOrEqual(4);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('ASTC-4x4 256x256 is >=4x smaller than RGBA8 tight-pack', () => {
|
|
202
|
+
const ratio = rgba8Bytes(256, 256) / compressedBytes('astc-4x4-unorm', 256, 256);
|
|
203
|
+
expect(ratio).toBeGreaterThanOrEqual(4);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('BC1 (ETC1S->BC1 delivery) 256x256 is >=8x smaller than RGBA8 tight-pack', () => {
|
|
207
|
+
const ratio = rgba8Bytes(256, 256) / compressedBytes('bc1-rgba-unorm', 256, 256);
|
|
208
|
+
expect(ratio).toBeGreaterThanOrEqual(8);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('ETC2-rgb8 (ETC1->RGB delivery) 256x256 is >=8x smaller than RGBA8 tight-pack', () => {
|
|
212
|
+
const ratio = rgba8Bytes(256, 256) / compressedBytes('etc2-rgb8unorm', 256, 256);
|
|
213
|
+
expect(ratio).toBeGreaterThanOrEqual(8);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// w17 — TDD: POD deep-compare byte-identical + falsification (AC-06)
|
|
2
|
+
//
|
|
3
|
+
// Validates that assets loaded from compressed catalog rows produce the
|
|
4
|
+
// SAME in-memory POD as loading from uncompressed catalog rows. Deep
|
|
5
|
+
// comparison covers all fields (vertices, indices, submeshes, etc.).
|
|
6
|
+
//
|
|
7
|
+
// FALSIFICATION: deliberately flip 1 byte in decompressed output and assert
|
|
8
|
+
// deep comparison FAILS — proves comparator has discriminability, not
|
|
9
|
+
// constant-pass (plan-strategy §5.4 requirement, mirroring feedback 61).
|
|
10
|
+
//
|
|
11
|
+
// TDD-RED before w19+w20 — decompression gate not yet wired.
|
|
12
|
+
//
|
|
13
|
+
// Plan decisions:
|
|
14
|
+
// AC-06: compressed/uncompressed path produces identical POD
|
|
15
|
+
// §5.4: falsification = flip 1 byte → deep comparison must FAIL
|
|
16
|
+
// D-10: all compressed fixture bytes programmatic via codec encode
|
|
17
|
+
|
|
18
|
+
import { describe, expect, it } from 'vitest';
|
|
19
|
+
|
|
20
|
+
describe('w17: POD deep-compare byte-identical + falsification', () => {
|
|
21
|
+
/**
|
|
22
|
+
* Test that `decompressZstd(compressZstd(original))` returns byte-identical
|
|
23
|
+
* output — the core AC-06 round-trip guarantee. This is a codec-level check
|
|
24
|
+
* that the encode/decode pair is lossless.
|
|
25
|
+
*
|
|
26
|
+
* Not TDD-red: this is a pure codec test and the codec is already complete
|
|
27
|
+
* from M1. The compressed→decompressed round-trip is already verified by
|
|
28
|
+
* w2 (zstd-roundtrip.unit.test.ts). We re-verify here for context.
|
|
29
|
+
*/
|
|
30
|
+
it('compressZstd → decompressZstd round-trip is byte-identical', async () => {
|
|
31
|
+
const { decompressZstd } = await import('@forgeax/engine-codec');
|
|
32
|
+
const { compressZstd } = await import('@forgeax/engine-codec/encode');
|
|
33
|
+
|
|
34
|
+
const sizes = [0, 1, 1024, 65536];
|
|
35
|
+
for (const size of sizes) {
|
|
36
|
+
const original = new Uint8Array(size);
|
|
37
|
+
for (let i = 0; i < size; i++) {
|
|
38
|
+
original[i] = (i * 17 + 3) % 256;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const compRes = await compressZstd(original);
|
|
42
|
+
expect(compRes.ok).toBe(true);
|
|
43
|
+
if (!compRes.ok) continue;
|
|
44
|
+
|
|
45
|
+
const decRes = await decompressZstd(compRes.value);
|
|
46
|
+
expect(decRes.ok).toBe(true);
|
|
47
|
+
if (!decRes.ok) continue;
|
|
48
|
+
|
|
49
|
+
// Byte-identical round-trip
|
|
50
|
+
expect(decRes.value).toEqual(original);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* FALSIFICATION: compress original bytes, flip 1 byte in the compressed
|
|
56
|
+
* output, decompress — the result must NOT equal the original (proving
|
|
57
|
+
* the comparator has discriminability).
|
|
58
|
+
*/
|
|
59
|
+
it('FALSIFICATION: flipping 1 byte in compressed output breaks round-trip', async () => {
|
|
60
|
+
const { decompressZstd } = await import('@forgeax/engine-codec');
|
|
61
|
+
const { compressZstd } = await import('@forgeax/engine-codec/encode');
|
|
62
|
+
|
|
63
|
+
const original = new Uint8Array(1024);
|
|
64
|
+
for (let i = 0; i < 1024; i++) original[i] = (i * 7) % 256;
|
|
65
|
+
|
|
66
|
+
const compRes = await compressZstd(original);
|
|
67
|
+
expect(compRes.ok).toBe(true);
|
|
68
|
+
if (!compRes.ok) return;
|
|
69
|
+
|
|
70
|
+
// Flip byte 1 (skip byte 0 — frame header magic may trigger early error)
|
|
71
|
+
const corrupted = new Uint8Array(compRes.value);
|
|
72
|
+
if (corrupted.length > 1) {
|
|
73
|
+
corrupted[1] = (corrupted[1] ?? 0) ^ 0xff;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const decRes = await decompressZstd(corrupted);
|
|
77
|
+
// After corruption, decompression should either fail or produce wrong bytes.
|
|
78
|
+
// If it succeeds (some corruptions produce valid-but-wrong output), it must
|
|
79
|
+
// NOT equal the original (this is the falsification check).
|
|
80
|
+
if (decRes.ok) {
|
|
81
|
+
expect(decRes.value).not.toEqual(original);
|
|
82
|
+
}
|
|
83
|
+
// If it fails, that's also fine — the falsification proves discriminability
|
|
84
|
+
// (corruption was detected).
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* POD deep-compare: simulate loading compressed vs uncompressed fixture
|
|
89
|
+
* and assert the resulting asset data is identical.
|
|
90
|
+
*
|
|
91
|
+
* TDD-RED: before w19, loading compressed mesh through AssetRegistry
|
|
92
|
+
* fails entirely. After w19+w20, this test loads both paths and compares
|
|
93
|
+
* the returned PODs.
|
|
94
|
+
*/
|
|
95
|
+
it('compressed vs uncompressed mesh-bin data is byte-identical after round-trip', async () => {
|
|
96
|
+
const { decompressZstd } = await import('@forgeax/engine-codec');
|
|
97
|
+
const { compressZstd } = await import('@forgeax/engine-codec/encode');
|
|
98
|
+
|
|
99
|
+
// Create a realistic mesh bin payload
|
|
100
|
+
const floatsPerVertex = 12;
|
|
101
|
+
const vlen = 3 * floatsPerVertex;
|
|
102
|
+
const ilen = 3;
|
|
103
|
+
const iwidth = 2;
|
|
104
|
+
const jsonTail =
|
|
105
|
+
'{"submeshes":[{"indexOffset":0,"indexCount":3,"vertexCount":36,"topology":"triangle-list"}],"aabb":[0,0,0,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0]}';
|
|
106
|
+
const jsonBytes = new TextEncoder().encode(jsonTail);
|
|
107
|
+
|
|
108
|
+
const header = new ArrayBuffer(28);
|
|
109
|
+
const dv = new DataView(header);
|
|
110
|
+
dv.setUint32(0, 2, true);
|
|
111
|
+
dv.setUint32(4, 1, true);
|
|
112
|
+
dv.setUint32(8, 12, true);
|
|
113
|
+
dv.setUint32(12, vlen, true);
|
|
114
|
+
dv.setUint32(16, ilen, true);
|
|
115
|
+
dv.setUint32(20, iwidth, true);
|
|
116
|
+
dv.setUint32(24, jsonBytes.length, true);
|
|
117
|
+
|
|
118
|
+
const vertices = new Float32Array(vlen);
|
|
119
|
+
for (let i = 0; i < 3; i++) {
|
|
120
|
+
const b = i * 12;
|
|
121
|
+
vertices[b + 0] = i;
|
|
122
|
+
vertices[b + 1] = 0;
|
|
123
|
+
vertices[b + 2] = 0;
|
|
124
|
+
vertices[b + 3] = 0;
|
|
125
|
+
vertices[b + 4] = 1;
|
|
126
|
+
vertices[b + 5] = 0;
|
|
127
|
+
vertices[b + 6] = 0;
|
|
128
|
+
vertices[b + 7] = 0;
|
|
129
|
+
vertices[b + 8] = 1;
|
|
130
|
+
vertices[b + 9] = 0;
|
|
131
|
+
vertices[b + 10] = 0;
|
|
132
|
+
vertices[b + 11] = 1;
|
|
133
|
+
}
|
|
134
|
+
const indices = new Uint16Array([0, 1, 2]);
|
|
135
|
+
|
|
136
|
+
const totalSize = 28 + vlen * 4 + ilen * iwidth + jsonBytes.length;
|
|
137
|
+
const meshBin = new Uint8Array(totalSize);
|
|
138
|
+
let offset = 0;
|
|
139
|
+
meshBin.set(new Uint8Array(header), offset);
|
|
140
|
+
offset += 28;
|
|
141
|
+
meshBin.set(new Uint8Array(vertices.buffer, vertices.byteOffset, vertices.byteLength), offset);
|
|
142
|
+
offset += vlen * 4;
|
|
143
|
+
meshBin.set(new Uint8Array(indices.buffer, indices.byteOffset, indices.byteLength), offset);
|
|
144
|
+
offset += ilen * iwidth;
|
|
145
|
+
meshBin.set(jsonBytes, offset);
|
|
146
|
+
|
|
147
|
+
// Compress + decompress
|
|
148
|
+
const compRes = await compressZstd(meshBin);
|
|
149
|
+
expect(compRes.ok).toBe(true);
|
|
150
|
+
if (!compRes.ok) return;
|
|
151
|
+
|
|
152
|
+
const decRes = await decompressZstd(compRes.value);
|
|
153
|
+
expect(decRes.ok).toBe(true);
|
|
154
|
+
if (!decRes.ok) return;
|
|
155
|
+
|
|
156
|
+
// The decompressed bytes should be byte-identical to the original mesh bin
|
|
157
|
+
expect(decRes.value).toEqual(meshBin);
|
|
158
|
+
|
|
159
|
+
// FALSIFICATION: flip a byte in the decompressed output and re-compare
|
|
160
|
+
const corrupted = new Uint8Array(decRes.value);
|
|
161
|
+
corrupted[28] = (corrupted[28] ?? 0) ^ 0xff; // flip a byte in the vertex data
|
|
162
|
+
expect(corrupted).not.toEqual(meshBin);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { CodecError, CodecErrorCode, CodecResult } from '@forgeax/engine-codec';
|
|
2
|
+
import { codecError } from '@forgeax/engine-codec';
|
|
3
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
describe('CodecError code/detail correlation', () => {
|
|
6
|
+
it('accepts every existing code with its matching detail payload', () => {
|
|
7
|
+
codecError('decompression-failed', { reason: 'reason' });
|
|
8
|
+
codecError('codec-init-failed', { stage: 'stage' });
|
|
9
|
+
codecError('ktx2-parse-failed', { reason: 'reason' });
|
|
10
|
+
codecError('ktx2-unsupported-scheme', { scheme: 3 });
|
|
11
|
+
codecError('transcode-failed', {
|
|
12
|
+
sourceFormat: 'uastc-ldr',
|
|
13
|
+
targetFormat: 'bc7-rgba-unorm',
|
|
14
|
+
});
|
|
15
|
+
codecError('ktx2-encode-failed', { mode: 'uastc-ldr', reason: 'reason' });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('narrows detail from the checked code', () => {
|
|
19
|
+
function assertNarrowing(error: CodecError): void {
|
|
20
|
+
switch (error.error.code) {
|
|
21
|
+
case 'decompression-failed':
|
|
22
|
+
expectTypeOf(error.error.detail.reason).toBeString();
|
|
23
|
+
break;
|
|
24
|
+
case 'codec-init-failed':
|
|
25
|
+
expectTypeOf(error.error.detail.stage).toBeString();
|
|
26
|
+
break;
|
|
27
|
+
case 'ktx2-parse-failed':
|
|
28
|
+
expectTypeOf(error.error.detail.reason).toBeString();
|
|
29
|
+
break;
|
|
30
|
+
case 'ktx2-unsupported-scheme':
|
|
31
|
+
expectTypeOf(error.error.detail.scheme).toBeNumber();
|
|
32
|
+
break;
|
|
33
|
+
case 'transcode-failed':
|
|
34
|
+
expectTypeOf(error.error.detail.sourceFormat).toBeString();
|
|
35
|
+
expectTypeOf(error.error.detail.targetFormat).toBeString();
|
|
36
|
+
break;
|
|
37
|
+
case 'ktx2-encode-failed':
|
|
38
|
+
expectTypeOf(error.error.detail.mode).toBeString();
|
|
39
|
+
expectTypeOf(error.error.detail.reason).toBeString();
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
assertNarrowing(codecError('decompression-failed', { reason: 'reason' }));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('rejects a detail belonging to a different code', () => {
|
|
48
|
+
// @ts-expect-error -- ktx2-unsupported-scheme accepts `{ scheme }`, not `{ reason }`.
|
|
49
|
+
codecError('ktx2-unsupported-scheme', { reason: 'wrong-detail' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('preserves the six-code public union and structural Result compatibility', () => {
|
|
53
|
+
expectTypeOf<CodecErrorCode>().toEqualTypeOf<
|
|
54
|
+
| 'decompression-failed'
|
|
55
|
+
| 'codec-init-failed'
|
|
56
|
+
| 'ktx2-parse-failed'
|
|
57
|
+
| 'ktx2-unsupported-scheme'
|
|
58
|
+
| 'transcode-failed'
|
|
59
|
+
| 'ktx2-encode-failed'
|
|
60
|
+
>();
|
|
61
|
+
|
|
62
|
+
const failure: CodecResult<Uint8Array> = codecError('ktx2-parse-failed', {
|
|
63
|
+
reason: 'reason',
|
|
64
|
+
});
|
|
65
|
+
expectTypeOf(failure).toMatchTypeOf<CodecResult<Uint8Array>>();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { CodecError, CodecErrorCode } from '@forgeax/engine-codec';
|
|
2
|
+
import { codecError, decompressZstd } from '@forgeax/engine-codec';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Unit tests for codec error shapes (w4)
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
describe('codec error paths (w4)', () => {
|
|
10
|
+
// --- decompression-failed (E2) ---
|
|
11
|
+
|
|
12
|
+
it('decompression-failed error has correct shape (E2)', async () => {
|
|
13
|
+
// Garbage bytes should trigger decompression-failed in the real impl.
|
|
14
|
+
const garbage = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
|
|
15
|
+
const result = await decompressZstd(garbage);
|
|
16
|
+
expect(result.ok).toBe(false);
|
|
17
|
+
if (result.ok) {
|
|
18
|
+
throw new Error('expected error result for garbage input');
|
|
19
|
+
}
|
|
20
|
+
expect(result.error.code).toBe('decompression-failed');
|
|
21
|
+
expect(typeof result.error.hint).toBe('string');
|
|
22
|
+
expect(result.error.hint.length).toBeGreaterThan(0);
|
|
23
|
+
expect(result.error.detail).toBeDefined();
|
|
24
|
+
// detail narrowed per code: reason field
|
|
25
|
+
expect(typeof (result.error.detail as { reason: string }).reason).toBe('string');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// --- codec-init-failed (E3) ---
|
|
29
|
+
|
|
30
|
+
it('codec-init-failed error shape via factory function', () => {
|
|
31
|
+
// Verify the factory produces correct shape (used when dynamic import fails).
|
|
32
|
+
// We test the factory directly since simulating dynamic import failure
|
|
33
|
+
// is fragile across bundlers.
|
|
34
|
+
const err = codecError('codec-init-failed', { stage: 'dynamic-import-fzstd' });
|
|
35
|
+
expect(err.ok).toBe(false);
|
|
36
|
+
expect(err.error.code).toBe('codec-init-failed');
|
|
37
|
+
expect(typeof err.error.hint).toBe('string');
|
|
38
|
+
expect(err.error.hint.length).toBeGreaterThan(0);
|
|
39
|
+
expect(err.error.detail).toEqual({ stage: 'dynamic-import-fzstd' });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// --- closed union exhaustiveness (compile-time check) ---
|
|
43
|
+
|
|
44
|
+
it('switch on CodecErrorCode is exhaustive (no default)', () => {
|
|
45
|
+
// This test compiles only if all union members are covered.
|
|
46
|
+
// If a member is added to CodecErrorCode and not handled here,
|
|
47
|
+
// TypeScript will emit a type error — this IS the test.
|
|
48
|
+
function handleCode(code: CodecErrorCode): string {
|
|
49
|
+
switch (code) {
|
|
50
|
+
case 'decompression-failed':
|
|
51
|
+
return 'decompression';
|
|
52
|
+
case 'codec-init-failed':
|
|
53
|
+
return 'init';
|
|
54
|
+
case 'ktx2-parse-failed':
|
|
55
|
+
return 'ktx2-parse';
|
|
56
|
+
case 'ktx2-unsupported-scheme':
|
|
57
|
+
return 'ktx2-unsupported';
|
|
58
|
+
case 'transcode-failed':
|
|
59
|
+
return 'transcode';
|
|
60
|
+
case 'ktx2-encode-failed':
|
|
61
|
+
return 'ktx2-encode';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// If we got here, exhaustive switch compiled without TS error.
|
|
65
|
+
// Verify all 6 codes produce distinct strings (not a degenerate pass-through).
|
|
66
|
+
const handled = [
|
|
67
|
+
handleCode('decompression-failed'),
|
|
68
|
+
handleCode('codec-init-failed'),
|
|
69
|
+
handleCode('ktx2-parse-failed'),
|
|
70
|
+
handleCode('ktx2-unsupported-scheme'),
|
|
71
|
+
handleCode('transcode-failed'),
|
|
72
|
+
handleCode('ktx2-encode-failed'),
|
|
73
|
+
];
|
|
74
|
+
expect(handled.length).toBe(6);
|
|
75
|
+
expect(new Set(handled).size).toBe(6); // all distinct
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// --- ktx2-parse-failed shape via factory ---
|
|
79
|
+
|
|
80
|
+
it('ktx2-parse-failed error shape via factory', () => {
|
|
81
|
+
const err = codecError('ktx2-parse-failed', { reason: 'truncated-identifier' });
|
|
82
|
+
expect(err.ok).toBe(false);
|
|
83
|
+
expect(err.error.code).toBe('ktx2-parse-failed');
|
|
84
|
+
expect(typeof err.error.hint).toBe('string');
|
|
85
|
+
expect(err.error.hint.length).toBeGreaterThan(0);
|
|
86
|
+
expect(err.error.detail).toEqual({ reason: 'truncated-identifier' });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// --- ktx2-unsupported-scheme shape via factory ---
|
|
90
|
+
|
|
91
|
+
it('ktx2-unsupported-scheme error shape via factory', () => {
|
|
92
|
+
const err = codecError('ktx2-unsupported-scheme', { scheme: 1 });
|
|
93
|
+
expect(err.ok).toBe(false);
|
|
94
|
+
expect(err.error.code).toBe('ktx2-unsupported-scheme');
|
|
95
|
+
expect(typeof err.error.hint).toBe('string');
|
|
96
|
+
expect(err.error.hint.length).toBeGreaterThan(0);
|
|
97
|
+
expect(err.error.detail).toEqual({ scheme: 1 });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// --- Result shape consistency ---
|
|
101
|
+
|
|
102
|
+
it('CodecError type is structurally consistent', () => {
|
|
103
|
+
// Verify that the CodecError type compiles with all 6 codes and preserves
|
|
104
|
+
// each code-specific payload at runtime.
|
|
105
|
+
const errors: CodecError[] = [
|
|
106
|
+
codecError('decompression-failed', { reason: 'test' }),
|
|
107
|
+
codecError('codec-init-failed', { stage: 'test' }),
|
|
108
|
+
codecError('ktx2-parse-failed', { reason: 'test' }),
|
|
109
|
+
codecError('ktx2-unsupported-scheme', { scheme: 0 }),
|
|
110
|
+
codecError('transcode-failed', {
|
|
111
|
+
sourceFormat: 'uastc-ldr',
|
|
112
|
+
targetFormat: 'bc7-rgba-unorm',
|
|
113
|
+
}),
|
|
114
|
+
codecError('ktx2-encode-failed', { mode: 'uastc-ldr', reason: 'test' }),
|
|
115
|
+
];
|
|
116
|
+
expect(errors).toHaveLength(6);
|
|
117
|
+
expect(errors.every((error) => error.error.expected.length > 0)).toBe(true);
|
|
118
|
+
expect(errors.every((error) => error.error.hint.length > 0)).toBe(true);
|
|
119
|
+
expect(errors.map((error) => error.error.detail)).toEqual([
|
|
120
|
+
{ reason: 'test' },
|
|
121
|
+
{ stage: 'test' },
|
|
122
|
+
{ reason: 'test' },
|
|
123
|
+
{ scheme: 0 },
|
|
124
|
+
{ sourceFormat: 'uastc-ldr', targetFormat: 'bc7-rgba-unorm' },
|
|
125
|
+
{ mode: 'uastc-ldr', reason: 'test' },
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
});
|