react-native-webgpu 0.10.4 → 0.11.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/Package.swift +24 -10
  2. package/android/cpp/AndroidPlatformContext.h +60 -3
  3. package/android/cpp/cpp-adapter.cpp +66 -6
  4. package/android/src/main/java/com/webgpu/ViewSnapshot.java +162 -0
  5. package/android/src/main/java/com/webgpu/WebGPUModule.java +68 -0
  6. package/apple/ApplePlatformContext.h +14 -1
  7. package/apple/ApplePlatformContext.mm +176 -1
  8. package/apple/WebGPUModule.mm +20 -1
  9. package/cpp/rnwgpu/PlatformContext.h +55 -0
  10. package/cpp/rnwgpu/RNWebGPUManager.cpp +7 -0
  11. package/cpp/rnwgpu/SurfaceRegistry.h +21 -2
  12. package/cpp/rnwgpu/api/GPU.cpp +1 -1
  13. package/cpp/rnwgpu/api/GPUCanvasContext.cpp +14 -2
  14. package/cpp/rnwgpu/api/GPUCanvasContext.h +8 -0
  15. package/cpp/rnwgpu/api/GPUQueue.cpp +187 -0
  16. package/cpp/rnwgpu/api/GPUQueue.h +16 -0
  17. package/cpp/rnwgpu/api/descriptors/GPUDrawElementImageDestination.h +60 -0
  18. package/cpp/rnwgpu/api/descriptors/GPUDrawElementImageSource.h +106 -0
  19. package/lib/commonjs/WebPolyfillGPUModule.js +7 -0
  20. package/lib/commonjs/WebPolyfillGPUModule.js.map +1 -1
  21. package/lib/commonjs/importDevice.js +3 -3
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/commonjs/main/index.js +33 -0
  24. package/lib/commonjs/main/index.js.map +1 -1
  25. package/lib/module/WebPolyfillGPUModule.js +7 -0
  26. package/lib/module/WebPolyfillGPUModule.js.map +1 -1
  27. package/lib/module/importDevice.js +3 -3
  28. package/lib/module/index.js.map +1 -1
  29. package/lib/module/main/index.js +33 -0
  30. package/lib/module/main/index.js.map +1 -1
  31. package/lib/typescript/lib/commonjs/importDevice.d.ts +3 -3
  32. package/lib/typescript/src/importDevice.d.ts +3 -3
  33. package/lib/typescript/src/index.d.ts +5 -2
  34. package/lib/typescript/src/index.d.ts.map +1 -1
  35. package/lib/typescript/src/main/index.d.ts.map +1 -1
  36. package/lib/typescript/src/types.d.ts +13 -0
  37. package/lib/typescript/src/types.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/WebPolyfillGPUModule.ts +10 -2
  40. package/src/importDevice.ts +3 -3
  41. package/src/index.tsx +29 -0
  42. package/src/main/index.tsx +49 -0
  43. package/src/types.ts +29 -0
@@ -10,10 +10,15 @@
10
10
  #import <ReactCommon/RCTTurboModule.h>
11
11
 
12
12
  #include "AppleVideoPlayer.h"
13
+ #import "RNWGUIKit.h"
13
14
 
14
15
  #include "RNWebGPUManager.h"
15
16
  #include "WebGPUModule.h"
16
17
 
