@forgeax/engine-runtime 0.1.28 → 0.1.29

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 (113) hide show
  1. package/README.md +98 -0
  2. package/dist/collect-scene-asset.d.ts.map +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.mjs +480 -15
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/scene-utils/mount-override-fold.d.ts +3 -1
  8. package/dist/scene-utils/mount-override-fold.d.ts.map +1 -1
  9. package/package.json +37 -32
  10. package/src/__tests__/animation-graph-collect-roundtrip.test.ts +4 -2
  11. package/src/__tests__/asset-registry-aabb.cases.unit.test.ts +5 -4
  12. package/src/__tests__/asset-registry-guid-reverse.test.ts +22 -27
  13. package/src/__tests__/asset-registry-mounts.test.ts +174 -734
  14. package/src/__tests__/asset-registry-scene.cases.unit.test.ts +9 -18
  15. package/src/__tests__/asset-registry.recursive.spec.ts +23 -29
  16. package/src/__tests__/camera-ortho.cases.unit.test.ts +12 -3
  17. package/src/__tests__/collect-scene-asset.test.ts +96 -113
  18. package/src/__tests__/collect-scene-mount-collapse.test.ts +64 -66
  19. package/src/__tests__/collect-transient-children-absent.test.ts +11 -17
  20. package/src/__tests__/collect-transient-roundtrip.test.ts +12 -13
  21. package/src/__tests__/collect-transient-skip.test.ts +20 -27
  22. package/src/__tests__/components.test-d.ts +17 -1
  23. package/src/__tests__/components.unit.test.ts +75 -27
  24. package/src/__tests__/composite-skybox-cross-world.browser.test.ts +4 -2
  25. package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +8 -4
  26. package/src/__tests__/dawn/instances-shadow.dawn.test.ts +4 -1
  27. package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +66 -92
  28. package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +200 -4
  29. package/src/__tests__/errors.unit.test.ts +13 -0
  30. package/src/__tests__/feature-depth-material.dawn.test.ts +23 -0
  31. package/src/__tests__/feature-depth-material.fixture.ts +473 -0
  32. package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +14 -14
  33. package/src/__tests__/geometry.unit.test.ts +42 -43
  34. package/src/__tests__/gpu-resource-release.cases.unit.test.ts +16 -9
  35. package/src/__tests__/handle-migration.test.ts +0 -1
  36. package/src/__tests__/instances-renderer-lifecycle.integration.test.ts +20 -14
  37. package/src/__tests__/instances.test-d.ts +13 -52
  38. package/src/__tests__/instantiate-mount-deferred-wiring.test.ts +42 -80
  39. package/src/__tests__/lazy-catalog.cases.unit.test.ts +17 -3
  40. package/src/__tests__/loader-registry.cases.unit.test.ts +4 -4
  41. package/src/__tests__/material-publication.browser.test.ts +29 -0
  42. package/src/__tests__/material-publication.commands.ts +23 -0
  43. package/src/__tests__/material-publication.dawn.test.ts +24 -0
  44. package/src/__tests__/material-publication.fixture.ts +349 -0
  45. package/src/__tests__/material-publication.server.ts +109 -0
  46. package/src/__tests__/parse-scene-payload-refs.cases.unit.test.ts +73 -96
  47. package/src/__tests__/pbr-pipeline.unit.test.ts +25 -11
  48. package/src/__tests__/point-light-shadow.browser.test.ts +8 -2
  49. package/src/__tests__/point-light-shadow.unit.test.ts +37 -8
  50. package/src/__tests__/record-all-topology.cases.unit.test.ts +52 -0
  51. package/src/__tests__/record-draw-branch.cases.unit.test.ts +1 -0
  52. package/src/__tests__/record-worldid-isolation.test.ts +47 -28
  53. package/src/__tests__/render-error-exhaustive.test-d.ts +24 -0
  54. package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +15 -4
  55. package/src/__tests__/render-system-extract.test.ts +61 -0
  56. package/src/__tests__/render-system-extract.unit.test.ts +21 -7
  57. package/src/__tests__/render-system-fold-bucket.unit.test.ts +33 -1
  58. package/src/__tests__/render-system-fold-material.unit.test.ts +32 -6
  59. package/src/__tests__/render-system-fold-modegate.unit.test.ts +26 -4
  60. package/src/__tests__/render-system-fold.dawn.test.ts +4 -1
  61. package/src/__tests__/render-system-mega.test.ts +50 -62
  62. package/src/__tests__/renderer-device-loss-provider-feasibility.dawn.test.ts +0 -1
  63. package/src/__tests__/renderer-device-loss-recovery.dawn.test.ts +0 -3
  64. package/src/__tests__/renderer-draw-world.cases.unit.test.ts +16 -513
  65. package/src/__tests__/resolve-scene-guids.cases.unit.test.ts +48 -43
  66. package/src/__tests__/rhi-null-command-flow.unit.test.ts +9 -7
  67. package/src/__tests__/roots-to-scene-asset.test.ts +27 -28
  68. package/src/__tests__/roundtrip-primitive.test.ts +24 -23
  69. package/src/__tests__/scene-equirect-roundtrip.test.ts +5 -16
  70. package/src/__tests__/scene-instantiate-cycle-runtime.test.ts +13 -29
  71. package/src/__tests__/scene-mount-fixed-point.test.ts +104 -227
  72. package/src/__tests__/scene-mount-roundtrip.test.ts +95 -961
  73. package/src/__tests__/scene-mount-wrapper-roundtrip.test.ts +36 -182
  74. package/src/__tests__/scene-sugar-instantiate.test.ts +20 -15
  75. package/src/__tests__/serialize-mounts-roundtrip.test.ts +108 -492
  76. package/src/__tests__/serialize-scene-asset.test.ts +36 -50
  77. package/src/__tests__/shadow-caster-empty-schema.test.ts +3 -1
  78. package/src/__tests__/shadow-fields-observable.dawn.test.ts +34 -19
  79. package/src/__tests__/shadow-mesh-ssbo-order.test.ts +5 -12
  80. package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +6 -4
  81. package/src/__tests__/skin-m2-extract-happy.cases.unit.test.ts +50 -2
  82. package/src/__tests__/skin-m2-extract-joint-error.cases.unit.test.ts +50 -2
  83. package/src/__tests__/skin-m2-extract-null-assets.cases.unit.test.ts +50 -2
  84. package/src/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.ts +50 -2
  85. package/src/__tests__/skin-palette-extract.cases.unit.test.ts +2 -0
  86. package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +93 -48
  87. package/src/__tests__/skylight-pipeline-layout.cases.unit.test.ts +6 -2
  88. package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +1 -1
  89. package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -3
  90. package/src/__tests__/template-game-default-api-contract.test.ts +21 -21
  91. package/src/__tests__/tonemap-hdr-target.cases.unit.test.ts +8 -5
  92. package/src/__tests__/tonemap-pipeline-split.cases.unit.test.ts +3 -0
  93. package/src/__tests__/transient-fields-collect.test.ts +4 -4
  94. package/src/__tests__/transient-runtime-behavior.test.ts +9 -9
  95. package/src/__tests__/vfx-depth-material.dawn.test.ts +42 -0
  96. package/src/__tests__/vfx-mesh-lighting.browser.test.ts +16 -0
  97. package/src/__tests__/vfx-mesh-lighting.commands.ts +14 -0
  98. package/src/__tests__/vfx-mesh-lighting.dawn.test.ts +16 -0
  99. package/src/__tests__/vfx-mesh-lighting.fixture.ts +883 -0
  100. package/src/__tests__/wave1-rendering-materials.browser.test.ts +617 -0
  101. package/src/__tests__/wave1-rendering-p0.browser.test.ts +751 -0
  102. package/src/__tests__/wave1-rendering-recovery.browser.test.ts +666 -0
  103. package/src/__tests__/wave1-shadow-diagnostic.browser.test.ts +582 -0
  104. package/src/__tests__/world-scene-despawn.test.ts +8 -8
  105. package/src/__tests__/world-scene-instantiate.test.ts +23 -29
  106. package/src/__tests__/world-scene-nested.test.ts +90 -163
  107. package/src/__tests__/world-scene-override.test.ts +17 -16
  108. package/src/__tests__/zero-compression-zero-load.integration.test.ts +1 -0
  109. package/src/collect-scene-asset.ts +637 -19
  110. package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
  111. package/src/index.ts +1 -0
  112. package/src/scene-utils/mount-override-fold.ts +5 -14
  113. package/src/__tests__/node-shims.d.ts +0 -31
