react-native-webgpu 0.8.1 → 0.8.3

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.
@@ -10,12 +10,29 @@
10
10
 
11
11
  using namespace facebook::react;
12
12
 
13
+ static const std::shared_ptr<const WebGPUViewProps> &defaultWebGPUViewProps() {
14
+ static const auto props = std::make_shared<const WebGPUViewProps>();
15
+ return props;
16
+ }
17
+
13
18
  @implementation WebGPUView
14
19
 
15
20
  + (ComponentDescriptorProvider)componentDescriptorProvider {
16
21
  return concreteComponentDescriptorProvider<WebGPUViewComponentDescriptor>();
17
22
  }
18
23
 
24
+ - (instancetype)initWithFrame:(CGRect)frame {
25
+ if (self = [super initWithFrame:frame]) {
26
+ /*
27
+ The base class initializes _props with a plain ViewProps instance;
28
+ updateProps: casts _props to WebGPUViewProps, so it must hold our
29
+ concrete props type from the start (RN 0.87+ asserts on this).
30
+ */
31
+ _props = defaultWebGPUViewProps();
32
+ }
33
+ return self;
34
+ }
35
+
19
36
  - (void)prepareForRecycle {
20
37
  [super prepareForRecycle];
21
38
  /*
@@ -24,6 +41,11 @@ using namespace facebook::react;
24
41
  the last usage in the new context.
25
42
  */
26
43
  self.contentView = nil;
44
+ /*
45
+ Reset to the default props (contextId == 0) so the next mount always
46
+ detects a contextId change and configures a fresh Metal layer.
47
+ */
48
+ _props = defaultWebGPUViewProps();
27
49
  }
28
50
 
