@vgai/engine 0.5.21 → 0.5.23

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 (339) hide show
  1. package/README.md +6 -3
  2. package/dist/adapter/adapter-module.d.ts.map +1 -1
  3. package/dist/adapter/adapter-module.js +2 -4
  4. package/dist/adapter/adapter-surface.d.ts +1 -1
  5. package/dist/adapter/adapter-surface.js +1 -1
  6. package/dist/adapter/authoring.d.ts +19 -21
  7. package/dist/adapter/authoring.d.ts.map +1 -1
  8. package/dist/adapter/authoring.js +2 -2
  9. package/dist/adapter/binding.d.ts +341 -0
  10. package/dist/adapter/binding.d.ts.map +1 -0
  11. package/dist/adapter/binding.js +148 -0
  12. package/dist/adapter/colyseus-networking-adapter.d.ts.map +1 -1
  13. package/dist/adapter/colyseus-networking-adapter.js +23 -4
  14. package/dist/adapter/entrypoint-selection-readers.d.ts +24 -0
  15. package/dist/adapter/entrypoint-selection-readers.d.ts.map +1 -0
  16. package/dist/adapter/entrypoint-selection-readers.js +63 -0
  17. package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -1
  18. package/dist/adapter/entrypoint-selection-source.js +1 -44
  19. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
  20. package/dist/adapter/finders/scenes-from-entrypoint-selection.js +1 -52
  21. package/dist/adapter/host-context.d.ts +46 -15
  22. package/dist/adapter/host-context.d.ts.map +1 -1
  23. package/dist/adapter/host-context.js +2 -9
  24. package/dist/adapter/index.d.ts +11 -12
  25. package/dist/adapter/index.d.ts.map +1 -1
  26. package/dist/adapter/index.js +9 -10
  27. package/dist/adapter/ingest/game-contract.d.ts +0 -3
  28. package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
  29. package/dist/adapter/ingest/game-contract.js +0 -3
  30. package/dist/adapter/ingest/scene-capture.d.ts +19 -23
  31. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  32. package/dist/adapter/ingest/scene-capture.js +143 -17
  33. package/dist/adapter/ingest/visible-capture-window.d.ts +34 -0
  34. package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
  35. package/dist/adapter/manifest-interpreter.d.ts +59 -0
  36. package/dist/adapter/manifest-interpreter.d.ts.map +1 -0
  37. package/dist/adapter/manifest-interpreter.js +50 -0
  38. package/dist/adapter/native-debug-module.d.ts +71 -3
  39. package/dist/adapter/native-debug-module.d.ts.map +1 -1
  40. package/dist/adapter/native-debug-module.js +146 -10
  41. package/dist/adapter/rapier-physics-adapter.d.ts +31 -1
  42. package/dist/adapter/rapier-physics-adapter.d.ts.map +1 -1
  43. package/dist/adapter/rapier-physics-adapter.js +50 -36
  44. package/dist/adapter/renderer-config.d.ts +3 -3
  45. package/dist/adapter/renderer-config.js +3 -3
  46. package/dist/adapter/root-adapter.d.ts +7 -7
  47. package/dist/adapter/root-adapter.js +4 -4
  48. package/dist/adapter/system-adapter.d.ts +45 -7
  49. package/dist/adapter/system-adapter.d.ts.map +1 -1
  50. package/dist/adapter/system-seam-contract.d.ts +3 -3
  51. package/dist/adapter/system-seam-contract.js +3 -3
  52. package/dist/adapter/system-slot.d.ts +168 -0
  53. package/dist/adapter/system-slot.d.ts.map +1 -0
  54. package/dist/adapter/system-slot.js +229 -0
  55. package/dist/adapter/transform.d.ts +11 -1
  56. package/dist/adapter/transform.d.ts.map +1 -1
  57. package/dist/ai/navigation.d.ts +17 -1
  58. package/dist/ai/navigation.d.ts.map +1 -1
  59. package/dist/ai/navigation.js +26 -5
  60. package/dist/animation/anim-graph-types.d.ts +11 -3
  61. package/dist/animation/anim-graph-types.d.ts.map +1 -1
  62. package/dist/animation/theatre-clock-binding.d.ts +4 -4
  63. package/dist/animation/theatre-clock-binding.js +4 -4
  64. package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
  65. package/dist/animation/xstate-animation-binding.js +2 -2
  66. package/dist/animation/xstate-animation-meta.d.ts +13 -0
  67. package/dist/animation/xstate-animation-meta.d.ts.map +1 -1
  68. package/dist/animation/xstate-animation-meta.js +14 -0
  69. package/dist/asset-formats/index.d.ts +4 -7
  70. package/dist/asset-formats/index.d.ts.map +1 -1
  71. package/dist/asset-formats/index.js +4 -7
  72. package/dist/asset-formats/render-env.d.ts +5 -8
  73. package/dist/asset-formats/render-env.d.ts.map +1 -1
  74. package/dist/asset-formats/render-env.js +3 -6
  75. package/dist/asset-parse-error.d.ts +2 -5
  76. package/dist/asset-parse-error.d.ts.map +1 -1
  77. package/dist/asset-parse-error.js +2 -5
  78. package/dist/audio/bus-mixer.d.ts +101 -0
  79. package/dist/audio/bus-mixer.d.ts.map +1 -0
  80. package/dist/audio/bus-mixer.js +115 -0
  81. package/dist/canvas-react/index.d.ts +5 -12
  82. package/dist/canvas-react/index.d.ts.map +1 -1
  83. package/dist/canvas-react/index.js +5 -12
  84. package/dist/canvas-react/pixi-react-root-factory.d.ts +34 -21
  85. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
  86. package/dist/canvas-react/pixi-react-root-factory.js +274 -36
  87. package/dist/core/countdown-timer.d.ts +118 -0
  88. package/dist/core/countdown-timer.d.ts.map +1 -0
  89. package/dist/core/countdown-timer.js +137 -0
  90. package/dist/core/deferred-commands.d.ts +127 -0
  91. package/dist/core/deferred-commands.d.ts.map +1 -0
  92. package/dist/core/deferred-commands.js +132 -0
  93. package/dist/core/frame-pacing.d.ts +1 -1
  94. package/dist/core/frame-pacing.js +2 -2
  95. package/dist/core/game-loop.d.ts +1 -3
  96. package/dist/core/game-loop.d.ts.map +1 -1
  97. package/dist/core/game-loop.js +1 -3
  98. package/dist/core/seeded-random.d.ts +1 -1
  99. package/dist/core/sim-clock.d.ts +19 -8
  100. package/dist/core/sim-clock.d.ts.map +1 -1
  101. package/dist/core/sim-clock.js +18 -7
  102. package/dist/core/types.d.ts +1 -3
  103. package/dist/core/types.d.ts.map +1 -1
  104. package/dist/defaults.d.ts +6 -14
  105. package/dist/defaults.d.ts.map +1 -1
  106. package/dist/defaults.js +6 -14
  107. package/dist/dev/instruments.d.ts +38 -11
  108. package/dist/dev/instruments.d.ts.map +1 -1
  109. package/dist/dev/instruments.js +81 -11
  110. package/dist/dev/register-render-vitals.d.ts +1 -4
  111. package/dist/dev/register-render-vitals.d.ts.map +1 -1
  112. package/dist/dev/register-render-vitals.js +1 -4
  113. package/dist/dev/render-debug-adapter.d.ts +2 -3
  114. package/dist/dev/render-debug-adapter.d.ts.map +1 -1
  115. package/dist/dev/render-debug-adapter.js +2 -3
  116. package/dist/dev/render-vitals.d.ts +3 -3
  117. package/dist/dev/render-vitals.js +3 -3
  118. package/dist/dev/webgl-frame-capture.d.ts +1 -1
  119. package/dist/dev/webgl-frame-capture.js +1 -1
  120. package/dist/index.d.ts +2 -5
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +2 -4
  123. package/dist/input/host-pointer.d.ts +122 -0
  124. package/dist/input/host-pointer.d.ts.map +1 -0
  125. package/dist/input/host-pointer.js +160 -0
  126. package/dist/input/input-manager.d.ts +33 -5
  127. package/dist/input/input-manager.d.ts.map +1 -1
  128. package/dist/input/input-manager.js +48 -13
  129. package/dist/input/rebind-controller.d.ts.map +1 -1
  130. package/dist/input/rebind-controller.js +6 -8
  131. package/dist/input/schema.d.ts.map +1 -1
  132. package/dist/input/schema.js +56 -28
  133. package/dist/manifest/filename.d.ts.map +1 -1
  134. package/dist/manifest/filename.js +8 -12
  135. package/dist/manifest/load.d.ts +10 -10
  136. package/dist/manifest/load.d.ts.map +1 -1
  137. package/dist/manifest/load.js +9 -6
  138. package/dist/manifest/schema.d.ts +17 -21
  139. package/dist/manifest/schema.d.ts.map +1 -1
  140. package/dist/manifest/schema.js +45 -35
  141. package/dist/physics/collider-dimensions.d.ts +8 -12
  142. package/dist/physics/collider-dimensions.d.ts.map +1 -1
  143. package/dist/physics/collider-dimensions.js +8 -12
  144. package/dist/pixi/scene-capture.d.ts +11 -13
  145. package/dist/pixi/scene-capture.d.ts.map +1 -1
  146. package/dist/react/world-state.d.ts +1 -1
  147. package/dist/react/world-state.d.ts.map +1 -1
  148. package/dist/render/directional-shadow-fit.d.ts +88 -0
  149. package/dist/render/directional-shadow-fit.d.ts.map +1 -0
  150. package/dist/render/directional-shadow-fit.js +133 -0
  151. package/dist/render/environment-capture.d.ts +68 -0
  152. package/dist/render/environment-capture.d.ts.map +1 -0
  153. package/dist/render/environment-capture.js +90 -0
  154. package/dist/render/ibl-override-material.d.ts +97 -0
  155. package/dist/render/ibl-override-material.d.ts.map +1 -0
  156. package/dist/render/ibl-override-material.js +114 -0
  157. package/dist/render/light-camera-factory.d.ts +5 -7
  158. package/dist/render/light-camera-factory.d.ts.map +1 -1
  159. package/dist/render/light-camera-factory.js +8 -11
  160. package/dist/render/material-factory.js +1 -1
  161. package/dist/render/render-batch-system.d.ts.map +1 -1
  162. package/dist/render/render-features.d.ts +17 -6
  163. package/dist/render/render-features.d.ts.map +1 -1
  164. package/dist/render/render-features.js +0 -28
  165. package/dist/runtime/create-runtime.d.ts +34 -25
  166. package/dist/runtime/create-runtime.d.ts.map +1 -1
  167. package/dist/runtime/create-runtime.js +218 -93
  168. package/dist/runtime/debug-registry.d.ts +77 -8
  169. package/dist/runtime/debug-registry.d.ts.map +1 -1
  170. package/dist/runtime/debug-registry.js +3 -3
  171. package/dist/runtime/dev-build.d.ts +2 -2
  172. package/dist/runtime/dev-build.js +2 -2
  173. package/dist/runtime/game-input-seams.d.ts +51 -0
  174. package/dist/runtime/game-input-seams.d.ts.map +1 -0
  175. package/dist/runtime/game-input-seams.js +86 -0
  176. package/dist/runtime/game.d.ts +104 -40
  177. package/dist/runtime/game.d.ts.map +1 -1
  178. package/dist/runtime/game.js +119 -61
  179. package/dist/runtime/mount-game.d.ts +0 -16
  180. package/dist/runtime/mount-game.d.ts.map +1 -1
  181. package/dist/runtime/mount-game.js +26 -41
  182. package/dist/runtime/mount-manifest.d.ts +22 -14
  183. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  184. package/dist/runtime/mount-manifest.js +22 -20
  185. package/dist/runtime/playtest.d.ts +22 -0
  186. package/dist/runtime/playtest.d.ts.map +1 -0
  187. package/dist/runtime/playtest.js +12 -0
  188. package/dist/runtime/state-bridge.d.ts +2 -3
  189. package/dist/runtime/state-bridge.d.ts.map +1 -1
  190. package/dist/runtime/state-bridge.js +1 -0
  191. package/dist/setup/setup-audio.d.ts +12 -0
  192. package/dist/setup/setup-audio.d.ts.map +1 -1
  193. package/dist/setup/setup-audio.js +9 -10
  194. package/dist/setup/setup-renderer.d.ts +2 -25
  195. package/dist/setup/setup-renderer.d.ts.map +1 -1
  196. package/dist/setup/setup-renderer.js +2 -31
  197. package/dist/world3d-react/index.d.ts +15 -25
  198. package/dist/world3d-react/index.d.ts.map +1 -1
  199. package/dist/world3d-react/index.js +15 -25
  200. package/dist/world3d-react/r3f-root-factory.d.ts +28 -34
  201. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  202. package/dist/world3d-react/r3f-root-factory.js +403 -57
  203. package/dist/world3d-react/rapier-physics-bridge.d.ts +30 -17
  204. package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
  205. package/dist/world3d-react/rapier-physics-bridge.js +143 -58
  206. package/package.json +1 -1
  207. package/schemas/engine-capabilities.json +16 -14
  208. package/schemas/vgai-project.schema.json +35 -26
  209. package/src/adapter/adapter-module.ts +2 -4
  210. package/src/adapter/adapter-surface.ts +1 -1
  211. package/src/adapter/authoring.ts +19 -21
  212. package/src/adapter/binding.ts +468 -0
  213. package/src/adapter/colyseus-networking-adapter.ts +20 -5
  214. package/src/adapter/entrypoint-selection-readers.ts +66 -0
  215. package/src/adapter/entrypoint-selection-source.ts +1 -40
  216. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +1 -48
  217. package/src/adapter/host-context.ts +45 -15
  218. package/src/adapter/index.ts +23 -11
  219. package/src/adapter/ingest/game-contract.ts +0 -3
  220. package/src/adapter/ingest/scene-capture.ts +190 -39
  221. package/src/adapter/ingest/visible-capture-window.ts +35 -0
  222. package/src/adapter/manifest-interpreter.ts +64 -0
  223. package/src/adapter/native-debug-module.ts +248 -15
  224. package/src/adapter/rapier-physics-adapter.ts +76 -20
  225. package/src/adapter/renderer-config.ts +3 -3
  226. package/src/adapter/root-adapter.ts +7 -7
  227. package/src/adapter/system-adapter.ts +43 -8
  228. package/src/adapter/system-seam-contract.ts +3 -3
  229. package/src/adapter/system-slot.ts +291 -0
  230. package/src/adapter/transform.ts +18 -1
  231. package/src/ai/navigation.ts +30 -3
  232. package/src/animation/anim-graph-types.ts +11 -3
  233. package/src/animation/theatre-clock-binding.ts +4 -4
  234. package/src/animation/xstate-animation-binding.ts +2 -2
  235. package/src/animation/xstate-animation-meta.ts +16 -0
  236. package/src/asset-formats/index.ts +4 -7
  237. package/src/asset-formats/render-env.ts +3 -6
  238. package/src/asset-parse-error.ts +2 -5
  239. package/src/audio/bus-mixer.ts +161 -0
  240. package/src/canvas-react/index.ts +5 -21
  241. package/src/canvas-react/pixi-react-root-factory.tsx +301 -39
  242. package/src/core/countdown-timer.ts +188 -0
  243. package/src/core/deferred-commands.ts +174 -0
  244. package/src/core/frame-pacing.ts +2 -2
  245. package/src/core/game-loop.ts +1 -3
  246. package/src/core/seeded-random.ts +1 -1
  247. package/src/core/sim-clock.ts +19 -8
  248. package/src/core/types.ts +1 -3
  249. package/src/defaults.ts +6 -14
  250. package/src/dev/instruments.ts +108 -11
  251. package/src/dev/register-render-vitals.ts +1 -4
  252. package/src/dev/render-debug-adapter.ts +2 -3
  253. package/src/dev/render-vitals.ts +3 -3
  254. package/src/dev/webgl-frame-capture.ts +1 -1
  255. package/src/index.ts +2 -5
  256. package/src/input/host-pointer.ts +230 -0
  257. package/src/input/input-manager.ts +49 -13
  258. package/src/input/input-types.ts +1 -1
  259. package/src/input/rebind-controller.ts +6 -8
  260. package/src/input/schema.ts +216 -188
  261. package/src/manifest/filename.ts +8 -12
  262. package/src/manifest/load.ts +17 -13
  263. package/src/manifest/schema.ts +55 -47
  264. package/src/physics/collider-dimensions.ts +8 -12
  265. package/src/pixi/scene-capture.ts +12 -15
  266. package/src/react/world-state.tsx +1 -1
  267. package/src/render/directional-shadow-fit.ts +156 -0
  268. package/src/render/environment-capture.ts +102 -0
  269. package/src/render/ibl-override-material.ts +170 -0
  270. package/src/render/light-camera-factory.ts +8 -11
  271. package/src/render/material-factory.ts +1 -1
  272. package/src/render/render-batch-system.ts +14 -6
  273. package/src/render/render-features.ts +17 -35
  274. package/src/runtime/create-runtime.ts +270 -108
  275. package/src/runtime/debug-registry.ts +84 -8
  276. package/src/runtime/dev-build.ts +2 -2
  277. package/src/runtime/game-input-seams.ts +108 -0
  278. package/src/runtime/game.ts +231 -85
  279. package/src/runtime/mount-game.ts +31 -57
  280. package/src/runtime/mount-manifest.ts +47 -36
  281. package/src/runtime/playtest.ts +22 -0
  282. package/src/runtime/state-bridge.ts +3 -3
  283. package/src/setup/setup-audio.ts +21 -11
  284. package/src/setup/setup-renderer.ts +2 -61
  285. package/src/world3d-react/index.ts +15 -31
  286. package/src/world3d-react/r3f-root-factory.tsx +451 -60
  287. package/src/world3d-react/rapier-physics-bridge.tsx +154 -58
  288. package/dist/adapter/setup-three-root-adapter.d.ts +0 -86
  289. package/dist/adapter/setup-three-root-adapter.d.ts.map +0 -1
  290. package/dist/adapter/setup-three-root-adapter.js +0 -908
  291. package/dist/animation/clip-map.d.ts +0 -12
  292. package/dist/animation/clip-map.d.ts.map +0 -1
  293. package/dist/animation/clip-map.js +0 -31
  294. package/dist/asset-registry.d.ts +0 -38
  295. package/dist/asset-registry.d.ts.map +0 -1
  296. package/dist/asset-registry.js +0 -66
  297. package/dist/canvas-react/engine-bridge.d.ts +0 -45
  298. package/dist/canvas-react/engine-bridge.d.ts.map +0 -1
  299. package/dist/canvas-react/engine-bridge.js +0 -45
  300. package/dist/canvas-react/pixi-react-adapter.d.ts +0 -77
  301. package/dist/canvas-react/pixi-react-adapter.d.ts.map +0 -1
  302. package/dist/canvas-react/pixi-react-adapter.js +0 -294
  303. package/dist/canvas-react/world-context.d.ts +0 -98
  304. package/dist/canvas-react/world-context.d.ts.map +0 -1
  305. package/dist/canvas-react/world-context.js +0 -173
  306. package/dist/dev/debug-draw.d.ts +0 -24
  307. package/dist/dev/debug-draw.d.ts.map +0 -1
  308. package/dist/dev/debug-draw.js +0 -73
  309. package/dist/render/auto-batcher.d.ts +0 -34
  310. package/dist/render/auto-batcher.d.ts.map +0 -1
  311. package/dist/render/auto-batcher.js +0 -140
  312. package/dist/render/lod.d.ts +0 -13
  313. package/dist/render/lod.d.ts.map +0 -1
  314. package/dist/render/lod.js +0 -16
  315. package/dist/runtime/types.d.ts +0 -303
  316. package/dist/runtime/types.d.ts.map +0 -1
  317. package/dist/runtime/types.js +0 -1
  318. package/dist/world3d-react/engine-bridge.d.ts +0 -47
  319. package/dist/world3d-react/engine-bridge.d.ts.map +0 -1
  320. package/dist/world3d-react/engine-bridge.js +0 -73
  321. package/dist/world3d-react/r3f-adapter.d.ts +0 -58
  322. package/dist/world3d-react/r3f-adapter.d.ts.map +0 -1
  323. package/dist/world3d-react/r3f-adapter.js +0 -634
  324. package/dist/world3d-react/world-context.d.ts +0 -182
  325. package/dist/world3d-react/world-context.d.ts.map +0 -1
  326. package/dist/world3d-react/world-context.js +0 -235
  327. package/src/adapter/setup-three-root-adapter.ts +0 -1032
  328. package/src/animation/clip-map.ts +0 -34
  329. package/src/asset-registry.ts +0 -89
  330. package/src/canvas-react/engine-bridge.ts +0 -59
  331. package/src/canvas-react/pixi-react-adapter.tsx +0 -356
  332. package/src/canvas-react/world-context.ts +0 -253
  333. package/src/dev/debug-draw.ts +0 -80
  334. package/src/render/auto-batcher.ts +0 -168
  335. package/src/render/lod.ts +0 -17
  336. package/src/runtime/types.ts +0 -328
  337. package/src/world3d-react/engine-bridge.ts +0 -86
  338. package/src/world3d-react/r3f-adapter.tsx +0 -717
  339. package/src/world3d-react/world-context.ts +0 -358