@@ -12,6 +12,7 @@ import type { Renderer as RendererType } from '@forgeax/engine-render';
12
12
  import {
13
13
  Camera,
14
14
  DirectionalLight,
15
+ InstanceCollectionStore,
15
16
  Instances,
16
17
  MeshFilter,
17
18
  MeshRenderer,
@@ -213,6 +214,7 @@ function drawPublished(renderer: RendererType, world: WorldType) {
213
214
  },
214
215
  setVertexBuffer: () => undefined,
215
216
  setIndexBuffer: () => undefined,
217
+ setViewport: () => undefined,
216
218
  setBindGroup: (groupIndex: number, _bg: unknown, dynamicOffsets?: number[]) => {
217
219
  log.setBindGroupCount++;
218
220
  // w8-A: capture mesh uniform buffer offset at group=2.
@@ -297,6 +299,7 @@ function drawPublished(renderer: RendererType, world: WorldType) {
297
299
  materialShaders: [
298
300
  materialShaderStub('forgeax::default-standard-pbr'),
299
301
  materialShaderStub('forgeax::default-unlit'),
302
+ materialShaderStub('forgeax::default-shadow-caster'),
300
303
  ],
301
304
  };
302
305
  return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
@@ -435,7 +438,10 @@ function drawPublished(renderer: RendererType, world: WorldType) {
435
438
  drawPublished(renderer, world as WorldType);
436
439
 
437
440
  // DirectionalLight with merged shadow fields.
438
- expect(errors).toHaveLength(0);
441
+ expect(
442
+ errors,
443
+ JSON.stringify(errors, (_key, value) => (value instanceof Error ? value.stack : value)),
444
+ ).toHaveLength(0);
439
445
  // Standard Cluster owns the shadow, G-buffer, lighting, forward, and
440
446
  // output topology stages. Keep this structural rather than freezing a
441
447
  // pass-count literal that changes when a named stage is split.
@@ -1523,33 +1529,16 @@ describe('RenderSystem Skylight extract + record phase contract (plan-strategy D
1523
1529
  });
1524
1530
  });
