@forgeax/engine-assets-runtime 0.1.6 → 0.1.19
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 +11 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/asset-registry.d.ts +2 -0
- package/dist/asset-registry.d.ts.map +1 -1
- package/dist/handles.d.ts +2 -0
- package/dist/handles.d.ts.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +62 -9
- package/dist/index.mjs.map +1 -1
- package/dist/mipmap-generator.d.ts +3 -1
- package/dist/mipmap-generator.d.ts.map +1 -1
- package/dist/registry/instantiate.d.ts +2 -0
- package/dist/registry/instantiate.d.ts.map +1 -1
- package/dist/registry/validate-material.d.ts +1 -0
- package/dist/registry/validate-material.d.ts.map +1 -1
- package/package.json +14 -14
- package/src/__tests__/catalog-replica.unit.test.ts +36 -0
- package/src/__tests__/inline-pack-loaders.unit.test.ts +37 -0
- package/src/__tests__/instantiate-publication-fence.unit.test.ts +59 -1
- package/src/__tests__/material-ready.integration.test.ts +87 -6
- package/src/__tests__/material-stale-generation.unit.test.ts +1 -6
- package/src/__tests__/resolve-asset-handle.unit.test.ts +1 -1
- package/src/__tests__/scriptable-pack-public-surface.test-d.ts +11 -0
- package/src/__tests__/validate-material.unit.test.ts +46 -1
- package/src/asset-registry.ts +5 -0
- package/src/handles.ts +7 -1
- package/src/index.ts +17 -3
- package/src/material/inspection.ts +1 -1
- package/src/material/loader.ts +9 -9
- package/src/material/runtime-shader.ts +1 -1
- package/src/mipmap-generator.ts +43 -1
- package/src/registry/instantiate.ts +14 -4
- package/src/registry/load-by-guid.ts +1 -1
- package/src/registry/validate-material.ts +96 -3
|
@@ -22,7 +22,7 @@ export function materialParametersToParamSchema(
|
|
|
22
22
|
? ENGINE_INJECTED_TEXTURE_FIELDS
|
|
23
23
|
: undefined;
|
|
24
24
|
return parameters.flatMap((parameter): ParamSchemaEntry[] => {
|
|
25
|
-
if (parameter.
|
|
25
|
+
if (parameter.type === 'bool') return [];
|
|
26
26
|
if (parameter.type === 'texture') {
|
|
27
27
|
if (engineInjectedTextureFields?.has(parameter.name) === true) return [];
|
|
28
28
|
return [{ name: parameter.name, type: 'texture2d' }];
|
package/src/mipmap-generator.ts
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
// - `numMipLevels` is pure (no device argument); the WeakMap lookup only
|
|
46
46
|
// happens at pipeline-create time.
|
|
47
47
|
|
|
48
|
-
import { err, ok, type Result, RhiError } from '@forgeax/engine-rhi';
|
|
48
|
+
import { err, ok, type Result, RhiError, type RhiRenderPassEncoder } from '@forgeax/engine-rhi';
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Subset of `RhiDevice` consumed by the mipmap utility (charter F2: only
|
|
@@ -448,3 +448,45 @@ export function blitMipmapsSync(
|
|
|
448
448
|
|
|
449
449
|
return recordMipmapBlit<RhiError>(device, texture, levels, pipeline, cache);
|
|
450
450
|
}
|
|
451
|
+
|
|
452
|
+
/** Encode one prewarmed mip level into a caller-owned graph render pass. */
|
|
453
|
+
export function encodeMipmapLevel(
|
|
454
|
+
device: MipmapBlitDevice,
|
|
455
|
+
pass: RhiRenderPassEncoder,
|
|
456
|
+
sourceView: unknown,
|
|
457
|
+
format: GPUTextureFormat,
|
|
458
|
+
): Result<void, RhiError> {
|
|
459
|
+
const cache = deviceCache.get(device);
|
|
460
|
+
if (cache === undefined) {
|
|
461
|
+
return err(
|
|
462
|
+
new RhiError({
|
|
463
|
+
code: 'rhi-not-available',
|
|
464
|
+
expected: 'mipmap pipeline cache prewarmed for the graph device before encoding',
|
|
465
|
+
hint: 'call prewarmMipmapPipeline(device, formats) at renderer.ready',
|
|
466
|
+
}),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const pipeline = cache.pipelines.get(format);
|
|
470
|
+
if (pipeline === undefined) {
|
|
471
|
+
return err(
|
|
472
|
+
new RhiError({
|
|
473
|
+
code: 'rhi-not-available',
|
|
474
|
+
expected: `mipmap pipeline for format ${format} prewarmed`,
|
|
475
|
+
hint: `format ${format} was not prewarmed; add it to the renderer prewarm list`,
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
const bindGroup = device.createBindGroup({
|
|
480
|
+
label: 'mipmap-graph-bind-group',
|
|
481
|
+
layout: cache.layout,
|
|
482
|
+
entries: [
|
|
483
|
+
{ binding: 0, resource: { kind: 'sampler', value: cache.sampler } },
|
|
484
|
+
{ binding: 1, resource: { kind: 'textureView', value: sourceView } },
|
|
485
|
+
],
|
|
486
|
+
});
|
|
487
|
+
if (!bindGroup.ok) return bindGroup;
|
|
488
|
+
pass.setPipeline(pipeline);
|
|
489
|
+
pass.setBindGroup(0, bindGroup.value);
|
|
490
|
+
pass.draw(3, 1, 0, 0);
|
|
491
|
+
return ok(undefined);
|
|
492
|
+
}
|
|
@@ -78,16 +78,26 @@ function catalogEntriesForFence(registry: AssetRegistry): readonly CatalogEntry[
|
|
|
78
78
|
if (record.sourcePath === undefined) continue;
|
|
79
79
|
const candidate = { ...record, guid, sourcePath: record.sourcePath } as CatalogEntry;
|
|
80
80
|
const current = byGuid.get(guid.toLowerCase());
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
if (current === undefined || candidate.publication !== undefined) {
|
|
82
|
+
// A persisted Scene fence is captured from the complete pack-index
|
|
83
|
+
// publication at save time. If the CatalogReplica is still carrying its
|
|
84
|
+
// previous tuple during the watcher handoff, keep the same pack-index
|
|
85
|
+
// authority here or the freshly reopened scene will reject its own
|
|
86
|
+
// mount with asset-generation-fence-mismatch.
|
|
85
87
|
byGuid.set(guid.toLowerCase(), candidate);
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
return [...byGuid.values()];
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
/** Derive a publication fence from the registry's unified live projections. */
|
|
94
|
+
export function scenePublicationFenceFromRegistry(
|
|
95
|
+
registry: AssetRegistry,
|
|
96
|
+
outputGuid: string,
|
|
97
|
+
): Result<ScenePublicationFence, ScenePublicationFenceError> {
|
|
98
|
+
return scenePublicationFenceFromCatalog(catalogEntriesForFence(registry), outputGuid);
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
/**
|
|
92
102
|
* Post-spawn hook contract (D-1). A hook runs after instantiate spawns the
|
|
93
103
|
* scene subtree; the shipped implementation is runtime's `postSpawnResolveJoints`
|
|
@@ -104,7 +104,7 @@ export async function loadByGuid<T = Asset>(
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
function cookedRecordFromPayload(payload: Record<string, unknown>): unknown {
|
|
107
|
-
if (payload.schemaVersion === 'material-cook/
|
|
107
|
+
if (payload.schemaVersion === 'material-cook/3') return payload;
|
|
108
108
|
if (payload.cooked !== null && typeof payload.cooked === 'object') return payload.cooked;
|
|
109
109
|
if (payload.record !== null && typeof payload.record === 'object') return payload.record;
|
|
110
110
|
return undefined;
|
|
@@ -112,6 +112,100 @@ export function validateMaterialPasses(
|
|
|
112
112
|
return null;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
type TransmissionParameter =
|
|
116
|
+
| 'transmission'
|
|
117
|
+
| 'ior'
|
|
118
|
+
| 'thickness'
|
|
119
|
+
| 'attenuationColor'
|
|
120
|
+
| 'attenuationDistance'
|
|
121
|
+
| 'transmissionTexture'
|
|
122
|
+
| 'thicknessTexture';
|
|
123
|
+
|
|
124
|
+
function transmissionError(
|
|
125
|
+
material: string,
|
|
126
|
+
parameter: TransmissionParameter,
|
|
127
|
+
reason: 'non-finite' | 'range' | 'shape' | 'blend' | 'depth-write',
|
|
128
|
+
actual?: unknown,
|
|
129
|
+
): MaterialError {
|
|
130
|
+
return createMaterialError('material-transmission-contract-invalid', {
|
|
131
|
+
code: 'material-transmission-contract-invalid',
|
|
132
|
+
material,
|
|
133
|
+
parameter,
|
|
134
|
+
reason,
|
|
135
|
+
...(actual === undefined ? {} : { actual }),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validateTransmissionNumber(
|
|
140
|
+
material: string,
|
|
141
|
+
values: Record<string, unknown>,
|
|
142
|
+
parameter: 'transmission' | 'ior' | 'thickness' | 'attenuationDistance',
|
|
143
|
+
minimum: number,
|
|
144
|
+
maximum?: number,
|
|
145
|
+
): MaterialError | null {
|
|
146
|
+
const value = values[parameter];
|
|
147
|
+
if (value === undefined) return null;
|
|
148
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
149
|
+
return transmissionError(material, parameter, 'non-finite', value);
|
|
150
|
+
}
|
|
151
|
+
if (value < minimum || (maximum !== undefined && value > maximum)) {
|
|
152
|
+
return transmissionError(material, parameter, 'range', value);
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function validateMaterialTransmissionContract(asset: MaterialAsset): MaterialError | null {
|
|
158
|
+
const material = (asset.values?.materialName as string | undefined) ?? '<unnamed>';
|
|
159
|
+
const values = (asset.values ?? {}) as Record<string, unknown>;
|
|
160
|
+
const numericChecks: ReadonlyArray<
|
|
161
|
+
['transmission' | 'ior' | 'thickness' | 'attenuationDistance', number, number | undefined]
|
|
162
|
+
> = [
|
|
163
|
+
['transmission', 0, 1],
|
|
164
|
+
['ior', 1, undefined],
|
|
165
|
+
['thickness', 0, undefined],
|
|
166
|
+
['attenuationDistance', Number.MIN_VALUE, undefined],
|
|
167
|
+
];
|
|
168
|
+
for (const [parameter, minimum, maximum] of numericChecks) {
|
|
169
|
+
const error = validateTransmissionNumber(material, values, parameter, minimum, maximum);
|
|
170
|
+
if (error !== null) return error;
|
|
171
|
+
}
|
|
172
|
+
const attenuationColor = values.attenuationColor;
|
|
173
|
+
if (attenuationColor !== undefined) {
|
|
174
|
+
if (
|
|
175
|
+
!Array.isArray(attenuationColor) ||
|
|
176
|
+
attenuationColor.length !== 3 ||
|
|
177
|
+
attenuationColor.some(
|
|
178
|
+
(value) => typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1,
|
|
179
|
+
)
|
|
180
|
+
) {
|
|
181
|
+
return transmissionError(material, 'attenuationColor', 'shape', attenuationColor);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (const parameter of ['transmissionTexture', 'thicknessTexture'] as const) {
|
|
185
|
+
const value = values[parameter];
|
|
186
|
+
if (
|
|
187
|
+
value !== undefined &&
|
|
188
|
+
typeof value !== 'string' &&
|
|
189
|
+
!(typeof value === 'number' && Number.isInteger(value) && value >= 0) &&
|
|
190
|
+
(typeof value !== 'object' || value === null || Array.isArray(value))
|
|
191
|
+
) {
|
|
192
|
+
return transmissionError(material, parameter, 'shape', value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const transmission = values.transmission;
|
|
196
|
+
if (typeof transmission === 'number' && transmission > 0) {
|
|
197
|
+
for (const pass of asset.passes ?? []) {
|
|
198
|
+
if (pass.renderState?.blend !== undefined) {
|
|
199
|
+
return transmissionError(material, 'transmission', 'blend');
|
|
200
|
+
}
|
|
201
|
+
if (pass.renderState?.depthWriteEnabled === true) {
|
|
202
|
+
return transmissionError(material, 'transmission', 'depth-write');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
115
209
|
export interface MaterialEffectiveRootIdentity {
|
|
116
210
|
readonly guid: string;
|
|
117
211
|
readonly layoutIdentity: string;
|
|
@@ -121,11 +215,10 @@ export function validateMaterialCookIdentity(
|
|
|
121
215
|
record: Pick<CookedMaterialRecord, 'guid' | 'receipt'>,
|
|
122
216
|
root: MaterialEffectiveRootIdentity,
|
|
123
217
|
): MaterialError | null {
|
|
124
|
-
const actual = record.receipt.layoutIdentity;
|
|
218
|
+
const actual = record.receipt.identity.layoutIdentity;
|
|
125
219
|
if (
|
|
126
220
|
actual !== root.layoutIdentity ||
|
|
127
|
-
|
|
128
|
-
.derivedInterface?.layoutIdentity !== root.layoutIdentity
|
|
221
|
+
record.receipt.derivedInterface.layoutIdentity !== root.layoutIdentity
|
|
129
222
|
) {
|
|
130
223
|
return createMaterialError('material-derived-interface-mismatch', {
|
|
131
224
|
code: 'material-derived-interface-mismatch',
|