Shopify / Shopify/react-native-skia

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

Open
#3,137 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
8.6k
Forks
647
Avg merge
1d 17h
Merged PRs (30d)
35

Description

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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in apple/MetalWindowContext.mm, especially MetalWindowContext::MetalWindowContext, and compare the reported patch with the current behavior. Reproduce SkiaApi.Context() rendering from a non-main thread, then verify that the debug warning and reported delay are gone without breaking calls made on the main thread.

Written by the indexing model from the issue text.

Assessment

Tech stack
objective-c, react-native
Domain
computer-graphics, mobile-dev
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.