react-native-webgpu 0.10.5 → 0.11.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/cpp/AndroidPlatformContext.h +60 -3
- package/android/cpp/cpp-adapter.cpp +66 -6
- package/android/src/main/java/com/webgpu/ViewSnapshot.java +162 -0
- package/android/src/main/java/com/webgpu/WebGPUModule.java +68 -0
- package/apple/ApplePlatformContext.h +14 -1
- package/apple/ApplePlatformContext.mm +176 -1
- package/apple/WebGPUModule.mm +20 -1
- package/cpp/rnwgpu/PlatformContext.h +55 -0
- package/cpp/rnwgpu/RNWebGPUManager.cpp +7 -0
- package/cpp/rnwgpu/api/GPUQueue.cpp +187 -0
- package/cpp/rnwgpu/api/GPUQueue.h +16 -0
- package/cpp/rnwgpu/api/descriptors/GPUDrawElementImageDestination.h +60 -0
- package/cpp/rnwgpu/api/descriptors/GPUDrawElementImageSource.h +106 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/main/index.js +33 -0
- package/lib/commonjs/main/index.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/main/index.js +33 -0
- package/lib/module/main/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/main/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +13 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +29 -0
- package/src/main/index.tsx +49 -0
- package/src/types.ts +29 -0
|
@@ -21,9 +21,19 @@ namespace rnwgpu {
|
|
|
21
21
|
namespace jsi = facebook::jsi;
|
|
22
22
|
namespace jni = facebook::jni;
|
|
23
23
|
|
|
24
|
+
// The completion callbacks of one snapshotView request, heap-allocated and
|
|
25
|
+
// carried through Java as an opaque pointer (WebGPUModule.snapshotView ->
|
|
26
|
+
// onViewSnapshot), which hands them back exactly once.
|
|
27
|
+
struct ViewSnapshotCallbacks {
|
|
28
|
+
std::function<void(ImageData)> onSuccess;
|
|
29
|
+
std::function<void(std::string)> onError;
|
|
30
|
+
};
|
|
31
|
+
|
|
24
32
|
class AndroidPlatformContext : public PlatformContext {
|
|
25
33
|
private:
|
|
26
34
|
jobject _blobModule;
|
|
35
|
+
// Global ref to the com.webgpu.WebGPUModule instance (view snapshots).
|
|
36
|
+
jobject _module;
|
|
27
37
|
|
|
28
38
|
std::vector<uint8_t> resolveBlob(JNIEnv *env, const std::string &blobId,
|
|
29
39
|
double offset, double size) {
|
|
@@ -63,14 +73,61 @@ private:
|
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
public:
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
// Both arguments are global refs; the context releases them.
|
|
77
|
+
AndroidPlatformContext(jobject blobModule, jobject module)
|
|
78
|
+
: _blobModule(blobModule), _module(module) {}
|
|
68
79
|
~AndroidPlatformContext() {
|
|
80
|
+
JNIEnv *env = facebook::jni::Environment::current();
|
|
69
81
|
if (_blobModule) {
|
|
70
|
-
JNIEnv *env = facebook::jni::Environment::current();
|
|
71
82
|
env->DeleteGlobalRef(_blobModule);
|
|
72
83
|
_blobModule = nullptr;
|
|
73
84
|
}
|
|
85
|
+
if (_module) {
|
|
86
|
+
env->DeleteGlobalRef(_module);
|
|
87
|
+
_module = nullptr;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
void snapshotView(const ViewSnapshotRequest &request,
|
|
92
|
+
std::function<void(ImageData)> onSuccess,
|
|
93
|
+
std::function<void(std::string)> onError) override {
|
|
94
|
+
if (!_module) {
|
|
95
|
+
onError("drawElementImageToTexture: the WebGPU module is gone");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
jni::Environment::ensureCurrentThreadIsAttached();
|
|
99
|
+
JNIEnv *env = facebook::jni::Environment::current();
|
|
100
|
+
if (!env) {
|
|
101
|
+
onError("drawElementImageToTexture: couldn't get the JNI environment");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
jclass moduleClass = env->GetObjectClass(_module);
|
|
105
|
+
jmethodID method =
|
|
106
|
+
env->GetMethodID(moduleClass, "snapshotView", "(IDDDDIIJ)V");
|
|
107
|
+
env->DeleteLocalRef(moduleClass);
|
|
108
|
+
if (!method) {
|
|
109
|
+
env->ExceptionClear();
|
|
110
|
+
onError("drawElementImageToTexture: WebGPUModule.snapshotView not found");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Ownership passes to Java; Java_com_webgpu_WebGPUModule_onViewSnapshot
|
|
114
|
+
// (cpp-adapter.cpp) reclaims it.
|
|
115
|
+
auto *callbacks = new ViewSnapshotCallbacks{std::move(onSuccess),
|
|
116
|
+
std::move(onError)};
|
|
117
|
+
env->CallVoidMethod(_module, method, static_cast<jint>(request.viewTag),
|
|
118
|
+
static_cast<jdouble>(request.sourceX),
|
|
119
|
+
static_cast<jdouble>(request.sourceY),
|
|
120
|
+
static_cast<jdouble>(request.sourceWidth),
|
|
121
|
+
static_cast<jdouble>(request.sourceHeight),
|
|
122
|
+
static_cast<jint>(request.width),
|
|
123
|
+
static_cast<jint>(request.height),
|
|
124
|
+
reinterpret_cast<jlong>(callbacks));
|
|
125
|
+
if (env->ExceptionCheck()) {
|
|
126
|
+
env->ExceptionClear();
|
|
127
|
+
auto fail = std::move(callbacks->onError);
|
|
128
|
+
delete callbacks;
|
|
129
|
+
fail("drawElementImageToTexture: WebGPUModule.snapshotView threw");
|
|
130
|
+
}
|
|
74
131
|
}
|
|
75
132
|
|
|
76
133
|
wgpu::Surface makeSurface(wgpu::Instance instance, void *window, int width,
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
#include <cstring>
|
|
1
2
|
#include <memory>
|
|
3
|
+
#include <string>
|
|
2
4
|
#include <unordered_map>
|
|
5
|
+
#include <utility>
|
|
3
6
|
|
|
4
7
|
#include <fbjni/fbjni.h>
|
|
5
8
|
#include <jni.h>
|
|
6
9
|
#include <jsi/jsi.h>
|
|
7
10
|
|
|
8
11
|
#include <ReactCommon/CallInvokerHolder.h>
|
|
12
|
+
#include <android/bitmap.h>
|
|
9
13
|
#include <android/hardware_buffer_jni.h>
|
|
10
14
|
#include <android/native_window_jni.h>
|
|
11
15
|
#include <webgpu/webgpu_cpp.h>
|
|
@@ -19,21 +23,77 @@
|
|
|
19
23
|
std::shared_ptr<rnwgpu::RNWebGPUManager> manager;
|
|
20
24
|
|
|
21
25
|
extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUModule_initializeNative(
|
|
22
|
-
JNIEnv *env, jobject
|
|
23
|
-
jobject
|
|
26
|
+
JNIEnv *env, jobject thiz, jlong jsRuntime, jobject jsCallInvokerHolder,
|
|
27
|
+
jobject blobModule) {
|
|
24
28
|
auto runtime = reinterpret_cast<facebook::jsi::Runtime *>(jsRuntime);
|
|
25
29
|
jobject globalBlobModule = env->NewGlobalRef(blobModule);
|
|
30
|
+
jobject globalModule = env->NewGlobalRef(thiz);
|
|
26
31
|
auto jsCallInvoker{
|
|
27
32
|
facebook::jni::alias_ref<facebook::react::CallInvokerHolder::javaobject>{
|
|
28
33
|
reinterpret_cast<facebook::react::CallInvokerHolder::javaobject>(
|
|
29
|
-
jsCallInvokerHolder)} -> cthis()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
std::make_shared<rnwgpu::AndroidPlatformContext>(globalBlobModule);
|
|
34
|
+
jsCallInvokerHolder)} -> cthis()->getCallInvoker()};
|
|
35
|
+
auto platformContext = std::make_shared<rnwgpu::AndroidPlatformContext>(
|
|
36
|
+
globalBlobModule, globalModule);
|
|
33
37
|
manager = std::make_shared<rnwgpu::RNWebGPUManager>(runtime, jsCallInvoker,
|
|
34
38
|
platformContext);
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
// Completion of WebGPUModule.snapshotView (UI thread). Reclaims the callbacks
|
|
42
|
+
// handed to Java and delivers either the bitmap's pixels or the error.
|
|
43
|
+
extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUModule_onViewSnapshot(
|
|
44
|
+
JNIEnv *env, jobject /* this */, jlong callbackPointer, jobject bitmap,
|
|
45
|
+
jstring error) {
|
|
46
|
+
std::unique_ptr<rnwgpu::ViewSnapshotCallbacks> callbacks(
|
|
47
|
+
reinterpret_cast<rnwgpu::ViewSnapshotCallbacks *>(callbackPointer));
|
|
48
|
+
if (!callbacks) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (error != nullptr) {
|
|
52
|
+
const char *chars = env->GetStringUTFChars(error, nullptr);
|
|
53
|
+
std::string message(chars ? chars : "drawElementImageToTexture failed");
|
|
54
|
+
if (chars) {
|
|
55
|
+
env->ReleaseStringUTFChars(error, chars);
|
|
56
|
+
}
|
|
57
|
+
callbacks->onError(std::move(message));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (bitmap == nullptr) {
|
|
61
|
+
callbacks->onError("drawElementImageToTexture: no bitmap was produced");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
AndroidBitmapInfo info;
|
|
65
|
+
if (AndroidBitmap_getInfo(env, bitmap, &info) !=
|
|
66
|
+
ANDROID_BITMAP_RESULT_SUCCESS ||
|
|
67
|
+
info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
|
|
68
|
+
callbacks->onError(
|
|
69
|
+
"drawElementImageToTexture: unexpected snapshot bitmap format");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
void *pixels = nullptr;
|
|
73
|
+
if (AndroidBitmap_lockPixels(env, bitmap, &pixels) !=
|
|
74
|
+
ANDROID_BITMAP_RESULT_SUCCESS ||
|
|
75
|
+
pixels == nullptr) {
|
|
76
|
+
callbacks->onError(
|
|
77
|
+
"drawElementImageToTexture: couldn't lock the snapshot bitmap");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
rnwgpu::ImageData image;
|
|
81
|
+
image.width = info.width;
|
|
82
|
+
image.height = info.height;
|
|
83
|
+
image.format = wgpu::TextureFormat::RGBA8Unorm;
|
|
84
|
+
// Bitmap.Config.ARGB_8888 stores premultiplied RGBA bytes.
|
|
85
|
+
image.premultiplied = true;
|
|
86
|
+
const size_t rowBytes = static_cast<size_t>(info.width) * 4;
|
|
87
|
+
image.data.resize(rowBytes * info.height);
|
|
88
|
+
const auto *src = static_cast<const uint8_t *>(pixels);
|
|
89
|
+
for (uint32_t row = 0; row < info.height; ++row) {
|
|
90
|
+
std::memcpy(image.data.data() + row * rowBytes, src + row * info.stride,
|
|
91
|
+
rowBytes);
|
|
92
|
+
}
|
|
93
|
+
AndroidBitmap_unlockPixels(env, bitmap);
|
|
94
|
+
callbacks->onSuccess(std::move(image));
|
|
95
|
+
}
|
|
96
|
+
|
|
37
97
|
extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceChanged(
|
|
38
98
|
JNIEnv *env, jobject thiz, jobject surface, jint contextId, jfloat width,
|
|
39
99
|
jfloat height) {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
package com.webgpu;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap;
|
|
4
|
+
import android.graphics.Canvas;
|
|
5
|
+
import android.graphics.ColorSpace;
|
|
6
|
+
import android.graphics.HardwareRenderer;
|
|
7
|
+
import android.graphics.PixelFormat;
|
|
8
|
+
import android.graphics.RecordingCanvas;
|
|
9
|
+
import android.graphics.RenderNode;
|
|
10
|
+
import android.hardware.HardwareBuffer;
|
|
11
|
+
import android.media.Image;
|
|
12
|
+
import android.media.ImageReader;
|
|
13
|
+
import android.os.Build;
|
|
14
|
+
import android.util.Log;
|
|
15
|
+
import android.view.View;
|
|
16
|
+
|
|
17
|
+
import androidx.annotation.NonNull;
|
|
18
|
+
import androidx.annotation.RequiresApi;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Rasterizes a native view for GPUQueue.drawElementImageToTexture. Must be
|
|
22
|
+
* called on the UI thread.
|
|
23
|
+
*
|
|
24
|
+
* On API 29+ the view is rendered through {@link HardwareRenderer}: the
|
|
25
|
+
* recording reuses the children's existing display lists, so the result is
|
|
26
|
+
* exactly what the window's own renderer draws (shadows, hardware layers,
|
|
27
|
+
* TextureView content). Older devices, or a failed hardware render, fall back
|
|
28
|
+
* to drawing the view into a software canvas, which misses anything that is
|
|
29
|
+
* only drawn hardware-accelerated. SurfaceView content is composed outside
|
|
30
|
+
* the view hierarchy and is captured by neither path.
|
|
31
|
+
*/
|
|
32
|
+
final class ViewSnapshot {
|
|
33
|
+
private static final String TAG = "WebGPUViewSnapshot";
|
|
34
|
+
|
|
35
|
+
private ViewSnapshot() {}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param sourceX/Y/Width/Height the source rectangle in the view's own
|
|
39
|
+
* coordinate space, in pixels (already scaled by the display density)
|
|
40
|
+
* @param width/height the output size in pixels
|
|
41
|
+
* @return a software ARGB_8888 (premultiplied) bitmap of exactly
|
|
42
|
+
* width x height pixels
|
|
43
|
+
*/
|
|
44
|
+
@NonNull
|
|
45
|
+
static Bitmap snapshot(
|
|
46
|
+
@NonNull View view,
|
|
47
|
+
float sourceX,
|
|
48
|
+
float sourceY,
|
|
49
|
+
float sourceWidth,
|
|
50
|
+
float sourceHeight,
|
|
51
|
+
int width,
|
|
52
|
+
int height) {
|
|
53
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && view.isHardwareAccelerated()) {
|
|
54
|
+
try {
|
|
55
|
+
return snapshotHardware(view, sourceX, sourceY, sourceWidth, sourceHeight, width, height);
|
|
56
|
+
} catch (Throwable t) {
|
|
57
|
+
Log.w(TAG, "HardwareRenderer snapshot failed, falling back to software rendering", t);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return snapshotSoftware(view, sourceX, sourceY, sourceWidth, sourceHeight, width, height);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@NonNull
|
|
64
|
+
private static Bitmap snapshotSoftware(
|
|
65
|
+
@NonNull View view,
|
|
66
|
+
float sourceX,
|
|
67
|
+
float sourceY,
|
|
68
|
+
float sourceWidth,
|
|
69
|
+
float sourceHeight,
|
|
70
|
+
int width,
|
|
71
|
+
int height) {
|
|
72
|
+
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
|
73
|
+
Canvas canvas = new Canvas(bitmap);
|
|
74
|
+
canvas.scale(width / sourceWidth, height / sourceHeight);
|
|
75
|
+
canvas.translate(-sourceX, -sourceY);
|
|
76
|
+
view.draw(canvas);
|
|
77
|
+
return bitmap;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@RequiresApi(Build.VERSION_CODES.Q)
|
|
81
|
+
@NonNull
|
|
82
|
+
private static Bitmap snapshotHardware(
|
|
83
|
+
@NonNull View view,
|
|
84
|
+
float sourceX,
|
|
85
|
+
float sourceY,
|
|
86
|
+
float sourceWidth,
|
|
87
|
+
float sourceHeight,
|
|
88
|
+
int width,
|
|
89
|
+
int height) {
|
|
90
|
+
// The renderer draws into an ImageReader whose buffers are GPU-sampleable
|
|
91
|
+
// (a later zero-copy path can import them directly); for now the frame is
|
|
92
|
+
// read back into a software bitmap.
|
|
93
|
+
ImageReader reader =
|
|
94
|
+
ImageReader.newInstance(
|
|
95
|
+
width,
|
|
96
|
+
height,
|
|
97
|
+
PixelFormat.RGBA_8888,
|
|
98
|
+
1,
|
|
99
|
+
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE | HardwareBuffer.USAGE_GPU_COLOR_OUTPUT);
|
|
100
|
+
HardwareRenderer renderer = new HardwareRenderer();
|
|
101
|
+
Image image = null;
|
|
102
|
+
try {
|
|
103
|
+
renderer.setSurface(reader.getSurface());
|
|
104
|
+
// Match the framework's default light so elevation shadows come out
|
|
105
|
+
// like on screen (ThreadedRenderer reads these from the theme; the
|
|
106
|
+
// Material defaults are used here).
|
|
107
|
+
float density = view.getResources().getDisplayMetrics().density;
|
|
108
|
+
renderer.setLightSourceGeometry(
|
|
109
|
+
width / 2f, -400f * density, 500f * density, 800f * density);
|
|
110
|
+
renderer.setLightSourceAlpha(0.039f, 0.19f);
|
|
111
|
+
|
|
112
|
+
RenderNode root = new RenderNode("rnwgpu-view-snapshot");
|
|
113
|
+
root.setPosition(0, 0, width, height);
|
|
114
|
+
RecordingCanvas canvas = root.beginRecording(width, height);
|
|
115
|
+
try {
|
|
116
|
+
canvas.scale(width / sourceWidth, height / sourceHeight);
|
|
117
|
+
canvas.translate(-sourceX, -sourceY);
|
|
118
|
+
view.draw(canvas);
|
|
119
|
+
} finally {
|
|
120
|
+
root.endRecording();
|
|
121
|
+
}
|
|
122
|
+
renderer.setContentRoot(root);
|
|
123
|
+
|
|
124
|
+
HardwareRenderer.FrameRenderRequest request = renderer.createRenderRequest();
|
|
125
|
+
request.setWaitForPresent(true);
|
|
126
|
+
int status = request.syncAndDraw();
|
|
127
|
+
int failed =
|
|
128
|
+
HardwareRenderer.SYNC_LOST_SURFACE_REWARD_IF_FOUND
|
|
129
|
+
| HardwareRenderer.SYNC_CONTEXT_IS_STOPPED
|
|
130
|
+
| HardwareRenderer.SYNC_FRAME_DROPPED;
|
|
131
|
+
if ((status & failed) != 0) {
|
|
132
|
+
throw new IllegalStateException("HardwareRenderer.syncAndDraw failed with status " + status);
|
|
133
|
+
}
|
|
134
|
+
image = reader.acquireNextImage();
|
|
135
|
+
if (image == null) {
|
|
136
|
+
throw new IllegalStateException("HardwareRenderer produced no image");
|
|
137
|
+
}
|
|
138
|
+
HardwareBuffer buffer = image.getHardwareBuffer();
|
|
139
|
+
if (buffer == null) {
|
|
140
|
+
throw new IllegalStateException("HardwareRenderer image has no HardwareBuffer");
|
|
141
|
+
}
|
|
142
|
+
Bitmap hardware =
|
|
143
|
+
Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB));
|
|
144
|
+
buffer.close();
|
|
145
|
+
if (hardware == null) {
|
|
146
|
+
throw new IllegalStateException("Bitmap.wrapHardwareBuffer failed");
|
|
147
|
+
}
|
|
148
|
+
Bitmap software = hardware.copy(Bitmap.Config.ARGB_8888, false);
|
|
149
|
+
hardware.recycle();
|
|
150
|
+
if (software == null) {
|
|
151
|
+
throw new IllegalStateException("could not read back the hardware bitmap");
|
|
152
|
+
}
|
|
153
|
+
return software;
|
|
154
|
+
} finally {
|
|
155
|
+
if (image != null) {
|
|
156
|
+
image.close();
|
|
157
|
+
}
|
|
158
|
+
renderer.destroy();
|
|
159
|
+
reader.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
package com.webgpu;
|
|
2
2
|
|
|
3
|
+
import android.graphics.Bitmap;
|
|
3
4
|
import android.util.Log;
|
|
5
|
+
import android.view.View;
|
|
4
6
|
|
|
5
7
|
import androidx.annotation.OptIn;
|
|
6
8
|
|
|
@@ -11,7 +13,10 @@ import com.facebook.proguard.annotations.DoNotStrip;
|
|
|
11
13
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
12
14
|
import com.facebook.react.bridge.JavaScriptContextHolder;
|
|
13
15
|
import com.facebook.react.bridge.ReactMethod;
|
|
16
|
+
import com.facebook.react.bridge.UIManager;
|
|
17
|
+
import com.facebook.react.bridge.UiThreadUtil;
|
|
14
18
|
import com.facebook.react.common.annotations.FrameworkAPI;
|
|
19
|
+
import com.facebook.react.uimanager.UIManagerHelper;
|
|
15
20
|
import com.facebook.react.module.annotations.ReactModule;
|
|
16
21
|
import com.facebook.react.modules.blob.BlobModule;
|
|
17
22
|
import com.facebook.react.modules.blob.BlobProvider;
|
|
@@ -47,4 +52,67 @@ public class WebGPUModule extends NativeWebGPUModuleSpec {
|
|
|
47
52
|
@OptIn(markerClass = FrameworkAPI.class)
|
|
48
53
|
@DoNotStrip
|
|
49
54
|
private native void initializeNative(long jsRuntime, CallInvokerHolderImpl jsInvoker, BlobModule blobModule);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Called from C++ (AndroidPlatformContext::snapshotView) on an arbitrary
|
|
58
|
+
* thread for GPUQueue.drawElementImageToTexture. Hops to the UI thread,
|
|
59
|
+
* resolves the view for {@code tag}, rasterizes the requested rectangle and
|
|
60
|
+
* always reports back through {@link #onViewSnapshot}, with either a bitmap
|
|
61
|
+
* or an error message. {@code callback} is an opaque native pointer that
|
|
62
|
+
* must be handed back exactly once.
|
|
63
|
+
*
|
|
64
|
+
* The source rectangle is in density-independent points (React Native
|
|
65
|
+
* layout units) and is converted to pixels here; a non-positive
|
|
66
|
+
* sourceWidth/sourceHeight means the whole view. A zero width/height means
|
|
67
|
+
* the natural pixel size of the source rectangle.
|
|
68
|
+
*/
|
|
69
|
+
@DoNotStrip
|
|
70
|
+
void snapshotView(
|
|
71
|
+
final int tag,
|
|
72
|
+
final double sourceX,
|
|
73
|
+
final double sourceY,
|
|
74
|
+
final double sourceWidth,
|
|
75
|
+
final double sourceHeight,
|
|
76
|
+
final int width,
|
|
77
|
+
final int height,
|
|
78
|
+
final long callback) {
|
|
79
|
+
UiThreadUtil.runOnUiThread(
|
|
80
|
+
() -> {
|
|
81
|
+
Bitmap bitmap = null;
|
|
82
|
+
String error = null;
|
|
83
|
+
try {
|
|
84
|
+
ReactApplicationContext context = getReactApplicationContext();
|
|
85
|
+
UIManager uiManager = UIManagerHelper.getUIManagerForReactTag(context, tag);
|
|
86
|
+
View view = uiManager == null ? null : uiManager.resolveView(tag);
|
|
87
|
+
if (view == null) {
|
|
88
|
+
throw new IllegalArgumentException(
|
|
89
|
+
"drawElementImageToTexture: no native view found for tag "
|
|
90
|
+
+ tag
|
|
91
|
+
+ " (is the view mounted, and rendered with collapsable={false}?)");
|
|
92
|
+
}
|
|
93
|
+
if (view.getWidth() <= 0 || view.getHeight() <= 0) {
|
|
94
|
+
throw new IllegalStateException(
|
|
95
|
+
"drawElementImageToTexture: the view has no size yet");
|
|
96
|
+
}
|
|
97
|
+
float density = view.getResources().getDisplayMetrics().density;
|
|
98
|
+
float sx = (float) (sourceX * density);
|
|
99
|
+
float sy = (float) (sourceY * density);
|
|
100
|
+
float sw = sourceWidth > 0 ? (float) (sourceWidth * density) : view.getWidth();
|
|
101
|
+
float sh = sourceHeight > 0 ? (float) (sourceHeight * density) : view.getHeight();
|
|
102
|
+
int w = width > 0 ? width : Math.round(sw);
|
|
103
|
+
int h = height > 0 ? height : Math.round(sh);
|
|
104
|
+
if (w <= 0 || h <= 0) {
|
|
105
|
+
throw new IllegalArgumentException(
|
|
106
|
+
"drawElementImageToTexture: the source rectangle is empty");
|
|
107
|
+
}
|
|
108
|
+
bitmap = ViewSnapshot.snapshot(view, sx, sy, sw, sh, w, h);
|
|
109
|
+
} catch (Throwable t) {
|
|
110
|
+
error = t.getMessage() != null ? t.getMessage() : t.toString();
|
|
111
|
+
}
|
|
112
|
+
onViewSnapshot(callback, bitmap, error);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@DoNotStrip
|
|
117
|
+
private native void onViewSnapshot(long callback, Bitmap bitmap, String error);
|
|
50
118
|
}
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
3
|
#include "PlatformContext.h"
|
|
4
|
+
#include <functional>
|
|
4
5
|
#include <string>
|
|
5
6
|
|
|
6
7
|
namespace rnwgpu {
|
|
7
8
|
|
|
8
9
|
class ApplePlatformContext : public PlatformContext {
|
|
9
10
|
public:
|
|
10
|
-
|
|
11
|
+
// Resolves a React tag to its native view (an unretained
|
|
12
|
+
// UIView* / NSView* bridged to void*, or nullptr). Called on the main thread
|
|
13
|
+
// only. Provided by WebGPUModule from React Native's view registry.
|
|
14
|
+
using ViewLookup = std::function<void *(int)>;
|
|
15
|
+
|
|
16
|
+
explicit ApplePlatformContext(ViewLookup viewLookup = nullptr);
|
|
11
17
|
~ApplePlatformContext() = default;
|
|
12
18
|
|
|
19
|
+
void snapshotView(const ViewSnapshotRequest &request,
|
|
20
|
+
std::function<void(ImageData)> onSuccess,
|
|
21
|
+
std::function<void(std::string)> onError) override;
|
|
22
|
+
|
|
13
23
|
wgpu::Surface makeSurface(wgpu::Instance instance, void *surface, int width,
|
|
14
24
|
int height) override;
|
|
15
25
|
|
|
@@ -39,6 +49,9 @@ public:
|
|
|
39
49
|
std::string writeTestVideoFile() override;
|
|
40
50
|
|
|
41
51
|
VideoFrameHandle wrapNativeBuffer(void *pointer) override;
|
|
52
|
+
|
|
53
|
+
private:
|
|
54
|
+
ViewLookup _viewLookup;
|
|
42
55
|
};
|
|
43
56
|
|
|
44
57
|
} // namespace rnwgpu
|
|
@@ -10,10 +10,15 @@
|
|
|
10
10
|
#import <ReactCommon/RCTTurboModule.h>
|
|
11
11
|
|
|
12
12
|
#include "AppleVideoPlayer.h"
|
|
13
|
+
#import "RNWGUIKit.h"
|
|
13
14
|
|
|
14
15
|
#include "RNWebGPUManager.h"
|
|
15
16
|
#include "WebGPUModule.h"
|
|
16
17
|
|
|
18
|
+
#include <cmath>
|
|
19
|
+
#include <string>
|
|
20
|
+
#include <utility>
|
|
21
|
+
|
|
17
22
|
namespace rnwgpu {
|
|
18
23
|
|
|
19
24
|
void checkIfUsingSimulatorWithAPIValidation() {
|
|
@@ -30,10 +35,180 @@ void checkIfUsingSimulatorWithAPIValidation() {
|
|
|
30
35
|
#endif
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
ApplePlatformContext::ApplePlatformContext()
|
|
38
|
+
ApplePlatformContext::ApplePlatformContext(ViewLookup viewLookup)
|
|
39
|
+
: _viewLookup(std::move(viewLookup)) {
|
|
34
40
|
checkIfUsingSimulatorWithAPIValidation();
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
namespace {
|
|
44
|
+
|
|
45
|
+
// Rasterize `request` on the main thread into tightly packed, premultiplied
|
|
46
|
+
// BGRA8 pixels. Throws std::runtime_error with a user-facing message.
|
|
47
|
+
ImageData snapshotViewOnMainThread(const ApplePlatformContext::ViewLookup &lookup,
|
|
48
|
+
const ViewSnapshotRequest &request) {
|
|
49
|
+
if (!lookup) {
|
|
50
|
+
throw std::runtime_error(
|
|
51
|
+
"drawElementImageToTexture: no view registry is available");
|
|
52
|
+
}
|
|
53
|
+
RNWGPlatformView *view = (__bridge RNWGPlatformView *)lookup(request.viewTag);
|
|
54
|
+
if (view == nil) {
|
|
55
|
+
throw std::runtime_error(
|
|
56
|
+
"drawElementImageToTexture: no native view found for tag " +
|
|
57
|
+
std::to_string(request.viewTag) +
|
|
58
|
+
" (is the view mounted, and rendered with collapsable={false}?)");
|
|
59
|
+
}
|
|
60
|
+
const CGRect bounds = view.bounds;
|
|
61
|
+
if (bounds.size.width <= 0 || bounds.size.height <= 0) {
|
|
62
|
+
throw std::runtime_error(
|
|
63
|
+
"drawElementImageToTexture: the view has no size yet");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Source rectangle in points (view space); the whole view by default.
|
|
67
|
+
const double sourceX = request.sourceX;
|
|
68
|
+
const double sourceY = request.sourceY;
|
|
69
|
+
const double sourceWidth =
|
|
70
|
+
request.sourceWidth > 0 ? request.sourceWidth : bounds.size.width;
|
|
71
|
+
const double sourceHeight =
|
|
72
|
+
request.sourceHeight > 0 ? request.sourceHeight : bounds.size.height;
|
|
73
|
+
|
|
74
|
+
// Natural pixel size: points times the screen scale the view is shown at.
|
|
75
|
+
#if !TARGET_OS_OSX
|
|
76
|
+
CGFloat scale = view.window.screen.scale;
|
|
77
|
+
if (scale <= 0) {
|
|
78
|
+
scale = UIScreen.mainScreen.scale;
|
|
79
|
+
}
|
|
80
|
+
#else
|
|
81
|
+
CGFloat scale = view.window.backingScaleFactor;
|
|
82
|
+
if (scale <= 0) {
|
|
83
|
+
scale = NSScreen.mainScreen.backingScaleFactor;
|
|
84
|
+
}
|
|
85
|
+
#endif
|
|
86
|
+
const uint32_t width =
|
|
87
|
+
request.width > 0
|
|
88
|
+
? request.width
|
|
89
|
+
: static_cast<uint32_t>(std::llround(sourceWidth * scale));
|
|
90
|
+
const uint32_t height =
|
|
91
|
+
request.height > 0
|
|
92
|
+
? request.height
|
|
93
|
+
: static_cast<uint32_t>(std::llround(sourceHeight * scale));
|
|
94
|
+
if (width == 0 || height == 0) {
|
|
95
|
+
throw std::runtime_error(
|
|
96
|
+
"drawElementImageToTexture: the source rectangle is empty");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The view's bounds are drawn scaled so that the source rectangle covers
|
|
100
|
+
// the whole output.
|
|
101
|
+
const CGFloat scaleX = static_cast<CGFloat>(width) / sourceWidth;
|
|
102
|
+
const CGFloat scaleY = static_cast<CGFloat>(height) / sourceHeight;
|
|
103
|
+
const CGRect drawRect =
|
|
104
|
+
CGRectMake(-sourceX * scaleX, -sourceY * scaleY,
|
|
105
|
+
bounds.size.width * scaleX, bounds.size.height * scaleY);
|
|
106
|
+
|
|
107
|
+
CGImageRef cgImage = NULL;
|
|
108
|
+
#if !TARGET_OS_OSX
|
|
109
|
+
// drawViewHierarchyInRect renders what is on screen (including Metal /
|
|
110
|
+
// CAMetalLayer content), which is what "the element as displayed" means.
|
|
111
|
+
// afterScreenUpdates:YES flushes pending layout and layer changes first, so
|
|
112
|
+
// a snapshot taken right after a state change reflects that change.
|
|
113
|
+
UIGraphicsImageRendererFormat *format =
|
|
114
|
+
[UIGraphicsImageRendererFormat defaultFormat];
|
|
115
|
+
format.scale = 1.0;
|
|
116
|
+
format.opaque = NO;
|
|
117
|
+
format.preferredRange = UIGraphicsImageRendererFormatRangeStandard;
|
|
118
|
+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc]
|
|
119
|
+
initWithSize:CGSizeMake(width, height)
|
|
120
|
+
format:format];
|
|
121
|
+
// drawViewHierarchyInRect returns NO and draws nothing when the view is not
|
|
122
|
+
// in a window (e.g. a screen pushed under another one). The image would
|
|
123
|
+
// then be fully transparent, so treat it as a failure instead.
|
|
124
|
+
__block BOOL drawn = NO;
|
|
125
|
+
UIImage *image = [renderer
|
|
126
|
+
imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull context) {
|
|
127
|
+
drawn = [view drawViewHierarchyInRect:drawRect afterScreenUpdates:YES];
|
|
128
|
+
}];
|
|
129
|
+
if (!drawn) {
|
|
130
|
+
throw std::runtime_error(
|
|
131
|
+
"drawElementImageToTexture: the view could not be rendered (is it "
|
|
132
|
+
"attached to a window?)");
|
|
133
|
+
}
|
|
134
|
+
cgImage = image.CGImage;
|
|
135
|
+
if (cgImage != NULL) {
|
|
136
|
+
CGImageRetain(cgImage);
|
|
137
|
+
}
|
|
138
|
+
#else
|
|
139
|
+
NSBitmapImageRep *rep = [view bitmapImageRepForCachingDisplayInRect:bounds];
|
|
140
|
+
if (rep != nil) {
|
|
141
|
+
[view cacheDisplayInRect:bounds toBitmapImageRep:rep];
|
|
142
|
+
cgImage = rep.CGImage;
|
|
143
|
+
if (cgImage != NULL) {
|
|
144
|
+
CGImageRetain(cgImage);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
#endif
|
|
148
|
+
if (cgImage == NULL) {
|
|
149
|
+
throw std::runtime_error(
|
|
150
|
+
"drawElementImageToTexture: the view could not be rendered");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Normalize into premultiplied BGRA8 with no row padding, whatever the
|
|
154
|
+
// renderer produced.
|
|
155
|
+
ImageData result;
|
|
156
|
+
result.width = width;
|
|
157
|
+
result.height = height;
|
|
158
|
+
result.format = wgpu::TextureFormat::BGRA8Unorm;
|
|
159
|
+
result.premultiplied = true;
|
|
160
|
+
result.data.assign(static_cast<size_t>(width) * height * 4, 0);
|
|
161
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
162
|
+
CGContextRef context = CGBitmapContextCreate(
|
|
163
|
+
result.data.data(), width, height, 8, static_cast<size_t>(width) * 4,
|
|
164
|
+
colorSpace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
|
|
165
|
+
CGColorSpaceRelease(colorSpace);
|
|
166
|
+
if (context == NULL) {
|
|
167
|
+
CGImageRelease(cgImage);
|
|
168
|
+
throw std::runtime_error(
|
|
169
|
+
"drawElementImageToTexture: could not allocate the pixel buffer");
|
|
170
|
+
}
|
|
171
|
+
#if !TARGET_OS_OSX
|
|
172
|
+
// The renderer already produced exactly width x height pixels.
|
|
173
|
+
const CGRect imageRect = CGRectMake(0, 0, width, height);
|
|
174
|
+
#else
|
|
175
|
+
// The cached display covers the whole view at the backing scale; draw it so
|
|
176
|
+
// that the source rectangle fills the output. CoreGraphics' origin is the
|
|
177
|
+
// bottom-left corner, hence the flipped vertical offset.
|
|
178
|
+
const CGRect imageRect = CGRectMake(
|
|
179
|
+
-sourceX * scaleX,
|
|
180
|
+
-(bounds.size.height - sourceY - sourceHeight) * scaleY,
|
|
181
|
+
bounds.size.width * scaleX, bounds.size.height * scaleY);
|
|
182
|
+
#endif
|
|
183
|
+
CGContextSetBlendMode(context, kCGBlendModeCopy);
|
|
184
|
+
CGContextDrawImage(context, imageRect, cgImage);
|
|
185
|
+
CGContextRelease(context);
|
|
186
|
+
CGImageRelease(cgImage);
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
} // namespace
|
|
191
|
+
|
|
192
|
+
void ApplePlatformContext::snapshotView(
|
|
193
|
+
const ViewSnapshotRequest &request,
|
|
194
|
+
std::function<void(ImageData)> onSuccess,
|
|
195
|
+
std::function<void(std::string)> onError) {
|
|
196
|
+
// Copies: the block outlives this call.
|
|
197
|
+
ViewLookup lookup = _viewLookup;
|
|
198
|
+
ViewSnapshotRequest snapshotRequest = request;
|
|
199
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
200
|
+
@autoreleasepool {
|
|
201
|
+
try {
|
|
202
|
+
onSuccess(snapshotViewOnMainThread(lookup, snapshotRequest));
|
|
203
|
+
} catch (const std::exception &error) {
|
|
204
|
+
onError(error.what());
|
|
205
|
+
} catch (...) {
|
|
206
|
+
onError("drawElementImageToTexture: unknown native error");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
37
212
|
wgpu::Surface ApplePlatformContext::makeSurface(wgpu::Instance instance,
|
|
38
213
|
void *surface, int width,
|
|
39
214
|
int height) {
|