rn-gamekit 0.1.0

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 (231) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +131 -0
  4. package/lib/module/assets/defineAssets.js +48 -0
  5. package/lib/module/assets/defineAssets.js.map +1 -0
  6. package/lib/module/assets/errors.js +38 -0
  7. package/lib/module/assets/errors.js.map +1 -0
  8. package/lib/module/assets/types.js +2 -0
  9. package/lib/module/assets/types.js.map +1 -0
  10. package/lib/module/assets/validation.js +99 -0
  11. package/lib/module/assets/validation.js.map +1 -0
  12. package/lib/module/core/frameDriver.js +20 -0
  13. package/lib/module/core/frameDriver.js.map +1 -0
  14. package/lib/module/core/input/createInputBuffer.js +296 -0
  15. package/lib/module/core/input/createInputBuffer.js.map +1 -0
  16. package/lib/module/core/input/types.js +4 -0
  17. package/lib/module/core/input/types.js.map +1 -0
  18. package/lib/module/core/session/createGameSession.js +554 -0
  19. package/lib/module/core/session/createGameSession.js.map +1 -0
  20. package/lib/module/core/session/deepFreeze.js +155 -0
  21. package/lib/module/core/session/deepFreeze.js.map +1 -0
  22. package/lib/module/core/session/diagnostics.js +2 -0
  23. package/lib/module/core/session/diagnostics.js.map +1 -0
  24. package/lib/module/core/session/types.js +43 -0
  25. package/lib/module/core/session/types.js.map +1 -0
  26. package/lib/module/definition/defineGame.js +57 -0
  27. package/lib/module/definition/defineGame.js.map +1 -0
  28. package/lib/module/definition/types.js +4 -0
  29. package/lib/module/definition/types.js.map +1 -0
  30. package/lib/module/geometry/types.js +2 -0
  31. package/lib/module/geometry/types.js.map +1 -0
  32. package/lib/module/index.js +12 -0
  33. package/lib/module/index.js.map +1 -0
  34. package/lib/module/package.json +1 -0
  35. package/lib/module/react/GamePointerInput.js +385 -0
  36. package/lib/module/react/GamePointerInput.js.map +1 -0
  37. package/lib/module/react/GameView.js +223 -0
  38. package/lib/module/react/GameView.js.map +1 -0
  39. package/lib/module/react/alphaClock.js +42 -0
  40. package/lib/module/react/alphaClock.js.map +1 -0
  41. package/lib/module/react/assets/createGameAssetStore.js +358 -0
  42. package/lib/module/react/assets/createGameAssetStore.js.map +1 -0
  43. package/lib/module/react/assets/decodeSkiaImage.js +54 -0
  44. package/lib/module/react/assets/decodeSkiaImage.js.map +1 -0
  45. package/lib/module/react/assets/errors.js +10 -0
  46. package/lib/module/react/assets/errors.js.map +1 -0
  47. package/lib/module/react/assets/useGameAssets.js +160 -0
  48. package/lib/module/react/assets/useGameAssets.js.map +1 -0
  49. package/lib/module/react/bindAppLifecycle.js +58 -0
  50. package/lib/module/react/bindAppLifecycle.js.map +1 -0
  51. package/lib/module/react/bindGameSession.js +37 -0
  52. package/lib/module/react/bindGameSession.js.map +1 -0
  53. package/lib/module/react/gestureLifecycle.js +68 -0
  54. package/lib/module/react/gestureLifecycle.js.map +1 -0
  55. package/lib/module/react/instrumentation.js +4 -0
  56. package/lib/module/react/instrumentation.js.map +1 -0
  57. package/lib/module/react/pointerBinding.js +196 -0
  58. package/lib/module/react/pointerBinding.js.map +1 -0
  59. package/lib/module/react/pointerCoalescer.js +224 -0
  60. package/lib/module/react/pointerCoalescer.js.map +1 -0
  61. package/lib/module/react/pointerContainment.js +27 -0
  62. package/lib/module/react/pointerContainment.js.map +1 -0
  63. package/lib/module/react/sprites/GameSprite.js +116 -0
  64. package/lib/module/react/sprites/GameSprite.js.map +1 -0
  65. package/lib/module/react/sprites/GameWorld2D.js +44 -0
  66. package/lib/module/react/sprites/GameWorld2D.js.map +1 -0
  67. package/lib/module/react/sprites/Sprite.js +147 -0
  68. package/lib/module/react/sprites/Sprite.js.map +1 -0
  69. package/lib/module/react/sprites/SpriteBatch.js +173 -0
  70. package/lib/module/react/sprites/SpriteBatch.js.map +1 -0
  71. package/lib/module/react/sprites/spriteBatchPolicy.js +25 -0
  72. package/lib/module/react/sprites/spriteBatchPolicy.js.map +1 -0
  73. package/lib/module/react/sprites/spriteTransform.js +135 -0
  74. package/lib/module/react/sprites/spriteTransform.js.map +1 -0
  75. package/lib/module/react/viewportBinding.js +101 -0
  76. package/lib/module/react/viewportBinding.js.map +1 -0
  77. package/lib/module/react.js +13 -0
  78. package/lib/module/react.js.map +1 -0
  79. package/lib/module/scene/defineScene.js +29 -0
  80. package/lib/module/scene/defineScene.js.map +1 -0
  81. package/lib/module/scene/types.js +4 -0
  82. package/lib/module/scene/types.js.map +1 -0
  83. package/lib/module/sprites/sampleSpriteClip.js +50 -0
  84. package/lib/module/sprites/sampleSpriteClip.js.map +1 -0
  85. package/lib/module/sprites/spriteAnimationState.js +165 -0
  86. package/lib/module/sprites/spriteAnimationState.js.map +1 -0
  87. package/lib/module/testing.js +57 -0
  88. package/lib/module/testing.js.map +1 -0
  89. package/lib/module/viewport2d/index.js +4 -0
  90. package/lib/module/viewport2d/index.js.map +1 -0
  91. package/lib/module/viewport2d/math.js +129 -0
  92. package/lib/module/viewport2d/math.js.map +1 -0
  93. package/lib/module/viewport2d/types.js +2 -0
  94. package/lib/module/viewport2d/types.js.map +1 -0
  95. package/lib/typescript/package.json +1 -0
  96. package/lib/typescript/src/assets/defineAssets.d.ts +20 -0
  97. package/lib/typescript/src/assets/defineAssets.d.ts.map +1 -0
  98. package/lib/typescript/src/assets/errors.d.ts +20 -0
  99. package/lib/typescript/src/assets/errors.d.ts.map +1 -0
  100. package/lib/typescript/src/assets/types.d.ts +138 -0
  101. package/lib/typescript/src/assets/types.d.ts.map +1 -0
  102. package/lib/typescript/src/assets/validation.d.ts +15 -0
  103. package/lib/typescript/src/assets/validation.d.ts.map +1 -0
  104. package/lib/typescript/src/core/frameDriver.d.ts +12 -0
  105. package/lib/typescript/src/core/frameDriver.d.ts.map +1 -0
  106. package/lib/typescript/src/core/input/createInputBuffer.d.ts +12 -0
  107. package/lib/typescript/src/core/input/createInputBuffer.d.ts.map +1 -0
  108. package/lib/typescript/src/core/input/types.d.ts +64 -0
  109. package/lib/typescript/src/core/input/types.d.ts.map +1 -0
  110. package/lib/typescript/src/core/session/createGameSession.d.ts +18 -0
  111. package/lib/typescript/src/core/session/createGameSession.d.ts.map +1 -0
  112. package/lib/typescript/src/core/session/deepFreeze.d.ts +27 -0
  113. package/lib/typescript/src/core/session/deepFreeze.d.ts.map +1 -0
  114. package/lib/typescript/src/core/session/diagnostics.d.ts +36 -0
  115. package/lib/typescript/src/core/session/diagnostics.d.ts.map +1 -0
  116. package/lib/typescript/src/core/session/types.d.ts +104 -0
  117. package/lib/typescript/src/core/session/types.d.ts.map +1 -0
  118. package/lib/typescript/src/definition/defineGame.d.ts +66 -0
  119. package/lib/typescript/src/definition/defineGame.d.ts.map +1 -0
  120. package/lib/typescript/src/definition/types.d.ts +56 -0
  121. package/lib/typescript/src/definition/types.d.ts.map +1 -0
  122. package/lib/typescript/src/geometry/types.d.ts +8 -0
  123. package/lib/typescript/src/geometry/types.d.ts.map +1 -0
  124. package/lib/typescript/src/index.d.ts +18 -0
  125. package/lib/typescript/src/index.d.ts.map +1 -0
  126. package/lib/typescript/src/react/GamePointerInput.d.ts +37 -0
  127. package/lib/typescript/src/react/GamePointerInput.d.ts.map +1 -0
  128. package/lib/typescript/src/react/GameView.d.ts +61 -0
  129. package/lib/typescript/src/react/GameView.d.ts.map +1 -0
  130. package/lib/typescript/src/react/alphaClock.d.ts +29 -0
  131. package/lib/typescript/src/react/alphaClock.d.ts.map +1 -0
  132. package/lib/typescript/src/react/assets/createGameAssetStore.d.ts +33 -0
  133. package/lib/typescript/src/react/assets/createGameAssetStore.d.ts.map +1 -0
  134. package/lib/typescript/src/react/assets/decodeSkiaImage.d.ts +12 -0
  135. package/lib/typescript/src/react/assets/decodeSkiaImage.d.ts.map +1 -0
  136. package/lib/typescript/src/react/assets/errors.d.ts +8 -0
  137. package/lib/typescript/src/react/assets/errors.d.ts.map +1 -0
  138. package/lib/typescript/src/react/assets/useGameAssets.d.ts +35 -0
  139. package/lib/typescript/src/react/assets/useGameAssets.d.ts.map +1 -0
  140. package/lib/typescript/src/react/bindAppLifecycle.d.ts +32 -0
  141. package/lib/typescript/src/react/bindAppLifecycle.d.ts.map +1 -0
  142. package/lib/typescript/src/react/bindGameSession.d.ts +14 -0
  143. package/lib/typescript/src/react/bindGameSession.d.ts.map +1 -0
  144. package/lib/typescript/src/react/gestureLifecycle.d.ts +37 -0
  145. package/lib/typescript/src/react/gestureLifecycle.d.ts.map +1 -0
  146. package/lib/typescript/src/react/instrumentation.d.ts +47 -0
  147. package/lib/typescript/src/react/instrumentation.d.ts.map +1 -0
  148. package/lib/typescript/src/react/pointerBinding.d.ts +104 -0
  149. package/lib/typescript/src/react/pointerBinding.d.ts.map +1 -0
  150. package/lib/typescript/src/react/pointerCoalescer.d.ts +106 -0
  151. package/lib/typescript/src/react/pointerCoalescer.d.ts.map +1 -0
  152. package/lib/typescript/src/react/pointerContainment.d.ts +12 -0
  153. package/lib/typescript/src/react/pointerContainment.d.ts.map +1 -0
  154. package/lib/typescript/src/react/sprites/GameSprite.d.ts +75 -0
  155. package/lib/typescript/src/react/sprites/GameSprite.d.ts.map +1 -0
  156. package/lib/typescript/src/react/sprites/GameWorld2D.d.ts +29 -0
  157. package/lib/typescript/src/react/sprites/GameWorld2D.d.ts.map +1 -0
  158. package/lib/typescript/src/react/sprites/Sprite.d.ts +43 -0
  159. package/lib/typescript/src/react/sprites/Sprite.d.ts.map +1 -0
  160. package/lib/typescript/src/react/sprites/SpriteBatch.d.ts +41 -0
  161. package/lib/typescript/src/react/sprites/SpriteBatch.d.ts.map +1 -0
  162. package/lib/typescript/src/react/sprites/spriteBatchPolicy.d.ts +10 -0
  163. package/lib/typescript/src/react/sprites/spriteBatchPolicy.d.ts.map +1 -0
  164. package/lib/typescript/src/react/sprites/spriteTransform.d.ts +86 -0
  165. package/lib/typescript/src/react/sprites/spriteTransform.d.ts.map +1 -0
  166. package/lib/typescript/src/react/viewportBinding.d.ts +42 -0
  167. package/lib/typescript/src/react/viewportBinding.d.ts.map +1 -0
  168. package/lib/typescript/src/react.d.ts +20 -0
  169. package/lib/typescript/src/react.d.ts.map +1 -0
  170. package/lib/typescript/src/scene/defineScene.d.ts +26 -0
  171. package/lib/typescript/src/scene/defineScene.d.ts.map +1 -0
  172. package/lib/typescript/src/scene/types.d.ts +71 -0
  173. package/lib/typescript/src/scene/types.d.ts.map +1 -0
  174. package/lib/typescript/src/sprites/sampleSpriteClip.d.ts +24 -0
  175. package/lib/typescript/src/sprites/sampleSpriteClip.d.ts.map +1 -0
  176. package/lib/typescript/src/sprites/spriteAnimationState.d.ts +45 -0
  177. package/lib/typescript/src/sprites/spriteAnimationState.d.ts.map +1 -0
  178. package/lib/typescript/src/testing.d.ts +28 -0
  179. package/lib/typescript/src/testing.d.ts.map +1 -0
  180. package/lib/typescript/src/viewport2d/index.d.ts +3 -0
  181. package/lib/typescript/src/viewport2d/index.d.ts.map +1 -0
  182. package/lib/typescript/src/viewport2d/math.d.ts +25 -0
  183. package/lib/typescript/src/viewport2d/math.d.ts.map +1 -0
  184. package/lib/typescript/src/viewport2d/types.d.ts +59 -0
  185. package/lib/typescript/src/viewport2d/types.d.ts.map +1 -0
  186. package/package.json +120 -0
  187. package/src/assets/defineAssets.ts +62 -0
  188. package/src/assets/errors.ts +64 -0
  189. package/src/assets/types.ts +164 -0
  190. package/src/assets/validation.ts +158 -0
  191. package/src/core/frameDriver.ts +31 -0
  192. package/src/core/input/createInputBuffer.ts +337 -0
  193. package/src/core/input/types.ts +67 -0
  194. package/src/core/session/createGameSession.ts +652 -0
  195. package/src/core/session/deepFreeze.ts +158 -0
  196. package/src/core/session/diagnostics.ts +35 -0
  197. package/src/core/session/types.ts +122 -0
  198. package/src/definition/defineGame.ts +94 -0
  199. package/src/definition/types.ts +67 -0
  200. package/src/geometry/types.ts +7 -0
  201. package/src/index.ts +84 -0
  202. package/src/react/GamePointerInput.tsx +478 -0
  203. package/src/react/GameView.tsx +317 -0
  204. package/src/react/alphaClock.ts +59 -0
  205. package/src/react/assets/createGameAssetStore.ts +459 -0
  206. package/src/react/assets/decodeSkiaImage.ts +64 -0
  207. package/src/react/assets/errors.ts +7 -0
  208. package/src/react/assets/useGameAssets.ts +171 -0
  209. package/src/react/bindAppLifecycle.ts +79 -0
  210. package/src/react/bindGameSession.ts +41 -0
  211. package/src/react/gestureLifecycle.ts +73 -0
  212. package/src/react/instrumentation.ts +54 -0
  213. package/src/react/pointerBinding.ts +226 -0
  214. package/src/react/pointerCoalescer.ts +234 -0
  215. package/src/react/pointerContainment.ts +23 -0
  216. package/src/react/sprites/GameSprite.tsx +192 -0
  217. package/src/react/sprites/GameWorld2D.tsx +46 -0
  218. package/src/react/sprites/Sprite.tsx +194 -0
  219. package/src/react/sprites/SpriteBatch.tsx +219 -0
  220. package/src/react/sprites/spriteBatchPolicy.ts +21 -0
  221. package/src/react/sprites/spriteTransform.ts +163 -0
  222. package/src/react/viewportBinding.ts +104 -0
  223. package/src/react.ts +37 -0
  224. package/src/scene/defineScene.ts +38 -0
  225. package/src/scene/types.ts +79 -0
  226. package/src/sprites/sampleSpriteClip.ts +51 -0
  227. package/src/sprites/spriteAnimationState.ts +176 -0
  228. package/src/testing.ts +61 -0
  229. package/src/viewport2d/index.ts +7 -0
  230. package/src/viewport2d/math.ts +134 -0
  231. package/src/viewport2d/types.ts +63 -0
