react-native-webgpu 0.13.1 → 0.14.1

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 (43) hide show
  1. package/android/CMakeLists.txt +1 -0
  2. package/android/cpp/AndroidPlatformContext.h +0 -9
  3. package/android/cpp/cpp-adapter.cpp +6 -4
  4. package/android/src/main/java/com/webgpu/WebGPUView.java +8 -0
  5. package/apple/ApplePlatformContext.h +0 -3
  6. package/apple/ApplePlatformContext.mm +0 -10
  7. package/apple/MetalView.mm +4 -3
  8. package/cpp/jsi/JSIConverter.h +6 -11
  9. package/cpp/jsi/NativeObject.h +130 -18
  10. package/cpp/rnwgpu/EnforceRange.h +68 -0
  11. package/cpp/rnwgpu/PlatformContext.h +0 -2
  12. package/cpp/rnwgpu/RNWebGPUManager.cpp +7 -95
  13. package/cpp/rnwgpu/RNWebGPUManager.h +4 -3
  14. package/cpp/rnwgpu/WebGPURuntime.cpp +91 -0
  15. package/cpp/rnwgpu/WebGPURuntime.h +36 -0
  16. package/cpp/rnwgpu/api/Convertors.h +10 -1
  17. package/cpp/rnwgpu/api/DestroyedError.h +26 -0
  18. package/cpp/rnwgpu/api/GPU.cpp +3 -57
  19. package/cpp/rnwgpu/api/GPUDevice.cpp +17 -9
  20. package/cpp/rnwgpu/api/GPUError.h +15 -0
  21. package/cpp/rnwgpu/api/GPUInternalError.h +2 -0
  22. package/cpp/rnwgpu/api/GPUOutOfMemoryError.h +2 -0
  23. package/cpp/rnwgpu/api/GPUQueue.cpp +7 -21
  24. package/cpp/rnwgpu/api/GPUSharedFence.cpp +39 -12
  25. package/cpp/rnwgpu/api/GPUSharedFence.h +21 -4
  26. package/cpp/rnwgpu/api/GPUSharedTextureMemory.cpp +80 -4
  27. package/cpp/rnwgpu/api/GPUSharedTextureMemory.h +29 -6
  28. package/cpp/rnwgpu/api/GPUTexture.h +6 -0
  29. package/cpp/rnwgpu/api/GPUValidationError.h +2 -0
  30. package/cpp/rnwgpu/api/descriptors/Unions.h +80 -0
  31. package/lib/commonjs/external/reanimated/registerWebGPUForReanimated.js +13 -3
  32. package/lib/commonjs/external/reanimated/registerWebGPUForReanimated.js.map +1 -1
  33. package/lib/module/external/reanimated/registerWebGPUForReanimated.js +13 -3
  34. package/lib/module/external/reanimated/registerWebGPUForReanimated.js.map +1 -1
  35. package/lib/typescript/lib/commonjs/external/reanimated/registerWebGPUForReanimated.d.ts.map +1 -1
  36. package/lib/typescript/lib/module/external/reanimated/registerWebGPUForReanimated.d.ts.map +1 -1
  37. package/lib/typescript/src/external/reanimated/registerWebGPUForReanimated.d.ts +12 -0
  38. package/lib/typescript/src/external/reanimated/registerWebGPUForReanimated.d.ts.map +1 -1
  39. package/lib/typescript/src/types.d.ts +2 -0
  40. package/lib/typescript/src/types.d.ts.map +1 -1
  41. package/package.json +2 -2
  42. package/src/external/reanimated/registerWebGPUForReanimated.ts +29 -7
  43. package/src/types.ts +14 -1
