From f14b4887cc522b4df9f4d9514f54afbd57ba95a4 Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Fri, 16 Jan 2026 06:32:38 -0800 Subject: [PATCH 1/4] Move AnimationBackend initialization from Animated, Split of "Move AnimationBackend to its own DisplayLink on iOS" (#55103) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/55103 This diff decouples AnimationBackend from Animated. Now the backend is intialized in the Scheduler, from where it's passed to UIManager. Animation frontends (such as Animated) can then obtain a reference to the backend, and use it to schedule animation frame updates. # Changelog [General] [Changed] - Moved AnimationBackend initiailzation to `Scheduler` [General] [Added] - `AnimationChoreographer` interface with an implementation for fantom tests Differential Revision: D89663251 Reviewed By: zeyap --- .../ReactAndroid/src/main/jni/CMakeLists.txt | 3 + .../ReactCommon/React-Fabric.podspec | 1 + .../animated/NativeAnimatedNodesManager.cpp | 10 +-- .../NativeAnimatedNodesManagerProvider.cpp | 19 ++--- .../NativeAnimatedNodesManagerProvider.h | 1 - .../animationbackend/AnimationBackend.cpp | 81 +++++++++++-------- .../animationbackend/AnimationBackend.h | 28 +++---- .../AnimationBackendCommitHook.cpp | 6 +- .../AnimationBackendCommitHook.h | 2 +- .../animationbackend/AnimationChoreographer.h | 39 +++++++++ .../renderer/animationbackend/CMakeLists.txt | 1 - .../react/renderer/scheduler/CMakeLists.txt | 2 + .../react/renderer/scheduler/Scheduler.cpp | 15 ++++ .../react/renderer/scheduler/Scheduler.h | 2 - .../renderer/scheduler/SchedulerToolbox.h | 7 ++ .../react/renderer/uimanager/UIManager.cpp | 16 ++-- .../react/renderer/uimanager/UIManager.h | 4 +- .../uimanager/UIManagerAnimationBackend.h | 8 +- .../react/runtime/ReactHost.cpp | 10 ++- .../react/runtime/ReactHost.h | 4 +- .../src/TesterAnimationChoreographer.cpp | 27 +++++++ .../tester/src/TesterAnimationChoreographer.h | 26 ++++++ .../tester/src/TesterAppDelegate.cpp | 30 +++++-- .../tester/src/TesterAppDelegate.h | 3 + 24 files changed, 244 insertions(+), 101 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationChoreographer.h create mode 100644 private/react-native-fantom/tester/src/TesterAnimationChoreographer.cpp create mode 100644 private/react-native-fantom/tester/src/TesterAnimationChoreographer.h diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index dc7447d202c85f..979ec533075f28 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -82,6 +82,7 @@ add_react_common_subdir(react/debug) add_react_common_subdir(react/featureflags) add_react_common_subdir(react/performance/cdpmetrics) add_react_common_subdir(react/performance/timeline) +add_react_common_subdir(react/renderer/animationbackend) add_react_common_subdir(react/renderer/animations) add_react_common_subdir(react/renderer/attributedstring) add_react_common_subdir(react/renderer/componentregistry) @@ -200,6 +201,7 @@ add_library(reactnative $ $ $ + $ $ $ $ @@ -293,6 +295,7 @@ target_include_directories(reactnative $ $ $ + $ $ $ $ diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 3e31bdd04e05d8..7b452f6e4cf7fc 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -157,6 +157,7 @@ Pod::Spec.new do |s| ss.source_files = podspec_sources("react/renderer/scheduler/**/*.{m,mm,cpp,h}", "react/renderer/scheduler/**/*.h") ss.header_dir = "react/renderer/scheduler" + ss.dependency "React-Fabric/animationbackend" ss.dependency "React-performancecdpmetrics" ss.dependency "React-performancetimeline" ss.dependency "React-Fabric/observers/events" diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index 762c5eb6b49e9e..26c272e37b869b 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -35,10 +35,6 @@ #include #include -#ifdef RN_USE_ANIMATION_BACKEND -#include -#endif - namespace facebook::react { // Global function pointer for getting current time. Current time @@ -559,10 +555,8 @@ void NativeAnimatedNodesManager::startRenderCallbackIfNeeded(bool isAsync) { if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { #ifdef RN_USE_ANIMATION_BACKEND if (auto animationBackend = animationBackend_.lock()) { - std::static_pointer_cast(animationBackend) - ->start( - [this](float /*f*/) { return pullAnimationMutations(); }, - isAsync); + animationBackend->start( + [this](float /*f*/) { return pullAnimationMutations(); }, isAsync); } #endif diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp index b0d08ce645fe6c..dcfd6eaa33fcb0 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp @@ -88,24 +88,17 @@ NativeAnimatedNodesManagerProvider::getOrCreate( if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { #ifdef RN_USE_ANIMATION_BACKEND - // TODO: this should be initialized outside of animated, but for now it - // was convenient to do it here - animationBackend_ = std::make_shared( - std::move(startOnRenderCallback_), - std::move(stopOnRenderCallback_), - std::move(directManipulationCallback), - std::move(fabricCommitCallback), - uiManager, - jsInvoker); + auto animationBackend = uiManager->unstable_getAnimationBackend().lock(); + react_native_assert( + animationBackend != nullptr && "animationBackend is nullptr"); + animationBackend->registerJSInvoker(jsInvoker); nativeAnimatedNodesManager_ = - std::make_shared(animationBackend_); + std::make_shared(animationBackend); nativeAnimatedDelegate_ = std::make_shared( - animationBackend_); - - uiManager->unstable_setAnimationBackend(animationBackend_); + animationBackend); #endif } else { nativeAnimatedNodesManager_ = diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.h b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.h index de8263df1ecdfe..9b1164d3278fa2 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.h +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.h @@ -32,7 +32,6 @@ class NativeAnimatedNodesManagerProvider { std::shared_ptr getEventEmitterListener(); private: - std::shared_ptr animationBackend_; std::shared_ptr nativeAnimatedNodesManager_; std::shared_ptr eventEmitterListenerContainer_; diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index 0b0d25e9d3739e..b83e3a7a65092d 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -6,11 +6,13 @@ */ #include "AnimationBackend.h" +#include "AnimatedPropsRegistry.h" + #include #include #include #include -#include "AnimatedPropsRegistry.h" +#include namespace facebook::react { @@ -51,20 +53,14 @@ static inline Props::Shared cloneProps( } AnimationBackend::AnimationBackend( - StartOnRenderCallback&& startOnRenderCallback, - StopOnRenderCallback&& stopOnRenderCallback, - DirectManipulationCallback&& directManipulationCallback, - FabricCommitCallback&& fabricCommitCallback, - UIManager* uiManager, - std::shared_ptr jsInvoker) - : startOnRenderCallback_(std::move(startOnRenderCallback)), - stopOnRenderCallback_(std::move(stopOnRenderCallback)), - directManipulationCallback_(std::move(directManipulationCallback)), - fabricCommitCallback_(std::move(fabricCommitCallback)), - animatedPropsRegistry_(std::make_shared()), - uiManager_(uiManager), - jsInvoker_(std::move(jsInvoker)), - commitHook_(uiManager, animatedPropsRegistry_) {} + std::shared_ptr animationChoreographer, + std::shared_ptr uiManager) + : animatedPropsRegistry_(std::make_shared()), + animationChoreographer_(std::move(animationChoreographer)), + commitHook_(*uiManager, animatedPropsRegistry_), + uiManager_(std::move(uiManager)) { + react_native_assert(uiManager_.expired() == false); +} void AnimationBackend::onAnimationFrame(double timestamp) { std::unordered_map surfaceUpdates; @@ -98,23 +94,18 @@ void AnimationBackend::onAnimationFrame(double timestamp) { requestAsyncFlushForSurfaces(asyncFlushSurfaces); } -void AnimationBackend::start(const Callback& callback, bool isAsync) { +void AnimationBackend::start(const Callback& callback, bool /*isAsync*/) { callbacks.push_back(callback); - // TODO: startOnRenderCallback_ should provide the timestamp from the - // platform - if (startOnRenderCallback_) { - startOnRenderCallback_( - [this]() { - onAnimationFrame( - std::chrono::steady_clock::now().time_since_epoch().count() / - 1000); - }, - isAsync); + if (!isRenderCallbackStarted_) { + animationChoreographer_->resume(); + isRenderCallbackStarted_ = true; } } -void AnimationBackend::stop(bool isAsync) { - if (stopOnRenderCallback_) { - stopOnRenderCallback_(isAsync); + +void AnimationBackend::stop(bool /*isAsync*/) { + if (isRenderCallbackStarted_) { + animationChoreographer_->pause(); + isRenderCallbackStarted_ = false; } callbacks.clear(); } @@ -127,9 +118,15 @@ void AnimationBackend::trigger() { void AnimationBackend::commitUpdates( SurfaceId surfaceId, SurfaceUpdates& surfaceUpdates) { + auto uiManager = uiManager_.lock(); + if (!uiManager) { + return; + } + auto& surfaceFamilies = surfaceUpdates.families; auto& updates = surfaceUpdates.propsMap; - uiManager_->getShadowTreeRegistry().visit( + + uiManager->getShadowTreeRegistry().visit( surfaceId, [&surfaceFamilies, &updates](const ShadowTree& shadowTree) { shadowTree.commit( [&surfaceFamilies, @@ -160,19 +157,28 @@ void AnimationBackend::synchronouslyUpdateProps( const std::unordered_map& updates) { for (auto& [tag, animatedProps] : updates) { // TODO: We shouldn't repack it into dynamic, but for that a rewrite - // of directManipulationCallback_ is needed + // of synchronouslyUpdateViewOnUIThread is needed auto dyn = animationbackend::packAnimatedProps(animatedProps); - directManipulationCallback_(tag, std::move(dyn)); + if (auto uiManager = uiManager_.lock()) { + uiManager->synchronouslyUpdateViewOnUIThread(tag, dyn); + } } } void AnimationBackend::requestAsyncFlushForSurfaces( const std::set& surfaces) { + react_native_assert( + jsInvoker_ != nullptr || + surfaces.empty() && "jsInvoker_ was not provided"); for (const auto& surfaceId : surfaces) { // perform an empty commit on the js thread, to force the commit hook to // push updated shadow nodes to react through RSNRU - jsInvoker_->invokeAsync([this, surfaceId]() { - uiManager_->getShadowTreeRegistry().visit( + jsInvoker_->invokeAsync([weakUIManager = uiManager_, surfaceId]() { + auto uiManager = weakUIManager.lock(); + if (!uiManager) { + return; + } + uiManager->getShadowTreeRegistry().visit( surfaceId, [](const ShadowTree& shadowTree) { shadowTree.commit( [](const RootShadowNode& oldRootShadowNode) { @@ -189,4 +195,11 @@ void AnimationBackend::clearRegistry(SurfaceId surfaceId) { animatedPropsRegistry_->clear(surfaceId); } +void AnimationBackend::registerJSInvoker( + std::shared_ptr jsInvoker) { + if (!jsInvoker_) { + jsInvoker_ = jsInvoker; + } +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h index 62030edff94f67..bd64b95f381d90 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h @@ -19,6 +19,7 @@ #include "AnimatedProps.h" #include "AnimatedPropsRegistry.h" #include "AnimationBackendCommitHook.h" +#include "AnimationChoreographer.h" namespace facebook::react { @@ -49,36 +50,29 @@ struct AnimationMutations { class AnimationBackend : public UIManagerAnimationBackend { public: using Callback = std::function; - using StartOnRenderCallback = std::function &&, bool /* isAsync */)>; - using StopOnRenderCallback = std::function; - using DirectManipulationCallback = std::function; - using FabricCommitCallback = std::function &)>; + using ResumeCallback = std::function; + using PauseCallback = std::function; std::vector callbacks; - const StartOnRenderCallback startOnRenderCallback_; - const StopOnRenderCallback stopOnRenderCallback_; - const DirectManipulationCallback directManipulationCallback_; - const FabricCommitCallback fabricCommitCallback_; std::shared_ptr animatedPropsRegistry_; - UIManager *uiManager_; - std::shared_ptr jsInvoker_; + std::shared_ptr animationChoreographer_; AnimationBackendCommitHook commitHook_; + std::weak_ptr uiManager_; + std::shared_ptr jsInvoker_; + bool isRenderCallbackStarted_{false}; AnimationBackend( - StartOnRenderCallback &&startOnRenderCallback, - StopOnRenderCallback &&stopOnRenderCallback, - DirectManipulationCallback &&directManipulationCallback, - FabricCommitCallback &&fabricCommitCallback, - UIManager *uiManager, - std::shared_ptr jsInvoker); + std::shared_ptr animationChoreographer, + std::shared_ptr uiManager); void commitUpdates(SurfaceId surfaceId, SurfaceUpdates &surfaceUpdates); void synchronouslyUpdateProps(const std::unordered_map &updates); void requestAsyncFlushForSurfaces(const std::set &surfaces); void clearRegistry(SurfaceId surfaceId) override; + void registerJSInvoker(std::shared_ptr jsInvoker) override; void onAnimationFrame(double timestamp) override; void trigger() override; - void start(const Callback &callback, bool isAsync); + void start(const Callback &callback, bool isAsync) override; void stop(bool isAsync) override; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp index 2058d3d5caa747..19898d233e610c 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp @@ -7,13 +7,15 @@ #include +#include + namespace facebook::react { AnimationBackendCommitHook::AnimationBackendCommitHook( - UIManager* uiManager, + UIManager& uiManager, std::shared_ptr animatedPropsRegistry) : animatedPropsRegistry_(std::move(animatedPropsRegistry)) { - uiManager->registerCommitHook(*this); + uiManager.registerCommitHook(*this); } RootShadowNode::Unshared AnimationBackendCommitHook::shadowTreeWillCommit( diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.h index 7d6c723d6632c9..83b9bcf7899277 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.h @@ -19,7 +19,7 @@ class AnimationBackendCommitHook : public UIManagerCommitHook { std::shared_ptr animatedPropsRegistry_; public: - AnimationBackendCommitHook(UIManager *uiManager, std::shared_ptr animatedPropsRegistry); + AnimationBackendCommitHook(UIManager &uiManager, std::shared_ptr animatedPropsRegistry); RootShadowNode::Unshared shadowTreeWillCommit( const ShadowTree &shadowTree, const RootShadowNode::Shared &oldRootShadowNode, diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationChoreographer.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationChoreographer.h new file mode 100644 index 00000000000000..43179eba6446a7 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationChoreographer.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +/* + * This class serves as an interface for native animation frame scheduling that can be used as abstraction in + * ReactCxxPlatform. + */ +class AnimationChoreographer { + public: + virtual ~AnimationChoreographer() = default; + + virtual void resume() = 0; + virtual void pause() = 0; + void setAnimationBackend(std::weak_ptr animationBackend) + { + animationBackend_ = animationBackend; + } + void onAnimationFrame(float timestamp) const + { + if (auto animationBackend = animationBackend_.lock()) { + animationBackend->onAnimationFrame(timestamp); + } + } + + private: + std::weak_ptr animationBackend_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/animationbackend/CMakeLists.txt index f44356c3ce7ec4..aea055997c5de1 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/CMakeLists.txt @@ -20,7 +20,6 @@ target_link_libraries(react_renderer_animationbackend react_renderer_graphics react_renderer_mounting react_renderer_uimanager - react_renderer_scheduler glog folly_runtime ) diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt index 27263be747403c..2563bc12c6e06f 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt @@ -21,6 +21,7 @@ target_link_libraries(react_renderer_scheduler react_featureflags react_performance_cdpmetrics react_performance_timeline + react_renderer_animationbackend react_renderer_componentregistry react_renderer_core react_renderer_debug @@ -36,3 +37,4 @@ target_link_libraries(react_renderer_scheduler ) target_compile_reactnative_options(react_renderer_scheduler PRIVATE) target_compile_options(react_renderer_scheduler PRIVATE -Wpedantic) +target_compile_definitions(react_renderer_scheduler PRIVATE RN_USE_ANIMATION_BACKEND) diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 8c42afc52964cd..268cffdc191f10 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -21,6 +21,9 @@ #include #include #include +#ifdef RN_USE_ANIMATION_BACKEND +#include +#endif namespace facebook::react { @@ -55,6 +58,18 @@ Scheduler::Scheduler( auto uiManager = std::make_shared(runtimeExecutor_, contextContainer_); + if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { +#ifdef RN_USE_ANIMATION_BACKEND + auto animationBackend = std::make_shared( + schedulerToolbox.animationChoreographer, uiManager); + + schedulerToolbox.animationChoreographer->setAnimationBackend( + animationBackend); + + uiManager->unstable_setAnimationBackend(animationBackend); +#endif + } + auto eventOwnerBox = std::make_shared(); eventOwnerBox->owner = eventDispatcher_; diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h index c324655da7255a..80be380a48e1a7 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -46,8 +46,6 @@ class Scheduler final : public UIManagerDelegate { /* * Registers and unregisters a `SurfaceHandler` object in the `Scheduler`. - * All registered `SurfaceHandler` objects must be unregistered - * (with the same `Scheduler`) before their deallocation. */ void registerSurface(const SurfaceHandler &surfaceHandler) const noexcept; void unregisterSurface(const SurfaceHandler &surfaceHandler) const noexcept; diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerToolbox.h b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerToolbox.h index f1446cf4c0dc1f..d38e85303edee4 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerToolbox.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerToolbox.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -58,6 +59,12 @@ struct SchedulerToolbox final { * A list of `UIManagerCommitHook`s that should be registered in `UIManager`. */ std::vector> commitHooks; + + /* + * Platform-specific choreographer for scheduling animation frame + * callbacks. Required when useSharedAnimatedBackend() is enabled. + */ + std::shared_ptr animationChoreographer; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 931bfce1c1e10a..e3f3fb5c967239 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -207,9 +207,7 @@ void UIManager::completeSurface( surfaceId, shadowTree.getCurrentRevision().rootShadowNode); if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { - if (auto animationBackend = animationBackend_.lock()) { - animationBackend->clearRegistry(surfaceId); - } + animationBackend_->clearRegistry(surfaceId); } } }); @@ -437,8 +435,8 @@ void UIManager::setNativeProps_DEPRECATED( if (family.nativeProps_DEPRECATED) { // Values in `rawProps` patch (take precedence over) // `nativeProps_DEPRECATED`. For example, if both `nativeProps_DEPRECATED` - // and `rawProps` contain key 'A'. Value from `rawProps` overrides what was - // previously in `nativeProps_DEPRECATED`. + // and `rawProps` contain key 'A'. Value from `rawProps` overrides what + // was previously in `nativeProps_DEPRECATED`. family.nativeProps_DEPRECATED = std::make_unique(mergeDynamicProps( *family.nativeProps_DEPRECATED, @@ -529,9 +527,9 @@ std::shared_ptr UIManager::findShadowNodeByTag_DEPRECATED( // pointer to a root node because of the possible data race. // To work around this, we ask for a commit and immediately cancel it // returning `nullptr` instead of a new shadow tree. - // We don't want to add a way to access a stored pointer to a root node - // because this `findShadowNodeByTag` is deprecated. It is only added - // to make migration to the new architecture easier. + // We don't want to add a way to access a stored pointer to a root + // node because this `findShadowNodeByTag` is deprecated. It is only + // added to make migration to the new architecture easier. shadowTree.tryCommit( [&](const RootShadowNode& oldRootShadowNode) { rootShadowNode = &oldRootShadowNode; @@ -687,7 +685,7 @@ void UIManager::setNativeAnimatedDelegate( } void UIManager::unstable_setAnimationBackend( - std::weak_ptr animationBackend) { + std::shared_ptr animationBackend) { animationBackend_ = animationBackend; } diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index 794a5559a553fe..eac4b5d9a49a1a 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -64,7 +64,7 @@ class UIManager final : public ShadowTreeDelegate { /** * Sets and gets UIManager's AnimationBackend reference. */ - void unstable_setAnimationBackend(std::weak_ptr animationBackend); + void unstable_setAnimationBackend(std::shared_ptr animationBackend); std::weak_ptr unstable_getAnimationBackend(); /** @@ -248,7 +248,7 @@ class UIManager final : public ShadowTreeDelegate { std::unique_ptr lazyShadowTreeRevisionConsistencyManager_; - std::weak_ptr animationBackend_; + std::shared_ptr animationBackend_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h index 75b6232810457c..2cecaa09313b75 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h @@ -7,20 +7,26 @@ #pragma once +#include #include #include namespace facebook::react { +struct AnimationMutations; + class UIManagerAnimationBackend { public: + using Callback = std::function; + virtual ~UIManagerAnimationBackend() = default; virtual void onAnimationFrame(double timestamp) = 0; - // TODO: T240293839 Move over start() function and mutation types + virtual void start(const Callback &callback, bool isAsync) = 0; virtual void stop(bool isAsync) = 0; virtual void clearRegistry(SurfaceId surfaceId) = 0; virtual void trigger() = 0; + virtual void registerJSInvoker(std::shared_ptr jsInvoker) = 0; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp index ca1f38300f3824..7affd88dca0427 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,7 @@ struct ReactInstanceData { std::shared_ptr animatedNodesManagerProvider; ReactInstance::BindingsInstallFunc bindingsInstallFunc; + std::shared_ptr animationChoreographer; }; ReactHost::ReactHost( @@ -66,7 +68,8 @@ ReactHost::ReactHost( std::shared_ptr logBoxSurfaceDelegate, std::shared_ptr animatedNodesManagerProvider, - ReactInstance::BindingsInstallFunc bindingsInstallFunc) + ReactInstance::BindingsInstallFunc bindingsInstallFunc, + std::shared_ptr animationChoreographer) : reactInstanceConfig_(std::move(reactInstanceConfig)) { auto componentRegistryFactory = mountingManager->getComponentRegistryFactory(); @@ -82,7 +85,8 @@ ReactHost::ReactHost( .turboModuleProviders = std::move(turboModuleProviders), .logBoxSurfaceDelegate = logBoxSurfaceDelegate, .animatedNodesManagerProvider = animatedNodesManagerProvider, - .bindingsInstallFunc = std::move(bindingsInstallFunc)}); + .bindingsInstallFunc = std::move(bindingsInstallFunc), + .animationChoreographer = std::move(animationChoreographer)}); if (!reactInstanceData_->contextContainer ->find(MessageQueueThreadFactoryKey) .has_value()) { @@ -223,11 +227,13 @@ void ReactHost::createReactInstance() { return runLoopObserverManager->createEventBeat( ownerBox, *runtimeScheduler); }; + toolbox.animationChoreographer = reactInstanceData_->animationChoreographer; schedulerDelegate_ = std::make_unique( reactInstanceData_->mountingManager); scheduler_ = std::make_unique(toolbox, nullptr, schedulerDelegate_.get()); + surfaceManager_ = std::make_unique(*scheduler_); reactInstanceData_->mountingManager->setSchedulerTaskExecutor( diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.h b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.h index f589d16bb4b361..47645e4a119842 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.h +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -51,7 +52,8 @@ class ReactHost { TurboModuleProviders turboModuleProviders = {}, std::shared_ptr logBoxSurfaceDelegate = nullptr, std::shared_ptr animatedNodesManagerProvider = nullptr, - ReactInstance::BindingsInstallFunc bindingsInstallFunc = nullptr); + ReactInstance::BindingsInstallFunc bindingsInstallFunc = nullptr, + std::shared_ptr animationChoreographer = nullptr); ReactHost(const ReactHost &) = delete; ReactHost &operator=(const ReactHost &) = delete; ReactHost(ReactHost &&) noexcept = delete; diff --git a/private/react-native-fantom/tester/src/TesterAnimationChoreographer.cpp b/private/react-native-fantom/tester/src/TesterAnimationChoreographer.cpp new file mode 100644 index 00000000000000..0c9f9a5d07ab13 --- /dev/null +++ b/private/react-native-fantom/tester/src/TesterAnimationChoreographer.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "TesterAnimationChoreographer.h" +#include +#include + +namespace facebook::react { + +void TesterAnimationChoreographer::resume() { + isPaused_ = false; +} +void TesterAnimationChoreographer::pause() { + isPaused_ = true; +} + +void TesterAnimationChoreographer::runUITick(float timestamp) { + if (!isPaused_) { + onAnimationFrame(timestamp); + } +} + +} // namespace facebook::react diff --git a/private/react-native-fantom/tester/src/TesterAnimationChoreographer.h b/private/react-native-fantom/tester/src/TesterAnimationChoreographer.h new file mode 100644 index 00000000000000..d60c8e8a0b56f3 --- /dev/null +++ b/private/react-native-fantom/tester/src/TesterAnimationChoreographer.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +class TesterAnimationChoreographer : public AnimationChoreographer { + public: + void resume() override; + void pause() override; + void runUITick(float timestamp); + + private: + bool isPaused_{false}; +}; + +} // namespace facebook::react diff --git a/private/react-native-fantom/tester/src/TesterAppDelegate.cpp b/private/react-native-fantom/tester/src/TesterAppDelegate.cpp index 77a6cd17376b36..35afbb35aabd3e 100644 --- a/private/react-native-fantom/tester/src/TesterAppDelegate.cpp +++ b/private/react-native-fantom/tester/src/TesterAppDelegate.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -109,11 +110,19 @@ TesterAppDelegate::TesterAppDelegate( g_setNativeAnimatedNowTimestampFunction(StubClock::now); - auto provider = std::make_shared( - [this](std::function&& onRender, bool /*isAsync*/) { - onAnimationRender_ = std::move(onRender); - }, - [this](bool /*isAsync*/) { onAnimationRender_ = nullptr; }); + std::shared_ptr provider; + + if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + provider = std::make_shared(); + } else { + provider = std::make_shared( + [this](std::function&& onRender, bool /*isAsync*/) { + onAnimationRender_ = std::move(onRender); + }, + [this](bool /*isAsync*/) { onAnimationRender_ = nullptr; }); + } + + animationChoreographer_ = std::make_shared(); reactHost_ = std::make_unique( reactInstanceConfig, @@ -125,7 +134,9 @@ TesterAppDelegate::TesterAppDelegate( nullptr, turboModuleProviders, nullptr, - std::move(provider)); + std::move(provider), + nullptr, + animationChoreographer_); // Ensure that the ReactHost initialisation is completed. // This will call `setupJSNativeFantom`. @@ -253,7 +264,12 @@ void TesterAppDelegate::produceFramesForDuration(double milliseconds) { } void TesterAppDelegate::runUITick() { - if (onAnimationRender_) { + if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + auto microseconds = std::chrono::duration_cast( + StubClock::now().time_since_epoch()) + .count(); + animationChoreographer_->runUITick(static_cast(microseconds) / 1000); + } else if (onAnimationRender_) { onAnimationRender_(); } } diff --git a/private/react-native-fantom/tester/src/TesterAppDelegate.h b/private/react-native-fantom/tester/src/TesterAppDelegate.h index b8dc581348d0cd..d754f958570157 100644 --- a/private/react-native-fantom/tester/src/TesterAppDelegate.h +++ b/private/react-native-fantom/tester/src/TesterAppDelegate.h @@ -13,6 +13,7 @@ #include #include +#include "TesterAnimationChoreographer.h" #include "TesterMountingManager.h" namespace facebook::jsi { @@ -72,6 +73,8 @@ class TesterAppDelegate { std::shared_ptr mountingManager_; + std::shared_ptr animationChoreographer_; + private: void runUITick(); From 53714ac7ab76cdfdd73985c9fb2c9595e3619d56 Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Fri, 16 Jan 2026 06:32:38 -0800 Subject: [PATCH 2/4] Move AnimationBackend to its own Choreographer on Android (#55124) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/55124 On Android AnimationBackend has now its own instance of the Choreographer that it interacts with. # Changelog [General] [Removed] - `UIManagerNativeAnimatedDelegateBackendImpl` [General] [Added] - `AnimationBackendChoreographer`, `AndroidAnimationChoregrapher` Differential Revision: D90327370 --- .../fabric/AnimationBackendChoreographer.kt | 85 +++++++++++++++++++ .../react/fabric/FabricUIManagerBinding.kt | 12 +++ .../fabric/FabricUIManagerProviderImpl.kt | 3 + .../facebook/react/runtime/ReactInstance.kt | 4 + .../fabric/AndroidAnimationChoreographer.h | 38 +++++++++ .../react/fabric/FabricUIManagerBinding.cpp | 23 +++++ .../jni/react/fabric/FabricUIManagerBinding.h | 7 ++ .../fabric/JAnimationBackendChoreographer.cpp | 23 +++++ .../fabric/JAnimationBackendChoreographer.h | 37 ++++++++ .../NativeAnimatedNodesManagerProvider.cpp | 4 - .../animationbackend/AnimationBackend.cpp | 12 --- .../animationbackend/AnimationBackend.h | 10 --- .../react/renderer/uimanager/UIManager.cpp | 6 +- 13 files changed, 236 insertions(+), 28 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/AnimationBackendChoreographer.kt create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/fabric/AndroidAnimationChoreographer.h create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/fabric/JAnimationBackendChoreographer.cpp create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/fabric/JAnimationBackendChoreographer.h diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/AnimationBackendChoreographer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/AnimationBackendChoreographer.kt new file mode 100644 index 00000000000000..1684b77fbcb8a9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/AnimationBackendChoreographer.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.fabric + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.modules.core.ReactChoreographer +import com.facebook.react.uimanager.GuardedFrameCallback +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.synchronized + +internal fun interface AnimationFrameCallback { + fun onAnimationFrame(frameTimeMs: Double) +} + +internal class AnimationBackendChoreographer( + reactApplicationContext: ReactApplicationContext, +) { + + var frameCallback: AnimationFrameCallback? = null + private var lastFrameTimeMs: Double = 0.0 + private val reactChoreographer: ReactChoreographer = ReactChoreographer.getInstance() + private val choreographerCallback: GuardedFrameCallback = + object : GuardedFrameCallback(reactApplicationContext) { + override fun doFrameGuarded(frameTimeNanos: Long) { + executeFrameCallback(frameTimeNanos) + } + } + private val callbackPosted: AtomicBoolean = AtomicBoolean() + private val paused: AtomicBoolean = AtomicBoolean(true) + + /* + * resume() and pause() should be called with the same lock to avoid race conditions. + */ + + fun resume() { + if (paused.getAndSet(false)) { + scheduleCallback() + } + } + + fun pause() { + synchronized(paused) { + if (!paused.getAndSet(true) && callbackPosted.getAndSet(false)) { + reactChoreographer.removeFrameCallback( + ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, + choreographerCallback, + ) + } + } + } + + private fun scheduleCallback() { + synchronized(paused) { + if (!paused.get() && !callbackPosted.getAndSet(true)) { + reactChoreographer.postFrameCallback( + ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, + choreographerCallback, + ) + } + } + } + + private fun executeFrameCallback(frameTimeNanos: Long) { + callbackPosted.set(false) + val currentFrameTimeMs = calculateTimestamp(frameTimeNanos) + // It is possible for ChoreographerCallback to be executed twice within the same frame + // due to frame drops. If this occurs, the additional callback execution should be ignored. + if (currentFrameTimeMs > lastFrameTimeMs) { + frameCallback?.onAnimationFrame(currentFrameTimeMs) + } + + lastFrameTimeMs = currentFrameTimeMs + scheduleCallback() + } + + private fun calculateTimestamp(frameTimeNanos: Long): Double { + val nanosecondsInMilliseconds = 1000000.0 + return frameTimeNanos / nanosecondsInMilliseconds + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt index 55818f21103314..ac8e4e2ef07e07 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt @@ -79,18 +79,30 @@ internal class FabricUIManagerBinding : HybridClassBase() { external fun driveCxxAnimations() + external fun driveAnimationBackend(frameTimeMs: Double) + external fun drainPreallocateViewsQueue() external fun reportMount(surfaceId: Int) + external fun setAnimationBackendChoreographer( + animationBackendChoreographer: AnimationBackendChoreographer + ) + fun register( runtimeExecutor: RuntimeExecutor, runtimeScheduler: RuntimeScheduler, fabricUIManager: FabricUIManager, eventBeatManager: EventBeatManager, componentFactory: ComponentFactory, + animationBackendChoreographer: AnimationBackendChoreographer, ) { fabricUIManager.setBinding(this) + animationBackendChoreographer.frameCallback = AnimationFrameCallback { frameTimeMs: Double -> + driveAnimationBackend(frameTimeMs) + } + setAnimationBackendChoreographer(animationBackendChoreographer) + installFabricUIManager( runtimeExecutor, runtimeScheduler, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerProviderImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerProviderImpl.kt index 8bcd7e658e0ca4..a0a81ff1fa3877 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerProviderImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerProviderImpl.kt @@ -58,6 +58,8 @@ public class FabricUIManagerProviderImpl( val runtimeExecutor = catalystInstance?.runtimeExecutor val runtimeScheduler = catalystInstance?.runtimeScheduler + val animationBackendChoreographer = AnimationBackendChoreographer(context) + if (runtimeExecutor != null && runtimeScheduler != null) { binding.register( runtimeExecutor, @@ -65,6 +67,7 @@ public class FabricUIManagerProviderImpl( fabricUIManager, eventBeatManager, componentFactory, + animationBackendChoreographer, ) } else { throw IllegalStateException( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt index 4999f867050426..6601c86c8b293c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt @@ -43,6 +43,7 @@ import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.devsupport.InspectorFlags.getIsProfilingBuild import com.facebook.react.devsupport.StackTraceHelper import com.facebook.react.devsupport.interfaces.DevSupportManager +import com.facebook.react.fabric.AnimationBackendChoreographer import com.facebook.react.fabric.ComponentFactory import com.facebook.react.fabric.FabricUIManager import com.facebook.react.fabric.FabricUIManagerBinding @@ -250,6 +251,8 @@ internal class ReactInstance( // Misc initialization that needs to be done before Fabric init DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(context) + val animationBackendChoreographer = AnimationBackendChoreographer(context) + val binding = FabricUIManagerBinding() binding.register( getBufferedRuntimeExecutor(), @@ -257,6 +260,7 @@ internal class ReactInstance( fabricUIManager, eventBeatManager, componentFactory, + animationBackendChoreographer, ) // Initialize the FabricUIManager diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AndroidAnimationChoreographer.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AndroidAnimationChoreographer.h new file mode 100644 index 00000000000000..b4cc3a4ced40a9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/AndroidAnimationChoreographer.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include "JAnimationBackendChoreographer.h" + +namespace facebook::react { + +class AndroidAnimationChoreographer : public AnimationChoreographer { + public: + explicit AndroidAnimationChoreographer(jni::alias_ref jChoreographer) + : jChoreographer_(jni::make_global(jChoreographer)) + { + } + + void resume() override + { + jChoreographer_->resume(); + } + + void pause() override + { + jChoreographer_->pause(); + } + + private: + jni::global_ref jChoreographer_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp index 3b2be76a229812..b11e09664d572f 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp @@ -7,6 +7,7 @@ #include "FabricUIManagerBinding.h" +#include "AndroidAnimationChoreographer.h" #include "AndroidEventBeat.h" #include "ComponentFactory.h" #include "EventBeatManager.h" @@ -54,6 +55,10 @@ void FabricUIManagerBinding::driveCxxAnimations() { getScheduler()->animationTick(); } +void FabricUIManagerBinding::driveAnimationBackend(jdouble frameTimeMs) { + animationChoreographer_->onAnimationFrame(static_cast(frameTimeMs)); +} + void FabricUIManagerBinding::drainPreallocateViewsQueue() { auto mountingManager = getMountingManager("drainPreallocateViewsQueue"); if (!mountingManager) { @@ -572,6 +577,11 @@ void FabricUIManagerBinding::installFabricUIManager( toolbox.eventBeatFactory = eventBeatFactory; + react_native_assert( + animationChoreographer_ != nullptr && + "AnimationChoreographer is nullptr"); + toolbox.animationChoreographer = animationChoreographer_; + animationDriver_ = std::make_shared( runtimeExecutor, contextContainer, this); scheduler_ = @@ -797,6 +807,9 @@ void FabricUIManagerBinding::registerNatives() { "setPixelDensity", FabricUIManagerBinding::setPixelDensity), makeNativeMethod( "driveCxxAnimations", FabricUIManagerBinding::driveCxxAnimations), + makeNativeMethod( + "driveAnimationBackend", + FabricUIManagerBinding::driveAnimationBackend), makeNativeMethod( "drainPreallocateViewsQueue", FabricUIManagerBinding::drainPreallocateViewsQueue), @@ -816,7 +829,17 @@ void FabricUIManagerBinding::registerNatives() { makeNativeMethod( "getRelativeAncestorList", FabricUIManagerBinding::getRelativeAncestorList), + makeNativeMethod( + "setAnimationBackendChoreographer", + FabricUIManagerBinding::setAnimationBackendChoreographer), }); } +void FabricUIManagerBinding::setAnimationBackendChoreographer( + jni::alias_ref + animationBackendChoreographer) { + animationChoreographer_ = std::make_shared( + animationBackendChoreographer); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h index 29adfde41cebb6..a84659c54123d8 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h @@ -21,6 +21,7 @@ #include #include +#include "AndroidAnimationChoreographer.h" #include "JFabricUIManager.h" #include "SurfaceHandlerBinding.h" @@ -116,6 +117,8 @@ class FabricUIManagerBinding : public jni::HybridClass, void driveCxxAnimations(); + void driveAnimationBackend(jdouble frameTimeMs); + void drainPreallocateViewsQueue(); void reportMount(SurfaceId surfaceId); @@ -153,6 +156,10 @@ class FabricUIManagerBinding : public jni::HybridClass, float pointScaleFactor_ = 1; bool enableFabricLogs_{false}; + + std::shared_ptr animationChoreographer_; + + void setAnimationBackendChoreographer(jni::alias_ref animationBackend); }; } // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/JAnimationBackendChoreographer.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/JAnimationBackendChoreographer.cpp new file mode 100644 index 00000000000000..1f0272d11ae42f --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/JAnimationBackendChoreographer.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "JAnimationBackendChoreographer.h" + +namespace facebook::react { + +void JAnimationBackendChoreographer::resume() const { + static const auto resumeMethod = + javaClassStatic()->getMethod("resume"); + resumeMethod(self()); +} + +void JAnimationBackendChoreographer::pause() const { + static const auto pauseMethod = javaClassStatic()->getMethod("pause"); + pauseMethod(self()); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/JAnimationBackendChoreographer.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/JAnimationBackendChoreographer.h new file mode 100644 index 00000000000000..064dac173aca5a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/JAnimationBackendChoreographer.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +/** + * JNI wrapper for the AnimationBackendChoreographer Kotlin class. + * This class provides the bridge between C++ and the Android AnimationBackendChoreographer + * which handles animation frame scheduling via ReactChoreographer. + */ +class JAnimationBackendChoreographer : public jni::JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/fabric/AnimationBackendChoreographer;"; + + /** + * Resumes animation frame callbacks. + * This method should be called when animations need to start or resume. + */ + void resume() const; + + /** + * Pauses animation frame callbacks. + * This method should be called when animations should be paused (e.g., when + * the app goes to background). + */ + void pause() const; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp index dcfd6eaa33fcb0..bd451c45d0cbef 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp @@ -95,10 +95,6 @@ NativeAnimatedNodesManagerProvider::getOrCreate( nativeAnimatedNodesManager_ = std::make_shared(animationBackend); - - nativeAnimatedDelegate_ = - std::make_shared( - animationBackend); #endif } else { nativeAnimatedNodesManager_ = diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index b83e3a7a65092d..fe9206337c0b12 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -16,18 +16,6 @@ namespace facebook::react { -UIManagerNativeAnimatedDelegateBackendImpl:: - UIManagerNativeAnimatedDelegateBackendImpl( - std::weak_ptr animationBackend) - : animationBackend_(std::move(animationBackend)) {} - -void UIManagerNativeAnimatedDelegateBackendImpl::runAnimationFrame() { - if (auto animationBackendStrong = animationBackend_.lock()) { - animationBackendStrong->onAnimationFrame( - std::chrono::steady_clock::now().time_since_epoch().count() / 1000); - } -} - static inline Props::Shared cloneProps( AnimatedProps& animatedProps, const ShadowNode& shadowNode) { diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h index bd64b95f381d90..41b377594dc89c 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h @@ -25,16 +25,6 @@ namespace facebook::react { class AnimationBackend; -class UIManagerNativeAnimatedDelegateBackendImpl : public UIManagerNativeAnimatedDelegate { - public: - explicit UIManagerNativeAnimatedDelegateBackendImpl(std::weak_ptr animationBackend); - - void runAnimationFrame() override; - - private: - std::weak_ptr animationBackend_; -}; - struct AnimationMutation { Tag tag; std::shared_ptr family; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index e3f3fb5c967239..c1d12096c2fda5 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -702,8 +702,10 @@ void UIManager::animationTick() const { }); } - if (auto nativeAnimatedDelegate = nativeAnimatedDelegate_.lock()) { - nativeAnimatedDelegate->runAnimationFrame(); + if (!ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (auto nativeAnimatedDelegate = nativeAnimatedDelegate_.lock()) { + nativeAnimatedDelegate->runAnimationFrame(); + } } } From 19be1ca01bf182b510b03156fcb9e8cc95283863 Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Fri, 16 Jan 2026 06:32:38 -0800 Subject: [PATCH 3/4] Move AnimationBackend to its own DisplayLink on iOS (#55123) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/55123 On iOS and macOS AnimationBackend has now its own instance of the DisplayLink that it interacts with (through `RCTAnimationChoreographer`). # Changelog [General] [Added] - `RCTAnimationChoreographer` to `RCTScheduler` Differential Revision: D90327371 --- .../react-native/React/Fabric/RCTScheduler.mm | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/react-native/React/Fabric/RCTScheduler.mm b/packages/react-native/React/Fabric/RCTScheduler.mm index 6cc4e5b6feb730..a53eb997a1eef9 100644 --- a/packages/react-native/React/Fabric/RCTScheduler.mm +++ b/packages/react-native/React/Fabric/RCTScheduler.mm @@ -7,6 +7,7 @@ #import "RCTScheduler.h" +#import #import #import #import @@ -113,6 +114,52 @@ void activityDidChange(const RunLoopObserver::Delegate *delegate, RunLoopObserve void *scheduler_; }; +@interface RCTAnimationChoreographerDisplayLinkTarget : NSObject +@property (nonatomic, assign) AnimationChoreographer *choreographer; +- (void)displayLinkTick:(CADisplayLink *)sender; +@end + +@implementation RCTAnimationChoreographerDisplayLinkTarget +- (void)displayLinkTick:(CADisplayLink *)sender +{ + if (_choreographer != nullptr) { + _choreographer->onAnimationFrame(static_cast(sender.targetTimestamp * 1000)); + } +} +@end + +class RCTAnimationChoreographer : public AnimationChoreographer { + CADisplayLink *_animationDisplayLink; + RCTAnimationChoreographerDisplayLinkTarget *_displayLinkTarget; + + public: + RCTAnimationChoreographer() : _displayLinkTarget([[RCTAnimationChoreographerDisplayLinkTarget alloc] init]) + { + _displayLinkTarget.choreographer = this; + } + ~RCTAnimationChoreographer() override + { + if (_animationDisplayLink != nil) { + [_animationDisplayLink invalidate]; + _animationDisplayLink = nil; + } + _displayLinkTarget.choreographer = nullptr; + } + void resume() override + { + if (_animationDisplayLink == nil) { + _animationDisplayLink = [CADisplayLink displayLinkWithTarget:_displayLinkTarget + selector:@selector(displayLinkTick:)]; + [_animationDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } + [_animationDisplayLink setPaused:NO]; + } + void pause() override + { + [_animationDisplayLink setPaused:YES]; + } +}; + @implementation RCTScheduler { std::unique_ptr _scheduler; std::shared_ptr _animationDriver; @@ -137,6 +184,10 @@ - (instancetype)initWithToolbox:(SchedulerToolbox)toolbox _uiRunLoopObserver->setDelegate(_layoutAnimationDelegateProxy.get()); } + if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + toolbox.animationChoreographer = std::make_shared(); + } + _scheduler = std::make_unique( toolbox, (_animationDriver ? _animationDriver.get() : nullptr), _delegateProxy.get()); } From 520335d4b9de94697bc80677408e5aa24d818599 Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Fri, 16 Jan 2026 08:17:02 -0800 Subject: [PATCH 4/4] Make AnimationBackend methods thread-safe (#55205) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/55205 In `AnimationBackend` the start and stop methods can be called from both the JS and UI thread (Animated does that in some cases). To handle that both of these methods are guarded with a mutex. Also the list of callbacks is also guarded with the mutex. # Changelog [General] [Added] - `std::mutex` to `AnimationBackend` to protect `start`, `stop` and `callbacks`. Differential Revision: D90505528 --- .../animated/NativeAnimatedNodesManager.cpp | 8 +++-- .../animated/NativeAnimatedNodesManager.h | 4 +++ .../animationbackend/AnimationBackend.cpp | 34 +++++++++++++++---- .../animationbackend/AnimationBackend.h | 31 +++++++++++------ .../uimanager/UIManagerAnimationBackend.h | 5 +-- 5 files changed, 59 insertions(+), 23 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index 26c272e37b869b..1de48a496266f4 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -555,8 +555,8 @@ void NativeAnimatedNodesManager::startRenderCallbackIfNeeded(bool isAsync) { if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { #ifdef RN_USE_ANIMATION_BACKEND if (auto animationBackend = animationBackend_.lock()) { - animationBackend->start( - [this](float /*f*/) { return pullAnimationMutations(); }, isAsync); + animationBackendCallbackId_ = animationBackend->start( + [this](float /*f*/) { return pullAnimationMutations(); }); } #endif @@ -577,11 +577,13 @@ void NativeAnimatedNodesManager::stopRenderCallbackIfNeeded( auto isRenderCallbackStarted = isRenderCallbackStarted_.exchange(false); if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { +#ifdef RN_USE_ANIMATION_BACKEND if (isRenderCallbackStarted) { if (auto animationBackend = animationBackend_.lock()) { - animationBackend->stop(isAsync); + animationBackend->stop(animationBackendCallbackId_); } } +#endif return; } diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h index 95267a259b7f05..523774037a03f4 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h @@ -286,6 +286,10 @@ class NativeAnimatedNodesManager { bool warnedAboutGraphTraversal_ = false; #endif +#ifdef RN_USE_ANIMATION_BACKEND + CallbackId animationBackendCallbackId_{0}; +#endif + friend class ColorAnimatedNode; friend class AnimationDriver; friend class AnimationTestsBase; diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index fe9206337c0b12..79fb12dfd6500a 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -51,11 +51,17 @@ AnimationBackend::AnimationBackend( } void AnimationBackend::onAnimationFrame(double timestamp) { + std::vector callbacksCopy; std::unordered_map surfaceUpdates; std::set asyncFlushSurfaces; - for (auto& callback : callbacks) { - auto mutations = callback(static_cast(timestamp)); + { + std::lock_guard lock(mutex_); + callbacksCopy = callbacks; + } + + for (auto& callbackWithId : callbacksCopy) { + auto mutations = callbackWithId.callback(static_cast(timestamp)); asyncFlushSurfaces.merge(mutations.asyncFlushSurfaces); for (auto& mutation : mutations.batch) { const auto family = mutation.family; @@ -82,20 +88,34 @@ void AnimationBackend::onAnimationFrame(double timestamp) { requestAsyncFlushForSurfaces(asyncFlushSurfaces); } -void AnimationBackend::start(const Callback& callback, bool /*isAsync*/) { - callbacks.push_back(callback); +CallbackId AnimationBackend::start(const Callback& callback) { + std::lock_guard lock(mutex_); + + auto callbackId = nextCallbackId_++; + callbacks.push_back({.callbackId = callbackId, .callback = callback}); if (!isRenderCallbackStarted_) { animationChoreographer_->resume(); isRenderCallbackStarted_ = true; } + + return callbackId; } -void AnimationBackend::stop(bool /*isAsync*/) { - if (isRenderCallbackStarted_) { +void AnimationBackend::stop(CallbackId callbackId) { + std::lock_guard lock(mutex_); + + auto it = std::find_if(callbacks.begin(), callbacks.end(), [&](auto& c) { + return c.callbackId == callbackId; + }); + if (it == callbacks.end()) { + return; + } + + callbacks.erase(it); + if (isRenderCallbackStarted_ && callbacks.empty()) { animationChoreographer_->pause(); isRenderCallbackStarted_ = false; } - callbacks.clear(); } void AnimationBackend::trigger() { diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h index 41b377594dc89c..cb80194346a573 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h @@ -37,20 +37,18 @@ struct AnimationMutations { std::set asyncFlushSurfaces; }; +using Callback = std::function; + +struct CallbackWithId { + CallbackId callbackId; + Callback callback; +}; + class AnimationBackend : public UIManagerAnimationBackend { public: - using Callback = std::function; using ResumeCallback = std::function; using PauseCallback = std::function; - std::vector callbacks; - std::shared_ptr animatedPropsRegistry_; - std::shared_ptr animationChoreographer_; - AnimationBackendCommitHook commitHook_; - std::weak_ptr uiManager_; - std::shared_ptr jsInvoker_; - bool isRenderCallbackStarted_{false}; - AnimationBackend( std::shared_ptr animationChoreographer, std::shared_ptr uiManager); @@ -62,7 +60,18 @@ class AnimationBackend : public UIManagerAnimationBackend { void onAnimationFrame(double timestamp) override; void trigger() override; - void start(const Callback &callback, bool isAsync) override; - void stop(bool isAsync) override; + CallbackId start(const Callback &callback) override; + void stop(CallbackId callbackId) override; + + private: + std::vector callbacks; + std::shared_ptr animatedPropsRegistry_; + std::shared_ptr animationChoreographer_; + AnimationBackendCommitHook commitHook_; + std::weak_ptr uiManager_; + std::shared_ptr jsInvoker_; + bool isRenderCallbackStarted_{false}; + CallbackId nextCallbackId_{0}; + std::mutex mutex_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h index 2cecaa09313b75..f7de19c4a80233 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h @@ -14,6 +14,7 @@ namespace facebook::react { struct AnimationMutations; +using CallbackId = uint64_t; class UIManagerAnimationBackend { public: @@ -22,8 +23,8 @@ class UIManagerAnimationBackend { virtual ~UIManagerAnimationBackend() = default; virtual void onAnimationFrame(double timestamp) = 0; - virtual void start(const Callback &callback, bool isAsync) = 0; - virtual void stop(bool isAsync) = 0; + virtual CallbackId start(const Callback &callback) = 0; + virtual void stop(CallbackId callbackId) = 0; virtual void clearRegistry(SurfaceId surfaceId) = 0; virtual void trigger() = 0; virtual void registerJSInvoker(std::shared_ptr jsInvoker) = 0;