react-native-webgpu 0.15.1 → 0.16.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/android/CMakeLists.txt +5 -0
- package/android/cpp/cpp-adapter.cpp +160 -49
- package/android/src/main/java/com/webgpu/SurfaceFrameRateVote.java +196 -0
- package/android/src/main/java/com/webgpu/WebGPUView.java +29 -2
- package/apple/ApplePlatformContext.h +1 -0
- package/cpp/jsi/JSICache.cpp +2 -0
- package/cpp/jsi/NativeObject.h +36 -12
- package/cpp/rnwgpu/RNWebGPUManager.cpp +4 -0
- package/cpp/rnwgpu/SurfaceRegistry.h +28 -4
- package/cpp/rnwgpu/api/GPUDevice.cpp +96 -0
- package/cpp/rnwgpu/api/GPUDevice.h +22 -0
- package/cpp/rnwgpu/api/GPUError.h +1 -0
- package/cpp/rnwgpu/api/GPUExtent2D.h +56 -0
- package/cpp/rnwgpu/api/GPUExternalTexture.cpp +32 -408
- package/cpp/rnwgpu/api/GPUExternalTexture.h +12 -37
- package/cpp/rnwgpu/api/GPUInternalError.h +1 -0
- package/cpp/rnwgpu/api/GPUNativeBufferExternalTexture.h +60 -0
- package/cpp/rnwgpu/api/GPUNativeBufferTexture.h +52 -0
- package/cpp/rnwgpu/api/GPUNativeBufferWrapper.cpp +144 -0
- package/cpp/rnwgpu/api/GPUNativeBufferWrapper.h +142 -0
- package/cpp/rnwgpu/api/GPUOutOfMemoryError.h +1 -0
- package/cpp/rnwgpu/api/GPUQueue.h +1 -0
- package/cpp/rnwgpu/api/GPUSharedFence.h +1 -0
- package/cpp/rnwgpu/api/GPUSharedTextureMemory.cpp +12 -44
- package/cpp/rnwgpu/api/GPUSharedTextureMemory.h +1 -0
- package/cpp/rnwgpu/api/GPUUncapturedErrorEvent.h +1 -0
- package/cpp/rnwgpu/api/GPUValidationError.h +1 -0
- package/cpp/rnwgpu/api/NativeFrameImport.cpp +638 -0
- package/cpp/rnwgpu/api/NativeFrameImport.h +86 -0
- package/cpp/rnwgpu/api/VideoFrame.h +21 -7
- package/cpp/rnwgpu/api/descriptors/GPUExternalTextureDescriptor.h +153 -1
- package/cpp/rnwgpu/api/descriptors/GPUNativeBufferTextureDescriptor.h +55 -0
- package/cpp/rnwgpu/async/RuntimeContext.cpp +1 -0
- package/lib/commonjs/Canvas.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/Canvas.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/Canvas.d.ts +2 -31
- package/lib/typescript/src/Canvas.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +33 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Canvas.tsx +3 -36
- package/src/index.tsx +24 -0
- package/src/types.ts +97 -0
|
@@ -59,7 +59,7 @@ struct Size {
|
|
|
59
59
|
using NativeSurfaceReleaser = std::function<void(void *)>;
|
|
60
60
|
|
|
61
61
|
#if defined(__ANDROID__)
|
|
62
|
-
// --- AHB-pool presentation mode (
|
|
62
|
+
// --- AHB-pool presentation mode (WebGPUNativeBufferView)
|
|
63
63
|
// -----------------------------
|
|
64
64
|
//
|
|
65
65
|
// A third presentation backend (besides the on-screen wgpu::Surface swapchain
|
|
@@ -71,7 +71,7 @@ using NativeSurfaceReleaser = std::function<void(void *)>;
|
|
|
71
71
|
// like the swapchain, so the canvas texture always matches the app's other
|
|
72
72
|
// attachments (e.g. its depth texture).
|
|
73
73
|
|
|
74
|
-
#define RNWGPU_LOG_TAG "
|
|
74
|
+
#define RNWGPU_LOG_TAG "WebGPUNativeBufferView"
|
|
75
75
|
// Failure / anomaly reporting only (never per-frame).
|
|
76
76
|
#define RNWGPU_POOL_WARN(...) \
|
|
77
77
|
__android_log_print(ANDROID_LOG_WARN, RNWGPU_LOG_TAG, __VA_ARGS__)
|
|
@@ -435,7 +435,7 @@ public:
|
|
|
435
435
|
bool hasNativeSurface() {
|
|
436
436
|
std::shared_lock<std::shared_mutex> lock(_mutex);
|
|
437
437
|
#if defined(__ANDROID__)
|
|
438
|
-
// A
|
|
438
|
+
// A WebGPUNativeBufferView in pool mode owns presentation the same way a
|
|
439
439
|
// surface-backed view does, and retires the entry on its own teardown.
|
|
440
440
|
if (_poolMode.load()) {
|
|
441
441
|
return true;
|
|
@@ -548,16 +548,27 @@ public:
|
|
|
548
548
|
if (device) {
|
|
549
549
|
dawn::native::metal::WaitForCommandsToBeScheduled(device.Get());
|
|
550
550
|
}
|
|
551
|
+
#endif
|
|
552
|
+
#if defined(__ANDROID__)
|
|
553
|
+
std::function<void()> framePresented;
|
|
551
554
|
#endif
|
|
552
555
|
{
|
|
553
556
|
std::unique_lock<std::shared_mutex> lock(_mutex);
|
|
554
557
|
if (_surface && _acquiredFromSurface) {
|
|
555
558
|
_surface.Present();
|
|
559
|
+
#if defined(__ANDROID__)
|
|
560
|
+
framePresented = _framePresented;
|
|
561
|
+
#endif
|
|
556
562
|
}
|
|
557
563
|
_acquiredFromSurface = false;
|
|
558
564
|
_frameInFlight = false;
|
|
559
565
|
_frameEpoch++;
|
|
560
566
|
}
|
|
567
|
+
#if defined(__ANDROID__)
|
|
568
|
+
if (framePresented) {
|
|
569
|
+
framePresented();
|
|
570
|
+
}
|
|
571
|
+
#endif
|
|
561
572
|
applyPendingAttach();
|
|
562
573
|
}
|
|
563
574
|
|
|
@@ -691,6 +702,16 @@ public:
|
|
|
691
702
|
std::lock_guard<std::mutex> poolLock(_poolMutex);
|
|
692
703
|
_poolFrameReady = std::move(cb);
|
|
693
704
|
}
|
|
705
|
+
|
|
706
|
+
// Invoked from the presenting thread (never under _mutex) after each frame
|
|
707
|
+
// presentFrame() shows on the attached surface; the blit of the last
|
|
708
|
+
// offscreen frame onto a newly attached surface does not count. The view
|
|
709
|
+
// uses it to vote the display's refresh rate while the canvas animates
|
|
710
|
+
// (see SurfaceFrameRateVote.java). Pass nullptr to unregister.
|
|
711
|
+
void setFramePresentedCallback(std::function<void()> cb) {
|
|
712
|
+
std::unique_lock<std::shared_mutex> lock(_mutex);
|
|
713
|
+
_framePresented = std::move(cb);
|
|
714
|
+
}
|
|
694
715
|
#endif
|
|
695
716
|
|
|
696
717
|
private:
|
|
@@ -704,7 +725,7 @@ private:
|
|
|
704
725
|
{
|
|
705
726
|
std::unique_lock<std::shared_mutex> lock(_mutex);
|
|
706
727
|
#if defined(__ANDROID__)
|
|
707
|
-
// Leaving pool mode (the
|
|
728
|
+
// Leaving pool mode (the WebGPUNativeBufferView detached): drop the
|
|
708
729
|
// pool. A configured context keeps rendering into the offscreen
|
|
709
730
|
// fallback, exactly like the surface path.
|
|
710
731
|
if (_poolMode.load()) {
|
|
@@ -1239,6 +1260,9 @@ private:
|
|
|
1239
1260
|
static constexpr int kPoolSize =
|
|
1240
1261
|
5; // 2 held + 1 rendering + 1 waiting + 1 ready
|
|
1241
1262
|
|
|
1263
|
+
// See setFramePresentedCallback(); read under _mutex, called outside it.
|
|
1264
|
+
std::function<void()> _framePresented;
|
|
1265
|
+
|
|
1242
1266
|
std::atomic<bool> _poolMode{false};
|
|
1243
1267
|
std::mutex _poolMutex;
|
|
1244
1268
|
std::condition_variable _freeCv; // a slot returned to Free
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#include "GPUDevice.h"
|
|
2
2
|
|
|
3
|
+
#include <cstdio>
|
|
3
4
|
#include <memory>
|
|
4
5
|
#include <string>
|
|
5
6
|
#include <unordered_set>
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
#include "GPUInternalError.h"
|
|
16
17
|
#include "GPUOutOfMemoryError.h"
|
|
17
18
|
#include "GPUValidationError.h"
|
|
19
|
+
#include "NativeFrameImport.h"
|
|
18
20
|
#include "SurfaceRegistry.h"
|
|
19
21
|
|
|
20
22
|
namespace rnwgpu {
|
|
@@ -130,6 +132,14 @@ std::shared_ptr<GPUSupportedLimits> GPUDevice::getLimits() {
|
|
|
130
132
|
return std::make_shared<GPUSupportedLimits>(limits);
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
std::shared_ptr<GPUAdapterInfo> GPUDevice::getAdapterInfo() {
|
|
136
|
+
wgpu::AdapterInfo info = {};
|
|
137
|
+
if (_instance.GetAdapterInfo(&info) != wgpu::Status::Success) {
|
|
138
|
+
throw std::runtime_error("failed to get the device's adapter info");
|
|
139
|
+
}
|
|
140
|
+
return std::make_shared<GPUAdapterInfo>(info);
|
|
141
|
+
}
|
|
142
|
+
|
|
133
143
|
std::shared_ptr<GPUQueue> GPUDevice::getQueue() {
|
|
134
144
|
auto result = _instance.GetQueue();
|
|
135
145
|
return std::make_shared<GPUQueue>(_instance, result, _async, _videoFrameBlit,
|
|
@@ -413,6 +423,92 @@ std::shared_ptr<GPUSharedFence> GPUDevice::importSharedFence(
|
|
|
413
423
|
std::move(label));
|
|
414
424
|
}
|
|
415
425
|
|
|
426
|
+
std::shared_ptr<GPUNativeBufferTexture> GPUDevice::createNativeBufferTexture(
|
|
427
|
+
std::shared_ptr<VideoFrame> source,
|
|
428
|
+
std::optional<std::shared_ptr<GPUNativeBufferTextureDescriptor>>
|
|
429
|
+
descriptor) {
|
|
430
|
+
static constexpr const char *kCaller = "GPUDevice::createNativeBufferTexture";
|
|
431
|
+
if (!source) {
|
|
432
|
+
throw std::runtime_error("GPUDevice::createNativeBufferTexture(): "
|
|
433
|
+
"source (NativeVideoFrame) is required");
|
|
434
|
+
}
|
|
435
|
+
std::string label;
|
|
436
|
+
std::optional<double> usage;
|
|
437
|
+
if (descriptor.has_value() && *descriptor) {
|
|
438
|
+
label = (*descriptor)->label.value_or("");
|
|
439
|
+
usage = (*descriptor)->usage;
|
|
440
|
+
}
|
|
441
|
+
auto memory =
|
|
442
|
+
native_frame::importMemory(_instance, source->handle(), label, kCaller);
|
|
443
|
+
wgpu::Texture texture;
|
|
444
|
+
if (usage.has_value()) {
|
|
445
|
+
// Dawn's layer describes the texture from the memory's properties, with
|
|
446
|
+
// the caller's usage. An unsupported usage is a validation error and an
|
|
447
|
+
// error texture, as with createTexture().
|
|
448
|
+
wgpu::SharedTextureMemoryProperties properties{};
|
|
449
|
+
(void)memory.GetProperties(&properties);
|
|
450
|
+
wgpu::TextureDescriptor desc{};
|
|
451
|
+
if (!label.empty()) {
|
|
452
|
+
desc.label = wgpu::StringView(label.c_str(), label.size());
|
|
453
|
+
}
|
|
454
|
+
desc.usage = static_cast<wgpu::TextureUsage>(static_cast<uint64_t>(*usage));
|
|
455
|
+
desc.dimension = wgpu::TextureDimension::e2D;
|
|
456
|
+
desc.size = properties.size;
|
|
457
|
+
desc.format = properties.format;
|
|
458
|
+
desc.mipLevelCount = 1;
|
|
459
|
+
desc.sampleCount = 1;
|
|
460
|
+
texture = memory.CreateTexture(&desc);
|
|
461
|
+
} else {
|
|
462
|
+
texture = memory.CreateTexture();
|
|
463
|
+
}
|
|
464
|
+
if (texture == nullptr) {
|
|
465
|
+
throw std::runtime_error("GPUDevice::createNativeBufferTexture(): "
|
|
466
|
+
"CreateTexture returned null");
|
|
467
|
+
}
|
|
468
|
+
auto wrapped = std::make_shared<GPUTexture>(std::move(texture), label);
|
|
469
|
+
return std::make_shared<GPUNativeBufferTexture>(
|
|
470
|
+
_instance, std::move(memory), std::move(wrapped), std::move(source),
|
|
471
|
+
std::move(label));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
std::shared_ptr<GPUNativeBufferExternalTexture>
|
|
475
|
+
GPUDevice::createNativeBufferExternalTexture(
|
|
476
|
+
std::shared_ptr<VideoFrame> source,
|
|
477
|
+
std::optional<std::shared_ptr<GPUExternalTextureDescriptor>> descriptor) {
|
|
478
|
+
static constexpr const char *kCaller =
|
|
479
|
+
"GPUDevice::createNativeBufferExternalTexture";
|
|
480
|
+
if (!source) {
|
|
481
|
+
throw std::runtime_error(
|
|
482
|
+
"GPUDevice::createNativeBufferExternalTexture(): source "
|
|
483
|
+
"(NativeVideoFrame) is required");
|
|
484
|
+
}
|
|
485
|
+
std::shared_ptr<GPUExternalTextureDescriptor> options =
|
|
486
|
+
descriptor.has_value() && *descriptor
|
|
487
|
+
? *descriptor
|
|
488
|
+
: std::make_shared<GPUExternalTextureDescriptor>();
|
|
489
|
+
const auto &frame = source->handle();
|
|
490
|
+
std::string label = options->label.value_or("external-texture");
|
|
491
|
+
// The import, the texture and the external texture, once per buffer. No
|
|
492
|
+
// access begins here: the wrapper's beginAccess() / endAccess() bracket
|
|
493
|
+
// each use.
|
|
494
|
+
auto memory = native_frame::importMemory(_instance, frame, label, kCaller);
|
|
495
|
+
auto texture = memory.CreateTexture();
|
|
496
|
+
if (texture == nullptr) {
|
|
497
|
+
throw std::runtime_error(
|
|
498
|
+
"GPUDevice::createNativeBufferExternalTexture(): CreateTexture "
|
|
499
|
+
"returned null");
|
|
500
|
+
}
|
|
501
|
+
auto external = native_frame::createExternalTexture(
|
|
502
|
+
_instance, memory, texture, frame, *options, label, kCaller);
|
|
503
|
+
// Non-owning: the wrapper owns the import (see GPUExternalTexture's
|
|
504
|
+
// constructor).
|
|
505
|
+
auto externalTexture = std::make_shared<GPUExternalTexture>(
|
|
506
|
+
std::move(external), nullptr, nullptr, nullptr, label);
|
|
507
|
+
return std::make_shared<GPUNativeBufferExternalTexture>(
|
|
508
|
+
_instance, std::move(memory), std::move(texture),
|
|
509
|
+
std::move(externalTexture), std::move(source), std::move(label));
|
|
510
|
+
}
|
|
511
|
+
|
|
416
512
|
async::AsyncTaskHandle GPUDevice::createComputePipelineAsync(
|
|
417
513
|
jsi::Runtime &runtime,
|
|
418
514
|
std::shared_ptr<GPUComputePipelineDescriptor> descriptor) {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
#include "webgpu/webgpu_cpp.h"
|
|
22
22
|
|
|
23
|
+
#include "GPUAdapterInfo.h"
|
|
23
24
|
#include "GPUBindGroup.h"
|
|
24
25
|
#include "GPUBindGroupDescriptor.h"
|
|
25
26
|
#include "GPUBindGroupLayout.h"
|
|
@@ -34,6 +35,9 @@
|
|
|
34
35
|
#include "GPUError.h"
|
|
35
36
|
#include "GPUExternalTexture.h"
|
|
36
37
|
#include "GPUExternalTextureDescriptor.h"
|
|
38
|
+
#include "GPUNativeBufferExternalTexture.h"
|
|
39
|
+
#include "GPUNativeBufferTexture.h"
|
|
40
|
+
#include "GPUNativeBufferTextureDescriptor.h"
|
|
37
41
|
#include "GPUPipelineLayout.h"
|
|
38
42
|
#include "GPUPipelineLayoutDescriptor.h"
|
|
39
43
|
#include "GPUQuerySet.h"
|
|
@@ -124,6 +128,17 @@ public:
|
|
|
124
128
|
std::shared_ptr<GPUSharedTextureMemoryDescriptor> descriptor);
|
|
125
129
|
std::shared_ptr<GPUSharedFence>
|
|
126
130
|
importSharedFence(std::shared_ptr<GPUSharedFenceDescriptor> descriptor);
|
|
131
|
+
// Import a native buffer once and bracket each use with beginAccess /
|
|
132
|
+
// endAccess: the counterparts of createTexture / createExternalTexture in
|
|
133
|
+
// GPUAndroidHardwareBufferUtil of Dawn's Android layer.
|
|
134
|
+
std::shared_ptr<GPUNativeBufferTexture> createNativeBufferTexture(
|
|
135
|
+
std::shared_ptr<VideoFrame> source,
|
|
136
|
+
std::optional<std::shared_ptr<GPUNativeBufferTextureDescriptor>>
|
|
137
|
+
descriptor);
|
|
138
|
+
std::shared_ptr<GPUNativeBufferExternalTexture>
|
|
139
|
+
createNativeBufferExternalTexture(
|
|
140
|
+
std::shared_ptr<VideoFrame> source,
|
|
141
|
+
std::optional<std::shared_ptr<GPUExternalTextureDescriptor>> descriptor);
|
|
127
142
|
std::shared_ptr<GPUBindGroupLayout> createBindGroupLayout(
|
|
128
143
|
std::shared_ptr<GPUBindGroupLayoutDescriptor> descriptor);
|
|
129
144
|
std::shared_ptr<GPUPipelineLayout>
|
|
@@ -153,6 +168,7 @@ public:
|
|
|
153
168
|
|
|
154
169
|
std::unordered_set<std::string> getFeatures();
|
|
155
170
|
std::shared_ptr<GPUSupportedLimits> getLimits();
|
|
171
|
+
std::shared_ptr<GPUAdapterInfo> getAdapterInfo();
|
|
156
172
|
std::shared_ptr<GPUQueue> getQueue();
|
|
157
173
|
jsi::Value getLost(jsi::Runtime &runtime, const jsi::Object &wrapper);
|
|
158
174
|
void notifyDeviceLost(wgpu::DeviceLostReason reason, std::string message);
|
|
@@ -183,6 +199,10 @@ public:
|
|
|
183
199
|
&GPUDevice::importSharedTextureMemory);
|
|
184
200
|
installMethod(runtime, prototype, "importSharedFence",
|
|
185
201
|
&GPUDevice::importSharedFence);
|
|
202
|
+
installMethod(runtime, prototype, "createNativeBufferTexture",
|
|
203
|
+
&GPUDevice::createNativeBufferTexture);
|
|
204
|
+
installMethod(runtime, prototype, "createNativeBufferExternalTexture",
|
|
205
|
+
&GPUDevice::createNativeBufferExternalTexture);
|
|
186
206
|
installMethod(runtime, prototype, "createBindGroupLayout",
|
|
187
207
|
&GPUDevice::createBindGroupLayout);
|
|
188
208
|
installMethod(runtime, prototype, "createPipelineLayout",
|
|
@@ -211,6 +231,8 @@ public:
|
|
|
211
231
|
&GPUDevice::popErrorScope);
|
|
212
232
|
installGetter(runtime, prototype, "features", &GPUDevice::getFeatures);
|
|
213
233
|
installGetter(runtime, prototype, "limits", &GPUDevice::getLimits);
|
|
234
|
+
installGetter(runtime, prototype, "adapterInfo",
|
|
235
|
+
&GPUDevice::getAdapterInfo);
|
|
214
236
|
installGetter(runtime, prototype, "queue", &GPUDevice::getQueue);
|
|
215
237
|
// `lost` is installed manually: the getter needs the wrapper object
|
|
216
238
|
// itself, because the promise (and its resolve function) are cached as
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <memory>
|
|
4
|
+
#include <stdexcept>
|
|
5
|
+
|
|
6
|
+
#include "JSIConverter.h"
|
|
7
|
+
|
|
8
|
+
#include "webgpu/webgpu_cpp.h"
|
|
9
|
+
|
|
10
|
+
namespace rnwgpu {
|
|
11
|
+
|
|
12
|
+
// A 2D size, as an object ({ width, height }) or an array ([width, height]),
|
|
13
|
+
// the two forms the spec gives GPUExtent3D. Both members are required in
|
|
14
|
+
// either form: a crop size with a missing one would be empty.
|
|
15
|
+
struct GPUExtent2D {
|
|
16
|
+
double width = 0;
|
|
17
|
+
double height = 0;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
template <> struct JSIConverter<std::shared_ptr<GPUExtent2D>> {
|
|
21
|
+
static std::shared_ptr<GPUExtent2D>
|
|
22
|
+
fromJSI(jsi::Runtime &runtime, const jsi::Value &arg, bool outOfBounds) {
|
|
23
|
+
auto result = std::make_unique<GPUExtent2D>();
|
|
24
|
+
if (!outOfBounds && arg.isObject()) {
|
|
25
|
+
if (arg.getObject(runtime).isArray(runtime)) {
|
|
26
|
+
auto array = arg.getObject(runtime).asArray(runtime);
|
|
27
|
+
if (array.size(runtime) < 2) {
|
|
28
|
+
throw std::runtime_error(
|
|
29
|
+
"Expected an array of size 2 for GPUExtent2D");
|
|
30
|
+
}
|
|
31
|
+
result->width = array.getValueAtIndex(runtime, 0).asNumber();
|
|
32
|
+
result->height = array.getValueAtIndex(runtime, 1).asNumber();
|
|
33
|
+
} else {
|
|
34
|
+
auto object = arg.getObject(runtime);
|
|
35
|
+
auto width = object.getProperty(runtime, "width");
|
|
36
|
+
auto height = object.getProperty(runtime, "height");
|
|
37
|
+
if (!width.isNumber() || !height.isNumber()) {
|
|
38
|
+
throw std::runtime_error(
|
|
39
|
+
"Expected numbers for the width and height of GPUExtent2D");
|
|
40
|
+
}
|
|
41
|
+
result->width = width.asNumber();
|
|
42
|
+
result->height = height.asNumber();
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
throw std::runtime_error("Expected an object for GPUExtent2D");
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
static jsi::Value toJSI(jsi::Runtime &runtime,
|
|
50
|
+
std::shared_ptr<GPUExtent2D> arg) {
|
|
51
|
+
// No conversions here
|
|
52
|
+
return jsi::Value::null();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
} // namespace rnwgpu
|