@nayan-ui/engine 1.0.0-beta.3 → 1.0.0-beta.5

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 (71) hide show
  1. package/README.md +229 -113
  2. package/lib/module/assets/AssetLoader.js +1 -1
  3. package/lib/module/assets/ProceduralCache.js +31 -4
  4. package/lib/module/assets/font.js +10 -7
  5. package/lib/module/assets/model.js +41 -19
  6. package/lib/module/assets/storage.shared.js +2 -0
  7. package/lib/module/assets/storage.web.js +1 -0
  8. package/lib/module/assets/texture.js +14 -11
  9. package/lib/module/index.js +4 -1
  10. package/lib/module/media/audio.js +6 -3
  11. package/lib/module/native/BrowserEngine.js +17 -0
  12. package/lib/module/render/GameView.js +86 -18
  13. package/lib/module/render/light.js +24 -0
  14. package/lib/module/render/quality.js +12 -5
  15. package/lib/module/render/useWorld.js +72 -0
  16. package/lib/module/world/Scoreboard.js +21 -3
  17. package/lib/module/world/TransformBatch.js +5 -3
  18. package/lib/module/world/World.js +27 -13
  19. package/lib/module/world/color.js +80 -0
  20. package/lib/module/world/desc.js +6 -5
  21. package/lib/module/world/effects.js +20 -8
  22. package/lib/typescript/src/assets/AssetLoader.d.ts +1 -1
  23. package/lib/typescript/src/assets/ProceduralCache.d.ts +12 -0
  24. package/lib/typescript/src/assets/model.d.ts +10 -0
  25. package/lib/typescript/src/index.d.ts +4 -1
  26. package/lib/typescript/src/native/EngineBridge.d.ts +6 -2
  27. package/lib/typescript/src/native/spec/NativeNayanEngine.d.ts +29 -0
  28. package/lib/typescript/src/render/GameView.d.ts +19 -7
  29. package/lib/typescript/src/render/light.d.ts +12 -0
  30. package/lib/typescript/src/render/quality.d.ts +4 -2
  31. package/lib/typescript/src/render/useWorld.d.ts +20 -0
  32. package/lib/typescript/src/types.d.ts +13 -4
  33. package/lib/typescript/src/world/Scoreboard.d.ts +12 -2
  34. package/lib/typescript/src/world/TransformBatch.d.ts +2 -2
  35. package/lib/typescript/src/world/World.d.ts +10 -2
  36. package/lib/typescript/src/world/color.d.ts +18 -0
  37. package/lib/typescript/src/world/effects.d.ts +13 -12
  38. package/lib/typescript/src/world/types.d.ts +11 -6
  39. package/lib/web/engine_core_bg.wasm +0 -0
  40. package/lib/web/index.js +521 -255
  41. package/lib/web/types/src/assets/AssetLoader.d.ts +1 -1
  42. package/lib/web/types/src/assets/ProceduralCache.d.ts +12 -0
  43. package/lib/web/types/src/assets/model.d.ts +10 -0
  44. package/lib/web/types/src/index.d.ts +4 -1
  45. package/lib/web/types/src/native/EngineBridge.d.ts +6 -2
  46. package/lib/web/types/src/native/spec/NativeNayanEngine.d.ts +29 -0
  47. package/lib/web/types/src/render/GameView.d.ts +19 -7
  48. package/lib/web/types/src/render/light.d.ts +11 -0
  49. package/lib/web/types/src/render/quality.d.ts +4 -2
  50. package/lib/web/types/src/render/useWorld.d.ts +19 -0
  51. package/lib/web/types/src/types.d.ts +13 -4
  52. package/lib/web/types/src/world/Scoreboard.d.ts +12 -2
  53. package/lib/web/types/src/world/TransformBatch.d.ts +2 -2
  54. package/lib/web/types/src/world/World.d.ts +10 -2
  55. package/lib/web/types/src/world/color.d.ts +17 -0
  56. package/lib/web/types/src/world/effects.d.ts +13 -12
  57. package/lib/web/types/src/world/types.d.ts +11 -6
  58. package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceView.java +87 -26
  59. package/native/core/build/EngineCore.xcframework/ios-arm64/libengine_core.a +0 -0
  60. package/native/core/build/EngineCore.xcframework/ios-arm64-simulator/libengine_core.a +0 -0
  61. package/native/core/build/android/arm64-v8a/libengine_core.a +0 -0
  62. package/native/core/build/android/armeabi-v7a/libengine_core.a +0 -0
  63. package/native/core/build/android/x86/libengine_core.a +0 -0
  64. package/native/core/build/android/x86_64/libengine_core.a +0 -0
  65. package/native/core/include/engine_core.h +9 -0
  66. package/native/cpp/NayanEngineModule.cpp +171 -0
  67. package/native/cpp/NayanEngineModule.h +30 -0
  68. package/native/cpp/generated/NayanEngineSpecJSI.h +105 -0
  69. package/native/ios/NayanEngineSurface.mm +30 -1
  70. package/package.json +2 -2
  71. package/src/native/spec/NativeNayanEngine.ts +39 -0