29
51
  - (MetalView *)getContentView {
@@ -19,9 +19,35 @@
19
19
 
20
20
  namespace rnwgpu {
21
21
 
22
+ namespace {
23
+
24
+ // Hidden own properties used by device.lost. The promise cache lives on the
25
+ // device wrapper (the spec requires `lost` to return a stable promise) and
26
+ // the resolve function lives on the promise object itself, so both are traced
27
+ // by the GC as part of the device's JS object graph instead of being rooted
28
+ // from C++ (issue #445).
29
+ constexpr const char *kLostPromiseProp = "__rnwgpuLostPromise";
30
+ constexpr const char *kLostResolveProp = "__rnwgpuLostResolve";
31
+
32
+ void defineHiddenProperty(jsi::Runtime &runtime, const jsi::Object &target,
33
+ const char *name, const jsi::Value &value) {
34
+ auto objectCtor = runtime.global().getPropertyAsObject(runtime, "Object");
35
+ auto defineProperty =
36
+ objectCtor.getPropertyAsFunction(runtime, "defineProperty");
37
+ jsi::Object descriptor(runtime);
38
+ descriptor.setProperty(runtime, "value", value);
39
+ descriptor.setProperty(runtime, "enumerable", false);
40
+ descriptor.setProperty(runtime, "writable", false);
41
+ descriptor.setProperty(runtime, "configurable", false);
42
+ defineProperty.call(runtime, target,
43
+ jsi::String::createFromUtf8(runtime, name), descriptor);
44
+ }
45
+
46
+ } // namespace
47
+
22
48
  void GPUDevice::notifyDeviceLost(wgpu::DeviceLostReason reason,
23
49
  std::string message) {
24
- std::optional<async::AsyncTaskHandle::ResolveFunction> resolveToCall;
50
+ std::vector<PendingLostPromise> toResolve;
25
51
  std::shared_ptr<GPUDeviceLostInfo> info;
26
52
  {
27
53
  std::lock_guard<std::mutex> lock(_lostMutex);
@@ -32,22 +58,49 @@ void GPUDevice::notifyDeviceLost(wgpu::DeviceLostReason reason,
32
58
  _lostSettled = true;
33
59
  _lostInfo = std::make_shared<GPUDeviceLostInfo>(reason, std::move(message));
34
60
  info = _lostInfo;
61
+ toResolve = std::move(_lostPromises);
62
+ _lostPromises.clear();
63
+ }
35
64
 
36
- if (_lostResolve.has_value()) {
37
- resolveToCall = std::move(*_lostResolve);
38
- _lostResolve.reset();
39
- }
40
-
41
- _lostHandle.reset();
65
+ if (toResolve.empty()) {
66
+ return;
42
67
  }
43
68
 
44
- // Settle outside the lock: resolve() only enqueues onto the JS thread.
45
- if (resolveToCall.has_value()) {
46
- (*resolveToCall)([info](jsi::Runtime &runtime) mutable {
47
- return JSIConverter<std::shared_ptr<GPUDeviceLostInfo>>::toJSI(runtime,
48
- info);
49
- });
69
+ // getLost() only registers promises when the device's context has a
70
+ // CallInvoker (main JS runtime), so a non-empty list implies an invoker.
71
+ auto invoker = _async ? _async->callInvoker() : nullptr;
72
+ if (!invoker) {
73
+ return;
50
74
  }
75
+
76
+ // Settle on the owning runtime's JS thread. The promises are held weakly:
77
+ // if the device graph was collected in the meantime there is nothing to do,
78
+ // since nobody could observe the resolution anyway. The shared_ptr is only
79
+ // there because jsi::WeakObject is move-only and std::function requires a
80
+ // copyable closure; it also ensures the WeakObjects are destroyed on the JS
81
+ // thread.
82
+ auto pending =
83
+ std::make_shared<std::vector<PendingLostPromise>>(std::move(toResolve));
84
+ invoker->invokeAsync([pending, info]() {
85
+ for (auto &entry : *pending) {
86
+ auto &runtime = *entry.runtime;
87
+ auto locked = entry.promise.lock(runtime);
88
+ if (!locked.isObject()) {
89
+ continue;
90
+ }
91
+ auto promiseObj = locked.getObject(runtime);
92
+ auto resolveProp = promiseObj.getProperty(runtime, kLostResolveProp);
93
+ if (!resolveProp.isObject() ||
94
+ !resolveProp.getObject(runtime).isFunction(runtime)) {
95
+ continue;
96
+ }
97
+ auto resolveFn = resolveProp.getObject(runtime).getFunction(runtime);
98
+ resolveFn.call(runtime,
99
+ JSIConverter<std::shared_ptr<GPUDeviceLostInfo>>::toJSI(
100
+ runtime, info));
101
+ }
102
+ pending->clear();
103
+ });
51
104
  }
52
105
 
53
106
  void GPUDevice::forceLossForTesting() {
@@ -523,47 +576,84 @@ std::unordered_set<std::string> GPUDevice::getFeatures() {
523
576
  return result;
524
577
  }
525
578
 
526
- async::AsyncTaskHandle GPUDevice::getLost() {
527
- // Held across the whole body: the postTask callback below runs synchronously
528
- // on this (JS) thread and touches the same _lost* fields, so it must not
529
- // re-lock. notifyDeviceLost() takes the same lock from its (possibly worker)
530
- // thread.
531
- std::lock_guard<std::mutex> lock(_lostMutex);
532
- if (_lostHandle.has_value()) {
533
- return *_lostHandle;
534
- }
535
-
536
- if (_lostSettled && _lostInfo) {
537
- return _async->postTask(
538
- [info = _lostInfo](
539
- const async::AsyncTaskHandle::ResolveFunction &resolve,
540
- const async::AsyncTaskHandle::RejectFunction & /*reject*/) {
541
- resolve([info](jsi::Runtime &runtime) mutable {
542
- return JSIConverter<std::shared_ptr<GPUDeviceLostInfo>>::toJSI(
543
- runtime, info);
544
- });
545
- },
546
- /*keepPumping=*/false);
547
- }
548
-
549
- auto handle = _async->postTask(
550
- [this](const async::AsyncTaskHandle::ResolveFunction &resolve,
551
- const async::AsyncTaskHandle::RejectFunction & /*reject*/) {
552
- if (_lostSettled && _lostInfo) {
553
- resolve([info = _lostInfo](jsi::Runtime &runtime) mutable {
554
- return JSIConverter<std::shared_ptr<GPUDeviceLostInfo>>::toJSI(
555
- runtime, info);
556
- });
557
- return;
558
- }
559
-
560
- // Resolved later from notifyDeviceLost().
561
- _lostResolve = resolve;
562
- },
563
- /*keepPumping=*/false);
564
-
565
- _lostHandle = handle;
566
- return handle;
579
+ jsi::Value GPUDevice::getLost(jsi::Runtime &runtime,
580
+ const jsi::Object &wrapper) {
581
+ // The promise is cached on the wrapper, not natively: a strong native
582
+ // reference would be a GC root, keeping the promise's .then reactions (and
583
+ // anything they capture, e.g. a whole three.js renderer) alive forever
584
+ // (issue #445).
585
+ auto cached = wrapper.getProperty(runtime, kLostPromiseProp);
586
+ if (cached.isObject()) {
587
+ return cached;
588
+ }
589
+
590
+ auto promiseCtor = runtime.global().getPropertyAsObject(runtime, "Promise");
591
+
592
+ std::shared_ptr<GPUDeviceLostInfo> settledInfo;
593
+ {
594
+ std::lock_guard<std::mutex> lock(_lostMutex);
595
+ if (_lostSettled) {
596
+ settledInfo = _lostInfo;
597
+ }
598
+ }
599
+
600
+ jsi::Value promiseValue;
601
+ if (settledInfo) {
602
+ auto info = JSIConverter<std::shared_ptr<GPUDeviceLostInfo>>::toJSI(
603
+ runtime, settledInfo);
604
+ promiseValue = promiseCtor.getPropertyAsFunction(runtime, "resolve")
605
+ .callWithThis(runtime, promiseCtor, info);
606
+ } else {
607
+ // new Promise(executor): the executor runs synchronously inside the
608
+ // constructor call, capturing the resolve function.
609
+ auto capturedResolve = std::make_shared<jsi::Value>();
610
+ auto executor = jsi::Function::createFromHostFunction(
611
+ runtime, jsi::PropNameID::forUtf8(runtime, "lostExecutor"), 2,
612
+ [capturedResolve](jsi::Runtime &rt, const jsi::Value & /*thisVal*/,
613
+ const jsi::Value *args, size_t count) -> jsi::Value {
614
+ if (count > 0) {
615
+ *capturedResolve = jsi::Value(rt, args[0]);
616
+ }
617
+ return jsi::Value::undefined();
618
+ });
619
+ auto promiseObj = promiseCtor.asFunction(runtime)
620
+ .callAsConstructor(runtime, executor)
621
+ .getObject(runtime);
622
+
623
+ // Stash the resolve function on the promise itself so the GC traces it as
624
+ // part of the promise graph.
625
+ defineHiddenProperty(runtime, promiseObj, kLostResolveProp,
626
+ *capturedResolve);
627
+
628
+ // Register a WEAK reference so notifyDeviceLost() can settle the promise
629
+ // if it is still alive. Only wired for the device's own runtime when a
630
+ // CallInvoker exists (main JS runtime): spontaneous events are delivered
631
+ // through that invoker. A promise created on another runtime stays
632
+ // pending, matching the previous best-effort behavior.
633
+ bool settledMeanwhile = false;
634
+ if (_async && _async->callInvoker() && &runtime == &_async->runtime()) {
635
+ std::lock_guard<std::mutex> lock(_lostMutex);
636
+ if (_lostSettled) {
637
+ // The device was lost between the check above and now.
638
+ settledMeanwhile = true;
639
+ settledInfo = _lostInfo;
640
+ } else {
641
+ _lostPromises.push_back(
642
+ PendingLostPromise{&runtime, jsi::WeakObject(runtime, promiseObj)});
643
+ }
644
+ }
645
+ if (settledMeanwhile) {
646
+ auto resolveFn =
647
+ promiseObj.getPropertyAsFunction(runtime, kLostResolveProp);
648
+ resolveFn.call(runtime,
649
+ JSIConverter<std::shared_ptr<GPUDeviceLostInfo>>::toJSI(
650
+ runtime, settledInfo));
651
+ }
652
+ promiseValue = jsi::Value(runtime, promiseObj);
653
+ }
654
+
655
+ defineHiddenProperty(runtime, wrapper, kLostPromiseProp, promiseValue);
656
+ return promiseValue;
567
657
  }
568
658
  void GPUDevice::addEventListener(std::string type, jsi::Function callback) {
569
659
  auto funcPtr = std::make_shared<jsi::Function>(std::move(callback));
@@ -45,11 +45,11 @@
45
45
  #include "GPURenderPipelineDescriptor.h"
46
46
  #include "GPUSampler.h"
47
47
  #include "GPUSamplerDescriptor.h"
48
+ #include "GPUShaderModule.h"
49
+ #include "GPUShaderModuleDescriptor.h"
48
50
  #include "GPUSharedFenceDescriptor.h"
49
51
  #include "GPUSharedTextureMemory.h"
50
52
  #include "GPUSharedTextureMemoryDescriptor.h"
51
- #include "GPUShaderModule.h"
52
- #include "GPUShaderModuleDescriptor.h"
53
53
  #include "GPUSupportedLimits.h"
54
54
  #include "GPUTexture.h"
55
55
  #include "GPUTextureDescriptor.h"
@@ -121,8 +121,8 @@ public:
121
121
  std::shared_ptr<GPUExternalTextureDescriptor> descriptor);
122
122
  std::shared_ptr<GPUSharedTextureMemory> importSharedTextureMemory(
123
123
  std::shared_ptr<GPUSharedTextureMemoryDescriptor> descriptor);
124
- std::shared_ptr<GPUSharedFence> importSharedFence(
125
- std::shared_ptr<GPUSharedFenceDescriptor> descriptor);
124
+ std::shared_ptr<GPUSharedFence>
125
+ importSharedFence(std::shared_ptr<GPUSharedFenceDescriptor> descriptor);
126
126
  std::shared_ptr<GPUBindGroupLayout> createBindGroupLayout(
127
127
  std::shared_ptr<GPUBindGroupLayoutDescriptor> descriptor);
128
128
  std::shared_ptr<GPUPipelineLayout>
@@ -153,7 +153,7 @@ public:
153
153
  std::unordered_set<std::string> getFeatures();
154
154
  std::shared_ptr<GPUSupportedLimits> getLimits();
155
155
  std::shared_ptr<GPUQueue> getQueue();
156
- async::AsyncTaskHandle getLost();
156
+ jsi::Value getLost(jsi::Runtime &runtime, const jsi::Object &wrapper);
157
157
  void notifyDeviceLost(wgpu::DeviceLostReason reason, std::string message);
158
158
  void notifyUncapturedError(wgpu::ErrorType type, std::string message);
159
159
  void forceLossForTesting();
@@ -211,7 +211,30 @@ public:
211
211
  installGetter(runtime, prototype, "features", &GPUDevice::getFeatures);
212
212
  installGetter(runtime, prototype, "limits", &GPUDevice::getLimits);
213
213
  installGetter(runtime, prototype, "queue", &GPUDevice::getQueue);
214
- installGetter(runtime, prototype, "lost", &GPUDevice::getLost);
214
+ // `lost` is installed manually: the getter needs the wrapper object
215
+ // itself, because the promise (and its resolve function) are cached as
216
+ // hidden properties on the JS side so the GC traces them as part of the
217
+ // device graph. Holding them strongly from C++ would root the promise's
218
+ // .then reactions forever (issue #445).
219
+ {
220
+ auto lostGetter = jsi::Function::createFromHostFunction(
221
+ runtime, jsi::PropNameID::forUtf8(runtime, "get_lost"), 0,
222
+ [](jsi::Runtime &rt, const jsi::Value &thisVal,
223
+ const jsi::Value * /*args*/, size_t /*count*/) -> jsi::Value {
224
+ auto native = GPUDevice::fromValue(rt, thisVal);
225
+ return native->getLost(rt, thisVal.getObject(rt));
226
+ });
227
+ auto objectCtor = runtime.global().getPropertyAsObject(runtime, "Object");
228
+ auto defineProperty =
229
+ objectCtor.getPropertyAsFunction(runtime, "defineProperty");
230
+ jsi::Object descriptor(runtime);
231
+ descriptor.setProperty(runtime, "get", lostGetter);
232
+ descriptor.setProperty(runtime, "enumerable", true);
233
+ descriptor.setProperty(runtime, "configurable", true);
234
+ defineProperty.call(runtime, prototype,
235
+ jsi::String::createFromUtf8(runtime, "lost"),
236
+ descriptor);
237
+ }
215
238
  installGetterSetter(runtime, prototype, "label", &GPUDevice::getLabel,
216
239
  &GPUDevice::setLabel);
217
240
  installMethod(runtime, prototype, "forceLossForTesting",
@@ -262,15 +285,28 @@ private:
262
285
  wgpu::Device _instance;
263
286
  std::shared_ptr<async::RuntimeContext> _async;
264
287
  std::string _label;
265
- // Guards the device-lost state below. In the ProcessEvents model both
266
- // notifyDeviceLost() (fired by Dawn during ProcessEvents) and getLost() run on
267
- // the owning runtime's own thread, but device destruction can also trigger
268
- // notifyDeviceLost() synchronously, so the mutex keeps these fields safe.
288
+ // Guards the device-lost state below. getLost() runs on a JS thread, but
289
+ // Dawn's AllowSpontaneous device-lost callback (and device destruction) can
290
+ // fire notifyDeviceLost() from other threads, so the mutex keeps these
291
+ // fields safe.
269
292
  std::mutex _lostMutex;
270
- std::optional<async::AsyncTaskHandle> _lostHandle;
271
293
  std::shared_ptr<GPUDeviceLostInfo> _lostInfo;
272
294
  bool _lostSettled = false;
273
- std::optional<async::AsyncTaskHandle::ResolveFunction> _lostResolve;
295
+ // Pending `lost` promises, held WEAKLY. A strong native reference would be
296
+ // a GC root: the promise's .then reactions (three.js captures its whole
297
+ // renderer there) could never be collected, pinning every GPU wrapper of
298
+ // the scene and its reported external memory forever (issue #445). The
299
+ // resolve function lives as a hidden property on the promise object itself,
300
+ // so if the promise is still alive when the device is lost we can settle
301
+ // it; if it was collected, nobody could have observed the resolution.
302
+ // Entries are only added for the device's own runtime when a CallInvoker is
303
+ // available (main JS runtime), matching the best-effort contract for
304
+ // spontaneous events.
305
+ struct PendingLostPromise {
306
+ jsi::Runtime *runtime;
307
+ jsi::WeakObject promise;
308
+ };
309
+ std::vector<PendingLostPromise> _lostPromises;
274
310
 
275
311
  // Event listeners storage - keyed by event type
276
312
  // Each entry contains a vector of shared_ptr to functions