Skip to content

SkiaApi.Context() fails if not ran on the main thread #3137

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Cyberax opened this issue May 10, 2025 · 4 comments
Open

SkiaApi.Context() fails if not ran on the main thread #3137

Cyberax opened this issue May 10, 2025 · 4 comments

Comments

@Cyberax
Copy link

Cyberax commented May 10, 2025

I'm using SkiaApi.Context() to render onto a surface obtained from WebGPU:

    const nativeSurface = this.canvasRef.getNativeSurface();

    const context = SkiaApi.Context(nativeSurface.surface, width, height);
    const surface = context.getSurface();
    runRendering(surface);
    surface.flush();
    context.present();
    surface.dispose();

This works, but produces a stack trace on the debug console with a message "Modifying properties of a view's layer off the main thread is not allowed". This not only pollutes the logs, but it also is slow (~300ms).

Here is the diff that solved my problem:

diff --git a/node_modules/@shopify/react-native-skia/apple/MetalWindowContext.mm b/node_modules/@shopify/react-native-skia/apple/MetalWindowContext.mm
index 7230307..eb8725f 100644
--- a/node_modules/@shopify/react-native-skia/apple/MetalWindowContext.mm
+++ b/node_modules/@shopify/react-native-skia/apple/MetalWindowContext.mm
@@ -3,6 +3,36 @@
 #include "MetalContext.h"
 #include "RNSkLog.h"
 
+bool layerNeedsUpdate(CAMetalLayer *layer, id<MTLDevice> device, int width, int height) {
+  bool res = false;
+  res = res || layer.framebufferOnly != NO;
+  res = res || layer.device != device;
+  res = res || layer.opaque != false;
+#if !TARGET_OS_OSX
+  res = res || layer.contentsScale != [UIScreen mainScreen].scale;
+#else
+  res = res || layer.contentsScale != [NSScreen mainScreen].backingScaleFactor;
+#endif // !TARGET_OS_OSX
+  res = res || layer.pixelFormat != MTLPixelFormatBGRA8Unorm;
+  res = res || layer.contentsGravity != kCAGravityBottomLeft;
+  res = res || layer.drawableSize.width != width || layer.drawableSize.height != height;
+  return res;
+}
+
+void doUpdateLayer(CAMetalLayer *layer, id<MTLDevice> device, int width, int height) {
+  layer.framebufferOnly = NO;
+  layer.device = device;
+  layer.opaque = false;
+#if !TARGET_OS_OSX
+  layer.contentsScale = [UIScreen mainScreen].scale;
+#else
+  layer.contentsScale = [NSScreen mainScreen].backingScaleFactor;
+#endif // !TARGET_OS_OSX
+  layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
+  layer.contentsGravity = kCAGravityBottomLeft;
+  layer.drawableSize = CGSizeMake(width, height);
+}
+
 MetalWindowContext::MetalWindowContext(GrDirectContext *directContext,
                                        id<MTLDevice> device,
                                        id<MTLCommandQueue> commandQueue,
@@ -12,17 +42,21 @@ MetalWindowContext::MetalWindowContext(GrDirectContext *directContext,
 #pragma clang diagnostic ignored "-Wunguarded-availability-new"
   _layer = (CAMetalLayer *)layer;
 #pragma clang diagnostic pop
-  _layer.framebufferOnly = NO;
-  _layer.device = device;
-  _layer.opaque = false;
-#if !TARGET_OS_OSX
-  _layer.contentsScale = [UIScreen mainScreen].scale;
-#else
-  _layer.contentsScale = [NSScreen mainScreen].backingScaleFactor;
-#endif // !TARGET_OS_OSX
-  _layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
-  _layer.contentsGravity = kCAGravityBottomLeft;
-  _layer.drawableSize = CGSizeMake(width, height);
+  // We can't just update the layer here, as this method will likely be called
+  // from a background thread. Updating the layer properties will then result in
+  // "Modifying properties of a view's layer off the main thread is not allowed"
+  // followed by a stack trace on the debug console.
+  if (layerNeedsUpdate(_layer, device, width, height)) {
+    // We need to check if we _are_ called on the main thread, as in this case
+    // the dispatch_sync will deadlock.
+    if ([[NSThread currentThread] isMainThread]) {
+      doUpdateLayer(_layer, device, width, height);
+    } else {
+      dispatch_sync(dispatch_get_main_queue(), ^{
+        doUpdateLayer(_layer, device, width, height);
+      });
+    }
+  }
 }
 
 sk_sp<SkSurface> MetalWindowContext::getSurface() {

This issue body was partially generated by patch-package.

@wcandillon
Copy link
Contributor

Please be aware that Skia Ganesh is very much not multi threaded. Can you provide more information on what you are doing/trying to do? This sounds interesting.

@Cyberax
Copy link
Author

Cyberax commented May 12, 2025

We're fine that it's not multi-threaded. We're using it to render a Skia overlay directly onto a hardware surface that we get from react-native-wgpu. I was honestly amazed that it just worked when we tried it!

The rendering is single-threaded, and is handled synchronously on the JS thread. That's why it's triggering the UI diagnostics, that expect the CALayer to be mutated only on the UI thread.

@wcandillon
Copy link
Contributor

That sounds really interesting.
is the idea that you draw something via react-native-wgpu and you want to use it in Skia? For Android, how would you do it? Via HardwareBuffer? (write on webgpu shared texture and hardware buffer to SkImage).

@Cyberax
Copy link
Author

Cyberax commented May 13, 2025

We're using Skia to render a 2D overlay with controls and high-quality text on top of a 3D canvas.

For now, we actually create a second WGPU canvas that overlays the 3D one, and only use the getNativeSurface().surface from it. We don't even do configure for that WGPU context.

I have plans to do rendering into a texture buffer, and it seems doable. On Android via ASurfaceTexture_acquireANativeWindow, for a surface that can then be attached to a GL context. I'm not sure about iOS yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants