/* -*- 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 "AnimationInfo.h" #include "mozilla/LayerAnimationInfo.h" #include "mozilla/layers/WebRenderLayerManager.h" #include "mozilla/layers/AnimationHelper.h" #include "mozilla/layers/CompositorThread.h" #include "mozilla/dom/Animation.h" #include "mozilla/dom/CSSTransition.h" #include "mozilla/dom/KeyframeEffect.h" #include "mozilla/EffectSet.h" #include "mozilla/PresShell.h" #include "mozilla/StaticPrefs_layout.h" #include "nsIContent.h" #include "nsLayoutUtils.h" #include "nsPresContextInlines.h" #include "nsStyleTransformMatrix.h" #include "PuppetWidget.h" namespace mozilla { namespace layers { using TransformReferenceBox = nsStyleTransformMatrix::TransformReferenceBox; AnimationInfo::AnimationInfo() : mCompositorAnimationsId(0), mMutated(false) {} AnimationInfo::~AnimationInfo() = default; void AnimationInfo::EnsureAnimationsId() { if (!mCompositorAnimationsId) { mCompositorAnimationsId = AnimationHelper::GetNextCompositorAnimationsId(); } } Animation* AnimationInfo::AddAnimation() { MOZ_ASSERT(!CompositorThreadHolder::IsInCompositorThread()); // Here generates a new id when the first animation is added and // this id is used to represent the animations in this layer. EnsureAnimationsId(); MOZ_ASSERT(!mPendingAnimations, "should have called ClearAnimations first"); Animation* anim = mAnimations.AppendElement(); mMutated = true; return anim; } Animation* AnimationInfo::AddAnimationForNextTransaction() { MOZ_ASSERT(!CompositorThreadHolder::IsInCompositorThread()); MOZ_ASSERT(mPendingAnimations, "should have called ClearAnimationsForNextTransaction first"); Animation* anim = mPendingAnimations->AppendElement(); return anim; } void AnimationInfo::ClearAnimations() { mPendingAnimations = nullptr; if (mAnimations.IsEmpty() && mStorageData.IsEmpty()) { return; } mAnimations.Clear(); mStorageData.Clear(); mMutated = true; } void AnimationInfo::ClearAnimationsForNextTransaction() { // Ensure we have a non-null mPendingAnimations to mark a future clear. if (!mPendingAnimations) { mPendingAnimations = MakeUnique(); } mPendingAnimations->Clear(); } bool AnimationInfo::StartPendingAnimations(const TimeStamp& aReadyTime) { bool updated = false; for (size_t animIdx = 0, animEnd = mAnimations.Length(); animIdx < animEnd; animIdx++) { Animation& anim = mAnimations[animIdx]; // If the animation is doing an async update of its playback rate, then we // want to match whatever its current time would be at *aReadyTime*. if (!std::isnan(anim.previousPlaybackRate()) && anim.startTime().isSome() && !anim.originTime().IsNull() && !anim.isNotPlaying()) { TimeDuration readyTime = aReadyTime - anim.originTime(); anim.holdTime() = dom::Animation::CurrentTimeFromTimelineTime( readyTime, anim.startTime().ref(), anim.previousPlaybackRate()); // Make start time null so that we know to update it below. anim.startTime() = Nothing(); } // If the animation is play-pending, resolve the start time. if (anim.startTime().isNothing() && !anim.originTime().IsNull() && !anim.isNotPlaying()) { TimeDuration readyTime = aReadyTime - anim.originTime(); anim.startTime() = Some(dom::Animation::StartTimeFromTimelineTime( readyTime, anim.holdTime(), anim.playbackRate())); updated = true; } } return updated; } bool AnimationInfo::ApplyPendingUpdatesForThisTransaction() { if (mPendingAnimations) { mAnimations = std::move(*mPendingAnimations); mPendingAnimations = nullptr; return true; } return false; } bool AnimationInfo::HasTransformAnimation() const { const nsCSSPropertyIDSet& transformSet = LayerAnimationInfo::GetCSSPropertiesFor(DisplayItemType::TYPE_TRANSFORM); for (uint32_t i = 0; i < mAnimations.Length(); i++) { if (transformSet.HasProperty(mAnimations[i].property())) { return true; } } return false; } /* static */ Maybe AnimationInfo::GetGenerationFromFrame( nsIFrame* aFrame, DisplayItemType aDisplayItemKey) { MOZ_ASSERT(aFrame->IsPrimaryFrame() || nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame)); // In case of continuation, KeyframeEffectReadOnly uses its first frame, // whereas nsDisplayItem uses its last continuation, so we have to use the // last continuation frame here. if (nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame)) { aFrame = nsLayoutUtils::LastContinuationOrIBSplitSibling(aFrame); } RefPtr animationData = GetWebRenderUserData(aFrame, (uint32_t)aDisplayItemKey); if (animationData) { return animationData->GetAnimationInfo().GetAnimationGeneration(); } return Nothing(); } /* static */ void AnimationInfo::EnumerateGenerationOnFrame( const nsIFrame* aFrame, const nsIContent* aContent, const CompositorAnimatableDisplayItemTypes& aDisplayItemTypes, AnimationGenerationCallback aCallback) { nsIWidget* widget = nsContentUtils::WidgetForContent(aContent); if (!widget) { return; } // If we haven't created a window renderer there's no animation generation // that we can have, thus we call the callback function with |Nothing()| for // the generation. if (!widget->HasWindowRenderer()) { for (auto displayItem : LayerAnimationInfo::sDisplayItemTypes) { aCallback(Nothing(), displayItem); } return; } WindowRenderer* renderer = widget->GetWindowRenderer(); MOZ_ASSERT(renderer); if (!renderer->AsWebRender()) { return; } // In case of continuation, nsDisplayItem uses its last continuation, so we // have to use the last continuation frame here. if (nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame)) { aFrame = nsLayoutUtils::LastContinuationOrIBSplitSibling(aFrame); } for (auto displayItem : LayerAnimationInfo::sDisplayItemTypes) { // For transform animations, the animation is on the primary frame but // |aFrame| is the style frame. const nsIFrame* frameToQuery = displayItem == DisplayItemType::TYPE_TRANSFORM ? nsLayoutUtils::GetPrimaryFrameFromStyleFrame(aFrame) : aFrame; RefPtr animationData = GetWebRenderUserData(frameToQuery, (uint32_t)displayItem); Maybe generation; if (animationData) { generation = animationData->GetAnimationInfo().GetAnimationGeneration(); } aCallback(generation, displayItem); } } static StyleTransformOperation ResolveTranslate( TransformReferenceBox& aRefBox, const LengthPercentage& aX, const LengthPercentage& aY = LengthPercentage::Zero(), const Length& aZ = Length{0}) { float x = nsStyleTransformMatrix::ProcessTranslatePart( aX, &aRefBox, &TransformReferenceBox::Width); float y = nsStyleTransformMatrix::ProcessTranslatePart( aY, &aRefBox, &TransformReferenceBox::Height); return StyleTransformOperation::Translate3D( LengthPercentage::FromPixels(x), LengthPercentage::FromPixels(y), aZ); } static StyleTranslate ResolveTranslate(const StyleTranslate& aValue, TransformReferenceBox& aRefBox) { if (aValue.IsTranslate()) { const auto& t = aValue.AsTranslate(); float x = nsStyleTransformMatrix::ProcessTranslatePart( t._0, &aRefBox, &TransformReferenceBox::Width); float y = nsStyleTransformMatrix::ProcessTranslatePart( t._1, &aRefBox, &TransformReferenceBox::Height); return StyleTranslate::Translate(LengthPercentage::FromPixels(x), LengthPercentage::FromPixels(y), t._2); } MOZ_ASSERT(aValue.IsNone()); return StyleTranslate::None(); } static StyleTransform ResolveTransformOperations( const StyleTransform& aTransform, TransformReferenceBox& aRefBox) { auto convertMatrix = [](const gfx::Matrix4x4& aM) { return StyleTransformOperation::Matrix3D(StyleGenericMatrix3D{ aM._11, aM._12, aM._13, aM._14, aM._21, aM._22, aM._23, aM._24, aM._31, aM._32, aM._33, aM._34, aM._41, aM._42, aM._43, aM._44}); }; Vector result; MOZ_RELEASE_ASSERT( result.initCapacity(aTransform.Operations().Length()), "Allocating vector of transform operations should be successful."); for (const StyleTransformOperation& op : aTransform.Operations()) { switch (op.tag) { case StyleTransformOperation::Tag::TranslateX: result.infallibleAppend(ResolveTranslate(aRefBox, op.AsTranslateX())); break; case StyleTransformOperation::Tag::TranslateY: result.infallibleAppend(ResolveTranslate( aRefBox, LengthPercentage::Zero(), op.AsTranslateY())); break; case StyleTransformOperation::Tag::TranslateZ: result.infallibleAppend( ResolveTranslate(aRefBox, LengthPercentage::Zero(), LengthPercentage::Zero(), op.AsTranslateZ())); break; case StyleTransformOperation::Tag::Translate: { const auto& translate = op.AsTranslate(); result.infallibleAppend( ResolveTranslate(aRefBox, translate._0, translate._1)); break; } case StyleTransformOperation::Tag::Translate3D: { const auto& translate = op.AsTranslate3D(); result.infallibleAppend(ResolveTranslate(aRefBox, translate._0, translate._1, translate._2)); break; } case StyleTransformOperation::Tag::InterpolateMatrix: { gfx::Matrix4x4 matrix; nsStyleTransformMatrix::ProcessInterpolateMatrix(matrix, op, aRefBox); result.infallibleAppend(convertMatrix(matrix)); break; } case StyleTransformOperation::Tag::AccumulateMatrix: { gfx::Matrix4x4 matrix; nsStyleTransformMatrix::ProcessAccumulateMatrix(matrix, op, aRefBox); result.infallibleAppend(convertMatrix(matrix)); break; } case StyleTransformOperation::Tag::RotateX: case StyleTransformOperation::Tag::RotateY: case StyleTransformOperation::Tag::RotateZ: case StyleTransformOperation::Tag::Rotate: case StyleTransformOperation::Tag::Rotate3D: case StyleTransformOperation::Tag::ScaleX: case StyleTransformOperation::Tag::ScaleY: case StyleTransformOperation::Tag::ScaleZ: case StyleTransformOperation::Tag::Scale: case StyleTransformOperation::Tag::Scale3D: case StyleTransformOperation::Tag::SkewX: case StyleTransformOperation::Tag::SkewY: case StyleTransformOperation::Tag::Skew: case StyleTransformOperation::Tag::Matrix: case StyleTransformOperation::Tag::Matrix3D: case StyleTransformOperation::Tag::Perspective: result.infallibleAppend(op); break; default: MOZ_ASSERT_UNREACHABLE("Function not handled yet!"); } } auto transform = StyleTransform{ StyleOwnedSlice(std::move(result))}; MOZ_ASSERT(!transform.HasPercent()); MOZ_ASSERT(transform.Operations().Length() == aTransform.Operations().Length()); return transform; } static Maybe GetScrollTimelineOptions( dom::AnimationTimeline* aTimeline) { if (!aTimeline || !aTimeline->IsScrollTimeline()) { return Nothing(); } const dom::ScrollTimeline* timeline = aTimeline->AsScrollTimeline(); MOZ_ASSERT(timeline->IsActive(), "We send scroll animation to the compositor only if its timeline " "is active"); ScrollableLayerGuid::ViewID source = ScrollableLayerGuid::NULL_SCROLL_ID; DebugOnly success = nsLayoutUtils::FindIDFor(timeline->SourceElement(), &source); MOZ_ASSERT(success, "We should have a valid ViewID for the scroller"); return Some(ScrollTimelineOptions(source, timeline->Axis())); } // FIXME: Bug 1489392: We don't have to normalize the path here if we accept // the spec issue which would like to normalize svg paths at computed time. static StyleOffsetPath NormalizeOffsetPath(const StyleOffsetPath& aOffsetPath) { if (aOffsetPath.IsPath()) { return StyleOffsetPath::Path( MotionPathUtils::NormalizeSVGPathData(aOffsetPath.AsPath())); } return StyleOffsetPath(aOffsetPath); } static void SetAnimatable(nsCSSPropertyID aProperty, const AnimationValue& aAnimationValue, nsIFrame* aFrame, TransformReferenceBox& aRefBox, layers::Animatable& aAnimatable) { MOZ_ASSERT(aFrame); if (aAnimationValue.IsNull()) { aAnimatable = null_t(); return; } switch (aProperty) { case eCSSProperty_background_color: { // We don't support color animation on the compositor yet so that we can // resolve currentColor at this moment. nscolor foreground = aFrame->Style()->GetVisitedDependentColor(&nsStyleText::mColor); aAnimatable = aAnimationValue.GetColor(foreground); break; } case eCSSProperty_opacity: aAnimatable = aAnimationValue.GetOpacity(); break; case eCSSProperty_rotate: aAnimatable = aAnimationValue.GetRotateProperty(); break; case eCSSProperty_scale: aAnimatable = aAnimationValue.GetScaleProperty(); break; case eCSSProperty_translate: aAnimatable = ResolveTranslate(aAnimationValue.GetTranslateProperty(), aRefBox); break; case eCSSProperty_transform: aAnimatable = ResolveTransformOperations( aAnimationValue.GetTransformProperty(), aRefBox); break; case eCSSProperty_offset_path: aAnimatable = NormalizeOffsetPath(aAnimationValue.GetOffsetPathProperty()); break; case eCSSProperty_offset_distance: aAnimatable = aAnimationValue.GetOffsetDistanceProperty(); break; case eCSSProperty_offset_rotate: aAnimatable = aAnimationValue.GetOffsetRotateProperty(); break; case eCSSProperty_offset_anchor: aAnimatable = aAnimationValue.GetOffsetAnchorProperty(); break; default: MOZ_ASSERT_UNREACHABLE("Unsupported property"); } } void AnimationInfo::AddAnimationForProperty( nsIFrame* aFrame, const AnimationProperty& aProperty, dom::Animation* aAnimation, const Maybe& aTransformData, Send aSendFlag) { MOZ_ASSERT(aAnimation->GetEffect(), "Should not be adding an animation without an effect"); MOZ_ASSERT(!aAnimation->GetCurrentOrPendingStartTime().IsNull() || !aAnimation->IsPlaying() || (aAnimation->GetTimeline() && aAnimation->GetTimeline()->TracksWallclockTime()), "If the animation has an unresolved start time it should either" " be static (so we don't need a start time) or else have a" " timeline capable of converting TimeStamps (so we can calculate" " one later"); layers::Animation* animation = (aSendFlag == Send::NextTransaction) ? AddAnimationForNextTransaction() : AddAnimation(); const TimingParams& timing = aAnimation->GetEffect()->NormalizedTiming(); // If we are starting a new transition that replaces an existing transition // running on the compositor, it is possible that the animation on the // compositor will have advanced ahead of the main thread. If we use as // the starting point of the new transition, the current value of the // replaced transition as calculated on the main thread using the refresh // driver time, the new transition will jump when it starts. Instead, we // re-calculate the starting point of the new transition by applying the // current TimeStamp to the parameters of the replaced transition. // // We need to do this here, rather than when we generate the new transition, // since after generating the new transition other requestAnimationFrame // callbacks may run that introduce further lag between the main thread and // the compositor. dom::CSSTransition* cssTransition = aAnimation->AsCSSTransition(); if (cssTransition) { cssTransition->UpdateStartValueFromReplacedTransition(); } animation->originTime() = !aAnimation->GetTimeline() ? TimeStamp() : aAnimation->GetTimeline()->ToTimeStamp(TimeDuration()); dom::Nullable startTime = aAnimation->GetCurrentOrPendingStartTime(); if (startTime.IsNull()) { animation->startTime() = Nothing(); } else { animation->startTime() = Some(startTime.Value()); } animation->holdTime() = aAnimation->GetCurrentTimeAsDuration().Value(); const ComputedTiming computedTiming = aAnimation->GetEffect()->GetComputedTiming(); animation->delay() = timing.Delay(); animation->endDelay() = timing.EndDelay(); animation->duration() = computedTiming.mDuration; animation->iterations() = static_cast(computedTiming.mIterations); animation->iterationStart() = static_cast(computedTiming.mIterationStart); animation->direction() = static_cast(timing.Direction()); animation->fillMode() = static_cast(computedTiming.mFill); animation->property() = aProperty.mProperty; animation->playbackRate() = static_cast(aAnimation->CurrentOrPendingPlaybackRate()); animation->previousPlaybackRate() = aAnimation->HasPendingPlaybackRate() ? static_cast(aAnimation->PlaybackRate()) : std::numeric_limits::quiet_NaN(); animation->transformData() = aTransformData; animation->easingFunction() = timing.TimingFunction(); animation->iterationComposite() = static_cast( aAnimation->GetEffect()->AsKeyframeEffect()->IterationComposite()); animation->isNotPlaying() = !aAnimation->IsPlaying(); animation->isNotAnimating() = false; animation->scrollTimelineOptions() = GetScrollTimelineOptions(aAnimation->GetTimeline()); TransformReferenceBox refBox(aFrame); // If the animation is additive or accumulates, we need to pass its base value // to the compositor. AnimationValue baseStyle = aAnimation->GetEffect()->AsKeyframeEffect()->BaseStyle( aProperty.mProperty); if (!baseStyle.IsNull()) { SetAnimatable(aProperty.mProperty, baseStyle, aFrame, refBox, animation->baseStyle()); } else { animation->baseStyle() = null_t(); } for (uint32_t segIdx = 0; segIdx < aProperty.mSegments.Length(); segIdx++) { const AnimationPropertySegment& segment = aProperty.mSegments[segIdx]; AnimationSegment* animSegment = animation->segments().AppendElement(); SetAnimatable(aProperty.mProperty, segment.mFromValue, aFrame, refBox, animSegment->startState()); SetAnimatable(aProperty.mProperty, segment.mToValue, aFrame, refBox, animSegment->endState()); animSegment->startPortion() = segment.mFromKey; animSegment->endPortion() = segment.mToKey; animSegment->startComposite() = static_cast(segment.mFromComposite); animSegment->endComposite() = static_cast(segment.mToComposite); animSegment->sampleFn() = segment.mTimingFunction; } } // Let's use an example to explain this function: // // We have 4 playing animations (without any !important rule or transition): // Animation A: [ transform, rotate ]. // Animation B: [ rotate, scale ]. // Animation C: [ transform, margin-left ]. // Animation D: [ opacity, margin-left ]. // // Normally, GetAnimationsForCompositor(|transform-like properties|) returns: // [ Animation A, Animation B, Animation C ], which is the first argument of // this function. // // In this function, we want to re-organize the list as (Note: don't care // the order of properties): // [ // { rotate: [ Animation A, Animation B ] }, // { scale: [ Animation B ] }, // { transform: [ Animation A, Animation C ] }, // ] // // Therefore, AddAnimationsForProperty() will append each animation property // into AnimationInfo, as a final list of layers::Animation: // [ // { rotate: Animation A }, // { rotate: Animation B }, // { scale: Animation B }, // { transform: Animation A }, // { transform: Animation C }, // ] // // And then, for each transaction, we send this list to the compositor thread. static HashMap>> GroupAnimationsByProperty(const nsTArray>& aAnimations, const nsCSSPropertyIDSet& aPropertySet) { HashMap>> groupedAnims; for (const RefPtr& anim : aAnimations) { const dom::KeyframeEffect* effect = anim->GetEffect()->AsKeyframeEffect(); MOZ_ASSERT(effect); for (const AnimationProperty& property : effect->Properties()) { if (!aPropertySet.HasProperty(property.mProperty)) { continue; } auto animsForPropertyPtr = groupedAnims.lookupForAdd(property.mProperty); if (!animsForPropertyPtr) { DebugOnly rv = groupedAnims.add(animsForPropertyPtr, property.mProperty, nsTArray>()); MOZ_ASSERT(rv, "Should have enough memory"); } animsForPropertyPtr->value().AppendElement(anim); } } return groupedAnims; } bool AnimationInfo::AddAnimationsForProperty( nsIFrame* aFrame, const EffectSet* aEffects, const nsTArray>& aCompositorAnimations, const Maybe& aTransformData, nsCSSPropertyID aProperty, Send aSendFlag, WebRenderLayerManager* aLayerManager) { bool addedAny = false; // Add from first to last (since last overrides) for (dom::Animation* anim : aCompositorAnimations) { if (!anim->IsRelevant()) { continue; } dom::KeyframeEffect* keyframeEffect = anim->GetEffect() ? anim->GetEffect()->AsKeyframeEffect() : nullptr; MOZ_ASSERT(keyframeEffect, "A playing animation should have a keyframe effect"); const AnimationProperty* property = keyframeEffect->GetEffectiveAnimationOfProperty(aProperty, *aEffects); if (!property) { continue; } // Note that if the property is overridden by !important rules, // GetEffectiveAnimationOfProperty returns null instead. // This is what we want, since if we have animations overridden by // !important rules, we don't want to send them to the compositor. MOZ_ASSERT( anim->CascadeLevel() != EffectCompositor::CascadeLevel::Animations || !aEffects->PropertiesWithImportantRules().HasProperty(aProperty), "GetEffectiveAnimationOfProperty already tested the property " "is not overridden by !important rules"); // Don't add animations that are pending if their timeline does not // track wallclock time. This is because any pending animations on layers // will have their start time updated with the current wallclock time. // If we can't convert that wallclock time back to an equivalent timeline // time, we won't be able to update the content animation and it will end // up being out of sync with the layer animation. // // Currently this only happens when the timeline is driven by a refresh // driver under test control. In this case, the next time the refresh // driver is advanced it will trigger any pending animations. if (anim->Pending() && (anim->GetTimeline() && !anim->GetTimeline()->TracksWallclockTime())) { continue; } AddAnimationForProperty(aFrame, *property, anim, aTransformData, aSendFlag); keyframeEffect->SetIsRunningOnCompositor(aProperty, true); addedAny = true; if (aTransformData && aTransformData->partialPrerenderData() && aLayerManager) { aLayerManager->AddPartialPrerenderedAnimation(GetCompositorAnimationsId(), anim); } } return addedAny; } // Returns which pre-rendered area's sides are overflowed from the pre-rendered // rect. // // We don't need to make jank animations when we are going to composite the // area where there is no overflowed area even if it's outside of the // pre-rendered area. static SideBits GetOverflowedSides(const nsRect& aOverflow, const nsRect& aPartialPrerenderArea) { SideBits sides = SideBits::eNone; if (aOverflow.X() < aPartialPrerenderArea.X()) { sides |= SideBits::eLeft; } if (aOverflow.Y() < aPartialPrerenderArea.Y()) { sides |= SideBits::eTop; } if (aOverflow.XMost() > aPartialPrerenderArea.XMost()) { sides |= SideBits::eRight; } if (aOverflow.YMost() > aPartialPrerenderArea.YMost()) { sides |= SideBits::eBottom; } return sides; } static std::pair GetClipRectAndTransformForPartialPrerender( const nsIFrame* aFrame, int32_t aDevPixelsToAppUnits, const nsIFrame* aClipFrame, const nsIScrollableFrame* aScrollFrame) { MOZ_ASSERT(aClipFrame); gfx::Matrix4x4 transformInClip = nsLayoutUtils::GetTransformToAncestor(RelativeTo{aFrame->GetParent()}, RelativeTo{aClipFrame}) .GetMatrix(); if (aScrollFrame) { transformInClip.PostTranslate( LayoutDevicePoint::FromAppUnits(aScrollFrame->GetScrollPosition(), aDevPixelsToAppUnits) .ToUnknownPoint()); } // We don't necessarily use nsLayoutUtils::CalculateCompositionSizeForFrame // since this is a case where we don't use APZ at all. return std::make_pair( LayoutDeviceRect::FromAppUnits(aScrollFrame ? aScrollFrame->GetScrollPortRect() : aClipFrame->GetRectRelativeToSelf(), aDevPixelsToAppUnits) * LayoutDeviceToLayerScale2D() * LayerToParentLayerScale(), transformInClip); } static PartialPrerenderData GetPartialPrerenderData( const nsIFrame* aFrame, const nsDisplayItem* aItem) { const nsRect& partialPrerenderedRect = aItem->GetUntransformedPaintRect(); nsRect overflow = aFrame->InkOverflowRectRelativeToSelf(); ScrollableLayerGuid::ViewID scrollId = ScrollableLayerGuid::NULL_SCROLL_ID; const nsIFrame* clipFrame = nsLayoutUtils::GetNearestOverflowClipFrame(aFrame->GetParent()); const nsIScrollableFrame* scrollFrame = do_QueryFrame(clipFrame); if (!clipFrame) { // If there is no suitable clip frame in the same document, use the // root one. scrollFrame = aFrame->PresShell()->GetRootScrollFrameAsScrollable(); if (scrollFrame) { clipFrame = do_QueryFrame(scrollFrame); } else { // If there is no root scroll frame, use the viewport frame. clipFrame = aFrame->PresShell()->GetRootFrame(); } } // If the scroll frame is asyncronously scrollable, try to find the scroll id. if (scrollFrame && !scrollFrame->GetScrollStyles().IsHiddenInBothDirections() && nsLayoutUtils::AsyncPanZoomEnabled(aFrame)) { const bool isInPositionFixed = nsLayoutUtils::IsInPositionFixedSubtree(aFrame); const ActiveScrolledRoot* asr = aItem->GetActiveScrolledRoot(); const nsIFrame* asrScrollableFrame = asr ? do_QueryFrame(asr->mScrollableFrame) : nullptr; if (!isInPositionFixed && asr && aFrame->PresContext() == asrScrollableFrame->PresContext()) { scrollId = asr->GetViewId(); MOZ_ASSERT(clipFrame == asrScrollableFrame); } else { // Use the root scroll id in the same document if the target frame is in // position:fixed subtree or there is no ASR or the ASR is in a different // ancestor document. scrollId = nsLayoutUtils::ScrollIdForRootScrollFrame(aFrame->PresContext()); MOZ_ASSERT(clipFrame == aFrame->PresShell()->GetRootScrollFrame()); } } int32_t devPixelsToAppUnits = aFrame->PresContext()->AppUnitsPerDevPixel(); auto [clipRect, transformInClip] = GetClipRectAndTransformForPartialPrerender( aFrame, devPixelsToAppUnits, clipFrame, scrollFrame); return PartialPrerenderData{ LayoutDeviceRect::FromAppUnits(partialPrerenderedRect, devPixelsToAppUnits), GetOverflowedSides(overflow, partialPrerenderedRect), scrollId, clipRect, transformInClip, LayoutDevicePoint()}; // will be set by caller. } enum class AnimationDataType { WithMotionPath, WithoutMotionPath, }; static Maybe CreateAnimationData( nsIFrame* aFrame, nsDisplayItem* aItem, DisplayItemType aType, layers::LayersBackend aLayersBackend, AnimationDataType aDataType, const Maybe& aPosition) { if (aType != DisplayItemType::TYPE_TRANSFORM) { return Nothing(); } // XXX Performance here isn't ideal for SVG. We'd prefer to avoid resolving // the dimensions of refBox. That said, we only get here if there are CSS // animations or transitions on this element, and that is likely to be a // lot rarer than transforms on SVG (the frequency of which drives the need // for TransformReferenceBox). TransformReferenceBox refBox(aFrame); const nsRect bounds(0, 0, refBox.Width(), refBox.Height()); // all data passed directly to the compositor should be in dev pixels int32_t devPixelsToAppUnits = aFrame->PresContext()->AppUnitsPerDevPixel(); float scale = devPixelsToAppUnits; gfx::Point3D offsetToTransformOrigin = nsDisplayTransform::GetDeltaToTransformOrigin(aFrame, refBox, scale); nsPoint origin; if (aLayersBackend == layers::LayersBackend::LAYERS_WR) { // leave origin empty, because we are sending it separately on the // stacking context that we are pushing to WR, and WR will automatically // include it when picking up the animated transform values } else if (aItem) { // This branch is for display items to leverage the cache of // nsDisplayListBuilder. origin = aItem->ToReferenceFrame(); } else { // This branch is running for restyling. // Animations are animated at the coordination of the reference // frame outside, not the given frame itself. The given frame // is also reference frame too, so the parent's reference frame // are used. nsIFrame* referenceFrame = nsLayoutUtils::GetReferenceFrame( nsLayoutUtils::GetCrossDocParentFrameInProcess(aFrame)); origin = aFrame->GetOffsetToCrossDoc(referenceFrame); } Maybe motionPathData; if (aDataType == AnimationDataType::WithMotionPath) { const StyleTransformOrigin& styleOrigin = aFrame->StyleDisplay()->mTransformOrigin; CSSPoint motionPathOrigin = nsStyleTransformMatrix::Convert2DPosition( styleOrigin.horizontal, styleOrigin.vertical, refBox); CSSPoint anchorAdjustment = MotionPathUtils::ComputeAnchorPointAdjustment(*aFrame); motionPathData = Some(layers::MotionPathData( motionPathOrigin, anchorAdjustment, RayReferenceData(aFrame))); } Maybe partialPrerenderData; if (aItem && static_cast(aItem)->IsPartialPrerender()) { partialPrerenderData = Some(GetPartialPrerenderData(aFrame, aItem)); if (aLayersBackend == layers::LayersBackend::LAYERS_WR) { MOZ_ASSERT(aPosition); partialPrerenderData->position() = *aPosition; } } return Some(TransformData(origin, offsetToTransformOrigin, bounds, devPixelsToAppUnits, motionPathData, partialPrerenderData)); } void AnimationInfo::AddNonAnimatingTransformLikePropertiesStyles( const nsCSSPropertyIDSet& aNonAnimatingProperties, nsIFrame* aFrame, Send aSendFlag) { auto appendFakeAnimation = [this, aSendFlag](nsCSSPropertyID aProperty, Animatable&& aBaseStyle) { layers::Animation* animation = (aSendFlag == Send::NextTransaction) ? AddAnimationForNextTransaction() : AddAnimation(); animation->property() = aProperty; animation->baseStyle() = std::move(aBaseStyle); animation->easingFunction() = Nothing(); animation->isNotAnimating() = true; }; const nsStyleDisplay* display = aFrame->StyleDisplay(); // A simple optimization. We don't need to send offset-* properties if we // don't have offset-path and offset-position. // FIXME: Bug 1559232: Add offset-position here. bool hasMotion = !display->mOffsetPath.IsNone() || !aNonAnimatingProperties.HasProperty(eCSSProperty_offset_path); for (nsCSSPropertyID id : aNonAnimatingProperties) { switch (id) { case eCSSProperty_transform: if (!display->mTransform.IsNone()) { TransformReferenceBox refBox(aFrame); appendFakeAnimation( id, ResolveTransformOperations(display->mTransform, refBox)); } break; case eCSSProperty_translate: if (!display->mTranslate.IsNone()) { TransformReferenceBox refBox(aFrame); appendFakeAnimation(id, ResolveTranslate(display->mTranslate, refBox)); } break; case eCSSProperty_rotate: if (!display->mRotate.IsNone()) { appendFakeAnimation(id, display->mRotate); } break; case eCSSProperty_scale: if (!display->mScale.IsNone()) { appendFakeAnimation(id, display->mScale); } break; case eCSSProperty_offset_path: if (!display->mOffsetPath.IsNone()) { appendFakeAnimation(id, NormalizeOffsetPath(display->mOffsetPath)); } break; case eCSSProperty_offset_distance: if (hasMotion && !display->mOffsetDistance.IsDefinitelyZero()) { appendFakeAnimation(id, display->mOffsetDistance); } break; case eCSSProperty_offset_rotate: if (hasMotion && (!display->mOffsetRotate.auto_ || display->mOffsetRotate.angle.ToDegrees() != 0.0)) { appendFakeAnimation(id, display->mOffsetRotate); } break; case eCSSProperty_offset_anchor: if (hasMotion && !display->mOffsetAnchor.IsAuto()) { appendFakeAnimation(id, display->mOffsetAnchor); } break; default: MOZ_ASSERT_UNREACHABLE("Unsupported transform-like properties"); } } } void AnimationInfo::AddAnimationsForDisplayItem( nsIFrame* aFrame, nsDisplayListBuilder* aBuilder, nsDisplayItem* aItem, DisplayItemType aType, WebRenderLayerManager* aLayerManager, const Maybe& aPosition) { Send sendFlag = !aBuilder ? Send::NextTransaction : Send::Immediate; if (sendFlag == Send::NextTransaction) { ClearAnimationsForNextTransaction(); } else { ClearAnimations(); } // Update the animation generation on the layer. We need to do this before // any early returns since even if we don't add any animations to the // layer, we still need to mark it as up-to-date with regards to animations. // Otherwise, in RestyleManager we'll notice the discrepancy between the // animation generation numbers and update the layer indefinitely. EffectSet* effects = EffectSet::GetEffectSetForFrame(aFrame, aType); uint64_t animationGeneration = effects ? effects->GetAnimationGeneration() : 0; SetAnimationGeneration(animationGeneration); if (!effects || effects->IsEmpty()) { return; } EffectCompositor::ClearIsRunningOnCompositor(aFrame, aType); const nsCSSPropertyIDSet& propertySet = LayerAnimationInfo::GetCSSPropertiesFor(aType); const nsTArray> matchedAnimations = EffectCompositor::GetAnimationsForCompositor(aFrame, propertySet); if (matchedAnimations.IsEmpty()) { return; } // If the frame is not prerendered, bail out. // Do this check only during layer construction; during updating the // caller is required to check it appropriately. if (aItem && !aItem->CanUseAsyncAnimations(aBuilder)) { // EffectCompositor needs to know that we refused to run this animation // asynchronously so that it will not throttle the main thread // animation. aFrame->SetProperty(nsIFrame::RefusedAsyncAnimationProperty(), true); return; } const HashMap>> compositorAnimations = GroupAnimationsByProperty(matchedAnimations, propertySet); Maybe transformData = CreateAnimationData(aFrame, aItem, aType, aLayerManager->GetBackendType(), compositorAnimations.has(eCSSProperty_offset_path) || !aFrame->StyleDisplay()->mOffsetPath.IsNone() ? AnimationDataType::WithMotionPath : AnimationDataType::WithoutMotionPath, aPosition); // Bug 1424900: Drop this pref check after shipping individual transforms. // Bug 1582554: Drop this pref check after shipping motion path. const bool hasMultipleTransformLikeProperties = (StaticPrefs::layout_css_individual_transform_enabled() || StaticPrefs::layout_css_motion_path_enabled()) && aType == DisplayItemType::TYPE_TRANSFORM; nsCSSPropertyIDSet nonAnimatingProperties = nsCSSPropertyIDSet::TransformLikeProperties(); for (auto iter = compositorAnimations.iter(); !iter.done(); iter.next()) { // Note: We can skip offset-* if there is no offset-path/offset-position // animations and styles. However, it should be fine and may be better to // send these information to the compositor because 1) they are simple data // structure, 2) AddAnimationsForProperty() marks these animations as // running on the composiror, so CanThrottle() returns true for them, and // we avoid running these animations on the main thread. bool added = AddAnimationsForProperty(aFrame, effects, iter.get().value(), transformData, iter.get().key(), sendFlag, aLayerManager); if (added && transformData) { // Only copy TransformLikeMetaData in the first animation property. transformData.reset(); } if (hasMultipleTransformLikeProperties && added) { nonAnimatingProperties.RemoveProperty(iter.get().key()); } } // If some transform-like properties have animations, but others not, and // those non-animating transform-like properties have non-none // transform/translate/rotate/scale styles or non-initial value for motion // path properties, we also pass their styles into the compositor, so the // final transform matrix (on the compositor) could take them into account. if (hasMultipleTransformLikeProperties && // For these cases we don't need to send the property style values to // the compositor: // 1. No property has running animations on the compositor. (i.e. All // properties should be handled by main thread) // 2. All properties have running animations on the compositor. // (i.e. Those running animations should override the styles.) !nonAnimatingProperties.Equals( nsCSSPropertyIDSet::TransformLikeProperties()) && !nonAnimatingProperties.IsEmpty()) { AddNonAnimatingTransformLikePropertiesStyles(nonAnimatingProperties, aFrame, sendFlag); } } } // namespace layers } // namespace mozilla