@forgeax/engine-runtime 0.1.6 → 0.1.7
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 +16 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/collect-scene-asset.d.ts +6 -3
- package/dist/collect-scene-asset.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +5 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +30 -30
- package/src/__tests__/asset-registry-guid-reverse.test.ts +33 -0
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +39 -6
- package/src/__tests__/errors.unit.test.ts +24 -0
- package/src/__tests__/lights.unit.test.ts +15 -11
- package/src/__tests__/pbr-pipeline.unit.test.ts +7 -4
- package/src/__tests__/render-error-exhaustive.test-d.ts +36 -0
- package/src/__tests__/render-feature-prepared-graphics.browser.test.ts +5 -1
- package/src/__tests__/renderer.test-d.ts +2 -0
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +14 -0
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +3 -3
- package/src/__tests__/ssao-passes.test.ts +4 -0
- package/src/collect-scene-asset.ts +14 -7
- package/src/index.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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.7",
|
|
30
|
+
"@forgeax/engine-assets-runtime": "0.1.7",
|
|
31
|
+
"@forgeax/engine-audio-webaudio": "0.1.7",
|
|
32
|
+
"@forgeax/engine-codec": "0.1.7",
|
|
33
|
+
"@forgeax/engine-debug-draw": "0.1.7",
|
|
34
|
+
"@forgeax/engine-ecs": "0.1.7",
|
|
35
|
+
"@forgeax/engine-font": "0.1.7",
|
|
36
|
+
"@forgeax/engine-geometry": "0.1.7",
|
|
37
|
+
"@forgeax/engine-graphics-extras": "0.1.7",
|
|
38
|
+
"@forgeax/engine-image": "0.1.7",
|
|
39
|
+
"@forgeax/engine-input": "0.1.7",
|
|
40
|
+
"@forgeax/engine-math": "0.1.7",
|
|
41
|
+
"@forgeax/engine-pack": "0.1.7",
|
|
42
|
+
"@forgeax/engine-profiler": "0.1.7",
|
|
43
|
+
"@forgeax/engine-render": "0.1.7",
|
|
44
|
+
"@forgeax/engine-render-graph": "0.1.7",
|
|
45
|
+
"@forgeax/engine-rhi": "0.1.7",
|
|
46
|
+
"@forgeax/engine-rhi-null": "0.1.7",
|
|
47
|
+
"@forgeax/engine-rhi-webgpu": "0.1.7",
|
|
48
|
+
"@forgeax/engine-rhi-wgpu": "0.1.7",
|
|
49
|
+
"@forgeax/engine-scene": "0.1.7",
|
|
50
|
+
"@forgeax/engine-shader": "0.1.7",
|
|
51
|
+
"@forgeax/engine-skinning": "0.1.7",
|
|
52
|
+
"@forgeax/engine-types": "0.1.7",
|
|
53
|
+
"@forgeax/engine-vfx": "0.1.7"
|
|
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.7",
|
|
57
|
+
"@forgeax/engine-naga": "0.1.7",
|
|
58
|
+
"@forgeax/engine-state": "0.1.7",
|
|
59
|
+
"@forgeax/engine-vite-plugin-shader": "0.1.7",
|
|
60
60
|
"@types/node": "^20.14.0"
|
|
61
61
|
},
|
|
62
62
|
"forgeax": {
|
|
@@ -18,6 +18,7 @@ import * as SceneOwner from '@forgeax/engine-scene';
|
|
|
18
18
|
import {
|
|
19
19
|
type Asset,
|
|
20
20
|
AssetRegistry,
|
|
21
|
+
HANDLE_CUBE,
|
|
21
22
|
resolveAssetHandle,
|
|
22
23
|
SceneCollectAssetGuidUnresolvedError,
|
|
23
24
|
} from '@forgeax/engine-assets-runtime';
|
|
@@ -205,6 +206,38 @@ describe('m1-t2 — uncatalogued scene MISS path', () => {
|
|
|
205
206
|
});
|
|
206
207
|
});
|
|
207
208
|
|
|
209
|
+
describe('builtin reverse lookup ownership', () => {
|
|
210
|
+
it('collects a builtin mesh GUID without cataloguing the process-static payload', () => {
|
|
211
|
+
const registry = { guidOf: () => undefined };
|
|
212
|
+
const world = new World();
|
|
213
|
+
prepareWorld(world);
|
|
214
|
+
const scene: SceneAsset = {
|
|
215
|
+
kind: 'scene',
|
|
216
|
+
entities: [
|
|
217
|
+
{
|
|
218
|
+
localId: 0 as never,
|
|
219
|
+
components: {
|
|
220
|
+
Transform: {},
|
|
221
|
+
MeshFilter: { assetHandle: HANDLE_CUBE as never },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
const sceneHandle = world.allocSharedRef<'SceneAsset', SceneAsset>('SceneAsset', scene);
|
|
227
|
+
const instantiated = SceneOwner.worldInstantiateScene(world, sceneHandle);
|
|
228
|
+
expect(instantiated.ok).toBe(true);
|
|
229
|
+
if (!instantiated.ok) return;
|
|
230
|
+
|
|
231
|
+
const collected = rootsToSceneAsset(registry, world, [instantiated.value.root]);
|
|
232
|
+
expect(collected.ok).toBe(true);
|
|
233
|
+
if (!collected.ok) return;
|
|
234
|
+
const meshEntity = collected.value.entities.find(
|
|
235
|
+
(entity) => entity.components.MeshFilter !== undefined,
|
|
236
|
+
);
|
|
237
|
+
expect(meshEntity?.components.MeshFilter?.assetHandle).toBe(GUID_A);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
208
241
|
// ── w14 (M4) PROBE: mounted-scene-with-material identity anchor ──────────
|
|
209
242
|
//
|
|
210
243
|
// feat-20260713 M4 / w14 (D-6, probe-first). Empirically anchors the true
|
|
@@ -3,9 +3,22 @@ import { fileURLToPath } from 'node:url';
|
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
|
|
5
5
|
type CookedRecord = {
|
|
6
|
-
readonly
|
|
7
|
-
readonly
|
|
8
|
-
readonly
|
|
6
|
+
readonly specializationKey: string;
|
|
7
|
+
readonly artifactDigest: string;
|
|
8
|
+
readonly artifact: { readonly digest: string; readonly bytes: readonly number[] };
|
|
9
|
+
readonly resolved?: {
|
|
10
|
+
readonly values?: unknown;
|
|
11
|
+
};
|
|
12
|
+
readonly receipt?: {
|
|
13
|
+
readonly identity?: {
|
|
14
|
+
readonly layoutIdentity?: string;
|
|
15
|
+
readonly programIdentity?: string;
|
|
16
|
+
readonly pipelineIdentity?: string;
|
|
17
|
+
readonly cookIdentity?: string;
|
|
18
|
+
readonly compilerFingerprint?: string;
|
|
19
|
+
};
|
|
20
|
+
readonly schemaVersion?: string;
|
|
21
|
+
};
|
|
9
22
|
};
|
|
10
23
|
|
|
11
24
|
type MaterialRow = {
|
|
@@ -34,9 +47,29 @@ describe('custom-shader cooked MaterialAsset fixture', () => {
|
|
|
34
47
|
const derived = fixture.assets.find((asset) => asset.payload.role === 'derived');
|
|
35
48
|
expect(root?.payload.cooked).toBeDefined();
|
|
36
49
|
expect(derived?.payload.cooked).toBeDefined();
|
|
37
|
-
expect(derived?.payload.cooked?.
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
expect(derived?.payload.cooked?.specializationKey).toBe(
|
|
51
|
+
root?.payload.cooked?.specializationKey,
|
|
52
|
+
);
|
|
53
|
+
expect(derived?.payload.cooked?.artifactDigest).toBe(root?.payload.cooked?.artifactDigest);
|
|
54
|
+
expect(derived?.payload.cooked?.artifact.bytes).toEqual(root?.payload.cooked?.artifact.bytes);
|
|
55
|
+
expect(derived?.payload.cooked?.resolved?.values).toEqual(
|
|
56
|
+
root?.payload.cooked?.resolved?.values,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('keeps cooked layout identity and authored coordinate transforms', () => {
|
|
61
|
+
const root = fixture.assets.find((asset) => asset.payload.role === 'root');
|
|
62
|
+
const cooked = root?.payload.cooked;
|
|
63
|
+
expect(cooked?.receipt?.schemaVersion).toBe('material-cook/3');
|
|
64
|
+
expect(cooked?.receipt?.identity?.layoutIdentity).toMatch(/^sha256-/);
|
|
65
|
+
expect(cooked?.receipt?.identity?.programIdentity).toMatch(/^sha256:/);
|
|
66
|
+
expect(cooked?.receipt?.identity?.pipelineIdentity).toMatch(/^sha256:/);
|
|
67
|
+
expect(cooked?.receipt?.identity?.cookIdentity).toMatch(/^sha256:/);
|
|
68
|
+
expect(cooked?.receipt?.identity?.compilerFingerprint).toMatch(/^sha256-/);
|
|
69
|
+
expect(cooked?.resolved?.values).toMatchObject({
|
|
70
|
+
baseColorUvTransform: [0, 0, 1, 1],
|
|
71
|
+
normalUvTransform: [0.125, 0.25, 2, 2],
|
|
72
|
+
});
|
|
40
73
|
});
|
|
41
74
|
|
|
42
75
|
it('runs the real Dawn path for exactly 300 frames without unexpected RHI errors', () => {
|
|
@@ -500,6 +500,18 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
500
500
|
return 'render feature prepared state mismatch';
|
|
501
501
|
case 'render-feature-draw-recording-failed':
|
|
502
502
|
return 'render feature draw recording failed';
|
|
503
|
+
case 'points-lines-invalid-style':
|
|
504
|
+
return 'points lines invalid style';
|
|
505
|
+
case 'points-lines-topology-mismatch':
|
|
506
|
+
return 'points lines topology mismatch';
|
|
507
|
+
case 'points-lines-style-unsupported':
|
|
508
|
+
return 'points lines style unsupported';
|
|
509
|
+
case 'points-lines-material-unsupported':
|
|
510
|
+
return 'points lines material unsupported';
|
|
511
|
+
case 'points-lines-budget-exceeded':
|
|
512
|
+
return 'points lines budget exceeded';
|
|
513
|
+
case 'points-lines-prepare-failed':
|
|
514
|
+
return 'points lines prepare failed';
|
|
503
515
|
}
|
|
504
516
|
}
|
|
505
517
|
expect(exhaustive('equirect-projection-failed')).toBe('equirect projection failed');
|
|
@@ -620,6 +632,18 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
620
632
|
return 'ok';
|
|
621
633
|
case 'render-feature-draw-recording-failed':
|
|
622
634
|
return 'ok';
|
|
635
|
+
case 'points-lines-invalid-style':
|
|
636
|
+
return 'ok';
|
|
637
|
+
case 'points-lines-topology-mismatch':
|
|
638
|
+
return 'ok';
|
|
639
|
+
case 'points-lines-style-unsupported':
|
|
640
|
+
return 'ok';
|
|
641
|
+
case 'points-lines-material-unsupported':
|
|
642
|
+
return 'ok';
|
|
643
|
+
case 'points-lines-budget-exceeded':
|
|
644
|
+
return 'ok';
|
|
645
|
+
case 'points-lines-prepare-failed':
|
|
646
|
+
return 'ok';
|
|
623
647
|
}
|
|
624
648
|
}
|
|
625
649
|
expect(exhaustive('hdrp-deferred-caps-insufficient')).toBe('ok');
|
|
@@ -2232,10 +2232,11 @@ function spawnValidatedLight(
|
|
|
2232
2232
|
//
|
|
2233
2233
|
// visibility flags mirror the WebGPU GPUShaderStage bitmask:
|
|
2234
2234
|
// VERTEX = 0x1, FRAGMENT = 0x2.
|
|
2235
|
+
const VISIBILITY_VERTEX = 0x1;
|
|
2235
2236
|
const VISIBILITY_FRAGMENT = 0x2;
|
|
2236
2237
|
const VISIBILITY_VERTEX_FRAGMENT = 0x1 | 0x2;
|
|
2237
2238
|
|
|
2238
|
-
describe('binding 8 = spotShadowMap (D-5 always-on
|
|
2239
|
+
describe('binding 8 = spotShadowMap (D-5 always-on shadow entry)', () => {
|
|
2239
2240
|
it('declares binding 8 as a depth 2D texture, FRAGMENT-only', () => {
|
|
2240
2241
|
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2241
2242
|
const b8 = entries.find((e) => e.binding === 8);
|
|
@@ -2253,10 +2254,12 @@ function spawnValidatedLight(
|
|
|
2253
2254
|
expect(noStorage.some((e) => e.binding === 8)).toBe(true);
|
|
2254
2255
|
});
|
|
2255
2256
|
|
|
2256
|
-
it('binding
|
|
2257
|
+
it('reserves binding 9 and keeps Points/Lines at binding 10', () => {
|
|
2257
2258
|
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2258
2259
|
expect(entries.some((e) => e.binding === 9)).toBe(false);
|
|
2259
|
-
|
|
2260
|
+
const pointsLines = entries.find((e) => e.binding === 10);
|
|
2261
|
+
expect(pointsLines?.buffer?.type).toBe('uniform');
|
|
2262
|
+
expect(pointsLines?.visibility).toBe(VISIBILITY_VERTEX);
|
|
2260
2263
|
});
|
|
2261
2264
|
});
|
|
2262
2265
|
|
|
@@ -2272,18 +2275,19 @@ function spawnValidatedLight(
|
|
|
2272
2275
|
// UBO (binding 0). This block locks that the spot matrices add ZERO new view
|
|
2273
2276
|
// BGL buffer bindings (the WebGL2 budget invariant).
|
|
2274
2277
|
describe('spotLightViewProj folded into View UBO (binding 0), not a new binding', () => {
|
|
2275
|
-
it('view BGL
|
|
2278
|
+
it('view BGL includes only the vertex Points/Lines UBO beyond binding 7', () => {
|
|
2276
2279
|
// Enumerate uniform-buffer entries on the view BGL. After the w25 fold,
|
|
2277
|
-
//
|
|
2278
|
-
//
|
|
2279
|
-
// (shadowCasterCascade)
|
|
2280
|
+
// uniform buffers are binding 0 (View UBO, carries the spot matrices
|
|
2281
|
+
// in its tail), binding 6 (point shadowParams), binding 7
|
|
2282
|
+
// (shadowCasterCascade), and binding 10 (Points/Lines view). No
|
|
2283
|
+
// standalone spot-matrix uniform buffer exists.
|
|
2280
2284
|
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2281
2285
|
const uniformBufferBindings = entries
|
|
2282
2286
|
.filter((e) => e.buffer?.type === 'uniform')
|
|
2283
2287
|
.map((e) => e.binding)
|
|
2284
2288
|
.sort((a, b) => a - b);
|
|
2285
2289
|
// storageBuffer=true: bindings 1+2 are read-only-storage (not uniform).
|
|
2286
|
-
expect(uniformBufferBindings).toEqual([0, 6, 7]);
|
|
2290
|
+
expect(uniformBufferBindings).toEqual([0, 6, 7, 10]);
|
|
2287
2291
|
});
|
|
2288
2292
|
|
|
2289
2293
|
it('storageBuffer=false: WebGL2 fallback uniform-buffer bindings stay within budget', () => {
|
|
@@ -2348,12 +2352,12 @@ function spawnValidatedLight(
|
|
|
2348
2352
|
});
|
|
2349
2353
|
});
|
|
2350
2354
|
|
|
2351
|
-
describe('full binding roster
|
|
2352
|
-
it('exposes
|
|
2355
|
+
describe('full binding roster reserves 9 for Points/Lines at 10', () => {
|
|
2356
|
+
it('exposes bindings 0..8 plus binding 10, with binding 9 reserved', () => {
|
|
2353
2357
|
const bindings = buildPbrViewBglEntries({ storageBuffer: true })
|
|
2354
2358
|
.map((e) => e.binding)
|
|
2355
2359
|
.sort((a, b) => a - b);
|
|
2356
|
-
expect(bindings).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
|
2360
|
+
expect(bindings).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 10]);
|
|
2357
2361
|
});
|
|
2358
2362
|
});
|
|
2359
2363
|
});
|
|
@@ -44,7 +44,7 @@ function makeSpec(): PipelineSpec {
|
|
|
44
44
|
|
|
45
45
|
describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', () => {
|
|
46
46
|
describe('pbr-view', () => {
|
|
47
|
-
it('storageBuffer=true:
|
|
47
|
+
it('storageBuffer=true: 10 entries with read-only-storage on bindings 1+2', () => {
|
|
48
48
|
const spec = makeSpec();
|
|
49
49
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
50
50
|
kind: 'pbr-view',
|
|
@@ -56,12 +56,15 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
56
56
|
};
|
|
57
57
|
expect(out).toEqual(expected);
|
|
58
58
|
// feat-20260625-spot-light-shadow-mapping: binding 8 (spot shadow atlas,
|
|
59
|
-
// w14) raised the view BGL to 9 entries (always-on, D-5).
|
|
59
|
+
// w14) raised the view BGL to 9 entries (always-on, D-5). The
|
|
60
|
+
// Points/Lines viewport UBO occupies binding 10, while w25 (scope-amend
|
|
60
61
|
// webkit-fallback) folded the former binding 9 spotLightViewProj matrices
|
|
61
62
|
// UBO into the View UBO (`view.spotLightViewProj`), so the BGL stays at 9
|
|
62
|
-
// entries (bindings 0..8) — keeping the WebGL2 fallback fragment uniform-
|
|
63
|
+
// entries (bindings 0..8 plus binding 10) — keeping the WebGL2 fallback fragment uniform-
|
|
63
64
|
// buffer count <= 11 (GLES 3.0).
|
|
64
|
-
expect(out.entries.length).toBe(
|
|
65
|
+
expect(out.entries.length).toBe(10);
|
|
66
|
+
expect(out.entries.find((entry) => entry.binding === 9)).toBeUndefined();
|
|
67
|
+
expect(out.entries.find((entry) => entry.binding === 10)?.buffer?.type).toBe('uniform');
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
it('storageBuffer=false: bindings 1+2 fall back to uniform', () => {
|
|
@@ -84,6 +84,18 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
84
84
|
return code;
|
|
85
85
|
case 'render-feature-draw-recording-failed':
|
|
86
86
|
return code;
|
|
87
|
+
case 'points-lines-invalid-style':
|
|
88
|
+
return code;
|
|
89
|
+
case 'points-lines-topology-mismatch':
|
|
90
|
+
return code;
|
|
91
|
+
case 'points-lines-style-unsupported':
|
|
92
|
+
return code;
|
|
93
|
+
case 'points-lines-material-unsupported':
|
|
94
|
+
return code;
|
|
95
|
+
case 'points-lines-budget-exceeded':
|
|
96
|
+
return code;
|
|
97
|
+
case 'points-lines-prepare-failed':
|
|
98
|
+
return code;
|
|
87
99
|
default: {
|
|
88
100
|
const exhaustive: never = code;
|
|
89
101
|
return exhaustive;
|
|
@@ -240,6 +252,30 @@ function narrowRenderError(err: RenderError): void {
|
|
|
240
252
|
void err.detail.authoredValue;
|
|
241
253
|
void err.detail.projected;
|
|
242
254
|
break;
|
|
255
|
+
case 'points-lines-invalid-style':
|
|
256
|
+
void err.detail.component;
|
|
257
|
+
void err.detail.field;
|
|
258
|
+
break;
|
|
259
|
+
case 'points-lines-topology-mismatch':
|
|
260
|
+
void err.detail.submesh;
|
|
261
|
+
void err.detail.actual;
|
|
262
|
+
break;
|
|
263
|
+
case 'points-lines-style-unsupported':
|
|
264
|
+
void err.detail.member;
|
|
265
|
+
void err.detail.supported;
|
|
266
|
+
break;
|
|
267
|
+
case 'points-lines-material-unsupported':
|
|
268
|
+
void err.detail.material;
|
|
269
|
+
void err.detail.pass;
|
|
270
|
+
break;
|
|
271
|
+
case 'points-lines-budget-exceeded':
|
|
272
|
+
void err.detail.requested;
|
|
273
|
+
void err.detail.limit;
|
|
274
|
+
break;
|
|
275
|
+
case 'points-lines-prepare-failed':
|
|
276
|
+
void err.detail.owner;
|
|
277
|
+
void err.detail.generation;
|
|
278
|
+
break;
|
|
243
279
|
default: {
|
|
244
280
|
const exhaustive: never = err;
|
|
245
281
|
void exhaustive;
|
|
@@ -45,6 +45,10 @@ describe('prepared graphics browser contract', () => {
|
|
|
45
45
|
canvas.remove();
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
// This negative path intentionally exercises a real WebGPU pipeline before
|
|
49
|
+
// the renderer reports the structured stage error. Its cold lavapipe start
|
|
50
|
+
// has exceeded Vitest's 15s default in CI; keep the assertion bounded at the
|
|
51
|
+
// owner rather than weakening the global browser timeout.
|
|
48
52
|
it('reports forged prepared state through subscribe without renderer diagnostics getters', async () => {
|
|
49
53
|
if (typeof navigator?.gpu?.requestAdapter !== 'function') {
|
|
50
54
|
throw new Error('WebGPU is unavailable');
|
|
@@ -80,5 +84,5 @@ describe('prepared graphics browser contract', () => {
|
|
|
80
84
|
});
|
|
81
85
|
expect(renderer).not.toHaveProperty('renderFeatureDiagnostics');
|
|
82
86
|
canvas.remove();
|
|
83
|
-
});
|
|
87
|
+
}, 30_000);
|
|
84
88
|
});
|
|
@@ -49,6 +49,8 @@ describe('subscribe projects the discriminated Renderer event contract', () => {
|
|
|
49
49
|
return `${event.previous}->${event.current}`;
|
|
50
50
|
case 'error':
|
|
51
51
|
return event.error.code;
|
|
52
|
+
case 'frame-submitted':
|
|
53
|
+
return `frame:${event.frameId}@${event.deviceGeneration}`;
|
|
52
54
|
}
|
|
53
55
|
};
|
|
54
56
|
expectTypeOf(describeEvent).returns.toEqualTypeOf<string>();
|
|
@@ -17,6 +17,12 @@ function frameRequest(lease: Parameters<Renderer['draw']>[0]['leases'][number])
|
|
|
17
17
|
describe('RhiNull command flow', () => {
|
|
18
18
|
it('returns the synchronous receipt after the host submits a frame', async () => {
|
|
19
19
|
const renderer = await requireRenderer(canvas(), { rhi }, { shaderManifestUrl: manifest });
|
|
20
|
+
const events: Array<{
|
|
21
|
+
readonly kind: string;
|
|
22
|
+
readonly frameId?: number;
|
|
23
|
+
readonly deviceGeneration?: number;
|
|
24
|
+
}> = [];
|
|
25
|
+
const unsubscribe = renderer.subscribe((event) => events.push(event));
|
|
20
26
|
const world = new World();
|
|
21
27
|
const attached = renderer.attach(world);
|
|
22
28
|
expect(attached.ok).toBe(true);
|
|
@@ -26,9 +32,17 @@ describe('RhiNull command flow', () => {
|
|
|
26
32
|
const frame = renderer.draw(frameRequest(attached.value));
|
|
27
33
|
expect(frame.ok).toBe(true);
|
|
28
34
|
if (!frame.ok) return;
|
|
35
|
+
expect(events.filter((event) => event.kind === 'frame-submitted')).toEqual([
|
|
36
|
+
{
|
|
37
|
+
kind: 'frame-submitted',
|
|
38
|
+
frameId: frame.value.frameId,
|
|
39
|
+
deviceGeneration: frame.value.deviceGeneration,
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
29
42
|
const completed = await frame.value.completed;
|
|
30
43
|
expect(completed.ok).toBe(true);
|
|
31
44
|
expect(renderer.inspect().frame.frameId).toBe(frame.value.frameId);
|
|
45
|
+
unsubscribe();
|
|
32
46
|
renderer.dispose();
|
|
33
47
|
});
|
|
34
48
|
|
|
@@ -47,7 +47,7 @@ function makeSpec(shaderId: string): PipelineSpec {
|
|
|
47
47
|
|
|
48
48
|
describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
49
49
|
describe('pbr-view BGL (light buffers; binding 1+2)', () => {
|
|
50
|
-
it('sprite-lit vs sprite share the same
|
|
50
|
+
it('sprite-lit vs sprite share the same 10-entry pbr-view BGL under storage-buffer caps', () => {
|
|
51
51
|
const spriteSpec = makeSpec('forgeax::sprite');
|
|
52
52
|
const spriteLitSpec = makeSpec('forgeax::sprite-lit');
|
|
53
53
|
const sprite = buildBindGroupLayoutDescriptor(spriteSpec, {
|
|
@@ -59,10 +59,10 @@ describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
|
59
59
|
caps: { storageBuffer: true },
|
|
60
60
|
});
|
|
61
61
|
expect(JSON.stringify(spriteLit)).toBe(JSON.stringify(sprite));
|
|
62
|
-
expect(spriteLit.entries.length).toBe(
|
|
62
|
+
expect(spriteLit.entries.length).toBe(10); // binding 10 is the shared Points/Lines view UBO
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it('sprite-lit vs sprite share the same
|
|
65
|
+
it('sprite-lit vs sprite share the same 10-entry pbr-view BGL under uniform fallback caps (AC-10)', () => {
|
|
66
66
|
const spriteSpec = makeSpec('forgeax::sprite');
|
|
67
67
|
const spriteLitSpec = makeSpec('forgeax::sprite-lit');
|
|
68
68
|
const sprite = buildBindGroupLayoutDescriptor(spriteSpec, {
|
|
@@ -778,6 +778,10 @@ describe('post-process bindgroup identity cache (issue #670)', () => {
|
|
|
778
778
|
views.bloomBlurV,
|
|
779
779
|
);
|
|
780
780
|
expect(bindGroupCreates[5]?.entries[0]?.resource.value).toBe(views.cubemap);
|
|
781
|
+
expect(bindGroupCreates[5]?.entries[2]?.resource.value).toEqual({
|
|
782
|
+
buffer: ctx.pipelineState.viewUniformBuffer,
|
|
783
|
+
size: 960,
|
|
784
|
+
});
|
|
781
785
|
|
|
782
786
|
recordAll();
|
|
783
787
|
expect(bindGroupCreates).toHaveLength(6);
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
// proven to be a mount entity, components is left undefined (one-time
|
|
45
45
|
// normalization on first reload; fixed-point from second collect onward).
|
|
46
46
|
|
|
47
|
-
import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
48
47
|
import {
|
|
48
|
+
builtinMeshGuid,
|
|
49
49
|
resolveAssetHandle,
|
|
50
50
|
SceneCollectAssetGuidUnresolvedError,
|
|
51
51
|
SceneCollectEntityRefOutOfClosureError,
|
|
@@ -78,6 +78,11 @@ import type {
|
|
|
78
78
|
} from '@forgeax/engine-types';
|
|
79
79
|
import { foldMountOverrides } from './scene-utils/mount-override-fold';
|
|
80
80
|
|
|
81
|
+
/** The collector needs identity lookup, not an authoring registry implementation. */
|
|
82
|
+
export interface SceneAssetGuidLookup {
|
|
83
|
+
guidOf(asset: Asset): string | undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
// Shared helpers
|
|
82
87
|
function _isArrayLike(value: unknown): value is ArrayLike<unknown> {
|
|
83
88
|
// SceneAsset values are ordinary JSON arrays at the boundary. Use the
|
|
@@ -116,14 +121,16 @@ function classifyFieldSchema(fieldType: string | undefined): SchemaFieldClass |
|
|
|
116
121
|
// lives in exactly one place; `field` names the failing field in the error.
|
|
117
122
|
function _handleToGuid(
|
|
118
123
|
world: World,
|
|
119
|
-
registry:
|
|
124
|
+
registry: SceneAssetGuidLookup,
|
|
120
125
|
handle: number,
|
|
121
126
|
field: string,
|
|
122
127
|
): Result<string | undefined, SceneCollectAssetGuidUnresolvedError> {
|
|
123
128
|
if (handle === 0) return ok(undefined); // NULL sentinel
|
|
129
|
+
const builtinGuid = builtinMeshGuid(handle as unknown as Handle<string, 'shared'>);
|
|
130
|
+
if (builtinGuid !== undefined) return ok(builtinGuid);
|
|
124
131
|
const assetRes = resolveAssetHandle(world, handle as unknown as Handle<string, 'shared'>);
|
|
125
132
|
if (!assetRes.ok) return err(new SceneCollectAssetGuidUnresolvedError(field, handle));
|
|
126
|
-
const guid = registry.
|
|
133
|
+
const guid = registry.guidOf(assetRes.value as Asset);
|
|
127
134
|
if (guid === undefined) return err(new SceneCollectAssetGuidUnresolvedError(field, handle));
|
|
128
135
|
return ok(guid);
|
|
129
136
|
}
|
|
@@ -135,7 +142,7 @@ function _handleToGuid(
|
|
|
135
142
|
// values pass through untouched.
|
|
136
143
|
function _serializeSharedFieldValue(
|
|
137
144
|
world: World,
|
|
138
|
-
registry:
|
|
145
|
+
registry: SceneAssetGuidLookup,
|
|
139
146
|
classification: SchemaFieldClass,
|
|
140
147
|
value: unknown,
|
|
141
148
|
field: string,
|
|
@@ -166,7 +173,7 @@ function _serializeSharedFieldValue(
|
|
|
166
173
|
// override IS that field so it cannot be omitted). Non-shared fields pass through.
|
|
167
174
|
function _serializeOverrideValueHandles(
|
|
168
175
|
world: World,
|
|
169
|
-
registry:
|
|
176
|
+
registry: SceneAssetGuidLookup,
|
|
170
177
|
ov: MountOverride,
|
|
171
178
|
): Result<unknown, SceneCollectAssetGuidUnresolvedError> {
|
|
172
179
|
const comp = world.components.resolve(ov.comp);
|
|
@@ -253,7 +260,7 @@ export function serializeSceneAssetToPack(
|
|
|
253
260
|
* filter (not subtree pruning): graft entities under members survive as owned.
|
|
254
261
|
*/
|
|
255
262
|
export function rootsToSceneAsset(
|
|
256
|
-
registry:
|
|
263
|
+
registry: SceneAssetGuidLookup,
|
|
257
264
|
world: World,
|
|
258
265
|
roots: EntityHandle[],
|
|
259
266
|
): Result<
|
|
@@ -415,7 +422,7 @@ export function rootsToSceneAsset(
|
|
|
415
422
|
sh.value as unknown as number,
|
|
416
423
|
),
|
|
417
424
|
);
|
|
418
|
-
const g = registry.
|
|
425
|
+
const g = registry.guidOf(pr.value as Asset);
|
|
419
426
|
if (g === undefined)
|
|
420
427
|
return err(
|
|
421
428
|
new SceneCollectAssetGuidUnresolvedError(
|
package/src/index.ts
CHANGED
|
@@ -326,7 +326,11 @@ export { quat } from '@forgeax/engine-math';
|
|
|
326
326
|
// collectSceneAsset removed — replaced by rootsToSceneAsset (forest entry,
|
|
327
327
|
// schema-derived field dispatch, engine-self-contained GUID resolution).
|
|
328
328
|
// serializeSceneAssetToPack now uses schema-derived refs[] index (D-1/D-2).
|
|
329
|
-
export {
|
|
329
|
+
export {
|
|
330
|
+
rootsToSceneAsset,
|
|
331
|
+
type SceneAssetGuidLookup,
|
|
332
|
+
serializeSceneAssetToPack,
|
|
333
|
+
} from './collect-scene-asset';
|
|
330
334
|
// feat-20260626 M6 / m6-4: debug-draw auto-attach glue is re-exported from the
|
|
331
335
|
// main barrel (was a separate tsup entry). The separate entry produced a SECOND
|
|
332
336
|
// module copy of the mutable `registeredDebugDraw` registry: createApp set it on
|