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
@@ -3,13 +3,18 @@
3
3
  #include <array>
4
4
  #include <cmath>
5
5
  #include <memory>
6
+ #include <optional>
6
7
  #include <string>
7
8
  #include <utility>
8
9
 
9
10
  #include "GPUExternalTextureDescriptor.h"
10
11
 
12
+ #include "dawn/common/ExternalTextureParams.h"
13
+
11
14
  #if defined(__ANDROID__)
12
15
  #include <android/hardware_buffer.h>
16
+ #include <android/log.h>
17
+ #include <poll.h>
13
18
  #endif
14
19
 
15
20
  namespace rnwgpu {
@@ -49,117 +54,80 @@ static bool isOpaqueYCbCrFormat(uint32_t format) {
49
54
  }
50
55
  #endif
51
56
 
52
- // Identity gamut (BT.709 -> sRGB, same primaries) as a 3x3 column-major matrix.
53
- static const float kIdentityGamutMatrix[9] = {
54
- 1.0f, 0.0f, 0.0f, //
55
- 0.0f, 1.0f, 0.0f, //
56
- 0.0f, 0.0f, 1.0f, //
57
- };
58
-
59
- // Piecewise gamma transfer-function parameters Dawn expects:
60
- // for |x| < D: y = sign(x) * (C * |x| + F)
61
- // else : y = sign(x) * (pow(A * |x| + B, G) + E)
62
- // sRGB decode (encoded -> linear).
63
- static const float kSrgbDecodeParams[7] = {
64
- 2.4f, // G
65
- 1.0f / 1.055f, // A
66
- 0.055f / 1.055f, // B
67
- 1.0f / 12.92f, // C
68
- 0.04045f, // D
69
- 0.0f, // E
70
- 0.0f, // F
71
- };
72
- // sRGB encode (linear -> encoded).
73
- static const float kSrgbEncodeParams[7] = {
74
- 1.0f / 2.4f, // G
75
- 1.055f, // A
76
- 0.0f, // B
77
- 12.92f, // C
78
- 0.0031308f, // D
79
- -0.055f, // E
80
- 0.0f, // F
81
- };
82
-
83
- // Identity transfer (y = x). Used when the sampled surface is already in the
84
- // render target's color space: a single-plane BGRA IOSurface, or the Android
85
- // opaque-YCbCr path where the Vulkan sampler already produced RGB. Dawn
86
- // dereferences the transfer-function arrays unconditionally
87
- // (ComputeExternalTextureParams), so these must be non-null even when no
88
- // conversion is wanted.
89
- static const float kIdentityTransferParams[7] = {
90
- 1.0f, // G
91
- 1.0f, // A
92
- 0.0f, // B
93
- 0.0f, // C
94
- 0.0f, // D
95
- 0.0f, // E
96
- 0.0f, // F
97
- };
98
-
99
- // Identity passthrough for GPUExternalTexture.yuvToRgbMatrix: the sampled
100
- // texel is already RGB (Apple's biplanar path where Dawn converts in the
101
- // sampler transform, RGBA surfaces, or an Android AHB whose driver reports
102
- // RGB_IDENTITY). A 3x4 row-major matrix mapping [c.r, c.g, c.b, 1] to itself.
103
- static constexpr std::array<float, 12> kYuvPassthroughMatrix = {
104
- 1.0f, 0.0f, 0.0f, 0.0f, //
105
- 0.0f, 1.0f, 0.0f, 0.0f, //
106
- 0.0f, 0.0f, 1.0f, 0.0f, //
107
- };
108
-
109
- // Build a 3x4 row-major matrix mapping the *sampled* gamma-encoded
110
- // [Y, Cb, Cr, 1] (each normalized to [0,1]) to gamma-encoded R'G'B', from the
111
- // Vulkan-suggested YCbCr model + range of an Android external-format buffer.
112
- // Needed because Dawn's opaque-YCbCr sampling path hard-codes an RGB_IDENTITY
113
- // conversion (SamplerVk.cpp::GetYCbCrForTextureView, crbug.com/497675620), so
114
- // the shader receives raw Y/Cb/Cr and must convert itself.
115
- // The VkSamplerYcbcrModelConversion / VkSamplerYcbcrRange values are inlined
116
- // to avoid a Vulkan header dependency in this cross-platform file.
117
- [[maybe_unused]] static std::array<float, 12>
118
- makeYuvToRgbMatrix(uint32_t vkModel, uint32_t vkRange) {
119
- constexpr uint32_t kModelRgbIdentity = 0; // VK_..._RGB_IDENTITY
120
- constexpr uint32_t kModel709 = 2; // VK_..._YCBCR_709
121
- constexpr uint32_t kModel601 = 3; // VK_..._YCBCR_601
122
- constexpr uint32_t kModel2020 = 4; // VK_..._YCBCR_2020
123
- constexpr uint32_t kRangeItuNarrow = 1; // VK_SAMPLER_YCBCR_RANGE_ITU_NARROW
124
-
125
- if (vkModel == kModelRgbIdentity) {
126
- // The buffer content is RGB; nothing to convert.
127
- return kYuvPassthroughMatrix;
57
+ // The color space of a YUV frame with every member known. The producer reports
58
+ // what it knows (VideoFrameHandle.colorSpace); the matrix and range it leaves
59
+ // out come from `fallbackMatrix` / `fallbackRange`, the platform's best guess
60
+ // for the buffer, and the rest follows from the matrix: the primaries of the
61
+ // standard that defines it, and the SMPTE 170M transfer function that BT.601,
62
+ // BT.709 and BT.2020 SDR video is encoded with. An identity matrix means the
63
+ // samples are RGB already, which the identity transfer leaves untouched.
64
+ static wgpu::ColorSpaceDawn
65
+ resolveColorSpace(const VideoFrameColorSpace &reported,
66
+ wgpu::ColorSpaceYCbCrMatrixDawn fallbackMatrix,
67
+ wgpu::ColorSpaceYCbCrRangeDawn fallbackRange) {
68
+ wgpu::ColorSpaceDawn colorSpace{};
69
+ colorSpace.yCbCrMatrix = reported.matrix.value_or(fallbackMatrix);
70
+ colorSpace.yCbCrRange = reported.range.value_or(fallbackRange);
71
+ if (reported.primaries.has_value()) {
72
+ colorSpace.primaries = *reported.primaries;
73
+ } else {
74
+ switch (colorSpace.yCbCrMatrix) {
75
+ case wgpu::ColorSpaceYCbCrMatrixDawn::Rec601:
76
+ colorSpace.primaries = wgpu::ColorSpacePrimariesDawn::Rec601;
77
+ break;
78
+ case wgpu::ColorSpaceYCbCrMatrixDawn::Rec2020:
79
+ colorSpace.primaries = wgpu::ColorSpacePrimariesDawn::Rec2020;
80
+ break;
81
+ default:
82
+ // BT.709 shares the sRGB primaries (Dawn's helper only knows them under
83
+ // that name).
84
+ colorSpace.primaries = wgpu::ColorSpacePrimariesDawn::SRGB;
85
+ break;
86
+ }
128
87
  }
129
- float kr;
130
- float kb;
131
- switch (vkModel) {
132
- case kModel709:
133
- kr = 0.2126f;
134
- kb = 0.0722f;
135
- break;
136
- case kModel2020:
137
- kr = 0.2627f;
138
- kb = 0.0593f;
139
- break;
140
- case kModel601:
141
- default:
142
- // YCBCR_IDENTITY and unknown models: Android camera streams are BT.601 in
143
- // practice, so that is the sane default.
144
- kr = 0.299f;
145
- kb = 0.114f;
146
- break;
88
+ if (reported.transfer.has_value()) {
89
+ colorSpace.transfer = *reported.transfer;
90
+ } else {
91
+ colorSpace.transfer =
92
+ colorSpace.yCbCrMatrix == wgpu::ColorSpaceYCbCrMatrixDawn::Identity
93
+ ? wgpu::ColorSpaceTransferDawn::Identity
94
+ : wgpu::ColorSpaceTransferDawn::SMPTE_170M;
147
95
  }
148
- const float kg = 1.0f - kr - kb;
149
- const bool narrow = vkRange == kRangeItuNarrow;
150
- const float yScale = narrow ? 255.0f / 219.0f : 1.0f;
151
- const float cScale = narrow ? 255.0f / 224.0f : 1.0f;
152
- const float yOffset = narrow ? 16.0f / 255.0f : 0.0f;
153
- const float cOffset = 128.0f / 255.0f;
154
- const float crR = 2.0f * (1.0f - kr) * cScale;
155
- const float cbG = -2.0f * kb * (1.0f - kb) / kg * cScale;
156
- const float crG = -2.0f * kr * (1.0f - kr) / kg * cScale;
157
- const float cbB = 2.0f * (1.0f - kb) * cScale;
158
- return {
159
- yScale, 0.0f, crR, -yScale * yOffset - crR * cOffset, //
160
- yScale, cbG, crG, -yScale * yOffset - (cbG + crG) * cOffset, //
161
- yScale, cbB, 0.0f, -yScale * yOffset - cbB * cOffset, //
162
- };
96
+ return colorSpace;
97
+ }
98
+
99
+ // The ExternalTextureDescriptor parameters that convert samples in `source` to
100
+ // RGB in the color space the descriptor asks for ("srgb" by default,
101
+ // "display-p3"): the YCbCr matrix, the range, the source transfer function,
102
+ // the gamut conversion and the destination transfer function. Dawn applies
103
+ // them while sampling the external texture. The helper is Dawn's own, the one
104
+ // Chromium feeds video frames through.
105
+ static dawn::ExternalTextureColorSpaceParams
106
+ colorSpaceParams(const wgpu::ColorSpaceDawn &source,
107
+ const std::optional<std::string> &destination) {
108
+ auto target = destination.has_value() && *destination == "display-p3"
109
+ ? wgpu::PredefinedColorSpace::DisplayP3
110
+ : wgpu::PredefinedColorSpace::SRGB;
111
+ dawn::ExternalTextureColorSpaceParams params;
112
+ if (dawn::ComputeExternalTextureParams(source, target, &params) !=
113
+ wgpu::Status::Success) {
114
+ throw std::runtime_error(
115
+ "GPUExternalTexture::Create(): the color space of the frame cannot be "
116
+ "converted to '" +
117
+ destination.value_or("srgb") + "'");
118
+ }
119
+ return params;
120
+ }
121
+
122
+ // Dawn dereferences the four arrays unconditionally, so a descriptor always
123
+ // carries a full set, the no-op one for frames that are RGB already.
124
+ static void
125
+ applyColorSpaceParams(wgpu::ExternalTextureDescriptor &descriptor,
126
+ const dawn::ExternalTextureColorSpaceParams &params) {
127
+ descriptor.yuvToRgbConversionMatrix = params.yuvToRgbConversionMatrix.data();
128
+ descriptor.srcTransferFunctionParameters = params.srcTransferFunction.data();
129
+ descriptor.dstTransferFunctionParameters = params.dstTransferFunction.data();
130
+ descriptor.gamutConversionMatrix = params.gamutConversionMatrix.data();
163
131
  }
164
132
 
165
133
  // Map a rotation in degrees (0 / 90 / 180 / 270) to Dawn's enum. Anything that
@@ -250,26 +218,27 @@ std::shared_ptr<GPUExternalTexture> GPUExternalTexture::Create(
250
218
  plane1 = texture.CreateView(&v);
251
219
  }
252
220
 
253
- // 5. Build the ExternalTextureDescriptor. We hand Dawn explicit YUV→RGB and
254
- // sRGB transfer-function parameters so the sampler does the full color
255
- // conversion in hardware.
221
+ // 5. Build the ExternalTextureDescriptor. Dawn converts the YUV planes to
222
+ // RGB in the destination color space while sampling, with the matrix,
223
+ // range, transfer function and gamut of the frame's color space (from the
224
+ // CVPixelBuffer attachments; BT.709 limited range, the norm for HD video,
225
+ // where they are missing). A BGRA frame is RGB already: no-op parameters.
226
+ dawn::ExternalTextureColorSpaceParams params =
227
+ isYuv ? colorSpaceParams(
228
+ resolveColorSpace(frame.colorSpace,
229
+ wgpu::ColorSpaceYCbCrMatrixDawn::Rec709,
230
+ wgpu::ColorSpaceYCbCrRangeDawn::Narrow),
231
+ descriptor->colorSpace)
232
+ : dawn::GetNoopColorSpaceParams();
256
233
  wgpu::ExternalTextureDescriptor extDesc{};
257
234
  if (!label.empty()) {
258
235
  extDesc.label = wgpu::StringView(label.c_str(), label.size());
259
236
  }
260
237
  extDesc.plane0 = plane0;
261
- extDesc.gamutConversionMatrix = kIdentityGamutMatrix;
262
238
  if (isYuv) {
263
239
  extDesc.plane1 = plane1;
264
- extDesc.yuvToRgbConversionMatrix = frame.yuvToRgbMatrix;
265
- extDesc.srcTransferFunctionParameters = kSrgbDecodeParams;
266
- extDesc.dstTransferFunctionParameters = kSrgbEncodeParams;
267
- } else {
268
- // BGRA is already RGB in the target color space; pass it through. Dawn
269
- // dereferences these arrays unconditionally, so they must be non-null.
270
- extDesc.srcTransferFunctionParameters = kIdentityTransferParams;
271
- extDesc.dstTransferFunctionParameters = kIdentityTransferParams;
272
240
  }
241
+ applyColorSpaceParams(extDesc, params);
273
242
  extDesc.cropOrigin = {0, 0};
274
243
  extDesc.cropSize = {frame.width, frame.height};
275
244
  extDesc.apparentSize = {frame.width, frame.height};
@@ -287,11 +256,7 @@ std::shared_ptr<GPUExternalTexture> GPUExternalTexture::Create(
287
256
 
288
257
  return std::make_shared<GPUExternalTexture>(
289
258
  std::move(external), std::move(memory), std::move(texture),
290
- std::move(descriptor->source), std::move(label),
291
- // Dawn's Metal path applies the YUV->RGB conversion (from
292
- // frame.yuvToRgbMatrix above) inside the sampling transform, so the
293
- // sampled texel is already RGB.
294
- kYuvPassthroughMatrix);
259
+ std::move(descriptor->source), std::move(label));
295
260
  #elif defined(__ANDROID__)
296
261
  // 0. A YUV frame imports as an OpaqueYCbCrAndroid texture, which Dawn only
297
262
  // accepts on a device with OpaqueYCbCrAndroidForExternalTexture.
@@ -344,49 +309,103 @@ std::shared_ptr<GPUExternalTexture> GPUExternalTexture::Create(
344
309
  // 3. Begin access. Vulkan requires us to advertise the incoming VkImage
345
310
  // layout (UNDEFINED is fine for the first acquisition of an AHB whose
346
311
  // contents we expect Dawn to read as-is).
312
+ //
313
+ // The producer of the frame (the video decoder, the camera) may still be
314
+ // writing it: ImageReader hands a buffer out as soon as it is queued, and
315
+ // only waits on the producer's fence for CPU access. The fence travels
316
+ // with the frame as a sync fd; imported as a shared fence, BeginAccess
317
+ // makes the GPU wait on it before any work that samples the texture.
318
+ // Dawn duplicates the descriptor on import; the frame keeps its own.
347
319
  wgpu::SharedTextureMemoryBeginAccessDescriptor begin{};
348
320
  begin.initialized = true;
349
321
  begin.concurrentRead = false;
350
322
  wgpu::SharedTextureMemoryVkImageLayoutBeginState beginLayout{};
351
323
  begin.nextInChain = &beginLayout;
324
+ wgpu::SharedFence acquireFence;
325
+ const uint64_t kBinaryFenceSignaled = 1;
326
+ if (frame.acquireFenceFd >= 0) {
327
+ if (device.HasFeature(wgpu::FeatureName::SharedFenceSyncFD)) {
328
+ wgpu::SharedFenceSyncFDDescriptor fdDesc{};
329
+ fdDesc.handle = frame.acquireFenceFd;
330
+ wgpu::SharedFenceDescriptor fenceDesc{};
331
+ fenceDesc.nextInChain = &fdDesc;
332
+ acquireFence = device.ImportSharedFence(&fenceDesc);
333
+ if (acquireFence) {
334
+ begin.fenceCount = 1;
335
+ begin.fences = &acquireFence;
336
+ begin.signaledValueCount = 1;
337
+ begin.signaledValues = &kBinaryFenceSignaled;
338
+ }
339
+ } else {
340
+ // A device without shared-fence-sync-fd (imported from another library
341
+ // that did not request it) cannot wait on the GPU: wait here instead. A
342
+ // sync fd becomes readable once it signals.
343
+ pollfd pfd{};
344
+ pfd.fd = frame.acquireFenceFd;
345
+ pfd.events = POLLIN;
346
+ int polled = poll(&pfd, 1, 3000);
347
+ if (polled <= 0) {
348
+ __android_log_print(ANDROID_LOG_WARN, "GPUExternalTexture",
349
+ "the acquire fence of the frame did not signal "
350
+ "within 3s; sampling it anyway");
351
+ }
352
+ }
353
+ }
352
354
  if (memory.BeginAccess(texture, &begin) != wgpu::Status::Success) {
353
355
  throw std::runtime_error(
354
356
  "GPUExternalTexture::Create(): BeginAccess failed");
355
357
  }
356
358
 
357
- // 4. Resolve the YUV->RGB conversion the *shader* must apply. Dawn imports
358
- // every YUV / implementation-defined AHB format as OpaqueYCbCrAndroid
359
- // (AHBFunctions.cpp::FormatFromAHardwareBufferFormat) and samples it
360
- // through a Vulkan SamplerYcbcrConversion that is hard-coded to
361
- // RGB_IDENTITY (SamplerVk.cpp::GetYCbCrForTextureView,
362
- // crbug.com/497675620), so the sample always comes back as raw
363
- // [Y, Cb, Cr]. The driver's *suggested* model + range are captured at
364
- // import time though; read them back from the shared memory's properties
365
- // and derive the correct conversion matrix, exposed to JS as
366
- // GPUExternalTexture.yuvToRgbMatrix. Defined color formats (e.g. an RGBA
367
- // AHB -> RGBA8Unorm) sample as RGB directly and get the passthrough.
368
- std::array<float, 12> yuvToRgbMatrix = kYuvPassthroughMatrix;
359
+ // 4. The color conversion. Dawn imports every YUV AHardwareBuffer as an
360
+ // OpaqueYCbCrAndroid texture and samples it through a Vulkan YCbCr
361
+ // sampler whose own conversion is the identity (SamplerVk.cpp), so the
362
+ // sample is raw Y, Cb, Cr; the external texture then applies the
363
+ // descriptor's YCbCr matrix, transfer functions and gamut conversion
364
+ // while sampling (Dawn's YUV AHB specialization), the same way it does
365
+ // for the biplanar textures of the other backends. The frame's color
366
+ // space comes from its producer (Image.getDataSpace on API 33, the
367
+ // container's color info before that); what it leaves unknown falls back
368
+ // to the YCbCr model and range the driver suggests for the buffer, read
369
+ // from the shared memory's properties, and to BT.601 limited range, the
370
+ // camera norm, when the driver has no opinion. A color-format AHB (RGBA8,
371
+ // R8, ...) samples as RGB directly: no-op parameters.
372
+ dawn::ExternalTextureColorSpaceParams params =
373
+ dawn::GetNoopColorSpaceParams();
369
374
  if (texture.GetFormat() == wgpu::TextureFormat::OpaqueYCbCrAndroid) {
375
+ auto fallbackMatrix = wgpu::ColorSpaceYCbCrMatrixDawn::Rec601;
376
+ auto fallbackRange = wgpu::ColorSpaceYCbCrRangeDawn::Narrow;
370
377
  wgpu::SharedTextureMemoryAHardwareBufferProperties ahbProps{};
371
378
  wgpu::SharedTextureMemoryProperties props{};
372
379
  props.nextInChain = &ahbProps;
373
380
  if (memory.GetProperties(&props) == wgpu::Status::Success) {
374
- yuvToRgbMatrix = makeYuvToRgbMatrix(ahbProps.yCbCrInfo.vkYCbCrModel,
375
- ahbProps.yCbCrInfo.vkYCbCrRange);
376
- } else {
377
- // Fall back to the Android camera norm rather than passthrough.
378
- yuvToRgbMatrix = makeYuvToRgbMatrix(/* YCBCR_601 */ 3,
379
- /* ITU_NARROW */ 1);
381
+ // VkSamplerYcbcrModelConversion / VkSamplerYcbcrRange values, inlined
382
+ // to keep this cross-platform file free of Vulkan headers.
383
+ switch (ahbProps.yCbCrInfo.vkYCbCrModel) {
384
+ case 0: // VK_SAMPLER_YCBCR_MODEL_CONVERSION_RGB_IDENTITY: RGB data
385
+ fallbackMatrix = wgpu::ColorSpaceYCbCrMatrixDawn::Identity;
386
+ fallbackRange = wgpu::ColorSpaceYCbCrRangeDawn::Identity;
387
+ break;
388
+ case 2: // VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_709
389
+ fallbackMatrix = wgpu::ColorSpaceYCbCrMatrixDawn::Rec709;
390
+ break;
391
+ case 4: // VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_2020
392
+ fallbackMatrix = wgpu::ColorSpaceYCbCrMatrixDawn::Rec2020;
393
+ break;
394
+ default: // YCBCR_601, YCBCR_IDENTITY and unknown models
395
+ break;
396
+ }
397
+ if (fallbackMatrix != wgpu::ColorSpaceYCbCrMatrixDawn::Identity) {
398
+ fallbackRange = ahbProps.yCbCrInfo.vkYCbCrRange == 0
399
+ ? wgpu::ColorSpaceYCbCrRangeDawn::Full
400
+ : wgpu::ColorSpaceYCbCrRangeDawn::Narrow;
401
+ }
380
402
  }
403
+ params = colorSpaceParams(
404
+ resolveColorSpace(frame.colorSpace, fallbackMatrix, fallbackRange),
405
+ descriptor->colorSpace);
381
406
  }
382
407
 
383
- // 5. Build the ExternalTextureDescriptor: a single (opaque or RGB) plane
384
- // with identity transfer. The YUV->RGB conversion cannot run inside
385
- // Dawn's external-texture transform here: the single-plane branch of the
386
- // Tint transform ignores yuvToRgbConversionMatrix, which is why the
387
- // matrix is surfaced to the shader instead. The gamut/transfer arrays
388
- // must still be non-null: ComputeExternalTextureParams dereferences them
389
- // unconditionally.
408
+ // 5. Build the ExternalTextureDescriptor: a single (opaque or RGB) plane.
390
409
  wgpu::ExternalTextureDescriptor extDesc{};
391
410
  if (!label.empty()) {
392
411
  extDesc.label = wgpu::StringView(label.c_str(), label.size());
@@ -394,11 +413,9 @@ std::shared_ptr<GPUExternalTexture> GPUExternalTexture::Create(
394
413
  extDesc.cropOrigin = {0, 0};
395
414
  extDesc.cropSize = {frame.width, frame.height};
396
415
  extDesc.apparentSize = {frame.width, frame.height};
397
- extDesc.gamutConversionMatrix = kIdentityGamutMatrix;
398
416
  wgpu::TextureView plane0 = texture.CreateView();
399
417
  extDesc.plane0 = plane0;
400
- extDesc.srcTransferFunctionParameters = kIdentityTransferParams;
401
- extDesc.dstTransferFunctionParameters = kIdentityTransferParams;
418
+ applyColorSpaceParams(extDesc, params);
402
419
  extDesc.mirrored = descriptor->mirrored.value_or(false);
403
420
  extDesc.rotation =
404
421
  toExternalTextureRotation(descriptor->rotation.value_or(0));
@@ -413,8 +430,7 @@ std::shared_ptr<GPUExternalTexture> GPUExternalTexture::Create(
413
430
 
414
431
  return std::make_shared<GPUExternalTexture>(
415
432
  std::move(external), std::move(memory), std::move(texture),
416
- std::move(descriptor->source), std::move(label),
417
- std::move(yuvToRgbMatrix));
433
+ std::move(descriptor->source), std::move(label));
418
434
  #else
419
435
  throw std::runtime_error(
420
436
  "GPUExternalTexture::Create(): not yet implemented on this "
@@ -38,12 +38,10 @@ public:
38
38
  // the producer (e.g. AVPlayer) can reclaim the IOSurface.
39
39
  GPUExternalTexture(wgpu::ExternalTexture instance,
40
40
  wgpu::SharedTextureMemory memory, wgpu::Texture texture,
41
- std::shared_ptr<VideoFrame> source, std::string label,
42
- std::array<float, 12> yuvToRgbMatrix)
41
+ std::shared_ptr<VideoFrame> source, std::string label)
43
42
  : NativeObject(CLASS_NAME), _instance(std::move(instance)),
44
43
  _memory(std::move(memory)), _texture(std::move(texture)),
45
- _source(std::move(source)), _label(std::move(label)),
46
- _yuvToRgbMatrix(yuvToRgbMatrix) {}
44
+ _source(std::move(source)), _label(std::move(label)) {}
47
45
 
48
46
  ~GPUExternalTexture() override { destroy(); }
49
47
 
@@ -93,25 +91,11 @@ public:
93
91
  }
94
92
  }
95
93
 
96
- // Non-spec extension. A 3x4 row-major matrix mapping the *sampled* texel
97
- // [c.r, c.g, c.b, 1] to gamma-encoded R'G'B'. When Dawn's sampler already
98
- // produces RGB (Apple's biplanar path, RGBA surfaces), this is the identity
99
- // passthrough; on the Android opaque-YCbCr path the sample comes back as raw
100
- // [Y, Cb, Cr] (Dawn hard-codes an RGB_IDENTITY Vulkan conversion, see
101
- // crbug.com/497675620) and this matrix is derived from the driver's
102
- // suggested YCbCr model + range for the buffer. Shaders can therefore apply
103
- // it unconditionally after textureSampleBaseClampToEdge.
104
- std::vector<double> getYuvToRgbMatrix() {
105
- return std::vector<double>(_yuvToRgbMatrix.begin(), _yuvToRgbMatrix.end());
106
- }
107
-
108
94
  static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) {
109
95
  installGetter(runtime, prototype, "__brand", &GPUExternalTexture::getBrand);
110
96
  installGetterSetter(runtime, prototype, "label",
111
97
  &GPUExternalTexture::getLabel,
112
98
  &GPUExternalTexture::setLabel);
113
- installGetter(runtime, prototype, "yuvToRgbMatrix",
114
- &GPUExternalTexture::getYuvToRgbMatrix);
115
99
  installMethod(runtime, prototype, "destroy", &GPUExternalTexture::destroy);
116
100
  }
117
101
 
@@ -140,7 +124,6 @@ private:
140
124
  wgpu::Texture _texture;
141
125
  std::shared_ptr<VideoFrame> _source;
142
126
  std::string _label;
143
- std::array<float, 12> _yuvToRgbMatrix;
144
127
  };
145
128
 
146
129
  } // namespace rnwgpu
@@ -256,6 +256,26 @@ void GPUQueue::copyExternalImageToTexture(
256
256
  }
257
257
  }
258
258
 
259
+ // Releases a frame the platform handed over that is not going to be drawn.
260
+ static void releaseFrame(VideoFrameHandle &frame) {
261
+ if (frame.deleter) {
262
+ frame.deleter();
263
+ frame.deleter = nullptr;
264
+ }
265
+ frame.handle = nullptr;
266
+ }
267
+
268
+ // A rejected JS promise. A resolve factory cannot reject the task's promise
269
+ // itself; resolving it with this value makes it adopt the rejection.
270
+ static jsi::Value rejectedPromise(jsi::Runtime &runtime,
271
+ const std::string &message) {
272
+ auto promiseConstructor =
273
+ runtime.global().getPropertyAsObject(runtime, "Promise");
274
+ auto reject = promiseConstructor.getPropertyAsFunction(runtime, "reject");
275
+ jsi::JSError error(runtime, message);
276
+ return reject.callWithThis(runtime, promiseConstructor, error.value());
277
+ }
278
+
259
279
  async::AsyncTaskHandle GPUQueue::drawElementImageToTexture(
260
280
  jsi::Runtime &runtime, std::shared_ptr<GPUDrawElementImageSource> source,
261
281
  std::shared_ptr<GPUDrawElementImageDestination> destination) {
@@ -337,6 +357,18 @@ async::AsyncTaskHandle GPUQueue::drawElementImageToTexture(
337
357
  "a positive width and height");
338
358
  }
339
359
 
360
+ // With RENDER_ATTACHMENT as well, a platform that renders the view into a
361
+ // GPU buffer (Android 13 and above, through HardwareRenderer) hands that
362
+ // buffer over instead of reading the pixels back, and the view is drawn into
363
+ // the destination on the GPU (VideoFrameBlit), which writes the color aspect
364
+ // of the first layer. An omitted aspect means "all" (dst.aspect itself stays
365
+ // at Dawn's Undefined default, which WriteTexture reads the same way).
366
+ request.allowFrame =
367
+ (texture.GetUsage() & wgpu::TextureUsage::RenderAttachment) != 0 &&
368
+ info->aspect.value_or(wgpu::TextureAspect::All) ==
369
+ wgpu::TextureAspect::All &&
370
+ dst.origin.z == 0;
371
+
340
372
  // Destination size (pixels). Omitted means the source rectangle's natural
341
373
  // pixel size (points times the device pixel ratio), which the platform
342
374
  // computes; the bounds check then happens once the pixels are back.
@@ -372,13 +404,15 @@ async::AsyncTaskHandle GPUQueue::drawElementImageToTexture(
372
404
  auto context =
373
405
  async::RuntimeContext::getOrCreate(runtime, _async->instance());
374
406
  auto queue = _instance;
407
+ auto device = _device;
408
+ auto blit = _videoFrameBlit;
375
409
  // Keeps the destination texture alive until the write is issued.
376
410
  auto gpuTexture = info->texture;
377
411
  return context->postTask(
378
412
  [platform, request, dst, mipWidth, mipHeight, destinationIsBgra,
379
- destinationPremultiplied, queue,
380
- gpuTexture](const async::AsyncTaskHandle::ResolveFunction &resolve,
381
- const async::AsyncTaskHandle::RejectFunction &reject) {
413
+ destinationPremultiplied, device, queue, gpuTexture,
414
+ blit](const async::AsyncTaskHandle::ResolveFunction &resolve,
415
+ const async::AsyncTaskHandle::RejectFunction &reject) {
382
416
  platform->snapshotView(
383
417
  request,
384
418
  [resolve, reject, dst, mipWidth, mipHeight, destinationIsBgra,
@@ -432,6 +466,67 @@ async::AsyncTaskHandle GPUQueue::drawElementImageToTexture(
432
466
  return jsi::Value::undefined();
433
467
  });
434
468
  },
469
+ [resolve, reject, dst, mipWidth, mipHeight,
470
+ destinationPremultiplied, device, queue, gpuTexture,
471
+ blit](VideoFrameHandle frame) mutable {
472
+ // Arbitrary (UI) thread: validate without touching JSI. A frame
473
+ // that is not drawn is released here.
474
+ if (frame.handle == nullptr || frame.width == 0 ||
475
+ frame.height == 0) {
476
+ releaseFrame(frame);
477
+ reject("drawElementImageToTexture: the view produced no "
478
+ "pixels (is it mounted with a non-zero size?)");
479
+ return;
480
+ }
481
+ if (frame.width >
482
+ mipWidth - std::min<uint32_t>(dst.origin.x, mipWidth) ||
483
+ frame.height >
484
+ mipHeight - std::min<uint32_t>(dst.origin.y, mipHeight)) {
485
+ auto message =
486
+ "drawElementImageToTexture: the view's natural pixel "
487
+ "size (" +
488
+ std::to_string(frame.width) + "x" +
489
+ std::to_string(frame.height) +
490
+ ") does not fit in the destination texture at "
491
+ "destination.origin; pass destination.size to scale "
492
+ "it, or create a larger texture";
493
+ releaseFrame(frame);
494
+ reject(std::move(message));
495
+ return;
496
+ }
497
+ resolve([frame = std::move(frame), dst, destinationPremultiplied,
498
+ device, queue, gpuTexture,
499
+ blit](jsi::Runtime &runtime) mutable -> jsi::Value {
500
+ // Owning runtime's thread. Draw the buffer into the
501
+ // destination region on the GPU: importing it as an external
502
+ // texture makes the GPU wait on the renderer's fence first,
503
+ // and the blit converts the renderer's premultiplied pixels to
504
+ // straight alpha unless the destination asks for premultiplied
505
+ // ones. The frame is released once the draw is submitted (Dawn
506
+ // keeps its own reference to the buffer for in-flight work).
507
+ auto videoFrame =
508
+ std::make_shared<VideoFrame>(std::move(frame));
509
+ VideoFrameBlit::Request blitRequest;
510
+ blitRequest.frame = videoFrame;
511
+ blitRequest.width = videoFrame->handle().width;
512
+ blitRequest.height = videoFrame->handle().height;
513
+ blitRequest.alpha = destinationPremultiplied
514
+ ? VideoFrameBlit::Alpha::Premultiplied
515
+ : VideoFrameBlit::Alpha::Straight;
516
+ blitRequest.texture = dst.texture;
517
+ blitRequest.mipLevel = dst.mipLevel;
518
+ blitRequest.dstX = dst.origin.x;
519
+ blitRequest.dstY = dst.origin.y;
520
+ try {
521
+ blit->copy(device, queue, blitRequest);
522
+ } catch (const std::exception &error) {
523
+ videoFrame->release();
524
+ return rejectedPromise(runtime, error.what());
525
+ }
526
+ videoFrame->release();
527
+ return jsi::Value::undefined();
528
+ });
529
+ },
435
530
  [reject](std::string error) { reject(std::move(error)); });
436
531
  });
437
532
  }
