@forgeax/engine-codec 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +268 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/basis-encode-fast-preset.unit.test.d.ts +2 -0
- package/dist/__tests__/basis-encode-fast-preset.unit.test.d.ts.map +1 -0
- package/dist/__tests__/basis-encode-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/basis-encode-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/basis-encode-mode-owner.test-d.d.ts +2 -0
- package/dist/__tests__/basis-encode-mode-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/basis-transcoder.unit.test.d.ts +2 -0
- package/dist/__tests__/basis-transcoder.unit.test.d.ts.map +1 -0
- package/dist/__tests__/block-format.unit.test.d.ts +2 -0
- package/dist/__tests__/block-format.unit.test.d.ts.map +1 -0
- package/dist/__tests__/byte-identical-falsification.test.d.ts +2 -0
- package/dist/__tests__/byte-identical-falsification.test.d.ts.map +1 -0
- package/dist/__tests__/codec-errors.test-d.d.ts +2 -0
- package/dist/__tests__/codec-errors.test-d.d.ts.map +1 -0
- package/dist/__tests__/codec-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/codec-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/compression-field.unit.test.d.ts +19 -0
- package/dist/__tests__/compression-field.unit.test.d.ts.map +1 -0
- package/dist/__tests__/encode-pixel-limit.unit.test.d.ts +2 -0
- package/dist/__tests__/encode-pixel-limit.unit.test.d.ts.map +1 -0
- package/dist/__tests__/encode-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/encode-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-basis-capability-matrix.unit.test.d.ts +2 -0
- package/dist/__tests__/ktx2-basis-capability-matrix.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-errors.test.d.ts +2 -0
- package/dist/__tests__/ktx2-errors.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-parse-scheme0.test.d.ts +2 -0
- package/dist/__tests__/ktx2-parse-scheme0.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-parse-scheme2.test.d.ts +2 -0
- package/dist/__tests__/ktx2-parse-scheme2.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-to-texture.integration.test.d.ts +2 -0
- package/dist/__tests__/ktx2-to-texture.integration.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-validation.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-validation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/transcode-select-target.unit.test.d.ts +2 -0
- package/dist/__tests__/transcode-select-target.unit.test.d.ts.map +1 -0
- package/dist/__tests__/wasm-init-smoke.test.d.ts +2 -0
- package/dist/__tests__/wasm-init-smoke.test.d.ts.map +1 -0
- package/dist/__tests__/zstd-deterministic.unit.test.d.ts +2 -0
- package/dist/__tests__/zstd-deterministic.unit.test.d.ts.map +1 -0
- package/dist/__tests__/zstd-encode-singleton.unit.test.d.ts +2 -0
- package/dist/__tests__/zstd-encode-singleton.unit.test.d.ts.map +1 -0
- package/dist/__tests__/zstd-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/zstd-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/zstd-singleton.unit.test.d.ts +2 -0
- package/dist/__tests__/zstd-singleton.unit.test.d.ts.map +1 -0
- package/dist/basis-transcoder.d.ts +86 -0
- package/dist/basis-transcoder.d.ts.map +1 -0
- package/dist/block-format.d.ts +41 -0
- package/dist/block-format.d.ts.map +1 -0
- package/dist/encode/basis-encode.d.ts +68 -0
- package/dist/encode/basis-encode.d.ts.map +1 -0
- package/dist/encode/index.d.ts +10 -0
- package/dist/encode/index.d.ts.map +1 -0
- package/dist/encode/index.mjs +207 -0
- package/dist/encode/index.mjs.map +1 -0
- package/dist/encode-impl.d.ts +30 -0
- package/dist/encode-impl.d.ts.map +1 -0
- package/dist/errors.d.ts +61 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +672 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ktx2.d.ts +111 -0
- package/dist/ktx2.d.ts.map +1 -0
- package/dist/transcode.d.ts +55 -0
- package/dist/transcode.d.ts.map +1 -0
- package/dist/wasm/basis-types.d.ts +179 -0
- package/dist/wasm/basis-types.d.ts.map +1 -0
- package/dist/zstd.d.ts +28 -0
- package/dist/zstd.d.ts.map +1 -0
- package/package.json +71 -0
- package/pkg/basis_transcoder.mjs +2 -0
- package/pkg/basis_transcoder.wasm +0 -0
- package/pkg/encode/basis_encoder.mjs +2 -0
- package/pkg/encode/basis_encoder.wasm +0 -0
- package/scripts/build-wasm.mjs +370 -0
- package/scripts/content-key.mjs +85 -0
- package/scripts/ensure-wasm.mjs +38 -0
- package/scripts/fetch-basis.mjs +150 -0
- package/scripts/fetch-wasm.mjs +97 -0
- package/src/__tests__/basis-encode-fast-preset.unit.test.ts +204 -0
- package/src/__tests__/basis-encode-lifecycle.unit.test.ts +249 -0
- package/src/__tests__/basis-encode-mode-owner.test-d.ts +39 -0
- package/src/__tests__/basis-transcoder.unit.test.ts +502 -0
- package/src/__tests__/block-format.unit.test.ts +215 -0
- package/src/__tests__/byte-identical-falsification.test.ts +164 -0
- package/src/__tests__/codec-errors.test-d.ts +67 -0
- package/src/__tests__/codec-errors.unit.test.ts +128 -0
- package/src/__tests__/compression-field.unit.test.ts +245 -0
- package/src/__tests__/encode-pixel-limit.unit.test.ts +117 -0
- package/src/__tests__/encode-roundtrip.unit.test.ts +239 -0
- package/src/__tests__/ktx2-basis-capability-matrix.unit.test.ts +241 -0
- package/src/__tests__/ktx2-errors.test.ts +239 -0
- package/src/__tests__/ktx2-parse-scheme0.test.ts +263 -0
- package/src/__tests__/ktx2-parse-scheme2.test.ts +220 -0
- package/src/__tests__/ktx2-scheme1-and-transcode-errors.unit.test.ts +178 -0
- package/src/__tests__/ktx2-to-texture.integration.test.ts +384 -0
- package/src/__tests__/runtime-validation.unit.test.ts +19 -0
- package/src/__tests__/transcode-select-target.unit.test.ts +167 -0
- package/src/__tests__/wasm-init-smoke.test.ts +102 -0
- package/src/__tests__/zstd-deterministic.unit.test.ts +49 -0
- package/src/__tests__/zstd-encode-singleton.unit.test.ts +63 -0
- package/src/__tests__/zstd-roundtrip.unit.test.ts +87 -0
- package/src/__tests__/zstd-singleton.unit.test.ts +105 -0
- package/src/basis-transcoder.ts +380 -0
- package/src/block-format.ts +128 -0
- package/src/encode/basis-encode.ts +264 -0
- package/src/encode/index.ts +19 -0
- package/src/encode-impl.ts +103 -0
- package/src/errors.ts +79 -0
- package/src/index.ts +38 -0
- package/src/ktx2.ts +469 -0
- package/src/transcode.ts +99 -0
- package/src/wasm/basis-types.ts +220 -0
- package/src/zstd.ts +85 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
_basisTranscoderInitCount,
|
|
6
|
+
_setBasisTranscoderImporter,
|
|
7
|
+
initBasisTranscoder,
|
|
8
|
+
transcodeBasis,
|
|
9
|
+
transcodeKtx2,
|
|
10
|
+
} from '../basis-transcoder.js';
|
|
11
|
+
import type { Ktx2Parsed } from '../ktx2.js';
|
|
12
|
+
import { parseKtx2 } from '../ktx2.js';
|
|
13
|
+
import type {
|
|
14
|
+
BasisEncoderModule,
|
|
15
|
+
BasisModuleFactory,
|
|
16
|
+
BasisTranscoderModule,
|
|
17
|
+
} from '../wasm/basis-types.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* basis-transcoder glue unit tests (w14).
|
|
21
|
+
*
|
|
22
|
+
* Validates:
|
|
23
|
+
* 1. Lazy-init singleton (D-10): first initBasisTranscoder() loads the WASM
|
|
24
|
+
* module once; a second call returns the cache without re-importing.
|
|
25
|
+
* 2. transcodeKtx2() on a real UASTC KTX2 (encoder-produced) returns non-empty
|
|
26
|
+
* per-mip block bytes for a BC7 target.
|
|
27
|
+
* 3. Module imports cleanly in node (this file runs under the node vitest
|
|
28
|
+
* project); the same dynamic-import .mjs path is browser-legal (M5/e2e).
|
|
29
|
+
*
|
|
30
|
+
* pkg/ is a gitignored emcc build artifact (AC-12). When it is not built
|
|
31
|
+
* (contributor without emsdk) the WASM-dependent cases skip; CI builds it in the
|
|
32
|
+
* build-artifacts job so the gate runs there. The init-counting case uses an
|
|
33
|
+
* injected fake importer, so it runs regardless of pkg/ presence.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const TRANSCODER_GLUE = new URL('../../pkg/basis_transcoder.mjs', import.meta.url);
|
|
37
|
+
const ENCODER_GLUE = new URL('../../pkg/encode/basis_encoder.mjs', import.meta.url);
|
|
38
|
+
const pkgBuilt =
|
|
39
|
+
existsSync(fileURLToPath(TRANSCODER_GLUE)) && existsSync(fileURLToPath(ENCODER_GLUE));
|
|
40
|
+
|
|
41
|
+
async function loadEncoder(): Promise<BasisEncoderModule> {
|
|
42
|
+
const factory = (
|
|
43
|
+
(await import(/* @vite-ignore */ ENCODER_GLUE.href)) as {
|
|
44
|
+
default: BasisModuleFactory<BasisEncoderModule>;
|
|
45
|
+
}
|
|
46
|
+
).default;
|
|
47
|
+
const mod = await factory({
|
|
48
|
+
locateFile: () => new URL('../../pkg/encode/basis_encoder.wasm', import.meta.url).href,
|
|
49
|
+
});
|
|
50
|
+
mod.initializeBasis();
|
|
51
|
+
return mod;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A deterministic WxH RGBA image. */
|
|
55
|
+
function makeRgba(w: number, h: number): Uint8Array {
|
|
56
|
+
const img = new Uint8Array(w * h * 4);
|
|
57
|
+
for (let i = 0; i < w * h; i++) {
|
|
58
|
+
img[i * 4] = (i * 7) & 255;
|
|
59
|
+
img[i * 4 + 1] = (i * 13) & 255;
|
|
60
|
+
img[i * 4 + 2] = (i * 29) & 255;
|
|
61
|
+
img[i * 4 + 3] = 255;
|
|
62
|
+
}
|
|
63
|
+
return img;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Encode a WxH RGBA image into a UASTC KTX2 container (Basis payload). */
|
|
67
|
+
async function encodeUastcKtx2(w: number, h: number): Promise<Uint8Array> {
|
|
68
|
+
const enc = await loadEncoder();
|
|
69
|
+
const encoder = new enc.BasisEncoder();
|
|
70
|
+
encoder.setSliceSourceImage(0, makeRgba(w, h), w, h, 0);
|
|
71
|
+
encoder.setCreateKTX2File(true);
|
|
72
|
+
encoder.setKTX2UASTCSupercompression(true);
|
|
73
|
+
encoder.setFormatMode(enc.basis_tex_format.cUASTC_LDR_4x4.value);
|
|
74
|
+
encoder.setPerceptual(true);
|
|
75
|
+
encoder.setMipGen(false);
|
|
76
|
+
const out = new Uint8Array(1 << 20);
|
|
77
|
+
const n = encoder.encode(out);
|
|
78
|
+
encoder.delete();
|
|
79
|
+
if (n <= 0) throw new Error('encode failed for test setup');
|
|
80
|
+
return out.slice(0, n);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
_setBasisTranscoderImporter();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('initBasisTranscoder — lazy-init singleton (D-10, w14)', () => {
|
|
88
|
+
it('importer not invoked until first init call', () => {
|
|
89
|
+
_setBasisTranscoderImporter(() => Promise.resolve({} as never));
|
|
90
|
+
expect(_basisTranscoderInitCount()).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('loads exactly once and returns the cache on the second call', async () => {
|
|
94
|
+
let calls = 0;
|
|
95
|
+
const fake = { transcoder_texture_format: {}, KTX2File: null } as never;
|
|
96
|
+
_setBasisTranscoderImporter(() => {
|
|
97
|
+
calls++;
|
|
98
|
+
return Promise.resolve(fake);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const a = await initBasisTranscoder();
|
|
102
|
+
const b = await initBasisTranscoder();
|
|
103
|
+
expect(a).toBe(b);
|
|
104
|
+
expect(calls).toBe(1);
|
|
105
|
+
expect(_basisTranscoderInitCount()).toBe(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('clears the cached failure so the next call retries', async () => {
|
|
109
|
+
let calls = 0;
|
|
110
|
+
_setBasisTranscoderImporter(() => {
|
|
111
|
+
calls++;
|
|
112
|
+
return Promise.reject(new Error('boom'));
|
|
113
|
+
});
|
|
114
|
+
await expect(initBasisTranscoder()).rejects.toThrow();
|
|
115
|
+
await expect(initBasisTranscoder()).rejects.toThrow();
|
|
116
|
+
expect(calls).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const TEST_TARGET = 'bc7-rgba-unorm' as const;
|
|
121
|
+
const TEST_SENTINEL = 'm112-controlled-wasm-throw';
|
|
122
|
+
const TEST_CLOSE_SENTINEL = 'm119-controlled-close-throw';
|
|
123
|
+
const TEST_FORMAT = { cTFBC7_RGBA: { value: 7 } };
|
|
124
|
+
|
|
125
|
+
function fakeParsed(rawBytes: Uint8Array): Ktx2Parsed {
|
|
126
|
+
return { rawBytes, dfd: { colorModel: 166 } } as unknown as Ktx2Parsed;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createKtx2OperationModule() {
|
|
130
|
+
let shouldThrow = true;
|
|
131
|
+
let shouldCloseThrow = false;
|
|
132
|
+
const state = {
|
|
133
|
+
closeCount: 0,
|
|
134
|
+
inputBytes: [] as Uint8Array[],
|
|
135
|
+
targetEnums: [] as number[],
|
|
136
|
+
transcodeLevels: [] as number[],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
class FakeKtx2File {
|
|
140
|
+
constructor(bytes: Uint8Array) {
|
|
141
|
+
state.inputBytes.push(bytes);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
close(): void {
|
|
145
|
+
state.closeCount++;
|
|
146
|
+
if (shouldCloseThrow) throw TEST_CLOSE_SENTINEL;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
isValid(): boolean {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
startTranscoding(): number {
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getLevels(): number {
|
|
158
|
+
return 2;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getImageLevelInfo(
|
|
162
|
+
level: number,
|
|
163
|
+
_layer: number,
|
|
164
|
+
_face: number,
|
|
165
|
+
): { origWidth: number; origHeight: number } {
|
|
166
|
+
return level === 0 ? { origWidth: 8, origHeight: 4 } : { origWidth: 4, origHeight: 2 };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getImageTranscodedSizeInBytes(
|
|
170
|
+
level: number,
|
|
171
|
+
_layer: number,
|
|
172
|
+
_face: number,
|
|
173
|
+
format: number,
|
|
174
|
+
): number {
|
|
175
|
+
state.targetEnums.push(format);
|
|
176
|
+
return level === 0 ? 16 : 4;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
transcodeImage(
|
|
180
|
+
dst: Uint8Array,
|
|
181
|
+
level: number,
|
|
182
|
+
_layer: number,
|
|
183
|
+
_face: number,
|
|
184
|
+
_format: number,
|
|
185
|
+
_getAlphaForOpaqueFormats: number,
|
|
186
|
+
_channel0: number,
|
|
187
|
+
_channel1: number,
|
|
188
|
+
): number {
|
|
189
|
+
state.transcodeLevels.push(level);
|
|
190
|
+
if (shouldThrow && level === 1) throw TEST_SENTINEL;
|
|
191
|
+
dst.fill(level + 1);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
getWidth(): number {
|
|
196
|
+
return 8;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getHeight(): number {
|
|
200
|
+
return 4;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const module = {
|
|
205
|
+
transcoder_texture_format: TEST_FORMAT,
|
|
206
|
+
KTX2File: FakeKtx2File,
|
|
207
|
+
} as unknown as BasisTranscoderModule;
|
|
208
|
+
return {
|
|
209
|
+
module,
|
|
210
|
+
state,
|
|
211
|
+
repair(): void {
|
|
212
|
+
shouldThrow = false;
|
|
213
|
+
},
|
|
214
|
+
refuseClose(): void {
|
|
215
|
+
shouldCloseThrow = true;
|
|
216
|
+
},
|
|
217
|
+
repairClose(): void {
|
|
218
|
+
shouldCloseThrow = false;
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function createBasisOperationModule() {
|
|
224
|
+
let shouldThrow = true;
|
|
225
|
+
let shouldCloseThrow = false;
|
|
226
|
+
const state = {
|
|
227
|
+
closeCount: 0,
|
|
228
|
+
inputBytes: [] as Uint8Array[],
|
|
229
|
+
targetEnums: [] as number[],
|
|
230
|
+
transcodeLevels: [] as number[],
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
class FakeBasisFile {
|
|
234
|
+
constructor(bytes: Uint8Array) {
|
|
235
|
+
state.inputBytes.push(bytes);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
close(): void {
|
|
239
|
+
state.closeCount++;
|
|
240
|
+
if (shouldCloseThrow) throw TEST_CLOSE_SENTINEL;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getNumImages(): number {
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
getNumLevels(_imageIndex: number): number {
|
|
248
|
+
return 2;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getImageWidth(_imageIndex: number, level: number): number {
|
|
252
|
+
return level === 0 ? 8 : 4;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getImageHeight(_imageIndex: number, level: number): number {
|
|
256
|
+
return level === 0 ? 4 : 2;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
startTranscoding(): number {
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
getImageTranscodedSizeInBytes(_imageIndex: number, level: number, format: number): number {
|
|
264
|
+
state.targetEnums.push(format);
|
|
265
|
+
return level === 0 ? 16 : 4;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
transcodeImage(
|
|
269
|
+
dst: Uint8Array,
|
|
270
|
+
_imageIndex: number,
|
|
271
|
+
level: number,
|
|
272
|
+
_format: number,
|
|
273
|
+
_decodeFlags: number,
|
|
274
|
+
): number {
|
|
275
|
+
state.transcodeLevels.push(level);
|
|
276
|
+
if (shouldThrow && level === 1) throw TEST_SENTINEL;
|
|
277
|
+
dst.fill(level + 1);
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const module = {
|
|
283
|
+
transcoder_texture_format: TEST_FORMAT,
|
|
284
|
+
KTX2File: class {},
|
|
285
|
+
BasisFile: FakeBasisFile,
|
|
286
|
+
} as unknown as BasisTranscoderModule;
|
|
287
|
+
return {
|
|
288
|
+
module,
|
|
289
|
+
state,
|
|
290
|
+
repair(): void {
|
|
291
|
+
shouldThrow = false;
|
|
292
|
+
},
|
|
293
|
+
refuseClose(): void {
|
|
294
|
+
shouldCloseThrow = true;
|
|
295
|
+
},
|
|
296
|
+
repairClose(): void {
|
|
297
|
+
shouldCloseThrow = false;
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function createConstructorThrowingModule(): BasisTranscoderModule {
|
|
303
|
+
class ThrowingKtx2File {
|
|
304
|
+
constructor(_bytes: Uint8Array) {
|
|
305
|
+
throw TEST_SENTINEL;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
class ThrowingBasisFile {
|
|
310
|
+
constructor(_bytes: Uint8Array) {
|
|
311
|
+
throw TEST_SENTINEL;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
transcoder_texture_format: TEST_FORMAT,
|
|
317
|
+
KTX2File: ThrowingKtx2File,
|
|
318
|
+
BasisFile: ThrowingBasisFile,
|
|
319
|
+
} as unknown as BasisTranscoderModule;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
describe('transcode throw containment and same-module retry (M112)', () => {
|
|
323
|
+
it('contains KTX2 operation throws, closes once, and retries the same input/module deterministically', async () => {
|
|
324
|
+
const rawBytes = new Uint8Array([1, 2, 3]);
|
|
325
|
+
const parsed = fakeParsed(rawBytes);
|
|
326
|
+
const fixture = createKtx2OperationModule();
|
|
327
|
+
let importerCalls = 0;
|
|
328
|
+
_setBasisTranscoderImporter(() => {
|
|
329
|
+
importerCalls++;
|
|
330
|
+
return Promise.resolve(fixture.module);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
fixture.refuseClose();
|
|
334
|
+
const failed = await transcodeKtx2(parsed, TEST_TARGET);
|
|
335
|
+
expect(failed.ok).toBe(false);
|
|
336
|
+
if (failed.ok) throw new Error('expected operation throw to return a CodecError');
|
|
337
|
+
expect(failed.error.code).toBe('transcode-failed');
|
|
338
|
+
expect(failed.error.detail).toEqual({
|
|
339
|
+
sourceFormat: 'dfd-model-166',
|
|
340
|
+
targetFormat: TEST_TARGET,
|
|
341
|
+
});
|
|
342
|
+
expect(JSON.stringify(failed)).not.toContain(TEST_SENTINEL);
|
|
343
|
+
expect(JSON.stringify(failed)).not.toContain(TEST_CLOSE_SENTINEL);
|
|
344
|
+
expect(fixture.state.closeCount).toBe(1);
|
|
345
|
+
expect(fixture.state.transcodeLevels).toEqual([0, 1]);
|
|
346
|
+
|
|
347
|
+
fixture.repair();
|
|
348
|
+
const cleanupFailed = await transcodeKtx2(parsed, TEST_TARGET);
|
|
349
|
+
expect(cleanupFailed.ok).toBe(false);
|
|
350
|
+
if (cleanupFailed.ok) throw new Error('expected close throw to return a CodecError');
|
|
351
|
+
expect(cleanupFailed.error.code).toBe('transcode-failed');
|
|
352
|
+
expect(cleanupFailed.error.detail).toEqual({
|
|
353
|
+
sourceFormat: 'dfd-model-166',
|
|
354
|
+
targetFormat: TEST_TARGET,
|
|
355
|
+
});
|
|
356
|
+
expect(JSON.stringify(cleanupFailed)).not.toContain(TEST_CLOSE_SENTINEL);
|
|
357
|
+
expect(fixture.state.closeCount).toBe(2);
|
|
358
|
+
|
|
359
|
+
fixture.repairClose();
|
|
360
|
+
const repaired = await transcodeKtx2(parsed, TEST_TARGET);
|
|
361
|
+
expect(repaired.ok).toBe(true);
|
|
362
|
+
if (!repaired.ok) throw new Error('expected repaired KTX2 transcode to succeed');
|
|
363
|
+
expect(repaired.value.format).toBe(TEST_TARGET);
|
|
364
|
+
expect(repaired.value.width).toBe(8);
|
|
365
|
+
expect(repaired.value.height).toBe(4);
|
|
366
|
+
expect(
|
|
367
|
+
repaired.value.mips.map(({ level, width, height, data }) => ({
|
|
368
|
+
level,
|
|
369
|
+
width,
|
|
370
|
+
height,
|
|
371
|
+
data: [...data],
|
|
372
|
+
})),
|
|
373
|
+
).toEqual([
|
|
374
|
+
{ level: 0, width: 8, height: 4, data: new Array(16).fill(1) },
|
|
375
|
+
{ level: 1, width: 4, height: 2, data: new Array(4).fill(2) },
|
|
376
|
+
]);
|
|
377
|
+
|
|
378
|
+
const third = await transcodeKtx2(parsed, TEST_TARGET);
|
|
379
|
+
expect(third).toEqual(repaired);
|
|
380
|
+
expect(importerCalls).toBe(1);
|
|
381
|
+
expect(_basisTranscoderInitCount()).toBe(1);
|
|
382
|
+
expect(fixture.state.inputBytes).toHaveLength(4);
|
|
383
|
+
expect(fixture.state.inputBytes.every((bytes) => bytes === rawBytes)).toBe(true);
|
|
384
|
+
expect(fixture.state.targetEnums).toEqual([7, 7, 7, 7, 7, 7, 7, 7]);
|
|
385
|
+
expect(fixture.state.closeCount).toBe(4);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('contains raw Basis operation throws and retries the same bytes/module deterministically', async () => {
|
|
389
|
+
const rawBytes = new Uint8Array([4, 5, 6]);
|
|
390
|
+
const fixture = createBasisOperationModule();
|
|
391
|
+
let importerCalls = 0;
|
|
392
|
+
_setBasisTranscoderImporter(() => {
|
|
393
|
+
importerCalls++;
|
|
394
|
+
return Promise.resolve(fixture.module);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
fixture.refuseClose();
|
|
398
|
+
const failed = await transcodeBasis(rawBytes, TEST_TARGET);
|
|
399
|
+
expect(failed.ok).toBe(false);
|
|
400
|
+
if (failed.ok) throw new Error('expected raw Basis operation throw to return a CodecError');
|
|
401
|
+
expect(failed.error.code).toBe('transcode-failed');
|
|
402
|
+
expect(failed.error.detail).toEqual({ sourceFormat: 'raw-basis', targetFormat: TEST_TARGET });
|
|
403
|
+
expect(JSON.stringify(failed)).not.toContain(TEST_SENTINEL);
|
|
404
|
+
expect(JSON.stringify(failed)).not.toContain(TEST_CLOSE_SENTINEL);
|
|
405
|
+
expect(fixture.state.closeCount).toBe(1);
|
|
406
|
+
expect(fixture.state.transcodeLevels).toEqual([0, 1]);
|
|
407
|
+
|
|
408
|
+
fixture.repair();
|
|
409
|
+
const cleanupFailed = await transcodeBasis(rawBytes, TEST_TARGET);
|
|
410
|
+
expect(cleanupFailed.ok).toBe(false);
|
|
411
|
+
if (cleanupFailed.ok) throw new Error('expected close throw to return a CodecError');
|
|
412
|
+
expect(cleanupFailed.error.code).toBe('transcode-failed');
|
|
413
|
+
expect(cleanupFailed.error.detail).toEqual({
|
|
414
|
+
sourceFormat: 'raw-basis',
|
|
415
|
+
targetFormat: TEST_TARGET,
|
|
416
|
+
});
|
|
417
|
+
expect(JSON.stringify(cleanupFailed)).not.toContain(TEST_CLOSE_SENTINEL);
|
|
418
|
+
expect(fixture.state.closeCount).toBe(2);
|
|
419
|
+
|
|
420
|
+
fixture.repairClose();
|
|
421
|
+
const repaired = await transcodeBasis(rawBytes, TEST_TARGET);
|
|
422
|
+
expect(repaired.ok).toBe(true);
|
|
423
|
+
if (!repaired.ok) throw new Error('expected repaired raw Basis transcode to succeed');
|
|
424
|
+
expect(
|
|
425
|
+
repaired.value.mips.map(({ level, width, height, data }) => ({
|
|
426
|
+
level,
|
|
427
|
+
width,
|
|
428
|
+
height,
|
|
429
|
+
data: [...data],
|
|
430
|
+
})),
|
|
431
|
+
).toEqual([
|
|
432
|
+
{ level: 0, width: 8, height: 4, data: new Array(16).fill(1) },
|
|
433
|
+
{ level: 1, width: 4, height: 2, data: new Array(4).fill(2) },
|
|
434
|
+
]);
|
|
435
|
+
|
|
436
|
+
const third = await transcodeBasis(rawBytes, TEST_TARGET);
|
|
437
|
+
expect(third).toEqual(repaired);
|
|
438
|
+
expect(importerCalls).toBe(1);
|
|
439
|
+
expect(_basisTranscoderInitCount()).toBe(1);
|
|
440
|
+
expect(fixture.state.inputBytes).toHaveLength(4);
|
|
441
|
+
expect(fixture.state.inputBytes.every((bytes) => bytes === rawBytes)).toBe(true);
|
|
442
|
+
expect(fixture.state.targetEnums).toEqual([7, 7, 7, 7, 7, 7, 7, 7]);
|
|
443
|
+
expect(fixture.state.closeCount).toBe(4);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('contains KTX2 and raw Basis constructor throws without attempting close', async () => {
|
|
447
|
+
const module = createConstructorThrowingModule();
|
|
448
|
+
_setBasisTranscoderImporter(() => Promise.resolve(module));
|
|
449
|
+
|
|
450
|
+
const ktx2 = await transcodeKtx2(fakeParsed(new Uint8Array([7])), TEST_TARGET);
|
|
451
|
+
expect(ktx2.ok).toBe(false);
|
|
452
|
+
if (ktx2.ok) throw new Error('expected KTX2 constructor throw to return a CodecError');
|
|
453
|
+
expect(ktx2.error.code).toBe('transcode-failed');
|
|
454
|
+
expect(ktx2.error.detail).toEqual({ sourceFormat: 'dfd-model-166', targetFormat: TEST_TARGET });
|
|
455
|
+
|
|
456
|
+
const basis = await transcodeBasis(new Uint8Array([8]), TEST_TARGET);
|
|
457
|
+
expect(basis.ok).toBe(false);
|
|
458
|
+
if (basis.ok) throw new Error('expected Basis constructor throw to return a CodecError');
|
|
459
|
+
expect(basis.error.code).toBe('transcode-failed');
|
|
460
|
+
expect(basis.error.detail).toEqual({ sourceFormat: 'raw-basis', targetFormat: TEST_TARGET });
|
|
461
|
+
expect(JSON.stringify({ ktx2, basis })).not.toContain(TEST_SENTINEL);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe.skipIf(!pkgBuilt)('transcodeKtx2 — real UASTC -> BC7 (w14)', () => {
|
|
466
|
+
let uastcKtx2: Uint8Array;
|
|
467
|
+
|
|
468
|
+
beforeAll(async () => {
|
|
469
|
+
uastcKtx2 = await encodeUastcKtx2(16, 16);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('transcodes every mip level into non-empty BC7 block bytes', async () => {
|
|
473
|
+
_setBasisTranscoderImporter();
|
|
474
|
+
const parsed = await parseKtx2(uastcKtx2);
|
|
475
|
+
expect(parsed.ok).toBe(true);
|
|
476
|
+
if (!parsed.ok) throw new Error(`parse failed: ${parsed.error.code}`);
|
|
477
|
+
|
|
478
|
+
const result = await transcodeKtx2(parsed.value, 'bc7-rgba-unorm');
|
|
479
|
+
expect(result.ok).toBe(true);
|
|
480
|
+
if (!result.ok) throw new Error(`transcode failed: ${result.error.code}`);
|
|
481
|
+
|
|
482
|
+
expect(result.value.format).toBe('bc7-rgba-unorm');
|
|
483
|
+
expect(result.value.width).toBe(16);
|
|
484
|
+
expect(result.value.height).toBe(16);
|
|
485
|
+
expect(result.value.mips.length).toBeGreaterThanOrEqual(1);
|
|
486
|
+
const base = result.value.mips[0];
|
|
487
|
+
expect(base).toBeDefined();
|
|
488
|
+
if (!base) throw new Error('no base mip');
|
|
489
|
+
// 16x16 BC7 = 4x4 blocks * 16 bytes = 256 bytes.
|
|
490
|
+
expect(base.data.length).toBe(256);
|
|
491
|
+
expect(base.data.byteLength).toBeGreaterThan(0);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('the real WASM importer loads once across two transcodes', async () => {
|
|
495
|
+
_setBasisTranscoderImporter();
|
|
496
|
+
const parsed = await parseKtx2(uastcKtx2);
|
|
497
|
+
if (!parsed.ok) throw new Error('parse failed');
|
|
498
|
+
await transcodeKtx2(parsed.value, 'bc7-rgba-unorm');
|
|
499
|
+
await transcodeKtx2(parsed.value, 'bc7-rgba-unorm');
|
|
500
|
+
expect(_basisTranscoderInitCount()).toBe(1);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
@@ -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
|
+
});
|