react-native-webgpu 0.5.11 → 0.5.13

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 (57) hide show
  1. package/README.md +84 -0
  2. package/android/CMakeLists.txt +2 -0
  3. package/android/cpp/AndroidPlatformContext.h +121 -0
  4. package/apple/ApplePlatformContext.h +13 -0
  5. package/apple/ApplePlatformContext.mm +145 -0
  6. package/apple/AppleVideoPlayer.h +31 -0
  7. package/apple/AppleVideoPlayer.mm +314 -0
  8. package/cpp/rnwgpu/ArrayBuffer.h +51 -7
  9. package/cpp/rnwgpu/PlatformContext.h +81 -0
  10. package/cpp/rnwgpu/RNWebGPUManager.cpp +12 -0
  11. package/cpp/rnwgpu/api/Convertors.h +33 -11
  12. package/cpp/rnwgpu/api/GPU.cpp +27 -0
  13. package/cpp/rnwgpu/api/GPUAdapter.cpp +109 -33
  14. package/cpp/rnwgpu/api/GPUDevice.cpp +58 -5
  15. package/cpp/rnwgpu/api/GPUDevice.h +6 -0
  16. package/cpp/rnwgpu/api/GPUExternalTexture.cpp +335 -0
  17. package/cpp/rnwgpu/api/GPUExternalTexture.h +47 -2
  18. package/cpp/rnwgpu/api/GPUFeatures.h +4 -1
  19. package/cpp/rnwgpu/api/GPUShaderModule.cpp +2 -3
  20. package/cpp/rnwgpu/api/GPUSharedTextureMemory.cpp +80 -0
  21. package/cpp/rnwgpu/api/GPUSharedTextureMemory.h +71 -0
  22. package/cpp/rnwgpu/api/ImageBitmap.h +8 -0
  23. package/cpp/rnwgpu/api/RNWebGPU.h +63 -21
  24. package/cpp/rnwgpu/api/RnFeatures.h +53 -0
  25. package/cpp/rnwgpu/api/VideoFrame.h +76 -0
  26. package/cpp/rnwgpu/api/VideoPlayer.h +69 -0
  27. package/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h +4 -1
  28. package/cpp/rnwgpu/api/descriptors/GPUDawnTogglesDescriptor.h +57 -0
  29. package/cpp/rnwgpu/api/descriptors/GPUDeviceDescriptor.h +18 -3
  30. package/cpp/rnwgpu/api/descriptors/GPUExternalTextureDescriptor.h +35 -33
  31. package/cpp/rnwgpu/api/descriptors/GPUSharedTextureMemoryDescriptor.h +62 -0
  32. package/cpp/rnwgpu/api/descriptors/Unions.h +10 -0
  33. package/cpp/webgpu/webgpu.h +78 -17
  34. package/cpp/webgpu/webgpu_cpp.h +1014 -1249
  35. package/cpp/webgpu/webgpu_cpp_print.h +99 -10
  36. package/lib/commonjs/Canvas.js.map +1 -1
  37. package/lib/commonjs/index.js.map +1 -1
  38. package/lib/module/Canvas.js.map +1 -1
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/typescript/src/Canvas.d.ts +0 -10
  41. package/lib/typescript/src/Canvas.d.ts.map +1 -1
  42. package/lib/typescript/src/index.d.ts +20 -1
  43. package/lib/typescript/src/index.d.ts.map +1 -1
  44. package/lib/typescript/src/types.d.ts +32 -1
  45. package/lib/typescript/src/types.d.ts.map +1 -1
  46. package/libs/android/arm64-v8a/libwebgpu_dawn.so +0 -0
  47. package/libs/android/armeabi-v7a/libwebgpu_dawn.so +0 -0
  48. package/libs/android/x86/libwebgpu_dawn.so +0 -0
  49. package/libs/android/x86_64/libwebgpu_dawn.so +0 -0
  50. package/libs/apple/libwebgpu_dawn.xcframework/Info.plist +6 -6
  51. package/libs/apple/libwebgpu_dawn.xcframework/ios-arm64/libwebgpu_dawn.a +0 -0
  52. package/libs/apple/libwebgpu_dawn.xcframework/ios-arm64_x86_64-simulator/libwebgpu_dawn.a +0 -0
  53. package/libs/apple/libwebgpu_dawn.xcframework/macos-arm64_x86_64/libwebgpu_dawn.a +0 -0
  54. package/package.json +3 -3
  55. package/src/Canvas.tsx +0 -15
  56. package/src/index.tsx +62 -2
  57. package/src/types.ts +83 -1
