Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// Copyright (C) 2016 Gunnar Sletta <gunnar@sletta.org>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qquickanimatorcontroller_p.h"
#include "qquickanimatorjob_p.h"
#include "qquickanimator_p.h"
#include "qquickanimator_p_p.h"
#include <private/qquickitem_p.h>
#if QT_CONFIG(quick_shadereffect)
#include <private/qquickshadereffect_p.h>
#endif
#include <private/qanimationgroupjob_p.h>
#include <qcoreapplication.h>
#include <qdebug.h>
QT_BEGIN_NAMESPACE
struct QQuickTransformAnimatorHelperStore
{
QHash<QQuickItem *, QQuickTransformAnimatorJob::Helper *> store;
QMutex mutex;
QQuickTransformAnimatorJob::Helper *acquire(QQuickItem *item) {
mutex.lock();
QQuickTransformAnimatorJob::Helper *helper = store.value(item);
if (!helper) {
helper = new QQuickTransformAnimatorJob::Helper();
helper->item = item;
store[item] = helper;
} else {
++helper->ref;
}
mutex.unlock();
return helper;
}
void release(QQuickTransformAnimatorJob::Helper *helper) {
mutex.lock();
if (--helper->ref == 0) {
store.remove(helper->item);
delete helper;
}
mutex.unlock();
}
};
Q_GLOBAL_STATIC(QQuickTransformAnimatorHelperStore, qquick_transform_animatorjob_helper_store);
QQuickAnimatorProxyJob::QQuickAnimatorProxyJob(QAbstractAnimationJob *job,
QQuickAbstractAnimation *animation)
: m_controller(nullptr)
, m_internalState(State_Stopped)
{
m_job.reset(job);
m_isRenderThreadProxy = true;
setLoopCount(job->loopCount());
// Instead of setting duration to job->duration() we need to set it to -1 so that
// it runs as long as the job is running on the render thread. If we gave it
// an explicit duration, it would be stopped, potentially stopping the RT animation
// prematurely.
// This means that the animation driver will tick on the GUI thread as long
// as the animation is running on the render thread, but this overhead will
// be negligiblie compared to animating and re-rendering the scene on the render thread.
m_duration = -1;
QObject *ctx = findAnimationContext(animation);
if (!ctx) {
qWarning("QtQuick: unable to find animation context for RT animation...");
return;
}
QQuickWindow *window = qobject_cast<QQuickWindow *>(ctx);
if (window) {
setWindow(window);
} else {
QQuickItem *item = qobject_cast<QQuickItem *>(ctx);
if (item->window())
setWindow(item->window());
connect(item, &QQuickItem::windowChanged, this, &QQuickAnimatorProxyJob::windowChanged);
}
}
void QQuickAnimatorProxyJob::updateLoopCount(int loopCount)
{
m_job->setLoopCount(loopCount);
}
QQuickAnimatorProxyJob::~QQuickAnimatorProxyJob()
{
if (m_job && m_controller)
m_controller->cancel(m_job);
m_job.reset();
}
QObject *QQuickAnimatorProxyJob::findAnimationContext(QQuickAbstractAnimation *a)
{
QObject *p = a->parent();
while (p != nullptr && qobject_cast<QQuickWindow *>(p) == nullptr && qobject_cast<QQuickItem *>(p) == nullptr)
p = p->parent();
return p;
}
void QQuickAnimatorProxyJob::updateCurrentTime(int)
{
if (m_internalState != State_Running)
return;
// Copy current loop number from the job
// we could make currentLoop() virtual but it would be less efficient
m_currentLoop = m_job->currentLoop();
// A proxy which is being ticked should be associated with a window, (see
// setWindow() below). If we get here when there is no more controller we
// have a problem.
Q_ASSERT(m_controller);
// We do a simple check here to see if the animator has run and stopped on
// the render thread. isPendingStart() will perform a check against jobs
// that have been scheduled for start, but that will not yet have entered
// the actual running state.
// Secondly, we make an unprotected read of the job's state to figure out
// if it is running, but this is ok, since we're only reading the state
// and if the render thread should happen to be writing it concurrently,
// we might get the wrong value for this update, but then we'll simply
// pick it up on the next iterationm when the job is stopped and render
// thread is no longer using it.
if (!m_controller->isPendingStart(m_job)
&& !m_job->isRunning()) {
stop();
}
}
void QQuickAnimatorProxyJob::updateState(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State)
{
if (m_state == Running) {
m_internalState = State_Starting;
if (m_controller) {
m_internalState = State_Running;
m_controller->start(m_job);
}
} else if (newState == Stopped) {
m_internalState = State_Stopped;
if (m_controller) {
syncBackCurrentValues();
m_controller->cancel(m_job);
}
} else if (newState == Paused) {
m_internalState = State_Paused;
if (m_controller) {
syncBackCurrentValues();
m_controller->pause(m_job);
}
}
}
void QQuickAnimatorProxyJob::debugAnimation(QDebug d) const
{
d << "QuickAnimatorProxyJob("<< Qt::hex << (const void *) this << Qt::dec
<< "state:" << state() << "duration:" << duration()
<< "proxying: (" << job() << ')';
}
void QQuickAnimatorProxyJob::windowChanged(QQuickWindow *window)
{
setWindow(window);
}
void QQuickAnimatorProxyJob::setWindow(QQuickWindow *window)
{
if (!window) {
if (m_job && m_controller) {
disconnect(m_controller->window(), &QQuickWindow::sceneGraphInitialized,
this, &QQuickAnimatorProxyJob::sceneGraphInitialized);
m_controller->cancel(m_job);
}
m_controller = nullptr;
stop();
} else if (!m_controller && m_job) {
m_controller = QQuickWindowPrivate::get(window)->animationController.get();
if (window->isSceneGraphInitialized())
readyToAnimate();
else
connect(window, &QQuickWindow::sceneGraphInitialized, this, &QQuickAnimatorProxyJob::sceneGraphInitialized);
}
}
void QQuickAnimatorProxyJob::sceneGraphInitialized()
{
if (m_controller) {
disconnect(m_controller->window(), &QQuickWindow::sceneGraphInitialized, this, &QQuickAnimatorProxyJob::sceneGraphInitialized);
readyToAnimate();
}
}
void QQuickAnimatorProxyJob::readyToAnimate()
{
Q_ASSERT(m_controller);
if (m_internalState == State_Starting) {
m_internalState = State_Running;
m_controller->start(m_job);
}
}
static void qquick_syncback_helper(QAbstractAnimationJob *job)
{
if (job->isRenderThreadJob()) {
static_cast<QQuickAnimatorJob *>(job)->writeBack();
} else if (job->isGroup()) {
QAnimationGroupJob *g = static_cast<QAnimationGroupJob *>(job);
for (QAbstractAnimationJob *a : *g->children())
qquick_syncback_helper(a);
}
}
void QQuickAnimatorProxyJob::syncBackCurrentValues()
{
if (m_job)
qquick_syncback_helper(m_job.data());
}
QQuickAnimatorJob::QQuickAnimatorJob()
: m_target(nullptr)
, m_controller(nullptr)
, m_from(0)
, m_to(0)
, m_value(0)
, m_duration(0)
, m_isTransform(false)
, m_isUniform(false)
{
m_isRenderThreadJob = true;
}
void QQuickAnimatorJob::debugAnimation(QDebug d) const
{
d << "QuickAnimatorJob(" << Qt::hex << (const void *) this << Qt::dec
<< ") state:" << state() << "duration:" << duration()
<< "target:" << m_target << "value:" << m_value;
}
qreal QQuickAnimatorJob::progress(int time) const
{
return m_easing.valueForProgress((m_duration == 0) ? qreal(1) : qreal(time) / qreal(m_duration));
}
void QQuickAnimatorJob::boundValue()
{
qreal rangeMin = m_from;
qreal rangeMax = m_to;
if (m_from > m_to) {
rangeMax = m_from;
rangeMin = m_to;
}
m_value = qBound(rangeMin, m_value, rangeMax);
}
qreal QQuickAnimatorJob::value() const
{
qreal value = m_to;
if (m_controller) {
m_controller->lock();
value = m_value;
m_controller->unlock();
}
return value;
}
void QQuickAnimatorJob::setTarget(QQuickItem *target)
{
m_target = target;
}
void QQuickAnimatorJob::initialize(QQuickAnimatorController *controller)
{
m_controller = controller;
}
QQuickTransformAnimatorJob::QQuickTransformAnimatorJob()
: m_helper(nullptr)
{
m_isTransform = true;
}
QQuickTransformAnimatorJob::~QQuickTransformAnimatorJob()
{
if (m_helper)
qquick_transform_animatorjob_helper_store()->release(m_helper);
}
void QQuickTransformAnimatorJob::setTarget(QQuickItem *item)
{
// In the extremely unlikely event that the target of an animator has been
// changed into a new item that sits in the exact same pointer address, we
// want to force syncing it again.
if (m_helper && m_target)
m_helper->wasSynced = false;
QQuickAnimatorJob::setTarget(item);
}
void QQuickTransformAnimatorJob::preSync()
{
// If the target has changed or become null, release and reset the helper
if (m_helper && (m_helper->item != m_target || !m_target)) {
qquick_transform_animatorjob_helper_store()->release(m_helper);
m_helper = nullptr;
}
if (!m_target) {
invalidate();
return;
}
if (!m_helper) {
m_helper = qquick_transform_animatorjob_helper_store()->acquire(m_target);
// This is a bit superfluous, but it ends up being simpler than the
// alternative. When an item happens to land on the same address as a
// previous item, that helper might not have been fully cleaned up by
// the time it gets taken back into use. As an alternative to storing
// connections to each and every item's QObject::destroyed() and
// having to clean those up afterwards, we simply sync all helpers on
// the first run. The sync is only done once for the run of an
// animation and it is a fairly light function (compared to storing
// potentially thousands of connections and managing their lifetime.
m_helper->wasSynced = false;
}
m_helper->sync();
}
void QQuickTransformAnimatorJob::invalidate()
{
if (m_helper)
m_helper->node = nullptr;
}
void QQuickTransformAnimatorJob::Helper::sync()
{
const quint32 mask = QQuickItemPrivate::Position
| QQuickItemPrivate::BasicTransform
| QQuickItemPrivate::TransformOrigin
| QQuickItemPrivate::Size;
QQuickItemPrivate *d = QQuickItemPrivate::get(item);
#if QT_CONFIG(quick_shadereffect)
if (d->extra.isAllocated()
&& d->extra->layer
&& d->extra->layer->enabled()) {
d = QQuickItemPrivate::get(d->extra->layer->m_effectSource);
}
#endif
quint32 dirty = mask & d->dirtyAttributes;
if (!wasSynced) {
dirty = 0xffffffffu;
wasSynced = true;
}
// We update the node before checking on dirty, as the node might have changed without the animator running
node = d->itemNode();
if (dirty == 0)
return;
if (dirty & QQuickItemPrivate::Position) {
dx = item->x();
dy = item->y();
}
if (dirty & QQuickItemPrivate::BasicTransform) {
scale = item->scale();
rotation = item->rotation();
}
if (dirty & (QQuickItemPrivate::TransformOrigin | QQuickItemPrivate::Size)) {
QPointF o = item->transformOriginPoint();
ox = o.x();
oy = o.y();
}
}
void QQuickTransformAnimatorJob::Helper::commit()
{
if (!wasChanged || !node)
return;
QMatrix4x4 m;
m.translate(dx, dy);
m.translate(ox, oy);
m.scale(scale);
m.rotate(rotation, 0, 0, 1);
m.translate(-ox, -oy);
node->setMatrix(m);
wasChanged = false;
}
void QQuickTransformAnimatorJob::commit()
{
if (m_helper)
m_helper->commit();
}
void QQuickXAnimatorJob::writeBack()
{
if (m_target)
m_target->setX(value());
}
void QQuickXAnimatorJob::updateCurrentTime(int time)
{
if (!m_helper)
return;
m_value = m_from + (m_to - m_from) * progress(time);
m_helper->dx = m_value;
m_helper->wasChanged = true;
}
void QQuickYAnimatorJob::writeBack()
{
if (m_target)
m_target->setY(value());
}
void QQuickYAnimatorJob::updateCurrentTime(int time)
{
if (!m_helper)
return;
m_value = m_from + (m_to - m_from) * progress(time);
m_helper->dy = m_value;
m_helper->wasChanged = true;
}
void QQuickScaleAnimatorJob::writeBack()
{
if (m_target)
m_target->setScale(value());
}
void QQuickScaleAnimatorJob::updateCurrentTime(int time)
{
if (!m_helper)
return;
m_value = m_from + (m_to - m_from) * progress(time);
m_helper->scale = m_value;
m_helper->wasChanged = true;
}
QQuickRotationAnimatorJob::QQuickRotationAnimatorJob()
: m_direction(QQuickRotationAnimator::Numerical)
{
}
extern QVariant _q_interpolateShortestRotation(qreal &f, qreal &t, qreal progress);
extern QVariant _q_interpolateClockwiseRotation(qreal &f, qreal &t, qreal progress);
extern QVariant _q_interpolateCounterclockwiseRotation(qreal &f, qreal &t, qreal progress);
void QQuickRotationAnimatorJob::updateCurrentTime(int time)
{
if (!m_helper)
return;
float t = progress(time);
switch (m_direction) {
case QQuickRotationAnimator::Clockwise:
m_value = _q_interpolateClockwiseRotation(m_from, m_to, t).toFloat();
// The logic in _q_interpolateClockwise comes out a bit wrong
// for the case of X->0 where 0<X<360. It ends on 360 which it
// shouldn't.
if (t == 1)
m_value = m_to;
break;
case QQuickRotationAnimator::Counterclockwise:
m_value = _q_interpolateCounterclockwiseRotation(m_from, m_to, t).toFloat();
break;
case QQuickRotationAnimator::Shortest:
m_value = _q_interpolateShortestRotation(m_from, m_to, t).toFloat();
break;
case QQuickRotationAnimator::Numerical:
m_value = m_from + (m_to - m_from) * t;
break;
}
m_helper->rotation = m_value;
m_helper->wasChanged = true;
}
void QQuickRotationAnimatorJob::writeBack()
{
if (m_target)
m_target->setRotation(value());
}
QQuickOpacityAnimatorJob::QQuickOpacityAnimatorJob()
: m_opacityNode(nullptr)
{
}
void QQuickOpacityAnimatorJob::postSync()
{
if (!m_target) {
invalidate();
return;
}
QQuickItemPrivate *d = QQuickItemPrivate::get(m_target);
#if QT_CONFIG(quick_shadereffect)
if (d->extra.isAllocated()
&& d->extra->layer
&& d->extra->layer->enabled()) {
d = QQuickItemPrivate::get(d->extra->layer->m_effectSource);
}
#endif
m_opacityNode = d->opacityNode();
if (!m_opacityNode) {
m_opacityNode = new QSGOpacityNode();
/* The item node subtree is like this
*
* itemNode
* (opacityNode) optional
* (clipNode) optional
* (rootNode) optional
* children / paintNode
*
* If the opacity node doesn't exist, we need to insert it into
* the hierarchy between itemNode and clipNode or rootNode. If
* neither clip or root exists, we need to reparent all children
* from itemNode to opacityNode.
*/
QSGNode *iNode = d->itemNode();
QSGNode *child = d->childContainerNode();
if (child != iNode) {
if (child->parent())
child->parent()->removeChildNode(child);
m_opacityNode->appendChildNode(child);
iNode->appendChildNode(m_opacityNode);
} else {
iNode->reparentChildNodesTo(m_opacityNode);
iNode->appendChildNode(m_opacityNode);
}
d->extra.value().opacityNode = m_opacityNode;
updateCurrentTime(0);
}
Q_ASSERT(m_opacityNode);
}
void QQuickOpacityAnimatorJob::invalidate()
{
m_opacityNode = nullptr;
}
void QQuickOpacityAnimatorJob::writeBack()
{
if (m_target)
m_target->setOpacity(value());
}
void QQuickOpacityAnimatorJob::updateCurrentTime(int time)
{
if (!m_opacityNode)
return;
m_value = m_from + (m_to - m_from) * progress(time);
m_opacityNode->setOpacity(m_value);
}
#if QT_CONFIG(quick_shadereffect)
QQuickUniformAnimatorJob::QQuickUniformAnimatorJob()
{
m_isUniform = true;
}
void QQuickUniformAnimatorJob::setTarget(QQuickItem *target)
{
// Check target is of expected type
if (qobject_cast<QQuickShaderEffect *>(target) != nullptr)
m_target = target;
}
void QQuickUniformAnimatorJob::updateCurrentTime(int time)
{
if (!m_effect || m_target != m_effect)
return;
m_value = m_from + (m_to - m_from) * progress(time);
m_effect->updateUniformValue(m_uniform, m_value);
}
void QQuickUniformAnimatorJob::writeBack()
{
if (m_target)
m_target->setProperty(m_uniform, value());
}
void QQuickUniformAnimatorJob::postSync()
{
if (!m_target) {
invalidate();
return;
}
m_effect = qobject_cast<QQuickShaderEffect *>(m_target);
}
void QQuickUniformAnimatorJob::invalidate()
{
}
#endif
QT_END_NAMESPACE
#include "moc_qquickanimatorjob_p.cpp"