18
+ #include <cmath>
19
+ #include <string>
20
+ #include <utility>
21
+
17
22
  namespace rnwgpu {
18
23
 
19
24
  void checkIfUsingSimulatorWithAPIValidation() {
@@ -30,10 +35,180 @@ void checkIfUsingSimulatorWithAPIValidation() {
30
35
  #endif
31
36
  }
32
37
 
33
- ApplePlatformContext::ApplePlatformContext() {
38
+ ApplePlatformContext::ApplePlatformContext(ViewLookup viewLookup)
39
+ : _viewLookup(std::move(viewLookup)) {
34
40
  checkIfUsingSimulatorWithAPIValidation();
35
41
  }
36
42
 
43
+ namespace {
44
+
45
+ // Rasterize `request` on the main thread into tightly packed, premultiplied
46
+ // BGRA8 pixels. Throws std::runtime_error with a user-facing message.
47
+ ImageData snapshotViewOnMainThread(const ApplePlatformContext::ViewLookup &lookup,
48
+ const ViewSnapshotRequest &request) {
49
+ if (!lookup) {
50
+ throw std::runtime_error(
51
+ "drawElementImageToTexture: no view registry is available");
52
+ }
53
+ RNWGPlatformView *view = (__bridge RNWGPlatformView *)lookup(request.viewTag);
54
+ if (view == nil) {
55
+ throw std::runtime_error(
56
+ "drawElementImageToTexture: no native view found for tag " +
57
+ std::to_string(request.viewTag) +
58
+ " (is the view mounted, and rendered with collapsable={false}?)");
59
+ }
60
+ const CGRect bounds = view.bounds;
61
+ if (bounds.size.width <= 0 || bounds.size.height <= 0) {
62
+ throw std::runtime_error(
63
+ "drawElementImageToTexture: the view has no size yet");
64
+ }
65
+
66
+ // Source rectangle in points (view space); the whole view by default.
67
+ const double sourceX = request.sourceX;
68
+ const double sourceY = request.sourceY;
69
+ const double sourceWidth =
70
+ request.sourceWidth > 0 ? request.sourceWidth : bounds.size.width;
71
+ const double sourceHeight =
72
+ request.sourceHeight > 0 ? request.sourceHeight : bounds.size.height;
73
+
74
+ // Natural pixel size: points times the screen scale the view is shown at.
75
+ #if !TARGET_OS_OSX
76
+ CGFloat scale = view.window.screen.scale;
77
+ if (scale <= 0) {
78
+ scale = UIScreen.mainScreen.scale;
79
+ }
80
+ #else
81
+ CGFloat scale = view.window.backingScaleFactor;
82
+ if (scale <= 0) {
83
+ scale = NSScreen.mainScreen.backingScaleFactor;
84
+ }
85
+ #endif
86
+ const uint32_t width =
87
+ request.width > 0
88
+ ? request.width
89
+ : static_cast<uint32_t>(std::llround(sourceWidth * scale));
90
+ const uint32_t height =
91
+ request.height > 0
92
+ ? request.height
93
+ : static_cast<uint32_t>(std::llround(sourceHeight * scale));
94
+ if (width == 0 || height == 0) {
95
+ throw std::runtime_error(
96
+ "drawElementImageToTexture: the source rectangle is empty");
97
+ }
98
+
99
+ // The view's bounds are drawn scaled so that the source rectangle covers
100
+ // the whole output.
101
+ const CGFloat scaleX = static_cast<CGFloat>(width) / sourceWidth;
102
+ const CGFloat scaleY = static_cast<CGFloat>(height) / sourceHeight;
103
+ const CGRect drawRect =
104
+ CGRectMake(-sourceX * scaleX, -sourceY * scaleY,
105
+ bounds.size.width * scaleX, bounds.size.height * scaleY);
106
+
107
+ CGImageRef cgImage = NULL;
108
+ #if !TARGET_OS_OSX
109
+ // drawViewHierarchyInRect renders what is on screen (including Metal /
110
+ // CAMetalLayer content), which is what "the element as displayed" means.
111
+ // afterScreenUpdates:YES flushes pending layout and layer changes first, so
112
+ // a snapshot taken right after a state change reflects that change.
113
+ UIGraphicsImageRendererFormat *format =
114
+ [UIGraphicsImageRendererFormat defaultFormat];
115
+ format.scale = 1.0;
116
+ format.opaque = NO;
117
+ format.preferredRange = UIGraphicsImageRendererFormatRangeStandard;
118
+ UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc]
119
+ initWithSize:CGSizeMake(width, height)
120
+ format:format];
121
+ // drawViewHierarchyInRect returns NO and draws nothing when the view is not
122
+ // in a window (e.g. a screen pushed under another one). The image would
123
+ // then be fully transparent, so treat it as a failure instead.
124
+ __block BOOL drawn = NO;
125
+ UIImage *image = [renderer
126
+ imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull context) {
127
+ drawn = [view drawViewHierarchyInRect:drawRect afterScreenUpdates:YES];
128
+ }];
129
+ if (!drawn) {
130
+ throw std::runtime_error(
131
+ "drawElementImageToTexture: the view could not be rendered (is it "
132
+ "attached to a window?)");
133
+ }
134
+ cgImage = image.CGImage;
135
+ if (cgImage != NULL) {
136
+ CGImageRetain(cgImage);
137
+ }
138
+ #else
139
+ NSBitmapImageRep *rep = [view bitmapImageRepForCachingDisplayInRect:bounds];
140
+ if (rep != nil) {
141
+ [view cacheDisplayInRect:bounds toBitmapImageRep:rep];
142
+ cgImage = rep.CGImage;
143
+ if (cgImage != NULL) {
144
+ CGImageRetain(cgImage);
145
+ }
146
+ }
147
+ #endif
148
+ if (cgImage == NULL) {
149
+ throw std::runtime_error(
150
+ "drawElementImageToTexture: the view could not be rendered");
151
+ }
152
+
153
+ // Normalize into premultiplied BGRA8 with no row padding, whatever the
154
+ // renderer produced.
155
+ ImageData result;
156
+ result.width = width;
157
+ result.height = height;
158
+ result.format = wgpu::TextureFormat::BGRA8Unorm;
159
+ result.premultiplied = true;
160
+ result.data.assign(static_cast<size_t>(width) * height * 4, 0);
161
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
162
+ CGContextRef context = CGBitmapContextCreate(
163
+ result.data.data(), width, height, 8, static_cast<size_t>(width) * 4,
164
+ colorSpace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
165
+ CGColorSpaceRelease(colorSpace);
166
+ if (context == NULL) {
167
+ CGImageRelease(cgImage);
168
+ throw std::runtime_error(
169
+ "drawElementImageToTexture: could not allocate the pixel buffer");
170
+ }
171
+ #if !TARGET_OS_OSX
172
+ // The renderer already produced exactly width x height pixels.
173
+ const CGRect imageRect = CGRectMake(0, 0, width, height);
174
+ #else
175
+ // The cached display covers the whole view at the backing scale; draw it so
176
+ // that the source rectangle fills the output. CoreGraphics' origin is the
177
+ // bottom-left corner, hence the flipped vertical offset.
178
+ const CGRect imageRect = CGRectMake(
179
+ -sourceX * scaleX,
180
+ -(bounds.size.height - sourceY - sourceHeight) * scaleY,
181
+ bounds.size.width * scaleX, bounds.size.height * scaleY);
182
+ #endif
183
+ CGContextSetBlendMode(context, kCGBlendModeCopy);
184
+ CGContextDrawImage(context, imageRect, cgImage);
185
+ CGContextRelease(context);
186
+ CGImageRelease(cgImage);
187
+ return result;
188
+ }
189
+
190
+ } // namespace
191
+
192
+ void ApplePlatformContext::snapshotView(
193
+ const ViewSnapshotRequest &request,
194
+ std::function<void(ImageData)> onSuccess,
195
+ std::function<void(std::string)> onError) {
196
+ // Copies: the block outlives this call.
197
+ ViewLookup lookup = _viewLookup;
198
+ ViewSnapshotRequest snapshotRequest = request;
199
+ dispatch_async(dispatch_get_main_queue(), ^{
200
+ @autoreleasepool {
201
+ try {
202
+ onSuccess(snapshotViewOnMainThread(lookup, snapshotRequest));
203
+ } catch (const std::exception &error) {
204
+ onError(error.what());
205
+ } catch (...) {
206
+ onError("drawElementImageToTexture: unknown native error");
207
+ }
208
+ }
209
+ });
210
+ }
211
+
37
212
  wgpu::Surface ApplePlatformContext::makeSurface(wgpu::Instance instance,
38
213
  void *surface, int width,
39
214
  int height) {
@@ -1,6 +1,7 @@
1
1
  #import "WebGPUModule.h"
2
2
  #include "ApplePlatformContext.h"
3
3
  #import "GPUCanvasContext.h"
4
+ #import "RNWGUIKit.h"
4
5
 
5
6
  #import <React/RCTBridge+Private.h>
6
7
  #import <React/RCTCallInvoker.h>
@@ -31,6 +32,11 @@ static std::shared_ptr<rnwgpu::RNWebGPUManager> webgpuManager;
31
32
  // calls setCallInvoker: during module initialization.
32
33
  @synthesize callInvoker = _callInvoker;
33
34
 
35
+ // Synthesize the view registry so the TurboModule infra injects it too. It
36
+ // resolves React tags to native views in both Bridge and Bridgeless mode
37
+ // (Fabric component views included), which drawElementImageToTexture needs.
38
+ @synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
39
+
34
40
  + (std::shared_ptr<rnwgpu::RNWebGPUManager>)getManager {
35
41
  return webgpuManager;
36
42
  }
@@ -74,8 +80,21 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) {
74
80
  return [NSNumber numberWithBool:NO];
75
81
  }
76
82
 
83
+ // Main-thread only (see ApplePlatformContext::ViewLookup). Weak: the module
84
+ // is invalidated on reload, after which lookups simply fail.
85
+ __weak WebGPUModule *weakSelf = self;
86
+ rnwgpu::ApplePlatformContext::ViewLookup viewLookup =
87
+ [weakSelf](int tag) -> void * {
88
+ WebGPUModule *strongSelf = weakSelf;
89
+ if (strongSelf == nil) {
90
+ return nullptr;
91
+ }
92
+ RNWGPlatformView *view =
93
+ [strongSelf.viewRegistry_DEPRECATED viewForReactTag:@(tag)];
94
+ return (__bridge void *)view;
95
+ };
77
96
  std::shared_ptr<rnwgpu::PlatformContext> platformContext =
78
- std::make_shared<rnwgpu::ApplePlatformContext>();
97
+ std::make_shared<rnwgpu::ApplePlatformContext>(std::move(viewLookup));
79
98
  webgpuManager = std::make_shared<rnwgpu::RNWebGPUManager>(runtime, jsInvoker,
80
99
  platformContext);
81
100
  return @true;
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include <functional>
4
4
  #include <memory>
5
+ #include <mutex>
5
6
  #include <span>
6
7
  #include <string>
7
8
  #include <utility>
@@ -72,11 +73,55 @@ public:
72
73
  virtual void pause() = 0;
73
74
  };
