@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
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ export { Context } from '@forgeax/engine-plugin';
3
3
  export { createFullscreenRenderFeature } from '@forgeax/engine-render/authoring';
4
4
  import { Camera, perspective, renderComponentsPlugin, CAMERA_PROJECTION_PERSPECTIVE } from '@forgeax/engine-render';
5
5
  import { RhiError } from '@forgeax/engine-rhi';
6
- import { attachRecorder, decodeTape, openReplay, decodeToRgba8, buildTapeIndex, createRhiDebugError } from '@forgeax/engine-rhi-debug';
6
+ import { attachRecorder, decodeTape, openReplay, decodeToRgba8, buildTapeIndex, replayDeviceRequest, createRhiDebugError } from '@forgeax/engine-rhi-debug';
7
7
  import * as engineRuntimeModule from '@forgeax/engine-runtime';
8
8
  import { EngineEnvironmentError } from '@forgeax/engine-runtime';
9
9
  import { loadRhiPack, constructRuntimeRendererHost } from '@forgeax/engine-runtime/internal/renderer-host';
@@ -217,6 +217,29 @@ var init_browser_remote_bridge = __esm({
217
217
  }
218
218
  });
219
219
 
220
+ // src/browser-frame-signal.ts
221
+ var FORGEAX_FRAME_SUBMITTED_DATASET = "forgeaxFrameSubmitted";
222
+ var FORGEAX_FRAME_SUBMITTED_EVENT = "forgeax:frame-submitted";
223
+ function resetBrowserFrameSubmitted(canvas) {
224
+ const documentElement = canvas.ownerDocument?.documentElement;
225
+ if (documentElement !== void 0) {
226
+ delete documentElement.dataset[FORGEAX_FRAME_SUBMITTED_DATASET];
227
+ }
228
+ }
229
+ function publishBrowserFrameSubmitted(canvas, event) {
230
+ const documentElement = canvas.ownerDocument?.documentElement;
231
+ if (documentElement !== void 0) {
232
+ documentElement.dataset[FORGEAX_FRAME_SUBMITTED_DATASET] = String(event.frameId);
233
+ }
234
+ if (typeof CustomEvent === "function" && typeof canvas.dispatchEvent === "function") {
235
+ canvas.dispatchEvent(
236
+ new CustomEvent(FORGEAX_FRAME_SUBMITTED_EVENT, {
237
+ detail: Object.freeze({ ...event })
238
+ })
239
+ );
240
+ }
241
+ }
242
+
220
243
  // src/animation-asset-lookup.ts
