react-native-webgpu 0.17.0 → 0.19.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 (43) hide show
  1. package/android/cpp/AndroidPlatformContext.h +8 -0
  2. package/apple/ApplePlatformContext.h +2 -0
  3. package/apple/ApplePlatformContext.mm +12 -0
  4. package/apple/MetalLayerColorSpace.mm +20 -8
  5. package/cpp/rnwgpu/CanvasColorSpace.h +9 -0
  6. package/cpp/rnwgpu/PlatformContext.h +6 -0
  7. package/cpp/rnwgpu/RNWebGPUManager.cpp +3 -3
  8. package/cpp/rnwgpu/SurfaceRegistry.h +101 -30
  9. package/cpp/rnwgpu/WebGPURuntime.cpp +30 -11
  10. package/cpp/rnwgpu/WebGPURuntime.h +7 -2
  11. package/cpp/rnwgpu/api/GPUCanvasContext.cpp +2 -1
  12. package/cpp/rnwgpu/api/GPUCanvasContext.h +10 -0
  13. package/cpp/rnwgpu/api/RNWebGPU.h +63 -5
  14. package/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h +18 -0
  15. package/cpp/rnwgpu/async/RuntimeContext.cpp +43 -27
  16. package/cpp/rnwgpu/async/RuntimeContext.h +19 -11
  17. package/lib/commonjs/Canvas.js +3 -4
  18. package/lib/commonjs/Canvas.js.map +1 -1
  19. package/lib/commonjs/WebPolyfillGPUModule.js +2 -0
  20. package/lib/commonjs/WebPolyfillGPUModule.js.map +1 -1
  21. package/lib/commonjs/importDevice.js +3 -2
  22. package/lib/commonjs/importDevice.js.map +1 -1
  23. package/lib/module/Canvas.js +3 -4
  24. package/lib/module/Canvas.js.map +1 -1
  25. package/lib/module/WebPolyfillGPUModule.js +2 -0
  26. package/lib/module/WebPolyfillGPUModule.js.map +1 -1
  27. package/lib/module/importDevice.js +3 -2
  28. package/lib/module/importDevice.js.map +1 -1
  29. package/lib/typescript/lib/commonjs/Canvas.d.ts.map +1 -1
  30. package/lib/typescript/lib/commonjs/importDevice.d.ts +3 -2
  31. package/lib/typescript/lib/commonjs/importDevice.d.ts.map +1 -1
  32. package/lib/typescript/lib/module/Canvas.d.ts.map +1 -1
  33. package/lib/typescript/lib/module/importDevice.d.ts.map +1 -1
  34. package/lib/typescript/src/Canvas.d.ts.map +1 -1
  35. package/lib/typescript/src/importDevice.d.ts +3 -2
  36. package/lib/typescript/src/importDevice.d.ts.map +1 -1
  37. package/lib/typescript/src/index.d.ts +2 -0
  38. package/lib/typescript/src/index.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/Canvas.tsx +3 -4
  41. package/src/WebPolyfillGPUModule.ts +3 -0
  42. package/src/importDevice.ts +3 -2
  43. package/src/index.tsx +12 -1
@@ -3,6 +3,7 @@
3
3
  #include <android/bitmap.h>
4
4
  #include <android/data_space.h>
5
5
  #include <android/hardware_buffer.h>
6
+ #include <android/native_window.h>
6
7
  #include <dlfcn.h>
7
8
  #include <jni.h>
8
9
 
@@ -469,6 +470,13 @@ public:
469
470
  "writeTestVideoFile is not yet implemented on Android.");
470
471
  }
471
472
 