@@ -0,0 +1,91 @@
1
+ #include "WebGPURuntime.h"
2
+
3
+ #include <dlfcn.h>
4
+
5
+ #include <iterator>
6
+
7
+ namespace rnwgpu {
8
+
9
+ namespace {
10
+
11
+ wgpu::Instance createInstance() {
12
+ // If a Graphite build of react-native-skia is loaded in this
13
+ // process, adopt its wgpu::Instance instead of creating our own, so
14
+ // Graphite's device and every JS-created device live on one instance. That
15
+ // makes pointer-based handoff (RNWebGPU.importDevice of Skia's device)
16
+ // sound: the async pump serves the shared instance, so callbacks on
17
+ // imported devices settle. Discovered at runtime via dlsym so there is no
18
+ // build-time coupling and native-module init order does not matter. Both
19
+ // packages must link the same single Dawn copy (they vendor the same
20
+ // artifact) for the handle to be meaningful.
21
+ using GetInstanceFn = WGPUInstance (*)();
22
+ if (auto getSkiaInstance = reinterpret_cast<GetInstanceFn>(
23
+ dlsym(RTLD_DEFAULT, "rnskia_getWGPUInstance"))) {
24
+ if (WGPUInstance external = getSkiaInstance()) {
25
+ wgpuInstanceAddRef(external);
26
+ return wgpu::Instance::Acquire(external);
27
+ }
28
+ }
29
+
30
+ static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
31
+ wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1,
32
+ .requiredFeatures = &kTimedWaitAny};
33
+
34
+ wgpu::InstanceLimits limits{.timedWaitAnyMaxCount = 64};
35
+ instanceDesc.requiredLimits = &limits;
36
+
37
+ // Expose Dawn's experimental adapter features. Several features needed by
38
+ // our Android external-texture path (YCbCrVulkanSamplers,
39
+ // OpaqueYCbCrAndroidForExternalTexture) are tagged Experimental in Dawn's
40
+ // feature table and are otherwise filtered out of adapter.features by
41
+ // PhysicalDeviceBase::GetSupportedFeatures. The allow_unsafe_apis toggle
42
+ // disables that filter so the features become visible;
43
+ // expose_wgsl_experimental_features is the parallel toggle for WGSL language
44
+ // features.
45
+ //
46
+ // Trade-off: these are instance-level Dawn toggles, so they apply to every
47
+ // adapter/device created from this instance, not just the external-texture
48
+ // path. There is no finer-grained, per-feature mechanism to surface these.
49
+ // The exposure is acceptable because the toggle only *un-hides* experimental
50
+ // features in adapter.features; it does not enable any of them. Nothing
51
+ // experimental becomes active unless application code explicitly lists that
52
+ // feature in requiredFeatures at device creation, so the default behavior of
53
+ // a device that asks for no experimental features is unchanged.
54
+ static const char *const kEnabledToggles[] = {
55
+ "allow_unsafe_apis",
56
+ "expose_wgsl_experimental_features",
57
+ };
58
+ wgpu::DawnTogglesDescriptor toggles;
59
+ toggles.enabledToggleCount = std::size(kEnabledToggles);
60
+ toggles.enabledToggles = kEnabledToggles;
61
+ instanceDesc.nextInChain = &toggles;
62
+
63
+ return wgpu::CreateInstance(&instanceDesc);
64
+ }
65
+
66
+ } // namespace
67
+
68
+ WebGPURuntime &WebGPURuntime::shared() {
69
+ static auto *runtime = new WebGPURuntime();
70
+ return *runtime;
71
+ }
72
+
73
+ wgpu::Instance WebGPURuntime::instance() {
74
+ std::call_once(_instanceOnce, [this] { _instance = createInstance(); });
75
+ return _instance;
76
+ }
77
+
78
+ wgpu::Surface WebGPURuntime::makeSurface(void *nativeSurface) {
79
+ #if defined(__ANDROID__)
80
+ wgpu::SurfaceSourceAndroidNativeWindow source;
81
+ source.window = nativeSurface;
82
+ #else
83
+ wgpu::SurfaceSourceMetalLayer source;
84
+ source.layer = nativeSurface;
85
+ #endif
86
+ wgpu::SurfaceDescriptor descriptor;
87
+ descriptor.nextInChain = &source;
88
+ return instance().CreateSurface(&descriptor);
89
+ }
90
+
91
+ } // namespace rnwgpu
@@ -0,0 +1,36 @@
1
+ #pragma once
2
+
3
+ #include <mutex>
4
+
5
+ #include "webgpu/webgpu_cpp.h"
6
+
7
+ namespace rnwgpu {
8
+
9
+ // Process-lifetime GPU state shared by every React instance, JS runtime and
10
+ // thread. It holds nothing tied to one React instance (no jsi::Runtime,
11
+ // CallInvoker or BlobModule): those stay with the per-instance module.
12
+ // Intentionally never destroyed, so there is no static-destruction ordering to
13
+ // get wrong and it outlives a React reload.
14
+ class WebGPURuntime {
15
+ public:
16
+ static WebGPURuntime &shared();
17
+
18
+ // The process-wide instance: Skia Graphite's when that is loaded, otherwise
19
+ // our own. Created on first use. Safe to call from any thread.
20
+ wgpu::Instance instance();
21
+
22
+ // Creates a surface on the shared instance for a CAMetalLayer (Apple) or an
23
+ // ANativeWindow (Android). Safe to call from any thread.
24
+ wgpu::Surface makeSurface(void *nativeSurface);
25
+
26
+ WebGPURuntime(const WebGPURuntime &) = delete;
27
+ WebGPURuntime &operator=(const WebGPURuntime &) = delete;
28
+
29
+ private:
30
+ WebGPURuntime() = default;
31
+
32
+ std::once_flag _instanceOnce;
33
+ wgpu::Instance _instance;
34
+ };
35
+
36
+ } // namespace rnwgpu
@@ -10,6 +10,7 @@
10
10
 
