diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /gfx/layers/AnimationHelper.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/layers/AnimationHelper.cpp')
-rw-r--r-- | gfx/layers/AnimationHelper.cpp | 836 |
1 files changed, 836 insertions, 0 deletions
diff --git a/gfx/layers/AnimationHelper.cpp b/gfx/layers/AnimationHelper.cpp new file mode 100644 index 0000000000..e72889e4d4 --- /dev/null +++ b/gfx/layers/AnimationHelper.cpp @@ -0,0 +1,836 @@ +/* -*- 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 "AnimationHelper.h" +#include "base/process_util.h" +#include "gfx2DGlue.h" // for ThebesRect +#include "gfxLineSegment.h" // for gfxLineSegment +#include "gfxPoint.h" // for gfxPoint +#include "gfxQuad.h" // for gfxQuad +#include "gfxRect.h" // for gfxRect +#include "gfxUtils.h" // for gfxUtils::TransformToQuad +#include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction +#include "mozilla/dom/AnimationEffectBinding.h" // for dom::FillMode +#include "mozilla/dom/KeyframeEffectBinding.h" // for dom::IterationComposite +#include "mozilla/dom/KeyframeEffect.h" // for dom::KeyFrameEffectReadOnly +#include "mozilla/dom/Nullable.h" // for dom::Nullable +#include "mozilla/layers/APZSampler.h" // for APZSampler +#include "mozilla/layers/CompositorThread.h" // for CompositorThreadHolder +#include "mozilla/LayerAnimationInfo.h" // for GetCSSPropertiesFor() +#include "mozilla/MotionPathUtils.h" // for ResolveMotionPath() +#include "mozilla/ServoBindings.h" // for Servo_ComposeAnimationSegment, etc +#include "mozilla/StyleAnimationValue.h" // for StyleAnimationValue, etc +#include "nsDeviceContext.h" // for AppUnitsPerCSSPixel +#include "nsDisplayList.h" // for nsDisplayTransform, etc + +namespace mozilla { +namespace layers { + +static dom::Nullable<TimeDuration> CalculateElapsedTimeForScrollTimeline( + const Maybe<APZSampler::ScrollOffsetAndRange> aScrollMeta, + const ScrollTimelineOptions& aOptions, const StickyTimeDuration& aEndTime, + const TimeDuration& aStartTime, float aPlaybackRate) { + // We return Nothing If the associated APZ controller is not available + // (because it may be destroyed but this animation is still alive). + if (!aScrollMeta) { + // This may happen after we reload a page. There may be a race condition + // because the animation is still alive but the APZ is destroyed. In this + // case, this animation is invalid, so we return nullptr. + return nullptr; + } + + const bool isHorizontal = + aOptions.axis() == layers::ScrollDirection::eHorizontal; + double range = + isHorizontal ? aScrollMeta->mRange.width : aScrollMeta->mRange.height; + MOZ_ASSERT( + range > 0, + "We don't expect to get a zero or negative range on the compositor"); + + // The offset may be negative if the writing mode is from right to left. + // Use std::abs() here to avoid getting a negative progress. + double position = + std::abs(isHorizontal ? aScrollMeta->mOffset.x : aScrollMeta->mOffset.y); + double progress = position / range; + // Just in case to avoid getting a progress more than 100%, for overscrolling. + progress = std::min(progress, 1.0); + auto timelineTime = TimeDuration(aEndTime.MultDouble(progress)); + return dom::Animation::CurrentTimeFromTimelineTime(timelineTime, aStartTime, + aPlaybackRate); +} + +static dom::Nullable<TimeDuration> CalculateElapsedTime( + const APZSampler* aAPZSampler, const LayersId& aLayersId, + const MutexAutoLock& aProofOfMapLock, const PropertyAnimation& aAnimation, + const TimeStamp aPreviousFrameTime, const TimeStamp aCurrentFrameTime, + const AnimatedValue* aPreviousValue) { + // ------------------------------------- + // Case 1: scroll-timeline animations. + // ------------------------------------- + if (aAnimation.mScrollTimelineOptions) { + MOZ_ASSERT( + aAPZSampler, + "We don't send scroll animations to the compositor if APZ is disabled"); + + return CalculateElapsedTimeForScrollTimeline( + aAPZSampler->GetCurrentScrollOffsetAndRange( + aLayersId, aAnimation.mScrollTimelineOptions.value().source(), + aProofOfMapLock), + aAnimation.mScrollTimelineOptions.value(), aAnimation.mTiming.EndTime(), + aAnimation.mStartTime.refOr(aAnimation.mHoldTime), + aAnimation.mPlaybackRate); + } + + // ------------------------------------- + // Case 2: document-timeline animations. + // ------------------------------------- + MOZ_ASSERT( + (!aAnimation.mOriginTime.IsNull() && aAnimation.mStartTime.isSome()) || + aAnimation.mIsNotPlaying, + "If we are playing, we should have an origin time and a start time"); + + // Determine if the animation was play-pending and used a ready time later + // than the previous frame time. + // + // To determine this, _all_ of the following conditions need to hold: + // + // * There was no previous animation value (i.e. this is the first frame for + // the animation since it was sent to the compositor), and + // * The animation is playing, and + // * There is a previous frame time, and + // * The ready time of the animation is ahead of the previous frame time. + // + bool hasFutureReadyTime = false; + if (!aPreviousValue && !aAnimation.mIsNotPlaying && + !aPreviousFrameTime.IsNull()) { + // This is the inverse of the calculation performed in + // AnimationInfo::StartPendingAnimations to calculate the start time of + // play-pending animations. + // Note that we have to calculate (TimeStamp + TimeDuration) last to avoid + // underflow in the middle of the calulation. + const TimeStamp readyTime = + aAnimation.mOriginTime + + (aAnimation.mStartTime.ref() + + aAnimation.mHoldTime.MultDouble(1.0 / aAnimation.mPlaybackRate)); + hasFutureReadyTime = !readyTime.IsNull() && readyTime > aPreviousFrameTime; + } + // Use the previous vsync time to make main thread animations and compositor + // more closely aligned. + // + // On the first frame where we have animations the previous timestamp will + // not be set so we simply use the current timestamp. As a result we will + // end up painting the first frame twice. That doesn't appear to be + // noticeable, however. + // + // Likewise, if the animation is play-pending, it may have a ready time that + // is *after* |aPreviousFrameTime| (but *before* |aCurrentFrameTime|). + // To avoid flicker we need to use |aCurrentFrameTime| to avoid temporarily + // jumping backwards into the range prior to when the animation starts. + const TimeStamp& timeStamp = aPreviousFrameTime.IsNull() || hasFutureReadyTime + ? aCurrentFrameTime + : aPreviousFrameTime; + + // If the animation is not currently playing, e.g. paused or + // finished, then use the hold time to stay at the same position. + TimeDuration elapsedDuration = + aAnimation.mIsNotPlaying || aAnimation.mStartTime.isNothing() + ? aAnimation.mHoldTime + : (timeStamp - aAnimation.mOriginTime - aAnimation.mStartTime.ref()) + .MultDouble(aAnimation.mPlaybackRate); + return elapsedDuration; +} + +enum class CanSkipCompose { + IfPossible, + No, +}; +// This function samples the animation for a specific property. We may have +// multiple animations for a single property, and the later animations override +// the eariler ones. This function returns the sampled animation value, +// |aAnimationValue| for a single CSS property. +static AnimationHelper::SampleResult SampleAnimationForProperty( + const APZSampler* aAPZSampler, const LayersId& aLayersId, + const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime, + TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue, + CanSkipCompose aCanSkipCompose, + nsTArray<PropertyAnimation>& aPropertyAnimations, + RefPtr<StyleAnimationValue>& aAnimationValue) { + MOZ_ASSERT(!aPropertyAnimations.IsEmpty(), "Should have animations"); + + auto reason = AnimationHelper::SampleResult::Reason::None; + bool hasInEffectAnimations = false; +#ifdef DEBUG + // In cases where this function returns a SampleResult::Skipped, we actually + // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the + // call site that the value that would have been computed matches the stored + // value that we end up using. This flag is used to ensure we populate + // aAnimationValue in this scenario. + bool shouldBeSkipped = false; +#endif + // Process in order, since later animations override earlier ones. + for (PropertyAnimation& animation : aPropertyAnimations) { + dom::Nullable<TimeDuration> elapsedDuration = CalculateElapsedTime( + aAPZSampler, aLayersId, aProofOfMapLock, animation, aPreviousFrameTime, + aCurrentFrameTime, aPreviousValue); + + const auto progressTimelinePosition = + animation.mScrollTimelineOptions + ? dom::Animation::AtProgressTimelineBoundary( + TimeDuration::FromMilliseconds( + PROGRESS_TIMELINE_DURATION_MILLISEC), + elapsedDuration, animation.mStartTime.refOr(TimeDuration()), + animation.mPlaybackRate) + : dom::Animation::ProgressTimelinePosition::NotBoundary; + + ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt( + elapsedDuration, animation.mTiming, animation.mPlaybackRate, + progressTimelinePosition); + + if (computedTiming.mProgress.IsNull()) { + // For the scroll-driven animations, it's possible to let it go between + // the active phase and the before/after phase, and so its progress + // becomes null. In this case, we shouldn't just skip this animation. + // Instead, we have to reset the previous sampled result. Basically, we + // use |mProgressOnLastCompose| to check if it goes from the active phase. + // If so, we set the returned |mReason| to ScrollToDelayPhase to let the + // caller know we need to use the base style for this property. + // + // If there are any other animations which need to be sampled together + // (in the same property animation group), this |reason| will be ignored. + if (animation.mScrollTimelineOptions && + !animation.mProgressOnLastCompose.IsNull() && + (computedTiming.mPhase == ComputedTiming::AnimationPhase::Before || + computedTiming.mPhase == ComputedTiming::AnimationPhase::After)) { + // Appearally, we go back to delay, so need to reset the last + // composition meta data. This is necessary because + // 1. this animation is in delay so it shouldn't have any composition + // meta data, and + // 2. we will not go into this condition multiple times during delay + // phase because we rely on |mProgressOnLastCompose|. + animation.ResetLastCompositionValues(); + reason = AnimationHelper::SampleResult::Reason::ScrollToDelayPhase; + } + continue; + } + + dom::IterationCompositeOperation iterCompositeOperation = + animation.mIterationComposite; + + // Skip calculation if the progress hasn't changed since the last + // calculation. + // Note that we don't skip calculate this animation if there is another + // animation since the other animation might be 'accumulate' or 'add', or + // might have a missing keyframe (i.e. this animation value will be used in + // the missing keyframe). + // FIXME Bug 1455476: We should do this optimizations for the case where + // the layer has multiple animations and multiple properties. + if (aCanSkipCompose == CanSkipCompose::IfPossible && + !dom::KeyframeEffect::HasComputedTimingChanged( + computedTiming, iterCompositeOperation, + animation.mProgressOnLastCompose, + animation.mCurrentIterationOnLastCompose)) { +#ifdef DEBUG + shouldBeSkipped = true; +#else + return AnimationHelper::SampleResult::Skipped(); +#endif + } + + uint32_t segmentIndex = 0; + size_t segmentSize = animation.mSegments.Length(); + PropertyAnimation::SegmentData* segment = animation.mSegments.Elements(); + while (segment->mEndPortion < computedTiming.mProgress.Value() && + segmentIndex < segmentSize - 1) { + ++segment; + ++segmentIndex; + } + + double positionInSegment = + (computedTiming.mProgress.Value() - segment->mStartPortion) / + (segment->mEndPortion - segment->mStartPortion); + + double portion = StyleComputedTimingFunction::GetPortion( + segment->mFunction, positionInSegment, computedTiming.mBeforeFlag); + + // Like above optimization, skip calculation if the target segment isn't + // changed and if the portion in the segment isn't changed. + // This optimization is needed for CSS animations/transitions with step + // timing functions (e.g. the throbber animation on tabs or frame based + // animations). + // FIXME Bug 1455476: Like the above optimization, we should apply this + // optimizations for multiple animation cases and multiple properties as + // well. + if (aCanSkipCompose == CanSkipCompose::IfPossible && + animation.mSegmentIndexOnLastCompose == segmentIndex && + !animation.mPortionInSegmentOnLastCompose.IsNull() && + animation.mPortionInSegmentOnLastCompose.Value() == portion) { +#ifdef DEBUG + shouldBeSkipped = true; +#else + return AnimationHelper::SampleResult::Skipped(); +#endif + } + + AnimationPropertySegment animSegment; + animSegment.mFromKey = 0.0; + animSegment.mToKey = 1.0; + animSegment.mFromValue = AnimationValue(segment->mStartValue); + animSegment.mToValue = AnimationValue(segment->mEndValue); + animSegment.mFromComposite = segment->mStartComposite; + animSegment.mToComposite = segment->mEndComposite; + + // interpolate the property + aAnimationValue = + Servo_ComposeAnimationSegment( + &animSegment, aAnimationValue, + animation.mSegments.LastElement().mEndValue, iterCompositeOperation, + portion, computedTiming.mCurrentIteration) + .Consume(); + +#ifdef DEBUG + if (shouldBeSkipped) { + return AnimationHelper::SampleResult::Skipped(); + } +#endif + + hasInEffectAnimations = true; + animation.mProgressOnLastCompose = computedTiming.mProgress; + animation.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration; + animation.mSegmentIndexOnLastCompose = segmentIndex; + animation.mPortionInSegmentOnLastCompose.SetValue(portion); + } + + auto rv = hasInEffectAnimations ? AnimationHelper::SampleResult::Sampled() + : AnimationHelper::SampleResult(); + rv.mReason = reason; + return rv; +} + +// This function samples the animations for a group of CSS properties. We may +// have multiple CSS properties in a group (e.g. transform-like properties). +// So the returned animation array, |aAnimationValues|, include all the +// animation values of these CSS properties. +AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode( + const APZSampler* aAPZSampler, const LayersId& aLayersId, + const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime, + TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue, + nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups, + nsTArray<RefPtr<StyleAnimationValue>>& aAnimationValues /* out */) { + MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(), + "Should be called with animation data"); + MOZ_ASSERT(aAnimationValues.IsEmpty(), + "Should be called with empty aAnimationValues"); + + nsTArray<RefPtr<StyleAnimationValue>> baseStyleOfDelayAnimations; + nsTArray<RefPtr<StyleAnimationValue>> nonAnimatingValues; + for (PropertyAnimationGroup& group : aPropertyAnimationGroups) { + // Initialize animation value with base style. + RefPtr<StyleAnimationValue> currValue = group.mBaseStyle; + + CanSkipCompose canSkipCompose = + aPreviousValue && aPropertyAnimationGroups.Length() == 1 && + group.mAnimations.Length() == 1 + ? CanSkipCompose::IfPossible + : CanSkipCompose::No; + + MOZ_ASSERT( + !group.mAnimations.IsEmpty() || + nsCSSPropertyIDSet::TransformLikeProperties().HasProperty( + group.mProperty), + "Only transform-like properties can have empty PropertyAnimation list"); + + // For properties which are not animating (i.e. their values are always the + // same), we store them in a different array, and then merge them into the + // final result (a.k.a. aAnimationValues) because we shouldn't take them + // into account for SampleResult. (In other words, these properties + // shouldn't affect the optimization.) + if (group.mAnimations.IsEmpty()) { + nonAnimatingValues.AppendElement(std::move(currValue)); + continue; + } + + SampleResult result = SampleAnimationForProperty( + aAPZSampler, aLayersId, aProofOfMapLock, aPreviousFrameTime, + aCurrentFrameTime, aPreviousValue, canSkipCompose, group.mAnimations, + currValue); + + // FIXME: Bug 1455476: Do optimization for multiple properties. For now, + // the result is skipped only if the property count == 1. + if (result.IsSkipped()) { +#ifdef DEBUG + aAnimationValues.AppendElement(std::move(currValue)); +#endif + return result; + } + + if (!result.IsSampled()) { + if (result.mReason == SampleResult::Reason::ScrollToDelayPhase) { + MOZ_ASSERT(currValue && currValue == group.mBaseStyle); + baseStyleOfDelayAnimations.AppendElement(std::move(currValue)); + } + continue; + } + + // Insert the interpolation result into the output array. + MOZ_ASSERT(currValue); + aAnimationValues.AppendElement(std::move(currValue)); + } + + SampleResult rv = + aAnimationValues.IsEmpty() ? SampleResult() : SampleResult::Sampled(); + + // If there is no other sampled result, we may store these base styles + // (together with the non-animating values) to the webrenderer before it gets + // sync with the main thread. + if (rv.IsNone() && !baseStyleOfDelayAnimations.IsEmpty()) { + aAnimationValues.AppendElements(std::move(baseStyleOfDelayAnimations)); + rv.mReason = SampleResult::Reason::ScrollToDelayPhase; + } + + if (!aAnimationValues.IsEmpty()) { + aAnimationValues.AppendElements(std::move(nonAnimatingValues)); + } + return rv; +} + +static dom::FillMode GetAdjustedFillMode(const Animation& aAnimation) { + // Adjust fill mode so that if the main thread is delayed in clearing + // this animation we don't introduce flicker by jumping back to the old + // underlying value. + auto fillMode = static_cast<dom::FillMode>(aAnimation.fillMode()); + float playbackRate = aAnimation.playbackRate(); + switch (fillMode) { + case dom::FillMode::None: + if (playbackRate > 0) { + fillMode = dom::FillMode::Forwards; + } else if (playbackRate < 0) { + fillMode = dom::FillMode::Backwards; + } + break; + case dom::FillMode::Backwards: + if (playbackRate > 0) { + fillMode = dom::FillMode::Both; + } + break; + case dom::FillMode::Forwards: + if (playbackRate < 0) { + fillMode = dom::FillMode::Both; + } + break; + default: + break; + } + return fillMode; +} + +#ifdef DEBUG +static bool HasTransformLikeAnimations(const AnimationArray& aAnimations) { + nsCSSPropertyIDSet transformSet = + nsCSSPropertyIDSet::TransformLikeProperties(); + + for (const Animation& animation : aAnimations) { + if (animation.isNotAnimating()) { + continue; + } + + if (transformSet.HasProperty(animation.property())) { + return true; + } + } + + return false; +} +#endif + +AnimationStorageData AnimationHelper::ExtractAnimations( + const LayersId& aLayersId, const AnimationArray& aAnimations) { + AnimationStorageData storageData; + storageData.mLayersId = aLayersId; + + nsCSSPropertyID prevID = eCSSProperty_UNKNOWN; + PropertyAnimationGroup* currData = nullptr; + DebugOnly<const layers::Animatable*> currBaseStyle = nullptr; + + for (const Animation& animation : aAnimations) { + // Animations with same property are grouped together, so we can just + // check if the current property is the same as the previous one for + // knowing this is a new group. + if (prevID != animation.property()) { + // Got a different group, we should create a different array. + currData = storageData.mAnimation.AppendElement(); + currData->mProperty = animation.property(); + if (animation.transformData()) { + MOZ_ASSERT(!storageData.mTransformData, + "Only one entry has TransformData"); + storageData.mTransformData = animation.transformData(); + } + + prevID = animation.property(); + + // Reset the debug pointer. + currBaseStyle = nullptr; + } + + MOZ_ASSERT(currData); + if (animation.baseStyle().type() != Animatable::Tnull_t) { + MOZ_ASSERT(!currBaseStyle || *currBaseStyle == animation.baseStyle(), + "Should be the same base style"); + + currData->mBaseStyle = AnimationValue::FromAnimatable( + animation.property(), animation.baseStyle()); + currBaseStyle = &animation.baseStyle(); + } + + // If this layers::Animation sets isNotAnimating to true, it only has + // base style and doesn't have any animation information, so we can skip + // the rest steps. (And so its PropertyAnimationGroup::mAnimation will be + // an empty array.) + if (animation.isNotAnimating()) { + MOZ_ASSERT(nsCSSPropertyIDSet::TransformLikeProperties().HasProperty( + animation.property()), + "Only transform-like properties could set this true"); + + if (animation.property() == eCSSProperty_offset_path) { + MOZ_ASSERT(currData->mBaseStyle, + "Fixed offset-path should have base style"); + MOZ_ASSERT(HasTransformLikeAnimations(aAnimations)); + + AnimationValue value{currData->mBaseStyle}; + const StyleOffsetPath& offsetPath = value.GetOffsetPathProperty(); + if (offsetPath.IsPath()) { + MOZ_ASSERT(!storageData.mCachedMotionPath, + "Only one offset-path: path() is set"); + + RefPtr<gfx::PathBuilder> builder = + MotionPathUtils::GetCompositorPathBuilder(); + storageData.mCachedMotionPath = + MotionPathUtils::BuildPath(offsetPath.AsPath(), builder); + } + } + + continue; + } + + PropertyAnimation* propertyAnimation = + currData->mAnimations.AppendElement(); + + propertyAnimation->mOriginTime = animation.originTime(); + propertyAnimation->mStartTime = animation.startTime(); + propertyAnimation->mHoldTime = animation.holdTime(); + propertyAnimation->mPlaybackRate = animation.playbackRate(); + propertyAnimation->mIterationComposite = + static_cast<dom::IterationCompositeOperation>( + animation.iterationComposite()); + propertyAnimation->mIsNotPlaying = animation.isNotPlaying(); + propertyAnimation->mTiming = + TimingParams{animation.duration(), + animation.delay(), + animation.endDelay(), + animation.iterations(), + animation.iterationStart(), + static_cast<dom::PlaybackDirection>(animation.direction()), + GetAdjustedFillMode(animation), + animation.easingFunction()}; + propertyAnimation->mScrollTimelineOptions = + animation.scrollTimelineOptions(); + + nsTArray<PropertyAnimation::SegmentData>& segmentData = + propertyAnimation->mSegments; + for (const AnimationSegment& segment : animation.segments()) { + segmentData.AppendElement(PropertyAnimation::SegmentData{ + AnimationValue::FromAnimatable(animation.property(), + segment.startState()), + AnimationValue::FromAnimatable(animation.property(), + segment.endState()), + segment.sampleFn(), segment.startPortion(), segment.endPortion(), + static_cast<dom::CompositeOperation>(segment.startComposite()), + static_cast<dom::CompositeOperation>(segment.endComposite())}); + } + } + +#ifdef DEBUG + // Sanity check that the grouped animation data is correct by looking at the + // property set. + if (!storageData.mAnimation.IsEmpty()) { + nsCSSPropertyIDSet seenProperties; + for (const auto& group : storageData.mAnimation) { + nsCSSPropertyID id = group.mProperty; + + MOZ_ASSERT(!seenProperties.HasProperty(id), "Should be a new property"); + seenProperties.AddProperty(id); + } + + MOZ_ASSERT( + seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor( + DisplayItemType::TYPE_TRANSFORM)) || + seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor( + DisplayItemType::TYPE_OPACITY)) || + seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor( + DisplayItemType::TYPE_BACKGROUND_COLOR)), + "The property set of output should be the subset of transform-like " + "properties, opacity, or background_color."); + + if (seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor( + DisplayItemType::TYPE_TRANSFORM))) { + MOZ_ASSERT(storageData.mTransformData, "Should have TransformData"); + } + + if (seenProperties.HasProperty(eCSSProperty_offset_path)) { + MOZ_ASSERT(storageData.mTransformData, "Should have TransformData"); + MOZ_ASSERT(storageData.mTransformData->motionPathData(), + "Should have MotionPathData"); + } + } +#endif + + return storageData; +} + +uint64_t AnimationHelper::GetNextCompositorAnimationsId() { + static uint32_t sNextId = 0; + ++sNextId; + + uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId()); + uint64_t nextId = procId; + nextId = nextId << 32 | sNextId; + return nextId; +} + +gfx::Matrix4x4 AnimationHelper::ServoAnimationValueToMatrix4x4( + const nsTArray<RefPtr<StyleAnimationValue>>& aValues, + const TransformData& aTransformData, gfx::Path* aCachedMotionPath) { + using nsStyleTransformMatrix::TransformReferenceBox; + + // This is a bit silly just to avoid the transform list copy from the + // animation transform list. + auto noneTranslate = StyleTranslate::None(); + auto noneRotate = StyleRotate::None(); + auto noneScale = StyleScale::None(); + const StyleTransform noneTransform; + + const StyleTranslate* translate = nullptr; + const StyleRotate* rotate = nullptr; + const StyleScale* scale = nullptr; + const StyleTransform* transform = nullptr; + const StyleOffsetPath* path = nullptr; + const StyleLengthPercentage* distance = nullptr; + const StyleOffsetRotate* offsetRotate = nullptr; + const StylePositionOrAuto* anchor = nullptr; + + for (const auto& value : aValues) { + MOZ_ASSERT(value); + nsCSSPropertyID id = Servo_AnimationValue_GetPropertyId(value); + switch (id) { + case eCSSProperty_transform: + MOZ_ASSERT(!transform); + transform = Servo_AnimationValue_GetTransform(value); + break; + case eCSSProperty_translate: + MOZ_ASSERT(!translate); + translate = Servo_AnimationValue_GetTranslate(value); + break; + case eCSSProperty_rotate: + MOZ_ASSERT(!rotate); + rotate = Servo_AnimationValue_GetRotate(value); + break; + case eCSSProperty_scale: + MOZ_ASSERT(!scale); + scale = Servo_AnimationValue_GetScale(value); + break; + case eCSSProperty_offset_path: + MOZ_ASSERT(!path); + path = Servo_AnimationValue_GetOffsetPath(value); + break; + case eCSSProperty_offset_distance: + MOZ_ASSERT(!distance); + distance = Servo_AnimationValue_GetOffsetDistance(value); + break; + case eCSSProperty_offset_rotate: + MOZ_ASSERT(!offsetRotate); + offsetRotate = Servo_AnimationValue_GetOffsetRotate(value); + break; + case eCSSProperty_offset_anchor: + MOZ_ASSERT(!anchor); + anchor = Servo_AnimationValue_GetOffsetAnchor(value); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property"); + } + } + + TransformReferenceBox refBox(nullptr, aTransformData.bounds()); + Maybe<ResolvedMotionPathData> motion = MotionPathUtils::ResolveMotionPath( + path, distance, offsetRotate, anchor, aTransformData.motionPathData(), + refBox, aCachedMotionPath); + + // We expect all our transform data to arrive in device pixels + gfx::Point3D transformOrigin = aTransformData.transformOrigin(); + nsDisplayTransform::FrameTransformProperties props( + translate ? *translate : noneTranslate, rotate ? *rotate : noneRotate, + scale ? *scale : noneScale, transform ? *transform : noneTransform, + motion, transformOrigin); + + return nsDisplayTransform::GetResultingTransformMatrix( + props, refBox, aTransformData.appUnitsPerDevPixel()); +} + +static uint8_t CollectOverflowedSideLines(const gfxQuad& aPrerenderedQuad, + SideBits aOverflowSides, + gfxLineSegment sideLines[4]) { + uint8_t count = 0; + + if (aOverflowSides & SideBits::eTop) { + sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[0], + aPrerenderedQuad.mPoints[1]); + count++; + } + if (aOverflowSides & SideBits::eRight) { + sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[1], + aPrerenderedQuad.mPoints[2]); + count++; + } + if (aOverflowSides & SideBits::eBottom) { + sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[2], + aPrerenderedQuad.mPoints[3]); + count++; + } + if (aOverflowSides & SideBits::eLeft) { + sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[3], + aPrerenderedQuad.mPoints[0]); + count++; + } + + return count; +} + +enum RegionBits : uint8_t { + Inside = 0, + Left = (1 << 0), + Right = (1 << 1), + Bottom = (1 << 2), + Top = (1 << 3), +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RegionBits); + +static RegionBits GetRegionBitsForPoint(double aX, double aY, + const gfxRect& aClip) { + RegionBits result = RegionBits::Inside; + if (aX < aClip.X()) { + result |= RegionBits::Left; + } else if (aX > aClip.XMost()) { + result |= RegionBits::Right; + } + + if (aY < aClip.Y()) { + result |= RegionBits::Bottom; + } else if (aY > aClip.YMost()) { + result |= RegionBits::Top; + } + return result; +}; + +// https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm +static bool LineSegmentIntersectsClip(double aX0, double aY0, double aX1, + double aY1, const gfxRect& aClip) { + RegionBits b0 = GetRegionBitsForPoint(aX0, aY0, aClip); + RegionBits b1 = GetRegionBitsForPoint(aX1, aY1, aClip); + + while (true) { + if (!(b0 | b1)) { + // Completely inside. + return true; + } + + if (b0 & b1) { + // Completely outside. + return false; + } + + double x, y; + // Choose an outside point. + RegionBits outsidePointBits = b1 > b0 ? b1 : b0; + if (outsidePointBits & RegionBits::Top) { + x = aX0 + (aX1 - aX0) * (aClip.YMost() - aY0) / (aY1 - aY0); + y = aClip.YMost(); + } else if (outsidePointBits & RegionBits::Bottom) { + x = aX0 + (aX1 - aX0) * (aClip.Y() - aY0) / (aY1 - aY0); + y = aClip.Y(); + } else if (outsidePointBits & RegionBits::Right) { + y = aY0 + (aY1 - aY0) * (aClip.XMost() - aX0) / (aX1 - aX0); + x = aClip.XMost(); + } else if (outsidePointBits & RegionBits::Left) { + y = aY0 + (aY1 - aY0) * (aClip.X() - aX0) / (aX1 - aX0); + x = aClip.X(); + } + + if (outsidePointBits == b0) { + aX0 = x; + aY0 = y; + b0 = GetRegionBitsForPoint(aX0, aY0, aClip); + } else { + aX1 = x; + aY1 = y; + b1 = GetRegionBitsForPoint(aX1, aY1, aClip); + } + } + MOZ_ASSERT_UNREACHABLE(); + return false; +} + +// static +bool AnimationHelper::ShouldBeJank(const LayoutDeviceRect& aPrerenderedRect, + SideBits aOverflowSides, + const gfx::Matrix4x4& aTransform, + const ParentLayerRect& aClipRect) { + if (aClipRect.IsEmpty()) { + return false; + } + + gfxQuad prerenderedQuad = gfxUtils::TransformToQuad( + ThebesRect(aPrerenderedRect.ToUnknownRect()), aTransform); + + gfxLineSegment sideLines[4]; + uint8_t overflowSideCount = + CollectOverflowedSideLines(prerenderedQuad, aOverflowSides, sideLines); + + gfxRect clipRect = ThebesRect(aClipRect.ToUnknownRect()); + for (uint8_t j = 0; j < overflowSideCount; j++) { + if (LineSegmentIntersectsClip(sideLines[j].mStart.x, sideLines[j].mStart.y, + sideLines[j].mEnd.x, sideLines[j].mEnd.y, + clipRect)) { + return true; + } + } + + // With step timing functions there are cases the transform jumps to a + // position where the partial pre-render area is totally outside of the clip + // rect without any intersection of the partial pre-render area and the clip + // rect happened in previous compositions but there remains visible area of + // the entire transformed area. + // + // So now all four points of the transformed partial pre-render rect are + // outside of the clip rect, if all these four points are in either side of + // the clip rect, we consider it's jank so that on the main-thread we will + // either a) rebuild the up-to-date display item if there remains visible area + // or b) no longer rebuild the display item if it's totally outside of the + // clip rect. + // + // Note that RegionBits::Left and Right are mutually exclusive, + // RegionBits::Top and Bottom are also mutually exclusive, so if there remains + // any bits, it means all four points are in the same side. + return GetRegionBitsForPoint(prerenderedQuad.mPoints[0].x, + prerenderedQuad.mPoints[0].y, clipRect) & + GetRegionBitsForPoint(prerenderedQuad.mPoints[1].x, + prerenderedQuad.mPoints[1].y, clipRect) & + GetRegionBitsForPoint(prerenderedQuad.mPoints[2].x, + prerenderedQuad.mPoints[2].y, clipRect) & + GetRegionBitsForPoint(prerenderedQuad.mPoints[3].x, + prerenderedQuad.mPoints[3].y, clipRect); +} + +} // namespace layers +} // namespace mozilla |