react-native-webgpu 0.19.1 → 0.20.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 (65) hide show
  1. package/Package.swift +19 -7
  2. package/android/build.gradle +7 -0
  3. package/apple/ApplePlatformContext.h +6 -1
  4. package/apple/ApplePlatformContext.mm +7 -8
  5. package/apple/MetalLayerColorSpace.mm +17 -1
  6. package/apple/WebGPUModule.mm +15 -1
  7. package/cpp/jsi/Promise.cpp +4 -0
  8. package/cpp/jsi/Promise.h +3 -0
  9. package/cpp/rnwgpu/CanvasColorSpace.h +5 -0
  10. package/cpp/rnwgpu/RNWebGPUManager.cpp +2 -4
  11. package/cpp/rnwgpu/SurfaceRegistry.h +16 -7
  12. package/cpp/rnwgpu/WebGPURuntime.cpp +2 -1
  13. package/cpp/rnwgpu/api/GPU.cpp +6 -6
  14. package/cpp/rnwgpu/api/GPU.h +1 -1
  15. package/cpp/rnwgpu/api/GPUAdapter.cpp +41 -3
  16. package/cpp/rnwgpu/api/GPUBuffer.cpp +5 -6
  17. package/cpp/rnwgpu/api/GPUCanvasContext.cpp +2 -1
  18. package/cpp/rnwgpu/api/GPUDevice.cpp +38 -47
  19. package/cpp/rnwgpu/api/GPUDevice.h +4 -5
  20. package/cpp/rnwgpu/api/GPUQueue.cpp +4 -2
  21. package/cpp/rnwgpu/api/GPUShaderModule.cpp +2 -2
  22. package/cpp/rnwgpu/api/RNWebGPU.h +9 -9
  23. package/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h +24 -0
  24. package/cpp/rnwgpu/async/AsyncTaskHandle.cpp +33 -45
  25. package/cpp/rnwgpu/async/AsyncTaskHandle.h +5 -6
  26. package/cpp/rnwgpu/async/RuntimeContext.cpp +321 -112
  27. package/cpp/rnwgpu/async/RuntimeContext.h +90 -82
  28. package/lib/commonjs/Canvas.js +1 -0
  29. package/lib/commonjs/Canvas.js.map +1 -1
  30. package/lib/commonjs/WebGPUViewNativeComponent.js.map +1 -1
  31. package/lib/commonjs/WebGPUViewNativeComponent.web.js.map +1 -1
  32. package/lib/commonjs/external/reanimated/registerWebGPUForReanimated.js +19 -1
  33. package/lib/commonjs/external/reanimated/registerWebGPUForReanimated.js.map +1 -1
  34. package/lib/commonjs/install.js +6 -5
  35. package/lib/commonjs/install.js.map +1 -1
  36. package/lib/module/Canvas.js +1 -0
  37. package/lib/module/Canvas.js.map +1 -1
  38. package/lib/module/WebGPUViewNativeComponent.js.map +1 -1
  39. package/lib/module/WebGPUViewNativeComponent.web.js.map +1 -1
  40. package/lib/module/external/reanimated/registerWebGPUForReanimated.js +19 -1
  41. package/lib/module/external/reanimated/registerWebGPUForReanimated.js.map +1 -1
  42. package/lib/module/install.js +6 -5
  43. package/lib/module/install.js.map +1 -1
  44. package/lib/typescript/lib/commonjs/Canvas.d.ts.map +1 -1
  45. package/lib/typescript/lib/commonjs/external/reanimated/registerWebGPUForReanimated.d.ts.map +1 -1
  46. package/lib/typescript/lib/commonjs/install.d.ts +6 -5
  47. package/lib/typescript/lib/commonjs/install.d.ts.map +1 -1
  48. package/lib/typescript/lib/module/Canvas.d.ts.map +1 -1
  49. package/lib/typescript/lib/module/external/reanimated/registerWebGPUForReanimated.d.ts.map +1 -1
  50. package/lib/typescript/lib/module/install.d.ts.map +1 -1
  51. package/lib/typescript/src/Canvas.d.ts.map +1 -1
  52. package/lib/typescript/src/WebGPUViewNativeComponent.d.ts +6 -7
  53. package/lib/typescript/src/WebGPUViewNativeComponent.d.ts.map +1 -1
  54. package/lib/typescript/src/WebGPUViewNativeComponent.web.d.ts +2 -3
  55. package/lib/typescript/src/WebGPUViewNativeComponent.web.d.ts.map +1 -1
  56. package/lib/typescript/src/external/reanimated/registerWebGPUForReanimated.d.ts +1 -0
  57. package/lib/typescript/src/external/reanimated/registerWebGPUForReanimated.d.ts.map +1 -1
  58. package/lib/typescript/src/install.d.ts +6 -5
  59. package/lib/typescript/src/install.d.ts.map +1 -1
  60. package/package.json +2 -2
  61. package/src/Canvas.tsx +4 -2
  62. package/src/WebGPUViewNativeComponent.ts +6 -10
  63. package/src/WebGPUViewNativeComponent.web.ts +2 -3
  64. package/src/external/reanimated/registerWebGPUForReanimated.ts +21 -1
  65. package/src/install.ts +6 -5
