From ab45d45bd415c19e0632aff80c8a37f1473d00bf Mon Sep 17 00:00:00 2001 From: umberto-sonnino Date: Fri, 21 Jul 2023 18:09:56 +0000 Subject: [PATCH] PLS Android Integration - Supports PLS & Skia renderer via a Kotlin flag! - Adds PLS in the build flow - Fix `premake` for Android adding in the architecture parameter to output the correct version - In `rive-android` the PLS repo name is stored in a secret variable but is effectively pointing to https://github.com/rive-app/rive-pls - Fixes a missing flag for logging in debug builds - Adds a new workflow for `rive-android` to build the AAR on push so we can easily test whether the build succeeds or fails before releasing - Some refactoring to rename some classes, methods, et al. - Some tests (definitely not enough of 'em here!) in Kotlin - Adds a custom runner to make sure we're running on a devices as PLS does not support emulators. Unfortunately, this means we can't run PLS View tests on the Emulator, which is a bit disappointing, but maybe we can figure that out at some point? Or stub it out if/when we *really* need it. - Other miscellaneous fixes Diffs= 9b75cdb96 Rive Renderer Android Integration (#5539) Co-authored-by: Umberto Sonnino --- .github/workflows/build.yml | 69 +++++ .github/workflows/release.yml | 8 + .github/workflows/tests.yml | 19 +- .rive_head | 2 +- CONTRIBUTING.md | 13 +- MEMORY_MANAGEMENT.md | 4 +- README.md | 74 ++++- .../app/rive/runtime/example/DeviceRunner.kt | 25 ++ .../example/RiveActivityLifecycleTest.kt | 80 +++-- .../example/RiveRendererActivityTest.kt | 50 +++ .../app/rive/runtime/example/TestUtils.kt | 101 ++++++ .../rive/runtime/example/LowLevelActivity.kt | 14 +- .../rive/runtime/example/SingleActivity.kt | 9 +- .../main/res/layout/single_rive_renderer.xml | 21 ++ .../runtime/kotlin/core/RiveFileLoadTest.kt | 15 + kotlin/src/main/cpp/CMakeLists.txt | 18 +- kotlin/src/main/cpp/Makefile | 3 + kotlin/src/main/cpp/build.rive.for.sh | 40 ++- .../include/helpers/android_skia_factory.hpp | 111 +++++++ .../main/cpp/include/helpers/egl_worker.hpp | 15 +- .../src/main/cpp/include/helpers/general.hpp | 37 ++- ..._thread_state.hpp => thread_state_egl.hpp} | 31 +- .../cpp/include/helpers/thread_state_pls.hpp | 37 +++ .../cpp/include/helpers/thread_state_skia.hpp | 37 +++ .../cpp/include/helpers/worker_thread.hpp | 30 +- kotlin/src/main/cpp/include/jni_refs.hpp | 36 +-- ...jni_renderer_skia.hpp => jni_renderer.hpp} | 33 +- .../main/cpp/include/models/worker_impl.hpp | 168 ++++++++++ .../cpp/src/bindings/bindings_artboard.cpp | 68 ++-- .../main/cpp/src/bindings/bindings_file.cpp | 31 +- .../main/cpp/src/bindings/bindings_helper.cpp | 18 +- .../main/cpp/src/bindings/bindings_init.cpp | 14 +- .../cpp/src/bindings/bindings_layer_state.cpp | 16 +- .../bindings_linear_animation_instance.cpp | 40 +-- .../cpp/src/bindings/bindings_renderer.cpp | 135 ++++++++ .../src/bindings/bindings_renderer_skia.cpp | 143 --------- .../bindings/bindings_rive_texture_view.cpp | 4 +- .../bindings_state_machine_input_instance.cpp | 18 +- .../bindings_state_machine_instance.cpp | 28 +- .../src/helpers/egl_share_thread_state.cpp | 259 ---------------- .../src/main/cpp/src/helpers/egl_worker.cpp | 24 +- kotlin/src/main/cpp/src/helpers/general.cpp | 183 +++-------- .../main/cpp/src/helpers/thread_state_egl.cpp | 139 +++++++++ .../main/cpp/src/helpers/thread_state_pls.cpp | 68 ++++ .../cpp/src/helpers/thread_state_skia.cpp | 120 ++++++++ .../main/cpp/src/helpers/worker_thread.cpp | 21 ++ kotlin/src/main/cpp/src/jni_refs.cpp | 62 ++-- .../src/main/cpp/src/models/jni_renderer.cpp | 168 ++++++++++ .../main/cpp/src/models/jni_renderer_skia.cpp | 291 ------------------ .../src/main/cpp/src/models/worker_impl.cpp | 140 +++++++++ kotlin/src/main/cpp/test/first_test.cpp | 2 +- .../rive/runtime/kotlin/RiveAnimationView.kt | 45 ++- .../runtime/kotlin/RiveArtboardRenderer.kt | 7 +- .../rive/runtime/kotlin/RiveInitializer.kt | 1 + .../rive/runtime/kotlin/RiveTextureView.kt | 7 +- .../java/app/rive/runtime/kotlin/core/File.kt | 7 +- .../rive/runtime/kotlin/core/RendererType.kt | 19 ++ .../java/app/rive/runtime/kotlin/core/Rive.kt | 19 +- .../{RendererSkia.kt => Renderer.kt} | 25 +- kotlin/src/main/res/values/attrs.xml | 5 + 60 files changed, 2028 insertions(+), 1199 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 app/src/androidTest/java/app/rive/runtime/example/DeviceRunner.kt create mode 100644 app/src/androidTest/java/app/rive/runtime/example/RiveRendererActivityTest.kt create mode 100644 app/src/androidTest/java/app/rive/runtime/example/TestUtils.kt create mode 100644 app/src/main/res/layout/single_rive_renderer.xml create mode 100644 kotlin/src/main/cpp/include/helpers/android_skia_factory.hpp rename kotlin/src/main/cpp/include/helpers/{egl_share_thread_state.hpp => thread_state_egl.hpp} (61%) create mode 100644 kotlin/src/main/cpp/include/helpers/thread_state_pls.hpp create mode 100644 kotlin/src/main/cpp/include/helpers/thread_state_skia.hpp rename kotlin/src/main/cpp/include/models/{jni_renderer_skia.hpp => jni_renderer.hpp} (62%) create mode 100644 kotlin/src/main/cpp/include/models/worker_impl.hpp create mode 100644 kotlin/src/main/cpp/src/bindings/bindings_renderer.cpp delete mode 100644 kotlin/src/main/cpp/src/bindings/bindings_renderer_skia.cpp delete mode 100644 kotlin/src/main/cpp/src/helpers/egl_share_thread_state.cpp create mode 100644 kotlin/src/main/cpp/src/helpers/thread_state_egl.cpp create mode 100644 kotlin/src/main/cpp/src/helpers/thread_state_pls.cpp create mode 100644 kotlin/src/main/cpp/src/helpers/thread_state_skia.cpp create mode 100644 kotlin/src/main/cpp/src/helpers/worker_thread.cpp create mode 100644 kotlin/src/main/cpp/src/models/jni_renderer.cpp delete mode 100644 kotlin/src/main/cpp/src/models/jni_renderer_skia.cpp create mode 100644 kotlin/src/main/cpp/src/models/worker_impl.cpp create mode 100644 kotlin/src/main/java/app/rive/runtime/kotlin/core/RendererType.kt rename kotlin/src/main/java/app/rive/runtime/kotlin/renderers/{RendererSkia.kt => Renderer.kt} (88%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..d4854f8d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,69 @@ +name: Build + +on: + push: + branches: [master] + +jobs: + publish: + name: Build the artifacts + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Check out code + uses: actions/checkout@v3 + with: + submodules: true + token: ${{ secrets.PAT_GITHUB }} + + - name: Rive Renderer Repo + uses: actions/checkout@v3 + with: + submodules: true + repository: ${{ secrets.RIVE_RENDERER_REPO }} + token: ${{ secrets.PAT_GITHUB }} + path: ./submodules/pls + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-region: us-west-2 + role-to-assume: ${{ secrets.ACTIONS_ROLE }} + + - name: Update Java + uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: "17" + + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + + - name: Install NDK & tools + # Starts from: pwd => /home/runner/work/rive/rive + # ANDROID_HOME => /usr/local/lib/android/sdk + run: | + set -x + echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install 'build-tools;30.0.3' platform-tools --sdk_root=${ANDROID_SDK_ROOT} + cd ${ANDROID_HOME} + wget -q https://dl.google.com/android/repository/android-ndk-r25b-linux.zip + unzip -q android-ndk-r25b-linux.zip + echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install 'cmake;3.22.1' --channel=0 --sdk_root=${ANDROID_SDK_ROOT} + + - name: Installing pre-requisites + run: | + set -x + # Install some dependencies & premake5 + sudo apt update && sudo apt-get -y install build-essential cmake clang g++ libgl1-mesa-dev libvorbis-dev libvpx-dev ninja-build + wget -q https://github.com/premake/premake-core/releases/download/v5.0.0-alpha15/premake-5.0.0-alpha15-linux.tar.gz + tar -xf premake-5.0.0-alpha15-linux.tar.gz + sudo mv premake5 /usr/local/bin + + - name: Build Android + env: + # ANDROID_SDK_ROOT has been in the env by 'setup-android' above + # and is => /usr/local/lib/android/sdk + NDK_PATH: ${{ env.ANDROID_SDK_ROOT }}/android-ndk-r25b + run: ./gradlew kotlin:assembleRelease diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8084b71b..66601a51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,14 @@ jobs: with: submodules: true token: ${{ secrets.PAT_GITHUB }} + + - name: Rive Renderer Repo + uses: actions/checkout@v3 + with: + submodules: true + repository: ${{ secrets.RIVE_RENDERER_REPO }} + token: ${{ secrets.PAT_GITHUB }} + path: ./submodules/pls - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cccbd407..e1efa063 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,16 +14,25 @@ jobs: id-token: write contents: read steps: - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-region: us-west-2 - role-to-assume: ${{ secrets.ACTIONS_ROLE }} - name: Checkout Code uses: actions/checkout@v3 with: submodules: true token: ${{ secrets.PAT_GITHUB }} + - name: Rive Renderer Repo + uses: actions/checkout@v3 + with: + submodules: true + repository: ${{ secrets.RIVE_RENDERER_REPO }} + token: ${{ secrets.PAT_GITHUB }} + path: ./submodules/pls + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-region: us-west-2 + role-to-assume: ${{ secrets.ACTIONS_ROLE }} + - name: Update Java uses: actions/setup-java@v2 diff --git a/.rive_head b/.rive_head index ee132d10..c23dda52 100644 --- a/.rive_head +++ b/.rive_head @@ -1 +1 @@ -420d27a5bc052e39f9592f1561070ad509589ed1 +9b75cdb96c2e7dc1f15551cbbf3e0f320759c078 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1d0aeeee..084c6f8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,17 +13,16 @@ The underlying [C++ runtime](https://github.com/rive-app/rive-cpp) is mapped to the `app.rive.runtime.kotlin.core` namespace. These allow more fine grained control for more complex animation loops. Our high level views are simply built on top of this. +#### `/cpp` && `/submodules` + +This runtime is built on top of our [C++ runtime](https://github.com/rive-app/rive-cpp). This is +included as a submodule in `/submodules`. The `/cpp` folder contains the C++ side of our Android bindings and is located at `/kotlin/src/main/cpp` + ### `/app` Multiple sample activities can be found here, this can be a useful reference for getting started with using the runtimes. -### `/cpp` && `/submodules` - -This runtime is built on top of our [C++ runtime](https://github.com/rive-app/rive-cpp). This is -included as a submodule in `/submodules`. The `/cpp` folder contains the C++ side of our bindings -into android. - ## Development workflow ### Running Locally @@ -39,7 +38,7 @@ run the test suite: - Open the project in Android Studio - Select the "Project" view (upper-right corner) -- Right-click on kotlin/src/androidTest and select "Run All Tests" +- Right-click on `kotlin/src/androidTest` and select "Run All Tests" ### Building `.so` files diff --git a/MEMORY_MANAGEMENT.md b/MEMORY_MANAGEMENT.md index 83bc1331..36fb97c9 100644 --- a/MEMORY_MANAGEMENT.md +++ b/MEMORY_MANAGEMENT.md @@ -46,7 +46,7 @@ The diagram below illustrates the class hierarchy for `RiveAnimationView` highli |RiveTextureView | +----------------------+ |abstract | - |renderer: RendererSkia| + |renderer: Renderer | +----------+-----------+ | | @@ -85,7 +85,7 @@ which in turn will cascade the `release()` call on its own dependents. has | +--------------------+ +------------+ +------+-----+ -|RiveArtboardRenderer+-is-a->|RendererSkia+-is-a->|NativeObject| +|RiveArtboardRenderer+-is-a->| Renderer +-is-a->|NativeObject| +---------+----------+ +------------+ +------^-----+ | | has | diff --git a/README.md b/README.md index 1ab51f38..d49bb462 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # Rive Android -![Rive hero image](https://rive-app.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fff44ed5f-1eea-4154-81ef-84547e61c3fd%2Frive_notion.png?table=block&id=f198cab2-c0bc-4ce8-970c-42220379bcf3&spaceId=9c949665-9ad9-445f-b9c4-5ee204f8b60c&width=2000&userId=&cache=v2) +![Rive hero image](https://cdn.rive.app/rive_logo_dark_bg.png) An Android runtime library for [Rive](https://rive.app). @@ -14,12 +14,13 @@ the [Maven](https://search.maven.org/artifact/app.rive/rive-android) repository. ## Table of contents -- :star: [Rive Overview](#rive-overview) +- โญ๏ธ [Rive Overview](#rive-overview) - ๐Ÿš€ [Getting Started & API docs](#getting-started) -- :mag: [Supported Versions](#supported-versions) -- :books: [Examples](#examples) +- ๐Ÿ” [Supported Versions](#supported-versions) +- ๐Ÿงช [Experimental Features](#experimental-features) +- ๐Ÿ“š [Examples](#examples) - ๐Ÿ‘จโ€๐Ÿ’ป [Contributing](#contributing) -- :question: [Issues](#issues) +- โ“ [Issues](#issues) ## Rive Overview @@ -28,9 +29,9 @@ create and run interactive animations anywhere. Designers and developers use our editor to create motion graphics that respond to different states and user inputs. Our lightweight open-source runtime libraries allow them to load their animations into apps, games, and websites. -:house_with_garden: [Homepage](https://rive.app/) +๐Ÿก [Homepage](https://rive.app/) -:blue_book: [General help docs](https://help.rive.app/) +๐Ÿ“˜ [General help docs](https://help.rive.app/) ๐Ÿ›  [Learning Rive](https://rive.app/learn-rive/) @@ -58,6 +59,32 @@ showcases a number of ways to manipulate Rives, including: - Utilizing a low-level API to build a render loop for more control over scenes - ...and more! +## Experimental Features + +The Rive renderer is available _experimentally_ in `7.0.0`. + +Read more about the Rive Renderer [here](https://rive.app/renderer). + +Please note that we don't recommend it for production builds yet as it's not fully compatible with the current Rive feature set (i.e., rendering images isn't supported yet). You might also encounter incompatibilities with specific devices - for example, the new renderer won't work on emulators just yet but only on physical devices. + +Your feedback is greatly appreciated during this stage and we'd love to hear from you! + +To use the new Rive renderer you can specify the parameter in XML: + +```xml + +``` + +Alternatively, specify the renderer when initializing Rive: + +```kotlin +Rive.init(applicationContext, defaultRenderer = RendererType.Rive) +``` + +This default value can still be overriden via XML. + ## Contributing We love contributions! Check out our [contributing docs](./CONTRIBUTING.md) to get more details into @@ -69,3 +96,36 @@ Have an issue with using the runtime, or want to suggest a feature/API to help m life better? Log an issue in our [issues](https://github.com/rive-app/rive-android/issues) tab! You can also browse older issues and discussion threads there to see solutions that may have worked for common problems. + +### Known Issues + +After `rive-android:6.0.0`, CMake is building the library, and you might run into the following error when `rive-android` is used alongside other native libraries: + +```shell +Execution failed for task ':app:mergeDebugNativeLibs'. +> A failure occurred while executing com.android.build.gradle.internal.tasks.MergeNativeLibsTask$MergeNativeLibsTaskWorkAction + > 2 files found with path 'lib/arm64-v8a/libc++_shared.so' from inputs: +โ€ฆ +``` + +You can fix this by adding this in your `build.gradle`: + +```gradle +android { + โ€ฆ + packagingOptions { + pickFirst "lib/x86/libc++_shared.so" + pickFirst "lib/x86_64/libc++_shared.so" + pickFirst "lib/armeabi-v7a/libc++_shared.so" + pickFirst "lib/arm64-v8a/libc++_shared.so" + } + โ€ฆ +} +``` + +### Breaking Changes + +#### **7.0.0** + +- `RendererSkia` is now called to `Renderer` + diff --git a/app/src/androidTest/java/app/rive/runtime/example/DeviceRunner.kt b/app/src/androidTest/java/app/rive/runtime/example/DeviceRunner.kt new file mode 100644 index 00000000..e4d8533c --- /dev/null +++ b/app/src/androidTest/java/app/rive/runtime/example/DeviceRunner.kt @@ -0,0 +1,25 @@ +import org.junit.Assume.assumeFalse +import org.junit.runner.notification.RunNotifier +import org.junit.runners.BlockJUnit4ClassRunner +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.InitializationError +import org.junit.runners.model.Statement + +annotation class OnDevice + +class RunOnDevice(testClass: Class<*>) : BlockJUnit4ClassRunner(testClass) { + @Throws(InitializationError::class) + constructor(testClass: Class<*>, notifier: RunNotifier) : this(testClass) + + override fun methodBlock(method: FrameworkMethod): Statement { + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + // Check whether this is an emulator. If it is skip the test. + assumeFalse(TestUtils.isProbablyRunningOnEmulator) + // Continue with the usual test + method.invokeExplosively(testClass.javaClass.newInstance()) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/app/rive/runtime/example/RiveActivityLifecycleTest.kt b/app/src/androidTest/java/app/rive/runtime/example/RiveActivityLifecycleTest.kt index 68103c4f..ebbcfaef 100644 --- a/app/src/androidTest/java/app/rive/runtime/example/RiveActivityLifecycleTest.kt +++ b/app/src/androidTest/java/app/rive/runtime/example/RiveActivityLifecycleTest.kt @@ -1,54 +1,50 @@ package app.rive.runtime.example -import android.content.Context -import android.content.Intent +import TestUtils.Companion.waitUntil import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import app.rive.runtime.kotlin.RiveAnimationView -import app.rive.runtime.kotlin.controllers.ControllerStateManagement import app.rive.runtime.kotlin.controllers.RiveFileController -import app.rive.runtime.kotlin.core.Rive +import app.rive.runtime.kotlin.core.File +import app.rive.runtime.kotlin.core.RendererType +import app.rive.runtime.kotlin.core.errors.RiveException import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import java.util.concurrent.TimeoutException -import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds -@ControllerStateManagement @RunWith(AndroidJUnit4::class) class RiveActivityLifecycleTest { - /** Temporarily copy-pasted from TestUtils as we iterate on these tests */ - private val context: Context by lazy { - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("app.rive.runtime.example", appContext.packageName) - Rive.init(appContext) - appContext - } - - private fun waitUntil( - atMost: Duration, - condition: () -> Boolean - ) { - val maxTime = atMost.inWholeMilliseconds - - val interval: Long = 50 - var elapsed: Long = 0 - do { - elapsed += interval - Thread.sleep(interval) - - if (elapsed > maxTime) { - throw TimeoutException("Took too long.") - } - } while (!condition()) + @Test + fun activityWithRiveView() { + val activityScenario = ActivityScenario.launch(SingleActivity::class.java); + lateinit var riveView: RiveAnimationView + lateinit var controller: RiveFileController + // Start the Activity. + activityScenario.onActivity { + riveView = it.findViewById(R.id.rive_single) + controller = riveView.controller + assertEquals(2, controller.refCount) + assertTrue(controller.isActive) + assertNotNull(controller.file) + assertNotNull(controller.activeArtboard) + // Defaults to Skia. + assertEquals(RendererType.Skia, riveView.rendererAttributes.rendererType) + assertEquals(riveView.rendererAttributes.rendererType, controller.file?.rendererType) + } + // Close it down. + activityScenario.close() + // Background thread deallocates asynchronously. + waitUntil(500.milliseconds) { controller.refCount == 0 } + assertFalse(controller.isActive) + assertNull(controller.file) + assertNull(controller.activeArtboard) } @Test - fun activityWithRiveView() { + fun activityWithRiveViewSetsWrongFileType() { val activityScenario = ActivityScenario.launch(SingleActivity::class.java); lateinit var riveView: RiveAnimationView lateinit var controller: RiveFileController @@ -61,6 +57,24 @@ class RiveActivityLifecycleTest { assertTrue(controller.isActive) assertNotNull(controller.file) assertNotNull(controller.activeArtboard) + // Defaults to Skia. + assertEquals(RendererType.Skia, riveView.rendererAttributes.rendererType) + assertEquals(riveView.rendererAttributes.rendererType, controller.file?.rendererType) + // Set wrong file type throws! + val customRendererFile = File( + it.resources.openRawResource(R.raw.off_road_car_blog).readBytes(), + RendererType.Rive + ) + assertEquals(RendererType.Rive, customRendererFile.rendererType) + val wrongFileTypeException = assertThrows(RiveException::class.java) { + // Boom! + riveView.setRiveFile(customRendererFile) + } + assertEquals( + "Incompatible Renderer types: file initialized with ${customRendererFile.rendererType.name}" + + " but View is set up for ${riveView.rendererAttributes.rendererType.name}", + wrongFileTypeException.message + ) } // Close it down. activityScenario.close() diff --git a/app/src/androidTest/java/app/rive/runtime/example/RiveRendererActivityTest.kt b/app/src/androidTest/java/app/rive/runtime/example/RiveRendererActivityTest.kt new file mode 100644 index 00000000..a151e8ef --- /dev/null +++ b/app/src/androidTest/java/app/rive/runtime/example/RiveRendererActivityTest.kt @@ -0,0 +1,50 @@ +package app.rive.runtime.example + +import RunOnDevice +import TestUtils.Companion.waitUntil +import android.content.Intent +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import app.rive.runtime.kotlin.RiveAnimationView +import app.rive.runtime.kotlin.controllers.RiveFileController +import app.rive.runtime.kotlin.core.RendererType +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.time.Duration.Companion.milliseconds + + +// Don't run on the Emulator: Rive renderer can't initialize there. +@RunWith(RunOnDevice::class) +class RiveRendererActivityTest { + @Test + fun activityWithRiveRenderer() { + val intent = + Intent(ApplicationProvider.getApplicationContext(), SingleActivity::class.java).apply { + putExtra("renderer", "Rive") + } + val activityScenario = ActivityScenario.launch(intent); + lateinit var riveView: RiveAnimationView + lateinit var controller: RiveFileController + // Start the Activity. + activityScenario.onActivity { + riveView = it.findViewById(R.id.rive_single) + controller = riveView.controller + + assertEquals(2, controller.refCount) + assertTrue(controller.isActive) + assertNotNull(controller.file) + assertNotNull(controller.activeArtboard) + // Specified Rive renderer. + assertEquals(RendererType.Rive, riveView.rendererAttributes.rendererType) + assertEquals(riveView.rendererAttributes.rendererType, controller.file?.rendererType) + } + // Close it down. + activityScenario.close() + // Background thread deallocates asynchronously. + waitUntil(500.milliseconds) { controller.refCount == 0 } + assertFalse(controller.isActive) + assertNull(controller.file) + assertNull(controller.activeArtboard) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/app/rive/runtime/example/TestUtils.kt b/app/src/androidTest/java/app/rive/runtime/example/TestUtils.kt new file mode 100644 index 00000000..ff8d4fa1 --- /dev/null +++ b/app/src/androidTest/java/app/rive/runtime/example/TestUtils.kt @@ -0,0 +1,101 @@ +import android.annotation.SuppressLint +import android.os.Build +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader +import java.lang.reflect.Method +import java.util.concurrent.TimeoutException +import kotlin.time.Duration + +class TestUtils { + companion object { + + fun waitUntil( + atMost: Duration, + condition: () -> Boolean + ) { + val maxTime = atMost.inWholeMilliseconds + + val interval: Long = 50 + var elapsed: Long = 0 + do { + elapsed += interval + Thread.sleep(interval) + + if (elapsed > maxTime) { + throw TimeoutException("Took too long.") + } + } while (!condition()) + + } + + + // Helper function to check whether it's running on an emulator + // Found on: https://stackoverflow.com/a/21505193 + val isProbablyRunningOnEmulator: Boolean by lazy { + // Android SDK emulator + return@lazy ((Build.MANUFACTURER == "Google" && Build.BRAND == "google" && + ((Build.FINGERPRINT.startsWith("google/sdk_gphone_") + && Build.FINGERPRINT.endsWith(":user/release-keys") + && Build.PRODUCT.startsWith("sdk_gphone_") + && Build.MODEL.startsWith("sdk_gphone_")) + //alternative + || (Build.FINGERPRINT.startsWith("google/sdk_gphone64_") + && (Build.FINGERPRINT.endsWith(":userdebug/dev-keys") || Build.FINGERPRINT.endsWith( + ":user/release-keys" + )) + && Build.PRODUCT.startsWith("sdk_gphone64_") + && Build.MODEL.startsWith("sdk_gphone64_")))) + // + || Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + //bluestacks + || "QC_Reference_Phone" == Build.BOARD && !"Xiaomi".equals( + Build.MANUFACTURER, + ignoreCase = true + ) + //bluestacks + || Build.MANUFACTURER.contains("Genymotion") + || Build.HOST.startsWith("Build") + //MSI App Player + || Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic") + || Build.PRODUCT == "google_sdk" + // another Android SDK emulator check + || SystemProperties.getProp("ro.kernel.qemu") == "1") + } + } + + object SystemProperties { + private var failedUsingReflection = false + private var getPropMethod: Method? = null + + @SuppressLint("PrivateApi") + fun getProp(propName: String, defaultResult: String = ""): String { + if (!failedUsingReflection) try { + if (getPropMethod == null) { + val clazz = Class.forName("android.os.SystemProperties") + getPropMethod = clazz.getMethod("get", String::class.java, String::class.java) + } + return getPropMethod!!.invoke(null, propName, defaultResult) as String? + ?: defaultResult + } catch (e: Exception) { + getPropMethod = null + failedUsingReflection = true + } + var process: Process? = null + try { + process = Runtime.getRuntime().exec("getprop \"$propName\" \"$defaultResult\"") + val reader = BufferedReader(InputStreamReader(process.inputStream)) + return reader.readLine() + } catch (_: IOException) { + } finally { + process?.destroy() + } + return defaultResult + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/rive/runtime/example/LowLevelActivity.kt b/app/src/main/java/app/rive/runtime/example/LowLevelActivity.kt index bc1bc5c6..f111b2fb 100644 --- a/app/src/main/java/app/rive/runtime/example/LowLevelActivity.kt +++ b/app/src/main/java/app/rive/runtime/example/LowLevelActivity.kt @@ -13,7 +13,7 @@ import app.rive.runtime.kotlin.core.Artboard import app.rive.runtime.kotlin.core.File import app.rive.runtime.kotlin.core.Fit import app.rive.runtime.kotlin.core.LinearAnimationInstance -import app.rive.runtime.kotlin.renderers.RendererSkia +import app.rive.runtime.kotlin.renderers.Renderer class LowLevelActivity : AppCompatActivity() { @@ -42,7 +42,7 @@ class LowLevelRiveView(context: Context) : RiveTextureView(context) { private lateinit var artboard: Artboard private lateinit var animationInstance: LinearAnimationInstance - private fun setupFile(skRenderer: RendererSkia) { + private fun setupFile(renderer: Renderer) { val resource = resources.openRawResource(R.raw.basketball) // Keep a reference to the file to keep resources around. file = File(resource.readBytes()) @@ -51,7 +51,7 @@ class LowLevelRiveView(context: Context) : RiveTextureView(context) { animationInstance = artboard.firstAnimation // This will be deleted with its dependents. - skRenderer.dependencies.add(file) + renderer.dependencies.add(file) } override fun createObserver(): LifecycleObserver { @@ -66,8 +66,8 @@ class LowLevelRiveView(context: Context) : RiveTextureView(context) { } } - override fun createRenderer(): RendererSkia { - val skRenderer = object : RendererSkia() { + override fun createRenderer(): Renderer { + val renderer = object : Renderer() { override fun draw() { artboard.let { @@ -87,8 +87,8 @@ class LowLevelRiveView(context: Context) : RiveTextureView(context) { } } // Call setup file only once we created the renderer. - setupFile(skRenderer) - return skRenderer + setupFile(renderer) + return renderer } } \ No newline at end of file diff --git a/app/src/main/java/app/rive/runtime/example/SingleActivity.kt b/app/src/main/java/app/rive/runtime/example/SingleActivity.kt index d5fa53b7..e0545fe2 100644 --- a/app/src/main/java/app/rive/runtime/example/SingleActivity.kt +++ b/app/src/main/java/app/rive/runtime/example/SingleActivity.kt @@ -4,9 +4,14 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity class SingleActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.single) + val isRiveRenderer = intent.getStringExtra("renderer") == "Rive" + setContentView( + if (isRiveRenderer) + R.layout.single_rive_renderer + else + R.layout.single + ) } } diff --git a/app/src/main/res/layout/single_rive_renderer.xml b/app/src/main/res/layout/single_rive_renderer.xml new file mode 100644 index 00000000..a4022595 --- /dev/null +++ b/app/src/main/res/layout/single_rive_renderer.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/kotlin/src/androidTest/java/app/rive/runtime/kotlin/core/RiveFileLoadTest.kt b/kotlin/src/androidTest/java/app/rive/runtime/kotlin/core/RiveFileLoadTest.kt index 8e63c9b1..27f1955e 100644 --- a/kotlin/src/androidTest/java/app/rive/runtime/kotlin/core/RiveFileLoadTest.kt +++ b/kotlin/src/androidTest/java/app/rive/runtime/kotlin/core/RiveFileLoadTest.kt @@ -37,4 +37,19 @@ class RiveFileLoadTest { val file = File(appContext.resources.openRawResource(R.raw.off_road_car_blog).readBytes()) assertEquals(5, file.firstArtboard.animationCount) } + + @Test + fun loadFileWithRendererType() { + val skiaFile = + File(appContext.resources.openRawResource(R.raw.off_road_car_blog).readBytes()) + assertEquals(5, skiaFile.firstArtboard.animationCount) + assertEquals(RendererType.Skia, skiaFile.rendererType) + + val customRendererFile = File( + appContext.resources.openRawResource(R.raw.off_road_car_blog).readBytes(), + RendererType.Rive + ) + assertEquals(5, customRendererFile.firstArtboard.animationCount) + assertEquals(RendererType.Rive, customRendererFile.rendererType) + } } \ No newline at end of file diff --git a/kotlin/src/main/cpp/CMakeLists.txt b/kotlin/src/main/cpp/CMakeLists.txt index fd7a8a96..39ae322f 100644 --- a/kotlin/src/main/cpp/CMakeLists.txt +++ b/kotlin/src/main/cpp/CMakeLists.txt @@ -14,15 +14,17 @@ set(CMAKE_VERBOSE_MAKEFILE ON) # Define RIVE_RUNTIME_DIR as an env variable so that `build.rive.for.sh` can also use it. if(EXISTS "${PROJECT_SOURCE_DIR}/../../../../submodules/rive-cpp") - set(ENV{RIVE_RUNTIME_DIR} "${PROJECT_SOURCE_DIR}/../../../../submodules/rive-cpp") + set(PACKAGES_DIR "${PROJECT_SOURCE_DIR}/../../../../") + set(ENV{RIVE_RUNTIME_DIR} "${PACKAGES_DIR}/submodules/rive-cpp") + set(PLS_DIR "${PACKAGES_DIR}/submodules/pls") else() - set(ENV{RIVE_RUNTIME_DIR} "${PROJECT_SOURCE_DIR}/../../../../../runtime") + set(PACKAGES_DIR "${PROJECT_SOURCE_DIR}/../../../../..") + set(ENV{RIVE_RUNTIME_DIR} "${PACKAGES_DIR}/runtime") + set(PLS_DIR "${PACKAGES_DIR}/pls") endif() -message("LOOKED FOR: '$ENV{RIVE_RUNTIME_DIR}'") message("BUILD TYPE: ${CMAKE_BUILD_TYPE}") - #[[ CMake adds these flags depending on CMAKE_BUILD_TYPE 1. Release: `-O3 -DNDEBUG` 2. Debug: `-O0 -g` @@ -31,11 +33,13 @@ message("BUILD TYPE: ${CMAKE_BUILD_TYPE}") ]] add_definitions(-DSK_GL) +add_definitions(-DRIVE_GLES) if (CMAKE_BUILD_TYPE STREQUAL "Debug") set(SKIA_DIR_NAME "skia_debug") set(DEBUG_FLAG "-d") set(CONFIG "debug") + add_definitions(-DDEBUG) endif () if (CMAKE_BUILD_TYPE STREQUAL "Release" OR @@ -43,6 +47,7 @@ if (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") set(SKIA_DIR_NAME "skia") set(CONFIG "release") + add_definitions(-DNDEBUG) endif () message("Skia location: ${SKIA_DIR_NAME}") @@ -62,6 +67,7 @@ endif() include_directories( include $ENV{RIVE_RUNTIME_DIR}/include + ${PLS_DIR}/include $ENV{RIVE_RUNTIME_DIR}/renderer/library/include $ENV{RIVE_RUNTIME_DIR}/skia/dependencies/${SKIA_DIR_NAME}/ $ENV{RIVE_RUNTIME_DIR}/skia/dependencies/${SKIA_DIR_NAME}/include/core @@ -71,7 +77,7 @@ include_directories( $ENV{RIVE_RUNTIME_DIR}/skia/renderer/include ) -file(GLOB SOURCES +file(GLOB SOURCES CONFIGURE_DEPENDS src/*.cpp src/bindings/*.cpp src/helpers/*.cpp @@ -83,6 +89,7 @@ add_library(rive-android SHARED ${SOURCES}) set(static_libs rive rive_skia_renderer + rive_pls_renderer skia ) @@ -105,6 +112,7 @@ find_library(gles-fdsalib GLESv3fdas) target_link_libraries(rive-android rive-lib rive_skia_renderer-lib + rive_pls_renderer-lib skia-lib ${log-lib} diff --git a/kotlin/src/main/cpp/Makefile b/kotlin/src/main/cpp/Makefile index b2eb92d7..63f41f1f 100644 --- a/kotlin/src/main/cpp/Makefile +++ b/kotlin/src/main/cpp/Makefile @@ -22,6 +22,7 @@ DEPS=$(O_FILES) \ $(BUILD_DIR)/librive.a \ $(BUILD_DIR)/libskia.a \ $(BUILD_DIR)/librive_skia_renderer.a \ + $(BUILD_DIR)/librive_pls_renderer.a \ LIBS=-llog -landroid -lEGL -lGLESv3 @@ -42,6 +43,7 @@ $(ODIR)/%.o: $(SRC_DIR)/%.cpp -DSK_GL \ -Iinclude \ -I$(RIVE_RUNTIME_DIR)/include \ + -I$(RIVE_RUNTIME_DIR)/../pls/include \ -I$(RIVE_RUNTIME_DIR)/renderer/library/include \ -I${RIVE_RUNTIME_DIR}/skia/dependencies/$(SKIA_DIR_NAME)/ \ -I${RIVE_RUNTIME_DIR}/skia/dependencies/$(SKIA_DIR_NAME)/include/core \ @@ -57,5 +59,6 @@ clean: rm -f $(BUILD_DIR)/librive.a rm -f $(BUILD_DIR)/libskia.a rm -f $(BUILD_DIR)/librive_skia_renderer.a + rm -f $(BUILD_DIR)/librive_pls_renderer.a rm -f $(BUILD_DIR)/libc++_static.a \ No newline at end of file diff --git a/kotlin/src/main/cpp/build.rive.for.sh b/kotlin/src/main/cpp/build.rive.for.sh index 40d23ae4..d10d9836 100755 --- a/kotlin/src/main/cpp/build.rive.for.sh +++ b/kotlin/src/main/cpp/build.rive.for.sh @@ -124,39 +124,47 @@ API=21 SKIA_ARCH= buildFor() { + if ${NEEDS_CLEAN}; then + echo "Cleaning everything!" + # Skia + pushd "$RIVE_RUNTIME_DIR"/skia/dependencies/"$SKIA_DIR_NAME" + bin/gn clean ./out/"$CONFIG"/"$SKIA_ARCH" + popd + # PLS + pushd "$RIVE_RUNTIME_DIR"/../pls/out + make config=$CONFIG clean + popd + # rive_skia_renderer + pushd "$RIVE_RUNTIME_DIR"/skia/renderer + ./build.sh -p android."$SKIA_ARCH" clean + popd + # Android lib + make clean + fi + # Build skia pushd "$RIVE_RUNTIME_DIR"/skia/dependencies ./make_skia_android.sh "$SKIA_ARCH" "$CONFIG" popd # Build librive_pls_renderer (internally builds librive) - # pushd "$RIVE_RUNTIME_DIR"/../pls/out - # premake5 --os=android --arch=$SKIA_ARCH gmake2 - # if ${NEEDS_CLEAN}; then - # make config=$CONFIG clean - # fi - # make config=$CONFIG -j20 rive rive_pls_renderer - # popd + pushd "$RIVE_RUNTIME_DIR"/../pls/out + premake5 --scripts="$RIVE_RUNTIME_DIR"/build --os=android --arch="$SKIA_ARCH" gmake2 + make config=$CONFIG -j20 rive rive_pls_renderer + popd # Build librive_skia_renderer (internally builds librive) pushd "$RIVE_RUNTIME_DIR"/skia/renderer - if ${NEEDS_CLEAN}; then - ./build.sh -p android."$SKIA_ARCH" "$CONFIG" clean - fi ./build.sh -p android."$SKIA_ARCH" "$CONFIG" popd # Cleanup our android build location. mkdir -p "$BUILD_DIR" - if ${NEEDS_CLEAN}; then - # echo 'cleaning!' - make clean - fi # copy in newly built rive/skia/skia_renderer files. cp "$RIVE_RUNTIME_DIR"/build/android/"$SKIA_ARCH"/bin/"${CONFIG}"/librive.a "$BUILD_DIR" cp "$RIVE_RUNTIME_DIR"/skia/renderer/build/android/"$SKIA_ARCH"/bin/${CONFIG}/librive_skia_renderer.a "$BUILD_DIR" - # cp "$RIVE_RUNTIME_DIR/../pls/out/android_$CONFIG/librive_pls_renderer.a" "$BUILD_DIR" + cp "$RIVE_RUNTIME_DIR/../pls/out/android/$SKIA_ARCH/$CONFIG/lib/librive_pls_renderer.a" "$BUILD_DIR" cp "$RIVE_RUNTIME_DIR"/skia/dependencies/"$SKIA_DIR_NAME"/out/"${CONFIG}"/"$SKIA_ARCH"/libskia.a "$BUILD_DIR" if ! ${ONLY_DEPS}; then @@ -169,7 +177,7 @@ buildFor() { mkdir -p "$BUILD_DIR"/obj make -j20 - JNI_DEST=../kotlin/src/main/jniLibs/$ARCH_NAME + JNI_DEST=../jniLibs/$ARCH_NAME mkdir -p "$JNI_DEST" cp "$BUILD_DIR"/libjnirivebridge.so "$JNI_DEST" } diff --git a/kotlin/src/main/cpp/include/helpers/android_skia_factory.hpp b/kotlin/src/main/cpp/include/helpers/android_skia_factory.hpp new file mode 100644 index 00000000..b4591057 --- /dev/null +++ b/kotlin/src/main/cpp/include/helpers/android_skia_factory.hpp @@ -0,0 +1,111 @@ +// +// Created by Umberto Sonnino on 7/18/23. +// +#ifndef RIVE_ANDROID_ANDROID_SKIA_FACTORY_HPP +#define RIVE_ANDROID_ANDROID_SKIA_FACTORY_HPP + +#include + +#include "skia_factory.hpp" +#include "helpers/general.hpp" + +class AndroidSkiaFactory : public rive::SkiaFactory +{ +public: + std::vector platformDecode(rive::Span span, + rive::SkiaFactory::ImageInfo* info) override + { + auto env = rive_android::GetJNIEnv(); + std::vector pixels; + + jclass cls = env->FindClass("app/rive/runtime/kotlin/core/Decoder"); + if (!cls) + { + LOGE("can't find class 'app/rive/runtime/kotlin/core/Decoder'"); + return pixels; + } + + jmethodID method = env->GetStaticMethodID(cls, "decodeToPixels", "([B)[I"); + if (!method) + { + LOGE("can't find static method decodeToPixels"); + return pixels; + } + + jbyteArray encoded = env->NewByteArray(rive_android::SizeTTOInt(span.size())); + if (!encoded) + { + LOGE("failed to allocate NewByteArray"); + return pixels; + } + + env->SetByteArrayRegion(encoded, + 0, + rive_android::SizeTTOInt(span.size()), + (jbyte*)span.data()); + auto jpixels = (jintArray)env->CallStaticObjectMethod(cls, method, encoded); + env->DeleteLocalRef(encoded); // no longer need encoded + + // At ths point, we have the decode results. Now we just need to convert + // it into the form we need (ImageInfo + premul pixels) + + size_t arrayCount = env->GetArrayLength(jpixels); + if (arrayCount < 2) + { + LOGE("bad array length (unexpected)"); + return pixels; + } + + int* rawPixels = env->GetIntArrayElements(jpixels, nullptr); + const uint32_t width = rawPixels[0]; + const uint32_t height = rawPixels[1]; + const size_t pixelCount = (size_t)width * height; + if (pixelCount == 0) + { + LOGE("don't support empty images (zero dimension)"); + return pixels; + } + if (2 + pixelCount < arrayCount) + { + LOGE("not enough elements in pixel array"); + return pixels; + } + + auto div255 = [](unsigned value) { return (value + 128) * 257 >> 16; }; + + pixels.resize(pixelCount * 4); + uint8_t* bytes = pixels.data(); + bool isOpaque = true; + for (size_t i = 0; i < pixelCount; ++i) + { + uint32_t p = rawPixels[2 + i]; + unsigned a = (p >> 24) & 0xFF; + unsigned r = (p >> 16) & 0xFF; + unsigned g = (p >> 8) & 0xFF; + unsigned b = (p >> 0) & 0xFF; + // convert to premul as needed + if (a != 255) + { + r = div255(r * a); + g = div255(g * a); + b = div255(b * a); + isOpaque = false; + } + bytes[0] = r; + bytes[1] = g; + bytes[2] = b; + bytes[3] = a; + bytes += 4; + } + env->ReleaseIntArrayElements(jpixels, rawPixels, 0); + + info->rowBytes = width * 4; // we're snug + info->width = width; + info->height = height; + info->colorType = ColorType::rgba; + info->alphaType = isOpaque ? AlphaType::opaque : AlphaType::premul; + return pixels; + } +}; + +#endif // RIVE_ANDROID_ANDROID_SKIA_FACTORY_HPP diff --git a/kotlin/src/main/cpp/include/helpers/egl_worker.hpp b/kotlin/src/main/cpp/include/helpers/egl_worker.hpp index adfac733..c84fddeb 100644 --- a/kotlin/src/main/cpp/include/helpers/egl_worker.hpp +++ b/kotlin/src/main/cpp/include/helpers/egl_worker.hpp @@ -1,20 +1,25 @@ -#pragma once +#ifndef RIVE_ANDROID_EGL_WORKER_HPP +#define RIVE_ANDROID_EGL_WORKER_HPP -#include "helpers/egl_share_thread_state.hpp" +#include "helpers/thread_state_egl.hpp" #include "helpers/worker_thread.hpp" #include "rive/refcnt.hpp" namespace rive_android { -class EGLWorker : public WorkerThread, public rive::RefCnt +class EGLWorker : public WorkerThread, public rive::RefCnt { public: - static rive::rcp Current(); + static rive::rcp Current(const RendererType); private: friend class rive::RefCnt; - EGLWorker() : WorkerThread("EGLWorker", Affinity::None) {} + EGLWorker(const RendererType rendererType) : + WorkerThread("EGLWorker", Affinity::None, rendererType) + {} ~EGLWorker(); }; } // namespace rive_android + +#endif // RIVE_ANDROID_EGL_WORKER_HPP \ No newline at end of file diff --git a/kotlin/src/main/cpp/include/helpers/general.hpp b/kotlin/src/main/cpp/include/helpers/general.hpp index 3296338d..163afc23 100644 --- a/kotlin/src/main/cpp/include/helpers/general.hpp +++ b/kotlin/src/main/cpp/include/helpers/general.hpp @@ -8,39 +8,50 @@ // Print only on debug builds. #if defined(DEBUG) || defined(LOG) -#define LOG_TAG (std::string(__FILE__ ":") + std::to_string(__LINE__)).c_str() +// CMake builds print out a lot of gibberish with this macro - comment it out for now. +// #define LOG_TAG (std::string(__FILE__ ":") + std::to_string(__LINE__)).c_str() +#define LOG_TAG "rive-android-jni" #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define EGL_ERR_CHECK() _check_egl_error(__FILE__, __LINE__) #else #define LOGE(...) #define LOGW(...) #define LOGD(...) #define LOGI(...) +#define EGL_ERR_CHECK() #endif namespace rive_android { -extern JavaVM* globalJavaVM; -extern int sdkVersion; -void setSDKVersion(); -void logReferenceTables(); -long import(uint8_t* bytes, jint length); +enum class RendererType +{ + Skia = 0, + Rive = 1 +}; + +extern JavaVM* g_JVM; +extern long g_sdkVersion; +void SetSDKVersion(); +void LogReferenceTables(); +long Import(uint8_t*, jint, RendererType = RendererType::Skia); -rive::Alignment getAlignment(JNIEnv* env, jobject jalignment); -rive::Fit getFit(JNIEnv* env, jobject jfit); -JNIEnv* getJNIEnv(); +rive::Alignment GetAlignment(JNIEnv*, jobject); +rive::Fit GetFit(JNIEnv*, jobject); +JNIEnv* GetJNIEnv(); -void detachThread(); +void DetachThread(); -std::string jstring2string(JNIEnv* env, jstring jStr); +std::string JStringToString(JNIEnv*, jstring); +int SizeTTOInt(size_t); #if defined(DEBUG) || defined(LOG) // luigi: this redirects stderr to android log (probably want to ifdef this // out for release) -void logThread(); -void _check_egl_error(const char* file, int line); +[[noreturn]] void LogThread(); +void _check_egl_error(const char*, int); #endif } // namespace rive_android #endif diff --git a/kotlin/src/main/cpp/include/helpers/egl_share_thread_state.hpp b/kotlin/src/main/cpp/include/helpers/thread_state_egl.hpp similarity index 61% rename from kotlin/src/main/cpp/include/helpers/egl_share_thread_state.hpp rename to kotlin/src/main/cpp/include/helpers/thread_state_egl.hpp index 15754836..bd136510 100644 --- a/kotlin/src/main/cpp/include/helpers/egl_share_thread_state.hpp +++ b/kotlin/src/main/cpp/include/helpers/thread_state_egl.hpp @@ -9,33 +9,34 @@ #include "helpers/general.hpp" #include "helpers/tracer.hpp" -#include "GrDirectContext.h" -#include "SkSurface.h" -#include "SkCanvas.h" -#include "SkColorSpace.h" - namespace rive_android { -class EGLShareThreadState +class EGLThreadState { public: - EGLShareThreadState(); - ~EGLShareThreadState(); + EGLThreadState(); + + virtual ~EGLThreadState() = 0; EGLSurface createEGLSurface(ANativeWindow*); - sk_sp createSkiaSurface(EGLSurface, int width, int height); - void destroySurface(EGLSurface); + virtual void destroySurface(EGLSurface) = 0; + + virtual void makeCurrent(EGLSurface) = 0; - void makeCurrent(EGLSurface); void swapBuffers(); -private: +protected: + virtual void releaseContext() = 0; + + EGLSurface m_currentSurface = EGL_NO_SURFACE; + EGLDisplay m_display = EGL_NO_DISPLAY; - EGLConfig m_config = static_cast(0); + EGLContext m_context = EGL_NO_CONTEXT; - sk_sp m_skContext = nullptr; - EGLSurface m_currentSurface = EGL_NO_SURFACE; + +private: + EGLConfig m_config = static_cast(0); }; } // namespace rive_android diff --git a/kotlin/src/main/cpp/include/helpers/thread_state_pls.hpp b/kotlin/src/main/cpp/include/helpers/thread_state_pls.hpp new file mode 100644 index 00000000..c99970f0 --- /dev/null +++ b/kotlin/src/main/cpp/include/helpers/thread_state_pls.hpp @@ -0,0 +1,37 @@ +// +// Created by Umberto Sonnino on 6/27/23. +// +#ifndef RIVE_ANDROID_THREAD_STATE_PLS_HPP +#define RIVE_ANDROID_THREAD_STATE_PLS_HPP + +#include + +#include "thread_state_egl.hpp" +#include "rive/pls/gl/pls_render_context_gl.hpp" + +namespace rive_android +{ +class PLSThreadState : public EGLThreadState +{ +public: + PLSThreadState() = default; + + ~PLSThreadState() { releaseContext(); } + + rive::pls::PLSRenderContextGL* plsContext() const { return m_plsContext.get(); } + + void destroySurface(EGLSurface eglSurface) override; + + void makeCurrent(EGLSurface eglSurface) override; + +protected: + void releaseContext() override; + +private: + std::unique_ptr m_plsContext; + + bool m_ownsCurrentSurface = false; +}; +} // namespace rive_android + +#endif // RIVE_ANDROID_THREAD_STATE_PLS_HPP diff --git a/kotlin/src/main/cpp/include/helpers/thread_state_skia.hpp b/kotlin/src/main/cpp/include/helpers/thread_state_skia.hpp new file mode 100644 index 00000000..10d85a86 --- /dev/null +++ b/kotlin/src/main/cpp/include/helpers/thread_state_skia.hpp @@ -0,0 +1,37 @@ +// +// Created by Umberto Sonnino on 6/27/23. +// +#ifndef RIVE_ANDROID_THREAD_STATE_SKIA_HPP +#define RIVE_ANDROID_THREAD_STATE_SKIA_HPP + +#include "thread_state_egl.hpp" + +#include "GrDirectContext.h" +#include "SkSurface.h" +#include "SkCanvas.h" +#include "SkColorSpace.h" + +namespace rive_android +{ +class SkiaThreadState : public EGLThreadState +{ +public: + SkiaThreadState() = default; + + ~SkiaThreadState() { releaseContext(); } + + sk_sp createSkiaSurface(EGLSurface, int width, int height); + + void destroySurface(EGLSurface) override; + + void makeCurrent(EGLSurface) override; + +protected: + void releaseContext() override; + +private: + sk_sp m_skContext = nullptr; +}; +} // namespace rive_android + +#endif // RIVE_ANDROID_THREAD_STATE_SKIA_HPP diff --git a/kotlin/src/main/cpp/include/helpers/worker_thread.hpp b/kotlin/src/main/cpp/include/helpers/worker_thread.hpp index 1ae8e65a..f2185209 100644 --- a/kotlin/src/main/cpp/include/helpers/worker_thread.hpp +++ b/kotlin/src/main/cpp/include/helpers/worker_thread.hpp @@ -24,19 +24,22 @@ #include #include "helpers/general.hpp" -#include "helpers/egl_share_thread_state.hpp" +#include "helpers/thread_state_egl.hpp" #include "thread.hpp" namespace rive_android { -template class WorkerThread +class WorkerThread { public: - using Work = std::function; + using Work = std::function; using WorkID = uint64_t; - WorkerThread(const char* name, Affinity affinity) : - mName(name), mAffinity(affinity), mThread(std::thread([this]() { threadMain(); })) + WorkerThread(const char* name, Affinity affinity, const RendererType rendererType) : + m_RendererType(rendererType), + mName(name), + mAffinity(affinity), + mThread(std::thread([this]() { threadMain(); })) {} ~WorkerThread() { terminateThread(); } @@ -90,14 +93,21 @@ template class WorkerThread assert(m_lastCompletedWorkID == m_lastPushedWorkID); } + RendererType rendererType() const { return m_RendererType; } + +protected: + const RendererType m_RendererType; + private: + static std::unique_ptr MakeThreadState(const RendererType type); + void threadMain() { setAffinity(mAffinity); pthread_setname_np(pthread_self(), mName.c_str()); - getJNIEnv(); // Attach thread to JVM. - ThreadState threadState; + GetJNIEnv(); // Attach thread to JVM. + std::unique_ptr threadState = MakeThreadState(m_RendererType); std::unique_lock lock(mWorkMutex); for (;;) @@ -116,13 +126,13 @@ template class WorkerThread } lock.unlock(); - work(&threadState); + work(threadState.get()); lock.lock(); ++m_lastCompletedWorkID; m_workedCompletedCondition.notify_all(); } - detachThread(); + DetachThread(); } const std::string mName; @@ -134,7 +144,7 @@ template class WorkerThread bool mIsTerminated = false; std::mutex mWorkMutex; - std::queue> mWorkQueue; + std::queue> mWorkQueue; std::condition_variable_any m_workPushedCondition; std::condition_variable_any m_workedCompletedCondition; }; diff --git a/kotlin/src/main/cpp/include/jni_refs.hpp b/kotlin/src/main/cpp/include/jni_refs.hpp index 8d7dc796..385ca12d 100644 --- a/kotlin/src/main/cpp/include/jni_refs.hpp +++ b/kotlin/src/main/cpp/include/jni_refs.hpp @@ -6,28 +6,28 @@ namespace rive_android { -extern jint throwRiveException(const char* message); -extern jint throwMalformedFileException(const char* message); -extern jint throwUnsupportedRuntimeVersionException(const char* message); -extern jclass getFitClass(); -extern jmethodID getFitNameMethodId(); +extern jint ThrowRiveException(const char* message); +extern jint ThrowMalformedFileException(const char* message); +extern jint ThrowUnsupportedRuntimeVersionException(const char* message); +extern jclass GetFitClass(); +extern jmethodID GetFitNameMethodId(); -extern jclass getAlignmentClass(); -extern jmethodID getAlignmentNameMethodId(); +extern jclass GetAlignmentClass(); +extern jmethodID GetAlignmentNameMethodId(); -extern jclass getLoopClass(); -extern jfieldID getNoneLoopField(); -extern jfieldID getOneShotLoopField(); -extern jfieldID getLoopLoopField(); -extern jfieldID getPingPongLoopField(); +extern jclass GetLoopClass(); +extern jfieldID GetNoneLoopField(); +extern jfieldID GetOneShotLoopField(); +extern jfieldID GetLoopLoopField(); +extern jfieldID GetPingPongLoopField(); -extern jclass getPointerFClass(); -extern jmethodID getPointFInitMethod(); -extern jfieldID getXFieldId(); -extern jfieldID getYFieldId(); +extern jclass GetPointerFClass(); +extern jmethodID GetPointFInitMethod(); +extern jfieldID GetXFieldId(); +extern jfieldID GetYFieldId(); -extern rive::AABB rectFToAABB(JNIEnv* env, jobject rectf); -extern void aabbToRectF(JNIEnv* env, const rive::AABB&, jobject rectf); +extern rive::AABB RectFToAABB(JNIEnv* env, jobject rectf); +extern void AABBToRectF(JNIEnv* env, const rive::AABB&, jobject rectf); } // namespace rive_android #endif \ No newline at end of file diff --git a/kotlin/src/main/cpp/include/models/jni_renderer_skia.hpp b/kotlin/src/main/cpp/include/models/jni_renderer.hpp similarity index 62% rename from kotlin/src/main/cpp/include/models/jni_renderer_skia.hpp rename to kotlin/src/main/cpp/include/models/jni_renderer.hpp index 36466077..477dbdd3 100644 --- a/kotlin/src/main/cpp/include/models/jni_renderer_skia.hpp +++ b/kotlin/src/main/cpp/include/models/jni_renderer.hpp @@ -1,32 +1,35 @@ -#ifndef _RIVE_ANDROID_JAVA_RENDERER_SKIA_HPP_ -#define _RIVE_ANDROID_JAVA_RENDERER_SKIA_HPP_ +#ifndef _RIVE_ANDROID_JNI_RENDERER_HPP_ +#define _RIVE_ANDROID_JNI_RENDERER_HPP_ #include #include -#include "skia_renderer.hpp" - #include "helpers/tracer.hpp" -#include "helpers/egl_share_thread_state.hpp" +#include "helpers/thread_state_egl.hpp" #include "helpers/egl_worker.hpp" +#include "models/worker_impl.hpp" +#include "rive/renderer.hpp" namespace rive_android { -class JNIRendererSkia +class JNIRenderer { public: - JNIRendererSkia(jobject ktObject, bool trace = false); + JNIRenderer(jobject ktObject, + bool trace = false, + const RendererType rendererType = RendererType::Skia); - ~JNIRendererSkia(); + ~JNIRenderer(); void setWindow(ANativeWindow* window); rive::Renderer* getRendererOnWorkerThread() const; - void start(long timeNs); + void start(long long timeNs); + void stop(); - void doFrame(long frameTimeNs); + void doFrame(long long frameTimeNs); float averageFps() const { return mAverageFps; } @@ -34,9 +37,7 @@ class JNIRendererSkia int height() const { return m_window ? ANativeWindow_getHeight(m_window) : -1; } private: - class WorkerSideImpl; - - rive::rcp mWorker = EGLWorker::Current(); + rive::rcp mWorker; jobject m_ktRenderer; @@ -45,7 +46,7 @@ class JNIRendererSkia ANativeWindow* m_window = nullptr; std::thread::id m_workerThreadID; - std::unique_ptr m_workerSideImpl; + std::unique_ptr m_workerImpl; /* Helpers for FPS calculations.*/ std::chrono::steady_clock::time_point mLastFrameTime; @@ -58,8 +59,6 @@ class JNIRendererSkia ITracer* getTracer(bool trace) const; void calculateFps(); - - void draw(EGLShareThreadState* threadState); }; } // namespace rive_android -#endif +#endif // _RIVE_ANDROID_JNI_RENDERER_HPP_ diff --git a/kotlin/src/main/cpp/include/models/worker_impl.hpp b/kotlin/src/main/cpp/include/models/worker_impl.hpp new file mode 100644 index 00000000..cc37e707 --- /dev/null +++ b/kotlin/src/main/cpp/include/models/worker_impl.hpp @@ -0,0 +1,168 @@ +// +// Created by Umberto Sonnino on 7/10/23. +// + +#ifndef RIVE_ANDROID_WORKER_IMPL_HPP +#define RIVE_ANDROID_WORKER_IMPL_HPP + +#include + +#include "jni_refs.hpp" + +#include "helpers/thread_state_egl.hpp" +#include "helpers/thread_state_skia.hpp" +#include "helpers/egl_worker.hpp" +#include "helpers/general.hpp" +#include "helpers/thread_state_pls.hpp" +#include "helpers/worker_thread.hpp" + +#include "models/dimensions_helper.hpp" + +#include "rive/pls/pls_render_context.hpp" +#include "rive/pls/pls_renderer.hpp" + +#include "SkSurface.h" + +namespace rive_android +{ +class WorkerImpl +{ +public: + static std::unique_ptr Make(struct ANativeWindow* window, + EGLThreadState* threadState, + const RendererType type); + + virtual ~WorkerImpl() + { + // Call destroy() first! + assert(!m_isStarted); + assert(m_eglSurface == EGL_NO_SURFACE); + } + + virtual void destroy(EGLThreadState* threadState) + { + if (m_eglSurface != EGL_NO_SURFACE) + { + threadState->destroySurface(m_eglSurface); + m_eglSurface = EGL_NO_SURFACE; + } + } + + void start(jobject ktRenderer, long long timeNs); + + void stop(); + + void doFrame(ITracer* tracer, + EGLThreadState* threadState, + jobject ktRenderer, + long frameTimeNs); + + virtual void clear(EGLThreadState* threadState) = 0; + + virtual void flush(EGLThreadState* threadState) = 0; + + virtual rive::Renderer* renderer() const = 0; + +protected: + WorkerImpl(struct ANativeWindow* window, EGLThreadState* threadState, bool* success) + { + *success = false; + m_eglSurface = threadState->createEGLSurface(window); + if (m_eglSurface == EGL_NO_SURFACE) + return; + *success = true; + } + + void* m_eglSurface = EGL_NO_SURFACE; + + jclass m_ktRendererClass = nullptr; + jmethodID m_ktDrawCallback = nullptr; + jmethodID m_ktAdvanceCallback = nullptr; + long mLastFrameTimeNs = 0; // TODO: this should be a std::chrono::time_point, or at least 64 + // bits. + bool m_isStarted = false; +}; + +class SkiaWorkerImpl : public WorkerImpl +{ +public: + SkiaWorkerImpl(struct ANativeWindow* window, EGLThreadState* threadState, bool* success) : + WorkerImpl(window, threadState, success) + { + if (!success) + { + return; + } + m_skSurface = static_cast(threadState) + ->createSkiaSurface(m_eglSurface, + ANativeWindow_getWidth(window), + ANativeWindow_getHeight(window)); + if (m_skSurface == nullptr) + return; + m_skRenderer = std::make_unique(m_skSurface->getCanvas()); + } + + void destroy(EGLThreadState* threadState) override; + + void clear(EGLThreadState* threadState) override; + + void flush(EGLThreadState* threadState) override; + + rive::Renderer* renderer() const override; + +protected: + sk_sp m_skSurface; + std::unique_ptr m_skRenderer; +}; + +class PLSWorkerImpl : public WorkerImpl +{ +public: + PLSWorkerImpl(struct ANativeWindow* window, EGLThreadState* threadState, bool* success) : + WorkerImpl(window, threadState, success) + { + if (!success) + { + return; + } + + threadState->makeCurrent(m_eglSurface); + PLSThreadState* plsThreadState = PLSWorkerImpl::PlsThreadState(threadState); + rive::pls::PLSRenderContextGL* plsContext = plsThreadState->plsContext(); + int width = ANativeWindow_getWidth(window); + int height = ANativeWindow_getHeight(window); + m_plsRenderTarget = plsContext->wrapGLRenderTarget(0, width, height); + if (m_plsRenderTarget == nullptr) + { + m_plsRenderTarget = plsContext->makeOffscreenRenderTarget(width, height); + } + if (m_plsRenderTarget == nullptr) + { + return; + } + m_plsRenderer = std::make_unique(plsContext); + *success = true; + } + + void destroy(EGLThreadState* threadState) override; + + void clear(EGLThreadState* threadState) override; + + void flush(EGLThreadState* threadState) override; + + rive::Renderer* renderer() const override; + +private: + rive::rcp m_plsRenderTarget; + + std::unique_ptr m_plsRenderer; + + // Cast away [threadState] to the the thread state expected by this implementation. + static PLSThreadState* PlsThreadState(EGLThreadState* threadState) + { + // Quite hacky, but this is a way to sort this out in C++ without RTTI... + return static_cast(threadState); + } +}; +} // namespace rive_android +#endif // RIVE_ANDROID_WORKER_IMPL_HPP diff --git a/kotlin/src/main/cpp/src/bindings/bindings_artboard.cpp b/kotlin/src/main/cpp/src/bindings/bindings_artboard.cpp index 449829b1..51a87373 100644 --- a/kotlin/src/main/cpp/src/bindings/bindings_artboard.cpp +++ b/kotlin/src/main/cpp/src/bindings/bindings_artboard.cpp @@ -1,6 +1,6 @@ #include "jni_refs.hpp" #include "helpers/general.hpp" -#include "models/jni_renderer_skia.hpp" +#include "models/jni_renderer.hpp" #include "rive/artboard.hpp" #include "rive/animation/linear_animation_instance.hpp" #include "rive/animation/state_machine_instance.hpp" @@ -20,37 +20,17 @@ extern "C" jobject thisObj, jlong ref) { - auto artboard = (rive::ArtboardInstance*)ref; + auto artboard = reinterpret_cast(ref); return env->NewStringUTF(artboard->name().c_str()); } - JNIEXPORT jlong JNICALL - Java_app_rive_runtime_kotlin_core_Artboard_cppFirstAnimation(JNIEnv* env, - jobject thisObj, - jlong ref) - { - auto artboard = (rive::ArtboardInstance*)ref; - // Creates a new instance. - return (jlong)artboard->animationAt(0).release(); - } - - JNIEXPORT jlong JNICALL - Java_app_rive_runtime_kotlin_core_Artboard_cppFirstStateMachine(JNIEnv* env, - jobject thisObj, - jlong ref) - { - auto artboard = (rive::ArtboardInstance*)ref; - // Creates a new instance. - return (jlong)artboard->stateMachineAt(0).release(); - } - JNIEXPORT jstring JNICALL Java_app_rive_runtime_kotlin_core_Artboard_cppAnimationNameByIndex(JNIEnv* env, jobject, jlong ref, jint index) { - auto artboard = (rive::ArtboardInstance*)ref; + auto artboard = reinterpret_cast(ref); rive::LinearAnimation* animation = artboard->animation(index); auto name = animation->name(); @@ -64,7 +44,7 @@ extern "C" jlong ref, jint index) { - auto artboard = (rive::ArtboardInstance*)ref; + auto artboard = reinterpret_cast(ref); rive::StateMachine* stateMachine = artboard->stateMachine(index); auto name = stateMachine->name(); @@ -78,7 +58,7 @@ extern "C" jlong ref, jint index) { - auto artboard = (rive::ArtboardInstance*)ref; + auto artboard = reinterpret_cast(ref); // Creates a new instance. return (jlong)artboard->animationAt(index).release(); } @@ -89,9 +69,9 @@ extern "C" jlong ref, jstring name) { - auto artboard = (rive::ArtboardInstance*)ref; + auto artboard = reinterpret_cast(ref); // Creates a new instance. - return (jlong)artboard->animationNamed(jstring2string(env, name)).release(); + return (jlong)artboard->animationNamed(JStringToString(env, name)).release(); } JNIEXPORT jint JNICALL @@ -99,7 +79,7 @@ extern "C" jobject thisObj, jlong ref) { - auto artboard = (rive::ArtboardInstance*)ref; + auto artboard = reinterpret_cast(ref); return (jint)artboard->animationCount(); } @@ -110,7 +90,7 @@ extern "C" jlong ref, jint index) { - auto artboard = (rive::ArtboardInstance*)ref; + auto artboard = reinterpret_cast(ref); // Creates a new instance. return (jlong)artboard->stateMachineAt(index).release(); } @@ -121,10 +101,10 @@ extern "C" jlong ref, jstring name) { - auto artboard = (rive::ArtboardInstance*)ref; + auto artboard = reinterpret_cast(ref); // Creates a new instance. - return (jlong)artboard->stateMachineNamed(jstring2string(env, name)).release(); + return (jlong)artboard->stateMachineNamed(JStringToString(env, name)).release(); } JNIEXPORT jint JNICALL @@ -132,7 +112,7 @@ extern "C" jobject thisObj, jlong ref) { - auto artboard = (rive::ArtboardInstance*)ref; + auto artboard = reinterpret_cast(ref); return (jint)artboard->stateMachineCount(); } @@ -143,7 +123,7 @@ extern "C" jlong ref, jfloat elapsedTime) { - auto artboard = (rive::ArtboardInstance*)ref; + auto artboard = reinterpret_cast(ref); return artboard->advance(elapsedTime); } @@ -153,7 +133,7 @@ extern "C" { auto cls = env->FindClass("android/graphics/RectF"); auto constructor = env->GetMethodID(cls, "", "(FFFF)V"); - const auto bounds = ((rive::ArtboardInstance*)ref)->bounds(); + const auto bounds = (reinterpret_cast(ref))->bounds(); auto res = env->NewObject(cls, constructor, bounds.left(), @@ -170,8 +150,8 @@ extern "C" jlong rendererRef) { // TODO: consolidate this to work with an abstracted JNI Renderer. - auto artboard = (rive::ArtboardInstance*)artboardRef; - auto jniWrapper = (JNIRendererSkia*)rendererRef; + auto artboard = reinterpret_cast(artboardRef); + auto jniWrapper = reinterpret_cast(rendererRef); rive::Renderer* renderer = jniWrapper->getRendererOnWorkerThread(); artboard->draw(renderer); } @@ -184,18 +164,20 @@ extern "C" jobject ktFit, jobject ktAlignment) { - // TODO: consolidate this to work with an abstracted JNI Renderer. - auto artboard = (rive::ArtboardInstance*)artboardRef; - auto jniWrapper = (JNIRendererSkia*)rendererRef; + auto artboard = reinterpret_cast(artboardRef); + auto jniWrapper = reinterpret_cast(rendererRef); rive::Renderer* renderer = jniWrapper->getRendererOnWorkerThread(); - rive::Fit fit = getFit(env, ktFit); - rive::Alignment alignment = getAlignment(env, ktAlignment); + rive::Fit fit = GetFit(env, ktFit); + rive::Alignment alignment = GetAlignment(env, ktAlignment); renderer->save(); renderer->align(fit, alignment, - rive::AABB(0, 0, jniWrapper->width(), jniWrapper->height()), + rive::AABB(0, + 0, + static_cast(jniWrapper->width()), + static_cast(jniWrapper->height())), artboard->bounds()); artboard->draw(renderer); renderer->restore(); @@ -205,7 +187,7 @@ extern "C" jobject thisObj, jlong ref) { - auto artboard = (rive::ArtboardInstance*)ref; + auto artboard = reinterpret_cast(ref); delete artboard; } diff --git a/kotlin/src/main/cpp/src/bindings/bindings_file.cpp b/kotlin/src/main/cpp/src/bindings/bindings_file.cpp index 9f3e285d..09444d5a 100644 --- a/kotlin/src/main/cpp/src/bindings/bindings_file.cpp +++ b/kotlin/src/main/cpp/src/bindings/bindings_file.cpp @@ -13,35 +13,26 @@ extern "C" JNIEXPORT jlong JNICALL Java_app_rive_runtime_kotlin_core_File_import(JNIEnv* env, jobject thisObj, jbyteArray bytes, - jint length) + jint length, + jint type) { - rive_android::setSDKVersion(); - + rive_android::SetSDKVersion(); + RendererType rendererType = static_cast(type); jbyte* byte_array = env->GetByteArrayElements(bytes, NULL); - long file = import((uint8_t*)byte_array, length); + long file = Import(reinterpret_cast(byte_array), length, rendererType); env->ReleaseByteArrayElements(bytes, byte_array, JNI_ABORT); return (jlong)file; } - // todo return default artboard instance. - JNIEXPORT jlong JNICALL Java_app_rive_runtime_kotlin_core_File_cppArtboard(JNIEnv* env, - jobject thisObj, - jlong ref) - { - auto file = (rive::File*)ref; - // Creates a new Artboard instance. - return (jlong)file->artboardAt(0).release(); - } - JNIEXPORT jlong JNICALL Java_app_rive_runtime_kotlin_core_File_cppArtboardByName(JNIEnv* env, jobject thisObj, jlong ref, jstring name) { - auto file = (rive::File*)ref; + auto file = reinterpret_cast(ref); // Creates a new Artboard instance. - return (jlong)file->artboardNamed(jstring2string(env, name)).release(); + return (jlong)file->artboardNamed(JStringToString(env, name)).release(); } JNIEXPORT void JNICALL Java_app_rive_runtime_kotlin_core_File_cppDelete(JNIEnv* env, @@ -49,7 +40,7 @@ extern "C" jlong ref) { // if we're wiping the file, we really should wipe all those refs? - auto file = (rive::File*)ref; + auto file = reinterpret_cast(ref); delete file; } @@ -57,7 +48,7 @@ extern "C" jobject thisObj, jlong ref) { - auto file = (rive::File*)ref; + auto file = reinterpret_cast(ref); return (jint)file->artboardCount(); } @@ -68,7 +59,7 @@ extern "C" jlong ref, jint index) { - auto file = (rive::File*)ref; + auto file = reinterpret_cast(ref); // Creates a new Artboard instance. return (jlong)file->artboardAt(index).release(); } @@ -79,7 +70,7 @@ extern "C" jlong ref, jint index) { - auto file = (rive::File*)ref; + auto file = reinterpret_cast(ref); auto artboard = file->artboard(index); auto name = artboard->name(); diff --git a/kotlin/src/main/cpp/src/bindings/bindings_helper.cpp b/kotlin/src/main/cpp/src/bindings/bindings_helper.cpp index 909b6eb3..c525f8f7 100644 --- a/kotlin/src/main/cpp/src/bindings/bindings_helper.cpp +++ b/kotlin/src/main/cpp/src/bindings/bindings_helper.cpp @@ -18,22 +18,22 @@ extern "C" jobject jalignment, jobject artboardSpaceRectF) { - auto fit = ::getFit(env, jfit); - auto alignment = ::getAlignment(env, jalignment); - auto artboardSpaceBounds = rectFToAABB(env, artboardSpaceRectF); - auto touchSpaceBounds = rectFToAABB(env, touchSpaceRectF); - jlong touchX = (jlong)env->GetFloatField(touchSpacePointF, getXFieldId()); - jlong touchY = (jlong)env->GetFloatField(touchSpacePointF, getYFieldId()); + auto fit = ::GetFit(env, jfit); + auto alignment = ::GetAlignment(env, jalignment); + auto artboardSpaceBounds = RectFToAABB(env, artboardSpaceRectF); + auto touchSpaceBounds = RectFToAABB(env, touchSpaceRectF); + jlong touchX = (jlong)env->GetFloatField(touchSpacePointF, GetXFieldId()); + jlong touchY = (jlong)env->GetFloatField(touchSpacePointF, GetYFieldId()); rive::Mat2D forward = rive::computeAlignment(fit, alignment, touchSpaceBounds, artboardSpaceBounds); rive::Mat2D inverse = forward.invertOrIdentity(); - auto touchLocation = rive::Vec2D(touchX, touchY); + auto touchLocation = rive::Vec2D(static_cast(touchX), static_cast(touchY)); rive::Vec2D convertedLocation = inverse * touchLocation; - return env->NewObject(getPointerFClass(), - getPointFInitMethod(), + return env->NewObject(GetPointerFClass(), + GetPointFInitMethod(), convertedLocation.x, convertedLocation.y); } diff --git a/kotlin/src/main/cpp/src/bindings/bindings_init.cpp b/kotlin/src/main/cpp/src/bindings/bindings_init.cpp index 6bfd2468..e6dd4570 100644 --- a/kotlin/src/main/cpp/src/bindings/bindings_init.cpp +++ b/kotlin/src/main/cpp/src/bindings/bindings_init.cpp @@ -24,15 +24,15 @@ extern "C" jobject artboardBoundsRectF, jobject requiredBoundsRectF) { - auto fit = ::getFit(env, jfit); - auto alignment = ::getAlignment(env, jalignment); - auto availableBounds = rectFToAABB(env, availableBoundsRectF); - auto artboardBounds = rectFToAABB(env, artboardBoundsRectF); + auto fit = ::GetFit(env, jfit); + auto alignment = ::GetAlignment(env, jalignment); + auto availableBounds = RectFToAABB(env, availableBoundsRectF); + auto artboardBounds = RectFToAABB(env, artboardBoundsRectF); DimensionsHelper helper; auto required = helper.computeDimensions(fit, alignment, availableBounds, artboardBounds); - aabbToRectF(env, required, requiredBoundsRectF); + AABBToRectF(env, required, requiredBoundsRectF); } JNIEXPORT void JNICALL Java_app_rive_runtime_kotlin_core_Rive_cppInitialize(JNIEnv* env, @@ -41,12 +41,12 @@ extern "C" #if defined(DEBUG) || defined(LOG) // luigi: again ifdef this out for release (or murder completely, but // it's nice to catch all fprintf to stderr). - std::thread t(logThread); + std::thread t(LogThread); // detach so it outlives the ref t.detach(); #endif // pretty much considered the entrypoint. - env->GetJavaVM(&::globalJavaVM); + env->GetJavaVM(&::g_JVM); } #ifdef __cplusplus diff --git a/kotlin/src/main/cpp/src/bindings/bindings_layer_state.cpp b/kotlin/src/main/cpp/src/bindings/bindings_layer_state.cpp index 668a66dd..ad09db37 100644 --- a/kotlin/src/main/cpp/src/bindings/bindings_layer_state.cpp +++ b/kotlin/src/main/cpp/src/bindings/bindings_layer_state.cpp @@ -23,7 +23,7 @@ extern "C" jlong ref) { - rive::LayerState* layerState = (rive::LayerState*)ref; + rive::LayerState* layerState = reinterpret_cast(ref); return layerState->is(); } @@ -33,7 +33,7 @@ extern "C" jlong ref) { - rive::LayerState* layerState = (rive::LayerState*)ref; + rive::LayerState* layerState = reinterpret_cast(ref); return layerState->is(); } @@ -43,7 +43,7 @@ extern "C" jlong ref) { - rive::LayerState* layerState = (rive::LayerState*)ref; + rive::LayerState* layerState = reinterpret_cast(ref); return layerState->is(); } @@ -53,7 +53,7 @@ extern "C" jlong ref) { - rive::LayerState* layerState = (rive::LayerState*)ref; + rive::LayerState* layerState = reinterpret_cast(ref); return layerState->is(); } @@ -63,7 +63,7 @@ extern "C" jlong ref) { - rive::LayerState* layerState = (rive::LayerState*)ref; + rive::LayerState* layerState = reinterpret_cast(ref); return layerState->is(); } @@ -73,7 +73,7 @@ extern "C" jlong ref) { - rive::LayerState* layerState = (rive::LayerState*)ref; + rive::LayerState* layerState = reinterpret_cast(ref); return layerState->is(); } @@ -83,7 +83,7 @@ extern "C" jlong ref) { - rive::LayerState* layerState = (rive::LayerState*)ref; + rive::LayerState* layerState = reinterpret_cast(ref); return layerState->is(); } @@ -92,7 +92,7 @@ extern "C" jobject thisObj, jlong ref) { - auto animationState = (rive::AnimationState*)ref; + auto animationState = reinterpret_cast(ref); auto animation = animationState->animation(); if (animation == nullptr) { diff --git a/kotlin/src/main/cpp/src/bindings/bindings_linear_animation_instance.cpp b/kotlin/src/main/cpp/src/bindings/bindings_linear_animation_instance.cpp index b86a2bbb..9868d38c 100644 --- a/kotlin/src/main/cpp/src/bindings/bindings_linear_animation_instance.cpp +++ b/kotlin/src/main/cpp/src/bindings/bindings_linear_animation_instance.cpp @@ -16,7 +16,7 @@ extern "C" jlong ref, jfloat elapsedTime) { - auto animationInstance = (rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); animationInstance->advance(elapsedTime); bool didLoop = animationInstance->didLoop(); @@ -29,19 +29,19 @@ extern "C" switch (loopType) { case rive::Loop::oneShot: - enumField = ::getOneShotLoopField(); + enumField = ::GetOneShotLoopField(); break; case rive::Loop::loop: - enumField = ::getLoopLoopField(); + enumField = ::GetLoopLoopField(); break; case rive::Loop::pingPong: - enumField = ::getPingPongLoopField(); + enumField = ::GetPingPongLoopField(); break; default: - enumField = ::getNoneLoopField(); + enumField = ::GetNoneLoopField(); break; } - jclass jClass = ::getLoopClass(); + jclass jClass = ::GetLoopClass(); loopValue = env->GetStaticObjectField(jClass, enumField); env->DeleteLocalRef(jClass); } @@ -55,7 +55,7 @@ extern "C" jlong ref, jfloat mix) { - auto animationInstance = (rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); animationInstance->apply(mix); } @@ -65,7 +65,7 @@ extern "C" jlong ref) { - auto animationInstance = (rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); return animationInstance->time(); } @@ -75,7 +75,7 @@ extern "C" jlong ref, jfloat time) { - auto animationInstance = (rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); animationInstance->time(time); } @@ -85,7 +85,7 @@ extern "C" jlong ref, jint direction) { - auto animationInstance = (rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); animationInstance->direction(direction); } @@ -94,8 +94,8 @@ extern "C" jobject thisObj, jlong ref) { - auto animationInstance = (rive::LinearAnimationInstance*)ref; - return animationInstance->direction(); + auto animationInstance = reinterpret_cast(ref); + return static_cast(animationInstance->direction()); } JNIEXPORT jint JNICALL @@ -103,7 +103,7 @@ extern "C" jobject thisObj, jlong ref) { - auto* animationInstance = (rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); return (jint)animationInstance->loop(); } @@ -113,7 +113,7 @@ extern "C" jlong ref, jint loopType) { - auto* animationInstance = (rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); animationInstance->loopValue(loopType); } @@ -125,7 +125,7 @@ extern "C" jlong ref) { - auto* animationInstance = (const rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); return env->NewStringUTF(animationInstance->animation()->name().c_str()); } @@ -134,7 +134,7 @@ extern "C" jobject thisObj, jlong ref) { - auto* animationInstance = (const rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); return (jint)animationInstance->animation()->duration(); } @@ -143,7 +143,7 @@ extern "C" jobject thisObj, jlong ref) { - auto* animationInstance = (const rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); return (jint)animationInstance->animation()->fps(); } @@ -152,7 +152,7 @@ extern "C" jobject thisObj, jlong ref) { - auto* animationInstance = (const rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); return (jint)animationInstance->animation()->workStart(); } @@ -161,14 +161,14 @@ extern "C" jobject thisObj, jlong ref) { - auto* animationInstance = (const rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); return (jint)animationInstance->animation()->workEnd(); } JNIEXPORT void JNICALL Java_app_rive_runtime_kotlin_core_LinearAnimationInstance_cppDelete(JNIEnv*, jobject, jlong ref) { - auto animationInstance = (rive::LinearAnimationInstance*)ref; + auto animationInstance = reinterpret_cast(ref); delete animationInstance; } diff --git a/kotlin/src/main/cpp/src/bindings/bindings_renderer.cpp b/kotlin/src/main/cpp/src/bindings/bindings_renderer.cpp new file mode 100644 index 00000000..129086df --- /dev/null +++ b/kotlin/src/main/cpp/src/bindings/bindings_renderer.cpp @@ -0,0 +1,135 @@ +#include +#include + +#include "jni_refs.hpp" +#include "helpers/general.hpp" +#include "helpers/worker_thread.hpp" + +#include "models/jni_renderer.hpp" +#include "rive/layout.hpp" +#include "rive/artboard.hpp" + +#ifdef __cplusplus +extern "C" +{ +#endif + + using namespace rive_android; + + // Renderer + JNIEXPORT jlong JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_constructor(JNIEnv* env, + jobject ktRenderer, + jboolean trace, + jint type) + { + RendererType rendererType = static_cast(type); + JNIRenderer* renderer = new JNIRenderer(ktRenderer, trace, rendererType); + return (jlong)renderer; + } + + JNIEXPORT void JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppDelete(JNIEnv*, jobject, jlong rendererRef) + { + JNIRenderer* renderer = reinterpret_cast(rendererRef); + delete renderer; + } + + JNIEXPORT void JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppStop(JNIEnv*, jobject, jlong rendererRef) + { + reinterpret_cast(rendererRef)->stop(); + } + + JNIEXPORT void JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppStart(JNIEnv*, + jobject, + jlong rendererRef, + jlong frameTimeNs) + { + reinterpret_cast(rendererRef)->start(frameTimeNs); + } + + JNIEXPORT void JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppDoFrame(JNIEnv*, + jobject, + jlong rendererRef, + jlong frameTimeNs) + { + reinterpret_cast(rendererRef)->doFrame(frameTimeNs); + } + + JNIEXPORT void JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppSetSurface(JNIEnv* env, + jobject, + jobject surface, + jlong rendererRef) + { + ANativeWindow* surfaceWindow = ANativeWindow_fromSurface(env, surface); + reinterpret_cast(rendererRef)->setWindow(surfaceWindow); + if (surfaceWindow) + { + // Release this handle. If the renderer grabbed a reference it + // won't deallocate. + ANativeWindow_release(surfaceWindow); + } + } + + JNIEXPORT void JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppDestroySurface(JNIEnv*, + jobject, + jlong rendererRef) + { + reinterpret_cast(rendererRef)->setWindow(nullptr); + } + + JNIEXPORT void JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppSave(JNIEnv*, jobject, jlong rendererRef) + { + reinterpret_cast(rendererRef)->getRendererOnWorkerThread()->save(); + } + + JNIEXPORT void JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppRestore(JNIEnv*, jobject, jlong rendererRef) + { + reinterpret_cast(rendererRef)->getRendererOnWorkerThread()->restore(); + } + + JNIEXPORT void JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppAlign(JNIEnv* env, + jobject thisObj, + jlong ref, + jobject ktFit, + jobject ktAlignment, + jobject targetBoundsRectF, + jobject sourceBoundsRectF) + { + JNIRenderer* jniWrapper = reinterpret_cast(ref); + rive::Fit fit = GetFit(env, ktFit); + rive::Alignment alignment = GetAlignment(env, ktAlignment); + rive::AABB targetBounds = RectFToAABB(env, targetBoundsRectF); + rive::AABB sourceBounds = RectFToAABB(env, sourceBoundsRectF); + jniWrapper->getRendererOnWorkerThread()->align(fit, alignment, targetBounds, sourceBounds); + } + + JNIEXPORT jint JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppWidth(JNIEnv*, jobject, jlong rendererRef) + { + return reinterpret_cast(rendererRef)->width(); + } + + JNIEXPORT jint JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppHeight(JNIEnv*, jobject, jlong rendererRef) + { + return reinterpret_cast(rendererRef)->height(); + } + + JNIEXPORT jfloat JNICALL + Java_app_rive_runtime_kotlin_renderers_Renderer_cppAvgFps(JNIEnv*, jobject, jlong rendererRef) + { + return reinterpret_cast(rendererRef)->averageFps(); + } + +#ifdef __cplusplus +} +#endif diff --git a/kotlin/src/main/cpp/src/bindings/bindings_renderer_skia.cpp b/kotlin/src/main/cpp/src/bindings/bindings_renderer_skia.cpp deleted file mode 100644 index dc106a4a..00000000 --- a/kotlin/src/main/cpp/src/bindings/bindings_renderer_skia.cpp +++ /dev/null @@ -1,143 +0,0 @@ -#include -#include - -#include "jni_refs.hpp" -#include "helpers/general.hpp" -#include "helpers/worker_thread.hpp" - -#include "models/jni_renderer_skia.hpp" -#include "rive/layout.hpp" -#include "rive/artboard.hpp" - -#ifdef __cplusplus -extern "C" -{ -#endif - - using namespace rive_android; - - // Skia Renderer - JNIEXPORT jlong JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_constructor(JNIEnv* env, - jobject ktRendererSkia, - jboolean trace) - { - ::JNIRendererSkia* renderer = new JNIRendererSkia(ktRendererSkia, trace); - return (jlong)renderer; - } - - JNIEXPORT void JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppDelete(JNIEnv*, - jobject, - jlong rendererRef) - { - auto renderer = reinterpret_cast(rendererRef); - delete renderer; - } - - JNIEXPORT void JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppStop(JNIEnv*, jobject, jlong rendererRef) - { - reinterpret_cast(rendererRef)->stop(); - } - - JNIEXPORT void JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppStart(JNIEnv*, - jobject, - jlong rendererRef, - jlong frameTimeNs) - { - reinterpret_cast(rendererRef)->start(frameTimeNs); - } - - JNIEXPORT void JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppDoFrame(JNIEnv*, - jobject, - jlong rendererRef, - jlong frameTimeNs) - { - reinterpret_cast(rendererRef)->doFrame(frameTimeNs); - } - - JNIEXPORT void JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppSetSurface(JNIEnv* env, - jobject, - jobject surface, - jlong rendererRef) - { - ANativeWindow* surfaceWindow = ANativeWindow_fromSurface(env, surface); - reinterpret_cast(rendererRef)->setWindow(surfaceWindow); - if (surfaceWindow) - { - // Release this handle. If the renderer grabbed a reference it - // won't deallocate. - ANativeWindow_release(surfaceWindow); - } - } - - JNIEXPORT void JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppDestroySurface(JNIEnv*, - jobject, - jlong rendererRef) - { - reinterpret_cast(rendererRef)->setWindow(nullptr); - } - - JNIEXPORT void JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppSave(JNIEnv*, jobject, jlong rendererRef) - { - reinterpret_cast(rendererRef)->getRendererOnWorkerThread()->save(); - } - - JNIEXPORT void JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppRestore(JNIEnv*, - jobject, - jlong rendererRef) - { - reinterpret_cast(rendererRef)->getRendererOnWorkerThread()->restore(); - } - - JNIEXPORT void JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppAlign(JNIEnv* env, - jobject thisObj, - jlong ref, - jobject ktFit, - jobject ktAlignment, - jobject targetBoundsRectF, - jobject sourceBoundsRectF) - { - JNIRendererSkia* jniWrapper = (JNIRendererSkia*)ref; - rive::Fit fit = getFit(env, ktFit); - rive::Alignment alignment = getAlignment(env, ktAlignment); - auto targetBounds = rectFToAABB(env, targetBoundsRectF); - auto sourceBounds = rectFToAABB(env, sourceBoundsRectF); - jniWrapper->getRendererOnWorkerThread()->align(fit, alignment, targetBounds, sourceBounds); - } - - JNIEXPORT jint JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppWidth(JNIEnv*, - jobject, - jlong rendererRef) - { - return reinterpret_cast(rendererRef)->width(); - } - - JNIEXPORT jint JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppHeight(JNIEnv*, - jobject, - jlong rendererRef) - { - return reinterpret_cast(rendererRef)->height(); - } - - JNIEXPORT jfloat JNICALL - Java_app_rive_runtime_kotlin_renderers_RendererSkia_cppAvgFps(JNIEnv*, - jobject, - jlong rendererRef) - { - return reinterpret_cast(rendererRef)->averageFps(); - } - -#ifdef __cplusplus -} -#endif diff --git a/kotlin/src/main/cpp/src/bindings/bindings_rive_texture_view.cpp b/kotlin/src/main/cpp/src/bindings/bindings_rive_texture_view.cpp index b9cb3848..41a037ce 100644 --- a/kotlin/src/main/cpp/src/bindings/bindings_rive_texture_view.cpp +++ b/kotlin/src/main/cpp/src/bindings/bindings_rive_texture_view.cpp @@ -1,7 +1,7 @@ #include #include -#include "models/jni_renderer_skia.hpp" +#include "models/jni_renderer.hpp" using namespace rive_android; @@ -14,7 +14,7 @@ extern "C" jobject, jlong rendererAddr) { - return reinterpret_cast(rendererAddr)->averageFps(); + return reinterpret_cast(rendererAddr)->averageFps(); } /** TODO: explore these helpers, might be useful for a few metrics diff --git a/kotlin/src/main/cpp/src/bindings/bindings_state_machine_input_instance.cpp b/kotlin/src/main/cpp/src/bindings/bindings_state_machine_input_instance.cpp index 1027f5be..0e553f0e 100644 --- a/kotlin/src/main/cpp/src/bindings/bindings_state_machine_input_instance.cpp +++ b/kotlin/src/main/cpp/src/bindings/bindings_state_machine_input_instance.cpp @@ -16,7 +16,7 @@ extern "C" jlong ref) { - rive::SMIInput* input = (rive::SMIInput*)ref; + rive::SMIInput* input = reinterpret_cast(ref); return env->NewStringUTF(input->name().c_str()); } @@ -24,7 +24,7 @@ extern "C" Java_app_rive_runtime_kotlin_core_SMIInput_cppIsBoolean(JNIEnv* env, jobject thisObj, jlong ref) { - rive::SMIInput* input = (rive::SMIInput*)ref; + rive::SMIInput* input = reinterpret_cast(ref); return input->input()->is(); } @@ -32,7 +32,7 @@ extern "C" Java_app_rive_runtime_kotlin_core_SMIInput_cppIsNumber(JNIEnv* env, jobject thisObj, jlong ref) { - rive::SMIInput* input = (rive::SMIInput*)ref; + rive::SMIInput* input = reinterpret_cast(ref); return input->input()->is(); } @@ -40,7 +40,7 @@ extern "C" Java_app_rive_runtime_kotlin_core_SMIInput_cppIsTrigger(JNIEnv* env, jobject thisObj, jlong ref) { - rive::SMIInput* input = (rive::SMIInput*)ref; + rive::SMIInput* input = reinterpret_cast(ref); return input->input()->is(); } @@ -48,7 +48,7 @@ extern "C" Java_app_rive_runtime_kotlin_core_SMIBoolean_cppValue(JNIEnv* env, jobject thisObj, jlong ref) { - rive::SMIBool* input = (rive::SMIBool*)ref; + rive::SMIBool* input = reinterpret_cast(ref); return input->value(); } @@ -59,7 +59,7 @@ extern "C" jboolean newValue) { - rive::SMIBool* input = (rive::SMIBool*)ref; + rive::SMIBool* input = reinterpret_cast(ref); input->value(newValue); } @@ -68,7 +68,7 @@ extern "C" jlong ref) { - rive::SMINumber* input = (rive::SMINumber*)ref; + rive::SMINumber* input = reinterpret_cast(ref); return input->value(); } @@ -78,7 +78,7 @@ extern "C" jfloat newValue) { - rive::SMINumber* input = (rive::SMINumber*)ref; + rive::SMINumber* input = reinterpret_cast(ref); input->value(newValue); } @@ -87,7 +87,7 @@ extern "C" jlong ref) { - rive::SMITrigger* input = (rive::SMITrigger*)ref; + rive::SMITrigger* input = reinterpret_cast(ref); input->fire(); } diff --git a/kotlin/src/main/cpp/src/bindings/bindings_state_machine_instance.cpp b/kotlin/src/main/cpp/src/bindings/bindings_state_machine_instance.cpp index 8b6bd3a1..7519c7e1 100644 --- a/kotlin/src/main/cpp/src/bindings/bindings_state_machine_instance.cpp +++ b/kotlin/src/main/cpp/src/bindings/bindings_state_machine_instance.cpp @@ -15,8 +15,7 @@ extern "C" jlong ref, jfloat elapsedTime) { - auto stateMachineInstance = (rive::StateMachineInstance*)ref; - + auto stateMachineInstance = reinterpret_cast(ref); return stateMachineInstance->advance(elapsedTime); } @@ -25,8 +24,8 @@ extern "C" jobject thisObj, jlong ref) { - auto stateMachineInstance = (rive::StateMachineInstance*)ref; - return stateMachineInstance->stateChangedCount(); + auto stateMachineInstance = reinterpret_cast(ref); + return SizeTTOInt(stateMachineInstance->stateChangedCount()); } JNIEXPORT jlong JNICALL @@ -35,7 +34,7 @@ extern "C" jlong ref, jint index) { - auto stateMachineInstance = (rive::StateMachineInstance*)ref; + auto stateMachineInstance = reinterpret_cast(ref); return (jlong)stateMachineInstance->stateChangedByIndex(index); } @@ -45,7 +44,7 @@ extern "C" jlong ref, jint index) { - auto stateMachineInstance = (rive::StateMachineInstance*)ref; + auto stateMachineInstance = reinterpret_cast(ref); return (jlong)stateMachineInstance->input(index); } @@ -55,9 +54,8 @@ extern "C" jobject thisObj, jlong ref) { - auto stateMachineInstance = (rive::StateMachineInstance*)ref; - - return (jlong)stateMachineInstance->inputCount(); + auto stateMachineInstance = reinterpret_cast(ref); + return SizeTTOInt(stateMachineInstance->inputCount()); } // ANIMATION @@ -66,7 +64,7 @@ extern "C" jobject thisObj, jlong ref) { - auto stateMachineInstance = (rive::StateMachineInstance*)ref; + auto stateMachineInstance = reinterpret_cast(ref); return env->NewStringUTF(stateMachineInstance->stateMachine()->name().c_str()); } @@ -75,7 +73,7 @@ extern "C" jobject thisObj, jlong ref) { - auto stateMachineInstance = (rive::StateMachineInstance*)ref; + auto stateMachineInstance = reinterpret_cast(ref); return (jint)stateMachineInstance->stateMachine()->layerCount(); } @@ -86,7 +84,7 @@ extern "C" jfloat x, jfloat y) { - auto stateMachineInstance = (rive::StateMachineInstance*)ref; + auto stateMachineInstance = reinterpret_cast(ref); stateMachineInstance->pointerDown(rive::Vec2D(x, y)); } @@ -97,7 +95,7 @@ extern "C" jfloat x, jfloat y) { - auto stateMachineInstance = (rive::StateMachineInstance*)ref; + auto stateMachineInstance = reinterpret_cast(ref); stateMachineInstance->pointerMove(rive::Vec2D(x, y)); } @@ -108,14 +106,14 @@ extern "C" jfloat x, jfloat y) { - auto stateMachineInstance = (rive::StateMachineInstance*)ref; + auto stateMachineInstance = reinterpret_cast(ref); stateMachineInstance->pointerUp(rive::Vec2D(x, y)); } JNIEXPORT void JNICALL Java_app_rive_runtime_kotlin_core_StateMachineInstance_cppDelete(JNIEnv*, jobject, jlong ref) { - auto stateMachineInstance = (rive::StateMachineInstance*)ref; + auto stateMachineInstance = reinterpret_cast(ref); delete stateMachineInstance; } diff --git a/kotlin/src/main/cpp/src/helpers/egl_share_thread_state.cpp b/kotlin/src/main/cpp/src/helpers/egl_share_thread_state.cpp deleted file mode 100644 index 021df895..00000000 --- a/kotlin/src/main/cpp/src/helpers/egl_share_thread_state.cpp +++ /dev/null @@ -1,259 +0,0 @@ -#include -#include - -#include "helpers/egl_share_thread_state.hpp" - -#include "GrDirectContext.h" -#include "gl/GrGLInterface.h" -#include "gl/GrGLAssembleInterface.h" -#include "SkSurface.h" - -#if defined(DEBUG) || defined(LOG) -#define EGL_ERR_CHECK() _check_egl_error(__FILE__, __LINE__) -#else -#define EGL_ERR_CHECK() -#endif - -namespace rive_android -{ -static bool config_has_attribute(EGLDisplay display, - EGLConfig config, - EGLint attribute, - EGLint value) -{ - EGLint outValue = 0; - EGLBoolean result = eglGetConfigAttrib(display, config, attribute, &outValue); - EGL_ERR_CHECK(); - return result && (outValue == value); -} - -EGLShareThreadState::EGLShareThreadState() -{ - m_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - if (m_display == EGL_NO_DISPLAY) - { - EGL_ERR_CHECK(); - LOGE("NO DISPLAY!?"); - return; - } - - if (!eglInitialize(m_display, 0, 0)) - { - EGL_ERR_CHECK(); - LOGE("eglInitialize() failed."); - return; - } - - const EGLint configAttributes[] = {EGL_RENDERABLE_TYPE, - EGL_OPENGL_ES2_BIT, - EGL_BLUE_SIZE, - 8, - EGL_GREEN_SIZE, - 8, - EGL_RED_SIZE, - 8, - EGL_DEPTH_SIZE, - 0, - EGL_STENCIL_SIZE, - 8, - EGL_ALPHA_SIZE, - 8, - EGL_NONE}; - - EGLint num_configs = 0; - if (!eglChooseConfig(m_display, configAttributes, nullptr, 0, &num_configs)) - { - EGL_ERR_CHECK(); - LOGE("eglChooseConfig() didn't find any (%d)", num_configs); - return; - } - - std::vector supportedConfigs(static_cast(num_configs)); - eglChooseConfig(m_display, - configAttributes, - supportedConfigs.data(), - num_configs, - &num_configs); - EGL_ERR_CHECK(); - - // Choose a config, either a match if possible or the first config - // otherwise - const auto configMatches = [&](EGLConfig config) { - if (!config_has_attribute(m_display, m_config, EGL_RED_SIZE, 8)) - return false; - if (!config_has_attribute(m_display, m_config, EGL_GREEN_SIZE, 8)) - return false; - if (!config_has_attribute(m_display, m_config, EGL_BLUE_SIZE, 8)) - return false; - if (!config_has_attribute(m_display, m_config, EGL_STENCIL_SIZE, 8)) - return false; - return config_has_attribute(m_display, m_config, EGL_DEPTH_SIZE, 0); - }; - - const auto configIter = - std::find_if(supportedConfigs.cbegin(), supportedConfigs.cend(), configMatches); - - m_config = (configIter != supportedConfigs.cend()) ? *configIter : supportedConfigs[0]; - - const EGLint contextAttributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; - - m_context = eglCreateContext(m_display, m_config, nullptr, contextAttributes); - if (m_context == EGL_NO_CONTEXT) - { - LOGE("eglCreateContext() failed."); - EGL_ERR_CHECK(); - } -} - -EGLShareThreadState::~EGLShareThreadState() -{ - LOGD("EGLThreadState getting destroyed! ๐Ÿงจ"); - - // Release Skia Context if has been init'd. - if (m_skContext.get()) - { - m_skContext->abandonContext(); - m_skContext.reset(nullptr); - } - - if (m_context != EGL_NO_CONTEXT) - { - eglDestroyContext(m_display, m_context); - EGL_ERR_CHECK(); - } - - eglReleaseThread(); - EGL_ERR_CHECK(); - - if (m_display != EGL_NO_DISPLAY) - { - eglTerminate(m_display); - EGL_ERR_CHECK(); - } -} - -EGLSurface EGLShareThreadState::createEGLSurface(ANativeWindow* window) -{ - if (!window) - { - return EGL_NO_SURFACE; - } - - LOGD("mSkiaContextManager.createWindowSurface()"); - auto res = eglCreateWindowSurface(m_display, m_config, window, nullptr); - EGL_ERR_CHECK(); - return res; -} - -void EGLShareThreadState::destroySurface(EGLSurface eglSurface) -{ - if (eglSurface == EGL_NO_SURFACE) - { - return; - } - - if (m_currentSurface == eglSurface) - { - makeCurrent(EGL_NO_SURFACE); - } - eglDestroySurface(m_display, eglSurface); - EGL_ERR_CHECK(); -} - -static sk_sp make_skia_context() -{ - LOGI("c_version()"); - auto c_version = reinterpret_cast(glGetString(GL_VERSION)); - if (c_version == nullptr) - { - EGL_ERR_CHECK(); - LOGE("c_version failed"); - return nullptr; - } - - auto get_proc = [](void* context, const char name[]) -> GrGLFuncPtr { - return reinterpret_cast(eglGetProcAddress(name)); - }; - std::string version(c_version); - auto interface = version.find("OpenGL ES") == std::string::npos - ? GrGLMakeAssembledGLInterface(nullptr, get_proc) - : GrGLMakeAssembledGLESInterface(nullptr, get_proc); - LOGI("OpenGL Version %s", version.c_str()); - if (!interface) - { - LOGE("GrGLMakeAssembledGL(ES)Interface failed."); - return nullptr; - } - auto ctx = GrDirectContext::MakeGL(interface); - LOGI("Skia Context Created %p", ctx.get()); - return ctx; -} - -sk_sp EGLShareThreadState::createSkiaSurface(EGLSurface eglSurface, - int width, - int height) -{ - // Width/Height getters return negative values on error. - // Probably a race condition with surfaces being reclaimed by the OS before - // this function completes. - if (width < 0 || height < 0) - { - LOGE("Window is unavailable."); - return nullptr; - } - - makeCurrent(eglSurface); - - LOGI("Set up window surface %dx%d", width, height); - if (m_skContext == nullptr) - { - m_skContext = make_skia_context(); - if (m_skContext == nullptr) - { - return nullptr; - } - } - - static GrGLFramebufferInfo fbInfo = {}; - fbInfo.fFBOID = 0u; - fbInfo.fFormat = GL_RGBA8; - - GrBackendRenderTarget backendRenderTarget(width, height, 1, 8, fbInfo); - static SkSurfaceProps surfaceProps(0, kUnknown_SkPixelGeometry); - - auto skSurface = SkSurface::MakeFromBackendRenderTarget(m_skContext.get(), - backendRenderTarget, - kBottomLeft_GrSurfaceOrigin, - kRGBA_8888_SkColorType, - nullptr, - &surfaceProps, - nullptr, - nullptr); - - if (!skSurface) - { - LOGE("SkSurface::MakeFromBackendRenderTarget() failed."); - return nullptr; - } - - return skSurface; -} - -void EGLShareThreadState::makeCurrent(EGLSurface eglSurface) -{ - if (eglSurface == m_currentSurface) - { - return; - } - auto ctx = eglSurface == EGL_NO_SURFACE ? EGL_NO_CONTEXT : m_context; - eglMakeCurrent(m_display, eglSurface, eglSurface, ctx); - m_currentSurface = eglSurface; - EGL_ERR_CHECK(); -} - -void EGLShareThreadState::swapBuffers() -{ - eglSwapBuffers(m_display, m_currentSurface); - EGL_ERR_CHECK(); -} -} // namespace rive_android diff --git a/kotlin/src/main/cpp/src/helpers/egl_worker.cpp b/kotlin/src/main/cpp/src/helpers/egl_worker.cpp index 83ad41aa..0fba058c 100644 --- a/kotlin/src/main/cpp/src/helpers/egl_worker.cpp +++ b/kotlin/src/main/cpp/src/helpers/egl_worker.cpp @@ -8,30 +8,34 @@ using namespace rive; namespace rive_android { static std::mutex s_eglWorkerMutex; -static EGLWorker* s_currentEGLWorker = nullptr; +static EGLWorker* s_currentWorkers[2] = {nullptr, nullptr}; -rcp EGLWorker::Current() +rcp EGLWorker::Current(const RendererType rendererType) { std::lock_guard lock(s_eglWorkerMutex); - if (s_currentEGLWorker == nullptr) + int workerIdx = static_cast(rendererType); + if (s_currentWorkers[workerIdx] == nullptr) { - LOGI("Created a new EGLWorker."); - s_currentEGLWorker = new EGLWorker; + LOGI("Created a new EGLWorker with type %s", + rendererType == RendererType::Skia ? "Skia" : "Rive"); + s_currentWorkers[workerIdx] = new EGLWorker(rendererType); } else { LOGI("Referenced an existing EGLWorker."); - s_currentEGLWorker->ref(); + s_currentWorkers[workerIdx]->ref(); } - return rcp(s_currentEGLWorker); + return rcp(s_currentWorkers[workerIdx]); } EGLWorker::~EGLWorker() { std::lock_guard lock(s_eglWorkerMutex); - LOGI("Deleting the current EGLWorker."); - assert(s_currentEGLWorker == this); + LOGI("Deleting the current %s EGLWorker.", + m_RendererType == RendererType::Skia ? "Skia" : "Rive"); + int workerIdx = static_cast(m_RendererType); + assert(s_currentWorkers[workerIdx] == this); terminateThread(); - s_currentEGLWorker = nullptr; + s_currentWorkers[workerIdx] = nullptr; } } // namespace rive_android diff --git a/kotlin/src/main/cpp/src/helpers/general.cpp b/kotlin/src/main/cpp/src/helpers/general.cpp index 8f91be01..fd3d49dc 100644 --- a/kotlin/src/main/cpp/src/helpers/general.cpp +++ b/kotlin/src/main/cpp/src/helpers/general.cpp @@ -1,8 +1,8 @@ #include "jni_refs.hpp" +#include "helpers/android_skia_factory.hpp" #include "helpers/general.hpp" #include "rive/file.hpp" -#include "rive/layout.hpp" -#include "skia_factory.hpp" +#include "rive/pls/pls_factory.hpp" #if defined(DEBUG) || defined(LOG) #include @@ -11,128 +11,28 @@ #include #endif -class AndroidSkiaFactory : public rive::SkiaFactory -{ -public: - std::vector platformDecode(rive::Span span, - rive::SkiaFactory::ImageInfo* info) override - { - auto env = rive_android::getJNIEnv(); - std::vector pixels; - - jclass cls = env->FindClass("app/rive/runtime/kotlin/core/Decoder"); - if (!cls) - { - LOGE("can't find class 'app/rive/runtime/kotlin/core/Decoder'"); - return pixels; - } - - jmethodID method = env->GetStaticMethodID(cls, "decodeToPixels", "([B)[I"); - if (!method) - { - LOGE("can't find static method decodeToPixels"); - return pixels; - } - - jbyteArray encoded = env->NewByteArray(span.size()); - if (!encoded) - { - LOGE("failed to allcoate NewByteArray"); - return pixels; - } - - env->SetByteArrayRegion(encoded, 0, span.size(), (jbyte*)span.data()); - auto jpixels = (jintArray)env->CallStaticObjectMethod(cls, method, encoded); - env->DeleteLocalRef(encoded); // no longer need encoded - - // At ths point, we have the decode results. Now we just need to convert - // it into the form we need (ImageInfo + premul pixels) - - size_t arrayCount = env->GetArrayLength(jpixels); - if (arrayCount < 2) - { - LOGE("bad array length (unexpected)"); - return pixels; - } - - int* rawPixels = env->GetIntArrayElements(jpixels, nullptr); - const uint32_t width = rawPixels[0]; - const uint32_t height = rawPixels[1]; - const size_t pixelCount = (size_t)width * height; - if (pixelCount == 0) - { - LOGE("don't support empty images (zero dimension)"); - return pixels; - } - if (2 + pixelCount < arrayCount) - { - LOGE("not enough elements in pixel array"); - return pixels; - } - - auto div255 = [](unsigned value) { return (value + 128) * 257 >> 16; }; - - pixels.resize(pixelCount * 4); - uint8_t* bytes = pixels.data(); - bool isOpaque = true; - for (size_t i = 0; i < pixelCount; ++i) - { - uint32_t p = rawPixels[2 + i]; - unsigned a = (p >> 24) & 0xFF; - unsigned r = (p >> 16) & 0xFF; - unsigned g = (p >> 8) & 0xFF; - unsigned b = (p >> 0) & 0xFF; - // convert to premul as needed - if (a != 255) - { - r = div255(r * a); - g = div255(g * a); - b = div255(b * a); - isOpaque = false; - } - bytes[0] = r; - bytes[1] = g; - bytes[2] = b; - bytes[3] = a; - bytes += 4; - } - env->ReleaseIntArrayElements(jpixels, rawPixels, 0); - - info->rowBytes = width * 4; // we're snug - info->width = width; - info->height = height; - info->colorType = ColorType::rgba; - info->alphaType = isOpaque ? AlphaType::opaque : AlphaType::premul; - return pixels; - } -}; - -static AndroidSkiaFactory gFactory; - -// luigi: murdered this due to our single renderer model right now...all canvas -// rendering won't work in this branch lets make sure we stich our rive android -// renderers into the rive namespace namespace rive -// { -// RenderPaint *makeRenderPaint() { return new rive_android::JNIRenderPaint(); -// } RenderPath *makeRenderPath() { return new rive_android::JNIRenderPath(); -// } } // namespace rive +/** + * Global factories that are used to instantiate render objects (paths, buffers, textures, etc.) + */ +static AndroidSkiaFactory g_SkiaFactory; +static rive::pls::PLSFactory g_RiveFactory; namespace rive_android { -JavaVM* globalJavaVM; -int sdkVersion; +JavaVM* g_JVM; +long g_sdkVersion; -JNIEnv* getJNIEnv() +JNIEnv* GetJNIEnv() { // double check it's all ok JNIEnv* g_env; - int getEnvStat = globalJavaVM->GetEnv((void**)&g_env, JNI_VERSION_1_6); + int getEnvStat = g_JVM->GetEnv((void**)&g_env, JNI_VERSION_1_6); if (getEnvStat == JNI_EDETACHED) { - // std::cout << "GetEnv: not attached" << std::endl; - if (globalJavaVM->AttachCurrentThread((JNIEnv**)&g_env, NULL) != 0) + LOGW("JVM::GetEnv - Not Attached."); + if (g_JVM->AttachCurrentThread((JNIEnv**)&g_env, NULL) != 0) { - // std::cout << "Failed to attach" << std::endl; + LOGE("Failed to attach current thread."); } } else if (getEnvStat == JNI_OK) @@ -141,35 +41,35 @@ JNIEnv* getJNIEnv() } else if (getEnvStat == JNI_EVERSION) { - // std::cout << "GetEnv: version not supported" << std::endl; + LOGE("JVM::GetEnv: unsupported version %d", getEnvStat); } return g_env; } -void detachThread() +void DetachThread() { - if (!(globalJavaVM->DetachCurrentThread() == JNI_OK)) + if (g_JVM->DetachCurrentThread() != JNI_OK) { - LOGE("Could not detach thread from JVM"); + LOGE("DetachCurrentThread failed."); } } -void logReferenceTables() +void LogReferenceTables() { - jclass vm_class = getJNIEnv()->FindClass("dalvik/system/VMDebug"); - jmethodID dump_mid = getJNIEnv()->GetStaticMethodID(vm_class, "dumpReferenceTables", "()V"); - getJNIEnv()->CallStaticVoidMethod(vm_class, dump_mid); + jclass vm_class = GetJNIEnv()->FindClass("dalvik/system/VMDebug"); + jmethodID dump_mid = GetJNIEnv()->GetStaticMethodID(vm_class, "dumpReferenceTables", "()V"); + GetJNIEnv()->CallStaticVoidMethod(vm_class, dump_mid); } -void setSDKVersion() +void SetSDKVersion() { char sdk_ver_str[255]; __system_property_get("ro.build.version.sdk", sdk_ver_str); - sdkVersion = atoi(sdk_ver_str); + g_sdkVersion = strtol(sdk_ver_str, NULL, 10); } -rive::Fit getFit(JNIEnv* env, jobject jfit) +rive::Fit GetFit(JNIEnv* env, jobject jfit) { - jstring fitValue = (jstring)env->CallObjectMethod(jfit, rive_android::getFitNameMethodId()); + jstring fitValue = (jstring)env->CallObjectMethod(jfit, rive_android::GetFitNameMethodId()); const char* fitValueNative = env->GetStringUTFChars(fitValue, 0); rive::Fit fit = rive::Fit::none; @@ -206,10 +106,10 @@ rive::Fit getFit(JNIEnv* env, jobject jfit) return fit; } -rive::Alignment getAlignment(JNIEnv* env, jobject jalignment) +rive::Alignment GetAlignment(JNIEnv* env, jobject jalignment) { jstring alignmentValue = - (jstring)env->CallObjectMethod(jalignment, rive_android::getAlignmentNameMethodId()); + (jstring)env->CallObjectMethod(jalignment, rive_android::GetAlignmentNameMethodId()); const char* alignmentValueNative = env->GetStringUTFChars(alignmentValue, 0); rive::Alignment alignment = rive::Alignment::center; @@ -254,38 +154,49 @@ rive::Alignment getAlignment(JNIEnv* env, jobject jalignment) return alignment; } -long import(uint8_t* bytes, jint length) +long Import(uint8_t* bytes, jint length, RendererType rendererType) { rive::ImportResult result; - auto file = - rive::File::import(rive::Span(bytes, length), &gFactory, &result).release(); + rive::Factory* fileFactory = rendererType == RendererType::Skia + ? static_cast(&g_SkiaFactory) + : static_cast(&g_RiveFactory); + rive::File* file = + rive::File::import(rive::Span(bytes, length), fileFactory, &result) + .release(); if (result == rive::ImportResult::success) { return (long)file; } else if (result == rive::ImportResult::unsupportedVersion) { - return throwUnsupportedRuntimeVersionException("Unsupported Rive File Version."); + return ThrowUnsupportedRuntimeVersionException("Unsupported Rive File Version."); } else if (result == rive::ImportResult::malformed) { - return throwMalformedFileException("Malformed Rive File."); + return ThrowMalformedFileException("Malformed Rive File."); } else { - return throwRiveException("Unknown error loading file."); + return ThrowRiveException("Unknown error loading file."); } } -std::string jstring2string(JNIEnv* env, jstring jStr) +std::string JStringToString(JNIEnv* env, jstring jStr) { + if (jStr == nullptr) + { + return std::string(); + } const char* cstr = env->GetStringUTFChars(jStr, NULL); std::string str = std::string(cstr); env->ReleaseStringUTFChars(jStr, cstr); return str; } + +int SizeTTOInt(size_t sizeT) { return sizeT > INT_MAX ? INT_MAX : static_cast(sizeT); } + #if defined(DEBUG) || defined(LOG) -void logThread() +[[noreturn]] void LogThread() { int pipes[2]; pipe(pipes); diff --git a/kotlin/src/main/cpp/src/helpers/thread_state_egl.cpp b/kotlin/src/main/cpp/src/helpers/thread_state_egl.cpp new file mode 100644 index 00000000..55807b62 --- /dev/null +++ b/kotlin/src/main/cpp/src/helpers/thread_state_egl.cpp @@ -0,0 +1,139 @@ +#include +#include + +#include "helpers/thread_state_egl.hpp" + +#include "GrDirectContext.h" +#include "gl/GrGLInterface.h" +#include "gl/GrGLAssembleInterface.h" +#include "SkSurface.h" + +namespace rive_android +{ +static bool config_has_attribute(EGLDisplay display, + EGLConfig config, + EGLint attribute, + EGLint value) +{ + EGLint outValue = 0; + EGLBoolean result = eglGetConfigAttrib(display, config, attribute, &outValue); + EGL_ERR_CHECK(); + return result && (outValue == value); +} + +EGLThreadState::EGLThreadState() +{ + m_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (m_display == EGL_NO_DISPLAY) + { + EGL_ERR_CHECK(); + LOGE("eglGetDisplay() failed."); + return; + } + + if (!eglInitialize(m_display, 0, 0)) + { + EGL_ERR_CHECK(); + LOGE("eglInitialize() failed."); + return; + } + + const EGLint configAttributes[] = {EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES2_BIT, + EGL_BLUE_SIZE, + 8, + EGL_GREEN_SIZE, + 8, + EGL_RED_SIZE, + 8, + EGL_DEPTH_SIZE, + 0, + EGL_STENCIL_SIZE, + 8, + EGL_ALPHA_SIZE, + 8, + EGL_NONE}; + + EGLint num_configs = 0; + if (!eglChooseConfig(m_display, configAttributes, nullptr, 0, &num_configs)) + { + EGL_ERR_CHECK(); + LOGE("eglChooseConfig() didn't find any (%d)", num_configs); + return; + } + + std::vector supportedConfigs(static_cast(num_configs)); + eglChooseConfig(m_display, + configAttributes, + supportedConfigs.data(), + num_configs, + &num_configs); + EGL_ERR_CHECK(); + + // Choose a config, either a match if possible or the first config otherwise + const auto configMatches = [&](EGLConfig config) { + if (!config_has_attribute(m_display, m_config, EGL_RED_SIZE, 8)) + return false; + if (!config_has_attribute(m_display, m_config, EGL_GREEN_SIZE, 8)) + return false; + if (!config_has_attribute(m_display, m_config, EGL_BLUE_SIZE, 8)) + return false; + if (!config_has_attribute(m_display, m_config, EGL_STENCIL_SIZE, 8)) + return false; + return config_has_attribute(m_display, m_config, EGL_DEPTH_SIZE, 0); + }; + + const auto configIter = + std::find_if(supportedConfigs.cbegin(), supportedConfigs.cend(), configMatches); + + m_config = (configIter != supportedConfigs.cend()) ? *configIter : supportedConfigs[0]; + + const EGLint contextAttributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; + + m_context = eglCreateContext(m_display, m_config, nullptr, contextAttributes); + if (m_context == EGL_NO_CONTEXT) + { + LOGE("eglCreateContext() failed."); + EGL_ERR_CHECK(); + } +} + +EGLThreadState::~EGLThreadState() +{ + LOGD("EGLThreadState getting destroyed! ๐Ÿงจ"); + + if (m_context != EGL_NO_CONTEXT) + { + eglDestroyContext(m_display, m_context); + EGL_ERR_CHECK(); + } + + eglReleaseThread(); + EGL_ERR_CHECK(); + + if (m_display != EGL_NO_DISPLAY) + { + eglTerminate(m_display); + EGL_ERR_CHECK(); + } +} + +EGLSurface EGLThreadState::createEGLSurface(ANativeWindow* window) +{ + if (!window) + { + return EGL_NO_SURFACE; + } + + LOGD("eglCreateWindowSurface()"); + auto res = eglCreateWindowSurface(m_display, m_config, window, nullptr); + EGL_ERR_CHECK(); + return res; +} + +void EGLThreadState::swapBuffers() +{ + eglSwapBuffers(m_display, m_currentSurface); + EGL_ERR_CHECK(); +} +} // namespace rive_android diff --git a/kotlin/src/main/cpp/src/helpers/thread_state_pls.cpp b/kotlin/src/main/cpp/src/helpers/thread_state_pls.cpp new file mode 100644 index 00000000..7748f34a --- /dev/null +++ b/kotlin/src/main/cpp/src/helpers/thread_state_pls.cpp @@ -0,0 +1,68 @@ +// +// Created by Umberto Sonnino on 7/13/23. +// +#include "helpers/thread_state_pls.hpp" + +namespace rive_android +{ +void PLSThreadState::destroySurface(EGLSurface eglSurface) +{ + if (eglSurface == EGL_NO_SURFACE) + { + return; + } + + if (m_currentSurface == eglSurface) + { + m_ownsCurrentSurface = true; + return; + } + eglDestroySurface(m_display, eglSurface); + EGL_ERR_CHECK(); +} + +void PLSThreadState::makeCurrent(EGLSurface eglSurface) +{ + if (eglSurface == m_currentSurface) + { + return; + } + + if (eglSurface == EGL_NO_SURFACE) + { + LOGE("Cannot make EGL_NO_SURFACE current"); + return; + } + eglMakeCurrent(m_display, eglSurface, eglSurface, m_context); + + if (m_ownsCurrentSurface) + { + eglDestroySurface(m_display, m_currentSurface); + EGL_ERR_CHECK(); + m_ownsCurrentSurface = false; + } + m_currentSurface = eglSurface; + EGL_ERR_CHECK(); + + if (m_plsContext == nullptr) + { + m_plsContext = rive::pls::PLSRenderContextGL::Make(); + } +} + +void PLSThreadState::releaseContext() +{ + if (m_plsContext.get()) + { + assert(m_currentSurface != EGL_NO_SURFACE); + m_plsContext.reset(); + } + + if (m_ownsCurrentSurface) + { + eglDestroySurface(m_display, m_currentSurface); + EGL_ERR_CHECK(); + } +} + +} // namespace rive_android diff --git a/kotlin/src/main/cpp/src/helpers/thread_state_skia.cpp b/kotlin/src/main/cpp/src/helpers/thread_state_skia.cpp new file mode 100644 index 00000000..a002303f --- /dev/null +++ b/kotlin/src/main/cpp/src/helpers/thread_state_skia.cpp @@ -0,0 +1,120 @@ +#include "helpers/thread_state_egl.hpp" +#include "helpers/thread_state_skia.hpp" +#include "gl/GrGLAssembleInterface.h" + +namespace rive_android +{ +static sk_sp make_skia_context() +{ + LOGI("c_version()"); + auto c_version = reinterpret_cast(glGetString(GL_VERSION)); + if (c_version == nullptr) + { + EGL_ERR_CHECK(); + LOGE("c_version failed"); + return nullptr; + } + + auto get_proc = [](void* context, const char name[]) -> GrGLFuncPtr { + return reinterpret_cast(eglGetProcAddress(name)); + }; + std::string version(c_version); + auto interface = version.find("OpenGL ES") == std::string::npos + ? GrGLMakeAssembledGLInterface(nullptr, get_proc) + : GrGLMakeAssembledGLESInterface(nullptr, get_proc); + LOGI("OpenGL Version %s", version.c_str()); + if (!interface) + { + LOGE("GrGLMakeAssembledGL(ES)Interface failed."); + return nullptr; + } + auto ctx = GrDirectContext::MakeGL(interface); + LOGI("Skia Context Created %p", ctx.get()); + return ctx; +} + +void SkiaThreadState::releaseContext() +{ + // Release Skia Context if has been init'd. + if (m_skContext.get()) + { + m_skContext->abandonContext(); + m_skContext.reset(nullptr); + } +} + +sk_sp SkiaThreadState::createSkiaSurface(EGLSurface eglSurface, int width, int height) +{ + // Width/Height getters return negative values on error. + // Probably a race condition with surfaces being reclaimed by the OS before + // this function completes. + if (width < 0 || height < 0) + { + LOGE("Window is unavailable."); + return nullptr; + } + + makeCurrent(eglSurface); + + LOGI("Set up window surface %dx%d", width, height); + if (m_skContext == nullptr) + { + m_skContext = make_skia_context(); + if (m_skContext == nullptr) + { + return nullptr; + } + } + + static GrGLFramebufferInfo fbInfo = {}; + fbInfo.fFBOID = 0u; + fbInfo.fFormat = GL_RGBA8; + + GrBackendRenderTarget backendRenderTarget(width, height, 1, 8, fbInfo); + static SkSurfaceProps surfaceProps(0, kUnknown_SkPixelGeometry); + + auto skSurface = SkSurface::MakeFromBackendRenderTarget(m_skContext.get(), + backendRenderTarget, + kBottomLeft_GrSurfaceOrigin, + kRGBA_8888_SkColorType, + nullptr, + &surfaceProps, + nullptr, + nullptr); + + if (!skSurface) + { + LOGE("SkSurface::MakeFromBackendRenderTarget() failed."); + return nullptr; + } + + return skSurface; +} + +void SkiaThreadState::destroySurface(EGLSurface eglSurface) +{ + if (eglSurface == EGL_NO_SURFACE) + { + return; + } + + if (m_currentSurface == eglSurface) + { + makeCurrent(EGL_NO_SURFACE); + } + eglDestroySurface(m_display, eglSurface); + EGL_ERR_CHECK(); +} + +void SkiaThreadState::makeCurrent(EGLSurface eglSurface) +{ + if (eglSurface == m_currentSurface) + { + return; + } + auto ctx = eglSurface == EGL_NO_SURFACE ? EGL_NO_CONTEXT : m_context; + eglMakeCurrent(m_display, eglSurface, eglSurface, ctx); + m_currentSurface = eglSurface; + EGL_ERR_CHECK(); +} +} // namespace rive_android diff --git a/kotlin/src/main/cpp/src/helpers/worker_thread.cpp b/kotlin/src/main/cpp/src/helpers/worker_thread.cpp new file mode 100644 index 00000000..404be746 --- /dev/null +++ b/kotlin/src/main/cpp/src/helpers/worker_thread.cpp @@ -0,0 +1,21 @@ +#include "helpers/thread_state_skia.hpp" +#include "helpers/thread_state_pls.hpp" +#include "helpers/worker_thread.hpp" + +namespace rive_android +{ +std::unique_ptr WorkerThread::MakeThreadState(const RendererType type) +{ + std::unique_ptr threadState; + if (type == RendererType::Skia) + { + threadState = std::make_unique(); + } + else + { + threadState = std::make_unique(); + } + + return threadState; +} +} // namespace rive_android diff --git a/kotlin/src/main/cpp/src/jni_refs.cpp b/kotlin/src/main/cpp/src/jni_refs.cpp index 0a5f9e4a..1ef7f7e0 100644 --- a/kotlin/src/main/cpp/src/jni_refs.cpp +++ b/kotlin/src/main/cpp/src/jni_refs.cpp @@ -7,10 +7,10 @@ namespace rive_android { -jclass getClass(const char* name) { return getJNIEnv()->FindClass(name); } +jclass getClass(const char* name) { return GetJNIEnv()->FindClass(name); } jmethodID getMethodId(jclass clazz, const char* name, const char* sig) { - JNIEnv* env = getJNIEnv(); + JNIEnv* env = GetJNIEnv(); jmethodID output = env->GetMethodID(clazz, name, sig); env->DeleteLocalRef(clazz); return output; @@ -18,7 +18,7 @@ jmethodID getMethodId(jclass clazz, const char* name, const char* sig) jfieldID getStaticFieldId(jclass clazz, const char* name, const char* sig) { - JNIEnv* env = getJNIEnv(); + JNIEnv* env = GetJNIEnv(); jfieldID output = env->GetStaticFieldID(clazz, name, sig); env->DeleteLocalRef(clazz); return output; @@ -26,71 +26,71 @@ jfieldID getStaticFieldId(jclass clazz, const char* name, const char* sig) jfieldID getFieldId(jclass clazz, const char* name, const char* sig) { - JNIEnv* env = getJNIEnv(); + JNIEnv* env = GetJNIEnv(); jfieldID output = env->GetFieldID(clazz, name, sig); env->DeleteLocalRef(clazz); return output; } -jint throwRiveException(const char* message) +jint ThrowRiveException(const char* message) { jclass exClass = getClass("app/rive/runtime/kotlin/core/errors/RiveException"); - return getJNIEnv()->ThrowNew(exClass, message); + return GetJNIEnv()->ThrowNew(exClass, message); } -jint throwMalformedFileException(const char* message) +jint ThrowMalformedFileException(const char* message) { jclass exClass = getClass("app/rive/runtime/kotlin/core/errors/MalformedFileException"); - return getJNIEnv()->ThrowNew(exClass, message); + return GetJNIEnv()->ThrowNew(exClass, message); } -jint throwUnsupportedRuntimeVersionException(const char* message) +jint ThrowUnsupportedRuntimeVersionException(const char* message) { jclass exClass = getClass("app/rive/runtime/kotlin/core/errors/" "UnsupportedRuntimeVersionException"); - return getJNIEnv()->ThrowNew(exClass, message); + return GetJNIEnv()->ThrowNew(exClass, message); } -jclass getFitClass() { return getClass("app/rive/runtime/kotlin/core/Fit"); }; -jmethodID getFitNameMethodId() +jclass GetFitClass() { return getClass("app/rive/runtime/kotlin/core/Fit"); }; +jmethodID GetFitNameMethodId() { - return getMethodId(getFitClass(), "name", "()Ljava/lang/String;"); + return getMethodId(GetFitClass(), "name", "()Ljava/lang/String;"); } -jclass getAlignmentClass() { return getClass("app/rive/runtime/kotlin/core/Alignment"); } -jmethodID getAlignmentNameMethodId() +jclass GetAlignmentClass() { return getClass("app/rive/runtime/kotlin/core/Alignment"); } +jmethodID GetAlignmentNameMethodId() { - return getMethodId(getAlignmentClass(), "name", "()Ljava/lang/String;"); + return getMethodId(GetAlignmentClass(), "name", "()Ljava/lang/String;"); }; -jclass getLoopClass() { return getClass("app/rive/runtime/kotlin/core/Loop"); }; +jclass GetLoopClass() { return getClass("app/rive/runtime/kotlin/core/Loop"); }; -jfieldID getNoneLoopField() +jfieldID GetNoneLoopField() { - return getStaticFieldId(getLoopClass(), "NONE", "Lapp/rive/runtime/kotlin/core/Loop;"); + return getStaticFieldId(GetLoopClass(), "NONE", "Lapp/rive/runtime/kotlin/core/Loop;"); }; -jfieldID getOneShotLoopField() +jfieldID GetOneShotLoopField() { - return getStaticFieldId(getLoopClass(), "ONESHOT", "Lapp/rive/runtime/kotlin/core/Loop;"); + return getStaticFieldId(GetLoopClass(), "ONESHOT", "Lapp/rive/runtime/kotlin/core/Loop;"); }; -jfieldID getLoopLoopField() +jfieldID GetLoopLoopField() { - return getStaticFieldId(getLoopClass(), "LOOP", "Lapp/rive/runtime/kotlin/core/Loop;"); + return getStaticFieldId(GetLoopClass(), "LOOP", "Lapp/rive/runtime/kotlin/core/Loop;"); }; -jfieldID getPingPongLoopField() +jfieldID GetPingPongLoopField() { - return getStaticFieldId(getLoopClass(), "PINGPONG", "Lapp/rive/runtime/kotlin/core/Loop;"); + return getStaticFieldId(GetLoopClass(), "PINGPONG", "Lapp/rive/runtime/kotlin/core/Loop;"); }; -jclass getPointerFClass() { return getClass("android/graphics/PointF"); }; +jclass GetPointerFClass() { return getClass("android/graphics/PointF"); }; -jfieldID getXFieldId() { return getFieldId(getPointerFClass(), "x", "F"); } +jfieldID GetXFieldId() { return getFieldId(GetPointerFClass(), "x", "F"); } -jfieldID getYFieldId() { return getFieldId(getPointerFClass(), "y", "F"); } +jfieldID GetYFieldId() { return getFieldId(GetPointerFClass(), "y", "F"); } -jmethodID getPointFInitMethod() { return getMethodId(getPointerFClass(), "", "(FF)V"); }; +jmethodID GetPointFInitMethod() { return getMethodId(GetPointerFClass(), "", "(FF)V"); }; static const char* AABBFieldNames[] = {"left", "top", "right", "bottom"}; -rive::AABB rectFToAABB(JNIEnv* env, jobject rectf) +rive::AABB RectFToAABB(JNIEnv* env, jobject rectf) { auto cls = env->FindClass("android/graphics/RectF"); float values[4]; @@ -102,7 +102,7 @@ rive::AABB rectFToAABB(JNIEnv* env, jobject rectf) return rive::AABB(values[0], values[1], values[2], values[3]); } -void aabbToRectF(JNIEnv* env, const rive::AABB& aabb, jobject rectf) +void AABBToRectF(JNIEnv* env, const rive::AABB& aabb, jobject rectf) { auto cls = env->FindClass("android/graphics/RectF"); const float values[4] = {aabb.left(), aabb.top(), aabb.right(), aabb.bottom()}; diff --git a/kotlin/src/main/cpp/src/models/jni_renderer.cpp b/kotlin/src/main/cpp/src/models/jni_renderer.cpp new file mode 100644 index 00000000..b2f36c58 --- /dev/null +++ b/kotlin/src/main/cpp/src/models/jni_renderer.cpp @@ -0,0 +1,168 @@ +#include +#include +#include +#include +#include +#include + +#include "models/jni_renderer.hpp" + +#include "GrBackendSurface.h" +#include "GrDirectContext.h" +#include "SkCanvas.h" +#include "SkSurface.h" +#include "SkImageInfo.h" +#include "gl/GrGLInterface.h" +#include "gl/GrGLAssembleInterface.h" + +using namespace std::chrono_literals; + +namespace rive_android +{ +JNIRenderer::JNIRenderer(jobject ktObject, + bool trace /* = false */, + const RendererType rendererType /* = RendererType::Skia */) : + // Grab a Global Ref to prevent Garbage Collection to clean up the object + // from under us since the destructor will be called from the render thread + // rather than the UI thread. + mWorker(EGLWorker::Current(rendererType)), + m_ktRenderer(GetJNIEnv()->NewGlobalRef(ktObject)), + m_tracer(getTracer(trace)) +{} + +JNIRenderer::~JNIRenderer() +{ + // Delete the worker thread objects. And since the worker has captured our "this" pointer, + // wait for it to finish processing our work before continuing the destruction process. + mWorker->runAndWait([=](EGLThreadState* threadState) { + if (!m_workerImpl) + return; + m_workerImpl->destroy(threadState); + }); + + // Clean up dependencies. + auto env = GetJNIEnv(); + jclass ktClass = env->GetObjectClass(m_ktRenderer); + auto disposeDeps = env->GetMethodID(ktClass, "disposeDependencies", "()V"); + env->CallVoidMethod(m_ktRenderer, disposeDeps); + + env->DeleteGlobalRef(m_ktRenderer); + if (m_tracer) + { + delete m_tracer; + } + + if (m_window != nullptr) + { + ANativeWindow_release(m_window); + } +} + +void JNIRenderer::setWindow(ANativeWindow* window) +{ + if (m_window != nullptr) + { + ANativeWindow_release(m_window); + } + m_window = window; + if (m_window != nullptr) + { + ANativeWindow_acquire(m_window); + } + mWorker->run([=](EGLThreadState* threadState) { + m_workerThreadID = std::this_thread::get_id(); + if (m_workerImpl) + { + m_workerImpl->destroy(threadState); + m_workerImpl.reset(); + } + if (m_window) + { + m_workerImpl = WorkerImpl::Make(m_window, threadState, mWorker->rendererType()); + } + }); +} + +rive::Renderer* JNIRenderer::getRendererOnWorkerThread() const +{ + assert(std::this_thread::get_id() == m_workerThreadID); + if (std::this_thread::get_id() != m_workerThreadID) + return nullptr; + if (!m_workerImpl) + return nullptr; + return m_workerImpl->renderer(); +} + +void JNIRenderer::start(long long timeNs) +{ + mWorker->run([=](EGLThreadState* threadState) { + if (!m_workerImpl) + return; + m_workerImpl->start(m_ktRenderer, timeNs); + }); + mLastFrameTime = std::chrono::steady_clock::now(); +} + +void JNIRenderer::stop() +{ + mWorker->run([=](EGLThreadState* threadState) { + if (!m_workerImpl) + return; + m_workerImpl->stop(); + }); +} + +void JNIRenderer::doFrame(long long frameTimeNs) +{ + mWorker->waitUntilComplete(m_workIDForLastFrame); + m_workIDForLastFrame = mWorker->run([=](EGLThreadState* threadState) { + if (!m_workerImpl) + return; + m_workerImpl->doFrame(m_tracer, threadState, m_ktRenderer, frameTimeNs); + }); + calculateFps(); +} + +ITracer* JNIRenderer::getTracer(bool trace) const +{ + if (!trace) + { + return new NoopTracer(); + } + + bool traceAvailable = android_get_device_api_level() >= 23; + if (traceAvailable) + { + return new Tracer(); + } + else + { + LOGE("JNIRenderer cannot enable tracing on API <23. Api " + "version is %d", + android_get_device_api_level()); + return new NoopTracer(); + } +} + +/** + * Calculate FPS over an average of 10 samples + */ +void JNIRenderer::calculateFps() +{ + m_tracer->beginSection("calculateFps()"); + static constexpr int FPS_SAMPLES = 10; + + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + + mFpsSum += 1.0f / ((now - mLastFrameTime).count() / 1e9f); + mFpsCount++; + if (mFpsCount == FPS_SAMPLES) + { + mAverageFps = mFpsSum / mFpsCount; + mFpsSum = 0; + mFpsCount = 0; + } + mLastFrameTime = now; + m_tracer->endSection(); +} +} // namespace rive_android diff --git a/kotlin/src/main/cpp/src/models/jni_renderer_skia.cpp b/kotlin/src/main/cpp/src/models/jni_renderer_skia.cpp deleted file mode 100644 index 07b3c3c8..00000000 --- a/kotlin/src/main/cpp/src/models/jni_renderer_skia.cpp +++ /dev/null @@ -1,291 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "models/jni_renderer_skia.hpp" - -#include "GrBackendSurface.h" -#include "GrDirectContext.h" -#include "SkCanvas.h" -#include "SkSurface.h" -#include "SkImageInfo.h" -#include "gl/GrGLInterface.h" -#include "gl/GrGLAssembleInterface.h" - -using namespace std::chrono_literals; - -namespace rive_android -{ -// Class that executes on JNIRendererSkia's worker thread. -class JNIRendererSkia::WorkerSideImpl -{ -public: - static std::unique_ptr Make(ANativeWindow* window, - EGLShareThreadState* threadState) - { - bool success; - std::unique_ptr impl(new WorkerSideImpl(window, threadState, &success)); - if (!success) - { - impl->destroy(threadState); - impl.reset(); - } - return impl; - } - - ~WorkerSideImpl() - { - // Call destroy() fist! - assert(!m_isStarted); - assert(m_eglSurface == EGL_NO_SURFACE); - } - - void destroy(EGLShareThreadState* threadState) - { - m_skRenderer.reset(); - m_skSurface.reset(); - if (m_eglSurface != EGL_NO_SURFACE) - { - threadState->destroySurface(m_eglSurface); - m_eglSurface = EGL_NO_SURFACE; - } - } - - rive::SkiaRenderer* skRenderer() const { return m_skRenderer.get(); } - - void start(jobject ktRenderer, long timeNs) - { - auto env = getJNIEnv(); - jclass ktClass = getJNIEnv()->GetObjectClass(ktRenderer); - m_ktRendererClass = reinterpret_cast(env->NewWeakGlobalRef(ktClass)); - m_ktDrawCallback = env->GetMethodID(m_ktRendererClass, "draw", "()V"); - m_ktAdvanceCallback = env->GetMethodID(m_ktRendererClass, "advance", "(F)V"); - mLastFrameTimeNs = timeNs; - m_isStarted = true; - } - - void stop() - { - auto env = getJNIEnv(); - if (m_ktRendererClass != nullptr) - { - env->DeleteWeakGlobalRef(m_ktRendererClass); - } - m_ktRendererClass = nullptr; - m_ktDrawCallback = nullptr; - m_ktAdvanceCallback = nullptr; - m_isStarted = false; - } - - void doFrame(ITracer* tracer, - EGLShareThreadState* threadState, - jobject ktRenderer, - long frameTimeNs) - { - if (!m_isStarted) - { - return; - } - - float elapsedMs = (frameTimeNs - mLastFrameTimeNs) * 1e-9f; - mLastFrameTimeNs = frameTimeNs; - - auto env = getJNIEnv(); - env->CallVoidMethod(ktRenderer, m_ktAdvanceCallback, elapsedMs); - - tracer->beginSection("draw()"); - - // Bind context to this thread. - threadState->makeCurrent(m_eglSurface); - m_skSurface->getCanvas()->clear(SkColor((0x00000000))); - // Kotlin callback. - env->CallVoidMethod(ktRenderer, m_ktDrawCallback); - - tracer->beginSection("flush()"); - m_skSurface->flushAndSubmit(); - tracer->endSection(); // flush - - tracer->beginSection("swapBuffers()"); - threadState->swapBuffers(); - - tracer->endSection(); // swapBuffers - tracer->endSection(); // draw() - } - -private: - WorkerSideImpl(ANativeWindow* window, EGLShareThreadState* threadState, bool* success) - { - *success = false; - m_eglSurface = threadState->createEGLSurface(window); - if (m_eglSurface == EGL_NO_SURFACE) - return; - m_skSurface = threadState->createSkiaSurface(m_eglSurface, - ANativeWindow_getWidth(window), - ANativeWindow_getHeight(window)); - if (m_skSurface == nullptr) - return; - m_skRenderer = std::make_unique(m_skSurface->getCanvas()); - *success = true; - } - - EGLSurface m_eglSurface = EGL_NO_SURFACE; - sk_sp m_skSurface; - std::unique_ptr m_skRenderer; - - jclass m_ktRendererClass = nullptr; - jmethodID m_ktDrawCallback = nullptr; - jmethodID m_ktAdvanceCallback = nullptr; - long mLastFrameTimeNs = 0; // TODO: this should be a std::chrono::time_point, or at least 64 - // bits. - bool m_isStarted = false; -}; - -JNIRendererSkia::JNIRendererSkia(jobject ktObject, bool trace) : - // Grab a Global Ref to prevent Garbage Collection to clean up the object - // from under us since the destructor will be called from the render thread - // rather than the UI thread. - m_ktRenderer(getJNIEnv()->NewGlobalRef(ktObject)), - m_tracer(getTracer(trace)) -{} - -JNIRendererSkia::~JNIRendererSkia() -{ - // Delete the worker thread objects. And since the worker has captured our "this" pointer, - // wait for it to finish processing our work before continuing the destruction process. - mWorker->runAndWait([=](EGLShareThreadState* threadState) { - if (!m_workerSideImpl) - return; - m_workerSideImpl->destroy(threadState); - }); - - // Clean up dependencies. - auto env = getJNIEnv(); - jclass ktClass = env->GetObjectClass(m_ktRenderer); - auto disposeDeps = env->GetMethodID(ktClass, "disposeDependencies", "()V"); - env->CallVoidMethod(m_ktRenderer, disposeDeps); - - env->DeleteGlobalRef(m_ktRenderer); - if (m_tracer) - { - delete m_tracer; - } - - if (m_window != nullptr) - { - ANativeWindow_release(m_window); - } -} - -void JNIRendererSkia::setWindow(ANativeWindow* window) -{ - if (m_window != nullptr) - { - ANativeWindow_release(m_window); - } - m_window = window; - if (m_window != nullptr) - { - ANativeWindow_acquire(m_window); - } - mWorker->run([=](EGLShareThreadState* threadState) { - m_workerThreadID = std::this_thread::get_id(); - if (m_workerSideImpl) - { - m_workerSideImpl->destroy(threadState); - m_workerSideImpl.reset(); - } - if (m_window) - { - m_workerSideImpl = WorkerSideImpl::Make(m_window, threadState); - } - }); -} - -rive::Renderer* JNIRendererSkia::getRendererOnWorkerThread() const -{ - assert(std::this_thread::get_id() == m_workerThreadID); - if (std::this_thread::get_id() != m_workerThreadID) - return nullptr; - if (!m_workerSideImpl) - return nullptr; - return m_workerSideImpl->skRenderer(); -} - -void JNIRendererSkia::start(long timeNs) -{ - mWorker->run([=](EGLShareThreadState* threadState) { - if (!m_workerSideImpl) - return; - m_workerSideImpl->start(m_ktRenderer, timeNs); - }); - mLastFrameTime = std::chrono::steady_clock::now(); -} - -void JNIRendererSkia::stop() -{ - // TODO: There is something wrong here when onVisibilityChanged() is called. - // Stop immediately - mWorker->run([=](EGLShareThreadState* threadState) { - if (!m_workerSideImpl) - return; - m_workerSideImpl->stop(); - }); -} - -void JNIRendererSkia::doFrame(long frameTimeNs) -{ - mWorker->waitUntilComplete(m_workIDForLastFrame); - m_workIDForLastFrame = mWorker->run([=](EGLShareThreadState* threadState) { - if (!m_workerSideImpl) - return; - m_workerSideImpl->doFrame(m_tracer, threadState, m_ktRenderer, frameTimeNs); - }); - calculateFps(); -} - -ITracer* JNIRendererSkia::getTracer(bool trace) const -{ - if (!trace) - { - return new NoopTracer(); - } - - bool traceAvailable = android_get_device_api_level() >= 23; - if (traceAvailable) - { - return new Tracer(); - } - else - { - LOGE("JNIRendererSkia cannot enable tracing on API <23. Api " - "version is %d", - android_get_device_api_level()); - return new NoopTracer(); - } -} - -/** - * Calculate FPS over an average of 10 samples - */ -void JNIRendererSkia::calculateFps() -{ - m_tracer->beginSection("calculateFps()"); - static constexpr int FPS_SAMPLES = 10; - - std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); - - mFpsSum += 1.0f / ((now - mLastFrameTime).count() / 1e9f); - mFpsCount++; - if (mFpsCount == FPS_SAMPLES) - { - mAverageFps = mFpsSum / mFpsCount; - mFpsSum = 0; - mFpsCount = 0; - } - mLastFrameTime = now; - m_tracer->endSection(); -} -} // namespace rive_android \ No newline at end of file diff --git a/kotlin/src/main/cpp/src/models/worker_impl.cpp b/kotlin/src/main/cpp/src/models/worker_impl.cpp new file mode 100644 index 00000000..662e4780 --- /dev/null +++ b/kotlin/src/main/cpp/src/models/worker_impl.cpp @@ -0,0 +1,140 @@ + +#include "models/worker_impl.hpp" + +namespace rive_android +{ +std::unique_ptr WorkerImpl::Make(struct ANativeWindow* window, + EGLThreadState* threadState, + const RendererType type) +{ + bool success; + std::unique_ptr impl; + if (type == RendererType::Skia) + { + impl = std::make_unique(window, threadState, &success); + } + else + { + impl = std::make_unique(window, threadState, &success); + } + if (!success) + { + impl->destroy(threadState); + impl.reset(); + } + return impl; +} + +void WorkerImpl::start(jobject ktRenderer, long long timeNs) +{ + auto env = GetJNIEnv(); + jclass ktClass = GetJNIEnv()->GetObjectClass(ktRenderer); + m_ktRendererClass = reinterpret_cast(env->NewWeakGlobalRef(ktClass)); + m_ktDrawCallback = env->GetMethodID(m_ktRendererClass, "draw", "()V"); + m_ktAdvanceCallback = env->GetMethodID(m_ktRendererClass, "advance", "(F)V"); + mLastFrameTimeNs = timeNs; + m_isStarted = true; +} + +void WorkerImpl::stop() +{ + auto env = GetJNIEnv(); + if (m_ktRendererClass != nullptr) + { + env->DeleteWeakGlobalRef(m_ktRendererClass); + } + m_ktRendererClass = nullptr; + m_ktDrawCallback = nullptr; + m_ktAdvanceCallback = nullptr; + m_isStarted = false; +} + +void WorkerImpl::doFrame(ITracer* tracer, + EGLThreadState* threadState, + jobject ktRenderer, + long frameTimeNs) +{ + if (!m_isStarted) + { + return; + } + + float elapsedMs = (frameTimeNs - mLastFrameTimeNs) * 1e-9f; + mLastFrameTimeNs = frameTimeNs; + + auto env = GetJNIEnv(); + env->CallVoidMethod(ktRenderer, m_ktAdvanceCallback, elapsedMs); + + tracer->beginSection("draw()"); + + // Bind context to this thread. + threadState->makeCurrent(m_eglSurface); + clear(threadState); + // Kotlin callback. + env->CallVoidMethod(ktRenderer, m_ktDrawCallback); + + tracer->beginSection("flush()"); + flush(threadState); + tracer->endSection(); // flush + + tracer->beginSection("swapBuffers()"); + threadState->swapBuffers(); + + tracer->endSection(); // swapBuffers + tracer->endSection(); // draw() +} + +/* SkiaWorkerImpl */ +void SkiaWorkerImpl::destroy(EGLThreadState* threadState) +{ + m_skRenderer.reset(); + m_skSurface.reset(); + WorkerImpl::destroy(threadState); +} + +void SkiaWorkerImpl::clear(EGLThreadState* threadState) +{ + m_skSurface->getCanvas()->clear(uint32_t((0x00000000))); +} + +void SkiaWorkerImpl::flush(EGLThreadState* threadState) { m_skSurface->flushAndSubmit(); } + +rive::Renderer* SkiaWorkerImpl::renderer() const { return m_skRenderer.get(); } + +/* PLSWorkerImpl */ +void PLSWorkerImpl::destroy(EGLThreadState* threadState) +{ + m_plsRenderer.reset(); + m_plsRenderTarget.reset(); + WorkerImpl::destroy(threadState); +} + +void PLSWorkerImpl::clear(EGLThreadState* threadState) +{ + PLSThreadState* plsThreadState = PLSWorkerImpl::PlsThreadState(threadState); + rive::pls::PLSRenderContextGL* plsContext = plsThreadState->plsContext(); + rive::pls::PLSRenderContext::FrameDescriptor frameDescriptor; + frameDescriptor.renderTarget = m_plsRenderTarget; + frameDescriptor.loadAction = rive::pls::PLSRenderContext::LoadAction::clear; + frameDescriptor.clearColor = 0; + plsContext->beginFrame(std::move(frameDescriptor)); +} + +void PLSWorkerImpl::flush(EGLThreadState* threadState) +{ + PLSThreadState* plsThreadState = PLSWorkerImpl::PlsThreadState(threadState); + rive::pls::PLSRenderContextGL* plsContext = plsThreadState->plsContext(); + plsContext->flush(); + if (m_plsRenderTarget->drawFramebufferID() != 0) + { + int w = SizeTTOInt(m_plsRenderTarget->width()); + int h = SizeTTOInt(m_plsRenderTarget->height()); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBindFramebuffer(GL_READ_FRAMEBUFFER, m_plsRenderTarget->sideFramebufferID()); + glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST); + } +} + +rive::Renderer* PLSWorkerImpl::renderer() const { return m_plsRenderer.get(); } + +} // namespace rive_android diff --git a/kotlin/src/main/cpp/test/first_test.cpp b/kotlin/src/main/cpp/test/first_test.cpp index b0cfe77e..e6c67a3e 100644 --- a/kotlin/src/main/cpp/test/first_test.cpp +++ b/kotlin/src/main/cpp/test/first_test.cpp @@ -17,7 +17,7 @@ TEST_CASE("Factorials of 1 and higher are computed (pass)", "[single-file]") { rive::rcp worker = EGLWorker::Current(); printf("AM I ALIVE?!\n"); - // worker->run([=](EGLShareThreadState* ts) { printf("I am alive!?\n"); }); + // worker->run([=](EGLThreadState* ts) { printf("I am alive!?\n"); }); REQUIRE(Factorial(1) == 1); REQUIRE(Factorial(2) == 2); REQUIRE(Factorial(3) == 6); diff --git a/kotlin/src/main/java/app/rive/runtime/kotlin/RiveAnimationView.kt b/kotlin/src/main/java/app/rive/runtime/kotlin/RiveAnimationView.kt index 1ccf6395..8453f5f6 100644 --- a/kotlin/src/main/java/app/rive/runtime/kotlin/RiveAnimationView.kt +++ b/kotlin/src/main/java/app/rive/runtime/kotlin/RiveAnimationView.kt @@ -23,8 +23,8 @@ import app.rive.runtime.kotlin.controllers.ControllerStateManagement import app.rive.runtime.kotlin.controllers.RiveFileController import app.rive.runtime.kotlin.core.* import app.rive.runtime.kotlin.core.errors.RiveException +import app.rive.runtime.kotlin.renderers.Renderer import app.rive.runtime.kotlin.renderers.RendererMetrics -import app.rive.runtime.kotlin.renderers.RendererSkia import com.android.volley.NetworkResponse import com.android.volley.ParseError import com.android.volley.Request @@ -67,9 +67,10 @@ open class RiveAnimationView(context: Context, attrs: AttributeSet? = null) : const val TAG = "RiveAnimationView" // Default attribute values. - const val alignmentIndexDefault = 4 - const val fitIndexDefault = 1 - const val loopIndexDefault = 3 + const val alignmentIndexDefault = 4 /* Alignment.CENTER */ + const val fitIndexDefault = 1 /* Fit.CONTAIN */ + const val loopIndexDefault = 3 /* Loop.AUTO */ + val rendererIndexDefault = Rive.defaultRendererType.value } open val defaultAutoplay = true @@ -173,6 +174,7 @@ open class RiveAnimationView(context: Context, attrs: AttributeSet? = null) : alignmentIndex: Int = alignmentIndexDefault, fitIndex: Int = fitIndexDefault, loopIndex: Int = loopIndexDefault, + rendererIndex: Int = rendererIndexDefault, var autoplay: Boolean, val riveTraceAnimations: Boolean = false, var artboardName: String?, @@ -183,6 +185,7 @@ open class RiveAnimationView(context: Context, attrs: AttributeSet? = null) : var alignment: Alignment = Alignment.fromIndex(alignmentIndex) var fit: Fit = Fit.fromIndex(fitIndex) var loop: Loop = Loop.fromIndex(loopIndex) + val rendererType: RendererType = RendererType.fromIndex(rendererIndex) } init { @@ -200,9 +203,15 @@ open class RiveAnimationView(context: Context, attrs: AttributeSet? = null) : ) rendererAttributes = RendererAttributes( - alignmentIndex = getInteger(R.styleable.RiveAnimationView_riveAlignment, 4), - fitIndex = getInteger(R.styleable.RiveAnimationView_riveFit, 1), - loopIndex = getInteger(R.styleable.RiveAnimationView_riveLoop, 3), + alignmentIndex = getInteger( + R.styleable.RiveAnimationView_riveAlignment, + alignmentIndexDefault + ), + fitIndex = getInteger(R.styleable.RiveAnimationView_riveFit, fitIndexDefault), + loopIndex = getInteger( + R.styleable.RiveAnimationView_riveLoop, + loopIndexDefault + ), autoplay = getBoolean(R.styleable.RiveAnimationView_riveAutoPlay, defaultAutoplay), riveTraceAnimations = @@ -211,6 +220,10 @@ open class RiveAnimationView(context: Context, attrs: AttributeSet? = null) : animationName = getString(R.styleable.RiveAnimationView_riveAnimation), stateMachineName = getString(R.styleable.RiveAnimationView_riveStateMachine), resource = resourceFromValue, + rendererIndex = getInteger( + R.styleable.RiveAnimationView_riveRenderer, + rendererIndexDefault + ), ) controller = RiveFileController( @@ -262,14 +275,14 @@ open class RiveAnimationView(context: Context, attrs: AttributeSet? = null) : // loadFromNetwork() releases after onComplete() is called. is ResourceType.ResourceUrl -> loadFromNetwork(resource.url, onComplete) is ResourceType.ResourceBytes -> { - val file = File(resource.bytes) + val file = File(resource.bytes, rendererAttributes.rendererType) onComplete(file) // Don't retain the handle. file.release() } is ResourceType.ResourceId -> resources.openRawResource(resource.id).use { - val file = File(it.readBytes()) + val file = File(it.readBytes(), rendererAttributes.rendererType) onComplete(file) // Don't retain the handle. file.release() @@ -285,6 +298,7 @@ open class RiveAnimationView(context: Context, attrs: AttributeSet? = null) : val queue = Volley.newRequestQueue(context) val stringRequest = RiveFileRequest( url, + rendererAttributes.rendererType, { file -> artboardRenderer?.setRiveFile(file) }, { throw IOException("Unable to download Rive file $url") } ) @@ -295,6 +309,7 @@ open class RiveAnimationView(context: Context, attrs: AttributeSet? = null) : val queue = Volley.newRequestQueue(context) val stringRequest = RiveFileRequest( url, + rendererAttributes.rendererType, { onComplete(it) it.release() @@ -600,6 +615,12 @@ open class RiveAnimationView(context: Context, attrs: AttributeSet? = null) : alignment: Alignment = Alignment.CENTER, loop: Loop = Loop.AUTO, ) { + if (file.rendererType != rendererAttributes.rendererType) { + throw RiveException( + "Incompatible Renderer types: file initialized with ${file.rendererType.name}" + + " but View is set up for ${rendererAttributes.rendererType.name}" + ) + } rendererAttributes.apply { this.artboardName = artboardName this.animationName = animationName @@ -637,11 +658,12 @@ open class RiveAnimationView(context: Context, attrs: AttributeSet? = null) : /** * Called from TextureView.onAttachedToWindow() - override for implementing a custom renderer. */ - override fun createRenderer(): RendererSkia { + override fun createRenderer(): Renderer { // Make the Renderer again every time this is visible and reset its state. return RiveArtboardRenderer( trace = rendererAttributes.riveTraceAnimations, controller = controller, + rendererType = rendererAttributes.rendererType, ) } @@ -863,6 +885,7 @@ class RiveViewLifecycleObserver(private val controller: RiveFileController) : // Custom Volley request to download and create rive files over http class RiveFileRequest( url: String, + private val rendererType: RendererType, private val listener: Response.Listener, errorListener: Response.ErrorListener ) : Request(Method.GET, url, errorListener) { @@ -872,7 +895,7 @@ class RiveFileRequest( override fun parseNetworkResponse(response: NetworkResponse?): Response { return try { val bytes = response?.data ?: ByteArray(0) - val file = File(bytes) + val file = File(bytes, rendererType) Response.success(file, HttpHeaderParser.parseCacheHeaders(response)) } catch (e: UnsupportedEncodingException) { Response.error(ParseError(e)) diff --git a/kotlin/src/main/java/app/rive/runtime/kotlin/RiveArtboardRenderer.kt b/kotlin/src/main/java/app/rive/runtime/kotlin/RiveArtboardRenderer.kt index 053e1f1e..0b347ab9 100644 --- a/kotlin/src/main/java/app/rive/runtime/kotlin/RiveArtboardRenderer.kt +++ b/kotlin/src/main/java/app/rive/runtime/kotlin/RiveArtboardRenderer.kt @@ -12,7 +12,9 @@ import app.rive.runtime.kotlin.core.Fit import app.rive.runtime.kotlin.core.Helpers import app.rive.runtime.kotlin.core.Loop import app.rive.runtime.kotlin.core.PlayableInstance -import app.rive.runtime.kotlin.renderers.RendererSkia +import app.rive.runtime.kotlin.core.RendererType +import app.rive.runtime.kotlin.core.Rive +import app.rive.runtime.kotlin.renderers.Renderer import kotlin.DeprecationLevel.WARNING @@ -31,8 +33,9 @@ open class RiveArtboardRenderer( var animationName: String? = null, var stateMachineName: String? = null, trace: Boolean = false, + rendererType: RendererType = Rive.defaultRendererType, controller: RiveFileController, -) : RendererSkia(trace) { +) : Renderer(rendererType, trace) { private var controller: RiveFileController = controller.also { it.onStart = ::start it.acquire() diff --git a/kotlin/src/main/java/app/rive/runtime/kotlin/RiveInitializer.kt b/kotlin/src/main/java/app/rive/runtime/kotlin/RiveInitializer.kt index 6e84f696..c2afc6c5 100644 --- a/kotlin/src/main/java/app/rive/runtime/kotlin/RiveInitializer.kt +++ b/kotlin/src/main/java/app/rive/runtime/kotlin/RiveInitializer.kt @@ -27,6 +27,7 @@ import app.rive.runtime.kotlin.core.Rive // implementation "androidx.startup:startup-runtime:1.0.0" // // Alternatively, you can call Rive.init(context) once when your app starts up and before Rive is used +// In fact, if you want to provide a custom renderer type you'll need to init Rive manually. // class RiveInitializer : Initializer { override fun create(context: Context) { diff --git a/kotlin/src/main/java/app/rive/runtime/kotlin/RiveTextureView.kt b/kotlin/src/main/java/app/rive/runtime/kotlin/RiveTextureView.kt index 238e4a8d..8b381cdd 100644 --- a/kotlin/src/main/java/app/rive/runtime/kotlin/RiveTextureView.kt +++ b/kotlin/src/main/java/app/rive/runtime/kotlin/RiveTextureView.kt @@ -10,9 +10,8 @@ import android.view.Surface import android.view.TextureView import android.view.View import androidx.annotation.CallSuper -import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleObserver -import app.rive.runtime.kotlin.renderers.RendererSkia +import app.rive.runtime.kotlin.renderers.Renderer abstract class RiveTextureView(context: Context, attrs: AttributeSet? = null) : TextureView(context, attrs), @@ -29,9 +28,9 @@ abstract class RiveTextureView(context: Context, attrs: AttributeSet? = null) : } protected val lifecycleObserver: LifecycleObserver by lazy { createObserver() } - protected var renderer: RendererSkia? = null + protected var renderer: Renderer? = null private lateinit var viewSurface: Surface - protected abstract fun createRenderer(): RendererSkia + protected abstract fun createRenderer(): Renderer protected abstract fun createObserver(): LifecycleObserver private val refreshPeriodNanos: Long by lazy { diff --git a/kotlin/src/main/java/app/rive/runtime/kotlin/core/File.kt b/kotlin/src/main/java/app/rive/runtime/kotlin/core/File.kt index df7986ad..eb6bda28 100644 --- a/kotlin/src/main/java/app/rive/runtime/kotlin/core/File.kt +++ b/kotlin/src/main/java/app/rive/runtime/kotlin/core/File.kt @@ -17,12 +17,13 @@ import app.rive.runtime.kotlin.core.errors.RiveException * * The rive editor will always let you download your file in the latest runtime format. */ -class File(bytes: ByteArray) : NativeObject(NULL_POINTER) { +class File(bytes: ByteArray, val rendererType: RendererType = Rive.defaultRendererType) : + NativeObject(NULL_POINTER) { init { - cppPointer = import(bytes, bytes.size) + cppPointer = import(bytes, bytes.size, rendererType.value) } - private external fun import(bytes: ByteArray, length: Int): Long + private external fun import(bytes: ByteArray, length: Int, rendererType: Int): Long private external fun cppArtboardByName(cppPointer: Long, name: String): Long private external fun cppArtboardByIndex(cppPointer: Long, index: Int): Long private external fun cppArtboardNameByIndex(cppPointer: Long, index: Int): String diff --git a/kotlin/src/main/java/app/rive/runtime/kotlin/core/RendererType.kt b/kotlin/src/main/java/app/rive/runtime/kotlin/core/RendererType.kt new file mode 100644 index 00000000..a253cf7c --- /dev/null +++ b/kotlin/src/main/java/app/rive/runtime/kotlin/core/RendererType.kt @@ -0,0 +1,19 @@ +package app.rive.runtime.kotlin.core + + +enum class RendererType(val value: Int) { + Skia(0), + Rive(1); + + companion object { + fun fromIndex(index: Int): RendererType { + val maxIndex = RendererType.values().size + if (index < 0 || index > maxIndex) { + throw IndexOutOfBoundsException( + "Invalid ${Companion::class.java} index value $index. It must be between 0 and $maxIndex" + ) + } + return RendererType.values()[index] + } + } +} \ No newline at end of file diff --git a/kotlin/src/main/java/app/rive/runtime/kotlin/core/Rive.kt b/kotlin/src/main/java/app/rive/runtime/kotlin/core/Rive.kt index 9ccc5101..55da0a37 100644 --- a/kotlin/src/main/java/app/rive/runtime/kotlin/core/Rive.kt +++ b/kotlin/src/main/java/app/rive/runtime/kotlin/core/Rive.kt @@ -14,25 +14,34 @@ object Rive { requiredBounds: RectF ) - private const val JNIRiveBridge = "jnirivebridge" private const val RiveAndroid = "rive-android" + /** + * Public getter for the default renderer type. + * This can be customized via [Rive.init] + */ + var defaultRendererType: RendererType = RendererType.Skia + private set + /** * Initialises Rive. * * This loads the c++ libraries required to use Rive objects and then makes sure we * initialize our cpp environment. * - * To handle loading .so files for the jnirivebridge yourself, use [initializeCppEnvironment] + * Specify the default renderer to be used when initializing [File] or [RiveAnimationView] + * by providing a [defaultRenderer] value. This defaults to [RendererType.Skia] + * + * To handle loading .so files for the rive-android lib yourself, use [initializeCppEnvironment] * instead. */ - fun init(context: Context) { + fun init(context: Context, defaultRenderer: RendererType = RendererType.Skia) { // NOTE: loadLibrary also allows us to specify a version, something we might want to take // advantage of ReLinker - .log { Log.d("ReLinkerLogs", "(${Thread.currentThread().id}) $it") } + // .log { Log.d("ReLinkerLogs", "(${Thread.currentThread().id}) $it") } .loadLibrary(context, RiveAndroid) -// .loadLibrary(context, JNIRiveBridge) + defaultRendererType = defaultRenderer initializeCppEnvironment() } diff --git a/kotlin/src/main/java/app/rive/runtime/kotlin/renderers/RendererSkia.kt b/kotlin/src/main/java/app/rive/runtime/kotlin/renderers/Renderer.kt similarity index 88% rename from kotlin/src/main/java/app/rive/runtime/kotlin/renderers/RendererSkia.kt rename to kotlin/src/main/java/app/rive/runtime/kotlin/renderers/Renderer.kt index 95a69009..683fde3d 100644 --- a/kotlin/src/main/java/app/rive/runtime/kotlin/renderers/RendererSkia.kt +++ b/kotlin/src/main/java/app/rive/runtime/kotlin/renderers/Renderer.kt @@ -7,8 +7,19 @@ import androidx.annotation.CallSuper import app.rive.runtime.kotlin.core.Alignment import app.rive.runtime.kotlin.core.Fit import app.rive.runtime.kotlin.core.NativeObject - -abstract class RendererSkia(val trace: Boolean = false) : +import app.rive.runtime.kotlin.core.RendererType +import app.rive.runtime.kotlin.core.Rive + +@Deprecated("RendererSkia is now Renderer", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("Renderer") +) +abstract class RendererSkia + +abstract class Renderer( + private val type: RendererType = Rive.defaultRendererType, + val trace: Boolean = false +) : NativeObject(NULL_POINTER), Choreographer.FrameCallback { // From NativeObject. @@ -33,13 +44,13 @@ abstract class RendererSkia(val trace: Boolean = false) : srcBounds: RectF ) - /** Instantiates JNIRendererSkia in C++ */ - private external fun constructor(trace: Boolean): Long + /** Instantiates JNIRenderer in C++ */ + private external fun constructor(trace: Boolean, type: Int): Long @CallSuper open fun make() { if (!hasCppObject) { - cppPointer = constructor(trace) + cppPointer = constructor(trace, type.value) } } @@ -52,7 +63,7 @@ abstract class RendererSkia(val trace: Boolean = false) : abstract fun advance(elapsed: Float) /** - * Starts the SkiaRenderer & registers for frameCallbacks + * Starts the Renderer & registers for frameCallbacks * * Goal: * When we trigger start, doFrame gets called once per frame @@ -183,7 +194,7 @@ abstract class RendererSkia(val trace: Boolean = false) : /** * Deletes all this renderer's dependents. * - * Called internally by the JNI within ~JNIRendererSkia() + * Called internally by the JNI within ~JNIRenderer() * * N.B. this function is marked as `protected` instead of `private` because * otherwise it's inaccessible from JNI on API < 24 diff --git a/kotlin/src/main/res/values/attrs.xml b/kotlin/src/main/res/values/attrs.xml index 7ce8abc2..3bdb9735 100644 --- a/kotlin/src/main/res/values/attrs.xml +++ b/kotlin/src/main/res/values/attrs.xml @@ -32,6 +32,10 @@ + + + + @@ -48,5 +52,6 @@ + \ No newline at end of file