@@ -0,0 +1,317 @@
1
+ import { Canvas } from '@shopify/react-native-skia';
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ type ComponentType,
9
+ type ReactNode,
10
+ } from 'react';
11
+ import {
12
+ AppState,
13
+ StyleSheet,
14
+ View,
15
+ type StyleProp,
16
+ type ViewStyle,
17
+ } from 'react-native';
18
+ import { useFrameCallback, useSharedValue, type SharedValue } from 'react-native-reanimated';
19
+ import { scheduleOnRN } from 'react-native-worklets';
20
+
21
+ import type { InputMap, SceneMap } from '../definition/types';
22
+ import type { AssetGroupMap, LoadedAssets } from '../assets/types';
23
+ import type { CommitFrame, GameSession } from '../core/session/types';
24
+ import type { ResolvedViewport2D } from '../viewport2d';
25
+ import { advanceAlpha } from './alphaClock';
26
+ import { bindAppLifecycle } from './bindAppLifecycle';
27
+ import { bindGameSession } from './bindGameSession';
28
+ import type { GameViewInstrumentation } from './instrumentation';
29
+ import { bindingForViewport, type ViewportBinding } from './viewportBinding';
30
+
31
+ /** Stable imperative values supplied to a Skia renderer component. */
32
+ export interface GameRendererProps<
33
+ TScenes extends SceneMap,
34
+ TAssets extends AssetGroupMap = AssetGroupMap,
35
+ > {
36
+ /** Latest simulation commit, updated at commit frequency only. */
37
+ readonly frame: SharedValue<CommitFrame<TScenes>>;
38
+ /**
39
+ * UI-owned presentation fraction: advances on the UI runtime and resets
40
+ * to zero on every new commit. Never extrapolates past 1.
41
+ */
42
+ readonly alpha: SharedValue<number>;
43
+ /** Latest resolved viewport shared with the input adapter. */
44
+ readonly viewport: SharedValue<ResolvedViewport2D | undefined>;
45
+ /** The stable loaded asset lease (T7.5); shape-only renderers omit it. */
46
+ readonly assets?: LoadedAssets<TAssets>;
47
+ }
48
+
49
+ /** Props for the Skia-backed GameKit view. */
50
+ export interface GameViewProps<
51
+ TScenes extends SceneMap,
52
+ TInput extends InputMap,
53
+ TAssets extends AssetGroupMap = AssetGroupMap,
54
+ > {
55
+ /** Externally owned headless game session. */
56
+ readonly game: GameSession<TScenes, TInput>;
57
+ /**
58
+ * Explicit presentation key (RF6): remounts the per-session presentation
59
+ * when the session changes. The fallback is a stable per-session identity
60
+ * allocated from a WeakMap (T8.6) — never object stringification, which
61
+ * maps every session to the same value.
62
+ */
63
+ readonly presentationKey?: string | number;
64
+ /** The stable loaded asset lease; shape-only games omit it. */
65
+ readonly assets?: LoadedAssets<TAssets>;
66
+ /** Stable Skia renderer component for the session's scene snapshots. */
67
+ readonly renderer: ComponentType<GameRendererProps<TScenes, TAssets>>;
68
+ /** Optional measurement callbacks for the Performance Lab (F1). */
69
+ readonly instrumentation?: GameViewInstrumentation;
70
+ /** Static React HUD and controls mounted above the canvas. */
71
+ readonly children?: ReactNode;
72
+ /** Optional style for the mounted surface. */
73
+ readonly style?: StyleProp<ViewStyle>;
74
+ }
75
+
76
+ /**
77
+ * Context supplying the resolved viewport to pointer input children.
78
+ *
79
+ * `GamePointerInput` consumes the binding for JS-side conversion and the
80
+ * shared value for the UI-side containment mirror, so drawing and hit
81
+ * testing always use the same resolved viewport instance for a layout
82
+ * revision.
83
+ */
84
+ export interface GameViewport {
85
+ readonly binding: ViewportBinding;
86
+ /** Resolved viewport shared value (layout-only updates). */
87
+ readonly viewport: SharedValue<ResolvedViewport2D | undefined>;
88
+ }
89
+
90
+ export const GameViewportContext = createContext<GameViewport | null>(null);
91
+
92
+ /**
93
+ * Mount a headless GameSession into a Skia canvas.
94
+ *
95
+ * Simulation commits update the frame shared value at commit frequency (one
96
+ * per fixed step, never per display frame); the presentation fraction lives
97
+ * on the UI runtime and is advanced by a frame callback that resets on every
98
+ * new commit, clamps at 1, and never extrapolates. The clock is gated by a
99
+ * `running` shared value so pause, background, unmount, and dispose hold the
100
+ * presentation. React owns the surface and static overlay tree; it is never
101
+ * the frame store. The session is started while mounted, paused on cleanup
102
+ * and app backgrounding, and is never disposed by this component — the
103
+ * creator owns disposal.
104
+ */
105
+ /**
106
+ * Per-session presentation binding (RF6).
107
+ *
108
+ * Keyed by the session so the frame, alpha, epoch, and clock initialize from
109
+ * the new session BEFORE its renderer can execute — a replacement renderer
110
+ * never observes the previous session's frame, even when both scenes are
111
+ * named the same.
112
+ */
113
+ function GamePresentation<
114
+ TScenes extends SceneMap,
115
+ TInput extends InputMap,
116
+ TAssets extends AssetGroupMap,
117
+ >({
118
+ game,
119
+ assets,
120
+ renderer,
121
+ viewportValue,
122
+ instrumentationRef,
123
+ }: {
124
+ readonly game: GameSession<TScenes, TInput>;
125
+ readonly assets: LoadedAssets<TAssets> | undefined;
126
+ readonly renderer: ComponentType<GameRendererProps<TScenes, TAssets>>;
127
+ readonly viewportValue: SharedValue<ResolvedViewport2D | undefined>;
128
+ readonly instrumentationRef: { readonly current: GameViewInstrumentation | undefined };
129
+ }) {
130
+ const Renderer = renderer;
131
+ const frame = useSharedValue<CommitFrame<TScenes>>(() => game.getRenderFrame());
132
+ const alpha = useSharedValue(0);
133
+ const running = useSharedValue(true);
134
+ // Binding epoch: bumped per (re)subscription so a replacement session whose
135
+ // revision restarts at zero is still accepted by the UI clock.
136
+ const epoch = useSharedValue(0);
137
+ const clockEpoch = useSharedValue(0);
138
+ const clockRevision = useSharedValue(-1);
139
+ // F1: last revision/epoch the UI clock observed, so the instrumentation can
140
+ // report the FIRST UI frame that saw a new commit.
141
+ const observedRevision = useSharedValue(-1);
142
+ const observedEpoch = useSharedValue(-1);
143
+
144
+ useEffect(() => {
145
+ epoch.value += 1;
146
+ // RF6: the frame is seeded at mount from this session; the keyed remount
147
+ // guarantees the renderer below never reads the previous session's frame.
148
+ frame.value = game.getRenderFrame();
149
+ const cleanupBinding = bindGameSession(game, (nextFrame) => {
150
+ frame.value = nextFrame;
151
+ instrumentationRef.current?.onPresentCommit?.(nextFrame.revision, Date.now());
152
+ });
153
+ const cleanupLifecycle = bindAppLifecycle(AppState, {
154
+ getStatus: () => game.status,
155
+ pause: () => {
156
+ running.value = false;
157
+ if (game.status !== 'disposed') {
158
+ game.pause();
159
+ }
160
+ },
161
+ resume: () => {
162
+ if (game.status !== 'disposed') {
163
+ game.start();
164
+ running.value = true;
165
+ }
166
+ },
167
+ });
168
+ return () => {
169
+ cleanupLifecycle();
170
+ cleanupBinding();
171
+ };
172
+ }, [epoch, frame, game, running]);
173
+
174
+ // F1: the first UI frame that sees a new commit revision is reported back
175
+ // to the RN runtime through scheduleOnRN — never by calling the JS hook
176
+ // directly from the UI worklet (cross-runtime calls crash the app).
177
+ const reportUiObserved = useCallback((revision: number, atMs: number) => {
178
+ instrumentationRef.current?.onUiRevisionObserved?.(revision, atMs);
179
+ }, []);
180
+
181
+
182
+ // UI-owned alpha clock: advances only while the session is running,
183
+ // resets on every new commit, clamps at 1 and holds (no extrapolation).
184
+ useFrameCallback((frameInfo) => {
185
+ 'worklet';
186
+ if (!running.value) {
187
+ return;
188
+ }
189
+ const envelope = frame.value;
190
+ if (observedEpoch.value !== epoch.value || observedRevision.value !== envelope.revision) {
191
+ observedEpoch.value = epoch.value;
192
+ observedRevision.value = envelope.revision;
193
+ scheduleOnRN(reportUiObserved, envelope.revision, Date.now());
194
+ }
195
+ const previousState = {
196
+ epoch: clockEpoch.value,
197
+ revision: clockRevision.value,
198
+ alpha: alpha.value,
199
+ };
200
+ const nextState = advanceAlpha(
201
+ previousState,
202
+ {
203
+ epoch: epoch.value,
204
+ revision: envelope.revision,
205
+ stepMs: envelope.stepMs,
206
+ },
207
+ frameInfo.timeSincePreviousFrame ?? 1000 / 60,
208
+ );
209
+ clockEpoch.value = nextState.epoch;
210
+ clockRevision.value = nextState.revision;
211
+ alpha.value = nextState.alpha;
212
+ });
213
+
214
+ return (
215
+ <Renderer
216
+ frame={frame}
217
+ alpha={alpha}
218
+ viewport={viewportValue}
219
+ {...(assets === undefined ? {} : { assets })}
220
+ />
221
+ );
222
+ }
223
+
224
+ // Stable per-session fallback identity (T8.6): one incrementing id per
225
+ // session object for the life of the module. A replaced session is a new
226
+ // object and therefore a new id; the same session keeps its id across
227
+ // renders so the presentation never remounts spuriously.
228
+ const sessionPresentationIds = new WeakMap<object, number>();
229
+ let nextSessionPresentationId = 1;
230
+ function sessionPresentationId(game: object): number {
231
+ let id = sessionPresentationIds.get(game);
232
+ if (id === undefined) {
233
+ id = nextSessionPresentationId;
234
+ nextSessionPresentationId += 1;
235
+ sessionPresentationIds.set(game, id);
236
+ }
237
+ return id;
238
+ }
239
+
240
+ export function GameView<
241
+ TScenes extends SceneMap,
242
+ TInput extends InputMap,
243
+ TAssets extends AssetGroupMap = AssetGroupMap,
244
+ >({
245
+ game,
246
+ presentationKey,
247
+ assets,
248
+ renderer: Renderer,
249
+ children,
250
+ style,
251
+ instrumentation,
252
+ }: GameViewProps<TScenes, TInput, TAssets>) {
253
+ const instrumentationRef = useRef(instrumentation);
254
+
255
+ const bindingRef = useRef<ViewportBinding | null>(null);
256
+ bindingRef.current = bindingForViewport(game.viewport, bindingRef.current);
257
+ const binding = bindingRef.current;
258
+ const viewportValue = useSharedValue<ResolvedViewport2D | undefined>(undefined);
259
+ const viewportContext = useMemo<GameViewport>(
260
+ () => ({ binding, viewport: viewportValue }),
261
+ [binding, viewportValue],
262
+ );
263
+
264
+ useEffect(() => {
265
+ instrumentationRef.current = instrumentation;
266
+ }, [instrumentation]);
267
+
268
+ useEffect(() => {
269
+ viewportValue.value = binding.resolved;
270
+ }, [binding, viewportValue]);
271
+
272
+ useEffect(
273
+ () => () => {
274
+ binding.dispose();
275
+ },
276
+ [binding],
277
+ );
278
+
279
+ return (
280
+ <GameViewportContext.Provider value={viewportContext}>
281
+ <View
282
+ style={[styles.surface, style]}
283
+ onLayout={(event) => {
284
+ const { width, height } = event.nativeEvent.layout;
285
+ binding.setSurfaceSize({ width, height });
286
+ viewportValue.value = binding.resolved;
287
+ }}
288
+ >
289
+ <Canvas style={StyleSheet.absoluteFill}>
290
+ {/* RF6/T8.6: the per-session presentation is keyed by an explicit
291
+ presentation key when provided, otherwise by a stable
292
+ per-session identity — never object stringification — so the
293
+ frame/alpha/epoch initialize before the renderer can run. */}
294
+ <GamePresentation
295
+ key={presentationKey ?? sessionPresentationId(game)}
296
+ game={game}
297
+ assets={assets}
298
+ renderer={Renderer}
299
+ viewportValue={viewportValue}
300
+ instrumentationRef={instrumentationRef}
301
+ />
302
+ </Canvas>
303
+ {children === undefined ? null : (
304
+ <View pointerEvents="box-none" style={StyleSheet.absoluteFill}>
305
+ {children}
306
+ </View>
307
+ )}
308
+ </View>
309
+ </GameViewportContext.Provider>
310
+ );
311
+ }
312
+
313
+ const styles = StyleSheet.create({
314
+ surface: {
315
+ overflow: 'hidden',
316
+ },
317
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * UI-owned alpha clock (T5).
3
+ *
4
+ * Pure, worklet-safe presentation clock: `alpha` advances from UI frame
5
+ * deltas against the simulation step duration and resets to zero whenever a
6
+ * newer commit arrives. It never extrapolates past the current snapshot
7
+ * (clamped at 1, then held) and ignores stale or duplicate commits so a
8
+ * delayed write cannot reset presentation backward.
9
+ *
10
+ * The `epoch` guards session replacement: a new GameView binding carries a
11
+ * fresh epoch, so a replacement session whose revision restarts at zero is
12
+ * still accepted, while a delayed write from an old epoch is ignored.
13
+ */
14
+
15
+ export interface AlphaClockCommit {
16
+ /** Monotonic binding epoch; new epoch accepts any revision. */
17
+ readonly epoch: number;
18
+ /** Session commit revision; new revisions reset the clock. */
19
+ readonly revision: number;
20
+ /** Fixed simulation step duration in milliseconds. */
21
+ readonly stepMs: number;
22
+ }
23
+
24
+ export interface AlphaClockState {
25
+ readonly epoch: number;
26
+ readonly revision: number;
27
+ readonly alpha: number;
28
+ }
29
+
30
+ /** Advance the clock by one UI frame; returns the next alpha state. */
31
+ export function advanceAlpha(
32
+ previous: AlphaClockState,
33
+ commit: AlphaClockCommit,
34
+ frameDeltaMs: number,
35
+ ): AlphaClockState {
36
+ 'worklet';
37
+ if (
38
+ commit.epoch > previous.epoch ||
39
+ (commit.epoch === previous.epoch && commit.revision > previous.revision)
40
+ ) {
41
+ return { epoch: commit.epoch, revision: commit.revision, alpha: 0 };
42
+ }
43
+ if (
44
+ commit.epoch < previous.epoch ||
45
+ (commit.epoch === previous.epoch && commit.revision < previous.revision)
46
+ ) {
47
+ return previous; // A stale write is ignored entirely, alpha preserved.
48
+ }
49
+ if (previous.alpha >= 1) {
50
+ return previous; // Clamp at 1: hold, never extrapolate.
51
+ }
52
+ const delta = Math.max(0, frameDeltaMs) / commit.stepMs;
53
+ const next = previous.alpha + delta;
54
+ return {
55
+ epoch: previous.epoch,
56
+ revision: previous.revision,
57
+ alpha: next >= 1 ? 1 : next,
58
+ };
59
+ }