Bug 1626165 - Part 2: Replace the start value and start time of the transition on the compositor. r=layout-reviewers,firefox-animation-reviewers,hiro
Although we re-compute the start value of the transition when sending it to the compositor, we still want to use the last sampled animation value as the new start value, to avoid any possible jittery. Also, we replace the start time with the previous sample time on the compositor as well to make sure we use the proper start time for the transition if the main thread is busy. Differential Revision: https://phabricator.services.mozilla.com/D209889
This commit is contained in:
@@ -276,14 +276,14 @@ double CSSTransition::CurrentValuePortion() const {
|
|||||||
return computedTiming.mProgress.Value();
|
return computedTiming.mProgress.Value();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSSTransition::UpdateStartValueFromReplacedTransition() {
|
bool CSSTransition::UpdateStartValueFromReplacedTransition() {
|
||||||
MOZ_ASSERT(mEffect && mEffect->AsKeyframeEffect() &&
|
MOZ_ASSERT(mEffect && mEffect->AsKeyframeEffect() &&
|
||||||
mEffect->AsKeyframeEffect()->HasAnimationOfPropertySet(
|
mEffect->AsKeyframeEffect()->HasAnimationOfPropertySet(
|
||||||
nsCSSPropertyIDSet::CompositorAnimatables()),
|
nsCSSPropertyIDSet::CompositorAnimatables()),
|
||||||
"Should be called for compositor-runnable transitions");
|
"Should be called for compositor-runnable transitions");
|
||||||
|
|
||||||
if (!mReplacedTransition) {
|
if (!mReplacedTransition) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't set |mReplacedTransition| if the timeline of this transition is
|
// We don't set |mReplacedTransition| if the timeline of this transition is
|
||||||
@@ -308,6 +308,8 @@ void CSSTransition::UpdateStartValueFromReplacedTransition() {
|
|||||||
mReplacedTransition->mTimingFunction, computedTiming.mProgress.Value(),
|
mReplacedTransition->mTimingFunction, computedTiming.mProgress.Value(),
|
||||||
computedTiming.mBeforeFlag);
|
computedTiming.mBeforeFlag);
|
||||||
|
|
||||||
|
// FIXME: Bug 1634945. We may have to use the last value on the compositor
|
||||||
|
// to replace the start value.
|
||||||
const AnimationValue& replacedFrom = mReplacedTransition->mFromValue;
|
const AnimationValue& replacedFrom = mReplacedTransition->mFromValue;
|
||||||
const AnimationValue& replacedTo = mReplacedTransition->mToValue;
|
const AnimationValue& replacedTo = mReplacedTransition->mToValue;
|
||||||
AnimationValue startValue;
|
AnimationValue startValue;
|
||||||
@@ -321,6 +323,8 @@ void CSSTransition::UpdateStartValueFromReplacedTransition() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mReplacedTransition.reset();
|
mReplacedTransition.reset();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSSTransition::SetEffectFromStyle(KeyframeEffect* aEffect) {
|
void CSSTransition::SetEffectFromStyle(KeyframeEffect* aEffect) {
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class CSSTransition final : public Animation {
|
|||||||
// For a new transition interrupting an existing transition on the
|
// For a new transition interrupting an existing transition on the
|
||||||
// compositor, update the start value to match the value of the replaced
|
// compositor, update the start value to match the value of the replaced
|
||||||
// transitions at the current time.
|
// transitions at the current time.
|
||||||
void UpdateStartValueFromReplacedTransition();
|
bool UpdateStartValueFromReplacedTransition();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual ~CSSTransition() {
|
virtual ~CSSTransition() {
|
||||||
@@ -222,6 +222,8 @@ class CSSTransition final : public Animation {
|
|||||||
// for the third transition (from 0px/2px to 10px) will be 0.8.
|
// for the third transition (from 0px/2px to 10px) will be 0.8.
|
||||||
double mReversePortion = 1.0;
|
double mReversePortion = 1.0;
|
||||||
|
|
||||||
|
// The information of the old transition we'd like to replace with "this"
|
||||||
|
// transition.
|
||||||
Maybe<ReplacedTransitionProperties> mReplacedTransition;
|
Maybe<ReplacedTransitionProperties> mReplacedTransition;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
#include "AnimationHelper.h"
|
#include "AnimationHelper.h"
|
||||||
|
#include "CompositorAnimationStorage.h"
|
||||||
#include "base/process_util.h"
|
#include "base/process_util.h"
|
||||||
#include "gfx2DGlue.h" // for ThebesRect
|
#include "gfx2DGlue.h" // for ThebesRect
|
||||||
#include "gfxLineSegment.h" // for gfxLineSegment
|
#include "gfxLineSegment.h" // for gfxLineSegment
|
||||||
@@ -445,7 +446,9 @@ static bool HasTransformLikeAnimations(const AnimationArray& aAnimations) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
AnimationStorageData AnimationHelper::ExtractAnimations(
|
AnimationStorageData AnimationHelper::ExtractAnimations(
|
||||||
const LayersId& aLayersId, const AnimationArray& aAnimations) {
|
const LayersId& aLayersId, const AnimationArray& aAnimations,
|
||||||
|
const CompositorAnimationStorage* aStorage,
|
||||||
|
const TimeStamp& aPreviousSampleTime) {
|
||||||
AnimationStorageData storageData;
|
AnimationStorageData storageData;
|
||||||
storageData.mLayersId = aLayersId;
|
storageData.mLayersId = aLayersId;
|
||||||
|
|
||||||
@@ -537,12 +540,37 @@ AnimationStorageData AnimationHelper::ExtractAnimations(
|
|||||||
propertyAnimation->mScrollTimelineOptions =
|
propertyAnimation->mScrollTimelineOptions =
|
||||||
animation.scrollTimelineOptions();
|
animation.scrollTimelineOptions();
|
||||||
|
|
||||||
|
RefPtr<StyleAnimationValue> startValue;
|
||||||
|
if (animation.replacedTransitionId()) {
|
||||||
|
if (const auto* animatedValue =
|
||||||
|
aStorage->GetAnimatedValue(*animation.replacedTransitionId())) {
|
||||||
|
startValue = animatedValue->AsAnimationValue(animation.property());
|
||||||
|
// Basically, the timeline time is increasing monotonically, so it may
|
||||||
|
// not make sense to have a negative start time (i.e. the case when
|
||||||
|
// aPreviousSampleTime is behind the origin time). Therefore, if the
|
||||||
|
// previous sample time is less than the origin time, we skip the
|
||||||
|
// replacement of the start time.
|
||||||
|
if (!aPreviousSampleTime.IsNull() &&
|
||||||
|
(aPreviousSampleTime >= animation.originTime())) {
|
||||||
|
propertyAnimation->mStartTime =
|
||||||
|
Some(aPreviousSampleTime - animation.originTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
MOZ_ASSERT(animation.segments().Length() == 1,
|
||||||
|
"The CSS Transition only has one segement");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nsTArray<PropertyAnimation::SegmentData>& segmentData =
|
nsTArray<PropertyAnimation::SegmentData>& segmentData =
|
||||||
propertyAnimation->mSegments;
|
propertyAnimation->mSegments;
|
||||||
for (const AnimationSegment& segment : animation.segments()) {
|
for (const AnimationSegment& segment : animation.segments()) {
|
||||||
segmentData.AppendElement(PropertyAnimation::SegmentData{
|
segmentData.AppendElement(PropertyAnimation::SegmentData{
|
||||||
AnimationValue::FromAnimatable(animation.property(),
|
// Note that even though we re-compute the start value on the main
|
||||||
segment.startState()),
|
// thread, we still replace it with the last sampled value, to avoid
|
||||||
|
// any possible lag.
|
||||||
|
startValue ? startValue
|
||||||
|
: AnimationValue::FromAnimatable(animation.property(),
|
||||||
|
segment.startState()),
|
||||||
AnimationValue::FromAnimatable(animation.property(),
|
AnimationValue::FromAnimatable(animation.property(),
|
||||||
segment.endState()),
|
segment.endState()),
|
||||||
segment.sampleFn(), segment.startPortion(), segment.endPortion(),
|
segment.sampleFn(), segment.startPortion(), segment.endPortion(),
|
||||||
|
|||||||
@@ -145,7 +145,9 @@ class AnimationHelper {
|
|||||||
* 3. background color property: background-color.
|
* 3. background color property: background-color.
|
||||||
*/
|
*/
|
||||||
static AnimationStorageData ExtractAnimations(
|
static AnimationStorageData ExtractAnimations(
|
||||||
const LayersId& aLayersId, const AnimationArray& aAnimations);
|
const LayersId& aLayersId, const AnimationArray& aAnimations,
|
||||||
|
const CompositorAnimationStorage* aStorage,
|
||||||
|
const TimeStamp& aPreviousSampleTime);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a unique id to represent the compositor animation between child
|
* Get a unique id to represent the compositor animation between child
|
||||||
|
|||||||
@@ -418,8 +418,16 @@ void AnimationInfo::AddAnimationForProperty(
|
|||||||
// since after generating the new transition other requestAnimationFrame
|
// since after generating the new transition other requestAnimationFrame
|
||||||
// callbacks may run that introduce further lag between the main thread and
|
// callbacks may run that introduce further lag between the main thread and
|
||||||
// the compositor.
|
// the compositor.
|
||||||
|
//
|
||||||
|
// Note that we will replace the start value with the last sampled animation
|
||||||
|
// value on the compositor.
|
||||||
|
// The computation here is for updating the keyframe values, to make sure the
|
||||||
|
// computed values on the main thread don't behind the rendering result on the
|
||||||
|
// compositor too much.
|
||||||
|
bool needReplaceTransition = false;
|
||||||
if (dom::CSSTransition* cssTransition = aAnimation->AsCSSTransition()) {
|
if (dom::CSSTransition* cssTransition = aAnimation->AsCSSTransition()) {
|
||||||
cssTransition->UpdateStartValueFromReplacedTransition();
|
needReplaceTransition =
|
||||||
|
cssTransition->UpdateStartValueFromReplacedTransition();
|
||||||
}
|
}
|
||||||
|
|
||||||
animation->originTime() =
|
animation->originTime() =
|
||||||
@@ -463,6 +471,12 @@ void AnimationInfo::AddAnimationForProperty(
|
|||||||
animation->isNotAnimating() = false;
|
animation->isNotAnimating() = false;
|
||||||
animation->scrollTimelineOptions() =
|
animation->scrollTimelineOptions() =
|
||||||
GetScrollTimelineOptions(aAnimation->GetTimeline());
|
GetScrollTimelineOptions(aAnimation->GetTimeline());
|
||||||
|
// We set this flag to let the compositor know that the start value of this
|
||||||
|
// transition is replaced. The compositor may replace the start value with its
|
||||||
|
// last sampled animation value, instead of using the segment.mFromValue we
|
||||||
|
// send to the compositor, to avoid any potential lag.
|
||||||
|
animation->replacedTransitionId() =
|
||||||
|
needReplaceTransition ? Some(GetCompositorAnimationsId()) : Nothing();
|
||||||
|
|
||||||
TransformReferenceBox refBox(aFrame);
|
TransformReferenceBox refBox(aFrame);
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,31 @@ namespace layers {
|
|||||||
|
|
||||||
using gfx::Matrix4x4;
|
using gfx::Matrix4x4;
|
||||||
|
|
||||||
|
already_AddRefed<StyleAnimationValue> AnimatedValue::AsAnimationValue(
|
||||||
|
nsCSSPropertyID aProperty) const {
|
||||||
|
RefPtr<StyleAnimationValue> result;
|
||||||
|
mValue.match(
|
||||||
|
[&](const AnimationTransform& aTransform) {
|
||||||
|
// Linear search. It's likely that the length of the array is one in
|
||||||
|
// most common case, so it shouldn't have much performance impact.
|
||||||
|
for (const auto& value : Transform().mAnimationValues) {
|
||||||
|
AnimatedPropertyID property(eCSSProperty_UNKNOWN);
|
||||||
|
Servo_AnimationValue_GetPropertyId(value, &property);
|
||||||
|
if (property.mID == aProperty) {
|
||||||
|
result = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[&](const float& aOpacity) {
|
||||||
|
result = Servo_AnimationValue_Opacity(aOpacity).Consume();
|
||||||
|
},
|
||||||
|
[&](const nscolor& aColor) {
|
||||||
|
result = Servo_AnimationValue_Color(aProperty, aColor).Consume();
|
||||||
|
});
|
||||||
|
return result.forget();
|
||||||
|
}
|
||||||
|
|
||||||
void CompositorAnimationStorage::ClearById(const uint64_t& aId) {
|
void CompositorAnimationStorage::ClearById(const uint64_t& aId) {
|
||||||
MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
|
MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
|
||||||
MutexAutoLock lock(mLock);
|
MutexAutoLock lock(mLock);
|
||||||
@@ -120,20 +145,21 @@ OMTAValue CompositorAnimationStorage::GetOMTAValue(const uint64_t& aId) const {
|
|||||||
|
|
||||||
void CompositorAnimationStorage::SetAnimatedValue(
|
void CompositorAnimationStorage::SetAnimatedValue(
|
||||||
uint64_t aId, AnimatedValue* aPreviousValue,
|
uint64_t aId, AnimatedValue* aPreviousValue,
|
||||||
const gfx::Matrix4x4& aFrameTransform, const TransformData& aData) {
|
const gfx::Matrix4x4& aFrameTransform, const TransformData& aData,
|
||||||
|
SampledAnimationArray&& aValue) {
|
||||||
mLock.AssertCurrentThreadOwns();
|
mLock.AssertCurrentThreadOwns();
|
||||||
|
|
||||||
if (!aPreviousValue) {
|
if (!aPreviousValue) {
|
||||||
MOZ_ASSERT(!mAnimatedValues.Contains(aId));
|
MOZ_ASSERT(!mAnimatedValues.Contains(aId));
|
||||||
mAnimatedValues.InsertOrUpdate(
|
mAnimatedValues.InsertOrUpdate(
|
||||||
aId,
|
aId, MakeUnique<AnimatedValue>(gfx::Matrix4x4(), aFrameTransform, aData,
|
||||||
MakeUnique<AnimatedValue>(gfx::Matrix4x4(), aFrameTransform, aData));
|
std::move(aValue)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MOZ_ASSERT(aPreviousValue->Is<AnimationTransform>());
|
MOZ_ASSERT(aPreviousValue->Is<AnimationTransform>());
|
||||||
MOZ_ASSERT(aPreviousValue == GetAnimatedValue(aId));
|
MOZ_ASSERT(aPreviousValue == GetAnimatedValue(aId));
|
||||||
|
|
||||||
aPreviousValue->SetTransform(aFrameTransform, aData);
|
aPreviousValue->SetTransform(aFrameTransform, aData, std::move(aValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompositorAnimationStorage::SetAnimatedValue(uint64_t aId,
|
void CompositorAnimationStorage::SetAnimatedValue(uint64_t aId,
|
||||||
@@ -168,14 +194,15 @@ void CompositorAnimationStorage::SetAnimatedValue(uint64_t aId,
|
|||||||
aPreviousValue->SetOpacity(aOpacity);
|
aPreviousValue->SetOpacity(aOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompositorAnimationStorage::SetAnimations(uint64_t aId,
|
void CompositorAnimationStorage::SetAnimations(
|
||||||
const LayersId& aLayersId,
|
uint64_t aId, const LayersId& aLayersId, const AnimationArray& aValue,
|
||||||
const AnimationArray& aValue) {
|
const TimeStamp& aPreviousSampleTime) {
|
||||||
MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
|
MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
|
||||||
MutexAutoLock lock(mLock);
|
MutexAutoLock lock(mLock);
|
||||||
|
|
||||||
mAnimations[aId] = std::make_unique<AnimationStorageData>(
|
mAnimations[aId] =
|
||||||
AnimationHelper::ExtractAnimations(aLayersId, aValue));
|
std::make_unique<AnimationStorageData>(AnimationHelper::ExtractAnimations(
|
||||||
|
aLayersId, aValue, this, aPreviousSampleTime));
|
||||||
|
|
||||||
PROFILER_MARKER("SetAnimation", GRAPHICS,
|
PROFILER_MARKER("SetAnimation", GRAPHICS,
|
||||||
MarkerInnerWindowId(mCompositorBridge->GetInnerWindowId()),
|
MarkerInnerWindowId(mCompositorBridge->GetInnerWindowId()),
|
||||||
@@ -277,7 +304,8 @@ void CompositorAnimationStorage::StoreAnimatedValue(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetAnimatedValue(aId, aAnimatedValueEntry, frameTransform, transformData);
|
SetAnimatedValue(aId, aAnimatedValueEntry, frameTransform, transformData,
|
||||||
|
std::move(aAnimationValues));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ struct AnimationTransform {
|
|||||||
*/
|
*/
|
||||||
gfx::Matrix4x4 mFrameTransform;
|
gfx::Matrix4x4 mFrameTransform;
|
||||||
TransformData mData;
|
TransformData mData;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Store the previous sampled transform-like animation values.
|
||||||
|
* It's unfortunate we have to keep the previous sampled animation value for
|
||||||
|
* replacing the running transition, because we can not re-create the
|
||||||
|
* AnimationValues from the matrix.
|
||||||
|
* Note: We expect the length is one in most cases.
|
||||||
|
*/
|
||||||
|
SampledAnimationArray mAnimationValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AnimatedValue final {
|
struct AnimatedValue final {
|
||||||
@@ -57,22 +66,27 @@ struct AnimatedValue final {
|
|||||||
|
|
||||||
AnimatedValue(const gfx::Matrix4x4& aTransformInDevSpace,
|
AnimatedValue(const gfx::Matrix4x4& aTransformInDevSpace,
|
||||||
const gfx::Matrix4x4& aFrameTransform,
|
const gfx::Matrix4x4& aFrameTransform,
|
||||||
const TransformData& aData)
|
const TransformData& aData, SampledAnimationArray&& aValue)
|
||||||
: mValue(AsVariant(AnimationTransform{aTransformInDevSpace,
|
: mValue(AsVariant(AnimationTransform{
|
||||||
aFrameTransform, aData})) {}
|
aTransformInDevSpace, aFrameTransform, aData, std::move(aValue)})) {
|
||||||
|
}
|
||||||
|
|
||||||
explicit AnimatedValue(const float& aValue) : mValue(AsVariant(aValue)) {}
|
explicit AnimatedValue(const float& aValue) : mValue(AsVariant(aValue)) {}
|
||||||
|
|
||||||
explicit AnimatedValue(nscolor aValue) : mValue(AsVariant(aValue)) {}
|
explicit AnimatedValue(nscolor aValue) : mValue(AsVariant(aValue)) {}
|
||||||
|
|
||||||
|
// Note: Only transforms need to store the sampled AnimationValue because it's
|
||||||
|
// impossible to re-create the AnimationValue from the matrix.
|
||||||
void SetTransform(const gfx::Matrix4x4& aFrameTransform,
|
void SetTransform(const gfx::Matrix4x4& aFrameTransform,
|
||||||
const TransformData& aData) {
|
const TransformData& aData,
|
||||||
|
SampledAnimationArray&& aValue) {
|
||||||
MOZ_ASSERT(mValue.is<AnimationTransform>());
|
MOZ_ASSERT(mValue.is<AnimationTransform>());
|
||||||
AnimationTransform& previous = mValue.as<AnimationTransform>();
|
AnimationTransform& previous = mValue.as<AnimationTransform>();
|
||||||
previous.mFrameTransform = aFrameTransform;
|
previous.mFrameTransform = aFrameTransform;
|
||||||
if (previous.mData != aData) {
|
if (previous.mData != aData) {
|
||||||
previous.mData = aData;
|
previous.mData = aData;
|
||||||
}
|
}
|
||||||
|
previous.mAnimationValues = std::move(aValue);
|
||||||
}
|
}
|
||||||
void SetOpacity(float aOpacity) {
|
void SetOpacity(float aOpacity) {
|
||||||
MOZ_ASSERT(mValue.is<float>());
|
MOZ_ASSERT(mValue.is<float>());
|
||||||
@@ -83,6 +97,8 @@ struct AnimatedValue final {
|
|||||||
mValue.as<nscolor>() = aColor;
|
mValue.as<nscolor>() = aColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
already_AddRefed<StyleAnimationValue> AsAnimationValue(nsCSSPropertyID) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AnimatedValueType mValue;
|
AnimatedValueType mValue;
|
||||||
};
|
};
|
||||||
@@ -129,7 +145,8 @@ class CompositorAnimationStorage final {
|
|||||||
* Set the animations based on the unique id
|
* Set the animations based on the unique id
|
||||||
*/
|
*/
|
||||||
void SetAnimations(uint64_t aId, const LayersId& aLayersId,
|
void SetAnimations(uint64_t aId, const LayersId& aLayersId,
|
||||||
const AnimationArray& aAnimations);
|
const AnimationArray& aAnimations,
|
||||||
|
const TimeStamp& aPreviousSampleTime);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sample animation based the given timestamps and store them in this
|
* Sample animation based the given timestamps and store them in this
|
||||||
@@ -154,14 +171,14 @@ class CompositorAnimationStorage final {
|
|||||||
*/
|
*/
|
||||||
void ClearById(const uint64_t& aId);
|
void ClearById(const uint64_t& aId);
|
||||||
|
|
||||||
private:
|
|
||||||
~CompositorAnimationStorage() = default;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the animated value if a given id can map to its animated value
|
* Return the animated value if a given id can map to its animated value
|
||||||
*/
|
*/
|
||||||
AnimatedValue* GetAnimatedValue(const uint64_t& aId) const;
|
AnimatedValue* GetAnimatedValue(const uint64_t& aId) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
~CompositorAnimationStorage() = default;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the animation transform based on the unique id and also
|
* Set the animation transform based on the unique id and also
|
||||||
* set up |aFrameTransform| and |aData| for OMTA testing.
|
* set up |aFrameTransform| and |aData| for OMTA testing.
|
||||||
@@ -171,7 +188,8 @@ class CompositorAnimationStorage final {
|
|||||||
*/
|
*/
|
||||||
void SetAnimatedValue(uint64_t aId, AnimatedValue* aPreviousValue,
|
void SetAnimatedValue(uint64_t aId, AnimatedValue* aPreviousValue,
|
||||||
const gfx::Matrix4x4& aFrameTransform,
|
const gfx::Matrix4x4& aFrameTransform,
|
||||||
const TransformData& aData);
|
const TransformData& aData,
|
||||||
|
SampledAnimationArray&& aValue);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Similar to above but for opacity.
|
* Similar to above but for opacity.
|
||||||
|
|||||||
@@ -240,6 +240,8 @@ struct Animation {
|
|||||||
Animatable baseStyle;
|
Animatable baseStyle;
|
||||||
// An optional data specific for transform like properies.
|
// An optional data specific for transform like properies.
|
||||||
TransformData? transformData;
|
TransformData? transformData;
|
||||||
|
// This indicates that a transition whose start value should be replaced.
|
||||||
|
uint64_t? replacedTransitionId;
|
||||||
// If this is present, the animation is driven by a ScrollTimeline, and
|
// If this is present, the animation is driven by a ScrollTimeline, and
|
||||||
// this structure contains information about that timeline.
|
// this structure contains information about that timeline.
|
||||||
ScrollTimelineOptions? scrollTimelineOptions;
|
ScrollTimelineOptions? scrollTimelineOptions;
|
||||||
|
|||||||
@@ -167,8 +167,8 @@ void OMTASampler::SetAnimations(
|
|||||||
const nsTArray<layers::Animation>& aAnimations) {
|
const nsTArray<layers::Animation>& aAnimations) {
|
||||||
MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
|
MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
|
||||||
MutexAutoLock lock(mStorageLock);
|
MutexAutoLock lock(mStorageLock);
|
||||||
|
MutexAutoLock timeLock(mSampleTimeLock);
|
||||||
mAnimStorage->SetAnimations(aId, aLayersId, aAnimations);
|
mAnimStorage->SetAnimations(aId, aLayersId, aAnimations, mPreviousSampleTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OMTASampler::HasAnimations() const {
|
bool OMTASampler::HasAnimations() const {
|
||||||
|
|||||||
@@ -208,6 +208,90 @@ function findKeyframesRule(name) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOMTAWorking() {
|
||||||
|
function waitForDocumentLoad() {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (document.readyState === "complete") {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
window.addEventListener("load", resolve);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPaintListener() {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (typeof window.waitForAllPaints !== "function") {
|
||||||
|
var script = document.createElement("script");
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = function () {
|
||||||
|
reject(new Error("Failed to load paint listener"));
|
||||||
|
};
|
||||||
|
script.src = "/tests/SimpleTest/paint_listener.js";
|
||||||
|
var firstScript = document.scripts[0];
|
||||||
|
firstScript.parentNode.insertBefore(script, firstScript);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create keyframes rule
|
||||||
|
const animationName = "a6ce3091ed85"; // Random name to avoid clashes
|
||||||
|
var ruleText =
|
||||||
|
"@keyframes " +
|
||||||
|
animationName +
|
||||||
|
" { from { opacity: 0.5 } to { opacity: 0.5 } }";
|
||||||
|
var style = document.createElement("style");
|
||||||
|
style.appendChild(document.createTextNode(ruleText));
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// Create animation target
|
||||||
|
var div = document.createElement("div");
|
||||||
|
document.body.appendChild(div);
|
||||||
|
|
||||||
|
// Give the target geometry so it is eligible for layerization
|
||||||
|
div.style.width = "100px";
|
||||||
|
div.style.height = "100px";
|
||||||
|
div.style.backgroundColor = "white";
|
||||||
|
|
||||||
|
var utils = SpecialPowers.DOMWindowUtils;
|
||||||
|
|
||||||
|
// Common clean up code
|
||||||
|
var cleanUp = function () {
|
||||||
|
div.remove();
|
||||||
|
style.remove();
|
||||||
|
if (utils.isTestControllingRefreshes) {
|
||||||
|
utils.restoreNormalRefresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return waitForDocumentLoad()
|
||||||
|
.then(loadPaintListener)
|
||||||
|
.then(function () {
|
||||||
|
// Put refresh driver under test control and flush all pending style,
|
||||||
|
// layout and paint to avoid the situation that waitForPaintsFlush()
|
||||||
|
// receives unexpected MozAfterpaint event for those pending
|
||||||
|
// notifications.
|
||||||
|
utils.advanceTimeAndRefresh(0);
|
||||||
|
return waitForPaintsFlushed();
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
div.style.animation = animationName + " 10s";
|
||||||
|
|
||||||
|
return waitForPaintsFlushed();
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
var opacity = utils.getOMTAStyle(div, "opacity");
|
||||||
|
cleanUp();
|
||||||
|
return Promise.resolve(opacity == 0.5);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
cleanUp();
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Checks if off-main thread animation (OMTA) is available, and if it is, runs
|
// Checks if off-main thread animation (OMTA) is available, and if it is, runs
|
||||||
// the provided callback function. If OMTA is not available or is not
|
// the provided callback function. If OMTA is not available or is not
|
||||||
// functioning correctly, the second callback, aOnSkip, is run instead.
|
// functioning correctly, the second callback, aOnSkip, is run instead.
|
||||||
@@ -261,88 +345,6 @@ function runOMTATest(aTestFunction, aOnSkip, specialPowersForPrefs) {
|
|||||||
ok(false, err);
|
ok(false, err);
|
||||||
aOnSkip();
|
aOnSkip();
|
||||||
});
|
});
|
||||||
|
|
||||||
function isOMTAWorking() {
|
|
||||||
// Create keyframes rule
|
|
||||||
const animationName = "a6ce3091ed85"; // Random name to avoid clashes
|
|
||||||
var ruleText =
|
|
||||||
"@keyframes " +
|
|
||||||
animationName +
|
|
||||||
" { from { opacity: 0.5 } to { opacity: 0.5 } }";
|
|
||||||
var style = document.createElement("style");
|
|
||||||
style.appendChild(document.createTextNode(ruleText));
|
|
||||||
document.head.appendChild(style);
|
|
||||||
|
|
||||||
// Create animation target
|
|
||||||
var div = document.createElement("div");
|
|
||||||
document.body.appendChild(div);
|
|
||||||
|
|
||||||
// Give the target geometry so it is eligible for layerization
|
|
||||||
div.style.width = "100px";
|
|
||||||
div.style.height = "100px";
|
|
||||||
div.style.backgroundColor = "white";
|
|
||||||
|
|
||||||
// Common clean up code
|
|
||||||
var cleanUp = function () {
|
|
||||||
div.remove();
|
|
||||||
style.remove();
|
|
||||||
if (utils.isTestControllingRefreshes) {
|
|
||||||
utils.restoreNormalRefresh();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return waitForDocumentLoad()
|
|
||||||
.then(loadPaintListener)
|
|
||||||
.then(function () {
|
|
||||||
// Put refresh driver under test control and flush all pending style,
|
|
||||||
// layout and paint to avoid the situation that waitForPaintsFlush()
|
|
||||||
// receives unexpected MozAfterpaint event for those pending
|
|
||||||
// notifications.
|
|
||||||
utils.advanceTimeAndRefresh(0);
|
|
||||||
return waitForPaintsFlushed();
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
div.style.animation = animationName + " 10s";
|
|
||||||
|
|
||||||
return waitForPaintsFlushed();
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
var opacity = utils.getOMTAStyle(div, "opacity");
|
|
||||||
cleanUp();
|
|
||||||
return Promise.resolve(opacity == 0.5);
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
cleanUp();
|
|
||||||
return Promise.reject(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitForDocumentLoad() {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
if (document.readyState === "complete") {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
window.addEventListener("load", resolve);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPaintListener() {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
if (typeof window.waitForAllPaints !== "function") {
|
|
||||||
var script = document.createElement("script");
|
|
||||||
script.onload = resolve;
|
|
||||||
script.onerror = function () {
|
|
||||||
reject(new Error("Failed to load paint listener"));
|
|
||||||
};
|
|
||||||
script.src = "/tests/SimpleTest/paint_listener.js";
|
|
||||||
var firstScript = document.scripts[0];
|
|
||||||
firstScript.parentNode.insertBefore(script, firstScript);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common architecture for setting up a series of asynchronous animation tests
|
// Common architecture for setting up a series of asynchronous animation tests
|
||||||
|
|||||||
@@ -696,6 +696,9 @@ fail-if = ["xorigin"]
|
|||||||
|
|
||||||
["test_transitions_replacement_on_busy_frame.html"]
|
["test_transitions_replacement_on_busy_frame.html"]
|
||||||
|
|
||||||
|
["test_transitions_replacement_on_busy_frame_omta.html"]
|
||||||
|
skip-if = ["os == 'linux' && tsan && verify"] # intermittent in chaos mode
|
||||||
|
|
||||||
["test_transitions_replacement_with_setKeyframes.html"]
|
["test_transitions_replacement_with_setKeyframes.html"]
|
||||||
|
|
||||||
["test_transitions_step_functions.html"]
|
["test_transitions_step_functions.html"]
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<!--
|
||||||
|
https://bugzilla.mozilla.org/show_bug.cgi?id=1626165
|
||||||
|
-->
|
||||||
|
<head>
|
||||||
|
<meta charset=utf-8>
|
||||||
|
<title>Test for bug 1626165</title>
|
||||||
|
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||||
|
<script src="/tests/SimpleTest/paint_listener.js"></script>
|
||||||
|
<script src="animation_utils.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
|
||||||
|
<style>
|
||||||
|
#target {
|
||||||
|
height: 100px;
|
||||||
|
width: 100px;
|
||||||
|
background: green;
|
||||||
|
transition: translate 2s steps(2, start);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="target"></div>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
SimpleTest.waitForExplicitFinish();
|
||||||
|
|
||||||
|
const OMTAPrefKey = 'layers.offmainthreadcomposition.async-animations';
|
||||||
|
const omtaEnabled =
|
||||||
|
SpecialPowers.DOMWindowUtils.layerManagerRemote &&
|
||||||
|
SpecialPowers.getBoolPref(OMTAPrefKey);
|
||||||
|
|
||||||
|
function waitForAnimationFrames(aFrameCount) {
|
||||||
|
const timeAtStart = window.document.timeline.currentTime;
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
function handleFrame() {
|
||||||
|
if (
|
||||||
|
timeAtStart != window.document.timeline.currentTime &&
|
||||||
|
--aFrameCount <= 0
|
||||||
|
) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
window.requestAnimationFrame(handleFrame); // wait another frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame(handleFrame);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', async function() {
|
||||||
|
if (!omtaEnabled) {
|
||||||
|
ok(true, 'Skipping the test since OMTA is disabled');
|
||||||
|
SimpleTest.finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await isOMTAWorking();
|
||||||
|
} catch (error) {
|
||||||
|
ok(true, 'Skipping the test since OMTA may have issues');
|
||||||
|
SimpleTest.finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const div = document.getElementById('target');
|
||||||
|
|
||||||
|
// Start first transition
|
||||||
|
div.style.translate = '400px';
|
||||||
|
const firstTransition = div.getAnimations()[0];
|
||||||
|
|
||||||
|
// Wait for first transition to start running on the main thread and
|
||||||
|
// compositor.
|
||||||
|
await firstTransition.ready;
|
||||||
|
await waitForPaintsFlushed();
|
||||||
|
|
||||||
|
// Wait for some frames to make sure we have OMTA style there, to avoid the
|
||||||
|
// possible intermittent (on Android especially).
|
||||||
|
await waitForAnimationFrames(10);
|
||||||
|
|
||||||
|
// We create a transition from 0px to 400px, so the current value is 200px
|
||||||
|
// (i.e. 50%).
|
||||||
|
let matrix = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, "translate");
|
||||||
|
ok(matricesRoughlyEqual(convertTo3dMatrix(matrix),
|
||||||
|
convertTo3dMatrix("matrix(1, 0, 0, 1, 200, 0)")),
|
||||||
|
"The current value of the 1st transition after ready");
|
||||||
|
|
||||||
|
// Start second transition
|
||||||
|
div.style.translate = '600px';
|
||||||
|
const secondTransition = div.getAnimations()[0];
|
||||||
|
|
||||||
|
// Tie up main thread for 1200ms. In the meantime, the first transition
|
||||||
|
// will continue running on the compositor. If we don't update the start
|
||||||
|
// point of the second transition, it will appear to jump when it starts.
|
||||||
|
const startTime = performance.now();
|
||||||
|
while (performance.now() - startTime < 1200);
|
||||||
|
|
||||||
|
await waitForPaintsFlushed();
|
||||||
|
|
||||||
|
// We should create a transition from 400px to 600px, so the final current
|
||||||
|
// value is 500px (i.e. 50%).
|
||||||
|
matrix = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, "translate");
|
||||||
|
ok(matricesRoughlyEqual(convertTo3dMatrix(matrix),
|
||||||
|
convertTo3dMatrix("matrix(1, 0, 0, 1, 500, 0)")),
|
||||||
|
"The current value of the 2nd transition after replacing the start value");
|
||||||
|
|
||||||
|
SimpleTest.finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user