react-native-webgpu 0.14.1 → 0.15.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 (105) 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 +75 -3
  11. package/apple/AppleVideoPlayer.mm +68 -35
  12. package/cpp/dawn/common/ExternalTextureParams.h +67 -0
  13. package/cpp/rnwgpu/PlatformContext.h +65 -16
  14. package/cpp/rnwgpu/SurfaceRegistry.h +39 -5
  15. package/cpp/rnwgpu/api/GPUAdapter.cpp +35 -0
  16. package/cpp/rnwgpu/api/GPUExternalTexture.cpp +172 -156
  17. package/cpp/rnwgpu/api/GPUExternalTexture.h +2 -19
  18. package/cpp/rnwgpu/api/GPUQueue.cpp +98 -3
  19. package/cpp/rnwgpu/api/RNWebGPU.h +12 -3
  20. package/cpp/rnwgpu/api/VideoFrameBlit.h +39 -24
  21. package/cpp/rnwgpu/api/descriptors/GPUExternalTextureDescriptor.h +10 -3
  22. package/lib/commonjs/Canvas.js +36 -6
  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 +37 -7
  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/package.json +1 -1
  96. package/src/Canvas.tsx +48 -29
  97. package/src/WebPolyfillGPUModule.ts +3 -1
  98. package/src/constants.ts +6 -3
  99. package/src/index.tsx +18 -10
  100. package/src/install.ts +9 -18
  101. package/src/main/index.tsx +7 -10
  102. package/src/main/index.web.tsx +1 -1
  103. package/src/main/installNative.ts +20 -0
  104. package/src/main/installNative.web.ts +5 -0
  105. package/src/mock.ts +60 -1
@@ -1,6 +1,8 @@
1
1
  package com.webgpu;
2
2
 
3
3
  import android.graphics.Bitmap;
4
+ import android.hardware.HardwareBuffer;
5
+ import android.os.Build;
4
6
  import android.util.Log;
5
7
  import android.view.View;
6
8
 