@@ -8,19 +8,27 @@ import com.facebook.react.bridge.WritableMap;
8
8
  import com.facebook.react.uimanager.UIManagerHelper;
9
9
  import com.facebook.react.uimanager.events.Event;
10
10
  import com.facebook.react.uimanager.events.EventDispatcher;
11
- import android.graphics.SurfaceTexture;
11
+ import android.graphics.PixelFormat;
12
+ import android.os.Build;
12
13
  import android.view.Surface;
14
+ import android.view.SurfaceHolder;
15
+ import android.view.SurfaceView;
13
16
  import android.view.MotionEvent;
14
17
  import android.view.ScaleGestureDetector;
15
18
  import android.view.Choreographer;
16
- import android.view.TextureView;
17
19
 
18
20
  /**
19
- * A surface the Rust renderer draws into (Vulkan, or OpenGL ES when Vulkan isn't usable). A
20
- * TextureView keeps the last frame in the view tree during navigation's removal animation, even
21
- * after React stops rendering. JS refers to it by `surfaceId`.
21
+ * A surface the Rust renderer draws into (Vulkan, or OpenGL ES when Vulkan isn't usable). JS refers
22
+ * to it by `surfaceId`.
23
+ *
24
+ * A SurfaceView, not a TextureView: its frames go straight to the system compositor (often as a
25
+ * hardware overlay), instead of being copied into the app's own view tree by the UI thread's
26
+ * renderer every frame. With a TextureView each game frame also redraws the React Native window and
27
+ * makes the JS thread wait on the UI thread for a free buffer, which is what made heavy 3D scenes
28
+ * stutter and hang on Android. The surface keeps showing its last frame after the renderer detaches,
29
+ * until the view leaves the window, so navigation's exit animation still shows the game.
22
30
  */
