react-native-webgpu 0.10.2 → 0.10.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.
@@ -34,44 +34,51 @@ namespace jsi = facebook::jsi;
34
34
  template <typename Derived> class NativeObject;
35
35
 
36
36
  /**
37
- * Registry for NativeObject prototype installers.
38
- * This allows BoxedWebGPUObject::unbox() to install prototypes on any runtime
39
- * by looking up the brand name and calling the appropriate installer.
37
+ * Registry mapping a NativeObject class brand (CLASS_NAME) to a reconstructor
38
+ * that rebuilds a fully functional JS object (prototype + native state) on
39
+ * any runtime. This is what lets BoxedWebGPUObject::unbox() bring objects to
40
+ * worklet runtimes.
40
41
  */
41
42
  class NativeObjectRegistry {
42
43
  public:
43
- using InstallerFunc = std::function<void(jsi::Runtime &)>;
44
+ using Reconstructor = std::function<jsi::Value(
45
+ jsi::Runtime &, std::shared_ptr<jsi::NativeState>)>;
44
46
 
45
47
  static NativeObjectRegistry &getInstance() {
46
48
  static NativeObjectRegistry instance;
47
49
  return instance;
48
50
  }
49
51
 
50
- void registerInstaller(const std::string &brand, InstallerFunc installer) {
52
+ void registerClass(const std::string &brand, Reconstructor reconstructor) {
51
53
  std::lock_guard<std::mutex> lock(_mutex);
52
- if (_installers.count(brand) != 0) {
54
+ if (_reconstructors.count(brand) != 0) {
53
55
  // The brand is the key unbox() uses to rebuild objects on worklet
54
56
  // runtimes - a duplicate would let one class hijack another's unboxing.
55
57
  throw std::runtime_error("Duplicate native object brand registered: " +
56
58
  brand);
57
59
  }
58
- _installers[brand] = std::move(installer);
60
+ _reconstructors[brand] = std::move(reconstructor);
59
61
  }
60
62
 
61
- bool installPrototype(jsi::Runtime &runtime, const std::string &brand) {
62
- std::lock_guard<std::mutex> lock(_mutex);
63
- auto it = _installers.find(brand);
64
- if (it != _installers.end()) {
65
- it->second(runtime);
66
- return true;
63
+ jsi::Value reconstruct(jsi::Runtime &runtime, const std::string &brand,
64
+ std::shared_ptr<jsi::NativeState> state) {
65
+ Reconstructor reconstructor;
66
+ {
67
+ std::lock_guard<std::mutex> lock(_mutex);
68
+ auto it = _reconstructors.find(brand);
69
+ if (it == _reconstructors.end()) {
70
+ throw jsi::JSError(runtime,
71
+ "No native class registered for brand: " + brand);
72
+ }
73
+ reconstructor = it->second;
67
74
  }
68
- return false;
75
+ return reconstructor(runtime, std::move(state));
69
76
  }
70
77
 
71
78
  private:
72
79
  NativeObjectRegistry() = default;
73
80
  std::mutex _mutex;
74
- std::unordered_map<std::string, InstallerFunc> _installers;
81
+ std::unordered_map<std::string, Reconstructor> _reconstructors;
75
82
  };
76
83
 
77
84
  /**
@@ -88,7 +95,8 @@ private:
88
95
  * Usage pattern with registerCustomSerializable:
89
96
  * - pack(): Call WebGPU.box(obj) to create a BoxedWebGPUObject (HostObject)
90
97
  * - The HostObject is serialized by Worklets and transferred to UI runtime
91
- * - unpack(): Call boxed.unbox() to get back the original object with prototype
98
+ * - unpack(): Call boxed.unbox() to get the object's wrapper on that runtime
99
+ * (cached per runtime, see NativeObject::create), prototype included
92
100
  *
93
101
  * This is similar to NitroModules.box()/unbox() pattern.
94
102
  */
@@ -101,37 +109,15 @@ public:
101
109
  jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override {
102
110
  auto propName = name.utf8(runtime);
103
111
  if (propName == "unbox") {
112
+ auto state = _nativeState;
113
+ auto brand = _brand;
104
114
  return jsi::Function::createFromHostFunction(
105
115
  runtime, jsi::PropNameID::forUtf8(runtime, "unbox"), 0,
106
- [this](jsi::Runtime &rt, const jsi::Value & /*thisVal*/,
107
- const jsi::Value * /*args*/,
108
- size_t /*count*/) -> jsi::Value {
109
- // Try to get the prototype from the global constructor
110
- auto ctor = rt.global().getProperty(rt, _brand.c_str());
111
- if (!ctor.isObject()) {
112
- // Constructor doesn't exist on this runtime - install it
113
- NativeObjectRegistry::getInstance().installPrototype(rt, _brand);
114
- ctor = rt.global().getProperty(rt, _brand.c_str());
115
- }
116
-
117
- // Create a new object and attach the native state
118
- jsi::Object obj(rt);
119
- obj.setNativeState(rt, _nativeState);
120
-
121
- // Set the prototype if constructor exists
122
- if (ctor.isObject()) {
123
- auto ctorObj = ctor.getObject(rt);
124
- auto proto = ctorObj.getProperty(rt, "prototype");
125
- if (proto.isObject()) {
126
- auto objectCtor =
127
- rt.global().getPropertyAsObject(rt, "Object");
128
- auto setPrototypeOf =
129
- objectCtor.getPropertyAsFunction(rt, "setPrototypeOf");
130
- setPrototypeOf.call(rt, obj, proto);
131
- }
132
- }
133
-
134
- return std::move(obj);
116
+ [state, brand](jsi::Runtime &rt, const jsi::Value & /*thisVal*/,
117
+ const jsi::Value * /*args*/,
118
+ size_t /*count*/) -> jsi::Value {
119
+ return NativeObjectRegistry::getInstance().reconstruct(rt, brand,
120
+ state);
135
121
  });
136
122
  }
137
123
  if (propName == "__boxedWebGPU") {
@@ -305,15 +291,38 @@ public:
305
291
  * created internally by the native code).
306
292
  *
307
293
  * Also registers this class with NativeObjectRegistry so that
308
- * BoxedWebGPUObject::unbox() can install prototypes on secondary runtimes.
294
+ * BoxedWebGPUObject::unbox() can rebuild its objects on secondary runtimes.
309
295
  */
310
296
  static void installConstructor(jsi::Runtime &runtime) {
311
- // Register this class's installer in the registry (only needs to happen once)
297
+ // Register the reconstructor used by BoxedWebGPUObject::unbox() to
298
+ // rebuild this object (prototype + native state) on another runtime.
312
299
  static std::once_flag registryFlag;
313
300
  std::call_once(registryFlag, []() {
314
- NativeObjectRegistry::getInstance().registerInstaller(
301
+ NativeObjectRegistry::getInstance().registerClass(
315
302
  Derived::CLASS_NAME,
316
- [](jsi::Runtime &rt) { Derived::installConstructor(rt); });
303
+ [](jsi::Runtime &rt,
304
+ std::shared_ptr<jsi::NativeState> state) -> jsi::Value {
305
+ auto instance = std::dynamic_pointer_cast<Derived>(state);
306
+ if (instance == nullptr) {
307
+ throw jsi::JSError(rt, "Invalid boxed native object state");
308
+ }
309
+ // The constructor makes `instanceof` work on this runtime too.
310
+ if (!rt.global().hasProperty(rt, Derived::CLASS_NAME)) {
311
+ Derived::installConstructor(rt);
312
+ }
313
+ // Unboxing hands back the wrapper cached for the target runtime,
314
+ // creating it on first use. Keep the runtime the object was
315
+ // originally created on: async native code (GPUDevice error/lost
316
+ // events, GPUAdapter::requestDevice) delivers into the creation
317
+ // runtime, and rebinding it to a worklet runtime would invoke
318
+ // main-runtime jsi::Functions on the wrong runtime and thread.
319
+ auto *originalRuntime = instance->getCreationRuntime();
320
+ auto value = Derived::create(rt, instance);
321
+ if (originalRuntime != nullptr) {
322
+ instance->setCreationRuntime(originalRuntime);
323
+ }
324
+ return value;
325
+ });
317
326
  });
318
327
 
319
328
  installPrototype(runtime);
@@ -345,10 +354,36 @@ public:
345
354
  }
346
355
 
347
356
  /**
348
- * Create a JS object with native state attached.
357
+ * Returns the JS wrapper of `instance` on `runtime`, creating it on first
358
+ * use.
359
+ *
360
+ * A native object has at most one live wrapper per runtime. The wrapper is
361
+ * cached (weakly, so it stays collectable) in the runtime's JSICache, and
362
+ * converting the same object again (returning it from another native
363
+ * call, unboxing it in a worklet on every frame, ...) hands back that
364
+ * wrapper instead of a new one. Each wrapper reports the native memory the
365
+ * object owns to the GC of its runtime through setExternalMemoryPressure,
366
+ * so the memory is charged exactly once per runtime that can reach the
367
+ * object, however often it is converted; charging every conversion would
368
+ * multiply the amount by the number of captures in a worklet until Hermes
369
+ * hit its max heap size. The hint is refreshed on every round trip for
370
+ * objects whose size changes after creation.
349
371
  */
350
372
  static jsi::Value create(jsi::Runtime &runtime,
351
373
  std::shared_ptr<Derived> instance) {
374
+ auto &cache = JSICache::get(runtime);
375
+ const void *key = instance.get();
376
+
377
+ auto existing = cache.lockWrapper(runtime, key);
378
+ if (existing.isObject()) {
379
+ auto pressure = instance->getMemoryPressure();
380
+ if (pressure > 0) {
381
+ existing.getObject(runtime).setExternalMemoryPressure(runtime,
382
+ pressure);
383
+ }
384
+ return existing;
385
+ }
386
+
352
387
  installPrototype(runtime);
353
388
 
354
389
  // Store creation runtime for logging etc.
@@ -374,6 +409,7 @@ public:
374
409
  obj.setExternalMemoryPressure(runtime, pressure);
375
410
  }
376
411
 
412
+ cache.setWrapper(runtime, key, obj);
377
413
  return std::move(obj);
378
414
  }
379
415