@forgeax/engine-app 0.1.6 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/browser-frame-signal.browser.test.d.ts +2 -0
  3. package/dist/__tests__/browser-frame-signal.browser.test.d.ts.map +1 -0
  4. package/dist/__tests__/browser-frame-signal.unit.test.d.ts +2 -0
  5. package/dist/__tests__/browser-frame-signal.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/tool-preview-runtime-contract.unit.test.d.ts +2 -0
  7. package/dist/__tests__/tool-preview-runtime-contract.unit.test.d.ts.map +1 -0
  8. package/dist/browser-frame-signal.d.ts +17 -0
  9. package/dist/browser-frame-signal.d.ts.map +1 -0
  10. package/dist/canvas-policy.d.ts.map +1 -1
  11. package/dist/create-app.d.ts.map +1 -1
  12. package/dist/engine-worker-runtime.mjs +1 -0
  13. package/dist/engine-worker-runtime.mjs.map +1 -1
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.mjs +105 -29
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/internal/browser-rhi-debug-runtime.d.ts +2 -2
  19. package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -1
  20. package/dist/tool-preview/bootstrap.d.ts.map +1 -1
  21. package/package.json +25 -25
  22. package/src/__tests__/browser-frame-signal.browser.test.ts +20 -0
  23. package/src/__tests__/browser-frame-signal.unit.test.ts +35 -0
  24. package/src/__tests__/create-app-stop.test.ts +10 -6
  25. package/src/__tests__/create-app.test.ts +13 -1
  26. package/src/__tests__/tool-preview-runtime-contract.unit.test.ts +47 -0
  27. package/src/browser-frame-signal.ts +44 -0
  28. package/src/canvas-policy.ts +1 -0
  29. package/src/create-app.ts +13 -0
  30. package/src/index.ts +7 -0
  31. package/src/internal/browser-rhi-debug-runtime.ts +9 -6
  32. package/src/tool-preview/bootstrap.ts +55 -13
@@ -1,10 +1,10 @@
1
1
  import type { Renderer } from '@forgeax/engine-render';
2
2
  import type { RhiDevice, RhiInstance } from '@forgeax/engine-rhi';
