/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "SMILAnimationController.h" #include #include "mozilla/AutoRestore.h" #include "mozilla/PresShell.h" #include "mozilla/PresShellInlines.h" #include "mozilla/RestyleManager.h" #include "mozilla/SMILTimedElement.h" #include "mozilla/dom/DocumentInlines.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/SVGAnimationElement.h" #include "nsContentUtils.h" #include "nsCSSProps.h" #include "nsRefreshDriver.h" #include "mozilla/dom/Document.h" #include "SMILCompositor.h" #include "SMILCSSProperty.h" using namespace mozilla::dom; namespace mozilla { //---------------------------------------------------------------------- // SMILAnimationController implementation //---------------------------------------------------------------------- // ctors, dtors, factory methods SMILAnimationController::SMILAnimationController(Document* aDoc) : mDocument(aDoc) { MOZ_ASSERT(aDoc, "need a non-null document"); if (nsRefreshDriver* refreshDriver = GetRefreshDriver()) { mStartTime = refreshDriver->MostRecentRefresh(); } else { mStartTime = mozilla::TimeStamp::Now(); } mCurrentSampleTime = mStartTime; Begin(); } SMILAnimationController::~SMILAnimationController() { NS_ASSERTION(mAnimationElementTable.Count() == 0, "Animation controller shouldn't be tracking any animation" " elements when it dies"); NS_ASSERTION(!mRegisteredWithRefreshDriver, "Leaving stale entry in refresh driver's observer list"); } void SMILAnimationController::Disconnect() { MOZ_ASSERT(mDocument, "disconnecting when we weren't connected...?"); MOZ_ASSERT(mRefCnt.get() == 1, "Expecting to disconnect when doc is sole remaining owner"); NS_ASSERTION(mPauseState & SMILTimeContainer::PAUSE_PAGEHIDE, "Expecting to be paused for pagehide before disconnect"); StopSampling(GetRefreshDriver()); mDocument = nullptr; // (raw pointer) } //---------------------------------------------------------------------- // SMILTimeContainer methods: void SMILAnimationController::Pause(uint32_t aType) { SMILTimeContainer::Pause(aType); UpdateSampling(); } void SMILAnimationController::Resume(uint32_t aType) { bool wasPaused = !!mPauseState; // Update mCurrentSampleTime so that calls to GetParentTime--used for // calculating parent offsets--are accurate mCurrentSampleTime = mozilla::TimeStamp::Now(); SMILTimeContainer::Resume(aType); if (wasPaused && !mPauseState) { UpdateSampling(); } } SMILTime SMILAnimationController::GetParentTime() const { return (SMILTime)(mCurrentSampleTime - mStartTime).ToMilliseconds(); } //---------------------------------------------------------------------- // nsARefreshObserver methods: NS_IMPL_ADDREF(SMILAnimationController) NS_IMPL_RELEASE(SMILAnimationController) // nsRefreshDriver Callback function void SMILAnimationController::WillRefresh(mozilla::TimeStamp aTime) { // Although we never expect aTime to go backwards, when we initialise the // animation controller, if we can't get hold of a refresh driver we // initialise mCurrentSampleTime to Now(). It may be possible that after // doing so we get sampled by a refresh driver whose most recent refresh time // predates when we were initialised, so to be safe we make sure to take the // most recent time here. aTime = std::max(mCurrentSampleTime, aTime); // Sleep detection: If the time between samples is a whole lot greater than we // were expecting then we assume the computer went to sleep or someone's // messing with the clock. In that case, fiddle our parent offset and use our // average time between samples to calculate the new sample time. This // prevents us from hanging while trying to catch up on all the missed time. // Smoothing of coefficient for the average function. 0.2 should let us track // the sample rate reasonably tightly without being overly affected by // occasional delays. static const double SAMPLE_DUR_WEIGHTING = 0.2; // If the elapsed time exceeds our expectation by this number of times we'll // initiate special behaviour to basically ignore the intervening time. static const double SAMPLE_DEV_THRESHOLD = 200.0; SMILTime elapsedTime = (SMILTime)(aTime - mCurrentSampleTime).ToMilliseconds(); if (mAvgTimeBetweenSamples == 0) { // First sample. mAvgTimeBetweenSamples = elapsedTime; } else { if (elapsedTime > SAMPLE_DEV_THRESHOLD * mAvgTimeBetweenSamples) { // Unexpectedly long delay between samples. NS_WARNING( "Detected really long delay between samples, continuing from " "previous sample"); mParentOffset += elapsedTime - mAvgTimeBetweenSamples; } // Update the moving average. Due to truncation here the average will // normally be a little less than it should be but that's probably ok. mAvgTimeBetweenSamples = (SMILTime)(elapsedTime * SAMPLE_DUR_WEIGHTING + mAvgTimeBetweenSamples * (1.0 - SAMPLE_DUR_WEIGHTING)); } mCurrentSampleTime = aTime; Sample(); } //---------------------------------------------------------------------- // Animation element registration methods: void SMILAnimationController::RegisterAnimationElement( SVGAnimationElement* aAnimationElement) { const bool wasEmpty = mAnimationElementTable.IsEmpty(); mAnimationElementTable.PutEntry(aAnimationElement); if (wasEmpty) { UpdateSampling(); } } void SMILAnimationController::UnregisterAnimationElement( SVGAnimationElement* aAnimationElement) { mAnimationElementTable.RemoveEntry(aAnimationElement); if (mAnimationElementTable.IsEmpty()) { UpdateSampling(); } } //---------------------------------------------------------------------- // Page show/hide void SMILAnimationController::OnPageShow() { Resume(SMILTimeContainer::PAUSE_PAGEHIDE); } void SMILAnimationController::OnPageHide() { Pause(SMILTimeContainer::PAUSE_PAGEHIDE); } //---------------------------------------------------------------------- // Cycle-collection support void SMILAnimationController::Traverse( nsCycleCollectionTraversalCallback* aCallback) { // Traverse last compositor table if (mLastCompositorTable) { for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) { SMILCompositor* compositor = iter.Get(); compositor->Traverse(aCallback); } } } void SMILAnimationController::Unlink() { mLastCompositorTable = nullptr; } //---------------------------------------------------------------------- // Refresh driver lifecycle related methods void SMILAnimationController::NotifyRefreshDriverCreated( nsRefreshDriver* aRefreshDriver) { UpdateSampling(); } void SMILAnimationController::NotifyRefreshDriverDestroying( nsRefreshDriver* aRefreshDriver) { StopSampling(aRefreshDriver); } //---------------------------------------------------------------------- // Timer-related implementation helpers bool SMILAnimationController::ShouldSample() const { return !mPauseState && !mAnimationElementTable.IsEmpty() && !mChildContainerTable.IsEmpty(); } void SMILAnimationController::UpdateSampling() { const bool shouldSample = ShouldSample(); const bool isSampling = mRegisteredWithRefreshDriver; if (shouldSample == isSampling) { return; } nsRefreshDriver* driver = GetRefreshDriver(); if (!driver) { return; } if (shouldSample) { // We're effectively resuming from a pause so update our current sample time // or else it will confuse our "average time between samples" calculations. mCurrentSampleTime = mozilla::TimeStamp::Now(); driver->AddRefreshObserver(this, FlushType::Style, "SMIL animations"); mRegisteredWithRefreshDriver = true; Sample(); // Run the first sample manually. } else { StopSampling(driver); } } void SMILAnimationController::StopSampling(nsRefreshDriver* aRefreshDriver) { if (aRefreshDriver && mRegisteredWithRefreshDriver) { // NOTE: The document might already have been detached from its PresContext // (and RefreshDriver), which would make GetRefreshDriver() return null. MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(), "Stopping sampling with wrong refresh driver"); aRefreshDriver->RemoveRefreshObserver(this, FlushType::Style); mRegisteredWithRefreshDriver = false; } } //---------------------------------------------------------------------- // Sample-related methods and callbacks void SMILAnimationController::DoSample() { DoSample(true); // Skip unchanged time containers } void SMILAnimationController::DoSample(bool aSkipUnchangedContainers) { if (!mDocument) { NS_ERROR("Shouldn't be sampling after document has disconnected"); return; } if (mRunningSample) { NS_ERROR("Shouldn't be recursively sampling"); return; } bool isStyleFlushNeeded = mResampleNeeded; mResampleNeeded = false; nsCOMPtr document(mDocument); // keeps 'this' alive too // Set running sample flag -- do this before flushing styles so that when we // flush styles we don't end up requesting extra samples AutoRestore autoRestoreRunningSample(mRunningSample); mRunningSample = true; // STEP 1: Bring model up to date // (i) Rewind elements where necessary // (ii) Run milestone samples RewindElements(); DoMilestoneSamples(); // STEP 2: Sample the child time containers // // When we sample the child time containers they will simply record the sample // time in document time. TimeContainerHashtable activeContainers(mChildContainerTable.Count()); for (SMILTimeContainer* container : mChildContainerTable.Keys()) { if (!container) { continue; } if (!container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN) && (container->NeedsSample() || !aSkipUnchangedContainers)) { container->ClearMilestones(); container->Sample(); container->MarkSeekFinished(); activeContainers.PutEntry(container); } } // STEP 3: (i) Sample the timed elements AND // (ii) Create a table of compositors // // (i) Here we sample the timed elements (fetched from the // SVGAnimationElements) which determine from the active time if the // element is active and what its simple time etc. is. This information is // then passed to its time client (SMILAnimationFunction). // // (ii) During the same loop we also build up a table that contains one // compositor for each animated attribute and which maps animated elements to // the corresponding compositor for their target attribute. // // Note that this compositor table needs to be allocated on the heap so we can // store it until the next sample. This lets us find out which elements were // animated in sample 'n-1' but not in sample 'n' (and hence need to have // their animation effects removed in sample 'n'). // // Parts (i) and (ii) are not functionally related but we combine them here to // save iterating over the animation elements twice. // Create the compositor table UniquePtr currentCompositorTable( new SMILCompositorTable(0)); nsTArray> animElems( mAnimationElementTable.Count()); for (SVGAnimationElement* animElem : mAnimationElementTable.Keys()) { SampleTimedElement(animElem, &activeContainers); AddAnimationToCompositorTable(animElem, currentCompositorTable.get(), isStyleFlushNeeded); animElems.AppendElement(animElem); } activeContainers.Clear(); // STEP 4: Compare previous sample's compositors against this sample's. // (Transfer cached base values across, & remove animation effects from // no-longer-animated targets.) if (mLastCompositorTable) { // * Transfer over cached base values, from last sample's compositors for (auto iter = currentCompositorTable->Iter(); !iter.Done(); iter.Next()) { SMILCompositor* compositor = iter.Get(); SMILCompositor* lastCompositor = mLastCompositorTable->GetEntry(compositor->GetKey()); if (lastCompositor) { compositor->StealCachedBaseValue(lastCompositor); } } // * For each compositor in current sample's hash table, remove entry from // prev sample's hash table -- we don't need to clear animation // effects of those compositors, since they're still being animated. for (const auto& key : currentCompositorTable->Keys()) { mLastCompositorTable->RemoveEntry(key); } // * For each entry that remains in prev sample's hash table (i.e. for // every target that's no longer animated), clear animation effects. for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) { iter.Get()->ClearAnimationEffects(); } } // return early if there are no active animations to avoid a style flush if (currentCompositorTable->Count() == 0) { mLastCompositorTable = nullptr; return; } if (isStyleFlushNeeded) { document->FlushPendingNotifications(FlushType::Style); } // WARNING: // WARNING: the above flush may have destroyed the pres shell and/or // WARNING: frames and other layout related objects. // WARNING: // STEP 5: Compose currently-animated attributes. // XXXdholbert: This step traverses our animation targets in an effectively // random order. For animation from/to 'inherit' values to work correctly // when the inherited value is *also* being animated, we really should be // traversing our animated nodes in an ancestors-first order (bug 501183) bool mightHavePendingStyleUpdates = false; for (auto iter = currentCompositorTable->Iter(); !iter.Done(); iter.Next()) { iter.Get()->ComposeAttribute(mightHavePendingStyleUpdates); } // Update last compositor table mLastCompositorTable = std::move(currentCompositorTable); mMightHavePendingStyleUpdates = mightHavePendingStyleUpdates; NS_ASSERTION(!mResampleNeeded, "Resample dirty flag set during sample!"); } void SMILAnimationController::RewindElements() { const bool rewindNeeded = std::any_of( mChildContainerTable.Keys().cbegin(), mChildContainerTable.Keys().cend(), [](SMILTimeContainer* container) { return container->NeedsRewind(); }); if (!rewindNeeded) return; for (SVGAnimationElement* animElem : mAnimationElementTable.Keys()) { SMILTimeContainer* timeContainer = animElem->GetTimeContainer(); if (timeContainer && timeContainer->NeedsRewind()) { animElem->TimedElement().Rewind(); } } for (SMILTimeContainer* container : mChildContainerTable.Keys()) { container->ClearNeedsRewind(); } } void SMILAnimationController::DoMilestoneSamples() { // We need to sample the timing model but because SMIL operates independently // of the frame-rate, we can get one sample at t=0s and the next at t=10min. // // In between those two sample times a whole string of significant events // might be expected to take place: events firing, new interdependencies // between animations resolved and dissolved, etc. // // Furthermore, at any given time, we want to sample all the intervals that // end at that time BEFORE any that begin. This behaviour is implied by SMIL's // endpoint-exclusive timing model. // // So we have the animations (specifically the timed elements) register the // next significant moment (called a milestone) in their lifetime and then we // step through the model at each of these moments and sample those animations // registered for those times. This way events can fire in the correct order, // dependencies can be resolved etc. SMILTime sampleTime = INT64_MIN; while (true) { // We want to find any milestones AT OR BEFORE the current sample time so we // initialise the next milestone to the moment after (1ms after, to be // precise) the current sample time and see if there are any milestones // before that. Any other milestones will be dealt with in a subsequent // sample. SMILMilestone nextMilestone(GetCurrentTimeAsSMILTime() + 1, true); for (SMILTimeContainer* container : mChildContainerTable.Keys()) { if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) { continue; } SMILMilestone thisMilestone; bool didGetMilestone = container->GetNextMilestoneInParentTime(thisMilestone); if (didGetMilestone && thisMilestone < nextMilestone) { nextMilestone = thisMilestone; } } if (nextMilestone.mTime > GetCurrentTimeAsSMILTime()) { break; } nsTArray> elements; for (SMILTimeContainer* container : mChildContainerTable.Keys()) { if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) { continue; } container->PopMilestoneElementsAtMilestone(nextMilestone, elements); } uint32_t length = elements.Length(); // During the course of a sampling we don't want to actually go backwards. // Due to negative offsets, early ends and the like, a timed element might // register a milestone that is actually in the past. That's fine, but it's // still only going to get *sampled* with whatever time we're up to and no // earlier. // // Because we're only performing this clamping at the last moment, the // animations will still all get sampled in the correct order and // dependencies will be appropriately resolved. sampleTime = std::max(nextMilestone.mTime, sampleTime); for (uint32_t i = 0; i < length; ++i) { SVGAnimationElement* elem = elements[i].get(); MOZ_ASSERT(elem, "nullptr animation element in list"); SMILTimeContainer* container = elem->GetTimeContainer(); if (!container) // The container may be nullptr if the element has been detached from // its parent since registering a milestone. continue; SMILTimeValue containerTimeValue = container->ParentToContainerTime(sampleTime); if (!containerTimeValue.IsDefinite()) continue; // Clamp the converted container time to non-negative values. SMILTime containerTime = std::max(0, containerTimeValue.GetMillis()); if (nextMilestone.mIsEnd) { elem->TimedElement().SampleEndAt(containerTime); } else { elem->TimedElement().SampleAt(containerTime); } } } } /*static*/ void SMILAnimationController::SampleTimedElement( SVGAnimationElement* aElement, TimeContainerHashtable* aActiveContainers) { SMILTimeContainer* timeContainer = aElement->GetTimeContainer(); if (!timeContainer) return; // We'd like to call timeContainer->NeedsSample() here and skip all timed // elements that belong to paused time containers that don't need a sample, // but that doesn't work because we've already called Sample() on all the time // containers so the paused ones don't need a sample any more and they'll // return false. // // Instead we build up a hashmap of active time containers during the previous // step (SampleTimeContainer) and then test here if the container for this // timed element is in the list. if (!aActiveContainers->GetEntry(timeContainer)) return; SMILTime containerTime = timeContainer->GetCurrentTimeAsSMILTime(); MOZ_ASSERT(!timeContainer->IsSeeking(), "Doing a regular sample but the time container is still seeking"); aElement->TimedElement().SampleAt(containerTime); } /*static*/ void SMILAnimationController::AddAnimationToCompositorTable( SVGAnimationElement* aElement, SMILCompositorTable* aCompositorTable, bool& aStyleFlushNeeded) { // Add a compositor to the hash table if there's not already one there SMILTargetIdentifier key; if (!GetTargetIdentifierForAnimation(aElement, key)) // Something's wrong/missing about animation's target; skip this animation return; SMILAnimationFunction& func = aElement->AnimationFunction(); // Only add active animation functions. If there are no active animations // targeting an attribute, no compositor will be created and any previously // applied animations will be cleared. if (func.IsActiveOrFrozen()) { // Look up the compositor for our target, & add our animation function // to its list of animation functions. SMILCompositor* result = aCompositorTable->PutEntry(key); result->AddAnimationFunction(&func); } else if (func.HasChanged()) { // Look up the compositor for our target, and force it to skip the // "nothing's changed so don't bother compositing" optimization for this // sample. |func| is inactive, but it's probably *newly* inactive (since // it's got HasChanged() == true), so we need to make sure to recompose // its target. SMILCompositor* result = aCompositorTable->PutEntry(key); result->ToggleForceCompositing(); // We've now made sure that |func|'s inactivity will be reflected as of // this sample. We need to clear its HasChanged() flag so that it won't // trigger this same clause in future samples (until it changes again). func.ClearHasChanged(); } aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample(); } static inline bool IsTransformAttribute(int32_t aNamespaceID, nsAtom* aAttributeName) { return aNamespaceID == kNameSpaceID_None && (aAttributeName == nsGkAtoms::transform || aAttributeName == nsGkAtoms::patternTransform || aAttributeName == nsGkAtoms::gradientTransform); } // Helper function that, given a SVGAnimationElement, looks up its target // element & target attribute and populates a SMILTargetIdentifier // for this target. /*static*/ bool SMILAnimationController::GetTargetIdentifierForAnimation( SVGAnimationElement* aAnimElem, SMILTargetIdentifier& aResult) { // Look up target (animated) element Element* targetElem = aAnimElem->GetTargetElementContent(); if (!targetElem) // Animation has no target elem -- skip it. return false; // Look up target (animated) attribute // SMILANIM section 3.1, attributeName may // have an XMLNS prefix to indicate the XML namespace. RefPtr attributeName; int32_t attributeNamespaceID; if (!aAnimElem->GetTargetAttributeName(&attributeNamespaceID, getter_AddRefs(attributeName))) // Animation has no target attr -- skip it. return false; // animateTransform can only animate transforms, conversely transforms // can only be animated by animateTransform if (IsTransformAttribute(attributeNamespaceID, attributeName) != (aAnimElem->IsSVGElement(nsGkAtoms::animateTransform))) return false; // Construct the key aResult.mElement = targetElem; aResult.mAttributeName = attributeName; aResult.mAttributeNamespaceID = attributeNamespaceID; return true; } bool SMILAnimationController::PreTraverse() { return PreTraverseInSubtree(nullptr); } bool SMILAnimationController::PreTraverseInSubtree(Element* aRoot) { MOZ_ASSERT(NS_IsMainThread()); if (!mMightHavePendingStyleUpdates) { return false; } nsPresContext* context = mDocument->GetPresContext(); if (!context) { return false; } bool foundElementsNeedingRestyle = false; for (SVGAnimationElement* animElement : mAnimationElementTable.Keys()) { SMILTargetIdentifier key; if (!GetTargetIdentifierForAnimation(animElement, key)) { // Something's wrong/missing about animation's target; skip this animation continue; } // Ignore restyles that aren't in the flattened tree subtree rooted at // aRoot. if (aRoot && !nsContentUtils::ContentIsFlattenedTreeDescendantOf( key.mElement, aRoot)) { continue; } context->RestyleManager()->PostRestyleEventForAnimations( key.mElement, PseudoStyleType::NotPseudo, RestyleHint::RESTYLE_SMIL); foundElementsNeedingRestyle = true; } // Only clear the mMightHavePendingStyleUpdates flag if we definitely posted // all restyles. if (!aRoot) { mMightHavePendingStyleUpdates = false; } return foundElementsNeedingRestyle; } //---------------------------------------------------------------------- // Add/remove child time containers nsresult SMILAnimationController::AddChild(SMILTimeContainer& aChild) { const bool wasEmpty = mChildContainerTable.IsEmpty(); TimeContainerPtrKey* key = mChildContainerTable.PutEntry(&aChild); NS_ENSURE_TRUE(key, NS_ERROR_OUT_OF_MEMORY); if (wasEmpty) { UpdateSampling(); } return NS_OK; } void SMILAnimationController::RemoveChild(SMILTimeContainer& aChild) { mChildContainerTable.RemoveEntry(&aChild); if (mChildContainerTable.IsEmpty()) { UpdateSampling(); } } // Helper method nsRefreshDriver* SMILAnimationController::GetRefreshDriver() { if (!mDocument) { NS_ERROR("Requesting refresh driver after document has disconnected!"); return nullptr; } nsPresContext* context = mDocument->GetPresContext(); return context ? context->RefreshDriver() : nullptr; } void SMILAnimationController::FlagDocumentNeedsFlush() { if (PresShell* presShell = mDocument->GetPresShell()) { presShell->SetNeedStyleFlush(); } } } // namespace mozilla