@forgeax/engine-runtime 0.1.20 → 0.1.21
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 +102 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts.map +1 -0
- package/dist/__tests__/lights-preservation.fixup.test.d.ts +2 -0
- package/dist/__tests__/lights-preservation.fixup.test.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +30 -30
- package/src/__tests__/active-camera.unit.test.ts +6 -8
- package/src/__tests__/asset-registry.recursive.spec.ts +1 -1
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +2 -2
- package/src/__tests__/components.test-d.ts +10 -3
- package/src/__tests__/components.unit.test.ts +13 -7
- package/src/__tests__/create-renderer-assembly.integration.test.ts +24 -0
- package/src/__tests__/create-renderer-gpu-pass-timing.unit.test.ts +32 -0
- package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +66 -0
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +9 -1
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +15 -2
- package/src/__tests__/errors.unit.test.ts +51 -20
- package/src/__tests__/extract-frames-merge.test.ts +48 -40
- package/src/__tests__/extract-skin-mismatch.unit.test.ts +6 -2
- package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +13 -0
- package/src/__tests__/geometry.unit.test.ts +7 -10
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +2 -1
- package/src/__tests__/light-snapshot.test-d.ts +8 -0
- package/src/__tests__/lights-preservation.fixup.test.ts +127 -0
- package/src/__tests__/lights.unit.test.ts +75 -2235
- package/src/__tests__/pbr-pipeline.unit.test.ts +6 -1
- package/src/__tests__/pipeline.unit.test.ts +48 -202
- package/src/__tests__/point-light-shadow.browser.test.ts +208 -83
- package/src/__tests__/point-light-shadow.unit.test.ts +61 -4
- package/src/__tests__/render-error-exhaustive.test-d.ts +63 -9
- package/src/__tests__/render-system-mega.test.ts +30 -41
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +4 -1
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +4 -1
- package/src/__tests__/render-system-skin-bg.unit.test.ts +14 -9
- package/src/__tests__/renderer-recover.unit.test.ts +8 -0
- package/src/__tests__/renderer-surface.unit.test.ts +229 -3
- package/src/__tests__/shadow-csm-atlas.test.ts +41 -0
- package/src/__tests__/shadow-csm-component.test.ts +31 -24
- package/src/__tests__/shadow-csm-extract.test.ts +156 -47
- package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +72 -0
- package/src/__tests__/shadow-csm-shader.dawn.test.ts +19 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +41 -45
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +29 -0
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +85 -20
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +3 -1
- package/src/__tests__/skylight-lazy-projection.test.ts +8 -5
- package/src/__tests__/ssao-bgl.test.ts +10 -16
- package/src/__tests__/ssao-passes.test.ts +19 -2
- package/src/__tests__/systems.unit.test.ts +44 -31
- package/src/__tests__/view-ubo-layout.browser.test.ts +20 -10
- package/src/createRenderer.ts +1 -0
- package/src/index.ts +1 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -26,37 +26,37 @@
|
|
|
26
26
|
"LICENSE"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@forgeax/engine-animation": "0.1.
|
|
30
|
-
"@forgeax/engine-assets-runtime": "0.1.
|
|
31
|
-
"@forgeax/engine-audio-webaudio": "0.1.
|
|
32
|
-
"@forgeax/engine-codec": "0.1.
|
|
33
|
-
"@forgeax/engine-debug-draw": "0.1.
|
|
34
|
-
"@forgeax/engine-ecs": "0.1.
|
|
35
|
-
"@forgeax/engine-font": "0.1.
|
|
36
|
-
"@forgeax/engine-geometry": "0.1.
|
|
37
|
-
"@forgeax/engine-graphics-extras": "0.1.
|
|
38
|
-
"@forgeax/engine-image": "0.1.
|
|
39
|
-
"@forgeax/engine-input": "0.1.
|
|
40
|
-
"@forgeax/engine-math": "0.1.
|
|
41
|
-
"@forgeax/engine-pack": "0.1.
|
|
42
|
-
"@forgeax/engine-profiler": "0.1.
|
|
43
|
-
"@forgeax/engine-render": "0.1.
|
|
44
|
-
"@forgeax/engine-render-graph": "0.1.
|
|
45
|
-
"@forgeax/engine-rhi": "0.1.
|
|
46
|
-
"@forgeax/engine-rhi-null": "0.1.
|
|
47
|
-
"@forgeax/engine-rhi-webgpu": "0.1.
|
|
48
|
-
"@forgeax/engine-rhi-wgpu": "0.1.
|
|
49
|
-
"@forgeax/engine-scene": "0.1.
|
|
50
|
-
"@forgeax/engine-shader": "0.1.
|
|
51
|
-
"@forgeax/engine-skinning": "0.1.
|
|
52
|
-
"@forgeax/engine-types": "0.1.
|
|
53
|
-
"@forgeax/engine-vfx": "0.1.
|
|
29
|
+
"@forgeax/engine-animation": "0.1.21",
|
|
30
|
+
"@forgeax/engine-assets-runtime": "0.1.21",
|
|
31
|
+
"@forgeax/engine-audio-webaudio": "0.1.21",
|
|
32
|
+
"@forgeax/engine-codec": "0.1.21",
|
|
33
|
+
"@forgeax/engine-debug-draw": "0.1.21",
|
|
34
|
+
"@forgeax/engine-ecs": "0.1.21",
|
|
35
|
+
"@forgeax/engine-font": "0.1.21",
|
|
36
|
+
"@forgeax/engine-geometry": "0.1.21",
|
|
37
|
+
"@forgeax/engine-graphics-extras": "0.1.21",
|
|
38
|
+
"@forgeax/engine-image": "0.1.21",
|
|
39
|
+
"@forgeax/engine-input": "0.1.21",
|
|
40
|
+
"@forgeax/engine-math": "0.1.21",
|
|
41
|
+
"@forgeax/engine-pack": "0.1.21",
|
|
42
|
+
"@forgeax/engine-profiler": "0.1.21",
|
|
43
|
+
"@forgeax/engine-render": "0.1.21",
|
|
44
|
+
"@forgeax/engine-render-graph": "0.1.21",
|
|
45
|
+
"@forgeax/engine-rhi": "0.1.21",
|
|
46
|
+
"@forgeax/engine-rhi-null": "0.1.21",
|
|
47
|
+
"@forgeax/engine-rhi-webgpu": "0.1.21",
|
|
48
|
+
"@forgeax/engine-rhi-wgpu": "0.1.21",
|
|
49
|
+
"@forgeax/engine-scene": "0.1.21",
|
|
50
|
+
"@forgeax/engine-shader": "0.1.21",
|
|
51
|
+
"@forgeax/engine-skinning": "0.1.21",
|
|
52
|
+
"@forgeax/engine-types": "0.1.21",
|
|
53
|
+
"@forgeax/engine-vfx": "0.1.21"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@forgeax/engine-import": "0.1.
|
|
57
|
-
"@forgeax/engine-naga": "0.1.
|
|
58
|
-
"@forgeax/engine-state": "0.1.
|
|
59
|
-
"@forgeax/engine-vite-plugin-shader": "0.1.
|
|
56
|
+
"@forgeax/engine-import": "0.1.21",
|
|
57
|
+
"@forgeax/engine-naga": "0.1.21",
|
|
58
|
+
"@forgeax/engine-state": "0.1.21",
|
|
59
|
+
"@forgeax/engine-vite-plugin-shader": "0.1.21",
|
|
60
60
|
"@types/node": "^20.14.0"
|
|
61
61
|
},
|
|
62
62
|
"forgeax": {
|
|
@@ -147,16 +147,14 @@ describe('w7 — getActiveCamera / setActiveCamera', () => {
|
|
|
147
147
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
148
148
|
|
|
149
149
|
describe('w7 — extractFrame ActiveCamera selection', () => {
|
|
150
|
-
it('(2) no ActiveCamera + multiple cameras ->
|
|
150
|
+
it('(2) no ActiveCamera + multiple display cameras -> first display camera surfaces', () => {
|
|
151
151
|
const world = new World();
|
|
152
152
|
const assets = makeAssets();
|
|
153
153
|
spawnCamera(world, 1.0);
|
|
154
154
|
spawnCamera(world, 2.0);
|
|
155
155
|
const frame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
156
|
-
// No selection ->
|
|
157
|
-
|
|
158
|
-
// does NOT prune when there is no ActiveCamera.
|
|
159
|
-
expect(frame.cameras.length).toBe(2);
|
|
156
|
+
// No selection -> the first display camera remains the display authority.
|
|
157
|
+
expect(frame.cameras.length).toBe(1);
|
|
160
158
|
});
|
|
161
159
|
|
|
162
160
|
it('(1) ActiveCamera pointing at 2nd camera -> only that camera surfaces', () => {
|
|
@@ -171,15 +169,15 @@ describe('w7 — extractFrame ActiveCamera selection', () => {
|
|
|
171
169
|
expect(frame.cameras[0]?.fov).toBeCloseTo(2.0, 5);
|
|
172
170
|
});
|
|
173
171
|
|
|
174
|
-
it('(3) ActiveCamera pointing at non-existent entity -> fallback to first
|
|
172
|
+
it('(3) ActiveCamera pointing at non-existent entity -> fallback to first display camera', () => {
|
|
175
173
|
const world = new World();
|
|
176
174
|
const assets = makeAssets();
|
|
177
175
|
spawnCamera(world, 1.0);
|
|
178
176
|
spawnCamera(world, 2.0);
|
|
179
177
|
setActiveCamera(world, 999999);
|
|
180
178
|
const frame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
181
|
-
// Unresolvable -> behave as if no ActiveCamera
|
|
182
|
-
expect(frame.cameras.length).toBe(
|
|
179
|
+
// Unresolvable -> behave as if no ActiveCamera.
|
|
180
|
+
expect(frame.cameras.length).toBe(1);
|
|
183
181
|
});
|
|
184
182
|
|
|
185
183
|
it('(4) single camera + ActiveCamera pointing at it -> behavior unchanged (one camera)', () => {
|
|
@@ -1166,7 +1166,7 @@ const LEAF_FIXTURES: readonly LeafFixture[] = [
|
|
|
1166
1166
|
kind: 'render-pipeline',
|
|
1167
1167
|
makeAsset: () => ({
|
|
1168
1168
|
kind: 'render-pipeline' as const,
|
|
1169
|
-
pipelineId: 'forgeax::
|
|
1169
|
+
pipelineId: 'forgeax::standard' as const,
|
|
1170
1170
|
}),
|
|
1171
1171
|
},
|
|
1172
1172
|
];
|
|
@@ -61,8 +61,8 @@ const SUT_ATTRIBUTABLE_RENDER_CODES: ReadonlySet<string> = new Set([
|
|
|
61
61
|
'vertex-storage-buffer-unavailable',
|
|
62
62
|
'mesh-ssbo-capacity-exceeded',
|
|
63
63
|
'mesh-ssbo-ceiling-reached',
|
|
64
|
-
'
|
|
65
|
-
'
|
|
64
|
+
'standard-light-budget-exceeded',
|
|
65
|
+
'standard-cluster-index-overflow',
|
|
66
66
|
]);
|
|
67
67
|
|
|
68
68
|
// Suppress WebGPU teardown race: chromium fires unhandled OperationError
|
|
@@ -67,7 +67,7 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
|
|
|
67
67
|
expectTypeOf<{ materials: readonly [0] }>().not.toMatchTypeOf<SpawnData>();
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
it('Camera data shape has
|
|
70
|
+
it('Camera data shape has 21 fields (18 number + clearColor array + autoAspect boolean + target)', () => {
|
|
71
71
|
type Data = ShapeOf<SchemaOf<typeof Camera>>;
|
|
72
72
|
expectTypeOf<keyof Data>().toEqualTypeOf<
|
|
73
73
|
| 'fov'
|
|
@@ -90,6 +90,7 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
|
|
|
90
90
|
| 'bloomBlurRadius'
|
|
91
91
|
| 'clearColor'
|
|
92
92
|
| 'autoAspect'
|
|
93
|
+
| 'target'
|
|
93
94
|
>();
|
|
94
95
|
expectTypeOf<Data['fov']>().toEqualTypeOf<number>();
|
|
95
96
|
expectTypeOf<Data['far']>().toEqualTypeOf<number>();
|
|
@@ -107,9 +108,10 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
|
|
|
107
108
|
expectTypeOf<Data['clearColor']>().toEqualTypeOf<Float32Array>();
|
|
108
109
|
// feat-20260617 / M3: AC-09 -- bool column narrows to boolean, not number.
|
|
109
110
|
expectTypeOf<Data['autoAspect']>().toEqualTypeOf<boolean>();
|
|
111
|
+
expectTypeOf<Data['target']>().toEqualTypeOf<Handle<'RenderTarget', 'shared'>>();
|
|
110
112
|
});
|
|
111
113
|
|
|
112
|
-
it('DirectionalLight data shape
|
|
114
|
+
it('DirectionalLight data shape includes closed shadow quality fields', () => {
|
|
113
115
|
// feat-20260621: DirectionalLightShadow merged into DirectionalLight via castShadow toggle.
|
|
114
116
|
// shadowDistance replaced the nearPlane/farPlane pair (near derives from camera).
|
|
115
117
|
// feat-20260709 M2: direction/color collapsed to array<f32,3> columns.
|
|
@@ -126,13 +128,18 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
|
|
|
126
128
|
| 'depthBias'
|
|
127
129
|
| 'normalBias'
|
|
128
130
|
| 'shadowDistance'
|
|
129
|
-
| '
|
|
131
|
+
| 'shadowFilter'
|
|
132
|
+
| 'shadowAngularRadius'
|
|
133
|
+
| 'maxPenumbraTexels'
|
|
130
134
|
>();
|
|
131
135
|
expectTypeOf<Data['direction']>().toEqualTypeOf<Float32Array>();
|
|
132
136
|
expectTypeOf<Data['color']>().toEqualTypeOf<Float32Array>();
|
|
133
137
|
expectTypeOf<Data['intensity']>().toEqualTypeOf<number>();
|
|
134
138
|
// bool column narrows to boolean, not number.
|
|
135
139
|
expectTypeOf<Data['castShadow']>().toEqualTypeOf<boolean>();
|
|
140
|
+
expectTypeOf<Data['shadowFilter']>().toEqualTypeOf<number>();
|
|
141
|
+
expectTypeOf<Data['shadowAngularRadius']>().toEqualTypeOf<number>();
|
|
142
|
+
expectTypeOf<Data['maxPenumbraTexels']>().toEqualTypeOf<number>();
|
|
136
143
|
});
|
|
137
144
|
});
|
|
138
145
|
|
|
@@ -176,9 +176,9 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
176
176
|
expect(schemaRecord.materials).toBe('array<shared<MaterialAsset>>');
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
it('Camera has
|
|
179
|
+
it('Camera has 21 fields (17 f32 + historyVersion u32 + clearColor array<f32,4> + autoAspect bool + target)', () => {
|
|
180
180
|
expect(Camera.name).toBe('Camera');
|
|
181
|
-
expect(Object.keys(componentSchema(Camera)).length).toBe(
|
|
181
|
+
expect(Object.keys(componentSchema(Camera)).length).toBe(21);
|
|
182
182
|
expect(componentSchema(Camera).fov).toBe('f32');
|
|
183
183
|
expect(componentSchema(Camera).aspect).toBe('f32');
|
|
184
184
|
expect(componentSchema(Camera).near).toBe('f32');
|
|
@@ -211,13 +211,13 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
211
211
|
expect(componentSchema(Camera).autoAspect).toBe('bool');
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
-
it('DirectionalLight has
|
|
214
|
+
it('DirectionalLight has 14 fields: 3 light + castShadow bool + closed shadow quality fields', () => {
|
|
215
215
|
// feat-20260621: DirectionalLightShadow merged into DirectionalLight via castShadow toggle.
|
|
216
216
|
// shadowDistance replaced the nearPlane/farPlane pair (near derives from camera).
|
|
217
217
|
// feat-20260709 M2: direction/color collapsed from 6 per-axis scalars to
|
|
218
218
|
// two array<f32,3> columns (3 light fields: direction + color + intensity).
|
|
219
219
|
expect(DirectionalLight.name).toBe('DirectionalLight');
|
|
220
|
-
expect(Object.keys(componentSchema(DirectionalLight)).length).toBe(
|
|
220
|
+
expect(Object.keys(componentSchema(DirectionalLight)).length).toBe(14);
|
|
221
221
|
// 3 light fields
|
|
222
222
|
expect(componentSchema(DirectionalLight).direction).toBe('array<f32, 3>');
|
|
223
223
|
expect(componentSchema(DirectionalLight).color).toBe('array<f32, 3>');
|
|
@@ -231,7 +231,10 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
231
231
|
expect(componentSchema(DirectionalLight).depthBias).toBe('f32');
|
|
232
232
|
expect(componentSchema(DirectionalLight).normalBias).toBe('f32');
|
|
233
233
|
expect(componentSchema(DirectionalLight).shadowDistance).toBe('f32');
|
|
234
|
-
expect(componentSchema(DirectionalLight).
|
|
234
|
+
expect(componentSchema(DirectionalLight).shadowFilter).toBe('enum');
|
|
235
|
+
expect(componentSchema(DirectionalLight).shadowAngularRadius).toBe('f32');
|
|
236
|
+
expect(componentSchema(DirectionalLight).maxPenumbraTexels).toBe('f32');
|
|
237
|
+
expect('pcfKernelSize' in componentSchema(DirectionalLight)).toBe(false);
|
|
235
238
|
});
|
|
236
239
|
|
|
237
240
|
it('all 5 components are frozen tokens with owner-assigned identities', () => {
|
|
@@ -1408,7 +1411,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1408
1411
|
// ────────────────────────────────────────────────────────────────────────────
|
|
1409
1412
|
|
|
1410
1413
|
describe('Camera.fields reflection (w14 AC-07 SSOT)', () => {
|
|
1411
|
-
it('Camera.fields has exactly
|
|
1414
|
+
it('Camera.fields has exactly 21 keys matching the Camera column set', () => {
|
|
1412
1415
|
const keys = Object.keys(Camera.fields).sort();
|
|
1413
1416
|
expect(keys).toEqual([
|
|
1414
1417
|
'antialias',
|
|
@@ -1428,6 +1431,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1428
1431
|
'near',
|
|
1429
1432
|
'projection',
|
|
1430
1433
|
'right',
|
|
1434
|
+
'target',
|
|
1431
1435
|
'tonemap',
|
|
1432
1436
|
'top',
|
|
1433
1437
|
'whitePoint',
|
|
@@ -1443,7 +1447,9 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1443
1447
|
? 'array<f32, 4>'
|
|
1444
1448
|
: key === 'historyVersion'
|
|
1445
1449
|
? 'u32'
|
|
1446
|
-
: '
|
|
1450
|
+
: key === 'target'
|
|
1451
|
+
? 'shared<RenderTarget>'
|
|
1452
|
+
: 'f32';
|
|
1447
1453
|
expect(Camera.fields[key].type).toBe(expected);
|
|
1448
1454
|
}
|
|
1449
1455
|
});
|
|
@@ -36,6 +36,30 @@ describe('runtime renderer host assembly', () => {
|
|
|
36
36
|
host.value.renderer.dispose();
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
it('repeats null-device assembly and frame observation without device loss', async () => {
|
|
40
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
41
|
+
const host = await constructRendererHost(canvas(), { rhi }, { shaderManifestUrl: manifest });
|
|
42
|
+
expect(host.ok).toBe(true);
|
|
43
|
+
if (!host.ok) continue;
|
|
44
|
+
const world = new World();
|
|
45
|
+
const attached = host.value.renderer.attach(world);
|
|
46
|
+
expect(attached.ok).toBe(true);
|
|
47
|
+
if (!attached.ok) {
|
|
48
|
+
host.value.renderer.dispose();
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
expect(world.update().ok).toBe(true);
|
|
52
|
+
const frame = host.value.renderer.draw(frameRequest(attached.value));
|
|
53
|
+
expect(frame.ok).toBe(true);
|
|
54
|
+
if (frame.ok) {
|
|
55
|
+
expect((await host.value.renderer.observe(frame.value, { include: ['timings'] })).ok).toBe(
|
|
56
|
+
true,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
host.value.renderer.dispose();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
39
63
|
it('keeps runtime construction errors structured', async () => {
|
|
40
64
|
const result = await createRenderer(canvas(), { rhi: undefined });
|
|
41
65
|
expect(result.ok).toBe(false);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import type { GpuPassTimingOptions, RendererOptions } from '@forgeax/engine-render';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
describe('runtime GPU pass timing forwarding', () => {
|
|
7
|
+
it('keeps one option object and projects the exact field to Render', () => {
|
|
8
|
+
const gpuPassTiming = { maxPassesPerFrame: 8 } satisfies GpuPassTimingOptions;
|
|
9
|
+
const options = { gpuPassTiming } satisfies RendererOptions;
|
|
10
|
+
expect(options.gpuPassTiming).toBe(gpuPassTiming);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('forwards without owning admission, sessions, handles, or CPU capture', () => {
|
|
14
|
+
const source = readFileSync(
|
|
15
|
+
fileURLToPath(new URL('../createRenderer.ts', import.meta.url)),
|
|
16
|
+
'utf8',
|
|
17
|
+
);
|
|
18
|
+
expect(source).toContain('gpuPassTiming: options.gpuPassTiming');
|
|
19
|
+
expect(source).not.toContain('createGpuPassTimingSession');
|
|
20
|
+
expect(source).not.toContain('ProfileCapture');
|
|
21
|
+
expect(source).not.toContain('passCatalog');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('keeps the no-option path free of a timing field', () => {
|
|
25
|
+
const source = readFileSync(
|
|
26
|
+
fileURLToPath(new URL('../createRenderer.ts', import.meta.url)),
|
|
27
|
+
'utf8',
|
|
28
|
+
);
|
|
29
|
+
expect(source).toContain('options === undefined');
|
|
30
|
+
expect(source).toContain('gpuPassTiming: options.gpuPassTiming');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -13,8 +13,74 @@
|
|
|
13
13
|
|
|
14
14
|
import { createRenderer, EngineEnvironmentError } from '@forgeax/engine-runtime';
|
|
15
15
|
import { describe, expect, it } from 'vitest';
|
|
16
|
+
import { projectDirectionalShadowInspection } from '../../../render/src/assembly/directional-shadow-inspection';
|
|
17
|
+
import { resolveDirectionalShadowBackendAdmission } from '../../../render/src/render-pipeline';
|
|
16
18
|
|
|
17
19
|
describe('createRenderer without navigator.gpu (browser-no-webgpu)', () => {
|
|
20
|
+
it('requires the shared backend admission truth table for WebGL2 fallback and capable failure', async () => {
|
|
21
|
+
const module = (await import('../../../render/src/render-pipeline')) as Record<string, unknown>;
|
|
22
|
+
const resolver = module.resolveDirectionalShadowBackendAdmission;
|
|
23
|
+
expect(typeof resolver).toBe('function');
|
|
24
|
+
const resolve = resolver as (input: {
|
|
25
|
+
backendKind: 'webgpu' | 'wgpu-webgl2';
|
|
26
|
+
requested: 'pcssMedium' | 'pcssHigh';
|
|
27
|
+
candidate: 'accepted' | 'failed';
|
|
28
|
+
lastKnownGood?: 'pcf3' | 'pcf5';
|
|
29
|
+
}) => {
|
|
30
|
+
effective: string;
|
|
31
|
+
status: string;
|
|
32
|
+
fallbackReason?: string;
|
|
33
|
+
};
|
|
34
|
+
expect(
|
|
35
|
+
resolve({ backendKind: 'wgpu-webgl2', requested: 'pcssMedium', candidate: 'accepted' }),
|
|
36
|
+
).toMatchObject({
|
|
37
|
+
effective: 'pcf3',
|
|
38
|
+
status: 'fallback',
|
|
39
|
+
fallbackReason: 'webgl2-unsupported',
|
|
40
|
+
});
|
|
41
|
+
expect(
|
|
42
|
+
resolve({
|
|
43
|
+
backendKind: 'webgpu',
|
|
44
|
+
requested: 'pcssHigh',
|
|
45
|
+
candidate: 'failed',
|
|
46
|
+
lastKnownGood: 'pcf3',
|
|
47
|
+
}),
|
|
48
|
+
).toMatchObject({ effective: 'pcf3', status: 'rejected', fallbackReason: 'candidate-failed' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps capable candidate failure explicit while retaining the LKG inspection facts', () => {
|
|
52
|
+
const inspection = projectDirectionalShadowInspection({
|
|
53
|
+
admission: resolveDirectionalShadowBackendAdmission({
|
|
54
|
+
backendKind: 'webgpu',
|
|
55
|
+
requested: 'pcssHigh',
|
|
56
|
+
candidate: 'failed',
|
|
57
|
+
lastKnownGood: 'pcf3',
|
|
58
|
+
}),
|
|
59
|
+
cascadeCount: 4,
|
|
60
|
+
mapSize: 2048,
|
|
61
|
+
atlasBytes: 67_108_864,
|
|
62
|
+
writerPasses: 4,
|
|
63
|
+
blockerTaps: 0,
|
|
64
|
+
filterTapUpperBound: 9,
|
|
65
|
+
seamTapUpperBound: 9,
|
|
66
|
+
deviceGeneration: 2,
|
|
67
|
+
graphGeneration: 6,
|
|
68
|
+
error: {
|
|
69
|
+
code: 'shadow-candidate-failed',
|
|
70
|
+
expected: 'the capable backend candidate compiles and validates',
|
|
71
|
+
hint: 'fix the authoring input, then retry or recover',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
expect(inspection).toMatchObject({
|
|
75
|
+
effective: 'pcf3',
|
|
76
|
+
status: 'rejected',
|
|
77
|
+
fallbackReason: 'candidate-failed',
|
|
78
|
+
lastKnownGood: true,
|
|
79
|
+
graphGeneration: 6,
|
|
80
|
+
error: { hint: expect.stringContaining('retry') },
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
18
84
|
it('navigator.gpu is absent or non-functional in this browser environment', async () => {
|
|
19
85
|
const nav = globalThis.navigator as { gpu?: GPU };
|
|
20
86
|
if (nav.gpu === undefined) return;
|
|
@@ -62,6 +62,12 @@ const BUFFER_USAGE_MAP_READ = 0x0001;
|
|
|
62
62
|
const BUFFER_USAGE_COPY_DST = 0x0008;
|
|
63
63
|
const MAP_MODE_READ = 0x0001;
|
|
64
64
|
|
|
65
|
+
// Windows Dawn first-touch construction for the two renderer/readback probes
|
|
66
|
+
// can exceed Vitest's project-wide 30-second timeout while still completing
|
|
67
|
+
// well inside the nightly platform budget. Keep this owner-specific gate
|
|
68
|
+
// bounded so a valid cold-start probe is not retried as a false failure.
|
|
69
|
+
const SPRITE_NINESLICE_DAWN_TEST_TIMEOUT_MS = 120_000;
|
|
70
|
+
|
|
65
71
|
// Distinctive 4x4 texture: corners RED (cells (0,0)/(3,0)/(0,3)/(3,3)),
|
|
66
72
|
// remaining cells BLUE. With slices=[0.25,0.25,0.25,0.25] in atlas-uv space
|
|
67
73
|
// the 4 corners line up with the 4 corner texels -- making the central band
|
|
@@ -325,7 +331,9 @@ function midBandDistance(a: CapturedFrame, b: CapturedFrame): number {
|
|
|
325
331
|
}
|
|
326
332
|
|
|
327
333
|
describe('feat-20260527-sprite-nineslice w14 dawn smoke (stretch vs tile divergence)', () => {
|
|
328
|
-
it('stretch + tile sprite frames diverge in the mid-band after w15 (red until w15 lands)',
|
|
334
|
+
it('stretch + tile sprite frames diverge in the mid-band after w15 (red until w15 lands)', {
|
|
335
|
+
timeout: SPRITE_NINESLICE_DAWN_TEST_TIMEOUT_MS,
|
|
336
|
+
}, async () => {
|
|
329
337
|
// Asymmetric scale: x=4 vs y=1. With slices=[0.25,0.25,0.25,0.25] and the
|
|
330
338
|
// 4x4 corner-checker texture, the post-w15 9-slice path keeps corner
|
|
331
339
|
// zones at fixed world dimensions while the legacy pre-w15 path stretches
|
|
@@ -55,6 +55,17 @@ const MAP_MODE_READ = 0x0001;
|
|
|
55
55
|
const INSTANCE_COUNT = 5;
|
|
56
56
|
const SPACING = 2.5;
|
|
57
57
|
|
|
58
|
+
const SHADOW_FILTER = (() => {
|
|
59
|
+
const value = (
|
|
60
|
+
globalThis as unknown as { process?: { env?: Record<string, string | undefined> } }
|
|
61
|
+
).process?.env?.FORGEAX_SHADOW_FILTER;
|
|
62
|
+
if (value === 'pcssMedium') return 4;
|
|
63
|
+
if (value === 'pcssHigh') return 5;
|
|
64
|
+
if (value === 'pcf1') return 1;
|
|
65
|
+
if (value === 'pcf5') return 3;
|
|
66
|
+
return 2;
|
|
67
|
+
})();
|
|
68
|
+
|
|
58
69
|
function buildTranslationGrid(): Float32Array {
|
|
59
70
|
const out = new Float32Array(INSTANCE_COUNT * 16);
|
|
60
71
|
const half = ((INSTANCE_COUNT - 1) * SPACING) / 2;
|
|
@@ -263,7 +274,9 @@ describe('w10 -- shadow instanced dawn smoke (AC-05 behavioral)', () => {
|
|
|
263
274
|
depthBias: 0.005,
|
|
264
275
|
normalBias: 0.05,
|
|
265
276
|
shadowDistance: 50,
|
|
266
|
-
|
|
277
|
+
shadowFilter: SHADOW_FILTER,
|
|
278
|
+
shadowAngularRadius: 0.00465,
|
|
279
|
+
maxPenumbraTexels: 32,
|
|
267
280
|
},
|
|
268
281
|
});
|
|
269
282
|
|
|
@@ -285,7 +298,7 @@ describe('w10 -- shadow instanced dawn smoke (AC-05 behavioral)', () => {
|
|
|
285
298
|
{ component: Instances, data: { transforms: instanceTransforms } },
|
|
286
299
|
);
|
|
287
300
|
|
|
288
|
-
// Render 300 frames for temporal stability (PCF shadow, light accumulation)
|
|
301
|
+
// Render 300 frames for temporal stability (PCF or PCSS shadow, light accumulation)
|
|
289
302
|
const attachment = renderer.attach(world);
|
|
290
303
|
expect(attachment.ok).toBe(true);
|
|
291
304
|
if (!attachment.ok) throw attachment.error;
|
|
@@ -349,7 +349,7 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
349
349
|
});
|
|
350
350
|
});
|
|
351
351
|
|
|
352
|
-
describe('t2 - AC-15 ImageErrorCode exhaustive switch (new
|
|
352
|
+
describe('t2 - AC-15 ImageErrorCode exhaustive switch (new members)', () => {
|
|
353
353
|
it('ImageErrorCode includes image-hdr-decode-failed', () => {
|
|
354
354
|
const code: ImageErrorCode = 'image-hdr-decode-failed';
|
|
355
355
|
expect(code).toBe('image-hdr-decode-failed');
|
|
@@ -378,10 +378,17 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
378
378
|
return 'atlas size exceeded';
|
|
379
379
|
case 'atlas-region-mismatch':
|
|
380
380
|
return 'atlas region mismatch';
|
|
381
|
+
// feat-20260829-deep-agent-game-feedback-resolution M5 —
|
|
382
|
+
// PixelSurface authoring errors remain part of the closed image
|
|
383
|
+
// error vocabulary and must stay covered by every exhaustive
|
|
384
|
+
// consumer switch.
|
|
385
|
+
case 'image-surface-invalid':
|
|
386
|
+
return 'surface invalid';
|
|
381
387
|
}
|
|
382
388
|
}
|
|
383
389
|
expect(exhaustive('image-decode-failed')).toBe('decode failed');
|
|
384
390
|
expect(exhaustive('image-hdr-decode-failed')).toBe('hdr decode failed');
|
|
391
|
+
expect(exhaustive('image-surface-invalid')).toBe('surface invalid');
|
|
385
392
|
});
|
|
386
393
|
});
|
|
387
394
|
});
|
|
@@ -420,6 +427,8 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
420
427
|
return 'cleanup failed';
|
|
421
428
|
case 'scene-data-unavailable':
|
|
422
429
|
return 'scene data unavailable';
|
|
430
|
+
case 'standard-profile-invalid':
|
|
431
|
+
return 'standard profile invalid';
|
|
423
432
|
case 'frame-receipt-stale':
|
|
424
433
|
return 'frame receipt stale';
|
|
425
434
|
case 'renderer-contract-failed':
|
|
@@ -464,14 +473,24 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
464
473
|
return 'mesh ssbo capacity exceeded';
|
|
465
474
|
case 'mesh-ssbo-ceiling-reached':
|
|
466
475
|
return 'mesh ssbo ceiling reached';
|
|
467
|
-
case '
|
|
468
|
-
return '
|
|
469
|
-
case '
|
|
470
|
-
return '
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
case '
|
|
474
|
-
return '
|
|
476
|
+
case 'render-target-descriptor-invalid':
|
|
477
|
+
return 'render target descriptor invalid';
|
|
478
|
+
case 'render-target-capability-missing':
|
|
479
|
+
return 'render target capability missing';
|
|
480
|
+
case 'render-target-state-invalid':
|
|
481
|
+
return 'render target state invalid';
|
|
482
|
+
case 'render-target-operation-failed':
|
|
483
|
+
return 'render target operation failed';
|
|
484
|
+
case 'reflection-probe-budget-exceeded':
|
|
485
|
+
return 'reflection probe budget exceeded';
|
|
486
|
+
case 'render-intent-invalid':
|
|
487
|
+
return 'render intent invalid';
|
|
488
|
+
case 'standard-light-budget-exceeded':
|
|
489
|
+
return 'standard light budget exceeded';
|
|
490
|
+
case 'standard-cluster-index-overflow':
|
|
491
|
+
return 'standard cluster index overflow';
|
|
492
|
+
case 'standard-cluster-transport-unavailable':
|
|
493
|
+
return 'standard cluster transport unavailable';
|
|
475
494
|
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w17 (D-5):
|
|
476
495
|
// bidirectional Skin <-> pbr-skin material mismatch detected at extract.
|
|
477
496
|
case 'skin-material-mismatch':
|
|
@@ -551,16 +570,15 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
551
570
|
});
|
|
552
571
|
|
|
553
572
|
/*
|
|
554
|
-
*
|
|
555
|
-
* RuntimeErrorCode +3 members — exhaustive narrow test.
|
|
573
|
+
* Standard lighting closed error codes — exhaustive narrow test.
|
|
556
574
|
*
|
|
557
|
-
* AC-07: three
|
|
575
|
+
* AC-07: three Standard lighting codes are members of the closed render union.
|
|
558
576
|
* Each error code narrows without a default branch in switch (err.code).
|
|
559
577
|
*/
|
|
560
|
-
describe('
|
|
561
|
-
it('
|
|
562
|
-
const code: RuntimeLayerErrorCode = '
|
|
563
|
-
expect(code).toBe('
|
|
578
|
+
describe('Standard lighting error members', () => {
|
|
579
|
+
it('standard-cluster-transport-unavailable is a valid RenderErrorCode', () => {
|
|
580
|
+
const code: RuntimeLayerErrorCode = 'standard-cluster-transport-unavailable';
|
|
581
|
+
expect(code).toBe('standard-cluster-transport-unavailable');
|
|
564
582
|
});
|
|
565
583
|
|
|
566
584
|
it('exhaustive switch covers all 3 new members alongside existing members', () => {
|
|
@@ -591,6 +609,7 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
591
609
|
case 'cleanup-failed':
|
|
592
610
|
return 'ok';
|
|
593
611
|
case 'scene-data-unavailable':
|
|
612
|
+
case 'standard-profile-invalid':
|
|
594
613
|
return 'ok';
|
|
595
614
|
case 'frame-receipt-stale':
|
|
596
615
|
return 'ok';
|
|
@@ -636,9 +655,9 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
636
655
|
return 'ok';
|
|
637
656
|
case 'mesh-ssbo-ceiling-reached':
|
|
638
657
|
return 'ok';
|
|
639
|
-
case '
|
|
658
|
+
case 'standard-light-budget-exceeded':
|
|
640
659
|
return 'ok';
|
|
641
|
-
case '
|
|
660
|
+
case 'standard-cluster-index-overflow':
|
|
642
661
|
return 'ok';
|
|
643
662
|
case 'skin-material-mismatch':
|
|
644
663
|
return 'ok';
|
|
@@ -650,7 +669,19 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
650
669
|
return 'ok';
|
|
651
670
|
case 'joint-entity-dangling':
|
|
652
671
|
return 'ok';
|
|
653
|
-
case '
|
|
672
|
+
case 'standard-cluster-transport-unavailable':
|
|
673
|
+
return 'ok';
|
|
674
|
+
case 'render-target-descriptor-invalid':
|
|
675
|
+
return 'ok';
|
|
676
|
+
case 'render-target-capability-missing':
|
|
677
|
+
return 'ok';
|
|
678
|
+
case 'render-target-state-invalid':
|
|
679
|
+
return 'ok';
|
|
680
|
+
case 'render-target-operation-failed':
|
|
681
|
+
return 'ok';
|
|
682
|
+
case 'reflection-probe-budget-exceeded':
|
|
683
|
+
return 'ok';
|
|
684
|
+
case 'render-intent-invalid':
|
|
654
685
|
return 'ok';
|
|
655
686
|
case 'point-shadow-atlas-uninitialized':
|
|
656
687
|
return 'ok';
|
|
@@ -702,7 +733,7 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
702
733
|
return 'ok';
|
|
703
734
|
}
|
|
704
735
|
}
|
|
705
|
-
expect(exhaustive('
|
|
736
|
+
expect(exhaustive('standard-cluster-transport-unavailable')).toBe('ok');
|
|
706
737
|
expect(exhaustive('point-shadow-atlas-uninitialized')).toBe('ok');
|
|
707
738
|
expect(exhaustive('point-shadow-atlas-bounds-violation')).toBe('ok');
|
|
708
739
|
});
|