74
75
 
76
+ // A request to rasterize a native React Native view (looked up by its React
77
+ // tag) for GPUQueue.drawElementImageToTexture.
78
+ //
79
+ // The source rectangle is expressed in the view's own coordinate space, in
80
+ // density-independent points (the units of React Native layout). A
81
+ // non-positive sourceWidth/sourceHeight means "the whole view". The
82
+ // destination size is in pixels: the platform rasterizes the source rectangle
83
+ // scaled to exactly width x height.
84
+ struct ViewSnapshotRequest {
85
+ int viewTag = 0;
86
+ double sourceX = 0;
87
+ double sourceY = 0;
88
+ double sourceWidth = 0;
89
+ double sourceHeight = 0;
90
+ uint32_t width = 0;
91
+ uint32_t height = 0;
92
+ };
93
+
75
94
  class PlatformContext {
76
95
  public:
77
96
  PlatformContext() = default;
78
97
  virtual ~PlatformContext() = default;
79
98
 
99
+ // The platform context of the currently installed RNWebGPUManager, for API
100
+ // objects (e.g. GPUQueue) that are created without a reference to it. Set on
101
+ // install, cleared on teardown; a dev reload replaces it.
102
+ static void setCurrent(std::shared_ptr<PlatformContext> context) {
103
+ std::lock_guard<std::mutex> lock(currentMutex());
104
+ currentSlot() = std::move(context);
105
+ }
106
+ static std::shared_ptr<PlatformContext> current() {
107
+ std::lock_guard<std::mutex> lock(currentMutex());
108
+ return currentSlot();
109
+ }
110
+
111
+ // Rasterize a native view into CPU pixels (see ViewSnapshotRequest). The
112
+ // implementation hops to the platform UI thread, so this is asynchronous:
113
+ // exactly one of onSuccess / onError is invoked, from an arbitrary thread
114
+ // (typically the UI thread). Callers must not touch JSI in the callbacks.
115
+ //
116
+ // The returned ImageData is tightly packed (no row padding), 4 bytes per
117
+ // pixel, `format` is rgba8unorm or bgra8unorm depending on what the platform
118
+ // rasterizer produces natively, and `premultiplied` reports its alpha
119
+ // representation.
120
+ virtual void
121
+ snapshotView(const ViewSnapshotRequest &request,
122
+ std::function<void(ImageData)> onSuccess,
123
+ std::function<void(std::string)> onError) = 0;
124
+
80
125
  virtual wgpu::Surface makeSurface(wgpu::Instance instance, void *surface,
81
126
  int width, int height) = 0;
82
127
  virtual ImageData createImageBitmap(std::string blobId, double offset,
@@ -129,6 +174,16 @@ public:
129
174
  // and return its absolute path. Lets the SharedTextureMemory example play
130
175
  // a real decoded video without bundling an asset.
131
176
  virtual std::string writeTestVideoFile() = 0;
177
+
178
+ private:
179
+ static std::mutex &currentMutex() {
180
+ static std::mutex mutex;
181
+ return mutex;
182
+ }
183
+ static std::shared_ptr<PlatformContext> &currentSlot() {
184
+ static std::shared_ptr<PlatformContext> slot;
185
+ return slot;
186
+ }
132
187
  };
133
188
 
134
189
  } // namespace rnwgpu