11
11
  #include "webgpu/webgpu_cpp.h"
12
12
 
13
+ #include "EnforceRange.h"
13
14
  #include "GPUBindGroupDescriptor.h"
14
15
  #include "GPUBindGroupEntry.h"
15
16
  #include "GPUBindGroupLayoutDescriptor.h"
@@ -161,11 +162,19 @@ public:
161
162
  return true;
162
163
  }
163
164
 
165
+ // JS numbers destined for an integer field are WebIDL [EnforceRange]
166
+ // integers in WebGPU, so an out-of-range value is a TypeError rather than a
167
+ // silent wrap (and an undefined cast). Flags are enums here and keep the
168
+ // plain cast, as GPUFlagsConstant is not [EnforceRange].
164
169
  template <typename T>
165
170
  [[nodiscard]] typename std::enable_if<
166
171
  (std::is_arithmetic<T>::value || std::is_enum<T>::value), bool>::type
167
172
  Convert(T &out, const double &in) {
168
- out = static_cast<T>(in);
173
+ if constexpr (std::is_integral_v<T> && !std::is_same_v<T, bool>) {
174
+ out = enforceRange<T>(in);
175
+ } else {
176
+ out = static_cast<T>(in);
177
+ }
169
178
  return true;
170
179
  }
171
180
 
@@ -0,0 +1,26 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+
5
+ #include "webgpu/webgpu_cpp.h"
6
+
7
+ namespace rnwgpu {
8
+
9
+ // 'GPUSharedFence "label"', or just the class name when unlabeled.
10
+ inline std::string describeDestroyed(const char *className,
11
+ const std::string &label) {
12
+ return label.empty() ? std::string(className)
13
+ : std::string(className) + " \"" + label + "\"";
14
+ }
15
+
16
+ // Reports a use of a destroyed object the way Dawn reports an invalid one: a
17
+ // validation error, caught by error scopes or else delivered to the device's
18
+ // uncapturederror event.
19
+ inline void reportDestroyedUse(wgpu::Device &device, const char *className,
20
+ const std::string &label, const char *method) {
21
+ auto message = "Destroyed " + describeDestroyed(className, label) +
22
+ " used in " + method + "().";
23
+ device.InjectError(wgpu::ErrorType::Validation, message.c_str());
24
+ }
25
+
26
+ } // namespace rnwgpu
@@ -1,7 +1,5 @@
1
1
  #include "GPU.h"
2
2
 
3
- #include <dlfcn.h>
4
-
5
3
  #include <cstdio>
6
4
  #include <memory>
7
5
  #include <string>
@@ -11,65 +9,13 @@
11
9
 
12
10
  #include "Convertors.h"
13
11
  #include "JSIConverter.h"
12
+ #include "WebGPURuntime.h"
14
13
  #include "rnwgpu/async/RuntimeContext.h"
15
14
 