@@ -1,11 +1,19 @@
1
1
  #include "RuntimeContext.h"
2
2
 
3
+ #include <condition_variable>
4
+ #include <cstdint>
5
+ #include <deque>
3
6
  #include <memory>
4
7
  #include <stdexcept>
8
+ #include <thread>
5
9
  #include <unordered_map>
6
10
  #include <utility>
7
11
  #include <vector>
8
12
 
13
+ #if defined(__APPLE__) || defined(__ANDROID__)
14
+ #include <pthread.h>
15
+ #endif
16
+
9
17
  #include <ReactCommon/CallInvoker.h>
10
18
 
11
19
  #include "AsyncTaskHandle.h"
@@ -19,47 +27,130 @@ struct RuntimeData {
19
27
  };
20
28
  constexpr const char *TAG = "RuntimeContext";
21
29
 
22
- // The main JS runtime and its CallInvoker, registered once on install. The
23
- // context created for sMainRuntime gets sMainInvoker; spontaneous events
24
- // (device.lost) on a main-runtime device are delivered through it without the
25
- // pump. Worklet runtimes have no invoker (best-effort, see the header doc).
30
+ // The main JS runtime and its CallInvoker, registered on install (again
31
+ // after a reload) on the JS thread, and read from any thread: the waiter
32
+ // thread posts through them.
33
+ std::mutex sMainMutex;
26
34
  jsi::Runtime *sMainRuntime = nullptr;
27
35
  std::shared_ptr<facebook::react::CallInvoker> sMainInvoker;
36
+ // Whether a wake that could not run has been reported: once per process.
37
+ std::atomic<bool> sWakeLogged{false};
38
+
39
+ jsi::Runtime *mainRuntime() {
40
+ std::lock_guard<std::mutex> lock(sMainMutex);
41
+ return sMainRuntime;
42
+ }
43
+
44
+ std::shared_ptr<facebook::react::CallInvoker> mainInvoker() {
45
+ std::lock_guard<std::mutex> lock(sMainMutex);
46
+ return sMainInvoker;
47
+ }
28
48
 