@@ -67,6 +67,10 @@ RNWebGPUManager::RNWebGPUManager(
67
67
  // no invoker (best-effort; see README "Threading model").
68
68
  async::RuntimeContext::registerMainRuntime(_jsRuntime, _jsCallInvoker);
69
69
 
70
+ // Expose the platform context to API objects created later without a
71
+ // reference to it (GPUQueue::drawElementImageToTexture).
72
+ PlatformContext::setCurrent(_platformContext);
73
+
70
74
  auto gpu = std::make_shared<GPU>(*_jsRuntime);
71
75
  auto rnWebGPU =
72
76
  std::make_shared<RNWebGPU>(gpu, _platformContext, _jsCallInvoker);
@@ -248,6 +252,9 @@ RNWebGPUManager::~RNWebGPUManager() {
248
252
  // contextId counter, and stale entries would alias new canvases onto dead
249
253
  // surfaces.
250
254
  SurfaceRegistry::getInstance().clear();
255
+ if (PlatformContext::current() == _platformContext) {
256
+ PlatformContext::setCurrent(nullptr);
257
+ }
251
258
  _jsRuntime = nullptr;
252
259
  _jsCallInvoker = nullptr;
253
260
  }
@@ -446,8 +446,10 @@ public:
446
446
 
447
447
  // Returns the texture for the current frame: the surface's swapchain texture
448
448
  // when a surface is attached and healthy, an offscreen texture otherwise.
449
- // Never returns null; throws when called before configure().
450
- wgpu::Texture getCurrentTexture() {
449
+ // Never returns null; throws when called before configure(). When frameEpoch
450
+ // is given, it receives the epoch identifying this frame (see
451
+ // isCurrentFrame).
452
+ wgpu::Texture getCurrentTexture(uint64_t *frameEpoch = nullptr) {
451
453
  // Start-of-frame boundary; a new acquire supersedes any previous frame
452
454
  // that never presented.
453
455
  applyPendingAttach(/* supersedeInFlightFrame = */ true);
@@ -471,6 +473,9 @@ public:
471
473
  _frameInFlight = true;
472
474
  _acquiredFromSurface = false;
473
475
  _frameEpoch++;
476
+ if (frameEpoch) {
477
+ *frameEpoch = _frameEpoch;
478
+ }
474
479
  return texture;
475
480
  }
476
481
  // No slot available (pool not allocated yet, unsupported device, view
@@ -488,6 +493,9 @@ public:
488
493
  _frameInFlight = true;
489
494
  _acquiredFromSurface = false;
490
495
  _frameEpoch++;
496
+ if (frameEpoch) {
497
+ *frameEpoch = _frameEpoch;
498
+ }
491
499
  if (_surface) {
492
500
  auto texture = acquireSurfaceTextureLocked();
493
501
  if (texture) {
@@ -504,6 +512,17 @@ public:
504
512
  return _texture;
505
513
  }
506
514
 
515
+ // True while the frame started by the getCurrentTexture() call that
516
+ // returned frameEpoch is still the current one: nothing presented,
517
+ // configured, resized, unconfigured, detached, or adopted a surface since.
518
+ // A pending attach also ends it, so a frame that never presents cannot block
519
+ // adoption: the next getCurrentTexture() supersedes it, as an uncached call
520
+ // would.
521
+ bool isCurrentFrame(uint64_t frameEpoch) {
522
+ std::shared_lock<std::shared_mutex> lock(_mutex);
523
+ return _frameInFlight && !_hasPendingAttach && _frameEpoch == frameEpoch;
524
+ }
525
+
507
526
  // Present the current frame. Runs synchronously on the thread that did
508
527
  // getCurrentTexture/submit (main JS, Reanimated UI, or a worklet runtime),
509
528
  // preserving Dawn surface thread-affinity. Frames whose texture was not
@@ -16,7 +16,7 @@
16
16
  namespace rnwgpu {
17
17
 
18
18
  GPU::GPU(jsi::Runtime & /*runtime*/) : NativeObject(CLASS_NAME) {
19
- // If a Graphite build of @shopify/react-native-skia is loaded in this
19
+ // If a Graphite build of react-native-skia is loaded in this
20
20
  // process, adopt its wgpu::Instance instead of creating our own, so
21
21
  // Graphite's device and every JS-created device live on one instance. That
22
22
  // makes pointer-based handoff (RNWebGPU.importDevice of Skia's device)
@@ -3,6 +3,7 @@
3
3
  #include "RNWebGPUManager.h"
4
4
  #include <algorithm>
5
5
  #include <memory>
6
+ #include <mutex>
6
7
  #include <utility>
7
8
  #include <vector>
8
9
 
@@ -52,7 +53,17 @@ std::shared_ptr<GPUTexture> GPUCanvasContext::getCurrentTexture() {
52
53
  _surfaceInfo->reconfigure(width, height);
53
54
  }
54
55
 
55
- auto texture = _surfaceInfo->getCurrentTexture();
56
+ // Keep both the native texture and its JS wrapper stable until the frame
57
+ // ends. NativeObject::create already caches JS wrappers by native identity.
58
+ // The frame ends on any SurfaceInfo transition (present, configure, resize,
59
+ // unconfigure, surface detach or adoption), tracked by its frame epoch.
60
+ std::lock_guard<std::mutex> lock(_currentTextureMutex);
61
+ if (_currentTexture && _surfaceInfo->isCurrentFrame(_currentTextureEpoch)) {
62
+ return _currentTexture;
63
+ }
64
+ _currentTexture.reset();
65
+
66
+ auto texture = _surfaceInfo->getCurrentTexture(&_currentTextureEpoch);
56
67
  if (texture == nullptr) {
57
68
  throw std::runtime_error(
58
69
  "[WebGPU] getCurrentTexture() failed to acquire a texture");
@@ -64,7 +75,8 @@ std::shared_ptr<GPUTexture> GPUCanvasContext::getCurrentTexture() {
64
75
 
65
76
  // Pass reportsMemoryPressure=false to avoid triggering spurious Hermes GC
66
77
  // cycles every frame since the canvas texture doesn't own the buffer.
67
- return std::make_shared<GPUTexture>(texture, "", false);
78
+ _currentTexture = std::make_shared<GPUTexture>(texture, "", false);
79
+ return _currentTexture;
68
80
  }
69
81
 
70
82
  void GPUCanvasContext::present() {
@@ -1,6 +1,8 @@
1
1
  #pragma once
2
2
 
3
+ #include <cstdint>
3
4
  #include <memory>
5
+ #include <mutex>
4
6
  #include <string>
5
7
  #include <utility>
6
8
 
@@ -66,6 +68,12 @@ private:
66
68
  std::shared_ptr<Canvas> _canvas;
67
69
  std::shared_ptr<SurfaceInfo> _surfaceInfo;
68
70
  std::shared_ptr<GPU> _gpu;
71
+ // The current frame's texture, valid while
72
+ // _surfaceInfo->isCurrentFrame(_currentTextureEpoch). Guarded by
73
+ // _currentTextureMutex: contexts can be driven from worklet runtimes.
74
+ std::mutex _currentTextureMutex;
75
+ std::shared_ptr<GPUTexture> _currentTexture;
76
+ uint64_t _currentTextureEpoch = 0;
69
77
  };
70
78
 
71
79
  } // namespace rnwgpu
@@ -1,10 +1,16 @@
1
1
  #include "GPUQueue.h"
2
2
 
3
+ #include <algorithm>
4
+ #include <cmath>
3
5
  #include <limits>
4
6
  #include <memory>
7
+ #include <string>
8
+ #include <utility>
5
9
  #include <vector>
6
10
 
7
11
  #include "Convertors.h"
12
+ #include "ImageBitmap.h"
13
+ #include "PlatformContext.h"
8
14
 
9
15
  namespace rnwgpu {
10
16
 
@@ -193,6 +199,187 @@ void GPUQueue::copyExternalImageToTexture(
193
199
  }
194
200
  }
195
201
 
202
+ async::AsyncTaskHandle GPUQueue::drawElementImageToTexture(
203
+ jsi::Runtime &runtime, std::shared_ptr<GPUDrawElementImageSource> source,
204
+ std::shared_ptr<GPUDrawElementImageDestination> destination) {
205
+ auto platform = PlatformContext::current();
206
+ if (!platform) {
207
+ throw std::runtime_error(
208
+ "drawElementImageToTexture: react-native-webgpu is not installed");
209
+ }
210
+ if (!destination || !destination->info || !destination->info->texture) {
211
+ throw std::runtime_error(
212
+ "drawElementImageToTexture: `destination.texture` is required");
213
+ }
214
+ const auto &info = destination->info;
215
+
216
+ // Destination texture: only the 8-bit RGBA/BGRA formats the platform
217
+ // rasterizers produce natively are accepted (with a byte swizzle between
218
+ // the two); anything else would need a GPU blit.
219
+ wgpu::TexelCopyTextureInfo dst{};
220
+ Convertor conv;
221
+ if (!conv(dst.aspect, info->aspect) || !conv(dst.mipLevel, info->mipLevel) ||
222
+ !conv(dst.origin, info->origin) || !conv(dst.texture, info->texture)) {
223
+ throw std::runtime_error(
224
+ "drawElementImageToTexture: invalid destination");
225
+ }
226
+ const wgpu::Texture texture = info->texture->get();
227
+ bool destinationIsBgra = false;
228
+ switch (texture.GetFormat()) {
229
+ case wgpu::TextureFormat::RGBA8Unorm:
230
+ case wgpu::TextureFormat::RGBA8UnormSrgb:
231
+ destinationIsBgra = false;
232
+ break;
233
+ case wgpu::TextureFormat::BGRA8Unorm:
234
+ case wgpu::TextureFormat::BGRA8UnormSrgb:
235
+ destinationIsBgra = true;
236
+ break;
237
+ default:
238
+ throw std::runtime_error(
239
+ "drawElementImageToTexture: the destination texture must be "
240
+ "rgba8unorm, rgba8unorm-srgb, bgra8unorm or bgra8unorm-srgb");
241
+ }
242
+ if (!(texture.GetUsage() & wgpu::TextureUsage::CopyDst)) {
243
+ throw std::runtime_error(
244
+ "drawElementImageToTexture: the destination texture must have "
245
+ "GPUTextureUsage.COPY_DST");
246
+ }
247
+ if (texture.GetDimension() != wgpu::TextureDimension::e2D ||
248
+ texture.GetSampleCount() != 1) {
249
+ throw std::runtime_error(
250
+ "drawElementImageToTexture: the destination must be a single-sampled "
251
+ "2D texture");
252
+ }
253
+ if (dst.mipLevel >= texture.GetMipLevelCount()) {
254
+ throw std::runtime_error(
255
+ "drawElementImageToTexture: destination.mipLevel is out of range");
256
+ }
257
+ const uint32_t mipWidth =
258
+ std::max<uint32_t>(1, texture.GetWidth() >> dst.mipLevel);
259
+ const uint32_t mipHeight =
260
+ std::max<uint32_t>(1, texture.GetHeight() >> dst.mipLevel);
261
+ if (dst.origin.z >= texture.GetDepthOrArrayLayers()) {
262
+ throw std::runtime_error(
263
+ "drawElementImageToTexture: destination.origin.z is out of range");
264
+ }
265
+
266
+ // Source rectangle (points, view space). Omitted width/height mean "the
267
+ // whole view", which only the platform knows.
268
+ ViewSnapshotRequest request{};
269
+ request.viewTag = source->viewTag;
270
+ request.sourceX = source->sourceX.value_or(0.0);
271
+ request.sourceY = source->sourceY.value_or(0.0);
272
+ request.sourceWidth = source->sourceWidth.value_or(0.0);
273
+ request.sourceHeight = source->sourceHeight.value_or(0.0);
274
+ if (!std::isfinite(request.sourceX) || !std::isfinite(request.sourceY) ||
275
+ !std::isfinite(request.sourceWidth) ||
276
+ !std::isfinite(request.sourceHeight) ||
277
+ (source->sourceWidth.has_value() && request.sourceWidth <= 0) ||
278
+ (source->sourceHeight.has_value() && request.sourceHeight <= 0)) {
279
+ throw std::runtime_error(
280
+ "drawElementImageToTexture: the source rectangle must be finite with "
281
+ "a positive width and height");
282
+ }
283
+
284
+ // Destination size (pixels). Omitted means the source rectangle's natural
285
+ // pixel size (points times the device pixel ratio), which the platform
286
+ // computes; the bounds check then happens once the pixels are back.
287
+ if (destination->size.has_value()) {
288
+ wgpu::Extent3D size{};
289
+ if (!conv(size, destination->size.value())) {
290
+ throw std::runtime_error(
291
+ "drawElementImageToTexture: invalid destination.size");
292
+ }
293
+ if (size.width == 0 || size.height == 0 || size.depthOrArrayLayers != 1) {
294
+ throw std::runtime_error(
295
+ "drawElementImageToTexture: destination.size must have a positive "
296
+ "width and height and a depthOrArrayLayers of 1");
297
+ }
298
+ if (size.width > mipWidth - std::min(dst.origin.x, mipWidth) ||
299
+ size.height > mipHeight - std::min(dst.origin.y, mipHeight)) {
300
+ throw std::runtime_error(
301
+ "drawElementImageToTexture: destination.origin + destination.size "
302
+ "exceeds the texture's extent at destination.mipLevel");
303
+ }
304
+ request.width = size.width;
305
+ request.height = size.height;
306
+ }
307
+
308
+ // premultipliedAlpha defaults to false, like copyExternalImageToTexture.
309
+ const bool destinationPremultiplied =
310
+ info->premultipliedAlpha.value_or(false);
311
+
312
+ // Settle on the CALLING runtime's context (see GPUBuffer::mapAsync): the
313
+ // snapshot completes on the UI thread and is deposited into that context's
314
+ // mailbox; the texture write and the resolve then run on the runtime's own
315
+ // thread during its next tick.
316
+ auto context =
317
+ async::RuntimeContext::getOrCreate(runtime, _async->instance());
318
+ auto queue = _instance;
319
+ // Keeps the destination texture alive until the write is issued.
320
+ auto gpuTexture = info->texture;
321
+ return context->postTask(
322
+ [platform, request, dst, mipWidth, mipHeight, destinationIsBgra,
323
+ destinationPremultiplied, queue,
324
+ gpuTexture](const async::AsyncTaskHandle::ResolveFunction &resolve,
325
+ const async::AsyncTaskHandle::RejectFunction &reject) {
326
+ platform->snapshotView(
327
+ request,
328
+ [resolve, reject, dst, mipWidth, mipHeight, destinationIsBgra,
329
+ destinationPremultiplied, queue,
330
+ gpuTexture](ImageData image) mutable {
331
+ // Arbitrary (UI) thread: validate without touching JSI.
332
+ if (image.width == 0 || image.height == 0 ||
333
+ image.data.size() < image.width * image.height * 4) {
334
+ reject("drawElementImageToTexture: the view produced no "
335
+ "pixels (is it mounted with a non-zero size?)");
336
+ return;
337
+ }
338
+ if (image.width > mipWidth - std::min<uint32_t>(dst.origin.x,
339
+ mipWidth) ||
340
+ image.height > mipHeight - std::min<uint32_t>(dst.origin.y,
341
+ mipHeight)) {
342
+ reject("drawElementImageToTexture: the view's natural pixel "
343
+ "size (" +
344
+ std::to_string(image.width) + "x" +
345
+ std::to_string(image.height) +
346
+ ") does not fit in the destination texture at "
347
+ "destination.origin; pass destination.size to scale "
348
+ "it, or create a larger texture");
349
+ return;
350
+ }
351
+ resolve([image = std::move(image), dst, destinationIsBgra,
352
+ destinationPremultiplied, queue,
353
+ gpuTexture](jsi::Runtime &) mutable -> jsi::Value {
354
+ // Owning runtime's thread. Bring the pixels into the
355
+ // destination's channel order and alpha representation, then
356
+ // issue the write.
357
+ const bool sourceIsBgra =
358
+ image.format == wgpu::TextureFormat::BGRA8Unorm ||
359
+ image.format == wgpu::TextureFormat::BGRA8UnormSrgb;
360
+ uint8_t *pixels = image.data.data();
361
+ const size_t byteLength = image.width * image.height * 4;
362
+ if (sourceIsBgra != destinationIsBgra) {
363
+ for (size_t i = 0; i + 3 < byteLength; i += 4) {
364
+ std::swap(pixels[i], pixels[i + 2]);
365
+ }
366
+ }
367
+ convertAlpha(pixels, byteLength, image.premultiplied,
368
+ destinationPremultiplied);
369
+ wgpu::TexelCopyBufferLayout layout{};
370
+ layout.offset = 0;
371
+ layout.bytesPerRow = static_cast<uint32_t>(image.width * 4);
372
+ layout.rowsPerImage = static_cast<uint32_t>(image.height);
373
+ wgpu::Extent3D extent{static_cast<uint32_t>(image.width),
374
+ static_cast<uint32_t>(image.height), 1};
375
+ queue.WriteTexture(&dst, pixels, byteLength, &layout, &extent);
376
+ return jsi::Value::undefined();
377
+ });
378
+ },
379
+ [reject](std::string error) { reject(std::move(error)); });
380
+ });
381
+ }
382
+
196
383
  void GPUQueue::writeTexture(std::shared_ptr<GPUImageCopyTexture> destination,
197
384
  std::shared_ptr<ArrayBuffer> data,
198
385
  std::shared_ptr<GPUImageDataLayout> dataLayout,