1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "AnimationHelper.h"
8 #include "base/process_util.h"
9 #include "gfx2DGlue.h"                       // for ThebesRect
10 #include "gfxLineSegment.h"                  // for gfxLineSegment
11 #include "gfxPoint.h"                        // for gfxPoint
12 #include "gfxQuad.h"                         // for gfxQuad
13 #include "gfxRect.h"                         // for gfxRect
14 #include "gfxUtils.h"                        // for gfxUtils::TransformToQuad
15 #include "mozilla/ComputedTimingFunction.h"  // for ComputedTimingFunction
16 #include "mozilla/dom/AnimationEffectBinding.h"  // for dom::FillMode
17 #include "mozilla/dom/KeyframeEffectBinding.h"   // for dom::IterationComposite
18 #include "mozilla/dom/KeyframeEffect.h"       // for dom::KeyFrameEffectReadOnly
19 #include "mozilla/dom/Nullable.h"             // for dom::Nullable
20 #include "mozilla/layers/CompositorThread.h"  // for CompositorThreadHolder
21 #include "mozilla/layers/CompositorAnimationStorage.h"  // for CompositorAnimationStorage
22 #include "mozilla/layers/LayerAnimationUtils.h"  // for TimingFunctionToComputedTimingFunction
23 #include "mozilla/LayerAnimationInfo.h"  // for GetCSSPropertiesFor()
24 #include "mozilla/MotionPathUtils.h"     // for ResolveMotionPath()
25 #include "mozilla/ServoBindings.h"  // for Servo_ComposeAnimationSegment, etc
26 #include "mozilla/StyleAnimationValue.h"  // for StyleAnimationValue, etc
27 #include "nsDeviceContext.h"              // for AppUnitsPerCSSPixel
28 #include "nsDisplayList.h"                // for nsDisplayTransform, etc
29 
30 namespace mozilla {
31 namespace layers {
32 
33 enum class CanSkipCompose {
34   IfPossible,
35   No,
36 };
SampleAnimationForProperty(TimeStamp aPreviousFrameTime,TimeStamp aCurrentFrameTime,const AnimatedValue * aPreviousValue,CanSkipCompose aCanSkipCompose,nsTArray<PropertyAnimation> & aPropertyAnimations,RefPtr<RawServoAnimationValue> & aAnimationValue)37 static AnimationHelper::SampleResult SampleAnimationForProperty(
38     TimeStamp aPreviousFrameTime, TimeStamp aCurrentFrameTime,
39     const AnimatedValue* aPreviousValue, CanSkipCompose aCanSkipCompose,
40     nsTArray<PropertyAnimation>& aPropertyAnimations,
41     RefPtr<RawServoAnimationValue>& aAnimationValue) {
42   MOZ_ASSERT(!aPropertyAnimations.IsEmpty(), "Should have animations");
43   bool hasInEffectAnimations = false;
44 #ifdef DEBUG
45   // In cases where this function returns a SampleResult::Skipped, we actually
46   // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the
47   // call site that the value that would have been computed matches the stored
48   // value that we end up using. This flag is used to ensure we populate
49   // aAnimationValue in this scenario.
50   bool shouldBeSkipped = false;
51 #endif
52   // Process in order, since later animations override earlier ones.
53   for (PropertyAnimation& animation : aPropertyAnimations) {
54     MOZ_ASSERT(
55         (!animation.mOriginTime.IsNull() && animation.mStartTime.isSome()) ||
56             animation.mIsNotPlaying,
57         "If we are playing, we should have an origin time and a start time");
58 
59     // Determine if the animation was play-pending and used a ready time later
60     // than the previous frame time.
61     //
62     // To determine this, _all_ of the following conditions need to hold:
63     //
64     // * There was no previous animation value (i.e. this is the first frame for
65     //   the animation since it was sent to the compositor), and
66     // * The animation is playing, and
67     // * There is a previous frame time, and
68     // * The ready time of the animation is ahead of the previous frame time.
69     //
70     bool hasFutureReadyTime = false;
71     if (!aPreviousValue && !animation.mIsNotPlaying &&
72         !aPreviousFrameTime.IsNull()) {
73       // This is the inverse of the calculation performed in
74       // AnimationInfo::StartPendingAnimations to calculate the start time of
75       // play-pending animations.
76       // Note that we have to calculate (TimeStamp + TimeDuration) last to avoid
77       // underflow in the middle of the calulation.
78       const TimeStamp readyTime =
79           animation.mOriginTime +
80           (animation.mStartTime.ref() +
81            animation.mHoldTime.MultDouble(1.0 / animation.mPlaybackRate));
82       hasFutureReadyTime =
83           !readyTime.IsNull() && readyTime > aPreviousFrameTime;
84     }
85     // Use the previous vsync time to make main thread animations and compositor
86     // more closely aligned.
87     //
88     // On the first frame where we have animations the previous timestamp will
89     // not be set so we simply use the current timestamp.  As a result we will
90     // end up painting the first frame twice.  That doesn't appear to be
91     // noticeable, however.
92     //
93     // Likewise, if the animation is play-pending, it may have a ready time that
94     // is *after* |aPreviousFrameTime| (but *before* |aCurrentFrameTime|).
95     // To avoid flicker we need to use |aCurrentFrameTime| to avoid temporarily
96     // jumping backwards into the range prior to when the animation starts.
97     const TimeStamp& timeStamp =
98         aPreviousFrameTime.IsNull() || hasFutureReadyTime ? aCurrentFrameTime
99                                                           : aPreviousFrameTime;
100 
101     // If the animation is not currently playing, e.g. paused or
102     // finished, then use the hold time to stay at the same position.
103     TimeDuration elapsedDuration =
104         animation.mIsNotPlaying || animation.mStartTime.isNothing()
105             ? animation.mHoldTime
106             : (timeStamp - animation.mOriginTime - animation.mStartTime.ref())
107                   .MultDouble(animation.mPlaybackRate);
108 
109     ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt(
110         dom::Nullable<TimeDuration>(elapsedDuration), animation.mTiming,
111         animation.mPlaybackRate);
112 
113     if (computedTiming.mProgress.IsNull()) {
114       continue;
115     }
116 
117     dom::IterationCompositeOperation iterCompositeOperation =
118         animation.mIterationComposite;
119 
120     // Skip calculation if the progress hasn't changed since the last
121     // calculation.
122     // Note that we don't skip calculate this animation if there is another
123     // animation since the other animation might be 'accumulate' or 'add', or
124     // might have a missing keyframe (i.e. this animation value will be used in
125     // the missing keyframe).
126     // FIXME Bug 1455476: We should do this optimizations for the case where
127     // the layer has multiple animations and multiple properties.
128     if (aCanSkipCompose == CanSkipCompose::IfPossible &&
129         !dom::KeyframeEffect::HasComputedTimingChanged(
130             computedTiming, iterCompositeOperation,
131             animation.mProgressOnLastCompose,
132             animation.mCurrentIterationOnLastCompose)) {
133 #ifdef DEBUG
134       shouldBeSkipped = true;
135 #else
136       return AnimationHelper::SampleResult::Skipped;
137 #endif
138     }
139 
140     uint32_t segmentIndex = 0;
141     size_t segmentSize = animation.mSegments.Length();
142     PropertyAnimation::SegmentData* segment = animation.mSegments.Elements();
143     while (segment->mEndPortion < computedTiming.mProgress.Value() &&
144            segmentIndex < segmentSize - 1) {
145       ++segment;
146       ++segmentIndex;
147     }
148 
149     double positionInSegment =
150         (computedTiming.mProgress.Value() - segment->mStartPortion) /
151         (segment->mEndPortion - segment->mStartPortion);
152 
153     double portion = ComputedTimingFunction::GetPortion(
154         segment->mFunction, positionInSegment, computedTiming.mBeforeFlag);
155 
156     // Like above optimization, skip calculation if the target segment isn't
157     // changed and if the portion in the segment isn't changed.
158     // This optimization is needed for CSS animations/transitions with step
159     // timing functions (e.g. the throbber animation on tabs or frame based
160     // animations).
161     // FIXME Bug 1455476: Like the above optimization, we should apply this
162     // optimizations for multiple animation cases and multiple properties as
163     // well.
164     if (aCanSkipCompose == CanSkipCompose::IfPossible &&
165         animation.mSegmentIndexOnLastCompose == segmentIndex &&
166         !animation.mPortionInSegmentOnLastCompose.IsNull() &&
167         animation.mPortionInSegmentOnLastCompose.Value() == portion) {
168 #ifdef DEBUG
169       shouldBeSkipped = true;
170 #else
171       return AnimationHelper::SampleResult::Skipped;
172 #endif
173     }
174 
175     AnimationPropertySegment animSegment;
176     animSegment.mFromKey = 0.0;
177     animSegment.mToKey = 1.0;
178     animSegment.mFromValue = AnimationValue(segment->mStartValue);
179     animSegment.mToValue = AnimationValue(segment->mEndValue);
180     animSegment.mFromComposite = segment->mStartComposite;
181     animSegment.mToComposite = segment->mEndComposite;
182 
183     // interpolate the property
184     aAnimationValue =
185         Servo_ComposeAnimationSegment(
186             &animSegment, aAnimationValue,
187             animation.mSegments.LastElement().mEndValue, iterCompositeOperation,
188             portion, computedTiming.mCurrentIteration)
189             .Consume();
190 
191 #ifdef DEBUG
192     if (shouldBeSkipped) {
193       return AnimationHelper::SampleResult::Skipped;
194     }
195 #endif
196 
197     hasInEffectAnimations = true;
198     animation.mProgressOnLastCompose = computedTiming.mProgress;
199     animation.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
200     animation.mSegmentIndexOnLastCompose = segmentIndex;
201     animation.mPortionInSegmentOnLastCompose.SetValue(portion);
202   }
203 
204   return hasInEffectAnimations ? AnimationHelper::SampleResult::Sampled
205                                : AnimationHelper::SampleResult::None;
206 }
207 
SampleAnimationForEachNode(TimeStamp aPreviousFrameTime,TimeStamp aCurrentFrameTime,const AnimatedValue * aPreviousValue,nsTArray<PropertyAnimationGroup> & aPropertyAnimationGroups,nsTArray<RefPtr<RawServoAnimationValue>> & aAnimationValues)208 AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode(
209     TimeStamp aPreviousFrameTime, TimeStamp aCurrentFrameTime,
210     const AnimatedValue* aPreviousValue,
211     nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
212     nsTArray<RefPtr<RawServoAnimationValue>>& aAnimationValues /* out */) {
213   MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(),
214              "Should be called with animation data");
215   MOZ_ASSERT(aAnimationValues.IsEmpty(),
216              "Should be called with empty aAnimationValues");
217 
218   nsTArray<RefPtr<RawServoAnimationValue>> nonAnimatingValues;
219   for (PropertyAnimationGroup& group : aPropertyAnimationGroups) {
220     // Initialize animation value with base style.
221     RefPtr<RawServoAnimationValue> currValue = group.mBaseStyle;
222 
223     CanSkipCompose canSkipCompose =
224         aPreviousValue && aPropertyAnimationGroups.Length() == 1 &&
225                 group.mAnimations.Length() == 1
226             ? CanSkipCompose::IfPossible
227             : CanSkipCompose::No;
228 
229     MOZ_ASSERT(
230         !group.mAnimations.IsEmpty() ||
231             nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
232                 group.mProperty),
233         "Only transform-like properties can have empty PropertyAnimation list");
234 
235     // For properties which are not animating (i.e. their values are always the
236     // same), we store them in a different array, and then merge them into the
237     // final result (a.k.a. aAnimationValues) because we shouldn't take them
238     // into account for SampleResult. (In other words, these properties
239     // shouldn't affect the optimization.)
240     if (group.mAnimations.IsEmpty()) {
241       nonAnimatingValues.AppendElement(std::move(currValue));
242       continue;
243     }
244 
245     SampleResult result = SampleAnimationForProperty(
246         aPreviousFrameTime, aCurrentFrameTime, aPreviousValue, canSkipCompose,
247         group.mAnimations, currValue);
248 
249     // FIXME: Bug 1455476: Do optimization for multiple properties. For now,
250     // the result is skipped only if the property count == 1.
251     if (result == SampleResult::Skipped) {
252 #ifdef DEBUG
253       aAnimationValues.AppendElement(std::move(currValue));
254 #endif
255       return SampleResult::Skipped;
256     }
257 
258     if (result != SampleResult::Sampled) {
259       continue;
260     }
261 
262     // Insert the interpolation result into the output array.
263     MOZ_ASSERT(currValue);
264     aAnimationValues.AppendElement(std::move(currValue));
265   }
266 
267   SampleResult rv =
268       aAnimationValues.IsEmpty() ? SampleResult::None : SampleResult::Sampled;
269   if (rv == SampleResult::Sampled) {
270     aAnimationValues.AppendElements(std::move(nonAnimatingValues));
271   }
272   return rv;
273 }
274 
GetAdjustedFillMode(const Animation & aAnimation)275 static dom::FillMode GetAdjustedFillMode(const Animation& aAnimation) {
276   // Adjust fill mode so that if the main thread is delayed in clearing
277   // this animation we don't introduce flicker by jumping back to the old
278   // underlying value.
279   auto fillMode = static_cast<dom::FillMode>(aAnimation.fillMode());
280   float playbackRate = aAnimation.playbackRate();
281   switch (fillMode) {
282     case dom::FillMode::None:
283       if (playbackRate > 0) {
284         fillMode = dom::FillMode::Forwards;
285       } else if (playbackRate < 0) {
286         fillMode = dom::FillMode::Backwards;
287       }
288       break;
289     case dom::FillMode::Backwards:
290       if (playbackRate > 0) {
291         fillMode = dom::FillMode::Both;
292       }
293       break;
294     case dom::FillMode::Forwards:
295       if (playbackRate < 0) {
296         fillMode = dom::FillMode::Both;
297       }
298       break;
299     default:
300       break;
301   }
302   return fillMode;
303 }
304 
305 #ifdef DEBUG
HasTransformLikeAnimations(const AnimationArray & aAnimations)306 static bool HasTransformLikeAnimations(const AnimationArray& aAnimations) {
307   nsCSSPropertyIDSet transformSet =
308       nsCSSPropertyIDSet::TransformLikeProperties();
309 
310   for (const Animation& animation : aAnimations) {
311     if (animation.isNotAnimating()) {
312       continue;
313     }
314 
315     if (transformSet.HasProperty(animation.property())) {
316       return true;
317     }
318   }
319 
320   return false;
321 }
322 #endif
323 
ExtractAnimations(const LayersId & aLayersId,const AnimationArray & aAnimations)324 AnimationStorageData AnimationHelper::ExtractAnimations(
325     const LayersId& aLayersId, const AnimationArray& aAnimations) {
326   AnimationStorageData storageData;
327   storageData.mLayersId = aLayersId;
328 
329   nsCSSPropertyID prevID = eCSSProperty_UNKNOWN;
330   PropertyAnimationGroup* currData = nullptr;
331   DebugOnly<const layers::Animatable*> currBaseStyle = nullptr;
332 
333   for (const Animation& animation : aAnimations) {
334     // Animations with same property are grouped together, so we can just
335     // check if the current property is the same as the previous one for
336     // knowing this is a new group.
337     if (prevID != animation.property()) {
338       // Got a different group, we should create a different array.
339       currData = storageData.mAnimation.AppendElement();
340       currData->mProperty = animation.property();
341       if (animation.transformData()) {
342         MOZ_ASSERT(!storageData.mTransformData,
343                    "Only one entry has TransformData");
344         storageData.mTransformData = animation.transformData();
345       }
346 
347       prevID = animation.property();
348 
349       // Reset the debug pointer.
350       currBaseStyle = nullptr;
351     }
352 
353     MOZ_ASSERT(currData);
354     if (animation.baseStyle().type() != Animatable::Tnull_t) {
355       MOZ_ASSERT(!currBaseStyle || *currBaseStyle == animation.baseStyle(),
356                  "Should be the same base style");
357 
358       currData->mBaseStyle = AnimationValue::FromAnimatable(
359           animation.property(), animation.baseStyle());
360       currBaseStyle = &animation.baseStyle();
361     }
362 
363     // If this layers::Animation sets isNotAnimating to true, it only has
364     // base style and doesn't have any animation information, so we can skip
365     // the rest steps. (And so its PropertyAnimationGroup::mAnimation will be
366     // an empty array.)
367     if (animation.isNotAnimating()) {
368       MOZ_ASSERT(nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
369                      animation.property()),
370                  "Only transform-like properties could set this true");
371 
372       if (animation.property() == eCSSProperty_offset_path) {
373         MOZ_ASSERT(currData->mBaseStyle,
374                    "Fixed offset-path should have base style");
375         MOZ_ASSERT(HasTransformLikeAnimations(aAnimations));
376 
377         AnimationValue value{currData->mBaseStyle};
378         const StyleOffsetPath& offsetPath = value.GetOffsetPathProperty();
379         if (offsetPath.IsPath()) {
380           MOZ_ASSERT(!storageData.mCachedMotionPath,
381                      "Only one offset-path: path() is set");
382 
383           RefPtr<gfx::PathBuilder> builder =
384               MotionPathUtils::GetCompositorPathBuilder();
385           storageData.mCachedMotionPath =
386               MotionPathUtils::BuildPath(offsetPath.AsPath(), builder);
387         }
388       }
389 
390       continue;
391     }
392 
393     PropertyAnimation* propertyAnimation =
394         currData->mAnimations.AppendElement();
395 
396     propertyAnimation->mOriginTime = animation.originTime();
397     propertyAnimation->mStartTime = animation.startTime();
398     propertyAnimation->mHoldTime = animation.holdTime();
399     propertyAnimation->mPlaybackRate = animation.playbackRate();
400     propertyAnimation->mIterationComposite =
401         static_cast<dom::IterationCompositeOperation>(
402             animation.iterationComposite());
403     propertyAnimation->mIsNotPlaying = animation.isNotPlaying();
404     propertyAnimation->mTiming =
405         TimingParams{animation.duration(),
406                      animation.delay(),
407                      animation.endDelay(),
408                      animation.iterations(),
409                      animation.iterationStart(),
410                      static_cast<dom::PlaybackDirection>(animation.direction()),
411                      GetAdjustedFillMode(animation),
412                      AnimationUtils::TimingFunctionToComputedTimingFunction(
413                          animation.easingFunction())};
414 
415     nsTArray<PropertyAnimation::SegmentData>& segmentData =
416         propertyAnimation->mSegments;
417     for (const AnimationSegment& segment : animation.segments()) {
418       segmentData.AppendElement(PropertyAnimation::SegmentData{
419           AnimationValue::FromAnimatable(animation.property(),
420                                          segment.startState()),
421           AnimationValue::FromAnimatable(animation.property(),
422                                          segment.endState()),
423           AnimationUtils::TimingFunctionToComputedTimingFunction(
424               segment.sampleFn()),
425           segment.startPortion(), segment.endPortion(),
426           static_cast<dom::CompositeOperation>(segment.startComposite()),
427           static_cast<dom::CompositeOperation>(segment.endComposite())});
428     }
429   }
430 
431 #ifdef DEBUG
432   // Sanity check that the grouped animation data is correct by looking at the
433   // property set.
434   if (!storageData.mAnimation.IsEmpty()) {
435     nsCSSPropertyIDSet seenProperties;
436     for (const auto& group : storageData.mAnimation) {
437       nsCSSPropertyID id = group.mProperty;
438 
439       MOZ_ASSERT(!seenProperties.HasProperty(id), "Should be a new property");
440       seenProperties.AddProperty(id);
441     }
442 
443     MOZ_ASSERT(
444         seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
445             DisplayItemType::TYPE_TRANSFORM)) ||
446             seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
447                 DisplayItemType::TYPE_OPACITY)) ||
448             seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
449                 DisplayItemType::TYPE_BACKGROUND_COLOR)),
450         "The property set of output should be the subset of transform-like "
451         "properties, opacity, or background_color.");
452 
453     if (seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
454             DisplayItemType::TYPE_TRANSFORM))) {
455       MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
456     }
457 
458     if (seenProperties.HasProperty(eCSSProperty_offset_path)) {
459       MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
460       MOZ_ASSERT(storageData.mTransformData->motionPathData(),
461                  "Should have MotionPathData");
462     }
463   }
464 #endif
465 
466   return storageData;
467 }
468 
GetNextCompositorAnimationsId()469 uint64_t AnimationHelper::GetNextCompositorAnimationsId() {
470   static uint32_t sNextId = 0;
471   ++sNextId;
472 
473   uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId());
474   uint64_t nextId = procId;
475   nextId = nextId << 32 | sNextId;
476   return nextId;
477 }
478 
ServoAnimationValueToMatrix4x4(const nsTArray<RefPtr<RawServoAnimationValue>> & aValues,const TransformData & aTransformData,gfx::Path * aCachedMotionPath)479 gfx::Matrix4x4 AnimationHelper::ServoAnimationValueToMatrix4x4(
480     const nsTArray<RefPtr<RawServoAnimationValue>>& aValues,
481     const TransformData& aTransformData, gfx::Path* aCachedMotionPath) {
482   using nsStyleTransformMatrix::TransformReferenceBox;
483 
484   // This is a bit silly just to avoid the transform list copy from the
485   // animation transform list.
486   auto noneTranslate = StyleTranslate::None();
487   auto noneRotate = StyleRotate::None();
488   auto noneScale = StyleScale::None();
489   const StyleTransform noneTransform;
490 
491   const StyleTranslate* translate = nullptr;
492   const StyleRotate* rotate = nullptr;
493   const StyleScale* scale = nullptr;
494   const StyleTransform* transform = nullptr;
495   const StyleOffsetPath* path = nullptr;
496   const StyleLengthPercentage* distance = nullptr;
497   const StyleOffsetRotate* offsetRotate = nullptr;
498   const StylePositionOrAuto* anchor = nullptr;
499 
500   for (const auto& value : aValues) {
501     MOZ_ASSERT(value);
502     nsCSSPropertyID id = Servo_AnimationValue_GetPropertyId(value);
503     switch (id) {
504       case eCSSProperty_transform:
505         MOZ_ASSERT(!transform);
506         transform = Servo_AnimationValue_GetTransform(value);
507         break;
508       case eCSSProperty_translate:
509         MOZ_ASSERT(!translate);
510         translate = Servo_AnimationValue_GetTranslate(value);
511         break;
512       case eCSSProperty_rotate:
513         MOZ_ASSERT(!rotate);
514         rotate = Servo_AnimationValue_GetRotate(value);
515         break;
516       case eCSSProperty_scale:
517         MOZ_ASSERT(!scale);
518         scale = Servo_AnimationValue_GetScale(value);
519         break;
520       case eCSSProperty_offset_path:
521         MOZ_ASSERT(!path);
522         path = Servo_AnimationValue_GetOffsetPath(value);
523         break;
524       case eCSSProperty_offset_distance:
525         MOZ_ASSERT(!distance);
526         distance = Servo_AnimationValue_GetOffsetDistance(value);
527         break;
528       case eCSSProperty_offset_rotate:
529         MOZ_ASSERT(!offsetRotate);
530         offsetRotate = Servo_AnimationValue_GetOffsetRotate(value);
531         break;
532       case eCSSProperty_offset_anchor:
533         MOZ_ASSERT(!anchor);
534         anchor = Servo_AnimationValue_GetOffsetAnchor(value);
535         break;
536       default:
537         MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property");
538     }
539   }
540 
541   TransformReferenceBox refBox(nullptr, aTransformData.bounds());
542   Maybe<ResolvedMotionPathData> motion = MotionPathUtils::ResolveMotionPath(
543       path, distance, offsetRotate, anchor, aTransformData.motionPathData(),
544       refBox, aCachedMotionPath);
545 
546   // We expect all our transform data to arrive in device pixels
547   gfx::Point3D transformOrigin = aTransformData.transformOrigin();
548   nsDisplayTransform::FrameTransformProperties props(
549       translate ? *translate : noneTranslate, rotate ? *rotate : noneRotate,
550       scale ? *scale : noneScale, transform ? *transform : noneTransform,
551       motion, transformOrigin);
552 
553   return nsDisplayTransform::GetResultingTransformMatrix(
554       props, refBox, aTransformData.appUnitsPerDevPixel());
555 }
556 
CollectOverflowedSideLines(const gfxQuad & aPrerenderedQuad,SideBits aOverflowSides,gfxLineSegment sideLines[4])557 static uint8_t CollectOverflowedSideLines(const gfxQuad& aPrerenderedQuad,
558                                           SideBits aOverflowSides,
559                                           gfxLineSegment sideLines[4]) {
560   uint8_t count = 0;
561 
562   if (aOverflowSides & SideBits::eTop) {
563     sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[0],
564                                       aPrerenderedQuad.mPoints[1]);
565     count++;
566   }
567   if (aOverflowSides & SideBits::eRight) {
568     sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[1],
569                                       aPrerenderedQuad.mPoints[2]);
570     count++;
571   }
572   if (aOverflowSides & SideBits::eBottom) {
573     sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[2],
574                                       aPrerenderedQuad.mPoints[3]);
575     count++;
576   }
577   if (aOverflowSides & SideBits::eLeft) {
578     sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[3],
579                                       aPrerenderedQuad.mPoints[0]);
580     count++;
581   }
582 
583   return count;
584 }
585 
586 enum RegionBits : uint8_t {
587   Inside = 0,
588   Left = (1 << 0),
589   Right = (1 << 1),
590   Bottom = (1 << 2),
591   Top = (1 << 3),
592 };
593 
594 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RegionBits);
595 
GetRegionBitsForPoint(double aX,double aY,const gfxRect & aClip)596 static RegionBits GetRegionBitsForPoint(double aX, double aY,
597                                         const gfxRect& aClip) {
598   RegionBits result = RegionBits::Inside;
599   if (aX < aClip.X()) {
600     result |= RegionBits::Left;
601   } else if (aX > aClip.XMost()) {
602     result |= RegionBits::Right;
603   }
604 
605   if (aY < aClip.Y()) {
606     result |= RegionBits::Bottom;
607   } else if (aY > aClip.YMost()) {
608     result |= RegionBits::Top;
609   }
610   return result;
611 };
612 
613 // https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
LineSegmentIntersectsClip(double aX0,double aY0,double aX1,double aY1,const gfxRect & aClip)614 static bool LineSegmentIntersectsClip(double aX0, double aY0, double aX1,
615                                       double aY1, const gfxRect& aClip) {
616   RegionBits b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
617   RegionBits b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
618 
619   while (true) {
620     if (!(b0 | b1)) {
621       // Completely inside.
622       return true;
623     }
624 
625     if (b0 & b1) {
626       // Completely outside.
627       return false;
628     }
629 
630     double x, y;
631     // Choose an outside point.
632     RegionBits outsidePointBits = b1 > b0 ? b1 : b0;
633     if (outsidePointBits & RegionBits::Top) {
634       x = aX0 + (aX1 - aX0) * (aClip.YMost() - aY0) / (aY1 - aY0);
635       y = aClip.YMost();
636     } else if (outsidePointBits & RegionBits::Bottom) {
637       x = aX0 + (aX1 - aX0) * (aClip.Y() - aY0) / (aY1 - aY0);
638       y = aClip.Y();
639     } else if (outsidePointBits & RegionBits::Right) {
640       y = aY0 + (aY1 - aY0) * (aClip.XMost() - aX0) / (aX1 - aX0);
641       x = aClip.XMost();
642     } else if (outsidePointBits & RegionBits::Left) {
643       y = aY0 + (aY1 - aY0) * (aClip.X() - aX0) / (aX1 - aX0);
644       x = aClip.X();
645     }
646 
647     if (outsidePointBits == b0) {
648       aX0 = x;
649       aY0 = y;
650       b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
651     } else {
652       aX1 = x;
653       aY1 = y;
654       b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
655     }
656   }
657   MOZ_ASSERT_UNREACHABLE();
658   return false;
659 }
660 
661 // static
ShouldBeJank(const LayoutDeviceRect & aPrerenderedRect,SideBits aOverflowSides,const gfx::Matrix4x4 & aTransform,const ParentLayerRect & aClipRect)662 bool AnimationHelper::ShouldBeJank(const LayoutDeviceRect& aPrerenderedRect,
663                                    SideBits aOverflowSides,
664                                    const gfx::Matrix4x4& aTransform,
665                                    const ParentLayerRect& aClipRect) {
666   if (aClipRect.IsEmpty()) {
667     return false;
668   }
669 
670   gfxQuad prerenderedQuad = gfxUtils::TransformToQuad(
671       ThebesRect(aPrerenderedRect.ToUnknownRect()), aTransform);
672 
673   gfxLineSegment sideLines[4];
674   uint8_t overflowSideCount =
675       CollectOverflowedSideLines(prerenderedQuad, aOverflowSides, sideLines);
676 
677   gfxRect clipRect = ThebesRect(aClipRect.ToUnknownRect());
678   for (uint8_t j = 0; j < overflowSideCount; j++) {
679     if (LineSegmentIntersectsClip(sideLines[j].mStart.x, sideLines[j].mStart.y,
680                                   sideLines[j].mEnd.x, sideLines[j].mEnd.y,
681                                   clipRect)) {
682       return true;
683     }
684   }
685 
686   // With step timing functions there are cases the transform jumps to a
687   // position where the partial pre-render area is totally outside of the clip
688   // rect without any intersection of the partial pre-render area and the clip
689   // rect happened in previous compositions but there remains visible area of
690   // the entire transformed area.
691   //
692   // So now all four points of the transformed partial pre-render rect are
693   // outside of the clip rect, if all these four points are in either side of
694   // the clip rect, we consider it's jank so that on the main-thread we will
695   // either a) rebuild the up-to-date display item if there remains visible area
696   // or b) no longer rebuild the display item if it's totally outside of the
697   // clip rect.
698   //
699   // Note that RegionBits::Left and Right are mutually exclusive,
700   // RegionBits::Top and Bottom are also mutually exclusive, so if there remains
701   // any bits, it means all four points are in the same side.
702   return GetRegionBitsForPoint(prerenderedQuad.mPoints[0].x,
703                                prerenderedQuad.mPoints[0].y, clipRect) &
704          GetRegionBitsForPoint(prerenderedQuad.mPoints[1].x,
705                                prerenderedQuad.mPoints[1].y, clipRect) &
706          GetRegionBitsForPoint(prerenderedQuad.mPoints[2].x,
707                                prerenderedQuad.mPoints[2].y, clipRect) &
708          GetRegionBitsForPoint(prerenderedQuad.mPoints[3].x,
709                                prerenderedQuad.mPoints[3].y, clipRect);
710 }
711 
712 }  // namespace layers
713 }  // namespace mozilla
714