@@ -0,0 +1,314 @@
1
+ #include "AppleVideoPlayer.h"
2
+
3
+ #import <AVFoundation/AVFoundation.h>
4
+ #import <CoreVideo/CoreVideo.h>
5
+
6
+ #include <stdexcept>
7
+
8
+ namespace rnwgpu {
9
+
10
+ namespace {
11
+
12
+ // 3x4 row-major matrices mapping [Y, Cb, Cr, 1] to gamma-encoded R'G'B' (NOT
13
+ // linear): YCbCr is derived from gamma-encoded RGB, so this conversion stays in
14
+ // the encoded domain. The sRGB decode in srcTransferFunctionParameters
15
+ // linearizes afterward (see GPUExternalTexture.cpp). Limited-range (video
16
+ // range) means luma is 16..235, chroma is 16..240 (8-bit).
17
+ // Reference: https://en.wikipedia.org/wiki/YCbCr (BT.601 / BT.709).
18
+ static constexpr float kBT709LimitedToRgb[12] = {
19
+ 1.164383f, 0.000000f, 1.792741f, -0.972945f, //
20
+ 1.164383f, -0.213249f, -0.532909f, 0.301517f, //
21
+ 1.164383f, 2.112402f, 0.000000f, -1.133402f, //
22
+ };
23
+ static constexpr float kBT601LimitedToRgb[12] = {
24
+ 1.164383f, 0.000000f, 1.596027f, -0.874202f, //
25
+ 1.164383f, -0.391762f, -0.812968f, 0.531668f, //
26
+ 1.164383f, 2.017232f, 0.000000f, -1.085631f, //
27
+ };
28
+ static constexpr float kBT2020LimitedToRgb[12] = {
29
+ 1.164383f, 0.000000f, 1.678674f, -0.915688f, //
30
+ 1.164383f, -0.187326f, -0.650424f, 0.347459f, //
31
+ 1.164383f, 2.141772f, 0.000000f, -1.148145f, //
32
+ };
33
+
34
+ // Pick the right YUV→RGB matrix from the pixel buffer's color attachments.
35
+ // Falls back to BT.709 limited range (the right call for ≥720p H.264, which
36
+ // is what AVPlayer hands us for Big Buck Bunny and most streamed media).
37
+ static void fillYuvMatrix(CVPixelBufferRef pixelBuffer, float out[12]) {
38
+ CFTypeRef matrixKey = CVBufferGetAttachment(
39
+ pixelBuffer, kCVImageBufferYCbCrMatrixKey, nullptr);
40
+ const float *src = kBT709LimitedToRgb;
41
+ if (matrixKey) {
42
+ auto matrix = (CFStringRef)matrixKey;
43
+ if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_ITU_R_601_4) ||
44
+ CFEqual(matrix, kCVImageBufferYCbCrMatrix_SMPTE_240M_1995)) {
45
+ src = kBT601LimitedToRgb;
46
+ } else if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_ITU_R_2020)) {
47
+ src = kBT2020LimitedToRgb;
48
+ }
49
+ }
50
+ for (int i = 0; i < 12; ++i) {
51
+ out[i] = src[i];
52
+ }
53
+ }
54
+
55
+ // Map a CVPixelBuffer's pixel format to our VideoPixelFormat enum.
56
+ static VideoPixelFormat pixelFormatFromCVPixelBuffer(
57
+ CVPixelBufferRef pixelBuffer) {
58
+ OSType type = CVPixelBufferGetPixelFormatType(pixelBuffer);
59
+ switch (type) {
60
+ case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
61
+ case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
62
+ return VideoPixelFormat::NV12;
63
+ default:
64
+ return VideoPixelFormat::BGRA8;
65
+ }
66
+ }
67
+
68
+ class AppleVideoPlayer : public IVideoPlayer {
69
+ public:
70
+ AppleVideoPlayer(AVPlayer *player, AVPlayerItemVideoOutput *output,
71
+ id loopObserver)
72
+ : _player(player), _output(output), _loopObserver(loopObserver) {}
73
+
74
+ ~AppleVideoPlayer() override {
75
+ if (_loopObserver) {
76
+ [[NSNotificationCenter defaultCenter] removeObserver:_loopObserver];
77
+ _loopObserver = nil;
78
+ }
79
+ [_player pause];
80
+ _player = nil;
81
+ _output = nil;
82
+ }
83
+
84
+ VideoFrameHandle copyLatestFrame() override {
85
+ CMTime currentTime = [_output itemTimeForHostTime:CACurrentMediaTime()];
86
+ if (![_output hasNewPixelBufferForItemTime:currentTime]) {
87
+ return {};
88
+ }
89
+ // copyPixelBufferForItemTime returns a +1 retained CVPixelBuffer; we then
90
+ // hand it to wrapCVPixelBuffer which adds another retain. Balance with a
91
+ // CFRelease here so we don't leak.
92
+ CVPixelBufferRef pixelBuffer =
93
+ [_output copyPixelBufferForItemTime:currentTime
94
+ itemTimeForDisplay:nullptr];
95
+ if (!pixelBuffer) {
96
+ return {};
97
+ }
98
+ try {
99
+ auto handle = wrapCVPixelBuffer(pixelBuffer);
100
+ CFRelease(pixelBuffer);
101
+ return handle;
102
+ } catch (...) {
103
+ CFRelease(pixelBuffer);
104
+ throw;
105
+ }
106
+ }
107
+
108
+ void play() override { [_player play]; }
109
+ void pause() override { [_player pause]; }
110
+
111
+ private:
112
+ AVPlayer *_player;
113
+ AVPlayerItemVideoOutput *_output;
114
+ id _loopObserver;
115
+ };
116
+
117
+ } // namespace
118
+
119
+ VideoFrameHandle wrapCVPixelBuffer(CVPixelBufferRef pixelBuffer) {
120
+ if (!pixelBuffer) {
121
+ throw std::runtime_error("wrapCVPixelBuffer: pointer is null");
122
+ }
123
+ IOSurfaceRef ioSurface = CVPixelBufferGetIOSurface(pixelBuffer);
124
+ if (!ioSurface) {
125
+ throw std::runtime_error(
126
+ "wrapCVPixelBuffer: pixel buffer is not IOSurface-backed (was the "
127
+ "camera/video pipeline configured for Metal/IOSurface output?)");
128
+ }
129
+
130
+ // Retain the pixel buffer so the caller can release theirs immediately.
131
+ CFRetain(pixelBuffer);
132
+
133
+ VideoFrameHandle handle;
134
+ handle.handle = (void *)ioSurface;
135
+ handle.width = static_cast<uint32_t>(CVPixelBufferGetWidth(pixelBuffer));
136
+ handle.height = static_cast<uint32_t>(CVPixelBufferGetHeight(pixelBuffer));
137
+ handle.pixelFormat = pixelFormatFromCVPixelBuffer(pixelBuffer);
138
+ if (handle.pixelFormat == VideoPixelFormat::NV12) {
139
+ fillYuvMatrix(pixelBuffer, handle.yuvToRgbMatrix);
140
+ }
141
+ handle.deleter = [pixelBuffer]() { CFRelease(pixelBuffer); };
142
+ return handle;
143
+ }
144
+
145
+ std::unique_ptr<IVideoPlayer>
146
+ createAppleVideoPlayer(const std::string &path, VideoPixelFormat format) {
147
+ NSString *nsPath = [NSString stringWithUTF8String:path.c_str()];
148
+ NSURL *url;
149
+ if ([nsPath hasPrefix:@"http://"] || [nsPath hasPrefix:@"https://"] ||
150
+ [nsPath hasPrefix:@"file://"]) {
151
+ url = [NSURL URLWithString:nsPath];
152
+ } else {
153
+ url = [NSURL fileURLWithPath:nsPath];
154
+ }
155
+
156
+ AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
157
+ if (!item) {
158
+ throw std::runtime_error("createAppleVideoPlayer: failed to create "
159
+ "AVPlayerItem");
160
+ }
161
+
162
+ // NV12 (kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) lets us hand the
163
+ // IOSurface straight to Dawn as a multi-planar texture for
164
+ // importExternalTexture. BGRA is the "decode + convert" path for the
165
+ // single-plane SharedTextureMemory demo.
166
+ OSType pixelFormat = format == VideoPixelFormat::NV12
167
+ ? kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
168
+ : kCVPixelFormatType_32BGRA;
169
+ NSDictionary *outputSettings = @{
170
+ (NSString *)kCVPixelBufferPixelFormatTypeKey : @(pixelFormat),
171
+ (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{},
172
+ (NSString *)kCVPixelBufferMetalCompatibilityKey : @YES,
173
+ };
174
+ AVPlayerItemVideoOutput *output = [[AVPlayerItemVideoOutput alloc]
175
+ initWithPixelBufferAttributes:outputSettings];
176
+ [item addOutput:output];
177
+
178
+ AVPlayer *player = [AVPlayer playerWithPlayerItem:item];
179
+ player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
180
+
181
+ // Loop on end-of-stream by seeking back to zero.
182
+ __weak AVPlayer *weakPlayer = player;
183
+ id loopObserver = [[NSNotificationCenter defaultCenter]
184
+ addObserverForName:AVPlayerItemDidPlayToEndTimeNotification
185
+ object:item
186
+ queue:[NSOperationQueue mainQueue]
187
+ usingBlock:^(NSNotification * /*note*/) {
188
+ [weakPlayer seekToTime:kCMTimeZero];
189
+ [weakPlayer play];
190
+ }];
191
+
192
+ return std::make_unique<AppleVideoPlayer>(player, output, loopObserver);
193
+ }
194
+
195
+ std::string writeAppleTestVideoFile() {
196
+ NSString *tmpDir = NSTemporaryDirectory();
197
+ NSString *outputPath =
198
+ [tmpDir stringByAppendingPathComponent:@"rnwgpu-test-video.mp4"];
199
+ NSURL *outputURL = [NSURL fileURLWithPath:outputPath];
200
+
201
+ // If the file already exists, reuse it. This makes the example zero-cost on
202
+ // subsequent runs.
203
+ if ([[NSFileManager defaultManager] fileExistsAtPath:outputPath]) {
204
+ return [outputPath UTF8String];
205
+ }
206
+
207
+ NSError *error = nil;
208
+ AVAssetWriter *writer = [AVAssetWriter assetWriterWithURL:outputURL
209
+ fileType:AVFileTypeMPEG4
210
+ error:&error];
211
+ if (error || !writer) {
212
+ throw std::runtime_error(
213
+ std::string("writeTestVideoFile: AVAssetWriter init failed: ") +
214
+ [[error localizedDescription] UTF8String]);
215
+ }
216
+
217
+ const int kWidth = 256;
218
+ const int kHeight = 256;
219
+ const int kFps = 30;
220
+ const int kSeconds = 3;
221
+ const int kTotalFrames = kFps * kSeconds;
222
+
223
+ NSDictionary *videoSettings = @{
224
+ AVVideoCodecKey : AVVideoCodecTypeH264,
225
+ AVVideoWidthKey : @(kWidth),
226
+ AVVideoHeightKey : @(kHeight),
227
+ };
228
+ AVAssetWriterInput *writerInput =
229
+ [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
230
+ outputSettings:videoSettings];
231
+ writerInput.expectsMediaDataInRealTime = NO;
232
+
233
+ NSDictionary *bufferAttrs = @{
234
+ (NSString *)kCVPixelBufferPixelFormatTypeKey :
235
+ @(kCVPixelFormatType_32BGRA),
236
+ (NSString *)kCVPixelBufferWidthKey : @(kWidth),
237
+ (NSString *)kCVPixelBufferHeightKey : @(kHeight),
238
+ (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{},
239
+ };
240
+ AVAssetWriterInputPixelBufferAdaptor *adaptor =
241
+ [AVAssetWriterInputPixelBufferAdaptor
242
+ assetWriterInputPixelBufferAdaptorWithAssetWriterInput:writerInput
243
+ sourcePixelBufferAttributes:bufferAttrs];
244
+ [writer addInput:writerInput];
245
+
246
+ if (![writer startWriting]) {
247
+ throw std::runtime_error(
248
+ std::string("writeTestVideoFile: startWriting failed: ") +
249
+ [[writer.error localizedDescription] UTF8String]);
250
+ }
251
+ [writer startSessionAtSourceTime:kCMTimeZero];
252
+
253
+ for (int i = 0; i < kTotalFrames; ++i) {
254
+ // Spin briefly if the input is not ready (the adaptor pool fills up).
255
+ while (!writerInput.isReadyForMoreMediaData) {
256
+ [NSThread sleepForTimeInterval:0.005];
257
+ }
258
+
259
+ CVPixelBufferRef pixelBuffer = NULL;
260
+ CVReturn err = CVPixelBufferPoolCreatePixelBuffer(
261
+ kCFAllocatorDefault, adaptor.pixelBufferPool, &pixelBuffer);
262
+ if (err != kCVReturnSuccess || !pixelBuffer) {
263
+ throw std::runtime_error("writeTestVideoFile: pool exhausted");
264
+ }
265
+
266
+ CVPixelBufferLockBaseAddress(pixelBuffer, 0);
267
+ uint8_t *base =
268
+ static_cast<uint8_t *>(CVPixelBufferGetBaseAddress(pixelBuffer));
269
+ size_t rowBytes = CVPixelBufferGetBytesPerRow(pixelBuffer);
270
+ // Procedural pattern: scrolling diagonal stripes + per-frame color shift.
271
+ int phase = i * 6;
272
+ for (int y = 0; y < kHeight; ++y) {
273
+ uint8_t *row = base + y * rowBytes;
274
+ for (int x = 0; x < kWidth; ++x) {
275
+ uint8_t r = static_cast<uint8_t>((x + phase) & 0xFF);
276
+ uint8_t g = static_cast<uint8_t>((y + phase * 2) & 0xFF);
277
+ uint8_t b =
278
+ static_cast<uint8_t>(((x + y + phase) & 0x40) ? 220 : 30);
279
+ row[x * 4 + 0] = b;
280
+ row[x * 4 + 1] = g;
281
+ row[x * 4 + 2] = r;
282
+ row[x * 4 + 3] = 0xFF;
283
+ }
284
+ }
285
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
286
+
287
+ CMTime pts = CMTimeMake(i, kFps);
288
+ if (![adaptor appendPixelBuffer:pixelBuffer withPresentationTime:pts]) {
289
+ CFRelease(pixelBuffer);
290
+ throw std::runtime_error(
291
+ std::string("writeTestVideoFile: appendPixelBuffer failed: ") +
292
+ [[writer.error localizedDescription] UTF8String]);
293
+ }
294
+ CFRelease(pixelBuffer);
295
+ }
296
+
297
+ [writerInput markAsFinished];
298
+ dispatch_semaphore_t sem = dispatch_semaphore_create(0);
299
+ [writer finishWritingWithCompletionHandler:^{
300
+ dispatch_semaphore_signal(sem);
301
+ }];
302
+ dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
303
+
304
+ if (writer.status != AVAssetWriterStatusCompleted) {
305
+ throw std::runtime_error(
306
+ std::string("writeTestVideoFile: writer finished with status ") +
307
+ std::to_string(static_cast<long>(writer.status)) + ": " +
308
+ [[writer.error localizedDescription] UTF8String]);
309
+ }
310
+
311
+ return [outputPath UTF8String];
312
+ }
313
+
314
+ } // namespace rnwgpu
@@ -1,6 +1,8 @@
1
1
  #pragma once
2
2
  #include <jsi/jsi.h>
3
3
 
4
+ #include <cmath>
5
+ #include <cstdint>
4
6
  #include <memory>
5
7
 
6
8
  #include "JSIConverter.h"
@@ -47,16 +49,58 @@ template <> struct JSIConverter<std::shared_ptr<ArrayBuffer>> {
47
49
  if (bufferProp.isObject() &&
48
50
  bufferProp.getObject(runtime).isArrayBuffer(runtime)) {
49
51
  auto buff = bufferProp.getObject(runtime);
50
- auto bytesPerElements =
51
- obj.getProperty(runtime, "BYTES_PER_ELEMENT").asNumber();
52
52
  auto arrayBuffer = buff.getArrayBuffer(runtime);
53
- auto byteOffset = static_cast<size_t>(
54
- obj.getProperty(runtime, "byteOffset").asNumber());
55
- auto byteLength = static_cast<size_t>(
56
- obj.getProperty(runtime, "byteLength").asNumber());
53
+ const size_t bufferSize = arrayBuffer.size(runtime);
54
+
55
+ // byteOffset / byteLength are user-readable JS properties, not values
56
+ // the engine guarantees (unlike Dawn's node binding, which reads them
57
+ // off the engine's typed-array view). Read them as doubles so we can
58
+ // reject negative, non-integral, NaN/Inf, or oversized values before
59
+ // they wrap around when cast to size_t.
60
+ const double byteOffsetValue =
61
+ obj.getProperty(runtime, "byteOffset").asNumber();
62
+ const double byteLengthValue =
63
+ obj.getProperty(runtime, "byteLength").asNumber();
64
+
65
+ auto isValidByteIndex = [](double value) {
66
+ return std::isfinite(value) && value >= 0.0 &&
67
+ value <= static_cast<double>(SIZE_MAX) &&
68
+ std::floor(value) == value;
69
+ };
70
+ if (!isValidByteIndex(byteOffsetValue) ||
71
+ !isValidByteIndex(byteLengthValue)) {
72
+ throw std::runtime_error(
73
+ "ArrayBuffer::fromJSI: invalid byteOffset/byteLength");
74
+ }
75
+
76
+ const size_t byteOffset = static_cast<size_t>(byteOffsetValue);
77
+ const size_t byteLength = static_cast<size_t>(byteLengthValue);
78
+
79
+ // Overflow-safe bounds check: byteOffset + byteLength <= bufferSize.
80
+ if (byteOffset > bufferSize ||
81
+ byteLength > bufferSize - byteOffset) {
82
+ throw std::runtime_error(
83
+ "ArrayBuffer::fromJSI: view bounds [byteOffset, byteOffset + "
84
+ "byteLength) exceed the backing ArrayBuffer size");
85
+ }
86
+
87
+ // BYTES_PER_ELEMENT is absent on a DataView; default to 1. A spoofed
88
+ // object could report 0 (or a negative/NaN value), so clamp to a
89
+ // minimum of 1 to avoid a later division by zero in writeBuffer.
90
+ size_t bytesPerElements = 1;
91
+ if (obj.hasProperty(runtime, "BYTES_PER_ELEMENT")) {
92
+ auto bpe = obj.getProperty(runtime, "BYTES_PER_ELEMENT");
93
+ if (bpe.isNumber()) {
94
+ const double value = bpe.asNumber();
95
+ if (std::isfinite(value) && value >= 1.0) {
96
+ bytesPerElements = static_cast<size_t>(value);
97
+ }
98
+ }
99
+ }
100
+
57
101
  return std::make_shared<ArrayBuffer>(
58
102
  arrayBuffer.data(runtime) + byteOffset, byteLength,
59
- static_cast<size_t>(bytesPerElements));
103
+ bytesPerElements);
60
104
  }
61
105
  }