29
- // ProcessEvents() on one wgpu::Instance is serialized across the runtimes that
30
- // share it by a mutex per instance, which each context resolves once, on
31
- // creation. Pumping another library's instance (the one owning a device passed
32
- // to importDevice) can block on that library's device lock; with one mutex per
33
- // instance that never stalls the pumps of our own instance. A mutex is held
34
- // only across the ProcessEvents call itself, never while running JS / mailbox
35
- // settle-actions, so it cannot deadlock against the per-context mailbox mutex.
36
- std::shared_ptr<std::mutex> processEventsMutexFor(WGPUInstance instance) {
37
- static std::mutex registryMutex;
38
- static std::unordered_map<WGPUInstance, std::weak_ptr<std::mutex>> registry;
39
- std::lock_guard<std::mutex> lock(registryMutex);
40
- auto &slot = registry[instance];
41
- if (auto existing = slot.lock()) {
42
- return existing;
43
- }
44
- // Contexts retain their instance, so its handle cannot be reused while one
45
- // of them is alive: an expired slot belonged to an instance whose runtimes
46
- // are all gone, and an instance reusing that address starts afresh.
47
- auto created = std::make_shared<std::mutex>();
48
- slot = created;
49
- return created;
49
+ void logException(const char *what, const std::exception_ptr &error) {
50
+ try {
51
+ std::rethrow_exception(error);
52
+ } catch (const std::exception &exception) {
53
+ Logger::logToConsole("[%s] %s: %s", TAG, what, exception.what());
54
+ } catch (...) {
55
+ Logger::logToConsole("[%s] %s.", TAG, what);
56
+ }
50
57
  }
58
+
59
+ /**
60
+ * The thread waiting on the futures of one instance: one future at a time,
61
+ * in the order they were handed over, in Instance::WaitAny, which sleeps
62
+ * until the GPU or Dawn's worker is done and then runs the callback. Never
63
+ * destroyed: the instance it retains is shared by every runtime, and a
64
+ * future may be outstanding during process teardown.
65
+ */
66
+ class Waiter {
67
+ public:
68
+ static Waiter &forInstance(const wgpu::Instance &instance) {
69
+ static std::mutex registryMutex;
70
+ static std::unordered_map<WGPUInstance, Waiter *> registry;
71
+ std::lock_guard<std::mutex> lock(registryMutex);
72
+ auto &slot = registry[instance.Get()];
73
+ if (slot == nullptr) {
74
+ slot = new Waiter(instance);
75
+ }
76
+ return *slot;
77
+ }
78
+
79
+ void enqueue(wgpu::Future future) {
80
+ {
81
+ std::lock_guard<std::mutex> lock(_mutex);
82
+ _futures.push_back(future);
83
+ }
84
+ _condition.notify_one();
85
+ }
86
+
87
+ private:
88
+ explicit Waiter(wgpu::Instance instance) : _instance(std::move(instance)) {
89
+ std::thread([this]() { run(); }).detach();
90
+ }
91
+
92
+ void run() {
93
+ #if defined(__APPLE__)
94
+ pthread_setname_np("RNWebGPU Waiter");
95
+ #elif defined(__ANDROID__)
96
+ pthread_setname_np(pthread_self(), "RNWebGPU Waiter");
97
+ #endif
98
+ for (;;) {
99
+ wgpu::Future future;
100
+ {
101
+ std::unique_lock<std::mutex> lock(_mutex);
102
+ _condition.wait(lock, [this]() { return !_futures.empty(); });
103
+ future = _futures.front();
104
+ _futures.pop_front();
105
+ }
106
+ auto status = _instance.WaitAny(future, UINT64_MAX);
107
+ if (status != wgpu::WaitStatus::Success) {
108
+ Logger::logToConsole("[%s] WaitAny failed with status %d", TAG,
109
+ static_cast<int>(status));
110
+ }
111
+ }
112
+ }
113
+
114
+ wgpu::Instance _instance;
115
+ std::mutex _mutex;
116
+ std::condition_variable _condition;
117
+ std::deque<wgpu::Future> _futures;
118
+ };
51
119
  } // namespace
52
120
 
53
121
  void RuntimeContext::registerMainRuntime(
54
122
  jsi::Runtime *runtime,
55
123
  std::shared_ptr<facebook::react::CallInvoker> invoker) {
124
+ std::lock_guard<std::mutex> lock(sMainMutex);
56
125
  sMainRuntime = runtime;
57
126
  sMainInvoker = std::move(invoker);
58
127
  }
59
128
 
129
+ RuntimeContext::Kind RuntimeContext::kindOf(jsi::Runtime &runtime) {
130
+ auto main = mainRuntime();
131
+ if (&runtime == main) {
132
+ return Kind::Main;
133
+ }
134
+ // react-native-worklets' RuntimeKind: 1 for React Native, 2 for the UI
135
+ // runtime, more for the runtimes of the app.
136
+ auto kind = runtime.global().getProperty(runtime, "__RUNTIME_KIND");
137
+ if (kind.isNumber() && kind.asNumber() == 2) {
138
+ return Kind::UI;
139
+ }
140
+ if (main == nullptr && (!kind.isNumber() || kind.asNumber() == 1)) {
141
+ // No main runtime registered (yet): the React Native runtime, as far as
142
+ // anything can tell.
143
+ return Kind::Main;
144
+ }
145
+ // Another runtime, marked or not: settling it through the main runtime's
146
+ // CallInvoker would settle it with the main runtime while its promise
147
+ // lives on this one. Its own setTimeout reaches it.
148
+ return Kind::Mailbox;
149
+ }
150
+
60
151
  RuntimeContext::RuntimeContext(jsi::Runtime &runtime, wgpu::Instance instance)
