Bug 1167519 - Part 1: Calculate plausible starting value on compositor with TimeStamp::Now() when replacing an old transtion. r?birtles
Transitions on the compositor sometimes go further ahead while the
main-thread is busy. When the transition on the compositor is replaced by a
new one, until now we calculate the current position of the old one with the
most recent refresh time. But if the replace is done on a busy frame, the
calculated position will be far from the real position on the compositor.
As a result, we can see jumping transitions after busy frames.
To mitigate this issue, we should calculate a plausible current position of
the old one with the current time just before sending the new transition to
the compositor, i.e., after all JS callback works have done.
The plausible value is stored into KeyframeEffectReadOnly::mProperties, not
into mKeyframes. If we store the value into keyframes,
KeyframeEffectReadOnly::GetKeyframes() will return a different values after
busy frames.
Note that with this patch getComputedStyle() does not return the plausible
value because this patch does not affect styling process. getComputedStyle
issue will be fixed in
bug 1273834.
MozReview-Commit-ID: B85kIx6qeyy
--- a/layout/base/nsDisplayList.cpp
+++ b/layout/base/nsDisplayList.cpp
@@ -385,16 +385,39 @@ AddAnimationForProperty(nsIFrame* aFrame
TransformReferenceBox refBox(aFrame);
layers::Animation* animation =
aPending ?
aLayer->AddAnimationForNextTransaction() :
aLayer->AddAnimation();
const TimingParams& timing = aAnimation->GetEffect()->SpecifiedTiming();
+
+ // If we are starting a new transition that replaces an existing transition
+ // running on the compositor, it is possible that the animation on the
+ // compositor will have advanced ahead of the main thread. If we use as
+ // the starting point of the new transition, the current value of the
+ // replaced transition as calculated on the main thread using the refresh
+ // driver time, the new transition will jump when it starts. Instead, we
+ // re-calculate the starting point of the new transition by applying the
+ // current TimeStamp to the parameters of the replaced transition.
+ //
+ // We need to do this here, rather than when we generate the new transition,
+ // since after generating the new transition other requestAnimationFrame
+ // callbacks may run that introduce further lag between the main thread and
+ // the compositor.
+ if (aAnimation->AsCSSTransition() &&
+ aAnimation->GetEffect()) {
+ MOZ_ASSERT(aAnimation->GetEffect()->AsTransition(),
+ "CSSTransition' effect should be an ElementPropertyTransition "
+ "until we fix bug 1049975");
+ aAnimation->GetEffect()->AsTransition()->
+ UpdateStartValueFromReplacedTransition();
+ }
+
const ComputedTiming computedTiming =
aAnimation->GetEffect()->GetComputedTiming();
Nullable<TimeDuration> startTime = aAnimation->GetCurrentOrPendingStartTime();
animation->startTime() = startTime.IsNull()
? TimeStamp()
: aAnimation->AnimationTimeToTimeStamp(
StickyTimeDuration(timing.mDelay));
animation->initialCurrentTime() = aAnimation->GetCurrentTime().Value()
--- a/layout/style/nsTransitionManager.cpp
+++ b/layout/style/nsTransitionManager.cpp
@@ -65,16 +65,57 @@ ElementPropertyTransition::CurrentValueP
"Got a null progress for a fill mode of 'both'");
MOZ_ASSERT(mKeyframes.Length() == 2,
"Should have two animation keyframes for a transition");
return ComputedTimingFunction::GetPortion(mKeyframes[0].mTimingFunction,
computedTiming.mProgress.Value(),
computedTiming.mBeforeFlag);
}
+void
+ElementPropertyTransition::UpdateStartValueFromReplacedTransition()
+{
+ if (!mReplacedTransition) {
+ return;
+ }
+ MOZ_ASSERT(nsCSSProps::PropHasFlags(TransitionProperty(),
+ CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR),
+ "The transition property should be able to be run on the "
+ "compositor");
+ MOZ_ASSERT(mTarget && mTarget->mElement->OwnerDoc(),
+ "We should have a valid document at this moment");
+
+ dom::DocumentTimeline* timeline = mTarget->mElement->OwnerDoc()->Timeline();
+ ComputedTiming computedTiming = GetComputedTimingAt(
+ dom::CSSTransition::GetCurrentTimeAt(*timeline,
+ TimeStamp::Now(),
+ mReplacedTransition->mStartTime,
+ mReplacedTransition->mPlaybackRate),
+ mReplacedTransition->mTiming);
+
+ if (!computedTiming.mProgress.IsNull()) {
+ double valuePosition =
+ ComputedTimingFunction::GetPortion(mReplacedTransition->mTimingFunction,
+ computedTiming.mProgress.Value(),
+ computedTiming.mBeforeFlag);
+ StyleAnimationValue startValue;
+ if (StyleAnimationValue::Interpolate(mProperties[0].mProperty,
+ mReplacedTransition->mFromValue,
+ mReplacedTransition->mToValue,
+ valuePosition, startValue)) {
+ MOZ_ASSERT(mProperties.Length() == 1 &&
+ mProperties[0].mSegments.Length() == 1,
+ "The transition should have one property and one segment");
+ mProperties[0].mSegments[0].mFromValue = Move(startValue);
+ }
+ }
+
+ mReplacedTransition.reset();
+}
+
////////////////////////// CSSTransition ////////////////////////////
JSObject*
CSSTransition::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
return dom::CSSTransitionBinding::Wrap(aCx, this, aGivenProto);
}
@@ -199,16 +240,33 @@ CSSTransition::HasLowerCompositeOrderTha
return mAnimationIndex < aOther.mAnimationIndex;
}
// 3. (Same transition generation): Sort by transition property
return nsCSSProps::GetStringValue(TransitionProperty()) <
nsCSSProps::GetStringValue(aOther.TransitionProperty());
}
+/* static */ Nullable<TimeDuration>
+CSSTransition::GetCurrentTimeAt(const DocumentTimeline& aTimeline,
+ const TimeStamp& aBaseTime,
+ const TimeDuration& aStartTime,
+ double aPlaybackRate)
+{
+ Nullable<TimeDuration> result;
+
+ Nullable<TimeDuration> timelineTime = aTimeline.ToTimelineTime(aBaseTime);
+ if (!timelineTime.IsNull()) {
+ result.SetValue((timelineTime.Value() - aStartTime)
+ .MultDouble(aPlaybackRate));
+ }
+
+ return result;
+}
+
////////////////////////// nsTransitionManager ////////////////////////////
NS_IMPL_CYCLE_COLLECTION(nsTransitionManager, mEventDispatcher)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(nsTransitionManager, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(nsTransitionManager, Release)
void
@@ -741,16 +799,39 @@ nsTransitionManager::ConsiderStartingTra
i == currentIndex ||
(animations[i]->GetEffect() &&
animations[i]->GetEffect()->AsTransition()->TransitionProperty()
!= aProperty),
"duplicate transitions for property");
}
#endif
if (haveCurrentTransition) {
+ // If this new transition is replacing an existing transition that is running
+ // on the compositor, we store select parameters from the replaced transition
+ // so that later, once all scripts have run, we can update the start value
+ // of the transition using TimeStamp::Now(). This allows us to avoid a
+ // large jump when starting a new transition when the main thread lags behind
+ // the compositor.
+ if (oldPT->IsCurrent() &&
+ oldPT->IsRunningOnCompositor() &&
+ !oldPT->GetAnimation()->GetStartTime().IsNull() &&
+ timeline == oldPT->GetAnimation()->GetTimeline()) {
+ const AnimationPropertySegment& segment =
+ oldPT->Properties()[0].mSegments[0];
+ pt->mReplacedTransition.emplace(
+ ElementPropertyTransition::ReplacedTransitionProperties({
+ oldPT->GetAnimation()->GetStartTime().Value(),
+ oldPT->GetAnimation()->PlaybackRate(),
+ oldPT->SpecifiedTiming(),
+ segment.mTimingFunction,
+ segment.mFromValue,
+ segment.mToValue
+ })
+ );
+ }
animations[currentIndex]->CancelFromStyle();
oldPT = nullptr; // Clear pointer so it doesn't dangle
animations[currentIndex] = animation;
} else {
if (!animations.AppendElement(animation)) {
NS_WARNING("out of memory");
return;
}
--- a/layout/style/nsTransitionManager.h
+++ b/layout/style/nsTransitionManager.h
@@ -90,16 +90,30 @@ struct ElementPropertyTransition : publi
// in again when the transition is back to 2px, the mReversePortion
// for the third transition (from 0px/2px to 10px) will be 0.8.
double mReversePortion;
// Compute the portion of the *value* space that we should be through
// at the current time. (The input to the transition timing function
// has time units, the output has value units.)
double CurrentValuePortion() const;
+
+ // For a new transition interrupting an existing transition on the
+ // compositor, update the start value to match the value of the replaced
+ // transitions at the current time.
+ void UpdateStartValueFromReplacedTransition();
+
+ struct ReplacedTransitionProperties {
+ TimeDuration mStartTime;
+ double mPlaybackRate;
+ TimingParams mTiming;
+ Maybe<ComputedTimingFunction> mTimingFunction;
+ StyleAnimationValue mFromValue, mToValue;
+ };
+ Maybe<ReplacedTransitionProperties> mReplacedTransition;
};
namespace dom {
class CSSTransition final : public Animation
{
public:
explicit CSSTransition(nsIGlobalObject* aGlobal)
@@ -179,16 +193,27 @@ public:
void SetOwningElement(const OwningElementRef& aElement)
{
mOwningElement = aElement;
}
// True for transitions that are generated from CSS markup and continue to
// reflect changes to that markup.
bool IsTiedToMarkup() const { return mOwningElement.IsSet(); }
+ // Return the animation current time based on a given TimeStamp, a given
+ // start time and a given playbackRate on a given timeline. This is useful
+ // when we estimate the current animated value running on the compositor
+ // because the animation on the compositor may be running ahead while
+ // main-thread is busy.
+ static Nullable<TimeDuration> GetCurrentTimeAt(
+ const DocumentTimeline& aTimeline,
+ const TimeStamp& aBaseTime,
+ const TimeDuration& aStartTime,
+ double aPlaybackRate);
+
protected:
virtual ~CSSTransition()
{
MOZ_ASSERT(!mOwningElement.IsSet(), "Owning element should be cleared "
"before a CSS transition is destroyed");
}
// Animation overrides