@@ -2,6 +2,8 @@
2
2
 
3
3
  #include <memory>
4
4
  #include <string>
5
+ #include <utility>
6
+ #include <vector>
5
7
 
6
8
  #include "NativeObject.h"
7
9
 
@@ -248,10 +250,14 @@ public:
248
250
  return std::make_shared<VideoFrame>(std::move(frame));
249
251
  }
250
252
 
251
- std::shared_ptr<VideoFrame> createTestVideoFrame(double width,
252
- double height) {
253
+ std::shared_ptr<VideoFrame>
254
+ createTestVideoFrame(double width, double height,
255
+ std::optional<std::string> pixelFormat) {
256
+ auto format = (pixelFormat && pixelFormat.value() == "nv12")
257
+ ? VideoPixelFormat::NV12
258
+ : VideoPixelFormat::BGRA8;
253
259
  auto frame = _platformContext->createTestVideoFrame(
254
- static_cast<uint32_t>(width), static_cast<uint32_t>(height));
260
+ static_cast<uint32_t>(width), static_cast<uint32_t>(height), format);
255
261
  return std::make_shared<VideoFrame>(std::move(frame));
256
262
  }
257
263
 
@@ -296,6 +302,9 @@ public:
296
302
  // simulated unmount re-runs JS effects without unmounting native views,
297
303
  // and removing the entry then would orphan the still-attached surface.
298
304
  // - No native surface: the JS side is the last owner and removes the entry.
305
+ // The SurfaceInfo stays reachable by id for as long as a GPUCanvasContext
306
+ // holds it, so a surface attaching after StrictMode's early cleanup still
307
+ // lands in the context the first effect run made.
299
308
  // The check-and-erase runs atomically under the registry lock
300
309
  // (removeSurfaceInfoIfDetached), serialized against the UI thread's
301
310
  // find-or-create + attach (SurfaceRegistry::attachSurface), so a concurrent