61
152
  : _runtime(runtime), _instance(std::move(instance)),
62
- _processEventsMutex(processEventsMutexFor(_instance.Get())) {
153
+ _kind(kindOf(runtime)) {
63
154
  Logger::logToConsole("[%s] Created (runtime=%p)", TAG, &runtime);
64
155
  }
65
156
 
@@ -73,39 +164,51 @@ RuntimeContext::getOrCreate(jsi::Runtime &runtime,
73
164
  runtime.setRuntimeData(runtimeDataUUID(), data);
74
165
  }
75
166
  // Contexts retain their instance, so its handle is a stable key for the
76
- // runtime's lifetime, and each instance's callbacks reach the matching pump.
77
- // A slot left null by a throwing constructor is simply filled next time.
167
+ // runtime's lifetime. A slot left null by a throwing constructor is simply
168
+ // filled next time.
78
169
  auto &context = data->contexts[instance.Get()];
79
170
  if (!context) {
80
171
  context = std::make_shared<RuntimeContext>(runtime, instance);
81
- // Only the main JS runtime's context carries the CallInvoker; it is used to
82
- // deliver spontaneous events (device.lost) without the pump.
83
- if (&runtime == sMainRuntime) {
84
- context->_callInvoker = sMainInvoker;
85
- }
86
172
  }
87
173
  return context;
88
174
  }
89
175
 