3
- import type { CreateShaderModuleFn, RecorderAttachment } from '@forgeax/engine-rhi-debug';
3
+ import type { CreateShaderModuleFn, RecorderAttachment, V7Tape } from '@forgeax/engine-rhi-debug';
4
4
  export interface BrowserRhiDebugRuntime {
5
5
  readonly rhi: RhiInstance;
6
6
  readonly attachment: RecorderAttachment;
7
- readonly createReplayDevice: () => Promise<import('@forgeax/engine-types').Result<RhiDevice, unknown>>;
7
+ readonly createReplayDevice: (tape: V7Tape) => Promise<import('@forgeax/engine-types').Result<RhiDevice, unknown>>;
8
8
  readonly createShaderModule: CreateShaderModuleFn;
9
9
  readonly rhiInstrumentation: import('@forgeax/engine-render/internal/construct-renderer').RhiBackendInstrumentation;
10
10
  attachRenderer(renderer: Renderer): () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"browser-rhi-debug-runtime.d.ts","sourceRoot":"","sources":["../../src/internal/browser-rhi-debug-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,KAAK,EACV,oBAAoB,EAEpB,kBAAkB,EACnB,MAAM,2BAA2B,CAAC;AAInC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;IACxC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,OAAO,CACxC,OAAO,uBAAuB,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAC3D,CAAC;IACF,QAAQ,CAAC,kBAAkB,EAAE,oBAAoB,CAAC;IAClD,QAAQ,CAAC,kBAAkB,EAAE,OAAO,oDAAoD,EAAE,yBAAyB,CAAC;IACpH,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI,CAAC;CAChD;AAmBD;;;;GAIG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAuBpF"}
1
+ {"version":3,"file":"browser-rhi-debug-runtime.d.ts","sourceRoot":"","sources":["../../src/internal/browser-rhi-debug-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,KAAK,EACV,oBAAoB,EAEpB,kBAAkB,EAClB,MAAM,EACP,MAAM,2BAA2B,CAAC;AAInC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;IACxC,QAAQ,CAAC,kBAAkB,EAAE,CAC3B,IAAI,EAAE,MAAM,KACT,OAAO,CAAC,OAAO,uBAAuB,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;IACzE,QAAQ,CAAC,kBAAkB,EAAE,oBAAoB,CAAC;IAClD,QAAQ,CAAC,kBAAkB,EAAE,OAAO,oDAAoD,EAAE,yBAAyB,CAAC;IACpH,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI,CAAC;CAChD;AAmBD;;;;GAIG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAyBpF"}
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../src/tool-preview/bootstrap.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../src/tool-preview/bootstrap.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAG7D,OAAO,KAAK,EAAE,GAAG,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtE,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACtB,MAAM,UAAU,CAAC;AAElB,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,KAAK,CAAC;AAE9E,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAC1E,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;CAChG;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IACvC,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;IAC7C,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,0BAA0B,CAAC;IAC/C,QAAQ,CAAC,OAAO,CAAC,EAAE,CACjB,GAAG,EAAE,GAAG,KACL,IAAI,GAAG,wBAAwB,GAAG,OAAO,CAAC,IAAI,GAAG,wBAAwB,CAAC,CAAC;IAChF,8EAA8E;IAC9E,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAC9B,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,wBAAwB,GAAG,SAAS,KAC1C,IAAI,GAAG,wBAAwB,GAAG,OAAO,CAAC,IAAI,GAAG,wBAAwB,CAAC,CAAC;IAChF,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9F;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,qCAAqC,GAAG,+BAA+B,CAAC;IACvF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACvE;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,wBAAwB,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAC3E,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,oBAAoB,CAAC,CAAC,CAAC;IACnE,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACvE,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/F,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,SAAS,OAAO,EAAE,CAAC;IACvC,QAAQ,CAAC,QAAQ,CAAC,EAAE,wBAAwB,CAAC;CAC9C;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACvE,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/F,QAAQ,CAAC,GAAG,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACxF,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,CAAC;IAC3C,QAAQ,CAAC,SAAS,EAAE,SAAS,WAAW,EAAE,CAAC;IAC3C,QAAQ,CAAC,eAAe,EAAE,UAAU,CAAC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,wBAAwB,CAAC;CAC9C;AAyFD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,oBAAoB,CAAC,CAAC,CA+T7D;AAED,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,oBAAoB,CAAC,CAAC,CAoVxD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-app",
3
- "version": "0.1.6",
3
+ "version": "0.1.19",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -23,32 +23,32 @@
23
23
  "LICENSE"
24
24
  ],
25
25
  "dependencies": {
26
- "@forgeax/engine-animation": "0.1.6",
27
- "@forgeax/engine-assets-runtime": "0.1.6",
28
- "@forgeax/engine-audio": "0.1.6",
29
- "@forgeax/engine-audio-webaudio": "0.1.6",
30
- "@forgeax/engine-debug-draw": "0.1.6",
31
- "@forgeax/engine-ecs": "0.1.6",
32
- "@forgeax/engine-input": "0.1.6",
33
- "@forgeax/engine-physics": "0.1.6",
34
- "@forgeax/engine-physics-rapier2d": "0.1.6",
35
- "@forgeax/engine-physics-rapier3d": "0.1.6",
36
- "@forgeax/engine-plugin": "0.1.6",
37
- "@forgeax/engine-profiler": "0.1.6",
38
- "@forgeax/engine-render": "0.1.6",
39
- "@forgeax/engine-rhi": "0.1.6",
40
- "@forgeax/engine-runtime": "0.1.6",
41
- "@forgeax/engine-scene": "0.1.6",
42
- "@forgeax/engine-state": "0.1.6",
43
- "@forgeax/engine-tool-runtime": "0.1.6",
44
- "@forgeax/engine-types": "0.1.6"
26
+ "@forgeax/engine-animation": "0.1.19",
27
+ "@forgeax/engine-assets-runtime": "0.1.19",
28
+ "@forgeax/engine-audio": "0.1.19",
29
+ "@forgeax/engine-audio-webaudio": "0.1.19",
30
+ "@forgeax/engine-debug-draw": "0.1.19",
31
+ "@forgeax/engine-ecs": "0.1.19",
32
+ "@forgeax/engine-input": "0.1.19",
33
+ "@forgeax/engine-physics": "0.1.19",
34
+ "@forgeax/engine-physics-rapier2d": "0.1.19",
35
+ "@forgeax/engine-physics-rapier3d": "0.1.19",
36
+ "@forgeax/engine-plugin": "0.1.19",
37
+ "@forgeax/engine-profiler": "0.1.19",
38
+ "@forgeax/engine-render": "0.1.19",
39
+ "@forgeax/engine-rhi": "0.1.19",
40
+ "@forgeax/engine-rhi-debug": "0.1.19",
41
+ "@forgeax/engine-runtime": "0.1.19",
42
+ "@forgeax/engine-scene": "0.1.19",
43
+ "@forgeax/engine-state": "0.1.19",
44
+ "@forgeax/engine-tool-runtime": "0.1.19",
45
+ "@forgeax/engine-types": "0.1.19"
45
46
  },
46
47
  "devDependencies": {
47
- "@forgeax/engine-remote": "0.1.6",
48
- "@forgeax/engine-rhi-debug": "0.1.6",
49
- "@forgeax/engine-rhi-null": "0.1.6",
50
- "@forgeax/engine-rhi-webgpu": "0.1.6",
51
- "@forgeax/engine-rhi-wgpu": "0.1.6",
48
+ "@forgeax/engine-remote": "0.1.19",
49
+ "@forgeax/engine-rhi-null": "0.1.19",
50
+ "@forgeax/engine-rhi-webgpu": "0.1.19",
51
+ "@forgeax/engine-rhi-wgpu": "0.1.19",
52
52
  "@types/ws": "^8.5.10",
53
53
  "@webgpu/types": "^0.1.71",
54
54
  "ws": "^8.20.0"
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ FORGEAX_FRAME_SUBMITTED_EVENT,
5
+ publishBrowserFrameSubmitted,
6
+ } from '../browser-frame-signal';
7
+
8
+ describe('publishBrowserFrameSubmitted in a browser', () => {
9
+ it('dispatches frame-submitted on a real HTMLCanvasElement', () => {
10
+ const canvas = document.createElement('canvas');
11
+ const received: Event[] = [];
12
+ canvas.addEventListener(FORGEAX_FRAME_SUBMITTED_EVENT, (event) => received.push(event));
13
+
14
+ publishBrowserFrameSubmitted(canvas, { frameId: 11, deviceGeneration: 4 });
15
+
16
+ expect(received).toHaveLength(1);
17
+ expect(received[0]).toBeInstanceOf(CustomEvent);
18
+ expect((received[0] as CustomEvent).detail).toEqual({ frameId: 11, deviceGeneration: 4 });
19
+ });
20
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ FORGEAX_FRAME_SUBMITTED_EVENT,
5
+ publishBrowserFrameSubmitted,
6
+ } from '../browser-frame-signal';
7
+
8
+ describe('publishBrowserFrameSubmitted', () => {
9
+ it('projects Dawn canvas progress without requiring DOM event methods', () => {
10
+ const documentElement = { dataset: {} as DOMStringMap };
11
+ const canvas = { ownerDocument: { documentElement } } as HTMLCanvasElement;
12
+
13
+ expect(() =>
14
+ publishBrowserFrameSubmitted(canvas, { frameId: 7, deviceGeneration: 2 }),
15
+ ).not.toThrow();
16
+ expect(documentElement.dataset.forgeaxFrameSubmitted).toBe('7');
17
+ });
18
+
19
+ it('dispatches the browser event when the canvas supports dispatchEvent', () => {
20
+ let dispatchedEvent: Event | undefined;
21
+ const dispatchEvent = (event: Event): boolean => {
22
+ dispatchedEvent = event;
23
+ return true;
24
+ };
25
+ const canvas = { dispatchEvent } as unknown as HTMLCanvasElement;
26
+
27
+ publishBrowserFrameSubmitted(canvas, { frameId: 9, deviceGeneration: 3 });
28
+
29
+ expect(dispatchedEvent).toBeInstanceOf(CustomEvent);
30
+ if (!(dispatchedEvent instanceof CustomEvent))
31
+ throw new Error('frame event was not dispatched');
32
+ expect(dispatchedEvent.type).toBe(FORGEAX_FRAME_SUBMITTED_EVENT);
33
+ expect(dispatchedEvent.detail).toEqual({ frameId: 9, deviceGeneration: 3 });
34
+ });
35
+ });
@@ -392,8 +392,9 @@ describe('feat-20260619 M3: auto-register audioTickSystem', () => {
392
392
  addEventListener: vi.fn(),
393
393
  removeEventListener: vi.fn(),
394
394
  });
395
- // biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor
396
- globalThis.AudioContext = vi.fn(() => makeMockAudioContextForApp()) as any;
395
+ globalThis.AudioContext = vi.fn(function AudioContextMock() {
396
+ return makeMockAudioContextForApp();
397
+ }) as unknown as typeof AudioContext;
397
398
 
398
399
  const rafStub = installRafStub();
399
400
 
@@ -458,6 +459,7 @@ describe('feat-20260619 M3: auto-register audioTickSystem', () => {
458
459
  expect(rafStub.tickFn).not.toBeNull();
459
460
  rafStub.tickFn?.();
460
461
 
462
+ expect(world.execution.health).toBe('healthy');
461
463
  expect(playSpy).toHaveBeenCalledTimes(1);
462
464
 
463
465
  app.stop();
@@ -477,8 +479,9 @@ describe('feat-20260619 M3: auto-register audioTickSystem', () => {
477
479
  addEventListener: vi.fn(),
478
480
  removeEventListener: vi.fn(),
479
481
  });
480
- // biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor
481
- globalThis.AudioContext = vi.fn(() => makeMockAudioContextForApp()) as any;
482
+ globalThis.AudioContext = vi.fn(function AudioContextMock() {
483
+ return makeMockAudioContextForApp();
484
+ }) as unknown as typeof AudioContext;
482
485
 
483
486
  const rafStub = installRafStub();
484
487
 
@@ -543,8 +546,9 @@ describe('feat-20260619 M3: auto-register audioTickSystem', () => {
543
546
  addEventListener: vi.fn(),
544
547
  removeEventListener: vi.fn(),
545
548
  });
546
- // biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor
547
- globalThis.AudioContext = vi.fn(() => makeMockAudioContextForApp()) as any;
549
+ globalThis.AudioContext = vi.fn(function AudioContextMock() {
550
+ return makeMockAudioContextForApp();
551
+ }) as unknown as typeof AudioContext;
548
552
 
549
553
  const rafStub = installRafStub();
550
554
 
@@ -23,7 +23,7 @@
23
23
 
24
24
  import { World } from '@forgeax/engine-ecs';
25
25
  import { Camera, orthographic, perspective } from '@forgeax/engine-render';
26
- import { describe, expect, it } from 'vitest';
26
+ import { describe, expect, it, vi } from 'vitest';
27
27
 
28
28
  import { syncCameraAspect, syncCanvasDrawingBuffer } from '../create-app';
29
29
  import { resolveRhiDebugFlag } from '../internal/rhi-debug-flag';
@@ -231,6 +231,18 @@ describe('create-app.test.ts', () => {
231
231
  expect(r.value.aspect).toBeCloseTo(2, 5);
232
232
  });
233
233
 
234
+ it('does not journal a same-f32 aspect on every frame', () => {
235
+ const world = new World();
236
+ world.spawn({ component: Camera, data: perspective({ fov: 1, aspect: 1 }) }).unwrap();
237
+ const set = vi.spyOn(world, 'set');
238
+
239
+ syncCameraAspect(world, 1280, 720);
240
+ syncCameraAspect(world, 1280, 720);
241
+
242
+ expect(set).toHaveBeenCalledTimes(1);
243
+ set.mockRestore();
244
+ });
245
+
234
246
  it('autoAspect=false camera is left untouched', () => {
235
247
  const world = new World();
236
248
  const cam = world
@@ -0,0 +1,47 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ const bootstrapSource = readFileSync(
5
+ new URL('../tool-preview/bootstrap.ts', import.meta.url),
6
+ 'utf8',
7
+ );
8
+ const runtimeSource = readFileSync(
9
+ new URL('../internal/browser-rhi-debug-runtime.ts', import.meta.url),
10
+ 'utf8',
11
+ );
12
+
13
+ describe('tool preview runtime contract', () => {
14
+ it('captures one rendered frame after warmup and the live-resource snapshot', () => {
15
+ const finalFrame = bootstrapSource.indexOf('if (frame === recipe.frames - 1)');
16
+ const armed = bootstrapSource.indexOf(
17
+ 'encodedCapture = runtime.attachment.captureFrame()',
18
+ finalFrame,
19
+ );
20
+ const snapshot = bootstrapSource.indexOf('runtime.attachment.frameBoundary()', armed);
21
+ const rendered = bootstrapSource.indexOf('app.stepFrame(recipe.deltaSeconds)', snapshot);
22
+ const finalized = bootstrapSource.indexOf(
23
+ 'const finalized = await runtime.attachment.frameBoundary()',
24
+ rendered,
25
+ );
26
+
27
+ expect(finalFrame).toBeGreaterThan(-1);
28
+ expect(armed).toBeGreaterThan(finalFrame);
29
+ expect(snapshot).toBeGreaterThan(armed);
30
+ expect(rendered).toBeGreaterThan(snapshot);
31
+ expect(finalized).toBeGreaterThan(rendered);
32
+ });
33
+
34
+ it('requests replay features and limits from the recorded tape', () => {
35
+ expect(bootstrapSource).toContain('runtime.createReplayDevice(parsed.value)');
36
+ expect(runtimeSource).toContain('createReplayDevice: async (tape)');
37
+ expect(runtimeSource).toContain(
38
+ 'replayDeviceRequest(tape, adapter.value.features, adapter.value.limits)',
39
+ );
40
+ });
41
+
42
+ it('publishes owner identity only after the recorded tape proves a subject draw', () => {
43
+ expect(bootstrapSource).toContain("resource.kind === 'material' || resource.kind === 'mesh'");
44
+ expect(bootstrapSource).toContain('return drawCalls > 2');
45
+ expect(bootstrapSource).toContain('observation: capture.resource.ownerFacts');
46
+ });
47
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Browser-visible render progress owned by the App frame loop.
3
+ *
4
+ * The Renderer already emits `frame-submitted` after its sole queue-submit
5
+ * boundary. This adapter projects that event onto the document without
6
+ * exposing a renderer handle or creating a second readiness registry. DevKit
7
+ * capture waits on the same projection, then verifies compositor pixels.
8
+ */
9
+
10
+ export const FORGEAX_FRAME_SUBMITTED_DATASET = 'forgeaxFrameSubmitted';
11
+ export const FORGEAX_FRAME_SUBMITTED_EVENT = 'forgeax:frame-submitted';
12
+
13
+ export interface BrowserFrameSubmitted {
14
+ readonly frameId: number;
15
+ readonly deviceGeneration: number;
16
+ }
17
+
18
+ export function resetBrowserFrameSubmitted(canvas: HTMLCanvasElement): void {
19
+ const documentElement = canvas.ownerDocument?.documentElement;
20
+ if (documentElement !== undefined) {
21
+ delete documentElement.dataset[FORGEAX_FRAME_SUBMITTED_DATASET];
22
+ }
23
+ }
24
+
25
+ export function publishBrowserFrameSubmitted(
26
+ canvas: HTMLCanvasElement,
27
+ event: BrowserFrameSubmitted,
28
+ ): void {
29
+ const documentElement = canvas.ownerDocument?.documentElement;
30
+ if (documentElement !== undefined) {
31
+ documentElement.dataset[FORGEAX_FRAME_SUBMITTED_DATASET] = String(event.frameId);
32
+ }
33
+ // Dawn-node smoke canvases intentionally expose only the drawing-buffer
34
+ // surface; they are not DOM EventTargets. Keep the document projection
35
+ // above useful for those hosts, while dispatching the browser event only
36
+ // when the host actually provides the optional EventTarget capability.
37
+ if (typeof CustomEvent === 'function' && typeof canvas.dispatchEvent === 'function') {
38
+ canvas.dispatchEvent(
39
+ new CustomEvent(FORGEAX_FRAME_SUBMITTED_EVENT, {
40
+ detail: Object.freeze({ ...event }),
41
+ }),
42
+ );
43
+ }
44
+ }
@@ -11,6 +11,7 @@ export function syncCameraAspect(world: World, canvasW: number, canvasH: number)
11
11
  if (!camera.ok) continue;
12
12
  if (camera.value.autoAspect !== true) continue;
13
13
  if (camera.value.projection !== CAMERA_PROJECTION_PERSPECTIVE) continue;
14
+ if (camera.value.aspect === Math.fround(aspect)) continue;
14
15
  world.set(row.entity, Camera, { aspect });
15
16
  }
16
17
  }
package/src/create-app.ts CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  import { err, ok, type Result } from '@forgeax/engine-types';
48
48
 
49
49
  import { createAnimationPayloadLookup } from './animation-asset-lookup';
50
+ import { publishBrowserFrameSubmitted, resetBrowserFrameSubmitted } from './browser-frame-signal';
50
51
  import type { AppErrorCode, AppErrorDetailFor } from './errors';
51
52
  import { APP_ERROR_HINTS, APP_EXPECTED, AppError } from './errors';
52
53
  import {
@@ -564,6 +565,7 @@ async function createAppFromCanvas(
564
565
  world,
565
566
  pluginContext,
566
567
  executionControl,
568
+ onFrameSubmitted: (event) => publishBrowserFrameSubmitted(canvas, event),
567
569
  ...(inputBackend !== undefined ? { inputBackend } : {}),
568
570
  ...(inputHandle === undefined
569
571
  ? {}
@@ -600,6 +602,7 @@ async function createAppFromCanvas(
600
602
 
601
603
  // The aspect-sync sidecar belongs to the canvas path because it owns the DOM
602
604
  // canvas. It runs as an Update system so it shares World scheduling semantics.
605
+ resetBrowserFrameSubmitted(canvas);
603
606
  const built = await buildApp(buildArgs);
604
607
  if (!built.ok) {
605
608
  await pluginContext.fiber.dispose();
@@ -805,6 +808,7 @@ export function syncCameraAspect(world: World, canvasW: number, canvasH: number)
805
808
  // perspective discriminator is the numeric column value.
806
809
  if (r.value.autoAspect !== true) continue;
807
810
  if (r.value.projection !== CAMERA_PROJECTION_PERSPECTIVE) continue;
811
+ if (r.value.aspect === Math.fround(aspect)) continue;
808
812
  world.set(entity, Camera, { aspect });
809
813
  }
810
814
  }
@@ -994,6 +998,11 @@ interface BuildAppArgs {
994
998
  | undefined;
995
999
  readonly profiler?: import('@forgeax/engine-profiler').Profiler;
996
1000
  readonly executionControl?: import('./execution/control').LocalExecutionControl;
1001
+ /** Canvas-form projection of the Renderer frame-submitted event. */
1002
+ readonly onFrameSubmitted?: (event: {
1003
+ readonly frameId: number;
1004
+ readonly deviceGeneration: number;
1005
+ }) => void;
997
1006
  }
998
1007
 
999
1008
  /**
@@ -1118,6 +1127,10 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
1118
1127
  return;
1119
1128
  }
1120
1129
  rendererUnsubscribe = renderer.subscribe((event) => {
1130
+ if (event.kind === 'frame-submitted') {
1131
+ args.onFrameSubmitted?.(event);
1132
+ return;
1133
+ }
1121
1134
  if (event.kind !== 'error') return;
1122
1135
  const e: RenderError = event.error;
1123
1136
  dispatch(e);
package/src/index.ts CHANGED
@@ -39,6 +39,13 @@ export {
39
39
  createFullscreenRenderFeature,
40
40
  type FullscreenRenderFeatureOptions,
41
41
  } from '@forgeax/engine-render/authoring';
42
+ export {
43
+ type BrowserFrameSubmitted,
44
+ FORGEAX_FRAME_SUBMITTED_DATASET,
45
+ FORGEAX_FRAME_SUBMITTED_EVENT,
46
+ publishBrowserFrameSubmitted,
47
+ resetBrowserFrameSubmitted,
48
+ } from './browser-frame-signal';
42
49
  export { createApp, measureCanvasDrawingBuffer, syncCanvasDrawingBuffer } from './create-app';
43
50
  export type {
44
51
  AppDetailCanvasDetached,
@@ -4,16 +4,17 @@ import type {
4
4
  CreateShaderModuleFn,
5
5
  RecordableBackend,
6
6
  RecorderAttachment,
7
+ V7Tape,
7
8
  } from '@forgeax/engine-rhi-debug';
8
- import { attachRecorder } from '@forgeax/engine-rhi-debug';
9
+ import { attachRecorder, replayDeviceRequest } from '@forgeax/engine-rhi-debug';
9
10
  import { createRhiInstrumentation } from './rhi-capture';
10
11
 
11
12
  export interface BrowserRhiDebugRuntime {
12
13
  readonly rhi: RhiInstance;
13
14
  readonly attachment: RecorderAttachment;
14
- readonly createReplayDevice: () => Promise<
15
- import('@forgeax/engine-types').Result<RhiDevice, unknown>
16
- >;
15
+ readonly createReplayDevice: (
16
+ tape: V7Tape,
17
+ ) => Promise<import('@forgeax/engine-types').Result<RhiDevice, unknown>>;
17
18
  readonly createShaderModule: CreateShaderModuleFn;
18
19
  readonly rhiInstrumentation: import('@forgeax/engine-render/internal/construct-renderer').RhiBackendInstrumentation;
19
20
  attachRenderer(renderer: Renderer): () => void;
@@ -51,10 +52,12 @@ export async function createBrowserRhiDebugRuntime(): Promise<BrowserRhiDebugRun
51
52
  attachment,
52
53
  createShaderModule: backend.createShaderModule,
53
54
  rhiInstrumentation: createRhiInstrumentation(attachment),
54
- createReplayDevice: async () => {
55
+ createReplayDevice: async (tape) => {
55
56
  const adapter = await backend.rhi.requestAdapter();
56
57
  if (!adapter.ok) return adapter;
57
- return adapter.value.requestDevice();
58
+ return adapter.value.requestDevice(
59
+ replayDeviceRequest(tape, adapter.value.features, adapter.value.limits),
60
+ );
58
61
  },
59
62
  attachRenderer(_renderer: Renderer): () => void {
60
63
  // Frame boundaries and device loss are connected by the typed
@@ -203,6 +203,12 @@ function resourceSubjectDigest(resource: ToolPreviewResourceFacts | undefined):
203
203
  return candidates.find((value): value is string => typeof value === 'string' && value.length > 0);
204
204
  }
205
205
 
206
+ function resourceSubjectDrawn(resource: ToolPreviewResourceFacts, drawCalls: number): boolean {
207
+ if (resource.kind === 'texture') return drawCalls > 0;
208
+ if (resource.kind === 'material' || resource.kind === 'mesh') return drawCalls > 2;
209
+ return resource.observation !== undefined;
210
+ }
211
+
206
212
  export async function replayToolPreviewCapture(
207
213
  capture: ToolPreviewCaptureResult,
208
214
  ): Promise<Result<ToolPreviewRunResult, ToolPreviewHostError>> {
@@ -217,13 +223,29 @@ export async function replayToolPreviewCapture(
217
223
  detail: { phase: 'tape-parse', cause: parsed.error },
218
224
  });
219
225
  }
220
- const subjectDigest = resourceSubjectDigest(capture.resource);
221
- if (capture.resource !== undefined && subjectDigest === undefined) {
226
+ const drawCalls = countDrawCalls(parsed.value);
227
+ const resource =
228
+ capture.resource !== undefined &&
229
+ capture.resource.observation === undefined &&
230
+ capture.resource.ownerFacts !== undefined &&
231
+ resourceSubjectDrawn(capture.resource, drawCalls)
232
+ ? { ...capture.resource, observation: capture.resource.ownerFacts }
233
+ : capture.resource;
234
+ const subjectDigest = resourceSubjectDigest(resource);
235
+ if (resource !== undefined && subjectDigest === undefined) {
222
236
  return err({
223
237
  code: 'tool-preview-bootstrap-failed',
224
238
  expected: 'the resource owner to publish a subject identity before evidence publication',
225
239
  hint: 'repair AssetRegistry owner facts or post-frame observation; recipe and trace are not subject identity',
226
- detail: { phase: 'resource-identity' },
240
+ detail: {
241
+ phase: 'resource-identity',
242
+ cause: {
243
+ kind: resource.kind,
244
+ drawCalls,
245
+ ownerFactsPublished: resource.ownerFacts !== undefined,
246
+ observationPublished: resource.observation !== undefined,
247
+ },
248
+ },
227
249
  });
228
250
  }
229
251
  let runtime: Awaited<ReturnType<typeof createBrowserRhiDebugRuntime>>;
@@ -237,7 +259,7 @@ export async function replayToolPreviewCapture(
237
259
  detail: { phase: 'replay-runtime', cause },
238
260
  });
239
261
  }
240
- const replayDevice = await runtime.createReplayDevice();
262
+ const replayDevice = await runtime.createReplayDevice(parsed.value);
241
263
  if (!replayDevice.ok) {
242
264
  return err({
243
265
  code: 'tool-preview-capability-unavailable',
@@ -259,7 +281,6 @@ export async function replayToolPreviewCapture(
259
281
  });
260
282
  }
261
283
  const replay = replayResult.value;
262
- const drawCalls = countDrawCalls(parsed.value);
263
284
  if (drawCalls === 0) {
264
285
  await replay.dispose();
265
286
  return err({
@@ -507,7 +528,7 @@ export async function replayToolPreviewCapture(
507
528
  },
508
529
  },
509
530
  },
510
- ...(capture.resource === undefined ? {} : { resource: capture.resource }),
531
+ ...(resource === undefined ? {} : { resource }),
511
532
  });
512
533
  }
513
534
 
@@ -673,7 +694,7 @@ export async function createToolPreviewHost(
673
694
  });
674
695
  const executeStartedAtMs = performance.now();
675
696
  const actionTrace: ToolPreviewAction[] = [];
676
- const encodedCapture = runtime.attachment.captureFrame();
697
+ let encodedCapture: ReturnType<typeof runtime.attachment.captureFrame> | undefined;
677
698
  for (let frame = 0; frame < recipe.frames; frame += 1) {
678
699
  for (const action of recipe.actions.filter((candidate) => candidate.frame === frame)) {
679
700
  let accepted = false;
@@ -697,6 +718,17 @@ export async function createToolPreviewHost(
697
718
  }
698
719
  actionTrace.push(action);
699
720
  }
721
+ if (frame === recipe.frames - 1) {
722
+ encodedCapture = runtime.attachment.captureFrame();
723
+ const snapshot = await runtime.attachment.frameBoundary();
724
+ if (!snapshot.ok)
725
+ return err({
726
+ code: 'tool-preview-bootstrap-failed',
727
+ expected: 'RHI-debug snapshots live resources before the captured frame',
728
+ hint: snapshot.error.hint,
729
+ detail: { phase: 'rhi-snapshot', cause: snapshot.error },
730
+ });
731
+ }
700
732
  const stepped = app.stepFrame(recipe.deltaSeconds);
701
733
  if (!stepped.ok)
702
734
  return err({
@@ -710,12 +742,14 @@ export async function createToolPreviewHost(
710
742
  // only the feature's intentional `next-frame` pending state.
711
743
  await new Promise<void>((resolve) => setTimeout(resolve, 0));
712
744
  }
713
- // RecorderAttachment captures one complete frame. The first boundary
714
- // snapshots live resources and the following boundary finalizes it;
715
- // explicit boundaries keep stepFrame() deterministic for preview hosts.
716
- await runtime.attachment.frameBoundary();
717
- await new Promise<void>((resolve) => setTimeout(resolve, 0));
718
- await runtime.attachment.frameBoundary();
745
+ const finalized = await runtime.attachment.frameBoundary();
746
+ if (!finalized.ok)
747
+ return err({
748
+ code: 'tool-preview-bootstrap-failed',
749
+ expected: 'RHI-debug finalizes the captured frame',
750
+ hint: finalized.error.hint,
751
+ detail: { phase: 'rhi-finalize', cause: finalized.error },
752
+ });
719
753
  if (options.collectResourceFacts !== undefined) {
720
754
  try {
721
755
  const collected = await options.collectResourceFacts(app, resourceFacts);
@@ -744,6 +778,14 @@ export async function createToolPreviewHost(
744
778
  detail: { phase: 'profiler-finish', cause: profile.error },
745
779
  });
746
780
  }
781
+ if (encodedCapture === undefined) {
782
+ return err({
783
+ code: 'tool-preview-bootstrap-failed',
784
+ expected: 'a positive-frame recipe to arm one captured frame',
785
+ hint: 'validate the recipe before running the preview host',
786
+ detail: { phase: 'rhi-capture' },
787
+ });
788
+ }
747
789
  const tape = await encodedCapture;
748
790
  if (!tape.ok)
749
791
  return err({