react-native-webgpu 0.14.2 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/android/cpp/AndroidColorSpace.h +107 -0
  2. package/android/cpp/AndroidPlatformContext.h +100 -9
  3. package/android/cpp/AndroidVideoPlayer.h +46 -1
  4. package/android/cpp/cpp-adapter.cpp +53 -1
  5. package/android/src/main/java/com/webgpu/SyncFences.java +88 -0
  6. package/android/src/main/java/com/webgpu/ViewSnapshot.java +66 -23
  7. package/android/src/main/java/com/webgpu/WebGPUModule.java +69 -5
  8. package/android/src/main/java/com/webgpu/WebGPUVideoPlayer.java +109 -0
  9. package/apple/ApplePlatformContext.h +5 -5
  10. package/apple/ApplePlatformContext.mm +301 -90
  11. package/apple/AppleVideoPlayer.mm +68 -35
  12. package/cpp/dawn/common/ExternalTextureParams.h +67 -0
  13. package/cpp/rnwgpu/PlatformContext.h +71 -16
  14. package/cpp/rnwgpu/api/GPUAdapter.cpp +35 -0
  15. package/cpp/rnwgpu/api/GPUExternalTexture.cpp +172 -156
  16. package/cpp/rnwgpu/api/GPUExternalTexture.h +2 -19
  17. package/cpp/rnwgpu/api/GPUQueue.cpp +101 -3
  18. package/cpp/rnwgpu/api/RNWebGPU.h +9 -3
  19. package/cpp/rnwgpu/api/VideoFrameBlit.h +39 -24
  20. package/cpp/rnwgpu/api/descriptors/GPUDrawElementImageSource.h +7 -0
  21. package/cpp/rnwgpu/api/descriptors/GPUExternalTextureDescriptor.h +10 -3
  22. package/lib/commonjs/Canvas.js +32 -5
  23. package/lib/commonjs/Canvas.js.map +1 -1
  24. package/lib/commonjs/WebPolyfillGPUModule.js +6 -3
  25. package/lib/commonjs/WebPolyfillGPUModule.js.map +1 -1
  26. package/lib/commonjs/constants.js +6 -3
  27. package/lib/commonjs/constants.js.map +1 -1
  28. package/lib/commonjs/index.js +1 -0
  29. package/lib/commonjs/index.js.map +1 -1
  30. package/lib/commonjs/install.js +9 -22
  31. package/lib/commonjs/install.js.map +1 -1
  32. package/lib/commonjs/main/index.js +8 -7
  33. package/lib/commonjs/main/index.js.map +1 -1
  34. package/lib/commonjs/main/index.web.js +1 -1
  35. package/lib/commonjs/main/installNative.js +20 -0
  36. package/lib/commonjs/main/installNative.js.map +1 -0
  37. package/lib/commonjs/main/installNative.web.js +4 -0
  38. package/lib/commonjs/main/installNative.web.js.map +1 -0
  39. package/lib/commonjs/mock.js +60 -13
  40. package/lib/commonjs/mock.js.map +1 -1
  41. package/lib/module/Canvas.js +33 -6
  42. package/lib/module/Canvas.js.map +1 -1
  43. package/lib/module/WebPolyfillGPUModule.js +6 -3
  44. package/lib/module/WebPolyfillGPUModule.js.map +1 -1
  45. package/lib/module/constants.js +6 -3
  46. package/lib/module/constants.js.map +1 -1
  47. package/lib/module/index.js +8 -1
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/install.js +8 -20
  50. package/lib/module/install.js.map +1 -1
  51. package/lib/module/main/index.js +6 -7
  52. package/lib/module/main/index.js.map +1 -1
  53. package/lib/module/main/index.web.js +1 -1
  54. package/lib/module/main/index.web.js.map +1 -1
  55. package/lib/module/main/installNative.js +18 -0
  56. package/lib/module/main/installNative.js.map +1 -0
  57. package/lib/module/main/installNative.web.js +6 -0
  58. package/lib/module/main/installNative.web.js.map +1 -0
  59. package/lib/module/mock.js +55 -1
  60. package/lib/module/mock.js.map +1 -1
  61. package/lib/typescript/lib/commonjs/Canvas.d.ts.map +1 -1
  62. package/lib/typescript/lib/commonjs/install.d.ts +41 -3
  63. package/lib/typescript/lib/commonjs/install.d.ts.map +1 -1
  64. package/lib/typescript/lib/commonjs/main/installNative.d.ts +2 -0
  65. package/lib/typescript/lib/commonjs/main/installNative.d.ts.map +1 -0
  66. package/lib/typescript/lib/commonjs/main/installNative.web.d.ts +2 -0
  67. package/lib/typescript/lib/commonjs/main/installNative.web.d.ts.map +1 -0
  68. package/lib/typescript/lib/commonjs/mock.d.ts +39 -0
  69. package/lib/typescript/lib/commonjs/mock.d.ts.map +1 -1
  70. package/lib/typescript/lib/module/Canvas.d.ts.map +1 -1
  71. package/lib/typescript/lib/module/install.d.ts +0 -1
  72. package/lib/typescript/lib/module/install.d.ts.map +1 -1
  73. package/lib/typescript/lib/module/main/installNative.d.ts +2 -0
  74. package/lib/typescript/lib/module/main/installNative.d.ts.map +1 -0
  75. package/lib/typescript/lib/module/main/installNative.web.d.ts +2 -0
  76. package/lib/typescript/lib/module/main/installNative.web.d.ts.map +1 -0
  77. package/lib/typescript/lib/module/mock.d.ts +39 -1
  78. package/lib/typescript/lib/module/mock.d.ts.map +1 -1
  79. package/lib/typescript/src/Canvas.d.ts.map +1 -1
  80. package/lib/typescript/src/constants.d.ts.map +1 -1
  81. package/lib/typescript/src/index.d.ts +2 -2
  82. package/lib/typescript/src/index.d.ts.map +1 -1
  83. package/lib/typescript/src/install.d.ts +0 -5
  84. package/lib/typescript/src/install.d.ts.map +1 -1
  85. package/lib/typescript/src/main/index.d.ts +1 -0
  86. package/lib/typescript/src/main/index.d.ts.map +1 -1
  87. package/lib/typescript/src/main/index.web.d.ts +1 -1
  88. package/lib/typescript/src/main/index.web.d.ts.map +1 -1
  89. package/lib/typescript/src/main/installNative.d.ts +2 -0
  90. package/lib/typescript/src/main/installNative.d.ts.map +1 -0
  91. package/lib/typescript/src/main/installNative.web.d.ts +2 -0
  92. package/lib/typescript/src/main/installNative.web.d.ts.map +1 -0
  93. package/lib/typescript/src/mock.d.ts +5 -1
  94. package/lib/typescript/src/mock.d.ts.map +1 -1
  95. package/lib/typescript/src/types.d.ts +1 -0
  96. package/lib/typescript/src/types.d.ts.map +1 -1
  97. package/package.json +1 -1
  98. package/src/Canvas.tsx +44 -28
  99. package/src/WebPolyfillGPUModule.ts +3 -1
  100. package/src/constants.ts +6 -3
  101. package/src/index.tsx +18 -10
  102. package/src/install.ts +9 -18
  103. package/src/main/index.tsx +7 -10
  104. package/src/main/index.web.tsx +1 -1
  105. package/src/main/installNative.ts +20 -0
  106. package/src/main/installNative.web.ts +5 -0
  107. package/src/mock.ts +60 -1
  108. package/src/types.ts +6 -0
