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