Skip to content

Commit 6a44efa

Browse files
Register animation frontends with the animation backend (#59585)
Summary: Stacked on #59584. Review the last two commits: the change and its tests. Producers of animation mutations, such as C++ Animated and Reanimated, use the shared animation backend through anonymous frame callbacks. Settings that differ between producers have nowhere to live. A producer can now register once as an `AnimationFrontend` with its frame callback, then start and stop it by id with `startFrontend` and `stopFrontend`. Each frame runs the callbacks passed to `start(callback)` first, then the started frontends in registration order. `start(callback)`, `stop(callbackId)` and `pushAnimationMutations(callback)` are unchanged. C++ Animated now registers a frontend instead of starting a callback. Its frame callback therefore runs after other producers' `start(callback)` callbacks instead of in the order they were started. The next PR adds the first frontend setting. ## Changelog: [INTERNAL] [ADDED] - Let animation producers register with the animation backend as frontends Pull Request resolved: #59585 Test Plan: - `AnimationBackendFrontendTest`: a started frontend runs once per frame, starting and stopping are idempotent, frames are requested while any frontend or callback is started, a frontend without a frame callback is not started, and frames run in registration order after the callbacks. - `NativeAnimatedFrontendTests.registersAndStartsAFrontend`: C++ Animated registers and starts a frontend with the backend instead of starting a callback. - The other `animationbackend` and `animated` gtests pass. - `yarn fantom packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js` and `yarn fantom packages/react-native/Libraries/Animated/__tests__/Animated-itest.js` pass. Differential Revision: D128154522 Pulled By: bartlomiejbloniarz
1 parent 7b4bc7d commit 6a44efa

12 files changed

Lines changed: 494 additions & 41 deletions

‎packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp‎

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -609,23 +609,9 @@ void NativeAnimatedNodesManager::startRenderCallbackIfNeeded(bool isAsync) {
609609
if (!animationBackend) {
610610
return;
611611
}
612-
// AnimationBackend::start registers the callback before returning the id.
613-
// Keep registration and id publication in one critical section; otherwise a
614-
// concurrent stop can observe no id and leave the registered callback
615-
// orphaned in the backend.
616612
std::lock_guard<std::mutex> lock(animationBackendCallbackMutex_);
617-
if (animationBackendCallbackId_.has_value()) {
618-
return;
619-
}
620-
auto weak = weak_from_this();
621-
auto callbackId = animationBackend->start(
622-
[weak](AnimationTimestamp timestamp) -> AnimationMutations {
623-
if (auto self = weak.lock()) {
624-
return self->pullAnimationMutations(timestamp);
625-
}
626-
return {};
627-
});
628-
animationBackendCallbackId_ = callbackId;
613+
animationBackend->startFrontend(
614+
animationFrontendIdLocked(*animationBackend));
629615
}
630616
}
631617

