[skottie] Lazy adapter sync

Expand the core animator logic to return whether the computed value is
changing on each tick.  Also rename tick/onTick -> seek/onSeek to better
reflect Skottie semantics.

This information allows us to skip adapter updates for static/hold
animation segments.

This effectively hoists some of the scene graph lazy-update logic to the
Skottie model level, and culls unneeded conversions (e.g. we were
converting ShapeValue -> SkPath on every tick, even when the shape was
not changing).

TBR=
Change-Id: I1ea4e19ae8f993d659826269de6b0465fec70189
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/279816
Reviewed-by: Mike Reed <reed@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
This commit is contained in:
Florin Malita 2020-03-27 09:49:03 -04:00 committed by Skia Commit-Bot
parent 34ed73b038
commit 0147de41c4
19 changed files with 116 additions and 61 deletions

View File

@ -122,7 +122,7 @@ sk_sp<sksg::Transform> AnimationBuilder::attachCamera(const skjson::ObjectValue&
auto adapter = sk_make_sp<CameraAdaper>(jlayer, jtransform, *this, viewport_size);
if (adapter->isStatic()) {
adapter->tick(0);
adapter->seek(0);
} else {
fCurrentAnimatorScope->push_back(adapter);
}

View File

@ -55,9 +55,12 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachNestedAnimation(const char* name
}
protected:
void onTick(float t) {
bool onSeek(float t) {
// TODO: we prolly need more sophisticated timeline mapping for nested animations.
fAnimation->seek(t * fTimeScale);
// TODO: bubble the real update status to clients?
return true;
}
private:

View File

@ -223,11 +223,13 @@ public:
, fOut(out) {}
protected:
void onTick(float t) override {
bool onSeek(float t) override {
// in/out may be inverted for time-reversed layers
const auto active = (t >= fIn && t < fOut) || (t > fOut && t <= fIn);
auto updated = false;
if (fLayerNode) {
updated |= (fLayerNode->isVisible() != active);
fLayerNode->setVisible(active);
}
@ -237,8 +239,10 @@ protected:
const auto dispatch_count = active ? fLayerAnimators.size()
: fTransformAnimatorsCount;
for (size_t i = 0; i < dispatch_count; ++i) {
fLayerAnimators[i]->tick(t);
updated |= fLayerAnimators[i]->seek(t);
}
return updated;
}
private:
@ -258,8 +262,9 @@ protected:
// When motion blur is present, time ticks are not passed to layer animators
// but to the motion blur effect. The effect then drives the animators/scene-graph
// during reval and render phases.
void onTick(float t) override {
bool onSeek(float t) override {
fMotionBlurEffect->setT(t);
return true;
}
private:

View File

@ -132,7 +132,7 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachOpacity(const skjson::ObjectValu
const auto dispatched = this->dispatchOpacityProperty(adapter->node());
if (adapter->isStatic()) {
adapter->tick(0);
adapter->seek(0);
if (!dispatched && adapter->node()->getOpacity() >= 1) {
// No obeservable effects - we can discard.
return child_node;
@ -488,7 +488,7 @@ void Animation::seekFrame(double t, sksg::InvalidationController* ic) {
comp_time = SkTPin<float>(fInPoint + t, fInPoint, kLastValidFrame);
for (const auto& anim : fAnimators) {
anim->tick(comp_time);
anim->seek(comp_time);
}
fScene->revalidate(ic);

View File

@ -119,7 +119,7 @@ public:
void attachDiscardableAdapter(sk_sp<T> adapter) const {
if (adapter->isStatic()) {
// Fire off a synthetic tick to force a single SG sync before discarding.
adapter->tick(0);
adapter->seek(0);
} else {
fCurrentAnimatorScope->push_back(std::move(adapter));
}

View File

@ -31,17 +31,6 @@ bool ValueTraits<Vec2Value>::FromJSON(const skjson::Value& jv, const internal::A
return Parse(jv, v);
}
template <>
bool ValueTraits<ScalarValue>::CanLerp(const ScalarValue&, const ScalarValue&) {
return true;
}
template <>
void ValueTraits<ScalarValue>::Lerp(const ScalarValue& v0, const ScalarValue& v1, float t,
ScalarValue* result) {
*result = v0 + (v1 - v0) * t;
}
template <>
template <>
SkScalar ValueTraits<ScalarValue>::As<SkScalar>(const ScalarValue& v) {
@ -191,24 +180,31 @@ static SkPoint lerp_point(const SkPoint& v0, const SkPoint& v1, const Sk2f& t) {
}
template <>
void ValueTraits<ShapeValue>::Lerp(const ShapeValue& v0, const ShapeValue& v1, float t,
bool ValueTraits<ShapeValue>::Lerp(const ShapeValue& v0, const ShapeValue& v1, float t,
ShapeValue* result) {
SkASSERT(v0.fVertices.size() == v1.fVertices.size());
SkASSERT(v0.fClosed == v1.fClosed);
result->fClosed = v0.fClosed;
auto updated = (result->fClosed != v0.fClosed || !result->fVolatile);
result->fClosed = v0.fClosed;
result->fVolatile = true; // interpolated values are volatile
const auto t2f = Sk2f(t);
result->fVertices.resize(v0.fVertices.size());
for (size_t i = 0; i < v0.fVertices.size(); ++i) {
result->fVertices[i] = BezierVertex({
const auto v = BezierVertex({
lerp_point(v0.fVertices[i].fInPoint , v1.fVertices[i].fInPoint , t2f),
lerp_point(v0.fVertices[i].fOutPoint, v1.fVertices[i].fOutPoint, t2f),
lerp_point(v0.fVertices[i].fVertex , v1.fVertices[i].fVertex , t2f)
});
updated |= (v != result->fVertices[i]);
result->fVertices[i] = v;
}
return updated;
}
template <>

View File

@ -33,7 +33,7 @@ struct ValueTraits {
static U As(const T&);
static bool CanLerp(const T&, const T&);
static void Lerp(const T&, const T&, float, T*);
static bool Lerp(const T&, const T&, float, T*);
};
using ScalarValue = SkScalar;

View File

@ -114,7 +114,7 @@ sk_sp<sksg::Transform> AnimationBuilder::attachMatrix2D(const skjson::ObjectValu
// The transform has no observable effects - we can discard.
return parent;
}
adapter->tick(0);
adapter->seek(0);
} else {
fCurrentAnimatorScope->push_back(adapter);
}
@ -182,7 +182,7 @@ sk_sp<sksg::Transform> AnimationBuilder::attachMatrix3D(const skjson::ObjectValu
// The transform has no observable effects - we can discard.
return parent;
}
adapter->tick(0);
adapter->seek(0);
} else {
fCurrentAnimatorScope->push_back(adapter);
}

View File

@ -13,11 +13,20 @@
namespace skottie::internal {
void AnimatablePropertyContainer::onTick(float t) {
bool AnimatablePropertyContainer::onSeek(float t) {
// The very first seek must trigger a sync, to ensure proper SG setup.
bool dirty = !fHasSynced;
for (const auto& animator : fAnimators) {
animator->tick(t);
dirty |= animator->seek(t);
}
this->onSync();
if (dirty) {
this->onSync();
fHasSynced = true;
}
return dirty;
}
void AnimatablePropertyContainer::attachDiscardableAdapter(
@ -27,7 +36,7 @@ void AnimatablePropertyContainer::attachDiscardableAdapter(
}
if (child->isStatic()) {
child->tick(0);
child->seek(0);
return;
}
@ -83,7 +92,7 @@ bool AnimatablePropertyContainer::bindImpl(const AnimationBuilder& abuilder,
if (animator->isConstant()) {
// If all keyframes are constant, there is no reason to treat this
// as an animated property - apply immediately and discard the animator.
animator->tick(0);
animator->seek(0);
} else {
fAnimators.push_back(std::move(animator));
}

View File

@ -28,12 +28,13 @@ class Animator : public SkRefCnt {
public:
virtual ~Animator() = default;
void tick(float t) { this->onTick(t); }
// Returns true if the state has changed.
bool seek(float t) { return this->onSeek(t); }
protected:
Animator() = default;
virtual void onTick(float t) = 0;
virtual bool onSeek(float t) = 0;
private:
Animator(const Animator&) = delete;
@ -63,7 +64,7 @@ protected:
void attachDiscardableAdapter(sk_sp<AnimatablePropertyContainer>);
private:
void onTick(float) final;
bool onSeek(float) final;
bool bindImpl(const AnimationBuilder&,
const skjson::ObjectValue*,
@ -71,6 +72,7 @@ private:
void*);
std::vector<sk_sp<Animator>> fAnimators;
bool fHasSynced = false;
};
} // namespace internal

View File

@ -285,17 +285,23 @@ private:
, fValues(std::move(vs))
, fTarget(target_value) {}
void onTick(float t) override {
bool onSeek(float t) override {
const auto& lerp_info = this->getLERPInfo(t);
bool updated;
if (lerp_info.isConstant()) {
*fTarget = fValues[SkToSizeT(lerp_info.vrec0.idx)];
updated = (*fTarget != fValues[SkToSizeT(lerp_info.vrec0.idx)]);
if (updated) {
*fTarget = fValues[SkToSizeT(lerp_info.vrec0.idx)];
}
} else {
ValueTraits<T>::Lerp(fValues[lerp_info.vrec0.idx],
fValues[lerp_info.vrec1.idx],
lerp_info.weight,
fTarget);
updated = ValueTraits<T>::Lerp(fValues[lerp_info.vrec0.idx],
fValues[lerp_info.vrec1.idx],
lerp_info.weight,
fTarget);
}
return updated;
}
const std::vector<T> fValues;

View File

@ -54,10 +54,13 @@ private:
: INHERITED(std::move(kfs), std::move(cms))
, fTarget(target_value) {}
void onTick(float t) override {
bool onSeek(float t) override {
const auto& lerp_info = this->getLERPInfo(t);
const auto old_value = *fTarget;
*fTarget = Lerp(lerp_info.vrec0.flt, lerp_info.vrec1.flt, lerp_info.weight);
return *fTarget != old_value;
}
ScalarValue* fTarget;

View File

@ -132,7 +132,14 @@ private:
, fValues(std::move(vs))
, fTarget(target_value) {}
void onTick(float t) override {
bool update(const Vec2Value& new_value) {
const auto updated = (new_value != *fTarget);
*fTarget = new_value;
return updated;
}
bool onSeek(float t) override {
const auto& lerp_info = this->getLERPInfo(t);
const auto& v0 = fValues[lerp_info.vrec0.idx];
@ -141,13 +148,12 @@ private:
// arc length.
SkPoint pos;
if (v0.cmeasure->getPosTan(lerp_info.weight * v0.cmeasure->length(), &pos, nullptr)) {
*fTarget = { pos.fX, pos.fY };
return;
return this->update({ pos.fX, pos.fY });
}
}
const auto& v1 = fValues[lerp_info.vrec1.idx];
*fTarget = Lerp(v0.v2, v1.v2, lerp_info.weight);
return this->update(Lerp(v0.v2, v1.v2, lerp_info.weight));
}
const std::vector<SpatialValue> fValues;

View File

@ -161,7 +161,7 @@ private:
fTarget->resize(fVecLen);
}
void onTick(float t) override {
bool onSeek(float t) override {
const auto& lerp_info = this->getLERPInfo(t);
SkASSERT(lerp_info.vrec0.idx + fVecLen <= fStorage.size());
@ -173,15 +173,22 @@ private:
auto* dst = fTarget->data();
if (lerp_info.isConstant()) {
std::copy(v0, v0 + fVecLen, dst);
return;
if (std::memcmp(dst, v0, fVecLen * sizeof(float))) {
std::copy(v0, v0 + fVecLen, dst);
return true;
}
return false;
}
size_t count = fVecLen;
bool updated = false;
while (count >= 4) {
Lerp(Sk4f::Load(v0), Sk4f::Load(v1), lerp_info.weight)
.store(dst);
const auto old_val = Sk4f::Load(dst),
new_val = Lerp(Sk4f::Load(v0), Sk4f::Load(v1), lerp_info.weight);
updated |= (new_val != old_val).anyTrue();
new_val.store(dst);
v0 += 4;
v1 += 4;
@ -190,8 +197,13 @@ private:
}
while (count-- > 0) {
*dst++ = Lerp(*v0++, *v1++, lerp_info.weight);
const auto new_val = Lerp(*v0++, *v1++, lerp_info.weight);
updated |= (new_val != *dst);
*dst++ = new_val;
}
return updated;
}
const std::vector<float> fStorage;

View File

@ -69,7 +69,7 @@ const sksg::RenderNode* MotionBlurEffect::onNodeAt(const SkPoint&) const {
SkRect MotionBlurEffect::seekToSample(size_t sample_idx, const SkMatrix& ctm) const {
SkASSERT(sample_idx < fSampleCount);
fAnimator->tick(fT + fPhase + fDT * sample_idx);
fAnimator->seek(fT + fPhase + fDT * sample_idx);
SkASSERT(this->children().size() == 1ul);
return this->children()[0]->revalidate(nullptr, ctm);

View File

@ -39,15 +39,20 @@ public:
, fTimeScale(time_scale)
, fIsMultiframe(fAsset->isMultiFrame()) {}
void onTick(float t) override {
bool onSeek(float t) override {
if (!fIsMultiframe && fImageNode->getImage()) {
// Single frame already resolved.
return;
return false;
}
auto frame = fAsset->getFrame((t + fTimeBias) * fTimeScale);
fImageTransformNode->setMatrix(image_matrix(frame, fAssetSize));
fImageNode->setImage(std::move(frame));
if (frame != fImageNode->getImage()) {
fImageTransformNode->setMatrix(image_matrix(frame, fAssetSize));
fImageNode->setImage(std::move(frame));
return true;
}
return false;
}
private:

View File

@ -52,18 +52,22 @@ public:
, fTimeBias(time_bias)
, fTimeScale(time_scale) {}
void onTick(float t) override {
bool onSeek(float t) override {
if (fRemapper) {
// When time remapping is active, |t| is fully driven externally.
fRemapper->tick(t);
fRemapper->seek(t);
t = fRemapper->t();
} else {
t = (t + fTimeBias) * fTimeScale;
}
bool updated = false;
for (const auto& anim : fAnimators) {
anim->tick(t);
updated |= anim->seek(t);
}
return updated;
}
private:

View File

@ -149,9 +149,13 @@ bool ValueTraits<TextValue>::CanLerp(const TextValue&, const TextValue&) {
}
template <>
void ValueTraits<TextValue>::Lerp(const TextValue& v0, const TextValue&, float, TextValue* result) {
bool ValueTraits<TextValue>::Lerp(const TextValue& v0, const TextValue&, float, TextValue* result) {
// Text value keyframes are treated as selectors, not as interpolated values.
*result = v0;
if (v0 != *result) {
*result = v0;
return true;
}
return false;
}
} // namespace skottie

View File

@ -31,7 +31,7 @@ public:
operator bool() const { return fDidBind; }
const T& operator()(float t) { this->tick(t); return fValue; }
const T& operator()(float t) { this->seek(t); return fValue; }
private:
void onSync() override {}