Bug 1839315: part 4) Link from `SheetLoadData::mWasAlternate` to spec. r=emilio DONTBUILD
[gecko.git] / gfx / layers / AnimationHelper.cpp
blob25256aa5a86bce0b31a51a723c0624f0b7ebf2c3
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 "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/ServoStyleConsts.h" // for StyleComputedTimingFunction
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/APZSampler.h" // for APZSampler
21 #include "mozilla/layers/CompositorThread.h" // for CompositorThreadHolder
22 #include "mozilla/LayerAnimationInfo.h" // for GetCSSPropertiesFor()
23 #include "mozilla/Maybe.h" // for Maybe<>
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 "nsCSSPropertyID.h" // for eCSSProperty_offset_path, etc
28 #include "nsDeviceContext.h" // for AppUnitsPerCSSPixel
29 #include "nsDisplayList.h" // for nsDisplayTransform, etc
31 namespace mozilla {
32 namespace layers {
34 static dom::Nullable<TimeDuration> CalculateElapsedTimeForScrollTimeline(
35 const Maybe<APZSampler::ScrollOffsetAndRange> aScrollMeta,
36 const ScrollTimelineOptions& aOptions, const StickyTimeDuration& aEndTime,
37 const TimeDuration& aStartTime, float aPlaybackRate) {
38 // We return Nothing If the associated APZ controller is not available
39 // (because it may be destroyed but this animation is still alive).
40 if (!aScrollMeta) {
41 // This may happen after we reload a page. There may be a race condition
42 // because the animation is still alive but the APZ is destroyed. In this
43 // case, this animation is invalid, so we return nullptr.
44 return nullptr;
47 const bool isHorizontal =
48 aOptions.axis() == layers::ScrollDirection::eHorizontal;
49 double range =
50 isHorizontal ? aScrollMeta->mRange.width : aScrollMeta->mRange.height;
51 MOZ_ASSERT(
52 range > 0,
53 "We don't expect to get a zero or negative range on the compositor");
55 // The offset may be negative if the writing mode is from right to left.
56 // Use std::abs() here to avoid getting a negative progress.
57 double position =
58 std::abs(isHorizontal ? aScrollMeta->mOffset.x : aScrollMeta->mOffset.y);
59 double progress = position / range;
60 // Just in case to avoid getting a progress more than 100%, for overscrolling.
61 progress = std::min(progress, 1.0);
62 auto timelineTime = TimeDuration(aEndTime.MultDouble(progress));
63 return dom::Animation::CurrentTimeFromTimelineTime(timelineTime, aStartTime,
64 aPlaybackRate);
67 static dom::Nullable<TimeDuration> CalculateElapsedTime(
68 const APZSampler* aAPZSampler, const LayersId& aLayersId,
69 const MutexAutoLock& aProofOfMapLock, const PropertyAnimation& aAnimation,
70 const TimeStamp aPreviousFrameTime, const TimeStamp aCurrentFrameTime,
71 const AnimatedValue* aPreviousValue) {
72 // -------------------------------------
73 // Case 1: scroll-timeline animations.
74 // -------------------------------------
75 if (aAnimation.mScrollTimelineOptions) {
76 MOZ_ASSERT(
77 aAPZSampler,
78 "We don't send scroll animations to the compositor if APZ is disabled");
80 return CalculateElapsedTimeForScrollTimeline(
81 aAPZSampler->GetCurrentScrollOffsetAndRange(
82 aLayersId, aAnimation.mScrollTimelineOptions.value().source(),
83 aProofOfMapLock),
84 aAnimation.mScrollTimelineOptions.value(), aAnimation.mTiming.EndTime(),
85 aAnimation.mStartTime.refOr(aAnimation.mHoldTime),
86 aAnimation.mPlaybackRate);
89 // -------------------------------------
90 // Case 2: document-timeline animations.
91 // -------------------------------------
92 MOZ_ASSERT(
93 (!aAnimation.mOriginTime.IsNull() && aAnimation.mStartTime.isSome()) ||
94 aAnimation.mIsNotPlaying,
95 "If we are playing, we should have an origin time and a start time");
97 // Determine if the animation was play-pending and used a ready time later
98 // than the previous frame time.
100 // To determine this, _all_ of the following conditions need to hold:
102 // * There was no previous animation value (i.e. this is the first frame for
103 // the animation since it was sent to the compositor), and
104 // * The animation is playing, and
105 // * There is a previous frame time, and
106 // * The ready time of the animation is ahead of the previous frame time.
108 bool hasFutureReadyTime = false;
109 if (!aPreviousValue && !aAnimation.mIsNotPlaying &&
110 !aPreviousFrameTime.IsNull()) {
111 // This is the inverse of the calculation performed in
112 // AnimationInfo::StartPendingAnimations to calculate the start time of
113 // play-pending animations.
114 // Note that we have to calculate (TimeStamp + TimeDuration) last to avoid
115 // underflow in the middle of the calulation.
116 const TimeStamp readyTime =
117 aAnimation.mOriginTime +
118 (aAnimation.mStartTime.ref() +
119 aAnimation.mHoldTime.MultDouble(1.0 / aAnimation.mPlaybackRate));
120 hasFutureReadyTime = !readyTime.IsNull() && readyTime > aPreviousFrameTime;
122 // Use the previous vsync time to make main thread animations and compositor
123 // more closely aligned.
125 // On the first frame where we have animations the previous timestamp will
126 // not be set so we simply use the current timestamp. As a result we will
127 // end up painting the first frame twice. That doesn't appear to be
128 // noticeable, however.
130 // Likewise, if the animation is play-pending, it may have a ready time that
131 // is *after* |aPreviousFrameTime| (but *before* |aCurrentFrameTime|).
132 // To avoid flicker we need to use |aCurrentFrameTime| to avoid temporarily
133 // jumping backwards into the range prior to when the animation starts.
134 const TimeStamp& timeStamp = aPreviousFrameTime.IsNull() || hasFutureReadyTime
135 ? aCurrentFrameTime
136 : aPreviousFrameTime;
138 // If the animation is not currently playing, e.g. paused or
139 // finished, then use the hold time to stay at the same position.
140 TimeDuration elapsedDuration =
141 aAnimation.mIsNotPlaying || aAnimation.mStartTime.isNothing()
142 ? aAnimation.mHoldTime
143 : (timeStamp - aAnimation.mOriginTime - aAnimation.mStartTime.ref())
144 .MultDouble(aAnimation.mPlaybackRate);
145 return elapsedDuration;
148 enum class CanSkipCompose {
149 IfPossible,
152 // This function samples the animation for a specific property. We may have
153 // multiple animations for a single property, and the later animations override
154 // the eariler ones. This function returns the sampled animation value,
155 // |aAnimationValue| for a single CSS property.
156 static AnimationHelper::SampleResult SampleAnimationForProperty(
157 const APZSampler* aAPZSampler, const LayersId& aLayersId,
158 const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime,
159 TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue,
160 CanSkipCompose aCanSkipCompose,
161 nsTArray<PropertyAnimation>& aPropertyAnimations,
162 RefPtr<StyleAnimationValue>& aAnimationValue) {
163 MOZ_ASSERT(!aPropertyAnimations.IsEmpty(), "Should have animations");
165 auto reason = AnimationHelper::SampleResult::Reason::None;
166 bool hasInEffectAnimations = false;
167 #ifdef DEBUG
168 // In cases where this function returns a SampleResult::Skipped, we actually
169 // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the
170 // call site that the value that would have been computed matches the stored
171 // value that we end up using. This flag is used to ensure we populate
172 // aAnimationValue in this scenario.
173 bool shouldBeSkipped = false;
174 #endif
175 // Process in order, since later animations override earlier ones.
176 for (PropertyAnimation& animation : aPropertyAnimations) {
177 dom::Nullable<TimeDuration> elapsedDuration = CalculateElapsedTime(
178 aAPZSampler, aLayersId, aProofOfMapLock, animation, aPreviousFrameTime,
179 aCurrentFrameTime, aPreviousValue);
181 const auto progressTimelinePosition =
182 animation.mScrollTimelineOptions
183 ? dom::Animation::AtProgressTimelineBoundary(
184 TimeDuration::FromMilliseconds(
185 PROGRESS_TIMELINE_DURATION_MILLISEC),
186 elapsedDuration, animation.mStartTime.refOr(TimeDuration()),
187 animation.mPlaybackRate)
188 : dom::Animation::ProgressTimelinePosition::NotBoundary;
190 ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt(
191 elapsedDuration, animation.mTiming, animation.mPlaybackRate,
192 progressTimelinePosition);
194 if (computedTiming.mProgress.IsNull()) {
195 // For the scroll-driven animations, it's possible to let it go between
196 // the active phase and the before/after phase, and so its progress
197 // becomes null. In this case, we shouldn't just skip this animation.
198 // Instead, we have to reset the previous sampled result. Basically, we
199 // use |mProgressOnLastCompose| to check if it goes from the active phase.
200 // If so, we set the returned |mReason| to ScrollToDelayPhase to let the
201 // caller know we need to use the base style for this property.
203 // If there are any other animations which need to be sampled together
204 // (in the same property animation group), this |reason| will be ignored.
205 if (animation.mScrollTimelineOptions &&
206 !animation.mProgressOnLastCompose.IsNull() &&
207 (computedTiming.mPhase == ComputedTiming::AnimationPhase::Before ||
208 computedTiming.mPhase == ComputedTiming::AnimationPhase::After)) {
209 // Appearally, we go back to delay, so need to reset the last
210 // composition meta data. This is necessary because
211 // 1. this animation is in delay so it shouldn't have any composition
212 // meta data, and
213 // 2. we will not go into this condition multiple times during delay
214 // phase because we rely on |mProgressOnLastCompose|.
215 animation.ResetLastCompositionValues();
216 reason = AnimationHelper::SampleResult::Reason::ScrollToDelayPhase;
218 continue;
221 dom::IterationCompositeOperation iterCompositeOperation =
222 animation.mIterationComposite;
224 // Skip calculation if the progress hasn't changed since the last
225 // calculation.
226 // Note that we don't skip calculate this animation if there is another
227 // animation since the other animation might be 'accumulate' or 'add', or
228 // might have a missing keyframe (i.e. this animation value will be used in
229 // the missing keyframe).
230 // FIXME Bug 1455476: We should do this optimizations for the case where
231 // the layer has multiple animations and multiple properties.
232 if (aCanSkipCompose == CanSkipCompose::IfPossible &&
233 !dom::KeyframeEffect::HasComputedTimingChanged(
234 computedTiming, iterCompositeOperation,
235 animation.mProgressOnLastCompose,
236 animation.mCurrentIterationOnLastCompose)) {
237 #ifdef DEBUG
238 shouldBeSkipped = true;
239 #else
240 return AnimationHelper::SampleResult::Skipped();
241 #endif
244 uint32_t segmentIndex = 0;
245 size_t segmentSize = animation.mSegments.Length();
246 PropertyAnimation::SegmentData* segment = animation.mSegments.Elements();
247 while (segment->mEndPortion < computedTiming.mProgress.Value() &&
248 segmentIndex < segmentSize - 1) {
249 ++segment;
250 ++segmentIndex;
253 double positionInSegment =
254 (computedTiming.mProgress.Value() - segment->mStartPortion) /
255 (segment->mEndPortion - segment->mStartPortion);
257 double portion = StyleComputedTimingFunction::GetPortion(
258 segment->mFunction, positionInSegment, computedTiming.mBeforeFlag);
260 // Like above optimization, skip calculation if the target segment isn't
261 // changed and if the portion in the segment isn't changed.
262 // This optimization is needed for CSS animations/transitions with step
263 // timing functions (e.g. the throbber animation on tabs or frame based
264 // animations).
265 // FIXME Bug 1455476: Like the above optimization, we should apply this
266 // optimizations for multiple animation cases and multiple properties as
267 // well.
268 if (aCanSkipCompose == CanSkipCompose::IfPossible &&
269 animation.mSegmentIndexOnLastCompose == segmentIndex &&
270 !animation.mPortionInSegmentOnLastCompose.IsNull() &&
271 animation.mPortionInSegmentOnLastCompose.Value() == portion) {
272 #ifdef DEBUG
273 shouldBeSkipped = true;
274 #else
275 return AnimationHelper::SampleResult::Skipped();
276 #endif
279 AnimationPropertySegment animSegment;
280 animSegment.mFromKey = 0.0;
281 animSegment.mToKey = 1.0;
282 animSegment.mFromValue = AnimationValue(segment->mStartValue);
283 animSegment.mToValue = AnimationValue(segment->mEndValue);
284 animSegment.mFromComposite = segment->mStartComposite;
285 animSegment.mToComposite = segment->mEndComposite;
287 // interpolate the property
288 aAnimationValue =
289 Servo_ComposeAnimationSegment(
290 &animSegment, aAnimationValue,
291 animation.mSegments.LastElement().mEndValue, iterCompositeOperation,
292 portion, computedTiming.mCurrentIteration)
293 .Consume();
295 #ifdef DEBUG
296 if (shouldBeSkipped) {
297 return AnimationHelper::SampleResult::Skipped();
299 #endif
301 hasInEffectAnimations = true;
302 animation.mProgressOnLastCompose = computedTiming.mProgress;
303 animation.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
304 animation.mSegmentIndexOnLastCompose = segmentIndex;
305 animation.mPortionInSegmentOnLastCompose.SetValue(portion);
308 auto rv = hasInEffectAnimations ? AnimationHelper::SampleResult::Sampled()
309 : AnimationHelper::SampleResult();
310 rv.mReason = reason;
311 return rv;
314 // This function samples the animations for a group of CSS properties. We may
315 // have multiple CSS properties in a group (e.g. transform-like properties).
316 // So the returned animation array, |aAnimationValues|, include all the
317 // animation values of these CSS properties.
318 AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode(
319 const APZSampler* aAPZSampler, const LayersId& aLayersId,
320 const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime,
321 TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue,
322 nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
323 nsTArray<RefPtr<StyleAnimationValue>>& aAnimationValues /* out */) {
324 MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(),
325 "Should be called with animation data");
326 MOZ_ASSERT(aAnimationValues.IsEmpty(),
327 "Should be called with empty aAnimationValues");
329 nsTArray<RefPtr<StyleAnimationValue>> baseStyleOfDelayAnimations;
330 nsTArray<RefPtr<StyleAnimationValue>> nonAnimatingValues;
331 for (PropertyAnimationGroup& group : aPropertyAnimationGroups) {
332 // Initialize animation value with base style.
333 RefPtr<StyleAnimationValue> currValue = group.mBaseStyle;
335 CanSkipCompose canSkipCompose =
336 aPreviousValue && aPropertyAnimationGroups.Length() == 1 &&
337 group.mAnimations.Length() == 1
338 ? CanSkipCompose::IfPossible
339 : CanSkipCompose::No;
341 MOZ_ASSERT(
342 !group.mAnimations.IsEmpty() ||
343 nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
344 group.mProperty),
345 "Only transform-like properties can have empty PropertyAnimation list");
347 // For properties which are not animating (i.e. their values are always the
348 // same), we store them in a different array, and then merge them into the
349 // final result (a.k.a. aAnimationValues) because we shouldn't take them
350 // into account for SampleResult. (In other words, these properties
351 // shouldn't affect the optimization.)
352 if (group.mAnimations.IsEmpty()) {
353 nonAnimatingValues.AppendElement(std::move(currValue));
354 continue;
357 SampleResult result = SampleAnimationForProperty(
358 aAPZSampler, aLayersId, aProofOfMapLock, aPreviousFrameTime,
359 aCurrentFrameTime, aPreviousValue, canSkipCompose, group.mAnimations,
360 currValue);
362 // FIXME: Bug 1455476: Do optimization for multiple properties. For now,
363 // the result is skipped only if the property count == 1.
364 if (result.IsSkipped()) {
365 #ifdef DEBUG
366 aAnimationValues.AppendElement(std::move(currValue));
367 #endif
368 return result;
371 if (!result.IsSampled()) {
372 if (result.mReason == SampleResult::Reason::ScrollToDelayPhase) {
373 MOZ_ASSERT(currValue && currValue == group.mBaseStyle);
374 baseStyleOfDelayAnimations.AppendElement(std::move(currValue));
376 continue;
379 // Insert the interpolation result into the output array.
380 MOZ_ASSERT(currValue);
381 aAnimationValues.AppendElement(std::move(currValue));
384 SampleResult rv =
385 aAnimationValues.IsEmpty() ? SampleResult() : SampleResult::Sampled();
387 // If there is no other sampled result, we may store these base styles
388 // (together with the non-animating values) to the webrenderer before it gets
389 // sync with the main thread.
390 if (rv.IsNone() && !baseStyleOfDelayAnimations.IsEmpty()) {
391 aAnimationValues.AppendElements(std::move(baseStyleOfDelayAnimations));
392 rv.mReason = SampleResult::Reason::ScrollToDelayPhase;
395 if (!aAnimationValues.IsEmpty()) {
396 aAnimationValues.AppendElements(std::move(nonAnimatingValues));
398 return rv;
401 static dom::FillMode GetAdjustedFillMode(const Animation& aAnimation) {
402 // Adjust fill mode so that if the main thread is delayed in clearing
403 // this animation we don't introduce flicker by jumping back to the old
404 // underlying value.
405 auto fillMode = static_cast<dom::FillMode>(aAnimation.fillMode());
406 float playbackRate = aAnimation.playbackRate();
407 switch (fillMode) {
408 case dom::FillMode::None:
409 if (playbackRate > 0) {
410 fillMode = dom::FillMode::Forwards;
411 } else if (playbackRate < 0) {
412 fillMode = dom::FillMode::Backwards;
414 break;
415 case dom::FillMode::Backwards:
416 if (playbackRate > 0) {
417 fillMode = dom::FillMode::Both;
419 break;
420 case dom::FillMode::Forwards:
421 if (playbackRate < 0) {
422 fillMode = dom::FillMode::Both;
424 break;
425 default:
426 break;
428 return fillMode;
431 #ifdef DEBUG
432 static bool HasTransformLikeAnimations(const AnimationArray& aAnimations) {
433 nsCSSPropertyIDSet transformSet =
434 nsCSSPropertyIDSet::TransformLikeProperties();
436 for (const Animation& animation : aAnimations) {
437 if (animation.isNotAnimating()) {
438 continue;
441 if (transformSet.HasProperty(animation.property())) {
442 return true;
446 return false;
448 #endif
450 AnimationStorageData AnimationHelper::ExtractAnimations(
451 const LayersId& aLayersId, const AnimationArray& aAnimations) {
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 nsTArray<PropertyAnimation::SegmentData>& segmentData =
544 propertyAnimation->mSegments;
545 for (const AnimationSegment& segment : animation.segments()) {
546 segmentData.AppendElement(PropertyAnimation::SegmentData{
547 AnimationValue::FromAnimatable(animation.property(),
548 segment.startState()),
549 AnimationValue::FromAnimatable(animation.property(),
550 segment.endState()),
551 segment.sampleFn(), segment.startPortion(), segment.endPortion(),
552 static_cast<dom::CompositeOperation>(segment.startComposite()),
553 static_cast<dom::CompositeOperation>(segment.endComposite())});
557 #ifdef DEBUG
558 // Sanity check that the grouped animation data is correct by looking at the
559 // property set.
560 if (!storageData.mAnimation.IsEmpty()) {
561 nsCSSPropertyIDSet seenProperties;
562 for (const auto& group : storageData.mAnimation) {
563 nsCSSPropertyID id = group.mProperty;
565 MOZ_ASSERT(!seenProperties.HasProperty(id), "Should be a new property");
566 seenProperties.AddProperty(id);
569 MOZ_ASSERT(
570 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
571 DisplayItemType::TYPE_TRANSFORM)) ||
572 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
573 DisplayItemType::TYPE_OPACITY)) ||
574 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
575 DisplayItemType::TYPE_BACKGROUND_COLOR)),
576 "The property set of output should be the subset of transform-like "
577 "properties, opacity, or background_color.");
579 if (seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
580 DisplayItemType::TYPE_TRANSFORM))) {
581 MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
584 if (seenProperties.HasProperty(eCSSProperty_offset_path)) {
585 MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
586 MOZ_ASSERT(storageData.mTransformData->motionPathData(),
587 "Should have MotionPathData");
590 #endif
592 return storageData;
595 uint64_t AnimationHelper::GetNextCompositorAnimationsId() {
596 static uint32_t sNextId = 0;
597 ++sNextId;
599 uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId());
600 uint64_t nextId = procId;
601 nextId = nextId << 32 | sNextId;
602 return nextId;
605 gfx::Matrix4x4 AnimationHelper::ServoAnimationValueToMatrix4x4(
606 const nsTArray<RefPtr<StyleAnimationValue>>& aValues,
607 const TransformData& aTransformData, gfx::Path* aCachedMotionPath) {
608 using nsStyleTransformMatrix::TransformReferenceBox;
610 // This is a bit silly just to avoid the transform list copy from the
611 // animation transform list.
612 auto noneTranslate = StyleTranslate::None();
613 auto noneRotate = StyleRotate::None();
614 auto noneScale = StyleScale::None();
615 const StyleTransform noneTransform;
617 const StyleTranslate* translate = nullptr;
618 const StyleRotate* rotate = nullptr;
619 const StyleScale* scale = nullptr;
620 const StyleTransform* transform = nullptr;
621 Maybe<StyleOffsetPath> path;
622 const StyleLengthPercentage* distance = nullptr;
623 const StyleOffsetRotate* offsetRotate = nullptr;
624 const StylePositionOrAuto* anchor = nullptr;
625 const StyleOffsetPosition* position = nullptr;
627 for (const auto& value : aValues) {
628 MOZ_ASSERT(value);
629 nsCSSPropertyID id = Servo_AnimationValue_GetPropertyId(value);
630 switch (id) {
631 case eCSSProperty_transform:
632 MOZ_ASSERT(!transform);
633 transform = Servo_AnimationValue_GetTransform(value);
634 break;
635 case eCSSProperty_translate:
636 MOZ_ASSERT(!translate);
637 translate = Servo_AnimationValue_GetTranslate(value);
638 break;
639 case eCSSProperty_rotate:
640 MOZ_ASSERT(!rotate);
641 rotate = Servo_AnimationValue_GetRotate(value);
642 break;
643 case eCSSProperty_scale:
644 MOZ_ASSERT(!scale);
645 scale = Servo_AnimationValue_GetScale(value);
646 break;
647 case eCSSProperty_offset_path:
648 MOZ_ASSERT(!path);
649 path.emplace(StyleOffsetPath::None());
650 Servo_AnimationValue_GetOffsetPath(value, path.ptr());
651 break;
652 case eCSSProperty_offset_distance:
653 MOZ_ASSERT(!distance);
654 distance = Servo_AnimationValue_GetOffsetDistance(value);
655 break;
656 case eCSSProperty_offset_rotate:
657 MOZ_ASSERT(!offsetRotate);
658 offsetRotate = Servo_AnimationValue_GetOffsetRotate(value);
659 break;
660 case eCSSProperty_offset_anchor:
661 MOZ_ASSERT(!anchor);
662 anchor = Servo_AnimationValue_GetOffsetAnchor(value);
663 break;
664 case eCSSProperty_offset_position:
665 MOZ_ASSERT(!position);
666 position = Servo_AnimationValue_GetOffsetPosition(value);
667 break;
668 default:
669 MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property");
673 TransformReferenceBox refBox(nullptr, aTransformData.bounds());
674 Maybe<ResolvedMotionPathData> motion = MotionPathUtils::ResolveMotionPath(
675 path.ptrOr(nullptr), distance, offsetRotate, anchor, position,
676 aTransformData.motionPathData(), refBox, aCachedMotionPath);
678 // We expect all our transform data to arrive in device pixels
679 gfx::Point3D transformOrigin = aTransformData.transformOrigin();
680 nsDisplayTransform::FrameTransformProperties props(
681 translate ? *translate : noneTranslate, rotate ? *rotate : noneRotate,
682 scale ? *scale : noneScale, transform ? *transform : noneTransform,
683 motion, transformOrigin);
685 return nsDisplayTransform::GetResultingTransformMatrix(
686 props, refBox, aTransformData.appUnitsPerDevPixel());
689 static uint8_t CollectOverflowedSideLines(const gfxQuad& aPrerenderedQuad,
690 SideBits aOverflowSides,
691 gfxLineSegment sideLines[4]) {
692 uint8_t count = 0;
694 if (aOverflowSides & SideBits::eTop) {
695 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[0],
696 aPrerenderedQuad.mPoints[1]);
697 count++;
699 if (aOverflowSides & SideBits::eRight) {
700 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[1],
701 aPrerenderedQuad.mPoints[2]);
702 count++;
704 if (aOverflowSides & SideBits::eBottom) {
705 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[2],
706 aPrerenderedQuad.mPoints[3]);
707 count++;
709 if (aOverflowSides & SideBits::eLeft) {
710 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[3],
711 aPrerenderedQuad.mPoints[0]);
712 count++;
715 return count;
718 enum RegionBits : uint8_t {
719 Inside = 0,
720 Left = (1 << 0),
721 Right = (1 << 1),
722 Bottom = (1 << 2),
723 Top = (1 << 3),
726 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RegionBits);
728 static RegionBits GetRegionBitsForPoint(double aX, double aY,
729 const gfxRect& aClip) {
730 RegionBits result = RegionBits::Inside;
731 if (aX < aClip.X()) {
732 result |= RegionBits::Left;
733 } else if (aX > aClip.XMost()) {
734 result |= RegionBits::Right;
737 if (aY < aClip.Y()) {
738 result |= RegionBits::Bottom;
739 } else if (aY > aClip.YMost()) {
740 result |= RegionBits::Top;
742 return result;
745 // https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
746 static bool LineSegmentIntersectsClip(double aX0, double aY0, double aX1,
747 double aY1, const gfxRect& aClip) {
748 RegionBits b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
749 RegionBits b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
751 while (true) {
752 if (!(b0 | b1)) {
753 // Completely inside.
754 return true;
757 if (b0 & b1) {
758 // Completely outside.
759 return false;
762 double x, y;
763 // Choose an outside point.
764 RegionBits outsidePointBits = b1 > b0 ? b1 : b0;
765 if (outsidePointBits & RegionBits::Top) {
766 x = aX0 + (aX1 - aX0) * (aClip.YMost() - aY0) / (aY1 - aY0);
767 y = aClip.YMost();
768 } else if (outsidePointBits & RegionBits::Bottom) {
769 x = aX0 + (aX1 - aX0) * (aClip.Y() - aY0) / (aY1 - aY0);
770 y = aClip.Y();
771 } else if (outsidePointBits & RegionBits::Right) {
772 y = aY0 + (aY1 - aY0) * (aClip.XMost() - aX0) / (aX1 - aX0);
773 x = aClip.XMost();
774 } else if (outsidePointBits & RegionBits::Left) {
775 y = aY0 + (aY1 - aY0) * (aClip.X() - aX0) / (aX1 - aX0);
776 x = aClip.X();
779 if (outsidePointBits == b0) {
780 aX0 = x;
781 aY0 = y;
782 b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
783 } else {
784 aX1 = x;
785 aY1 = y;
786 b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
789 MOZ_ASSERT_UNREACHABLE();
790 return false;
793 // static
794 bool AnimationHelper::ShouldBeJank(const LayoutDeviceRect& aPrerenderedRect,
795 SideBits aOverflowSides,
796 const gfx::Matrix4x4& aTransform,
797 const ParentLayerRect& aClipRect) {
798 if (aClipRect.IsEmpty()) {
799 return false;
802 gfxQuad prerenderedQuad = gfxUtils::TransformToQuad(
803 ThebesRect(aPrerenderedRect.ToUnknownRect()), aTransform);
805 gfxLineSegment sideLines[4];
806 uint8_t overflowSideCount =
807 CollectOverflowedSideLines(prerenderedQuad, aOverflowSides, sideLines);
809 gfxRect clipRect = ThebesRect(aClipRect.ToUnknownRect());
810 for (uint8_t j = 0; j < overflowSideCount; j++) {
811 if (LineSegmentIntersectsClip(sideLines[j].mStart.x, sideLines[j].mStart.y,
812 sideLines[j].mEnd.x, sideLines[j].mEnd.y,
813 clipRect)) {
814 return true;
818 // With step timing functions there are cases the transform jumps to a
819 // position where the partial pre-render area is totally outside of the clip
820 // rect without any intersection of the partial pre-render area and the clip
821 // rect happened in previous compositions but there remains visible area of
822 // the entire transformed area.
824 // So now all four points of the transformed partial pre-render rect are
825 // outside of the clip rect, if all these four points are in either side of
826 // the clip rect, we consider it's jank so that on the main-thread we will
827 // either a) rebuild the up-to-date display item if there remains visible area
828 // or b) no longer rebuild the display item if it's totally outside of the
829 // clip rect.
831 // Note that RegionBits::Left and Right are mutually exclusive,
832 // RegionBits::Top and Bottom are also mutually exclusive, so if there remains
833 // any bits, it means all four points are in the same side.
834 return GetRegionBitsForPoint(prerenderedQuad.mPoints[0].x,
835 prerenderedQuad.mPoints[0].y, clipRect) &
836 GetRegionBitsForPoint(prerenderedQuad.mPoints[1].x,
837 prerenderedQuad.mPoints[1].y, clipRect) &
838 GetRegionBitsForPoint(prerenderedQuad.mPoints[2].x,
839 prerenderedQuad.mPoints[2].y, clipRect) &
840 GetRegionBitsForPoint(prerenderedQuad.mPoints[3].x,
841 prerenderedQuad.mPoints[3].y, clipRect);
844 } // namespace layers
845 } // namespace mozilla