90
- AsyncTaskHandle RuntimeContext::postTask(const TaskCallback &callback,
91
- bool keepPumping) {
92
- auto handle = AsyncTaskHandle::create(shared_from_this(), keepPumping);
93
- if (!handle.valid()) {
94
- throw std::runtime_error("Failed to create AsyncTaskHandle.");
176
+ bool RuntimeContext::canSettle() const {
177
+ switch (_kind) {
178
+ case Kind::Main:
179
+ return mainInvoker() != nullptr;
180
+ case Kind::UI:
181
+ case Kind::Mailbox:
182
+ return true;
95
183
  }
184
+ return false;
185
+ }
96
186
 
97
- // Only pumping tasks (request/response ops) drive the ProcessEvents pump.
98
- // Spontaneous tasks (keepPumping == false, e.g. device.lost) never touch the
99
- // pump: they settle via the CallInvoker (see AsyncTaskHandle::State::schedule).
100
- if (keepPumping) {
101
- _pumpTasks.fetch_add(1, std::memory_order_acq_rel);
102
- requestTick();
187
+ AsyncTaskHandle RuntimeContext::postTask(const TaskCallback &callback) {
188
+ if (!canSettle()) {
189
+ throw std::runtime_error("Nothing can settle a promise on this runtime: "
190
+ "react-native-webgpu is not installed on it.");
103
191
  }
192
+ // A runtime that polls its mailbox starts doing so now, on its own thread;
193
+ // the settle action ends the operation (onTaskSettled). Without a timer
194
+ // nothing could ever settle the promise: none is handed out.
195
+ if (_kind != Kind::Main && !armTick(_runtime)) {
196
+ throw std::runtime_error("Nothing can settle a promise on this runtime: "
197
+ "it has no setTimeout.");
198
+ }
199
+ auto handle = AsyncTaskHandle::create(shared_from_this());
200
+ if (!handle.valid()) {
201
+ throw std::runtime_error("Failed to create AsyncTaskHandle.");
202
+ }
203
+ _outstanding.fetch_add(1, std::memory_order_acq_rel);
104
204
 
105
205
  auto resolve = handle.createResolveFunction();
106
206
  auto reject = handle.createRejectFunction();
107
207
  try {
108
- callback(resolve, reject);
208
+ auto future = callback(resolve, reject);
209
+ if (future.has_value()) {
210
+ awaitFuture(*future);
211
+ }
109
212
  } catch (const std::exception &exception) {
110
213
  reject(exception.what());
111
214
  } catch (...) {
@@ -114,92 +217,198 @@ AsyncTaskHandle RuntimeContext::postTask(const TaskCallback &callback,
114
217
  return handle;
115
218
  }
116
219
 
117
- void RuntimeContext::onTaskSettled(bool keepPumping) {
118
- if (keepPumping) {
119
- _pumpTasks.fetch_sub(1, std::memory_order_acq_rel);
220
+ void RuntimeContext::awaitFuture(wgpu::Future future) {
221
+ // Complete already (requestAdapter, popErrorScope, a lost device): the
222
+ // callback runs here, and nothing waits.
223
+ if (_instance.WaitAny(future, 0) == wgpu::WaitStatus::Success) {
224
+ return;
120
225
  }
226
+ Waiter::forInstance(_instance).enqueue(future);
121
227
  }
122
228
 
123
- void RuntimeContext::postSettle(std::function<void()> job) {
124
- if (!job) {
125
- return;
126
- }
127
- std::lock_guard<std::mutex> lock(_mailboxMutex);
128
- _mailbox.push_back(std::move(job));
229
+ void RuntimeContext::onTaskSettled() {
230
+ _outstanding.fetch_sub(1, std::memory_order_acq_rel);
129
231
  }
130
232
 
131
- void RuntimeContext::drainMailbox() {
132
- std::vector<std::function<void()>> jobs;
133
- {
134
- std::lock_guard<std::mutex> lock(_mailboxMutex);
135
- jobs.swap(_mailbox);
233
+ void RuntimeContext::post(Job job) {
234
+ switch (_kind) {
235
+ case Kind::Main: {
236
+ auto invoker = mainInvoker();
237
+ if (invoker == nullptr) {
238
+ return;
239
+ }
240
+ invoker->invokeAsync(facebook::react::CallFunc(std::move(job)));
241
+ return;
242
+ }
243
+ case Kind::UI:
244
+ case Kind::Mailbox: {
245
+ {
246
+ std::lock_guard<std::mutex> lock(_mailboxMutex);
247
+ _mailbox.push_back(std::move(job));
248
+ }
249
+ if (_kind == Kind::UI) {
250
+ wakeUI();
251
+ }
252
+ return;
136
253
  }
137
- // Run settle-actions on this (the owning) thread, NOT under the ProcessEvents
138
- // mutex, so JS continuations never execute while the pump lock is held.
139
- for (auto &job : jobs) {
140
- job();
141
254
  }
142
255
  }
143
256
 
144
- void RuntimeContext::requestTick() {
257
+ // Best effort, and coalesced (one wake in flight at a time): on the main
258
+ // runtime, worklets runs the function handed to __webgpuScheduleOnUI on the
259
+ // UI runtime, where it drains the mailbox ahead of the next tick. The
260
+ // function is a host function, which worklets passes by reference. Without
261
+ // the hook (registerWebGPUForReanimated has not run on this main runtime
262
+ // yet, a reload evaluating its bundle, say) the tick alone settles what is
263
+ // awaited, a spontaneous event waits for the next awaited operation, and
264
+ // the next post tries again. Whatever goes wrong on the way, the wake is
265
+ // given up, never the jobs.
266
+ void RuntimeContext::wakeUI() {
267
+ auto invoker = mainInvoker();
268
+ if (invoker == nullptr) {
269
+ return;
270
+ }
145
271
  bool expected = false;
146
- if (!_tickScheduled.compare_exchange_strong(expected, true,
147
- std::memory_order_acq_rel)) {
272
+ if (!_wakePending.compare_exchange_strong(expected, true,
273
+ std::memory_order_acq_rel)) {
148
274
  return;
149
275
  }
276
+ std::weak_ptr<RuntimeContext> weak = weak_from_this();
277
+ invoker->invokeAsync(
278
+ facebook::react::CallFunc([weak](jsi::Runtime &mainRuntime) {
279
+ auto self = weak.lock();
280
+ if (self == nullptr) {
281
+ return;
282
+ }
283
+ try {
284
+ auto schedule = mainRuntime.global().getProperty(
285
+ mainRuntime, "__webgpuScheduleOnUI");
286
+ if (!schedule.isObject() ||
287
+ !schedule.asObject(mainRuntime).isFunction(mainRuntime)) {
288
+ self->_wakePending.store(false, std::memory_order_release);
289
+ if (!sWakeLogged.exchange(true, std::memory_order_acq_rel)) {
290
+ Logger::logToConsole(
291
+ "[%s] __webgpuScheduleOnUI is not installed on the main "
292
+ "runtime (see registerWebGPUForReanimated): the UI "
293
+ "runtime settles its promises on its own timer for now, "
294
+ "and device.lost and uncapturederror reach it with its "
295
+ "next awaited operation.",
296
+ TAG);
297
+ }
298
+ return;
299
+ }
300
+ auto wake = jsi::Function::createFromHostFunction(
301
+ mainRuntime, jsi::PropNameID::forAscii(mainRuntime, "webgpuWake"),
302
+ 0,
303
+ [weak](jsi::Runtime &uiRuntime, const jsi::Value &,
304
+ const jsi::Value *, size_t) -> jsi::Value {
305
+ auto self = weak.lock();
306
+ // The UI runtime of the moment: after a reload, not the one
307
+ // this mailbox belongs to.
308
+ if (self == nullptr || &uiRuntime != &self->_runtime) {
309
+ return jsi::Value::undefined();
310
+ }
311
+ // Cleared before the drain: a job posted meanwhile is either
312
+ // in this drain or gets a wake of its own.
313
+ self->_wakePending.store(false, std::memory_order_release);
314
+ auto error = self->drainMailbox(uiRuntime);
315
+ if (error != nullptr) {
316
+ // Reported, not rethrown: the wake runs among the other
317
+ // jobs of worklets' scheduleOnUI batch, which the
318
+ // exception would cut short. (The tick rethrows its own
319
+ // as the timer's.)
320
+ logException("A job run by the wake on the UI runtime threw",
321
+ error);
322
+ }
323
+ return jsi::Value::undefined();
324
+ });
325
+ schedule.asObject(mainRuntime)
326
+ .asFunction(mainRuntime)
327
+ .call(mainRuntime, std::move(wake));
328
+ } catch (const std::exception &error) {
329
+ self->_wakePending.store(false, std::memory_order_release);
330
+ if (!sWakeLogged.exchange(true, std::memory_order_acq_rel)) {
331
+ Logger::logToConsole(
332
+ "[%s] Waking the UI runtime through __webgpuScheduleOnUI "
333
+ "failed, so it settles its promises on its own timer for "
334
+ "now: %s",
335
+ TAG, error.what());
336
+ }
337
+ }
338
+ }));
339
+ }
150
340
 
151
- // The pump only ever runs while a request/response op is outstanding, so it
152
- // always schedules as soon as possible (delay 0). postTask and tick both run
153
- // on the owning runtime's thread, so we schedule the next tick directly via
154
- // that runtime's own timer. setTimeout is available on the main RN runtime and
155
- // on worklet runtimes (backed by the worklets EventLoop); setImmediate /
156
- // queueMicrotask are fallbacks. We do NOT use queueMicrotask as the primary
157
- // mechanism: a self-rescheduling microtask never yields the microtask
158
- // checkpoint, starving the runtime's task loop.
159
- auto self = shared_from_this();
160
- jsi::Runtime &rt = _runtime;
161
- auto tickCallback = jsi::Function::createFromHostFunction(
162
- rt, jsi::PropNameID::forAscii(rt, "RNWGPUAsyncTick"), 0,
163
- [self](jsi::Runtime & /*runtime*/, const jsi::Value & /*thisVal*/,
164
- const jsi::Value * /*args*/, size_t /*count*/) -> jsi::Value {
165
- self->tick();
166
- return jsi::Value::undefined();
167
- });
168
-
169
- auto global = rt.global();
170
- auto setTimeoutValue = global.getProperty(rt, "setTimeout");
171
- if (setTimeoutValue.isObject() &&
172
- setTimeoutValue.asObject(rt).isFunction(rt)) {
173
- setTimeoutValue.asObject(rt).asFunction(rt).call(
174
- rt, jsi::Value(rt, tickCallback), jsi::Value(0.0));
175
- return;
341
+ bool RuntimeContext::armTick(jsi::Runtime &runtime) {
342
+ bool expected = false;
343
+ if (!_tickArmed.compare_exchange_strong(expected, true,
344
+ std::memory_order_acq_rel)) {
345
+ return true;
176
346
  }
177
- auto setImmediateValue = global.getProperty(rt, "setImmediate");
178
- if (setImmediateValue.isObject() &&
179
- setImmediateValue.asObject(rt).isFunction(rt)) {
180
- setImmediateValue.asObject(rt).asFunction(rt).call(
181
- rt, jsi::Value(rt, tickCallback));
182
- return;
347
+ // The runtime's own timer, on its own thread. Not queueMicrotask: a
348
+ // self-rescheduling microtask never yields the microtask checkpoint, which
349
+ // starves the runtime's task loop.
350
+ try {
351
+ auto setTimeout = runtime.global().getProperty(runtime, "setTimeout");
352
+ if (!setTimeout.isObject() ||
353
+ !setTimeout.asObject(runtime).isFunction(runtime)) {
354
+ _tickArmed.store(false, std::memory_order_release);
355
+ Logger::logToConsole("[%s] This runtime has no setTimeout: nothing "
356
+ "asynchronous can settle on it.",
357
+ TAG);
358
+ return false;
359
+ }
360
+ std::weak_ptr<RuntimeContext> weak = weak_from_this();
361
+ auto tick = jsi::Function::createFromHostFunction(
362
+ runtime, jsi::PropNameID::forAscii(runtime, "RNWGPUAsyncTick"), 0,
363
+ [weak](jsi::Runtime &runtime, const jsi::Value &, const jsi::Value *,
364
+ size_t) -> jsi::Value {
365
+ auto self = weak.lock();
366
+ if (self == nullptr) {
367
+ return jsi::Value::undefined();
368
+ }
369
+ self->_tickArmed.store(false, std::memory_order_release);
370
+ auto error = self->drainMailbox(runtime);
371
+ if (self->_outstanding.load(std::memory_order_acquire) > 0) {
372
+ self->armTick(runtime);
373
+ }
374
+ if (error != nullptr) {
375
+ // Thrown by a job, after the others ran and the next tick is
376
+ // armed: the runtime reports it as the timer's.
377
+ std::rethrow_exception(error);
378
+ }
379
+ return jsi::Value::undefined();
380
+ });
381
+ setTimeout.asObject(runtime).asFunction(runtime).call(
382
+ runtime, std::move(tick), jsi::Value(0.0));
383
+ return true;
384
+ } catch (const std::exception &error) {
385
+ _tickArmed.store(false, std::memory_order_release);
386
+ Logger::logToConsole("[%s] This runtime's setTimeout threw, so nothing "
387
+ "asynchronous can settle on it: %s",
388
+ TAG, error.what());
389
+ return false;
183
390
  }
184
- rt.queueMicrotask(std::move(tickCallback));
185
391
  }
186
392
 
187
- void RuntimeContext::tick() {
188
- _tickScheduled.store(false, std::memory_order_release);
393
+ // Runs the jobs posted so far, every one of them even when one throws: the
394
+ // first exception is returned for the caller to rethrow once the rest ran.
395
+ std::exception_ptr RuntimeContext::drainMailbox(jsi::Runtime &runtime) {
396
+ std::vector<Job> jobs;
189
397
  {
190
- // Serialize ProcessEvents across runtimes sharing this instance. Callbacks
191
- // fired here only deposit into mailboxes (postSettle), they do not run JS.
192
- std::lock_guard<std::mutex> lock(*_processEventsMutex);
193
- _instance.ProcessEvents();
194
- }
195
- // Settle this runtime's ready promises on this thread, outside the pump lock.
196
- drainMailbox();
197
- // Keep pumping only while a "pumping" task (active async work) is outstanding.
198
- // Non-pumping tasks (e.g. device.lost) intentionally do NOT keep the pump
199
- // alive: we prioritise battery over catching a device.lost fired while idle.
200
- if (_pumpTasks.load(std::memory_order_acquire) > 0) {
201
- requestTick();
398
+ std::lock_guard<std::mutex> lock(_mailboxMutex);
399
+ jobs.swap(_mailbox);
400
+ }
401
+ std::exception_ptr first = nullptr;
402
+ for (auto &job : jobs) {
403
+ try {
404
+ job(runtime);
405
+ } catch (...) {
406
+ if (first == nullptr) {
407
+ first = std::current_exception();
408
+ }
409
+ }
202
410
  }
411
+ return first;
203
412
  }
204
413
 
205
414
  jsi::UUID RuntimeContext::runtimeDataUUID() {