@forgeax/engine-render 0.1.34 → 0.1.36

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 (273) hide show
  1. package/README.md +69 -3
  2. package/dist/assembly/device-feature-admission.d.ts +2 -2
  3. package/dist/assembly/host-contract.d.ts +3 -2
  4. package/dist/assembly/host-contract.d.ts.map +1 -1
  5. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  6. package/dist/assembly/render-target-host.d.ts +5 -0
  7. package/dist/assembly/render-target-host.d.ts.map +1 -1
  8. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  9. package/dist/assembly/webgpu-renderer.d.ts +1 -0
  10. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  11. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  12. package/dist/authoring.mjs +2 -4
  13. package/dist/authoring.mjs.map +1 -1
  14. package/dist/chunk-2OZAS3WU.mjs +3489 -0
  15. package/dist/chunk-2OZAS3WU.mjs.map +1 -0
  16. package/dist/{chunk-UI5SFODX.mjs → chunk-BZQ4CDRF.mjs} +2338 -1991
  17. package/dist/chunk-BZQ4CDRF.mjs.map +1 -0
  18. package/dist/{chunk-MKDIT2EM.mjs → chunk-CJOV2XLO.mjs} +216 -261
  19. package/dist/chunk-CJOV2XLO.mjs.map +1 -0
  20. package/dist/chunk-D6WSBXJ2.mjs +1842 -0
  21. package/dist/chunk-D6WSBXJ2.mjs.map +1 -0
  22. package/dist/{chunk-HEB564WF.mjs → chunk-DRF6OULX.mjs} +4 -4
  23. package/dist/{chunk-HEB564WF.mjs.map → chunk-DRF6OULX.mjs.map} +1 -1
  24. package/dist/{chunk-OCDLEI6I.mjs → chunk-OB5WAFYH.mjs} +117 -5
  25. package/dist/chunk-OB5WAFYH.mjs.map +1 -0
  26. package/dist/{chunk-ODZWR6JQ.mjs → chunk-OEYKKMXG.mjs} +2042 -27
  27. package/dist/chunk-OEYKKMXG.mjs.map +1 -0
  28. package/dist/chunk-RIPAEGI6.mjs +48 -0
  29. package/dist/chunk-RIPAEGI6.mjs.map +1 -0
  30. package/dist/{chunk-ELJFJ2BU.mjs → chunk-X2KA6WHM.mjs} +29 -4
  31. package/dist/chunk-X2KA6WHM.mjs.map +1 -0
  32. package/dist/{chunk-BLXGONR2.mjs → chunk-XSFB7XXQ.mjs} +51 -4
  33. package/dist/chunk-XSFB7XXQ.mjs.map +1 -0
  34. package/dist/cloud/frame-context.d.ts +2 -2
  35. package/dist/cloud/frame-context.d.ts.map +1 -1
  36. package/dist/construct-renderer.mjs +936 -2140
  37. package/dist/construct-renderer.mjs.map +1 -1
  38. package/dist/device/device-scope.d.ts.map +1 -1
  39. package/dist/device/gpu-residency.d.ts +19 -19
  40. package/dist/device/gpu-residency.d.ts.map +1 -1
  41. package/dist/errors/render.d.ts +3 -2
  42. package/dist/errors/render.d.ts.map +1 -1
  43. package/dist/extract/material-context.d.ts +7 -0
  44. package/dist/extract/material-context.d.ts.map +1 -0
  45. package/dist/features/host.d.ts +5 -0
  46. package/dist/features/host.d.ts.map +1 -1
  47. package/dist/features/particle-mesh-layout.d.ts +19 -0
  48. package/dist/features/particle-mesh-layout.d.ts.map +1 -0
  49. package/dist/features/plan.d.ts +2 -0
  50. package/dist/features/plan.d.ts.map +1 -1
  51. package/dist/features/types.d.ts +8 -0
  52. package/dist/features/types.d.ts.map +1 -1
  53. package/dist/glyph-text-layout-system.d.ts +1 -1
  54. package/dist/glyph-text-layout-system.d.ts.map +1 -1
  55. package/dist/gpu-driven/production-raster.d.ts +1 -1
  56. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  57. package/dist/gpu-driven/shadow-ownership.d.ts +3 -3
  58. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -1
  59. package/dist/gpu-driven/shadow-views.d.ts +1 -1
  60. package/dist/gpu-driven/shadow-views.d.ts.map +1 -1
  61. package/dist/gpu-driven/view-gpu.d.ts +1 -1
  62. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  63. package/dist/ibl/frame-resources.d.ts.map +1 -1
  64. package/dist/ibl/skylight-bind-group.d.ts +4 -4
  65. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  66. package/dist/index.d.ts +3 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.mjs +542 -9
  69. package/dist/index.mjs.map +1 -1
  70. package/dist/instances.d.ts +3 -4
  71. package/dist/instances.d.ts.map +1 -1
  72. package/dist/internal.mjs +7 -8
  73. package/dist/internal.mjs.map +1 -1
  74. package/dist/pbr-pipeline.d.ts +3 -3
  75. package/dist/pbr-pipeline.d.ts.map +1 -1
  76. package/dist/pipeline/standard-output/lut-gpu.d.ts +2 -2
  77. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -1
  78. package/dist/pipeline/standard-output/resources.d.ts +2 -2
  79. package/dist/pipeline/standard-output/resources.d.ts.map +1 -1
  80. package/dist/publication/contract.d.ts +77 -0
  81. package/dist/publication/contract.d.ts.map +1 -0
  82. package/dist/publication/dependencies.d.ts +7 -0
  83. package/dist/publication/dependencies.d.ts.map +1 -0
  84. package/dist/publication/prepare-geometry.d.ts +9 -0
  85. package/dist/publication/prepare-geometry.d.ts.map +1 -0
  86. package/dist/publication/programs.d.ts +5 -0
  87. package/dist/publication/programs.d.ts.map +1 -0
  88. package/dist/publication/publisher.d.ts +34 -0
  89. package/dist/publication/publisher.d.ts.map +1 -0
  90. package/dist/publication/receiver.d.ts +29 -0
  91. package/dist/publication/receiver.d.ts.map +1 -0
  92. package/dist/publication/resource-scope.d.ts +18 -0
  93. package/dist/publication/resource-scope.d.ts.map +1 -0
  94. package/dist/publication/targets.d.ts +33 -0
  95. package/dist/publication/targets.d.ts.map +1 -0
  96. package/dist/publication/video.d.ts +5 -0
  97. package/dist/publication/video.d.ts.map +1 -0
  98. package/dist/record/fold-buckets.d.ts +3 -3
  99. package/dist/record/fold-buckets.d.ts.map +1 -1
  100. package/dist/record/frame-snapshot.d.ts +6 -6
  101. package/dist/record/frame-snapshot.d.ts.map +1 -1
  102. package/dist/record/frame.d.ts +5 -4
  103. package/dist/record/frame.d.ts.map +1 -1
  104. package/dist/record/helpers.d.ts +2 -2
  105. package/dist/record/helpers.d.ts.map +1 -1
  106. package/dist/record/main-pass-geometry.d.ts +2 -2
  107. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  108. package/dist/record/main-pass-material.d.ts +7 -9
  109. package/dist/record/main-pass-material.d.ts.map +1 -1
  110. package/dist/record/main-pass-sprite-draws.d.ts +3 -16
  111. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  112. package/dist/record/main-pass.d.ts.map +1 -1
  113. package/dist/record/prepared-material-bindings.d.ts +2 -3
  114. package/dist/record/prepared-material-bindings.d.ts.map +1 -1
  115. package/dist/record/render-context.d.ts +4 -2
  116. package/dist/record/render-context.d.ts.map +1 -1
  117. package/dist/record/shadow-pass.d.ts +2 -2
  118. package/dist/record/shadow-pass.d.ts.map +1 -1
  119. package/dist/record/sprite-instance-buffer.d.ts +31 -0
  120. package/dist/record/sprite-instance-buffer.d.ts.map +1 -0
  121. package/dist/record/target-capture-graph.d.ts +19 -0
  122. package/dist/record/target-capture-graph.d.ts.map +1 -0
  123. package/dist/record/typed-frame-graph.d.ts +2 -13
  124. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  125. package/dist/record/view-ubo.d.ts.map +1 -1
  126. package/dist/record/volume-params.d.ts +2 -2
  127. package/dist/record/volume-params.d.ts.map +1 -1
  128. package/dist/recovery/render-system-candidate.d.ts +5 -12
  129. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  130. package/dist/render-contract.d.ts +4 -1
  131. package/dist/render-contract.d.ts.map +1 -1
  132. package/dist/render-system-extract-tail.d.ts.map +1 -1
  133. package/dist/render-system-extract.d.ts +2 -0
  134. package/dist/render-system-extract.d.ts.map +1 -1
  135. package/dist/render-system-presentation.d.ts +2 -2
  136. package/dist/render-system-presentation.d.ts.map +1 -1
  137. package/dist/render-system-projections.d.ts +3 -2
  138. package/dist/render-system-projections.d.ts.map +1 -1
  139. package/dist/render-system-residency.d.ts +2 -2
  140. package/dist/render-system-residency.d.ts.map +1 -1
  141. package/dist/render-system.d.ts +3 -7
  142. package/dist/render-system.d.ts.map +1 -1
  143. package/dist/scene/render-scene.d.ts +8 -4
  144. package/dist/scene/render-scene.d.ts.map +1 -1
  145. package/dist/scene/render-source.d.ts +42 -0
  146. package/dist/scene/render-source.d.ts.map +1 -0
  147. package/dist/scene/source-systems.d.ts +6 -0
  148. package/dist/scene/source-systems.d.ts.map +1 -0
  149. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  150. package/dist/systems/skin-palette-types.d.ts +10 -0
  151. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  152. package/dist/systems/transparent-dispatch.d.ts +2 -2
  153. package/dist/systems/transparent-dispatch.d.ts.map +1 -1
  154. package/dist/systems/transparent-sort-config.d.ts +2 -1
  155. package/dist/systems/transparent-sort-config.d.ts.map +1 -1
  156. package/dist/temporal/index.mjs +6 -7
  157. package/dist/typed-shadow-passes.d.ts.map +1 -1
  158. package/package.json +21 -21
  159. package/src/__tests__/auto-exposure-transaction.integration.test.ts +4 -9
  160. package/src/__tests__/device-feature-admission.unit.test.ts +3 -2
  161. package/src/__tests__/device-scope-lifecycle.unit.test.ts +26 -0
  162. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +3 -4
  163. package/src/__tests__/gpu-driven-production.integration.test.ts +18 -1
  164. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +23 -0
  165. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +4 -1
  166. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +0 -1
  167. package/src/__tests__/material-vertex-texture.browser.test.ts +7 -0
  168. package/src/__tests__/material-vertex-texture.dawn.test.ts +7 -0
  169. package/src/__tests__/material-vertex-texture.fixture.ts +31 -0
  170. package/src/__tests__/materials-standard-contract.unit.test.ts +0 -1
  171. package/src/__tests__/physical-clearcoat.integration.test.ts +23 -23
  172. package/src/__tests__/physical-material-budget.unit.test.ts +40 -0
  173. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +5 -0
  174. package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
  175. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +107 -5
  176. package/src/__tests__/runtime-material-program.integration.test.ts +79 -0
  177. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +23 -3
  178. package/src/assembly/device-feature-admission.ts +2 -2
  179. package/src/assembly/host-contract.ts +5 -2
  180. package/src/assembly/recovery/renderer-recover.ts +9 -0
  181. package/src/assembly/render-target-host.ts +57 -47
  182. package/src/assembly/renderer-facade.ts +3 -1
  183. package/src/assembly/webgpu-ready.ts +1 -1
  184. package/src/assembly/webgpu-renderer.ts +101 -70
  185. package/src/assembly/webgpu-vertex-layouts.ts +2 -9
  186. package/src/cloud/frame-context.ts +2 -2
  187. package/src/device/device-scope.ts +3 -0
  188. package/src/device/gpu-residency.ts +26 -25
  189. package/src/errors/render.ts +3 -1
  190. package/src/extract/material-context.ts +21 -0
  191. package/src/features/host.ts +13 -5
  192. package/src/features/particle-mesh-layout.ts +25 -0
  193. package/src/features/plan.ts +2 -0
  194. package/src/features/types.ts +8 -0
  195. package/src/glyph-text-layout-system.ts +2 -2
  196. package/src/gpu-driven/production-raster.ts +9 -3
  197. package/src/gpu-driven/shadow-ownership.ts +3 -3
  198. package/src/gpu-driven/shadow-views.ts +27 -2
  199. package/src/gpu-driven/view-gpu.ts +5 -0
  200. package/src/ibl/frame-resources.ts +0 -1
  201. package/src/ibl/skylight-bind-group.ts +13 -25
  202. package/src/index.ts +9 -0
  203. package/src/instances.ts +9 -6
  204. package/src/pbr-pipeline.ts +15 -18
  205. package/src/pipeline/standard-output/lut-gpu.ts +3 -3
  206. package/src/pipeline/standard-output/resources.ts +2 -2
  207. package/src/publication/__tests__/derived-geometry.integration.test.ts +177 -0
  208. package/src/publication/__tests__/features.integration.test.ts +173 -0
  209. package/src/publication/__tests__/programs.unit.test.ts +25 -0
  210. package/src/publication/__tests__/publication.integration.test.ts +415 -0
  211. package/src/publication/__tests__/targets.integration.test.ts +120 -0
  212. package/src/publication/__tests__/video.browser.test.ts +50 -0
  213. package/src/publication/contract.ts +104 -0
  214. package/src/publication/dependencies.ts +54 -0
  215. package/src/publication/prepare-geometry.ts +94 -0
  216. package/src/publication/programs.ts +45 -0
  217. package/src/publication/publisher.ts +645 -0
  218. package/src/publication/receiver.ts +297 -0
  219. package/src/publication/resource-scope.ts +31 -0
  220. package/src/publication/targets.ts +182 -0
  221. package/src/publication/video.ts +33 -0
  222. package/src/record/__tests__/material-bg-cache.unit.test.ts +15 -1
  223. package/src/record/__tests__/prepared-runtime-material.integration.test.ts +134 -0
  224. package/src/record/__tests__/sprite-material-abi.unit.test.ts +8 -10
  225. package/src/record/__tests__/video-texture-view.unit.test.ts +9 -9
  226. package/src/record/fold-buckets.ts +8 -5
  227. package/src/record/frame-snapshot.ts +6 -6
  228. package/src/record/frame.ts +87 -28
  229. package/src/record/helpers.ts +2 -2
  230. package/src/record/main-pass-geometry.ts +21 -2
  231. package/src/record/main-pass-material.ts +37 -33
  232. package/src/record/main-pass-sprite-draws.ts +6 -152
  233. package/src/record/main-pass.ts +8 -7
  234. package/src/record/prepared-material-bindings.ts +16 -37
  235. package/src/record/render-context.ts +4 -2
  236. package/src/record/shadow-pass.ts +65 -7
  237. package/src/record/sprite-instance-buffer.ts +155 -0
  238. package/src/record/target-capture-graph.ts +258 -0
  239. package/src/record/typed-frame-graph.ts +11 -207
  240. package/src/record/view-ubo.ts +1 -1
  241. package/src/record/volume-params.ts +6 -6
  242. package/src/recovery/render-system-candidate.ts +10 -18
  243. package/src/render-contract.ts +6 -1
  244. package/src/render-system-extract-tail.ts +48 -23
  245. package/src/render-system-extract.ts +11 -2
  246. package/src/render-system-presentation.ts +2 -2
  247. package/src/render-system-projections.ts +3 -2
  248. package/src/render-system-residency.ts +2 -2
  249. package/src/render-system.ts +82 -54
  250. package/src/scene/render-scene.ts +110 -100
  251. package/src/scene/render-source.ts +90 -0
  252. package/src/scene/source-systems.ts +33 -0
  253. package/src/systems/skin-palette-allocator.ts +1 -0
  254. package/src/systems/skin-palette-types.ts +11 -0
  255. package/src/systems/transparent-dispatch.ts +2 -2
  256. package/src/systems/transparent-sort-config.ts +3 -1
  257. package/src/typed-shadow-passes.ts +36 -5
  258. package/dist/chunk-BLXGONR2.mjs.map +0 -1
  259. package/dist/chunk-ELJFJ2BU.mjs.map +0 -1
  260. package/dist/chunk-ILQ5MEZF.mjs +0 -32
  261. package/dist/chunk-ILQ5MEZF.mjs.map +0 -1
  262. package/dist/chunk-MKDIT2EM.mjs.map +0 -1
  263. package/dist/chunk-OCDLEI6I.mjs.map +0 -1
  264. package/dist/chunk-ODZWR6JQ.mjs.map +0 -1
  265. package/dist/chunk-RXZ3RGOU.mjs +0 -53
  266. package/dist/chunk-RXZ3RGOU.mjs.map +0 -1
  267. package/dist/chunk-SHFUFJ5N.mjs +0 -1781
  268. package/dist/chunk-SHFUFJ5N.mjs.map +0 -1
  269. package/dist/chunk-UI5SFODX.mjs.map +0 -1
  270. package/dist/chunk-UINHLCZA.mjs +0 -2389
  271. package/dist/chunk-UINHLCZA.mjs.map +0 -1
  272. package/dist/chunk-WEX45HKH.mjs +0 -1524
  273. package/dist/chunk-WEX45HKH.mjs.map +0 -1