1525
1531
 
1526
- // ─── from render-system-stride.test.ts ───
1532
+ // ─── from render-system-instance-collection.test.ts ───
1527
1533
  {
1528
- // w14 - RenderSystem extract entry stride defensive test (M3, AC-06).
1529
- //
1530
- // Locks the AC-06 invariant: when an entity carries an `Instances` component
1531
- // whose `transforms` array<f32> snapshot has a length that is NOT a multiple
1532
- // of 16, the RenderSystem extract stage MUST emit a structured `EcsError`
1533
- // with `code: 'instance-transforms-stride-mismatch'` BEFORE the GPU upload
1534
- // path is reached. The error carries a discriminated `.detail` per
1535
- // `EcsErrorDetail`:
1536
- //
1537
- // detail = { actualLength: number, expectedStride: 16 }
1534
+ // Renderer-owned collection authoring and extract boundary coverage.
1538
1535
  //
1539
- // Coverage matrix:
1540
- // (a) length === 16 -> pass; no error fires; renderable lands.
1541
- // (b) length === 32 -> pass; no error fires; renderable lands.
1542
- // (c) length === 15 -> fail; structured error fires with detail; no
1543
- // renderable lands.
1544
- // (d) length === 17 -> fail; structured error fires with detail; no
1545
- // renderable lands.
1536
+ // The ECS component carries only a collection id. The renderer validates the
1537
+ // canonical packed matrix payload before publication and reports a typed
1538
+ // InstanceCollectionError; malformed input never reaches GPU upload.
1546
1539
  //
1547
- // The defensive lives in `packages/runtime/src/render-system-extract.ts`
1548
- // inside the `Instances` archetype loop, immediately after the
1549
- // `world.get(entity, Instances)` snapshot is extracted but BEFORE any
1550
- // per-element copy / GPU upload. Violation routes through Layer-3
1551
- // `World._errorHandler` (set via `world.setErrorHandler`); the renderable
1552
- // MUST NOT be pushed (fail-fast halts the per-entity branch).
1540
+ // Coverage keeps legal collection association visible and rejects malformed
1541
+ // stride/range input with the Render-owned error facts.
1553
1542
 
1554
1543
  interface CollectedError {
1555
1544
  readonly code: string;
@@ -1564,7 +1553,11 @@ describe('RenderSystem Skylight extract + record phase contract (plan-strategy D
1564
1553
  return { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] };
1565
1554
  }
1566
1555
 
1567
- function makeWorld(): { world: World; collected: CollectedError[] } {
1556
+ function makeWorld(): {
1557
+ world: World;
1558
+ collected: CollectedError[];
1559
+ instanceCollections: InstanceCollectionStore;
1560
+ } {
1568
1561
  const collected: CollectedError[] = [];
1569
1562
  const world = new World();
1570
1563
  vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
@@ -1599,10 +1592,15 @@ describe('RenderSystem Skylight extract + record phase contract (plan-strategy D
1599
1592
  },
1600
1593
  })
1601
1594
  .unwrap();
1602
- return { world, collected };
1595
+ return { world, collected, instanceCollections: new InstanceCollectionStore() };
1603
1596
  }
