@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,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// fetch-wasm.mjs — Download the pre-built basis-wasm pkg/ bundle from GitHub
|
|
3
|
+
// Releases so no-Emscripten consumers (Studio setup, editor standalone, CI
|
|
4
|
+
// typecheck) can resolve @forgeax/engine-codec without an emcc toolchain and,
|
|
5
|
+
// crucially, without the multi-minute -O3 basis_universal encoder compile.
|
|
6
|
+
//
|
|
7
|
+
// Like @forgeax/engine-wgpu-wasm, the codec pkg/ carries multiple files (two
|
|
8
|
+
// emscripten glue .mjs + two .wasm, nested one level under encode/) that cannot
|
|
9
|
+
// be regenerated from a single .wasm, so the release asset is the WHOLE pkg/
|
|
10
|
+
// packed as a content-keyed .tar.gz; this script downloads and extracts it into
|
|
11
|
+
// packages/codec/pkg/.
|
|
12
|
+
//
|
|
13
|
+
// The content key (see content-key.mjs) is the SSOT shared with the CI publish
|
|
14
|
+
// job, so producer and consumer always agree on the asset name.
|
|
15
|
+
//
|
|
16
|
+
// Usage:
|
|
17
|
+
// pnpm -F @forgeax/engine-codec fetch-wasm
|
|
18
|
+
// node scripts/fetch-wasm.mjs
|
|
19
|
+
//
|
|
20
|
+
// Environment:
|
|
21
|
+
// GITHUB_TOKEN — optional Bearer token for private-repo auth. Falls back to
|
|
22
|
+
// `gh auth token`. Anonymous requests work for public repos.
|
|
23
|
+
// FORGEAX_SKIP_CODEC_WASM_FETCH — if set, exit 0 immediately (used by the
|
|
24
|
+
// non-fatal opt-out path when the toolchain owner wants to build locally).
|
|
25
|
+
//
|
|
26
|
+
// Exit codes: 0 on success (or skip); 1 on any FetchError. Callers that want a
|
|
27
|
+
// build fallback treat a non-zero exit as "release unavailable, compile via
|
|
28
|
+
// build:wasm" (graceful degradation: architecture-principles #9).
|
|
29
|
+
|
|
30
|
+
import { rm, readdir } from 'node:fs/promises';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { FetchError, getGitOrigin, getReleaseAsset, downloadAsset, extractTarball } from '../../../scripts/lib/fetch-wasm-lib.mjs';
|
|
34
|
+
import { PKG_ROOT, RELEASE_TAG, resolveAsset } from './content-key.mjs';
|
|
35
|
+
|
|
36
|
+
const PKG_DIR = join(PKG_ROOT, 'pkg');
|
|
37
|
+
const TMP_TARBALL = join(PKG_ROOT, '.pkg-fetch.tar.gz');
|
|
38
|
+
|
|
39
|
+
const PKG_LABEL = 'basis-wasm';
|
|
40
|
+
const BUILD_HINT =
|
|
41
|
+
'compile locally with `pnpm -F @forgeax/engine-codec build:wasm` (needs the Emscripten emcc toolchain)';
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Main
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
if (process.env.FORGEAX_SKIP_CODEC_WASM_FETCH) {
|
|
49
|
+
console.log('fetch-wasm: FORGEAX_SKIP_CODEC_WASM_FETCH set — skipping.');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('Resolving git origin...');
|
|
54
|
+
const { owner, repo } = getGitOrigin();
|
|
55
|
+
console.log(` origin: ${owner}/${repo}`);
|
|
56
|
+
|
|
57
|
+
console.log('Computing content key (fetch-basis.mjs + build-wasm.mjs)...');
|
|
58
|
+
const { sha256, assetName } = await resolveAsset();
|
|
59
|
+
console.log(` content SHA256: ${sha256}`);
|
|
60
|
+
console.log(` asset name: ${assetName}`);
|
|
61
|
+
|
|
62
|
+
console.log(`Fetching release tag "${RELEASE_TAG}" from ${owner}/${repo}...`);
|
|
63
|
+
const asset = await getReleaseAsset(owner, repo, RELEASE_TAG, assetName, {
|
|
64
|
+
pkgLabel: PKG_LABEL,
|
|
65
|
+
buildHint: BUILD_HINT,
|
|
66
|
+
missingAssetHint:
|
|
67
|
+
'The codec build scripts have uncommitted changes, or the release for this content is not yet published.',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(`Downloading ${assetName} (${(asset.size / 1024).toFixed(0)} KB)...`);
|
|
71
|
+
await downloadAsset(asset, TMP_TARBALL, {
|
|
72
|
+
owner,
|
|
73
|
+
repo,
|
|
74
|
+
tag: RELEASE_TAG,
|
|
75
|
+
pkgLabel: PKG_LABEL,
|
|
76
|
+
buildHint: BUILD_HINT,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
console.log(`Extracting into ${PKG_DIR} ...`);
|
|
80
|
+
await extractTarball(TMP_TARBALL, PKG_DIR);
|
|
81
|
+
await rm(TMP_TARBALL, { force: true });
|
|
82
|
+
|
|
83
|
+
const members = await readdir(PKG_DIR);
|
|
84
|
+
console.log(` pkg/ now holds: ${members.join(', ')}`);
|
|
85
|
+
console.log('Done.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
main().catch(async (e) => {
|
|
89
|
+
await rm(TMP_TARBALL, { force: true }).catch(() => {});
|
|
90
|
+
if (e instanceof FetchError) {
|
|
91
|
+
console.error(`\n[${e.code}] ${e.message}`);
|
|
92
|
+
console.error(`Hint: ${e.hint}`);
|
|
93
|
+
} else {
|
|
94
|
+
console.error('\nUnexpected error:', e.message || e);
|
|
95
|
+
}
|
|
96
|
+
process.exit(1);
|
|
97
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// basis-encode-fast-preset.unit.test.ts -- R-9 fast-preset gate for the w17
|
|
2
|
+
// basisEncode wrapper.
|
|
3
|
+
//
|
|
4
|
+
// M5/w38 flipped the sidecar compressionMode default to 'auto', routing every
|
|
5
|
+
// existing texture through Basis encode. The single-threaded encoder's default
|
|
6
|
+
// effort tiers pushed the model-loading demo build past 10 minutes. R-9 remedy
|
|
7
|
+
// (option 2) drops each arm to its lowest effort tier inside basisEncode:
|
|
8
|
+
// etc1s -> setETC1SCompressionLevel(0) (perf lever, quality unchanged)
|
|
9
|
+
// uastc-ldr -> setPackUASTCFlags(cPackUASTCLevelFastest)
|
|
10
|
+
// uastc-hdr -> setUASTCHDRQualityLevel(0)
|
|
11
|
+
//
|
|
12
|
+
// This gate guards the two load-bearing invariants the speedup must NOT break:
|
|
13
|
+
// (a) the fast-preset output is still a valid Basis KTX2 -- parseKtx2 accepts
|
|
14
|
+
// it and transcodeKtx2 produces a nonempty BC7 mip (functional).
|
|
15
|
+
// (b) AC-02 / R-11 determinism survives the effort change -- encoding the same
|
|
16
|
+
// pixels twice through basisEncode is byte-identical.
|
|
17
|
+
//
|
|
18
|
+
// This exercises the w17 basisEncode WRAPPER (not the raw encoder like w15), so
|
|
19
|
+
// it locks the preset wiring the wrapper adds. pkg/ is a gitignored emcc build
|
|
20
|
+
// artifact (AC-12); the WASM cases skip when it is not built (contributor
|
|
21
|
+
// without emsdk) and run in CI's build-artifacts job.
|
|
22
|
+
|
|
23
|
+
import { existsSync } from 'node:fs';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { parseKtx2, transcodeKtx2 } from '@forgeax/engine-codec';
|
|
26
|
+
import { basisEncode } from '@forgeax/engine-codec/encode';
|
|
27
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
28
|
+
import { _setBasisEncoderImporter } from '../encode/basis-encode.js';
|
|
29
|
+
|
|
30
|
+
const ENCODER_GLUE = new URL('../../pkg/encode/basis_encoder.mjs', import.meta.url);
|
|
31
|
+
const TRANSCODER_GLUE = new URL('../../pkg/basis_transcoder.mjs', import.meta.url);
|
|
32
|
+
const pkgBuilt =
|
|
33
|
+
existsSync(fileURLToPath(ENCODER_GLUE)) && existsSync(fileURLToPath(TRANSCODER_GLUE));
|
|
34
|
+
|
|
35
|
+
/** A deterministic WxH tight-packed RGBA image. */
|
|
36
|
+
function makeRgba(w: number, h: number): Uint8Array {
|
|
37
|
+
const img = new Uint8Array(w * h * 4);
|
|
38
|
+
for (let i = 0; i < w * h; i++) {
|
|
39
|
+
img[i * 4] = (i * 7) & 255;
|
|
40
|
+
img[i * 4 + 1] = (i * 13) & 255;
|
|
41
|
+
img[i * 4 + 2] = (i * 29) & 255;
|
|
42
|
+
img[i * 4 + 3] = 255;
|
|
43
|
+
}
|
|
44
|
+
return img;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A deterministic WxH rgba16float image (HDR arm source bytes). */
|
|
48
|
+
function makeHdrF16(w: number, h: number): Uint8Array {
|
|
49
|
+
const out = new Uint16Array(w * h * 4);
|
|
50
|
+
const toF16 = (value: number): number => {
|
|
51
|
+
const f32 = new Float32Array(1);
|
|
52
|
+
const u32 = new Uint32Array(f32.buffer);
|
|
53
|
+
f32[0] = value;
|
|
54
|
+
const x = u32[0] ?? 0;
|
|
55
|
+
const sign = (x >>> 16) & 0x8000;
|
|
56
|
+
const exp = (x >>> 23) & 0xff;
|
|
57
|
+
const mant = x & 0x7fffff;
|
|
58
|
+
if (exp === 0) return sign;
|
|
59
|
+
const e = exp - 127 + 15;
|
|
60
|
+
if (e <= 0) return sign;
|
|
61
|
+
if (e >= 0x1f) return sign | 0x7c00;
|
|
62
|
+
return sign | (e << 10) | (mant >> 13);
|
|
63
|
+
};
|
|
64
|
+
for (let i = 0; i < w * h; i++) {
|
|
65
|
+
out[i * 4] = toF16((i % 8) / 8);
|
|
66
|
+
out[i * 4 + 1] = toF16((i % 4) / 4);
|
|
67
|
+
out[i * 4 + 2] = toF16(0.5);
|
|
68
|
+
out[i * 4 + 3] = toF16(1);
|
|
69
|
+
}
|
|
70
|
+
return new Uint8Array(out.buffer);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
74
|
+
if (a.length !== b.length) return false;
|
|
75
|
+
for (let i = 0; i < a.length; i++) {
|
|
76
|
+
if (a[i] !== b[i]) return false;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe.skipIf(!pkgBuilt)('basisEncode fast preset (R-9 / w17)', () => {
|
|
82
|
+
beforeAll(() => {
|
|
83
|
+
// Use the real WASM importer (default). Reset the singleton so this file's
|
|
84
|
+
// first encode drives a clean init regardless of test-file ordering.
|
|
85
|
+
_setBasisEncoderImporter();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('etc1s fast preset round-trips through parseKtx2 + transcodeKtx2', async () => {
|
|
89
|
+
const r = await basisEncode(makeRgba(16, 16), {
|
|
90
|
+
mode: 'etc1s',
|
|
91
|
+
width: 16,
|
|
92
|
+
height: 16,
|
|
93
|
+
srgb: true,
|
|
94
|
+
perceptual: true,
|
|
95
|
+
uastcSupercompression: false,
|
|
96
|
+
mipGen: false,
|
|
97
|
+
});
|
|
98
|
+
expect(r.ok).toBe(true);
|
|
99
|
+
if (!r.ok) return;
|
|
100
|
+
const parsed = await parseKtx2(r.value);
|
|
101
|
+
expect(parsed.ok).toBe(true);
|
|
102
|
+
if (!parsed.ok) return;
|
|
103
|
+
const transcoded = await transcodeKtx2(parsed.value, 'bc7-rgba-unorm');
|
|
104
|
+
expect(transcoded.ok).toBe(true);
|
|
105
|
+
if (!transcoded.ok) return;
|
|
106
|
+
expect(transcoded.value.mips[0]?.data.length).toBeGreaterThan(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('uastc-ldr fast preset round-trips through parseKtx2 + transcodeKtx2', async () => {
|
|
110
|
+
const r = await basisEncode(makeRgba(16, 16), {
|
|
111
|
+
mode: 'uastc-ldr',
|
|
112
|
+
width: 16,
|
|
113
|
+
height: 16,
|
|
114
|
+
srgb: true,
|
|
115
|
+
perceptual: true,
|
|
116
|
+
uastcSupercompression: true,
|
|
117
|
+
mipGen: false,
|
|
118
|
+
});
|
|
119
|
+
expect(r.ok).toBe(true);
|
|
120
|
+
if (!r.ok) return;
|
|
121
|
+
const parsed = await parseKtx2(r.value);
|
|
122
|
+
expect(parsed.ok).toBe(true);
|
|
123
|
+
if (!parsed.ok) return;
|
|
124
|
+
const transcoded = await transcodeKtx2(parsed.value, 'bc7-rgba-unorm');
|
|
125
|
+
expect(transcoded.ok).toBe(true);
|
|
126
|
+
if (!transcoded.ok) return;
|
|
127
|
+
expect(transcoded.value.mips[0]?.data.length).toBeGreaterThan(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('uastc-hdr fast preset round-trips through parseKtx2 + transcodeKtx2', async () => {
|
|
131
|
+
const r = await basisEncode(makeHdrF16(16, 16), {
|
|
132
|
+
mode: 'uastc-hdr',
|
|
133
|
+
width: 16,
|
|
134
|
+
height: 16,
|
|
135
|
+
srgb: false,
|
|
136
|
+
perceptual: false,
|
|
137
|
+
uastcSupercompression: false,
|
|
138
|
+
mipGen: false,
|
|
139
|
+
});
|
|
140
|
+
expect(r.ok).toBe(true);
|
|
141
|
+
if (!r.ok) return;
|
|
142
|
+
const parsed = await parseKtx2(r.value);
|
|
143
|
+
expect(parsed.ok).toBe(true);
|
|
144
|
+
if (!parsed.ok) return;
|
|
145
|
+
const transcoded = await transcodeKtx2(parsed.value, 'bc6h-rgb-ufloat');
|
|
146
|
+
expect(transcoded.ok).toBe(true);
|
|
147
|
+
if (!transcoded.ok) return;
|
|
148
|
+
expect(transcoded.value.mips[0]?.data.length).toBeGreaterThan(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('AC-02: double encode with fast preset is byte-equal (etc1s)', async () => {
|
|
152
|
+
const img = makeRgba(16, 16);
|
|
153
|
+
const opts = {
|
|
154
|
+
mode: 'etc1s',
|
|
155
|
+
width: 16,
|
|
156
|
+
height: 16,
|
|
157
|
+
srgb: true,
|
|
158
|
+
perceptual: true,
|
|
159
|
+
uastcSupercompression: false,
|
|
160
|
+
mipGen: false,
|
|
161
|
+
} as const;
|
|
162
|
+
const a = await basisEncode(img, opts);
|
|
163
|
+
const b = await basisEncode(img, opts);
|
|
164
|
+
expect(a.ok && b.ok).toBe(true);
|
|
165
|
+
if (!(a.ok && b.ok)) return;
|
|
166
|
+
expect(bytesEqual(a.value, b.value)).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('AC-02: double encode with fast preset is byte-equal (uastc-ldr)', async () => {
|
|
170
|
+
const img = makeRgba(16, 16);
|
|
171
|
+
const opts = {
|
|
172
|
+
mode: 'uastc-ldr',
|
|
173
|
+
width: 16,
|
|
174
|
+
height: 16,
|
|
175
|
+
srgb: true,
|
|
176
|
+
perceptual: true,
|
|
177
|
+
uastcSupercompression: true,
|
|
178
|
+
mipGen: false,
|
|
179
|
+
} as const;
|
|
180
|
+
const a = await basisEncode(img, opts);
|
|
181
|
+
const b = await basisEncode(img, opts);
|
|
182
|
+
expect(a.ok && b.ok).toBe(true);
|
|
183
|
+
if (!(a.ok && b.ok)) return;
|
|
184
|
+
expect(bytesEqual(a.value, b.value)).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('AC-02: double encode with fast preset is byte-equal (uastc-hdr)', async () => {
|
|
188
|
+
const img = makeHdrF16(16, 16);
|
|
189
|
+
const opts = {
|
|
190
|
+
mode: 'uastc-hdr',
|
|
191
|
+
width: 16,
|
|
192
|
+
height: 16,
|
|
193
|
+
srgb: false,
|
|
194
|
+
perceptual: false,
|
|
195
|
+
uastcSupercompression: false,
|
|
196
|
+
mipGen: false,
|
|
197
|
+
} as const;
|
|
198
|
+
const a = await basisEncode(img, opts);
|
|
199
|
+
const b = await basisEncode(img, opts);
|
|
200
|
+
expect(a.ok && b.ok).toBe(true);
|
|
201
|
+
if (!(a.ok && b.ok)) return;
|
|
202
|
+
expect(bytesEqual(a.value, b.value)).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { BasisEncodeMode, BasisEncodeOptions } from '@forgeax/engine-codec/encode';
|
|
2
|
+
import {
|
|
3
|
+
_basisEncoderInitCount,
|
|
4
|
+
_setBasisEncoderImporter,
|
|
5
|
+
basisEncode,
|
|
6
|
+
} from '@forgeax/engine-codec/encode';
|
|
7
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
const TEST_CONSTRUCTOR_SENTINEL = 'm120-controlled-constructor-throw';
|
|
10
|
+
const TEST_DELETE_SENTINEL = 'm120-controlled-delete-throw';
|
|
11
|
+
const TEST_OPERATION_SENTINEL = 'm120-controlled-operation-throw';
|
|
12
|
+
const CLEANUP_FAILURE_REASON = 'encoder cleanup failed';
|
|
13
|
+
const CONSTRUCTOR_FAILURE_REASON = 'encoder constructor failed';
|
|
14
|
+
const MODES = ['etc1s', 'uastc-ldr', 'uastc-hdr'] as const;
|
|
15
|
+
|
|
16
|
+
interface FakeEncoderState {
|
|
17
|
+
readonly constructorIds: number[];
|
|
18
|
+
readonly deleteIds: number[];
|
|
19
|
+
readonly sourceCalls: { image: Uint8Array; width: number; height: number; hdr: boolean }[];
|
|
20
|
+
readonly formatModes: number[];
|
|
21
|
+
encodeCalls: number;
|
|
22
|
+
shouldConstructThrow: boolean;
|
|
23
|
+
shouldDeleteThrow: boolean;
|
|
24
|
+
shouldSourceFail: boolean;
|
|
25
|
+
shouldOperationThrow: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeOptions(mode: BasisEncodeMode): BasisEncodeOptions {
|
|
29
|
+
return {
|
|
30
|
+
mode,
|
|
31
|
+
width: 2,
|
|
32
|
+
height: 2,
|
|
33
|
+
srgb: mode !== 'uastc-hdr',
|
|
34
|
+
perceptual: mode !== 'uastc-hdr',
|
|
35
|
+
uastcSupercompression: mode === 'uastc-ldr',
|
|
36
|
+
mipGen: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makePixels(mode: BasisEncodeMode): Uint8Array {
|
|
41
|
+
return mode === 'uastc-hdr'
|
|
42
|
+
? new Uint8Array(2 * 2 * 4 * 2).fill(0x3c)
|
|
43
|
+
: new Uint8Array(2 * 2 * 4).fill(0x7f);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createFakeEncoderModule() {
|
|
47
|
+
const state: FakeEncoderState = {
|
|
48
|
+
constructorIds: [],
|
|
49
|
+
deleteIds: [],
|
|
50
|
+
sourceCalls: [],
|
|
51
|
+
formatModes: [],
|
|
52
|
+
encodeCalls: 0,
|
|
53
|
+
shouldConstructThrow: true,
|
|
54
|
+
shouldDeleteThrow: false,
|
|
55
|
+
shouldSourceFail: false,
|
|
56
|
+
shouldOperationThrow: false,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
class FakeBasisEncoder {
|
|
60
|
+
readonly id: number;
|
|
61
|
+
|
|
62
|
+
constructor() {
|
|
63
|
+
const id = state.constructorIds.length + 1;
|
|
64
|
+
state.constructorIds.push(id);
|
|
65
|
+
if (state.shouldConstructThrow) throw TEST_CONSTRUCTOR_SENTINEL;
|
|
66
|
+
this.id = id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
controlThreading(_enabled: boolean, _workers: number): void {}
|
|
70
|
+
|
|
71
|
+
setSliceSourceImage(
|
|
72
|
+
_slice: number,
|
|
73
|
+
image: Uint8Array,
|
|
74
|
+
width: number,
|
|
75
|
+
height: number,
|
|
76
|
+
_imageType: number,
|
|
77
|
+
): boolean {
|
|
78
|
+
state.sourceCalls.push({ image, width, height, hdr: false });
|
|
79
|
+
return !state.shouldSourceFail;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setSliceSourceImageHDR(
|
|
83
|
+
_slice: number,
|
|
84
|
+
image: Uint8Array,
|
|
85
|
+
width: number,
|
|
86
|
+
height: number,
|
|
87
|
+
_imageType: number,
|
|
88
|
+
_ldrSrgbToLinear: boolean,
|
|
89
|
+
_ldrToHdrNitMultiplier: number,
|
|
90
|
+
): boolean {
|
|
91
|
+
state.sourceCalls.push({ image, width, height, hdr: true });
|
|
92
|
+
return !state.shouldSourceFail;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setFormatMode(mode: number): void {
|
|
96
|
+
state.formatModes.push(mode);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setCreateKTX2File(_enabled: boolean): void {}
|
|
100
|
+
|
|
101
|
+
setKTX2UASTCSupercompression(_enabled: boolean): void {}
|
|
102
|
+
|
|
103
|
+
setKTX2AndBasisSRGBTransferFunc(_enabled: boolean): void {}
|
|
104
|
+
|
|
105
|
+
setPerceptual(_enabled: boolean): void {}
|
|
106
|
+
|
|
107
|
+
setMipGen(_enabled: boolean): void {}
|
|
108
|
+
|
|
109
|
+
setQualityLevel(_quality: number): void {}
|
|
110
|
+
|
|
111
|
+
setETC1SCompressionLevel(_level: number): void {}
|
|
112
|
+
|
|
113
|
+
setPackUASTCFlags(_flags: number): void {}
|
|
114
|
+
|
|
115
|
+
setUASTCHDRQualityLevel(_level: number): void {}
|
|
116
|
+
|
|
117
|
+
encode(destination: Uint8Array): number {
|
|
118
|
+
state.encodeCalls++;
|
|
119
|
+
if (state.shouldOperationThrow) throw TEST_OPERATION_SENTINEL;
|
|
120
|
+
const bytes = new Uint8Array([0xa1, state.formatModes.at(-1) ?? 0, 0x42, 0x99]);
|
|
121
|
+
destination.set(bytes);
|
|
122
|
+
return bytes.length;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
delete(): void {
|
|
126
|
+
state.deleteIds.push(this.id);
|
|
127
|
+
if (state.shouldDeleteThrow) throw TEST_DELETE_SENTINEL;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
state,
|
|
133
|
+
module: {
|
|
134
|
+
initializeBasis(): void {},
|
|
135
|
+
basis_tex_format: {
|
|
136
|
+
cETC1S: { value: 1 },
|
|
137
|
+
cUASTC_LDR_4x4: { value: 2 },
|
|
138
|
+
cUASTC_HDR_4x4: { value: 3 },
|
|
139
|
+
},
|
|
140
|
+
BasisEncoder: FakeBasisEncoder,
|
|
141
|
+
KTX2File: class {},
|
|
142
|
+
} as never,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
afterEach(() => {
|
|
147
|
+
_setBasisEncoderImporter();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('basisEncode lifecycle throw containment (M120)', () => {
|
|
151
|
+
it.each(
|
|
152
|
+
MODES,
|
|
153
|
+
)('contains lifecycle throws and retries the same %s module deterministically', async (mode) => {
|
|
154
|
+
const pixels = makePixels(mode);
|
|
155
|
+
const options = makeOptions(mode);
|
|
156
|
+
const fixture = createFakeEncoderModule();
|
|
157
|
+
let importerCalls = 0;
|
|
158
|
+
_setBasisEncoderImporter(() => {
|
|
159
|
+
importerCalls++;
|
|
160
|
+
return Promise.resolve(fixture.module);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const constructorFailure = await basisEncode(pixels, options);
|
|
164
|
+
expect(constructorFailure.ok).toBe(false);
|
|
165
|
+
if (constructorFailure.ok) throw new Error('expected constructor refusal');
|
|
166
|
+
expect(constructorFailure.error.code).toBe('ktx2-encode-failed');
|
|
167
|
+
expect(constructorFailure.error.detail).toEqual({
|
|
168
|
+
mode,
|
|
169
|
+
reason: CONSTRUCTOR_FAILURE_REASON,
|
|
170
|
+
});
|
|
171
|
+
expect(JSON.stringify(constructorFailure)).not.toContain(TEST_CONSTRUCTOR_SENTINEL);
|
|
172
|
+
expect(fixture.state.deleteIds).toEqual([]);
|
|
173
|
+
|
|
174
|
+
fixture.state.shouldConstructThrow = false;
|
|
175
|
+
fixture.state.shouldDeleteThrow = true;
|
|
176
|
+
const cleanupFailure = await basisEncode(pixels, options);
|
|
177
|
+
expect(cleanupFailure.ok).toBe(false);
|
|
178
|
+
if (cleanupFailure.ok) throw new Error('expected cleanup refusal');
|
|
179
|
+
expect(cleanupFailure.error.code).toBe('ktx2-encode-failed');
|
|
180
|
+
expect(cleanupFailure.error.detail).toEqual({
|
|
181
|
+
mode,
|
|
182
|
+
reason: CLEANUP_FAILURE_REASON,
|
|
183
|
+
});
|
|
184
|
+
expect(JSON.stringify(cleanupFailure)).not.toContain(TEST_DELETE_SENTINEL);
|
|
185
|
+
expect(fixture.state.deleteIds).toEqual([2]);
|
|
186
|
+
|
|
187
|
+
fixture.state.shouldOperationThrow = true;
|
|
188
|
+
const primaryFailure = await basisEncode(pixels, options);
|
|
189
|
+
expect(primaryFailure.ok).toBe(false);
|
|
190
|
+
if (primaryFailure.ok) throw new Error('expected primary operation refusal');
|
|
191
|
+
expect(primaryFailure.error.code).toBe('ktx2-encode-failed');
|
|
192
|
+
expect(primaryFailure.error.detail).toEqual({
|
|
193
|
+
mode,
|
|
194
|
+
reason: TEST_OPERATION_SENTINEL,
|
|
195
|
+
});
|
|
196
|
+
expect(JSON.stringify(primaryFailure)).toContain(TEST_OPERATION_SENTINEL);
|
|
197
|
+
expect(JSON.stringify(primaryFailure)).not.toContain(TEST_DELETE_SENTINEL);
|
|
198
|
+
expect(fixture.state.deleteIds).toEqual([2, 3]);
|
|
199
|
+
|
|
200
|
+
fixture.state.shouldOperationThrow = false;
|
|
201
|
+
fixture.state.shouldDeleteThrow = false;
|
|
202
|
+
const repaired = await basisEncode(pixels, options);
|
|
203
|
+
expect(repaired.ok).toBe(true);
|
|
204
|
+
if (!repaired.ok) throw new Error('expected repaired encode to succeed');
|
|
205
|
+
expect([...repaired.value]).toEqual([
|
|
206
|
+
0xa1,
|
|
207
|
+
mode === 'etc1s' ? 1 : mode === 'uastc-ldr' ? 2 : 3,
|
|
208
|
+
0x42,
|
|
209
|
+
0x99,
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
const third = await basisEncode(pixels, options);
|
|
213
|
+
expect(third).toEqual(repaired);
|
|
214
|
+
expect(importerCalls).toBe(1);
|
|
215
|
+
expect(_basisEncoderInitCount()).toBe(1);
|
|
216
|
+
expect(fixture.state.constructorIds).toEqual([1, 2, 3, 4, 5]);
|
|
217
|
+
expect(fixture.state.deleteIds).toEqual([2, 3, 4, 5]);
|
|
218
|
+
expect(fixture.state.sourceCalls).toHaveLength(4);
|
|
219
|
+
expect(fixture.state.sourceCalls.every(({ image }) => image === pixels)).toBe(true);
|
|
220
|
+
expect(
|
|
221
|
+
fixture.state.sourceCalls.every(({ width, height }) => width === 2 && height === 2),
|
|
222
|
+
).toBe(true);
|
|
223
|
+
expect(fixture.state.sourceCalls.every(({ hdr }) => hdr === (mode === 'uastc-hdr'))).toBe(true);
|
|
224
|
+
expect(fixture.state.formatModes).toEqual([
|
|
225
|
+
mode === 'etc1s' ? 1 : mode === 'uastc-ldr' ? 2 : 3,
|
|
226
|
+
mode === 'etc1s' ? 1 : mode === 'uastc-ldr' ? 2 : 3,
|
|
227
|
+
mode === 'etc1s' ? 1 : mode === 'uastc-ldr' ? 2 : 3,
|
|
228
|
+
mode === 'etc1s' ? 1 : mode === 'uastc-ldr' ? 2 : 3,
|
|
229
|
+
]);
|
|
230
|
+
expect(fixture.state.encodeCalls).toBe(4);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it.each(MODES)('does not construct an encoder for invalid %s dimensions', async (mode) => {
|
|
234
|
+
const fixture = createFakeEncoderModule();
|
|
235
|
+
fixture.state.shouldConstructThrow = false;
|
|
236
|
+
_setBasisEncoderImporter(() => Promise.resolve(fixture.module));
|
|
237
|
+
|
|
238
|
+
const result = await basisEncode(makePixels(mode), {
|
|
239
|
+
...makeOptions(mode),
|
|
240
|
+
width: 0,
|
|
241
|
+
});
|
|
242
|
+
expect(result.ok).toBe(false);
|
|
243
|
+
if (result.ok) throw new Error('expected invalid dimensions to fail');
|
|
244
|
+
expect(result.error.code).toBe('ktx2-encode-failed');
|
|
245
|
+
expect(result.error.detail).toEqual({ mode, reason: 'invalid dimensions 0x2' });
|
|
246
|
+
expect(fixture.state.constructorIds).toEqual([]);
|
|
247
|
+
expect(fixture.state.deleteIds).toEqual([]);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
5
|
+
import type { BasisEncodeMode } from '../encode/basis-encode.js';
|
|
6
|
+
import type { TranscodeModel } from '../transcode.js';
|
|
7
|
+
|
|
8
|
+
const basisEncodeSource = readFileSync(
|
|
9
|
+
new URL('../encode/basis-encode.ts', import.meta.url),
|
|
10
|
+
'utf8',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
describe('BasisEncodeMode owner', () => {
|
|
14
|
+
it('keeps the encode and transcode vocabularies bilaterally equal', () => {
|
|
15
|
+
expectTypeOf<BasisEncodeMode>().toEqualTypeOf<TranscodeModel>();
|
|
16
|
+
expectTypeOf<TranscodeModel>().toEqualTypeOf<BasisEncodeMode>();
|
|
17
|
+
|
|
18
|
+
const acceptsEncodeMode = (mode: BasisEncodeMode): BasisEncodeMode => mode;
|
|
19
|
+
const acceptsTranscodeModel = (model: TranscodeModel): TranscodeModel => model;
|
|
20
|
+
acceptsEncodeMode('etc1s');
|
|
21
|
+
acceptsEncodeMode('uastc-ldr');
|
|
22
|
+
acceptsEncodeMode('uastc-hdr');
|
|
23
|
+
acceptsTranscodeModel('etc1s');
|
|
24
|
+
acceptsTranscodeModel('uastc-ldr');
|
|
25
|
+
acceptsTranscodeModel('uastc-hdr');
|
|
26
|
+
// @ts-expect-error unknown Basis encode modes remain outside the closed owner union.
|
|
27
|
+
acceptsEncodeMode('basis-mode-not-real');
|
|
28
|
+
// @ts-expect-error unknown transcode models remain outside the closed owner union.
|
|
29
|
+
acceptsTranscodeModel('basis-mode-not-real');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('keeps BasisEncodeMode derived from the public transcode owner', () => {
|
|
33
|
+
expect(basisEncodeSource).toContain("import type { TranscodeModel } from '../transcode.js';");
|
|
34
|
+
expect(basisEncodeSource).toContain('export type BasisEncodeMode = TranscodeModel;');
|
|
35
|
+
expect(basisEncodeSource).not.toContain(
|
|
36
|
+
"export type BasisEncodeMode = 'etc1s' | 'uastc-ldr' | 'uastc-hdr';",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|