62
106
  }
@@ -4,6 +4,7 @@
4
4
  #include <memory>
5
5
  #include <span>
6
6
  #include <string>
7
+ #include <utility>
7
8
  #include <vector>
8
9
 
9
10
  #include "webgpu/webgpu_cpp.h"
@@ -17,6 +18,53 @@ struct ImageData {
17
18
  wgpu::TextureFormat format;
18
19
  };
19
20
 
21
+ // Pixel layout of a VideoFrame. Determines whether the underlying surface is
22
+ // a single RGBA plane or a biplanar Y / CbCr pair.
23
+ enum class VideoPixelFormat {
24
+ // Single-plane 8-bit BGRA (default; what RGBA-style sampling expects).
25
+ BGRA8,
26
+ // Biplanar 4:2:0 8-bit Y + interleaved CbCr (NV12). Used for the
27
+ // importExternalTexture path; needs the YUV→RGB conversion matrix below.
28
+ NV12,
29
+ };
30
+
31
+ // A native handle to a video frame that can be imported into a
32
+ // GPUSharedTextureMemory.
33
+ //
34
+ // - handle is an IOSurfaceRef on Apple platforms
35
+ // - handle is an AHardwareBuffer* on Android
36
+ //
37
+ // The deleter is responsible for releasing the underlying backing object
38
+ // (CVPixelBuffer / AHardwareBuffer) and must be called exactly once. The
39
+ // VideoFrame JS wrapper handles this on destruction.
40
+ struct VideoFrameHandle {
41
+ void *handle = nullptr;
42
+ uint32_t width = 0;
43
+ uint32_t height = 0;
44
+ VideoPixelFormat pixelFormat = VideoPixelFormat::BGRA8;
45
+ // 3x4 row-major matrix mapping [Y, U, V, 1] → linear [R, G, B]. Pre-computed
46
+ // at decode time from CVPixelBuffer attachments (kCVImageBufferYCbCrMatrixKey
47
+ // + range), with a BT.709 limited-range default. Only meaningful when
48
+ // pixelFormat == NV12.
49
+ float yuvToRgbMatrix[12] = {};
50
+ std::function<void()> deleter;
51
+ };
52
+
53
+ // Platform-implemented video source that hands out fresh IOSurface /
54
+ // AHardwareBuffer-backed frames as a video plays.
55
+ class IVideoPlayer {
56
+ public:
57
+ virtual ~IVideoPlayer() = default;
58
+
59
+ // Returns the latest decoded frame, or an empty handle (handle == nullptr)
60
+ // when no new frame is ready yet. Each non-empty return retains its backing
61
+ // surface; the VideoFrame wrapper releases it on destruction.
62
+ virtual VideoFrameHandle copyLatestFrame() = 0;
63
+
64
+ virtual void play() = 0;
65
+ virtual void pause() = 0;
66
+ };
67
+
20
68
  class PlatformContext {
21
69
  public:
22
70
  PlatformContext() = default;
@@ -41,6 +89,39 @@ public:
41
89
  createImageBitmapFromDataAsync(std::span<const uint8_t> data,
42
90
  std::function<void(ImageData)> onSuccess,
43
91
  std::function<void(std::string)> onError) = 0;
92
+
93
+ // Decode the first video frame of `path` (a local file path) into a
94
+ // native, GPU-shareable surface. Used by the SharedTextureMemory example;
95
+ // not intended as a long-term media-loading API.
96
+ virtual VideoFrameHandle loadVideoFrame(const std::string &path) = 0;
97
+
98
+ // Create a synthetic, GPU-shareable IOSurface/AHardwareBuffer filled with a
99
+ // generated test pattern. Avoids the need to bundle a video asset for the
100
+ // SharedTextureMemory example.
101
+ virtual VideoFrameHandle createTestVideoFrame(uint32_t width,
102
+ uint32_t height) = 0;
103
+
104
+ // Open a video file at `path` for playback. The returned player yields
105
+ // IOSurface / AHardwareBuffer-backed frames via copyLatestFrame().
106
+ //
107
+ // `format` selects the requested pixel layout. BGRA8 is the easiest target
108
+ // for a regular sampled GPUTexture; NV12 is the right shape for the
109
+ // importExternalTexture path (zero-copy biplanar YUV).
110
+ virtual std::unique_ptr<IVideoPlayer>
111
+ createVideoPlayer(const std::string &path, VideoPixelFormat format) = 0;
112
+
113
+ // Wrap a CVPixelBufferRef (Apple) or AHardwareBuffer* (Android) pointer
114
+ // obtained from another library (typically VisionCamera's
115
+ // Frame.getNativeBuffer().pointer) as one of our VideoFrame handles.
116
+ //
117
+ // We CFRetain / AHardwareBuffer_acquire on the way in, so callers can
118
+ // safely release their own reference immediately after.
119
+ virtual VideoFrameHandle wrapNativeBuffer(void *pointer) = 0;
120
+
121
+ // Write a small procedurally-generated test video to a temporary location
122
+ // and return its absolute path. Lets the SharedTextureMemory example play
123
+ // a real decoded video without bundling an asset.
124
+ virtual std::string writeTestVideoFile() = 0;
44
125
  };