23
- public class EngineSurfaceView extends TextureView implements TextureView.SurfaceTextureListener, Choreographer.FrameCallback {
31
+ public class EngineSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Choreographer.FrameCallback {
24
32
  private int surfaceId = -1;
25
33
  private Surface surface;
26
34
  private int width;
@@ -29,6 +37,33 @@ public class EngineSurfaceView extends TextureView implements TextureView.Surfac
29
37
  private int drawingMode;
30
38
  private int drawingBackground = 0xffffff;
31
39
  private boolean drawingTick;
40
+ private boolean clockRunning;
41
+
42
+ /**
43
+ * The game's display clock: once per refresh while the surface is attached and on screen, the
44
+ * engine is asked for its next frame on the JS thread (see `deliverFrame`). Unlike React Native's
45
+ * requestAnimationFrame, which can tick at half the display's rate, this follows every vsync; the
46
+ * frame itself is drawn on the engine's render thread.
47
+ */
48
+ private final Choreographer.FrameCallback clock = new Choreographer.FrameCallback() {
49
+ @Override public void doFrame(long frameTimeNanos) {
50
+ if (!clockRunning) return;
51
+ if (surfaceId >= 0 && surface != null) nativeVsync(surfaceId, frameTimeNanos);
52
+ Choreographer.getInstance().postFrameCallback(this);
53
+ }
54
+ };
55
+
56
+ private void startClock() {
57
+ if (!clockRunning && surfaceId >= 0 && surface != null && isShown()) {
58
+ clockRunning = true;
59
+ Choreographer.getInstance().postFrameCallback(clock);
60
+ }
61
+ }
62
+
63
+ private void stopClock() {
64
+ clockRunning = false;
65
+ Choreographer.getInstance().removeFrameCallback(clock);
66
+ }
32
67
  private long lastDrawingEvent;
33
68
  private int drawingPointer = -1;
34
69
  private boolean pencil;
@@ -38,7 +73,9 @@ public class EngineSurfaceView extends TextureView implements TextureView.Surfac
38
73
 
39
74
  public EngineSurfaceView(Context context) {
40
75
  super(context);
41
- setSurfaceTextureListener(this);
76
+ getHolder().addCallback(this);
77
+ // Frames are opaque: the compositor can skip blending the layer.
78
+ getHolder().setFormat(PixelFormat.OPAQUE);
42
79
  density = context.getResources().getDisplayMetrics().density;
43
80
  scaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
44
81
  @Override public boolean onScale(ScaleGestureDetector detector) {
@@ -60,10 +97,12 @@ public class EngineSurfaceView extends TextureView implements TextureView.Surfac
60
97
  }
61
98
 
62
99
  /**
63
- * Stops the renderer, but leaves the texture visible until navigation removes the view. Hiding it
64
- * here would replace the game's last frame with an empty screen during the exit animation.
100
+ * Stops the renderer, but leaves the surface (and its last frame) visible until navigation removes
101
+ * the view. Hiding it here would replace the game's last frame with an empty screen during the
102
+ * exit animation.
65
103
  */
66
104
  public void release() {
105
+ stopClock();
67
106
  stopDrawing();
68
107
  drawingId = -1;
69
108
  if (surfaceId >= 0) {
@@ -73,49 +112,62 @@ public class EngineSurfaceView extends TextureView implements TextureView.Surfac
73
112
  releaseSurface();
74
113
  }
75
114
 
115
+ /**
116
+ * Games ask for 60 frames a second. A SurfaceView's frames never invalidate the view tree, so on
117
+ * Android 15+ the system otherwise judges the app idle and drops its vsync (and with it React
118
+ * Native's frame callbacks) to 30 or 20 Hz, halving the frame rate and doubling load times.
119
+ */
120
+ private static final float GAME_FRAME_RATE = 60f;
121
+
122
+ private void requestFrameRate() {
123
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && surface != null && surface.isValid()) {
124
+ try {
125
+ surface.setFrameRate(GAME_FRAME_RATE, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
126
+ } catch (IllegalArgumentException | IllegalStateException ignored) {
127
+ // A surface being torn down; the next attach asks again.
128
+ }
129
+ }
130
+ if (Build.VERSION.SDK_INT >= 35) setRequestedFrameRate(REQUESTED_FRAME_RATE_CATEGORY_HIGH);
131
+ }
132
+
76
133
  private void attach() {
77
134
  if (surfaceId >= 0 && surface != null && surface.isValid() && width > 0 && height > 0) {
78
135
  nativeAttach(surfaceId, surface, width, height);
136
+ requestFrameRate();
79
137
  if (drawingId >= 0) nativeDrawingInvalidate(drawingId);
80
138
  startDrawing();
139
+ startClock();
81
140
  }
82
141
  }
83
142
 
84
143
  @Override
85
- public void onSurfaceTextureAvailable(SurfaceTexture texture, int w, int h) {
86
- surface = new Surface(texture);
87
- width = w;
88
- height = h;
89
- attach();
144
+ public void surfaceCreated(SurfaceHolder holder) {
145
+ surface = holder.getSurface();
90
146
  }
91
147
 
92
148
  @Override
93
- public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int w, int h) {
149
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
150
+ surface = holder.getSurface();
94
151
  width = w;
95
152
  height = h;
96
153
  attach();
97
154
  }
98
155
 
99
156
  @Override
100
- public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
157
+ public void surfaceDestroyed(SurfaceHolder holder) {
158
+ stopClock();
101
159
  stopDrawing();
102
- // Wait for the renderer before Android destroys the texture. Keep the id so a screen covered by
160
+ // Wait for the renderer before Android destroys the surface. Keep the id so a screen covered by
103
161
  // another route can attach a new surface when it comes back from the back stack.
104
162
  if (surfaceId >= 0) {
105
163
  nativeDetach(surfaceId);
106
164
  }
107
165
  releaseSurface();
108
- return true;
109
166
  }
110
167
 
111
- @Override
112
- public void onSurfaceTextureUpdated(SurfaceTexture texture) {}
113
-
168
+ /** The holder owns the surface: only forget it (releasing it here would break the next attach). */
114
169
  private void releaseSurface() {
115
- if (surface != null) {
116
- surface.release();
117
- surface = null;
118
- }
170
+ surface = null;
119
171
  }
120
172
 
121
173
  public void setDrawingId(int id) {
@@ -173,12 +225,19 @@ public class EngineSurfaceView extends TextureView implements TextureView.Surfac
173
225
  if (focused) startDrawing(); else stopDrawing();
174
226
  }
175
227
  @Override protected void onDetachedFromWindow() {
228
+ stopClock();
176
229
  stopDrawing();
177
230
  super.onDetachedFromWindow();
178
231
  }
179
232
  @Override protected void onWindowVisibilityChanged(int visibility) {
180
233
  super.onWindowVisibilityChanged(visibility);
181
- if (visibility == VISIBLE) startDrawing(); else stopDrawing();
234
+ if (visibility == VISIBLE) {
235
+ startDrawing();
236
+ startClock();
237
+ } else {
238
+ stopDrawing();
239
+ stopClock();
240
+ }
182
241
  }
183
242
  @Override public boolean onTouchEvent(MotionEvent event) {
184
243
  if (drawingId < 0) return super.onTouchEvent(event);
@@ -231,4 +290,6 @@ public class EngineSurfaceView extends TextureView implements TextureView.Surfac
231
290
  private static native boolean nativeAttach(int surfaceId, Surface surface, int width, int height);
232
291
 
233
292
  private static native void nativeDetach(int surfaceId);
293
+
294
+ private static native void nativeVsync(int surfaceId, long frameTimeNanos);
234
295
  }
@@ -265,6 +265,15 @@ void engine_android_init(void *java_vm, void *context);
265
265
  // Generates and registers all built-in shape meshes (cube, sphere, etc.) with the renderer.
266
266
  void engine_generate_builtin_meshes(void);
267
267
  // Vertex / index count of a registered renderer mesh, or 0.
268
+ /* engine_render_frame's arguments, but the frame is copied and drawn on the render thread; returns at once with the
269
+ * view-projection written. 1 once the surface has presented a frame, 0 before, -1 if the last frame failed. */
270
+ int32_t engine_render_publish(int32_t surface, const double *frame, size_t frame_len, const float *matrices, const float *colors,
271
+ const float *regions, size_t capacity, const uint32_t *ranges, size_t ranges_len, size_t count,
272
+ float *out_view_proj, const World *world);
273
+ /* [frames presented, smoothed render ms] for a surface drawn by the render thread. */
274
+ void engine_render_stats(int32_t surface, double *out);
275
+ /* A core model's geometry (mesh_create, glTF, glyphs) to the renderer, inside Rust; texture < 0 = none. 1, or 0 if unknown. */
276
+ int32_t engine_render_model(uint32_t mesh, int32_t texture, float shine);
268
277
  size_t engine_render_mesh_vertex_count(uint32_t mesh);
269
278
  size_t engine_render_mesh_index_count(uint32_t mesh);
270
279
  // Copies geometry into caller-owned buffers. Returns the actual count.
@@ -3,7 +3,14 @@
3
3
  #include <engine_core.h>
4
4
 
5
5
  #include <algorithm>
6
+ #include <atomic>
6
7
  #include <cstring>
8
+ #include <mutex>
9
+ #include <unordered_map>
10
+ #include <unordered_set>
11
+ #include <functional>
12
+ #include <thread>
13
+ #include <vector>
7
14
 
8
15
  #ifdef __ANDROID__
9
16
  #include <fbjni/fbjni.h>
@@ -130,8 +137,48 @@ void initAndroid() {
130
137
 
131
138
  } // namespace
132
139
 
140
+ namespace {
141
+ // Display-clock frames. The registry holds no JS values (those stay in the module, on the JS thread):
142
+ // the clock's thread only schedules a call by surface id, and a module torn down by a reload is never
143
+ // called again.
144
+ struct FrameClient {
145
+ std::shared_ptr<CallInvoker> invoker;
146
+ NayanEngineModule *owner;
147
+ std::shared_ptr<std::atomic<bool>> pending;
148
+ };
149
+ std::mutex framesMutex;
150
+ std::unordered_map<int, FrameClient> frameClients;
151
+ std::unordered_set<NayanEngineModule *> liveModules; // guarded by framesMutex
152
+ } // namespace
153
+
154
+ void deliverFrame(int surface, double timeMs) {
155
+ std::shared_ptr<CallInvoker> invoker;
156
+ NayanEngineModule *owner = nullptr;
157
+ std::shared_ptr<std::atomic<bool>> pending;
158
+ {
159
+ std::lock_guard<std::mutex> lock(framesMutex);
160
+ auto it = frameClients.find(surface);
161
+ if (it == frameClients.end() || it->second.pending->exchange(true)) return; // none, or JS hasn't run the last one
162
+ invoker = it->second.invoker;
163
+ owner = it->second.owner;
164
+ pending = it->second.pending;
165
+ }
166
+ invoker->invokeAsync(SchedulerPriority::UserBlockingPriority, [owner, surface, timeMs, pending](jsi::Runtime &rt) {
167
+ pending->store(false);
168
+ {
169
+ std::lock_guard<std::mutex> lock(framesMutex);
170
+ if (liveModules.count(owner) == 0) return;
171
+ }
172
+ owner->callFrame(rt, surface, timeMs); // modules are destroyed on this (JS) thread, so it's still alive here
173
+ });
174
+ }
175
+
133
176
  NayanEngineModule::NayanEngineModule(std::shared_ptr<CallInvoker> jsInvoker)
134
177
  : NativeNayanEngineCxxSpec(std::move(jsInvoker)) {
178
+ {
179
+ std::lock_guard<std::mutex> lock(framesMutex);
180
+ liveModules.insert(this);
181
+ }
135
182
  #ifdef __ANDROID__
136
183
  initAndroid();
137
184
  #endif
@@ -139,9 +186,45 @@ NayanEngineModule::NayanEngineModule(std::shared_ptr<CallInvoker> jsInvoker)
139
186
 
140
187
  // Worlds are freed when the module and every buffer aliasing them are gone.
141
188
  NayanEngineModule::~NayanEngineModule() {
189
+ {
190
+ std::lock_guard<std::mutex> lock(framesMutex);
191
+ liveModules.erase(this);
192
+ for (auto it = frameClients.begin(); it != frameClients.end();) it = it->second.owner == this ? frameClients.erase(it) : std::next(it);
193
+ }
142
194
  for (int document : drawings_) engine_drawing_destroy(document);
143
195
  }
144
196
 
197
+ bool NayanEngineModule::hasFrameClock(jsi::Runtime &) {
198
+ #if defined(__ANDROID__) || defined(__APPLE__)
199
+ return true; // EngineSurfaceView's Choreographer / NayanEngineSurfaceView's CADisplayLink call deliverFrame
200
+ #else
201
+ return false;
202
+ #endif
203
+ }
204
+
205
+ void NayanEngineModule::startFrames(jsi::Runtime &rt, double surface, jsi::Function onFrame) {
206
+ int s = static_cast<int>(surface);
207
+ frameCallbacks_.erase(s);
208
+ frameCallbacks_.emplace(s, std::move(onFrame));
209
+ std::lock_guard<std::mutex> lock(framesMutex);
210
+ frameClients[s] = FrameClient{jsInvoker_, this, std::make_shared<std::atomic<bool>>(false)};
211
+ }
212
+
213
+ void NayanEngineModule::stopFrames(jsi::Runtime &, double surface) {
214
+ int s = static_cast<int>(surface);
215
+ {
216
+ std::lock_guard<std::mutex> lock(framesMutex);
217
+ auto it = frameClients.find(s);
218
+ if (it != frameClients.end() && it->second.owner == this) frameClients.erase(it);
219
+ }
220
+ frameCallbacks_.erase(s);
221
+ }
222
+
223
+ void NayanEngineModule::callFrame(jsi::Runtime &rt, int surface, double timeMs) {
224
+ auto it = frameCallbacks_.find(surface);
225
+ if (it != frameCallbacks_.end()) it->second.call(rt, timeMs);
226
+ }
227
+
145
228
  namespace {
146
229
  // NaN, fractional and out-of-range ids are unknown (casting them to int is undefined or aliases another id).
147
230
  bool knownWorldId(double worldId, int next) {
@@ -213,6 +296,26 @@ jsi::ArrayBuffer fileData(jsi::Runtime &rt, jsi::Object &data, const char *metho
213
296
  return data.getArrayBuffer(rt);
214
297
  }
215
298
 
299
+ /** An ArrayBuffer's bytes, copied so a background thread can read them after the JS call returns. */
300
+ std::vector<uint8_t> copyBytes(jsi::Runtime &rt, jsi::Object &data, const char *method) {
301
+ jsi::ArrayBuffer buffer = fileData(rt, data, method);
302
+ const uint8_t *bytes = buffer.data(rt);
303
+ return std::vector<uint8_t>(bytes, bytes + buffer.size(rt));
304
+ }
305
+
306
+ /**
307
+ * Runs `job` (which must not touch JSI) on its own native thread and settles a JS promise with its
308
+ * result on the JS thread. Decoding images, glTF models, fonts and sounds this way keeps them off
309
+ * the JS thread, and several loads run in parallel on the phone's cores. The core's asset
310
+ * registries are locked, so concurrent loads are safe.
311
+ */
312
+ jsi::Value runInBackground(jsi::Runtime &rt, const std::shared_ptr<CallInvoker> &jsInvoker, std::function<double()> job) {
313
+ auto promise = std::make_shared<AsyncPromise<double>>(rt, jsInvoker);
314
+ jsi::Value result(rt, promise->get(rt));
315
+ std::thread([promise, job = std::move(job)]() { promise->resolve(job()); }).detach();
316
+ return result;
317
+ }
318
+
216
319
  } // namespace
217
320
 
218
321
  double NayanEngineModule::spawnDesc(jsi::Runtime &rt, double world, jsi::Object desc) {
@@ -438,6 +541,13 @@ double NayanEngineModule::modelLoad(jsi::Runtime &rt, jsi::Object data, bool cen
438
541
  return engine_model_load(buffer.data(rt), buffer.size(rt), center ? 1 : 0, static_cast<float>(fit)); // copied
439
542
  }
440
543
 
544
+ jsi::Value NayanEngineModule::modelLoadAsync(jsi::Runtime &rt, jsi::Object data, bool center, double fit) {
545
+ auto bytes = std::make_shared<std::vector<uint8_t>>(copyBytes(rt, data, "modelLoadAsync"));
546
+ return runInBackground(rt, jsInvoker_, [bytes, center, fit]() {
547
+ return static_cast<double>(engine_model_load(bytes->data(), bytes->size(), center ? 1 : 0, static_cast<float>(fit)));
548
+ });
549
+ }
550
+
441
551
  jsi::Object NayanEngineModule::modelVertices(jsi::Runtime &rt, double mesh) {
442
552
  uint32_t m = id(mesh);
443
553
  return external(rt, nullptr, engine_model_vertices(m), static_cast<size_t>(engine_model_vertex_count(m)) * 11 * sizeof(float));
@@ -458,6 +568,11 @@ double NayanEngineModule::modelTexture(jsi::Runtime &, double mesh) {
458
568
  return engine_model_texture(id(mesh));
459
569
  }
460
570
 
571
+ bool NayanEngineModule::renderModel(jsi::Runtime &, double mesh, double texture, double shine) {
572
+ int32_t t = texture >= 0 && texture <= INT32_MAX ? static_cast<int32_t>(texture) : -1;
573
+ return engine_render_model(id(mesh), t, static_cast<float>(shine)) != 0;
574
+ }
575
+
461
576
  double NayanEngineModule::meshAlias(jsi::Runtime &, double base) {
462
577
  return engine_mesh_alias(id(base));
463
578
  }
@@ -556,6 +671,11 @@ double NayanEngineModule::textureLoad(jsi::Runtime &rt, jsi::Object data) {
556
671
  return engine_texture_load(buffer.data(rt), buffer.size(rt)); // decoded and copied
557
672
  }
558
673
 
674
+ jsi::Value NayanEngineModule::textureLoadAsync(jsi::Runtime &rt, jsi::Object data) {
675
+ auto bytes = std::make_shared<std::vector<uint8_t>>(copyBytes(rt, data, "textureLoadAsync"));
676
+ return runInBackground(rt, jsInvoker_, [bytes]() { return static_cast<double>(engine_texture_load(bytes->data(), bytes->size())); });
677
+ }
678
+
559
679
  jsi::Object NayanEngineModule::texturePixels(jsi::Runtime &rt, double texture) {
560
680
  uint32_t size[2] = {0, 0};
561
681
  engine_texture_size(id(texture), size);
@@ -574,6 +694,15 @@ double NayanEngineModule::fontLoad(jsi::Runtime &rt, jsi::Object data, double de
574
694
  return engine_font_load(buffer.data(rt), buffer.size(rt), static_cast<float>(depth), utf8.data(), utf8.size(), static_cast<float>(weight));
575
695
  }
576
696
 
697
+ jsi::Value NayanEngineModule::fontLoadAsync(jsi::Runtime &rt, jsi::Object data, double depth, jsi::String chars, double weight) {
698
+ auto bytes = std::make_shared<std::vector<uint8_t>>(copyBytes(rt, data, "fontLoadAsync"));
699
+ auto utf8 = std::make_shared<std::string>(chars.utf8(rt));
700
+ return runInBackground(rt, jsInvoker_, [bytes, utf8, depth, weight]() {
701
+ return static_cast<double>(
702
+ engine_font_load(bytes->data(), bytes->size(), static_cast<float>(depth), utf8->data(), utf8->size(), static_cast<float>(weight)));
703
+ });
704
+ }
705
+
577
706
  jsi::Array NayanEngineModule::fontGlyphs(jsi::Runtime &rt, double font) {
578
707
  uint32_t f = id(font);
579
708
  const float *glyphs = engine_font_glyphs(f);
@@ -655,6 +784,38 @@ double NayanEngineModule::renderFrame(
655
784
  r.size(rt) / sizeof(uint32_t), n, out.size(rt) >= 16 * sizeof(float) ? reinterpret_cast<float *>(out.data(rt)) : nullptr, find(world));
656
785
  }
657
786
 
787
+ double NayanEngineModule::publishFrame(
788
+ jsi::Runtime &rt,
789
+ double surface,
790
+ jsi::Object frame,
791
+ jsi::Object matrices,
792
+ jsi::Object colors,
793
+ jsi::Object regions,
794
+ jsi::Object ranges,
795
+ double count,
796
+ jsi::Object viewProj,
797
+ double world) {
798
+ jsi::ArrayBuffer f = fileData(rt, frame, "publishFrame");
799
+ jsi::ArrayBuffer m = fileData(rt, matrices, "publishFrame");
800
+ jsi::ArrayBuffer c = fileData(rt, colors, "publishFrame");
801
+ jsi::ArrayBuffer g = fileData(rt, regions, "publishFrame");
802
+ jsi::ArrayBuffer r = fileData(rt, ranges, "publishFrame");
803
+ jsi::ArrayBuffer out = fileData(rt, viewProj, "publishFrame");
804
+ size_t capacity = std::min({m.size(rt) / (16 * sizeof(float)), c.size(rt) / (4 * sizeof(float)), g.size(rt) / (4 * sizeof(float))});
805
+ size_t n = count > 0 ? std::min(static_cast<size_t>(count), capacity) : 0;
806
+ return engine_render_publish(
807
+ static_cast<int32_t>(surface), reinterpret_cast<const double *>(f.data(rt)), f.size(rt) / sizeof(double),
808
+ reinterpret_cast<const float *>(m.data(rt)), reinterpret_cast<const float *>(c.data(rt)),
809
+ reinterpret_cast<const float *>(g.data(rt)), capacity, reinterpret_cast<const uint32_t *>(r.data(rt)),
810
+ r.size(rt) / sizeof(uint32_t), n, out.size(rt) >= 16 * sizeof(float) ? reinterpret_cast<float *>(out.data(rt)) : nullptr, find(world));
811
+ }
812
+
813
+ jsi::Array NayanEngineModule::renderStats(jsi::Runtime &rt, double surface) {
814
+ double out[2] = {0, 0};
815
+ engine_render_stats(static_cast<int32_t>(surface), out);
816
+ return jsi::Array::createWithElements(rt, out[0], out[1]);
817
+ }
818
+
658
819
  void NayanEngineModule::renderOverlay(
659
820
  jsi::Runtime &rt,
660
821
  double surface,
@@ -713,6 +874,11 @@ double NayanEngineModule::audioLoad(jsi::Runtime &rt, jsi::Object data) {
713
874
  return engine_audio_load_wav(buffer.data(rt), buffer.size(rt)); // copied by the core
714
875
  }
715
876
 
877
+ jsi::Value NayanEngineModule::audioLoadAsync(jsi::Runtime &rt, jsi::Object data) {
878
+ auto bytes = std::make_shared<std::vector<uint8_t>>(copyBytes(rt, data, "audioLoadAsync"));
879
+ return runInBackground(rt, jsInvoker_, [bytes]() { return static_cast<double>(engine_audio_load_wav(bytes->data(), bytes->size())); });
880
+ }
881
+
716
882
  double NayanEngineModule::audioPlay(jsi::Runtime &, double sound, double volume, double pan, double pitch, bool loop) {
717
883
  return static_cast<double>(engine_audio_play(id(sound), volume, pan, pitch, loop ? 1 : 0));
718
884
  }
@@ -800,6 +966,11 @@ Java_com_nayanui_engine_EngineSurfaceView_nativeAttach(JNIEnv *env, jclass, jint
800
966
  extern "C" JNIEXPORT void JNICALL Java_com_nayanui_engine_EngineSurfaceView_nativeDetach(JNIEnv *, jclass, jint surface) {
801
967
  releaseWindow(surface);
802
968
  }
969
+
970
+ /** Called by EngineSurfaceView's Choreographer once per display refresh while it's attached and visible. */
971
+ extern "C" JNIEXPORT void JNICALL Java_com_nayanui_engine_EngineSurfaceView_nativeVsync(JNIEnv *, jclass, jint surface, jlong frameTimeNanos) {
972
+ facebook::react::deliverFrame(surface, static_cast<double>(frameTimeNanos) / 1e6);
973
+ }
803
974
  extern "C" JNIEXPORT void JNICALL Java_com_nayanui_engine_EngineSurfaceView_nativeDrawingPresent(JNIEnv *, jclass, jint drawing, jint surface, jfloat width, jfloat height, jint color) {
804
975
  engine_drawing_present(drawing, surface, width, height, static_cast<uint32_t>(color));
805
976
  }
@@ -63,12 +63,14 @@ class NayanEngineModule : public NativeNayanEngineCxxSpec<NayanEngineModule> {
63
63
  double shapePrepare(jsi::Runtime &rt, jsi::Object vertices, jsi::Object indices, jsi::String options);
64
64
  double solidPrepare(jsi::Runtime &rt, jsi::String recipe, jsi::String options);
65
65
  double modelLoad(jsi::Runtime &rt, jsi::Object data, bool center, double fit);
66
+ jsi::Value modelLoadAsync(jsi::Runtime &rt, jsi::Object data, bool center, double fit);
66
67
  jsi::Object modelVertices(jsi::Runtime &rt, double mesh);
67
68
  jsi::Object modelIndices(jsi::Runtime &rt, double mesh);
68
69
  jsi::Array modelSize(jsi::Runtime &rt, double mesh);
69
70
  double modelTexture(jsi::Runtime &rt, double mesh);
70
71
  double meshAlias(jsi::Runtime &rt, double base);
71
72
  double meshCreate(jsi::Runtime &rt, jsi::Object vertices, jsi::Object indices);
73
+ bool renderModel(jsi::Runtime &rt, double mesh, double texture, double shine);
72
74
  jsi::Object pathExtrude(jsi::Runtime &rt, jsi::Object points, jsi::Array counts, double depth, bool evenOdd);
73
75
  void geometrySmoothNormals(jsi::Runtime &rt, jsi::Object vertices, jsi::Object indices);
74
76
  jsi::Object geometrySolid(jsi::Runtime &rt, jsi::String recipe);
@@ -85,9 +87,11 @@ class NayanEngineModule : public NativeNayanEngineCxxSpec<NayanEngineModule> {
85
87
  jsi::Object outIndices,
86
88
  double indexOffset);
87
89
  double textureLoad(jsi::Runtime &rt, jsi::Object data);
90
+ jsi::Value textureLoadAsync(jsi::Runtime &rt, jsi::Object data);
88
91
  jsi::Object texturePixels(jsi::Runtime &rt, double texture);
89
92
  jsi::Array textureSize(jsi::Runtime &rt, double texture);
90
93
  double fontLoad(jsi::Runtime &rt, jsi::Object data, double depth, jsi::String chars, double weight);
94
+ jsi::Value fontLoadAsync(jsi::Runtime &rt, jsi::Object data, double depth, jsi::String chars, double weight);
91
95
  jsi::Array fontGlyphs(jsi::Runtime &rt, double font);
92
96
  jsi::String loadError(jsi::Runtime &rt);
93
97
  void generateBuiltinMeshes(jsi::Runtime &rt);
@@ -106,6 +110,23 @@ class NayanEngineModule : public NativeNayanEngineCxxSpec<NayanEngineModule> {
106
110
  double count,
107
111
  jsi::Object viewProj,
108
112
  double world);
113
+ double publishFrame(
114
+ jsi::Runtime &rt,
115
+ double surface,
116
+ jsi::Object frame,
117
+ jsi::Object matrices,
118
+ jsi::Object colors,
119
+ jsi::Object regions,
120
+ jsi::Object ranges,
121
+ double count,
122
+ jsi::Object viewProj,
123
+ double world);
124
+ jsi::Array renderStats(jsi::Runtime &rt, double surface);
125
+ bool hasFrameClock(jsi::Runtime &rt);
126
+ void startFrames(jsi::Runtime &rt, double surface, jsi::Function onFrame);
127
+ void stopFrames(jsi::Runtime &rt, double surface);
128
+ /** Runs a surface's frame callback (on the JS thread); see `deliverFrame`. */
129
+ void callFrame(jsi::Runtime &rt, int surface, double timeMs);
109
130
  void renderOverlay(
110
131
  jsi::Runtime &rt,
111
132
  double surface,
@@ -122,6 +143,7 @@ class NayanEngineModule : public NativeNayanEngineCxxSpec<NayanEngineModule> {
122
143
  jsi::String drawingCommand(jsi::Runtime &rt, double document, jsi::String command);
123
144
  void drawingDestroy(jsi::Runtime &rt, double document);
124
145
  double audioLoad(jsi::Runtime &rt, jsi::Object data);
146
+ jsi::Value audioLoadAsync(jsi::Runtime &rt, jsi::Object data);
125
147
  double audioPlay(jsi::Runtime &rt, double sound, double volume, double pan, double pitch, bool loop);
126
148
  void audioStop(jsi::Runtime &rt, double voice);
127
149
  void audioSetVolume(jsi::Runtime &rt, double volume);
@@ -139,6 +161,14 @@ class NayanEngineModule : public NativeNayanEngineCxxSpec<NayanEngineModule> {
139
161
  std::unordered_map<int, std::shared_ptr<::World>> worlds_;
140
162
  int next_ = 1;
141
163
  std::unordered_set<int> drawings_;
164
+ /** Frame callbacks by surface; only touched on the JS thread. */
165
+ std::unordered_map<int, jsi::Function> frameCallbacks_;
142
166
  };
143
167
 
168
+ /**
169
+ * Called by the platform's display clock (any thread) once per refresh: schedules the surface's
170
+ * frame callback on the JS thread unless one is already waiting there.
171
+ */
172
+ void deliverFrame(int surface, double timeMs);
173
+
144
174
  } // namespace facebook::react