diff --git a/README.md b/README.md index 6aaab68a..e68179c8 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ The following table compares feature availability with the [previous Rive React | Accessibility semantics | ⚠️ | Editor-authored semantics → VoiceOver (iOS; Android in progress) | | Animation selection | ❌ | Animation playback not planned, use state machines | | Renderer options | ❌ | Single renderer option available (Rive) | +| GPU Canvas (3D content) | ⚠️ | Opt-in via `RiveRuntime.setGPUCanvasEnabled()` (new runtime only) | > **Note**: Several features in the table above (state machine inputs, text runs, and events) represent legacy approaches to runtime control. We recommend using data binding instead, as it provides a more maintainable way to control your Rive graphics (both at edit time and runtime). diff --git a/android/src/legacy/java/com/rive/RiveRenderBackendConfig.kt b/android/src/legacy/java/com/rive/RiveRenderBackendConfig.kt deleted file mode 100644 index b8d07d78..00000000 --- a/android/src/legacy/java/com/rive/RiveRenderBackendConfig.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.rive - -import android.util.Log -import com.margelo.nitro.rive.AndroidRenderBackend - -object RiveRenderBackendConfig { - private const val TAG = "RiveRenderBackendConfig" - - fun set(backend: AndroidRenderBackend) { - if (backend != AndroidRenderBackend.OPENGL) { - Log.w(TAG, "setAndroidRenderBackend($backend) ignored: the legacy backend only supports OpenGL rendering.") - } - } -} diff --git a/android/src/legacy/java/com/rive/RiveWorkerConfig.kt b/android/src/legacy/java/com/rive/RiveWorkerConfig.kt new file mode 100644 index 00000000..13dcff0c --- /dev/null +++ b/android/src/legacy/java/com/rive/RiveWorkerConfig.kt @@ -0,0 +1,23 @@ +package com.rive + +import android.util.Log +import com.margelo.nitro.rive.AndroidRenderBackend + +object RiveWorkerConfig { + private const val TAG = "RiveWorkerConfig" + + fun setRenderBackend(backend: AndroidRenderBackend) { + if (backend != AndroidRenderBackend.OPENGL) { + Log.w(TAG, "setAndroidRenderBackend($backend) ignored: the legacy backend only supports OpenGL rendering.") + } + } + + fun setGPUCanvasEnabled(enabled: Boolean) { + if (enabled) { + Log.w(TAG, "setGPUCanvasEnabled(true) ignored: the legacy backend does not support GPU Canvas.") + } + } + + val isGPUCanvasEnabled: Boolean + get() = false +} diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridRiveRuntime.kt b/android/src/main/java/com/margelo/nitro/rive/HybridRiveRuntime.kt index a9d5698f..b303fa30 100644 --- a/android/src/main/java/com/margelo/nitro/rive/HybridRiveRuntime.kt +++ b/android/src/main/java/com/margelo/nitro/rive/HybridRiveRuntime.kt @@ -4,7 +4,7 @@ import androidx.annotation.Keep import com.facebook.proguard.annotations.DoNotStrip import com.margelo.nitro.core.Promise import com.rive.RiveInitializer -import com.rive.RiveRenderBackendConfig +import com.rive.RiveWorkerConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -26,6 +26,13 @@ class HybridRiveRuntime : HybridRiveRuntimeSpec() { get() = RiveInitializer.error override fun setAndroidRenderBackend(backend: AndroidRenderBackend) { - RiveRenderBackendConfig.set(backend) + RiveWorkerConfig.setRenderBackend(backend) + } + + override val isGPUCanvasEnabled: Boolean + get() = RiveWorkerConfig.isGPUCanvasEnabled + + override fun setGPUCanvasEnabled(enabled: Boolean) { + RiveWorkerConfig.setGPUCanvasEnabled(enabled) } } diff --git a/android/src/new/java/com/margelo/nitro/rive/HybridRiveFileFactory.kt b/android/src/new/java/com/margelo/nitro/rive/HybridRiveFileFactory.kt index 83896a15..e9aba407 100644 --- a/android/src/new/java/com/margelo/nitro/rive/HybridRiveFileFactory.kt +++ b/android/src/new/java/com/margelo/nitro/rive/HybridRiveFileFactory.kt @@ -12,7 +12,8 @@ import app.rive.core.CommandQueue import com.facebook.proguard.annotations.DoNotStrip import com.margelo.nitro.core.ArrayBuffer import com.margelo.nitro.core.Promise -import com.rive.RiveRenderBackendConfig +import com.rive.DeferredRiveWorker +import com.rive.RiveWorkerConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -89,10 +90,15 @@ class HybridRiveFileFactory : HybridRiveFileFactorySpec() { Log.d(TAG, "RiveErrorLogger installed") } return sharedWorker ?: run { - val renderBackend = RiveRenderBackendConfig.resolveForWorker() - CommandQueue(renderBackend).also { + val config = RiveWorkerConfig.resolveForWorker() + val queue = if (config.gpuCanvasEnabled) { + DeferredRiveWorker.create(config.renderBackend) + } else { + CommandQueue(config.renderBackend) + } + queue.also { sharedWorker = it - Log.d(TAG, "Created CommandQueue (renderBackend=$renderBackend), refCount=${it.refCount}") + Log.d(TAG, "Created CommandQueue ($config), refCount=${it.refCount}") startPolling(it) } } diff --git a/android/src/new/java/com/margelo/nitro/rive/HybridViewModelBooleanProperty.kt b/android/src/new/java/com/margelo/nitro/rive/HybridViewModelBooleanProperty.kt index 458fd94d..d1a17dfa 100644 --- a/android/src/new/java/com/margelo/nitro/rive/HybridViewModelBooleanProperty.kt +++ b/android/src/new/java/com/margelo/nitro/rive/HybridViewModelBooleanProperty.kt @@ -1,29 +1,42 @@ package com.margelo.nitro.rive import androidx.annotation.Keep +import app.rive.RiveViewModelInstanceException import app.rive.ViewModelInstance import com.facebook.proguard.annotations.DoNotStrip import com.margelo.nitro.core.Promise +import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.runBlocking @Keep @DoNotStrip class HybridViewModelBooleanProperty( private val instance: ViewModelInstance, - private val path: String + private val path: String, + private val hasProperty: suspend (String) -> Boolean? = { null } ) : HybridViewModelBooleanPropertySpec(), BaseHybridViewModelProperty by BaseHybridViewModelPropertyImpl() { companion object { private const val TAG = "HybridViewModelBooleanProperty" } + private suspend fun requireProperty() { + if (hasProperty(path) == false) { + throw RiveViewModelInstanceException("Boolean property not found at path '$path'") + } + } + // Deprecated: Use getValueAsync (read) or set(value) (write) instead override var value: Boolean get() { DeprecationWarning.warn("BooleanProperty.value", "getValueAsync") return try { - runBlocking { instance.getBooleanFlow(path).first() } + runBlocking { + requireProperty() + instance.getBooleanFlow(path).first() + } } catch (e: Exception) { RiveLog.e(TAG, "getValue failed for path '$path': ${e.message}") false @@ -42,12 +55,23 @@ class HybridViewModelBooleanProperty( } override fun getValueAsync(): Promise { - return Promise.async { instance.getBooleanFlow(path).first() } + return Promise.async { + requireProperty() + instance.getBooleanFlow(path).first() + } } override fun addListener(onChanged: (value: Boolean) -> Unit): () -> Unit { val remover = addListenerInternal(onChanged) - ensureValueListenerJob(instance.getBooleanFlow(path)) + ensureValueListenerJob( + flow { + if (hasProperty(path) == false) { + RiveLog.e(TAG, "addListener: boolean property not found at path '$path'") + return@flow + } + emitAll(instance.getBooleanFlow(path)) + } + ) return remover } } diff --git a/android/src/new/java/com/margelo/nitro/rive/HybridViewModelInstance.kt b/android/src/new/java/com/margelo/nitro/rive/HybridViewModelInstance.kt index c18385f4..8fc1fbb8 100644 --- a/android/src/new/java/com/margelo/nitro/rive/HybridViewModelInstance.kt +++ b/android/src/new/java/com/margelo/nitro/rive/HybridViewModelInstance.kt @@ -2,11 +2,14 @@ package com.margelo.nitro.rive import android.util.Log import androidx.annotation.Keep +import app.rive.RiveViewModelInstanceException import app.rive.ViewModelInstance import app.rive.ViewModelInstanceSource +import app.rive.runtime.kotlin.core.ViewModel import app.rive.core.CommandQueue import com.facebook.proguard.annotations.DoNotStrip import com.margelo.nitro.core.Promise +import java.util.concurrent.ConcurrentHashMap @Keep @DoNotStrip @@ -65,7 +68,45 @@ class HybridViewModelInstance( HybridViewModelStringProperty(viewModelInstance, path) override fun booleanProperty(path: String) = - HybridViewModelBooleanProperty(viewModelInstance, path) + HybridViewModelBooleanProperty(viewModelInstance, path, ::hasBooleanProperty) + + private val booleanPathCache = ConcurrentHashMap() + + // rive-android 11.10+ answers a boolean read of an unknown path with an + // uninitialized byte as the jboolean, which CheckJNI turns into a process + // abort in debuggable builds (rive-app/rive-android#470). Resolve the path + // against ViewModel metadata first; null means the lookup itself failed and + // the read proceeds unguarded. + internal suspend fun hasBooleanProperty(path: String): Boolean? { + booleanPathCache[path]?.let { return it } + val file = parentFile.riveFile ?: return null + val parentPath = path.substringBeforeLast('/', "") + val leaf = path.substringAfterLast('/') + val result = try { + val vmName = if (parentPath.isEmpty()) { + viewModelName ?: viewModelInstance.getViewModelName() + } else { + val parent = try { + ViewModelInstance.create(file, ViewModelInstanceSource.Reference(viewModelInstance, parentPath)) + } catch (e: RiveViewModelInstanceException) { + return false.also { booleanPathCache[path] = it } + } + try { + parent.getViewModelName() + } finally { + parent.close() + } + } + file.getViewModelProperties(vmName).any { + it.name == leaf && it.type == ViewModel.PropertyDataType.BOOLEAN + } + } catch (e: Exception) { + RiveLog.w(TAG, "Could not resolve boolean property '$path': ${e.message}") + return null + } + booleanPathCache[path] = result + return result + } override fun colorProperty(path: String) = HybridViewModelColorProperty(viewModelInstance, path) diff --git a/android/src/new/java/com/rive/DeferredRiveWorker.java b/android/src/new/java/com/rive/DeferredRiveWorker.java new file mode 100644 index 00000000..af2c8c1c --- /dev/null +++ b/android/src/new/java/com/rive/DeferredRiveWorker.java @@ -0,0 +1,19 @@ +package com.rive; + +import app.rive.RenderBackend; +import app.rive.RiveInitializationException; +import app.rive.core.CommandQueue; + +/** + * rive-android 11.12 only reaches its deferred (GPU Canvas) worker through the + * Compose-only {@code rememberDeferredRiveWorker}; {@code CommandQueue.createDeferred} + * is Kotlin-internal. Java is not bound by Kotlin visibility, so this calls the + * internal entry point under its mangled JVM name. + */ +public final class DeferredRiveWorker { + private DeferredRiveWorker() {} + + public static CommandQueue create(RenderBackend renderBackend) throws RiveInitializationException { + return CommandQueue.Companion.createDeferred$kotlin_release(renderBackend, false); + } +} diff --git a/android/src/new/java/com/rive/RiveRenderBackendConfig.kt b/android/src/new/java/com/rive/RiveRenderBackendConfig.kt deleted file mode 100644 index f63c154d..00000000 --- a/android/src/new/java/com/rive/RiveRenderBackendConfig.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.rive - -import android.util.Log -import app.rive.RenderBackend -import com.margelo.nitro.rive.AndroidRenderBackend - -/** - * Process-wide render backend preference for the experimental backend's - * shared CommandQueue. The choice only takes effect if set before the - * worker is created (i.e. before the first Rive file is loaded). - */ -object RiveRenderBackendConfig { - private const val TAG = "RiveRenderBackendConfig" - - @Volatile - private var requested: RenderBackend = RenderBackend.OpenGL - - @Volatile - private var workerCreated = false - - fun set(backend: AndroidRenderBackend) { - if (workerCreated) { - Log.w( - TAG, - "setAndroidRenderBackend($backend) ignored: the shared render worker already exists " + - "(using $requested). Call it before loading any Rive files." - ) - return - } - requested = when (backend) { - AndroidRenderBackend.OPENGL -> RenderBackend.OpenGL - AndroidRenderBackend.VULKAN -> RenderBackend.Vulkan - } - } - - fun resolveForWorker(): RenderBackend { - workerCreated = true - return requested - } -} diff --git a/android/src/new/java/com/rive/RiveWorkerConfig.kt b/android/src/new/java/com/rive/RiveWorkerConfig.kt new file mode 100644 index 00000000..780b161e --- /dev/null +++ b/android/src/new/java/com/rive/RiveWorkerConfig.kt @@ -0,0 +1,57 @@ +package com.rive + +import android.util.Log +import app.rive.RenderBackend +import com.margelo.nitro.rive.AndroidRenderBackend + +/** + * Process-wide options for the new runtime's shared CommandQueue. They only + * take effect if set before the worker is created (i.e. before the first Rive + * file is loaded); later calls are logged and ignored. + */ +object RiveWorkerConfig { + private const val TAG = "RiveWorkerConfig" + + data class Resolved(val renderBackend: RenderBackend, val gpuCanvasEnabled: Boolean) + + private var requestedBackend: RenderBackend = RenderBackend.OpenGL + private var requestedGPUCanvas = false + private var resolved: Resolved? = null + + @Synchronized + fun setRenderBackend(backend: AndroidRenderBackend) { + resolved?.let { + Log.w( + TAG, + "setAndroidRenderBackend($backend) ignored: the shared render worker already exists " + + "(using ${it.renderBackend}). Call it before loading any Rive files." + ) + return + } + requestedBackend = when (backend) { + AndroidRenderBackend.OPENGL -> RenderBackend.OpenGL + AndroidRenderBackend.VULKAN -> RenderBackend.Vulkan + } + } + + @Synchronized + fun setGPUCanvasEnabled(enabled: Boolean) { + resolved?.let { + Log.w( + TAG, + "setGPUCanvasEnabled($enabled) ignored: the shared render worker already exists " + + "(GPU Canvas ${if (it.gpuCanvasEnabled) "enabled" else "disabled"}). " + + "Call it before loading any Rive files." + ) + return + } + requestedGPUCanvas = enabled + } + + val isGPUCanvasEnabled: Boolean + @Synchronized get() = resolved?.gpuCanvasEnabled ?: requestedGPUCanvas + + @Synchronized + fun resolveForWorker(): Resolved = + resolved ?: Resolved(requestedBackend, requestedGPUCanvas).also { resolved = it } +} diff --git a/docs/runtime-backends.md b/docs/runtime-backends.md index 49b567b5..a59d36f8 100644 --- a/docs/runtime-backends.md +++ b/docs/runtime-backends.md @@ -57,3 +57,23 @@ render worker is created (a later call logs a warning and is ignored). Vulkan requires Android 10 (API 29) or newer; rive-android automatically falls back to OpenGL when Vulkan is unavailable or fails to initialize. The call is a no-op on iOS and on the legacy Android backend. + +## GPU Canvas (new runtime only) + +Rive's GPU Canvas renderer is required for 3D content and is disabled by +default. Opt in per process, before loading any Rive files: + +```ts +import { RiveRuntime } from '@rive-app/react-native'; + +RiveRuntime.setGPUCanvasEnabled(true); +``` + +The choice is fixed once the shared render worker is created (a later call +logs a warning and is ignored); `RiveRuntime.isGPUCanvasEnabled()` reports the +setting in effect. On iOS this maps to +[`Worker(configuration: .init(enableGPUCanvas: true))`](https://rive.app/docs/runtimes/apple/gpu-canvas) +(rive-ios 6.25+). On Android it maps to rive-android 11.12's experimental +deferred renderer (`RiveWorker.createDeferred()`), which upstream describes as +temporary scaffolding on its way to becoming the default. The call is a no-op +on the legacy runtime. diff --git a/example/__tests__/viewmodel-properties.harness.ts b/example/__tests__/viewmodel-properties.harness.ts index 1e13841d..bd1edac3 100644 --- a/example/__tests__/viewmodel-properties.harness.ts +++ b/example/__tests__/viewmodel-properties.harness.ts @@ -185,6 +185,17 @@ describe('ViewModel Properties', () => { instance.booleanProperty('nonexistent')!.getValueAsync() ).rejects.toBeDefined(); + // Nested misses: bad leaf under a real nested view model, and a bad + // parent segment. Both aborted the process on rive-android 11.10+ + // before the metadata guard. + await expect( + instance.booleanProperty('pet/nonexistent')!.getValueAsync() + ).rejects.toBeDefined(); + + await expect( + instance.booleanProperty('nonexistent/likes_popcorn')!.getValueAsync() + ).rejects.toBeDefined(); + await expect( instance.colorProperty('nonexistent')!.getValueAsync() ).rejects.toBeDefined(); diff --git a/example/assets/rive/ore.riv b/example/assets/rive/ore.riv new file mode 100644 index 00000000..1c36b12c Binary files /dev/null and b/example/assets/rive/ore.riv differ diff --git a/example/src/demos/GPUCanvasDemo.tsx b/example/src/demos/GPUCanvasDemo.tsx new file mode 100644 index 00000000..ab24a0a8 --- /dev/null +++ b/example/src/demos/GPUCanvasDemo.tsx @@ -0,0 +1,70 @@ +import { useState } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { + Fit, + RiveRuntime, + RiveView, + useRiveFile, +} from '@rive-app/react-native'; +import type { Metadata } from '../shared/metadata'; + +// ore.riv is the GPU Canvas sample shipped with rive-android's example app. +const ORE = require('../../assets/rive/ore.riv'); + +export default function GPUCanvasDemo() { + const [gpuCanvas] = useState(() => { + RiveRuntime.setGPUCanvasEnabled(true); + return RiveRuntime.isGPUCanvasEnabled(); + }); + const { riveFile, error: fileError } = useRiveFile(ORE); + const [viewError, setViewError] = useState(null); + + return ( + + + {gpuCanvas + ? 'GPU Canvas enabled' + : 'GPU Canvas request ignored: a Rive file was already loaded in this process. Restart the app and open this page first.'} + + {fileError && {fileError.message}} + {viewError && {viewError}} + {riveFile && ( + setViewError(e.message)} + /> + )} + + ); +} + +GPUCanvasDemo.metadata = { + name: 'GPU Canvas (3D)', + description: + 'Enables RiveRuntime.setGPUCanvasEnabled(true) and renders a 3D scene. Must be the first Rive page opened after a cold start.', +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#0c1935', + }, + status: { + padding: 12, + textAlign: 'center', + fontSize: 14, + }, + ok: { color: '#8ef0a8' }, + warn: { color: '#ffcf70' }, + error: { + padding: 12, + color: '#ff8080', + textAlign: 'center', + }, + rive: { + flex: 1, + }, +}); diff --git a/ios/HybridRiveRuntime.swift b/ios/HybridRiveRuntime.swift index e7ea7701..5ff3efde 100644 --- a/ios/HybridRiveRuntime.swift +++ b/ios/HybridRiveRuntime.swift @@ -12,4 +12,10 @@ final class HybridRiveRuntime: HybridRiveRuntimeSpec { func setAndroidRenderBackend(backend: AndroidRenderBackend) throws { // Android-only setting; nothing to do on iOS. } + + var isGPUCanvasEnabled: Bool { RiveWorkerConfig.isGPUCanvasEnabled } + + func setGPUCanvasEnabled(enabled: Bool) throws { + RiveWorkerConfig.setGPUCanvasEnabled(enabled) + } } diff --git a/ios/legacy/RiveWorkerConfig.swift b/ios/legacy/RiveWorkerConfig.swift new file mode 100644 index 00000000..f5e277f1 --- /dev/null +++ b/ios/legacy/RiveWorkerConfig.swift @@ -0,0 +1,13 @@ +import Foundation + +enum RiveWorkerConfig { + private static let tag = "RiveWorkerConfig" + + static func setGPUCanvasEnabled(_ enabled: Bool) { + if enabled { + RiveLog.w(tag, "setGPUCanvasEnabled(true) ignored: the legacy backend does not support GPU Canvas.") + } + } + + static var isGPUCanvasEnabled: Bool { false } +} diff --git a/ios/new/HybridRiveFileFactory.swift b/ios/new/HybridRiveFileFactory.swift index 5273fe6d..d440e954 100644 --- a/ios/new/HybridRiveFileFactory.swift +++ b/ios/new/HybridRiveFileFactory.swift @@ -10,7 +10,7 @@ final class HybridRiveFileFactory: HybridRiveFileFactorySpec, @unchecked Sendabl if !(RiveRuntime.RiveLog.logger is RiveRuntimeLogger) { RiveRuntime.RiveLog.logger = RiveRuntimeLogger() } - return try await Worker() + return try await Worker(configuration: RiveWorkerConfig.resolveForWorker()) } func fromURL(url: String, loadCdn: Bool, referencedAssets: ReferencedAssetsType?) throws diff --git a/ios/new/RiveWorkerConfig.swift b/ios/new/RiveWorkerConfig.swift new file mode 100644 index 00000000..458ea95c --- /dev/null +++ b/ios/new/RiveWorkerConfig.swift @@ -0,0 +1,39 @@ +import Foundation +import RiveRuntime + +/// Process-wide options for the shared render worker. They only take effect if +/// set before the worker is created (i.e. before the first Rive file is +/// loaded); later calls are logged and ignored. +enum RiveWorkerConfig { + private static let tag = "RiveWorkerConfig" + private static let lock = NSLock() + private static var requestedGPUCanvas = false + private static var resolvedGPUCanvas: Bool? + + static func setGPUCanvasEnabled(_ enabled: Bool) { + lock.lock() + defer { lock.unlock() } + if let resolved = resolvedGPUCanvas { + RiveLog.w( + tag, + "setGPUCanvasEnabled(\(enabled)) ignored: the shared render worker already exists " + + "(GPU Canvas \(resolved ? "enabled" : "disabled")). Call it before loading any Rive files.") + return + } + requestedGPUCanvas = enabled + } + + static var isGPUCanvasEnabled: Bool { + lock.lock() + defer { lock.unlock() } + return resolvedGPUCanvas ?? requestedGPUCanvas + } + + static func resolveForWorker() -> Worker.Configuration { + lock.lock() + defer { lock.unlock() } + let enabled = resolvedGPUCanvas ?? requestedGPUCanvas + resolvedGPUCanvas = enabled + return .init(enableGPUCanvas: enabled) + } +} diff --git a/nitrogen/generated/android/c++/JHybridRiveRuntimeSpec.cpp b/nitrogen/generated/android/c++/JHybridRiveRuntimeSpec.cpp index 26a44c64..599e7856 100644 --- a/nitrogen/generated/android/c++/JHybridRiveRuntimeSpec.cpp +++ b/nitrogen/generated/android/c++/JHybridRiveRuntimeSpec.cpp @@ -58,6 +58,11 @@ namespace margelo::nitro::rive { auto __result = method(_javaPart); return __result != nullptr ? std::make_optional(__result->toStdString()) : std::nullopt; } + bool JHybridRiveRuntimeSpec::getIsGPUCanvasEnabled() { + static const auto method = _javaPart->javaClassStatic()->getMethod("isGPUCanvasEnabled"); + auto __result = method(_javaPart); + return static_cast(__result); + } // Methods std::shared_ptr> JHybridRiveRuntimeSpec::initialize() { @@ -79,5 +84,9 @@ namespace margelo::nitro::rive { static const auto method = _javaPart->javaClassStatic()->getMethod /* backend */)>("setAndroidRenderBackend"); method(_javaPart, JAndroidRenderBackend::fromCpp(backend)); } + void JHybridRiveRuntimeSpec::setGPUCanvasEnabled(bool enabled) { + static const auto method = _javaPart->javaClassStatic()->getMethod("setGPUCanvasEnabled"); + method(_javaPart, enabled); + } } // namespace margelo::nitro::rive diff --git a/nitrogen/generated/android/c++/JHybridRiveRuntimeSpec.hpp b/nitrogen/generated/android/c++/JHybridRiveRuntimeSpec.hpp index 5e5694cf..541d883e 100644 --- a/nitrogen/generated/android/c++/JHybridRiveRuntimeSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridRiveRuntimeSpec.hpp @@ -52,11 +52,13 @@ namespace margelo::nitro::rive { // Properties bool getIsInitialized() override; std::optional getInitError() override; + bool getIsGPUCanvasEnabled() override; public: // Methods std::shared_ptr> initialize() override; void setAndroidRenderBackend(AndroidRenderBackend backend) override; + void setGPUCanvasEnabled(bool enabled) override; private: jni::global_ref _javaPart; diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveRuntimeSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveRuntimeSpec.kt index b3c4ab56..97dedd32 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveRuntimeSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveRuntimeSpec.kt @@ -33,6 +33,10 @@ abstract class HybridRiveRuntimeSpec: HybridObject() { @get:DoNotStrip @get:Keep abstract val initError: String? + + @get:DoNotStrip + @get:Keep + abstract val isGPUCanvasEnabled: Boolean // Methods @DoNotStrip @@ -42,6 +46,10 @@ abstract class HybridRiveRuntimeSpec: HybridObject() { @DoNotStrip @Keep abstract fun setAndroidRenderBackend(backend: AndroidRenderBackend): Unit + + @DoNotStrip + @Keep + abstract fun setGPUCanvasEnabled(enabled: Boolean): Unit // Default implementation of `HybridObject.toString()` override fun toString(): String { diff --git a/nitrogen/generated/ios/c++/HybridRiveRuntimeSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridRiveRuntimeSpecSwift.hpp index 6dbdd045..ee46848e 100644 --- a/nitrogen/generated/ios/c++/HybridRiveRuntimeSpecSwift.hpp +++ b/nitrogen/generated/ios/c++/HybridRiveRuntimeSpecSwift.hpp @@ -73,6 +73,9 @@ namespace margelo::nitro::rive { auto __result = _swiftPart.getInitError(); return __result; } + inline bool getIsGPUCanvasEnabled() noexcept override { + return _swiftPart.isGPUCanvasEnabled(); + } public: // Methods @@ -90,6 +93,12 @@ namespace margelo::nitro::rive { std::rethrow_exception(__result.error()); } } + inline void setGPUCanvasEnabled(bool enabled) override { + auto __result = _swiftPart.setGPUCanvasEnabled(std::forward(enabled)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } private: RNRive::HybridRiveRuntimeSpec_cxx _swiftPart; diff --git a/nitrogen/generated/ios/swift/HybridRiveRuntimeSpec.swift b/nitrogen/generated/ios/swift/HybridRiveRuntimeSpec.swift index d3c65889..5b9839cf 100644 --- a/nitrogen/generated/ios/swift/HybridRiveRuntimeSpec.swift +++ b/nitrogen/generated/ios/swift/HybridRiveRuntimeSpec.swift @@ -12,10 +12,12 @@ public protocol HybridRiveRuntimeSpec_protocol: HybridObject { // Properties var isInitialized: Bool { get } var initError: String? { get } + var isGPUCanvasEnabled: Bool { get } // Methods func initialize() throws -> Promise func setAndroidRenderBackend(backend: AndroidRenderBackend) throws -> Void + func setGPUCanvasEnabled(enabled: Bool) throws -> Void } public extension HybridRiveRuntimeSpec_protocol { diff --git a/nitrogen/generated/ios/swift/HybridRiveRuntimeSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridRiveRuntimeSpec_cxx.swift index 47a658d6..51fb3024 100644 --- a/nitrogen/generated/ios/swift/HybridRiveRuntimeSpec_cxx.swift +++ b/nitrogen/generated/ios/swift/HybridRiveRuntimeSpec_cxx.swift @@ -140,6 +140,13 @@ open class HybridRiveRuntimeSpec_cxx { }() } } + + public final var isGPUCanvasEnabled: Bool { + @inline(__always) + get { + return self.__implementation.isGPUCanvasEnabled + } + } // Methods @inline(__always) @@ -171,4 +178,15 @@ open class HybridRiveRuntimeSpec_cxx { return bridge.create_Result_void_(__exceptionPtr) } } + + @inline(__always) + public final func setGPUCanvasEnabled(enabled: Bool) -> bridge.Result_void_ { + do { + try self.__implementation.setGPUCanvasEnabled(enabled: enabled) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } } diff --git a/nitrogen/generated/shared/c++/HybridRiveRuntimeSpec.cpp b/nitrogen/generated/shared/c++/HybridRiveRuntimeSpec.cpp index 969fce3b..909f5d4a 100644 --- a/nitrogen/generated/shared/c++/HybridRiveRuntimeSpec.cpp +++ b/nitrogen/generated/shared/c++/HybridRiveRuntimeSpec.cpp @@ -16,8 +16,10 @@ namespace margelo::nitro::rive { registerHybrids(this, [](Prototype& prototype) { prototype.registerHybridGetter("isInitialized", &HybridRiveRuntimeSpec::getIsInitialized); prototype.registerHybridGetter("initError", &HybridRiveRuntimeSpec::getInitError); + prototype.registerHybridGetter("isGPUCanvasEnabled", &HybridRiveRuntimeSpec::getIsGPUCanvasEnabled); prototype.registerHybridMethod("initialize", &HybridRiveRuntimeSpec::initialize); prototype.registerHybridMethod("setAndroidRenderBackend", &HybridRiveRuntimeSpec::setAndroidRenderBackend); + prototype.registerHybridMethod("setGPUCanvasEnabled", &HybridRiveRuntimeSpec::setGPUCanvasEnabled); }); } diff --git a/nitrogen/generated/shared/c++/HybridRiveRuntimeSpec.hpp b/nitrogen/generated/shared/c++/HybridRiveRuntimeSpec.hpp index af916759..2c67dcfc 100644 --- a/nitrogen/generated/shared/c++/HybridRiveRuntimeSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridRiveRuntimeSpec.hpp @@ -50,11 +50,13 @@ namespace margelo::nitro::rive { // Properties virtual bool getIsInitialized() = 0; virtual std::optional getInitError() = 0; + virtual bool getIsGPUCanvasEnabled() = 0; public: // Methods virtual std::shared_ptr> initialize() = 0; virtual void setAndroidRenderBackend(AndroidRenderBackend backend) = 0; + virtual void setGPUCanvasEnabled(bool enabled) = 0; protected: // Hybrid Setup diff --git a/package.json b/package.json index 32e8937d..c2bd3409 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,8 @@ }, "homepage": "https://github.com/rive-app/rive-nitro-react-native#readme", "runtimeVersions": { - "ios": "6.23.1", - "android": "11.9.1" + "ios": "6.26.0", + "android": "11.12.0" }, "publishConfig": { "registry": "https://registry.npmjs.org/" diff --git a/src/core/RiveRuntime.ts b/src/core/RiveRuntime.ts index b5da407c..7c6cc42d 100644 --- a/src/core/RiveRuntime.ts +++ b/src/core/RiveRuntime.ts @@ -36,4 +36,26 @@ export namespace RiveRuntime { export function setAndroidRenderBackend(backend: AndroidRenderBackend) { RiveRuntimeInternal.setAndroidRenderBackend(backend); } + + /** + * Enables Rive's GPU Canvas renderer, which is required to render 3D + * content. Applies process-wide and must be called before any Rive file is + * loaded — once the shared render worker exists the choice is fixed and a + * later call is ignored with a warning. + * + * Disabled by default. Backed by `Worker(configuration:)` on iOS and by + * rive-android's experimental deferred renderer on Android. No-op on the + * legacy runtime. + */ + export function setGPUCanvasEnabled(enabled: boolean) { + RiveRuntimeInternal.setGPUCanvasEnabled(enabled); + } + + /** + * Whether GPU Canvas is in effect: the setting the shared render worker was + * created with, or the requested setting while no worker exists yet. + */ + export function isGPUCanvasEnabled(): boolean { + return RiveRuntimeInternal.isGPUCanvasEnabled; + } } diff --git a/src/specs/RiveRuntime.nitro.ts b/src/specs/RiveRuntime.nitro.ts index 03e91909..b54bfe0a 100644 --- a/src/specs/RiveRuntime.nitro.ts +++ b/src/specs/RiveRuntime.nitro.ts @@ -11,4 +11,6 @@ export interface RiveRuntime readonly isInitialized: boolean; readonly initError: string | undefined; setAndroidRenderBackend(backend: AndroidRenderBackend): void; + setGPUCanvasEnabled(enabled: boolean): void; + readonly isGPUCanvasEnabled: boolean; }