45
126
 
46
127
  } // namespace rnwgpu
@@ -31,12 +31,15 @@
31
31
  #include "GPURenderPassEncoder.h"
32
32
  #include "GPURenderPipeline.h"
33
33
  #include "GPUSampler.h"
34
+ #include "GPUSharedTextureMemory.h"
34
35
  #include "GPUShaderModule.h"
35
36
  #include "GPUSupportedLimits.h"
36
37
  #include "GPUTexture.h"
37
38
  #include "GPUTextureView.h"
38
39
  #include "GPUUncapturedErrorEvent.h"
39
40
  #include "GPUValidationError.h"
41
+ #include "VideoFrame.h"
42
+ #include "VideoPlayer.h"
40
43
 
41
44
  // Enums
42
45
  #include "GPUBufferUsage.h"
@@ -64,6 +67,12 @@ RNWebGPUManager::RNWebGPUManager(
64
67
  auto rnWebGPU =
65
68
  std::make_shared<RNWebGPU>(gpu, _platformContext, _jsCallInvoker);
66
69
  _gpu = gpu->get();
70
+
71
+ // RNWebGPU needs its brand registered in NativeObjectRegistry so the boxing
72
+ // path can install the prototype on worklet runtimes. installConstructor
73
+ // does that registration but also sets globalThis.RNWebGPU = ctor, so we
74
+ // call it FIRST and then overwrite the global with the actual instance.
75
+ RNWebGPU::installConstructor(*_jsRuntime);
67
76
  _jsRuntime->global().setProperty(*_jsRuntime, "RNWebGPU",
68
77
  RNWebGPU::create(*_jsRuntime, rnWebGPU));
69
78
 
@@ -97,10 +106,13 @@ RNWebGPUManager::RNWebGPUManager(
97
106
  GPURenderPassEncoder::installConstructor(*_jsRuntime);
98
107
  GPURenderPipeline::installConstructor(*_jsRuntime);
99
108
  GPUSampler::installConstructor(*_jsRuntime);
109
+ GPUSharedTextureMemory::installConstructor(*_jsRuntime);
100
110
  GPUShaderModule::installConstructor(*_jsRuntime);
101
111
  GPUSupportedLimits::installConstructor(*_jsRuntime);
102
112
  GPUTexture::installConstructor(*_jsRuntime);
103
113
  GPUTextureView::installConstructor(*_jsRuntime);
114
+ VideoFrame::installConstructor(*_jsRuntime);
115
+ VideoPlayer::installConstructor(*_jsRuntime);
104
116
 
105
117
  // Install constant objects as plain JS objects with own properties
106
118
  _jsRuntime->global().setProperty(*_jsRuntime, "GPUBufferUsage",
@@ -252,13 +252,24 @@ public:
252
252
 
253
253
  [[nodiscard]] bool Convert(wgpu::BindGroupLayoutEntry &out,
254
254
  const GPUBindGroupLayoutEntry &in) {
255
- return Convert(out.binding, in.binding) &&
256
- Convert(out.visibility, in.visibility) &&
257
- Convert(out.buffer, in.buffer) && Convert(out.sampler, in.sampler) &&
258
- Convert(out.texture, in.texture) &&
259
- Convert(out.storageTexture, in.storageTexture);
260
- // no external textures here
261
- //&& Convert(out.externalTexture, in.externalTexture);
255
+ out = {};
256
+ if (!Convert(out.binding, in.binding) ||
257
+ !Convert(out.visibility, in.visibility) ||
258
+ !Convert(out.buffer, in.buffer) || !Convert(out.sampler, in.sampler) ||
259
+ !Convert(out.texture, in.texture) ||
260
+ !Convert(out.storageTexture, in.storageTexture)) {
261
+ return false;
262
+ }
263
+ if (in.externalTexture.has_value() &&
264
+ in.externalTexture.value() != nullptr) {
265
+ // External texture layouts bind via a chained struct rather than a
266
+ // direct field on BindGroupLayoutEntry. The chained struct must outlive
267
+ // the BindGroupLayoutEntry until Device::CreateBindGroupLayout returns,
268
+ // so we allocate it on the Convertor's arena.
269
+ auto *chain = Allocate<wgpu::ExternalTextureBindingLayout>();
270
+ out.nextInChain = chain;
271
+ }
272
+ return true;
262
273
  }
263
274
 
264
275
  [[nodiscard]] bool Convert(wgpu::BlendComponent &out,
@@ -422,9 +433,11 @@ public:
422
433
  }
423
434
 
424
435
  [[nodiscard]] bool Convert(wgpu::ExternalTextureBindingLayout &out,
425
- const GPUExternalTextureBindingLayout &in) {
426
- // no external textures at the moment
427
- return false;
436
+ const GPUExternalTextureBindingLayout & /*in*/) {
437
+ // ExternalTextureBindingLayout carries no fields of its own; its presence
438
+ // (as a chained struct) is what marks the entry as an external texture.
439
+ out = {};
440
+ return true;
428
441
  }
429
442
 
430
443
  [[nodiscard]] bool Convert(wgpu::ConstantEntry &out, const std::string &key,
@@ -729,7 +742,16 @@ public:
729
742
  out.buffer = buffer->get();
730
743
  return true;
731
744
  }
732
- // Not external textures at the moment
745
+ if (in.externalTexture != nullptr) {
746
+ // External textures bind via a chained struct rather than a direct field
747
+ // on BindGroupEntry. The chained struct must outlive the
748
+ // BindGroupEntry until Device::CreateBindGroup returns, so we allocate
749
+ // it on the Convertor's arena.
750
+ auto *chain = Allocate<wgpu::ExternalTextureBindingEntry>();
751
+ chain->externalTexture = in.externalTexture->get();
752
+ out.nextInChain = chain;
753
+ return true;
754
+ }
733
755
  return false;
734
756
  }
735
757
 
@@ -20,6 +20,33 @@ GPU::GPU(jsi::Runtime &runtime) : NativeObject(CLASS_NAME) {
20
20
 
21
21
  wgpu::InstanceLimits limits{.timedWaitAnyMaxCount = 64};
22
22
  instanceDesc.requiredLimits = &limits;
23
+
24
+ // Expose Dawn's experimental adapter features. Several features needed by
25
+ // our Android external-texture path (YCbCrVulkanSamplers,
26
+ // OpaqueYCbCrAndroidForExternalTexture) are tagged Experimental in Dawn's
27
+ // feature table and are otherwise filtered out of adapter.features by
28
+ // PhysicalDeviceBase::GetSupportedFeatures. The allow_unsafe_apis toggle
29
+ // disables that filter so the features become visible;
30
+ // expose_wgsl_experimental_features is the parallel toggle for WGSL language
31
+ // features.
32
+ //
33
+ // Trade-off: these are instance-level Dawn toggles, so they apply to every
34
+ // adapter/device created from this instance, not just the external-texture
35
+ // path. There is no finer-grained, per-feature mechanism to surface these.
36
+ // The exposure is acceptable because the toggle only *un-hides* experimental
37
+ // features in adapter.features; it does not enable any of them. Nothing
38
+ // experimental becomes active unless application code explicitly lists that
39
+ // feature in requiredFeatures at device creation, so the default behavior of
40
+ // a device that asks for no experimental features is unchanged.
41
+ static const char *const kEnabledToggles[] = {
42
+ "allow_unsafe_apis",
43
+ "expose_wgsl_experimental_features",
44
+ };
45
+ wgpu::DawnTogglesDescriptor toggles;
46
+ toggles.enabledToggleCount = std::size(kEnabledToggles);
47
+ toggles.enabledToggles = kEnabledToggles;
48
+ instanceDesc.nextInChain = &toggles;
49
+
23
50
  _instance = wgpu::CreateInstance(&instanceDesc);
24
51
 
25
52
  auto dispatcher = std::make_shared<async::JSIMicrotaskDispatcher>(runtime);