diff --git a/bun.lock b/bun.lock index 3c9236b9..e7cb532e 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,6 @@ "react-native-screens": "^4.19.0", "react-native-video": "^6.18.0", "react-native-worklets": "^0.7.1", - "react-native-worklets-core": "^1.6.2", "shared": "workspace:^", }, "devDependencies": { @@ -66,7 +65,6 @@ "react-native-screens": "~4.16.0", "react-native-video": "^6.18.0", "react-native-worklets": "0.5.1", - "react-native-worklets-core": "^1.6.2", "shared": "workspace:^", }, "devDependencies": { @@ -92,6 +90,7 @@ "react": "19.2.0", "react-native": "0.83.1", "react-native-builder-bob": "^0.40.17", + "react-native-worklets": "^0.7.1", "react-native-worklets-core": "^1.6.2", "release-it": "^15.0.0", "typescript": "^5.8.3", @@ -99,7 +98,8 @@ "peerDependencies": { "react": "*", "react-native": "*", - "react-native-worklets-core": ">=1.3.2", + "react-native-reanimated": ">=4.0.0", + "react-native-worklets": "*", }, }, }, diff --git a/examples/AppExampleFabric/index.js b/examples/AppExampleFabric/index.js index 0e41963a..e9b4e1f7 100644 --- a/examples/AppExampleFabric/index.js +++ b/examples/AppExampleFabric/index.js @@ -3,9 +3,6 @@ import App from 'shared/src/App' import { name as appName } from './app.json' import { StrictMode } from 'react' -import { version } from 'react-native-worklets-core/package.json' -console.log(`Using react-native-worklets-core@${version}`) - // Setup a logger for filament import { setLogger } from 'react-native-filament' // A function that can wrap a console log call to add a prefix diff --git a/examples/AppExampleFabric/package.json b/examples/AppExampleFabric/package.json index c1c47e0f..100c7c8b 100644 --- a/examples/AppExampleFabric/package.json +++ b/examples/AppExampleFabric/package.json @@ -27,7 +27,6 @@ "react-native-screens": "^4.19.0", "react-native-video": "^6.18.0", "react-native-worklets": "^0.7.1", - "react-native-worklets-core": "^1.6.2", "shared": "workspace:^" }, "devDependencies": { diff --git a/examples/ExpoExample/package.json b/examples/ExpoExample/package.json index d74164bc..f769905a 100644 --- a/examples/ExpoExample/package.json +++ b/examples/ExpoExample/package.json @@ -23,7 +23,6 @@ "react-native-screens": "~4.16.0", "react-native-video": "^6.18.0", "react-native-worklets": "0.5.1", - "react-native-worklets-core": "^1.6.2", "shared": "workspace:^" }, "devDependencies": { diff --git a/examples/Shared/src/App.tsx b/examples/Shared/src/App.tsx index 523b50bd..d327f0ed 100644 --- a/examples/Shared/src/App.tsx +++ b/examples/Shared/src/App.tsx @@ -3,6 +3,7 @@ import { StyleSheet, Pressable, Text, View, ScrollView } from 'react-native' import { NavigationContainer, useNavigation } from '@react-navigation/native' import { createNativeStackNavigator } from '@react-navigation/native-stack' import { GestureHandlerRootView } from 'react-native-gesture-handler' +import {SafeAreaView} from 'react-native-safe-area-context' import { AnimationTransitions } from './AnimationTransitions' import { AnimationTransitionsRecording } from './AnimationTransitionsRecording' @@ -50,6 +51,7 @@ function NavigationItem(props: { name: string; route: string }) { function HomeScreen() { return ( + @@ -69,6 +71,7 @@ function HomeScreen() { + ) } @@ -88,30 +91,30 @@ function App() { - - - - - - + /> */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} - - - - - - - - + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} - + {/* */} diff --git a/package.json b/package.json index 5b6f05ec..a02e14de 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ } ], "scripts": { - "postinstall": "bunx patch-package" }, "license": "MIT", "bugs": { diff --git a/package/android/CMakeLists.txt b/package/android/CMakeLists.txt index a7e1ec71..88304458 100644 --- a/package/android/CMakeLists.txt +++ b/package/android/CMakeLists.txt @@ -38,6 +38,7 @@ add_library( ../cpp/RNFChoreographer.cpp ../cpp/RNFChoreographerWrapper.cpp ../cpp/RNFListener.cpp + ../cpp/jsi/RNFBoxedHybridObject.cpp ../cpp/jsi/RNFHybridObject.cpp ../cpp/jsi/RNFPromise.cpp ../cpp/jsi/RNFPromiseFactory.cpp @@ -141,16 +142,15 @@ else() ) endif() -# Link with RNWC: -find_package(react-native-worklets-core REQUIRED CONFIG) -message("RN Filament: react-native-worklets core found! Enabling Worklets support...") -message("RN Filament: react-native-worklets-core found in ${react-native-worklets-core_DIR}") + +find_package(react-native-worklets REQUIRED) # <-- for Worklets target_link_libraries( ${PACKAGE_NAME} - react-native-worklets-core::rnworklets + react-native-worklets::worklets ) add_definitions(-DHAS_WORKLETS=1) + # Filament (local CMake project as a git submodule) message("RN Filament: Adding pre-compiled libraries in ${FILAMENT_DIR}...") file(GLOB FILAMENT_LIBRARIES "${FILAMENT_DIR}/lib/${ANDROID_ABI}/*.a") diff --git a/package/android/build.gradle b/package/android/build.gradle index 0ce7ec38..a67718fa 100644 --- a/package/android/build.gradle +++ b/package/android/build.gradle @@ -189,7 +189,7 @@ dependencies { // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" - implementation project(":react-native-worklets-core") + implementation project(":react-native-worklets") } if (isNewArchitectureEnabled()) { diff --git a/package/cpp/RNFFilamentProxy.cpp b/package/cpp/RNFFilamentProxy.cpp index c881994d..9444308f 100644 --- a/package/cpp/RNFFilamentProxy.cpp +++ b/package/cpp/RNFFilamentProxy.cpp @@ -10,6 +10,7 @@ #include "core/RNFEngineConfigHelper.h" #include "jsi/RNFPromise.h" #include "threading/RNFDispatcher.h" +#include "threading/RNFAsyncQueueImpl.h" #include #include @@ -31,7 +32,10 @@ void FilamentProxy::loadHybridMethods() { registerHybridMethod("getCurrentDispatcher", &FilamentProxy::getCurrentDispatcher, this); registerHybridGetter("hasWorklets", &FilamentProxy::getHasWorklets, this); #if HAS_WORKLETS - registerHybridMethod("createWorkletContext", &FilamentProxy::createWorkletContext, this); + // Newly added APIs: + registerHybridMethod("createWorkletAsyncQueue", &FilamentProxy::createWorkletAsyncQueue, this); + registerHybridMethod("installDispatcher", &FilamentProxy::installDispatcher, this); + registerHybridMethod("box", &FilamentProxy::box, this); #endif } @@ -44,23 +48,29 @@ bool FilamentProxy::getHasWorklets() { } #if HAS_WORKLETS -std::shared_ptr FilamentProxy::createWorkletContext() { - Logger::log(TAG, "Creating Worklet Context..."); - auto jsDispatcher = getJSDispatcher(); - auto runOnJS = [=](std::function&& function) { jsDispatcher->runAsync(std::move(function)); }; - auto renderThreadDispatcher = getRenderThreadDispatcher(); - auto runOnWorklet = [=](std::function&& function) { renderThreadDispatcher->runAsync(std::move(function)); }; - auto& runtime = getMainJSRuntime(); - auto workletContext = std::make_shared("FilamentRenderer", &runtime, runOnJS, runOnWorklet); - Logger::log(TAG, "Successfully created WorkletContext! Installing global Dispatcher..."); - - workletContext->invokeOnWorkletThread([=](RNWorklet::JsiWorkletContext*, jsi::Runtime& runtime) { +std::shared_ptr FilamentProxy::createWorkletAsyncQueue() { + Logger::log(TAG, "Creating Worklet AsyncQueue..."); + auto renderThreadDispatcher = getRenderThreadDispatcher(); +// auto runOnWorklet = [=](std::function&& function) { renderThreadDispatcher->runAsync(std::move(function)); }; + // TODO: i am pretty sure i should hold this dispatcher somewhere? or will the JS engine keep it alive as its NativeState? + auto asyncQueue = std::make_shared(renderThreadDispatcher); + + return asyncQueue; +} + + +jsi::Value FilamentProxy::installDispatcher(jsi::Runtime& runtime, const jsi::Value&, const jsi::Value*, size_t) { + auto renderThreadDispatcher = getRenderThreadDispatcher(); // todo: return dispatcher, and pass it here is cleaner i guess + // Note: one thing that is odd, is that this is called with the correct runtime, but on the "wrong" thread. + // this will still be called from the JS thread, but the runtime is the worklet runtime. Dispatcher::installRuntimeGlobalDispatcher(runtime, renderThreadDispatcher); - Logger::log(TAG, "Successfully installed global Dispatcher in WorkletContext!"); - }); + return jsi::Value::undefined(); +} - return workletContext; +std::shared_ptr FilamentProxy::box(const std::shared_ptr& hybridObject) { + return std::make_shared(hybridObject); } + #endif jsi::Value FilamentProxy::getCurrentDispatcher(jsi::Runtime& runtime, const jsi::Value&, const jsi::Value*, size_t) { diff --git a/package/cpp/RNFFilamentProxy.h b/package/cpp/RNFFilamentProxy.h index 6cf84b00..d731579f 100644 --- a/package/cpp/RNFFilamentProxy.h +++ b/package/cpp/RNFFilamentProxy.h @@ -18,21 +18,13 @@ #include "bullet/RNFBulletWrapper.h" #include "core/RNFEngineWrapper.h" #include "jsi/RNFHybridObject.h" +#include "jsi/RNFBoxedHybridObject.h" #include "test/RNFTestHybridObject.h" #include "threading/RNFDispatcher.h" +#include "threading/RNFAsyncQueueImpl.h" #include -#ifdef HAS_WORKLETS -#if __has_include() -// Old arch & CocoaPod headers on apple -#include -#else -// New arch android, where RNWC and RNF c++ modules are build inside the app's project -#include "WKTJsiWorkletContext.h" -#endif -#endif // HAS_WORKLETS - namespace margelo { using namespace facebook; @@ -93,6 +85,7 @@ class FilamentProxy : public HybridObject { #if HAS_WORKLETS /** + * TODO: update this comment * Create a new Worklet Context that runs on the Filament Renderer Thread. * * The FilamentProxy does not hold a strong reference to the Worklet Context, @@ -100,7 +93,9 @@ class FilamentProxy : public HybridObject { * * The caller (JS) is responsible for keeping the returned reference strong. */ - std::shared_ptr createWorkletContext(); + std::shared_ptr createWorkletAsyncQueue(); + jsi::Value installDispatcher(jsi::Runtime& runtime, const jsi::Value&, const jsi::Value*, size_t); + std::shared_ptr box(const std::shared_ptr& hybridObject); #endif public: diff --git a/package/cpp/jsi/RNFBoxedHybridObject.cpp b/package/cpp/jsi/RNFBoxedHybridObject.cpp new file mode 100644 index 00000000..aafbd51f --- /dev/null +++ b/package/cpp/jsi/RNFBoxedHybridObject.cpp @@ -0,0 +1,31 @@ +// +// BoxedHybridObject.cpp +// NitroModules +// +// Created by Marc Rousavy on 17.09.24. +// + +#include "RNFBoxedHybridObject.h" + +namespace margelo { + + std::vector RNFBoxedHybridObject::getPropertyNames(facebook::jsi::Runtime& runtime) { + return jsi::PropNameID::names(runtime, "unbox"); + } + + jsi::Value RNFBoxedHybridObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) { + std::string name = propName.utf8(runtime); + + if (name == "unbox") { + return jsi::Function::createFromHostFunction( + runtime, jsi::PropNameID::forUtf8(runtime, "unbox"), 0, + [hybridObject = _hybridObject](jsi::Runtime& runtime, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value { + return JSIConverter>::toJSI(runtime, hybridObject); +// return jsi::Object::createFromHostObject(runtime, hybridObject); + }); + } + + return jsi::Value::undefined(); + } + +} // namespace margelo::nitro diff --git a/package/cpp/jsi/RNFBoxedHybridObject.h b/package/cpp/jsi/RNFBoxedHybridObject.h new file mode 100644 index 00000000..5dcd686e --- /dev/null +++ b/package/cpp/jsi/RNFBoxedHybridObject.h @@ -0,0 +1,39 @@ +// +// Created by Hanno Gödecke on 11.01.26. +// From https://github.com/mrousavy/nitro/blob/afa41bae947ecf738c33569ead42eaf0825cc6d5/packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.hpp +// TODO: should be removable once migrated to nitro modules + RNW supporting nitro HybridObjects +// + +#pragma once + +#include "RNFHybridObject.h" +#include +#include + +namespace margelo { + + using namespace facebook; + +/** + * Represents a `HybridObject` that has been boxed into a `jsi::HostObject`. + * + * While `HybridObject`s are runtime agnostic, some threading/worklet libraries do not support copying over objects + * with `jsi::NativeState` and a prototype chain (which is what a `HybridObject` is), so Nitro offers support for + * boxing those `HybridObject`s into a type that those libraries support - which is a `jsi::HostObject`. + * + * Simply call `unbox()` on this `jsi::HostObject` from the new Runtime/context to get the `HybridObject` again. + */ + class RNFBoxedHybridObject final : public jsi::HostObject { + public: + explicit RNFBoxedHybridObject(const std::shared_ptr& hybridObject) : _hybridObject(hybridObject) {} + + public: + jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& propName) override; + std::vector getPropertyNames(jsi::Runtime& runtime) override; + + private: + std::shared_ptr _hybridObject; + }; + +} // namespace margelo::nitro + diff --git a/package/cpp/threading/RNFAsyncQueueImpl.h b/package/cpp/threading/RNFAsyncQueueImpl.h new file mode 100644 index 00000000..239dc3f2 --- /dev/null +++ b/package/cpp/threading/RNFAsyncQueueImpl.h @@ -0,0 +1,27 @@ +// +// Created by Hanno Gödecke on 11.01.26. +// + +#if __has_include() +#include +#elif __has_include() +#include +#else +#error react-native-worklets Prefab not found! +#endif + +#pragma once + +namespace margelo { + class RNFAsyncQueueImpl : public worklets::AsyncQueue { + public: + explicit RNFAsyncQueueImpl(std::shared_ptr dispatcher) : _dispatcher(std::move(dispatcher)) {} + + void push(std::function &&job) override { + _dispatcher->runAsync(std::move(job)); + } + + private: + std::shared_ptr _dispatcher; + }; +} // namespace margelo diff --git a/package/cpp/threading/RNFDispatcher.cpp b/package/cpp/threading/RNFDispatcher.cpp index 41e3c80d..9e7a2ece 100644 --- a/package/cpp/threading/RNFDispatcher.cpp +++ b/package/cpp/threading/RNFDispatcher.cpp @@ -1,6 +1,5 @@ #include "RNFDispatcher.h" #include "RNFLogger.h" -#include "jsi/RNFWorkletRuntimeCollector.h" namespace margelo { @@ -9,10 +8,7 @@ using namespace facebook; static constexpr auto GLOBAL_DISPATCHER_HOLDER_NAME = "__globalDispatcher"; void Dispatcher::installRuntimeGlobalDispatcher(jsi::Runtime& runtime, std::shared_ptr dispatcher) { - Logger::log(TAG, "Installing global Dispatcher Holder..."); - - // Track the runtime's lifetime - WorkletRuntimeCollector::install(runtime); + Logger::log(TAG, "Installing global Dispatcher Holder on runtime ptr: " + std::to_string(reinterpret_cast(&runtime)) + " desc: " + runtime.description()); jsi::Object dispatcherHolder(runtime); dispatcherHolder.setNativeState(runtime, dispatcher); diff --git a/package/package.json b/package/package.json index 0968dafe..fde08cd0 100644 --- a/package/package.json +++ b/package/package.json @@ -94,6 +94,7 @@ "react": "19.2.0", "react-native": "0.83.1", "react-native-builder-bob": "^0.40.17", + "react-native-worklets": "^0.7.1", "react-native-worklets-core": "^1.6.2", "release-it": "^15.0.0", "typescript": "^5.8.3" @@ -101,7 +102,8 @@ "peerDependencies": { "react": "*", "react-native": "*", - "react-native-worklets-core": ">=1.3.2" + "react-native-reanimated": ">=4.0.0", + "react-native-worklets": "*" }, "release-it": { "git": { diff --git a/package/scripts/build-filament.sh b/package/scripts/build-filament.sh index f6a007f7..346e1f34 100755 --- a/package/scripts/build-filament.sh +++ b/package/scripts/build-filament.sh @@ -120,7 +120,20 @@ if [ "$BUILD_ANDROID" = "true" ]; then # TODO(Marc): Use Filament from the Maven/Gradle library, to avoid shipping this huge dependency over npm. echo "[Filament] Building Filament for Android ($target)" # -v = Exclude Vulkan support - ./build.sh -p android "$target" + if [ "$CI" = "true" ]; then + # In CI, pipe through tail to keep only last 1000 lines (prevents disk space issues) + ./build.sh -p android "$target" 2>&1 | tail -n 1000 > filament-android-build.log + # Check the exit code of build.sh (first command in pipe) + if [ "${PIPESTATUS[0]}" -ne 0 ]; then + echo "Build failed! Last 1000 lines of output:" + cat filament-android-build.log + rm filament-android-build.log + exit 1 + fi + rm filament-android-build.log + else + ./build.sh -p android "$target" + fi # echo "Building Android .aar ($target)..." # cd android diff --git a/package/src/ErrorUtils.ts b/package/src/ErrorUtils.ts index d361ce06..970d6f77 100644 --- a/package/src/ErrorUtils.ts +++ b/package/src/ErrorUtils.ts @@ -1,4 +1,4 @@ -import { Worklets } from 'react-native-worklets-core' +import { runOnJS } from 'react-native-worklets' /** * Report an error to react native's `ErrorUtils` if available, or log to console otherwise. @@ -18,7 +18,7 @@ export function reportError(error: unknown, fatal: boolean): void { } } -const throwErrorOnJS = Worklets.createRunOnJS((message: string, stack: string | undefined, fatal: boolean) => { +const throwErrorOnJS = runOnJS((message: string, stack: string | undefined, fatal: boolean) => { const error = new Error() error.message = message error.stack = stack diff --git a/package/src/hooks/internal/useApplyTransformations.tsx b/package/src/hooks/internal/useApplyTransformations.tsx index e59a8942..17e53c06 100644 --- a/package/src/hooks/internal/useApplyTransformations.tsx +++ b/package/src/hooks/internal/useApplyTransformations.tsx @@ -4,7 +4,7 @@ import { useFilamentContext } from '../useFilamentContext' import { AABB, Entity, Float3 } from '../../types' import { areFloat3Equal, isWorkletSharedValue } from '../../utilities/helper' import { useWorkletEffect } from '../useWorkletEffect' -import { ISharedValue, Worklets } from 'react-native-worklets-core' +import { makeMutable, type SharedValue } from 'react-native-reanimated' type Params = { // If null it will not take the entity from the context, as it indicates that it will be provided through the param @@ -76,9 +76,9 @@ export function useApplyTransformations({ to: entity, transformProps, aabb }: Pa transformToUnitCube, ]) - const prevScaleShared = useRef(isWorkletSharedValue(scale) ? Worklets.createSharedValue(null) : null) - const prevRotateShared = useRef(isWorkletSharedValue(rotate) ? Worklets.createSharedValue(null) : null) - const prevPositionShared = useRef(isWorkletSharedValue(position) ? Worklets.createSharedValue(null) : null) + const prevScaleShared = useRef(isWorkletSharedValue(scale) ? makeMutable(null) : null) + const prevRotateShared = useRef(isWorkletSharedValue(rotate) ? makeMutable(null) : null) + const prevPositionShared = useRef(isWorkletSharedValue(position) ? makeMutable(null) : null) // Effects for when a transform option is a shared value (SRT) useWorkletEffect(() => { @@ -86,18 +86,19 @@ export function useApplyTransformations({ to: entity, transformProps, aabb }: Pa if (entity == null) return - const unsubscribers: (() => void)[] = [] + const unsubscribers: [SharedValue, number][] = [] // Generic handler for worklet transform values const createTransformHandler = ( value: any, - prevValueShared: ISharedValue | null, + prevValueShared: SharedValue | null, updater: (newValue: Float3) => void ) => { 'worklet' if (value == null || !isWorkletSharedValue(value) || Array.isArray(value)) return null - const unsubscribe = value.addListener(() => { + const randomNumericId = Number(Math.random().toString().substring(12)) + value.addListener(randomNumericId, () => { 'worklet' // Check if value has changed to avoid duplicate applications in strict mode @@ -113,7 +114,7 @@ export function useApplyTransformations({ to: entity, transformProps, aabb }: Pa } }) - unsubscribers.push(unsubscribe) + unsubscribers.push([value, randomNumericId]) return value } @@ -148,9 +149,10 @@ export function useApplyTransformations({ to: entity, transformProps, aabb }: Pa positionHandler.value = [positionHandler.value[0], positionHandler.value[1], positionHandler.value[2]] } - return () => { - 'worklet' - unsubscribers.forEach((unsubscribe) => unsubscribe()) - } + // TODO: bring back, currently crashes? + // return () => { + // 'worklet' + // unsubscribers.forEach(([value, id]) => value.removeListener(id)) + // } }) } diff --git a/package/src/hooks/useEngine.ts b/package/src/hooks/useEngine.ts index 8d9ede6c..29236d42 100644 --- a/package/src/hooks/useEngine.ts +++ b/package/src/hooks/useEngine.ts @@ -1,7 +1,9 @@ import type { Engine, EngineBackend, EngineConfig } from '../types' import { FilamentProxy } from '../native/FilamentProxy' -import { IWorkletContext, useWorklet } from 'react-native-worklets-core' import { useDisposableResource } from './useDisposableResource' +import { type WorkletRuntime } from 'react-native-worklets' +import { useCallback } from 'react' +import { scheduleOnRuntimeAsync } from '../utilities/scheduleOnRuntimeAsync' export interface EngineProps { /** @@ -15,15 +17,17 @@ export interface EngineProps { */ config?: EngineConfig - context: IWorkletContext + runtime: WorkletRuntime } -export function useEngine({ backend, config, context }: EngineProps): Engine | undefined { +export function useEngine({ backend, config, runtime }: EngineProps): Engine | undefined { // Important: create the engine on the worklet thread, so its owned by the worklet thread - const createEngine = useWorklet(context, () => { - 'worklet' - return FilamentProxy.createEngine(backend ?? undefined, config ?? undefined) - }) + const createEngine = useCallback(() => { + return scheduleOnRuntimeAsync(runtime, () => { + 'worklet' + return FilamentProxy.createEngine(backend ?? undefined, config ?? undefined) + }) + }, [backend, config, runtime]) const engine = useDisposableResource(createEngine) return engine diff --git a/package/src/hooks/useFilamentContext.ts b/package/src/hooks/useFilamentContext.ts index 12dace35..1d3ebb9f 100644 --- a/package/src/hooks/useFilamentContext.ts +++ b/package/src/hooks/useFilamentContext.ts @@ -11,7 +11,7 @@ import { TransformManager, View, } from '../types' -import { IWorkletContext } from 'react-native-worklets-core' +import { WorkletRuntime } from 'react-native-worklets' export type FilamentContextType = { engine: Engine @@ -23,7 +23,7 @@ export type FilamentContextType = { camera: RNFCamera renderer: Renderer nameComponentManager: NameComponentManager - workletContext: IWorkletContext + workletRuntime: WorkletRuntime /** * This is a private API, do not use this. diff --git a/package/src/hooks/useLightEntity.ts b/package/src/hooks/useLightEntity.ts index 06740b2b..34ff3579 100644 --- a/package/src/hooks/useLightEntity.ts +++ b/package/src/hooks/useLightEntity.ts @@ -1,15 +1,14 @@ import { useMemo } from 'react' import { LightConfig, LightManager } from '../types' -import { ISharedValue } from 'react-native-worklets-core' -import { useFilamentContext } from './useFilamentContext' import { useWorkletEffect } from './useWorkletEffect' import convertKelvinToLinearSRGB from '../utilities/convertKelvinToLinearSRGB' +import { type SharedValue } from 'react-native-reanimated' export type UseLightEntityProps = | LightConfig | (Omit & { - intensity?: number | ISharedValue - colorKelvin?: number | ISharedValue + intensity?: number | SharedValue + colorKelvin?: number | SharedValue }) /** @@ -60,7 +59,6 @@ export function useLightEntity(lightManager: LightManager, config: UseLightEntit ]) // Subscribe to the intensity shared value - const { workletContext } = useFilamentContext() useWorkletEffect(() => { 'worklet' const intensity = config.intensity @@ -69,12 +67,21 @@ export function useLightEntity(lightManager: LightManager, config: UseLightEntit const setIntensity = lightManager.setIntensity - return intensity.addListener( - workletContext.createRunAsync(() => { + const randomNumericId = Number(Math.random().toString().substring(12)) + intensity.addListener( + randomNumericId, + // TODO: do we need to wrap this in createRunAsync? + () => { 'worklet' setIntensity(entity, intensity.value) - }) + } ) + + // TODO: i believe this is causing crashes rn + // return () => { + // 'worklet' + // intensity.removeListener(randomNumericId) + // } }) // Subscribe to the colorKelvin shared value @@ -86,12 +93,17 @@ export function useLightEntity(lightManager: LightManager, config: UseLightEntit const setColor = lightManager.setColor - return colorKelvin.addListener( - workletContext.createRunAsync(() => { - 'worklet' - setColor(entity, convertKelvinToLinearSRGB(colorKelvin.value)) - }) - ) + const randomNumericId = Number(Math.random().toString().substring(12)) + colorKelvin.addListener(randomNumericId, () => { + 'worklet' + setColor(entity, convertKelvinToLinearSRGB(colorKelvin.value)) + }) + + // TODO: i believe this is causing crashes rn + // return () => { + // 'worklet' + // colorKelvin.removeListener(randomNumericId) + // } }) return entity diff --git a/package/src/hooks/useModel.ts b/package/src/hooks/useModel.ts index c9244554..de19d1a3 100644 --- a/package/src/hooks/useModel.ts +++ b/package/src/hooks/useModel.ts @@ -6,6 +6,7 @@ import usePrevious from './usePrevious' import { useWorkletEffect } from './useWorkletEffect' import { AABB, Entity } from '../types' import { useMemo } from 'react' +import { scheduleOnRuntimeAsync } from '../utilities/scheduleOnRuntimeAsync' export interface UseModelConfigParams { /** @@ -59,7 +60,7 @@ export type FilamentModel = */ export function useModel(source: BufferSource, props?: UseModelConfigParams): FilamentModel { const { shouldReleaseSourceData = true, addToScene = true, instanceCount } = props ?? {} - const { engine, scene, workletContext } = useFilamentContext() + const { engine, scene, workletRuntime } = useFilamentContext() const assetBuffer = useBuffer({ source: source, releaseOnUnmount: false }) // Note: the native cleanup of the asset will remove it automatically from the scene @@ -69,7 +70,7 @@ export function useModel(source: BufferSource, props?: UseModelConfigParams): Fi throw new Error('instanceCount must be greater than 0') } - return workletContext.runAsync(() => { + return scheduleOnRuntimeAsync(workletRuntime, () => { 'worklet' let loadedAsset: FilamentAsset @@ -84,7 +85,7 @@ export function useModel(source: BufferSource, props?: UseModelConfigParams): Fi return loadedAsset }) - }, [assetBuffer, workletContext, engine, instanceCount]) + }, [assetBuffer, instanceCount, workletRuntime, engine]) useWorkletEffect(() => { 'worklet' diff --git a/package/src/hooks/useWorkletCallback.ts b/package/src/hooks/useWorkletCallback.ts index 88c5d591..5793d075 100644 --- a/package/src/hooks/useWorkletCallback.ts +++ b/package/src/hooks/useWorkletCallback.ts @@ -1,27 +1,14 @@ -import { getWorkletDependencies } from 'react-native-worklets-core' import { useFilamentContext } from './useFilamentContext' -import { wrapWithErrorHandler } from '../ErrorUtils' -import { useMemo } from 'react' +import { useCallback } from 'react' +import { scheduleOnRuntimeAsync } from '../utilities/scheduleOnRuntimeAsync' /** * Creates a callback that can be executed in he separate worklet thread of the engine. */ export function useWorkletCallback any>(callback: T): (...args: Parameters) => Promise> { - const { workletContext } = useFilamentContext() + const { workletRuntime } = useFilamentContext() - // Note: from react-native-worklets-core/useWorklet - // As we want to wrap using `wrapWithErrorHandler` the dependencies must be captured from the - // callback, not from the wrapper. - - // As a dependency for this use-memo we use all of the values captured inside the worklet, - // as well as the unique context name. - const dependencies = [...getWorkletDependencies(callback)] - - return useMemo( - () => { - return workletContext.createRunAsync(wrapWithErrorHandler(callback)) - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - dependencies - ) + return useCallback(() => { + return scheduleOnRuntimeAsync(workletRuntime, callback) + }, [callback, workletRuntime]) } diff --git a/package/src/hooks/useWorkletEffect.ts b/package/src/hooks/useWorkletEffect.ts index 0b1573c5..ac7e1922 100644 --- a/package/src/hooks/useWorkletEffect.ts +++ b/package/src/hooks/useWorkletEffect.ts @@ -1,30 +1,23 @@ import { useEffect } from 'react' import { useFilamentContext } from './useFilamentContext' -import { getWorkletDependencies, isWorklet } from 'react-native-worklets-core' -import { wrapWithErrorHandler } from '../ErrorUtils' +import { scheduleOnRuntime } from 'react-native-worklets' +import { scheduleOnRuntimeAsync } from '../utilities/scheduleOnRuntimeAsync' type CleanupFn = () => void export function useWorkletEffect(workletFunction: () => CleanupFn | void) { - const { workletContext } = useFilamentContext() + const { workletRuntime } = useFilamentContext() useEffect(() => { - const cleanupPromise = workletContext.runAsync(wrapWithErrorHandler(workletFunction)) + const cleanupPromise = scheduleOnRuntimeAsync(workletRuntime, workletFunction) return () => { cleanupPromise.then((cleanup): void => { if (cleanup == null || typeof cleanup !== 'function') { // no cleanup function was returned, do nothing. return } - if (isWorklet(cleanup)) { - // call cleanup function on Worklet context - workletContext.runAsync(cleanup) - } else { - // call normal cleanup JS function on normal context - cleanup() - } + scheduleOnRuntime(workletRuntime, cleanup) }) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, getWorkletDependencies(workletFunction)) + }, [workletFunction, workletRuntime]) } diff --git a/package/src/native/FilamentProxy.ts b/package/src/native/FilamentProxy.ts index f05162a8..2e647760 100644 --- a/package/src/native/FilamentProxy.ts +++ b/package/src/native/FilamentProxy.ts @@ -2,13 +2,12 @@ import { FilamentBuffer } from './FilamentBuffer' import type { Engine } from '../types/Engine' import { FilamentView } from './FilamentViewTypes' import type { BulletAPI } from '../bullet/types/api' -import type { IWorkletContext } from 'react-native-worklets-core' import { EngineBackend, EngineConfig } from '../types' import { TFilamentRecorder } from '../types/FilamentRecorder' import { Choreographer } from '../types/Choreographer' import { Dispatcher } from './Dispatcher' import { FilamentModule } from './FilamentModule' -import { Worklets } from 'react-native-worklets-core' +import { createWorkletRuntime } from 'react-native-worklets' interface TestHybridObject { int: number @@ -74,7 +73,10 @@ export interface TFilamentProxy { */ readonly hasWorklets: boolean + createChoreographer(): Choreographer + /** + * TODO: update content * Create a Worklet context used for Rendering to Filament. * * This should only be called once, and the returned value should be kept strong. @@ -91,9 +93,12 @@ export interface TFilamentProxy { * }) * ``` */ - createWorkletContext: () => IWorkletContext - - createChoreographer(): Choreographer + // New APIs i added: + createWorkletAsyncQueue: () => object + installDispatcher: () => void + box: (hybridObject: T) => { + unbox: () => T + } } const successful = FilamentModule.install() @@ -114,15 +119,20 @@ if (proxy == null) { if (!proxy.hasWorklets) { throw new Error( - 'Failed to initialize react-native-filament - Worklets are not available (HAS_WORKLETS=false), did you install react-native-worklets-core?' + 'Failed to initialize react-native-filament - Worklets are not available (HAS_WORKLETS=false), did you install react-native-worklets & react-native-reanimated?' ) } export const FilamentProxy = proxy -// We must make sure that the Worklets API (module) is initialized (as its possible a lazy-loaded CxxTurboModule), -// to initialize we must only call any property of the module: -Worklets.defaultContext - -// Create our custom RNF worklet context: -export const FilamentWorkletContext = proxy.createWorkletContext() +const FilamentWorkletQueue = proxy.createWorkletAsyncQueue() +export const FilamentWorkletRuntime = createWorkletRuntime({ + name: 'FilamentWorkletRuntime', + useDefaultQueue: false, + customQueue: FilamentWorkletQueue, + initializer: () => { + 'worklet' + // Note: this will still be called from the JS thread, weird + proxy.installDispatcher() + }, +}) diff --git a/package/src/react/Animator.tsx b/package/src/react/Animator.tsx index e5dd65b5..cfa247b6 100644 --- a/package/src/react/Animator.tsx +++ b/package/src/react/Animator.tsx @@ -2,9 +2,9 @@ import React, { useEffect } from 'react' import { FilamentInstance } from '../types' import { RenderCallbackContext } from './RenderCallbackContext' import { useAnimator } from '../hooks/useAnimator' -import { ISharedValue, useSharedValue } from 'react-native-worklets-core' import usePrevious from '../hooks/usePrevious' import { ParentInstancesContext } from './ParentInstancesContext' +import { useSharedValue, type SharedValue } from 'react-native-reanimated' export type AnimationItem = { index: number @@ -17,7 +17,7 @@ export type AnimatorProps = { * The index of the animation to play. To find out the index for the animation you want to play, you can use the `onAnimationsLoaded` callback. * @default 0 **/ - animationIndex?: number | ISharedValue + animationIndex?: number | SharedValue /** * Returns a list of all animations for the model. @@ -117,12 +117,13 @@ function AnimatorImpl({ instance, animationIndex: animationIndexProp = 0, transi // Update previous index if the prop is a shared value: let value = animationIndexProp.value - const removeListener = animationIndexProp.addListener(() => { + const listenerId = Date.now() + animationIndexProp.addListener(listenerId, () => { prevAnimationIndex.value = value value = animationIndexProp.value }) return () => { - removeListener() + animationIndexProp.removeListener(listenerId) } }, [animationIndexProp, prevAnimationIndex, previousAnimationIndexProp]) diff --git a/package/src/react/Camera.tsx b/package/src/react/Camera.tsx index cea9e35c..11f9d960 100644 --- a/package/src/react/Camera.tsx +++ b/package/src/react/Camera.tsx @@ -1,4 +1,4 @@ -import { useSharedValue } from 'react-native-worklets-core' +import { useSharedValue } from 'react-native-reanimated' import { CameraManipulator, Float3 } from '../types' import { useFilamentContext } from '../hooks/useFilamentContext' import { RenderCallbackContext } from './RenderCallbackContext' diff --git a/package/src/react/FilamentScene.tsx b/package/src/react/FilamentScene.tsx index 8fd8c9d4..aa137512 100644 --- a/package/src/react/FilamentScene.tsx +++ b/package/src/react/FilamentScene.tsx @@ -1,15 +1,16 @@ -import { PropsWithChildren, useMemo } from 'react' -import { FilamentProxy, FilamentWorkletContext } from '../native/FilamentProxy' +import { PropsWithChildren, useCallback, useMemo } from 'react' +import { FilamentProxy, FilamentWorkletRuntime } from '../native/FilamentProxy' import { EngineProps, useEngine } from '../hooks/useEngine' import { useDisposableResource } from '../hooks/useDisposableResource' -import { useWorklet } from 'react-native-worklets-core' import React from 'react' import { Configurator, RendererConfigProps, ViewConfigProps } from './Configurator' import { FilamentContext, FilamentContextType } from '../hooks/useFilamentContext' import { RenderCallbackContext } from './RenderCallbackContext' +import { runOnJS, scheduleOnRuntime } from 'react-native-worklets' +import { Choreographer } from '../types' export type FilamentProviderProps = PropsWithChildren< - Omit & + Omit & ViewConfigProps & RendererConfigProps & { fallback?: React.ReactElement @@ -47,7 +48,7 @@ export type FilamentProviderProps = PropsWithChildren< */ export function FilamentScene({ children, fallback, config, backend, frameRateOptions, ...viewProps }: FilamentProviderProps) { // First create the engine, which we need to create (almost) all other filament APIs - const engine = useEngine({ config, backend, context: FilamentWorkletContext }) + const engine = useEngine({ config, backend, runtime: FilamentWorkletRuntime }) // Create all Filament APIs using the engine const transformManager = useDisposableResource(() => Promise.resolve(engine?.createTransformManager()), [engine]) @@ -61,10 +62,19 @@ export function FilamentScene({ children, fallback, config, backend, frameRateOp // Create a choreographer for this context tree const choreographer = useDisposableResource( - useWorklet(FilamentWorkletContext, () => { - 'worklet' - return FilamentProxy.createChoreographer() - }) + useCallback(() => { + // TODO: DRY this pattern? + let resolve: (engine: Choreographer) => void + const promise = new Promise((res) => { + resolve = res + }) + scheduleOnRuntime(FilamentWorkletRuntime, () => { + 'worklet' + const choreographerImpl = FilamentProxy.createChoreographer() + runOnJS(resolve)(choreographerImpl) + }) + return promise + }, []) ) // Construct the context object value: @@ -94,7 +104,7 @@ export function FilamentScene({ children, fallback, config, backend, frameRateOp camera, renderer, nameComponentManager, - workletContext: FilamentWorkletContext, + workletRuntime: FilamentWorkletRuntime, choreographer: choreographer, } }, [engine, transformManager, renderableManager, scene, lightManager, view, camera, renderer, nameComponentManager, choreographer]) diff --git a/package/src/react/FilamentView.tsx b/package/src/react/FilamentView.tsx index 69e85f3c..61aafe48 100644 --- a/package/src/react/FilamentView.tsx +++ b/package/src/react/FilamentView.tsx @@ -1,15 +1,16 @@ import React from 'react' import { FilamentProxy } from '../native/FilamentProxy' import FilamentNativeView, { type FilamentViewNativeType, type NativeProps } from '../native/specs/FilamentViewNativeComponent' -import { reportWorkletError, wrapWithErrorHandler } from '../ErrorUtils' import { FilamentContext } from '../hooks/useFilamentContext' import { RenderCallback, SwapChain } from 'react-native-filament' import type { SurfaceProvider, FilamentView as RNFFilamentView } from '../native/FilamentViewTypes' import { Listener } from '../types/Listener' import { findNodeHandle, GestureResponderEvent } from 'react-native' -import { Worklets } from 'react-native-worklets-core' import { getLogger } from '../utilities/logger/Logger' import { getTouchHandlers } from './TouchHandlerContext' +import { scheduleOnRuntimeAsync } from '../utilities/scheduleOnRuntimeAsync' +import { makeMutable } from 'react-native-reanimated' +import { reportWorkletError } from '../ErrorUtils' const Logger = getLogger() @@ -40,7 +41,7 @@ export class FilamentView extends React.PureComponent { private view: RNFFilamentView | undefined // There is a race condition where the surface might be destroyed before the swapchain is created. // For this we keep track of the surface state: - private isSurfaceAlive = Worklets.createSharedValue(true) + private isSurfaceAlive = makeMutable(true) private isComponentMounted = false private viewId: number @@ -75,7 +76,7 @@ export class FilamentView extends React.PureComponent { private latestToken = 0 private updateRenderCallback = async (callback: RenderCallback, swapChain: SwapChain) => { const currentToken = ++this.latestToken - const { renderer, view, workletContext, choreographer } = this.getContext() + const { renderer, view, workletRuntime, choreographer } = this.getContext() // When requesting to update the render callback we have to assume that the previous one is not valid anymore // ie. its pointing to already released resources from useDisposableResource: @@ -83,39 +84,30 @@ export class FilamentView extends React.PureComponent { // Adding a new render callback listener is an async operation Logger.debug('Setting render callback') - const listener = await workletContext.runAsync( - wrapWithErrorHandler(() => { + const listener = await scheduleOnRuntimeAsync(workletRuntime, () => { + 'worklet' + + // We need to create the function we pass to addFrameCallbackListener on the worklet thread, so that the + // underlying JSI function is owned by that thread. Only then can we call it on the worklet thread when + // the choreographer is calling its listeners. + return choreographer.addFrameCallbackListener((frameInfo) => { 'worklet' - // We need to create the function we pass to addFrameCallbackListener on the worklet thread, so that the - // underlying JSI function is owned by that thread. Only then can we call it on the worklet thread when - // the choreographer is calling its listeners. - return choreographer.addFrameCallbackListener((frameInfo) => { - 'worklet' - - if (!swapChain.isValid) { - // TODO: Supposedly fixed in https://github.com/margelo/react-native-filament/pull/210, remove this once proven - reportWorkletError( - new Error( - '[react-native-filament] SwapChain is invalid, cannot render frame.\nThis should never happen, please report an issue with reproduction steps.' - ) - ) - return - } - - try { - callback(frameInfo) - - if (renderer.beginFrame(swapChain, frameInfo.timestamp)) { - renderer.render(view) - renderer.endFrame() - } - } catch (error) { - reportWorkletError(error) - } - }) + if (!swapChain.isValid) { + // TODO: Supposedly fixed in https://github.com/margelo/react-native-filament/pull/210, remove this once proven + throw new Error( + '[react-native-filament] SwapChain is invalid, cannot render frame.\nThis should never happen, please report an issue with reproduction steps.' + ) + } + + callback(frameInfo) + + if (renderer.beginFrame(swapChain, frameInfo.timestamp)) { + renderer.render(view) + renderer.endFrame() + } }) - ) + }) // It can happen that after the listener was set the surface got destroyed already: if (!this.isComponentMounted || !this.isSurfaceAlive.value) { @@ -137,6 +129,7 @@ export class FilamentView extends React.PureComponent { // Calling this here ensures that only after the latest successful call for attaching a listener, the choreographer is started. Logger.debug('Starting choreographer') choreographer.start() + Logger.debug('Choreographer started!') } private getContext = () => { @@ -238,11 +231,11 @@ export class FilamentView extends React.PureComponent { Logger.debug('Surface created!') const isSurfaceAlive = this.isSurfaceAlive isSurfaceAlive.value = true - const { engine, workletContext } = this.getContext() + const { engine, workletRuntime } = this.getContext() // Create a swap chain … const enableTransparentRendering = this.props.enableTransparentRendering ?? true Logger.debug('Creating swap chain') - const swapChain = await workletContext.runAsync(() => { + const swapChain = await scheduleOnRuntimeAsync(workletRuntime, () => { 'worklet' if (!isSurfaceAlive.value) { diff --git a/package/src/react/RenderCallbackContext.tsx b/package/src/react/RenderCallbackContext.tsx index df5a4a3c..9fbf0aa6 100644 --- a/package/src/react/RenderCallbackContext.tsx +++ b/package/src/react/RenderCallbackContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, DependencyList, PropsWithChildren, useCallback, useContext, useEffect, useMemo } from 'react' import { RenderCallback } from 'react-native-filament' -import { ISharedValue, useSharedValue } from 'react-native-worklets-core' +import { SharedValue, useSharedValue } from 'react-native-reanimated' type RenderCallbackList = { callback: RenderCallback @@ -12,22 +12,12 @@ type RenderCallbackList = { * This context allows us to have multiple render callbacks, as we call them in the render callback. */ export type RenderContextType = { - renderCallbacks: ISharedValue + renderCallbacks: SharedValue addRenderCallback: (callback: RenderCallback) => () => void } export const makeRenderContext = () => { - const RenderContext = createContext({ - renderCallbacks: { - value: [], - addListener: () => { - throw new Error('RenderContextProvider not found') - }, - }, - addRenderCallback: () => { - throw new Error('RenderContextProvider not found') - }, - }) + const RenderContext = createContext(null!) const RenderContextProvider = ({ children }: PropsWithChildren) => { const renderCallbacks = useSharedValue([]) @@ -35,9 +25,9 @@ export const makeRenderContext = () => { (callback: RenderCallback) => { const id = Math.random().toString(36).substring(7) const entry = { callback, id } - renderCallbacks.value.push(entry) + renderCallbacks.set((prev) => [...prev, entry]) return () => { - renderCallbacks.value = renderCallbacks.value.filter((e) => e.id !== id) + renderCallbacks.set((prev) => prev.filter((e) => e.id !== id)) } }, [renderCallbacks] diff --git a/package/src/types/TransformProps.ts b/package/src/types/TransformProps.ts index 8238dedd..bec8f2ab 100644 --- a/package/src/types/TransformProps.ts +++ b/package/src/types/TransformProps.ts @@ -1,5 +1,5 @@ import { Float3 } from '.' -import { ISharedValue } from 'react-native-worklets-core' +import { type SharedValue } from 'react-native-reanimated' // TODO: WithAnimatedProps ? @@ -11,17 +11,17 @@ export type TransformationProps = { * Position in meters. Unit is in meters. * @default [0, 0, 0] */ - translate?: Float3 | ISharedValue + translate?: Float3 | SharedValue /** * Scale for each axis. Unit is in meters. */ - scale?: Float3 | ISharedValue + scale?: Float3 | SharedValue /** * Rotation for each axis in radians. */ - rotate?: Float3 | ISharedValue + rotate?: Float3 | SharedValue /** * If true, the current transformation of the entity will be multiplied with the new transformation. diff --git a/package/src/utilities/helper.ts b/package/src/utilities/helper.ts index dfefe589..b6088ca0 100644 --- a/package/src/utilities/helper.ts +++ b/package/src/utilities/helper.ts @@ -1,4 +1,4 @@ -import { ISharedValue } from 'react-native-worklets-core' +import { type SharedValue } from 'react-native-reanimated' import { Float3 } from '../types' export const areFloat3Equal = (a: Float3, b?: Float3): boolean => { @@ -6,7 +6,7 @@ export const areFloat3Equal = (a: Float3, b?: Float3): boolean => { return a[0] === b?.[0] && a[1] === b?.[1] && a[2] === b?.[2] } -export const isWorkletSharedValue = (value: any): value is ISharedValue => { +export const isWorkletSharedValue = (value: any): value is SharedValue => { 'worklet' return typeof value === 'object' && value != null && 'addListener' in value && typeof value.addListener === 'function' } diff --git a/package/src/utilities/scheduleOnRuntimeAsync.ts b/package/src/utilities/scheduleOnRuntimeAsync.ts new file mode 100644 index 00000000..f2d5494f --- /dev/null +++ b/package/src/utilities/scheduleOnRuntimeAsync.ts @@ -0,0 +1,14 @@ +import { runOnJS, scheduleOnRuntime, WorkletRuntime } from 'react-native-worklets' + +export function scheduleOnRuntimeAsync(runtime: WorkletRuntime, worklet: () => T): Promise { + let resolve: (result: T) => void + const promise = new Promise((res) => { + resolve = res + }) + scheduleOnRuntime(runtime, () => { + 'worklet' + const result = worklet() + runOnJS(resolve)(result) + }) + return promise +} diff --git a/patches/react-native-worklets-core+1.6.2.patch b/patches/react-native-worklets-core+1.6.2.patch deleted file mode 100644 index 54612060..00000000 --- a/patches/react-native-worklets-core+1.6.2.patch +++ /dev/null @@ -1,25 +0,0 @@ -diff --git a/node_modules/react-native-worklets-core/android/CMakeLists.txt b/node_modules/react-native-worklets-core/android/CMakeLists.txt -index cc4cbfe..66f81c1 100644 ---- a/node_modules/react-native-worklets-core/android/CMakeLists.txt -+++ b/node_modules/react-native-worklets-core/android/CMakeLists.txt -@@ -79,10 +79,17 @@ if(${JS_RUNTIME} STREQUAL "hermes") - - string(APPEND CMAKE_CXX_FLAGS " -DJS_RUNTIME_HERMES=1") - -- target_link_libraries( -- ${PACKAGE_NAME} -- hermes-engine::libhermes -+ if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 82) -+ target_link_libraries( -+ ${PACKAGE_NAME} -+ hermes-engine::hermesvm -+ ) -+ else() -+ target_link_libraries( -+ ${PACKAGE_NAME} -+ hermes-engine::libhermes - ) -+ endif() - - if(${HERMES_ENABLE_DEBUGGER}) - if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 76)