diff --git a/README.md b/README.md index 29bbb3f0..e237e224 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,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..f40a4203 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,7 @@ 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.RiveWorkerConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -88,13 +88,10 @@ class HybridRiveFileFactory : HybridRiveFileFactorySpec() { app.rive.RiveLog.logger = RiveErrorLogger Log.d(TAG, "RiveErrorLogger installed") } - return sharedWorker ?: run { - val renderBackend = RiveRenderBackendConfig.resolveForWorker() - CommandQueue(renderBackend).also { - sharedWorker = it - Log.d(TAG, "Created CommandQueue (renderBackend=$renderBackend), refCount=${it.refCount}") - startPolling(it) - } + return sharedWorker ?: RiveWorkerConfig.createWorker().also { + sharedWorker = it + Log.d(TAG, "Created CommandQueue (${RiveWorkerConfig.current}), refCount=${it.refCount}") + startPolling(it) } } diff --git a/android/src/new/java/com/rive/DeferredRiveWorker.kt b/android/src/new/java/com/rive/DeferredRiveWorker.kt new file mode 100644 index 00000000..f053bb39 --- /dev/null +++ b/android/src/new/java/com/rive/DeferredRiveWorker.kt @@ -0,0 +1,31 @@ +package com.rive + +import app.rive.RenderBackend +import app.rive.core.CommandQueue +import java.lang.reflect.InvocationTargetException + +/** + * rive-android only exposes its deferred (GPU Canvas) worker through the + * Compose-only `rememberDeferredRiveWorker`; `CommandQueue.createDeferred` is + * Kotlin-internal. Reflection reaches it under either its mangled JVM name or + * a plain `createDeferred`. + */ +internal object DeferredRiveWorker { + private val factory by lazy { + val signature = arrayOf(RenderBackend::class.java, Boolean::class.javaPrimitiveType) + CommandQueue.Companion::class.java.declaredMethods.firstOrNull { + (it.name == "createDeferred" || it.name.startsWith("createDeferred$")) && + it.parameterTypes.contentEquals(signature) + } + } + + /** Null when this rive-android has no deferred worker entry point. */ + fun createOrNull(renderBackend: RenderBackend): CommandQueue? { + val method = factory ?: return null + return try { + method.invoke(CommandQueue.Companion, renderBackend, false) as CommandQueue + } catch (e: InvocationTargetException) { + throw e.targetException + } + } +} 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..01d9c9e0 --- /dev/null +++ b/android/src/new/java/com/rive/RiveWorkerConfig.kt @@ -0,0 +1,80 @@ +package com.rive + +import android.util.Log +import app.rive.RenderBackend +import app.rive.core.CommandQueue +import com.margelo.nitro.rive.AndroidRenderBackend +import com.margelo.nitro.rive.RiveLog + +/** + * 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); once [createWorker] has succeeded, 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 + + /** The options the shared worker was built with; null until [createWorker] succeeds. */ + val current: Resolved? + @Synchronized get() = resolved + + /** + * Builds the shared worker from the requested options. GPU Canvas falls back + * to a regular worker when this rive-android has no deferred entry point. + */ + @Synchronized + fun createWorker(): CommandQueue { + val backend = requestedBackend + val deferred = if (requestedGPUCanvas) DeferredRiveWorker.createOrNull(backend) else null + if (requestedGPUCanvas && deferred == null) { + RiveLog.w( + TAG, + "GPU Canvas is not available in this rive-android version; rendering without it. " + + "3D content will not draw." + ) + } + val worker = deferred ?: CommandQueue(backend) + resolved = Resolved(backend, gpuCanvasEnabled = deferred != null) + return worker + } +} diff --git a/docs/runtime-backends.md b/docs/runtime-backends.md index 49b567b5..59540185 100644 --- a/docs/runtime-backends.md +++ b/docs/runtime-backends.md @@ -57,3 +57,27 @@ 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); +it needs rive-ios 6.25 or newer, and an older `RiveRuntimeIOSVersion` override +fails to compile. On Android it maps to rive-android's experimental deferred +worker, which upstream describes as temporary scaffolding on its way to becoming +the default; its entry point is not public, so the library resolves it by +reflection. If the rive-android in use has no deferred worker (e.g. an overridden +`Rive_RiveRuntimeAndroidVersion`), the worker is created without GPU Canvas, a +warning is logged through `RiveLog`, and `isGPUCanvasEnabled()` reports `false` +from then on. 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..a4726286 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. rive-android 11.10.0-11.12.0 aborted the process on + // these (rive-app/rive-android#470). + 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/assets/rive/ore.riv.d.ts b/example/assets/rive/ore.riv.d.ts new file mode 100644 index 00000000..d42da7d0 --- /dev/null +++ b/example/assets/rive/ore.riv.d.ts @@ -0,0 +1,20 @@ +// Generated by rive-gen-types — do not edit manually. @generated +/* eslint-disable */ +// Source: ore.riv +import type { RiveFileSource } from '@rive-app/react-native'; +declare const asset: RiveFileSource<{ + schemaVersion: 1; + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + enums: {}; + viewModels: { + ViewModel1: { + + }; + }; + referencedAssets: {}; +}>; +export default asset; diff --git a/example/src/demos/GPUCanvasDemo.tsx b/example/src/demos/GPUCanvasDemo.tsx new file mode 100644 index 00000000..f8d4faca --- /dev/null +++ b/example/src/demos/GPUCanvasDemo.tsx @@ -0,0 +1,87 @@ +import { useEffect, 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'); + +type Status = 'enabled' | 'ignored' | 'unavailable'; + +const STATUS_TEXT: Record = { + enabled: 'GPU Canvas enabled', + ignored: + 'GPU Canvas request ignored: a Rive file was already loaded in this process. Restart the app and open this page first.', + unavailable: + 'GPU Canvas is not available in this native SDK version; the scene below renders without it.', +}; + +export default function GPUCanvasDemo() { + const [status, setStatus] = useState(() => { + RiveRuntime.setGPUCanvasEnabled(true); + return RiveRuntime.isGPUCanvasEnabled() ? 'enabled' : 'ignored'; + }); + const { riveFile, error: fileError } = useRiveFile(ORE); + const [viewError, setViewError] = useState(null); + + // The worker exists once the file has loaded; only then is the flag final. + useEffect(() => { + if (riveFile && !RiveRuntime.isGPUCanvasEnabled()) { + setStatus((s) => (s === 'enabled' ? 'unavailable' : s)); + } + }, [riveFile]); + + return ( + + + {STATUS_TEXT[status]} + + {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 e4e96379..a3515255 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 84e0a254..1478e2d3 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveRuntimeSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveRuntimeSpec.kt @@ -34,6 +34,10 @@ abstract class HybridRiveRuntimeSpec: HybridObject() { @get:DoNotStrip @get:Keep abstract val initError: String? + + @get:DoNotStrip + @get:Keep + abstract val isGPUCanvasEnabled: Boolean // Methods @DoNotStrip @@ -43,6 +47,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/src/core/RiveRuntime.ts b/src/core/RiveRuntime.ts index b5da407c..991d0cb1 100644 --- a/src/core/RiveRuntime.ts +++ b/src/core/RiveRuntime.ts @@ -36,4 +36,27 @@ 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. Turns + * `false` after the first file load if the native SDK cannot provide it. + */ + 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; }