#include "cc/trees/image_animation_controller.h"
#include <algorithm>
#include <sstream>
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/task/single_thread_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "cc/base/features.h"
#include "cc/paint/image_animation_count.h"
namespace cc {
namespace {
const base::TimeDelta kAnimationResyncCutoff = base::Minutes(5);
base::TimeTicks SnappedTickTimeFromFrameTime(
const viz::BeginFrameArgs& args,
base::TimeTicks desired_frame_time) {
auto snapped_tick_time =
desired_frame_time.SnappedToNextTick(args.frame_time, args.interval);
DCHECK_GE(snapped_tick_time, desired_frame_time);
if (snapped_tick_time != desired_frame_time)
snapped_tick_time -= args.interval;
return snapped_tick_time;
}
}
ImageAnimationController::ImageAnimationController(
base::SingleThreadTaskRunner* task_runner,
Client* client,
bool enable_image_animation_resync)
: scheduler_(task_runner, client),
enable_image_animation_resync_(enable_image_animation_resync) {}
ImageAnimationController::~ImageAnimationController() = default;
void ImageAnimationController::UpdateAnimatedImage(
const DiscardableImageMap::AnimatedImageMetadata& data) {
AnimationState& animation_state = animation_state_map_[data.paint_image_id];
animation_state.UpdateMetadata(data);
}
void ImageAnimationController::RegisterAnimationDriver(
PaintImage::Id paint_image_id,
AnimationDriver* driver) {
auto it = animation_state_map_.find(paint_image_id);
CHECK(it != animation_state_map_.end());
it->second.AddDriver(driver);
registered_animations_.insert(paint_image_id);
}
void ImageAnimationController::UnregisterAnimationDriver(
PaintImage::Id paint_image_id,
AnimationDriver* driver) {
auto it = animation_state_map_.find(paint_image_id);
CHECK(it != animation_state_map_.end());
it->second.RemoveDriver(driver);
if (!it->second.has_drivers())
registered_animations_.erase(paint_image_id);
}
bool ImageAnimationController::IsRegistered(PaintImage::Id paint_image_id) {
return animation_state_map_.contains(paint_image_id);
}
const PaintImageIdFlatSet& ImageAnimationController::AnimateForSyncTree(
const viz::BeginFrameArgs& args) {
TRACE_EVENT1("cc", "ImageAnimationController::AnimateImagesForSyncTree",
"frame_time_from_now",
(base::TimeTicks::Now() - args.frame_time).InMillisecondsF());
DCHECK(images_animated_on_sync_tree_.empty());
scheduler_.WillAnimate();
std::optional<base::TimeTicks> next_invalidation_time;
for (auto id : registered_animations_) {
auto it = animation_state_map_.find(id);
CHECK(it != animation_state_map_.end());
AnimationState& state = it->second;
state.UpdateStateFromDrivers();
if (!state.ShouldAnimate()) {
TRACE_EVENT_INSTANT0(TRACE_DISABLED_BY_DEFAULT("cc.debug"),
"ShouldAnimate - early out",
TRACE_EVENT_SCOPE_THREAD);
continue;
}
if (state.AdvanceFrame(args, enable_image_animation_resync_)) {
images_animated_on_sync_tree_.insert(id);
}
TRACE_EVENT_INSTANT1(TRACE_DISABLED_BY_DEFAULT("cc.debug"),
"AnimationState", TRACE_EVENT_SCOPE_THREAD, "state",
state.ToString());
if (!state.ShouldAnimate())
continue;
DCHECK_GT(state.next_desired_tick_time(), args.frame_time);
if (!next_invalidation_time.has_value()) {
next_invalidation_time.emplace(state.next_desired_tick_time());
} else {
next_invalidation_time = std::min(state.next_desired_tick_time(),
next_invalidation_time.value());
}
}
if (next_invalidation_time.has_value())
scheduler_.Schedule(next_invalidation_time.value());
else
scheduler_.Cancel();
return images_animated_on_sync_tree_;
}
void ImageAnimationController::UpdateStateFromDrivers() {
TRACE_EVENT0("cc", "UpdateStateFromAnimationDrivers");
std::optional<base::TimeTicks> next_invalidation_time;
for (auto image_id : registered_animations_) {
auto it = animation_state_map_.find(image_id);
CHECK(it != animation_state_map_.end());
AnimationState& state = it->second;
state.UpdateStateFromDrivers();
if (!state.ShouldAnimate())
continue;
if (!next_invalidation_time.has_value()) {
next_invalidation_time.emplace(state.next_desired_tick_time());
} else {
next_invalidation_time = std::min(next_invalidation_time.value(),
state.next_desired_tick_time());
}
}
if (next_invalidation_time.has_value())
scheduler_.Schedule(next_invalidation_time.value());
else
scheduler_.Cancel();
}
void ImageAnimationController::DidActivate() {
TRACE_EVENT0("cc", "ImageAnimationController::WillActivate");
for (auto id : images_animated_on_sync_tree_) {
auto it = animation_state_map_.find(id);
CHECK(it != animation_state_map_.end());
it->second.PushPendingToActive();
}
images_animated_on_sync_tree_.clear();
if (did_navigate_) {
for (auto it = animation_state_map_.begin();
it != animation_state_map_.end();) {
if (it->second.has_drivers())
it++;
else
it = animation_state_map_.erase(it);
}
did_navigate_ = false;
}
}
size_t ImageAnimationController::GetFrameIndexForImage(
PaintImage::Id paint_image_id,
WhichTree tree) const {
const auto& it = animation_state_map_.find(paint_image_id);
CHECK(it != animation_state_map_.end());
return tree == WhichTree::PENDING_TREE ? it->second.pending_index()
: it->second.active_index();
}
void ImageAnimationController::WillBeginImplFrame(
const viz::BeginFrameArgs& args) {
scheduler_.WillBeginImplFrame(args);
}
const base::flat_set<
raw_ptr<ImageAnimationController::AnimationDriver, CtnExperimental>>&
ImageAnimationController::GetDriversForTesting(
PaintImage::Id paint_image_id) const {
const auto& it = animation_state_map_.find(paint_image_id);
CHECK(it != animation_state_map_.end());
return it->second.drivers_for_testing();
}
size_t ImageAnimationController::GetLastNumOfFramesSkippedForTesting(
PaintImage::Id paint_image_id) const {
const auto& it = animation_state_map_.find(paint_image_id);
CHECK(it != animation_state_map_.end());
return it->second.last_num_frames_skipped_for_testing();
}
std::optional<ImageAnimationController::ConsistentFrameDuration>
ImageAnimationController::GetConsistentContentFrameDuration() {
if (animation_state_map_.empty()) {
return std::nullopt;
}
std::optional<base::TimeDelta> frame_duration;
uint32_t num_images = 0u;
for (auto& [id, state] : animation_state_map_) {
if (!state.ShouldAnimate()) {
continue;
}
std::optional<base::TimeDelta> image_frame_duration =
state.GetConsistentContentFrameDuration();
if (!image_frame_duration) {
return std::nullopt;
}
if (frame_duration &&
frame_duration.value() != image_frame_duration.value()) {
return std::nullopt;
}
frame_duration = image_frame_duration.value();
num_images++;
}
if (!frame_duration) {
return std::nullopt;
}
return ConsistentFrameDuration{frame_duration.value(), num_images};
}
ImageAnimationController::AnimationState::AnimationState() = default;
ImageAnimationController::AnimationState::AnimationState(
AnimationState&& other) = default;
ImageAnimationController::AnimationState&
ImageAnimationController::AnimationState::operator=(AnimationState&& other) =
default;
ImageAnimationController::AnimationState::~AnimationState() {
DCHECK(drivers_.empty());
}
bool ImageAnimationController::AnimationState::ShouldAnimate() const {
return ShouldAnimate(current_state_.repetitions_completed,
current_state_.pending_index);
}
bool ImageAnimationController::AnimationState::ShouldAnimate(
int repetitions_completed,
size_t pending_index) const {
DCHECK(current_state_.repetitions_completed == 0 || is_complete());
if (!should_animate_from_drivers_)
return false;
switch (requested_repetitions_) {
case kAnimationLoopOnce:
if (repetitions_completed >= 1)
return false;
break;
case kAnimationNone:
NOTREACHED() << "We shouldn't be tracking kAnimationNone images";
case kAnimationLoopInfinite:
break;
default:
if (requested_repetitions_ <= repetitions_completed)
return false;
}
if (!frames_[NextFrameIndex(pending_index)].complete)
return false;
size_t last_frame_index = frames_.size() - 1;
if (completion_state_ != PaintImage::CompletionState::kDone &&
pending_index == last_frame_index) {
return false;
}
return true;
}
bool ImageAnimationController::AnimationState::AdvanceFrame(
const viz::BeginFrameArgs& args,
bool enable_image_animation_resync) {
DCHECK(ShouldAnimate(current_state_.repetitions_completed,
current_state_.pending_index));
const base::TimeTicks next_tick_time = args.frame_time + args.interval;
if (!animation_started_) {
DCHECK_EQ(current_state_.pending_index, 0u);
animation_started_time_ = args.frame_time;
current_state_.next_desired_frame_time =
args.frame_time + frames_[0].duration;
current_state_.next_desired_tick_time = SnappedTickTimeFromFrameTime(
args, current_state_.next_desired_frame_time);
animation_started_ = true;
}
if (args.frame_time < current_state_.next_desired_tick_time)
return needs_invalidation();
if (enable_image_animation_resync &&
args.frame_time - current_state_.next_desired_frame_time >
kAnimationResyncCutoff) {
TRACE_EVENT_INSTANT0("cc", "Resync - early out", TRACE_EVENT_SCOPE_THREAD);
DCHECK_EQ(current_state_.pending_index, active_index_);
current_state_.next_desired_frame_time =
args.frame_time + frames_[current_state_.pending_index].duration;
current_state_.next_desired_tick_time =
std::max(SnappedTickTimeFromFrameTime(
args, current_state_.next_desired_frame_time),
next_tick_time);
return needs_invalidation();
}
current_state_.num_of_frames_advanced = 0u;
current_state_ = AdvanceAnimationState(current_state_, args,
current_state_.next_desired_tick_time,
enable_image_animation_resync);
DCHECK_GE(current_state_.num_of_frames_advanced, 1u);
last_num_frames_skipped_ = current_state_.num_of_frames_advanced - 1u;
return needs_invalidation();
}
ImageAnimationController::AnimationState::AnimationAdvancementState
ImageAnimationController::AnimationState::AdvanceAnimationState(
AnimationAdvancementState animation_advancement_state,
const viz::BeginFrameArgs& args,
base::TimeTicks start,
bool enable_image_animation_resync) const {
const base::TimeTicks end = args.frame_time + args.interval;
base::TimeTicks elapsed_time = start;
while (elapsed_time < end &&
ShouldAnimate(animation_advancement_state.repetitions_completed,
animation_advancement_state.pending_index)) {
animation_advancement_state.num_of_frames_advanced++;
size_t next_frame_index =
NextFrameIndex(animation_advancement_state.pending_index);
elapsed_time += frames_[next_frame_index].duration;
TRACE_EVENT_INSTANT2(TRACE_DISABLED_BY_DEFAULT("cc.debug"),
"FrameDurationIndex", TRACE_EVENT_SCOPE_THREAD,
"frame_index", next_frame_index, "duration",
frames_[next_frame_index].duration.InMillisecondsF());
base::TimeTicks next_desired_frame_time =
animation_advancement_state.next_desired_frame_time +
frames_[next_frame_index].duration;
if (enable_image_animation_resync && next_frame_index == 0u &&
animation_advancement_state.repetitions_completed == 1 &&
next_desired_frame_time <= args.frame_time) {
animation_advancement_state.pending_index = 0u;
animation_advancement_state.next_desired_frame_time =
args.frame_time + frames_[0].duration;
animation_advancement_state.next_desired_tick_time = std::max(
SnappedTickTimeFromFrameTime(
args, animation_advancement_state.next_desired_frame_time),
end);
animation_advancement_state.repetitions_completed = 0;
break;
}
animation_advancement_state.pending_index = next_frame_index;
animation_advancement_state.next_desired_frame_time =
next_desired_frame_time;
animation_advancement_state.next_desired_tick_time =
SnappedTickTimeFromFrameTime(
args, animation_advancement_state.next_desired_frame_time);
if (animation_advancement_state.pending_index == frames_.size() - 1 &&
is_complete())
animation_advancement_state.repetitions_completed++;
}
DCHECK_GT(animation_advancement_state.num_of_frames_advanced, 0u);
return animation_advancement_state;
}
void ImageAnimationController::AnimationState::UpdateMetadata(
const DiscardableImageMap::AnimatedImageMetadata& data) {
paint_image_id_ = data.paint_image_id;
DCHECK_NE(data.repetition_count, kAnimationNone);
requested_repetitions_ = data.repetition_count;
DCHECK(frames_.size() <= data.frames.size())
<< "Updated recordings can only append frames";
frames_ = data.frames;
cached_consistent_frame_duration_valid_ = false;
DCHECK_GT(frames_.size(), 1u);
DCHECK(completion_state_ != PaintImage::CompletionState::kDone ||
data.completion_state == PaintImage::CompletionState::kDone)
<< "If the image was marked complete before, it can not be incomplete in "
"a new update";
completion_state_ = data.completion_state;
size_t last_frame_index = frames_.size() - 1;
if (current_state_.pending_index == last_frame_index && is_complete() &&
current_state_.repetitions_completed == 0)
current_state_.repetitions_completed++;
if (reset_animation_sequence_id_ < data.reset_animation_sequence_id) {
reset_animation_sequence_id_ = data.reset_animation_sequence_id;
ResetAnimation();
}
}
void ImageAnimationController::AnimationState::PushPendingToActive() {
active_index_ = current_state_.pending_index;
}
std::optional<base::TimeDelta>
ImageAnimationController::AnimationState::GetConsistentContentFrameDuration() {
if (!cached_consistent_frame_duration_valid_) {
ComputeConsistentContentFrameDuration();
}
cached_consistent_frame_duration_valid_ = true;
if (!cached_has_consistent_frame_duration_) {
return std::nullopt;
}
return cached_consistent_frame_duration_;
}
void ImageAnimationController::AnimationState::
ComputeConsistentContentFrameDuration() {
cached_has_consistent_frame_duration_ = false;
std::optional<base::TimeDelta> frame_duration;
for (const auto& metadata : frames_) {
if (frame_duration && frame_duration.value() != metadata.duration) {
return;
}
frame_duration = metadata.duration;
}
if (frame_duration) {
cached_has_consistent_frame_duration_ = true;
cached_consistent_frame_duration_ = frame_duration.value();
}
}
void ImageAnimationController::AnimationState::AddDriver(
AnimationDriver* driver) {
drivers_.insert(driver);
}
void ImageAnimationController::AnimationState::RemoveDriver(
AnimationDriver* driver) {
drivers_.erase(driver);
}
void ImageAnimationController::AnimationState::UpdateStateFromDrivers() {
should_animate_from_drivers_ = false;
for (AnimationDriver* driver : drivers_) {
if (driver->ShouldAnimate(paint_image_id_)) {
should_animate_from_drivers_ = true;
break;
}
}
}
void ImageAnimationController::AnimationState::ResetAnimation() {
animation_started_ = false;
current_state_.next_desired_frame_time = base::TimeTicks();
current_state_.repetitions_completed = 0;
current_state_.pending_index = 0u;
}
std::string ImageAnimationController::AnimationState::ToString() const {
std::ostringstream str;
str << "paint_image_id[" << paint_image_id_ << "]\nrequested_repetitions["
<< requested_repetitions_ << "]\nrepetitions_completed["
<< requested_repetitions_ << "]\ndrivers[" << drivers_.size()
<< "]\nactive_index[" << active_index_ << "]\npending_index["
<< current_state_.pending_index << "]\nnext_desired_frame_time["
<< (current_state_.next_desired_frame_time - animation_started_time_)
.InMillisecondsF()
<< "]\nnext_desired_tick_time["
<< (current_state_.next_desired_tick_time - animation_started_time_)
.InMillisecondsF()
<< "]\nshould_animate_from_drivers[" << should_animate_from_drivers_
<< "]\ncompletion_state[" << static_cast<int>(completion_state_) << "]";
return str.str();
}
size_t ImageAnimationController::AnimationState::NextFrameIndex(
size_t pending_index) const {
if (!animation_started_)
return 0u;
return (pending_index + 1) % frames_.size();
}
ImageAnimationController::InvalidationScheduler::InvalidationScheduler(
base::SingleThreadTaskRunner* task_runner,
Client* client)
: task_runner_(task_runner), client_(client) {
DCHECK(task_runner_->BelongsToCurrentThread());
}
ImageAnimationController::InvalidationScheduler::~InvalidationScheduler() {
DCHECK(task_runner_->BelongsToCurrentThread());
}
void ImageAnimationController::InvalidationScheduler::Schedule(
base::TimeTicks animation_time) {
auto now = now_callback_for_testing_.is_null()
? base::TimeTicks::Now()
: now_callback_for_testing_.Run();
if (state_ == InvalidationState::kPendingInvalidation ||
state_ == InvalidationState::kPendingImplFrame) {
return;
}
if (animation_time < now)
animation_time = now;
if (state_ == InvalidationState::kPendingRequestBeginFrame &&
animation_time == next_animation_time_)
return;
Cancel();
base::TimeDelta delta = animation_time - now;
TRACE_EVENT1("cc", "ScheduleFrameForImageAnimation", "delta",
delta.InMillisecondsF());
state_ = InvalidationState::kPendingRequestBeginFrame;
next_animation_time_ = animation_time;
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&InvalidationScheduler::RequestBeginFrame,
weak_factory_.GetWeakPtr()),
delta);
}
void ImageAnimationController::InvalidationScheduler::Cancel() {
state_ = InvalidationState::kIdle;
weak_factory_.InvalidateWeakPtrs();
}
void ImageAnimationController::InvalidationScheduler::RequestBeginFrame() {
TRACE_EVENT0(
"cc",
"ImageAnimationController::InvalidationScheduler::RequestBeginFrame");
DCHECK_EQ(state_, InvalidationState::kPendingRequestBeginFrame);
state_ = InvalidationState::kPendingImplFrame;
client_->RequestBeginFrameForAnimatedImages();
}
void ImageAnimationController::InvalidationScheduler::WillAnimate() {
if (state_ != InvalidationState::kPendingInvalidation)
return;
state_ = InvalidationState::kIdle;
next_animation_time_ = base::TimeTicks();
}
void ImageAnimationController::InvalidationScheduler::WillBeginImplFrame(
const viz::BeginFrameArgs& args) {
switch (state_) {
case InvalidationState::kIdle:
case InvalidationState::kPendingInvalidation:
break;
case InvalidationState::kPendingRequestBeginFrame:
if (args.frame_time >= next_animation_time_) {
RequestInvalidation();
}
break;
case InvalidationState::kPendingImplFrame:
if (args.frame_time >= next_animation_time_) {
RequestInvalidation();
} else {
client_->RequestBeginFrameForAnimatedImages();
}
break;
}
}
void ImageAnimationController::InvalidationScheduler::RequestInvalidation() {
TRACE_EVENT0(
"cc",
"ImageAnimationController::InvalidationScheduler::RequestInvalidation");
DCHECK_NE(state_, InvalidationState::kIdle);
DCHECK_NE(state_, InvalidationState::kPendingInvalidation);
Cancel();
state_ = InvalidationState::kPendingInvalidation;
client_->RequestInvalidationForAnimatedImages();
}
}