@@ -0,0 +1,415 @@
1
+ import {
2
+ AssetRegistry,
3
+ HANDLE_CUBE,
4
+ RuntimeMaterialValue,
5
+ RuntimeMeshVertices,
6
+ resolveAssetHandle,
7
+ } from '@forgeax/engine-assets-runtime';
8
+ import { Time, World } from '@forgeax/engine-ecs';
9
+ import { ChildOf, GlobalTransform, Transform } from '@forgeax/engine-scene';
10
+ import { ShaderRegistry } from '@forgeax/engine-shader';
11
+ import { Skin } from '@forgeax/engine-skinning';
12
+ import type { MaterialAsset, MeshAsset } from '@forgeax/engine-types';
13
+ import { describe, expect, it } from 'vitest';
14
+ import {
15
+ Camera,
16
+ Instances,
17
+ MeshFilter,
18
+ MeshRenderer,
19
+ Points,
20
+ PostProcessParams,
21
+ Skylight,
22
+ Visibility,
23
+ VisibilityStateValue,
24
+ } from '../../components';
25
+ import { PersistentRenderScene } from '../../scene/render-scene';
26
+ import { renderPublicationTransfers } from '../contract';
27
+ import { createRenderPublisher } from '../publisher';
28
+ import { RenderPublicationReceiver } from '../receiver';
29
+
30
+ function fixture() {
31
+ const world = new World();
32
+ const assets = new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined }));
33
+ const identity = { source: 'publication-test', epoch: 1 };
34
+ const publisher = createRenderPublisher(world, assets, identity);
35
+ world
36
+ .spawn(
37
+ { component: Transform, data: { pos: [0, 0, 5] } },
38
+ { component: Camera, data: { fov: Math.PI / 3 } },
39
+ )
40
+ .unwrap();
41
+ const spawn = () =>
42
+ world
43
+ .spawn(
44
+ { component: Transform, data: {} },
45
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
46
+ { component: MeshRenderer, data: {} },
47
+ )
48
+ .unwrap();
49
+ const first = spawn(),
50
+ second = spawn();
51
+ world.update(0).unwrap();
52
+ const receiver = new RenderPublicationReceiver(identity),
53
+ scene = new PersistentRenderScene();
54
+ const publish = () => {
55
+ const candidate = publisher.prepare(world.getResource(Time).elapsed).unwrap();
56
+ const packet = structuredClone(candidate.packet, {
57
+ transfer: renderPublicationTransfers(candidate.packet),
58
+ });
59
+ candidate.accept();
60
+ const saved = structuredClone(packet);
61
+ const accepted = receiver.accept(packet).unwrap();
62
+ const frame = scene.consumePublication(accepted);
63
+ const returned = structuredClone(renderPublicationTransfers(packet), {
64
+ transfer: renderPublicationTransfers(packet),
65
+ });
66
+ publisher.recycle(packet.revision, returned).unwrap();
67
+ // Preserve logical rows independently from the returned transport storage.
68
+ return { packet: saved, frame };
69
+ };
70
+ return { world, publisher, receiver, scene, first, second, spawn, publish };
71
+ }
72
+
73
+ describe('native render publication', () => {
74
+ it('publishes a numeric baseline, observes no static delta and retains the same CPU slots', () => {
75
+ const f = fixture();
76
+ const initial = f.publish();
77
+ expect(initial.frame.renderables).toHaveLength(2);
78
+ expect(initial.packet.templates).toHaveLength(1);
79
+ const slots = f.scene.compositionSlots().map((slot) => [slot.slot, slot.generation]);
80
+ const next = f.publish();
81
+ expect(
82
+ next.packet.upserts.length + next.packet.transforms.length + next.packet.removed.length,
83
+ ).toBe(0);
84
+ expect(next.packet.assets).toHaveLength(0);
85
+ expect(f.scene.compositionSlots().map((slot) => [slot.slot, slot.generation])).toEqual(slots);
86
+ f.publisher.dispose();
87
+ });
88
+
89
+ it('accumulates changes across a slow consumer without transferring ECS columns', () => {
90
+ const f = fixture();
91
+ f.publish();
92
+ f.world.set(f.first, Transform, { pos: [1, 0, 0] }).unwrap();
93
+ f.world.update(1 / 60).unwrap();
94
+ f.world.set(f.second, Transform, { pos: [2, 0, 0] }).unwrap();
95
+ f.world.update(1 / 60).unwrap();
96
+ const next = f.publish();
97
+ expect(next.packet.upserts).toHaveLength(0);
98
+ expect(new Set(next.packet.transformEntities)).toEqual(new Set([f.first, f.second]));
99
+ for (const entity of [f.first, f.second]) {
100
+ const matrix = f.world.get(entity, GlobalTransform).unwrap().world;
101
+ expect(matrix.byteLength).toBe(64);
102
+ expect([
103
+ ...(next.frame.renderables.find((row) => row.entityKey === entity)?.transform.world ?? []),
104
+ ]).toEqual([...matrix]);
105
+ }
106
+ f.publisher.dispose();
107
+ });
108
+
109
+ it('reconciles destruction and generation reuse without reviving the old render identity', () => {
110
+ const f = fixture();
111
+ f.publish();
112
+ f.world.despawn(f.first).unwrap();
113
+ const replacement = f.spawn();
114
+ f.world.update(0).unwrap();
115
+ const next = f.publish();
116
+ expect(next.packet.removed).toContain(f.first);
117
+ expect(next.frame.renderables.map((row) => row.entityKey)).toEqual(
118
+ expect.arrayContaining([replacement, f.second]),
119
+ );
120
+ expect(next.frame.renderables.some((row) => row.entityKey === f.first)).toBe(false);
121
+ f.publisher.dispose();
122
+ });
123
+
124
+ it('rejects old epochs before the first baseline and wrong revisions without advancing acceptance', () => {
125
+ const f = fixture();
126
+ const candidate = f.publisher.prepare(0).unwrap();
127
+ expect(f.receiver.accept({ ...candidate.packet, epoch: 0 })).toMatchObject({
128
+ ok: false,
129
+ error: { detail: { reason: 'identity' } },
130
+ });
131
+ expect(f.receiver.accept({ ...candidate.packet, base: 1 })).toMatchObject({
132
+ ok: false,
133
+ error: { detail: { reason: 'base' } },
134
+ });
135
+ expect(f.receiver.accept({ ...candidate.packet, revision: 2 })).toMatchObject({
136
+ ok: false,
137
+ error: { detail: { reason: 'revision' } },
138
+ });
139
+ expect(f.receiver.acceptedRevision).toBe(0);
140
+ candidate.discard();
141
+ f.publish();
142
+ expect(f.receiver.acceptedRevision).toBe(1);
143
+ f.publisher.dispose();
144
+ });
145
+
146
+ it('rejects malformed columns and missing dependencies without consuming the baseline', () => {
147
+ const f = fixture();
148
+ const candidate = f.publisher.prepare(0).unwrap();
149
+ for (const packet of [
150
+ { ...candidate.packet, transforms: new Float32Array(1) },
151
+ { ...candidate.packet, assets: [] },
152
+ { ...candidate.packet, upserts: new Uint32Array([f.first, 99]) },
153
+ { ...candidate.packet, removed: new Uint32Array([f.first]) },
154
+ ]) {
155
+ expect(f.receiver.accept(packet).ok).toBe(false);
156
+ expect(f.receiver.acceptedRevision).toBe(0);
157
+ }
158
+ candidate.discard();
159
+ expect(f.publish().frame.renderables).toHaveLength(2);
160
+ f.publisher.dispose();
161
+ });
162
+
163
+ it('retries observed transforms after a discarded synchronous publication', () => {
164
+ const f = fixture();
165
+ f.publish();
166
+ f.world.set(f.first, Transform, { pos: [3, 0, 0] }).unwrap();
167
+ f.world.update(0).unwrap();
168
+ const discarded = f.publisher.prepare(0).unwrap();
169
+ discarded.discard();
170
+ const next = f.publish();
171
+ expect(
172
+ next.frame.renderables.find((row) => row.entityKey === f.first)?.transform.world[12],
173
+ ).toBe(3);
174
+ f.publisher.dispose();
175
+ });
176
+ });
177
+
178
+ it('publishes shared material changes and their removal, and retires detached dependencies', () => {
179
+ const f = fixture();
180
+ const asset: MaterialAsset = {
181
+ kind: 'material',
182
+ passes: [
183
+ {
184
+ name: 'Forward',
185
+ program: { module: 'forgeax::default-standard-pbr' },
186
+ renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
187
+ },
188
+ ],
189
+ values: { baseColor: [1, 0, 0, 1] },
190
+ };
191
+ const handle = f.world.allocSharedRef('MaterialAsset', asset);
192
+ f.world.set(f.first, MeshRenderer, { materials: [handle] }).unwrap();
193
+ f.world.set(f.second, MeshRenderer, { materials: [handle] }).unwrap();
194
+ f.publish();
195
+ const content = f.world
196
+ .spawn({
197
+ component: RuntimeMaterialValue,
198
+ data: { asset: handle, parameter: 'baseColor', kind: 2, value: [0, 1, 0, 1] },
199
+ })
200
+ .unwrap();
201
+ const changed = f.publish();
202
+ expect(changed.packet.upserts.length).toBe(4);
203
+ expect(changed.packet.invalidatedAssets).toContain(handle);
204
+ expect(changed.frame.renderables.every((row) => row.material.baseColor[1] === 1)).toBe(true);
205
+ f.world.despawn(content).unwrap();
206
+ expect(f.publish().frame.renderables.every((row) => row.material.baseColor[0] === 1)).toBe(true);
207
+ f.world.despawn(f.first).unwrap();
208
+ f.world.despawn(f.second).unwrap();
209
+ expect(f.publish().packet.retiredAssets).toContain(handle);
210
+ f.publisher.dispose();
211
+ });
212
+
213
+ it('publishes projected mesh buffers and restores their base payload after content removal', () => {
214
+ const f = fixture();
215
+ f.publish();
216
+ const original = resolveAssetHandle<MeshAsset>(f.world, HANDLE_CUBE).unwrap();
217
+ const vertices = new Float32Array(original.vertices);
218
+ vertices[0] = Number(vertices[0]) + 0.25;
219
+ const content = f.world
220
+ .spawn({ component: RuntimeMeshVertices, data: { asset: HANDLE_CUBE, vertices } })
221
+ .unwrap();
222
+ const changed = f.publish();
223
+ expect(changed.packet.invalidatedAssets).toContain(HANDLE_CUBE);
224
+ expect(
225
+ (changed.packet.assets.find((row) => row.handle === HANDLE_CUBE)?.value as MeshAsset)
226
+ .vertices[0],
227
+ ).toBe(vertices[0]);
228
+ f.world.despawn(content).unwrap();
229
+ expect(
230
+ (f.publish().packet.assets.find((row) => row.handle === HANDLE_CUBE)?.value as MeshAsset)
231
+ .vertices[0],
232
+ ).toBe(original.vertices[0]);
233
+ f.publisher.dispose();
234
+ });
235
+
236
+ it('propagates parent visibility and reuses returned storage without allocation growth', () => {
237
+ const f = fixture();
238
+ f.publish();
239
+ const parent = f.world
240
+ .spawn(
241
+ { component: Transform, data: {} },
242
+ { component: Visibility, data: { state: VisibilityStateValue.visible } },
243
+ )
244
+ .unwrap();
245
+ f.world.addComponent(f.first, { component: ChildOf, data: { parent } }).unwrap();
246
+ f.world.update(0).unwrap();
247
+ f.publish();
248
+ f.world.set(parent, Visibility, { state: VisibilityStateValue.hidden }).unwrap();
249
+ expect(f.publish().frame.renderables.some((row) => row.entityKey === f.first)).toBe(false);
250
+ f.world.set(parent, Visibility, { state: VisibilityStateValue.visible }).unwrap();
251
+ f.publish();
252
+ const allocations = f.publisher.inspect().allocations;
253
+ for (let i = 0; i < 12; i++) {
254
+ f.world.set(f.second, Transform, { pos: [i, 0, 0] }).unwrap();
255
+ f.world.update(0).unwrap();
256
+ f.publish();
257
+ }
258
+ expect(f.publisher.inspect().allocations).toBe(allocations);
259
+ f.publisher.dispose();
260
+ });
261
+
262
+ it('ignores unbound geometry vocabulary just like local extraction', () => {
263
+ const f = fixture();
264
+ f.publish();
265
+ f.world.spawn({ component: Points, data: {} }).unwrap();
266
+ const result = f.publisher.prepare(0);
267
+ expect(result.ok).toBe(true);
268
+ if (result.ok) result.value.discard();
269
+ f.publisher.dispose();
270
+ });
271
+
272
+ it.each([
273
+ 10_000, 50_000,
274
+ ])('keeps static publication delta and native scanned rows at zero for %i entities', (count) => {
275
+ const f = fixture();
276
+ for (let i = 2; i < count; i++) f.spawn();
277
+ f.world.update(0).unwrap();
278
+ const baseline = f.publish();
279
+ expect(baseline.packet.upserts.length).toBe(count * 2);
280
+ const allocations = f.publisher.inspect().allocations;
281
+ for (let i = 0; i < 3; i++) {
282
+ const next = f.publish();
283
+ expect(
284
+ next.packet.upserts.length +
285
+ next.packet.transforms.length +
286
+ next.packet.removed.length +
287
+ next.packet.assets.length,
288
+ ).toBe(0);
289
+ expect(f.publisher.inspect().scannedRows).toBe(0);
290
+ expect(f.publisher.inspect().allocations).toBe(allocations);
291
+ }
292
+ f.publisher.dispose();
293
+ }, 60_000);
294
+
295
+ it('publishes instance poses without detaching source storage and accumulates skipped updates', () => {
296
+ const f = fixture();
297
+ const matrices = new Float32Array(32);
298
+ for (let i = 0; i < 2; i++) for (const j of [0, 5, 10, 15]) matrices[i * 16 + j] = 1;
299
+ f.world.addComponent(f.first, { component: Instances, data: { transforms: matrices } }).unwrap();
300
+ const initial = f.publish();
301
+ expect(
302
+ initial.frame.renderables.find((row) => row.entityKey === f.first)?.instances?.transforms,
303
+ ).toEqual(matrices);
304
+ matrices[12] = 0.5;
305
+ f.world.set(f.first, Instances, { transforms: matrices }).unwrap();
306
+ matrices[28] = -0.5;
307
+ f.world.set(f.first, Instances, { transforms: matrices }).unwrap();
308
+ const next = f.publish();
309
+ expect(
310
+ next.frame.renderables.find((row) => row.entityKey === f.first)?.instances?.transforms,
311
+ ).toEqual(matrices);
312
+ expect(f.world.get(f.first, Instances).unwrap().transforms.byteLength).toBe(128);
313
+ expect(f.publish().packet.upserts).toHaveLength(0);
314
+ f.publisher.dispose();
315
+ });
316
+
317
+ it('publishes frame-owned environment and post parameters without geometry churn', () => {
318
+ const f = fixture();
319
+ f.publish();
320
+ f.world.spawn({ component: Skylight, data: { intensity: 2 } }).unwrap();
321
+ f.world
322
+ .spawn({
323
+ component: PostProcessParams,
324
+ data: { shader: 'test-post', data: new Uint8Array([1, 2, 3, 4]) },
325
+ })
326
+ .unwrap();
327
+ const next = f.publish();
328
+ expect(next.frame.skylight?.intensity).toBe(2);
329
+ expect(next.frame.postProcessParams.get('test-post')).toEqual(new Uint8Array([1, 2, 3, 4]));
330
+ expect(next.packet.upserts).toHaveLength(0);
331
+ f.publisher.dispose();
332
+ });
333
+
334
+ it('publishes detached skin poses and follows joint changes across skipped source frames', () => {
335
+ const f = fixture();
336
+ const identity = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
337
+ const skeleton = f.world.allocSharedRef('SkeletonAsset', {
338
+ kind: 'skeleton',
339
+ jointCount: 1,
340
+ inverseBindMatrices: identity,
341
+ });
342
+ const joint = f.world.spawn({ component: Transform, data: {} }).unwrap();
343
+ f.world
344
+ .addComponent(f.first, {
345
+ component: Skin,
346
+ data: { skeleton, joints: new Uint32Array([joint]) },
347
+ })
348
+ .unwrap();
349
+ f.world.update(0).unwrap();
350
+ const initial = f.publish();
351
+ const pose = initial.packet.templates.find((t) => t.snapshot.skinPose !== undefined)?.snapshot
352
+ .skinPose;
353
+ expect(pose?.jointCount).toBe(1);
354
+ expect(initial.packet.templates.every((t) => t.snapshot.skin === undefined)).toBe(true);
355
+ f.world.set(joint, Transform, { pos: [0.5, 0, 0] }).unwrap();
356
+ f.world.update(1 / 60).unwrap();
357
+ f.world.set(joint, Transform, { pos: [1, 0, 0] }).unwrap();
358
+ f.world.update(1 / 60).unwrap();
359
+ const next = f.publish();
360
+ const nextPose = next.packet.templates.find((t) => t.snapshot.skinPose !== undefined)?.snapshot
361
+ .skinPose;
362
+ expect(nextPose?.jointWorlds[0]?.[12]).toBe(1);
363
+ expect(pose?.jointWorlds[0]?.[12]).toBe(0);
364
+ expect(f.publish().packet.upserts).toHaveLength(0);
365
+ f.world.removeComponent(f.first, Skin).unwrap();
366
+ expect(
367
+ f.publish().frame.renderables.find((row) => row.entityKey === f.first)?.skinPose,
368
+ ).toBeUndefined();
369
+ f.publisher.dispose();
370
+ });
371
+
372
+ it('renders a one-frame identity before its sealed removal and reuses two buffer slots', () => {
373
+ const f = fixture();
374
+ const seal = () => {
375
+ const candidate = f.publisher.prepare(0).unwrap();
376
+ const packet = structuredClone(candidate.packet, {
377
+ transfer: renderPublicationTransfers(candidate.packet),
378
+ });
379
+ candidate.accept();
380
+ return packet;
381
+ };
382
+ try {
383
+ const first = seal();
384
+ f.world.despawn(f.first).unwrap();
385
+ const replacement = f.spawn();
386
+ f.world.update(0).unwrap();
387
+ const second = seal();
388
+ // More source mutations cannot edit either already-transferred publication.
389
+ f.world.despawn(replacement).unwrap();
390
+ f.world.update(0).unwrap();
391
+ const firstFrame = f.scene.consumePublication(f.receiver.accept(first).unwrap());
392
+ expect(firstFrame.renderables.map((row) => row.entityKey)).toContain(f.first);
393
+ f.publisher.recycle(first.revision, renderPublicationTransfers(first)).unwrap();
394
+ const secondFrame = f.scene.consumePublication(f.receiver.accept(second).unwrap());
395
+ expect(secondFrame.renderables.map((row) => row.entityKey)).toContain(replacement);
396
+ expect(secondFrame.renderables.map((row) => row.entityKey)).not.toContain(f.first);
397
+ f.publisher.recycle(second.revision, renderPublicationTransfers(second)).unwrap();
398
+ // Warm both slots for equivalent changed-transform frames, then drain in pairs.
399
+ let warmAllocations: number | undefined;
400
+ for (let index = 0; index < 4; index++) {
401
+ const packets = [1, 2].map((x) => {
402
+ f.world.set(f.second, Transform, { pos: [x, index, 0] }).unwrap();
403
+ f.world.update(0).unwrap();
404
+ return seal();
405
+ });
406
+ const allocations = f.publisher.inspect().allocations;
407
+ for (const packet of packets)
408
+ f.publisher.recycle(packet.revision, renderPublicationTransfers(packet)).unwrap();
409
+ warmAllocations ??= allocations;
410
+ expect(allocations).toBe(warmAllocations);
411
+ }
412
+ } finally {
413
+ f.publisher.dispose();
414
+ }
415
+ });
@@ -0,0 +1,120 @@
1
+ import { AssetRegistry, HANDLE_CUBE } from '@forgeax/engine-assets-runtime';
2
+ import { World } from '@forgeax/engine-ecs';
3
+ import { Transform } from '@forgeax/engine-scene';
4
+ import { ShaderRegistry } from '@forgeax/engine-shader';
5
+ import { expect, it } from 'vitest';
6
+ import { createRenderTargetHost } from '../../assembly/render-target-host';
7
+ import { Camera, CubeCamera, MeshFilter, MeshRenderer } from '../../components';
8
+ import type { RenderResult } from '../../render-contract';
9
+ import type { RenderTargetDescriptor } from '../../targets/contracts';
10
+ import { resolveRenderTargetMaterialSource } from '../../targets/material-source';
11
+ import { renderPublicationTransfers } from '../contract';
12
+ import { createRenderPublisher } from '../publisher';
13
+ import { RenderPublicationReceiver } from '../receiver';
14
+ import { RenderPublicationTargetOwner, RenderPublicationTargetReceiver } from '../targets';
15
+
16
+ function unwrap<T, E>(result: RenderResult<T, E>): T {
17
+ if (!result.ok) throw result.error;
18
+ return result.value;
19
+ }
20
+ const descriptor: RenderTargetDescriptor = {
21
+ shape: '2d',
22
+ width: 32,
23
+ height: 32,
24
+ format: 'rgba8unorm-srgb',
25
+ mipLevels: 1,
26
+ sampleCount: 1,
27
+ sampled: true,
28
+ readback: false,
29
+ };
30
+
31
+ it('maps auxiliary/cube cameras and distinct material targets into the receiver owner, including resize and retirement', () => {
32
+ const world = new World();
33
+ const assets = new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined }));
34
+ const source = new RenderPublicationTargetOwner();
35
+ const host = createRenderTargetHost();
36
+ const projection = new RenderPublicationTargetReceiver(host);
37
+ const identity = { source: 'target-publication', epoch: 1 };
38
+ const publisher = createRenderPublisher(world, assets, identity, undefined, [], source);
39
+ const receiver = new RenderPublicationReceiver(identity);
40
+ const first = unwrap(source.authoring.createRenderTarget(descriptor));
41
+ const second = unwrap(source.authoring.createRenderTarget(descriptor));
42
+ const cube = unwrap(source.authoring.createRenderTarget({ ...descriptor, shape: 'cube' }));
43
+ const target = world.allocSharedRef('RenderTarget', first);
44
+ const camera = world
45
+ .spawn(
46
+ { component: Transform, data: { pos: [0, 0, 5] } },
47
+ { component: Camera, data: { target } },
48
+ )
49
+ .unwrap();
50
+ const cubeEntity = world
51
+ .spawn(
52
+ { component: Transform, data: {} },
53
+ { component: CubeCamera, data: { target: world.allocSharedRef('RenderTarget', cube) } },
54
+ )
55
+ .unwrap();
56
+ for (const target of [first, second]) {
57
+ const texture = unwrap(
58
+ source.authoring.createRenderTargetTextureSource(target, {
59
+ aspect: 'color',
60
+ dimension: '2d',
61
+ mipLevel: 0,
62
+ }),
63
+ );
64
+ const textureRef = world.allocSharedRef('RenderTargetTextureSource', texture);
65
+ const material = world.allocSharedRef('MaterialAsset', {
66
+ kind: 'material',
67
+ passes: [
68
+ {
69
+ name: 'Forward',
70
+ program: { module: 'forgeax::default-standard-pbr' },
71
+ renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
72
+ },
73
+ ],
74
+ values: { baseColorTexture: textureRef },
75
+ });
76
+ world
77
+ .spawn(
78
+ { component: Transform, data: {} },
79
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
80
+ { component: MeshRenderer, data: { materials: [material] } },
81
+ )
82
+ .unwrap();
83
+ }
84
+ world.update(0).unwrap();
85
+ const publish = () => {
86
+ const candidate = publisher.prepare(0).unwrap();
87
+ const packet = structuredClone(candidate.packet);
88
+ candidate.accept();
89
+ const result = projection.apply(receiver.accept(packet).unwrap());
90
+ publisher.recycle(packet.revision, renderPublicationTransfers(packet)).unwrap();
91
+ return result;
92
+ };
93
+ const initial = publish();
94
+ expect(initial.packet.templates).toHaveLength(2);
95
+ expect(initial.packet.targetSources).toHaveLength(2);
96
+ const remote = initial.frame.auxiliaryCameras[0]?.target;
97
+ expect(remote).toBeDefined();
98
+ expect(remote).not.toBe(first);
99
+ expect(host.descriptions().find((row) => row.target === remote)?.descriptor).toEqual(descriptor);
100
+ expect(initial.frame.cubeCameras).toHaveLength(1);
101
+ const bindings = initial.frame.renderables.map((row) => {
102
+ const source = row.material.textureSources?.get('baseColorTexture');
103
+ if (source === undefined) throw new Error('Missing published target source');
104
+ return resolveRenderTargetMaterialSource(source);
105
+ });
106
+ expect(bindings.every((binding) => binding !== undefined)).toBe(true);
107
+ expect(bindings[0]?.target).not.toBe(bindings[1]?.target);
108
+ unwrap(source.authoring.resizeRenderTarget(first, { ...descriptor, width: 64 }));
109
+ const resized = publish();
110
+ expect(resized.frame.auxiliaryCameras[0]?.target).toBe(remote);
111
+ expect(host.descriptions().find((row) => row.target === remote)?.descriptor.width).toBe(64);
112
+ world.despawn(camera).unwrap();
113
+ world.despawn(cubeEntity).unwrap();
114
+ unwrap(source.authoring.destroyRenderTarget(cube));
115
+ publish();
116
+ expect(host.descriptions()).toHaveLength(2);
117
+ publisher.dispose();
118
+ source.dispose();
119
+ host.dispose();
120
+ });
@@ -0,0 +1,50 @@
1
+ import { AssetRegistry } from '@forgeax/engine-assets-runtime';
2
+ import { World } from '@forgeax/engine-ecs';
3
+ import { ShaderRegistry } from '@forgeax/engine-shader';
4
+ import { expect, it } from 'vitest';
5
+ import { createRenderPublisher } from '../publisher';
6
+ import { RenderPublicationReceiver } from '../receiver';
7
+
8
+ it('owns accepted native frames, rejecting duplicates and closed frames before changing revision', () => {
9
+ const identity = { source: 'native-video-lifetime', epoch: 1 };
10
+ const publisher = createRenderPublisher(
11
+ new World(),
12
+ new AssetRegistry(new ShaderRegistry({ manifestUrl: undefined })),
13
+ identity,
14
+ );
15
+ const candidate = publisher.prepare(0).unwrap();
16
+ const receiver = new RenderPublicationReceiver(identity);
17
+ const canvas = new OffscreenCanvas(4, 4);
18
+ canvas.getContext('2d')?.fillRect(0, 0, 4, 4);
19
+ const source = new VideoFrame(canvas, { timestamp: 0 });
20
+ const first = source.clone(),
21
+ next = source.clone(),
22
+ closed = source.clone();
23
+ closed.close();
24
+ const row = { entity: 1, clip: 2, frame: first };
25
+ try {
26
+ expect(
27
+ receiver.accept({ ...candidate.packet, videoFrames: [row, { ...row, frame: next }] }).ok,
28
+ ).toBe(false);
29
+ expect(receiver.acceptedRevision).toBe(0);
30
+ const accepted = receiver.accept({ ...candidate.packet, videoFrames: [row] }).unwrap();
31
+ expect(accepted.resources.videoFrame(1, 2)).toBe(first);
32
+ const second = { ...candidate.packet, baseline: false, revision: 2, base: 1 };
33
+ expect(receiver.accept({ ...second, videoFrames: [{ ...row, frame: closed }] }).ok).toBe(false);
34
+ expect(receiver.acceptedRevision).toBe(1);
35
+ expect(first.displayWidth).toBe(4);
36
+ receiver.accept({ ...second, videoFrames: [{ ...row, frame: next }] }).unwrap();
37
+ expect(first.displayWidth).toBe(0);
38
+ receiver.dispose();
39
+ expect(next.displayWidth).toBe(0);
40
+ expect(source.displayWidth).toBe(4);
41
+ } finally {
42
+ receiver.dispose();
43
+ candidate.discard();
44
+ publisher.dispose();
45
+ source.close();
46
+ first.close();
47
+ next.close();
48
+ closed.close();
49
+ }
50
+ });
@@ -0,0 +1,104 @@
1
+ import type { TimeResource } from '@forgeax/engine-ecs';
2
+ import type { MaterialRuntimeArtifact, MaterialShaderEntry } from '@forgeax/engine-shader';
3
+ import type { Asset, Handle } from '@forgeax/engine-types';
4
+ import type { DispatchEntry, ExtractedFrame, RenderableSnapshot } from '../render-system-extract';
5
+ import type { TransparentSortConfig } from '../systems/transparent-sort-config';
6
+ import type { PublishedRenderTarget, PublishedRenderTargetSource } from './targets';
7
+
8
+ /** Chosen by App before creating the receiver; never inferred from the first packet. */
9
+ export interface RenderPublicationIdentity {
10
+ readonly source: string;
11
+ readonly epoch: number;
12
+ }
13
+
14
+ export type RenderPublicationMetadata = Omit<
15
+ ExtractedFrame,
16
+ | 'renderables'
17
+ | 'dispatch'
18
+ | 'visibilitySnapshots'
19
+ | 'featureVisibilitySnapshots'
20
+ | 'hiddenEntityReports'
21
+ | 'shadowCasterEntityKeys'
22
+ | 'shadowCasterDrawKeys'
23
+ | 'shadowCasterMembership'
24
+ >;
25
+
26
+ export interface RenderPublicationTemplate {
27
+ readonly snapshot: Omit<RenderableSnapshot, 'worldId' | 'entityKey' | 'transform'>;
28
+ readonly dispatch: readonly Omit<DispatchEntry, 'entityIndex' | 'renderableIndex'>[];
29
+ }
30
+
31
+ /** Numeric storage is publisher-owned; no ECS or asset buffer is transferred. */
32
+ export interface RenderPublication extends RenderPublicationIdentity {
33
+ readonly revision: number;
34
+ readonly base: number;
35
+ readonly baseline: boolean;
36
+ readonly time: TimeResource;
37
+ readonly sampleTimeSeconds: number;
38
+ readonly temporalReset: boolean;
39
+ readonly transparentSort: TransparentSortConfig;
40
+ readonly metadata: RenderPublicationMetadata;
41
+ readonly templates: readonly RenderPublicationTemplate[];
42
+ readonly videoFrames: readonly {
43
+ readonly entity: number;
44
+ readonly clip: number;
45
+ readonly frame: VideoFrame;
46
+ }[];
47
+ readonly targets: readonly PublishedRenderTarget[];
48
+ readonly targetSources: readonly PublishedRenderTargetSource[];
49
+ readonly features: readonly { readonly identity: string; readonly data: unknown }[];
50
+ readonly programs: readonly {
51
+ readonly key: string;
52
+ readonly shader: MaterialShaderEntry;
53
+ readonly artifact?: MaterialRuntimeArtifact;
54
+ }[];
55
+ /** entity, template index pairs. A publication source owns one World. */
56
+ readonly upserts: Uint32Array;
57
+ readonly removed: Uint32Array;
58
+ readonly transformEntities: Uint32Array;
59
+ readonly transforms: Float32Array;
60
+ readonly assets: readonly {
61
+ readonly handle: Handle<string, 'shared'>;
62
+ readonly guid?: string;
63
+ readonly value: Asset;
64
+ }[];
65
+ readonly retiredAssets: readonly Handle<string, 'shared'>[];
66
+ readonly invalidatedAssets: readonly Handle<string, 'shared'>[];
67
+ }
68
+
69
+ export interface PublishedRenderFrameInput {
70
+ readonly publication: RenderPublication;
71
+ /** Realm-local adapter invoked only after the feature's graph is submitted. */
72
+ readonly onFeatureSourceSubmitted?: (identity: string, feedback: unknown) => void;
73
+ }
74
+
75
+ export function renderPublicationTransfers(packet: RenderPublication): ArrayBuffer[] {
76
+ return [
77
+ packet.upserts.buffer,
78
+ packet.removed.buffer,
79
+ packet.transformEntities.buffer,
80
+ packet.transforms.buffer,
81
+ ] as ArrayBuffer[];
82
+ }
83
+
84
+ export type RenderPublicationFailure =
85
+ | 'identity'
86
+ | 'base'
87
+ | 'revision'
88
+ | 'shape'
89
+ | 'unsupported'
90
+ | 'in-flight'
91
+ | 'disposed';
92
+
93
+ export class RenderPublicationError extends Error {
94
+ readonly code = 'render-publication-invalid' as const;
95
+ readonly expected = 'one valid publication for the bound source session and accepted base';
96
+ readonly hint =
97
+ 'inspect detail.reason; repair the source or start a new session with a complete current baseline';
98
+ constructor(
99
+ readonly detail: { readonly reason: RenderPublicationFailure; readonly subject: string },
100
+ ) {
101
+ super(`render-publication-invalid: ${detail.reason}: ${detail.subject}`);
102
+ this.name = 'RenderPublicationError';
103
+ }
104
+ }