Backed out 2 changesets (bug 1931901) for causing build bustages in TelemetryScalar...
[gecko.git] / gfx / layers / AnimationHelper.cpp
blobe30cf5764ee881de5b2589b2d51e11e4af70d76f
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/. */
7 #include "AnimationHelper.h"
8 #include "CompositorAnimationStorage.h"
9 #include "base/process_util.h"
10 #include "gfx2DGlue.h" // for ThebesRect
11 #include "gfxLineSegment.h" // for gfxLineSegment
12 #include "gfxPoint.h" // for gfxPoint
13 #include "gfxQuad.h" // for gfxQuad
14 #include "gfxRect.h" // for gfxRect
15 #include "gfxUtils.h" // for gfxUtils::TransformToQuad
16 #include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction
17 #include "mozilla/dom/AnimationEffectBinding.h" // for dom::FillMode
18 #include "mozilla/dom/KeyframeEffectBinding.h" // for dom::IterationComposite
19 #include "mozilla/dom/KeyframeEffect.h" // for dom::KeyFrameEffectReadOnly
20 #include "mozilla/dom/Nullable.h" // for dom::Nullable
21 #include "mozilla/layers/APZSampler.h" // for APZSampler
22 #include "mozilla/AnimatedPropertyID.h"
23 #include "mozilla/LayerAnimationInfo.h" // for GetCSSPropertiesFor()
24 #include "mozilla/Maybe.h" // for Maybe<>
25 #include "mozilla/MotionPathUtils.h" // for ResolveMotionPath()
26 #include "mozilla/StyleAnimationValue.h" // for StyleAnimationValue, etc
27 #include "nsCSSPropertyID.h" // for eCSSProperty_offset_path, etc
28 #include "nsDisplayList.h" // for nsDisplayTransform, etc
30 namespace mozilla::layers {
32 static dom::Nullable<TimeDuration> CalculateElapsedTimeForScrollTimeline(
33 const Maybe<APZSampler::ScrollOffsetAndRange> aScrollMeta,
34 const ScrollTimelineOptions& aOptions, const StickyTimeDuration& aEndTime,
35 const TimeDuration& aStartTime, float aPlaybackRate) {
36 // We return Nothing If the associated APZ controller is not available
37 // (because it may be destroyed but this animation is still alive).
38 if (!aScrollMeta) {
39 // This may happen after we reload a page. There may be a race condition
40 // because the animation is still alive but the APZ is destroyed. In this
41 // case, this animation is invalid, so we return nullptr.
42 return nullptr;
45 const bool isHorizontal =
46 aOptions.axis() == layers::ScrollDirection::eHorizontal;
47 double range =
48 isHorizontal ? aScrollMeta->mRange.width : aScrollMeta->mRange.height;
49 MOZ_ASSERT(
50 range > 0,
51 "We don't expect to get a zero or negative range on the compositor");
53 // The offset may be negative if the writing mode is from right to left.
54 // Use std::abs() here to avoid getting a negative progress.
55 double position =
56 std::abs(isHorizontal ? aScrollMeta->mOffset.x : aScrollMeta->mOffset.y);
57 double progress = position / range;
58 // Just in case to avoid getting a progress more than 100%, for overscrolling.
59 progress = std::min(progress, 1.0);
60 auto timelineTime = TimeDuration(aEndTime.MultDouble(progress));
61 return dom::Animation::CurrentTimeFromTimelineTime(timelineTime, aStartTime,
62 aPlaybackRate);
65 static dom::Nullable<TimeDuration> CalculateElapsedTime(
66 const APZSampler* aAPZSampler, const LayersId& aLayersId,
67 const MutexAutoLock& aProofOfMapLock, const PropertyAnimation& aAnimation,
68 const TimeStamp aPreviousFrameTime, const TimeStamp aCurrentFrameTime,
69 const AnimatedValue* aPreviousValue) {
70 // -------------------------------------
71 // Case 1: scroll-timeline animations.
72 // -------------------------------------
73 if (aAnimation.mScrollTimelineOptions) {
74 MOZ_ASSERT(
75 aAPZSampler,
76 "We don't send scroll animations to the compositor if APZ is disabled");
78 return CalculateElapsedTimeForScrollTimeline(
79 aAPZSampler->GetCurrentScrollOffsetAndRange(
80 aLayersId, aAnimation.mScrollTimelineOptions.value().source(),
81 aProofOfMapLock),
82 aAnimation.mScrollTimelineOptions.value(), aAnimation.mTiming.EndTime(),
83 aAnimation.mStartTime.refOr(aAnimation.mHoldTime),
84 aAnimation.mPlaybackRate);
87 // -------------------------------------
88 // Case 2: document-timeline animations.
89 // -------------------------------------
90 MOZ_ASSERT(
91 (!aAnimation.mOriginTime.IsNull() && aAnimation.mStartTime.isSome()) ||
92 aAnimation.mIsNotPlaying,
93 "If we are playing, we should have an origin time and a start time");
95 // Determine if the animation was play-pending and used a ready time later
96 // than the previous frame time.
98 // To determine this, _all_ of the following conditions need to hold:
100 // * There was no previous animation value (i.e. this is the first frame for
101 // the animation since it was sent to the compositor), and
102 // * The animation is playing, and
103 // * There is a previous frame time, and
104 // * The ready time of the animation is ahead of the previous frame time.
106 bool hasFutureReadyTime = false;
107 if (!aPreviousValue && !aAnimation.mIsNotPlaying &&
108 !aPreviousFrameTime.IsNull()) {
109 // This is the inverse of the calculation performed in
110 // AnimationInfo::StartPendingAnimations to calculate the start time of
111 // play-pending animations.
112 // Note that we have to calculate (TimeStamp + TimeDuration) last to avoid
113 // underflow in the middle of the calulation.
114 const TimeStamp readyTime =
115 aAnimation.mOriginTime +
116 (aAnimation.mStartTime.ref() +
117 aAnimation.mHoldTime.MultDouble(1.0 / aAnimation.mPlaybackRate));
118 hasFutureReadyTime = !readyTime.IsNull() && readyTime > aPreviousFrameTime;
120 // Use the previous vsync time to make main thread animations and compositor
121 // more closely aligned.
123 // On the first frame where we have animations the previous timestamp will
124 // not be set so we simply use the current timestamp. As a result we will
125 // end up painting the first frame twice. That doesn't appear to be
126 // noticeable, however.
128 // Likewise, if the animation is play-pending, it may have a ready time that
129 // is *after* |aPreviousFrameTime| (but *before* |aCurrentFrameTime|).
130 // To avoid flicker we need to use |aCurrentFrameTime| to avoid temporarily
131 // jumping backwards into the range prior to when the animation starts.
132 const TimeStamp& timeStamp = aPreviousFrameTime.IsNull() || hasFutureReadyTime
133 ? aCurrentFrameTime
134 : aPreviousFrameTime;
136 // If the animation is not currently playing, e.g. paused or
137 // finished, then use the hold time to stay at the same position.
138 TimeDuration elapsedDuration =
139 aAnimation.mIsNotPlaying || aAnimation.mStartTime.isNothing()
140 ? aAnimation.mHoldTime
141 : (timeStamp - aAnimation.mOriginTime - aAnimation.mStartTime.ref())
142 .MultDouble(aAnimation.mPlaybackRate);
143 return elapsedDuration;
146 enum class CanSkipCompose {
147 IfPossible,
150 // This function samples the animation for a specific property. We may have
151 // multiple animations for a single property, and the later animations override
152 // the eariler ones. This function returns the sampled animation value,
153 // |aAnimationValue| for a single CSS property.
154 static AnimationHelper::SampleResult SampleAnimationForProperty(
155 const APZSampler* aAPZSampler, const LayersId& aLayersId,
156 const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime,
157 TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue,
158 CanSkipCompose aCanSkipCompose,
159 nsTArray<PropertyAnimation>& aPropertyAnimations,
160 RefPtr<StyleAnimationValue>& aAnimationValue) {
161 MOZ_ASSERT(!aPropertyAnimations.IsEmpty(), "Should have animations");
163 auto reason = AnimationHelper::SampleResult::Reason::None;
164 bool hasInEffectAnimations = false;
165 #ifdef DEBUG
166 // In cases where this function returns a SampleResult::Skipped, we actually
167 // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the
168 // call site that the value that would have been computed matches the stored
169 // value that we end up using. This flag is used to ensure we populate
170 // aAnimationValue in this scenario.
171 bool shouldBeSkipped = false;
172 #endif
173 // Process in order, since later animations override earlier ones.
174 for (PropertyAnimation& animation : aPropertyAnimations) {
175 dom::Nullable<TimeDuration> elapsedDuration = CalculateElapsedTime(
176 aAPZSampler, aLayersId, aProofOfMapLock, animation, aPreviousFrameTime,
177 aCurrentFrameTime, aPreviousValue);
179 const auto progressTimelinePosition =
180 animation.mScrollTimelineOptions
181 ? dom::Animation::AtProgressTimelineBoundary(
182 TimeDuration::FromMilliseconds(
183 PROGRESS_TIMELINE_DURATION_MILLISEC),
184 elapsedDuration, animation.mStartTime.refOr(TimeDuration()),
185 animation.mPlaybackRate)
186 : dom::Animation::ProgressTimelinePosition::NotBoundary;
188 ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt(
189 elapsedDuration, animation.mTiming, animation.mPlaybackRate,
190 progressTimelinePosition);
192 if (computedTiming.mProgress.IsNull()) {
193 // For the scroll-driven animations, it's possible to let it go between
194 // the active phase and the before/after phase, and so its progress
195 // becomes null. In this case, we shouldn't just skip this animation.
196 // Instead, we have to reset the previous sampled result. Basically, we
197 // use |mProgressOnLastCompose| to check if it goes from the active phase.
198 // If so, we set the returned |mReason| to ScrollToDelayPhase to let the
199 // caller know we need to use the base style for this property.
201 // If there are any other animations which need to be sampled together
202 // (in the same property animation group), this |reason| will be ignored.
203 if (animation.mScrollTimelineOptions &&
204 !animation.mProgressOnLastCompose.IsNull() &&
205 (computedTiming.mPhase == ComputedTiming::AnimationPhase::Before ||
206 computedTiming.mPhase == ComputedTiming::AnimationPhase::After)) {
207 // Appearally, we go back to delay, so need to reset the last
208 // composition meta data. This is necessary because
209 // 1. this animation is in delay so it shouldn't have any composition
210 // meta data, and
211 // 2. we will not go into this condition multiple times during delay
212 // phase because we rely on |mProgressOnLastCompose|.
213 animation.ResetLastCompositionValues();
214 reason = AnimationHelper::SampleResult::Reason::ScrollToDelayPhase;
216 continue;
219 dom::IterationCompositeOperation iterCompositeOperation =
220 animation.mIterationComposite;
222 // Skip calculation if the progress hasn't changed since the last
223 // calculation.
224 // Note that we don't skip calculate this animation if there is another
225 // animation since the other animation might be 'accumulate' or 'add', or
226 // might have a missing keyframe (i.e. this animation value will be used in
227 // the missing keyframe).
228 // FIXME Bug 1455476: We should do this optimizations for the case where
229 // the layer has multiple animations and multiple properties.
230 if (aCanSkipCompose == CanSkipCompose::IfPossible &&
231 !dom::KeyframeEffect::HasComputedTimingChanged(
232 computedTiming, iterCompositeOperation,
233 animation.mProgressOnLastCompose,
234 animation.mCurrentIterationOnLastCompose)) {
235 #ifdef DEBUG
236 shouldBeSkipped = true;
237 #else
238 return AnimationHelper::SampleResult::Skipped();
239 #endif
242 uint32_t segmentIndex = 0;
243 size_t segmentSize = animation.mSegments.Length();
244 PropertyAnimation::SegmentData* segment = animation.mSegments.Elements();
245 while (segment->mEndPortion < computedTiming.mProgress.Value() &&
246 segmentIndex < segmentSize - 1) {
247 ++segment;
248 ++segmentIndex;
251 double positionInSegment =
252 (computedTiming.mProgress.Value() - segment->mStartPortion) /
253 (segment->mEndPortion - segment->mStartPortion);
255 double portion = StyleComputedTimingFunction::GetPortion(
256 segment->mFunction, positionInSegment, computedTiming.mBeforeFlag);
258 // Like above optimization, skip calculation if the target segment isn't
259 // changed and if the portion in the segment isn't changed.
260 // This optimization is needed for CSS animations/transitions with step
261 // timing functions (e.g. the throbber animation on tabs or frame based
262 // animations).
263 // FIXME Bug 1455476: Like the above optimization, we should apply this
264 // optimizations for multiple animation cases and multiple properties as
265 // well.
266 if (aCanSkipCompose == CanSkipCompose::IfPossible &&
267 animation.mSegmentIndexOnLastCompose == segmentIndex &&
268 !animation.mPortionInSegmentOnLastCompose.IsNull() &&
269 animation.mPortionInSegmentOnLastCompose.Value() == portion) {
270 #ifdef DEBUG
271 shouldBeSkipped = true;
272 #else
273 return AnimationHelper::SampleResult::Skipped();
274 #endif
277 AnimationPropertySegment animSegment;
278 animSegment.mFromKey = 0.0;
279 animSegment.mToKey = 1.0;
280 animSegment.mFromValue = AnimationValue(segment->mStartValue);
281 animSegment.mToValue = AnimationValue(segment->mEndValue);
282 animSegment.mFromComposite = segment->mStartComposite;
283 animSegment.mToComposite = segment->mEndComposite;
285 // interpolate the property
286 aAnimationValue =
287 Servo_ComposeAnimationSegment(
288 &animSegment, aAnimationValue,
289 animation.mSegments.LastElement().mEndValue, iterCompositeOperation,
290 portion, computedTiming.mCurrentIteration)
291 .Consume();
293 #ifdef DEBUG
294 if (shouldBeSkipped) {
295 return AnimationHelper::SampleResult::Skipped();
297 #endif
299 hasInEffectAnimations = true;
300 animation.mProgressOnLastCompose = computedTiming.mProgress;
301 animation.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
302 animation.mSegmentIndexOnLastCompose = segmentIndex;
303 animation.mPortionInSegmentOnLastCompose.SetValue(portion);
306 auto rv = hasInEffectAnimations ? AnimationHelper::SampleResult::Sampled()
307 : AnimationHelper::SampleResult();
308 rv.mReason = reason;
309 return rv;
312 // This function samples the animations for a group of CSS properties. We may
313 // have multiple CSS properties in a group (e.g. transform-like properties).
314 // So the returned animation array, |aAnimationValues|, include all the
315 // animation values of these CSS properties.
316 AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode(
317 const APZSampler* aAPZSampler, const LayersId& aLayersId,
318 const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime,
319 TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue,
320 nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
321 SampledAnimationArray& aAnimationValues /* output */) {
322 MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(),
323 "Should be called with animation data");
324 MOZ_ASSERT(aAnimationValues.IsEmpty(),
325 "Should be called with empty aAnimationValues");
327 nsTArray<RefPtr<StyleAnimationValue>> baseStyleOfDelayAnimations;
328 nsTArray<RefPtr<StyleAnimationValue>> nonAnimatingValues;
329 for (PropertyAnimationGroup& group : aPropertyAnimationGroups) {
330 // Initialize animation value with base style.
331 RefPtr<StyleAnimationValue> currValue = group.mBaseStyle;
333 CanSkipCompose canSkipCompose =
334 aPreviousValue && aPropertyAnimationGroups.Length() == 1 &&
335 group.mAnimations.Length() == 1
336 ? CanSkipCompose::IfPossible
337 : CanSkipCompose::No;
339 MOZ_ASSERT(
340 !group.mAnimations.IsEmpty() ||
341 nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
342 group.mProperty),
343 "Only transform-like properties can have empty PropertyAnimation list");
345 // For properties which are not animating (i.e. their values are always the
346 // same), we store them in a different array, and then merge them into the
347 // final result (a.k.a. aAnimationValues) because we shouldn't take them
348 // into account for SampleResult. (In other words, these properties
349 // shouldn't affect the optimization.)
350 if (group.mAnimations.IsEmpty()) {
351 nonAnimatingValues.AppendElement(std::move(currValue));
352 continue;
355 SampleResult result = SampleAnimationForProperty(
356 aAPZSampler, aLayersId, aProofOfMapLock, aPreviousFrameTime,
357 aCurrentFrameTime, aPreviousValue, canSkipCompose, group.mAnimations,
358 currValue);
360 // FIXME: Bug 1455476: Do optimization for multiple properties. For now,
361 // the result is skipped only if the property count == 1.
362 if (result.IsSkipped()) {
363 #ifdef DEBUG
364 aAnimationValues.AppendElement(std::move(currValue));
365 #endif
366 return result;
369 if (!result.IsSampled()) {
370 if (result.mReason == SampleResult::Reason::ScrollToDelayPhase) {
371 MOZ_ASSERT(currValue && currValue == group.mBaseStyle);
372 baseStyleOfDelayAnimations.AppendElement(std::move(currValue));
374 continue;
377 // Insert the interpolation result into the output array.
378 MOZ_ASSERT(currValue);
379 aAnimationValues.AppendElement(std::move(currValue));
382 SampleResult rv =
383 aAnimationValues.IsEmpty() ? SampleResult() : SampleResult::Sampled();
385 // If there is no other sampled result, we may store these base styles
386 // (together with the non-animating values) to the webrenderer before it gets
387 // sync with the main thread.
388 if (rv.IsNone() && !baseStyleOfDelayAnimations.IsEmpty()) {
389 aAnimationValues.AppendElements(std::move(baseStyleOfDelayAnimations));
390 rv.mReason = SampleResult::Reason::ScrollToDelayPhase;
393 if (!aAnimationValues.IsEmpty()) {
394 aAnimationValues.AppendElements(std::move(nonAnimatingValues));
396 return rv;
399 static dom::FillMode GetAdjustedFillMode(const Animation& aAnimation) {
400 // Adjust fill mode so that if the main thread is delayed in clearing
401 // this animation we don't introduce flicker by jumping back to the old
402 // underlying value.
403 auto fillMode = static_cast<dom::FillMode>(aAnimation.fillMode());
404 float playbackRate = aAnimation.playbackRate();
405 switch (fillMode) {
406 case dom::FillMode::None:
407 if (playbackRate > 0) {
408 fillMode = dom::FillMode::Forwards;
409 } else if (playbackRate < 0) {
410 fillMode = dom::FillMode::Backwards;
412 break;
413 case dom::FillMode::Backwards:
414 if (playbackRate > 0) {
415 fillMode = dom::FillMode::Both;
417 break;
418 case dom::FillMode::Forwards:
419 if (playbackRate < 0) {
420 fillMode = dom::FillMode::Both;
422 break;
423 default:
424 break;
426 return fillMode;
429 #ifdef DEBUG
430 static bool HasTransformLikeAnimations(const AnimationArray& aAnimations) {
431 nsCSSPropertyIDSet transformSet =
432 nsCSSPropertyIDSet::TransformLikeProperties();
434 for (const Animation& animation : aAnimations) {
435 if (animation.isNotAnimating()) {
436 continue;
439 if (transformSet.HasProperty(animation.property())) {
440 return true;
444 return false;
446 #endif
448 AnimationStorageData AnimationHelper::ExtractAnimations(
449 const LayersId& aLayersId, const AnimationArray& aAnimations,
450 const CompositorAnimationStorage* aStorage,
451 const TimeStamp& aPreviousSampleTime) {
452 AnimationStorageData storageData;
453 storageData.mLayersId = aLayersId;
455 nsCSSPropertyID prevID = eCSSProperty_UNKNOWN;
456 PropertyAnimationGroup* currData = nullptr;
457 DebugOnly<const layers::Animatable*> currBaseStyle = nullptr;
459 for (const Animation& animation : aAnimations) {
460 // Animations with same property are grouped together, so we can just
461 // check if the current property is the same as the previous one for
462 // knowing this is a new group.
463 if (prevID != animation.property()) {
464 // Got a different group, we should create a different array.
465 currData = storageData.mAnimation.AppendElement();
466 currData->mProperty = animation.property();
467 if (animation.transformData()) {
468 MOZ_ASSERT(!storageData.mTransformData,
469 "Only one entry has TransformData");
470 storageData.mTransformData = animation.transformData();
473 prevID = animation.property();
475 // Reset the debug pointer.
476 currBaseStyle = nullptr;
479 MOZ_ASSERT(currData);
480 if (animation.baseStyle().type() != Animatable::Tnull_t) {
481 MOZ_ASSERT(!currBaseStyle || *currBaseStyle == animation.baseStyle(),
482 "Should be the same base style");
484 currData->mBaseStyle = AnimationValue::FromAnimatable(
485 animation.property(), animation.baseStyle());
486 currBaseStyle = &animation.baseStyle();
489 // If this layers::Animation sets isNotAnimating to true, it only has
490 // base style and doesn't have any animation information, so we can skip
491 // the rest steps. (And so its PropertyAnimationGroup::mAnimation will be
492 // an empty array.)
493 if (animation.isNotAnimating()) {
494 MOZ_ASSERT(nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
495 animation.property()),
496 "Only transform-like properties could set this true");
498 if (animation.property() == eCSSProperty_offset_path) {
499 MOZ_ASSERT(currData->mBaseStyle,
500 "Fixed offset-path should have base style");
501 MOZ_ASSERT(HasTransformLikeAnimations(aAnimations));
503 const StyleOffsetPath& offsetPath =
504 animation.baseStyle().get_StyleOffsetPath();
505 // FIXME: Bug 1837042. Cache all basic shapes.
506 if (offsetPath.IsPath()) {
507 MOZ_ASSERT(!storageData.mCachedMotionPath,
508 "Only one offset-path: path() is set");
510 RefPtr<gfx::PathBuilder> builder =
511 MotionPathUtils::GetCompositorPathBuilder();
512 storageData.mCachedMotionPath = MotionPathUtils::BuildSVGPath(
513 offsetPath.AsSVGPathData(), builder);
517 continue;
520 PropertyAnimation* propertyAnimation =
521 currData->mAnimations.AppendElement();
523 propertyAnimation->mOriginTime = animation.originTime();
524 propertyAnimation->mStartTime = animation.startTime();
525 propertyAnimation->mHoldTime = animation.holdTime();
526 propertyAnimation->mPlaybackRate = animation.playbackRate();
527 propertyAnimation->mIterationComposite =
528 static_cast<dom::IterationCompositeOperation>(
529 animation.iterationComposite());
530 propertyAnimation->mIsNotPlaying = animation.isNotPlaying();
531 propertyAnimation->mTiming =
532 TimingParams{animation.duration(),
533 animation.delay(),
534 animation.endDelay(),
535 animation.iterations(),
536 animation.iterationStart(),
537 static_cast<dom::PlaybackDirection>(animation.direction()),
538 GetAdjustedFillMode(animation),
539 animation.easingFunction()};
540 propertyAnimation->mScrollTimelineOptions =
541 animation.scrollTimelineOptions();
543 RefPtr<StyleAnimationValue> startValue;
544 if (animation.replacedTransitionId()) {
545 if (const auto* animatedValue =
546 aStorage->GetAnimatedValue(*animation.replacedTransitionId())) {
547 startValue = animatedValue->AsAnimationValue(animation.property());
548 // Basically, the timeline time is increasing monotonically, so it may
549 // not make sense to have a negative start time (i.e. the case when
550 // aPreviousSampleTime is behind the origin time). Therefore, if the
551 // previous sample time is less than the origin time, we skip the
552 // replacement of the start time.
553 if (!aPreviousSampleTime.IsNull() &&
554 (aPreviousSampleTime >= animation.originTime())) {
555 propertyAnimation->mStartTime =
556 Some(aPreviousSampleTime - animation.originTime());
559 MOZ_ASSERT(animation.segments().Length() == 1,
560 "The CSS Transition only has one segement");
564 nsTArray<PropertyAnimation::SegmentData>& segmentData =
565 propertyAnimation->mSegments;
566 for (const AnimationSegment& segment : animation.segments()) {
567 segmentData.AppendElement(PropertyAnimation::SegmentData{
568 // Note that even though we re-compute the start value on the main
569 // thread, we still replace it with the last sampled value, to avoid
570 // any possible lag.
571 startValue ? startValue
572 : AnimationValue::FromAnimatable(animation.property(),
573 segment.startState()),
574 AnimationValue::FromAnimatable(animation.property(),
575 segment.endState()),
576 segment.sampleFn(), segment.startPortion(), segment.endPortion(),
577 static_cast<dom::CompositeOperation>(segment.startComposite()),
578 static_cast<dom::CompositeOperation>(segment.endComposite())});
582 #ifdef DEBUG
583 // Sanity check that the grouped animation data is correct by looking at the
584 // property set.
585 if (!storageData.mAnimation.IsEmpty()) {
586 nsCSSPropertyIDSet seenProperties;
587 for (const auto& group : storageData.mAnimation) {
588 nsCSSPropertyID id = group.mProperty;
590 MOZ_ASSERT(!seenProperties.HasProperty(id), "Should be a new property");
591 seenProperties.AddProperty(id);
594 MOZ_ASSERT(
595 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
596 DisplayItemType::TYPE_TRANSFORM)) ||
597 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
598 DisplayItemType::TYPE_OPACITY)) ||
599 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
600 DisplayItemType::TYPE_BACKGROUND_COLOR)),
601 "The property set of output should be the subset of transform-like "
602 "properties, opacity, or background_color.");
604 if (seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
605 DisplayItemType::TYPE_TRANSFORM))) {
606 MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
609 if (seenProperties.HasProperty(eCSSProperty_offset_path)) {
610 MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
611 MOZ_ASSERT(storageData.mTransformData->motionPathData(),
612 "Should have MotionPathData");
615 #endif
617 return storageData;
620 uint64_t AnimationHelper::GetNextCompositorAnimationsId() {
621 static uint32_t sNextId = 0;
622 ++sNextId;
624 uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId());
625 uint64_t nextId = procId;
626 nextId = nextId << 32 | sNextId;
627 return nextId;
630 gfx::Matrix4x4 AnimationHelper::ServoAnimationValueToMatrix4x4(
631 const SampledAnimationArray& aValues, const TransformData& aTransformData,
632 gfx::Path* aCachedMotionPath) {
633 using nsStyleTransformMatrix::TransformReferenceBox;
635 // This is a bit silly just to avoid the transform list copy from the
636 // animation transform list.
637 auto noneTranslate = StyleTranslate::None();
638 auto noneRotate = StyleRotate::None();
639 auto noneScale = StyleScale::None();
640 const StyleTransform noneTransform;
642 const StyleTranslate* translate = nullptr;
643 const StyleRotate* rotate = nullptr;
644 const StyleScale* scale = nullptr;
645 const StyleTransform* transform = nullptr;
646 Maybe<StyleOffsetPath> path;
647 const StyleLengthPercentage* distance = nullptr;
648 const StyleOffsetRotate* offsetRotate = nullptr;
649 const StylePositionOrAuto* anchor = nullptr;
650 const StyleOffsetPosition* position = nullptr;
652 for (const auto& value : aValues) {
653 MOZ_ASSERT(value);
654 AnimatedPropertyID property(eCSSProperty_UNKNOWN);
655 Servo_AnimationValue_GetPropertyId(value, &property);
656 switch (property.mID) {
657 case eCSSProperty_transform:
658 MOZ_ASSERT(!transform);
659 transform = Servo_AnimationValue_GetTransform(value);
660 break;
661 case eCSSProperty_translate:
662 MOZ_ASSERT(!translate);
663 translate = Servo_AnimationValue_GetTranslate(value);
664 break;
665 case eCSSProperty_rotate:
666 MOZ_ASSERT(!rotate);
667 rotate = Servo_AnimationValue_GetRotate(value);
668 break;
669 case eCSSProperty_scale:
670 MOZ_ASSERT(!scale);
671 scale = Servo_AnimationValue_GetScale(value);
672 break;
673 case eCSSProperty_offset_path:
674 MOZ_ASSERT(!path);
675 path.emplace(StyleOffsetPath::None());
676 Servo_AnimationValue_GetOffsetPath(value, path.ptr());
677 break;
678 case eCSSProperty_offset_distance:
679 MOZ_ASSERT(!distance);
680 distance = Servo_AnimationValue_GetOffsetDistance(value);
681 break;
682 case eCSSProperty_offset_rotate:
683 MOZ_ASSERT(!offsetRotate);
684 offsetRotate = Servo_AnimationValue_GetOffsetRotate(value);
685 break;
686 case eCSSProperty_offset_anchor:
687 MOZ_ASSERT(!anchor);
688 anchor = Servo_AnimationValue_GetOffsetAnchor(value);
689 break;
690 case eCSSProperty_offset_position:
691 MOZ_ASSERT(!position);
692 position = Servo_AnimationValue_GetOffsetPosition(value);
693 break;
694 default:
695 MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property");
699 TransformReferenceBox refBox(nullptr, aTransformData.bounds());
700 Maybe<ResolvedMotionPathData> motion = MotionPathUtils::ResolveMotionPath(
701 path.ptrOr(nullptr), distance, offsetRotate, anchor, position,
702 aTransformData.motionPathData(), refBox, aCachedMotionPath);
704 // We expect all our transform data to arrive in device pixels
705 gfx::Point3D transformOrigin = aTransformData.transformOrigin();
706 nsDisplayTransform::FrameTransformProperties props(
707 translate ? *translate : noneTranslate, rotate ? *rotate : noneRotate,
708 scale ? *scale : noneScale, transform ? *transform : noneTransform,
709 motion, transformOrigin);
711 return nsDisplayTransform::GetResultingTransformMatrix(
712 props, refBox, aTransformData.appUnitsPerDevPixel());
715 static uint8_t CollectOverflowedSideLines(const gfxQuad& aPrerenderedQuad,
716 SideBits aOverflowSides,
717 gfxLineSegment sideLines[4]) {
718 uint8_t count = 0;
720 if (aOverflowSides & SideBits::eTop) {
721 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[0],
722 aPrerenderedQuad.mPoints[1]);
723 count++;
725 if (aOverflowSides & SideBits::eRight) {
726 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[1],
727 aPrerenderedQuad.mPoints[2]);
728 count++;
730 if (aOverflowSides & SideBits::eBottom) {
731 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[2],
732 aPrerenderedQuad.mPoints[3]);
733 count++;
735 if (aOverflowSides & SideBits::eLeft) {
736 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[3],
737 aPrerenderedQuad.mPoints[0]);
738 count++;
741 return count;
744 enum RegionBits : uint8_t {
745 Inside = 0,
746 Left = (1 << 0),
747 Right = (1 << 1),
748 Bottom = (1 << 2),
749 Top = (1 << 3),
752 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RegionBits);
754 static RegionBits GetRegionBitsForPoint(double aX, double aY,
755 const gfxRect& aClip) {
756 RegionBits result = RegionBits::Inside;
757 if (aX < aClip.X()) {
758 result |= RegionBits::Left;
759 } else if (aX > aClip.XMost()) {
760 result |= RegionBits::Right;
763 if (aY < aClip.Y()) {
764 result |= RegionBits::Bottom;
765 } else if (aY > aClip.YMost()) {
766 result |= RegionBits::Top;
768 return result;
771 // https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
772 static bool LineSegmentIntersectsClip(double aX0, double aY0, double aX1,
773 double aY1, const gfxRect& aClip) {
774 RegionBits b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
775 RegionBits b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
777 while (true) {
778 if (!(b0 | b1)) {
779 // Completely inside.
780 return true;
783 if (b0 & b1) {
784 // Completely outside.
785 return false;
788 double x, y;
789 // Choose an outside point.
790 RegionBits outsidePointBits = b1 > b0 ? b1 : b0;
791 if (outsidePointBits & RegionBits::Top) {
792 x = aX0 + (aX1 - aX0) * (aClip.YMost() - aY0) / (aY1 - aY0);
793 y = aClip.YMost();
794 } else if (outsidePointBits & RegionBits::Bottom) {
795 x = aX0 + (aX1 - aX0) * (aClip.Y() - aY0) / (aY1 - aY0);
796 y = aClip.Y();
797 } else if (outsidePointBits & RegionBits::Right) {
798 y = aY0 + (aY1 - aY0) * (aClip.XMost() - aX0) / (aX1 - aX0);
799 x = aClip.XMost();
800 } else if (outsidePointBits & RegionBits::Left) {
801 y = aY0 + (aY1 - aY0) * (aClip.X() - aX0) / (aX1 - aX0);
802 x = aClip.X();
805 if (outsidePointBits == b0) {
806 aX0 = x;
807 aY0 = y;
808 b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
809 } else {
810 aX1 = x;
811 aY1 = y;
812 b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
815 MOZ_ASSERT_UNREACHABLE();
816 return false;
819 // static
820 bool AnimationHelper::ShouldBeJank(const LayoutDeviceRect& aPrerenderedRect,
821 SideBits aOverflowSides,
822 const gfx::Matrix4x4& aTransform,
823 const ParentLayerRect& aClipRect) {
824 if (aClipRect.IsEmpty()) {
825 return false;
828 gfxQuad prerenderedQuad = gfxUtils::TransformToQuad(
829 ThebesRect(aPrerenderedRect.ToUnknownRect()), aTransform);
831 gfxLineSegment sideLines[4];
832 uint8_t overflowSideCount =
833 CollectOverflowedSideLines(prerenderedQuad, aOverflowSides, sideLines);
835 gfxRect clipRect = ThebesRect(aClipRect.ToUnknownRect());
836 for (uint8_t j = 0; j < overflowSideCount; j++) {
837 if (LineSegmentIntersectsClip(sideLines[j].mStart.x, sideLines[j].mStart.y,
838 sideLines[j].mEnd.x, sideLines[j].mEnd.y,
839 clipRect)) {
840 return true;
844 // With step timing functions there are cases the transform jumps to a
845 // position where the partial pre-render area is totally outside of the clip
846 // rect without any intersection of the partial pre-render area and the clip
847 // rect happened in previous compositions but there remains visible area of
848 // the entire transformed area.
850 // So now all four points of the transformed partial pre-render rect are
851 // outside of the clip rect, if all these four points are in either side of
852 // the clip rect, we consider it's jank so that on the main-thread we will
853 // either a) rebuild the up-to-date display item if there remains visible area
854 // or b) no longer rebuild the display item if it's totally outside of the
855 // clip rect.
857 // Note that RegionBits::Left and Right are mutually exclusive,
858 // RegionBits::Top and Bottom are also mutually exclusive, so if there remains
859 // any bits, it means all four points are in the same side.
860 return GetRegionBitsForPoint(prerenderedQuad.mPoints[0].x,
861 prerenderedQuad.mPoints[0].y, clipRect) &
862 GetRegionBitsForPoint(prerenderedQuad.mPoints[1].x,
863 prerenderedQuad.mPoints[1].y, clipRect) &
864 GetRegionBitsForPoint(prerenderedQuad.mPoints[2].x,
865 prerenderedQuad.mPoints[2].y, clipRect) &
866 GetRegionBitsForPoint(prerenderedQuad.mPoints[3].x,
867 prerenderedQuad.mPoints[3].y, clipRect);
870 } // namespace mozilla::layers