16
15
  namespace rnwgpu {
17
16
 
18
- GPU::GPU(jsi::Runtime & /*runtime*/) : NativeObject(CLASS_NAME) {
19
- // If a Graphite build of react-native-skia is loaded in this
20
- // process, adopt its wgpu::Instance instead of creating our own, so
21
- // Graphite's device and every JS-created device live on one instance. That
22
- // makes pointer-based handoff (RNWebGPU.importDevice of Skia's device)
23
- // sound: the async pump serves the shared instance, so callbacks on
24
- // imported devices settle. Discovered at runtime via dlsym so there is no
25
- // build-time coupling and native-module init order does not matter. Both
26
- // packages must link the same single Dawn copy (they vendor the same
27
- // artifact) for the handle to be meaningful.
28
- using GetInstanceFn = WGPUInstance (*)();
29
- if (auto getSkiaInstance = reinterpret_cast<GetInstanceFn>(
30
- dlsym(RTLD_DEFAULT, "rnskia_getWGPUInstance"))) {
31
- if (WGPUInstance external = getSkiaInstance()) {
32
- wgpuInstanceAddRef(external);
33
- _instance = wgpu::Instance::Acquire(external);
34
- return;
35
- }
36
- }
37
-
38
- static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
39
- wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1,
40
- .requiredFeatures = &kTimedWaitAny};
41
-
42
- wgpu::InstanceLimits limits{.timedWaitAnyMaxCount = 64};
43
- instanceDesc.requiredLimits = &limits;
44
-
45
- // Expose Dawn's experimental adapter features. Several features needed by
46
- // our Android external-texture path (YCbCrVulkanSamplers,
47
- // OpaqueYCbCrAndroidForExternalTexture) are tagged Experimental in Dawn's
48
- // feature table and are otherwise filtered out of adapter.features by
49
- // PhysicalDeviceBase::GetSupportedFeatures. The allow_unsafe_apis toggle
50
- // disables that filter so the features become visible;
51
- // expose_wgsl_experimental_features is the parallel toggle for WGSL language
52
- // features.
53
- //
54
- // Trade-off: these are instance-level Dawn toggles, so they apply to every
55
- // adapter/device created from this instance, not just the external-texture
56
- // path. There is no finer-grained, per-feature mechanism to surface these.
57
- // The exposure is acceptable because the toggle only *un-hides* experimental
58
- // features in adapter.features; it does not enable any of them. Nothing
59
- // experimental becomes active unless application code explicitly lists that
60
- // feature in requiredFeatures at device creation, so the default behavior of
61
- // a device that asks for no experimental features is unchanged.
62
- static const char *const kEnabledToggles[] = {
63
- "allow_unsafe_apis",
64
- "expose_wgsl_experimental_features",
65
- };
66
- wgpu::DawnTogglesDescriptor toggles;
67
- toggles.enabledToggleCount = std::size(kEnabledToggles);
68
- toggles.enabledToggles = kEnabledToggles;
69
- instanceDesc.nextInChain = &toggles;
70
-
71
- _instance = wgpu::CreateInstance(&instanceDesc);
72
- }
17
+ GPU::GPU(jsi::Runtime & /*runtime*/)
18
+ : NativeObject(CLASS_NAME), _instance(WebGPURuntime::shared().instance()) {}
73
19
 
74
20
  async::AsyncTaskHandle GPU::requestAdapter(
75
21
  jsi::Runtime &runtime,
@@ -345,14 +345,21 @@ std::shared_ptr<GPUSharedTextureMemory> GPUDevice::importSharedTextureMemory(
345
345
  #endif
346
346
 
347
347
  auto memory = _instance.ImportSharedTextureMemory(&desc);
348
- if (memory == nullptr) {
349
- throw std::runtime_error("GPUDevice::importSharedTextureMemory(): "
350
- "ImportSharedTextureMemory returned null - is the "
351
- "'shared-texture-memory-iosurface' (Apple) or "
352
- "'shared-texture-memory-ahardware-buffer' "
353
- "(Android) feature enabled on the device?");
354
- }
355
- return std::make_shared<GPUSharedTextureMemory>(std::move(memory),
348
+ // Dawn reports a failed import (feature missing, unusable handle) as a
349
+ // validation error and returns an error object rather than null, with no
350
+ // format in its properties: the format getter of any texture created from
351
+ // it would throw. Fail the import instead.
352
+ wgpu::SharedTextureMemoryProperties properties{};
353
+ if (memory == nullptr ||
354
+ memory.GetProperties(&properties) != wgpu::Status::Success ||
355
+ properties.format == wgpu::TextureFormat::Undefined) {
356
+ throw std::runtime_error(
357
+ "GPUDevice::importSharedTextureMemory(): the import failed - is the "
358
+ "'shared-texture-memory-iosurface' (Apple) or "
359
+ "'shared-texture-memory-ahardware-buffer' (Android) feature enabled "
360
+ "on the device, and the handle a usable IOSurface / AHardwareBuffer?");
361
+ }
362
+ return std::make_shared<GPUSharedTextureMemory>(_instance, std::move(memory),
356
363
  std::move(label));
357
364
  }
358
365
 
@@ -402,7 +409,8 @@ std::shared_ptr<GPUSharedFence> GPUDevice::importSharedFence(
402
409
  "GPUDevice::importSharedFence(): ImportSharedFence returned null - is "
403
410
  "the matching 'shared-fence-*' feature enabled on the device?");
404
411
  }
405
- return std::make_shared<GPUSharedFence>(std::move(fence), std::move(label));
412
+ return std::make_shared<GPUSharedFence>(_instance, std::move(fence),
413
+ std::move(label));
406
414
  }
