react-native-webgpu 0.5.17 → 0.6.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.
package/README.md CHANGED
@@ -8,6 +8,20 @@ WebGPU for React Native, powered by Dawn.
8
8
 
9
9
  [Installation instructions](https://wcandillon.github.io/react-native-webgpu/docs/getting-started/installation)
10
10
 
11
+ ## Expo config plugin
12
+
13
+ The package ships with an Expo config plugin that disables Metal API validation in the iOS Xcode scheme, which is required to run WebGPU on the iOS Simulator (Metal API validation reports false positives for Dawn that do not occur on device). Because the plugin is part of the package, plugin and package versions are always aligned.
14
+
15
+ ```json
16
+ {
17
+ "expo": {
18
+ "plugins": ["react-native-webgpu"]
19
+ }
20
+ }
21
+ ```
22
+
23
+ See the [Expo guide](https://wcandillon.github.io/react-native-webgpu/docs/getting-started/expo#config-plugin) for details.
24
+
11
25
  ## Contributing
12
26
 
13
27
  For detailed information on library development, building, testing, and contributing guidelines, please see [CONTRIBUTING.md](packages/webgpu/CONTRIBUTING.md).
@@ -241,8 +241,8 @@ public:
241
241
  const uint32_t stridePixels = actualDesc.stride;
242
242
 
243
243
  void *vaddr = nullptr;
244
- int rc = AHardwareBuffer_lock(buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY,
245
- -1, nullptr, &vaddr);
244
+ int rc = AHardwareBuffer_lock(
245
+ buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1, nullptr, &vaddr);
246
246
  if (rc != 0 || !vaddr) {
247
247
  AHardwareBuffer_release(buffer);
248
248
  throw std::runtime_error(
@@ -302,6 +302,21 @@ public:
302
302
  AHardwareBuffer_Desc desc = {};
303
303
  AHardwareBuffer_describe(buffer, &desc);
304
304
 
305
+ // Dawn derives the importable WebGPU usages from the AHB's usage bits;
306
+ // without GPU_SAMPLED_IMAGE the imported texture never gets
307
+ // TextureBinding and import fails deep inside Dawn with an opaque
308
+ // validation error. Surface the real cause here instead. (CameraX's
309
+ // default ImageReaders allocate CPU-only buffers; with
310
+ // react-native-vision-camera, use pixelFormat: 'native' which allocates
311
+ // GPU-sampleable buffers.)
312
+ if ((desc.usage & AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE) == 0) {
313
+ throw std::runtime_error(
314
+ "wrapNativeBuffer: this AHardwareBuffer was allocated without "
315
+ "AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE, so the GPU cannot sample "
316
+ "it. Camera/CPU pipelines must allocate frames with GPU usage (for "
317
+ "react-native-vision-camera, use pixelFormat: 'native').");
318
+ }
319
+
305
320
  AHardwareBuffer_acquire(buffer);
306
321
 
307
322
  VideoFrameHandle handle;
@@ -312,13 +327,13 @@ public:
312
327
  // Dawn's OpaqueYCbCrAndroidForExternalTexture path. Single-plane RGBA AHBs
313
328
  // take the plain BGRA8 path (sampled as a regular 2D texture).
314
329
  switch (desc.format) {
315
- case AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420:
316
- case AHARDWAREBUFFER_FORMAT_YCbCr_P010:
317
- handle.pixelFormat = VideoPixelFormat::NV12;
318
- break;
319
- default:
320
- handle.pixelFormat = VideoPixelFormat::BGRA8;
321
- break;
330
+ case AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420:
331
+ case AHARDWAREBUFFER_FORMAT_YCbCr_P010:
332
+ handle.pixelFormat = VideoPixelFormat::NV12;
333
+ break;
334
+ default:
335
+ handle.pixelFormat = VideoPixelFormat::BGRA8;
336
+ break;
322
337
  }
323
338
  handle.deleter = [buffer]() { AHardwareBuffer_release(buffer); };
324
339
  return handle;
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require("./plugin/build").default;
@@ -2,12 +2,17 @@
2
2
 
3
3
  #include <algorithm>
4
4
  #include <cstdio>
5
+ #include <cstring>
5
6
  #include <memory>
6
7
  #include <string>
7
8
  #include <unordered_set>
8
9
  #include <utility>
9
10
  #include <vector>
10
11
 
12
+ #ifdef __APPLE__
13
+ #include <TargetConditionals.h>
14
+ #endif
15
+
11
16
  #include "Convertors.h"
12
17
 
13
18
  #include "GPUFeatures.h"
@@ -150,16 +155,40 @@ async::AsyncTaskHandle GPUAdapter::requestDevice(
150
155
  for (const auto &t : dawnToggles->enabledToggles.value()) {
151
156
  enabledToggles.push_back(t.c_str());
152
157
  }
153
- toggles.enabledToggleCount = enabledToggles.size();
154
- toggles.enabledToggles = enabledToggles.data();
155
158
  }
156
159
  if (dawnToggles->disabledToggles) {
157
160
  for (const auto &t : dawnToggles->disabledToggles.value()) {
158
161
  disabledToggles.push_back(t.c_str());
159
162
  }
160
- toggles.disabledToggleCount = disabledToggles.size();
161
- toggles.disabledToggles = disabledToggles.data();
162
163
  }
164
+ }
165
+ // TODO: in the latest version of Dawn, this won't be needed (https://issues.chromium.org/issues/42241591)
166
+ #if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR
167
+ // The iOS Simulator only advertises MTLFeatureSet_iOS_GPUFamily2, so
168
+ // Dawn defaults disable_base_instance/disable_base_vertex on and then
169
+ // rejects every draw with a non-zero firstInstance or baseVertex,
170
+ // both core WebGPU. The simulator forwards Metal calls to the host
171
+ // GPU, which does support base vertex/instance drawing, so force the
172
+ // toggles off. Device builds are unaffected: WebGPU-capable iPhones
173
+ // and iPads are all GPUFamily3+. These are device-stage toggles, so
174
+ // they must be chained here rather than on the instance descriptor
175
+ // (instance-stage toggle parsing silently drops them).
176
+ static const char *const kSimulatorDisabledToggles[] = {
177
+ "disable_base_instance", "disable_base_vertex"};
178
+ for (const char *name : kSimulatorDisabledToggles) {
179
+ const bool explicitlyEnabled = std::any_of(
180
+ enabledToggles.begin(), enabledToggles.end(),
181
+ [name](const char *t) { return std::strcmp(t, name) == 0; });
182
+ if (!explicitlyEnabled) {
183
+ disabledToggles.push_back(name);
184
+ }
185
+ }
186
+ #endif
187
+ if (!enabledToggles.empty() || !disabledToggles.empty()) {
188
+ toggles.enabledToggleCount = enabledToggles.size();
189
+ toggles.enabledToggles = enabledToggles.data();
190
+ toggles.disabledToggleCount = disabledToggles.size();
191
+ toggles.disabledToggles = disabledToggles.data();
163
192
  deviceDesc.nextInChain = &toggles;
164
193
  }
165
194
  _instance.RequestDevice(
@@ -1,5 +1,6 @@
1
1
  #include "GPUExternalTexture.h"
2
2
 
3
+ #include <array>
3
4
  #include <cmath>
4
5
  #include <memory>
5
6
  #include <string>
@@ -56,44 +57,76 @@ static const float kIdentityTransferParams[7] = {
56
57
  0.0f, // F
57
58
  };
58
59
 
59
- // BT.709 limited-range YUV -> R'G'B' as a 3x4 row-major matrix mapping
60
- // [Y, Cb, Cr, 1] to gamma-encoded R'G'B' (NOT linear; the sRGB decode in
61
- // srcTransferFunctionParameters linearizes afterwards). Same values the Apple
62
- // NV12 path computes from the CVPixelBuffer; used for Android buffers that
63
- // arrive as a *defined* biplanar format (where we split the planes and convert
64
- // ourselves) rather than an opaque external-format AHB. Camera streams are
65
- // limited-range BT.709 in the overwhelming majority of cases; full-range /
66
- // BT.601 would need different coefficients (refine from the buffer's suggested
67
- // range if it matters).
68
- [[maybe_unused]] static const float kBT709LimitedToRgb[12] = {
69
- 1.164383f, 0.000000f, 1.792741f, -0.972945f, //
70
- 1.164383f, -0.213249f, -0.532909f, 0.301517f, //
71
- 1.164383f, 2.112402f, 0.000000f, -1.133402f, //
60
+ // Identity passthrough for GPUExternalTexture.yuvToRgbMatrix: the sampled
61
+ // texel is already RGB (Apple's biplanar path where Dawn converts in the
62
+ // sampler transform, RGBA surfaces, or an Android AHB whose driver reports
63
+ // RGB_IDENTITY). A 3x4 row-major matrix mapping [c.r, c.g, c.b, 1] to itself.
64
+ static constexpr std::array<float, 12> kYuvPassthroughMatrix = {
65
+ 1.0f, 0.0f, 0.0f, 0.0f, //
66
+ 0.0f, 1.0f, 0.0f, 0.0f, //
67
+ 0.0f, 0.0f, 1.0f, 0.0f, //
72
68
  };
73
69
 
74
- // True for the multi-planar Y + CbCr formats whose planes we can view as
75
- // Plane0Only (luma) / Plane1Only (chroma) and convert with an explicit matrix.
76
- // Excludes OpaqueYCbCrAndroid (external format, no plane views) and triplanar
77
- // formats (would need a third plane). Only referenced on Android.
78
- [[maybe_unused]] static bool isBiplanarYuvFormat(wgpu::TextureFormat format) {
79
- switch (format) {
80
- case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
81
- case wgpu::TextureFormat::R8BG8Biplanar422Unorm:
82
- case wgpu::TextureFormat::R8BG8Biplanar444Unorm:
83
- case wgpu::TextureFormat::R10X6BG10X6Biplanar420Unorm:
84
- case wgpu::TextureFormat::R10X6BG10X6Biplanar422Unorm:
85
- case wgpu::TextureFormat::R10X6BG10X6Biplanar444Unorm:
86
- return true;
70
+ // Build a 3x4 row-major matrix mapping the *sampled* gamma-encoded
71
+ // [Y, Cb, Cr, 1] (each normalized to [0,1]) to gamma-encoded R'G'B', from the
72
+ // Vulkan-suggested YCbCr model + range of an Android external-format buffer.
73
+ // Needed because Dawn's opaque-YCbCr sampling path hard-codes an RGB_IDENTITY
74
+ // conversion (SamplerVk.cpp::GetYCbCrForTextureView, crbug.com/497675620), so
75
+ // the shader receives raw Y/Cb/Cr and must convert itself.
76
+ // The VkSamplerYcbcrModelConversion / VkSamplerYcbcrRange values are inlined
77
+ // to avoid a Vulkan header dependency in this cross-platform file.
78
+ [[maybe_unused]] static std::array<float, 12>
79
+ makeYuvToRgbMatrix(uint32_t vkModel, uint32_t vkRange) {
80
+ constexpr uint32_t kModelRgbIdentity = 0; // VK_..._RGB_IDENTITY
81
+ constexpr uint32_t kModel709 = 2; // VK_..._YCBCR_709
82
+ constexpr uint32_t kModel601 = 3; // VK_..._YCBCR_601
83
+ constexpr uint32_t kModel2020 = 4; // VK_..._YCBCR_2020
84
+ constexpr uint32_t kRangeItuNarrow = 1; // VK_SAMPLER_YCBCR_RANGE_ITU_NARROW
85
+
86
+ if (vkModel == kModelRgbIdentity) {
87
+ // The buffer content is RGB; nothing to convert.
88
+ return kYuvPassthroughMatrix;
89
+ }
90
+ float kr;
91
+ float kb;
92
+ switch (vkModel) {
93
+ case kModel709:
94
+ kr = 0.2126f;
95
+ kb = 0.0722f;
96
+ break;
97
+ case kModel2020:
98
+ kr = 0.2627f;
99
+ kb = 0.0593f;
100
+ break;
101
+ case kModel601:
87
102
  default:
88
- return false;
103
+ // YCBCR_IDENTITY and unknown models: Android camera streams are BT.601 in
104
+ // practice, so that is the sane default.
105
+ kr = 0.299f;
106
+ kb = 0.114f;
107
+ break;
89
108
  }
109
+ const float kg = 1.0f - kr - kb;
110
+ const bool narrow = vkRange == kRangeItuNarrow;
111
+ const float yScale = narrow ? 255.0f / 219.0f : 1.0f;
112
+ const float cScale = narrow ? 255.0f / 224.0f : 1.0f;
113
+ const float yOffset = narrow ? 16.0f / 255.0f : 0.0f;
114
+ const float cOffset = 128.0f / 255.0f;
115
+ const float crR = 2.0f * (1.0f - kr) * cScale;
116
+ const float cbG = -2.0f * kb * (1.0f - kb) / kg * cScale;
117
+ const float crG = -2.0f * kr * (1.0f - kr) / kg * cScale;
118
+ const float cbB = 2.0f * (1.0f - kb) * cScale;
119
+ return {
120
+ yScale, 0.0f, crR, -yScale * yOffset - crR * cOffset, //
121
+ yScale, cbG, crG, -yScale * yOffset - (cbG + crG) * cOffset, //
122
+ yScale, cbB, 0.0f, -yScale * yOffset - cbB * cOffset, //
123
+ };
90
124
  }
91
125
 
92
126
  // Map a rotation in degrees (0 / 90 / 180 / 270) to Dawn's enum. Anything that
93
127
  // isn't a clean multiple of 90 snaps to the nearest quadrant; Dawn only
94
128
  // supports those four steps for external textures.
95
- static wgpu::ExternalTextureRotation
96
- toExternalTextureRotation(double degrees) {
129
+ static wgpu::ExternalTextureRotation toExternalTextureRotation(double degrees) {
97
130
  int quadrant = static_cast<int>(std::lround(degrees / 90.0)) & 3;
98
131
  switch (quadrant) {
99
132
  case 1:
@@ -216,7 +249,11 @@ std::shared_ptr<GPUExternalTexture> GPUExternalTexture::Create(
216
249
 
217
250
  return std::make_shared<GPUExternalTexture>(
218
251
  std::move(external), std::move(memory), std::move(texture),
219
- std::move(descriptor->source), std::move(label));
252
+ std::move(descriptor->source), std::move(label),
253
+ // Dawn's Metal path applies the YUV->RGB conversion (from
254
+ // frame.yuvToRgbMatrix above) inside the sampling transform, so the
255
+ // sampled texel is already RGB.
256
+ kYuvPassthroughMatrix);
220
257
  #elif defined(__ANDROID__)
221
258
  // 1. Import the AHardwareBuffer as SharedTextureMemory. For YUV AHBs this
222
259
  // yields a Dawn texture in the implementation-defined OpaqueYCbCrAndroid
@@ -257,32 +294,39 @@ std::shared_ptr<GPUExternalTexture> GPUExternalTexture::Create(
257
294
  "GPUExternalTexture::Create(): BeginAccess failed");
258
295
  }
259
296
 
260
- // 4. Build the ExternalTextureDescriptor. There are two cases depending on
261
- // how Dawn imported the AHB (see SharedTextureMemoryVk.cpp):
262
- //
263
- // a. *External* format (camera buffers whose layout has no Vulkan
264
- // equivalent) -> OpaqueYCbCrAndroid, a single opaque plane. Sampling
265
- // routes through a Vulkan SamplerYcbcrConversion whose model Dawn
266
- // copies verbatim from the AHB's suggestedYcbcrModel. We pass a single
267
- // plane + identity transfer and let that conversion (if any) run. NOTE:
268
- // when the driver reports RGB_IDENTITY the sample comes back as raw
269
- // Y/Cb/Cr; there is no public hook to override the model on this path.
270
- //
271
- // b. *Defined* biplanar format (e.g. R8BG8Biplanar420Unorm, exposed by the
272
- // dawn-multi-planar-formats feature) -> we split Plane0Only (luma) /
273
- // Plane1Only (chroma) and hand Dawn an explicit BT.709 matrix + sRGB
274
- // transfer, exactly like the iOS NV12 path. This makes numPlanes == 2
275
- // so the matrix is actually applied (the single-plane branch in Dawn's
276
- // Tint transform ignores yuvToRgbConversionMatrix).
277
- //
278
- // Either way we must pass non-null gamut/transfer arrays:
279
- // ComputeExternalTextureParams dereferences them unconditionally
280
- // (kIdentityTransferParams is defined at file scope).
281
- const bool isBiplanar = frame.pixelFormat == VideoPixelFormat::NV12 &&
282
- isBiplanarYuvFormat(texture.GetFormat());
297
+ // 4. Resolve the YUV->RGB conversion the *shader* must apply. Dawn imports
298
+ // every YUV / implementation-defined AHB format as OpaqueYCbCrAndroid
299
+ // (AHBFunctions.cpp::FormatFromAHardwareBufferFormat) and samples it
300
+ // through a Vulkan SamplerYcbcrConversion that is hard-coded to
301
+ // RGB_IDENTITY (SamplerVk.cpp::GetYCbCrForTextureView,
302
+ // crbug.com/497675620), so the sample always comes back as raw
303
+ // [Y, Cb, Cr]. The driver's *suggested* model + range are captured at
304
+ // import time though; read them back from the shared memory's properties
305
+ // and derive the correct conversion matrix, exposed to JS as
306
+ // GPUExternalTexture.yuvToRgbMatrix. Defined color formats (e.g. an RGBA
307
+ // AHB -> RGBA8Unorm) sample as RGB directly and get the passthrough.
308
+ std::array<float, 12> yuvToRgbMatrix = kYuvPassthroughMatrix;
309
+ if (texture.GetFormat() == wgpu::TextureFormat::OpaqueYCbCrAndroid) {
310
+ wgpu::SharedTextureMemoryAHardwareBufferProperties ahbProps{};
311
+ wgpu::SharedTextureMemoryProperties props{};
312
+ props.nextInChain = &ahbProps;
313
+ if (memory.GetProperties(&props)) {
314
+ yuvToRgbMatrix = makeYuvToRgbMatrix(ahbProps.yCbCrInfo.vkYCbCrModel,
315
+ ahbProps.yCbCrInfo.vkYCbCrRange);
316
+ } else {
317
+ // Fall back to the Android camera norm rather than passthrough.
318
+ yuvToRgbMatrix = makeYuvToRgbMatrix(/* YCBCR_601 */ 3,
319
+ /* ITU_NARROW */ 1);
320
+ }
321
+ }
283
322
 
284
- wgpu::TextureView plane0;
285
- wgpu::TextureView plane1;
323
+ // 5. Build the ExternalTextureDescriptor: a single (opaque or RGB) plane
324
+ // with identity transfer. The YUV->RGB conversion cannot run inside
325
+ // Dawn's external-texture transform here: the single-plane branch of the
326
+ // Tint transform ignores yuvToRgbConversionMatrix, which is why the
327
+ // matrix is surfaced to the shader instead. The gamut/transfer arrays
328
+ // must still be non-null: ComputeExternalTextureParams dereferences them
329
+ // unconditionally.
286
330
  wgpu::ExternalTextureDescriptor extDesc{};
287
331
  if (!label.empty()) {
288
332
  extDesc.label = wgpu::StringView(label.c_str(), label.size());
@@ -291,24 +335,10 @@ std::shared_ptr<GPUExternalTexture> GPUExternalTexture::Create(
291
335
  extDesc.cropSize = {frame.width, frame.height};
292
336
  extDesc.apparentSize = {frame.width, frame.height};
293
337
  extDesc.gamutConversionMatrix = kIdentityGamutMatrix;
294
- if (isBiplanar) {
295
- wgpu::TextureViewDescriptor v0{};
296
- v0.aspect = wgpu::TextureAspect::Plane0Only;
297
- plane0 = texture.CreateView(&v0);
298
- wgpu::TextureViewDescriptor v1{};
299
- v1.aspect = wgpu::TextureAspect::Plane1Only;
300
- plane1 = texture.CreateView(&v1);
301
- extDesc.plane0 = plane0;
302
- extDesc.plane1 = plane1;
303
- extDesc.yuvToRgbConversionMatrix = kBT709LimitedToRgb;
304
- extDesc.srcTransferFunctionParameters = kSrgbDecodeParams;
305
- extDesc.dstTransferFunctionParameters = kSrgbEncodeParams;
306
- } else {
307
- plane0 = texture.CreateView();
308
- extDesc.plane0 = plane0;
309
- extDesc.srcTransferFunctionParameters = kIdentityTransferParams;
310
- extDesc.dstTransferFunctionParameters = kIdentityTransferParams;
311
- }
338
+ wgpu::TextureView plane0 = texture.CreateView();
339
+ extDesc.plane0 = plane0;
340
+ extDesc.srcTransferFunctionParameters = kIdentityTransferParams;
341
+ extDesc.dstTransferFunctionParameters = kIdentityTransferParams;
312
342
  extDesc.mirrored = descriptor->mirrored.value_or(false);
313
343
  extDesc.rotation =
314
344
  toExternalTextureRotation(descriptor->rotation.value_or(0));
@@ -324,7 +354,8 @@ std::shared_ptr<GPUExternalTexture> GPUExternalTexture::Create(
324
354
 
325
355
  return std::make_shared<GPUExternalTexture>(
326
356
  std::move(external), std::move(memory), std::move(texture),
327
- std::move(descriptor->source), std::move(label));
357
+ std::move(descriptor->source), std::move(label),
358
+ std::move(yuvToRgbMatrix));
328
359
  #else
329
360
  throw std::runtime_error(
330
361
  "GPUExternalTexture::Create(): not yet implemented on this "
@@ -1,8 +1,10 @@
1
1
  #pragma once
2
2
 
3
+ #include <array>
3
4
  #include <memory>
4
5
  #include <string>
5
6
  #include <utility>
7
+ #include <vector>
6
8
 
7
9
  #include "Unions.h"
8
10
 
@@ -36,10 +38,12 @@ public:
36
38
  // the producer (e.g. AVPlayer) can reclaim the IOSurface.
37
39
  GPUExternalTexture(wgpu::ExternalTexture instance,
38
40
  wgpu::SharedTextureMemory memory, wgpu::Texture texture,
39
- std::shared_ptr<VideoFrame> source, std::string label)
41
+ std::shared_ptr<VideoFrame> source, std::string label,
42
+ std::array<float, 12> yuvToRgbMatrix)
40
43
  : NativeObject(CLASS_NAME), _instance(std::move(instance)),
41
44
  _memory(std::move(memory)), _texture(std::move(texture)),
42
- _source(std::move(source)), _label(std::move(label)) {}
45
+ _source(std::move(source)), _label(std::move(label)),
46
+ _yuvToRgbMatrix(yuvToRgbMatrix) {}
43
47
 
44
48
  ~GPUExternalTexture() override { destroy(); }
45
49
 
@@ -69,11 +73,25 @@ public:
69
73
  _instance.SetLabel(_label.c_str());
70
74
  }
71
75
 
76
+ // Non-spec extension. A 3x4 row-major matrix mapping the *sampled* texel
77
+ // [c.r, c.g, c.b, 1] to gamma-encoded R'G'B'. When Dawn's sampler already
78
+ // produces RGB (Apple's biplanar path, RGBA surfaces), this is the identity
79
+ // passthrough; on the Android opaque-YCbCr path the sample comes back as raw
80
+ // [Y, Cb, Cr] (Dawn hard-codes an RGB_IDENTITY Vulkan conversion, see
81
+ // crbug.com/497675620) and this matrix is derived from the driver's
82
+ // suggested YCbCr model + range for the buffer. Shaders can therefore apply
83
+ // it unconditionally after textureSampleBaseClampToEdge.
84
+ std::vector<double> getYuvToRgbMatrix() {
85
+ return std::vector<double>(_yuvToRgbMatrix.begin(), _yuvToRgbMatrix.end());
86
+ }
87
+
72
88
  static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) {
73
89
  installGetter(runtime, prototype, "__brand", &GPUExternalTexture::getBrand);
74
90
  installGetterSetter(runtime, prototype, "label",
75
91
  &GPUExternalTexture::getLabel,
76
92
  &GPUExternalTexture::setLabel);
93
+ installGetter(runtime, prototype, "yuvToRgbMatrix",
94
+ &GPUExternalTexture::getYuvToRgbMatrix);
77
95
  installMethod(runtime, prototype, "destroy", &GPUExternalTexture::destroy);
78
96
  }
79
97
 
@@ -85,6 +103,7 @@ private:
85
103
  wgpu::Texture _texture;
86
104
  std::shared_ptr<VideoFrame> _source;
87
105
  std::string _label;
106
+ std::array<float, 12> _yuvToRgbMatrix;
88
107
  };
89
108
 
90
109
  } // namespace rnwgpu
@@ -186,7 +186,14 @@ void RuntimeContext::tick() {
186
186
  }
187
187
 
188
188
  jsi::UUID RuntimeContext::runtimeDataUUID() {
189
- static const auto uuid = jsi::UUID();
189
+ // Fixed random UUID identifying react-native-webgpu's runtimeData slot.
190
+ // Never use the default-constructed all-zero UUID here: Hermes backs
191
+ // runtimeData with a DenseMap that reserves the all-zero UUID as its
192
+ // empty-bucket marker (and the all-0xff UUID as its tombstone), so using
193
+ // either as a key is undefined behavior. It crashes on worklet runtimes,
194
+ // where react-native-worklets seeds runtimeData before we do.
195
+ static constexpr jsi::UUID uuid(0xC06D5D2D, 0xB19B, 0x431B, 0xA89E,
196
+ 0x20F8A0B6E31BULL);
190
197
  return uuid;
191
198
  }
192
199
 
@@ -0,0 +1,3 @@
1
+ declare const _exports: any;
2
+ export = _exports;
3
+ //# sourceMappingURL=app.plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.plugin.d.ts","sourceRoot":"","sources":["../../app.plugin.js"],"names":[],"mappings":""}
@@ -34,6 +34,7 @@ declare global {
34
34
  }
35
35
  interface GPUExternalTexture {
36
36
  destroy(): void;
37
+ readonly yuvToRgbMatrix: number[];
37
38
  }
38
39
  function createImageBitmap(image: ArrayBuffer | ArrayBufferView): Promise<ImageBitmap>;
39
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EACd,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,gCAAgC,EAChC,YAAY,EACZ,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,SAAS,CAAC;AAEjB,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,wBAAwB,EACxB,wBAAwB,EACxB,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,gCAAgC,EAChC,oCAAoC,EACpC,sBAAsB,EACtB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,SAAS,CAAC;AAEjB,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,SAAS;QACjB,GAAG,EAAE,GAAG,CAAC;KACV;IAED,IAAI,SAAS,EAAE,SAAS,CAAC;IAEzB,IAAI,QAAQ,EAAE;QACZ,GAAG,EAAE,GAAG,CAAC;QACT,MAAM,EAAE,OAAO,CAAC;QAChB,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAC;QACtD,uBAAuB,EAAE,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,KACX,eAAe,CAAC;QACrB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,eAAe,GAAG,WAAW,KAAK,MAAM,CAAC;QACvE,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;QAC5C,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,gBAAgB,CAAC;QACnD,oBAAoB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,gBAAgB,CAAC;QAI1E,gCAAgC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,gBAAgB,CAAC;QACxE,iBAAiB,EAAE,CACjB,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,sBAAsB,KACjC,WAAW,CAAC;QACjB,kBAAkB,EAAE,MAAM,MAAM,CAAC;KAClC,CAAC;IAEF,UAAU,SAAS;QACjB,yBAAyB,CACvB,UAAU,EAAE,gCAAgC,GAC3C,sBAAsB,CAAC;QAC1B,iBAAiB,CAAC,UAAU,EAAE,wBAAwB,GAAG,cAAc,CAAC;KACzE;IAID,UAAU,mBAAmB;QAC3B,WAAW,CAAC,EAAE,wBAAwB,CAAC;KACxC;IAMD,UAAU,4BAA4B;QACpC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,CAAC;QAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB;IASD,UAAU,kBAAkB;QAC1B,OAAO,IAAI,IAAI,CAAC;KACjB;IAGD,SAAS,iBAAiB,CACxB,KAAK,EAAE,WAAW,GAAG,eAAe,GACnC,OAAO,CAAC,WAAW,CAAC,CAAC;CACzB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EACd,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,gCAAgC,EAChC,YAAY,EACZ,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,SAAS,CAAC;AAEjB,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,wBAAwB,EACxB,wBAAwB,EACxB,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,gCAAgC,EAChC,oCAAoC,EACpC,sBAAsB,EACtB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,SAAS,CAAC;AAEjB,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,SAAS;QACjB,GAAG,EAAE,GAAG,CAAC;KACV;IAED,IAAI,SAAS,EAAE,SAAS,CAAC;IAEzB,IAAI,QAAQ,EAAE;QACZ,GAAG,EAAE,GAAG,CAAC;QACT,MAAM,EAAE,OAAO,CAAC;QAChB,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAC;QACtD,uBAAuB,EAAE,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,KACX,eAAe,CAAC;QACrB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,eAAe,GAAG,WAAW,KAAK,MAAM,CAAC;QACvE,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;QAC5C,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,gBAAgB,CAAC;QACnD,oBAAoB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,gBAAgB,CAAC;QAI1E,gCAAgC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,gBAAgB,CAAC;QACxE,iBAAiB,EAAE,CACjB,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,sBAAsB,KACjC,WAAW,CAAC;QACjB,kBAAkB,EAAE,MAAM,MAAM,CAAC;KAClC,CAAC;IAEF,UAAU,SAAS;QACjB,yBAAyB,CACvB,UAAU,EAAE,gCAAgC,GAC3C,sBAAsB,CAAC;QAC1B,iBAAiB,CAAC,UAAU,EAAE,wBAAwB,GAAG,cAAc,CAAC;KACzE;IAID,UAAU,mBAAmB;QAC3B,WAAW,CAAC,EAAE,wBAAwB,CAAC;KACxC;IAMD,UAAU,4BAA4B;QACpC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,CAAC;QAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB;IASD,UAAU,kBAAkB;QAC1B,OAAO,IAAI,IAAI,CAAC;QAShB,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC;KACnC;IAGD,SAAS,iBAAiB,CACxB,KAAK,EAAE,WAAW,GAAG,eAAe,GACnC,OAAO,CAAC,WAAW,CAAC,CAAC;CACzB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-webgpu",
3
- "version": "0.5.17",
3
+ "version": "0.6.0",
4
4
  "description": "React Native WebGPU",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -18,15 +18,20 @@
18
18
  "cpp/**/*.{h,cpp}",
19
19
  "apple/**",
20
20
  "libs/**",
21
- "*.podspec"
21
+ "*.podspec",
22
+ "app.plugin.js",
23
+ "plugin/build/**"
22
24
  ],
23
25
  "dawn": "chromium/7849",
24
26
  "scripts": {
25
27
  "test": "NODE_OPTIONS='--experimental-require-module' jest -i",
26
28
  "test:ref": "REFERENCE=true NODE_OPTIONS='--experimental-require-module' jest -i",
29
+ "test:node": "NODE_WEBGPU=true NODE_OPTIONS='--experimental-require-module' jest -i",
30
+ "test:plugin": "jest -c plugin/jest.config.js",
27
31
  "lint": "eslint . --ext .ts,.tsx --max-warnings 0 --cache --fix",
28
32
  "tsc": "tsc --noEmit",
29
- "build": "bob build",
33
+ "build": "bob build && yarn build:plugin",
34
+ "build:plugin": "tsc -p plugin/tsconfig.build.json",
30
35
  "release": "semantic-release",
31
36
  "build-dawn": "tsx scripts/build/dawn.ts",
32
37
  "clean-dawn": "rimraf ./libs && rimraf ../../externals/dawn/out",
@@ -41,7 +46,9 @@
41
46
  "keywords": [
42
47
  "react-native",
43
48
  "ios",
44
- "android"
49
+ "android",
50
+ "expo",
51
+ "expo-config-plugin"
45
52
  ],
46
53
  "repository": {
47
54
  "type": "git",
@@ -59,6 +66,7 @@
59
66
  "provenance": true
60
67
  },
61
68
  "devDependencies": {
69
+ "@expo/config-plugins": "^57.0.3",
62
70
  "@semantic-release/commit-analyzer": "^13.0.0",
63
71
  "@semantic-release/exec": "^7.0.3",
64
72
  "@semantic-release/github": "^10.3.3",
@@ -100,6 +108,7 @@
100
108
  "ts-morph": "^22.0.0",
101
109
  "tsx": "^4.20.5",
102
110
  "typescript": "^5.2.2",
111
+ "webgpu": "0.4.0",
103
112
  "wgpu-matrix": "^3.0.2",
104
113
  "ws": "^8.18.0",
105
114
  "yargs": "^17.7.2"
@@ -0,0 +1,3 @@
1
+ import type { ConfigPlugin } from "@expo/config-plugins";
2
+ declare const _default: ConfigPlugin<void>;
3
+ export default _default;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_plugins_1 = require("@expo/config-plugins");
4
+ const withMetalApiValidation_1 = require("./withMetalApiValidation");
5
+ const pkg = require("../../package.json");
6
+ const withWebGPU = (config) => (0, withMetalApiValidation_1.withMetalApiValidationDisabled)(config);
7
+ // Expo resolves config plugins through their default export (re-exported by
8
+ // app.plugin.js at the package root).
9
+ // eslint-disable-next-line import/no-default-export
10
+ exports.default = (0, config_plugins_1.createRunOncePlugin)(withWebGPU, pkg.name, pkg.version);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Rewrites an Xcode scheme (.xcscheme XML) so that Metal API validation is
3
+ * disabled for the Run action — the same effect as unchecking
4
+ * Product → Scheme → Edit Scheme → Run → Diagnostics → Metal API Validation.
5
+ *
6
+ * Xcode encodes the setting as the `enableGPUValidationMode` attribute on the
7
+ * <LaunchAction> element: the attribute is absent when validation is enabled
8
+ * (the default) and set to "1" when disabled. This mirrors how Flutter
9
+ * disables Metal API validation in its templates
10
+ * (https://github.com/flutter/flutter/pull/159228).
11
+ */
12
+ export declare const setMetalApiValidationDisabled: (xcscheme: string) => string;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setMetalApiValidationDisabled = void 0;
4
+ /**
5
+ * Rewrites an Xcode scheme (.xcscheme XML) so that Metal API validation is
6
+ * disabled for the Run action — the same effect as unchecking
7
+ * Product → Scheme → Edit Scheme → Run → Diagnostics → Metal API Validation.
8
+ *
9
+ * Xcode encodes the setting as the `enableGPUValidationMode` attribute on the
10
+ * <LaunchAction> element: the attribute is absent when validation is enabled
11
+ * (the default) and set to "1" when disabled. This mirrors how Flutter
12
+ * disables Metal API validation in its templates
13
+ * (https://github.com/flutter/flutter/pull/159228).
14
+ */
15
+ const setMetalApiValidationDisabled = (xcscheme) => xcscheme.replace(/<LaunchAction([^>]*)>/, (launchAction, attributes) => {
16
+ if (attributes.includes("enableGPUValidationMode")) {
17
+ return launchAction.replace(/enableGPUValidationMode\s*=\s*"[^"]*"/, 'enableGPUValidationMode = "1"');
18
+ }
19
+ return `<LaunchAction${attributes}\n enableGPUValidationMode = "1">`;
20
+ });
21
+ exports.setMetalApiValidationDisabled = setMetalApiValidationDisabled;
@@ -0,0 +1,9 @@
1
+ import type { ConfigPlugin } from "@expo/config-plugins";
2
+ /**
3
+ * Disables Metal API validation in every shared Xcode scheme of the iOS
4
+ * project. Dawn (the WebGPU implementation) triggers false-positive
5
+ * validation errors on the iOS Simulator that do not occur on device.
6
+ * Manually unchecking the setting in Xcode does not survive
7
+ * `npx expo prebuild --clean`, which is why this is a config plugin.
8
+ */
9
+ export declare const withMetalApiValidationDisabled: ConfigPlugin;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.withMetalApiValidationDisabled = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const config_plugins_1 = require("@expo/config-plugins");
40
+ const setMetalApiValidation_1 = require("./setMetalApiValidation");
41
+ const getSharedSchemes = (platformProjectRoot) => {
42
+ if (!fs.existsSync(platformProjectRoot)) {
43
+ return [];
44
+ }
45
+ return fs
46
+ .readdirSync(platformProjectRoot)
47
+ .filter((entry) => entry.endsWith(".xcodeproj"))
48
+ .map((entry) => path.join(platformProjectRoot, entry, "xcshareddata", "xcschemes"))
49
+ .filter((schemesDir) => fs.existsSync(schemesDir))
50
+ .flatMap((schemesDir) => fs
51
+ .readdirSync(schemesDir)
52
+ .filter((file) => file.endsWith(".xcscheme"))
53
+ .map((file) => path.join(schemesDir, file)));
54
+ };
55
+ /**
56
+ * Disables Metal API validation in every shared Xcode scheme of the iOS
57
+ * project. Dawn (the WebGPU implementation) triggers false-positive
58
+ * validation errors on the iOS Simulator that do not occur on device.
59
+ * Manually unchecking the setting in Xcode does not survive
60
+ * `npx expo prebuild --clean`, which is why this is a config plugin.
61
+ */
62
+ const withMetalApiValidationDisabled = (config) => (0, config_plugins_1.withDangerousMod)(config, [
63
+ "ios",
64
+ async (cfg) => {
65
+ const schemes = getSharedSchemes(cfg.modRequest.platformProjectRoot);
66
+ for (const scheme of schemes) {
67
+ const contents = fs.readFileSync(scheme, "utf8");
68
+ fs.writeFileSync(scheme, (0, setMetalApiValidation_1.setMetalApiValidationDisabled)(contents));
69
+ }
70
+ return cfg;
71
+ },
72
+ ]);
73
+ exports.withMetalApiValidationDisabled = withMetalApiValidationDisabled;
package/src/index.tsx CHANGED
@@ -93,6 +93,15 @@ declare global {
93
93
  // pool (e.g. a camera/video player) and pile up GPU resources.
94
94
  interface GPUExternalTexture {
95
95
  destroy(): void;
96
+ // Non-spec extension: a 3x4 row-major matrix (12 numbers) mapping the
97
+ // sampled texel [r, g, b, 1] to gamma-encoded R'G'B'. On the Android
98
+ // opaque-YCbCr camera path, textureSampleBaseClampToEdge returns raw
99
+ // [Y, Cb, Cr] (Dawn hard-codes an RGB_IDENTITY Vulkan conversion); this
100
+ // matrix is derived per-buffer from the driver's suggested YCbCr model and
101
+ // range (BT.601/709/2020, full/narrow). Everywhere else (iOS, RGBA
102
+ // surfaces) it is the identity passthrough, so shaders can apply it
103
+ // unconditionally. Upload it as a uniform and multiply after sampling.
104
+ readonly yuvToRgbMatrix: number[];
96
105
  }
97
106
 
98
107
  // Extend createImageBitmap to accept ArrayBuffer/TypedArray (encoded image bytes)