diff --git a/docs/static/img/offscreen/multiple_circles.png b/docs/static/img/offscreen/multiple_circles.png
new file mode 100644
index 0000000000..7b6175be35
Binary files /dev/null and b/docs/static/img/offscreen/multiple_circles.png differ
diff --git a/example/src/Examples/API/Icons/index.tsx b/example/src/Examples/API/Icons/index.tsx
index 20ec9718ff..af042543a4 100644
--- a/example/src/Examples/API/Icons/index.tsx
+++ b/example/src/Examples/API/Icons/index.tsx
@@ -77,7 +77,8 @@ const Icon = ({ icon }: IconProps) => {
return ;
};
-const Screen = () => {
+type Props = { color: string };
+const Screen: React.FC = ({ color }) => {
const { github, octocat, stackExchange, overflow } = useSVGs();
return (
{
React Native Skia Canvas
- React Native View
-
React Native SVG
@@ -114,9 +113,9 @@ const Screen = () => {
);
};
-const HomeScreen = () => ;
+const HomeScreen = () => ;
-const SettingsScreen = () => ;
+const SettingsScreen = () => ;
const Tab = createBottomTabNavigator();
diff --git a/package/android/CMakeLists.txt b/package/android/CMakeLists.txt
index 97c8f0e3ce..1796679ad6 100644
--- a/package/android/CMakeLists.txt
+++ b/package/android/CMakeLists.txt
@@ -45,7 +45,7 @@ add_library(
"${PROJECT_SOURCE_DIR}/cpp/jni/JniPlatformContext.cpp"
"${PROJECT_SOURCE_DIR}/cpp/rnskia-android/RNSkOpenGLCanvasProvider.cpp"
- "${PROJECT_SOURCE_DIR}/cpp/rnskia-android/SkiaOpenGLRenderer.cpp"
+ "${PROJECT_SOURCE_DIR}/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.cpp"
"${PROJECT_SOURCE_DIR}/../cpp/jsi/JsiHostObject.cpp"
"${PROJECT_SOURCE_DIR}/../cpp/jsi/JsiValue.cpp"
diff --git a/package/android/cpp/jni/include/JniSkiaBaseView.h b/package/android/cpp/jni/include/JniSkiaBaseView.h
index cc2ae3176f..b42d12db3e 100644
--- a/package/android/cpp/jni/include/JniSkiaBaseView.h
+++ b/package/android/cpp/jni/include/JniSkiaBaseView.h
@@ -10,6 +10,8 @@
#include
#include
+#include
+
namespace RNSkia {
namespace jsi = facebook::jsi;
@@ -18,7 +20,7 @@ class JniSkiaBaseView {
public:
JniSkiaBaseView(jni::alias_ref skiaManager,
std::shared_ptr skiaView)
- : _manager(skiaManager->cthis()), _skiaView(skiaView) {}
+ : _manager(skiaManager->cthis()), _skiaAndroidView(skiaView) {}
~JniSkiaBaseView() {}
@@ -28,38 +30,97 @@ class JniSkiaBaseView {
protected:
virtual void updateTouchPoints(jni::JArrayDouble touches) {
- _skiaView->updateTouchPoints(touches);
+ _skiaAndroidView->updateTouchPoints(touches);
}
virtual void surfaceAvailable(jobject surface, int width, int height) {
- _skiaView->surfaceAvailable(surface, width, height);
+ _skiaAndroidView->surfaceAvailable(surface, width, height);
}
virtual void surfaceSizeChanged(int width, int height) {
- _skiaView->surfaceSizeChanged(width, height);
+ _skiaAndroidView->surfaceSizeChanged(width, height);
}
- virtual void surfaceDestroyed() { _skiaView->surfaceDestroyed(); }
+ virtual void surfaceDestroyed() { _skiaAndroidView->surfaceDestroyed(); }
- virtual void setMode(std::string mode) { _skiaView->setMode(mode); }
+ virtual void setMode(std::string mode) { _skiaAndroidView->setMode(mode); }
- virtual void setDebugMode(bool show) { _skiaView->setShowDebugInfo(show); }
+ virtual void setDebugMode(bool show) {
+ _skiaAndroidView->setShowDebugInfo(show);
+ }
virtual void registerView(int nativeId) {
- getSkiaManager()->registerSkiaView(nativeId, _skiaView->getSkiaView());
+ getSkiaManager()->registerSkiaView(nativeId,
+ _skiaAndroidView->getSkiaView());
}
virtual void unregisterView() {
- getSkiaManager()->setSkiaView(_skiaView->getSkiaView()->getNativeId(),
- nullptr);
+ getSkiaManager()->setSkiaView(
+ _skiaAndroidView->getSkiaView()->getNativeId(), nullptr);
getSkiaManager()->unregisterSkiaView(
- _skiaView->getSkiaView()->getNativeId());
- _skiaView->viewDidUnmount();
+ _skiaAndroidView->getSkiaView()->getNativeId());
+ _skiaAndroidView->viewDidUnmount();
}
+ /**
+ * Android specific method for rendering an offscreen GPU buffer to an Android
+ * bitmap. The result can be used to render the first frame of the Skia render
+ * to avoid flickering on android.
+ */
+ /*
+ // TODO: Remove if we find another solution for first frame rendering
+ // protected native Object renderToBitmap(Object bitmap, int width, int
+ height); virtual jobject renderToBitmap(jobject bitmapIn, int width, int
+ height) { auto platformContext = getSkiaManager()->getPlatformContext(); auto
+ provider = std::make_shared( platformContext,
+ []() {}, width, height);
+
+ // Render into a gpu backed buffer
+ _skiaAndroidView->getSkiaView()->getRenderer()->renderImmediate(provider);
+ auto rect = SkRect::MakeXYWH(0, 0, width, height);
+ auto image = provider->makeSnapshot(&rect);
+
+ AndroidBitmapInfo infoIn;
+ auto env = facebook::jni::Environment::current();
+ void *pixels;
+
+ // Get image info
+ if (AndroidBitmap_getInfo(env, bitmapIn, &infoIn) !=
+ ANDROID_BITMAP_RESULT_SUCCESS) {
+ return env->NewStringUTF("failed");
+ }
+
+ // Check image
+ if (infoIn.format != ANDROID_BITMAP_FORMAT_RGBA_8888 &&
+ infoIn.format != ANDROID_BITMAP_FORMAT_RGB_565) {
+ return env->NewStringUTF("Only support ANDROID_BITMAP_FORMAT_RGBA_8888 "
+ "and ANDROID_BITMAP_FORMAT_RGB_565");
+ }
+
+ auto imageInfo = SkImageInfo::Make(image->width(), image->height(),
+ image->colorType(), image->alphaType());
+
+ // Lock all images
+ if (AndroidBitmap_lockPixels(env, bitmapIn, &pixels) !=
+ ANDROID_BITMAP_RESULT_SUCCESS) {
+ return env->NewStringUTF("AndroidBitmap_lockPixels failed!");
+ }
+
+ // Set pixels from SkImage
+ image->readPixels(imageInfo, pixels, imageInfo.minRowBytes(), 0, 0);
+
+ // Unlocks everything
+ AndroidBitmap_unlockPixels(env, bitmapIn);
+
+ image = nullptr;
+ provider = nullptr;
+
+ return bitmapIn;
+ }*/
+
private:
JniSkiaManager *_manager;
- std::shared_ptr _skiaView;
+ std::shared_ptr _skiaAndroidView;
};
} // namespace RNSkia
diff --git a/package/android/cpp/jni/include/JniSkiaDomView.h b/package/android/cpp/jni/include/JniSkiaDomView.h
index 68b955def8..d842aa5fd3 100644
--- a/package/android/cpp/jni/include/JniSkiaDomView.h
+++ b/package/android/cpp/jni/include/JniSkiaDomView.h
@@ -34,18 +34,21 @@ class JniSkiaDomView : public jni::HybridClass,
}
static void registerNatives() {
- registerHybrid(
- {makeNativeMethod("initHybrid", JniSkiaDomView::initHybrid),
- makeNativeMethod("surfaceAvailable", JniSkiaDomView::surfaceAvailable),
- makeNativeMethod("surfaceDestroyed", JniSkiaDomView::surfaceDestroyed),
- makeNativeMethod("surfaceSizeChanged",
- JniSkiaDomView::surfaceSizeChanged),
- makeNativeMethod("setMode", JniSkiaDomView::setMode),
- makeNativeMethod("setDebugMode", JniSkiaDomView::setDebugMode),
- makeNativeMethod("updateTouchPoints",
- JniSkiaDomView::updateTouchPoints),
- makeNativeMethod("registerView", JniSkiaDomView::registerView),
- makeNativeMethod("unregisterView", JniSkiaDomView::unregisterView)});
+ registerHybrid({
+ makeNativeMethod("initHybrid", JniSkiaDomView::initHybrid),
+ makeNativeMethod("surfaceAvailable", JniSkiaDomView::surfaceAvailable),
+ makeNativeMethod("surfaceDestroyed", JniSkiaDomView::surfaceDestroyed),
+ makeNativeMethod("surfaceSizeChanged",
+ JniSkiaDomView::surfaceSizeChanged),
+ makeNativeMethod("setMode", JniSkiaDomView::setMode),
+ makeNativeMethod("setDebugMode", JniSkiaDomView::setDebugMode),
+ makeNativeMethod("updateTouchPoints",
+ JniSkiaDomView::updateTouchPoints),
+ makeNativeMethod("registerView", JniSkiaDomView::registerView),
+ makeNativeMethod("unregisterView", JniSkiaDomView::unregisterView)
+ // TODO: Remove if we find another solution for first frame rendering
+ // makeNativeMethod("renderToBitmap", JniSkiaDomView::renderToBitmap)
+ });
}
protected:
@@ -73,6 +76,11 @@ class JniSkiaDomView : public jni::HybridClass,
void unregisterView() override { JniSkiaBaseView::unregisterView(); }
+ // TODO: Remove if we find another solution for first frame rendering
+ /*jobject renderToBitmap(jobject bitmap, int width, int height) override {
+ return JniSkiaBaseView::renderToBitmap(bitmap, width, height);
+ }*/
+
private:
friend HybridBase;
diff --git a/package/android/cpp/jni/include/JniSkiaDrawView.h b/package/android/cpp/jni/include/JniSkiaDrawView.h
index 1cd88bafef..b79eb5495c 100644
--- a/package/android/cpp/jni/include/JniSkiaDrawView.h
+++ b/package/android/cpp/jni/include/JniSkiaDrawView.h
@@ -33,20 +33,21 @@ class JniSkiaDrawView : public jni::HybridClass,
}
static void registerNatives() {
- registerHybrid(
- {makeNativeMethod("initHybrid", JniSkiaDrawView::initHybrid),
- makeNativeMethod("surfaceAvailable",
- JniSkiaDrawView::surfaceAvailable),
- makeNativeMethod("surfaceDestroyed",
- JniSkiaDrawView::surfaceDestroyed),
- makeNativeMethod("surfaceSizeChanged",
- JniSkiaDrawView::surfaceSizeChanged),
- makeNativeMethod("setMode", JniSkiaDrawView::setMode),
- makeNativeMethod("setDebugMode", JniSkiaDrawView::setDebugMode),
- makeNativeMethod("updateTouchPoints",
- JniSkiaDrawView::updateTouchPoints),
- makeNativeMethod("registerView", JniSkiaDrawView::registerView),
- makeNativeMethod("unregisterView", JniSkiaDrawView::unregisterView)});
+ registerHybrid({
+ makeNativeMethod("initHybrid", JniSkiaDrawView::initHybrid),
+ makeNativeMethod("surfaceAvailable", JniSkiaDrawView::surfaceAvailable),
+ makeNativeMethod("surfaceDestroyed", JniSkiaDrawView::surfaceDestroyed),
+ makeNativeMethod("surfaceSizeChanged",
+ JniSkiaDrawView::surfaceSizeChanged),
+ makeNativeMethod("setMode", JniSkiaDrawView::setMode),
+ makeNativeMethod("setDebugMode", JniSkiaDrawView::setDebugMode),
+ makeNativeMethod("updateTouchPoints",
+ JniSkiaDrawView::updateTouchPoints),
+ makeNativeMethod("registerView", JniSkiaDrawView::registerView),
+ makeNativeMethod("unregisterView", JniSkiaDrawView::unregisterView),
+ // TODO: Remove if we find another solution for first frame rendering
+ // makeNativeMethod("renderToBitmap", JniSkiaDrawView::renderToBitmap)
+ });
}
protected:
@@ -74,6 +75,11 @@ class JniSkiaDrawView : public jni::HybridClass,
void unregisterView() override { JniSkiaBaseView::unregisterView(); }
+ // TODO: Remove if we find another solution for first frame rendering
+ /*jobject renderToBitmap(jobject bitmap, int width, int height) override {
+ return JniSkiaBaseView::renderToBitmap(bitmap, width, height);
+ }*/
+
private:
friend HybridBase;
diff --git a/package/android/cpp/jni/include/JniSkiaPictureView.h b/package/android/cpp/jni/include/JniSkiaPictureView.h
index e12113483f..039de3c24e 100644
--- a/package/android/cpp/jni/include/JniSkiaPictureView.h
+++ b/package/android/cpp/jni/include/JniSkiaPictureView.h
@@ -33,21 +33,24 @@ class JniSkiaPictureView : public jni::HybridClass,
}
static void registerNatives() {
- registerHybrid(
- {makeNativeMethod("initHybrid", JniSkiaPictureView::initHybrid),
- makeNativeMethod("surfaceAvailable",
- JniSkiaPictureView::surfaceAvailable),
- makeNativeMethod("surfaceDestroyed",
- JniSkiaPictureView::surfaceDestroyed),
- makeNativeMethod("surfaceSizeChanged",
- JniSkiaPictureView::surfaceSizeChanged),
- makeNativeMethod("setMode", JniSkiaPictureView::setMode),
- makeNativeMethod("setDebugMode", JniSkiaPictureView::setDebugMode),
- makeNativeMethod("updateTouchPoints",
- JniSkiaPictureView::updateTouchPoints),
- makeNativeMethod("registerView", JniSkiaPictureView::registerView),
- makeNativeMethod("unregisterView",
- JniSkiaPictureView::unregisterView)});
+ registerHybrid({
+ makeNativeMethod("initHybrid", JniSkiaPictureView::initHybrid),
+ makeNativeMethod("surfaceAvailable",
+ JniSkiaPictureView::surfaceAvailable),
+ makeNativeMethod("surfaceDestroyed",
+ JniSkiaPictureView::surfaceDestroyed),
+ makeNativeMethod("surfaceSizeChanged",
+ JniSkiaPictureView::surfaceSizeChanged),
+ makeNativeMethod("setMode", JniSkiaPictureView::setMode),
+ makeNativeMethod("setDebugMode", JniSkiaPictureView::setDebugMode),
+ makeNativeMethod("updateTouchPoints",
+ JniSkiaPictureView::updateTouchPoints),
+ makeNativeMethod("registerView", JniSkiaPictureView::registerView),
+ makeNativeMethod("unregisterView", JniSkiaPictureView::unregisterView),
+ // TODO: Remove if we find another solution for first frame rendering
+ // makeNativeMethod("renderToBitmap",
+ // JniSkiaPictureView::renderToBitmap)
+ });
}
protected:
@@ -75,6 +78,12 @@ class JniSkiaPictureView : public jni::HybridClass,
void unregisterView() override { JniSkiaBaseView::unregisterView(); }
+ /*
+ TODO: Remove if we find another solution for first frame rendering
+ jobject renderToBitmap(jobject bitmap, int width, int height) override {
+ return JniSkiaBaseView::renderToBitmap(bitmap, width, height);
+ }*/
+
private:
friend HybridBase;
diff --git a/package/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h b/package/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h
index 2a85adb54c..ca7c6e144c 100644
--- a/package/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h
+++ b/package/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h
@@ -7,7 +7,7 @@
#include
#include
-#include
+#include
namespace RNSkia {
namespace jsi = facebook::jsi;
@@ -38,7 +38,7 @@ class RNSkAndroidPlatformContext : public RNSkPlatformContext {
}
sk_sp makeOffscreenSurface(int width, int height) override {
- return MakeOffscreenGLSurface(width, height);
+ return SkiaOpenGLSurfaceFactory::makeOffscreenSurface(width, height);
}
void runOnMainThread(std::function task) override {
diff --git a/package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.cpp b/package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.cpp
index 5764b75b8c..c5e7ae9cd0 100644
--- a/package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.cpp
+++ b/package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.cpp
@@ -14,64 +14,60 @@ namespace RNSkia {
RNSkOpenGLCanvasProvider::RNSkOpenGLCanvasProvider(
std::function requestRedraw,
- std::shared_ptr context)
- : RNSkCanvasProvider(requestRedraw), _context(context) {}
+ std::shared_ptr platformContext)
+ : RNSkCanvasProvider(requestRedraw), _platformContext(platformContext) {}
RNSkOpenGLCanvasProvider::~RNSkOpenGLCanvasProvider() {}
-float RNSkOpenGLCanvasProvider::getScaledWidth() { return _width; }
+float RNSkOpenGLCanvasProvider::getScaledWidth() {
+ return _surfaceHolder ? _surfaceHolder->getWidth() : 0;
+}
-float RNSkOpenGLCanvasProvider::getScaledHeight() { return _height; }
+float RNSkOpenGLCanvasProvider::getScaledHeight() {
+ return _surfaceHolder ? _surfaceHolder->getHeight() : 0;
+}
bool RNSkOpenGLCanvasProvider::renderToCanvas(
const std::function &cb) {
- if (_renderer != nullptr) {
- return _renderer->run(cb, _width, _height);
+
+ if (_surfaceHolder != nullptr && cb != nullptr) {
+ // Get the surface
+ auto surface = _surfaceHolder->getSurface();
+ if (surface) {
+
+ // Ensure we are ready to render
+ if (!_surfaceHolder->makeCurrent()) {
+ return false;
+ }
+
+ // Draw into canvas using callback
+ cb(surface->getCanvas());
+
+ // Swap buffers and show on screen
+ return _surfaceHolder->present();
+
+ } else {
+ // the render context did not provide a surface
+ return false;
+ }
}
+
return false;
}
void RNSkOpenGLCanvasProvider::surfaceAvailable(jobject surface, int width,
int height) {
- _width = width;
- _height = height;
-
- if (_renderer == nullptr) {
- // Create renderer!
- _renderer = std::make_unique(surface);
+ // Create renderer!
+ _surfaceHolder =
+ SkiaOpenGLSurfaceFactory::makeWindowedSurface(surface, width, height);
- // Redraw
- _requestRedraw();
- }
+ // Post redraw request to ensure we paint in the next draw cycle.
+ _requestRedraw();
}
void RNSkOpenGLCanvasProvider::surfaceDestroyed() {
- if (_renderer != nullptr) {
- // teardown
- _renderer->teardown();
-
- // Teardown renderer on the render thread since OpenGL demands
- // same thread access for OpenGL contexts.
- std::condition_variable cv;
- std::mutex m;
- std::unique_lock lock(m);
-
- _context->runOnRenderThread([&cv, &m, weakSelf = weak_from_this()]() {
- // Lock
- std::unique_lock lock(m);
-
- auto self = weakSelf.lock();
- if (self) {
- if (self->_renderer != nullptr) {
- self->_renderer->run(nullptr, 0, 0);
- }
- // Remove renderer
- self->_renderer = nullptr;
- }
- cv.notify_one();
- });
-
- cv.wait(lock);
- }
+ // destroy the renderer (a unique pointer so the dtor will be called
+ // immediately.)
+ _surfaceHolder = nullptr;
}
void RNSkOpenGLCanvasProvider::surfaceSizeChanged(int width, int height) {
@@ -80,8 +76,9 @@ void RNSkOpenGLCanvasProvider::surfaceSizeChanged(int width, int height) {
// it comes to invalidating the surface.
return;
}
- _width = width;
- _height = height;
+
+ // Recreate RenderContext surface based on size change???
+ _surfaceHolder->resize(width, height);
// Redraw after size change
_requestRedraw();
diff --git a/package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.h b/package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.h
index f340b4e18e..d22a71294b 100644
--- a/package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.h
+++ b/package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.h
@@ -6,7 +6,7 @@
#include
-#include "SkiaOpenGLRenderer.h"
+#include "SkiaOpenGLSurfaceFactory.h"
#include
namespace RNSkia {
@@ -17,7 +17,7 @@ class RNSkOpenGLCanvasProvider
public:
RNSkOpenGLCanvasProvider(
std::function requestRedraw,
- std::shared_ptr context);
+ std::shared_ptr platformContext);
~RNSkOpenGLCanvasProvider();
@@ -34,9 +34,7 @@ class RNSkOpenGLCanvasProvider
void surfaceSizeChanged(int width, int height);
private:
- std::unique_ptr _renderer = nullptr;
- std::shared_ptr _context;
- float _width = -1;
- float _height = -1;
+ std::unique_ptr _surfaceHolder = nullptr;
+ std::shared_ptr _platformContext;
};
} // namespace RNSkia
diff --git a/package/android/cpp/rnskia-android/SkiaOpenGLHelper.h b/package/android/cpp/rnskia-android/SkiaOpenGLHelper.h
new file mode 100644
index 0000000000..68b0afdaea
--- /dev/null
+++ b/package/android/cpp/rnskia-android/SkiaOpenGLHelper.h
@@ -0,0 +1,310 @@
+#pragma once
+
+#include "EGL/egl.h"
+#include "GLES2/gl2.h"
+#include
+#include
+
+#include
+
+#include "RNSkLog.h"
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdocumentation"
+
+#include "SkCanvas.h"
+#include "SkColorSpace.h"
+#include "SkSurface.h"
+#include "include/gpu/GrDirectContext.h"
+#include "include/gpu/gl/GrGLInterface.h"
+
+#pragma clang diagnostic pop
+
+namespace RNSkia {
+
+/**
+ * Singleton holding the default display and shared eglContext that will be the
+ * first context we create so that we can share data between contexts.
+ */
+class OpenGLResourceHolder {
+private:
+ OpenGLResourceHolder() {
+ // Initialize OpenGL
+ glDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ if (glDisplay == EGL_NO_DISPLAY) {
+ RNSkLogger::logToConsole("eglGetDisplay failed : %i", glGetError());
+ return;
+ }
+
+ EGLint major;
+ EGLint minor;
+ if (eglInitialize(glDisplay, &major, &minor) != EGL_TRUE) {
+ RNSkLogger::logToConsole("eglInitialize failed : %i", glGetError());
+ return;
+ }
+
+ // Create a default shared context
+ glConfig = getConfig(glDisplay);
+
+ // Create OpenGL context attributes
+ EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
+
+ // Initialize the offscreen context for this thread
+ glContext =
+ eglCreateContext(glDisplay, glConfig, glContext, contextAttribs);
+ if (glContext == EGL_NO_CONTEXT) {
+ RNSkLogger::logToConsole("eglCreateContext failed : %i", glGetError());
+ }
+ }
+
+ ~OpenGLResourceHolder() {
+ if (glContext != EGL_NO_CONTEXT) {
+ eglDestroyContext(glDisplay, glContext);
+ glContext = EGL_NO_CONTEXT;
+ }
+
+ if (glDisplay != EGL_NO_DISPLAY) {
+ eglTerminate(glDisplay);
+ glDisplay = EGL_NO_DISPLAY;
+ }
+ }
+ /* Explicitly disallow copying. */
+ OpenGLResourceHolder(const OpenGLResourceHolder &) = delete;
+ OpenGLResourceHolder &operator=(const OpenGLResourceHolder &) = delete;
+
+public:
+ static OpenGLResourceHolder &getInstance() {
+ static OpenGLResourceHolder Instance;
+ return Instance;
+ }
+
+ /**
+ * The first context created will be considered the parent / shared context
+ * and will be used as the parent / shareable context when creating subsequent
+ * contexts.
+ */
+ std::atomic glContext = EGL_NO_CONTEXT;
+ /**
+ * Shared egl display
+ */
+ std::atomic glDisplay = EGL_NO_DISPLAY;
+
+ /**
+ * Shared eglConfig
+ */
+ std::atomic glConfig = 0;
+
+private:
+ /**
+ * Finds the correct EGL Config for the given parameters
+ * @param glDisplay
+ * @return Config or zero if no matching context could be found.
+ */
+ static EGLConfig getConfig(EGLDisplay glDisplay) {
+
+ EGLint att[] = {EGL_RENDERABLE_TYPE,
+ EGL_OPENGL_ES2_BIT,
+ EGL_ALPHA_SIZE,
+ 8,
+ EGL_BLUE_SIZE,
+ 8,
+ EGL_GREEN_SIZE,
+ 8,
+ EGL_RED_SIZE,
+ 8,
+ EGL_DEPTH_SIZE,
+ 0,
+ EGL_STENCIL_SIZE,
+ 0,
+ EGL_SAMPLE_BUFFERS,
+ 0,
+ EGL_NONE};
+
+ EGLint numConfigs;
+ EGLConfig glConfig = 0;
+ if (eglChooseConfig(glDisplay, att, &glConfig, 1, &numConfigs) !=
+ EGL_TRUE ||
+ numConfigs == 0) {
+ RNSkLogger::logToConsole(
+ "Failed to choose a config for %s surface. Error code: %d\n",
+ eglGetError());
+ return 0;
+ }
+
+ return glConfig;
+ }
+};
+
+struct SkiaOpenGLContext {
+ SkiaOpenGLContext() {
+ glContext = EGL_NO_CONTEXT;
+ gl1x1Surface = EGL_NO_SURFACE;
+ directContext = nullptr;
+ }
+ ~SkiaOpenGLContext() {
+ if (gl1x1Surface != EGL_NO_SURFACE) {
+ eglDestroySurface(OpenGLResourceHolder::getInstance().glDisplay,
+ gl1x1Surface);
+ gl1x1Surface = EGL_NO_SURFACE;
+ }
+
+ if (directContext) {
+ directContext->releaseResourcesAndAbandonContext();
+ directContext = nullptr;
+ }
+
+ if (glContext != EGL_NO_CONTEXT) {
+ eglDestroyContext(OpenGLResourceHolder::getInstance().glDisplay,
+ glContext);
+ glContext = EGL_NO_CONTEXT;
+ }
+ }
+ EGLContext glContext;
+ EGLSurface gl1x1Surface;
+ sk_sp directContext;
+};
+
+class SkiaOpenGLHelper {
+public:
+ /**
+ * Calls eglMakeCurrent on the surface provided using the provided
+ * thread context.
+ * @param context Skia OpenGL context to use
+ * @param surface Surface to set as current
+ * @return true if eglMakeCurrent was successfull
+ */
+ static bool makeCurrent(SkiaOpenGLContext *context, EGLSurface glSurface) {
+ // We don't need to call make current if we already are current:
+ if (eglGetCurrentSurface(EGL_DRAW) != glSurface ||
+ eglGetCurrentSurface(EGL_READ) != glSurface ||
+ eglGetCurrentContext() != context->glContext) {
+
+ // Make current!
+ if (eglMakeCurrent(OpenGLResourceHolder::getInstance().glDisplay,
+ glSurface, glSurface,
+ context->glContext) != EGL_TRUE) {
+ RNSkLogger::logToConsole("eglMakeCurrent failed: %d\n", eglGetError());
+ return false;
+ }
+ return true;
+ }
+ return true;
+ }
+
+ /**
+ * Creates a new windowed surface
+ * @param window ANativeWindow to create surface in
+ * @return EGLSurface or EGL_NO_SURFACE if the call failed
+ */
+ static EGLSurface createWindowedSurface(ANativeWindow *window) {
+ const EGLint attribs[] = {EGL_NONE};
+ return eglCreateWindowSurface(OpenGLResourceHolder::getInstance().glDisplay,
+ OpenGLResourceHolder::getInstance().glConfig,
+ window, attribs);
+ }
+
+ /**
+ * Destroys an egl surface
+ * @param glSurface
+ * @return
+ */
+ static bool destroySurface(EGLSurface glSurface) {
+ if (eglMakeCurrent(OpenGLResourceHolder::getInstance().glDisplay,
+ EGL_NO_SURFACE, EGL_NO_SURFACE,
+ EGL_NO_CONTEXT) != EGL_TRUE) {
+ RNSkLogger::logToConsole(
+ "destroySurface: Could not clear selected surface");
+ return false;
+ }
+ return eglDestroySurface(OpenGLResourceHolder::getInstance().glDisplay,
+ glSurface) == EGL_TRUE;
+ }
+
+ /**
+ * Calls the eglSwapBuffer in the current thread with the provided surface
+ * @param context Thread context
+ * @param glSurface surface to present
+ * @return true if eglSwapBuffers succeeded.
+ */
+ static bool swapBuffers(SkiaOpenGLContext *context, EGLSurface glSurface) {
+ if (eglSwapBuffers(OpenGLResourceHolder::getInstance().glDisplay,
+ glSurface) != EGL_TRUE) {
+ RNSkLogger::logToConsole("eglSwapBuffers failed: %d\n", eglGetError());
+ return false;
+ }
+ return true;
+ }
+
+ /***
+ * Creates a new Skia direct context backed by the provided eglContext in the
+ * SkiaOpenGLContext.
+ * @param context Context to store results in
+ * @param sharedContext Shared Context
+ * @return true if the call to create a skia direct context suceeded.
+ */
+ static bool createSkiaDirectContextIfNecessary(SkiaOpenGLContext *context) {
+ if (context->directContext == nullptr) {
+
+ // Create OpenGL context
+ createOpenGLContext(context);
+
+ // Create attributes for a simple 1x1 pbuffer surface that we can
+ // use to activate and create Skia direct context for
+ const EGLint offScreenSurfaceAttribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1,
+ EGL_NONE};
+
+ context->gl1x1Surface =
+ eglCreatePbufferSurface(OpenGLResourceHolder::getInstance().glDisplay,
+ OpenGLResourceHolder::getInstance().glConfig,
+ offScreenSurfaceAttribs);
+
+ if (context->gl1x1Surface == EGL_NO_SURFACE) {
+ RNSkLogger::logToConsole("Failed creating a 1x1 pbuffer surface");
+ return false;
+ }
+
+ // Activate
+ if (!makeCurrent(context, context->gl1x1Surface)) {
+ return false;
+ }
+
+ // Create the Skia context
+ auto backendInterface = GrGLMakeNativeInterface();
+ context->directContext = GrDirectContext::MakeGL(backendInterface);
+
+ if (context->directContext == nullptr) {
+ RNSkLogger::logToConsole("GrDirectContext::MakeGL failed");
+ return false;
+ }
+ }
+
+ // It all went well!
+ return true;
+ }
+
+private:
+ /**
+ * Creates a new GLContext.
+ * @param context Context to save results in
+ * @return True if the call to eglCreateContext returned a valid OpenGL
+ * Context or if the context already is setup.
+ */
+ static bool createOpenGLContext(SkiaOpenGLContext *context) {
+ // Create OpenGL context attributes
+ EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
+
+ // Initialize the offscreen context for this thread
+ context->glContext = eglCreateContext(
+ OpenGLResourceHolder::getInstance().glDisplay,
+ OpenGLResourceHolder::getInstance().glConfig,
+ OpenGLResourceHolder::getInstance().glContext, contextAttribs);
+
+ if (context->glContext == EGL_NO_CONTEXT) {
+ RNSkLogger::logToConsole("eglCreateContext failed: %d\n", eglGetError());
+ return EGL_NO_CONTEXT;
+ }
+
+ return true;
+ }
+};
+} // namespace RNSkia
\ No newline at end of file
diff --git a/package/android/cpp/rnskia-android/SkiaOpenGLRenderer.cpp b/package/android/cpp/rnskia-android/SkiaOpenGLRenderer.cpp
deleted file mode 100644
index c1bbd4edf0..0000000000
--- a/package/android/cpp/rnskia-android/SkiaOpenGLRenderer.cpp
+++ /dev/null
@@ -1,347 +0,0 @@
-#include "SkiaOpenGLRenderer.h"
-
-#include
-#include
-#include
-
-#pragma clang diagnostic push
-
-#define STENCIL_BUFFER_SIZE 8
-
-namespace RNSkia {
-/** Static members */
-sk_sp MakeOffscreenGLSurface(int width, int height) {
- EGLDisplay eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
- if (eglDisplay == EGL_NO_DISPLAY) {
- RNSkLogger::logToConsole("eglGetdisplay failed : %i", glGetError());
- return nullptr;
- }
-
- EGLint major;
- EGLint minor;
- if (!eglInitialize(eglDisplay, &major, &minor)) {
- RNSkLogger::logToConsole("eglInitialize failed : %i", glGetError());
- return nullptr;
- }
-
- EGLint att[] = {EGL_RENDERABLE_TYPE,
- EGL_OPENGL_ES2_BIT,
- EGL_SURFACE_TYPE,
- EGL_PBUFFER_BIT,
- EGL_ALPHA_SIZE,
- 8,
- EGL_BLUE_SIZE,
- 8,
- EGL_GREEN_SIZE,
- 8,
- EGL_RED_SIZE,
- 8,
- EGL_DEPTH_SIZE,
- 0,
- EGL_STENCIL_SIZE,
- 0,
- EGL_NONE};
-
- EGLint numConfigs;
- EGLConfig eglConfig;
- eglConfig = 0;
- if (!eglChooseConfig(eglDisplay, att, &eglConfig, 1, &numConfigs) ||
- numConfigs == 0) {
- RNSkLogger::logToConsole("Failed to choose a config %d\n", eglGetError());
- return nullptr;
- }
-
- EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
-
- EGLContext eglContext =
- eglCreateContext(eglDisplay, eglConfig, NULL, contextAttribs);
-
- if (eglContext == EGL_NO_CONTEXT) {
- RNSkLogger::logToConsole("eglCreateContext failed: %d\n", eglGetError());
- return nullptr;
- }
-
- const EGLint offScreenSurfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT,
- height, EGL_NONE};
- EGLSurface eglSurface =
- eglCreatePbufferSurface(eglDisplay, eglConfig, offScreenSurfaceAttribs);
- if (!eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
- RNSkLogger::logToConsole("eglMakeCurrent failed: %d\n", eglGetError());
- return nullptr;
- }
- GLint buffer;
- glGetIntegerv(GL_FRAMEBUFFER_BINDING, &buffer);
-
- GLint stencil;
- glGetIntegerv(GL_STENCIL_BITS, &stencil);
-
- GLint samples;
- glGetIntegerv(GL_SAMPLES, &samples);
-
- // Create the Skia backend context
- auto backendInterface = GrGLMakeNativeInterface();
- auto grContext = GrDirectContext::MakeGL(backendInterface);
- if (grContext == nullptr) {
- RNSkLogger::logToConsole("GrDirectContext::MakeGL failed");
- return nullptr;
- }
- auto maxSamples =
- grContext->maxSurfaceSampleCountForColorType(kRGBA_8888_SkColorType);
-
- if (samples > maxSamples)
- samples = maxSamples;
-
- GrGLFramebufferInfo fbInfo;
- fbInfo.fFBOID = buffer;
- fbInfo.fFormat = 0x8058;
-
- auto renderTarget =
- GrBackendRenderTarget(width, height, samples, stencil, fbInfo);
-
- struct OffscreenRenderContext {
- EGLDisplay display;
- EGLSurface surface;
- };
- auto ctx = new OffscreenRenderContext({eglDisplay, eglSurface});
-
- auto surface = SkSurfaces::WrapBackendRenderTarget(
- grContext.get(), renderTarget, kBottomLeft_GrSurfaceOrigin,
- kRGBA_8888_SkColorType, nullptr, nullptr,
- [](void *addr) {
- auto ctx = reinterpret_cast(addr);
- eglDestroySurface(ctx->display, ctx->surface);
- delete ctx;
- },
- reinterpret_cast(ctx));
- return surface;
-}
-
-std::shared_ptr
-SkiaOpenGLRenderer::getThreadDrawingContext() {
- auto threadId = std::this_thread::get_id();
- if (threadContexts.count(threadId) == 0) {
- auto drawingContext = std::make_shared();
- drawingContext->glContext = EGL_NO_CONTEXT;
- drawingContext->glDisplay = EGL_NO_DISPLAY;
- drawingContext->glConfig = 0;
- drawingContext->skContext = nullptr;
- threadContexts.emplace(threadId, drawingContext);
- }
- return threadContexts.at(threadId);
-}
-
-SkiaOpenGLRenderer::SkiaOpenGLRenderer(jobject surface) {
- _nativeWindow =
- ANativeWindow_fromSurface(facebook::jni::Environment::current(), surface);
-}
-
-SkiaOpenGLRenderer::~SkiaOpenGLRenderer() {
- // Release surface
- ANativeWindow_release(_nativeWindow);
- _nativeWindow = nullptr;
-}
-
-bool SkiaOpenGLRenderer::run(const std::function &cb,
- int width, int height) {
- switch (_renderState) {
- case RenderState::Initializing: {
- _renderState = RenderState::Rendering;
- // Just let the case drop to drawing - we have initialized
- // and we should be able to render (if the picture is set)
- }
- case RenderState::Rendering: {
- // Make sure to initialize the rendering pipeline
- if (!ensureInitialised()) {
- return false;
- }
-
- if (cb != nullptr) {
- // RNSkLogger::logToConsole("SKIARENDER - Render begin");
-
- getThreadDrawingContext()->skContext->resetContext();
-
- SkColorType colorType;
- // setup surface for fbo0
- GrGLFramebufferInfo fboInfo;
- fboInfo.fFBOID = 0;
- fboInfo.fFormat = 0x8058;
- colorType = kN32_SkColorType;
-
- GrBackendRenderTarget backendRT(width, height, 0, STENCIL_BUFFER_SIZE,
- fboInfo);
-
- SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
-
- sk_sp renderTarget(SkSurfaces::WrapBackendRenderTarget(
- getThreadDrawingContext()->skContext.get(), backendRT,
- kBottomLeft_GrSurfaceOrigin, colorType, nullptr, &props));
-
- auto canvas = renderTarget->getCanvas();
-
- // Draw picture into surface
- cb(canvas);
-
- // Flush
- canvas->flush();
-
- if (!eglSwapBuffers(getThreadDrawingContext()->glDisplay, _glSurface)) {
- RNSkLogger::logToConsole("eglSwapBuffers failed: %d\n", eglGetError());
- return false;
- }
-
- // RNSkLogger::logToConsole("SKIARENDER - render done");
- return true;
- }
-
- return false;
- }
- case RenderState::Finishing: {
- _renderState = RenderState::Done;
-
- // Release GL surface
- if (_glSurface != EGL_NO_SURFACE &&
- getThreadDrawingContext()->glDisplay != EGL_NO_DISPLAY) {
- eglDestroySurface(getThreadDrawingContext()->glDisplay, _glSurface);
- _glSurface = EGL_NO_SURFACE;
- }
-
- return true;
- }
- case RenderState::Done: {
- // Do nothing. We're done.
- return true;
- }
- }
-}
-
-bool SkiaOpenGLRenderer::ensureInitialised() {
- // Set up static OpenGL context
- if (!initStaticGLContext()) {
- return false;
- }
-
- // Set up OpenGL Surface
- if (!initGLSurface()) {
- return false;
- }
-
- // Init skia static context
- if (!initStaticSkiaContext()) {
- return false;
- }
-
- return true;
-}
-
-void SkiaOpenGLRenderer::teardown() { _renderState = RenderState::Finishing; }
-
-bool SkiaOpenGLRenderer::initStaticGLContext() {
- if (getThreadDrawingContext()->glContext != EGL_NO_CONTEXT) {
- return true;
- }
-
- getThreadDrawingContext()->glDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
- if (getThreadDrawingContext()->glDisplay == EGL_NO_DISPLAY) {
- RNSkLogger::logToConsole("eglGetdisplay failed : %i", glGetError());
- return false;
- }
-
- EGLint major;
- EGLint minor;
- if (!eglInitialize(getThreadDrawingContext()->glDisplay, &major, &minor)) {
- RNSkLogger::logToConsole("eglInitialize failed : %i", glGetError());
- return false;
- }
-
- EGLint att[] = {EGL_RENDERABLE_TYPE,
- EGL_OPENGL_ES2_BIT,
- EGL_SURFACE_TYPE,
- EGL_WINDOW_BIT,
- EGL_ALPHA_SIZE,
- 8,
- EGL_BLUE_SIZE,
- 8,
- EGL_GREEN_SIZE,
- 8,
- EGL_RED_SIZE,
- 8,
- EGL_DEPTH_SIZE,
- 0,
- EGL_STENCIL_SIZE,
- 0,
- EGL_NONE};
-
- EGLint numConfigs;
- getThreadDrawingContext()->glConfig = 0;
- if (!eglChooseConfig(getThreadDrawingContext()->glDisplay, att,
- &getThreadDrawingContext()->glConfig, 1, &numConfigs) ||
- numConfigs == 0) {
- RNSkLogger::logToConsole("Failed to choose a config %d\n", eglGetError());
- return false;
- }
-
- EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
-
- getThreadDrawingContext()->glContext = eglCreateContext(
- getThreadDrawingContext()->glDisplay, getThreadDrawingContext()->glConfig,
- NULL, contextAttribs);
-
- if (getThreadDrawingContext()->glContext == EGL_NO_CONTEXT) {
- RNSkLogger::logToConsole("eglCreateContext failed: %d\n", eglGetError());
- return false;
- }
-
- return true;
-}
-
-bool SkiaOpenGLRenderer::initStaticSkiaContext() {
- if (getThreadDrawingContext()->skContext != nullptr) {
- return true;
- }
-
- // Create the Skia backend context
- auto backendInterface = GrGLMakeNativeInterface();
- getThreadDrawingContext()->skContext =
- GrDirectContext::MakeGL(backendInterface);
- if (getThreadDrawingContext()->skContext == nullptr) {
- RNSkLogger::logToConsole("GrDirectContext::MakeGL failed");
- return false;
- }
-
- return true;
-}
-
-bool SkiaOpenGLRenderer::initGLSurface() {
- if (_nativeWindow == nullptr) {
- return false;
- }
-
- if (_glSurface != EGL_NO_SURFACE) {
- if (!eglMakeCurrent(getThreadDrawingContext()->glDisplay, _glSurface,
- _glSurface, getThreadDrawingContext()->glContext)) {
- RNSkLogger::logToConsole("eglMakeCurrent failed: %d\n", eglGetError());
- return false;
- }
- return true;
- }
-
- // Create the opengl surface
- _glSurface = eglCreateWindowSurface(getThreadDrawingContext()->glDisplay,
- getThreadDrawingContext()->glConfig,
- _nativeWindow, nullptr);
-
- if (_glSurface == EGL_NO_SURFACE) {
- RNSkLogger::logToConsole("eglCreateWindowSurface failed: %d\n",
- eglGetError());
- return false;
- }
-
- if (!eglMakeCurrent(getThreadDrawingContext()->glDisplay, _glSurface,
- _glSurface, getThreadDrawingContext()->glContext)) {
- RNSkLogger::logToConsole("eglMakeCurrent failed: %d\n", eglGetError());
- return false;
- }
-
- return true;
-}
-} // namespace RNSkia
\ No newline at end of file
diff --git a/package/android/cpp/rnskia-android/SkiaOpenGLRenderer.h b/package/android/cpp/rnskia-android/SkiaOpenGLRenderer.h
deleted file mode 100644
index 64710ab8ea..0000000000
--- a/package/android/cpp/rnskia-android/SkiaOpenGLRenderer.h
+++ /dev/null
@@ -1,124 +0,0 @@
-#pragma once
-
-#include
-
-#include "EGL/egl.h"
-#include "GLES2/gl2.h"
-#include "android/native_window.h"
-#include
-#include
-
-#include
-#include
-#include
-#include
-
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdocumentation"
-
-#include "SkCanvas.h"
-#include "SkColorSpace.h"
-#include "SkPicture.h"
-#include "SkSurface.h"
-
-#include "include/gpu/GrBackendSurface.h"
-#include "include/gpu/GrDirectContext.h"
-#include "include/gpu/ganesh/SkSurfaceGanesh.h"
-#include "include/gpu/gl/GrGLInterface.h"
-
-#pragma clang diagnostic pop
-
-namespace RNSkia {
-sk_sp MakeOffscreenGLSurface(int width, int height);
-
-using OpenGLDrawingContext = struct {
- EGLContext glContext;
- EGLDisplay glDisplay;
- EGLConfig glConfig;
- sk_sp skContext;
-};
-
-static std::unordered_map>
- threadContexts;
-
-enum RenderState : int {
- Initializing,
- Rendering,
- Finishing,
- Done,
-};
-
-class SkiaOpenGLRenderer {
-public:
- explicit SkiaOpenGLRenderer(jobject surface);
- ~SkiaOpenGLRenderer();
-
- /**
- * Initializes, renders and tears down the render pipeline depending on the
- * state of the renderer. All OpenGL/Skia context operations are done on a
- * separate thread which must be the same for all calls to the render method.
- *
- * @param callback Render callback
- * @param width Width of surface to render if there is a picture
- * @param height Height of surface to render if there is a picture
- */
- bool run(const std::function &cb, int width, int height);
-
- /**
- * Sets the state to finishing. Next time the renderer will be called it
- * will tear down and release its resources. It is important that this
- * is done on the same thread as the other OpenGL context stuff is handled.
- *
- * Teardown can be called fom whatever thread we want - but we must ensure
- * that at least one call to render on the render thread is done after calling
- * teardown.
- */
- void teardown();
-
-private:
- /**
- * Initializes all required OpenGL and Skia objects
- * @return True if initialization went well.
- */
- bool ensureInitialised();
-
- /**
- * Initializes the static OpenGL context that is shared between
- * all instances of the renderer.
- * @return True if initialization went well
- */
- bool initStaticGLContext();
-
- /**
- * Initializes the static Skia context that is shared between
- * all instances of the renderer
- * @return True if initialization went well
- */
- bool initStaticSkiaContext();
-
- /**
- * Inititalizes the OpenGL surface from the native view pointer we
- * got on initialization. Each renderer has its own OpenGL surface to
- * render on.
- * @return True if initialization went well
- */
- bool initGLSurface();
-
- /**
- * To be able to use static contexts (and avoid reloading the skia context for
- * each new view, we track the OpenGL and Skia drawing context per thread.
- * @return The drawing context for the current thread
- */
- static std::shared_ptr getThreadDrawingContext();
-
- EGLSurface _glSurface = EGL_NO_SURFACE;
-
- ANativeWindow *_nativeWindow = nullptr;
-
- int _prevWidth = 0;
- int _prevHeight = 0;
-
- std::atomic _renderState = {RenderState::Initializing};
-};
-} // namespace RNSkia
\ No newline at end of file
diff --git a/package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.cpp b/package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.cpp
new file mode 100644
index 0000000000..6fa5a4406f
--- /dev/null
+++ b/package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.cpp
@@ -0,0 +1,132 @@
+#include "SkiaOpenGLHelper.h"
+#include
+
+namespace RNSkia {
+
+thread_local SkiaOpenGLContext ThreadContextHolder::ThreadSkiaOpenGLContext;
+
+sk_sp SkiaOpenGLSurfaceFactory::makeOffscreenSurface(int width,
+ int height) {
+ // Setup OpenGL and Skia:
+ if (!SkiaOpenGLHelper::createSkiaDirectContextIfNecessary(
+ &ThreadContextHolder::ThreadSkiaOpenGLContext)) {
+
+ RNSkLogger::logToConsole(
+ "Could not create Skia Surface from native window / surface. "
+ "Failed creating Skia Direct Context");
+ return nullptr;
+ }
+
+ auto colorType = kN32_SkColorType;
+
+ SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
+
+ // Create texture
+ auto texture =
+ ThreadContextHolder::ThreadSkiaOpenGLContext.directContext
+ ->createBackendTexture(width, height, colorType, GrMipMapped::kNo,
+ GrRenderable::kYes);
+
+ struct ReleaseContext {
+ SkiaOpenGLContext *context;
+ GrBackendTexture texture;
+ };
+
+ auto releaseCtx = new ReleaseContext(
+ {&ThreadContextHolder::ThreadSkiaOpenGLContext, texture});
+
+ // Create a SkSurface from the GrBackendTexture
+ return SkSurfaces::WrapBackendTexture(
+ ThreadContextHolder::ThreadSkiaOpenGLContext.directContext.get(), texture,
+ kTopLeft_GrSurfaceOrigin, 0, colorType, nullptr, &props,
+ [](void *addr) {
+ auto releaseCtx = reinterpret_cast(addr);
+
+ releaseCtx->context->directContext->deleteBackendTexture(
+ releaseCtx->texture);
+ },
+ releaseCtx);
+}
+
+sk_sp WindowSurfaceHolder::getSurface() {
+ if (_skSurface == nullptr) {
+
+ // Setup OpenGL and Skia
+ if (!SkiaOpenGLHelper::createSkiaDirectContextIfNecessary(
+ &ThreadContextHolder::ThreadSkiaOpenGLContext)) {
+ RNSkLogger::logToConsole(
+ "Could not create Skia Surface from native window / surface. "
+ "Failed creating Skia Direct Context");
+ return nullptr;
+ }
+
+ // Now we can create a surface
+ _glSurface = SkiaOpenGLHelper::createWindowedSurface(_window);
+ if (_glSurface == EGL_NO_SURFACE) {
+ RNSkLogger::logToConsole(
+ "Could not create EGL Surface from native window / surface.");
+ return nullptr;
+ }
+
+ // Now make this one current
+ if (!SkiaOpenGLHelper::makeCurrent(
+ &ThreadContextHolder::ThreadSkiaOpenGLContext, _glSurface)) {
+ RNSkLogger::logToConsole(
+ "Could not create EGL Surface from native window / surface. Could "
+ "not set new surface as current surface.");
+ return nullptr;
+ }
+
+ // Set up parameters for the render target so that it
+ // matches the underlying OpenGL context.
+ GrGLFramebufferInfo fboInfo;
+
+ // We pass 0 as the framebuffer id, since the
+ // underlying Skia GrGlGpu will read this when wrapping the context in the
+ // render target and the GrGlGpu object.
+ fboInfo.fFBOID = 0;
+ fboInfo.fFormat = 0x8058; // GL_RGBA8
+
+ GLint stencil;
+ glGetIntegerv(GL_STENCIL_BITS, &stencil);
+
+ GLint samples;
+ glGetIntegerv(GL_SAMPLES, &samples);
+
+ auto colorType = kN32_SkColorType;
+
+ auto maxSamples =
+ ThreadContextHolder::ThreadSkiaOpenGLContext.directContext
+ ->maxSurfaceSampleCountForColorType(colorType);
+
+ if (samples > maxSamples) {
+ samples = maxSamples;
+ }
+
+ GrBackendRenderTarget renderTarget(_width, _height, samples, stencil,
+ fboInfo);
+
+ SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
+
+ struct ReleaseContext {
+ EGLSurface glSurface;
+ };
+
+ auto releaseCtx = new ReleaseContext({_glSurface});
+
+ // Create surface object
+ _skSurface = SkSurfaces::WrapBackendRenderTarget(
+ ThreadContextHolder::ThreadSkiaOpenGLContext.directContext.get(),
+ renderTarget, kBottomLeft_GrSurfaceOrigin, colorType, nullptr, &props,
+ [](void *addr) {
+ auto releaseCtx = reinterpret_cast(addr);
+ SkiaOpenGLHelper::destroySurface(releaseCtx->glSurface);
+ delete releaseCtx;
+ },
+ reinterpret_cast(releaseCtx));
+ }
+
+ return _skSurface;
+}
+
+} // namespace RNSkia
\ No newline at end of file
diff --git a/package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.h b/package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.h
new file mode 100644
index 0000000000..1568b97ab4
--- /dev/null
+++ b/package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.h
@@ -0,0 +1,125 @@
+#pragma once
+
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include "SkiaOpenGLHelper.h"
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdocumentation"
+
+#include "SkCanvas.h"
+#include "SkColorSpace.h"
+#include "SkSurface.h"
+#include "include/gpu/GrBackendSurface.h"
+#include "include/gpu/GrDirectContext.h"
+#include "include/gpu/ganesh/SkSurfaceGanesh.h"
+#include "include/gpu/gl/GrGLInterface.h"
+
+#pragma clang diagnostic pop
+
+namespace RNSkia {
+
+/**
+ * Holder of the thread local SkiaOpenGLContext member
+ */
+class ThreadContextHolder {
+public:
+ static thread_local SkiaOpenGLContext ThreadSkiaOpenGLContext;
+};
+
+/**
+ * Holder of the Windowed SkSurface with support for making current
+ * and presenting to screen
+ */
+class WindowSurfaceHolder {
+public:
+ WindowSurfaceHolder(jobject surface, int width, int height)
+ : _width(width), _height(height),
+ _window(ANativeWindow_fromSurface(facebook::jni::Environment::current(),
+ surface)) {}
+
+ ~WindowSurfaceHolder() { ANativeWindow_release(_window); }
+
+ int getWidth() { return _width; }
+ int getHeight() { return _height; }
+
+ /*
+ * Ensures that the holder has a valid surface and returns the surface.
+ */
+ sk_sp getSurface();
+
+ /**
+ * Resizes the surface
+ * @param width
+ * @param height
+ */
+ void resize(int width, int height) {
+ _width = width;
+ _height = height;
+ _skSurface = nullptr;
+ }
+
+ /**
+ * Sets the current surface as the active surface
+ * @return true if make current succeeds
+ */
+ bool makeCurrent() {
+ return SkiaOpenGLHelper::makeCurrent(
+ &ThreadContextHolder::ThreadSkiaOpenGLContext, _glSurface);
+ }
+
+ /**
+ * Presents the current drawing operations by swapping buffers
+ * @return true if make current succeeds
+ */
+ bool present() {
+ // Flush and submit the direct context
+ ThreadContextHolder::ThreadSkiaOpenGLContext.directContext
+ ->flushAndSubmit();
+
+ // Swap buffers
+ return SkiaOpenGLHelper::swapBuffers(
+ &ThreadContextHolder::ThreadSkiaOpenGLContext, _glSurface);
+ }
+
+private:
+ ANativeWindow *_window = nullptr;
+ sk_sp _skSurface = nullptr;
+ EGLSurface _glSurface = EGL_NO_SURFACE;
+ int _width = 0;
+ int _height = 0;
+};
+
+class SkiaOpenGLSurfaceFactory {
+public:
+ /**
+ * Creates a new Skia surface that is backed by a texture.
+ * @param width Width of surface
+ * @param height Height of surface
+ * @return An SkSurface backed by a texture.
+ */
+ static sk_sp makeOffscreenSurface(int width, int height);
+
+ /**
+ * Creates a windowed Skia Surface holder.
+ * @param width Initial width of surface
+ * @param height Initial height of surface
+ * @param window Window coming from Java
+ * @return A Surface holder
+ */
+ static std::unique_ptr
+ makeWindowedSurface(jobject window, int width, int height) {
+ return std::make_unique(window, width, height);
+ }
+};
+
+} // namespace RNSkia
\ No newline at end of file
diff --git a/package/android/src/main/java/com/shopify/reactnative/skia/SkiaBaseView.java b/package/android/src/main/java/com/shopify/reactnative/skia/SkiaBaseView.java
index 29fd4b3db0..7c54991978 100644
--- a/package/android/src/main/java/com/shopify/reactnative/skia/SkiaBaseView.java
+++ b/package/android/src/main/java/com/shopify/reactnative/skia/SkiaBaseView.java
@@ -2,14 +2,13 @@
import android.content.Context;
import android.graphics.SurfaceTexture;
+import android.util.Log;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.TextureView;
import com.facebook.jni.annotations.DoNotStrip;
-import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.views.view.ReactViewGroup;
-
public abstract class SkiaBaseView extends ReactViewGroup implements TextureView.SurfaceTextureListener {
@DoNotStrip
@@ -18,12 +17,43 @@ public abstract class SkiaBaseView extends ReactViewGroup implements TextureView
public SkiaBaseView(Context context) {
super(context);
+ // TODO: Remove if we find another solution for first frame rendering
+ //setWillNotDraw(!shouldRenderFirstFrameAsBitmap());
mTexture = new TextureView(context);
mTexture.setSurfaceTextureListener(this);
mTexture.setOpaque(false);
addView(mTexture);
}
+ /*@Override
+ TODO: Remove if we find another solution for first frame rendering
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ // If we haven't got a surface yet, let's ask the view to
+ // draw into a bitmap and then render the bitmap. This method
+ // is typically only called once - for the first frame, and
+ // then the surface will be available and all rendering will
+ // be done directly to the surface itself.
+ if (shouldRenderFirstFrameAsBitmap() && mSurface == null) {
+ int width = getWidth();
+ int height = getHeight();
+
+ if (width > 0 && height > 0) {
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Bitmap result = (Bitmap) renderToBitmap(bitmap, width, height);
+
+ canvas.drawBitmap(
+ result,
+ new Rect(0, 0, width, height),
+ new Rect(0, 0, width, height),
+ null);
+
+ bitmap.recycle();
+ }
+ }
+ }*/
+
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
@@ -102,28 +132,53 @@ private static int motionActionToType(int action) {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ Log.i("SkiaBaseView", "onSurfaceTextureAvailable " + width + "/" + height);
mSurface = new Surface(surface);
surfaceAvailable(mSurface, width, height);
+
+ /*
+ TODO: Remove if we find another solution for first frame rendering
+ // Clear rendered bitmap when the surface texture has rendered
+ // We'll post a message to the main loop asking to invalidate
+ if (shouldRenderFirstFrameAsBitmap()) {
+ postUpdate(new AtomicInteger());
+ }*/
}
+ /**
+ * This method is a way for us to clear the bitmap rendered on the first frame
+ * after at least 16 frames have passed - to avoid seeing blinks on the screen caused by
+ * TextureView frame sync issues. This is a hack to avoid those pesky blinks. Have no
+ * idea on how to sync the TextureView OpenGL updates.
+ * @param counter
+ */
+ /*
+ TODO: Remove if we find another solution for first frame rendering
+ void postUpdate(AtomicInteger counter) {
+ counter.getAndIncrement();
+ if (counter.get() > 16) {
+ invalidate();
+ } else {
+ this.post(() -> {
+ postUpdate(counter);
+ });
+ }
+ }*/
+
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ Log.i("SkiaBaseView", "onSurfaceTextureSizeChanged " + width + "/" + height);
surfaceSizeChanged(width, height);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
- // Notify the native side
- surfaceDestroyed();
+ Log.i("SkiaBaseView", "onSurfaceTextureDestroyed");
// https://developer.android.com/reference/android/view/TextureView.SurfaceTextureListener#onSurfaceTextureDestroyed(android.graphics.SurfaceTexture)
- // Invoked when the specified SurfaceTexture is about to be destroyed. If returns true,
- // no rendering should happen inside the surface texture after this method is invoked.
- // We've measured this and it seems like we need to call release and return true - and
- // then handle the issue with this being ripped out underneath the native layer in the C++
- // code.
+ surfaceDestroyed();
mSurface.release();
- // Return true - we promise that no more rendering will be done now.
- return true;
+ mSurface = null;
+ return false;
}
@Override
@@ -131,6 +186,17 @@ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
// Nothing special to do here
}
+ /**
+ * Returns true if the view is able to directly render on the
+ * main thread. This can f.ex then be used to create a first frame
+ * render of the view. Returns true by default - override if not.
+ */
+ /*
+ TODO: Remove if we find another solution for first frame rendering
+ protected boolean shouldRenderFirstFrameAsBitmap() {
+ return false;
+ }*/
+
protected abstract void surfaceAvailable(Object surface, int width, int height);
protected abstract void surfaceSizeChanged(int width, int height);
@@ -146,4 +212,7 @@ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
protected abstract void registerView(int nativeId);
protected abstract void unregisterView();
+
+ // TODO: Remove if we find another solution for first frame rendering
+ // protected native Object renderToBitmap(Object bitmap, int width, int height);
}
diff --git a/package/android/src/main/java/com/shopify/reactnative/skia/SkiaDomView.java b/package/android/src/main/java/com/shopify/reactnative/skia/SkiaDomView.java
index f33a3fbc65..b9ac7485f6 100644
--- a/package/android/src/main/java/com/shopify/reactnative/skia/SkiaDomView.java
+++ b/package/android/src/main/java/com/shopify/reactnative/skia/SkiaDomView.java
@@ -42,4 +42,6 @@ protected void finalize() throws Throwable {
protected native void unregisterView();
+ // TODO: Remove if we find another solution for first frame rendering
+ // protected native Object renderToBitmap(Object bitmap, int width, int height);
}
diff --git a/package/android/src/main/java/com/shopify/reactnative/skia/SkiaDrawView.java b/package/android/src/main/java/com/shopify/reactnative/skia/SkiaDrawView.java
index 533285ec8c..f84c093972 100644
--- a/package/android/src/main/java/com/shopify/reactnative/skia/SkiaDrawView.java
+++ b/package/android/src/main/java/com/shopify/reactnative/skia/SkiaDrawView.java
@@ -42,4 +42,6 @@ protected void finalize() throws Throwable {
protected native void unregisterView();
+ // TODO: Remove if we find another solution for first frame rendering
+ // protected native Object renderToBitmap(Object bitmap, int width, int height);
}
diff --git a/package/android/src/main/java/com/shopify/reactnative/skia/SkiaPictureView.java b/package/android/src/main/java/com/shopify/reactnative/skia/SkiaPictureView.java
index 7c1b9b5b3b..152be16c4a 100644
--- a/package/android/src/main/java/com/shopify/reactnative/skia/SkiaPictureView.java
+++ b/package/android/src/main/java/com/shopify/reactnative/skia/SkiaPictureView.java
@@ -42,4 +42,7 @@ protected void finalize() throws Throwable {
protected native void unregisterView();
+ // TODO: Remove if we find another solution for first frame rendering
+ // protected native Object renderToBitmap(Object bitmap, int width, int height);
+
}
diff --git a/package/cpp/api/JsiSkHostObjects.h b/package/cpp/api/JsiSkHostObjects.h
index 68410f9c45..f3b86b6daf 100644
--- a/package/cpp/api/JsiSkHostObjects.h
+++ b/package/cpp/api/JsiSkHostObjects.h
@@ -81,16 +81,6 @@ template class JsiSkWrappingHostObject : public JsiSkHostObject {
*/
virtual void releaseResources() = 0;
- /**
- Throws a runtime error if this method is called after the object has been
- disposed.
- */
- void ensureNotDisposed() {
- if (_isDisposed) {
- throw std::runtime_error("API Object accessed after it was disposed");
- }
- }
-
private:
void safeDispose() {
if (!_isDisposed) {
diff --git a/package/cpp/rnskia/RNSkJsView.cpp b/package/cpp/rnskia/RNSkJsView.cpp
index 3c4abb892f..8f3f30fb20 100644
--- a/package/cpp/rnskia/RNSkJsView.cpp
+++ b/package/cpp/rnskia/RNSkJsView.cpp
@@ -36,17 +36,41 @@ bool RNSkJsRenderer::tryRender(
void RNSkJsRenderer::renderImmediate(
std::shared_ptr canvasProvider) {
+ // Get start time to be able to calculate animations etc.
std::chrono::milliseconds ms =
std::chrono::duration_cast(
std::chrono::system_clock::now().time_since_epoch());
- canvasProvider->renderToCanvas([&](SkCanvas *canvas) {
- // Create jsi canvas
- auto jsiCanvas = std::make_shared(_platformContext);
- jsiCanvas->setCanvas(canvas);
- drawInJsiCanvas(std::move(jsiCanvas), canvasProvider->getScaledWidth(),
- canvasProvider->getScaledHeight(), ms.count() / 1000);
+ std::condition_variable cv;
+ std::mutex m;
+ std::unique_lock lock(m);
+
+ // We need to render on the javascript thread but block
+ // until we're done rendering. Render immediate is used
+ // to make images from the canvas.
+ _platformContext->runOnJavascriptThread([canvasProvider, ms, &cv, &m,
+ weakSelf = weak_from_this()]() {
+ // Lock
+ std::unique_lock lock(m);
+
+ auto self = weakSelf.lock();
+ if (self) {
+ canvasProvider->renderToCanvas([self, ms,
+ canvasProvider](SkCanvas *canvas) {
+ // Create jsi canvas
+ auto jsiCanvas = std::make_shared(self->_platformContext);
+ jsiCanvas->setCanvas(canvas);
+
+ self->drawInJsiCanvas(
+ std::move(jsiCanvas), canvasProvider->getScaledWidth(),
+ canvasProvider->getScaledHeight(), ms.count() / 1000);
+ });
+ }
+
+ cv.notify_one();
});
+
+ cv.wait(lock);
}
void RNSkJsRenderer::setDrawCallback(
@@ -99,12 +123,13 @@ void RNSkJsRenderer::performDraw(
if (_gpuDrawingLock->try_lock()) {
- // Post drawing message to the render thread where the picture recorded
+ // Post drawing message to the main thread where the picture recorded
// will be sent to the GPU/backend for rendering to screen.
+ // TODO: Which thread should we render on? I think it should be main thread!
auto gpuLock = _gpuDrawingLock;
- _platformContext->runOnRenderThread([weakSelf = weak_from_this(),
- p = std::move(p), gpuLock,
- canvasProvider]() {
+ _platformContext->runOnMainThread([weakSelf = weak_from_this(),
+ p = std::move(p), gpuLock,
+ canvasProvider]() {
auto self = weakSelf.lock();
if (self) {
// Draw the picture recorded on the real GPU canvas
diff --git a/package/cpp/rnskia/RNSkJsiViewApi.h b/package/cpp/rnskia/RNSkJsiViewApi.h
index 5096c6dfc2..5e02f760f3 100644
--- a/package/cpp/rnskia/RNSkJsiViewApi.h
+++ b/package/cpp/rnskia/RNSkJsiViewApi.h
@@ -162,7 +162,7 @@ class RNSkJsiViewApi : public RNJsi::JsiHostObject,
if (info->view != nullptr) {
if (count > 1 && !arguments[1].isUndefined() && !arguments[1].isNull()) {
auto rect = JsiSkRect::fromValue(runtime, arguments[1]);
- image = info->view->makeImageSnapshot(rect);
+ image = info->view->makeImageSnapshot(rect.get());
} else {
image = info->view->makeImageSnapshot(nullptr);
}
diff --git a/package/cpp/rnskia/RNSkView.h b/package/cpp/rnskia/RNSkView.h
index 5fbd5bc906..9463e213b1 100644
--- a/package/cpp/rnskia/RNSkView.h
+++ b/package/cpp/rnskia/RNSkView.h
@@ -86,11 +86,11 @@ class RNSkRenderer {
bool _showDebugOverlays;
};
-class RNSkImageCanvasProvider : public RNSkCanvasProvider {
+class RNSkOffscreenCanvasProvider : public RNSkCanvasProvider {
public:
- RNSkImageCanvasProvider(std::shared_ptr context,
- std::function requestRedraw, float width,
- float height)
+ RNSkOffscreenCanvasProvider(std::shared_ptr context,
+ std::function requestRedraw, float width,
+ float height)
: RNSkCanvasProvider(requestRedraw), _width(width), _height(height) {
_surface = context->makeOffscreenSurface(_width, _height);
}
@@ -98,7 +98,7 @@ class RNSkImageCanvasProvider : public RNSkCanvasProvider {
/**
Returns a snapshot of the current surface/canvas
*/
- sk_sp makeSnapshot(std::shared_ptr bounds) {
+ sk_sp makeSnapshot(SkRect *bounds) {
sk_sp image;
if (bounds != nullptr) {
SkIRect b = SkIRect::MakeXYWH(bounds->x(), bounds->y(), bounds->width(),
@@ -273,9 +273,9 @@ class RNSkView : public std::enable_shared_from_this {
/**
Renders the view into an SkImage instead of the screen.
*/
- sk_sp makeImageSnapshot(std::shared_ptr bounds) {
+ sk_sp makeImageSnapshot(SkRect *bounds) {
- auto provider = std::make_shared(
+ auto provider = std::make_shared(
getPlatformContext(), std::bind(&RNSkView::requestRedraw, this),
_canvasProvider->getScaledWidth(), _canvasProvider->getScaledHeight());
@@ -283,6 +283,8 @@ class RNSkView : public std::enable_shared_from_this {
return provider->makeSnapshot(bounds);
}
+ std::shared_ptr getRenderer() { return _renderer; }
+
protected:
std::shared_ptr getPlatformContext() {
return _platformContext;
@@ -290,7 +292,6 @@ class RNSkView : public std::enable_shared_from_this {
std::shared_ptr getCanvasProvider() {
return _canvasProvider;
}
- std::shared_ptr getRenderer() { return _renderer; }
/**
Ends an ongoing beginDrawCallback loop for this view. This method is made
@@ -399,7 +400,6 @@ class RNSkView : public std::enable_shared_from_this {
size_t _drawingLoopId = 0;
std::atomic _redrawRequestCounter = {1};
- bool _initialDrawingDone = false;
};
} // namespace RNSkia
diff --git a/package/src/renderer/__tests__/e2e/Offscreen.spec.tsx b/package/src/renderer/__tests__/e2e/Offscreen.spec.tsx
index bfaba89864..68a23de891 100644
--- a/package/src/renderer/__tests__/e2e/Offscreen.spec.tsx
+++ b/package/src/renderer/__tests__/e2e/Offscreen.spec.tsx
@@ -67,4 +67,45 @@ describe("Offscreen Drawings", () => {
);
checkImage(image, docPath("offscreen/circle.png"));
});
+ it("Should render to multiple offscreen surfaces at once", async () => {
+ const { width, height } = surface;
+ const raw = await surface.eval(
+ (Skia, ctx) => {
+ const r = ctx.width / 4;
+ const backSurface1 = Skia.Surface.MakeOffscreen(ctx.width, ctx.height)!;
+ const backSurface2 = Skia.Surface.MakeOffscreen(ctx.width, ctx.height)!;
+ const frontSurface = Skia.Surface.MakeOffscreen(ctx.width, ctx.height)!;
+ if (!backSurface1 || !backSurface2 || !frontSurface) {
+ throw new Error("Could not create offscreen surface");
+ }
+ // Paint to first surface
+ const canvas1 = backSurface1.getCanvas();
+ const paint1 = Skia.Paint();
+ paint1.setColor(Skia.Color("lightblue"));
+ canvas1.drawCircle(r, r, r, paint1);
+ backSurface1.flush();
+
+ // Paint to second surface
+ const canvas2 = backSurface2.getCanvas();
+ const paint2 = Skia.Paint();
+ paint2.setColor(Skia.Color("magenta"));
+ canvas2.drawCircle(r, r, r, paint2);
+ backSurface2.flush();
+
+ // TODO: When we've implemented sharing on iOS we can remove makeNonTextureImage here:
+ const image1 = backSurface1.makeImageSnapshot().makeNonTextureImage();
+ const image2 = backSurface2.makeImageSnapshot().makeNonTextureImage();
+
+ frontSurface.getCanvas().drawImage(image1, 0, 0);
+ frontSurface.getCanvas().drawImage(image2, ctx.width / 2, 0);
+ return frontSurface.makeImageSnapshot().encodeToBase64();
+ },
+ { width, height }
+ );
+ const { Skia } = importSkia();
+ const data = Skia.Data.fromBase64(raw);
+ const image = Skia.Image.MakeImageFromEncoded(data)!;
+ expect(data).toBeDefined();
+ checkImage(image, docPath("offscreen/multiple_circles.png"));
+ });
});