react-native-webgpu 0.19.2 → 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 +28 -2
  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
package/Package.swift CHANGED
@@ -56,21 +56,33 @@ let dawnVersion =
56
56
  // changing either package's Dawn reset the package caches (File > Packages >
57
57
  // Reset Package Caches) if this does not re-run.
58
58
  //
59
- // A react-native-skia that depends on react-native-webgpu-dawn itself must pin
60
- // the same version as this package. Older Graphite builds (react-native-skia
61
- // 3.0.x) vendor their own Dawn and record its release tag at
62
- // libs/.dawn-version in their binary packages; this package's `dawn` field
59
+ // A react-native-skia that depends on react-native-webgpu-dawn itself must
60
+ // resolve the same version as this package: the copy nested under it when the
61
+ // package manager could not hoist it, else the one next to it (Node's
62
+ // resolution, which react-native-skia's podspec compares too). A workspace
63
+ // that resolves the package to one version for both (yarn `resolutions`, npm
64
+ // `overrides`) passes while react-native-skia's own pin lags behind a Dawn
65
+ // bump: the app then contains that one Dawn. Older Graphite builds
66
+ // (react-native-skia 3.0.x) vendor their own Dawn and record its release tag
67
+ // at libs/.dawn-version in their binary packages; this package's `dawn` field
63
68
  // names the release that Dawn is interchangeable with.
64
69
  if let skiaPath = nodePackage("react-native-skia"),
65
70
  let skiaPackage = readJSON("\(skiaPath)/package.json"),