@@ -42,156 +42,279 @@ ApplePlatformContext::ApplePlatformContext(ViewLookup viewLookup)
42
42
 
43
43
  namespace {
44
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) {
45
+ // A view that UIKit declined to render (it is not in a window). The pixel
46
+ // path would fail the same way, so there is no point falling back to it.
47
+ struct ViewNotRenderable : std::runtime_error {
48
+ using std::runtime_error::runtime_error;
49
+ };
50
+
51
+ // The view, the output size and the rectangle to draw it in, resolved from a
52
+ // request on the main thread. Throws std::runtime_error with a user-facing
53
+ // message.
54
+ struct ResolvedSnapshot {
55
+ RNWGPlatformView *view = nil;
56
+ CGRect bounds = CGRectZero;
57
+ // The source rectangle in points (view space).
58
+ double sourceX = 0;
59
+ double sourceY = 0;
60
+ double sourceWidth = 0;
61
+ double sourceHeight = 0;
62
+ // The output size in pixels.
63
+ uint32_t width = 0;
64
+ uint32_t height = 0;
65
+ CGFloat scaleX = 1;
66
+ CGFloat scaleY = 1;
67
+ // The view's bounds, scaled and offset so that the source rectangle covers
68
+ // the whole output, in pixels and with UIKit's top-left origin.
69
+ CGRect drawRect = CGRectZero;
70
+ };
71
+
72
+ ResolvedSnapshot resolveSnapshot(const ApplePlatformContext::ViewLookup &lookup,
73
+ const ViewSnapshotRequest &request) {
49
74
  if (!lookup) {
50
75
  throw std::runtime_error(
51
76
  "drawElementImageToTexture: no view registry is available");
52
77
  }
53
- RNWGPlatformView *view = (__bridge RNWGPlatformView *)lookup(request.viewTag);
54
- if (view == nil) {
78
+ ResolvedSnapshot snapshot;
79
+ snapshot.view = (__bridge RNWGPlatformView *)lookup(request.viewTag);
80
+ if (snapshot.view == nil) {
55
81
  throw std::runtime_error(
56
82
  "drawElementImageToTexture: no native view found for tag " +
57
83
  std::to_string(request.viewTag) +
58
84
  " (is the view mounted, and rendered with collapsable={false}?)");
59
85
  }
60
- const CGRect bounds = view.bounds;
86
+ snapshot.bounds = snapshot.view.bounds;
87
+ const CGRect bounds = snapshot.bounds;
61
88
  if (bounds.size.width <= 0 || bounds.size.height <= 0) {
62
89
  throw std::runtime_error(
63
90
  "drawElementImageToTexture: the view has no size yet");
64
91
  }
65
92
 
66
93
  // 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 =
94
+ snapshot.sourceX = request.sourceX;
95
+ snapshot.sourceY = request.sourceY;
96
+ snapshot.sourceWidth =
70
97
  request.sourceWidth > 0 ? request.sourceWidth : bounds.size.width;
71
- const double sourceHeight =
98
+ snapshot.sourceHeight =
72
99
  request.sourceHeight > 0 ? request.sourceHeight : bounds.size.height;
73
100
 
74
101
  // Natural pixel size: points times the screen scale the view is shown at.
75
102
  #if !TARGET_OS_OSX
76
- CGFloat scale = view.window.screen.scale;
103
+ CGFloat scale = snapshot.view.window.screen.scale;
77
104
  if (scale <= 0) {
78
105
  scale = UIScreen.mainScreen.scale;
79
106
  }
80
107
  #else
81
- CGFloat scale = view.window.backingScaleFactor;
108
+ CGFloat scale = snapshot.view.window.backingScaleFactor;
82
109
  if (scale <= 0) {
83
110
  scale = NSScreen.mainScreen.backingScaleFactor;
84
111
  }
85
112
  #endif
86
- const uint32_t width =
113
+ snapshot.width =
87
114
  request.width > 0
88
115
  ? request.width
89
- : static_cast<uint32_t>(std::llround(sourceWidth * scale));
90
- const uint32_t height =
116
+ : static_cast<uint32_t>(std::llround(snapshot.sourceWidth * scale));
117
+ snapshot.height =
91
118
  request.height > 0
92
119
  ? request.height
93
- : static_cast<uint32_t>(std::llround(sourceHeight * scale));
94
- if (width == 0 || height == 0) {
120
+ : static_cast<uint32_t>(std::llround(snapshot.sourceHeight * scale));
121
+ if (snapshot.width == 0 || snapshot.height == 0) {
95
122
  throw std::runtime_error(
96
123
  "drawElementImageToTexture: the source rectangle is empty");
97
124
  }
98
125
 
99
126
  // The view's bounds are drawn scaled so that the source rectangle covers
100
127
  // 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);
128
+ snapshot.scaleX = static_cast<CGFloat>(snapshot.width) / snapshot.sourceWidth;
129
+ snapshot.scaleY =
130
+ static_cast<CGFloat>(snapshot.height) / snapshot.sourceHeight;
131
+ snapshot.drawRect = CGRectMake(-snapshot.sourceX * snapshot.scaleX,
132
+ -snapshot.sourceY * snapshot.scaleY,
133
+ bounds.size.width * snapshot.scaleX,
134
+ bounds.size.height * snapshot.scaleY);
135
+ return snapshot;
136
+ }
137
+
138
+ // A premultiplied BGRA8 Core Graphics context over `pixels`, the layout both
139
+ // snapshot paths produce. Returns NULL when Core Graphics rejects it.
140
+ CGContextRef createBgraContext(void *pixels, uint32_t width, uint32_t height,
141
+ size_t bytesPerRow) {
142
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
143
+ CGContextRef context = CGBitmapContextCreate(
144
+ pixels, width, height, 8, bytesPerRow, colorSpace,
145
+ kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
146
+ CGColorSpaceRelease(colorSpace);
147
+ return context;
148
+ }
106
149
 
107
- CGImageRef cgImage = NULL;
108
150
  #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
- }];
151
+ // Draws the view hierarchy through `context`, a bitmap context of exactly
152
+ // `snapshot.width` x `snapshot.height` pixels (Core Graphics orientation; the
153
+ // flip to UIKit's top-left origin is applied here).
154
+ //
155
+ // drawViewHierarchyInRect renders what is on screen (including Metal /
156
+ // CAMetalLayer content), which is what "the element as displayed" means.
157
+ // afterScreenUpdates:YES flushes pending layout and layer changes first, so a
158
+ // snapshot taken right after a state change reflects that change; NO draws
159
+ // the last committed frame and skips that round trip to the render server.
160
+ //
161
+ // drawViewHierarchyInRect returns NO and draws nothing when the view is not
162
+ // in a window (e.g. a screen pushed under another one). The output would then
163
+ // be fully transparent, so that is reported as a failure instead.
164
+ void drawViewHierarchy(const ResolvedSnapshot &snapshot, CGContextRef context,
165
+ bool afterScreenUpdates) {
166
+ CGContextSaveGState(context);
167
+ CGContextTranslateCTM(context, 0, snapshot.height);
168
+ CGContextScaleCTM(context, 1, -1);
169
+ UIGraphicsPushContext(context);
170
+ const BOOL drawn =
171
+ [snapshot.view drawViewHierarchyInRect:snapshot.drawRect
172
+ afterScreenUpdates:afterScreenUpdates ? YES : NO];
173
+ UIGraphicsPopContext();
174
+ CGContextRestoreGState(context);
129
175
  if (!drawn) {
130
- throw std::runtime_error(
176
+ throw ViewNotRenderable(
131
177
  "drawElementImageToTexture: the view could not be rendered (is it "
132
178
  "attached to a window?)");
133
179
  }
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);
180
+ }
181
+
182
+ // Releases a pixel buffer when it goes out of scope.
183
+ struct PixelBufferRelease {
184
+ CVPixelBufferRef buffer;
185
+ ~PixelBufferRelease() {
186
+ if (buffer != NULL) {
187
+ CFRelease(buffer);
145
188
  }
146
189
  }
147
- #endif
148
- if (cgImage == NULL) {
190
+ };
191
+
192
+ // The zero-copy path: renders the view into an IOSurface-backed BGRA pixel
193
+ // buffer, which the caller imports as shared texture memory and draws into
194
+ // the destination on the GPU (VideoFrameBlit, which also converts the
195
+ // premultiplied pixels to straight alpha when asked). UIKit rasterizes
196
+ // straight into the surface's memory through a Core Graphics context, so the
197
+ // pixels are never copied or converted on the CPU. Throws on an allocation
198
+ // failure, which the caller answers with the pixel path.
199
+ VideoFrameHandle renderViewIntoSurface(const ResolvedSnapshot &snapshot,
200
+ bool afterScreenUpdates) {
201
+ NSDictionary *attributes = @{
202
+ (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{},
203
+ (NSString *)kCVPixelBufferMetalCompatibilityKey : @YES,
204
+ };
205
+ CVPixelBufferRef pixelBuffer = NULL;
206
+ const CVReturn created =
207
+ CVPixelBufferCreate(kCFAllocatorDefault, snapshot.width, snapshot.height,
208
+ kCVPixelFormatType_32BGRA,
209
+ (__bridge CFDictionaryRef)attributes, &pixelBuffer);
210
+ if (created != kCVReturnSuccess || pixelBuffer == NULL) {
149
211
  throw std::runtime_error(
150
- "drawElementImageToTexture: the view could not be rendered");
212
+ "drawElementImageToTexture: could not allocate an IOSurface for the "
213
+ "view (CVPixelBufferCreate returned " +
214
+ std::to_string(created) + ")");
215
+ }
216
+ // wrapCVPixelBuffer retains its own reference for the frame.
217
+ PixelBufferRelease release{pixelBuffer};
218
+
219
+ if (CVPixelBufferLockBaseAddress(pixelBuffer, 0) != kCVReturnSuccess) {
220
+ throw std::runtime_error(
221
+ "drawElementImageToTexture: could not lock the IOSurface");
222
+ }
223
+ CGContextRef context = createBgraContext(
224
+ CVPixelBufferGetBaseAddress(pixelBuffer), snapshot.width, snapshot.height,
225
+ CVPixelBufferGetBytesPerRow(pixelBuffer));
226
+ if (context == NULL) {
227
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
228
+ throw std::runtime_error(
229
+ "drawElementImageToTexture: could not draw into the IOSurface");
230
+ }
231
+ try {
232
+ // A fresh surface holds whatever was in that memory, and the view may be
233
+ // transparent.
234
+ CGContextClearRect(context,
235
+ CGRectMake(0, 0, snapshot.width, snapshot.height));
236
+ drawViewHierarchy(snapshot, context, afterScreenUpdates);
237
+ } catch (...) {
238
+ CGContextRelease(context);
239
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
240
+ throw;
151
241
  }
242
+ CGContextRelease(context);
243
+ // Unlocking publishes the CPU writes to the GPU.
244
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
245
+ return wrapCVPixelBuffer(pixelBuffer);
246
+ }
247
+ #endif
152
248
 
153
- // Normalize into premultiplied BGRA8 with no row padding, whatever the
154
- // renderer produced.
249
+ // The pixel path: rasterizes the view into tightly packed, premultiplied
250
+ // BGRA8 pixels that the caller uploads with writeTexture.
251
+ ImageData renderViewIntoPixels(const ResolvedSnapshot &snapshot,
252
+ bool afterScreenUpdates) {
155
253
  ImageData result;
156
- result.width = width;
157
- result.height = height;
254
+ result.width = snapshot.width;
255
+ result.height = snapshot.height;
158
256
  result.format = wgpu::TextureFormat::BGRA8Unorm;
159
257
  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);
258
+ // Zeroed: the view may be transparent.
259
+ result.data.assign(static_cast<size_t>(snapshot.width) * snapshot.height * 4,
260
+ 0);
261
+ CGContextRef context =
262
+ createBgraContext(result.data.data(), snapshot.width, snapshot.height,
263
+ static_cast<size_t>(snapshot.width) * 4);
166
264
  if (context == NULL) {
167
- CGImageRelease(cgImage);
168
265
  throw std::runtime_error(
169
266
  "drawElementImageToTexture: could not allocate the pixel buffer");
170
267
  }
171
268
  #if !TARGET_OS_OSX
172
- // The renderer already produced exactly width x height pixels.
173
- const CGRect imageRect = CGRectMake(0, 0, width, height);
269
+ // UIKit draws straight into the vector; no intermediate image.
270
+ try {
271
+ drawViewHierarchy(snapshot, context, afterScreenUpdates);
272
+ } catch (...) {
273
+ CGContextRelease(context);
274
+ throw;
275
+ }
174
276
  #else
277
+ (void)afterScreenUpdates;
278
+ CGImageRef cgImage = NULL;
279
+ NSBitmapImageRep *rep =
280
+ [snapshot.view bitmapImageRepForCachingDisplayInRect:snapshot.bounds];
281
+ if (rep != nil) {
282
+ [snapshot.view cacheDisplayInRect:snapshot.bounds toBitmapImageRep:rep];
283
+ cgImage = rep.CGImage;
284
+ if (cgImage != NULL) {
285
+ CGImageRetain(cgImage);
286
+ }
287
+ }
288
+ if (cgImage == NULL) {
289
+ CGContextRelease(context);
290
+ throw std::runtime_error(
291
+ "drawElementImageToTexture: the view could not be rendered");
292
+ }
175
293
  // The cached display covers the whole view at the backing scale; draw it so
176
294
  // that the source rectangle fills the output. CoreGraphics' origin is the
177
295
  // 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
296
+ const CGRect imageRect =
297
+ CGRectMake(-snapshot.sourceX * snapshot.scaleX,
298
+ -(snapshot.bounds.size.height - snapshot.sourceY -
299
+ snapshot.sourceHeight) *
300
+ snapshot.scaleY,
301
+ snapshot.bounds.size.width * snapshot.scaleX,
302
+ snapshot.bounds.size.height * snapshot.scaleY);
183
303
  CGContextSetBlendMode(context, kCGBlendModeCopy);
184
304
  CGContextDrawImage(context, imageRect, cgImage);
185
- CGContextRelease(context);
186
305
  CGImageRelease(cgImage);
306
+ #endif
307
+ CGContextRelease(context);
187
308
  return result;
188
309
  }
189
310
 
190
311
  } // namespace
191
312
 
313
+ // On iOS a caller that can draw a GPU frame (allowFrame) gets the view
314
+ // rendered into an IOSurface; everything else, and macOS, gets pixels.
192
315
  void ApplePlatformContext::snapshotView(
193
- const ViewSnapshotRequest &request,
194
- std::function<void(ImageData)> onSuccess,
316
+ const ViewSnapshotRequest &request, std::function<void(ImageData)> onPixels,
317
+ std::function<void(VideoFrameHandle)> onFrame,
195
318
  std::function<void(std::string)> onError) {
196
319
  // Copies: the block outlives this call.
197
320
  ViewLookup lookup = _viewLookup;
@@ -199,7 +322,29 @@ void ApplePlatformContext::snapshotView(
199
322
  dispatch_async(dispatch_get_main_queue(), ^{
200
323
  @autoreleasepool {
201
324
  try {
202
- onSuccess(snapshotViewOnMainThread(lookup, snapshotRequest));
325
+ const ResolvedSnapshot snapshot =
326
+ resolveSnapshot(lookup, snapshotRequest);
327
+ #if !TARGET_OS_OSX
328
+ if (snapshotRequest.allowFrame) {
329
+ try {
330
+ onFrame(renderViewIntoSurface(snapshot,
331
+ snapshotRequest.afterScreenUpdates));
332
+ return;
333
+ } catch (const ViewNotRenderable &) {
334
+ throw;
335
+ } catch (const std::exception &error) {
336
+ // An IOSurface could not be set up; the pixel path draws the
337
+ // same view.
338
+ NSLog(@"[WebGPU] drawElementImageToTexture: %s; reading the "
339
+ @"pixels back instead",
340
+ error.what());
341
+ }
342
+ }
343
+ #else
344
+ (void)onFrame;
345
+ #endif
346
+ onPixels(
347
+ renderViewIntoPixels(snapshot, snapshotRequest.afterScreenUpdates));
203
348
  } catch (const std::exception &error) {
204
349
  onError(error.what());
205
350
  } catch (...) {
@@ -322,12 +467,10 @@ void ApplePlatformContext::createImageBitmapFromDataAsync(
322
467
  });
323
468
  }
324
469
 
325
- VideoFrameHandle
326
- ApplePlatformContext::loadVideoFrame(const std::string &path) {
470
+ VideoFrameHandle ApplePlatformContext::loadVideoFrame(const std::string &path) {
327
471
  NSString *nsPath = [NSString stringWithUTF8String:path.c_str()];
328
- NSURL *url = [nsPath hasPrefix:@"file://"]
329
- ? [NSURL URLWithString:nsPath]
330
- : [NSURL fileURLWithPath:nsPath];
472
+ NSURL *url = [nsPath hasPrefix:@"file://"] ? [NSURL URLWithString:nsPath]
473
+ : [NSURL fileURLWithPath:nsPath];
331
474
  AVURLAsset *asset = [AVURLAsset assetWithURL:url];
332
475
 
333
476
  NSArray<AVAssetTrack *> *videoTracks =
@@ -347,8 +490,7 @@ ApplePlatformContext::loadVideoFrame(const std::string &path) {
347
490
  }
348
491
 
349
492
  NSDictionary *outputSettings = @{
350
- (NSString *)kCVPixelBufferPixelFormatTypeKey :
351
- @(kCVPixelFormatType_32BGRA),
493
+ (NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
352
494
  (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{},
353
495
  (NSString *)kCVPixelBufferMetalCompatibilityKey : @YES,
354
496
  };
@@ -402,7 +544,7 @@ ApplePlatformContext::loadVideoFrame(const std::string &path) {
402
544
 
403
545
  std::unique_ptr<IVideoPlayer>
404
546
  ApplePlatformContext::createVideoPlayer(const std::string &path,
405
- VideoPixelFormat format) {
547
+ VideoPixelFormat format) {
406
548
  return createAppleVideoPlayer(path, format);
407
549
  }
408
550
 
@@ -414,8 +556,77 @@ VideoFrameHandle ApplePlatformContext::wrapNativeBuffer(void *pointer) {
414
556
  return wrapCVPixelBuffer(static_cast<CVPixelBufferRef>(pointer));
415
557
  }
416
558
 
559
+ // The NV12 variant of createTestVideoFrame: a biplanar BT.709 limited-range
560
+ // frame, red on the left half and green on the right, whose attachments
561
+ // declare its color space the way the decoder's frames do, so that the
562
+ // conversion of importExternalTexture is fully determined.
563
+ static VideoFrameHandle createTestYuvFrame(uint32_t width, uint32_t height) {
564
+ NSDictionary *attrs = @{
565
+ (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{},
566
+ (NSString *)kCVPixelBufferMetalCompatibilityKey : @YES,
567
+ };
568
+ CVPixelBufferRef pixelBuffer = NULL;
569
+ CVReturn err =
570
+ CVPixelBufferCreate(kCFAllocatorDefault, width, height,
571
+ kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
572
+ (__bridge CFDictionaryRef)attrs, &pixelBuffer);
573
+ if (err != kCVReturnSuccess || !pixelBuffer) {
574
+ throw std::runtime_error("createTestVideoFrame: unsupported: "
575
+ "CVPixelBufferCreate failed for a biplanar "
576
+ "YUV buffer");
577
+ }
578
+ CVBufferSetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey,
579
+ kCVImageBufferYCbCrMatrix_ITU_R_709_2,
580
+ kCVAttachmentMode_ShouldPropagate);
581
+ CVBufferSetAttachment(pixelBuffer, kCVImageBufferColorPrimariesKey,
582
+ kCVImageBufferColorPrimaries_ITU_R_709_2,
583
+ kCVAttachmentMode_ShouldPropagate);
584
+ CVBufferSetAttachment(pixelBuffer, kCVImageBufferTransferFunctionKey,
585
+ kCVImageBufferTransferFunction_ITU_R_709_2,
586
+ kCVAttachmentMode_ShouldPropagate);
587
+
588
+ CVPixelBufferLockBaseAddress(pixelBuffer, 0);
589
+ uint8_t *luma = static_cast<uint8_t *>(
590
+ CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0));
591
+ size_t lumaRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
592
+ for (uint32_t y = 0; y < height; ++y) {
593
+ uint8_t *row = luma + y * lumaRowBytes;
594
+ for (uint32_t x = 0; x < width; ++x) {
595
+ row[x] = x < width / 2 ? test_yuv_frame::kRedY : test_yuv_frame::kGreenY;
596
+ }
597
+ }
598
+ // The chroma plane is subsampled by two on both axes and interleaves Cb, Cr.
599
+ uint8_t *chroma = static_cast<uint8_t *>(
600
+ CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1));
601
+ size_t chromaRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
602
+ size_t chromaWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
603
+ size_t chromaHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
604
+ for (size_t y = 0; y < chromaHeight; ++y) {
605
+ uint8_t *row = chroma + y * chromaRowBytes;
606
+ for (size_t x = 0; x < chromaWidth; ++x) {
607
+ bool red = x < chromaWidth / 2;
608
+ row[x * 2 + 0] = red ? test_yuv_frame::kRedCb : test_yuv_frame::kGreenCb;
609
+ row[x * 2 + 1] = red ? test_yuv_frame::kRedCr : test_yuv_frame::kGreenCr;
610
+ }
611
+ }
612
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
613
+
614
+ try {
615
+ auto handle = wrapCVPixelBuffer(pixelBuffer);
616
+ CFRelease(pixelBuffer); // wrapCVPixelBuffer retained its own reference
617
+ return handle;
618
+ } catch (...) {
619
+ CFRelease(pixelBuffer);
620
+ throw;
621
+ }
622
+ }
623
+
417
624
  VideoFrameHandle
418
- ApplePlatformContext::createTestVideoFrame(uint32_t width, uint32_t height) {
625
+ ApplePlatformContext::createTestVideoFrame(uint32_t width, uint32_t height,
626
+ VideoPixelFormat format) {
627
+ if (format == VideoPixelFormat::NV12) {
628
+ return createTestYuvFrame(width, height);
629
+ }
419
630
  NSDictionary *attrs = @{
420
631
  (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{},
421
632
  (NSString *)kCVPixelBufferMetalCompatibilityKey : @YES,
@@ -12,47 +12,80 @@ namespace rnwgpu {
12
12
 
13
13
  namespace {
14
14
 
15
- // 3x4 row-major matrices mapping [Y, Cb, Cr, 1] to gamma-encoded R'G'B' (NOT
16
- // linear): YCbCr is derived from gamma-encoded RGB, so this conversion stays in
17
- // the encoded domain. The sRGB decode in srcTransferFunctionParameters
18
- // linearizes afterward (see GPUExternalTexture.cpp). Limited-range (video
19
- // range) means luma is 16..235, chroma is 16..240 (8-bit).
20
- // Reference: https://en.wikipedia.org/wiki/YCbCr (BT.601 / BT.709).
21
- static constexpr float kBT709LimitedToRgb[12] = {
22
- 1.164383f, 0.000000f, 1.792741f, -0.972945f, //
23
- 1.164383f, -0.213249f, -0.532909f, 0.301517f, //
24
- 1.164383f, 2.112402f, 0.000000f, -1.133402f, //
25
- };
26
- static constexpr float kBT601LimitedToRgb[12] = {
27
- 1.164383f, 0.000000f, 1.596027f, -0.874202f, //
28
- 1.164383f, -0.391762f, -0.812968f, 0.531668f, //
29
- 1.164383f, 2.017232f, 0.000000f, -1.085631f, //
30
- };
31
- static constexpr float kBT2020LimitedToRgb[12] = {
32
- 1.164383f, 0.000000f, 1.678674f, -0.915688f, //
33
- 1.164383f, -0.187326f, -0.650424f, 0.347459f, //
34
- 1.164383f, 2.141772f, 0.000000f, -1.148145f, //
35
- };
15
+ // The string attachment of a pixel buffer, or null when absent or not a string.
16
+ static CFStringRef stringAttachment(CVPixelBufferRef pixelBuffer,
17
+ CFStringRef key) {
18
+ CFTypeRef value = CVBufferGetAttachment(pixelBuffer, key, nullptr);
19
+ if (value == nullptr || CFGetTypeID(value) != CFStringGetTypeID()) {
20
+ return nullptr;
21
+ }
22
+ return static_cast<CFStringRef>(value);
23
+ }
36
24
 
37
- // Pick the right YUV→RGB matrix from the pixel buffer's color attachments.
38
- // Falls back to BT.709 limited range (the right call for ≥720p H.264, which
39
- // is what AVPlayer hands us for Big Buck Bunny and most streamed media).
40
- static void fillYuvMatrix(CVPixelBufferRef pixelBuffer, float out[12]) {
41
- CFTypeRef matrixKey =
42
- CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, nullptr);
43
- const float *src = kBT709LimitedToRgb;
44
- if (matrixKey) {
45
- auto matrix = (CFStringRef)matrixKey;
25
+ // The color space of a YUV pixel buffer, from its attachments: the YCbCr matrix
26
+ // (kCVImageBufferYCbCrMatrixKey), the range (the full-range pixel formats),
27
+ // the primaries and the transfer function. What the buffer does not declare
28
+ // stays unset and resolves at import time to BT.709 limited range, the norm
29
+ // for HD video (GPUExternalTexture.cpp).
30
+ static VideoFrameColorSpace
31
+ colorSpaceFromCVPixelBuffer(CVPixelBufferRef pixelBuffer) {
32
+ VideoFrameColorSpace colorSpace;
33
+ if (CFStringRef matrix =
34
+ stringAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey)) {
46
35
  if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_ITU_R_601_4) ||
47
36
  CFEqual(matrix, kCVImageBufferYCbCrMatrix_SMPTE_240M_1995)) {
48
- src = kBT601LimitedToRgb;
37
+ colorSpace.matrix = wgpu::ColorSpaceYCbCrMatrixDawn::Rec601;
38
+ } else if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_ITU_R_709_2)) {
39
+ colorSpace.matrix = wgpu::ColorSpaceYCbCrMatrixDawn::Rec709;
49
40
  } else if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_ITU_R_2020)) {
50
- src = kBT2020LimitedToRgb;
41
+ colorSpace.matrix = wgpu::ColorSpaceYCbCrMatrixDawn::Rec2020;
42
+ }
43
+ }
44
+ switch (CVPixelBufferGetPixelFormatType(pixelBuffer)) {
45
+ case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
46
+ colorSpace.range = wgpu::ColorSpaceYCbCrRangeDawn::Full;
47
+ break;
48
+ case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
49
+ colorSpace.range = wgpu::ColorSpaceYCbCrRangeDawn::Narrow;
50
+ break;
51
+ default:
52
+ break;
53
+ }
54
+ if (CFStringRef primaries =
55
+ stringAttachment(pixelBuffer, kCVImageBufferColorPrimariesKey)) {
56
+ if (CFEqual(primaries, kCVImageBufferColorPrimaries_ITU_R_709_2)) {
57
+ // BT.709 shares the sRGB primaries (Dawn's helper only knows them under
58
+ // that name).
59
+ colorSpace.primaries = wgpu::ColorSpacePrimariesDawn::SRGB;
60
+ } else if (CFEqual(primaries, kCVImageBufferColorPrimaries_EBU_3213) ||
61
+ CFEqual(primaries, kCVImageBufferColorPrimaries_SMPTE_C)) {
62
+ colorSpace.primaries = wgpu::ColorSpacePrimariesDawn::Rec601;
63
+ } else if (CFEqual(primaries, kCVImageBufferColorPrimaries_ITU_R_2020)) {
64
+ colorSpace.primaries = wgpu::ColorSpacePrimariesDawn::Rec2020;
65
+ } else if (CFEqual(primaries, kCVImageBufferColorPrimaries_P3_D65)) {
66
+ colorSpace.primaries = wgpu::ColorSpacePrimariesDawn::DisplayP3;
51
67
  }
52
68
  }
53
- for (int i = 0; i < 12; ++i) {
54
- out[i] = src[i];
69
+ if (CFStringRef transfer =
70
+ stringAttachment(pixelBuffer, kCVImageBufferTransferFunctionKey)) {
71
+ if (CFEqual(transfer, kCVImageBufferTransferFunction_ITU_R_709_2) ||
72
+ CFEqual(transfer, kCVImageBufferTransferFunction_SMPTE_240M_1995) ||
73
+ CFEqual(transfer, kCVImageBufferTransferFunction_ITU_R_2020)) {
74
+ // The SDR video OETF; SMPTE 170M is Dawn's name for its inverse.
75
+ colorSpace.transfer = wgpu::ColorSpaceTransferDawn::SMPTE_170M;
76
+ } else if (CFEqual(transfer, kCVImageBufferTransferFunction_sRGB)) {
77
+ colorSpace.transfer = wgpu::ColorSpaceTransferDawn::SRGB;
78
+ } else if (CFEqual(transfer, kCVImageBufferTransferFunction_Linear)) {
79
+ colorSpace.transfer = wgpu::ColorSpaceTransferDawn::Identity;
80
+ } else if (CFEqual(transfer,
81
+ kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)) {
82
+ colorSpace.transfer = wgpu::ColorSpaceTransferDawn::PQ;
83
+ } else if (CFEqual(transfer,
84
+ kCVImageBufferTransferFunction_ITU_R_2100_HLG)) {
85
+ colorSpace.transfer = wgpu::ColorSpaceTransferDawn::HLG;
86
+ }
55
87
  }
88
+ return colorSpace;
56
89
  }
57
90
 
58
91
  // Map a CVPixelBuffer's pixel format to our VideoPixelFormat enum.
@@ -257,7 +290,7 @@ VideoFrameHandle wrapCVPixelBuffer(CVPixelBufferRef pixelBuffer) {
257
290
  handle.height = static_cast<uint32_t>(CVPixelBufferGetHeight(pixelBuffer));
258
291
  handle.pixelFormat = pixelFormatFromCVPixelBuffer(pixelBuffer);
259
292
  if (handle.pixelFormat == VideoPixelFormat::NV12) {
260
- fillYuvMatrix(pixelBuffer, handle.yuvToRgbMatrix);
293
+ handle.colorSpace = colorSpaceFromCVPixelBuffer(pixelBuffer);
261
294
  }
262
295
  handle.deleter = [pixelBuffer]() { CFRelease(pixelBuffer); };
263
296
  return handle;