1604
1597
 
1605
- function spawnInstancedEntity(world: World, transforms: Float32Array): void {
1598
+ function spawnInstancedEntity(
1599
+ world: World,
1600
+ instanceCollections: InstanceCollectionStore,
1601
+ transforms: Float32Array,
1602
+ ): void {
1603
+ const collection = instanceCollections.create({ transforms }).unwrap();
1606
1604
  world
1607
1605
  .spawn(
1608
1606
  { component: Transform, data: makeIdentityTransform() },
@@ -1614,58 +1612,48 @@ describe('RenderSystem Skylight extract + record phase contract (plan-strategy D
1614
1612
  component: MeshRenderer,
1615
1613
  data: {} as never,
1616
1614
  },
1617
- { component: Instances, data: { transforms } },
1615
+ { component: Instances, data: { collectionId: collection.collectionId } },
1618
1616
  )
1619
1617
  .unwrap();
1620
1618
  }
1621
1619
 
1622
- describe('render-system-extract stride defensive (w14, AC-06)', () => {
1620
+ describe('renderer-owned instance collection validation', () => {
1623
1621
  it('(a) length === 16: no error; renderable lands', () => {
1624
- const { world, collected } = makeWorld();
1622
+ const { world, collected, instanceCollections } = makeWorld();
1625
1623
  const transforms = new Float32Array(16);
1626
- spawnInstancedEntity(world, transforms);
1627
- const frame = extractFrame(world, prepareExtractContext(world));
1628
- expect(collected.filter((c) => c.code === 'instance-transforms-stride-mismatch').length).toBe(
1629
- 0,
1630
- );
1624
+ spawnInstancedEntity(world, instanceCollections, transforms);
1625
+ const frame = extractFrame(world, prepareExtractContext(world, { instanceCollections }));
1626
+ expect(collected).toHaveLength(0);
1631
1627
  expect(frame.renderables.length).toBe(1);
1632
1628
  });
1633
1629
 
1634
1630
  it('(b) length === 32: no error; renderable lands', () => {
1635
- const { world, collected } = makeWorld();
1631
+ const { world, collected, instanceCollections } = makeWorld();
1636
1632
  const transforms = new Float32Array(32);
1637
- spawnInstancedEntity(world, transforms);
1638
- const frame = extractFrame(world, prepareExtractContext(world));
1639
- expect(collected.filter((c) => c.code === 'instance-transforms-stride-mismatch').length).toBe(
1640
- 0,
1641
- );
1633
+ spawnInstancedEntity(world, instanceCollections, transforms);
1634
+ const frame = extractFrame(world, prepareExtractContext(world, { instanceCollections }));
1635
+ expect(collected).toHaveLength(0);
1642
1636
  expect(frame.renderables.length).toBe(1);
1643
1637
  });
1644
1638
 
1645
- it('(c) length === 15: structured error fires; detail.actualLength === 15; expectedStride === 16; no renderable', () => {
1646
- const { world, collected } = makeWorld();
1639
+ it('(c) length === 15: structured authoring error carries typed facts', () => {
1640
+ const { instanceCollections } = makeWorld();
1647
1641
  const transforms = new Float32Array(15);
1648
- spawnInstancedEntity(world, transforms);
1649
- const frame = extractFrame(world, prepareExtractContext(world));
1650
- const stride = collected.filter((c) => c.code === 'instance-transforms-stride-mismatch');
1651
- expect(stride.length).toBe(1);
1652
- const detail = stride[0]?.detail as { actualLength: number; expectedStride: 16 };
1653
- expect(detail.actualLength).toBe(15);
1654
- expect(detail.expectedStride).toBe(16);
1655
- expect(frame.renderables.length).toBe(0);
1642
+ const result = instanceCollections.create({ transforms });
1643
+ expect(result.ok).toBe(false);
1644
+ if (result.ok) return;
1645
+ expect(result.error.code).toBe('instance-collection-invalid');
1646
+ expect(result.error.detail?.actual).toBe(15);
1656
1647
  });
1657
1648
 
1658
- it('(d) length === 17: structured error fires; detail.actualLength === 17; expectedStride === 16; no renderable', () => {
1659
- const { world, collected } = makeWorld();
1649
+ it('(d) length === 17: structured authoring error carries typed facts', () => {
1650
+ const { instanceCollections } = makeWorld();
1660
1651
  const transforms = new Float32Array(17);
1661
- spawnInstancedEntity(world, transforms);
1662
- const frame = extractFrame(world, prepareExtractContext(world));
1663
- const stride = collected.filter((c) => c.code === 'instance-transforms-stride-mismatch');
1664
- expect(stride.length).toBe(1);
1665
- const detail = stride[0]?.detail as { actualLength: number; expectedStride: 16 };
1666
- expect(detail.actualLength).toBe(17);
1667
- expect(detail.expectedStride).toBe(16);
1668
- expect(frame.renderables.length).toBe(0);
1652
+ const result = instanceCollections.create({ transforms });
1653
+ expect(result.ok).toBe(false);
1654
+ if (result.ok) return;
1655
+ expect(result.error.code).toBe('instance-collection-invalid');
1656
+ expect(result.error.detail?.actual).toBe(17);
1669
1657
  });
1670
1658
  });
1671
1659
  }
@@ -112,7 +112,6 @@ describe('real Dawn device-loss provider feasibility', () => {
112
112
  'No test-visible non-destroyed loss injection method is exposed by the real Dawn GPU, adapter, or device objects',
113
113
  providerSurface,
114
114
  };
115
- console.log(`[dawn-loss-feasibility] ${JSON.stringify(evidence)}`);
116
115
  expect(evidence.submittedWork).toBe(true);
117
116
  expect(['available', 'insufficient-evidence']).toContain(evidence.status);
118
117
  if (evidence.status === 'available') {
@@ -39,9 +39,6 @@ describe('real Dawn renderer device-loss recovery', () => {
39
39
  'A non-destroyed raw loss was observed, but this fixture does not exercise public Renderer recovery',
40
40
  );
41
41
  }
42
- console.log(
43
- `[dawn-loss-recovery] ${JSON.stringify({ ...evidence, rawLoss: rawLoss ?? 'provider-missing' })}`,
44
- );
45
42
  expect(evidence.submittedWork).toBe(true);
46
43
  expect(evidence.worlds).toHaveLength(2);
47
44
  expect(evidence.status).toBe('insufficient-evidence');