@forgeax/engine-runtime 0.1.7 → 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 +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__/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/backend-selection.d.ts.map +1 -1
- package/dist/index.mjs +20 -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__/dawn/instances-per-instance-pbr.dawn.test.ts +12 -11
- package/src/__tests__/errors.unit.test.ts +36 -0
- package/src/__tests__/extract-record-no-hardcoded-texture-fields.test.ts +4 -5
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +461 -18
- package/src/__tests__/geometry.unit.test.ts +3 -1
- package/src/__tests__/helpers/standard-material-manifest.ts +45 -0
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +8 -14
- package/src/__tests__/materials.unit.test.ts +2 -0
- package/src/__tests__/pbr-pipeline.unit.test.ts +8 -8
- package/src/__tests__/pipeline-cache-keying.unit.test.ts +6 -4
- package/src/__tests__/pipeline.unit.test.ts +5 -2
- package/src/__tests__/render-environment-consumer.integration.test.ts +33 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +57 -0
- package/src/__tests__/render-feature-prepared-graphics.fixture.ts +10 -0
- package/src/__tests__/render-system-mega.test.ts +3 -1
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +3 -1
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +3 -1
- 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__/skinned-shadow-mixed.dawn.test.ts +383 -0
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +7 -7
- package/src/__tests__/ssao-passes.test.ts +28 -12
- package/src/__tests__/systems.unit.test.ts +48 -31
- package/src/backend-selection.ts +4 -0
- package/src/renderer-host.ts +30 -3
|
@@ -197,6 +197,8 @@ describe('Materials.standard multi-pass (w14)', () => {
|
|
|
197
197
|
{ name: 'specularTintTexture', type: 'texture', optional: true },
|
|
198
198
|
{ name: 'emissiveTexture', type: 'texture', optional: true },
|
|
199
199
|
{ name: 'occlusionTexture', type: 'texture', optional: true },
|
|
200
|
+
{ name: 'transmissionTexture', type: 'texture', optional: true },
|
|
201
|
+
{ name: 'thicknessTexture', type: 'texture', optional: true },
|
|
200
202
|
]);
|
|
201
203
|
});
|
|
202
204
|
|
|
@@ -82,22 +82,22 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
describe('pbr-material-merged', () => {
|
|
85
|
-
it('
|
|
85
|
+
it('26 entries: user-region 17 (derived) + ibl 7 + transmission 2', () => {
|
|
86
86
|
const spec = makeSpec();
|
|
87
87
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
88
88
|
kind: 'pbr-material-merged',
|
|
89
89
|
});
|
|
90
90
|
// Post-M2 (D-1): user-region comes from derive(paramSchema).bglEntries
|
|
91
|
-
// (built-in standard-PBR
|
|
92
|
-
// appended at start = userRegion.length.
|
|
91
|
+
// (built-in standard-PBR 8-texture contract), then IBL + transmission
|
|
92
|
+
// are appended at start = userRegion.length.
|
|
93
93
|
const userRegion = buildPbrMaterialUserRegionEntries();
|
|
94
94
|
const afterIbl = [...userRegion, ...appendInjection(userRegion, 'ibl')];
|
|
95
95
|
const expected = {
|
|
96
96
|
label: 'pbr-material-skylight-bgl',
|
|
97
|
-
entries: [...afterIbl, ...appendInjection(afterIbl, '
|
|
97
|
+
entries: [...afterIbl, ...appendInjection(afterIbl, 'transmission')],
|
|
98
98
|
};
|
|
99
99
|
expect(out).toEqual(expected);
|
|
100
|
-
expect(out.entries.length).toBe(
|
|
100
|
+
expect(out.entries.length).toBe(26);
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
103
|
|
|
@@ -142,7 +142,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
142
142
|
entries: [
|
|
143
143
|
{
|
|
144
144
|
binding: 0,
|
|
145
|
-
visibility:
|
|
145
|
+
visibility: 0x3,
|
|
146
146
|
buffer: { type: 'read-only-storage', hasDynamicOffset: false },
|
|
147
147
|
},
|
|
148
148
|
],
|
|
@@ -185,7 +185,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
185
185
|
});
|
|
186
186
|
|
|
187
187
|
describe('unlit-material', () => {
|
|
188
|
-
it('
|
|
188
|
+
it('17 entries: base PBR material only (no skylight injection)', () => {
|
|
189
189
|
const spec = makeSpec();
|
|
190
190
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
191
191
|
kind: 'unlit-material',
|
|
@@ -194,7 +194,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
194
194
|
label: 'unlit-material-bgl',
|
|
195
195
|
entries: buildPbrMaterialUserRegionEntries(),
|
|
196
196
|
});
|
|
197
|
-
expect(out.entries.length).toBe(
|
|
197
|
+
expect(out.entries.length).toBe(17);
|
|
198
198
|
});
|
|
199
199
|
});
|
|
200
200
|
});
|
|
@@ -127,14 +127,16 @@ describe('cacheKeyOf passKind dimension', () => {
|
|
|
127
127
|
describe('PassKind open string + KNOWN_PASS_KINDS (feat-20260615 D-10)', () => {
|
|
128
128
|
// M2-T4 expanded KNOWN_PASS_KINDS from 4 to 6 entries by adding 'post-process'
|
|
129
129
|
// and 'skybox'; point-shadow-caster is the engine's depth-only point-light
|
|
130
|
-
// pass and is part of the discoverable catalogue as well.
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
// pass and is part of the discoverable catalogue as well. M3-T4 adds the
|
|
131
|
+
// Standard temporal producer to the shipped pass catalogue.
|
|
132
|
+
it('KNOWN_PASS_KINDS has exactly 8 entries', () => {
|
|
133
|
+
expect(KNOWN_PASS_KINDS).toHaveLength(8);
|
|
133
134
|
});
|
|
134
135
|
|
|
135
|
-
it('KNOWN_PASS_KINDS contains the
|
|
136
|
+
it('KNOWN_PASS_KINDS contains the 8 engine-shipped pass kinds', () => {
|
|
136
137
|
expect(KNOWN_PASS_KINDS).toContain('forward');
|
|
137
138
|
expect(KNOWN_PASS_KINDS).toContain('deferred');
|
|
139
|
+
expect(KNOWN_PASS_KINDS).toContain('temporal');
|
|
138
140
|
expect(KNOWN_PASS_KINDS).toContain('lighting');
|
|
139
141
|
expect(KNOWN_PASS_KINDS).toContain('shadow-caster');
|
|
140
142
|
expect(KNOWN_PASS_KINDS).toContain('point-shadow-caster');
|
|
@@ -138,6 +138,7 @@ import {
|
|
|
138
138
|
import { extractFrame, prepareExtractContext } from '../../../render/src/render-system-extract';
|
|
139
139
|
import { matchPass } from '../../../render/src/systems/pass-selector';
|
|
140
140
|
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
141
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
141
142
|
|
|
142
143
|
function makeExplicitNullRhi(spies: {
|
|
143
144
|
setIndexBuffer: ReturnType<typeof vi.fn>;
|
|
@@ -2407,7 +2408,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
2407
2408
|
sourcePath: `${identifier}.wgsl`,
|
|
2408
2409
|
composedWgsl: '/* stub */',
|
|
2409
2410
|
paramSchema: '[]',
|
|
2410
|
-
variants:
|
|
2411
|
+
variants:
|
|
2412
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
2411
2413
|
});
|
|
2412
2414
|
const manifest = {
|
|
2413
2415
|
schemaVersion: '1.0.0',
|
|
@@ -2697,7 +2699,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
2697
2699
|
sourcePath: `${identifier}.wgsl`,
|
|
2698
2700
|
composedWgsl: '/* stub */',
|
|
2699
2701
|
paramSchema: '[]',
|
|
2700
|
-
variants:
|
|
2702
|
+
variants:
|
|
2703
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
2701
2704
|
});
|
|
2702
2705
|
const manifest = {
|
|
2703
2706
|
schemaVersion: '1.0.0',
|
|
@@ -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':
|
|
@@ -171,10 +189,41 @@ function narrowRenderError(err: RenderError): void {
|
|
|
171
189
|
void err.detail.reason;
|
|
172
190
|
void err.detail.recovery;
|
|
173
191
|
break;
|
|
192
|
+
case 'scene-data-unavailable':
|
|
193
|
+
void err.detail.featureIdentity;
|
|
194
|
+
void err.detail.schema;
|
|
195
|
+
void err.detail.lane;
|
|
196
|
+
void err.detail.reason;
|
|
197
|
+
void err.detail.missingContributorIds;
|
|
198
|
+
void err.detail.omittedMissingContributorCount;
|
|
199
|
+
void err.detail.recovery;
|
|
200
|
+
break;
|
|
174
201
|
case 'shadow-invalid-config':
|
|
175
202
|
void err.detail.field; // string
|
|
176
203
|
void err.detail.value; // number
|
|
177
204
|
break;
|
|
205
|
+
case 'environment-source-conflict':
|
|
206
|
+
void err.detail.owners;
|
|
207
|
+
break;
|
|
208
|
+
case 'fog-cardinality':
|
|
209
|
+
void err.detail.count;
|
|
210
|
+
break;
|
|
211
|
+
case 'taa-caps-insufficient':
|
|
212
|
+
void err.detail.required;
|
|
213
|
+
void err.detail.available;
|
|
214
|
+
break;
|
|
215
|
+
case 'environment-generation-failed':
|
|
216
|
+
void err.detail.sourceKey;
|
|
217
|
+
void err.detail.stage;
|
|
218
|
+
break;
|
|
219
|
+
case 'atmosphere-invalid-parameter':
|
|
220
|
+
void err.detail.field;
|
|
221
|
+
void err.detail.value;
|
|
222
|
+
break;
|
|
223
|
+
case 'owner-stage-failed':
|
|
224
|
+
void err.detail.owner;
|
|
225
|
+
void err.detail.stage;
|
|
226
|
+
break;
|
|
178
227
|
case 'equirect-projection-failed':
|
|
179
228
|
void err.detail.handle; // number
|
|
180
229
|
break;
|
|
@@ -247,6 +296,9 @@ function narrowRenderError(err: RenderError): void {
|
|
|
247
296
|
void err.detail.backendReason;
|
|
248
297
|
void err.detail.operation;
|
|
249
298
|
break;
|
|
299
|
+
case 'taa-unavailable':
|
|
300
|
+
void err.detail.reason;
|
|
301
|
+
break;
|
|
250
302
|
case 'vertex-color-variant-conflict':
|
|
251
303
|
void err.detail.authored;
|
|
252
304
|
void err.detail.authoredValue;
|
|
@@ -276,6 +328,11 @@ function narrowRenderError(err: RenderError): void {
|
|
|
276
328
|
void err.detail.owner;
|
|
277
329
|
void err.detail.generation;
|
|
278
330
|
break;
|
|
331
|
+
case 'transmission-capability-missing':
|
|
332
|
+
void err.detail.material;
|
|
333
|
+
void err.detail.capability;
|
|
334
|
+
void err.detail.stage;
|
|
335
|
+
break;
|
|
279
336
|
default: {
|
|
280
337
|
const exhaustive: never = err;
|
|
281
338
|
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
|
|
|
@@ -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
|
|
@@ -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] },
|
|
@@ -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',
|