@forgeax/engine-app 0.1.4 → 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 +20 -14
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/create-app-error-retention.integration.test.d.ts +2 -0
- package/dist/__tests__/create-app-error-retention.integration.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts +2 -0
- package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts.map +1 -0
- package/dist/animation-asset-lookup.d.ts +1 -2
- package/dist/animation-asset-lookup.d.ts.map +1 -1
- package/dist/browser-frame-signal.d.ts +17 -0
- package/dist/browser-frame-signal.d.ts.map +1 -0
- package/dist/create-app.d.ts +31 -10
- package/dist/create-app.d.ts.map +1 -1
- package/dist/engine-worker-runtime.mjs +72 -129
- package/dist/engine-worker-runtime.mjs.map +1 -1
- package/dist/errors.d.ts +3 -12
- package/dist/errors.d.ts.map +1 -1
- package/dist/execution/attached-world-swap.d.ts +1 -12
- package/dist/execution/attached-world-swap.d.ts.map +1 -1
- package/dist/execution/bootstrap-url.d.ts +5 -0
- package/dist/execution/bootstrap-url.d.ts.map +1 -0
- package/dist/execution/control.d.ts +0 -2
- package/dist/execution/control.d.ts.map +1 -1
- package/dist/execution/engine-worker.d.ts +1 -0
- package/dist/execution/engine-worker.d.ts.map +1 -1
- package/dist/execution/host-controller.d.ts.map +1 -1
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/protocol.d.ts +4 -1
- package/dist/execution/protocol.d.ts.map +1 -1
- package/dist/execution/types.d.ts +17 -1
- package/dist/execution/types.d.ts.map +1 -1
- package/dist/game-context.d.ts +1 -1
- package/dist/game-context.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +357 -359
- package/dist/index.mjs.map +1 -1
- package/dist/internal/assembled-engine-profile.d.ts.map +1 -1
- package/dist/internal/assets-world-plugin.d.ts +3 -17
- package/dist/internal/assets-world-plugin.d.ts.map +1 -1
- package/dist/internal/browser-remote-bridge.d.ts +1 -2
- package/dist/internal/browser-remote-bridge.d.ts.map +1 -1
- package/dist/internal/browser-rhi-debug-runtime.d.ts +1 -1
- package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -1
- package/dist/internal/debug-draw.d.ts +9 -0
- package/dist/internal/debug-draw.d.ts.map +1 -0
- package/dist/internal/engine-profile-common.d.ts +2 -2
- package/dist/internal/engine-profile-common.d.ts.map +1 -1
- package/dist/internal/frame-loop.d.ts +7 -4
- package/dist/internal/frame-loop.d.ts.map +1 -1
- package/dist/internal/main-engine-profile.d.ts +4 -0
- package/dist/internal/main-engine-profile.d.ts.map +1 -1
- package/dist/internal/remote-serve-flag.d.ts +3 -7
- package/dist/internal/remote-serve-flag.d.ts.map +1 -1
- package/dist/internal/rhi-capture.d.ts +1 -1
- package/dist/internal/rhi-capture.d.ts.map +1 -1
- package/dist/internal/worker-engine-profile.d.ts +2 -0
- package/dist/internal/worker-engine-profile.d.ts.map +1 -1
- package/dist/renderer-plugin.d.ts +25 -0
- package/dist/renderer-plugin.d.ts.map +1 -0
- package/dist/tool-preview/bootstrap.d.ts.map +1 -1
- package/dist/types.d.ts +21 -61
- package/dist/types.d.ts.map +1 -1
- package/package.json +25 -26
- package/src/__tests__/callback-inference.test-d.ts +7 -3
- package/src/__tests__/consumer-migration.unit.test.ts +13 -0
- package/src/__tests__/create-app-error-retention.integration.test.ts +125 -0
- package/src/__tests__/create-app-features.test.ts +21 -10
- package/src/__tests__/create-app-listener-sync.test.ts +23 -19
- package/src/__tests__/create-app-physics-plugin-failure.integration.test.ts +142 -0
- package/src/__tests__/create-app-stop.test.ts +4 -12
- package/src/__tests__/create-app.test-d.ts +12 -11
- package/src/__tests__/create-app.test.ts +1 -60
- package/src/__tests__/create-renderer-error.integration.test.ts +3 -2
- package/src/__tests__/create-renderer-lifecycle.integration.test.ts +10 -5
- package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +7 -13
- package/src/__tests__/draw-source-single-world-regression.test.ts +23 -24
- package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +8 -0
- package/src/__tests__/engine-worker-world-swap.unit.test.ts +15 -22
- package/src/__tests__/errors-pointer-lock-failed.test.ts +4 -7
- package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +5 -6
- package/src/__tests__/execution-frame-order.unit.test.ts +2 -2
- package/src/__tests__/execution-frame-protocol.unit.test.ts +3 -4
- package/src/__tests__/execution-public-api.test-d.ts +4 -0
- package/src/__tests__/frame-loop-time-elapsed.test.ts +1 -1
- package/src/__tests__/frame-loop-time.unit.test.ts +1 -1
- package/src/__tests__/frame-loop-world-array.test.ts +82 -73
- package/src/__tests__/plugin-profiles.unit.test.ts +3 -3
- package/src/__tests__/profiler-default-off.test.ts +1 -1
- package/src/__tests__/profiler-frame-token.test.ts +17 -30
- package/src/__tests__/profiler-phase-integration.test.ts +15 -27
- package/src/__tests__/surface-handoff.test.ts +1 -4
- package/src/__tests__/visibility-remote.integration.test.ts +2 -5
- package/src/__tests__/worker-execution-stop.test.ts +64 -7
- package/src/animation-asset-lookup.ts +2 -2
- package/src/browser-frame-signal.ts +44 -0
- package/src/create-app.ts +208 -295
- package/src/errors.ts +27 -39
- package/src/execution/attached-world-swap.ts +4 -38
- package/src/execution/bootstrap-url.ts +25 -0
- package/src/execution/control.ts +3 -7
- package/src/execution/engine-worker-runtime.ts +23 -40
- package/src/execution/engine-worker.ts +2 -0
- package/src/execution/host-controller.ts +8 -2
- package/src/execution/index.ts +1 -0
- package/src/execution/protocol.ts +4 -1
- package/src/execution/types.ts +18 -1
- package/src/game-context.ts +1 -1
- package/src/index.ts +18 -4
- package/src/internal/assembled-engine-profile.ts +3 -11
- package/src/internal/assets-world-plugin.ts +7 -53
- package/src/internal/browser-remote-bridge.ts +2 -3
- package/src/internal/browser-rhi-debug-runtime.ts +1 -1
- package/src/internal/debug-draw.ts +38 -0
- package/src/internal/engine-profile-common.ts +2 -3
- package/src/internal/frame-loop.ts +77 -63
- package/src/internal/main-engine-profile.ts +21 -17
- package/src/internal/remote-serve-flag.ts +5 -14
- package/src/internal/rhi-capture.ts +1 -1
- package/src/internal/worker-engine-profile.ts +10 -10
- package/src/renderer-plugin.ts +96 -0
- package/src/tool-preview/bootstrap.ts +1 -14
- package/src/types.ts +22 -77
- package/src/vite-env.d.ts +0 -2
- package/dist/__tests__/app-rebuild.test.d.ts +0 -2
- package/dist/__tests__/app-rebuild.test.d.ts.map +0 -1
- package/dist/__tests__/asset-registry-types.test-d.d.ts +0 -2
- package/dist/__tests__/asset-registry-types.test-d.d.ts.map +0 -1
- package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts +0 -2
- package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts.map +0 -1
- package/dist/__tests__/draw-source-headless-full-chain.test.d.ts +0 -2
- package/dist/__tests__/draw-source-headless-full-chain.test.d.ts.map +0 -1
- package/dist/__tests__/ecs-import.unit.test.d.ts +0 -2
- package/dist/__tests__/ecs-import.unit.test.d.ts.map +0 -1
- package/dist/__tests__/execution-world-health-owner.test-d.d.ts +0 -2
- package/dist/__tests__/execution-world-health-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/remote-serve-flag.test.d.ts +0 -2
- package/dist/__tests__/remote-serve-flag.test.d.ts.map +0 -1
- package/dist/assets-runtime-assembly.d.ts +0 -19
- package/dist/assets-runtime-assembly.d.ts.map +0 -1
- package/src/__tests__/app-rebuild.test.ts +0 -31
- package/src/__tests__/asset-registry-types.test-d.ts +0 -16
- package/src/__tests__/assets-runtime-assembly.integration.test.ts +0 -68
- package/src/__tests__/draw-source-headless-full-chain.test.ts +0 -226
- package/src/__tests__/ecs-import.unit.test.ts +0 -40
- package/src/__tests__/execution-world-health-owner.test-d.ts +0 -9
- package/src/__tests__/remote-serve-flag.test.ts +0 -24
- package/src/assets-runtime-assembly.ts +0 -70
|
@@ -28,24 +28,21 @@
|
|
|
28
28
|
// seam insertion point)
|
|
29
29
|
|
|
30
30
|
import { World } from '@forgeax/engine-ecs';
|
|
31
|
-
import
|
|
31
|
+
import { createRenderReadLease } from '@forgeax/engine-ecs/projection';
|
|
32
|
+
import type { Renderer, RenderFrameInput } from '@forgeax/engine-render';
|
|
32
33
|
import { describe, expect, it } from 'vitest';
|
|
33
34
|
import { createFrameLoop, type FrameLoopOptions } from '../internal/frame-loop';
|
|
34
35
|
|
|
35
36
|
interface DrawCall {
|
|
36
|
-
readonly
|
|
37
|
-
readonly options: unknown;
|
|
37
|
+
readonly request: RenderFrameInput;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function makeSpyRenderer(): { renderer: Renderer; calls: DrawCall[] } {
|
|
41
41
|
const calls: DrawCall[] = [];
|
|
42
42
|
const renderer = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
detachWorld: () => {},
|
|
47
|
-
draw(worlds: unknown, options: unknown): { ok: true; value: undefined } {
|
|
48
|
-
calls.push({ worlds, options });
|
|
43
|
+
attach: (world: World) => ({ ok: true, value: createRenderReadLease(world) }),
|
|
44
|
+
draw(request: RenderFrameInput): { ok: true; value: undefined } {
|
|
45
|
+
calls.push({ request });
|
|
49
46
|
return { ok: true, value: undefined };
|
|
50
47
|
},
|
|
51
48
|
onError(): () => void {
|
|
@@ -119,11 +116,10 @@ describe('drawSource seam degrades to single-world path (w8, AC-03 regression)',
|
|
|
119
116
|
|
|
120
117
|
expect(calls.length).toBeGreaterThanOrEqual(3);
|
|
121
118
|
for (const call of calls) {
|
|
122
|
-
expect(
|
|
123
|
-
|
|
124
|
-
expect(
|
|
125
|
-
expect(
|
|
126
|
-
expect(call.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
|
|
119
|
+
expect(call.request.leases).toHaveLength(1);
|
|
120
|
+
expect(call.request.leases[0]?.worldIdentity).toBe(world.identity);
|
|
121
|
+
expect(call.request.camera.lease).toBe(call.request.leases[0]);
|
|
122
|
+
expect(call.request.environment.lease).toBe(call.request.leases[0]);
|
|
127
123
|
}
|
|
128
124
|
|
|
129
125
|
loop.stop();
|
|
@@ -150,11 +146,10 @@ describe('drawSource seam degrades to single-world path (w8, AC-03 regression)',
|
|
|
150
146
|
expect(pulls).toBeGreaterThanOrEqual(3);
|
|
151
147
|
expect(calls.length).toBeGreaterThanOrEqual(3);
|
|
152
148
|
for (const call of calls) {
|
|
153
|
-
expect(
|
|
154
|
-
|
|
155
|
-
expect(
|
|
156
|
-
expect(
|
|
157
|
-
expect(call.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
|
|
149
|
+
expect(call.request.leases).toHaveLength(1);
|
|
150
|
+
expect(call.request.leases[0]?.worldIdentity).toBe(world.identity);
|
|
151
|
+
expect(call.request.camera.lease).toBe(call.request.leases[0]);
|
|
152
|
+
expect(call.request.environment.lease).toBe(call.request.leases[0]);
|
|
158
153
|
}
|
|
159
154
|
|
|
160
155
|
loop.stop();
|
|
@@ -175,11 +170,15 @@ describe('drawSource seam degrades to single-world path (w8, AC-03 regression)',
|
|
|
175
170
|
pump(1);
|
|
176
171
|
|
|
177
172
|
expect(calls).toHaveLength(3);
|
|
178
|
-
expect(calls[0]?.
|
|
179
|
-
expect(calls[
|
|
180
|
-
expect(calls[1]?.
|
|
181
|
-
expect(calls[
|
|
182
|
-
expect(calls[
|
|
173
|
+
expect(calls[0]?.request.leases).toHaveLength(1);
|
|
174
|
+
expect(calls[0]?.request.leases[0]?.worldIdentity).toBe(world.identity);
|
|
175
|
+
expect(calls[1]?.request.leases).toHaveLength(2);
|
|
176
|
+
expect(calls[1]?.request.leases[0]?.worldIdentity).toBe(world.identity);
|
|
177
|
+
expect(calls[1]?.request.leases[1]?.worldIdentity).toBe(overlay.identity);
|
|
178
|
+
expect(calls[1]?.request.camera.lease).toBe(calls[1]?.request.leases[0]);
|
|
179
|
+
expect(calls[1]?.request.environment.lease).toBe(calls[1]?.request.leases[0]);
|
|
180
|
+
expect(calls[2]?.request.leases).toHaveLength(1);
|
|
181
|
+
expect(calls[2]?.request.leases[0]?.worldIdentity).toBe(world.identity);
|
|
183
182
|
loop.stop();
|
|
184
183
|
});
|
|
185
184
|
});
|
|
@@ -38,6 +38,10 @@ describe('Engine Worker thick bootstrap channel', () => {
|
|
|
38
38
|
bootstrapUrl: 'https://example.test/bootstrap.js',
|
|
39
39
|
bootstrapData: { gameId: 'sample' },
|
|
40
40
|
bootstrapPort: channel.port2,
|
|
41
|
+
assetCatalog: {
|
|
42
|
+
url: '/__pack/scopes/sample/7/catalog.json',
|
|
43
|
+
expectedScope: { scopeId: 'sample', generation: 7 },
|
|
44
|
+
},
|
|
41
45
|
timeoutMs: 100,
|
|
42
46
|
tier: 'engine-worker',
|
|
43
47
|
workerFactory: () => worker as unknown as Worker,
|
|
@@ -49,6 +53,10 @@ describe('Engine Worker thick bootstrap channel', () => {
|
|
|
49
53
|
kind: 'init',
|
|
50
54
|
bootstrapData: { gameId: 'sample' },
|
|
51
55
|
bootstrapPort: channel.port2,
|
|
56
|
+
assetCatalog: {
|
|
57
|
+
url: '/__pack/scopes/sample/7/catalog.json',
|
|
58
|
+
expectedScope: { scopeId: 'sample', generation: 7 },
|
|
59
|
+
},
|
|
52
60
|
});
|
|
53
61
|
expect(worker.posts[0]?.transfer).toEqual([offscreen, channel.port2]);
|
|
54
62
|
started.ok && started.value.dispose();
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { World } from '@forgeax/engine-ecs';
|
|
2
|
-
import type { Renderer } from '@forgeax/engine-render';
|
|
2
|
+
import type { Renderer, RenderWorldLease } from '@forgeax/engine-render';
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { commitAttachedWorld, SerializedRebuildQueue } from '../execution/attached-world-swap';
|
|
5
5
|
|
|
6
|
-
function rendererOwner(): Pick<Renderer, '
|
|
6
|
+
function rendererOwner(): Pick<Renderer, 'attach'> {
|
|
7
|
+
const lease = {
|
|
8
|
+
worldIdentity: {},
|
|
9
|
+
generation: 0,
|
|
10
|
+
readChanges: vi.fn(),
|
|
11
|
+
querySpans: vi.fn(),
|
|
12
|
+
inspectCursor: vi.fn(),
|
|
13
|
+
dispose: vi.fn(),
|
|
14
|
+
} as unknown as RenderWorldLease;
|
|
7
15
|
return {
|
|
8
|
-
|
|
9
|
-
detachWorld: vi.fn(),
|
|
16
|
+
attach: vi.fn(() => ({ ok: true as const, value: lease })),
|
|
10
17
|
};
|
|
11
18
|
}
|
|
12
19
|
|
|
@@ -36,41 +43,27 @@ describe('worker attached World swap', () => {
|
|
|
36
43
|
|
|
37
44
|
it('publishes the candidate before detaching the previous World', async () => {
|
|
38
45
|
const renderer = rendererOwner();
|
|
39
|
-
const previous = new World();
|
|
40
46
|
const next = new World();
|
|
41
47
|
|
|
42
|
-
await expect(commitAttachedWorld(renderer,
|
|
43
|
-
|
|
44
|
-
);
|
|
45
|
-
expect(renderer.attachWorld).toHaveBeenCalledWith(next);
|
|
46
|
-
expect(renderer.detachWorld).toHaveBeenCalledOnce();
|
|
47
|
-
expect(renderer.detachWorld).toHaveBeenCalledWith(previous);
|
|
48
|
+
await expect(commitAttachedWorld(renderer, next, async () => true)).resolves.toBe(true);
|
|
49
|
+
expect(renderer.attach).toHaveBeenCalledWith(next);
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
it('detaches a candidate whose bootstrap fails and keeps the previous World', async () => {
|
|
51
53
|
const renderer = rendererOwner();
|
|
52
|
-
const previous = new World();
|
|
53
54
|
const next = new World();
|
|
54
55
|
|
|
55
|
-
await expect(commitAttachedWorld(renderer,
|
|
56
|
-
false,
|
|
57
|
-
);
|
|
58
|
-
expect(renderer.detachWorld).toHaveBeenCalledOnce();
|
|
59
|
-
expect(renderer.detachWorld).toHaveBeenCalledWith(next);
|
|
60
|
-
expect(renderer.detachWorld).not.toHaveBeenCalledWith(previous);
|
|
56
|
+
await expect(commitAttachedWorld(renderer, next, async () => false)).resolves.toBe(false);
|
|
61
57
|
});
|
|
62
58
|
|
|
63
59
|
it('detaches a candidate whose bootstrap throws', async () => {
|
|
64
60
|
const renderer = rendererOwner();
|
|
65
|
-
const previous = new World();
|
|
66
61
|
const next = new World();
|
|
67
62
|
|
|
68
63
|
await expect(
|
|
69
|
-
commitAttachedWorld(renderer,
|
|
64
|
+
commitAttachedWorld(renderer, next, async () => {
|
|
70
65
|
throw new Error('bootstrap failed');
|
|
71
66
|
}),
|
|
72
67
|
).rejects.toThrow('bootstrap failed');
|
|
73
|
-
expect(renderer.detachWorld).toHaveBeenCalledOnce();
|
|
74
|
-
expect(renderer.detachWorld).toHaveBeenCalledWith(next);
|
|
75
68
|
});
|
|
76
69
|
});
|
|
@@ -34,7 +34,6 @@ describe('AppErrorCode closed union', () => {
|
|
|
34
34
|
'app-not-started',
|
|
35
35
|
'app-already-running',
|
|
36
36
|
'app-canvas-detached',
|
|
37
|
-
'app-invalid-canvas-pixel-ratio',
|
|
38
37
|
'app-frame-step-invalid',
|
|
39
38
|
'app-system-update-failed',
|
|
40
39
|
'app-pointer-lock-failed',
|
|
@@ -47,11 +46,11 @@ describe('AppErrorCode closed union', () => {
|
|
|
47
46
|
'app-execution-rebuild-failed',
|
|
48
47
|
];
|
|
49
48
|
|
|
50
|
-
expect(allCodes).toHaveLength(
|
|
49
|
+
expect(allCodes).toHaveLength(13);
|
|
51
50
|
|
|
52
51
|
// Each member must be unique.
|
|
53
52
|
const unique = new Set(allCodes);
|
|
54
|
-
expect(unique.size).toBe(
|
|
53
|
+
expect(unique.size).toBe(13);
|
|
55
54
|
});
|
|
56
55
|
|
|
57
56
|
it('exhaustive switch over AppErrorCode has zero default branch', () => {
|
|
@@ -68,8 +67,6 @@ describe('AppErrorCode closed union', () => {
|
|
|
68
67
|
return 'already running';
|
|
69
68
|
case 'app-canvas-detached':
|
|
70
69
|
return 'canvas detached';
|
|
71
|
-
case 'app-invalid-canvas-pixel-ratio':
|
|
72
|
-
return 'invalid canvas pixel ratio';
|
|
73
70
|
case 'app-frame-step-invalid':
|
|
74
71
|
return 'frame step invalid';
|
|
75
72
|
case 'app-system-update-failed':
|
|
@@ -189,12 +186,12 @@ describe("'app-pointer-lock-failed' detail shape", () => {
|
|
|
189
186
|
describe('APP_EXPECTED / APP_ERROR_HINTS bidirectional symmetry', () => {
|
|
190
187
|
it('APP_EXPECTED has one key per AppErrorCode member', () => {
|
|
191
188
|
const keys = Object.keys(APP_EXPECTED);
|
|
192
|
-
expect(keys).toHaveLength(
|
|
189
|
+
expect(keys).toHaveLength(13);
|
|
193
190
|
});
|
|
194
191
|
|
|
195
192
|
it('APP_ERROR_HINTS has one key per AppErrorCode member', () => {
|
|
196
193
|
const keys = Object.keys(APP_ERROR_HINTS);
|
|
197
|
-
expect(keys).toHaveLength(
|
|
194
|
+
expect(keys).toHaveLength(13);
|
|
198
195
|
});
|
|
199
196
|
|
|
200
197
|
it('APP_EXPECTED and APP_ERROR_HINTS have the same key set', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createWorldContext,
|
|
1
|
+
import { createWorldContext, World } from '@forgeax/engine-ecs';
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import {
|
|
4
4
|
executionBootstrapHostPlugin,
|
|
@@ -44,9 +44,8 @@ describe('execution bootstrap isolation', () => {
|
|
|
44
44
|
const url = moduleUrl(`export default function(){return {plugins:[{
|
|
45
45
|
name:'module-system',inject:['world'],apply(ctx){
|
|
46
46
|
const system={name:'module-system',queries:[],fn(){}};
|
|
47
|
-
|
|
48
|
-
ctx.world.
|
|
49
|
-
ctx.effect(()=>()=>ctx.world.removeSystem(update,system.name),'test/system');
|
|
47
|
+
ctx.world.addSystem(ctx.world.scheduleToken('Update'),system).unwrap();
|
|
48
|
+
ctx.effect(()=>()=>ctx.world.removeSystem(ctx.world.scheduleToken('Update'),system.name),'test/system');
|
|
50
49
|
}
|
|
51
50
|
}]}}`);
|
|
52
51
|
const prepared = await prepareBootstrapEntry(url, undefined);
|
|
@@ -54,9 +53,9 @@ describe('execution bootstrap isolation', () => {
|
|
|
54
53
|
if (!prepared.ok) return;
|
|
55
54
|
const world = new World();
|
|
56
55
|
const context = await createWorldContext(world, prepared.value.plugins ?? []);
|
|
57
|
-
expect(world.inspect().scheduleSystemCount(Update)).toBe(1);
|
|
56
|
+
expect(world.inspect().scheduleSystemCount(world.scheduleToken('Update'))).toBe(1);
|
|
58
57
|
await context.fiber.dispose();
|
|
59
|
-
expect(world.inspect().scheduleSystemCount(Update)).toBe(0);
|
|
58
|
+
expect(world.inspect().scheduleSystemCount(world.scheduleToken('Update'))).toBe(0);
|
|
60
59
|
});
|
|
61
60
|
|
|
62
61
|
it('rejects a missing default export structurally', async () => {
|
|
@@ -21,8 +21,8 @@ describe('execution frame ordering', () => {
|
|
|
21
21
|
};
|
|
22
22
|
};
|
|
23
23
|
const ledger = new FrameCreditLedger('world');
|
|
24
|
-
const frame = ledger.issue(1 / 60, sample
|
|
25
|
-
expect(ledger.issue(1 / 60, sample
|
|
24
|
+
const frame = ledger.issue(1 / 60, sample);
|
|
25
|
+
expect(ledger.issue(1 / 60, sample)).toBeUndefined();
|
|
26
26
|
trace.push('update', 'draw');
|
|
27
27
|
ledger.complete({
|
|
28
28
|
kind: 'frame-complete',
|
|
@@ -16,10 +16,9 @@ const sample: InputBackendSample = {
|
|
|
16
16
|
describe('frame credit protocol', () => {
|
|
17
17
|
it('allows one in-flight frame and handles duplicate, late and old identities without writes', () => {
|
|
18
18
|
const ledger = new FrameCreditLedger('world-1');
|
|
19
|
-
const first = ledger.issue(0.016, () => sample
|
|
19
|
+
const first = ledger.issue(0.016, () => sample);
|
|
20
20
|
expect(first?.frameId).toBe(1);
|
|
21
|
-
expect(
|
|
22
|
-
expect(ledger.issue(0.016, () => sample, { width: 320, height: 180 })).toBeUndefined();
|
|
21
|
+
expect(ledger.issue(0.016, () => sample)).toBeUndefined();
|
|
23
22
|
expect(
|
|
24
23
|
ledger.complete({
|
|
25
24
|
kind: 'frame-complete',
|
|
@@ -56,6 +55,6 @@ describe('frame credit protocol', () => {
|
|
|
56
55
|
kernelWaitMs: 0,
|
|
57
56
|
}),
|
|
58
57
|
).toBe('duplicate');
|
|
59
|
-
expect(ledger.issue(0.016, () => sample
|
|
58
|
+
expect(ledger.issue(0.016, () => sample)?.frameId).toBe(2);
|
|
60
59
|
});
|
|
61
60
|
});
|
|
@@ -7,4 +7,8 @@ it('discovers execution from the public App surface', () => {
|
|
|
7
7
|
expectTypeOf(app.execution).toEqualTypeOf<ExecutionControl>();
|
|
8
8
|
expectTypeOf(app.execution.report()).toEqualTypeOf<ExecutionReport>();
|
|
9
9
|
expectTypeOf(options.execution?.bootstrap).toEqualTypeOf<string | URL | undefined>();
|
|
10
|
+
expectTypeOf(options.execution?.assetCatalog?.url).toEqualTypeOf<string | undefined>();
|
|
11
|
+
expectTypeOf(options.execution?.assetCatalog?.expectedScope).toEqualTypeOf<
|
|
12
|
+
{ readonly scopeId: string; readonly generation: number } | undefined
|
|
13
|
+
>();
|
|
10
14
|
});
|
|
@@ -20,7 +20,7 @@ function makeSpyRenderer(): Renderer {
|
|
|
20
20
|
return {
|
|
21
21
|
backend: 'webgpu' as const,
|
|
22
22
|
ready: Promise.resolve({ ok: true, value: undefined }),
|
|
23
|
-
|
|
23
|
+
attach: () => ({ ok: true, value: undefined }),
|
|
24
24
|
detachWorld: () => {},
|
|
25
25
|
draw(): { ok: true; value: undefined } {
|
|
26
26
|
return { ok: true, value: undefined };
|
|
@@ -8,7 +8,7 @@ function renderer(): Renderer {
|
|
|
8
8
|
return {
|
|
9
9
|
backend: 'webgpu',
|
|
10
10
|
ready: Promise.resolve({ ok: true, value: undefined }),
|
|
11
|
-
|
|
11
|
+
attach: () => ({ ok: true, value: undefined }),
|
|
12
12
|
detachWorld: () => {},
|
|
13
13
|
draw: () => ({ ok: true, value: undefined }),
|
|
14
14
|
onError: () => () => {},
|
|
@@ -15,27 +15,38 @@
|
|
|
15
15
|
// world identity path (worldId 0) is preserved (AC-03 regression guarantee).
|
|
16
16
|
|
|
17
17
|
import { Update, World } from '@forgeax/engine-ecs';
|
|
18
|
-
import type { Renderer } from '@forgeax/engine-render';
|
|
18
|
+
import type { Renderer, RenderWorldLease } from '@forgeax/engine-render';
|
|
19
19
|
import { describe, expect, it, vi } from 'vitest';
|
|
20
20
|
import { createFrameLoop } from '../internal/frame-loop';
|
|
21
21
|
|
|
22
22
|
interface DrawCall {
|
|
23
|
-
readonly
|
|
24
|
-
|
|
23
|
+
readonly request: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function lease(onDispose: () => void = () => {}): RenderWorldLease {
|
|
27
|
+
return {
|
|
28
|
+
worldIdentity: {},
|
|
29
|
+
generation: 0,
|
|
30
|
+
readChanges: vi.fn(),
|
|
31
|
+
querySpans: vi.fn(),
|
|
32
|
+
inspectCursor: vi.fn(),
|
|
33
|
+
dispose: onDispose,
|
|
34
|
+
} as unknown as RenderWorldLease;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
function makeSpyRenderer(): { renderer: Renderer; calls: DrawCall[] } {
|
|
28
38
|
const calls: DrawCall[] = [];
|
|
29
39
|
const renderer = {
|
|
30
40
|
backend: 'webgpu' as const,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return { ok: true, value: undefined };
|
|
41
|
+
attach(): { ok: true; value: RenderWorldLease } {
|
|
42
|
+
return { ok: true, value: lease() };
|
|
34
43
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
draw(request: unknown): {
|
|
45
|
+
ok: true;
|
|
46
|
+
value: { frameId: number; deviceGeneration: number; completed: true };
|
|
47
|
+
} {
|
|
48
|
+
calls.push({ request });
|
|
49
|
+
return { ok: true, value: { frameId: calls.length, deviceGeneration: 0, completed: true } };
|
|
39
50
|
},
|
|
40
51
|
onError(): () => void {
|
|
41
52
|
return () => {
|
|
@@ -92,15 +103,13 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
92
103
|
.unwrap();
|
|
93
104
|
const renderer = {
|
|
94
105
|
backend: 'webgpu' as const,
|
|
95
|
-
|
|
96
|
-
attachWorld(): { ok: true; value: undefined } {
|
|
106
|
+
attach(): { ok: true; value: RenderWorldLease } {
|
|
97
107
|
events.push('attach');
|
|
98
|
-
return { ok: true, value:
|
|
108
|
+
return { ok: true, value: lease() };
|
|
99
109
|
},
|
|
100
|
-
|
|
101
|
-
draw(): { ok: true; value: undefined } {
|
|
110
|
+
draw(): { ok: true; value: { frameId: number; deviceGeneration: number; completed: true } } {
|
|
102
111
|
events.push('draw');
|
|
103
|
-
return { ok: true, value:
|
|
112
|
+
return { ok: true, value: { frameId: 1, deviceGeneration: 0, completed: true } };
|
|
104
113
|
},
|
|
105
114
|
onError: () => () => {},
|
|
106
115
|
dispose: () => {},
|
|
@@ -133,7 +142,7 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
133
142
|
]);
|
|
134
143
|
});
|
|
135
144
|
|
|
136
|
-
it('calls renderer.draw
|
|
145
|
+
it('calls renderer.draw with one lease-bound world per running frame', () => {
|
|
137
146
|
const world = new World();
|
|
138
147
|
const { renderer, calls } = makeSpyRenderer();
|
|
139
148
|
const { raf, caf, now, pump } = makeSyncScheduler();
|
|
@@ -148,13 +157,11 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
148
157
|
|
|
149
158
|
expect(calls.length).toBeGreaterThanOrEqual(3);
|
|
150
159
|
for (const call of calls) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// owner is the required, defaulted-to-0 index (single-world identity).
|
|
157
|
-
expect(call.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
|
|
160
|
+
expect(call.request).toMatchObject({
|
|
161
|
+
leases: [{ worldIdentity: expect.any(Object) }],
|
|
162
|
+
camera: { lease: expect.any(Object) },
|
|
163
|
+
environment: { lease: expect.any(Object) },
|
|
164
|
+
});
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
loop.stop();
|
|
@@ -202,11 +209,9 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
202
209
|
const rhiErr = { code: 'rhi-not-available' } as const;
|
|
203
210
|
const renderer = {
|
|
204
211
|
backend: 'webgpu' as const,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
draw(worlds: unknown, options: unknown): { ok: false; error: typeof rhiErr } {
|
|
209
|
-
calls.push({ worlds, options });
|
|
212
|
+
attach: () => ({ ok: true as const, value: lease() }),
|
|
213
|
+
draw(request: unknown): { ok: false; error: typeof rhiErr } {
|
|
214
|
+
calls.push({ request });
|
|
210
215
|
return { ok: false, error: rhiErr };
|
|
211
216
|
},
|
|
212
217
|
onError(): () => void {
|
|
@@ -226,26 +231,24 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
226
231
|
// start() schedules the first tick via raf; pump once to run it.
|
|
227
232
|
pump(1);
|
|
228
233
|
|
|
229
|
-
expect(calls[0]?.
|
|
230
|
-
|
|
234
|
+
expect(calls[0]?.request).toMatchObject({
|
|
235
|
+
camera: { lease: expect.any(Object) },
|
|
236
|
+
environment: { lease: expect.any(Object) },
|
|
237
|
+
});
|
|
231
238
|
expect(onError).toHaveBeenCalledWith(rhiErr);
|
|
232
239
|
|
|
233
240
|
loop.stop();
|
|
234
241
|
});
|
|
235
242
|
|
|
236
|
-
it('keeps the rAF heartbeat
|
|
243
|
+
it('keeps the rAF heartbeat and submits receipt-bound frames', () => {
|
|
237
244
|
const world = new World();
|
|
238
|
-
let reason: 'alive' | 'device-lost' = 'alive';
|
|
239
245
|
const calls: unknown[] = [];
|
|
240
246
|
const renderer = {
|
|
241
247
|
backend: 'webgpu' as const,
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
detachWorld: () => {},
|
|
245
|
-
health: () => ({ reason, recoverable: reason === 'device-lost' }),
|
|
246
|
-
draw(): { ok: true; value: undefined } {
|
|
248
|
+
attach: () => ({ ok: true as const, value: lease() }),
|
|
249
|
+
draw(): { ok: true; value: { frameId: number; deviceGeneration: number; completed: true } } {
|
|
247
250
|
calls.push(undefined);
|
|
248
|
-
return { ok: true, value:
|
|
251
|
+
return { ok: true, value: { frameId: calls.length, deviceGeneration: 0, completed: true } };
|
|
249
252
|
},
|
|
250
253
|
onError(): () => void {
|
|
251
254
|
return () => {};
|
|
@@ -257,14 +260,8 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
257
260
|
loop.start();
|
|
258
261
|
pump(1);
|
|
259
262
|
expect(calls).toHaveLength(1);
|
|
260
|
-
|
|
261
|
-
reason = 'device-lost';
|
|
262
263
|
pump(3);
|
|
263
|
-
expect(calls).toHaveLength(
|
|
264
|
-
|
|
265
|
-
reason = 'alive';
|
|
266
|
-
pump(1);
|
|
267
|
-
expect(calls).toHaveLength(2);
|
|
264
|
+
expect(calls).toHaveLength(4);
|
|
268
265
|
loop.stop();
|
|
269
266
|
});
|
|
270
267
|
|
|
@@ -282,7 +279,12 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
282
279
|
expect(stepped.ok).toBe(true);
|
|
283
280
|
expect(update).toHaveBeenCalledTimes(1);
|
|
284
281
|
expect(update).toHaveBeenCalledWith(1 / 60);
|
|
285
|
-
expect(calls).
|
|
282
|
+
expect(calls).toHaveLength(1);
|
|
283
|
+
expect(calls[0]?.request).toMatchObject({
|
|
284
|
+
leases: [{ worldIdentity: expect.any(Object) }],
|
|
285
|
+
camera: { lease: expect.any(Object) },
|
|
286
|
+
environment: { lease: expect.any(Object) },
|
|
287
|
+
});
|
|
286
288
|
|
|
287
289
|
expect(loop.resume().ok).toBe(true);
|
|
288
290
|
expect(loop.stop().ok).toBe(true);
|
|
@@ -298,7 +300,9 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
298
300
|
expect(idle.ok).toBe(false);
|
|
299
301
|
if (!idle.ok) {
|
|
300
302
|
expect(idle.error.code).toBe('app-frame-step-invalid');
|
|
301
|
-
|
|
303
|
+
if ('detail' in idle.error) {
|
|
304
|
+
expect(idle.error.detail).toEqual({ state: 'idle', deltaSeconds: 1 / 60, reason: 'state' });
|
|
305
|
+
}
|
|
302
306
|
}
|
|
303
307
|
|
|
304
308
|
expect(loop.start().ok).toBe(true);
|
|
@@ -322,15 +326,14 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
322
326
|
const detached: World[] = [];
|
|
323
327
|
const renderer = {
|
|
324
328
|
backend: 'webgpu' as const,
|
|
325
|
-
|
|
326
|
-
attachWorld(candidate: World) {
|
|
329
|
+
attach(candidate: World) {
|
|
327
330
|
attached.push(candidate);
|
|
328
|
-
return { ok: true as const, value:
|
|
329
|
-
},
|
|
330
|
-
detachWorld(candidate: World): void {
|
|
331
|
-
detached.push(candidate);
|
|
331
|
+
return { ok: true as const, value: lease(() => detached.push(candidate)) };
|
|
332
332
|
},
|
|
333
|
-
draw: () => ({
|
|
333
|
+
draw: () => ({
|
|
334
|
+
ok: true as const,
|
|
335
|
+
value: { frameId: 1, deviceGeneration: 0, completed: true as const },
|
|
336
|
+
}),
|
|
334
337
|
onError: () => () => {},
|
|
335
338
|
dispose: () => {},
|
|
336
339
|
} as unknown as Renderer;
|
|
@@ -361,12 +364,14 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
361
364
|
const detached: World[] = [];
|
|
362
365
|
const renderer = {
|
|
363
366
|
backend: 'webgpu' as const,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
attach: (candidate: World) => ({
|
|
368
|
+
ok: true as const,
|
|
369
|
+
value: lease(() => detached.push(candidate)),
|
|
370
|
+
}),
|
|
371
|
+
draw: () => ({
|
|
372
|
+
ok: true as const,
|
|
373
|
+
value: { frameId: 1, deviceGeneration: 0, completed: true as const },
|
|
374
|
+
}),
|
|
370
375
|
onError: () => () => {},
|
|
371
376
|
dispose: () => {},
|
|
372
377
|
} as unknown as Renderer;
|
|
@@ -392,12 +397,14 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
392
397
|
const detached: World[] = [];
|
|
393
398
|
const renderer = {
|
|
394
399
|
backend: 'webgpu' as const,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
400
|
+
attach: (candidate: World) => ({
|
|
401
|
+
ok: true as const,
|
|
402
|
+
value: lease(() => detached.push(candidate)),
|
|
403
|
+
}),
|
|
404
|
+
draw: () => ({
|
|
405
|
+
ok: true as const,
|
|
406
|
+
value: { frameId: 1, deviceGeneration: 0, completed: true as const },
|
|
407
|
+
}),
|
|
401
408
|
onError: () => () => {},
|
|
402
409
|
dispose: () => {},
|
|
403
410
|
} as unknown as Renderer;
|
|
@@ -418,12 +425,14 @@ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with own
|
|
|
418
425
|
const detached: World[] = [];
|
|
419
426
|
const renderer = {
|
|
420
427
|
backend: 'webgpu' as const,
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
428
|
+
attach: (candidate: World) => ({
|
|
429
|
+
ok: true as const,
|
|
430
|
+
value: lease(() => detached.push(candidate)),
|
|
431
|
+
}),
|
|
432
|
+
draw: () => ({
|
|
433
|
+
ok: true as const,
|
|
434
|
+
value: { frameId: 1, deviceGeneration: 0, completed: true as const },
|
|
435
|
+
}),
|
|
427
436
|
onError: () => () => {},
|
|
428
437
|
dispose: () => {},
|
|
429
438
|
} as unknown as Renderer;
|
|
@@ -2,13 +2,13 @@ import type { AudioBackend } from '@forgeax/engine-audio';
|
|
|
2
2
|
import { createWorldContext, World } from '@forgeax/engine-ecs';
|
|
3
3
|
import { type InputBackend, ownedInputBackendPlugin } from '@forgeax/engine-input';
|
|
4
4
|
import { Context, type Plugin } from '@forgeax/engine-plugin';
|
|
5
|
-
import {
|
|
5
|
+
import type { Renderer } from '@forgeax/engine-render';
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
|
-
|
|
8
7
|
import { assembledEngineProfile } from '../internal/assembled-engine-profile';
|
|
9
8
|
import { mainEngineProfile } from '../internal/main-engine-profile';
|
|
10
9
|
import { remoteServerPlugin } from '../internal/remote-server-plugin';
|
|
11
10
|
import { workerEngineProfile } from '../internal/worker-engine-profile';
|
|
11
|
+
import { ownedRendererPlugin } from '../renderer-plugin';
|
|
12
12
|
|
|
13
13
|
interface PluginMetadata {
|
|
14
14
|
readonly name?: string;
|
|
@@ -74,7 +74,7 @@ function expectDag(profile: readonly Plugin[], roots: readonly string[]): void {
|
|
|
74
74
|
expect(visited, 'built-in profile dependency cycle').toBe(profile.length);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const renderer = { assets: {} } as Renderer;
|
|
77
|
+
const renderer = { assets: {} } as unknown as Renderer;
|
|
78
78
|
const input = { sample: () => ({}) } as InputBackend;
|
|
79
79
|
const audio = {} as AudioBackend;
|
|
80
80
|
describe('Engine built-in plugin profiles', () => {
|
|
@@ -9,7 +9,7 @@ function makeRenderer(): Renderer {
|
|
|
9
9
|
return {
|
|
10
10
|
backend: 'webgpu',
|
|
11
11
|
ready: Promise.resolve({ ok: true, value: undefined }),
|
|
12
|
-
|
|
12
|
+
attach: () => ({ ok: true, value: undefined }),
|
|
13
13
|
detachWorld: () => {},
|
|
14
14
|
draw: () => ({ ok: true, value: undefined }),
|
|
15
15
|
onError: () => () => {},
|