@@ -651,24 +637,29 @@ void NativeAnimatedNodesManager::stopRenderCallbackIfNeeded(
651637

652638
{
653639
auto animationBackend = animationBackend_.lock();
654-
CallbackId callbackId = 0;
655-
{
656-
std::lock_guard<std::mutex> lock(animationBackendCallbackMutex_);
657-
if (!animationBackendCallbackId_.has_value()) {
658-
return;
659-
}
660-
// Clear the published id while holding the same mutex that protects
661-
// start registration. This makes a concurrent start either see an active
662-
// callback or wait until this stop owns the callback id.
663-
callbackId = *animationBackendCallbackId_;
664-
animationBackendCallbackId_.reset();
665-
}
666-
if (animationBackend) {
667-
animationBackend->stop(callbackId);
640+
std::lock_guard<std::mutex> lock(animationBackendCallbackMutex_);
641+
if (animationBackend && animationFrontendId_.has_value()) {
642+
animationBackend->stopFrontend(*animationFrontendId_);
668643
}
669644
}
670645
}
671646

647+
AnimationFrontendId NativeAnimatedNodesManager::animationFrontendIdLocked(
648+
UIManagerAnimationBackend& animationBackend) {
649+
if (!animationFrontendId_.has_value()) {
650+
animationFrontendId_ = animationBackend.registerFrontend(
651+
{.onAnimationFrame =
652+
[weak = weak_from_this()](
653+
AnimationTimestamp timestamp) -> AnimationMutations {
654+
if (auto self = weak.lock()) {
655+
return self->pullAnimationMutations(timestamp);
656+
}
657+
return {};
658+
}});
659+
}
660+
return *animationFrontendId_;
661+
}
662+
672663
bool NativeAnimatedNodesManager::isAnimationUpdateNeeded() const noexcept {
673664
return !activeAnimations_.empty() || !updatedNodeTags_.empty() ||
674665
isEventAnimationInProgress_;

‎packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ class NativeAnimatedNodesManager : public std::enable_shared_from_this<NativeAni
219219
private:
220220
void stopRenderCallbackIfNeeded(bool isAsync) noexcept;
221221

222+
AnimationFrontendId animationFrontendIdLocked(UIManagerAnimationBackend &animationBackend);
223+
222224
bool onAnimationFrame(double timestamp);
223225

224226
bool isAnimationUpdateNeeded() const noexcept;
@@ -302,10 +304,10 @@ class NativeAnimatedNodesManager : public std::enable_shared_from_this<NativeAni
302304
bool warnedAboutGraphTraversal_ = false;
303305
#endif
304306

305-
// Protects the register/publish and exchange/stop lifecycle for the shared
306-
// AnimationBackend callback.
307+
// Protects the registration of the frontend and its start and stop.
307308
std::mutex animationBackendCallbackMutex_;
308-
std::optional<CallbackId> animationBackendCallbackId_;
309+
// Registered on the first start, and kept until the backend is destroyed.
310+
std::optional<AnimationFrontendId> animationFrontendId_;
309311

310312
friend class ColorAnimatedNode;
311313
friend class AnimationDriver;

‎packages/react-native/ReactCommon/react/renderer/animated/tests/AnimationTestsBase.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ class AnimationTestsBase : public testing::Test {
4040
NativeAnimatedNodesManager::isOnRenderThread_ = true;
4141
}
4242

43+
static void runOnRenderThread() noexcept
44+
{
45+
NativeAnimatedNodesManager::isOnRenderThread_ = true;
46+
}
47+
4348
bool nodeNeedsUpdate(Tag nodeTag) const
4449
{
4550
return nodesManager_->updatedNodeTags_.contains(nodeTag);

‎packages/react-native/ReactCommon/react/renderer/animated/tests/EventAnimationDriverTests.cpp‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ class FakeAnimationBackend : public UIManagerAnimationBackend {
2222
AnimationTimestamp pushTimestamp{0};
2323

2424
void onAnimationFrame(AnimationTimestamp /*timestamp*/) override {}
25+
AnimationFrontendId registerFrontend(
26+
AnimationFrontend /*frontend*/) override {
27+
return 1;
28+
}
29+
void startFrontend(AnimationFrontendId /*frontendId*/) override {}
30+
void stopFrontend(AnimationFrontendId /*frontendId*/) override {}
2531
CallbackId start(const Callback& /*callback*/) override {
2632
return 0;
2733
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "AnimationTestsBase.h"
9+
10+
#include <react/renderer/animated/NativeAnimatedNodesManager.h>
11+
#include <react/renderer/animationbackend/AnimationMutation.h>
12+
#include <react/renderer/components/view/ViewComponentDescriptor.h>
13+
#include <react/renderer/element/ComponentBuilder.h>
14+
#include <react/renderer/element/Element.h>
15+
#include <react/renderer/element/testUtils.h>
16+
#include <react/renderer/uimanager/UIManagerAnimationBackend.h>
17+
#include <react/utils/ContextContainer.h>
18+
19+
namespace facebook::react {
20+
21+
namespace {
22+
23+
class RecordingAnimationBackend : public UIManagerAnimationBackend {
24+
public:
25+
std::vector<AnimationFrontend> frontends;
26+
std::vector<AnimationFrontendId> startedFrontends;
27+
int startedCallbacks{0};
28+
29+
void onAnimationFrame(AnimationTimestamp /*timestamp*/) override {}
30+
AnimationFrontendId registerFrontend(AnimationFrontend frontend) override {
31+
frontends.push_back(std::move(frontend));
32+
return static_cast<AnimationFrontendId>(frontends.size());
33+
}
34+
void startFrontend(AnimationFrontendId frontendId) override {
35+
startedFrontends.push_back(frontendId);
36+
}
37+
void stopFrontend(AnimationFrontendId /*frontendId*/) override {}
38+
CallbackId start(const Callback& /*callback*/) override {
39+
startedCallbacks++;
40+
return 0;
41+
}
42+
void stop(CallbackId /*callbackId*/) override {}
43+
void clearRegistry(SurfaceId /*surfaceId*/) override {}
44+
void clearRegistryOnSurfaceStop(SurfaceId /*surfaceId*/) override {}
45+
void trigger() override {}
46+
void pushAnimationMutations(const Callback& /*callback*/) override {}
47+
void registerJSInvoker(std::shared_ptr<CallInvoker> /*jsInvoker*/) override {}
48+
};
49+
50+
constexpr Tag kValueNode = 1;
51+
constexpr Tag kStyleNode = 2;
52+
constexpr Tag kPropsNode = 3;
53+
constexpr Tag kView = 10;
54+
55+
} // namespace
56+
57+
class NativeAnimatedFrontendTests : public AnimationTestsBase {
58+
protected:
59+
void SetUp() override {
60+
runOnRenderThread();
61+
}
62+
63+
// Starts animating the view's opacity in four frames.
64+
void startAnimation() {
65+
manager_ = std::make_shared<NativeAnimatedNodesManager>(backend_);
66+
manager_->createAnimatedNode(
67+
kValueNode,
68+
folly::dynamic::object("type", "value")("value", 0)("offset", 0));
69+
manager_->createAnimatedNode(
70+
kStyleNode,
71+
folly::dynamic::object("type", "style")(
72+
"style", folly::dynamic::object("opacity", kValueNode)));
73+
manager_->createAnimatedNode(
74+
kPropsNode,
75+
folly::dynamic::object("type", "props")(
76+
"props", folly::dynamic::object("style", kStyleNode)));
77+
manager_->connectAnimatedNodes(kValueNode, kStyleNode);
78+
manager_->connectAnimatedNodes(kStyleNode, kPropsNode);
79+
manager_->connectAnimatedNodeToView(kPropsNode, kView);
80+
manager_->connectAnimatedNodeToShadowNodeFamily(
81+
kPropsNode,
82+
std::const_pointer_cast<ShadowNodeFamily>(view_->getFamilyShared()));
83+
manager_->startAnimatingNode(
84+
1,
85+
kValueNode,
86+
folly::dynamic::object("type", "frames")(
87+
"frames", folly::dynamic::array(0.0, 0.5, 1.0))("toValue", 1),
88+
std::nullopt);
89+
manager_->startRenderCallbackIfNeeded(false);
90+
}
91+
92+
std::shared_ptr<RecordingAnimationBackend> backend_ =
93+
std::make_shared<RecordingAnimationBackend>();
94+
std::shared_ptr<NativeAnimatedNodesManager> manager_;
95+
ComponentBuilder builder_ =
96+
simpleComponentBuilder(std::make_shared<ContextContainer>());
97+
std::shared_ptr<ViewShadowNode> view_ =
98+
builder_.build(Element<ViewShadowNode>().surfaceId(1).tag(kView));
99+
};
100+
101+
TEST_F(NativeAnimatedFrontendTests, registersAndStartsAFrontend) {
102+
startAnimation();
103+
104+
ASSERT_EQ(backend_->frontends.size(), 1u);
105+
EXPECT_EQ(backend_->startedFrontends, std::vector<AnimationFrontendId>{1});
106+
EXPECT_EQ(backend_->startedCallbacks, 0);
107+
}
108+
109+
} // namespace facebook::react

‎packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp‎

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,34 +163,59 @@ void AnimationBackend::applyMutations(std::vector<AnimationMutations> batches) {
163163

164164
void AnimationBackend::onAnimationFrame(AnimationTimestamp timestamp) {
165165
std::vector<CallbackWithId> callbacksCopy;
166+
std::vector<AnimationFrontend> startedFrontends;
166167

167168
{
168169
std::lock_guard lock(mutex_);
169170
callbacksCopy = callbacks;
171+
for (const auto& registered : frontends_) {
172+
if (registered.started) {
173+
startedFrontends.push_back(registered.frontend);
174+
}
175+
}
170176
}
171177

172178
// Sized up front rather than grown: MSVC's std::set move isn't noexcept, so
173179
// growing a vector of AnimationMutations would try to copy move-only props.
174-
std::vector<AnimationMutations> batches(callbacksCopy.size());
180+
std::vector<AnimationMutations> batches(
181+
callbacksCopy.size() + startedFrontends.size());
175182
std::transform(
176183
callbacksCopy.begin(),
177184
callbacksCopy.end(),
178185
batches.begin(),
179186
[timestamp](const CallbackWithId& callbackWithId) {
180187
return callbackWithId.callback(timestamp);
181188
});
189+
for (size_t i = 0; i < startedFrontends.size(); i++) {
190+
batches[callbacksCopy.size() + i] =
191+
startedFrontends[i].onAnimationFrame(timestamp);
192+
}
182193
applyMutations(std::move(batches));
183194
}
184195

196+
void AnimationBackend::resumeFramesLocked() {
197+
if (!isRenderCallbackStarted_) {
198+
animationChoreographer_->resume();
199+
isRenderCallbackStarted_ = true;
200+
}
201+
}
202+
203+
void AnimationBackend::pauseFramesIfIdleLocked() {
204+
if (isRenderCallbackStarted_ && callbacks.empty() &&
205+
std::none_of(frontends_.begin(), frontends_.end(), [](auto& frontend) {
206+
return frontend.started;
207+
})) {
208+
animationChoreographer_->pause();
209+
isRenderCallbackStarted_ = false;
210+
}
211+
}
212+
185213
CallbackId AnimationBackend::start(const Callback& callback) {
186214
std::lock_guard lock(mutex_);
187215

188216
auto callbackId = nextCallbackId_++;
189217
callbacks.push_back({.callbackId = callbackId, .callback = callback});
190-
if (!isRenderCallbackStarted_) {
191-
animationChoreographer_->resume();
192-
isRenderCallbackStarted_ = true;
193-
}
218+
resumeFramesLocked();
194219

195220
return callbackId;
196221
}
@@ -206,10 +231,34 @@ void AnimationBackend::stop(CallbackId callbackId) {
206231
}
207232

208233
callbacks.erase(it);
209-
if (isRenderCallbackStarted_ && callbacks.empty()) {
210-
animationChoreographer_->pause();
211-
isRenderCallbackStarted_ = false;
234+
pauseFramesIfIdleLocked();
235+
}
236+
237+
AnimationFrontendId AnimationBackend::registerFrontend(
238+
AnimationFrontend frontend) {
239+
std::lock_guard lock(mutex_);
240+
frontends_.push_back({.frontend = std::move(frontend)});
241+
return static_cast<AnimationFrontendId>(frontends_.size());
242+
}
243+
244+
void AnimationBackend::startFrontend(AnimationFrontendId frontendId) {
245+
std::lock_guard lock(mutex_);
246+
auto& registered = frontends_.at(frontendId - 1);
247+
if (registered.started || !registered.frontend.onAnimationFrame) {
248+
return;
249+
}
250+
registered.started = true;
251+
resumeFramesLocked();
252+
}
253+
254+
void AnimationBackend::stopFrontend(AnimationFrontendId frontendId) {
255+
std::lock_guard lock(mutex_);
256+
auto& registered = frontends_.at(frontendId - 1);
257+
if (!registered.started) {
258+
return;
212259
}
260+
registered.started = false;
261+
pauseFramesIfIdleLocked();
213262
}
214263

215264
void AnimationBackend::trigger() {

‎packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ struct CallbackWithId {
3636
Callback callback;
3737
};
3838

39+
struct RegisteredFrontend {
40+
AnimationFrontend frontend;
41+
bool started{false};
42+
};
43+
3944
class AnimationBackend : public UIManagerAnimationBackend {
4045
public:
4146
using ResumeCallback = std::function<void()>;
@@ -56,6 +61,9 @@ class AnimationBackend : public UIManagerAnimationBackend {
5661
void pushAnimationMutations(const Callback &callback) override;
5762
CallbackId start(const Callback &callback) override;
5863
void stop(CallbackId callbackId) override;
64+
AnimationFrontendId registerFrontend(AnimationFrontend frontend) override;
65+
void startFrontend(AnimationFrontendId frontendId) override;
66+
void stopFrontend(AnimationFrontendId frontendId) override;
5967

6068
private:
6169
using SurfaceUpdates = std::unordered_map<Tag, AnimationMutation>;
@@ -69,7 +77,11 @@ class AnimationBackend : public UIManagerAnimationBackend {
6977
std::unordered_map<SurfaceId, SurfaceUpdates> &surfaceUpdates,
7078
const std::set<SurfaceId> &asyncFlushSurfaces);
7179
void applyMutations(std::vector<AnimationMutations> batches);
80+
void resumeFramesLocked();
81+
void pauseFramesIfIdleLocked();
7282
std::vector<CallbackWithId> callbacks;
83+
// Indexed by frontend id - 1.
84+
std::vector<RegisteredFrontend> frontends_;
7385
std::shared_ptr<AnimatedPropsRegistry> animatedPropsRegistry_;
7486
std::shared_ptr<AnimationChoreographer> animationChoreographer_;
7587
std::unique_ptr<AnimationBackendCommitHook> commitHook_;

0 commit comments

Comments
 (0)