221
244
  function createAnimationPayloadLookup(source) {
222
245
  return (guid) => source?.lookup(guid);
@@ -330,8 +353,8 @@ var APP_EXPECTED = Object.fromEntries(
330
353
  var APP_ERROR_HINTS = Object.fromEntries(
331
354
  Object.entries(appErrorPolicy).map(([code, policy]) => [code, policy.hint])
332
355
  );
333
- function isAppError(err13) {
334
- return err13 instanceof AppErrorClass;
356
+ function isAppError(err14) {
357
+ return err14 instanceof AppErrorClass;
335
358
  }
336
359
  function executionBootstrapHostPlugin(host) {
337
360
  return {
@@ -809,16 +832,16 @@ var ErrorFanoutRegistry = class {
809
832
  * console.error(err) so the unhandled error is visible in devtools
810
833
  * (charter P3 explicit failure: silent drop is a footgun).
811
834
  */
812
- fire(err13) {
835
+ fire(err14) {
813
836
  if (this.listeners.size === 0) {
814
837
  if (!this.silenceUnhandledErrors) {
815
- console.error(err13);
838
+ console.error(err14);
816
839
  }
817
840
  return;
818
841
  }
819
842
  const snapshot = Array.from(this.listeners);
820
843
  for (const cb of snapshot) {
821
- cb(err13);
844
+ cb(err14);
822
845
  }
823
846
  }
824
847
  /**
@@ -2000,13 +2023,13 @@ function attachInputAuto(canvas, options = {}) {
2000
2023
  ...options.lockProvider ? { lockProvider: options.lockProvider } : {},
2001
2024
  onLockError: (detail) => {
2002
2025
  if (onLockErrorDispatch) {
2003
- const err13 = new AppError({
2026
+ const err14 = new AppError({
2004
2027
  code: "app-pointer-lock-failed",
2005
2028
  expected: APP_EXPECTED["app-pointer-lock-failed"],
2006
2029
  hint: APP_ERROR_HINTS["app-pointer-lock-failed"],
2007
2030
  detail
2008
2031
  });
2009
- onLockErrorDispatch(err13);
2032
+ onLockErrorDispatch(err14);
2010
2033
  }
2011
2034
  }
2012
2035
  };
@@ -2065,21 +2088,27 @@ function inputPlugin() {
2065
2088
  }
2066
2089
  };
2067
2090
  }
2068
-
2069
- // ../render/src/record/render-context.ts
2070
- function selectSwapChainFormat(storageBufferCapable) {
2071
- if (!storageBufferCapable) return { storage: "rgba8unorm", view: "rgba8unorm-srgb" };
2091
+ function selectSwapChainFormat(storageBufferCapable, surfaceViewFormats = true) {
2092
+ if (!storageBufferCapable) {
2093
+ return {
2094
+ storage: "rgba8unorm",
2095
+ view: surfaceViewFormats ? "rgba8unorm-srgb" : "rgba8unorm"
2096
+ };
2097
+ }
2072
2098
  const nav = globalThis.navigator;
2073
2099
  const gpu = nav?.gpu;
2074
2100
  const getPreferred = gpu?.getPreferredCanvasFormat;
2075
2101
  if (gpu !== void 0 && typeof getPreferred === "function") {
2076
2102
  const storage = getPreferred.call(gpu);
2077
2103
  const view = storage === "rgba8unorm" ? "rgba8unorm-srgb" : storage === "bgra8unorm" ? "bgra8unorm-srgb" : storage;
2078
- return { storage, view };
2104
+ return {
2105
+ storage,
2106
+ view: surfaceViewFormats ? view : storage
2107
+ };
2079
2108
  }
2080
2109
  return {
2081
2110
  storage: "rgba8unorm",
2082
- view: "rgba8unorm-srgb",
2111
+ view: surfaceViewFormats ? "rgba8unorm-srgb" : "rgba8unorm",
2083
2112
  fallbackReason: "preferred-canvas-format-missing"
2084
2113
  };
2085
2114
  }
@@ -2616,6 +2645,7 @@ async function createAppFromCanvas(canvas, opts, bundler) {
2616
2645
  world,
2617
2646
  pluginContext,
2618
2647
  executionControl,
2648
+ onFrameSubmitted: (event) => publishBrowserFrameSubmitted(canvas, event),
2619
2649
  ...inputBackend !== void 0 ? { inputBackend } : {},
2620
2650
  ...inputHandle === void 0 ? {} : {
2621
2651
  wireOnLockErrorDispatch: (dispatch) => {
@@ -2646,6 +2676,7 @@ async function createAppFromCanvas(canvas, opts, bundler) {
2646
2676
  if (remoteHandle !== void 0) {
2647
2677
  Object.assign(buildArgs, { remoteHandle });
2648
2678
  }
2679
+ resetBrowserFrameSubmitted(canvas);
2649
2680
  const built = await buildApp(buildArgs);
2650
2681
  if (!built.ok) {
2651
2682
  await pluginContext.fiber.dispose();
@@ -2735,6 +2766,7 @@ function syncCameraAspect(world, canvasW, canvasH) {
2735
2766
  if (!r.ok) continue;
2736
2767
  if (r.value.autoAspect !== true) continue;
2737
2768
  if (r.value.projection !== CAMERA_PROJECTION_PERSPECTIVE) continue;
2769
+ if (r.value.aspect === Math.fround(aspect)) continue;
2738
2770
  world.set(entity, Camera, { aspect });
2739
2771
  }
2740
2772
  }
@@ -2938,6 +2970,10 @@ async function buildApp(args) {
2938
2970
  return;
2939
2971
  }
2940
2972
  rendererUnsubscribe = renderer.subscribe((event) => {
2973
+ if (event.kind === "frame-submitted") {
2974
+ args.onFrameSubmitted?.(event);
2975
+ return;
2976
+ }
2941
2977
  if (event.kind !== "error") return;
2942
2978
  const e = event.error;
2943
2979
  dispatch(e);
@@ -3113,8 +3149,8 @@ var LOAD_GAME_EXPECTED = Object.fromEntries(
3113
3149
  var LOAD_GAME_ERROR_HINTS = Object.fromEntries(
3114
3150
  Object.entries(loadGameErrorPolicy).map(([code, policy]) => [code, policy.hint])
3115
3151
  );
3116
- function isLoadGameError(err13) {
3117
- return err13 instanceof LoadGameErrorClass;
3152
+ function isLoadGameError(err14) {
3153
+ return err14 instanceof LoadGameErrorClass;
3118
3154
  }
3119
3155
  function isPlugin(value) {
3120
3156
  return typeof value === "function" || typeof value === "object" && value !== null && "apply" in value && typeof value.apply === "function";
@@ -3175,10 +3211,12 @@ async function createBrowserRhiDebugRuntime() {
3175
3211
  attachment,
3176
3212
  createShaderModule: backend.createShaderModule,
3177
3213
  rhiInstrumentation: createRhiInstrumentation(attachment),
3178
- createReplayDevice: async () => {
3214
+ createReplayDevice: async (tape) => {
3179
3215
  const adapter = await backend.rhi.requestAdapter();
3180
3216
  if (!adapter.ok) return adapter;
3181
- return adapter.value.requestDevice();
3217
+ return adapter.value.requestDevice(
3218
+ replayDeviceRequest(tape, adapter.value.features, adapter.value.limits)
3219
+ );
3182
3220
  },
3183
3221
  attachRenderer(_renderer) {
3184
3222
  return () => {
@@ -3316,6 +3354,11 @@ function resourceSubjectDigest(resource) {
3316
3354
  ];
3317
3355
  return candidates.find((value) => typeof value === "string" && value.length > 0);
3318
3356
  }
3357
+ function resourceSubjectDrawn(resource, drawCalls) {
3358
+ if (resource.kind === "texture") return drawCalls > 0;
3359
+ if (resource.kind === "material" || resource.kind === "mesh") return drawCalls > 2;
3360
+ return resource.observation !== void 0;
3361
+ }
3319
3362
  async function replayToolPreviewCapture(capture) {
3320
3363
  const analyzeStartedAtMs = performance.now();
3321
3364
  const tapeBytes = dataUriBytes(capture.tape.jsonUri);
@@ -3328,13 +3371,23 @@ async function replayToolPreviewCapture(capture) {
3328
3371
  detail: { phase: "tape-parse", cause: parsed.error }
3329
3372
  });
3330
3373
  }
3331
- const subjectDigest = resourceSubjectDigest(capture.resource);
3332
- if (capture.resource !== void 0 && subjectDigest === void 0) {
3374
+ const drawCalls = countDrawCalls(parsed.value);
3375
+ const resource = capture.resource !== void 0 && capture.resource.observation === void 0 && capture.resource.ownerFacts !== void 0 && resourceSubjectDrawn(capture.resource, drawCalls) ? { ...capture.resource, observation: capture.resource.ownerFacts } : capture.resource;
3376
+ const subjectDigest = resourceSubjectDigest(resource);
3377
+ if (resource !== void 0 && subjectDigest === void 0) {
3333
3378
  return err({
3334
3379
  code: "tool-preview-bootstrap-failed",
3335
3380
  expected: "the resource owner to publish a subject identity before evidence publication",
3336
3381
  hint: "repair AssetRegistry owner facts or post-frame observation; recipe and trace are not subject identity",
3337
- detail: { phase: "resource-identity" }
3382
+ detail: {
3383
+ phase: "resource-identity",
3384
+ cause: {
3385
+ kind: resource.kind,
3386
+ drawCalls,
3387
+ ownerFactsPublished: resource.ownerFacts !== void 0,
3388
+ observationPublished: resource.observation !== void 0
3389
+ }
3390
+ }
3338
3391
  });
3339
3392
  }
3340
3393
  let runtime;
@@ -3348,7 +3401,7 @@ async function replayToolPreviewCapture(capture) {
3348
3401
  detail: { phase: "replay-runtime", cause }
3349
3402
  });
3350
3403
  }
3351
- const replayDevice = await runtime.createReplayDevice();
3404
+ const replayDevice = await runtime.createReplayDevice(parsed.value);
3352
3405
  if (!replayDevice.ok) {
3353
3406
  return err({
3354
3407
  code: "tool-preview-capability-unavailable",
@@ -3370,7 +3423,6 @@ async function replayToolPreviewCapture(capture) {
3370
3423
  });
3371
3424
  }
3372
3425
  const replay = replayResult.value;
3373
- const drawCalls = countDrawCalls(parsed.value);
3374
3426
  if (drawCalls === 0) {
3375
3427
  await replay.dispose();
3376
3428
  return err({
@@ -3590,7 +3642,7 @@ async function replayToolPreviewCapture(capture) {
3590
3642
  }
3591
3643
  }
3592
3644
  },
3593
- ...capture.resource === void 0 ? {} : { resource: capture.resource }
3645
+ ...resource === void 0 ? {} : { resource }
3594
3646
  });
3595
3647
  }
3596
3648
  async function createToolPreviewHost(options) {
@@ -3743,7 +3795,7 @@ async function createToolPreviewHost(options) {
3743
3795
  });
3744
3796
  const executeStartedAtMs = performance.now();
3745
3797
  const actionTrace = [];
3746
- const encodedCapture = runtime.attachment.captureFrame();
3798
+ let encodedCapture;
3747
3799
  for (let frame = 0; frame < recipe.frames; frame += 1) {
3748
3800
  for (const action of recipe.actions.filter((candidate) => candidate.frame === frame)) {
3749
3801
  let accepted = false;
@@ -3767,6 +3819,17 @@ async function createToolPreviewHost(options) {
3767
3819
  }
3768
3820
  actionTrace.push(action);
3769
3821
  }
3822
+ if (frame === recipe.frames - 1) {
3823
+ encodedCapture = runtime.attachment.captureFrame();
3824
+ const snapshot = await runtime.attachment.frameBoundary();
3825
+ if (!snapshot.ok)
3826
+ return err({
3827
+ code: "tool-preview-bootstrap-failed",
3828
+ expected: "RHI-debug snapshots live resources before the captured frame",
3829
+ hint: snapshot.error.hint,
3830
+ detail: { phase: "rhi-snapshot", cause: snapshot.error }
3831
+ });
3832
+ }
3770
3833
  const stepped = app.stepFrame(recipe.deltaSeconds);
3771
3834
  if (!stepped.ok)
3772
3835
  return err({
@@ -3776,9 +3839,14 @@ async function createToolPreviewHost(options) {
3776
3839
  });
3777
3840
  await new Promise((resolve) => setTimeout(resolve, 0));
3778
3841
  }
3779
- await runtime.attachment.frameBoundary();
3780
- await new Promise((resolve) => setTimeout(resolve, 0));
3781
- await runtime.attachment.frameBoundary();
3842
+ const finalized = await runtime.attachment.frameBoundary();
3843
+ if (!finalized.ok)
3844
+ return err({
3845
+ code: "tool-preview-bootstrap-failed",
3846
+ expected: "RHI-debug finalizes the captured frame",
3847
+ hint: finalized.error.hint,
3848
+ detail: { phase: "rhi-finalize", cause: finalized.error }
3849
+ });
3782
3850
  if (options.collectResourceFacts !== void 0) {
3783
3851
  try {
3784
3852
  const collected = await options.collectResourceFacts(app, resourceFacts);
@@ -3807,6 +3875,14 @@ async function createToolPreviewHost(options) {
3807
3875
  detail: { phase: "profiler-finish", cause: profile.error }
3808
3876
  });
3809
3877
  }
3878
+ if (encodedCapture === void 0) {
3879
+ return err({
3880
+ code: "tool-preview-bootstrap-failed",
3881
+ expected: "a positive-frame recipe to arm one captured frame",
3882
+ hint: "validate the recipe before running the preview host",
3883
+ detail: { phase: "rhi-capture" }
3884
+ });
3885
+ }
3810
3886
  const tape = await encodedCapture;
3811
3887
  if (!tape.ok)
3812
3888
  return err({
@@ -3946,6 +4022,6 @@ function fitToolPreviewCameraToAabb(aabb, options) {
3946
4022
  };
3947
4023
  }
3948
4024
 
3949
- export { APP_ERROR_HINTS, APP_EXPECTED, APP_PHASE_CATALOG, AppError, EXECUTION_CAPABILITY_NAMES, EXECUTION_REPORT_SCHEMA_VERSION, EXECUTION_REQUESTED_TIERS, EXECUTION_TIERS, LOAD_GAME_ERROR_HINTS, LOAD_GAME_EXPECTED, LoadGameError, createApp, createExecutionReport, createToolPreviewEvidence, createToolPreviewHost, createToolPreviewRecipe, ensureFallbackCamera, executionBootstrapHostPlugin, fitToolPreviewCameraToAabb, gameHostPlugin, inputPlugin, isAppError, isExecutionReport, isLoadGameError, joinResourcePreviewEvidence, joinToolPreviewEvidence, loadBootstrapEntry, loadGame, measureCanvasDrawingBuffer, missingExecutionCapabilities, ownedRendererPlugin, prepareBootstrapEntry, probeExecutionCapabilities, renderFeatureHostPlugin, renderFeaturePlugin, rendererPlugin, replayToolPreviewCapture, selectExecutionTier, syncCanvasDrawingBuffer, unavailableExecutionCapabilities, validateExecutionBootstrapData, validateToolPreviewTrace };
4025
+ export { APP_ERROR_HINTS, APP_EXPECTED, APP_PHASE_CATALOG, AppError, EXECUTION_CAPABILITY_NAMES, EXECUTION_REPORT_SCHEMA_VERSION, EXECUTION_REQUESTED_TIERS, EXECUTION_TIERS, FORGEAX_FRAME_SUBMITTED_DATASET, FORGEAX_FRAME_SUBMITTED_EVENT, LOAD_GAME_ERROR_HINTS, LOAD_GAME_EXPECTED, LoadGameError, createApp, createExecutionReport, createToolPreviewEvidence, createToolPreviewHost, createToolPreviewRecipe, ensureFallbackCamera, executionBootstrapHostPlugin, fitToolPreviewCameraToAabb, gameHostPlugin, inputPlugin, isAppError, isExecutionReport, isLoadGameError, joinResourcePreviewEvidence, joinToolPreviewEvidence, loadBootstrapEntry, loadGame, measureCanvasDrawingBuffer, missingExecutionCapabilities, ownedRendererPlugin, prepareBootstrapEntry, probeExecutionCapabilities, publishBrowserFrameSubmitted, renderFeatureHostPlugin, renderFeaturePlugin, rendererPlugin, replayToolPreviewCapture, resetBrowserFrameSubmitted, selectExecutionTier, syncCanvasDrawingBuffer, unavailableExecutionCapabilities, validateExecutionBootstrapData, validateToolPreviewTrace };
3950
4026
  //# sourceMappingURL=index.mjs.map
3951
4027
  //# sourceMappingURL=index.mjs.map