66
- let skiaDawn = (skiaPackage["dependencies"] as? [String: String])?["react-native-webgpu-dawn"]
71
+ let skiaDawnPin = (skiaPackage["dependencies"] as? [String: String])?["react-native-webgpu-dawn"]
67
72
  {
73
+ let skiaDawnPath = [
74
+ "\(skiaPath)/node_modules/react-native-webgpu-dawn",
75
+ "\(skiaPath)/../react-native-webgpu-dawn",
76
+ ].first { FileManager.default.fileExists(atPath: "\($0)/package.json") }
77
+ let skiaDawn =
78
+ skiaDawnPath.flatMap { readJSON("\($0)/package.json")?["version"] as? String } ?? skiaDawnPin
68
79
  if skiaDawn != dawnVersion {
69
80
  fatalError(
70
81
  """
71
82
  react-native-webgpu: Dawn version mismatch. This package links \
72
- react-native-webgpu-dawn \(dawnVersion) but react-native-skia depends on \
73
- \(skiaDawn). Align the two packages so the app contains exactly one Dawn.
83
+ react-native-webgpu-dawn \(dawnVersion) but react-native-skia resolves \
84
+ \(skiaDawn) (it depends on \(skiaDawnPin)). Align the two packages so the \
85
+ app contains exactly one Dawn.
74
86
  """)
75
87
  }
76
88
  } else {
@@ -97,6 +97,13 @@ def supportsNamespace() {
97
97
  android {
98
98
  compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
99
99
 
100
+ if (rootProject.hasProperty("ndkPath")) {
101
+ ndkPath = rootProject.ext.ndkPath
102
+ }
103
+ if (rootProject.hasProperty("ndkVersion")) {
104
+ ndkVersion = rootProject.ext.ndkVersion
105
+ }
106
+
100
107
  defaultConfig {
101
108
  minSdkVersion resolveMinSdkVersion()
102
109
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
@@ -13,8 +13,12 @@ public:
13
13
  // UIView* / NSView* bridged to void*, or nullptr). Called on the main thread
14
14
  // only. Provided by WebGPUModule from React Native's view registry.
15
15
  using ViewLookup = std::function<void *(int)>;
16
+ // Returns React Native's RCTBlobManager (unretained, bridged to void*), or
17
+ // nullptr. Provided by WebGPUModule from React Native's module registry.
18
+ using BlobManagerLookup = std::function<void *()>;
16
19
 
17
- explicit ApplePlatformContext(ViewLookup viewLookup = nullptr);
20
+ explicit ApplePlatformContext(ViewLookup viewLookup,
21
+ BlobManagerLookup blobManagerLookup);
18
22
  ~ApplePlatformContext() = default;
19
23
 
20
24
  void snapshotView(const ViewSnapshotRequest &request,
@@ -52,6 +56,7 @@ public:
52
56
 
53
57
  private:
54
58
  ViewLookup _viewLookup;
59
+ BlobManagerLookup _blobManagerLookup;
55
60
  };
56
61
 
57
62
  } // namespace rnwgpu
@@ -6,7 +6,6 @@
6
6
  #import <CoreVideo/CoreVideo.h>
7
7
  #import <ImageIO/ImageIO.h>
8
8
  #import <React/RCTBlobManager.h>
9
- #import <React/RCTBridge+Private.h>
10
9
  #import <ReactCommon/RCTTurboModule.h>
11
10
 
12
11
  #include "AppleVideoPlayer.h"
@@ -35,8 +34,10 @@ void checkIfUsingSimulatorWithAPIValidation() {
35
34
  #endif
36
35
  }
37
36
 
38
- ApplePlatformContext::ApplePlatformContext(ViewLookup viewLookup)
39
- : _viewLookup(std::move(viewLookup)) {
37
+ ApplePlatformContext::ApplePlatformContext(ViewLookup viewLookup,
38
+ BlobManagerLookup blobManagerLookup)
39
+ : _viewLookup(std::move(viewLookup)),
40
+ _blobManagerLookup(std::move(blobManagerLookup)) {
40
41
  checkIfUsingSimulatorWithAPIValidation();
41
42
  }
42
43
 
@@ -360,8 +361,7 @@ static std::span<const uint8_t> nsDataToSpan(NSData *data) {
360
361
 
361
362
  ImageData ApplePlatformContext::createImageBitmap(std::string blobId,
362
363
  double offset, double size) {
363
- RCTBlobManager *blobManager =
364
- [[RCTBridge currentBridge] moduleForClass:RCTBlobManager.class];
364
+ RCTBlobManager *blobManager = (__bridge RCTBlobManager *)_blobManagerLookup();
365
365
  NSData *blobData =
366
366
  [blobManager resolve:[NSString stringWithUTF8String:blobId.c_str()]
367
367
  offset:(long)offset
@@ -378,9 +378,8 @@ void ApplePlatformContext::createImageBitmapAsync(
378
378
  std::string blobId, double offset, double size,
379
379
  std::function<void(ImageData)> onSuccess,
380
380
  std::function<void(std::string)> onError) {
381
- // Resolve blob on current thread (requires RCTBridge access)
382
- RCTBlobManager *blobManager =
383
- [[RCTBridge currentBridge] moduleForClass:RCTBlobManager.class];
381
+ // Resolve blob on current thread
382
+ RCTBlobManager *blobManager = (__bridge RCTBlobManager *)_blobManagerLookup();
384
383
  NSData *blobData =
385
384
  [blobManager resolve:[NSString stringWithUTF8String:blobId.c_str()]
386
385
  offset:(long)offset
@@ -19,9 +19,18 @@ namespace rnwgpu {
19
19
  // "display-p3" the layer is tagged Display P3 instead, or its extended range
20
20
  // variant on a float format, so the values reach a wide-gamut display without
21
21
  // being converted from sRGB first.
22
+ //
23
+ // GPUCanvasConfiguration.toneMapping.mode says what happens to values above
24
+ // 1.0 on a float canvas. Core Animation keeps an extended colorspace inside
25
+ // the display's SDR range unless the layer asks for extended dynamic range
26
+ // content, so without it the values clip at white: that is "standard". For
27
+ // "extended" the layer opts in, and the values use the display's headroom
28
+ // (EDR) as they do in a browser. An 8-bit canvas cannot hold values above 1.0,
29
+ // so the flag only applies to a float format.
22
30
  void applyCAMetalLayerColorSpace(void *nativeSurface,
23
31
  wgpu::TextureFormat format,
24
- CanvasColorSpace canvasColorSpace) {
32
+ CanvasColorSpace canvasColorSpace,
33
+ CanvasToneMappingMode toneMappingMode) {
25
34
  CALayer *layer = (__bridge CALayer *)nativeSurface;
26
35
  if (![layer isKindOfClass:[CAMetalLayer class]]) {
27
36
  return;
@@ -43,6 +52,13 @@ void applyCAMetalLayerColorSpace(void *nativeSurface,
43
52
  // 8-bit sRGB canvas.
44
53
  metalLayer.colorspace = nil;
45
54
  }
55
+ const bool extendedRange =
56
+ isFloat && toneMappingMode == CanvasToneMappingMode::Extended;
57
+ if (@available(iOS 16.0, tvOS 16.0, *)) {
58
+ if (metalLayer.wantsExtendedDynamicRangeContent != extendedRange) {
59
+ metalLayer.wantsExtendedDynamicRangeContent = extendedRange;
60
+ }
61
+ }
46
62
  // The change must be set synchronously so the first present already sees
47
63
  // it, and it must reach the render server. On a non-main thread (RN JS or
48
64
  // worklet runtime) the property lands in that thread's implicit
@@ -37,6 +37,10 @@ static std::shared_ptr<rnwgpu::RNWebGPUManager> webgpuManager;
37
37
  // (Fabric component views included), which drawElementImageToTexture needs.
38
38
  @synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
39
39
 
40
+ // Synthesize the module registry so the TurboModule infra injects it too. It
41
+ // reaches React Native's BlobModule in both Bridge and Bridgeless mode.
42
+ @synthesize moduleRegistry = _moduleRegistry;
43
+
40
44
  + (std::shared_ptr<rnwgpu::RNWebGPUManager>)getManager {
41
45
  return webgpuManager;
42
46
  }
@@ -93,8 +97,18 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) {
93
97
  [strongSelf.viewRegistry_DEPRECATED viewForReactTag:@(tag)];
94
98
  return (__bridge void *)view;
95
99
  };
100
+ rnwgpu::ApplePlatformContext::BlobManagerLookup blobManagerLookup =
101
+ [weakSelf]() -> void * {
102
+ WebGPUModule *strongSelf = weakSelf;
103
+ if (strongSelf == nil) {
104
+ return nullptr;
105
+ }
106
+ return (__bridge void *)[strongSelf.moduleRegistry
107
+ moduleForName:"BlobModule"];
108
+ };
96
109
  std::shared_ptr<rnwgpu::PlatformContext> platformContext =
97
- std::make_shared<rnwgpu::ApplePlatformContext>(std::move(viewLookup));
110
+ std::make_shared<rnwgpu::ApplePlatformContext>(
111
+ std::move(viewLookup), std::move(blobManagerLookup));
98
112
  webgpuManager = std::make_shared<rnwgpu::RNWebGPUManager>(runtime, jsInvoker,
99
113
  platformContext);
100
114
  return @true;
@@ -41,4 +41,8 @@ void Promise::reject(std::string message) {
41
41
  _rejecter.call(runtime, error.value());
42
42
  }
43
43
 
44
+ void Promise::reject(const jsi::JSError& error) {
45
+ _rejecter.call(runtime, error.value());
46
+ }
47
+
44
48
  } // namespace rnwgpu
package/cpp/jsi/Promise.h CHANGED
@@ -15,7 +15,10 @@ public:
15
15
  Promise(jsi::Runtime& runtime, jsi::Function&& resolver, jsi::Function&& rejecter);
16
16
 
17
17
  void resolve(jsi::Value&& result);
18
+ /** Rejects with a new Error carrying `error` as its message. */
18
19
  void reject(std::string error);
20
+ /** Rejects with the JS error `error` carries, as thrown. */
21
+ void reject(const jsi::JSError& error);
19
22
 
20
23
  public:
21
24
  jsi::Runtime& runtime;
@@ -6,4 +6,9 @@ namespace rnwgpu {
6
6
  // values are interpreted when it is composited.
7
7
  enum class CanvasColorSpace { SRGB, DisplayP3 };
8
8
 
9
+ // GPUCanvasConfiguration.toneMapping.mode (GPUCanvasToneMappingMode): whether
10
+ // values above 1.0 on a float canvas are clamped to the display's SDR range
11
+ // ("standard", the default) or reach its extended range ("extended").
12
+ enum class CanvasToneMappingMode { Standard, Extended };
13
+
9
14
  } // namespace rnwgpu
@@ -64,10 +64,8 @@ RNWebGPUManager::RNWebGPUManager(
64
64
  : _jsRuntime(jsRuntime), _jsCallInvoker(jsCallInvoker),
65
65
  _platformContext(platformContext) {
66
66
 
67
- // Register the main runtime + its CallInvoker so spontaneous events
68
- // (device.lost / uncapturederror) on main-runtime devices can be delivered to
69
- // the JS thread without the ProcessEvents pump. Worklet-runtime devices have
70
- // no invoker (best-effort; see README "Threading model").
67
+ // Register the main runtime + its CallInvoker: how async results reach the
68
+ // main runtime, and the way to the UI runtime (see async::RuntimeContext).
71
69
  async::RuntimeContext::registerMainRuntime(_jsRuntime, _jsCallInvoker);
72
70
 
73
71
  // Expose the platform context to API objects created later without a
@@ -39,11 +39,13 @@ namespace rnwgpu {
39
39
 
40
40
  #ifdef __APPLE__
41
41
  // Tags the CAMetalLayer with the colorspace matching the configured texture
42
- // format and the canvas's colorSpace. Implemented in
42
+ // format and the canvas's colorSpace, and opts it into the display's extended
43
+ // dynamic range for an "extended" toneMapping mode. Implemented in
43
44
  // apple/MetalLayerColorSpace.mm.
44
45
  void applyCAMetalLayerColorSpace(void *nativeSurface,
45
46
  wgpu::TextureFormat format,
46
- CanvasColorSpace colorSpace);
47
+ CanvasColorSpace colorSpace,
48
+ CanvasToneMappingMode toneMappingMode);
47
49
  #endif
48
50
 
49
51
  struct NativeInfo {
@@ -297,7 +299,8 @@ public:
297
299
  }
298
300
  configureSurfaceLocked(config);
299
301
  #ifdef __APPLE__
300
- applyCAMetalLayerColorSpace(_nativeSurface, _config.format, _colorSpace);
302
+ applyCAMetalLayerColorSpace(_nativeSurface, _config.format, _colorSpace,
303
+ _toneMappingMode);
301
304
  #endif
302
305
  if (blit) {
303
306
  presentBlit = blitOffscreenToSurfaceLocked();
@@ -333,13 +336,16 @@ public:
333
336
  // --- Rendering thread
334
337
  // -------------------------------------------------------
335
338
 
336
- void configure(wgpu::SurfaceConfiguration &newConfig,
337
- std::vector<wgpu::TextureFormat> viewFormats,
338
- CanvasColorSpace colorSpace = CanvasColorSpace::SRGB) {
339
+ void configure(
340
+ wgpu::SurfaceConfiguration &newConfig,
341
+ std::vector<wgpu::TextureFormat> viewFormats,
342
+ CanvasColorSpace colorSpace = CanvasColorSpace::SRGB,
343
+ CanvasToneMappingMode toneMappingMode = CanvasToneMappingMode::Standard) {
339
344
  applyPendingAttach(/* supersedeInFlightFrame = */ true);
340
345
  std::unique_lock<std::shared_mutex> lock(_mutex);
341
346
  _viewFormats = std::move(viewFormats);
342
347
  _colorSpace = colorSpace;
348
+ _toneMappingMode = toneMappingMode;
343
349
  _config = newConfig;
344
350
  // The caller's viewFormats storage dies with the call; point the stored
345
351
  // configuration at our own copy.
@@ -405,6 +411,7 @@ public:
405
411
  _texture = nullptr;
406
412
  _config = {};
407
413
  _colorSpace = CanvasColorSpace::SRGB;
414
+ _toneMappingMode = CanvasToneMappingMode::Standard;
408
415
  _viewFormats.clear();
409
416
  _acquiredFromSurface = false;
410
417
  _frameEpoch++;
@@ -911,7 +918,8 @@ private:
911
918
  if (_surface) {
912
919
  configureSurfaceLocked(_config);
913
920
  #ifdef __APPLE__
914
- applyCAMetalLayerColorSpace(_nativeSurface, _config.format, _colorSpace);
921
+ applyCAMetalLayerColorSpace(_nativeSurface, _config.format, _colorSpace,
922
+ _toneMappingMode);
915
923
  #endif
916
924
  } else {
917
925
  _texture = createOffscreenTextureLocked();
@@ -1276,6 +1284,7 @@ private:
1276
1284
  wgpu::SurfaceConfiguration _config;
1277
1285
  std::vector<wgpu::TextureFormat> _viewFormats;
1278
1286
  CanvasColorSpace _colorSpace = CanvasColorSpace::SRGB;
1287
+ CanvasToneMappingMode _toneMappingMode = CanvasToneMappingMode::Standard;
1279
1288
  // Keeps the Dawn instance alive for as long as any canvas exists.
1280
1289
  wgpu::Instance _gpu;
1281
1290
  // Native view size in dp (surfaced as clientWidth/clientHeight on the JS
@@ -18,7 +18,8 @@ wgpu::Instance createInstance() {
18
18
  // react-native-webgpu installs (see the RTLD_NOLOAD lookup below), so the
19
19
  // native-module init order decides whether the instance is shared.
20
20
  // RNWebGPU.importDevice does not depend on it: an imported device's async
21
- // work is pumped on the instance that owns it (see async::RuntimeContext).
21
+ // work is waited for on the instance that owns it (see
22
+ // async::RuntimeContext).
22
23
  // Both packages must link the same single Dawn copy (they vendor the same
23
24
  // artifact) for the handles they exchange to be meaningful.
24
25
  using GetInstanceFn = WGPUInstance (*)();
@@ -33,17 +33,17 @@ async::AsyncTaskHandle GPU::requestAdapter(
33
33
  aOptions.backendType = kDefaultBackendType;
34
34
 
35
35
  // Per-runtime context: async ops requested on this runtime resolve on this
36
- // runtime's own thread (via its ProcessEvents pump).
36
+ // runtime's own thread (see RuntimeContext).
37
37
  auto context = async::RuntimeContext::getOrCreate(runtime, _instance);
38
38
  return context->postTask(
39
39
  [this, aOptions,
40
40
  context](const async::AsyncTaskHandle::ResolveFunction &resolve,
41
41
  const async::AsyncTaskHandle::RejectFunction &reject) {
42
- _instance.RequestAdapter(
43
- &aOptions, wgpu::CallbackMode::AllowProcessEvents,
44
- [context, resolve,
45
- reject](wgpu::RequestAdapterStatus status, wgpu::Adapter adapter,
46
- wgpu::StringView message) {
42
+ return _instance.RequestAdapter(
43
+ &aOptions, wgpu::CallbackMode::WaitAnyOnly,
44
+ [context, resolve, reject](wgpu::RequestAdapterStatus status,
45
+ wgpu::Adapter adapter,
46
+ wgpu::StringView message) {
47
47
  if (message.length) {
48
48
  fprintf(stderr, "%s", message.data);
49
49
  }
@@ -33,7 +33,7 @@ public:
33
33
  std::string getBrand() { return CLASS_NAME; }
34
34
 
35
35
  // requestAdapter needs the calling runtime so each runtime gets its own
36
- // RuntimeContext (and ProcessEvents pump on its own thread).
36
+ // RuntimeContext, which settles the promise on that runtime's thread.
37
37
  async::AsyncTaskHandle requestAdapter(
38
38
  jsi::Runtime &runtime,
39
39
  std::optional<std::shared_ptr<GPURequestAdapterOptions>> options);
@@ -271,6 +271,32 @@ async::AsyncTaskHandle GPUAdapter::requestDevice(
271
271
  enabledToggles.push_back(name);
272
272
  }
273
273
  }
274
+ #endif
275
+ #if defined(__APPLE__)
276
+ // iOS refuses GPU work from an app in the background, and Dawn loses
277
+ // the device over a refused command buffer. With this toggle Dawn
278
+ // holds the commits while the app is in the background, from
279
+ // UIApplication's own notifications, and commits them when it comes
280
+ // back: nothing is lost, nothing is refused, and a promise on the
281
+ // held work settles once the app is back (the same toggle Skia
282
+ // requests on the device it shares, see react-native-skia's
283
+ // DawnUtils::requestDevice). Dawn applies it on iOS and tvOS only, and
284
+ // a Dawn without it (react-native-webgpu-dawn before 157.1.0) drops
285
+ // the name. The caller opts out through disabledToggles.
286
+ {
287
+ static const char *const kName =
288
+ "metal_suspend_submissions_in_background";
289
+ const auto named = [](const char *t) {
290
+ return std::strcmp(t, kName) == 0;
291
+ };
292
+ const bool explicitlyDisabled = std::any_of(
293
+ disabledToggles.begin(), disabledToggles.end(), named);
294
+ const bool alreadyEnabled =
295
+ std::any_of(enabledToggles.begin(), enabledToggles.end(), named);
296
+ if (!explicitlyDisabled && !alreadyEnabled) {
297
+ enabledToggles.push_back(kName);
298
+ }
299
+ }
274
300
  #endif
275
301
  if (!enabledToggles.empty() || !disabledToggles.empty()) {
276
302
  toggles.enabledToggleCount = enabledToggles.size();
@@ -279,8 +305,8 @@ async::AsyncTaskHandle GPUAdapter::requestDevice(
279
305
  toggles.disabledToggles = disabledToggles.data();
280
306
  deviceDesc.nextInChain = &toggles;
281
307
  }
282
- _instance.RequestDevice(
283
- &deviceDesc, wgpu::CallbackMode::AllowProcessEvents,
308
+ return _instance.RequestDevice(
309
+ &deviceDesc, wgpu::CallbackMode::WaitAnyOnly,
284
310
  [context, resolve, reject, label, creationRuntime,
285
311
  deviceLostBinding](wgpu::RequestDeviceStatus status,
286
312
  wgpu::Device device, wgpu::StringView message) {
@@ -55,18 +55,17 @@ async::AsyncTaskHandle GPUBuffer::mapAsync(jsi::Runtime &runtime,
55
55
  // Post to the CALLING runtime's context, not the one captured at buffer
56
56
  // creation (_async): the buffer may have been created on another runtime and
57
57
  // boxed across (e.g. device created on the main JS runtime, mapAsync called
58
- // from a worklet). The returned Promise lives on the calling runtime, so it
59
- // must be settled from that runtime's own thread — and postTask itself
60
- // schedules the pump through its context's runtime (setTimeout), which is
61
- // only safe for the runtime we are currently executing on.
58
+ // The returned Promise lives on the calling runtime, so it
59
+ // must be settled from that runtime's own thread, which its context knows
60
+ // how to reach (see RuntimeContext).
62
61
  auto context =
63
62
  async::RuntimeContext::getOrCreate(runtime, _async->instance());
64
63
  return context->postTask(
65
64
  [bufferHandle, mode, resolvedOffset,
66
65
  rangeSize](const async::AsyncTaskHandle::ResolveFunction &resolve,
67
66
  const async::AsyncTaskHandle::RejectFunction &reject) {
68
- bufferHandle.MapAsync(
69
- mode, resolvedOffset, rangeSize, wgpu::CallbackMode::AllowProcessEvents,
67
+ return bufferHandle.MapAsync(
68
+ mode, resolvedOffset, rangeSize, wgpu::CallbackMode::WaitAnyOnly,
70
69
  [resolve, reject](wgpu::MapAsyncStatus status,
71
70
  wgpu::StringView message) {
72
71
  switch (status) {
@@ -30,7 +30,8 @@ void GPUCanvasContext::configure(
30
30
  #endif
31
31
  surfaceConfiguration.presentMode = wgpu::PresentMode::Fifo;
32
32
  _surfaceInfo->configure(surfaceConfiguration, std::move(viewFormats),
33
- configuration->colorSpace);
33
+ configuration->colorSpace,
34
+ configuration->toneMappingMode);
34
35
  }
35
36
 
36
37
  void GPUCanvasContext::unconfigure() { _surfaceInfo->unconfigure(); }
@@ -68,41 +68,35 @@ void GPUDevice::notifyDeviceLost(wgpu::DeviceLostReason reason,
68
68
  return;
69
69
  }
70
70
 
71
- // getLost() only registers promises when the device's context has a
72
- // CallInvoker (main JS runtime), so a non-empty list implies an invoker.
73
- auto invoker = _async ? _async->callInvoker() : nullptr;
74
- if (!invoker) {
75
- return;
76
- }
77
-
78
- // Settle on the owning runtime's JS thread. The promises are held weakly:
79
- // if the device graph was collected in the meantime there is nothing to do,
80
- // since nobody could observe the resolution anyway. The shared_ptr is only
81
- // there because jsi::WeakObject is move-only and std::function requires a
82
- // copyable closure; it also ensures the WeakObjects are destroyed on the JS
83
- // thread.
84
- auto pending =
85
- std::make_shared<std::vector<PendingLostPromise>>(std::move(toResolve));
86
- invoker->invokeAsync([pending, info]() {
87
- for (auto &entry : *pending) {
88
- auto &runtime = *entry.runtime;
89
- auto locked = entry.promise.lock(runtime);
71
+ // Settle each promise on the thread of the runtime that read it, through
72
+ // that runtime's context. The promises are held weakly: if the device graph
73
+ // was collected in the meantime there is nothing to do, since nobody could
74
+ // observe the resolution anyway. The shared_ptr is only there because
75
+ // jsi::WeakObject is move-only and std::function requires a copyable
76
+ // closure.
77
+ for (auto &entry : toResolve) {
78
+ auto context = entry.context.lock();
79
+ if (context == nullptr) {
80
+ continue;
81
+ }
82
+ auto pending = std::make_shared<jsi::WeakObject>(std::move(entry.promise));
83
+ context->post([pending, info](jsi::Runtime &runtime) {
84
+ auto locked = pending->lock(runtime);
90
85
  if (!locked.isObject()) {
91
- continue;
86
+ return;
92
87
  }
93
88
  auto promiseObj = locked.getObject(runtime);
94
89
  auto resolveProp = promiseObj.getProperty(runtime, kLostResolveProp);
95
90
  if (!resolveProp.isObject() ||
96
91
  !resolveProp.getObject(runtime).isFunction(runtime)) {
97
- continue;
92
+ return;
98
93
  }
99
94
  auto resolveFn = resolveProp.getObject(runtime).getFunction(runtime);
100
95
  resolveFn.call(runtime,
101
96
  JSIConverter<std::shared_ptr<GPUDeviceLostInfo>>::toJSI(
102
97
  runtime, info));
103
- }
104
- pending->clear();
105
- });
98
+ });
99
+ }
106
100
  }
107
101
 
108
102
  void GPUDevice::forceLossForTesting() {
@@ -534,8 +528,8 @@ async::AsyncTaskHandle GPUDevice::createComputePipelineAsync(
534
528
  const async::AsyncTaskHandle::RejectFunction
535
529
  &reject) {
536
530
  (void)descriptor;
537
- device.CreateComputePipelineAsync(
538
- &desc, wgpu::CallbackMode::AllowProcessEvents,
531
+ return device.CreateComputePipelineAsync(
532
+ &desc, wgpu::CallbackMode::WaitAnyOnly,
539
533
  [pipelineHolder, resolve,
540
534
  reject](wgpu::CreatePipelineAsyncStatus status,
541
535
  wgpu::ComputePipeline pipeline, wgpu::StringView msg) {
@@ -581,8 +575,8 @@ async::AsyncTaskHandle GPUDevice::createRenderPipelineAsync(
581
575
  const async::AsyncTaskHandle::RejectFunction
582
576
  &reject) {
583
577
  (void)descriptor;
584
- device.CreateRenderPipelineAsync(
585
- &desc, wgpu::CallbackMode::AllowProcessEvents,
578
+ return device.CreateRenderPipelineAsync(
579
+ &desc, wgpu::CallbackMode::WaitAnyOnly,
586
580
  [pipelineHolder, resolve,
587
581
  reject](wgpu::CreatePipelineAsyncStatus status,
588
582
  wgpu::RenderPipeline pipeline, wgpu::StringView msg) {
@@ -617,8 +611,8 @@ async::AsyncTaskHandle GPUDevice::popErrorScope(jsi::Runtime &runtime) {
617
611
  &resolve,
618
612
  const async::AsyncTaskHandle::RejectFunction
619
613
  &reject) {
620
- device.PopErrorScope(
621
- wgpu::CallbackMode::AllowProcessEvents,
614
+ return device.PopErrorScope(
615
+ wgpu::CallbackMode::WaitAnyOnly,
622
616
  [resolve, reject](wgpu::PopErrorScopeStatus status,
623
617
  wgpu::ErrorType type, wgpu::StringView message) {
624
618
  if (status == wgpu::PopErrorScopeStatus::Error ||
@@ -731,12 +725,12 @@ jsi::Value GPUDevice::getLost(jsi::Runtime &runtime,
731
725
  *capturedResolve);
732
726
 
733
727
  // Register a WEAK reference so notifyDeviceLost() can settle the promise
734
- // if it is still alive. Only wired for the device's own runtime when a
735
- // CallInvoker exists (main JS runtime): spontaneous events are delivered
736
- // through that invoker. A promise created on another runtime stays
737
- // pending, matching the previous best-effort behavior.
728
+ // if it is still alive, on the thread of the runtime that read it,
729
+ // through that runtime's context (see RuntimeContext::post).
738
730
  bool settledMeanwhile = false;
739
- if (_async && _async->callInvoker() && &runtime == &_async->runtime()) {
731
+ if (_async) {
732
+ auto context =
733
+ async::RuntimeContext::getOrCreate(runtime, _async->instance());
740
734
  std::lock_guard<std::mutex> lock(_lostMutex);
741
735
  if (_lostSettled) {
742
736
  // The device was lost between the check above and now.
@@ -744,7 +738,7 @@ jsi::Value GPUDevice::getLost(jsi::Runtime &runtime,
744
738
  settledInfo = _lostInfo;
745
739
  } else {
746
740
  _lostPromises.push_back(
747
- PendingLostPromise{&runtime, jsi::WeakObject(runtime, promiseObj)});
741
+ PendingLostPromise{jsi::WeakObject(runtime, promiseObj), context});
748
742
  }
749
743
  }
750
744
  if (settledMeanwhile) {
@@ -775,20 +769,17 @@ void GPUDevice::removeEventListener(std::string type, jsi::Function callback) {
775
769
 
776
770
  void GPUDevice::notifyUncapturedError(wgpu::ErrorType type,
777
771
  std::string message) {
778
- // Dawn can surface an uncaptured error from any ProcessEvents pump (a worklet
779
- // runtime sharing this instance may pump it on the wrong thread). Marshal to
780
- // the owning runtime's JS thread via its CallInvoker before touching JSI. The
781
- // invoker is wired only for the main JS runtime, so a device created on a
782
- // worklet runtime does not deliver uncaptured errors to JS (best-effort; see
783
- // README "Threading model").
784
- auto invoker = _async ? _async->callInvoker() : nullptr;
785
- if (!invoker) {
772
+ // Dawn reports an uncaptured error from whichever thread detects it. The
773
+ // listeners run on the thread of the runtime the device was created on,
774
+ // through its context (see RuntimeContext::post).
775
+ if (!_async) {
786
776
  return;
787
777
  }
788
778
  auto self = shared_from_this();
789
- invoker->invokeAsync([self, type, message = std::move(message)]() mutable {
790
- self->deliverUncapturedError(type, std::move(message));
791
- });
779
+ _async->post(
780
+ [self, type, message = std::move(message)](jsi::Runtime &) mutable {
781
+ self->deliverUncapturedError(type, std::move(message));
782
+ });
792
783
  }
793
784
 
794
785
  void GPUDevice::deliverUncapturedError(wgpu::ErrorType type,
@@ -302,7 +302,7 @@ private:
302
302
  friend class GPUAdapter;
303
303
 
304
304
  // Runs the uncapturederror listeners on the creation runtime's JS thread.
305
- // Invoked from notifyUncapturedError via the main CallInvoker.
305
+ // Invoked from notifyUncapturedError through the runtime's context.
306
306
  void deliverUncapturedError(wgpu::ErrorType type, std::string message);
307
307
 
308
308
  wgpu::Device _instance;
@@ -327,12 +327,11 @@ private:
327
327
  // resolve function lives as a hidden property on the promise object itself,
328
328
  // so if the promise is still alive when the device is lost we can settle
329
329
  // it; if it was collected, nobody could have observed the resolution.
330
- // Entries are only added for the device's own runtime when a CallInvoker is
331
- // available (main JS runtime), matching the best-effort contract for
332
- // spontaneous events.
330
+ // Each entry is settled on the thread of the runtime that read `lost`,
331
+ // through that runtime's context; a runtime gone since is skipped.
333
332
  struct PendingLostPromise {
334
- jsi::Runtime *runtime;
335
333
  jsi::WeakObject promise;
334
+ std::weak_ptr<async::RuntimeContext> context;
336
335
  };
337
336
  std::vector<PendingLostPromise> _lostPromises;
338
337
 
@@ -94,8 +94,8 @@ async::AsyncTaskHandle GPUQueue::onSubmittedWorkDone(jsi::Runtime &runtime) {
94
94
  return context->postTask(
95
95
  [queue](const async::AsyncTaskHandle::ResolveFunction &resolve,
96
96
  const async::AsyncTaskHandle::RejectFunction &reject) {
97
- queue.OnSubmittedWorkDone(
98
- wgpu::CallbackMode::AllowProcessEvents,
97
+ return queue.OnSubmittedWorkDone(
98
+ wgpu::CallbackMode::WaitAnyOnly,
99
99
  [resolve, reject](wgpu::QueueWorkDoneStatus status,
100
100
  wgpu::StringView message) {
101
101
  if (status == wgpu::QueueWorkDoneStatus::Success) {
@@ -531,6 +531,8 @@ async::AsyncTaskHandle GPUQueue::drawElementImageToTexture(
531
531
  });
532
532
  },
533
533
  [reject](std::string error) { reject(std::move(error)); });
534
+ // Not Dawn's work: nothing to wait for, the snapshot calls back.
535
+ return std::nullopt;
534
536
  });
535
537
  }
536
538
 
@@ -19,8 +19,8 @@ GPUShaderModule::getCompilationInfo(jsi::Runtime &runtime) {
19
19
  [module](const async::AsyncTaskHandle::ResolveFunction &resolve,
20
20
  const async::AsyncTaskHandle::RejectFunction &reject) {
21
21
  auto result = std::make_shared<GPUCompilationInfo>();
22
- module.GetCompilationInfo(
23
- wgpu::CallbackMode::AllowProcessEvents,
22
+ return module.GetCompilationInfo(
23
+ wgpu::CallbackMode::WaitAnyOnly,
24
24
  [result, resolve,
25
25
  reject](wgpu::CompilationInfoRequestStatus status,
26
26
  const wgpu::CompilationInfo *compilationInfo) {