407
415
 
408
416
  async::AsyncTaskHandle GPUDevice::createComputePipelineAsync(
@@ -24,6 +24,21 @@ public:
24
24
  installGetter(runtime, prototype, "message", &GPUError::getMessage);
25
25
  }
26
26
 
27
+ // GPUValidationError, GPUOutOfMemoryError and GPUInternalError extend
28
+ // GPUError in the spec: chain their prototype to GPUError.prototype so
29
+ // `error instanceof GPUError` holds.
30
+ static void inheritPrototype(jsi::Runtime &runtime, jsi::Object &prototype) {
31
+ installPrototype(runtime);
32
+ auto *parent = getCachedPrototype(runtime);
33
+ if (parent == nullptr) {
34
+ return;
35
+ }
36
+ auto objectCtor = runtime.global().getPropertyAsObject(runtime, "Object");
37
+ auto setPrototypeOf =
38
+ objectCtor.getPropertyAsFunction(runtime, "setPrototypeOf");
39
+ setPrototypeOf.call(runtime, prototype, *parent);
40
+ }
41
+
27
42
  protected:
28
43
  // Protected constructor for subclasses
29
44
  GPUError(const char *className, std::string message)
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include <string>
4
4
 
5
+ #include "GPUError.h"
5
6
  #include "NativeObject.h"
6
7
 
