@forgeax/engine-assets-runtime 0.1.19 → 0.1.20
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/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/texture-pack-loader.unit.test.d.ts +2 -0
- package/dist/__tests__/texture-pack-loader.unit.test.d.ts.map +1 -0
- package/dist/decode-image-bytes.d.ts +2 -2
- package/dist/decode-image-bytes.d.ts.map +1 -1
- package/dist/errors/asset.d.ts +20 -0
- package/dist/errors/asset.d.ts.map +1 -1
- package/dist/index.mjs +259 -231
- package/dist/index.mjs.map +1 -1
- package/dist/loaders/pack-artifact.d.ts +25 -1
- package/dist/loaders/pack-artifact.d.ts.map +1 -1
- package/dist/registry/validate-material.d.ts +25 -0
- package/dist/registry/validate-material.d.ts.map +1 -1
- package/package.json +14 -14
- package/src/__tests__/decode-image-bytes.browser.test.ts +5 -8
- package/src/__tests__/pack-basis-load.integration.test.ts +66 -7
- package/src/__tests__/texture-pack-loader.unit.test.ts +145 -0
- package/src/decode-image-bytes.ts +6 -11
- package/src/errors/asset.ts +33 -0
- package/src/loaders/pack-artifact.ts +218 -17
- package/src/registry/validate-material.ts +54 -0
|
@@ -1,4 +1,26 @@
|
|
|
1
|
-
import type { Loader } from '@forgeax/engine-types';
|
|
1
|
+
import type { LoadContext, Loader, TextureAsset } from '@forgeax/engine-types';
|
|
2
|
+
import { AssetError } from '@forgeax/engine-types';
|
|
3
|
+
import { TexturePackVerificationError } from '../errors/asset.js';
|
|
4
|
+
import type { PackLoaderInput } from '../loader-registry';
|
|
5
|
+
export interface TexturePackLoadInput {
|
|
6
|
+
readonly pack: PackLoaderInput;
|
|
7
|
+
readonly sourceKey: string;
|
|
8
|
+
readonly generation: number;
|
|
9
|
+
readonly expectedDigest: string;
|
|
10
|
+
}
|
|
11
|
+
export type VerifiedTexturePack = {
|
|
12
|
+
readonly asset: TextureAsset;
|
|
13
|
+
readonly sourceKey: string;
|
|
14
|
+
readonly generation: number;
|
|
15
|
+
readonly digest: string;
|
|
16
|
+
};
|
|
17
|
+
export type VerifiedTexturePackResult = {
|
|
18
|
+
readonly ok: true;
|
|
19
|
+
readonly value: VerifiedTexturePack;
|
|
20
|
+
} | {
|
|
21
|
+
readonly ok: false;
|
|
22
|
+
readonly error: TexturePackVerificationError | AssetError;
|
|
23
|
+
};
|
|
2
24
|
export type CodecContextFailure = {
|
|
3
25
|
readonly code: 'basis-profile-unsupported';
|
|
4
26
|
readonly detail: {
|
|
@@ -11,6 +33,8 @@ export type CodecContextFailure = {
|
|
|
11
33
|
readonly dfdColorSpace: 'srgb' | 'linear' | 'unknown';
|
|
12
34
|
};
|
|
13
35
|
};
|
|
36
|
+
/** Verify producer facts and then expose one accepted texture projection. */
|
|
37
|
+
export declare function loadVerifiedTexturePack(input: TexturePackLoadInput, ctx: LoadContext): Promise<VerifiedTexturePackResult>;
|
|
14
38
|
export declare const textureLoader: Loader;
|
|
15
39
|
export declare const equirectLoader: Loader;
|
|
16
40
|
export declare const fontLoader: Loader;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pack-artifact.d.ts","sourceRoot":"","sources":["../../src/loaders/pack-artifact.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"pack-artifact.d.ts","sourceRoot":"","sources":["../../src/loaders/pack-artifact.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAKV,WAAW,EACX,MAAM,EAGN,YAAY,EAEb,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAuB,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAEL,4BAA4B,EAC7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAI1D,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GACjC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAA;CAAE,GAC1D;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,4BAA4B,GAAG,UAAU,CAAA;CAAE,CAAC;AAkPtF,MAAM,MAAM,mBAAmB,GAC3B;IACE,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,QAAQ,CAAC;QAC/C,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;KACvD,CAAC;CACH,CAAC;AAkRN,6EAA6E;AAC7E,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,oBAAoB,EAC3B,GAAG,EAAE,WAAW,GACf,OAAO,CAAC,yBAAyB,CAAC,CAiHpC;AA8GD,eAAO,MAAM,aAAa,EAAE,MAI3B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,MAI5B,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,MAIxB,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,MAkBlC,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,MA0B3B,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAMlD,CAAC"}
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import type { CookedMaterialRecord } from '@forgeax/engine-pack';
|
|
2
2
|
import { AssetError, type MaterialAsset, type MaterialError } from '@forgeax/engine-types';
|
|
3
3
|
import type { AssetRegistry } from '../asset-registry';
|
|
4
|
+
export interface TexturePublicationStateSnapshot {
|
|
5
|
+
readonly guid: string;
|
|
6
|
+
readonly sourceKey: string;
|
|
7
|
+
readonly acceptedGeneration: number;
|
|
8
|
+
readonly acceptedDigest: string;
|
|
9
|
+
readonly candidateGeneration?: number;
|
|
10
|
+
readonly candidateFailure?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TexturePublicationState {
|
|
13
|
+
reject(candidate: {
|
|
14
|
+
readonly generation: number;
|
|
15
|
+
readonly reason: string;
|
|
16
|
+
}): TexturePublicationStateSnapshot;
|
|
17
|
+
accept(candidate: {
|
|
18
|
+
readonly generation: number;
|
|
19
|
+
readonly digest: string;
|
|
20
|
+
}): TexturePublicationStateSnapshot;
|
|
21
|
+
}
|
|
22
|
+
/** Candidate/accepted state that keeps the last known good publication honest. */
|
|
23
|
+
export declare function createTexturePublicationState(input: {
|
|
24
|
+
readonly guid: string;
|
|
25
|
+
readonly sourceKey: string;
|
|
26
|
+
readonly generation: number;
|
|
27
|
+
readonly digest: string;
|
|
28
|
+
}): TexturePublicationState;
|
|
4
29
|
/**
|
|
5
30
|
* Validate a MaterialAsset's passes[] against the ShaderRegistry's
|
|
6
31
|
* paramSchema (union semantics: all declared params across all passes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-material.d.ts","sourceRoot":"","sources":["../../src/registry/validate-material.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EACL,UAAU,EAEV,KAAK,aAAa,EAClB,KAAK,aAAa,EAEnB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,aAAa,GACnB,UAAU,GAAG,IAAI,CAoFnB;AA4CD,wBAAgB,oCAAoC,CAAC,KAAK,EAAE,aAAa,GAAG,aAAa,GAAG,IAAI,CAkD/F;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,MAAM,GAAG,SAAS,CAAC,EACtD,IAAI,EAAE,6BAA6B,GAClC,aAAa,GAAG,IAAI,CAiBtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,aAAa,EACxB,KAAK,EAAE,aAAa,GACnB,UAAU,GAAG,IAAI,CA8GnB;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,aAAa,EACxB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,GACb,OAAO,CAgCT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CA8BhG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,MAAM,GACf,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,CAIjC"}
|
|
1
|
+
{"version":3,"file":"validate-material.d.ts","sourceRoot":"","sources":["../../src/registry/validate-material.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EACL,UAAU,EAEV,KAAK,aAAa,EAClB,KAAK,aAAa,EAEnB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,SAAS,EAAE;QAChB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACzB,GAAG,+BAA+B,CAAC;IACpC,MAAM,CAAC,SAAS,EAAE;QAChB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACzB,GAAG,+BAA+B,CAAC;CACrC;AAED,kFAAkF;AAClF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GAAG,uBAAuB,CA0B1B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,aAAa,GACnB,UAAU,GAAG,IAAI,CAoFnB;AA4CD,wBAAgB,oCAAoC,CAAC,KAAK,EAAE,aAAa,GAAG,aAAa,GAAG,IAAI,CAkD/F;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,MAAM,GAAG,SAAS,CAAC,EACtD,IAAI,EAAE,6BAA6B,GAClC,aAAa,GAAG,IAAI,CAiBtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,aAAa,EACxB,KAAK,EAAE,aAAa,GACnB,UAAU,GAAG,IAAI,CA8GnB;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,aAAa,EACxB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,GACb,OAAO,CAgCT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CA8BhG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,MAAM,GACf,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,CAIjC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-assets-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -26,22 +26,22 @@
|
|
|
26
26
|
"LICENSE"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@forgeax/engine-codec": "0.1.
|
|
30
|
-
"@forgeax/engine-ecs": "0.1.
|
|
31
|
-
"@forgeax/engine-geometry": "0.1.
|
|
32
|
-
"@forgeax/engine-graphics-extras": "0.1.
|
|
33
|
-
"@forgeax/engine-image": "0.1.
|
|
34
|
-
"@forgeax/engine-pack": "0.1.
|
|
35
|
-
"@forgeax/engine-plugin": "0.1.
|
|
36
|
-
"@forgeax/engine-rhi": "0.1.
|
|
37
|
-
"@forgeax/engine-scene": "0.1.
|
|
38
|
-
"@forgeax/engine-shader": "0.1.
|
|
39
|
-
"@forgeax/engine-types": "0.1.
|
|
40
|
-
"@forgeax/engine-ui": "0.1.
|
|
29
|
+
"@forgeax/engine-codec": "0.1.20",
|
|
30
|
+
"@forgeax/engine-ecs": "0.1.20",
|
|
31
|
+
"@forgeax/engine-geometry": "0.1.20",
|
|
32
|
+
"@forgeax/engine-graphics-extras": "0.1.20",
|
|
33
|
+
"@forgeax/engine-image": "0.1.20",
|
|
34
|
+
"@forgeax/engine-pack": "0.1.20",
|
|
35
|
+
"@forgeax/engine-plugin": "0.1.20",
|
|
36
|
+
"@forgeax/engine-rhi": "0.1.20",
|
|
37
|
+
"@forgeax/engine-scene": "0.1.20",
|
|
38
|
+
"@forgeax/engine-shader": "0.1.20",
|
|
39
|
+
"@forgeax/engine-types": "0.1.20",
|
|
40
|
+
"@forgeax/engine-ui": "0.1.20",
|
|
41
41
|
"@webgpu/types": "^0.1.71"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@forgeax/engine-skinning": "0.1.
|
|
44
|
+
"@forgeax/engine-skinning": "0.1.20",
|
|
45
45
|
"@webgpu/types": "^0.1.71"
|
|
46
46
|
},
|
|
47
47
|
"forgeax": {
|
|
@@ -30,7 +30,6 @@ import { World } from '@forgeax/engine-ecs';
|
|
|
30
30
|
import type { TextureAsset } from '@forgeax/engine-types';
|
|
31
31
|
import { describe, expect, it } from 'vitest';
|
|
32
32
|
import { decodeImageBytes } from '../decode-image-bytes';
|
|
33
|
-
import { numMipLevels } from '../mipmap-generator';
|
|
34
33
|
|
|
35
34
|
// 1x1 red PNG (RGBA 8-bit). Universally decodable by browser createImageBitmap.
|
|
36
35
|
const PNG_1X1_RED_BASE64 =
|
|
@@ -50,14 +49,13 @@ describe('decodeImageBytes browser real-PNG happy path (AC-01)', () => {
|
|
|
50
49
|
if (!result.ok) return;
|
|
51
50
|
const pod = result.value;
|
|
52
51
|
expect(pod.kind).toBe('texture');
|
|
53
|
-
expect(pod.width).toBeGreaterThan(0);
|
|
54
|
-
expect(pod.height).toBeGreaterThan(0);
|
|
52
|
+
expect(pod.shape.extent.width).toBeGreaterThan(0);
|
|
53
|
+
expect(pod.shape.extent.height).toBeGreaterThan(0);
|
|
55
54
|
// RGBA8 tight-packed: width * height * 4 bytes.
|
|
56
|
-
expect(pod.data.length).toBe(pod.width * pod.height * 4);
|
|
55
|
+
expect(pod.data.length).toBe(pod.shape.extent.width * pod.shape.extent.height * 4);
|
|
57
56
|
expect(pod.format).toBe('rgba8unorm-srgb');
|
|
58
57
|
expect(pod.colorSpace).toBe('srgb');
|
|
59
|
-
expect(pod.
|
|
60
|
-
expect(pod.mipLevelCount).toBe(numMipLevels({ width: pod.width, height: pod.height }));
|
|
58
|
+
expect(pod.mips).toEqual({ kind: 'generate' });
|
|
61
59
|
});
|
|
62
60
|
|
|
63
61
|
it('AC-01: opts.colorSpace="linear" + opts.mipmap=false narrows format + mipLevelCount', async () => {
|
|
@@ -70,8 +68,7 @@ describe('decodeImageBytes browser real-PNG happy path (AC-01)', () => {
|
|
|
70
68
|
const pod = result.value;
|
|
71
69
|
expect(pod.format).toBe('rgba8unorm');
|
|
72
70
|
expect(pod.colorSpace).toBe('linear');
|
|
73
|
-
expect(pod.
|
|
74
|
-
expect(pod.mipLevelCount).toBe(1);
|
|
71
|
+
expect(pod.mips).toEqual({ kind: 'none' });
|
|
75
72
|
});
|
|
76
73
|
});
|
|
77
74
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import type { LoadContext, TextureAsset } from '@forgeax/engine-types';
|
|
@@ -7,7 +8,11 @@ import type {
|
|
|
7
8
|
BasisModuleFactory,
|
|
8
9
|
} from '../../../codec/src/wasm/basis-types.js';
|
|
9
10
|
import type { CodecContextFailure } from '../loaders/pack-artifact.js';
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
loadVerifiedTexturePack,
|
|
13
|
+
PACK_ARTIFACT_LOADERS,
|
|
14
|
+
textureLoader,
|
|
15
|
+
} from '../loaders/pack-artifact.js';
|
|
11
16
|
|
|
12
17
|
const TRANSCODER_GLUE = new URL('../../../codec/pkg/basis_transcoder.mjs', import.meta.url);
|
|
13
18
|
const ENCODER_GLUE = new URL('../../../codec/pkg/encode/basis_encoder.mjs', import.meta.url);
|
|
@@ -52,6 +57,10 @@ const context: LoadContext = {
|
|
|
52
57
|
device: undefined,
|
|
53
58
|
};
|
|
54
59
|
|
|
60
|
+
function digest(bytes: Uint8Array): string {
|
|
61
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
describe.skipIf(!pkgBuilt)('Pack runtime raw Basis loader', () => {
|
|
56
65
|
it('keeps context failure code and detail correlated as a closed union', () => {
|
|
57
66
|
type ExpectedContextFailure =
|
|
@@ -79,7 +88,11 @@ describe.skipIf(!pkgBuilt)('Pack runtime raw Basis loader', () => {
|
|
|
79
88
|
{
|
|
80
89
|
guid: '019f0000-0000-7000-8000-000000000304',
|
|
81
90
|
kind: 'texture',
|
|
82
|
-
payload: {
|
|
91
|
+
payload: {
|
|
92
|
+
shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
|
|
93
|
+
colorSpace: 'linear',
|
|
94
|
+
mips: { kind: 'none' },
|
|
95
|
+
},
|
|
83
96
|
refs: [],
|
|
84
97
|
artifacts: {
|
|
85
98
|
body: {
|
|
@@ -101,12 +114,10 @@ describe.skipIf(!pkgBuilt)('Pack runtime raw Basis loader', () => {
|
|
|
101
114
|
if (!result.ok) return;
|
|
102
115
|
expect(result.value).toMatchObject({
|
|
103
116
|
kind: 'texture',
|
|
104
|
-
width: 4,
|
|
105
|
-
height: 4,
|
|
117
|
+
shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
|
|
106
118
|
format: 'rgba8unorm',
|
|
107
119
|
colorSpace: 'linear',
|
|
108
|
-
|
|
109
|
-
mipLevelCount: 1,
|
|
120
|
+
mips: { kind: 'packed', levelCount: 1 },
|
|
110
121
|
});
|
|
111
122
|
expect(result.value.data.byteLength).toBeGreaterThan(0);
|
|
112
123
|
});
|
|
@@ -119,7 +130,11 @@ describe.skipIf(!pkgBuilt)('Pack runtime raw Basis loader', () => {
|
|
|
119
130
|
{
|
|
120
131
|
guid: '019f0000-0000-7000-8000-000000000305',
|
|
121
132
|
kind: 'texture',
|
|
122
|
-
payload: {
|
|
133
|
+
payload: {
|
|
134
|
+
shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
|
|
135
|
+
colorSpace: 'srgb',
|
|
136
|
+
mips: { kind: 'none' },
|
|
137
|
+
},
|
|
123
138
|
refs: [],
|
|
124
139
|
artifacts: {
|
|
125
140
|
body: {
|
|
@@ -165,4 +180,48 @@ describe('Pack artifact loader matrix', () => {
|
|
|
165
180
|
['equirect', 'font', 'render-pipeline', 'texture', 'tileset'].sort(),
|
|
166
181
|
);
|
|
167
182
|
});
|
|
183
|
+
|
|
184
|
+
it('preserves array shape and generation provenance through verified loading', async () => {
|
|
185
|
+
const bytes = new Uint8Array(8 * 4 * 3).fill(91);
|
|
186
|
+
const artifactDigest = digest(bytes);
|
|
187
|
+
const result = await loadVerifiedTexturePack(
|
|
188
|
+
{
|
|
189
|
+
sourceKey: 'array/layers',
|
|
190
|
+
generation: 4,
|
|
191
|
+
expectedDigest: artifactDigest,
|
|
192
|
+
pack: {
|
|
193
|
+
guid: '019f0000-0000-7000-8000-000000000306',
|
|
194
|
+
kind: 'texture',
|
|
195
|
+
payload: {
|
|
196
|
+
shape: { viewDimension: '2d-array', extent: { width: 8, height: 4, layers: 3 } },
|
|
197
|
+
format: 'r8unorm',
|
|
198
|
+
colorSpace: 'linear',
|
|
199
|
+
mips: { kind: 'none' },
|
|
200
|
+
},
|
|
201
|
+
refs: [],
|
|
202
|
+
artifacts: {
|
|
203
|
+
body: {
|
|
204
|
+
bytes,
|
|
205
|
+
descriptor: {
|
|
206
|
+
path: 'array.raw',
|
|
207
|
+
mediaType: 'application/x-forgeax-r8',
|
|
208
|
+
byteLength: bytes.byteLength,
|
|
209
|
+
integrity: { algorithm: 'sha256', digest: artifactDigest },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
context,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(result.ok).toBe(true);
|
|
219
|
+
if (!result.ok) return;
|
|
220
|
+
expect(result.value.asset.shape).toEqual({
|
|
221
|
+
viewDimension: '2d-array',
|
|
222
|
+
extent: { width: 8, height: 4, layers: 3 },
|
|
223
|
+
});
|
|
224
|
+
expect(result.value.generation).toBe(4);
|
|
225
|
+
expect(result.value.sourceKey).toBe('array/layers');
|
|
226
|
+
});
|
|
168
227
|
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { LoadContext } from '@forgeax/engine-types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { loadVerifiedTexturePack, type TexturePackLoadInput } from '../loaders/pack-artifact.js';
|
|
5
|
+
import {
|
|
6
|
+
createTexturePublicationState,
|
|
7
|
+
type TexturePublicationState,
|
|
8
|
+
} from '../registry/validate-material.js';
|
|
9
|
+
|
|
10
|
+
const GUID = '019ffa97-3000-7000-8000-000000000301';
|
|
11
|
+
const SOURCE_KEY = 'density/volume';
|
|
12
|
+
|
|
13
|
+
function digest(bytes: Uint8Array): string {
|
|
14
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const context: LoadContext = {
|
|
18
|
+
fetchBinary: async () => ({ ok: false as const, error: new Error('not used') }),
|
|
19
|
+
resolveRef: async () => ({ ok: false as const, error: new Error('not used') }),
|
|
20
|
+
transcodeCaps: { bc: false, etc2: false, astc: false },
|
|
21
|
+
device: undefined,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function input(
|
|
25
|
+
bytes: Uint8Array,
|
|
26
|
+
overrides: Partial<TexturePackLoadInput> = {},
|
|
27
|
+
): TexturePackLoadInput {
|
|
28
|
+
const actualDigest = digest(bytes);
|
|
29
|
+
return {
|
|
30
|
+
sourceKey: SOURCE_KEY,
|
|
31
|
+
generation: 2,
|
|
32
|
+
expectedDigest: actualDigest,
|
|
33
|
+
pack: {
|
|
34
|
+
guid: GUID,
|
|
35
|
+
kind: 'texture',
|
|
36
|
+
payload: {
|
|
37
|
+
shape: { viewDimension: '3d', extent: { width: 2, height: 2, depth: 2 } },
|
|
38
|
+
format: 'r8unorm',
|
|
39
|
+
colorSpace: 'linear',
|
|
40
|
+
mips: { kind: 'none' },
|
|
41
|
+
},
|
|
42
|
+
refs: [],
|
|
43
|
+
artifacts: {
|
|
44
|
+
body: {
|
|
45
|
+
bytes,
|
|
46
|
+
descriptor: {
|
|
47
|
+
path: 'density.raw',
|
|
48
|
+
mediaType: 'application/x-forgeax-r8',
|
|
49
|
+
byteLength: 8,
|
|
50
|
+
integrity: { algorithm: 'sha256', digest: actualDigest },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('verified texture Pack loader', () => {
|
|
60
|
+
it('rejects bad byte length with GUID, generation, and loader stage', async () => {
|
|
61
|
+
const result = await loadVerifiedTexturePack(input(new Uint8Array(7)), context);
|
|
62
|
+
|
|
63
|
+
expect(result.ok).toBe(false);
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
expect(result.error.code).toBe('texture-pack-verification-failed');
|
|
66
|
+
expect(result.error.detail).toMatchObject({
|
|
67
|
+
guid: GUID,
|
|
68
|
+
sourceKey: SOURCE_KEY,
|
|
69
|
+
generation: 2,
|
|
70
|
+
stage: 'loader',
|
|
71
|
+
expectedBytes: 8,
|
|
72
|
+
actualBytes: 7,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('rejects digest and canonical order mismatch before exposing an asset', async () => {
|
|
78
|
+
const digestMismatch = await loadVerifiedTexturePack(
|
|
79
|
+
input(new Uint8Array(8), { expectedDigest: 'sha256:not-the-artifact' }),
|
|
80
|
+
context,
|
|
81
|
+
);
|
|
82
|
+
expect(digestMismatch.ok).toBe(false);
|
|
83
|
+
if (!digestMismatch.ok) {
|
|
84
|
+
expect(digestMismatch.error.code).toBe('texture-pack-verification-failed');
|
|
85
|
+
expect(digestMismatch.error.detail).toMatchObject({
|
|
86
|
+
guid: GUID,
|
|
87
|
+
generation: 2,
|
|
88
|
+
stage: 'loader',
|
|
89
|
+
cause: 'digest-mismatch',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const orderMismatch = await loadVerifiedTexturePack(
|
|
94
|
+
input(new Uint8Array(8), {
|
|
95
|
+
pack: {
|
|
96
|
+
...input(new Uint8Array(8)).pack,
|
|
97
|
+
payload: {
|
|
98
|
+
...input(new Uint8Array(8)).pack.payload,
|
|
99
|
+
packingOrder: 'row-major,mip-major,image-major',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
context,
|
|
104
|
+
);
|
|
105
|
+
expect(orderMismatch.ok).toBe(false);
|
|
106
|
+
if (!orderMismatch.ok) {
|
|
107
|
+
expect(orderMismatch.error.detail).toMatchObject({
|
|
108
|
+
guid: GUID,
|
|
109
|
+
stage: 'loader',
|
|
110
|
+
cause: 'packing-order-mismatch',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('advances accepted generation only after a verified candidate succeeds', async () => {
|
|
116
|
+
const state: TexturePublicationState = createTexturePublicationState({
|
|
117
|
+
guid: GUID,
|
|
118
|
+
sourceKey: SOURCE_KEY,
|
|
119
|
+
generation: 1,
|
|
120
|
+
digest: 'sha256:accepted',
|
|
121
|
+
});
|
|
122
|
+
const failed = await loadVerifiedTexturePack(input(new Uint8Array(7)), context);
|
|
123
|
+
expect(failed.ok).toBe(false);
|
|
124
|
+
const afterFailure = state.reject({
|
|
125
|
+
generation: 2,
|
|
126
|
+
reason: 'byte-length',
|
|
127
|
+
});
|
|
128
|
+
expect(afterFailure).toEqual({
|
|
129
|
+
guid: GUID,
|
|
130
|
+
sourceKey: SOURCE_KEY,
|
|
131
|
+
acceptedGeneration: 1,
|
|
132
|
+
acceptedDigest: 'sha256:accepted',
|
|
133
|
+
candidateGeneration: 2,
|
|
134
|
+
candidateFailure: 'byte-length',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const accepted = state.accept({ generation: 2, digest: 'sha256:repaired' });
|
|
138
|
+
expect(accepted).toEqual({
|
|
139
|
+
guid: GUID,
|
|
140
|
+
sourceKey: SOURCE_KEY,
|
|
141
|
+
acceptedGeneration: 2,
|
|
142
|
+
acceptedDigest: 'sha256:repaired',
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
// 3. Map `DecodedImage` -> `TextureAsset` POD; `format` derives from
|
|
11
11
|
// `opts.colorSpace` (srgb -> 'rgba8unorm-srgb' / linear ->
|
|
12
12
|
// 'rgba8unorm', mirrors `image-importer.ts` colorSpaceToFormat); when
|
|
13
|
-
// `opts.mipmap !== false`,
|
|
14
|
-
//
|
|
13
|
+
// `opts.mipmap !== false`, the mip policy derives from the decoded shape
|
|
14
|
+
// (charter Derive-Don't-Duplicate).
|
|
15
15
|
//
|
|
16
16
|
// decode-image-bytes.ts is the SINGLE assets-runtime file allowed to
|
|
17
17
|
// statically import @forgeax/engine-image (charter D-2; the
|
|
@@ -24,7 +24,6 @@ import { decodeImageInBrowser } from '@forgeax/engine-image';
|
|
|
24
24
|
import type { ImageError, Result, TextureAsset } from '@forgeax/engine-types';
|
|
25
25
|
import { err, ok } from '@forgeax/engine-types';
|
|
26
26
|
import { makeImageError } from './image-error';
|
|
27
|
-
import { numMipLevels } from './mipmap-generator';
|
|
28
27
|
|
|
29
28
|
/** v1 mime whitelist -- PNG / JPEG only (charter P1 progressive disclosure). */
|
|
30
29
|
const SUPPORTED_MIMES = ['image/png', 'image/jpeg'] as const;
|
|
@@ -60,8 +59,8 @@ function isSupportedMime(mime: string): mime is SupportedMime {
|
|
|
60
59
|
* @param opts - Optional decode overrides:
|
|
61
60
|
* - `colorSpace`: `'srgb'` (default) or `'linear'`. Derives POD `format`:
|
|
62
61
|
* `srgb -> 'rgba8unorm-srgb'`, `linear -> 'rgba8unorm'`.
|
|
63
|
-
* - `mipmap`: `true` (default) or `false`. When true,
|
|
64
|
-
*
|
|
62
|
+
* - `mipmap`: `true` (default) or `false`. When true, the producer requests
|
|
63
|
+
* generated mips; when false, it keeps one authored level.
|
|
65
64
|
* @returns `ok(TextureAsset)` on success; `err(ImageError)` on failure.
|
|
66
65
|
*
|
|
67
66
|
* @example
|
|
@@ -110,17 +109,13 @@ export async function decodeImageBytes(
|
|
|
110
109
|
|
|
111
110
|
const dec = decoded.value;
|
|
112
111
|
const format: GPUTextureFormat = colorSpace === 'srgb' ? 'rgba8unorm-srgb' : 'rgba8unorm';
|
|
113
|
-
const mipLevelCount = mipmap ? numMipLevels({ width: dec.width, height: dec.height }) : 1;
|
|
114
|
-
|
|
115
112
|
const pod: TextureAsset = {
|
|
116
113
|
kind: 'texture',
|
|
117
|
-
width: dec.width,
|
|
118
|
-
height: dec.height,
|
|
114
|
+
shape: { viewDimension: '2d', extent: { width: dec.width, height: dec.height } },
|
|
119
115
|
format,
|
|
120
116
|
data: dec.bytes,
|
|
121
117
|
colorSpace,
|
|
122
|
-
mipmap,
|
|
123
|
-
mipLevelCount,
|
|
118
|
+
mips: mipmap ? { kind: 'generate' } : { kind: 'none' },
|
|
124
119
|
};
|
|
125
120
|
return ok(pod);
|
|
126
121
|
}
|
package/src/errors/asset.ts
CHANGED
|
@@ -18,6 +18,39 @@ import {
|
|
|
18
18
|
type AssetMeshBinContractViolationReason,
|
|
19
19
|
} from '@forgeax/engine-types';
|
|
20
20
|
|
|
21
|
+
export type TexturePackVerificationCause =
|
|
22
|
+
| 'byte-length'
|
|
23
|
+
| 'digest-mismatch'
|
|
24
|
+
| 'packing-order-mismatch'
|
|
25
|
+
| 'shape-mismatch';
|
|
26
|
+
|
|
27
|
+
export interface TexturePackVerificationDetail {
|
|
28
|
+
readonly guid: string;
|
|
29
|
+
readonly sourceKey: string;
|
|
30
|
+
readonly generation: number;
|
|
31
|
+
readonly stage: 'producer' | 'loader';
|
|
32
|
+
readonly cause: TexturePackVerificationCause;
|
|
33
|
+
readonly expectedBytes?: number;
|
|
34
|
+
readonly actualBytes?: number;
|
|
35
|
+
readonly expectedDigest?: string;
|
|
36
|
+
readonly actualDigest?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Structured failure for the producer/loader texture contract. */
|
|
40
|
+
export class TexturePackVerificationError extends Error {
|
|
41
|
+
readonly code = 'texture-pack-verification-failed' as const;
|
|
42
|
+
readonly expected: string;
|
|
43
|
+
readonly hint = 'repair the producer output and retry the same texture GUID generation';
|
|
44
|
+
readonly detail: TexturePackVerificationDetail;
|
|
45
|
+
|
|
46
|
+
constructor(detail: TexturePackVerificationDetail, expected: string) {
|
|
47
|
+
super(`[TexturePackVerificationError] ${detail.cause}: ${expected}`);
|
|
48
|
+
this.name = 'TexturePackVerificationError';
|
|
49
|
+
this.expected = expected;
|
|
50
|
+
this.detail = detail;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
21
54
|
export class MeshBinAssetError extends AssetError {
|
|
22
55
|
readonly subject = 'mesh-bin' as const;
|
|
23
56
|
readonly sourceKey: string;
|