@@ -57,9 +59,12 @@ public class WebGPUModule extends NativeWebGPUModuleSpec {
57
59
  * Called from C++ (AndroidPlatformContext::snapshotView) on an arbitrary
58
60
  * thread for GPUQueue.drawElementImageToTexture. Hops to the UI thread,
59
61
  * resolves the view for {@code tag}, rasterizes the requested rectangle and
60
- * always reports back through {@link #onViewSnapshot}, with either a bitmap
61
- * or an error message. {@code callback} is an opaque native pointer that
62
- * must be handed back exactly once.
62
+ * always reports back exactly once: through {@link #onViewSnapshotBuffer}
63
+ * with the buffer the hardware renderer drew into, when the caller allows
64
+ * it and the platform can tell when the drawing is done (API 33), or else
65
+ * through {@link #onViewSnapshot} with a bitmap or an error message.
66
+ * {@code callback} is an opaque native pointer that must be handed back
67
+ * exactly once.
63
68
  *
64
69
  * The source rectangle is in density-independent points (React Native
65
70
  * layout units) and is converted to pixels here; a non-positive
@@ -75,11 +80,15 @@ public class WebGPUModule extends NativeWebGPUModuleSpec {
75
80
  final double sourceHeight,
76
81
  final int width,
77
82
  final int height,
83
+ final boolean allowHardwareBuffer,
78
84
  final long callback) {
79
85
  UiThreadUtil.runOnUiThread(
80
86
  () -> {
81
87
  Bitmap bitmap = null;
82
88
  String error = null;
89
+ // Set once the callback was handed to onViewSnapshotBuffer; nothing
90
+ // may report again after that.
91
+ final boolean[] handedOver = {false};
83
92
  try {
84
93
  ReactApplicationContext context = getReactApplicationContext();
85
94
  UIManager uiManager = UIManagerHelper.getUIManagerForReactTag(context, tag);
@@ -105,17 +114,72 @@ public class WebGPUModule extends NativeWebGPUModuleSpec {
105
114
  throw new IllegalArgumentException(
106
115
  "drawElementImageToTexture: the source rectangle is empty");
107
116
  }
108
- bitmap = ViewSnapshot.snapshot(view, sx, sy, sw, sh, w, h);
117
+ // The zero-copy path: the renderer's buffer goes to the GPU with
118
+ // the fence that says when the drawing is done. The fence is
119
+ // exposed from API 33; before that, and when the hardware render
120
+ // fails, the pixels are read back into a bitmap as usual.
121
+ if (allowHardwareBuffer
122
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
123
+ && view.isHardwareAccelerated()) {
124
+ try {
125
+ ViewSnapshot.renderHardware(
126
+ view,
127
+ sx,
128
+ sy,
129
+ sw,
130
+ sh,
131
+ w,
132
+ h,
133
+ image -> {
134
+ HardwareBuffer buffer = image.getHardwareBuffer();
135
+ if (buffer == null) {
136
+ throw new IllegalStateException(
137
+ "HardwareRenderer image has no HardwareBuffer");
138
+ }
139
+ int fenceFd = SyncFences.acquireFenceFd(image);
140
+ // Native acquires its own reference to the buffer and
141
+ // adopts the fence descriptor.
142
+ handedOver[0] = true;
143
+ onViewSnapshotBuffer(callback, buffer, fenceFd);
144
+ buffer.close();
145
+ return null;
146
+ });
147
+ } catch (Throwable t) {
148
+ if (handedOver[0]) {
149
+ throw t;
150
+ }
151
+ Log.w(NAME, "HardwareRenderer snapshot failed, reading the pixels back instead", t);
152
+ }
153
+ }
154
+ if (!handedOver[0]) {
155
+ bitmap = ViewSnapshot.snapshot(view, sx, sy, sw, sh, w, h);
156
+ }
109
157
  } catch (Throwable t) {
158
+ if (handedOver[0]) {
159
+ // The result is already on its way; only the cleanup failed.
160
+ Log.w(NAME, "drawElementImageToTexture: cleanup after the snapshot failed", t);
161
+ return;
162
+ }
110
163
  error = t.getMessage() != null ? t.getMessage() : t.toString();
111
164
  }
112
- onViewSnapshot(callback, bitmap, error);
165
+ if (!handedOver[0]) {
166
+ onViewSnapshot(callback, bitmap, error);
167
+ }
113
168
  });
114
169
  }
115
170
 
116
171
  @DoNotStrip
117
172
  private native void onViewSnapshot(long callback, Bitmap bitmap, String error);
118
173
 
174
+ /**
175
+ * The zero-copy completion of {@link #snapshotView}: the buffer the hardware
176
+ * renderer drew the view into and the fence that signals when the drawing
177
+ * is done (a sync file descriptor native takes ownership of, -1 for none).
178
+ * Native acquires its own reference to the buffer before returning.
179
+ */
180
+ @DoNotStrip
181
+ private native void onViewSnapshotBuffer(long callback, HardwareBuffer buffer, int fenceFd);
182
+
119
183
  /**
120
184
  * Called from C++ (AndroidPlatformContext::createVideoPlayer) for
121
185
  * RNWebGPU.createVideoPlayer. Throws when the source cannot be opened; the
@@ -61,6 +61,23 @@ public class WebGPUVideoPlayer {
61
61
  private volatile int rotation = 0;
62
62
  private volatile double frameRate = 0;
63
63
  private volatile double durationSeconds = 0;
64
+ // The color space of the video track as the container declares it, packed
65
+ // like a data space (see dataSpaceFromContainer); 0 when it says nothing.
66
+ private volatile int containerDataSpace = 0;
67
+
68
+ // The layout of android.hardware.DataSpace (API 33), which is also the
69
+ // ADataSpace of the NDK: standard << 16, transfer << 22, range << 27. Spelled
70
+ // out so that the mapping compiles and runs on every supported API level.
71
+ private static final int DATASPACE_STANDARD_BT709 = 1 << 16;
72
+ private static final int DATASPACE_STANDARD_BT601_625 = 2 << 16;
73
+ private static final int DATASPACE_STANDARD_BT601_525 = 4 << 16;
74
+ private static final int DATASPACE_STANDARD_BT2020 = 6 << 16;
75
+ private static final int DATASPACE_TRANSFER_LINEAR = 1 << 22;
76
+ private static final int DATASPACE_TRANSFER_SMPTE_170M = 3 << 22;
77
+ private static final int DATASPACE_TRANSFER_ST2084 = 7 << 22;
78
+ private static final int DATASPACE_TRANSFER_HLG = 8 << 22;
79
+ private static final int DATASPACE_RANGE_FULL = 1 << 27;
80
+ private static final int DATASPACE_RANGE_LIMITED = 2 << 27;
64
81
 
65
82
  WebGPUVideoPlayer(Context context, String source) {
66
83
  Uri uri = parseSource(source);
@@ -160,6 +177,17 @@ public class WebGPUVideoPlayer {
160
177
  if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) {
161
178
  frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE);
162
179
  }
180
+ containerDataSpace =
181
+ dataSpaceFromContainer(
182
+ format.containsKey(MediaFormat.KEY_COLOR_STANDARD)
183
+ ? format.getInteger(MediaFormat.KEY_COLOR_STANDARD)
184
+ : 0,
185
+ format.containsKey(MediaFormat.KEY_COLOR_TRANSFER)
186
+ ? format.getInteger(MediaFormat.KEY_COLOR_TRANSFER)
187
+ : 0,
188
+ format.containsKey(MediaFormat.KEY_COLOR_RANGE)
189
+ ? format.getInteger(MediaFormat.KEY_COLOR_RANGE)
190
+ : 0);
163
191
  break;
164
192
  }
165
193
  }
@@ -185,6 +213,77 @@ public class WebGPUVideoPlayer {
185
213
  }
186
214
  }
187
215
 
216
+ // The MediaFormat color keys of the video track (KEY_COLOR_STANDARD,
217
+ // KEY_COLOR_TRANSFER, KEY_COLOR_RANGE; 0 when absent) packed like a data
218
+ // space, so that the native side reads one representation whatever its
219
+ // source.
220
+ private static int dataSpaceFromContainer(int standard, int transfer, int range) {
221
+ int dataSpace = 0;
222
+ switch (standard) {
223
+ case MediaFormat.COLOR_STANDARD_BT709:
224
+ dataSpace |= DATASPACE_STANDARD_BT709;
225
+ break;
226
+ case MediaFormat.COLOR_STANDARD_BT601_PAL:
227
+ dataSpace |= DATASPACE_STANDARD_BT601_625;
228
+ break;
229
+ case MediaFormat.COLOR_STANDARD_BT601_NTSC:
230
+ dataSpace |= DATASPACE_STANDARD_BT601_525;
231
+ break;
232
+ case MediaFormat.COLOR_STANDARD_BT2020:
233
+ dataSpace |= DATASPACE_STANDARD_BT2020;
234
+ break;
235
+ default:
236
+ break;
237
+ }
238
+ switch (transfer) {
239
+ case MediaFormat.COLOR_TRANSFER_LINEAR:
240
+ dataSpace |= DATASPACE_TRANSFER_LINEAR;
241
+ break;
242
+ case MediaFormat.COLOR_TRANSFER_SDR_VIDEO:
243
+ dataSpace |= DATASPACE_TRANSFER_SMPTE_170M;
244
+ break;
245
+ case MediaFormat.COLOR_TRANSFER_ST2084:
246
+ dataSpace |= DATASPACE_TRANSFER_ST2084;
247
+ break;
248
+ case MediaFormat.COLOR_TRANSFER_HLG:
249
+ dataSpace |= DATASPACE_TRANSFER_HLG;
250
+ break;
251
+ default:
252
+ break;
253
+ }
254
+ switch (range) {
255
+ case MediaFormat.COLOR_RANGE_FULL:
256
+ dataSpace |= DATASPACE_RANGE_FULL;
257
+ break;
258
+ case MediaFormat.COLOR_RANGE_LIMITED:
259
+ dataSpace |= DATASPACE_RANGE_LIMITED;
260
+ break;
261
+ default:
262
+ break;
263
+ }
264
+ return dataSpace;
265
+ }
266
+
267
+ /**
268
+ * The color space of a frame returned by {@link #copyLatestFrame}, as a data space (the
269
+ * packing of {@code android.hardware.DataSpace}): the one the decoder stamps on the frame
270
+ * (API 33), or what the container declares for the video track. 0 when neither says anything.
271
+ */
272
+ @DoNotStrip
273
+ public int frameDataSpace(Image image) {
274
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
275
+ try {
276
+ int dataSpace = image.getDataSpace();
277
+ if (dataSpace != 0) {
278
+ return dataSpace;
279
+ }
280
+ } catch (IllegalStateException e) {
281
+ // The frame was closed under us; fall back to the container.
282
+ }
283
+ }
284
+ return containerDataSpace;
285
+ }
286
+
188
287
  // The decoder reports the frame size (possibly more than once for adaptive
189
288
  // streams); the ImageReader that receives the frames is sized accordingly.
190
289
  private void onVideoSizeChanged(int w, int h) {
@@ -273,6 +372,16 @@ public class WebGPUVideoPlayer {
273
372
  return image;
274
373
  }
275
374
 
375
+ /**
376
+ * The acquire fence of a frame returned by {@link #copyLatestFrame}, as a sync file descriptor
377
+ * the caller owns, or -1 when there is none. The decoder may still be writing the frame when it
378
+ * is acquired; the GPU work that samples it has to wait on this fence (see {@link SyncFences}).
379
+ */
380
+ @DoNotStrip
381
+ public static int acquireFenceFd(Image image) {
382
+ return SyncFences.acquireFenceFd(image);
383
+ }
384
+
276
385
  /** Hands the buffer of a frame returned by {@link #copyLatestFrame} back to the decoder. */
277
386
  @DoNotStrip
278
387
  public void closeImage(Image image) {
@@ -17,7 +17,8 @@ public:
17
17
  ~ApplePlatformContext() = default;
18
18
 
19
19
  void snapshotView(const ViewSnapshotRequest &request,
20
- std::function<void(ImageData)> onSuccess,
20
+ std::function<void(ImageData)> onPixels,
21
+ std::function<void(VideoFrameHandle)> onFrame,
21
22
  std::function<void(std::string)> onError) override;
22
23
 
23
24
  ImageData createImageBitmap(std::string blobId, double offset,
@@ -36,12 +37,11 @@ public:
36
37
 
37
38
  VideoFrameHandle loadVideoFrame(const std::string &path) override;
38
39
 
39
- VideoFrameHandle createTestVideoFrame(uint32_t width,
40
- uint32_t height) override;
40
+ VideoFrameHandle createTestVideoFrame(uint32_t width, uint32_t height,
41
+ VideoPixelFormat format) override;
41
42
 
42
43
  std::unique_ptr<IVideoPlayer>
43
- createVideoPlayer(const std::string &path,
44
- VideoPixelFormat format) override;
44
+ createVideoPlayer(const std::string &path, VideoPixelFormat format) override;
45
45
 
46
46
  std::string writeTestVideoFile() override;
47
47
 
@@ -189,9 +189,12 @@ ImageData snapshotViewOnMainThread(const ApplePlatformContext::ViewLookup &looku
189
189
 
190
190
  } // namespace
191
191
 
192
+ // Always delivers pixels: the view is drawn into a CPU bitmap, so there is no
193
+ // GPU frame to hand over (onFrame is unused).
192
194
  void ApplePlatformContext::snapshotView(
193
195
  const ViewSnapshotRequest &request,
194
- std::function<void(ImageData)> onSuccess,
196
+ std::function<void(ImageData)> onPixels,
197
+ std::function<void(VideoFrameHandle)> /* onFrame */,
195
198
  std::function<void(std::string)> onError) {
196
199
  // Copies: the block outlives this call.
197
200
  ViewLookup lookup = _viewLookup;
@@ -199,7 +202,7 @@ void ApplePlatformContext::snapshotView(
199
202
  dispatch_async(dispatch_get_main_queue(), ^{
200
203
  @autoreleasepool {
201
204
  try {
202
- onSuccess(snapshotViewOnMainThread(lookup, snapshotRequest));
205
+ onPixels(snapshotViewOnMainThread(lookup, snapshotRequest));
203
206
  } catch (const std::exception &error) {
204
207
  onError(error.what());
205
208
  } catch (...) {
@@ -414,8 +417,77 @@ VideoFrameHandle ApplePlatformContext::wrapNativeBuffer(void *pointer) {
414
417
  return wrapCVPixelBuffer(static_cast<CVPixelBufferRef>(pointer));
415
418
  }
416
419
 
420
+ // The NV12 variant of createTestVideoFrame: a biplanar BT.709 limited-range
421
+ // frame, red on the left half and green on the right, whose attachments
422
+ // declare its color space the way the decoder's frames do, so that the
423
+ // conversion of importExternalTexture is fully determined.
424
+ static VideoFrameHandle createTestYuvFrame(uint32_t width, uint32_t height) {
425
+ NSDictionary *attrs = @{
426
+ (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{},
427
+ (NSString *)kCVPixelBufferMetalCompatibilityKey : @YES,
428
+ };
429
+ CVPixelBufferRef pixelBuffer = NULL;
430
+ CVReturn err =
431
+ CVPixelBufferCreate(kCFAllocatorDefault, width, height,
432
+ kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
433
+ (__bridge CFDictionaryRef)attrs, &pixelBuffer);
434
+ if (err != kCVReturnSuccess || !pixelBuffer) {
435
+ throw std::runtime_error("createTestVideoFrame: unsupported: "
436
+ "CVPixelBufferCreate failed for a biplanar "
437
+ "YUV buffer");
438
+ }
439
+ CVBufferSetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey,
440
+ kCVImageBufferYCbCrMatrix_ITU_R_709_2,
441
+ kCVAttachmentMode_ShouldPropagate);
442
+ CVBufferSetAttachment(pixelBuffer, kCVImageBufferColorPrimariesKey,
443
+ kCVImageBufferColorPrimaries_ITU_R_709_2,
444
+ kCVAttachmentMode_ShouldPropagate);
445
+ CVBufferSetAttachment(pixelBuffer, kCVImageBufferTransferFunctionKey,
446
+ kCVImageBufferTransferFunction_ITU_R_709_2,
447
+ kCVAttachmentMode_ShouldPropagate);
448
+
449
+ CVPixelBufferLockBaseAddress(pixelBuffer, 0);
450
+ uint8_t *luma = static_cast<uint8_t *>(
451
+ CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0));
452
+ size_t lumaRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
453
+ for (uint32_t y = 0; y < height; ++y) {
454
+ uint8_t *row = luma + y * lumaRowBytes;
455
+ for (uint32_t x = 0; x < width; ++x) {
456
+ row[x] = x < width / 2 ? test_yuv_frame::kRedY : test_yuv_frame::kGreenY;
457
+ }
458
+ }
459
+ // The chroma plane is subsampled by two on both axes and interleaves Cb, Cr.
460
+ uint8_t *chroma = static_cast<uint8_t *>(
461
+ CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1));
462
+ size_t chromaRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
463
+ size_t chromaWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
464
+ size_t chromaHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
465
+ for (size_t y = 0; y < chromaHeight; ++y) {
466
+ uint8_t *row = chroma + y * chromaRowBytes;
467
+ for (size_t x = 0; x < chromaWidth; ++x) {
468
+ bool red = x < chromaWidth / 2;
469
+ row[x * 2 + 0] = red ? test_yuv_frame::kRedCb : test_yuv_frame::kGreenCb;
470
+ row[x * 2 + 1] = red ? test_yuv_frame::kRedCr : test_yuv_frame::kGreenCr;
471
+ }
472
+ }
473
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
474
+
475
+ try {
476
+ auto handle = wrapCVPixelBuffer(pixelBuffer);
477
+ CFRelease(pixelBuffer); // wrapCVPixelBuffer retained its own reference
478
+ return handle;
479
+ } catch (...) {
480
+ CFRelease(pixelBuffer);
481
+ throw;
482
+ }
483
+ }
484
+
417
485
  VideoFrameHandle
418
- ApplePlatformContext::createTestVideoFrame(uint32_t width, uint32_t height) {
486
+ ApplePlatformContext::createTestVideoFrame(uint32_t width, uint32_t height,
487
+ VideoPixelFormat format) {
488
+ if (format == VideoPixelFormat::NV12) {
489
+ return createTestYuvFrame(width, height);
490
+ }
419
491
  NSDictionary *attrs = @{
420
492
  (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{},
421
493
  (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;
@@ -0,0 +1,67 @@
1
+ // Copyright 2026 The Dawn & Tint Authors
2
+ //
3
+ // Redistribution and use in source and binary forms, with or without
4
+ // modification, are permitted provided that the following conditions are met:
5
+ //
6
+ // 1. Redistributions of source code must retain the above copyright notice, this
7
+ // list of conditions and the following disclaimer.
8
+ //
9
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ // this list of conditions and the following disclaimer in the documentation
11
+ // and/or other materials provided with the distribution.
12
+ //
13
+ // 3. Neither the name of the copyright holder nor the names of its
14
+ // contributors may be used to endorse or promote products derived from
15
+ // this software without specific prior written permission.
16
+ //
17
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+
28
+ // DAWN_VENDORED_EXTERNAL_TEXTURE_PARAMS
29
+ //
30
+ // Vendored from Dawn's src/dawn/common/ExternalTextureParams.h, unchanged. The
31
+ // functions it declares are compiled into the Dawn binaries that
32
+ // react-native-webgpu-dawn ships (libwebgpu_dawn.so, libwebgpu_dawn.a), which
33
+ // export them, but the package only ships Dawn's public headers. The copy is
34
+ // pinned to the Dawn commit of the package in scripts/check-dawn.ts: on a Dawn
35
+ // bump, diff it against the new commit's file and re-sync it.
36
+
37
+ #ifndef SRC_DAWN_COMMON_EXTERNALTEXTUREPARAMS_H_
38
+ #define SRC_DAWN_COMMON_EXTERNALTEXTUREPARAMS_H_
39
+
40
+ #include <webgpu/webgpu_cpp.h>
41
+
42
+ #include <array>
43
+
44
+ namespace dawn {
45
+
46
+ // Color space parameters that can be passed to an ExternalTexture.
47
+ struct ExternalTextureColorSpaceParams {
48
+ // Stored row-major to save uniform buffer space. (so one full row after another)
49
+ // TODO(https://crbug.com/496604550): That's an implementation detail that should not be exposed
50
+ // in the API, Dawn can transpose internally.
51
+ std::array<float, 12> yuvToRgbConversionMatrix{};
52
+ std::array<float, 7> srcTransferFunction = {};
53
+ std::array<float, 7> dstTransferFunction = {}; // Really the inverse transfer function.
54
+ // Stored column major to match WGSL. (so one full column after another)
55
+ std::array<float, 9> gamutConversionMatrix{};
56
+ };
57
+
58
+ // Helper function to make parameters for an ExternalTextureDescriptor
59
+ wgpu::Status ComputeExternalTextureParams(const wgpu::ColorSpaceDawn& srcColorSpace,
60
+ wgpu::PredefinedColorSpace dstColorSpace,
61
+ ExternalTextureColorSpaceParams* out);
62
+
63
+ ExternalTextureColorSpaceParams GetNoopColorSpaceParams();
64
+
65
+ } // namespace dawn
66
+
67
+ #endif // SRC_DAWN_COMMON_EXTERNALTEXTUREPARAMS_H_