7
8
  namespace rnwgpu {
@@ -20,6 +21,7 @@ public:
20
21
  std::string getMessage() { return _message; }
21
22
 
22
23
  static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) {
24
+ GPUError::inheritPrototype(runtime, prototype);
23
25
  installGetter(runtime, prototype, "__brand", &GPUInternalError::getBrand);
24
26
  installGetter(runtime, prototype, "message", &GPUInternalError::getMessage);
25
27
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include <string>
4
4
 
5
+ #include "GPUError.h"
5
6
  #include "NativeObject.h"
6
7
 
7
8
  namespace rnwgpu {
@@ -20,6 +21,7 @@ public:
20
21
  std::string getMessage() { return _message; }
21
22
 
22
23
  static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) {
24
+ GPUError::inheritPrototype(runtime, prototype);
23
25
  installGetter(runtime, prototype, "__brand",
24
26
  &GPUOutOfMemoryError::getBrand);
25
27
  installGetter(runtime, prototype, "message",
@@ -9,6 +9,7 @@
9
9
  #include <vector>
10
10
 
11
11
  #include "Convertors.h"
12
+ #include "EnforceRange.h"
12
13
  #include "ImageBitmap.h"
13
14
  #include "PlatformContext.h"
14
15
 
@@ -126,15 +127,9 @@ void GPUQueue::copyVideoFrameToTexture(
126
127
  throw std::runtime_error(
127
128
  "copyExternalImageToTexture: a native frame is a single 2D image");
128
129
  }
129
- constexpr double kMaxCoordinate =
130
- static_cast<double>(std::numeric_limits<uint32_t>::max());
131
- auto coordinate = [&](double value, const char *name) {
132
- if (!(value >= 0) || value > kMaxCoordinate) {
133
- throw std::runtime_error(std::string("copyExternalImageToTexture: ") +
134
- name +
135
- " must be a non-negative integer coordinate");
136
- }
137
- return static_cast<uint32_t>(value);
130
+ // GPUOrigin2D coordinates are [EnforceRange] unsigned long.
131
+ auto coordinate = [](double value, const char * /*name*/) {
132
+ return enforceRange<uint32_t>(value);
138
133
  };
139
134
  const double rotation = source.rotation.value_or(0);
140
135
  if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
@@ -208,18 +203,9 @@ void GPUQueue::copyExternalImageToTexture(
208
203
  }
209
204
 
210
205
  const auto origin = source->origin.value_or(nullptr);
211
- // GPUOrigin2D coordinates are [EnforceRange] unsigned long: negative,
212
- // NaN, or out-of-range doubles must be rejected here because casting
213
- // them to an unsigned integer is undefined behavior.
214
- constexpr double kMaxOrigin =
215
- static_cast<double>(std::numeric_limits<uint32_t>::max());
216
- if (origin && (!(origin->x >= 0) || !(origin->y >= 0) ||
217
- origin->x > kMaxOrigin || origin->y > kMaxOrigin)) {
218
- throw std::runtime_error(
219
- "The source origin must be a non-negative integer coordinate.");
220
- }
221
- const size_t sourceOriginX = origin ? static_cast<size_t>(origin->x) : 0;
222
- const size_t sourceOriginY = origin ? static_cast<size_t>(origin->y) : 0;
206
+ // GPUOrigin2D coordinates are [EnforceRange] unsigned long.
207
+ const size_t sourceOriginX = origin ? enforceRange<uint32_t>(origin->x) : 0;
208
+ const size_t sourceOriginY = origin ? enforceRange<uint32_t>(origin->y) : 0;
223
209
  if (sourceOriginX > source->source->getWidth() ||
224
210
  sz.width > source->source->getWidth() - sourceOriginX ||
225
211
  sourceOriginY > source->source->getHeight() ||
@@ -3,6 +3,8 @@
3
3
  #include <cstdint>
4
4
  #include <string>
5
5
 
6
+ #include "DestroyedError.h"
7
+
6
8
  #if defined(__ANDROID__)
7
9
  #include <fcntl.h>
8
10
  #endif
@@ -32,10 +34,30 @@ std::string sharedFenceTypeToString(wgpu::SharedFenceType type) {
32
34
  }
33
35
  }
34
36
 
37
+ // Dawn reports a fence it can't export (an invalid one) as a validation error
38
+ // and leaves the type at 0. Fail the call too, like a destroyed fence does,
39
+ // rather than returning a record with no type and no handle.
40
+ void throwIfNotExported(jsi::Runtime &runtime,
41
+ const wgpu::SharedFenceExportInfo &info) {
42
+ if (info.type == wgpu::SharedFenceType(0)) {
43
+ throw jsi::JSError(runtime, "GPUSharedFence::export() failed");
44
+ }
45
+ }
46
+
35
47
  } // namespace
36
48
 
49
+ void GPUSharedFence::injectDestroyedError(const char *method) {
50
+ reportDestroyedUse(_device, CLASS_NAME, _label, method);
51
+ }
52
+
37
53
  jsi::Value GPUSharedFence::exportInfo(jsi::Runtime &runtime, const jsi::Value &,
38
54
  const jsi::Value *, size_t) {
55
+ // Fails like an export Dawn rejects: a validation error, then the throw.
56
+ if (!_instance) {
57
+ injectDestroyedError("export");
58
+ throw jsi::JSError(runtime,
59
+ "GPUSharedFence::export(): the fence was destroyed");
60
+ }
39
61
  wgpu::SharedFenceExportInfo info{};
40
62
  uint64_t handle = 0;
41
63
 
@@ -44,25 +66,30 @@ jsi::Value GPUSharedFence::exportInfo(jsi::Runtime &runtime, const jsi::Value &,
44
66
  wgpu::SharedFenceMTLSharedEventExportInfo mtlInfo{};
45
67
  info.nextInChain = &mtlInfo;
46
68
  _instance.ExportInfo(&info);
69
+ throwIfNotExported(runtime, info);
47
70
  handle = reinterpret_cast<uint64_t>(mtlInfo.sharedEvent);
48
71
  #elif defined(__ANDROID__)
49
- // Android: the handle is an OS file descriptor (sync_fd). Dawn's ExportInfo returns a BORROWED fd — it is
50
- // owned by the SharedFence and closed when the fence is destroyed. This exported handle is documented as
51
- // caller-owned (the caller must close() it), so dup() it. Without the dup the same fd is closed twice —
52
- // once by the caller and once by Dawn on fence destruction — tripping Android's fdsan (double-close abort).
72
+ // Android: the handle is an OS file descriptor (sync_fd). Dawn's ExportInfo
73
+ // returns a BORROWED fd — it is owned by the SharedFence and closed when the
74
+ // fence is destroyed. This exported handle is documented as caller-owned (the
75
+ // caller must close() it), so dup() it. Without the dup the same fd is closed
76
+ // twice — once by the caller and once by Dawn on fence destruction — tripping
77
+ // Android's fdsan (double-close abort).
53
78
  wgpu::SharedFenceSyncFDExportInfo fdInfo{};
54
79
  info.nextInChain = &fdInfo;
55
80
  _instance.ExportInfo(&info);
56
- int exportedFd =
57
- fdInfo.handle >= 0 ? ::fcntl(fdInfo.handle, F_DUPFD_CLOEXEC, 0) : fdInfo.handle;
81
+ throwIfNotExported(runtime, info);
82
+ int exportedFd = fdInfo.handle >= 0
83
+ ? ::fcntl(fdInfo.handle, F_DUPFD_CLOEXEC, 0)
84
+ : fdInfo.handle;
58
85
  handle = static_cast<uint64_t>(static_cast<uint32_t>(exportedFd));
59
86
  #else
60
- // react-native-webgpu only targets Apple (Metal) and Android (Vulkan). On any
61
- // other platform there is no native handle convention to expose, so fail loudly
62
- // rather than handing back a meaningless handle of 0.
63
- throw jsi::JSError(runtime,
64
- "GPUSharedFence::export(): unsupported platform (only "
65
- "Apple/Metal and Android/Vulkan are supported)");
87
+ // react-native-webgpu only targets Apple (Metal) and Android (Vulkan). On any
88
+ // other platform there is no native handle convention to expose, so fail
89
+ // loudly rather than handing back a meaningless handle of 0.
90
+ throw jsi::JSError(runtime,
91
+ "GPUSharedFence::export(): unsupported platform (only "
92
+ "Apple/Metal and Android/Vulkan are supported)");
66
93
  #endif
67
94
 
68
95
  jsi::Object result(runtime);
@@ -17,9 +17,10 @@ class GPUSharedFence : public NativeObject<GPUSharedFence> {
17
17
  public:
18
18
  static constexpr const char *CLASS_NAME = "GPUSharedFence";
19
19
 
20
- explicit GPUSharedFence(wgpu::SharedFence instance, std::string label)
21
- : NativeObject(CLASS_NAME), _instance(std::move(instance)),
22
- _label(std::move(label)) {}
20
+ explicit GPUSharedFence(wgpu::Device device, wgpu::SharedFence instance,
21
+ std::string label)
22
+ : NativeObject(CLASS_NAME), _device(std::move(device)),
23
+ _instance(std::move(instance)), _label(std::move(label)) {}
23
24
 
24
25
  public:
25
26
  std::string getBrand() { return CLASS_NAME; }
@@ -33,19 +34,35 @@ public:
33
34
  std::string getLabel() { return _label; }
34
35
  void setLabel(const std::string &label) {
35
36
  _label = label;
36
- _instance.SetLabel(_label.c_str());
37
+ if (_instance) {
38
+ _instance.SetLabel(_label.c_str());
39
+ }
37
40
  }
38
41
 
42
+ // Releases the fence now (closing its sync fd on Android) rather than when
43
+ // the JS object is garbage collected, which a runtime making fences every
44
+ // frame may not do for a while. Dawn keeps its own reference as long as it
45
+ // needs the fence (a beginAccess wait). A later use is a validation error
46
+ // and a throw.
47
+ void destroy() { _instance = nullptr; }
48
+
39
49
  static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) {
40
50
  installGetter(runtime, prototype, "__brand", &GPUSharedFence::getBrand);
41
51
  installMethod(runtime, prototype, "export", &GPUSharedFence::exportInfo);
52
+ installMethod(runtime, prototype, "destroy", &GPUSharedFence::destroy);
42
53
  installGetterSetter(runtime, prototype, "label", &GPUSharedFence::getLabel,
43
54
  &GPUSharedFence::setLabel);
44
55
  }
45
56
 
46
57
  inline wgpu::SharedFence get() { return _instance; }
47
58
 
59
+ // Reports a use of the destroyed fence the way Dawn reports an invalid
60
+ // object: a validation error, caught by error scopes or else delivered to
61
+ // the device's uncapturederror event.
62
+ void injectDestroyedError(const char *method);
63
+
48
64
  private:
65
+ wgpu::Device _device;
49
66
  wgpu::SharedFence _instance;
50
67
  std::string _label;
51
68
  };