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.
@@ -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
- explicit AndroidPlatformContext(jobject blobModule)
67
- : _blobModule(blobModule) {}
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 /* this */, jlong jsRuntime,
23
- jobject jsCallInvokerHolder, jobject blobModule) {
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
- ->getCallInvoker()};
31
- auto platformContext =
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
- ApplePlatformContext();
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) {