Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
14 changes: 0 additions & 14 deletions android/src/legacy/java/com/rive/RiveRenderBackendConfig.kt

This file was deleted.

23 changes: 23 additions & 0 deletions android/src/legacy/java/com/rive/RiveWorkerConfig.kt
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Boolean> 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
Expand All @@ -42,12 +55,23 @@ class HybridViewModelBooleanProperty(
}

override fun getValueAsync(): Promise<Boolean> {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String, Boolean>()

// 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)
Expand Down
19 changes: 19 additions & 0 deletions android/src/new/java/com/rive/DeferredRiveWorker.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
40 changes: 0 additions & 40 deletions android/src/new/java/com/rive/RiveRenderBackendConfig.kt

This file was deleted.

57 changes: 57 additions & 0 deletions android/src/new/java/com/rive/RiveWorkerConfig.kt
Original file line number Diff line number Diff line change
@@ -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 }
}
20 changes: 20 additions & 0 deletions docs/runtime-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
11 changes: 11 additions & 0 deletions example/__tests__/viewmodel-properties.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Binary file added example/assets/rive/ore.riv
Binary file not shown.
Loading
Loading