From 80b09703234659b2dd5c34821f68e522a0ea2c16 Mon Sep 17 00:00:00 2001 From: Christian Falch <875252+chrfalch@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:06:06 +0200 Subject: [PATCH] Android: New OpenGL based rendering for windowed and offscreen surfaces (#1722) --- .../static/img/offscreen/multiple_circles.png | Bin 0 -> 4273 bytes example/src/Examples/API/Icons/index.tsx | 11 +- package/android/CMakeLists.txt | 2 +- .../android/cpp/jni/include/JniSkiaBaseView.h | 87 ++++- .../android/cpp/jni/include/JniSkiaDomView.h | 32 +- .../android/cpp/jni/include/JniSkiaDrawView.h | 34 +- .../cpp/jni/include/JniSkiaPictureView.h | 39 +- .../RNSkAndroidPlatformContext.h | 4 +- .../RNSkOpenGLCanvasProvider.cpp | 85 +++-- .../rnskia-android/RNSkOpenGLCanvasProvider.h | 10 +- .../cpp/rnskia-android/SkiaOpenGLHelper.h | 310 ++++++++++++++++ .../cpp/rnskia-android/SkiaOpenGLRenderer.cpp | 347 ------------------ .../cpp/rnskia-android/SkiaOpenGLRenderer.h | 124 ------- .../SkiaOpenGLSurfaceFactory.cpp | 132 +++++++ .../rnskia-android/SkiaOpenGLSurfaceFactory.h | 125 +++++++ .../reactnative/skia/SkiaBaseView.java | 91 ++++- .../shopify/reactnative/skia/SkiaDomView.java | 2 + .../reactnative/skia/SkiaDrawView.java | 2 + .../reactnative/skia/SkiaPictureView.java | 3 + package/cpp/api/JsiSkHostObjects.h | 10 - package/cpp/rnskia/RNSkJsView.cpp | 45 ++- package/cpp/rnskia/RNSkJsiViewApi.h | 2 +- package/cpp/rnskia/RNSkView.h | 18 +- .../renderer/__tests__/e2e/Offscreen.spec.tsx | 41 +++ 24 files changed, 931 insertions(+), 625 deletions(-) create mode 100644 docs/static/img/offscreen/multiple_circles.png create mode 100644 package/android/cpp/rnskia-android/SkiaOpenGLHelper.h delete mode 100644 package/android/cpp/rnskia-android/SkiaOpenGLRenderer.cpp delete mode 100644 package/android/cpp/rnskia-android/SkiaOpenGLRenderer.h create mode 100644 package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.cpp create mode 100644 package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.h diff --git a/docs/static/img/offscreen/multiple_circles.png b/docs/static/img/offscreen/multiple_circles.png new file mode 100644 index 0000000000000000000000000000000000000000..7b6175be35aed51f9da03d836e064cbefb8de35e GIT binary patch literal 4273 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 literal 0 HcmV?d00001 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")); + }); });