@forgeax/engine-runtime 0.1.7 → 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/README.md +45 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/helpers/standard-material-manifest.d.ts +12 -0
- package/dist/__tests__/helpers/standard-material-manifest.d.ts.map +1 -0
- package/dist/__tests__/helpers/tilemap-assets.d.ts.map +1 -1
- package/dist/__tests__/material-texture-dimension-mismatch.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-dimension-mismatch.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-environment-consumer.integration.test.d.ts +2 -0
- package/dist/__tests__/render-environment-consumer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/__tests__/render-feature-prepared-graphics.fixture.d.ts.map +1 -1
- package/dist/__tests__/renderer-host-fallback.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-host-fallback.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skinned-shadow-mixed.dawn.test.d.ts +2 -0
- package/dist/__tests__/skinned-shadow-mixed.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/texture-dimensions.browser.test.d.ts +2 -0
- package/dist/__tests__/texture-dimensions.browser.test.d.ts.map +1 -0
- package/dist/__tests__/volumetric-fog-record-recovery.integration.test.d.ts +2 -0
- package/dist/__tests__/volumetric-fog-record-recovery.integration.test.d.ts.map +1 -0
- package/dist/backend-selection.d.ts.map +1 -1
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.mjs +21 -1
- package/dist/index.mjs.map +1 -1
- package/dist/renderer-host.d.ts +2 -1
- package/dist/renderer-host.d.ts.map +1 -1
- package/dist/renderer-host.mjs +21 -2
- package/dist/renderer-host.mjs.map +1 -1
- package/package.json +30 -30
- package/src/__tests__/asset-registry.recursive.spec.ts +12 -18
- package/src/__tests__/asset.unit.test.ts +28 -25
- package/src/__tests__/basis-catalog-dispatch.integration.test.ts +10 -9
- package/src/__tests__/compressed-texture-physical-extent.integration.test.ts +12 -10
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +2 -3
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +12 -11
- package/src/__tests__/dawn/material-alpha.dawn.test.ts +2 -3
- package/src/__tests__/dawn/mipmap-srgb-linear.dawn.test.ts +2 -3
- package/src/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.ts +2 -3
- package/src/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.ts +2 -3
- package/src/__tests__/dawn/upload-texture.dawn.test.ts +4 -6
- package/src/__tests__/derive-bgl-integration.test.ts +11 -0
- package/src/__tests__/equirect-bc6h.integration.test.ts +6 -4
- package/src/__tests__/errors.unit.test.ts +56 -0
- package/src/__tests__/extract-record-no-hardcoded-texture-fields.test.ts +4 -5
- package/src/__tests__/four-path-compression.integration.test.ts +2 -2
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +461 -18
- package/src/__tests__/geometry.unit.test.ts +3 -1
- package/src/__tests__/gpu-resource-cow-survivor.test.ts +2 -3
- package/src/__tests__/helpers/standard-material-manifest.ts +45 -0
- package/src/__tests__/helpers/tilemap-assets.ts +3 -4
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +4 -2
- package/src/__tests__/lights.unit.test.ts +38 -17
- package/src/__tests__/loadbyguid-texture-intern.unit.test.ts +3 -4
- package/src/__tests__/loader-name-survival.test.ts +2 -3
- package/src/__tests__/material-texture-dimension-mismatch.unit.test.ts +18 -0
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +18 -16
- package/src/__tests__/materials.unit.test.ts +2 -0
- package/src/__tests__/mip-gate.unit.test.ts +11 -8
- package/src/__tests__/msaa-sprite-pixel-diff.dawn.test.ts +2 -3
- package/src/__tests__/new-kind-refs-contract.test.ts +2 -3
- package/src/__tests__/pbr-pipeline.unit.test.ts +31 -12
- package/src/__tests__/pipeline-cache-keying.unit.test.ts +6 -4
- package/src/__tests__/pipeline.unit.test.ts +23 -21
- package/src/__tests__/point-light-shadow.unit.test.ts +6 -5
- package/src/__tests__/render-environment-consumer.integration.test.ts +33 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +86 -0
- package/src/__tests__/render-feature-prepared-graphics.fixture.ts +10 -0
- package/src/__tests__/render-system-extract.test.ts +4 -6
- package/src/__tests__/render-system-mega.test.ts +3 -1
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +7 -7
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +5 -4
- package/src/__tests__/render-system-record.test.ts +8 -6
- package/src/__tests__/renderer-host-fallback.unit.test.ts +112 -0
- package/src/__tests__/renderer-lifecycle.integration.test.ts +10 -0
- package/src/__tests__/renderer-surface.unit.test.ts +66 -0
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +5 -1
- package/src/__tests__/shadow-csm-tile-consistency.test.ts +5 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +56 -39
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +385 -0
- package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +4 -6
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -9
- package/src/__tests__/ssao-passes.test.ts +28 -12
- package/src/__tests__/systems.unit.test.ts +63 -33
- package/src/__tests__/texture-dimensions.browser.test.ts +127 -0
- package/src/__tests__/transcode-fallback.unit.test.ts +5 -1
- package/src/__tests__/volumetric-fog-record-recovery.integration.test.ts +180 -0
- package/src/backend-selection.ts +4 -0
- package/src/createRenderer.ts +3 -0
- package/src/renderer-host.ts +30 -3
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const environmentSource = readFileSync(
|
|
6
|
+
fileURLToPath(new URL('../../../render/src/extract/environment.ts', import.meta.url)),
|
|
7
|
+
'utf8',
|
|
8
|
+
);
|
|
9
|
+
const vfxSource = readFileSync(
|
|
10
|
+
fileURLToPath(
|
|
11
|
+
new URL('../../../vfx-render/src/feature/gpu-particle-feature.ts', import.meta.url),
|
|
12
|
+
),
|
|
13
|
+
'utf8',
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
describe('runtime Environment consumer ownership', () => {
|
|
17
|
+
it('consumes extracted Environment facts without a second Fog resource topology', () => {
|
|
18
|
+
expect(environmentSource).toContain('createEnvironmentExtractionContext');
|
|
19
|
+
expect(environmentSource).not.toContain('createFullscreenRenderFeature');
|
|
20
|
+
expect(vfxSource).not.toMatch(/fog.*(history|counter|upload)/i);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('indexes the public recovery sequence without exposing graph or device state', () => {
|
|
24
|
+
const runtimeReadme = readFileSync(
|
|
25
|
+
fileURLToPath(new URL('../../README.md', import.meta.url)),
|
|
26
|
+
'utf8',
|
|
27
|
+
);
|
|
28
|
+
expect(runtimeReadme).toContain('inspect()');
|
|
29
|
+
expect(runtimeReadme).toContain('typed `detail`');
|
|
30
|
+
expect(runtimeReadme).toMatch(/retry the same\s+request/);
|
|
31
|
+
expect(runtimeReadme).toContain('graph, device, or');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -38,14 +38,30 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
38
38
|
return code;
|
|
39
39
|
case 'cleanup-failed':
|
|
40
40
|
return code;
|
|
41
|
+
case 'taa-unavailable':
|
|
42
|
+
return code;
|
|
41
43
|
case 'frame-receipt-stale':
|
|
42
44
|
return code;
|
|
43
45
|
case 'renderer-contract-failed':
|
|
44
46
|
return code;
|
|
45
47
|
case 'observation-unavailable':
|
|
46
48
|
return code;
|
|
49
|
+
case 'scene-data-unavailable':
|
|
50
|
+
return code;
|
|
47
51
|
case 'shadow-invalid-config':
|
|
48
52
|
return code;
|
|
53
|
+
case 'environment-source-conflict':
|
|
54
|
+
return code;
|
|
55
|
+
case 'fog-cardinality':
|
|
56
|
+
return code;
|
|
57
|
+
case 'taa-caps-insufficient':
|
|
58
|
+
return code;
|
|
59
|
+
case 'environment-generation-failed':
|
|
60
|
+
return code;
|
|
61
|
+
case 'atmosphere-invalid-parameter':
|
|
62
|
+
return code;
|
|
63
|
+
case 'owner-stage-failed':
|
|
64
|
+
return code;
|
|
49
65
|
case 'equirect-projection-failed':
|
|
50
66
|
return code;
|
|
51
67
|
case 'hdrp-light-budget-exceeded':
|
|
@@ -84,6 +100,8 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
84
100
|
return code;
|
|
85
101
|
case 'render-feature-draw-recording-failed':
|
|
86
102
|
return code;
|
|
103
|
+
case 'transmission-capability-missing':
|
|
104
|
+
return code;
|
|
87
105
|
case 'points-lines-invalid-style':
|
|
88
106
|
return code;
|
|
89
107
|
case 'points-lines-topology-mismatch':
|
|
@@ -96,6 +114,16 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
96
114
|
return code;
|
|
97
115
|
case 'points-lines-prepare-failed':
|
|
98
116
|
return code;
|
|
117
|
+
case 'projector-binding-failed':
|
|
118
|
+
return code;
|
|
119
|
+
case 'volume-owner-conflict':
|
|
120
|
+
return code;
|
|
121
|
+
case 'volume-density-shape-mismatch':
|
|
122
|
+
return code;
|
|
123
|
+
case 'volume-invalid-bounds':
|
|
124
|
+
return code;
|
|
125
|
+
case 'volume-invalid-parameters':
|
|
126
|
+
return code;
|
|
99
127
|
default: {
|
|
100
128
|
const exhaustive: never = code;
|
|
101
129
|
return exhaustive;
|
|
@@ -171,10 +199,41 @@ function narrowRenderError(err: RenderError): void {
|
|
|
171
199
|
void err.detail.reason;
|
|
172
200
|
void err.detail.recovery;
|
|
173
201
|
break;
|
|
202
|
+
case 'scene-data-unavailable':
|
|
203
|
+
void err.detail.featureIdentity;
|
|
204
|
+
void err.detail.schema;
|
|
205
|
+
void err.detail.lane;
|
|
206
|
+
void err.detail.reason;
|
|
207
|
+
void err.detail.missingContributorIds;
|
|
208
|
+
void err.detail.omittedMissingContributorCount;
|
|
209
|
+
void err.detail.recovery;
|
|
210
|
+
break;
|
|
174
211
|
case 'shadow-invalid-config':
|
|
175
212
|
void err.detail.field; // string
|
|
176
213
|
void err.detail.value; // number
|
|
177
214
|
break;
|
|
215
|
+
case 'environment-source-conflict':
|
|
216
|
+
void err.detail.owners;
|
|
217
|
+
break;
|
|
218
|
+
case 'fog-cardinality':
|
|
219
|
+
void err.detail.count;
|
|
220
|
+
break;
|
|
221
|
+
case 'taa-caps-insufficient':
|
|
222
|
+
void err.detail.required;
|
|
223
|
+
void err.detail.available;
|
|
224
|
+
break;
|
|
225
|
+
case 'environment-generation-failed':
|
|
226
|
+
void err.detail.sourceKey;
|
|
227
|
+
void err.detail.stage;
|
|
228
|
+
break;
|
|
229
|
+
case 'atmosphere-invalid-parameter':
|
|
230
|
+
void err.detail.field;
|
|
231
|
+
void err.detail.value;
|
|
232
|
+
break;
|
|
233
|
+
case 'owner-stage-failed':
|
|
234
|
+
void err.detail.owner;
|
|
235
|
+
void err.detail.stage;
|
|
236
|
+
break;
|
|
178
237
|
case 'equirect-projection-failed':
|
|
179
238
|
void err.detail.handle; // number
|
|
180
239
|
break;
|
|
@@ -247,6 +306,9 @@ function narrowRenderError(err: RenderError): void {
|
|
|
247
306
|
void err.detail.backendReason;
|
|
248
307
|
void err.detail.operation;
|
|
249
308
|
break;
|
|
309
|
+
case 'taa-unavailable':
|
|
310
|
+
void err.detail.reason;
|
|
311
|
+
break;
|
|
250
312
|
case 'vertex-color-variant-conflict':
|
|
251
313
|
void err.detail.authored;
|
|
252
314
|
void err.detail.authoredValue;
|
|
@@ -276,6 +338,30 @@ function narrowRenderError(err: RenderError): void {
|
|
|
276
338
|
void err.detail.owner;
|
|
277
339
|
void err.detail.generation;
|
|
278
340
|
break;
|
|
341
|
+
case 'transmission-capability-missing':
|
|
342
|
+
void err.detail.material;
|
|
343
|
+
void err.detail.capability;
|
|
344
|
+
void err.detail.stage;
|
|
345
|
+
break;
|
|
346
|
+
case 'projector-binding-failed':
|
|
347
|
+
void err.detail.guid;
|
|
348
|
+
void err.detail.status;
|
|
349
|
+
break;
|
|
350
|
+
case 'volume-owner-conflict':
|
|
351
|
+
void err.detail.ownerCount;
|
|
352
|
+
break;
|
|
353
|
+
case 'volume-density-shape-mismatch':
|
|
354
|
+
void err.detail.guid;
|
|
355
|
+
void err.detail.viewDimension;
|
|
356
|
+
break;
|
|
357
|
+
case 'volume-invalid-bounds':
|
|
358
|
+
void err.detail.min;
|
|
359
|
+
void err.detail.max;
|
|
360
|
+
break;
|
|
361
|
+
case 'volume-invalid-parameters':
|
|
362
|
+
void err.detail.field;
|
|
363
|
+
void err.detail.value;
|
|
364
|
+
break;
|
|
279
365
|
default: {
|
|
280
366
|
const exhaustive: never = err;
|
|
281
367
|
void exhaustive;
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import { Camera } from '@forgeax/engine-render';
|
|
9
9
|
import { Transform } from '@forgeax/engine-scene';
|
|
10
10
|
import { ok } from '@forgeax/engine-types';
|
|
11
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
11
12
|
|
|
12
13
|
export type PreparedFeatureMode = 'accepted' | 'empty' | 'mismatch' | 'recovery';
|
|
13
14
|
|
|
@@ -19,6 +20,15 @@ export const preparedManifest = `data:application/json,${encodeURIComponent(
|
|
|
19
20
|
{ hash: 'unlit000', wgsl: '/* unlit stub */', glsl: '', bindings: '' },
|
|
20
21
|
{ hash: 'tonemap0', wgsl: '/* tonemap stub */', glsl: '', bindings: '' },
|
|
21
22
|
],
|
|
23
|
+
materialShaders: [
|
|
24
|
+
{
|
|
25
|
+
identifier: 'forgeax::default-standard-pbr',
|
|
26
|
+
sourcePath: 'forgeax::default-standard-pbr.wgsl',
|
|
27
|
+
composedWgsl: '/* stub */',
|
|
28
|
+
paramSchema: '[]',
|
|
29
|
+
variants: standardMaterialShaderVariants(),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
22
32
|
}),
|
|
23
33
|
)}`;
|
|
24
34
|
|
|
@@ -1193,12 +1193,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
1193
1193
|
'TextureAsset',
|
|
1194
1194
|
{
|
|
1195
1195
|
kind: 'texture',
|
|
1196
|
-
width: 1,
|
|
1197
|
-
height: 1,
|
|
1196
|
+
shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
|
|
1198
1197
|
format: 'rgba8unorm',
|
|
1199
1198
|
data: new Uint8Array([255, 255, 255, 255]),
|
|
1200
1199
|
colorSpace: 'linear',
|
|
1201
|
-
|
|
1200
|
+
mips: { kind: 'none' },
|
|
1202
1201
|
},
|
|
1203
1202
|
);
|
|
1204
1203
|
}
|
|
@@ -1211,12 +1210,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
1211
1210
|
const textureGuid = AssetGuid.random();
|
|
1212
1211
|
assets.catalog(textureGuid, {
|
|
1213
1212
|
kind: 'texture',
|
|
1214
|
-
width: 1,
|
|
1215
|
-
height: 1,
|
|
1213
|
+
shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
|
|
1216
1214
|
format: 'rgba8unorm',
|
|
1217
1215
|
data: new Uint8Array([255, 255, 255, 255]),
|
|
1218
1216
|
colorSpace: 'linear',
|
|
1219
|
-
|
|
1217
|
+
mips: { kind: 'none' },
|
|
1220
1218
|
} satisfies TextureAsset);
|
|
1221
1219
|
const matHandle = world.allocSharedRef<'MaterialAsset', MaterialAsset>('MaterialAsset', {
|
|
1222
1220
|
kind: 'material',
|
|
@@ -20,6 +20,7 @@ import { Transform } from '@forgeax/engine-scene';
|
|
|
20
20
|
import type { Handle } from '@forgeax/engine-types';
|
|
21
21
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
22
22
|
import { extractFrame, prepareExtractContext } from '../../../render/src/render-system-extract';
|
|
23
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
23
24
|
import { drawWithOwners } from './renderer-test-utils';
|
|
24
25
|
|
|
25
26
|
type RendererErrorObservation = {
|
|
@@ -278,7 +279,8 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
278
279
|
sourcePath: `${identifier}.wgsl`,
|
|
279
280
|
composedWgsl: '/* stub */',
|
|
280
281
|
paramSchema: '[]',
|
|
281
|
-
variants:
|
|
282
|
+
variants:
|
|
283
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
282
284
|
});
|
|
283
285
|
const manifest = {
|
|
284
286
|
schemaVersion: '1.0.0',
|
|
@@ -33,6 +33,7 @@ import type { World as WorldType } from '@forgeax/engine-ecs';
|
|
|
33
33
|
import type { Renderer as RendererType } from '@forgeax/engine-render';
|
|
34
34
|
import type { Handle, MaterialAsset, MeshAsset, TextureAsset } from '@forgeax/engine-types';
|
|
35
35
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
36
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
36
37
|
|
|
37
38
|
function canonicalTestMeshAttributes(vertexCount: number) {
|
|
38
39
|
return {
|
|
@@ -195,7 +196,8 @@ function buildManifestDataUrl(): string {
|
|
|
195
196
|
sourcePath: `${identifier}.wgsl`,
|
|
196
197
|
composedWgsl: '/* stub */',
|
|
197
198
|
paramSchema,
|
|
198
|
-
variants:
|
|
199
|
+
variants:
|
|
200
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
199
201
|
});
|
|
200
202
|
// The standard-PBR shader declares baseColorTexture / metallicRoughnessTexture
|
|
201
203
|
// / normalTexture as texture2d params. Texture resolution is now schema-driven
|
|
@@ -321,8 +323,7 @@ function makeChequerTexture(): TextureAsset {
|
|
|
321
323
|
// textureView sentinel.
|
|
322
324
|
return {
|
|
323
325
|
kind: 'texture',
|
|
324
|
-
width: 2,
|
|
325
|
-
height: 2,
|
|
326
|
+
shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
|
|
326
327
|
// colorSpace=linear+rgba8unorm to satisfy the runtime's
|
|
327
328
|
// srgb-format consistency validator (layer 7c-1: srgb requires
|
|
328
329
|
// rgba8unorm-srgb). The test only cares that each texture's
|
|
@@ -330,7 +331,7 @@ function makeChequerTexture(): TextureAsset {
|
|
|
330
331
|
format: 'rgba8unorm',
|
|
331
332
|
data: new Uint8Array(2 * 2 * 4),
|
|
332
333
|
colorSpace: 'linear',
|
|
333
|
-
|
|
334
|
+
mips: { kind: 'none' },
|
|
334
335
|
} as unknown as TextureAsset;
|
|
335
336
|
}
|
|
336
337
|
|
|
@@ -584,12 +585,11 @@ async function spawnPbrMissingTextureScene(): Promise<unknown> {
|
|
|
584
585
|
// existing debug-pink fallback.
|
|
585
586
|
const unresidentTexture = {
|
|
586
587
|
kind: 'texture',
|
|
587
|
-
width: 2,
|
|
588
|
-
height: 2,
|
|
588
|
+
shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
|
|
589
589
|
format: 'rgba8unorm-srgb',
|
|
590
590
|
data: new Uint8Array(2 * 2 * 4),
|
|
591
591
|
colorSpace: 'srgb',
|
|
592
|
-
|
|
592
|
+
mips: { kind: 'none' },
|
|
593
593
|
} as unknown as TextureAsset;
|
|
594
594
|
const badTexHandle = world.allocSharedRef('TextureAsset', unresidentTexture) as unknown as Handle<
|
|
595
595
|
'TextureAsset',
|
|
@@ -26,6 +26,7 @@ import type { World as WorldType } from '@forgeax/engine-ecs';
|
|
|
26
26
|
import type { Renderer as RendererType } from '@forgeax/engine-render';
|
|
27
27
|
import type { Handle, MaterialAsset, MeshAsset, TextureAsset } from '@forgeax/engine-types';
|
|
28
28
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
29
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
29
30
|
|
|
30
31
|
function canonicalTestMeshAttributes(vertexCount: number) {
|
|
31
32
|
return {
|
|
@@ -161,7 +162,8 @@ function buildManifestDataUrl(): string {
|
|
|
161
162
|
sourcePath: `${identifier}.wgsl`,
|
|
162
163
|
composedWgsl: '/* stub */',
|
|
163
164
|
paramSchema,
|
|
164
|
-
variants:
|
|
165
|
+
variants:
|
|
166
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
165
167
|
});
|
|
166
168
|
const pbrParamSchema = JSON.stringify([
|
|
167
169
|
{ name: 'baseColor', type: 'color', default: [1, 1, 1, 1] },
|
|
@@ -254,12 +256,11 @@ function cameraTransform() {
|
|
|
254
256
|
function makeTex(): TextureAsset {
|
|
255
257
|
return {
|
|
256
258
|
kind: 'texture',
|
|
257
|
-
width: 2,
|
|
258
|
-
height: 2,
|
|
259
|
+
shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
|
|
259
260
|
format: 'rgba8unorm',
|
|
260
261
|
data: new Uint8Array(2 * 2 * 4),
|
|
261
262
|
colorSpace: 'linear',
|
|
262
|
-
|
|
263
|
+
mips: { kind: 'none' },
|
|
263
264
|
} as unknown as TextureAsset;
|
|
264
265
|
}
|
|
265
266
|
|
|
@@ -55,7 +55,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
55
55
|
// asserts the PBR baseline against literal byte values.
|
|
56
56
|
//
|
|
57
57
|
// Coverage:
|
|
58
|
-
// -
|
|
58
|
+
// - 384 B payload (STANDARD_PBR_UBO_SIZE)
|
|
59
59
|
// - slot 0 = baseColor.rgb + 1 (alpha hardcoded)
|
|
60
60
|
// - slot 1 first 8 B = metallic, roughness (f32x2)
|
|
61
61
|
// - slot 1 last 4 u32 (channelMap) = [2, 1, 0, 0]
|
|
@@ -108,7 +108,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
108
108
|
expect(typeof mod.buildPbrMaterialUboPayload).toBe('function');
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
it('
|
|
111
|
+
it('384 B payload size + standard PBR slot layout (baseline)', () => {
|
|
112
112
|
if (typeof mod.buildPbrMaterialUboPayload !== 'function') {
|
|
113
113
|
throw new Error('helper not exported yet');
|
|
114
114
|
}
|
|
@@ -124,7 +124,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
124
124
|
specularTint: [0.11, 0.22, 0.33],
|
|
125
125
|
});
|
|
126
126
|
const buf = mod.buildPbrMaterialUboPayload(snap);
|
|
127
|
-
expect(buf.byteLength).toBe(
|
|
127
|
+
expect(buf.byteLength).toBe(384);
|
|
128
128
|
const f32 = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
|
|
129
129
|
// feat-20260613 fix-issue-1 (D-8 channelMap split): the 4 channelMap
|
|
130
130
|
// u32 slots collapse into 4 independent f32 channel selectors at
|
|
@@ -161,7 +161,9 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
161
161
|
expect(f32[20]).toBeCloseTo(0.11);
|
|
162
162
|
expect(f32[21]).toBeCloseTo(0.22);
|
|
163
163
|
expect(f32[22]).toBeCloseTo(0.33);
|
|
164
|
-
|
|
164
|
+
// normalScale remains at the scalar end of the authored region after
|
|
165
|
+
// transmission fields moved the engine-owned coordinate records.
|
|
166
|
+
expect(f32[23]).toBe(1);
|
|
165
167
|
});
|
|
166
168
|
|
|
167
169
|
it('writes schema-carried alphaCutoff into the baseline PBR payload', () => {
|
|
@@ -180,15 +182,15 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
180
182
|
const apply = mod.applyMaterialTextureUvScales;
|
|
181
183
|
if (typeof apply !== 'function') throw new Error('helper not exported yet');
|
|
182
184
|
const world = new World();
|
|
183
|
-
const pbrPayload = new ArrayBuffer(
|
|
185
|
+
const pbrPayload = new ArrayBuffer(384);
|
|
184
186
|
apply(pbrPayload, makePbrSnapshot({}), world);
|
|
185
187
|
const pbrF32 = new Float32Array(pbrPayload);
|
|
186
|
-
expect(pbrF32[26]).toBe(1);
|
|
187
188
|
expect(pbrF32[34]).toBe(1);
|
|
188
189
|
expect(pbrF32[42]).toBe(1);
|
|
189
190
|
expect(pbrF32[50]).toBe(1);
|
|
190
191
|
expect(pbrF32[58]).toBe(1);
|
|
191
192
|
expect(pbrF32[66]).toBe(1);
|
|
193
|
+
expect(pbrF32[74]).toBe(1);
|
|
192
194
|
|
|
193
195
|
const spritePayload = new ArrayBuffer(128);
|
|
194
196
|
apply(
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { EngineEnvironmentError } from '@forgeax/engine-render/internal/construct-renderer';
|
|
2
|
+
import { RhiError } from '@forgeax/engine-rhi';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
constructRendererHost: vi.fn(),
|
|
7
|
+
loadBackendPack: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('@forgeax/engine-render/internal/construct-renderer', async (importOriginal) => {
|
|
11
|
+
const actual =
|
|
12
|
+
await importOriginal<typeof import('@forgeax/engine-render/internal/construct-renderer')>();
|
|
13
|
+
return { ...actual, constructRendererHost: mocks.constructRendererHost };
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
vi.mock('../backend-selection', async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal<typeof import('../backend-selection')>();
|
|
18
|
+
return { ...actual, loadBackendPack: mocks.loadBackendPack };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
import { constructRuntimeRendererHost } from '../renderer-host';
|
|
22
|
+
|
|
23
|
+
function environmentError(code: 'adapter-unavailable' | 'limit-exceeded') {
|
|
24
|
+
return new EngineEnvironmentError('no usable rendering backend', {
|
|
25
|
+
webgpuError: new RhiError({
|
|
26
|
+
code,
|
|
27
|
+
expected: 'a usable WebGPU backend',
|
|
28
|
+
hint: 'select another backend',
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function canvas() {
|
|
34
|
+
return { getContext: vi.fn((_kind?: string) => null) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('runtime renderer backend fallback boundary', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.resetAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('preserves ordinary construction errors and does not enter Channel 3', async () => {
|
|
43
|
+
const firstPack = { name: 'webgpu' };
|
|
44
|
+
const fallbackPack = { name: 'wgpu' };
|
|
45
|
+
const target = canvas();
|
|
46
|
+
const ordinaryError = new Error('shader compile failed');
|
|
47
|
+
mocks.loadBackendPack
|
|
48
|
+
.mockResolvedValueOnce({ ok: true, value: firstPack })
|
|
49
|
+
.mockResolvedValueOnce({ ok: true, value: fallbackPack });
|
|
50
|
+
mocks.constructRendererHost.mockImplementation(
|
|
51
|
+
async (hostCanvas: typeof target, _options: unknown, _bundler: unknown, pack: unknown) => {
|
|
52
|
+
if (pack === fallbackPack) hostCanvas.getContext('webgl2');
|
|
53
|
+
return { ok: false, error: ordinaryError };
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const result = await constructRuntimeRendererHost(target);
|
|
58
|
+
|
|
59
|
+
expect(result).toEqual({ ok: false, error: ordinaryError });
|
|
60
|
+
expect(mocks.loadBackendPack).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(mocks.constructRendererHost).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(target.getContext).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('uses one Channel 3 attempt for an environment-class WebGPU failure', async () => {
|
|
66
|
+
const firstPack = { name: 'webgpu' };
|
|
67
|
+
const fallbackPack = { name: 'wgpu' };
|
|
68
|
+
const target = canvas();
|
|
69
|
+
const environmentFailure = environmentError('adapter-unavailable');
|
|
70
|
+
mocks.loadBackendPack
|
|
71
|
+
.mockResolvedValueOnce({ ok: true, value: firstPack })
|
|
72
|
+
.mockResolvedValueOnce({ ok: true, value: fallbackPack });
|
|
73
|
+
mocks.constructRendererHost
|
|
74
|
+
.mockResolvedValueOnce({ ok: false, error: environmentFailure })
|
|
75
|
+
.mockImplementationOnce(async (hostCanvas: typeof target, _options, _bundler, pack) => {
|
|
76
|
+
if (pack === fallbackPack) hostCanvas.getContext('webgl2');
|
|
77
|
+
return { ok: true, value: {} };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const result = await constructRuntimeRendererHost(target);
|
|
81
|
+
|
|
82
|
+
expect(result.ok).toBe(true);
|
|
83
|
+
expect(mocks.loadBackendPack).toHaveBeenCalledTimes(2);
|
|
84
|
+
expect(mocks.loadBackendPack).toHaveBeenNthCalledWith(2, undefined, true);
|
|
85
|
+
expect(mocks.constructRendererHost).toHaveBeenNthCalledWith(
|
|
86
|
+
2,
|
|
87
|
+
target,
|
|
88
|
+
undefined,
|
|
89
|
+
undefined,
|
|
90
|
+
fallbackPack,
|
|
91
|
+
);
|
|
92
|
+
expect(target.getContext).toHaveBeenCalledOnce();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('never falls back for an explicitly injected RHI and retains structured detail', async () => {
|
|
96
|
+
const firstPack = { name: 'explicit' };
|
|
97
|
+
const target = canvas();
|
|
98
|
+
const environmentFailure = environmentError('limit-exceeded');
|
|
99
|
+
const explicitRhi = {};
|
|
100
|
+
mocks.loadBackendPack.mockResolvedValueOnce({ ok: true, value: firstPack });
|
|
101
|
+
mocks.constructRendererHost.mockResolvedValueOnce({ ok: false, error: environmentFailure });
|
|
102
|
+
|
|
103
|
+
const result = await constructRuntimeRendererHost(target, { rhi: explicitRhi as never });
|
|
104
|
+
|
|
105
|
+
expect(result).toEqual({ ok: false, error: environmentFailure });
|
|
106
|
+
expect(result.ok ? undefined : result.error).toBe(environmentFailure);
|
|
107
|
+
expect(environmentFailure.detail.webgpuError).toMatchObject({ code: 'limit-exceeded' });
|
|
108
|
+
expect(mocks.loadBackendPack).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect(mocks.constructRendererHost).toHaveBeenCalledTimes(1);
|
|
110
|
+
expect(target.getContext).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { World } from '@forgeax/engine-ecs';
|
|
2
2
|
import { rhi } from '@forgeax/engine-rhi-null';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
4
5
|
import { requireRenderer } from './renderer-test-utils';
|
|
5
6
|
|
|
6
7
|
function canvas(): HTMLCanvasElement {
|
|
@@ -15,6 +16,15 @@ const manifest = `data:application/json,${encodeURIComponent(
|
|
|
15
16
|
{ hash: 'unlit000', wgsl: '/* unlit */', glsl: '', bindings: '' },
|
|
16
17
|
{ hash: 'tonemap0', wgsl: '/* tonemap */', glsl: '', bindings: '' },
|
|
17
18
|
],
|
|
19
|
+
materialShaders: [
|
|
20
|
+
{
|
|
21
|
+
identifier: 'forgeax::default-standard-pbr',
|
|
22
|
+
sourcePath: 'forgeax::default-standard-pbr.wgsl',
|
|
23
|
+
composedWgsl: '/* stub */',
|
|
24
|
+
paramSchema: '[]',
|
|
25
|
+
variants: standardMaterialShaderVariants(),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
18
28
|
}),
|
|
19
29
|
)}`;
|
|
20
30
|
|
|
@@ -345,6 +345,72 @@ describe('Surface retry (w5)', () => {
|
|
|
345
345
|
expect(ctxCalls.n).toBe(1);
|
|
346
346
|
expect(cfgCalls.n).toBe(0);
|
|
347
347
|
});
|
|
348
|
+
|
|
349
|
+
it('keeps an existing configured LKG when a reconfigure candidate fails proof validation', () => {
|
|
350
|
+
const cfgCalls = { n: 0 };
|
|
351
|
+
const ctxCalls = { n: 0 };
|
|
352
|
+
const ps = makePipelineState();
|
|
353
|
+
const reg = new HealthListenerRegistry();
|
|
354
|
+
const mockCtx = {
|
|
355
|
+
...makeSurfaceCtx(cfgCalls, ctxCalls, 1),
|
|
356
|
+
presentationProof: { descriptor: true, acquisition: false, validation: true },
|
|
357
|
+
};
|
|
358
|
+
const pipelineState = ps as unknown as Parameters<typeof acquireSwapChainTarget>[1];
|
|
359
|
+
const dev = makeMockDevice(ps);
|
|
360
|
+
dev.caps = { backendKind: 'wgpu-webgl2', storageBuffer: false };
|
|
361
|
+
const errors: unknown[] = [];
|
|
362
|
+
|
|
363
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock internals
|
|
364
|
+
const internals: any = {
|
|
365
|
+
canvas: { width: 800, height: 600 },
|
|
366
|
+
device: dev,
|
|
367
|
+
context: mockCtx,
|
|
368
|
+
getPipelineState: () => ps,
|
|
369
|
+
errorRegistry: {
|
|
370
|
+
add: () => () => {},
|
|
371
|
+
fire: (error: unknown) => errors.push(error),
|
|
372
|
+
clear: () => {},
|
|
373
|
+
},
|
|
374
|
+
healthRegistry: reg,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const target = acquireSwapChainTarget(internals, pipelineState);
|
|
378
|
+
|
|
379
|
+
expect(target).toBeNull();
|
|
380
|
+
expect(cfgCalls.n).toBe(1);
|
|
381
|
+
expect(ctxCalls.n).toBe(1);
|
|
382
|
+
expect(ps.perPassResources.configured).toBe(true);
|
|
383
|
+
expect(errors).toHaveLength(1);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('keeps the first surface failure unconfigured and skips the retry', () => {
|
|
387
|
+
const cfgCalls = { n: 0 };
|
|
388
|
+
const ctxCalls = { n: 0 };
|
|
389
|
+
const ps = makePipelineState();
|
|
390
|
+
ps.perPassResources.configured = false;
|
|
391
|
+
const mockCtx = {
|
|
392
|
+
...makeSurfaceCtx(cfgCalls, ctxCalls, 1),
|
|
393
|
+
presentationProof: { descriptor: true, acquisition: false, validation: true },
|
|
394
|
+
};
|
|
395
|
+
const pipelineState = ps as unknown as Parameters<typeof acquireSwapChainTarget>[1];
|
|
396
|
+
const dev = makeMockDevice(ps);
|
|
397
|
+
dev.caps = { backendKind: 'wgpu-webgl2', storageBuffer: false };
|
|
398
|
+
|
|
399
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock internals
|
|
400
|
+
const internals: any = {
|
|
401
|
+
canvas: { width: 800, height: 600 },
|
|
402
|
+
device: dev,
|
|
403
|
+
context: mockCtx,
|
|
404
|
+
getPipelineState: () => ps,
|
|
405
|
+
errorRegistry: { add: () => () => {}, fire: () => {}, clear: () => {} },
|
|
406
|
+
healthRegistry: new HealthListenerRegistry(),
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
expect(acquireSwapChainTarget(internals, pipelineState)).toBeNull();
|
|
410
|
+
expect(cfgCalls.n).toBe(1);
|
|
411
|
+
expect(ctxCalls.n).toBe(1);
|
|
412
|
+
expect(ps.perPassResources.configured).toBe(false);
|
|
413
|
+
});
|
|
348
414
|
});
|
|
349
415
|
|
|
350
416
|
// ── w6: consecutive surface failure -> internal-fault (AC-04) ────────────────
|
|
@@ -21,6 +21,7 @@ import type { World as WorldType } from '@forgeax/engine-ecs';
|
|
|
21
21
|
import type { Renderer as RendererType } from '@forgeax/engine-render';
|
|
22
22
|
import type { Handle } from '@forgeax/engine-types';
|
|
23
23
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
24
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
24
25
|
|
|
25
26
|
const ENGINE = '../createRenderer';
|
|
26
27
|
|
|
@@ -152,7 +153,10 @@ function buildManifestDataUrl(): string {
|
|
|
152
153
|
sourcePath: `${identifier}.wgsl`,
|
|
153
154
|
composedWgsl,
|
|
154
155
|
paramSchema: '[]',
|
|
155
|
-
variants:
|
|
156
|
+
variants:
|
|
157
|
+
identifier === 'forgeax::default-standard-pbr'
|
|
158
|
+
? standardMaterialShaderVariants(composedWgsl)
|
|
159
|
+
: [],
|
|
156
160
|
});
|
|
157
161
|
const manifest = {
|
|
158
162
|
schemaVersion: '1.0.0',
|
|
@@ -28,6 +28,7 @@ import type { World as WorldType } from '@forgeax/engine-ecs';
|
|
|
28
28
|
import type { Renderer as RendererType } from '@forgeax/engine-render';
|
|
29
29
|
import type { Handle } from '@forgeax/engine-types';
|
|
30
30
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
31
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
31
32
|
|
|
32
33
|
const ENGINE = '../createRenderer';
|
|
33
34
|
|
|
@@ -143,7 +144,10 @@ function buildManifestDataUrl(): string {
|
|
|
143
144
|
sourcePath: `${identifier}.wgsl`,
|
|
144
145
|
composedWgsl,
|
|
145
146
|
paramSchema: '[]',
|
|
146
|
-
variants:
|
|
147
|
+
variants:
|
|
148
|
+
identifier === 'forgeax::default-standard-pbr'
|
|
149
|
+
? standardMaterialShaderVariants(composedWgsl)
|
|
150
|
+
: [],
|
|
147
151
|
});
|
|
148
152
|
const manifest = {
|
|
149
153
|
schemaVersion: '1.0.0',
|