// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "cc/trees/image_animation_controller.h" #include "base/bind.h" #include "base/metrics/histogram_macros.h" #include "base/trace_event/trace_event.h" #include "cc/paint/image_animation_count.h" namespace cc { namespace { // The maximum number of time an animation can be delayed before it is reset to // start from the beginning, instead of fast-forwarding to catch up to the // desired frame. const base::TimeDelta kAnimationResyncCutoff = base::TimeDelta::FromMinutes(5); } // namespace ImageAnimationController::ImageAnimationController( base::SingleThreadTaskRunner* task_runner, base::RepeatingClosure invalidation_callback, bool enable_image_animation_resync) : notifier_(task_runner, invalidation_callback), 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); DCHECK(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); DCHECK(it != animation_state_map_.end()); it->second.RemoveDriver(driver); if (!it->second.has_drivers()) registered_animations_.erase(paint_image_id); } const PaintImageIdFlatSet& ImageAnimationController::AnimateForSyncTree( base::TimeTicks now) { TRACE_EVENT0("cc", "ImageAnimationController::AnimateImagesForSyncTree"); DCHECK(images_animated_on_sync_tree_.empty()); notifier_.WillAnimate(); base::Optional next_invalidation_time; for (auto id : registered_animations_) { auto it = animation_state_map_.find(id); DCHECK(it != animation_state_map_.end()); AnimationState& state = it->second; // Is anyone still interested in animating this image? state.UpdateStateFromDrivers(); if (!state.ShouldAnimate()) continue; // If we were able to advance this animation, invalidate it on the sync // tree. if (state.AdvanceFrame(now, enable_image_animation_resync_)) images_animated_on_sync_tree_.insert(id); // Update the next invalidation time to the earliest time at which we need // a frame to animate an image. // Note its important to check ShouldAnimate() here again since advancing to // a new frame on the sync tree means we might not need to animate this // image any longer. if (!state.ShouldAnimate()) continue; DCHECK_GT(state.next_desired_frame_time(), now); if (!next_invalidation_time.has_value()) { next_invalidation_time.emplace(state.next_desired_frame_time()); } else { next_invalidation_time = std::min(state.next_desired_frame_time(), next_invalidation_time.value()); } } if (next_invalidation_time.has_value()) notifier_.Schedule(now, next_invalidation_time.value()); else notifier_.Cancel(); return images_animated_on_sync_tree_; } void ImageAnimationController::UpdateStateFromDrivers(base::TimeTicks now) { TRACE_EVENT0("cc", "UpdateStateFromAnimationDrivers"); base::Optional next_invalidation_time; for (auto image_id : registered_animations_) { auto it = animation_state_map_.find(image_id); DCHECK(it != animation_state_map_.end()); AnimationState& state = it->second; state.UpdateStateFromDrivers(); // Note that by not updating the |next_invalidation_time| from this image // here, we will cancel any pending invalidation scheduled for this image // when updating the |notifier_| at the end of this loop. if (!state.ShouldAnimate()) continue; if (!next_invalidation_time.has_value()) { next_invalidation_time.emplace(state.next_desired_frame_time()); } else { next_invalidation_time = std::min(next_invalidation_time.value(), state.next_desired_frame_time()); } } if (next_invalidation_time.has_value()) notifier_.Schedule(now, next_invalidation_time.value()); else notifier_.Cancel(); } void ImageAnimationController::DidActivate() { TRACE_EVENT0("cc", "ImageAnimationController::WillActivate"); for (auto id : images_animated_on_sync_tree_) { auto it = animation_state_map_.find(id); DCHECK(it != animation_state_map_.end()); it->second.PushPendingToActive(); } images_animated_on_sync_tree_.clear(); // We would retain state for images with no drivers (no recordings) to allow // resuming of animations. However, since the animation will be re-started // from the beginning after navigation, we can avoid maintaining the state. 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); DCHECK(it != animation_state_map_.end()); return tree == WhichTree::PENDING_TREE ? it->second.pending_index() : it->second.active_index(); } const base::flat_set& ImageAnimationController::GetDriversForTesting( PaintImage::Id paint_image_id) const { const auto& it = animation_state_map_.find(paint_image_id); DCHECK(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); DCHECK(it != animation_state_map_.end()); return it->second.last_num_frames_skipped_for_testing(); } 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 { DCHECK(repetitions_completed_ == 0 || is_complete()); // If we have no drivers for this image, no need to animate it. 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"; break; case kAnimationLoopInfinite: break; default: if (requested_repetitions_ <= repetitions_completed_) return false; } // If we have not yet received all data for this image, we can not advance to // an incomplete frame. if (!frames_[NextFrameIndex()].complete) return false; // If we don't have all data for this image, we can not trust the frame count // and loop back to the first frame. size_t last_frame_index = frames_.size() - 1; if (completion_state_ != PaintImage::CompletionState::DONE && pending_index_ == last_frame_index) return false; return true; } bool ImageAnimationController::AnimationState::AdvanceFrame( base::TimeTicks now, bool enable_image_animation_resync) { DCHECK(ShouldAnimate()); // Start the animation from the first frame, if not yet started. We don't need // an invalidation here if the pending and active tree are both displaying the // first frame. Its possible for the 2 to be different if the animation was // reset, in which case we are starting again from the first frame on the // pending tree. if (!animation_started_) { DCHECK_EQ(pending_index_, 0u); next_desired_frame_time_ = now + frames_[0].duration; animation_started_ = true; return pending_index_ != active_index_; } // Don't advance the animation if its not time yet to move to the next frame. if (now < next_desired_frame_time_) return false; // If the animation is more than 5 min out of date, we don't bother catching // up and start again from the current frame. // Note that we don't need to invalidate this image since the active tree // is already displaying the current frame. if (enable_image_animation_resync && now - next_desired_frame_time_ > kAnimationResyncCutoff) { DCHECK_EQ(pending_index_, active_index_); next_desired_frame_time_ = now + frames_[pending_index_].duration; return false; } // Keep catching up the animation until we reach the frame we should be // displaying now. // TODO(khushalsagar): Avoid unnecessary iterations for skipping whole loops // in the animations. size_t last_frame_index = frames_.size() - 1; size_t num_of_frames_advanced = 0u; while (next_desired_frame_time_ <= now && ShouldAnimate()) { num_of_frames_advanced++; size_t next_frame_index = NextFrameIndex(); base::TimeTicks next_desired_frame_time = next_desired_frame_time_ + frames_[next_frame_index].duration; // The image may load more slowly than it's supposed to animate, so that by // the time we reach the end of the first repetition, we're well behind. // Start the animation from the first frame in this case, so that we don't // skip frames (or whole iterations) trying to "catch up". This is a // tradeoff: It guarantees users see the whole animation the second time // through and don't miss any repetitions, and is closer to what other // browsers do; on the other hand, it makes animations "less accurate" for // pages that try to sync an image and some other resource (e.g. audio), // especially if users switch tabs (and thus stop drawing the animation, // which will pause it) during that initial loop, then switch back later. if (enable_image_animation_resync && next_frame_index == 0u && repetitions_completed_ == 1 && next_desired_frame_time <= now) { pending_index_ = 0u; next_desired_frame_time_ = now + frames_[0].duration; repetitions_completed_ = 0; break; } pending_index_ = next_frame_index; next_desired_frame_time_ = next_desired_frame_time; // If we are advancing to the last frame and the image has been completely // loaded (which means that the frame count is known to be accurate), we // just finished a loop in the animation. if (pending_index_ == last_frame_index && is_complete()) repetitions_completed_++; } // We should have advanced a single frame, anything more than that are frames // skipped trying to catch up. DCHECK_GT(num_of_frames_advanced, 0u); last_num_frames_skipped_ = num_of_frames_advanced - 1u; UMA_HISTOGRAM_COUNTS_100000("AnimatedImage.NumOfFramesSkipped.Compositor", last_num_frames_skipped_); return pending_index_ != active_index_; } 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; DCHECK_GT(frames_.size(), 1u); DCHECK(completion_state_ != PaintImage::CompletionState::DONE || data.completion_state == PaintImage::CompletionState::DONE) << "If the image was marked complete before, it can not be incomplete in " "a new update"; completion_state_ = data.completion_state; // Update the repetition count in case we have displayed the last frame and // we now know the frame count to be accurate. size_t last_frame_index = frames_.size() - 1; if (pending_index_ == last_frame_index && is_complete() && repetitions_completed_ == 0) repetitions_completed_++; // Reset the animation if the sequence id received in this recording was // incremented. 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_ = pending_index_; } 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 (auto* driver : drivers_) { if (driver->ShouldAnimate(paint_image_id_)) { should_animate_from_drivers_ = true; break; } } } void ImageAnimationController::AnimationState::ResetAnimation() { animation_started_ = false; next_desired_frame_time_ = base::TimeTicks(); repetitions_completed_ = 0; pending_index_ = 0u; // Don't reset the |active_index_|, tiles on the active tree still need it. } size_t ImageAnimationController::AnimationState::NextFrameIndex() const { if (!animation_started_) return 0u; return (pending_index_ + 1) % frames_.size(); } ImageAnimationController::DelayedNotifier::DelayedNotifier( base::SingleThreadTaskRunner* task_runner, base::RepeatingClosure closure) : task_runner_(task_runner), closure_(std::move(closure)), weak_factory_(this) { DCHECK(task_runner_->BelongsToCurrentThread()); } ImageAnimationController::DelayedNotifier::~DelayedNotifier() { DCHECK(task_runner_->BelongsToCurrentThread()); } void ImageAnimationController::DelayedNotifier::Schedule( base::TimeTicks now, base::TimeTicks notification_time) { // If an animation is already pending, don't schedule another invalidation. // We will schedule the next invalidation based on the latest animation state // during AnimateForSyncTree. if (animation_pending_) return; // The requested notification time can be in the past. For instance, if an // animation was paused because the image became invisible. if (notification_time < now) notification_time = now; // If we already have a notification scheduled to run at this time, no need to // Cancel it. if (pending_notification_time_.has_value() && notification_time == pending_notification_time_.value()) return; // Cancel the pending notification since we the requested notification time // has changed. Cancel(); TRACE_EVENT2("cc", "ScheduleInvalidationForImageAnimation", "notification_time", notification_time, "now", now); pending_notification_time_.emplace(notification_time); task_runner_->PostDelayedTask( FROM_HERE, base::Bind(&DelayedNotifier::Notify, weak_factory_.GetWeakPtr()), notification_time - now); } void ImageAnimationController::DelayedNotifier::Cancel() { pending_notification_time_.reset(); weak_factory_.InvalidateWeakPtrs(); } void ImageAnimationController::DelayedNotifier::Notify() { pending_notification_time_.reset(); animation_pending_ = true; closure_.Run(); } void ImageAnimationController::DelayedNotifier::WillAnimate() { animation_pending_ = false; } } // namespace cc