From 8949c78b4584746cd838911871c8c5c5f306e56d Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Thu, 6 Nov 2025 02:51:20 -0800 Subject: [PATCH 1/3] Start AnimationBackend callbacks once per frame in Animated (#54426) Summary: Withouth the backend, Animated triggers the start function once per frame (by updating an atomic bool). We now do the same with animation backend. Reviewed By: zeyap Differential Revision: D85234259 --- .../animated/NativeAnimatedNodesManager.cpp | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index bf56e2fee4dd..7d2e25522360 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -519,6 +519,17 @@ NativeAnimatedNodesManager::ensureEventEmitterListener() noexcept { } void NativeAnimatedNodesManager::startRenderCallbackIfNeeded(bool isAsync) { + // This method can be called from either the UI thread or JavaScript thread. + // It ensures `startOnRenderCallback_` is called exactly once using atomic + // operations. We use std::atomic_bool rather than std::mutex to avoid + // potential deadlocks that could occur if we called external code while + // holding a mutex. + auto isRenderCallbackStarted = isRenderCallbackStarted_.exchange(true); + if (isRenderCallbackStarted) { + // onRender callback is already started. + return; + } + if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { #ifdef RN_USE_ANIMATION_BACKEND if (auto animationBackend = animationBackend_.lock()) { @@ -531,16 +542,6 @@ void NativeAnimatedNodesManager::startRenderCallbackIfNeeded(bool isAsync) { return; } - // This method can be called from either the UI thread or JavaScript thread. - // It ensures `startOnRenderCallback_` is called exactly once using atomic - // operations. We use std::atomic_bool rather than std::mutex to avoid - // potential deadlocks that could occur if we called external code while - // holding a mutex. - auto isRenderCallbackStarted = isRenderCallbackStarted_.exchange(true); - if (isRenderCallbackStarted) { - // onRender callback is already started. - return; - } if (startOnRenderCallback_) { startOnRenderCallback_([this]() { onRender(); }, isAsync); @@ -549,18 +550,21 @@ void NativeAnimatedNodesManager::startRenderCallbackIfNeeded(bool isAsync) { void NativeAnimatedNodesManager::stopRenderCallbackIfNeeded( bool isAsync) noexcept { - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { - if (auto animationBackend = animationBackend_.lock()) { - animationBackend->stop(isAsync); - } - return; - } // When multiple threads reach this point, only one thread should call // stopOnRenderCallback_. This synchronization is primarily needed during // destruction of NativeAnimatedNodesManager. In normal operation, // stopRenderCallbackIfNeeded is always called from the UI thread. auto isRenderCallbackStarted = isRenderCallbackStarted_.exchange(false); + if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (isRenderCallbackStarted) { + if (auto animationBackend = animationBackend_.lock()) { + animationBackend->stop(isAsync); + } + } + return; + } + if (isRenderCallbackStarted) { if (stopOnRenderCallback_) { stopOnRenderCallback_(isAsync); From 49d78d5fda102d5d592014fe3e846015bc2b82e4 Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Thu, 6 Nov 2025 02:51:20 -0800 Subject: [PATCH 2/3] Make android frame updates work with AnimationBackend (#54425) Summary: Animation frames on Android are tiggered from UIManagerNativeAnimatedDelegate::runAnimationFrame, so we implement that for the backend Reviewed By: zeyap Differential Revision: D84055754 --- .../animated/NativeAnimatedNodesManager.cpp | 1 + .../NativeAnimatedNodesManagerProvider.cpp | 14 ++++++--- .../animationbackend/AnimationBackend.cpp | 31 ++++++++++++++----- .../animationbackend/AnimationBackend.h | 12 +++++++ 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index 7d2e25522360..355e69ac7010 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -992,6 +992,7 @@ AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() { AnimationMutation{tag, nullptr, propsBuilder.get()}); containsChange = true; } + updateViewPropsDirect_.clear(); } if (!containsChange) { diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp index b92bf481d471..ed571e15019b 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp @@ -80,12 +80,16 @@ NativeAnimatedNodesManagerProvider::getOrCreate( std::move(directManipulationCallback), std::move(fabricCommitCallback), uiManager); -#endif nativeAnimatedNodesManager_ = std::make_shared(animationBackend_); + nativeAnimatedDelegate_ = + std::make_shared( + animationBackend_); + uiManager->unstable_setAnimationBackend(animationBackend_); +#endif } else { nativeAnimatedNodesManager_ = std::make_shared( @@ -93,6 +97,10 @@ NativeAnimatedNodesManagerProvider::getOrCreate( std::move(fabricCommitCallback), std::move(startOnRenderCallback_), std::move(stopOnRenderCallback_)); + + nativeAnimatedDelegate_ = + std::make_shared( + nativeAnimatedNodesManager_); } addEventEmitterListener( @@ -117,10 +125,6 @@ NativeAnimatedNodesManagerProvider::getOrCreate( return false; })); - nativeAnimatedDelegate_ = - std::make_shared( - nativeAnimatedNodesManager_); - uiManager->setNativeAnimatedDelegate(nativeAnimatedDelegate_); // TODO: remove force casting. diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index c9a9b062ee32..2aaa449bb05a 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -10,6 +10,18 @@ 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) { @@ -108,15 +120,20 @@ void AnimationBackend::onAnimationFrame(double timestamp) { void AnimationBackend::start(const Callback& callback, bool isAsync) { callbacks.push_back(callback); // TODO: startOnRenderCallback_ should provide the timestamp from the platform - startOnRenderCallback_( - [this]() { - onAnimationFrame( - std::chrono::steady_clock::now().time_since_epoch().count() / 1000); - }, - isAsync); + if (startOnRenderCallback_) { + startOnRenderCallback_( + [this]() { + onAnimationFrame( + std::chrono::steady_clock::now().time_since_epoch().count() / + 1000); + }, + isAsync); + } } void AnimationBackend::stop(bool isAsync) { - stopOnRenderCallback_(isAsync); + if (stopOnRenderCallback_) { + stopOnRenderCallback_(isAsync); + } callbacks.clear(); } diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h index 9236370c3829..544beae7b16a 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h @@ -18,6 +18,18 @@ 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; const ShadowNodeFamily *family; From eb3ca72a23151f62df97e49701b69362fd7e692a Mon Sep 17 00:00:00 2001 From: Bartlomiej Bloniarz Date: Thu, 6 Nov 2025 02:51:20 -0800 Subject: [PATCH 3/3] Split families by surfaceId in Animation Backend (#54424) Summary: AnimationBackend might be handling mulitple surfaces at once, so we need to separate the updates, by `SurfaceId` Reviewed By: zeyap Differential Revision: D84055753 --- .../animationbackend/AnimationBackend.cpp | 68 ++++++++++++------- .../animationbackend/AnimationBackend.h | 4 +- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index 2aaa449bb05a..6b49decc8cf2 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -99,19 +99,23 @@ AnimationBackend::AnimationBackend( void AnimationBackend::onAnimationFrame(double timestamp) { std::unordered_map updates; - std::unordered_set families; + std::unordered_map> + surfaceToFamilies; bool hasAnyLayoutUpdates = false; for (auto& callback : callbacks) { auto muatations = callback(static_cast(timestamp)); for (auto& mutation : muatations) { hasAnyLayoutUpdates |= mutationHasLayoutUpdates(mutation); - families.insert(mutation.family); + const auto family = mutation.family; + if (family != nullptr) { + surfaceToFamilies[family->getSurfaceId()].insert(family); + } updates[mutation.tag] = std::move(mutation.props); } } if (hasAnyLayoutUpdates) { - commitUpdatesWithFamilies(families, updates); + commitUpdates(surfaceToFamilies, updates); } else { synchronouslyUpdateProps(updates); } @@ -137,29 +141,43 @@ void AnimationBackend::stop(bool isAsync) { callbacks.clear(); } -void AnimationBackend::commitUpdatesWithFamilies( - const std::unordered_set& families, +void AnimationBackend::commitUpdates( + const std::unordered_map< + SurfaceId, + std::unordered_set>& surfaceToFamilies, std::unordered_map& updates) { - uiManager_->getShadowTreeRegistry().enumerate( - [families, &updates](const ShadowTree& shadowTree, bool& /*stop*/) { - shadowTree.commit( - [families, &updates](const RootShadowNode& oldRootShadowNode) { - return std::static_pointer_cast( - oldRootShadowNode.cloneMultiple( - families, - [families, &updates]( - const ShadowNode& shadowNode, - const ShadowNodeFragment& fragment) { - auto& animatedProps = updates.at(shadowNode.getTag()); - auto newProps = cloneProps(animatedProps, shadowNode); - return shadowNode.clone( - {newProps, - fragment.children, - shadowNode.getState()}); - })); - }, - {.mountSynchronously = true}); - }); + for (const auto& surfaceEntry : surfaceToFamilies) { + const auto& surfaceId = surfaceEntry.first; + const auto& surfaceFamilies = surfaceEntry.second; + uiManager_->getShadowTreeRegistry().visit( + surfaceId, [&surfaceFamilies, &updates](const ShadowTree& shadowTree) { + shadowTree.commit( + [&surfaceFamilies, + &updates](const RootShadowNode& oldRootShadowNode) { + return std::static_pointer_cast( + oldRootShadowNode.cloneMultiple( + surfaceFamilies, + [&surfaceFamilies, &updates]( + const ShadowNode& shadowNode, + const ShadowNodeFragment& fragment) { + auto newProps = + ShadowNodeFragment::propsPlaceholder(); + if (surfaceFamilies.contains( + &shadowNode.getFamily())) { + auto& animatedProps = + updates.at(shadowNode.getTag()); + newProps = cloneProps(animatedProps, shadowNode); + } + return shadowNode.clone( + {.props = newProps, + .children = fragment.children, + .state = shadowNode.getState(), + .runtimeShadowNodeReference = false}); + })); + }, + {.mountSynchronously = true}); + }); + } } void AnimationBackend::synchronouslyUpdateProps( diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h index 544beae7b16a..ed19f5a52eed 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h @@ -59,8 +59,8 @@ class AnimationBackend : public UIManagerAnimationBackend { DirectManipulationCallback &&directManipulationCallback, FabricCommitCallback &&fabricCommitCallback, UIManager *uiManager); - void commitUpdatesWithFamilies( - const std::unordered_set &families, + void commitUpdates( + const std::unordered_map> &surfaceToFamilies, std::unordered_map &updates); void synchronouslyUpdateProps(const std::unordered_map &updates);