@forgeax/engine-pack 0.1.25 → 0.1.26
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/README.md +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.mjs +115 -67
- package/dist/build.mjs.map +1 -1
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/evidence/material-cook.d.ts +23 -12
- package/dist/evidence/material-cook.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +116 -68
- package/dist/index.mjs.map +1 -1
- package/dist/material-cook.d.ts +1 -1
- package/dist/material-cook.d.ts.map +1 -1
- package/dist/material-cook.mjs +116 -68
- package/dist/material-cook.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/material-cook-receipt.unit.test.ts +9 -9
- package/src/__tests__/material-cook-schema.unit.test.ts +172 -21
- package/src/__tests__/material-surface-publication.unit.test.ts +12 -9
- package/src/__tests__/material-surface-wire.unit.test.ts +2 -2
- package/src/build.ts +2 -2
- package/src/evidence/material-cook.ts +164 -99
- package/src/index.ts +5 -2
- package/src/material-cook.ts +5 -2
- package/src/schema/material-cook.schema.json +249 -88
|
@@ -5,13 +5,15 @@ import { describe, expect, it } from 'vitest';
|
|
|
5
5
|
import {
|
|
6
6
|
type CookedMaterialRecord,
|
|
7
7
|
createMaterialArtifactDigest,
|
|
8
|
+
createMaterialProgramSetDigest,
|
|
9
|
+
type MaterialCookProgram,
|
|
8
10
|
projectCookedMaterialRecord,
|
|
9
11
|
serializeCookedMaterialRecord,
|
|
10
12
|
validateCookedMaterialRecord,
|
|
11
13
|
} from '../evidence/material-cook.js';
|
|
12
14
|
|
|
13
15
|
const record: CookedMaterialRecord = {
|
|
14
|
-
schemaVersion: 'material-cook/
|
|
16
|
+
schemaVersion: 'material-cook/4',
|
|
15
17
|
guid: 'mat-root',
|
|
16
18
|
authored: {
|
|
17
19
|
kind: 'material',
|
|
@@ -25,14 +27,34 @@ const record: CookedMaterialRecord = {
|
|
|
25
27
|
values: { roughness: 0.5 },
|
|
26
28
|
},
|
|
27
29
|
refs: { parent: [], textures: [], samplers: [], modules: ['core/pbr'] },
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
programs: [
|
|
31
|
+
{
|
|
32
|
+
specializationKey: 'sha256:program',
|
|
33
|
+
selections: [
|
|
34
|
+
{
|
|
35
|
+
pass: 'forward',
|
|
36
|
+
context: {
|
|
37
|
+
backend: 'webgpu',
|
|
38
|
+
capability: 'storage-buffer',
|
|
39
|
+
pipeline: 'forward',
|
|
40
|
+
geometry: 'mesh',
|
|
41
|
+
pass: 'forward',
|
|
42
|
+
profile: 'forgeax-material-wgsl-v1',
|
|
43
|
+
toolchain: 'naga-oil',
|
|
44
|
+
instrumentation: 'none',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
artifact: {
|
|
49
|
+
mediaType: 'text/wgsl',
|
|
50
|
+
path: 'materials/mat-root/forward.wgsl',
|
|
51
|
+
digest: 'sha256:artifact',
|
|
52
|
+
bytes: new TextEncoder().encode('shader'),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
34
56
|
receipt: {
|
|
35
|
-
schemaVersion: 'material-cook/
|
|
57
|
+
schemaVersion: 'material-cook/4',
|
|
36
58
|
sourceClosure: ['materials/mat-root.material.json'],
|
|
37
59
|
profile: 'webgpu/v1',
|
|
38
60
|
compilerVersion: 'compiler/1',
|
|
@@ -60,8 +82,21 @@ const record: CookedMaterialRecord = {
|
|
|
60
82
|
},
|
|
61
83
|
};
|
|
62
84
|
|
|
85
|
+
function seal(value: CookedMaterialRecord): CookedMaterialRecord {
|
|
86
|
+
return {
|
|
87
|
+
...value,
|
|
88
|
+
receipt: {
|
|
89
|
+
...value.receipt,
|
|
90
|
+
identity: {
|
|
91
|
+
...value.receipt.identity,
|
|
92
|
+
artifactDigest: createMaterialProgramSetDigest(value.programs, value.resolved.passes),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
63
98
|
describe('cooked material schema', () => {
|
|
64
|
-
it('requires material-cook/
|
|
99
|
+
it('requires material-cook/4 identity and derived interface association', () => {
|
|
65
100
|
const schema = JSON.parse(
|
|
66
101
|
readFileSync(
|
|
67
102
|
fileURLToPath(new URL('../schema/material-cook.schema.json', import.meta.url)),
|
|
@@ -74,14 +109,14 @@ describe('cooked material schema', () => {
|
|
|
74
109
|
>;
|
|
75
110
|
required: string[];
|
|
76
111
|
};
|
|
77
|
-
expect(schema.properties.schemaVersion?.const).toBe('material-cook/
|
|
112
|
+
expect(schema.properties.schemaVersion?.const).toBe('material-cook/4');
|
|
78
113
|
expect(schema.properties.receipt?.required).toEqual(
|
|
79
114
|
expect.arrayContaining(['identity', 'derivedInterface']),
|
|
80
115
|
);
|
|
81
116
|
expect(schema.properties.receipt?.properties?.derivedInterface).toBeDefined();
|
|
82
117
|
});
|
|
83
118
|
|
|
84
|
-
it('defines the
|
|
119
|
+
it('defines the v4 identity, provenance, and generation contract', () => {
|
|
85
120
|
const schema = JSON.parse(
|
|
86
121
|
readFileSync(
|
|
87
122
|
fileURLToPath(new URL('../schema/material-cook.schema.json', import.meta.url)),
|
|
@@ -93,7 +128,7 @@ describe('cooked material schema', () => {
|
|
|
93
128
|
{ const?: string; required?: string[]; properties?: Record<string, unknown> }
|
|
94
129
|
>;
|
|
95
130
|
};
|
|
96
|
-
expect(schema.properties.schemaVersion?.const).toBe('material-cook/
|
|
131
|
+
expect(schema.properties.schemaVersion?.const).toBe('material-cook/4');
|
|
97
132
|
expect(schema.properties.receipt?.properties?.identity).toMatchObject({
|
|
98
133
|
type: 'object',
|
|
99
134
|
required: expect.arrayContaining([
|
|
@@ -119,25 +154,141 @@ describe('cooked material schema', () => {
|
|
|
119
154
|
});
|
|
120
155
|
|
|
121
156
|
it('serializes a stable cooked DTO and derives an artifact digest', () => {
|
|
122
|
-
const first = serializeCookedMaterialRecord(record);
|
|
123
|
-
const second = serializeCookedMaterialRecord({ ...record });
|
|
157
|
+
const first = serializeCookedMaterialRecord(seal(record));
|
|
158
|
+
const second = serializeCookedMaterialRecord(seal({ ...record }));
|
|
124
159
|
|
|
125
160
|
expect(first).toBe(second);
|
|
126
|
-
expect(createMaterialArtifactDigest(
|
|
161
|
+
expect(createMaterialArtifactDigest(new TextEncoder().encode('shader'))).toBe(
|
|
127
162
|
'sha256:e137e75a5e0e7a623ca39de480667a55b43e0eedec58767634ebaef07c33383a',
|
|
128
163
|
);
|
|
129
|
-
expect(validateCookedMaterialRecord(JSON.parse(first))).toEqual({
|
|
164
|
+
expect(validateCookedMaterialRecord(JSON.parse(first))).toEqual({
|
|
165
|
+
ok: true,
|
|
166
|
+
value: seal(record),
|
|
167
|
+
});
|
|
130
168
|
});
|
|
131
169
|
|
|
132
170
|
it('projects cooked data without exposing an authored GUID entry', () => {
|
|
133
171
|
const projection = projectCookedMaterialRecord(record);
|
|
134
172
|
|
|
135
173
|
expect(projection.resolved.values).toEqual({ roughness: 0.5 });
|
|
136
|
-
expect(projection.artifact.digest).toBe('sha256:artifact');
|
|
174
|
+
expect(projection.programs[0]?.artifact.digest).toBe('sha256:artifact');
|
|
137
175
|
expect(projection).not.toHaveProperty('guid');
|
|
138
176
|
expect(projection).not.toHaveProperty('authored');
|
|
139
177
|
});
|
|
140
178
|
|
|
179
|
+
it('admits different programs with equal entry names and shared programs with multiple selections', () => {
|
|
180
|
+
const primary = record.programs[0] as MaterialCookProgram;
|
|
181
|
+
const passes = [
|
|
182
|
+
...record.resolved.passes,
|
|
183
|
+
{
|
|
184
|
+
name: 'second',
|
|
185
|
+
program: { module: 'other', vertexEntry: 'vs_main', fragmentEntry: 'fs_main' },
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
const selection = {
|
|
189
|
+
...primary.selections[0],
|
|
190
|
+
pass: 'second',
|
|
191
|
+
} as MaterialCookProgram['selections'][number];
|
|
192
|
+
const distinct = seal({
|
|
193
|
+
...record,
|
|
194
|
+
resolved: { ...record.resolved, passes },
|
|
195
|
+
programs: [
|
|
196
|
+
primary,
|
|
197
|
+
{
|
|
198
|
+
...primary,
|
|
199
|
+
specializationKey: 'second',
|
|
200
|
+
artifact: {
|
|
201
|
+
...primary.artifact,
|
|
202
|
+
path: 'second.wgsl',
|
|
203
|
+
digest: 'sha256:second',
|
|
204
|
+
bytes: new TextEncoder().encode('other'),
|
|
205
|
+
},
|
|
206
|
+
selections: [selection],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
expect(validateCookedMaterialRecord(distinct)).toMatchObject({ ok: true });
|
|
211
|
+
const shared = seal({
|
|
212
|
+
...distinct,
|
|
213
|
+
programs: [{ ...primary, selections: [...primary.selections, selection] }],
|
|
214
|
+
});
|
|
215
|
+
expect(validateCookedMaterialRecord(shared)).toMatchObject({ ok: true });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('rejects ambiguous selections, unpublished passes and unknown context axes', () => {
|
|
219
|
+
const primary = record.programs[0] as MaterialCookProgram;
|
|
220
|
+
expect(
|
|
221
|
+
validateCookedMaterialRecord({
|
|
222
|
+
...record,
|
|
223
|
+
programs: [primary, { ...primary, specializationKey: 'duplicate' }],
|
|
224
|
+
}),
|
|
225
|
+
).toMatchObject({
|
|
226
|
+
ok: false,
|
|
227
|
+
error: { detail: { actual: 'ambiguous Pass/context selection' } },
|
|
228
|
+
});
|
|
229
|
+
expect(
|
|
230
|
+
validateCookedMaterialRecord({
|
|
231
|
+
...record,
|
|
232
|
+
resolved: {
|
|
233
|
+
...record.resolved,
|
|
234
|
+
passes: [...record.resolved.passes, { name: 'missing', program: { module: 'other' } }],
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
).toMatchObject({ ok: false, error: { detail: { actual: 'unpublished Pass' } } });
|
|
238
|
+
expect(
|
|
239
|
+
validateCookedMaterialRecord({
|
|
240
|
+
...record,
|
|
241
|
+
programs: [
|
|
242
|
+
{
|
|
243
|
+
...primary,
|
|
244
|
+
selections: [
|
|
245
|
+
{ pass: 'forward', context: { ...primary.selections[0]?.context, arbitrary: true } },
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
}),
|
|
250
|
+
).toMatchObject({
|
|
251
|
+
ok: false,
|
|
252
|
+
error: { detail: { field: 'programs[0].selections[0].context.arbitrary' } },
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('rejects stale manifests after entry or context changes and rejects legacy publication fields', () => {
|
|
257
|
+
const sealed = seal(record);
|
|
258
|
+
expect(
|
|
259
|
+
validateCookedMaterialRecord({
|
|
260
|
+
...sealed,
|
|
261
|
+
resolved: {
|
|
262
|
+
...sealed.resolved,
|
|
263
|
+
passes: [{ name: 'forward', program: { module: 'core/pbr', fragmentEntry: 'fs_other' } }],
|
|
264
|
+
},
|
|
265
|
+
}),
|
|
266
|
+
).toMatchObject({ ok: false, error: { detail: { field: 'receipt.identity.artifactDigest' } } });
|
|
267
|
+
const primary = record.programs[0] as MaterialCookProgram;
|
|
268
|
+
expect(
|
|
269
|
+
validateCookedMaterialRecord({
|
|
270
|
+
...sealed,
|
|
271
|
+
programs: [
|
|
272
|
+
{
|
|
273
|
+
...primary,
|
|
274
|
+
selections: [
|
|
275
|
+
{
|
|
276
|
+
pass: 'forward',
|
|
277
|
+
context: { ...primary.selections[0]?.context, geometry: 'skinned' },
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
}),
|
|
283
|
+
).toMatchObject({ ok: false, error: { detail: { field: 'receipt.identity.artifactDigest' } } });
|
|
284
|
+
expect(
|
|
285
|
+
validateCookedMaterialRecord({ ...sealed, schemaVersion: 'material-cook/3' }),
|
|
286
|
+
).toMatchObject({ ok: false });
|
|
287
|
+
expect(validateCookedMaterialRecord({ ...sealed, artifact: primary.artifact })).toMatchObject({
|
|
288
|
+
ok: false,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
141
292
|
it('rejects missing fields and unsupported schema versions', () => {
|
|
142
293
|
expect(validateCookedMaterialRecord({ ...record, refs: undefined })).toMatchObject({
|
|
143
294
|
ok: false,
|
|
@@ -154,7 +305,7 @@ describe('cooked material schema', () => {
|
|
|
154
305
|
it('rejects a Standard record whose receipt layer plan is stale', () => {
|
|
155
306
|
const standardPass = { name: 'forward', program: { module: 'forgeax::default-standard-pbr' } };
|
|
156
307
|
const standardPlan = deriveStandardLayerPlan(record.resolved.parameters, [standardPass]);
|
|
157
|
-
const standardRecord = {
|
|
308
|
+
const standardRecord = seal({
|
|
158
309
|
...record,
|
|
159
310
|
resolved: { ...record.resolved, passes: [standardPass] },
|
|
160
311
|
receipt: {
|
|
@@ -164,7 +315,7 @@ describe('cooked material schema', () => {
|
|
|
164
315
|
layerPlanIdentity: standardPlan.identity,
|
|
165
316
|
},
|
|
166
317
|
},
|
|
167
|
-
};
|
|
318
|
+
});
|
|
168
319
|
expect(validateCookedMaterialRecord(standardRecord)).toMatchObject({ ok: true });
|
|
169
320
|
expect(
|
|
170
321
|
validateCookedMaterialRecord({
|
|
@@ -185,7 +336,7 @@ describe('cooked material schema', () => {
|
|
|
185
336
|
it('rejects a Standard record whose receipt omits the layer plan identity', () => {
|
|
186
337
|
const standardPass = { name: 'forward', program: { module: 'forgeax::default-standard-pbr' } };
|
|
187
338
|
const standardPlan = deriveStandardLayerPlan(record.resolved.parameters, [standardPass]);
|
|
188
|
-
const standardRecord = {
|
|
339
|
+
const standardRecord = seal({
|
|
189
340
|
...record,
|
|
190
341
|
resolved: { ...record.resolved, passes: [standardPass] },
|
|
191
342
|
receipt: {
|
|
@@ -195,7 +346,7 @@ describe('cooked material schema', () => {
|
|
|
195
346
|
layerPlanIdentity: standardPlan.identity,
|
|
196
347
|
},
|
|
197
348
|
},
|
|
198
|
-
};
|
|
349
|
+
});
|
|
199
350
|
expect(
|
|
200
351
|
validateCookedMaterialRecord({
|
|
201
352
|
...standardRecord,
|
|
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
import { Materials } from '@forgeax/engine-render';
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { validateCookedMaterialRecord } from '../material-cook.js';
|
|
7
8
|
import { NativeCookerRegistry } from '../native-cooker-registry.js';
|
|
8
9
|
|
|
9
10
|
const compilerModule = pathToFileURL(
|
|
@@ -39,15 +40,17 @@ describe('Surface Pack publication', () => {
|
|
|
39
40
|
source: material,
|
|
40
41
|
sourcePath,
|
|
41
42
|
});
|
|
42
|
-
const cooked = draft.payload.cooked
|
|
43
|
-
readonly receipt: { readonly sourceClosure: readonly string[] };
|
|
44
|
-
readonly artifact: { readonly path: string; readonly bytes: Uint8Array };
|
|
45
|
-
};
|
|
43
|
+
const cooked = validateCookedMaterialRecord(draft.payload.cooked).unwrap();
|
|
46
44
|
expect(cooked.receipt.sourceClosure).toContain(sourcePath);
|
|
47
|
-
expect(cooked.
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
expect(cooked.programs.length).toBeGreaterThan(0);
|
|
46
|
+
for (const program of cooked.programs) {
|
|
47
|
+
const artifact = draft.artifacts[program.artifact.path];
|
|
48
|
+
expect(artifact?.bytes.byteLength).toBeGreaterThan(0);
|
|
49
|
+
expect(artifact?.bytes).toEqual(new Uint8Array(program.artifact.bytes));
|
|
50
|
+
}
|
|
51
|
+
expect(
|
|
52
|
+
cooked.resolved.passes?.find((pass) => pass.name === 'shadow-caster')?.program,
|
|
53
|
+
).toMatchObject({ module: 'forgeax::default-shadow-caster', fragmentEntry: 'fs_shadow' });
|
|
51
54
|
} finally {
|
|
52
55
|
await rm(root, { recursive: true, force: true });
|
|
53
56
|
}
|
|
@@ -154,6 +157,6 @@ describe('Surface Pack publication', () => {
|
|
|
154
157
|
guid: GUID,
|
|
155
158
|
source: material,
|
|
156
159
|
}),
|
|
157
|
-
).rejects.
|
|
160
|
+
).rejects.toMatchObject({ code: 'material-physical-contract-invalid' });
|
|
158
161
|
});
|
|
159
162
|
});
|
|
@@ -9,12 +9,12 @@ const customOptions = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
describe('custom Surface MaterialAsset wire projection', () => {
|
|
12
|
-
it('uses Standard
|
|
12
|
+
it('uses Standard modules plus the default shadow caster and the existing Surface slot wire', () => {
|
|
13
13
|
const material = Materials.standard(customOptions as never);
|
|
14
14
|
expect(material.passes?.map((pass) => pass.program.module)).toEqual([
|
|
15
15
|
'forgeax_material::standard',
|
|
16
16
|
'forgeax_material::standard',
|
|
17
|
-
'
|
|
17
|
+
'forgeax::default-shadow-caster',
|
|
18
18
|
]);
|
|
19
19
|
expect(
|
|
20
20
|
material.passes?.every(
|
package/src/build.ts
CHANGED
|
@@ -57,11 +57,11 @@ export {
|
|
|
57
57
|
createMaterialArtifactDigest,
|
|
58
58
|
type MaterialCookArtifact,
|
|
59
59
|
type MaterialCookIdentityExpectation,
|
|
60
|
+
type MaterialCookProgram,
|
|
61
|
+
type MaterialCookProgramContext,
|
|
60
62
|
type MaterialCookReceipt,
|
|
61
63
|
type MaterialCookRecordError,
|
|
62
64
|
type MaterialCookRefs,
|
|
63
|
-
type MaterialCookVariant,
|
|
64
|
-
type MaterialCookVariantContext,
|
|
65
65
|
projectCookedMaterialRecord,
|
|
66
66
|
serializeCookedMaterialRecord,
|
|
67
67
|
serializeMaterialCookReceipt,
|