@@ -1,717 +0,0 @@
1
- /**
2
- * `createR3FAdapter` — wraps a react-three-fiber tree as a first-party
3
- * `RootAdapter` so an R3F scene mounts as an ordinary `kind: "three"`
4
- * world: the engine hands `mount()` a `ThreeHostContext` (ITS OWN renderer +
5
- * canvas + gated loop, per `../adapter/host-context.ts`); this returns a
6
- * `MountedThreeRoot` (`../adapter/root-adapter.ts`) whose `scene`/`camera`
7
- * are fiber's REAL `THREE.Scene`/`THREE.Camera` instances.
8
- *
9
- * Upstreamed from `examples/r3f-first-party/src/r3f-adapter.tsx` (R4 — the
10
- * `@engine/world3d-react` opt-in module, mirroring how `pixi/` is the
11
- * PixiJS surface's opt-in home). The mount semantics
12
- * are byte-for-byte the example's live-proven Phase-1 bridge. External adopters
13
- * (alien-stories-style) keep the exact same calling contract.
14
- *
15
- * three.js identity: this module never imports `three` itself for scene objects
16
- * — fiber's internal `import * as THREE from 'three'` must resolve to the SAME
17
- * instance the engine's `ThreeHostContext.three` points at, which the importing
18
- * project guarantees via its Vite `resolve.dedupe: ['three', 'react',
19
- * 'react-dom']` (see `examples/r3f-first-party/vite.config.ts`).
20
- */
21
-
22
- import {
23
- advance,
24
- createRoot,
25
- extend,
26
- flushSync,
27
- events as pointerEvents,
28
- type RootState,
29
- useFrame,
30
- } from '@react-three/fiber';
31
- import { createElement, Fragment, type ReactNode, useEffect, useMemo } from 'react';
32
- import type { MountedThreeRoot, RootAdapter, ThreeHostContext } from '../adapter';
33
- import { applyWorldRendererConfig, type WorldRendererConfig } from '../adapter/renderer-config';
34
- import type { SystemAdapters } from '../adapter/system-adapter';
35
- import { type RenderVitalsRegistration, registerRenderVitals } from '../dev/register-render-vitals';
36
- import {
37
- createRenderDebugAdapter,
38
- frameCaptureContextFor,
39
- type RenderDebugWiring,
40
- } from '../dev/render-debug-adapter';
41
- import { collectRenderMemory } from '../dev/render-memory';
42
- import { RENDER_SUBMIT_PHASE } from '../dev/render-vitals';
43
- import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
44
- import { WorldProvider } from '../react/world-state';
45
- import { createSoftParticleDepthPass } from '../render/soft-particle-depth';
46
- import { getDebugRegistry } from '../runtime/debug-registry';
47
- import { devBuildEnabled } from '../runtime/dev-build';
48
- import { EngineBridge, type EngineBridgeValue } from './engine-bridge';
49
- import { createR3FRootContext, DEFAULT_INPUT_MAP_PATH, wireGameInputSeams } from './world-context';
50
-
51
- /** The slice of `WebGLRenderer.info` the vitals reporter reads. Declared
52
- * structurally rather than imported from `three`, per this module's own
53
- * identity rule (see the header): a duck-typed host renderer legitimately has
54
- * no `info` at all, which is why every read of it is guarded. */
55
- interface RendererCounters {
56
- readonly render: { readonly calls: number; readonly triangles: number; readonly frame: number };
57
- readonly memory: { readonly geometries: number; readonly textures: number };
58
- }
59
-
60
- /**
61
- * Scene depth for any soft-particle system this world mounted, drawn between
62
- * gameplay and the draw.
63
- *
64
- * `useFrame`'s priority is the whole reason this is a component rather than an
65
- * engine phase: `systems.run(dt)` below finishes BEFORE `advance()`, and
66
- * `advance()` is where every `useFrame` gameplay callback moves the frame's
67
- * transforms — depth taken from an engine phase would be one frame stale.
68
- * Priority `0` puts it after every negative-priority callback (fiber sorts
69
- * subscribers by priority) and before both a positive-priority composer and
70
- * fiber's own `state.gl.render` — and, unlike a positive priority, it does NOT
71
- * flip fiber into manual-render mode (`internal.priority` only counts
72
- * subscribers with `priority > 0`), so mounting it can never stop an ordinary
73
- * world from drawing itself.
74
- *
75
- * Rendered as the LAST child of the adapter's tree so its subscription lands
76
- * after the world's own (React completes children before siblings, and fiber's
77
- * sort is stable) — the tie-break among priority-0 callbacks is insertion
78
- * order.
79
- *
80
- * It is mounted only when THIS host carries a real WebGL context. The editor's
81
- * design-time host deliberately reconciles the scene without one because the
82
- * editor viewport owns the real renderer and camera; that viewport runs the
83
- * same generic depth pass immediately before it presents the adopted scene.
84
- *
85
- * Costs nothing while no soft-particle system is armed — see
86
- * `../render/soft-particle-depth.ts` for what "armed" means and what the pass
87
- * spends when it is.
88
- */
89
- function SoftParticleDepth(): null {
90
- const pass = useMemo(() => createSoftParticleDepthPass(), []);
91
- useEffect(() => () => pass.dispose(), [pass]);
92
- useFrame(({ gl, scene, camera }) => pass.render(gl, scene, camera), 0);
93
- return null;
94
- }
95
-
96
- /** What {@link createR3FAdapter} needs to build one `RootAdapter`. */
97
- export interface CreateR3FAdapterOptions {
98
- /** Stable id (telemetry/registry/conformance) — `RootAdapter.id`. */
99
- readonly id: string;
100
- /** The R3F scene tree to mount — drei helpers, `useFrame` hooks, etc. all
101
- * work unchanged (design §1.C: "drei works unchanged (it is fiber-context
102
- * userland)"). */
103
- readonly content: ReactNode;
104
- /** Input map loaded through the game-owned `Game.loadInputMap` (load-once
105
- * across roots). Defaults to the classic adapter's conventional path,
106
- * `/inputmaps/default.inputmap.json`; `null` opts out (a world with no
107
- * actions). A missing/invalid map degrades loudly without failing the
108
- * mount — see `world-context.ts`. */
109
- readonly inputMapPath?: string | null | undefined;
110
- /**
111
- * Transitional compatibility for explicit pre-native adapters. The
112
- * default-export root factory always passes `false`: ordinary R3F trees get
113
- * no vgai context/provider or private scheduler. Remove this switch with
114
- * the last explicit legacy adapter migration.
115
- */
116
- readonly engineRuntime?: boolean | undefined;
117
- /** The colour pipeline this world was AUTHORED for, applied to the host's renderer for the life
118
- * of the mount and restored on dispose. Omit it (every world here does) to keep the host's own
119
- * defaults; declare it when the world's colours were picked against a different engine's
120
- * pipeline — see `../adapter/renderer-config.ts`. */
121
- readonly renderer?: WorldRendererConfig | undefined;
122
- }
123
-
124
- /**
125
- * Build a `RootAdapter` that mounts `options.content` through
126
- * react-three-fiber, gated entirely by the host's own loop and rendering
127
- * through the host's own `WebGLRenderer` — never a second renderer, never a
128
- * second `requestAnimationFrame` loop (design §1.C).
129
- */
130
- export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter {
131
- const { id, content, inputMapPath, engineRuntime = true, renderer: rendererConfig } = options;
132
-
133
- return {
134
- id,
135
-
136
- async mount(host: ThreeHostContext): Promise<MountedThreeRoot> {
137
- // Headless honesty — every `mount()` must work headless: the
138
- // conformance kit / CI
139
- // runs `mount()` in Node — no canvas, no WebGL, no fiber reconciler.
140
- // Guard the ENTIRE fiber mount behind `!host.headless` and return a
141
- // bare, real scene + camera instead (still `instanceof
142
- // host.three.Scene` / `.Camera` — the identity rule holds even here,
143
- // since this uses the host's own `three` instance, not a fresh
144
- // import). Residual: the hierarchy is empty under headless
145
- // conformance — recorded as design O2, not hidden; a react-nil-style
146
- // headless reconciler mount is the known fix, out of scope for v1.
147
- if (host.headless) {
148
- const scene = new host.three.Scene();
149
- const camera = new host.three.PerspectiveCamera();
150
- return {
151
- kind: 'three',
152
- scene,
153
- camera,
154
- drivesOwnLoop: false,
155
- dispose(): void {
156
- /* nothing was ever mounted */
157
- },
158
- };
159
- }
160
-
161
- // Fiber v9 made the THREE catalogue tree-shakeable: `<Canvas>` calls
162
- // `extend(THREE)` for you, a bare `createRoot` does NOT — without this,
163
- // the FIRST three intrinsic in the tree (`<color>`, `<ambientLight>`,
164
- // …) throws "X is not part of the THREE namespace! Did you forget to
165
- // extend?" at reconcile time. Extending with `host.three` (not a fresh
166
- // `import * as THREE`) keeps the catalogue on the host's deduped three
167
- // instance — the same identity rule the returned scene/camera rely on.
168
- // `extend` merges into a module-global catalogue, so calling it once
169
- // per mount is idempotent.
170
- extend(host.three as unknown as Parameters<typeof extend>[0]);
171
-
172
- // The engine runtime this world OWNS: a real SystemRunner,
173
- // provided to the R3F tree via `EngineBridge`.
174
- // `update(dt)` below runs the phases under the host loop, so registered
175
- // systems tick in engine phase order and freeze under
176
- // pause exactly like `useFrame` work does.
177
- //
178
- // HONESTY NOTE: the manager's ctx is an `R3FGameContext`, not the full
179
- // `GameContext`. The shared game-scoped subsystems are REAL
180
- // classic-adapter parity (`./world-context.ts`):
181
- // `ctx.debug`/`ctx.random`/`ctx.game`/`ctx.roots`, and `ctx.input` (the
182
- // game-owned InputManager, with this world's virtual-input/actions/trace
183
- // debug seams registered and the project's input map loaded). The
184
- // subsystems an R3F world genuinely does not build (Rapier, composer,
185
- // audio, particles, debugDraw) are absent from that TYPE, so reaching
186
- // for one is a compile error rather than anything that can happen at
187
- // runtime. `scene`/`camera` are backfilled right after fiber's first
188
- // commit resolves them, before any tick can run.
189
- const runtime = engineRuntime ? createR3FRootContext(host, { id, inputMapPath }) : null;
190
- // `ctx` rides the bridge so ordinary components can reach engine
191
- // services through `useGameContext()`.
192
- const bridge: EngineBridgeValue | null = runtime ? { ctx: runtime.ctx } : null;
193
- const gameDebugRegistry = host.game ? getDebugRegistry(host.game) : null;
194
- // THE HOST STILL WIRES THIS GAME'S INPUT WHEN THE WORLD TAKES NO CONTEXT.
195
- //
196
- // A default-exported world mounts with `engineRuntime: false` — no vgai
197
- // runtime context enters its React tree, by design. That says nothing
198
- // about the GAME-scoped input the host owns from the outside: the
199
- // project's declared map, and this root's `game.input.*` seams on the
200
- // debug registry. Skipping those made a fresh scaffold's bot/autoplay/
201
- // `vgai eval` input doors throw `DEBUG_INPUT_UNAVAILABLE` and left the
202
- // declared actions unloaded — in the ONE world shape `vgai create`
203
- // produces. Same host-vs-context split as `WorldProvider` below.
204
- //
205
- // `inputMapPath` unset (what the default-export factory passes) means
206
- // "the conventional path, if this project ships one" — quiet when it
207
- // does not, since a brand-new project declares no actions yet.
208
- const hostInputReady =
209
- runtime || !gameDebugRegistry
210
- ? null
211
- : wireGameInputSeams(host, gameDebugRegistry, {
212
- id,
213
- inputMapPath: inputMapPath ?? DEFAULT_INPUT_MAP_PATH,
214
- optionalInputMap: inputMapPath === undefined,
215
- });
216
- // Actions must exist before any component's init()/first frame reads
217
- // them — wait for the (never-rejecting) input-map load before the first
218
- // commit.
219
- await (runtime?.inputMapReady ?? hostInputReady);
220
-
221
- // The world's own colour pipeline. Applied AFTER `root.configure()` below — see the comment
222
- // at its call site for why "before" is the version that silently does nothing.
223
- let restoreRendererConfig: () => void = () => {};
224
-
225
- const canvas = host.surface.canvas;
226
- const root = createRoot(canvas);
227
-
228
- // `RootState` (the live scene/camera/gl fiber built) only arrives via
229
- // the `onCreated` callback — `root.render()`'s return value is
230
- // TECHNICALLY the same store, but `onCreated` is the hook the design
231
- // doc's §1.C sketch names, and waiting for it (rather than assuming
232
- // the first commit already ran synchronously) is the honest choice
233
- // under React 19's concurrent renderer, which does not guarantee a
234
- // synchronous first commit the way legacy ReactDOM.render did.
235
- let resolveState!: (state: RootState) => void;
236
- const statePromise = new Promise<RootState>((resolve) => {
237
- resolveState = resolve;
238
- });
239
- let resolveEffectsReady!: () => void;
240
- const effectsReady = new Promise<void>((resolve) => {
241
- resolveEffectsReady = resolve;
242
- });
243
- // Rendered after the game's content. React runs passive effects in tree
244
- // order, so when this fires every useDebugProvider/useDebugCommand in
245
- // the initial tree has registered. A Play acknowledgement is a claim
246
- // that the mounted game's command surface exists, not merely that Fiber
247
- // allocated a scene.
248
- function MountEffectsReady(): null {
249
- useEffect(resolveEffectsReady, []);
250
- return null;
251
- }
252
-
253
- // `root.configure()` is ASYNC in fiber v9 (`Promise<ReconcilerRoot>`)
254
- // — the design doc's §1.C sketch shows it called synchronously; the
255
- // real v9 API (verified against
256
- // `node_modules/@react-three/fiber/dist/declarations/src/core/index.d.ts`)
257
- // requires awaiting it before `render()`.
258
- await root.configure({
259
- // The engine's renderer, not a second one (design §1.C) — fiber
260
- // renders THROUGH `host.renderer` instead of constructing its own
261
- // `WebGLRenderer`.
262
- gl: host.renderer,
263
- // …and the host's shadow map, not fiber's.
264
- //
265
- // `configure()` runs `gl.shadowMap.enabled = !!shadows`
266
- // UNCONDITIONALLY, against its own default of `false`. Passing nothing
267
- // therefore does not mean "leave it alone" — it means "turn shadows
268
- // off on the renderer the host just configured". Every three root
269
- // silently did that (FX-5): `applyRendererSettings` sets
270
- // `shadowMap.enabled` from the `shadows` render feature, which
271
- // defaults ON, and the setting had no surviving reader on this path —
272
- // so `castShadow`/`receiveShadow` all over a world produced nothing
273
- // and the scene read flat, with no error anywhere.
274
- //
275
- // Reading it back off the renderer is exactly "don't clobber": a
276
- // boolean also pins `shadowMap.type` to `PCFSoftShadowMap`, which is
277
- // what `createHostRenderer` already sets, so the round trip is
278
- // lossless and the HOST stays the one deciding.
279
- //
280
- // Optional-chained even though `WebGLRenderer.shadowMap` is not
281
- // optional: a headless/stub host renderer is a real shape in this
282
- // repo's tests, and fiber's own `if (gl.shadowMap)` guard means the
283
- // value it reads there is ignored anyway.
284
- shadows: host.renderer.shadowMap?.enabled ?? false,
285
- // The engine's gated loop is the ONLY loop — fiber must never run
286
- // its own rAF (that would defeat editor pause; design §1.C's
287
- // `drivesOwnLoop: false` contract, guide §2's loop-model note).
288
- frameloop: 'never',
289
- // `RenderProps.size` types as the FULL `Size` (width/height/top/left),
290
- // not `Partial<Size>` — the design sketch's `{ width, height }` alone
291
- // does not satisfy fiber v9's real type, so `top`/`left` are pinned
292
- // to 0 (this bridge always fills its whole canvas; no offset
293
- // viewport in v1).
294
- size: { width: host.surface.width, height: host.surface.height, top: 0, left: 0 },
295
- // Fiber's pointer-event manager, passed EXPLICITLY.
296
- //
297
- // This comment used to say `events` was "deliberately left at fiber's
298
- // default (binds to `gl.domElement`)". That was false, and it cost
299
- // every R3F world in this engine its mesh-level pointer props —
300
- // `onClick`, `onPointerOver`, `onPointerMissed` were dead everywhere.
301
- // There IS no default for a bare `createRoot`: the
302
- // `events = createPointerEvents` default lives in the `<Canvas>`
303
- // COMPONENT, which this bridge does not use, and `configure` gates on
304
- // `if (events && !state.events.handlers)`. With `events` undefined the
305
- // store kept its initial `{ priority, enabled, connected: false }` —
306
- // no handlers, nothing bound to the canvas, no error.
307
- //
308
- // Found by the WO-P6 builder, whose control experiment isolated the
309
- // variable properly: the same stub renderer, the same
310
- // `frameloop: 'never'`, the same jsdom canvas and pointer sequence,
311
- // differing ONLY in passing `events` — and `onClick` fired. Fiber is
312
- // fine under a host-driven loop; we omitted one property.
313
- //
314
- // (Design O1's open question about a delegating multi-canvas input
315
- // router still only matters for a shell+net composite; this bridge is
316
- // the single-canvas world O1 says is unaffected.)
317
- events: pointerEvents,
318
- onCreated: (state) => resolveState(state),
319
- });
320
-
321
- // THE WORLD'S OWN COLOUR PIPELINE — and it must land HERE, after `configure()`.
322
- //
323
- // `configure()` writes `outputColorSpace` and `toneMapping` onto the renderer
324
- // UNCONDITIONALLY, from its own `linear`/`flat` props whose defaults are `false`/`false` —
325
- // i.e. sRGB output and ACES. That is the SAME trap as the `shadows` note above, and it is
326
- // silent: applying a world's config before `configure()` leaves every field set and every
327
- // pixel unchanged (measured on the squash-the-creeps Godot port — the ground stayed at byte
328
- // 138 with `toneMapping: 'none'` declared and applied).
329
- //
330
- // Applying it here is still before any frame: `advance()` only runs from `update()` below,
331
- // and `render()` on the next line merely reconciles.
332
- if (rendererConfig !== undefined) {
333
- restoreRendererConfig = applyWorldRendererConfig(host.three, host.renderer, rendererConfig);
334
- }
335
- // No `<StrictMode>` (design §1.C: "no StrictMode: host mounts once")
336
- // — this bridge mounts exactly once per `mount()` call; StrictMode's
337
- // deliberate double-invoke of effects would double-subscribe
338
- // `useFrame` callbacks against a host loop that only ticks once per
339
- // frame. The `EngineBridge` provider threads the component runtime
340
- // (above) into the tree (W3), where `useGameContext()` reads it.
341
- // R3F IS React, so the React seam every dom root already has —
342
- // `useWorldState`, `useDebugProvider`/`useDebugCommand`/`useDebugEmit`
343
- // (`../react/world-state`) — should work identically inside a three root.
344
- // Without this provider a behavior-as-a-hook could only reach the debug
345
- // registry by destructuring `useGameContext()` and calling
346
- // `debug?.registerStateProvider(...)` by hand, which has no unmount
347
- // story: the template's follow-camera hook leaked a provider per remount
348
- // until this landed. `host.game` is genuinely absent in bare/headless
349
- // hosts, so the provider is conditional and those hooks stay the inert
350
- // no-ops they already document themselves to be.
351
- //
352
- // READ FROM `host.game`, NEVER FROM `runtime` — the two are not the same
353
- // question, and conflating them silently dark-ended the seam for every
354
- // scaffolded game. The IDIOMATIC three world (`export default function
355
- // World()`) mounts through `resolveR3FEntryAdapter` with
356
- // `engineRuntime: false` (`r3f-root-factory.tsx`), so it has no
357
- // `runtime` and no `bridge` — by design, because the vgai runtime
358
- // CONTEXT (`useGameContext`) must not enter an ecosystem-native tree.
359
- // `WorldProvider` is not that context: it carries the Game handle the
360
- // host already holds, and it is the only way a game's own
361
- // `useDebugProvider('stat', …)` can reach the ONE game-scoped registry
362
- // the editor/`vgai eval` read. Keyed off `runtime`, every
363
- // default-exported world's registrations no-opped in silence and
364
- // `vgai eval 'await game.providers()'` listed only the built-ins.
365
- // The canvas lane never had the gap (`canvas-react/
366
- // pixi-react-root-factory.tsx` keeps its entry runtime on).
367
- const game = host.game;
368
- // `SoftParticleDepth` trails the world's own tree deliberately — see its
369
- // docblock for why the subscription must land last. A no-context design
370
- // mount omits this fiber callback because the adopting editor viewport
371
- // runs the pass with its own renderer and camera instead.
372
- const world = createElement(
373
- Fragment,
374
- null,
375
- content,
376
- runtime && typeof host.renderer.getContext === 'function'
377
- ? createElement(SoftParticleDepth, { key: 'vgai-soft-particle-depth' })
378
- : null,
379
- createElement(MountEffectsReady, { key: 'vgai-mount-effects-ready' }),
380
- );
381
- // The two providers are INDEPENDENT: `EngineBridge` rides `bridge` (the
382
- // vgai runtime context, absent for a native default-exported world),
383
- // `WorldProvider` rides `host.game` (the React seam, present whenever a
384
- // Game hosts this world). Nesting the second inside the first is what
385
- // made the seam a hostage of the first.
386
- const provided = game ? createElement(WorldProvider, { game }, world) : world;
387
- root.render(
388
- bridge ? createElement(EngineBridge.Provider, { value: bridge }, provided) : provided,
389
- );
390
- // A reconcile-time crash (e.g. a missing `extend` catalogue entry)
391
- // surfaces as an uncaught window error and `onCreated` never fires —
392
- // without this guard, `mount()` would await `statePromise` FOREVER and
393
- // silently wedge every world declared after this one (roots mount
394
- // sequentially). Convert that class of failure into a loud mount error.
395
- const state = await new Promise<RootState>((resolve, reject) => {
396
- const onError = (event: ErrorEvent) => {
397
- cleanup();
398
- root.unmount();
399
- runtime?.dispose();
400
- if (!runtime) gameDebugRegistry?.strip(id);
401
- restoreRendererConfig();
402
- reject(
403
- new Error(
404
- `r3f-adapter: fiber crashed before its first commit — ${event.message} ` +
405
- '(mount() fails loudly instead of hanging on onCreated)',
406
- ),
407
- );
408
- };
409
- const timer = setTimeout(() => {
410
- cleanup();
411
- root.unmount();
412
- runtime?.dispose();
413
- if (!runtime) gameDebugRegistry?.strip(id);
414
- restoreRendererConfig();
415
- reject(
416
- new Error(
417
- 'r3f-adapter: onCreated did not fire within 10s — the R3F tree never reached ' +
418
- 'its first commit (mount() fails loudly instead of hanging)',
419
- ),
420
- );
421
- }, 10_000);
422
- const cleanup = () => {
423
- clearTimeout(timer);
424
- window.removeEventListener('error', onError);
425
- };
426
- window.addEventListener('error', onError);
427
- void Promise.all([statePromise, effectsReady]).then(([s]) => {
428
- cleanup();
429
- resolve(s);
430
- });
431
- });
432
-
433
- // Clock hardening (field-proven by the first external adopter,
434
- // alien-stories' proto): fiber's internal `update()` calls
435
- // `state.clock.getDelta()` BEFORE its `frameloop:'never'` branch, and a
436
- // RUNNING (or autoStart) `THREE.Clock` accumulates WALL time into
437
- // `elapsedTime` as a side effect — skewing the deltas the 'never'
438
- // branch derives from the game timestamps `update(dt)` feeds below.
439
- // Stopped + autoStart=false makes `getDelta()` a pure no-op, so
440
- // `useFrame` deltas come from game time alone.
441
- state.clock.autoStart = false;
442
- state.clock.stop();
443
-
444
- // Backfill the component runtime's ctx with fiber's real scene/camera
445
- // (W3 — see the HONESTY NOTE above). This happens before the first
446
- // `update(dt)` tick, so no component ever observes them missing.
447
- runtime?.setSceneCamera(state.scene, state.camera);
448
-
449
- // THE STORE IS THE TRUTH, NOT THE `onCreated` SNAPSHOT.
450
- //
451
- // `state` is one immutable zustand snapshot: fiber's store is
452
- // `create<RootState>()`, and every `set(...)` produces a NEW state
453
- // object rather than mutating this one. So a world that legitimately
454
- // REPLACES the default camera — `useThree(s => s.set)({ camera })`,
455
- // which is exactly what drei's `makeDefault` does — never reaches
456
- // anything read off `state` directly.
457
- //
458
- // That is not cosmetic: fiber's own `update()` renders
459
- // `state.gl.render(state.scene, state.camera)` off the state object it
460
- // is HANDED, so `advance(t, true, state)` below pinned the render
461
- // camera to fiber's default `PerspectiveCamera` at (0, 0, 5) forever.
462
- // Measured on the squash-the-creeps Godot port (WO-G14): the world set
463
- // its authored `OrthographicCamera` and the frame still rendered from
464
- // (0, 0, 5) — a point INSIDE that game's 60x2x60 ground box, so every
465
- // ground polygon was back-facing, was culled, and the "ground" was the
466
- // cleared canvas. Nothing anywhere reported an error; edit mode looked
467
- // right because the editor renders with its own camera.
468
- //
469
- // `state.get()` is fiber's own `StoreApi.getState`, carried on
470
- // `RootState` for precisely this reason, so reading it per frame is the
471
- // supported way to see the live root. It is a plain map read.
472
- const live = (): RootState => state.get();
473
-
474
- // --- Live render vitals (issue #1504), for an R3F world -----------------
475
- // The SAME door the classic adapter seeds
476
- // (`../adapter/setup-three-root-adapter.ts`), seeded here for the same
477
- // reason: a running game must be able to explain its own frame cost
478
- // through the debug registry, with no capability to install and nothing
479
- // for a game to write. It has to be seeded in BOTH adapters because they
480
- // are two independent mount paths onto the same `Game` — and a TSX/R3F
481
- // world is the only shape a scaffolded project's three root has, so a
482
- // door wired only into the classic path is a door no scaffolded project
483
- // ever gets (issue #1518: `game.state('render.vitals')` answered "no
484
- // state provider registered" in the field while every other debug
485
- // reading of the same world worked).
486
- //
487
- // The gate is the same three conditions, minus the one that cannot
488
- // arise: `devBuildEnabled()` (`../runtime/dev-build.ts`, the ONE owner
489
- // of "is this a dev context" — a ship build registers nothing and pays
490
- // nothing), and a `Game` shell (the readings are folded out of that
491
- // game's profiler frames). "Not headless" needs no term here: a headless
492
- // mount returned above, before fiber was ever configured.
493
- //
494
- // Two module instances, one answer: under the packaged editor this
495
- // module is served out of the PROJECT's own Vite graph (see
496
- // `packages/editor/vite-plugin-react-world-runtime.ts` — the packaged
497
- // shell imports `resolveR3FEntryAdapter` through the project-rooted
498
- // server precisely so fiber/react/three identity is the project's), so
499
- // `import.meta.env.DEV` here is the project server's `true`, not the
500
- // prebuilt shell's `false`.
501
- //
502
- // OWNER of the registration+fold: this mount, ended by `dispose()` below
503
- // (the fold) and `runtime.dispose()`'s scoped `strip(id)` (the
504
- // registrations) — the same split the classic adapter documents.
505
- let renderVitals: RenderVitalsRegistration | null = null;
506
- if (host.game && devBuildEnabled()) {
507
- // Same auto-enable as the classic adapter: the readings are folded out
508
- // of profiler frames, and the profiler is a flag the editor already
509
- // sets on play — under the dev gate this is the same cost arriving a
510
- // little earlier, not a new one.
511
- host.game.profiler.enabled = true;
512
- if (!gameDebugRegistry) {
513
- throw new Error('r3f-adapter: mounted Game has no debug registry.');
514
- }
515
- renderVitals = registerRenderVitals({
516
- registry: gameDebugRegistry,
517
- worldId: id,
518
- profiler: host.game.profiler,
519
- scene: live().scene,
520
- renderer: host.renderer,
521
- });
522
- }
523
-
524
- // Engine-owned render introspection, built over the SAME renderer and
525
- // scene Fiber uses. A design-time stand-in has no context and a
526
- // headless mount returned above, so both honestly omit the capability.
527
- // `frameCaptureContextFor` is the same construction gate as the classic
528
- // Three adapter; no editor-side renderer inference is involved.
529
- const rendererContext =
530
- typeof host.renderer.getContext === 'function' ? host.renderer.getContext() : undefined;
531
- const frameCaptureContext = frameCaptureContextFor(false, rendererContext);
532
- const frameCapture = frameCaptureContext
533
- ? createWebGLFrameCapture(frameCaptureContext)
534
- : null;
535
- const renderDebugWiring: RenderDebugWiring | null = frameCapture
536
- ? createRenderDebugAdapter({
537
- capture: frameCapture,
538
- scene: state.scene,
539
- memory: () => collectRenderMemory(live().scene, host.renderer.info),
540
- })
541
- : null;
542
- // The session debug registry is host instrumentation, not component
543
- // context. Publishing the existing adapter here lets coverage and
544
- // editor panels see the same door without wrapping the R3F tree.
545
- const systemAdapters: SystemAdapters =
546
- runtime?.systemAdapters ?? (gameDebugRegistry ? { debug: gameDebugRegistry.adapter } : {});
547
- if (renderDebugWiring) systemAdapters.renderDebug = renderDebugWiring.adapter;
548
-
549
- // The engine drives every `useFrame` through the mounted world's
550
- // `update(dt)` hook, never off a raw host-loop callback. That is the
551
- // whole pause story: `runFrameImpl` (`runtime/game.ts`) calls
552
- // `mounted.update?.(dt)` per substep for a host-driven
553
- // (`drivesOwnLoop: false`) world and SKIPS it while that world is
554
- // frozen, and `Game.play.step()` ticks it exactly once — whereas any
555
- // callback run outside `game.runFrame`'s gate would fire
556
- // unconditionally, pause or not. Advancing fiber from such a callback
557
- // LOOKS right and silently breaks acceptance gate 2 — proven by the
558
- // 36-r3f-first-party e2e, whose paused instance-matrix samples kept
559
- // moving until this moved to `update`. (The former `ThreeHostContext.loop`
560
- // seam that made the wrong path reachable was deleted, zero consumers.)
561
- //
562
- // `advance(timestamp, runGlobalEffects, state)`'s `timestamp` is
563
- // consumed as `THREE.Clock.elapsedTime` DIRECTLY when
564
- // `frameloop:'never'` (verified against fiber's `update()`
565
- // implementation, not just its `.d.ts` — the declared signature alone
566
- // doesn't say this): `delta = timestamp - clock.elapsedTime;
567
- // clock.elapsedTime = timestamp`. So `timestamp` must be a
568
- // monotonically increasing SECONDS value — GAME time, not wall time:
569
- // `update` simply isn't called while this world is frozen, so
570
- // accumulating its `dt` (seconds; the loop runs a fixed 1/60 timestep)
571
- // means fiber's clock does not advance across a pause. Wall clock
572
- // (`performance.now()`) would leak the pause duration into the first
573
- // resumed frame as one giant `useFrame` delta — the exact "time passed
574
- // while frozen" illusion acceptance gate 2 forbids.
575
- let elapsed = 0;
576
- /** The legacy component ctx camera last backfilled by `update()`. */
577
- let backfilledCamera = state.camera;
578
-
579
- return {
580
- kind: 'three',
581
- // Fiber's REAL `THREE.Scene`/`THREE.Camera`. Checkout development
582
- // dedupes `three`; a registry-installed project and the prebundled
583
- // editor can legitimately have distinct constructor identities, so
584
- // editor boundaries recognize Three's stable structural brands
585
- // (`isScene`, `isCamera`) rather than relying only on `instanceof`.
586
- // `state.camera`'s type (`Camera = (OrthographicCamera |
587
- // PerspectiveCamera) & { manual?: boolean }`) is a structural subtype
588
- // of `THREE.Camera`, so no cast is needed.
589
- //
590
- // Both are GETTERS over the live store, not snapshot fields: the host
591
- // and the editor must see the camera the world actually renders
592
- // through, including one the world swapped in after the first commit
593
- // (see `live` above).
594
- get scene() {
595
- return live().scene;
596
- },
597
- get camera() {
598
- return live().camera;
599
- },
600
- drivesOwnLoop: false,
601
- // REPORTED for a host that draws this scene with a renderer other than
602
- // the one it mounted us against — see `MountedThreeRoot.rendererConfig`.
603
- // It is the same object already applied to `host.renderer` above; the
604
- // report never applies anything itself.
605
- rendererConfig: rendererConfig,
606
- // Adapter surface: `debug` (the shared game registry) and, when a real
607
- // WebGL2 context exists, `renderDebug` are engine-seeded. Game code
608
- // adds its own capabilities via `ctx.registerSystemAdapter` exactly
609
- // as in a classic world.
610
- systems: systemAdapters,
611
- update(dt: number): void {
612
- elapsed += dt;
613
- const current = live();
614
- // A world that swapped the default camera gets it onto `ctx` too —
615
- // `ctx.camera` and the rendered camera are the same fact, and a
616
- // component reading one while the frame uses the other is the
617
- // silent kind of wrong. Identity-gated, so this is one comparison
618
- // per frame in the overwhelmingly common no-swap case.
619
- if (runtime && current.camera !== backfilledCamera) {
620
- backfilledCamera = current.camera;
621
- runtime.setSceneCamera(current.scene, current.camera);
622
- }
623
- // Engine phases FIRST (registered systems mutate transforms),
624
- // then fiber's advance (useFrame callbacks + the actual render see
625
- // the fresh state). Both are host-gated: neither runs while frozen.
626
- runtime?.systems.run(dt);
627
- renderDebugWiring?.beforeRender();
628
- try {
629
- const profiler = host.game?.profiler;
630
- if (!profiler?.enabled) {
631
- advance(elapsed, true, current);
632
- return;
633
- }
634
- // Issue #1504's measurement, produced where the draw actually
635
- // happens. `advance()` under `frameloop: 'never'` is what calls
636
- // `gl.render(scene, camera)`, so this bracket — not a phase span
637
- // inside `systems` — is the frame's CPU submission cost, and it is
638
- // also what makes this frame a PRESENTATION as far as the vitals
639
- // fold is concerned (`../dev/render-vitals.ts`: no bracket, no
640
- // display frame). The profiler's phase clock is a stack, so
641
- // bracketing here cannot truncate an enclosing phase.
642
- const info = host.renderer.info as RendererCounters | undefined;
643
- const passesBefore = info?.render.frame ?? 0;
644
- profiler.beginPhase();
645
- try {
646
- advance(elapsed, true, current);
647
- } finally {
648
- profiler.endPhase(RENDER_SUBMIT_PHASE);
649
- }
650
- // Checked, not assumed — a duck-typed/design-time renderer supplies
651
- // only what a mount needs, and a mount with no counters must report
652
- // NOTHING rather than throw once per frame (the classic adapter's
653
- // `postFrame` pays for the same assumption for the same reason).
654
- // `gpuMs` is null: this path runs no GPU timer, and the profiler
655
- // keeps the previous reading rather than inventing a zero.
656
- if (info) {
657
- profiler.reportRender({
658
- gpuMs: null,
659
- drawCalls: info.render.calls,
660
- triangles: info.render.triangles,
661
- geometries: info.memory.geometries,
662
- textures: info.memory.textures,
663
- renderPasses: info.render.frame - passesBefore,
664
- });
665
- }
666
- } finally {
667
- renderDebugWiring?.afterRender();
668
- }
669
- },
670
- resize(width: number, height: number): void {
671
- live().setSize(width, height);
672
- },
673
- dispose(): void {
674
- // Reject/restore a pending capture while its renderables still
675
- // exist, then unmount component effects and strip runtime state.
676
- renderDebugWiring?.dispose();
677
- // The editor audits and disposes the mounted game's window/document realm immediately
678
- // after `session.stop()`. Fiber's ordinary `unmount()` submits a concurrent reconciler
679
- // update, so component effect cleanups could still be pending at that boundary and the
680
- // audit would truthfully report/reclaim listeners the world was about to remove itself.
681
- // Force the unmount COMMIT (not fiber's deliberately delayed renderer/context disposal)
682
- // to finish here: a stopped root owns no live component lifecycle beyond this method.
683
- flushSync(() => root.unmount());
684
- // The vitals fold owns two `profiler.subscribe` registrations, and
685
- // `runtime.dispose()`'s `strip(id)` removes the REGISTRATIONS only —
686
- // this is the one path that ends the subscriptions, exactly as the
687
- // classic adapter's teardown documents. Before the strip, so a
688
- // subscription can never outlive the door it feeds.
689
- renderVitals?.dispose();
690
- renderVitals = null;
691
- runtime?.dispose();
692
- // The no-context path has no `runtime.dispose()` to carry its scoped
693
- // strip, so it ends its own registrations here — the same
694
- // `strip(id)`, at the same moment. That is what ends this world's
695
- // PROVIDERS AND COMMANDS (the render-vitals door above is the one
696
- // this path actually registers under `id`); sibling roots are
697
- // untouched.
698
- //
699
- // It does NOT end the input seams `wireGameInputSeams` set, and
700
- // nothing else does either: `strip` walks providers/commands only
701
- // (`runtime/debug-registry.ts`), so `virtualInputTargets`/
702
- // `inputActionsSources`/`inputTraceSources` keep this world's id
703
- // until a remount of the same id overwrites it. The runtime path has
704
- // always been identical, and the residue is inert rather than
705
- // hidden: every one of those closures reads GAME-scoped state
706
- // (`game.input`, `game.loop.fixedDt`, the game's seed), so a stale
707
- // entry actuates exactly what a live one would. What it can still do
708
- // is name a dead world in `DEBUG_INPUT_WORLD_NOT_FOUND`'s registered
709
- // list. Do not write "the strip clears the input seams" here again
710
- // without changing `strip` to actually do it.
711
- if (!runtime) gameDebugRegistry?.strip(id);
712
- restoreRendererConfig();
713
- },
714
- };
715
- },
716
- };
717
- }