1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "batchrenderer.h"
5
6#include <QImage>
7#include <QPainter>
8#include <QHash>
9#include <QMap>
10#include <QMutexLocker>
11#include <QLoggingCategory>
12#include <QThread>
13
14#include <QJsonDocument>
15#include <QJsonArray>
16
17#include <QtBodymovin/private/bmconstants_p.h>
18#include <QtBodymovin/private/bmbase_p.h>
19#include <QtBodymovin/private/bmimagelayer_p.h>
20#include <QtBodymovin/private/bmlayer_p.h>
21
22#include "lottieanimation.h"
23#include "lottierasterrenderer.h"
24
25QT_BEGIN_NAMESPACE
26
27Q_LOGGING_CATEGORY(lcLottieQtBodymovinRenderThread, "qt.lottieqt.bodymovin.render.thread");
28
29BatchRenderer *BatchRenderer::m_rendererInstance = nullptr;
30
31BatchRenderer::BatchRenderer()
32 : QThread()
33{
34 const QByteArray cacheStr = qgetenv(varName: "QLOTTIE_RENDER_CACHE_SIZE");
35 int cacheSize = cacheStr.toInt();
36 if (cacheSize > 0) {
37 qCDebug(lcLottieQtBodymovinRenderThread) << "Setting frame cache size to" << cacheSize;
38 m_cacheSize = cacheSize;
39 }
40}
41
42BatchRenderer::~BatchRenderer()
43{
44 QMutexLocker mlocker(&m_mutex);
45
46 for (Entry *entry : std::as_const(t&: m_animData)) {
47 qDeleteAll(c: entry->frameCache);
48 delete entry->bmTreeBlueprint;
49 delete entry;
50 }
51}
52
53BatchRenderer *BatchRenderer::instance()
54{
55 if (!m_rendererInstance)
56 m_rendererInstance = new BatchRenderer;
57
58 return m_rendererInstance;
59}
60
61void BatchRenderer::deleteInstance()
62{
63 delete m_rendererInstance;
64 m_rendererInstance = nullptr;
65}
66
67void BatchRenderer::registerAnimator(LottieAnimation *animator)
68{
69 QMutexLocker mlocker(&m_mutex);
70
71 qCDebug(lcLottieQtBodymovinRenderThread) << "Register Animator:"
72 << static_cast<void*>(animator);
73
74 Entry *&entry = m_animData[animator];
75 if (entry) {
76 qDeleteAll(c: entry->frameCache);
77 delete entry->bmTreeBlueprint;
78 delete entry;
79 entry = nullptr;
80 }
81 Q_ASSERT(entry == nullptr);
82 entry = new Entry;
83 entry->animator = animator;
84 entry->startFrame = animator->startFrame();
85 entry->endFrame = animator->endFrame();
86 entry->currentFrame = animator->startFrame();
87 entry->animDir = animator->direction();
88 entry->bmTreeBlueprint = new BMBase;
89 parse(rootElement: entry->bmTreeBlueprint, jsonSource: animator->jsonSource(), version: animator->version());
90 m_waitCondition.wakeAll();
91}
92
93void BatchRenderer::deregisterAnimator(LottieAnimation *animator)
94{
95 QMutexLocker mlocker(&m_mutex);
96
97 qCDebug(lcLottieQtBodymovinRenderThread) << "Deregister Animator:"
98 << static_cast<void*>(animator);
99
100 Entry *entry = m_animData.take(key: animator);
101 if (entry) {
102 qDeleteAll(c: entry->frameCache);
103 delete entry->bmTreeBlueprint;
104 delete entry;
105 }
106}
107
108bool BatchRenderer::gotoFrame(LottieAnimation *animator, int frame)
109{
110 QMutexLocker mlocker(&m_mutex);
111
112 Entry *entry = m_animData.value(key: animator, defaultValue: nullptr);
113 if (entry) {
114 qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:"
115 << static_cast<void*>(animator)
116 << "Goto frame:" << frame;
117 entry->currentFrame = frame;
118 entry->animDir = animator->direction();
119 pruneFrameCache(e: entry);
120 m_waitCondition.wakeAll();
121 return true;
122 }
123 return false;
124}
125
126void BatchRenderer::pruneFrameCache(Entry* e)
127{
128 QHash<int, BMBase*>::iterator removeCandidate = e->frameCache.end();
129 if (e->frameCache.size() == m_cacheSize &&
130 !e->frameCache.contains(key: e->currentFrame))
131 removeCandidate = e->frameCache.begin();
132
133 QHash<int, BMBase*>::iterator it = e->frameCache.begin();
134 while (it != e->frameCache.end()) {
135 int frame = it.key();
136 if ((frame - e->currentFrame) * e->animDir >= 0) { // same frame or same direction
137 if (removeCandidate != e->frameCache.end() &&
138 (removeCandidate.key() - frame) * e->animDir < 0)
139 removeCandidate = it;
140 ++it;
141 } else {
142 qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:" << static_cast<void*>(e->animator)
143 << "Remove frame from cache" << frame;
144 delete it.value();
145 it = e->frameCache.erase(it);
146 removeCandidate = e->frameCache.end();
147 }
148 }
149 if (removeCandidate != e->frameCache.end()) {
150 qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:"
151 << static_cast<void*>(e->animator)
152 << "Remove frame from cache"
153 << removeCandidate.key()
154 << "(Reason - cache is full)";
155 e->frameCache.erase(it: removeCandidate);
156 }
157 m_lastRenderedFrame = -1;
158}
159
160BMBase *BatchRenderer::getFrame(LottieAnimation *animator, int frameNumber)
161{
162 QMutexLocker mlocker(&m_mutex);
163
164 Entry *entry = m_animData.value(key: animator, defaultValue: nullptr);
165 if (entry)
166 return entry->frameCache.value(key: frameNumber, defaultValue: nullptr);
167 else
168 return nullptr;
169}
170
171void BatchRenderer::prerender(Entry *animEntry)
172{
173 while (animEntry->frameCache.size() < m_cacheSize) {
174 if (m_lastRenderedFrame == animEntry->currentFrame)
175 animEntry->currentFrame += animEntry->animDir;
176
177 BMBase *&bmTree = animEntry->frameCache[animEntry->currentFrame];
178 if (bmTree == nullptr) {
179 bmTree = new BMBase(*animEntry->bmTreeBlueprint);
180
181 for (BMBase *elem : bmTree->children()) {
182 if (elem->active(frame: animEntry->currentFrame))
183 elem->updateProperties( frame: animEntry->currentFrame);
184 }
185 }
186
187 qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:"
188 << static_cast<void*>(animEntry->animator)
189 << "Frame drawn to cache. FN:"
190 << animEntry->currentFrame;
191 emit frameReady(animator: animEntry->animator, frameNumber: animEntry->currentFrame);
192
193 animEntry->currentFrame += animEntry->animDir;
194
195 if (animEntry->currentFrame > animEntry->endFrame) {
196 animEntry->currentFrame = animEntry->startFrame;
197 } else if (animEntry->currentFrame < animEntry->startFrame) {
198 animEntry->currentFrame = animEntry->endFrame;
199 }
200 }
201}
202
203void BatchRenderer::frameRendered(LottieAnimation *animator, int frameNumber)
204{
205 QMutexLocker mlocker(&m_mutex);
206
207 Entry *entry = m_animData.value(key: animator, defaultValue: nullptr);
208 if (entry) {
209 qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:" << static_cast<void*>(animator)
210 << "Remove frame from cache" << frameNumber;
211
212 BMBase *root = entry->frameCache.take(key: frameNumber);
213 if (root != nullptr) {
214 delete root;
215 m_waitCondition.wakeAll();
216 }
217 m_lastRenderedFrame = frameNumber;
218 }
219}
220
221void BatchRenderer::run()
222{
223 qCDebug(lcLottieQtBodymovinRenderThread) << "rendering thread" << QThread::currentThread();
224
225 while (!isInterruptionRequested()) {
226 QMutexLocker mlocker(&m_mutex);
227
228 for (Entry *e : std::as_const(t&: m_animData))
229 prerender(animEntry: e);
230
231 m_waitCondition.wait(lockedMutex: &m_mutex);
232 }
233}
234
235int BatchRenderer::parse(BMBase *rootElement, const QByteArray &jsonSource,
236 const QVersionNumber &version) const
237{
238 QJsonDocument doc = QJsonDocument::fromJson(json: jsonSource);
239 QJsonObject rootObj = doc.object();
240
241 if (rootObj.empty())
242 return -1;
243
244 QMap<QString, QJsonObject> assets;
245 QJsonArray jsonLayers = rootObj.value(key: QLatin1String("layers")).toArray();
246 QJsonArray jsonAssets = rootObj.value(key: QLatin1String("assets")).toArray();
247 QJsonArray::const_iterator jsonAssetsIt = jsonAssets.constBegin();
248 while (jsonAssetsIt != jsonAssets.constEnd()) {
249 QJsonObject jsonAsset = (*jsonAssetsIt).toObject();
250
251 jsonAsset.insert(key: QLatin1String("fileSource"), value: QJsonValue::fromVariant(variant: m_animData.keys().last()->source()));
252 QString id = jsonAsset.value(key: QLatin1String("id")).toString();
253 assets.insert(key: id, value: jsonAsset);
254 jsonAssetsIt++;
255 }
256
257 QJsonArray::const_iterator jsonLayerIt = jsonLayers.constEnd();
258 while (jsonLayerIt != jsonLayers.constBegin()) {
259 jsonLayerIt--;
260 QJsonObject jsonLayer = (*jsonLayerIt).toObject();
261 if (jsonLayer.value(key: "ty").toInt() == 2) {
262 QString refId = jsonLayer.value(key: "refId").toString();
263 jsonLayer.insert(key: "asset", value: assets.value(key: refId));
264 }
265 BMLayer *layer = BMLayer::construct(definition: jsonLayer, version);
266 if (layer) {
267 layer->setParent(rootElement);
268 // Mask layers must be rendered before the layers they affect to
269 // although they appear before in layer hierarchy. For this reason
270 // move a mask after the affected layers, so it will be rendered first
271 if (layer->isMaskLayer())
272 rootElement->prependChild(child: layer);
273 else
274 rootElement->appendChild(child: layer);
275 }
276 }
277
278 return 0;
279}
280
281QT_END_NAMESPACE
282

source code of qtlottie/src/imports/rasterrenderer/batchrenderer.cpp