blob: 5651870de064ed05b7ae469ac2ac7ae0a1d49127 [file] [log] [blame]
/*
* Copyright (C) 2020-2025 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "AnimationTimelinesController.h"
#include "AnimationEventBase.h"
#include "CSSAnimation.h"
#include "CSSTransition.h"
#include "Document.h"
#include "DocumentTimeline.h"
#include "ElementInlines.h"
#include "EventLoop.h"
#include "KeyframeEffect.h"
#include "LocalDOMWindow.h"
#include "Logging.h"
#include "Page.h"
#include "ScrollTimeline.h"
#include "Settings.h"
#include "StyleOriginatedTimelinesController.h"
#include "ViewTimeline.h"
#include "WebAnimation.h"
#include <ranges>
#include <wtf/HashSet.h>
#include <wtf/Ref.h>
#include <wtf/text/TextStream.h>
#if ENABLE(THREADED_ANIMATION_RESOLUTION)
#include "AcceleratedEffectStackUpdater.h"
#endif
namespace WebCore {
DEFINE_ALLOCATOR_WITH_HEAP_IDENTIFIER(AnimationTimelinesController);
AnimationTimelinesController::AnimationTimelinesController(Document& document)
: m_cachedCurrentTimeClearanceTimer(*this, &AnimationTimelinesController::clearCachedCurrentTime)
, m_document(document)
{
if (RefPtr page = document.page()) {
if (page->settings().hiddenPageCSSAnimationSuspensionEnabled() && !page->isVisible())
suspendAnimations();
}
}
AnimationTimelinesController::~AnimationTimelinesController() = default;
void AnimationTimelinesController::addTimeline(AnimationTimeline& timeline)
{
m_timelines.add(timeline);
if (m_isSuspended)
timeline.suspendAnimations();
else
timeline.resumeAnimations();
}
void AnimationTimelinesController::removeTimeline(AnimationTimeline& timeline)
{
m_timelines.remove(timeline);
}
void AnimationTimelinesController::detachFromDocument()
{
m_pendingAnimationsProcessingTaskCancellationGroup.cancel();
while (RefPtr timeline = m_timelines.takeAny())
timeline->detachFromDocument();
}
void AnimationTimelinesController::addPendingAnimation(WebAnimation& animation)
{
m_pendingAnimations.add(animation);
}
void AnimationTimelinesController::updateAnimationsAndSendEvents(ReducedResolutionSeconds timestamp)
{
auto previousMaximumAnimationFrameRate = maximumAnimationFrameRate();
// This will hold the frame rate at which we would schedule updates not
// accounting for the frame rate of animations.
std::optional<FramesPerSecond> defaultTimelineFrameRate;
// This will hold the frame rate used for this timeline until now.
std::optional<FramesPerSecond> previousTimelineFrameRate;
if (RefPtr page = m_document->page()) {
defaultTimelineFrameRate = page->preferredRenderingUpdateFramesPerSecond({ Page::PreferredRenderingUpdateOption::IncludeThrottlingReasons });
previousTimelineFrameRate = page->preferredRenderingUpdateFramesPerSecond({
Page::PreferredRenderingUpdateOption::IncludeThrottlingReasons,
Page::PreferredRenderingUpdateOption::IncludeAnimationsFrameRate
});
}
LOG_WITH_STREAM(Animations, stream << "AnimationTimelinesController::updateAnimationsAndSendEvents for time " << timestamp);
// We need to copy m_timelines before iterating over its members since the steps in this procedure may mutate m_timelines.
auto protectedTimelines = copyToVectorOf<Ref<AnimationTimeline>>(m_timelines);
// We need to freeze the current time even if no animation is running.
// document.timeline.currentTime may be called from a rAF callback and
// it has to match the rAF timestamp.
if (!m_isSuspended)
cacheCurrentTime(timestamp);
m_frameRateAligner.beginUpdate(timestamp, previousTimelineFrameRate);
// 1. Update the current time of all timelines associated with document passing now as the timestamp.
ASSERT(m_updatedScrollTimelines.isEmpty());
Vector<Ref<AnimationTimeline>> timelinesToUpdate;
Vector<Ref<WebAnimation>> animationsToRemove;
Vector<Ref<CSSTransition>> completedTransitions;
for (auto& timeline : protectedTimelines) {
auto shouldUpdateAnimationsAndSendEvents = timeline->documentWillUpdateAnimationsAndSendEvents();
if (shouldUpdateAnimationsAndSendEvents == AnimationTimeline::ShouldUpdateAnimationsAndSendEvents::No)
continue;
timelinesToUpdate.append(timeline.copyRef());
// https://drafts.csswg.org/scroll-animations-1/#event-loop
if (RefPtr scrollTimeline = dynamicDowncast<ScrollTimeline>(timeline))
m_updatedScrollTimelines.append(*scrollTimeline);
for (auto& animation : copyToVector(timeline->relevantAnimations())) {
if (animation->isSkippedContentAnimation())
continue;
if (animation->timeline() != timeline.ptr()) {
ASSERT(!animation->timeline());
continue;
}
// Even though this animation is relevant, its frame rate may be such that it should
// be disregarded during this update. If it does not specify an explicit frame rate,
// this means this animation uses the default frame rate at which we typically schedule
// updates not accounting for the frame rate of animations.
auto animationFrameRate = animation->frameRate();
if (!animationFrameRate)
animationFrameRate = defaultTimelineFrameRate;
if (animationFrameRate) {
ASSERT(*animationFrameRate > 0);
auto shouldUpdate = m_frameRateAligner.updateFrameRate(*animationFrameRate);
// Even if we're told not to update, any newly-added animation should fire right away,
// it will align with other animations of that frame rate at the next opportunity.
if (shouldUpdate == FrameRateAligner::ShouldUpdate::No && !animation->pending())
continue;
}
// This will notify the animation that timing has changed and will call automatically
// schedule invalidation if required for this animation.
animation->tick();
if (!animation->isRelevant() && !animation->needsTick() && !isPendingTimelineAttachment(animation))
animationsToRemove.append(animation);
if (RefPtr transition = dynamicDowncast<CSSTransition>(animation)) {
if (!transition->needsTick() && transition->playState() == WebAnimation::PlayState::Finished && transition->owningElement())
completedTransitions.append(*transition);
}
}
}
m_frameRateAligner.finishUpdate();
// If the maximum frame rate we've encountered is the same as the default frame rate,
// let's reset it to not have an explicit value which will indicate that there is no
// need to override the default animation frame rate to service animations.
auto maximumAnimationFrameRate = m_frameRateAligner.maximumFrameRate();
if (maximumAnimationFrameRate == defaultTimelineFrameRate)
maximumAnimationFrameRate = std::nullopt;
// Ensure the timeline updates at the maximum frame rate we've encountered for our animations.
if (previousMaximumAnimationFrameRate != maximumAnimationFrameRate) {
if (RefPtr page = m_document->page()) {
if (previousTimelineFrameRate != maximumAnimationFrameRate)
page->timelineControllerMaximumAnimationFrameRateDidChange(*this);
}
}
if (timelinesToUpdate.isEmpty())
return;
// 2. Remove replaced animations for document.
for (auto& timeline : protectedTimelines) {
if (RefPtr documentTimeline = dynamicDowncast<DocumentTimeline>(timeline))
documentTimeline->removeReplacedAnimations();
}
// 3. Perform a microtask checkpoint.
protectedDocument()->eventLoop().performMicrotaskCheckpoint();
if (RefPtr documentTimeline = m_document->existingTimeline()) {
// FIXME: pending animation events should be owned by this controller rather
// than the document timeline.
// 4. Let events to dispatch be a copy of doc's pending animation event queue.
// 5. Clear doc's pending animation event queue.
auto events = documentTimeline->prepareForPendingAnimationEventsDispatch();
// 6. Perform a stable sort of the animation events in events to dispatch as follows.
std::ranges::stable_sort(events, compareAnimationEventsByCompositeOrder);
// 7. Dispatch each of the events in events to dispatch at their corresponding target using the order established in the previous step.
for (auto& event : events)
event->target()->dispatchEvent(event);
}
// This will cancel any scheduled invalidation if we end up removing all animations.
for (auto& animation : animationsToRemove) {
// An animation that was initially marked as irrelevant may have changed while
// we were sending events, so redo the the check for whether it should be removed.
if (RefPtr timeline = animation->timeline()) {
if (!animation->isRelevant() && !animation->needsTick())
timeline->removeAnimation(animation);
}
}
// Now that animations that needed removal have been removed, update the list of completed transitions.
// This needs to happen after dealing with the list of animations to remove as the animation may have been
// removed from the list of completed transitions otherwise.
for (auto& completedTransition : completedTransitions) {
if (RefPtr documentTimeline = dynamicDowncast<DocumentTimeline>(completedTransition->timeline()))
documentTimeline->transitionDidComplete(WTFMove(completedTransition));
}
for (auto& timeline : timelinesToUpdate) {
if (RefPtr documentTimeline = dynamicDowncast<DocumentTimeline>(timeline))
documentTimeline->documentDidUpdateAnimationsAndSendEvents();
}
}
void AnimationTimelinesController::updateStaleScrollTimelines()
{
// https://drafts.csswg.org/scroll-animations-1/#event-loop
auto scrollTimelines = std::exchange(m_updatedScrollTimelines, { });
for (auto scrollTimeline : scrollTimelines)
scrollTimeline->updateCurrentTimeIfStale();
}
std::optional<Seconds> AnimationTimelinesController::timeUntilNextTickForAnimationsWithFrameRate(FramesPerSecond frameRate) const
{
if (!m_cachedCurrentTime)
return std::nullopt;
return m_frameRateAligner.timeUntilNextUpdateForFrameRate(frameRate, *m_cachedCurrentTime);
};
void AnimationTimelinesController::suspendAnimations()
{
if (m_isSuspended)
return;
if (!m_cachedCurrentTime)
m_cachedCurrentTime = liveCurrentTime();
m_cachedCurrentTimeClearanceTimer.stop();
for (Ref timeline : m_timelines)
timeline->suspendAnimations();
m_isSuspended = true;
}
void AnimationTimelinesController::resumeAnimations()
{
if (!m_isSuspended)
return;
m_isSuspended = false;
clearCachedCurrentTime();
for (Ref timeline : m_timelines)
timeline->resumeAnimations();
}
ReducedResolutionSeconds AnimationTimelinesController::liveCurrentTime() const
{
return m_document->window()->nowTimestamp();
}
std::optional<Seconds> AnimationTimelinesController::currentTime(UseCachedCurrentTime useCachedCurrentTime)
{
if (!m_document->window())
return std::nullopt;
if (useCachedCurrentTime == UseCachedCurrentTime::No && !m_isSuspended)
return liveCurrentTime();
if (!m_cachedCurrentTime)
cacheCurrentTime(liveCurrentTime());
return *m_cachedCurrentTime;
}
void AnimationTimelinesController::cacheCurrentTime(ReducedResolutionSeconds newCurrentTime)
{
if (m_cachedCurrentTime == newCurrentTime)
return;
m_cachedCurrentTimeClearanceTimer.stop();
// We can get in a situation where the event loop will not run a task that had been enqueued.
// If that is the case, we must clear the task group and run the callback prior to adding a
// new task.
if (m_pendingAnimationsProcessingTaskCancellationGroup.hasPendingTask() && m_cachedCurrentTime) {
m_pendingAnimationsProcessingTaskCancellationGroup.cancel();
processPendingAnimations();
}
m_cachedCurrentTime = newCurrentTime;
// As we've advanced to a new current time, we want all animations created during this run
// loop to have this newly-cached current time as their start time. To that end, we schedule
// a task to set that current time on all animations created until then as their pending
// start time.
if (!m_pendingAnimationsProcessingTaskCancellationGroup.hasPendingTask()) {
CancellableTask task(m_pendingAnimationsProcessingTaskCancellationGroup, std::bind(&AnimationTimelinesController::processPendingAnimations, this));
m_document->eventLoop().queueTask(TaskSource::InternalAsyncTask, WTFMove(task));
}
if (!m_isSuspended) {
// In order to not have a stale cached current time, we schedule a timer to reset it
// in the time it would take an animation frame to run under normal circumstances.
RefPtr page = m_document->page();
auto renderingUpdateInterval = page ? page->preferredRenderingUpdateInterval() : FullSpeedAnimationInterval;
m_cachedCurrentTimeClearanceTimer.startOneShot(renderingUpdateInterval);
}
}
void AnimationTimelinesController::clearCachedCurrentTime()
{
ASSERT(!m_isSuspended);
m_cachedCurrentTime = std::nullopt;
}
void AnimationTimelinesController::processPendingAnimations()
{
if (m_isSuspended || !m_cachedCurrentTime)
return;
ASSERT(!m_pendingAnimationsProcessingTaskCancellationGroup.hasPendingTask());
auto pendingAnimations = std::exchange(m_pendingAnimations, { });
for (Ref pendingAnimation : pendingAnimations) {
if (pendingAnimation->timeline() && pendingAnimation->timeline()->isMonotonic())
pendingAnimation->setPendingStartTime(*m_cachedCurrentTime);
}
}
bool AnimationTimelinesController::isPendingTimelineAttachment(const WebAnimation& animation) const
{
CheckedPtr styleOriginatedTimelinesController = protectedDocument()->styleOriginatedTimelinesController();
return styleOriginatedTimelinesController && styleOriginatedTimelinesController->isPendingTimelineAttachment(animation);
}
#if ENABLE(THREADED_ANIMATION_RESOLUTION)
AcceleratedEffectStackUpdater& AnimationTimelinesController::acceleratedEffectStackUpdater()
{
if (!m_acceleratedEffectStackUpdater)
m_acceleratedEffectStackUpdater = makeUnique<AcceleratedEffectStackUpdater>(m_document.get());
return *m_acceleratedEffectStackUpdater;
}
#endif
} // namespace WebCore