473
+ std::function<void(void *)> retainNativeWindow(void *window) override {
474
+ ANativeWindow_acquire(static_cast<ANativeWindow *>(window));
475
+ return [](void *nativeWindow) {
476
+ ANativeWindow_release(static_cast<ANativeWindow *>(nativeWindow));
477
+ };
478
+ }
479
+
472
480
  VideoFrameHandle wrapNativeBuffer(void *pointer) override {
473
481
  if (!pointer) {
474
482
  throw std::runtime_error("wrapNativeBuffer: pointer is null");
@@ -48,6 +48,8 @@ public:
48
48
 
49
49
  VideoFrameHandle wrapNativeBuffer(void *pointer) override;
50
50
 
51
+ std::function<void(void *)> retainNativeWindow(void *window) override;
52
+
51
53
  private:
52
54
  ViewLookup _viewLookup;
53
55
  };
@@ -556,6 +556,18 @@ VideoFrameHandle ApplePlatformContext::wrapNativeBuffer(void *pointer) {
556
556
  return wrapCVPixelBuffer(static_cast<CVPixelBufferRef>(pointer));
557
557
  }
558
558
 
559
+ std::function<void(void *)>
560
+ ApplePlatformContext::retainNativeWindow(void *window) {
561
+ CFRetain(window);
562
+ return [](void *layer) {
563
+ // As MetalView's: the releaser can run on the rendering thread, and
564
+ // CALayer teardown belongs on the main thread.
565
+ dispatch_async(dispatch_get_main_queue(), ^{
566
+ CFRelease(layer);
567
+ });
568
+ };
569
+ }
570
+
559
571
  // The NV12 variant of createTestVideoFrame: a biplanar BT.709 limited-range
560
572
  // frame, red on the left half and green on the right, whose attachments
561
573
  // declare its color space the way the decoder's frames do, so that the
@@ -3,32 +3,44 @@
3
3
  #import <QuartzCore/CAMetalLayer.h>
4
4
  #import <QuartzCore/CATransaction.h>
5
5
 
6
+ #include "CanvasColorSpace.h"
6
7
  #include "webgpu/webgpu_cpp.h"
7
8
 
8
9
  namespace rnwgpu {
9
10
 
10
- // WebGPU canvas values are sRGB-encoded regardless of the texture format
11
- // (GPUCanvasConfiguration.colorSpace defaults to "srgb"), so the same shader
11
+ // WebGPU canvas values are encoded with the sRGB transfer function regardless
12
+ // of the texture format, and GPUCanvasConfiguration.colorSpace says which
13
+ // primaries they are in: "srgb" (the default) or "display-p3". The same shader
12
14
  // output must display identically on bgra8unorm and rgba16float surfaces.
13
15
  // Core Animation interprets a float-format layer with a nil colorspace as
14
16
  // extended *linear* sRGB, which displays the same values noticeably brighter.
15
17
  // Tagging the layer as (gamma-encoded) extended sRGB matches the browser
16
- // behavior: identical colors, with the extra precision of float16.
18
+ // behavior: identical colors, with the extra precision of float16. For
19
+ // "display-p3" the layer is tagged Display P3 instead, or its extended range
20
+ // variant on a float format, so the values reach a wide-gamut display without
21
+ // being converted from sRGB first.
17
22
  void applyCAMetalLayerColorSpace(void *nativeSurface,
18
- wgpu::TextureFormat format) {
23
+ wgpu::TextureFormat format,
24
+ CanvasColorSpace canvasColorSpace) {
19
25
  CALayer *layer = (__bridge CALayer *)nativeSurface;
20
26
  if (![layer isKindOfClass:[CAMetalLayer class]]) {
21
27
  return;
22
28
  }
23
29
  auto metalLayer = static_cast<CAMetalLayer *>(layer);
24
- if (format == wgpu::TextureFormat::RGBA16Float) {
25
- CGColorSpaceRef colorSpace =
26
- CGColorSpaceCreateWithName(kCGColorSpaceExtendedSRGB);
30
+ const bool isFloat = format == wgpu::TextureFormat::RGBA16Float;
31
+ CFStringRef name = nullptr;
32
+ if (canvasColorSpace == CanvasColorSpace::DisplayP3) {
33
+ name = isFloat ? kCGColorSpaceExtendedDisplayP3 : kCGColorSpaceDisplayP3;
34
+ } else if (isFloat) {
35
+ name = kCGColorSpaceExtendedSRGB;
36
+ }
37
+ if (name != nullptr) {
38
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(name);
27
39
  metalLayer.colorspace = colorSpace;
28
40
  CGColorSpaceRelease(colorSpace);
29
41
  } else if (metalLayer.colorspace != nil) {
30
42
  // Restore the default (no color matching) when reconfiguring back to an
31
- // 8-bit format.
43
+ // 8-bit sRGB canvas.
32
44
  metalLayer.colorspace = nil;
33
45
  }
34
46
  // The change must be set synchronously so the first present already sees
@@ -0,0 +1,9 @@
1
+ #pragma once
2
+
3
+ namespace rnwgpu {
4
+
5
+ // GPUCanvasConfiguration.colorSpace (PredefinedColorSpace): how the canvas's
6
+ // values are interpreted when it is composited.
7
+ enum class CanvasColorSpace { SRGB, DisplayP3 };
8
+
9
+ } // namespace rnwgpu
@@ -243,6 +243,12 @@ public:
243
243
  // safely release their own reference immediately after.
244
244
  virtual VideoFrameHandle wrapNativeBuffer(void *pointer) = 0;
245
245
 
246
+ // Retain a native window obtained from another library, to present to it
247
+ // (an ANativeWindow* on Android, a CAMetalLayer* on Apple platforms), and
248
+ // return the releaser that balances that reference once the surface is done
249
+ // with it. The releaser can run on any thread.
250
+ virtual std::function<void(void *)> retainNativeWindow(void *window) = 0;
251
+
246
252
  // Write a small procedurally-generated test video to a temporary location
247
253
  // and return its absolute path. Lets the SharedTextureMemory example play
248
254
  // a real decoded video without bundling an asset.
@@ -167,9 +167,9 @@ void RNWebGPUManager::flushPendingSurfaceTransition(
167
167
  }
168
168
 
169
169
  RNWebGPUManager::~RNWebGPUManager() {
170
- // Drop all canvas registry entries: after a reload the JS side restarts its
171
- // contextId counter, and stale entries would alias new canvases onto dead
172
- // surfaces.
170
+ // Drop the canvas registry entries of this runtime's bundle: its canvases
171
+ // never run their JS cleanup, and their ids are not handed out again (see
172
+ // SurfaceRegistry::allocateContextId).
173
173
  SurfaceRegistry::getInstance().clear();
174
174
  if (PlatformContext::current() == _platformContext) {
175
175
  PlatformContext::setCurrent(nullptr);
@@ -19,6 +19,9 @@
19
19
 
20
20
  #include "webgpu/webgpu_cpp.h"
21
21
 
22
+ #include "CanvasColorSpace.h"
23
+ #include "WebGPURuntime.h"
24
+
22
25
  #if defined(__ANDROID__)
23
26
  #include <android/hardware_buffer.h>
24
27
  #include <android/log.h>
@@ -36,9 +39,11 @@ namespace rnwgpu {
36
39
 
37
40
  #ifdef __APPLE__
38
41
  // Tags the CAMetalLayer with the colorspace matching the configured texture
39
- // format. Implemented in apple/MetalLayerColorSpace.mm.
42
+ // format and the canvas's colorSpace. Implemented in
43
+ // apple/MetalLayerColorSpace.mm.
40
44
  void applyCAMetalLayerColorSpace(void *nativeSurface,
41
- wgpu::TextureFormat format);
45
+ wgpu::TextureFormat format,
46
+ CanvasColorSpace colorSpace);
42
47
  #endif
43
48
 
44
49
  struct NativeInfo {
@@ -123,12 +128,13 @@ struct AHBPool {
123
128
  // Ownership & threading model:
124
129
  // - A registry entry is created on first use (by whichever of JS/native gets
125
130
  // there first) and lives exactly as long as its JS Canvas: contextIds are
126
- // never reused. It is removed by the native view's teardown (MetalView
127
- // dealloc / WebGPUViewManager.onDropViewInstance) when a surface is
128
- // attached, or by RNWebGPU.destroyContext (the Canvas unmount cleanup) when
129
- // none ever was — see RNWebGPU::destroyContext for why the split. Surface
130
- // destruction alone (backgrounding, TextureView teardown) never removes an
131
- // entry; it only detaches the surface.
131
+ // never reused, not even across a reload (see allocateContextId). It is
132
+ // removed by the native view's teardown (MetalView dealloc /
133
+ // WebGPUViewManager.onDropViewInstance) when a surface is attached, or by
134
+ // RNWebGPU.destroyContext (the Canvas unmount cleanup) when none ever was —
135
+ // see RNWebGPU::destroyContext for why the split. Surface destruction alone
136
+ // (backgrounding, TextureView teardown) never removes an entry; it only
137
+ // detaches the surface.
132
138
  // - Attaching a surface is LATCHED: the UI thread stores it as pending
133
139
  // (attachSurface) and it is adopted at the next frame boundary — start of
134
140
  // getCurrentTexture or end of presentFrame — on whichever thread renders
@@ -147,8 +153,20 @@ struct AHBPool {
147
153
  // native surface exists.
148
154
  class SurfaceInfo {
149
155
  public:
150
- SurfaceInfo(wgpu::Instance gpu, int width, int height)
151
- : _gpu(std::move(gpu)), _width(width), _height(height) {}
156
+ // releaseSurfaceOnUnconfigure is for a surface on a window the caller
157
+ // handed in (see SurfaceRegistry::makeUnregisteredSurfaceInfo), which no
158
+ // view attaches or detaches: unconfigure() then destroys the Dawn surface
159
+ // rather than only unconfiguring it, and configure() makes a new one for
160
+ // the same window. On Android, a window keeps the buffers it handed to the
161
+ // swapchain until the VkSurfaceKHR is destroyed, which Surface::Unconfigure
162
+ // does not do (it parks the swapchain for the next Configure): a MediaCodec
163
+ // input surface would otherwise keep them until the context is garbage
164
+ // collected. The window itself is retained until the SurfaceInfo is
165
+ // destroyed.
166
+ SurfaceInfo(wgpu::Instance gpu, int width, int height,
167
+ bool releaseSurfaceOnUnconfigure = false)
168
+ : _gpu(std::move(gpu)), _width(width), _height(height),
169
+ _releaseSurfaceOnUnconfigure(releaseSurfaceOnUnconfigure) {}
152
170
 
153
171
  ~SurfaceInfo() {
154
172
  #if defined(__ANDROID__)
@@ -279,7 +297,7 @@ public:
279
297
  }
280
298
  configureSurfaceLocked(config);
281
299
  #ifdef __APPLE__
282
- applyCAMetalLayerColorSpace(_nativeSurface, _config.format);
300
+ applyCAMetalLayerColorSpace(_nativeSurface, _config.format, _colorSpace);
283
301
  #endif
284
302
  if (blit) {
285
303
  presentBlit = blitOffscreenToSurfaceLocked();
@@ -316,10 +334,12 @@ public:
316
334
  // -------------------------------------------------------
317
335
 
318
336
  void configure(wgpu::SurfaceConfiguration &newConfig,
319
- std::vector<wgpu::TextureFormat> viewFormats) {
337
+ std::vector<wgpu::TextureFormat> viewFormats,
338
+ CanvasColorSpace colorSpace = CanvasColorSpace::SRGB) {
320
339
  applyPendingAttach(/* supersedeInFlightFrame = */ true);
321
340
  std::unique_lock<std::shared_mutex> lock(_mutex);
322
341
  _viewFormats = std::move(viewFormats);
342
+ _colorSpace = colorSpace;
323
343
  _config = newConfig;
324
344
  // The caller's viewFormats storage dies with the call; point the stored
325
345
  // configuration at our own copy.
@@ -346,6 +366,10 @@ public:
346
366
  _poolUnsupported = false;
347
367
  }
348
368
  #endif
369
+ if (_releaseSurfaceOnUnconfigure && !_surface && _nativeSurface) {
370
+ // unconfigure() destroyed the surface; make a new one for the window.
371
+ recreateSurfaceLocked();
372
+ }
349
373
  _configureLocked();
350
374
  }
351
375
 
@@ -365,10 +389,22 @@ public:
365
389
  void unconfigure() {
366
390
  std::unique_lock<std::shared_mutex> lock(_mutex);
367
391
  if (_surface) {
368
- _surface.Unconfigure();
392
+ if (_releaseSurfaceOnUnconfigure) {
393
+ // Destroy the surface, and so its swapchain, while their device is
394
+ // still alive. The window is kept: configure() makes a new surface
395
+ // for it (see the constructor).
396
+ _surface = nullptr;
397
+ #if defined(__ANDROID__)
398
+ // DAWN_WORKAROUND_DEVICE_DESTROY_BEFORE_SURFACE_RELEASE
399
+ _surfaceDevice = nullptr;
400
+ #endif
401
+ } else {
402
+ _surface.Unconfigure();
403
+ }
369
404
  }
370
405
  _texture = nullptr;
371
406
  _config = {};
407
+ _colorSpace = CanvasColorSpace::SRGB;
372
408
  _viewFormats.clear();
373
409
  _acquiredFromSurface = false;
374
410
  _frameEpoch++;
@@ -875,7 +911,7 @@ private:
875
911
  if (_surface) {
876
912
  configureSurfaceLocked(_config);
877
913
  #ifdef __APPLE__
878
- applyCAMetalLayerColorSpace(_nativeSurface, _config.format);
914
+ applyCAMetalLayerColorSpace(_nativeSurface, _config.format, _colorSpace);
879
915
  #endif
880
916
  } else {
881
917
  _texture = createOffscreenTextureLocked();
@@ -906,26 +942,24 @@ private:
906
942
  #endif
907
943
  }
908
944
 
909
- #if defined(__ANDROID__)
910
- // DAWN_WORKAROUND_DEVICE_DESTROY_BEFORE_SURFACE_RELEASE
911
945
  // Replace _surface with a new, unconfigured surface for the same native
912
- // window. Dropping the old one runs ~Surface, which detaches both the
913
- // active and the recycled swapchain against their still-alive device.
946
+ // window: after unconfigure() destroyed it (see the constructor), or on
947
+ // Android for DAWN_WORKAROUND_DEVICE_DESTROY_BEFORE_SURFACE_RELEASE, where
948
+ // dropping the old one runs ~Surface, which detaches both the active and
949
+ // the recycled swapchain against their still-alive device.
914
950
  void recreateSurfaceLocked() {
915
951
  _surface = nullptr;
952
+ #if defined(__ANDROID__)
953
+ // DAWN_WORKAROUND_DEVICE_DESTROY_BEFORE_SURFACE_RELEASE
916
954
  _surfaceDevice = nullptr;
955
+ #endif
917
956
  // A frame acquired from the old surface can no longer be presented.
918
957
  _acquiredFromSurface = false;
919
958
  _frameEpoch++;
920
959
  if (_nativeSurface) {
921
- wgpu::SurfaceSourceAndroidNativeWindow source;
922
- source.window = _nativeSurface;
923
- wgpu::SurfaceDescriptor descriptor;
924
- descriptor.nextInChain = &source;
925
- _surface = _gpu.CreateSurface(&descriptor);
960
+ _surface = WebGPURuntime::makeSurface(_gpu, _nativeSurface);
926
961
  }
927
962
  }
928
- #endif
929
963
 
930
964
  #if defined(__ANDROID__)
931
965
  // Drop every pool generation and the ready / in-flight slot bookkeeping.
@@ -1241,12 +1275,15 @@ private:
1241
1275
  // that _config.viewFormats points at.
1242
1276
  wgpu::SurfaceConfiguration _config;
1243
1277
  std::vector<wgpu::TextureFormat> _viewFormats;
1278
+ CanvasColorSpace _colorSpace = CanvasColorSpace::SRGB;
1244
1279
  // Keeps the Dawn instance alive for as long as any canvas exists.
1245
1280
  wgpu::Instance _gpu;
1246
1281
  // Native view size in dp (surfaced as clientWidth/clientHeight on the JS
1247
1282
  // canvas).
1248
1283
  int _width;
1249
1284
  int _height;
1285
+ // See the constructor.
1286
+ bool _releaseSurfaceOnUnconfigure = false;
1250
1287
 
1251
1288
  #if defined(__ANDROID__)
1252
1289
  // Pool state. Guarded by _poolMutex (never nested under _mutex while
@@ -1296,6 +1333,15 @@ public:
1296
1333
  SurfaceRegistry(const SurfaceRegistry &) = delete;
1297
1334
  SurfaceRegistry &operator=(const SurfaceRegistry &) = delete;
1298
1335
 
1336
+ // The id of a Canvas about to mount. Ids are handed out here, once for the
1337
+ // life of the process, rather than counted by each JS runtime from the
1338
+ // same start: this registry outlives a reload, and the views of the bundle
1339
+ // being replaced are torn down (removeSurfaceInfo) on the UI thread with
1340
+ // no ordering against the new bundle's first canvases. An id counted from
1341
+ // the same start as its predecessor would let the predecessor's teardown
1342
+ // detach and erase the entry of a live canvas.
1343
+ int allocateContextId() { return _nextContextId.fetch_add(1); }
1344
+
1299
1345
  std::shared_ptr<SurfaceInfo> getSurfaceInfo(int id) {
1300
1346
  std::shared_lock<std::shared_mutex> lock(_mutex);
1301
1347
  auto it = _registry.find(id);
@@ -1357,6 +1403,19 @@ public:
1357
1403
  _registry.erase(it);
1358
1404
  }
1359
1405
 
1406
+ // A SurfaceInfo that is not in the registry, for a context presenting to a
1407
+ // native window of the caller's (see
1408
+ // RNWebGPU::MakeWebGPUCanvasContextFromNativeWindow): the context owns it,
1409
+ // no view attaches to it, and it releases its surface on unconfigure (see
1410
+ // the SurfaceInfo constructor). It is still tracked for
1411
+ // releaseSurfacesForDevice.
1412
+ std::shared_ptr<SurfaceInfo>
1413
+ makeUnregisteredSurfaceInfo(wgpu::Instance gpu, int width, int height) {
1414
+ std::unique_lock<std::shared_mutex> lock(_mutex);
1415
+ return makeSurfaceInfoLocked(std::move(gpu), width, height,
1416
+ /* releaseSurfaceOnUnconfigure = */ true);
1417
+ }
1418
+
1360
1419
  // DAWN_WORKAROUND_DEVICE_DESTROY_BEFORE_SURFACE_RELEASE
1361
1420
  // See SurfaceInfo::releaseSurfaceForDevice. Lock order is registry ->
1362
1421
  // SurfaceInfo, matching every other path.
@@ -1382,8 +1441,9 @@ public:
1382
1441
  }
1383
1442
 
1384
1443
  // Drops all entries. Called when the RN instance tears down (dev reload):
1385
- // JS context ids restart from scratch, so surviving entries would alias new
1386
- // canvases onto dead surfaces.
1444
+ // the canvases of the old bundle never run their JS cleanup, and nothing
1445
+ // reaches their ids again (see allocateContextId), so the entries would
1446
+ // otherwise stay for the life of the process.
1387
1447
  void clear() {
1388
1448
  std::unique_lock<std::shared_mutex> lock(_mutex);
1389
1449
  _registry.clear();
@@ -1410,15 +1470,25 @@ private:
1410
1470
  _retired.erase(retired);
1411
1471
  }
1412
1472
  if (!info) {
1413
- info = std::make_shared<SurfaceInfo>(gpu, width, height);
1414
- // DAWN_WORKAROUND_DEVICE_DESTROY_BEFORE_SURFACE_RELEASE
1415
- pruneAllSurfacesLocked();
1416
- _allSurfaces.push_back(info);
1473
+ info = makeSurfaceInfoLocked(std::move(gpu), width, height);
1417
1474
  }
1418
1475
  _registry[id] = info;
1419
1476
  return info;
1420
1477
  }
1421
1478
 
1479
+ // Every SurfaceInfo is made here, so releaseSurfacesForDevice reaches it
1480
+ // whether or not it is in _registry.
1481
+ std::shared_ptr<SurfaceInfo>
1482
+ makeSurfaceInfoLocked(wgpu::Instance gpu, int width, int height,
1483
+ bool releaseSurfaceOnUnconfigure = false) {
1484
+ auto info = std::make_shared<SurfaceInfo>(std::move(gpu), width, height,
1485
+ releaseSurfaceOnUnconfigure);
1486
+ // DAWN_WORKAROUND_DEVICE_DESTROY_BEFORE_SURFACE_RELEASE
1487
+ pruneAllSurfacesLocked();
1488
+ _allSurfaces.push_back(info);
1489
+ return info;
1490
+ }
1491
+
1422
1492
  void pruneRetiredLocked() {
1423
1493
  for (auto it = _retired.begin(); it != _retired.end();) {
1424
1494
  it = it->second.expired() ? _retired.erase(it) : ++it;
@@ -1436,6 +1506,7 @@ private:
1436
1506
  }
1437
1507
 
1438
1508
  mutable std::shared_mutex _mutex;
1509
+ std::atomic<int> _nextContextId{1};
1439
1510
  std::unordered_map<int, std::shared_ptr<SurfaceInfo>> _registry;
1440
1511
  // The entries JS retired while a GPUCanvasContext still held their
1441
1512
  // SurfaceInfo (see removeSurfaceInfoIfDetached). Non-owning; pruned on
@@ -9,18 +9,32 @@ namespace rnwgpu {
9
9
  namespace {
10
10
 
11
11
  wgpu::Instance createInstance() {
12
- // If a Graphite build of react-native-skia is loaded in this
12
+ // If a Graphite build of react-native-skia is already loaded in this
13
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.
14
+ // Graphite's device and every JS-created device live on one instance.
15
+ // Discovered at runtime via dlsym so there is no build-time coupling. The
16
+ // adoption is best-effort and decided once, when the instance is first
17
+ // used: on Android it only succeeds when librnskia.so is loaded before
18
+ // react-native-webgpu installs (see the RTLD_NOLOAD lookup below), so the
19
+ // native-module init order decides whether the instance is shared.
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).
22
+ // Both packages must link the same single Dawn copy (they vendor the same
23
+ // artifact) for the handles they exchange to be meaningful.
21
24
  using GetInstanceFn = WGPUInstance (*)();
22
- if (auto getSkiaInstance = reinterpret_cast<GetInstanceFn>(
23
- dlsym(RTLD_DEFAULT, "rnskia_getWGPUInstance"))) {
25
+ void *getter = dlsym(RTLD_DEFAULT, "rnskia_getWGPUInstance");
26
+ #ifdef __ANDROID__
27
+ // System.loadLibrary loads librnskia.so with RTLD_LOCAL, so RTLD_DEFAULT
28
+ // does not search it. RTLD_NOLOAD returns its handle only if it is already
29
+ // loaded, so this never loads Skia.
30
+ if (getter == nullptr) {
31
+ if (void *skia = dlopen("librnskia.so", RTLD_NOW | RTLD_NOLOAD)) {
32
+ getter = dlsym(skia, "rnskia_getWGPUInstance");
33
+ dlclose(skia);
34
+ }
35
+ }
36
+ #endif
37
+ if (auto getSkiaInstance = reinterpret_cast<GetInstanceFn>(getter)) {
24
38
  if (WGPUInstance external = getSkiaInstance()) {
25
39
  wgpuInstanceAddRef(external);
26
40
  return wgpu::Instance::Acquire(external);
@@ -76,6 +90,11 @@ wgpu::Instance WebGPURuntime::instance() {
76
90
  }
77
91
 
78
92
  wgpu::Surface WebGPURuntime::makeSurface(void *nativeSurface) {
93
+ return makeSurface(instance(), nativeSurface);
94
+ }
95
+
96
+ wgpu::Surface WebGPURuntime::makeSurface(const wgpu::Instance &gpu,
97
+ void *nativeSurface) {
79
98
  #if defined(__ANDROID__)
80
99
  wgpu::SurfaceSourceAndroidNativeWindow source;
81
100
  source.window = nativeSurface;
@@ -85,7 +104,7 @@ wgpu::Surface WebGPURuntime::makeSurface(void *nativeSurface) {
85
104
  #endif
86
105
  wgpu::SurfaceDescriptor descriptor;
87
106
  descriptor.nextInChain = &source;
88
- return instance().CreateSurface(&descriptor);
107
+ return gpu.CreateSurface(&descriptor);
89
108
  }
90
109
 
91
110
  } // namespace rnwgpu
@@ -15,14 +15,19 @@ class WebGPURuntime {
15
15
  public:
16
16
  static WebGPURuntime &shared();
17
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.
18
+ // The process-wide instance: Skia Graphite's when that is already loaded on
19
+ // first use, otherwise our own. Safe to call from any thread.
20
20
  wgpu::Instance instance();
21
21
 
22
22
  // Creates a surface on the shared instance for a CAMetalLayer (Apple) or an
23
23
  // ANativeWindow (Android). Safe to call from any thread.
24
24
  wgpu::Surface makeSurface(void *nativeSurface);
25
25
 
26
+ // The same on a given instance: a SurfaceInfo recreates its surface on the
27
+ // instance it was made with.
28
+ static wgpu::Surface makeSurface(const wgpu::Instance &gpu,
29
+ void *nativeSurface);
30
+
26
31
  WebGPURuntime(const WebGPURuntime &) = delete;
27
32
  WebGPURuntime &operator=(const WebGPURuntime &) = delete;
28
33
 
@@ -29,7 +29,8 @@ void GPUCanvasContext::configure(
29
29
  surfaceConfiguration.alphaMode = configuration->alphaMode;
30
30
  #endif
31
31
  surfaceConfiguration.presentMode = wgpu::PresentMode::Fifo;
32
- _surfaceInfo->configure(surfaceConfiguration, std::move(viewFormats));
32
+ _surfaceInfo->configure(surfaceConfiguration, std::move(viewFormats),
33
+ configuration->colorSpace);
33
34
  }
34
35
 
35
36
  void GPUCanvasContext::unconfigure() { _surfaceInfo->unconfigure(); }
@@ -35,6 +35,16 @@ public:
35
35
  registry.getSurfaceInfoOrCreate(contextId, _gpu->get(), width, height);
36
36
  }
37
37
 
38
+ // A context presenting to a surface of its own rather than a view's, outside
39
+ // of the registry (see RNWebGPU::MakeWebGPUCanvasContextFromNativeWindow).
40
+ GPUCanvasContext(std::shared_ptr<GPU> gpu,
41
+ std::shared_ptr<SurfaceInfo> surfaceInfo, int width,
42
+ int height)
43
+ : NativeObject(CLASS_NAME), _surfaceInfo(std::move(surfaceInfo)),
44
+ _gpu(std::move(gpu)) {
45
+ _canvas = std::make_shared<Canvas>(nullptr, width, height);
46
+ }
47
+
38
48
  public:
39
49
  std::string getBrand() { return CLASS_NAME; }
40
50
 
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #include <cmath>
3
4
  #include <memory>
4
5
  #include <string>
5
6
  #include <utility>
@@ -18,6 +19,7 @@
18
19
  #include "PlatformContext.h"
19
20
  #include "VideoFrame.h"
20
21
  #include "VideoPlayer.h"
22
+ #include "WebGPURuntime.h"
21
23
 
22
24
  #include <ReactCommon/CallInvoker.h>
23
25
 
@@ -83,13 +85,58 @@ public:
83
85
  return ctx;
84
86
  }
85
87
 
88
+ // A canvas context presenting to a native window that is not a WebGPU
89
+ // view, e.g. the input Surface of a MediaCodec encoder: an ANativeWindow*
90
+ // (Android) or a CAMetalLayer* (Apple), passed as a BigInt. Unlike a view's,
91
+ // the context owns its surface, outside of the SurfaceRegistry, and the
92
+ // caller drives it from one thread at a time. The context retains the
93
+ // window for as long as it lives; unconfigure() releases the surface (and
94
+ // on Android the buffers the window handed to it) and configure() makes a
95
+ // new one for the window, see the SurfaceInfo constructor.
96
+ std::shared_ptr<GPUCanvasContext>
97
+ MakeWebGPUCanvasContextFromNativeWindow(void *window, float width,
98
+ float height) {
99
+ if (window == nullptr) {
100
+ throw std::runtime_error(
101
+ "MakeWebGPUCanvasContextFromNativeWindow: expected a non-null "
102
+ "ANativeWindow (Android) or CAMetalLayer (Apple) pointer (BigInt)");
103
+ }
104
+ if (!(std::isfinite(width) && std::isfinite(height) && width > 0 &&
105
+ height > 0)) {
106
+ throw std::runtime_error(
107
+ "MakeWebGPUCanvasContextFromNativeWindow: expected a positive "
108
+ "width and height");
109
+ }
110
+ auto &runtime = WebGPURuntime::shared();
111
+ // Unlike a view's, this surface has no offscreen fallback worth having:
112
+ // nothing would ever show up, so fail here.
113
+ auto surface = runtime.makeSurface(window);
114
+ if (surface == nullptr) {
115
+ throw std::runtime_error(
116
+ "MakeWebGPUCanvasContextFromNativeWindow: could not create a surface "
117
+ "for the window; is it a valid ANativeWindow (Android) or "
118
+ "CAMetalLayer (Apple)?");
119
+ }
120
+ auto info = SurfaceRegistry::getInstance().makeUnregisteredSurfaceInfo(
121
+ runtime.instance(), static_cast<int>(width), static_cast<int>(height));
122
+ // The window is retained last, once nothing left here can throw: the
123
+ // SurfaceInfo owns the reference from here on. Adopted by the context's
124
+ // configure() (see SurfaceInfo::configure).
125
+ info->attachSurface(window, std::move(surface),
126
+ _platformContext->retainNativeWindow(window));
127
+ return std::make_shared<GPUCanvasContext>(_gpu, std::move(info), width,
128
+ height);
129
+ }
130
+
86
131
  // Wrap an externally created WGPUDevice (passed as a BigInt pointer, e.g.
87
132
  // Skia's Graphite device from Skia.getNativeDevice()) in a GPUDevice.
88
133
  // AddRefs the handle, so the original owner keeps its reference. Only sound
89
- // because the process links a single Dawn copy and the GPU instance is
90
- // shared with the exporter (see the GPU constructor): the calling runtime's
91
- // pump serves the instance the device lives on, so its async callbacks
92
- // settle normally.
134
+ // because the process links a single Dawn copy. The device's async
135
+ // callbacks are pumped on the instance that owns it (RuntimeContext is per
136
+ // runtime and per instance), which is not necessarily the shared one:
137
+ // WebGPURuntime adopts Skia's instance only when Skia's getter is reachable
138
+ // as the instance is first used (on Android, only once librnskia.so is
139
+ // loaded), and other exporters have their own.
93
140
  std::shared_ptr<GPUDevice> importDevice(jsi::Runtime &runtime,
94
141
  void *pointer) {
95
142
  if (pointer == nullptr) {
@@ -99,7 +146,8 @@ public:
99
146
  auto raw = reinterpret_cast<WGPUDevice>(pointer);
100
147
  wgpuDeviceAddRef(raw);
101
148
  wgpu::Device device = wgpu::Device::Acquire(raw);
102
- auto ctx = async::RuntimeContext::getOrCreate(runtime, _gpu->get());
149
+ auto ctx = async::RuntimeContext::getOrCreate(
150
+ runtime, device.GetAdapter().GetInstance());
103
151
  return std::make_shared<GPUDevice>(device, ctx, "Imported Device");
104
152
  }
105
153
 
@@ -282,6 +330,12 @@ public:
282
330
  return _platformContext->writeTestVideoFile();
283
331
  }
284
332
 
333
+ // The id for a Canvas about to mount, unique for the life of the process
334
+ // (see SurfaceRegistry::allocateContextId).
335
+ int allocateContextId() {
336
+ return rnwgpu::SurfaceRegistry::getInstance().allocateContextId();
337
+ }
338
+
285
339
  std::shared_ptr<Canvas> getNativeSurface(int contextId) {
286
340
  auto &registry = rnwgpu::SurfaceRegistry::getInstance();
287
341
  auto info = registry.getSurfaceInfo(contextId);
@@ -319,12 +373,16 @@ public:
319
373
  installGetter(runtime, prototype, "gpu", &RNWebGPU::getGPU);
320
374
  installMethod(runtime, prototype, "createImageBitmap",
321
375
  &RNWebGPU::createImageBitmap);
376
+ installMethod(runtime, prototype, "allocateContextId",
377
+ &RNWebGPU::allocateContextId);
322
378
  installMethod(runtime, prototype, "getNativeSurface",
323
379
  &RNWebGPU::getNativeSurface);
324
380
  installMethod(runtime, prototype, "destroyContext",
325
381
  &RNWebGPU::destroyContext);
326
382
  installMethod(runtime, prototype, "MakeWebGPUCanvasContext",
327
383
  &RNWebGPU::MakeWebGPUCanvasContext);
384
+ installMethod(runtime, prototype, "MakeWebGPUCanvasContextFromNativeWindow",
385
+ &RNWebGPU::MakeWebGPUCanvasContextFromNativeWindow);
328
386
  installMethod(runtime, prototype, "loadVideoFrame",
329
387
  &RNWebGPU::loadVideoFrame);
330
388
  installMethod(runtime, prototype, "createTestVideoFrame",