1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
|
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/ambient/ui/ambient_animation_frame_rate_controller.h"
#include "ash/frame_throttler/frame_throttling_controller.h"
#include "base/check.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "cc/paint/skottie_wrapper.h"
#include "components/viz/common/surfaces/frame_sink_id.h"
#include "ui/aura/window.h"
namespace ash {
namespace {
AmbientAnimationFrameRateSchedule BuildSchedule(lottie::Animation* animation) {
DCHECK(animation);
AmbientAnimationFrameRateSchedule schedule =
BuildAmbientAnimationFrameRateSchedule(
animation->skottie()->GetAllMarkers());
if (schedule.empty()) {
// This means the animation file needs to be fixed. This should never happen
// in the field in theory, but just in case, resort to the default frame
// rate schedule (no throttling).
LOG(DFATAL) << "Ambient animation has invalid frame rate markers.";
schedule = BuildDefaultFrameRateSchedule();
}
return schedule;
}
} // namespace
AmbientAnimationFrameRateController::AmbientAnimationFrameRateController(
FrameThrottlingController* frame_throttling_controller)
: frame_throttling_controller_(frame_throttling_controller),
current_section_(schedule_.end()) {
DCHECK(frame_throttling_controller_);
}
AmbientAnimationFrameRateController::~AmbientAnimationFrameRateController() {
ThrottleFrameRate(kDefaultFrameInterval);
}
void AmbientAnimationFrameRateController::AnimationFramePainted(
const lottie::Animation* animation,
float) {
if (animation != tracking_animation_.get())
return;
auto new_current_section = FindCurrentSection();
if (new_current_section == current_section_)
return;
DVLOG(1) << "Found new frame rate section: " << *new_current_section;
current_section_ = new_current_section;
ThrottleFrameRateForCurrentSection();
}
// Note either AnimationIsDeleting() or OnWindowDestroying() could come first.
// Whichever one does should cause both the window and animation to be removed
// from book-keeping entirely.
void AmbientAnimationFrameRateController::AnimationIsDeleting(
const lottie::Animation* deleting_animation) {
aura::Window* window_to_remove = nullptr;
for (const auto& [window, animation] : windows_to_throttle_) {
if (animation == deleting_animation) {
window_to_remove = window;
break;
}
}
DCHECK(window_to_remove);
RemoveWindowToThrottle(window_to_remove);
}
void AmbientAnimationFrameRateController::OnWindowDestroying(
aura::Window* window) {
RemoveWindowToThrottle(window);
}
void AmbientAnimationFrameRateController::AddWindowToThrottle(
aura::Window* window,
lottie::Animation* animation) {
DCHECK(window);
DCHECK(window->GetFrameSinkId().is_valid())
<< "Window missing frame sink id: " << window->GetId();
DCHECK(animation);
if (windows_to_throttle_.contains(window))
return;
if (tracking_animation_) {
DCHECK_EQ(tracking_animation_->skottie()->id(), animation->skottie()->id())
<< "All lottie animations must have the same json content";
}
windows_to_throttle_[window] = animation;
TrySetNewTrackingAnimation();
// Always observe even if the incoming |animation| is not the
// |tracking_animation_| so that we get notified when AnimationIsDeleting().
animation_observations_.AddObservation(animation);
window_observations_.AddObservation(window);
// Update throttling with the expanded list of |windows_to_throttle_|.
ThrottleFrameRateForCurrentSection();
}
AmbientAnimationFrameRateScheduleIterator
AmbientAnimationFrameRateController::FindCurrentSection() const {
DCHECK(tracking_animation_);
std::optional<float> current_progress =
tracking_animation_->GetCurrentProgress();
if (!current_progress) {
DVLOG(1) << "Animation is not playing currently. Cannot map timestamp to "
"scheduled frame rate.";
return schedule_.end();
}
// Always start searching from the last section the animation was on. Since
// animations progress linearly in small increments, most of the time, the
// |current_section_| will not change.
AmbientAnimationFrameRateScheduleIterator new_current_section =
current_section_ == schedule_.end() ? schedule_.begin()
: current_section_;
AmbientAnimationFrameRateScheduleIterator orig_current_section =
new_current_section;
while (!new_current_section->Contains(*current_progress)) {
++new_current_section;
// Note the AmbientAnimationFrameRateSchedule by design is contiguous. Every
// possible timestamp falls within a section of the schedule, so it's
// impossible to infinite loop here.
DCHECK(new_current_section != orig_current_section)
<< "Infinite loop detected. AmbientAnimationFrameRateSchedule has gap "
"and is malformed.";
// The schedule is cyclic. Loop back to the beginning.
if (new_current_section == schedule_.end())
new_current_section = schedule_.begin();
}
return new_current_section;
}
void AmbientAnimationFrameRateController::ThrottleFrameRateForCurrentSection() {
// TODO(esum): There is a corner case not accounted for yet. Say the frame
// interval is large (1 second). And say we throttle to 1 fps at time 10 sec,
// and we need to restore the default 60 fps at 19.1 sec. We will get:
// AnimationFramePainted(10 sec)
// AnimationFramePainted(11 sec)
// ...
// AnimationFramePainted(19 sec) - Still not past the 19.1 second mark
// AnimationFramePainted(20 sec) - Switch back to 60 fps here.
//
// This is bad because we switch back .9 seconds too late, which is a lot.
// To fix this, we could start a timer that fires when the current section is
// over and we need to switch to the new frame rate. It currently is not a
// problem because in practice, the frame rates never get small enough to
// notice this issue. But it will be a problem with the slideshow lottie
// animation.
if (current_section_ != schedule_.end())
ThrottleFrameRate(current_section_->frame_interval);
}
void AmbientAnimationFrameRateController::ThrottleFrameRate(
base::TimeDelta frame_interval) {
std::vector<raw_ptr<aura::Window, VectorExperimental>> windows_as_vector;
for (const auto& [window, animation] : windows_to_throttle_) {
windows_as_vector.push_back(window);
}
if (frame_interval == kDefaultFrameInterval) {
VLOG(1) << "Resetting frame rate to default";
frame_throttling_controller_->EndThrottling();
} else {
DVLOG(1) << "Throttling frame rate to " << frame_interval.ToHz() << "hz";
frame_throttling_controller_->StartThrottling(windows_as_vector,
frame_interval);
}
}
void AmbientAnimationFrameRateController::RemoveWindowToThrottle(
aura::Window* window) {
window_observations_.RemoveObservation(window);
auto iter = windows_to_throttle_.find(window);
DCHECK(iter != windows_to_throttle_.end());
lottie::Animation* unregistered_animation = iter->second;
animation_observations_.RemoveObservation(unregistered_animation);
windows_to_throttle_.erase(iter);
if (unregistered_animation != tracking_animation_)
return;
tracking_animation_ = nullptr;
TrySetNewTrackingAnimation();
if (tracking_animation_) {
DVLOG(1)
<< "Observing new lottie Animation. Resetting frame rate throttling.";
ThrottleFrameRateForCurrentSection();
} else {
DVLOG(1) << "No more lottie animations are active. Restoring default frame "
"rate and going idle...";
ThrottleFrameRate(kDefaultFrameInterval);
}
}
void AmbientAnimationFrameRateController::TrySetNewTrackingAnimation() {
if (tracking_animation_) {
DVLOG(4) << "Tracking animation already set.";
return;
}
if (windows_to_throttle_.empty()) {
DVLOG(4) << "No lottie animations to track. Going idle.";
return;
}
tracking_animation_ = windows_to_throttle_.begin()->second.get();
schedule_ = BuildSchedule(tracking_animation_.get());
// Set |current_section_| to be |schedule_.end()| temporarily so that
// FindCurrentSection() knows to start searching the new schedule from
// scratch.
current_section_ = schedule_.end();
current_section_ = FindCurrentSection();
}
} // namespace ash
|