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 | |
25 | QT_BEGIN_NAMESPACE |
26 | |
27 | Q_LOGGING_CATEGORY(lcLottieQtBodymovinRenderThread, "qt.lottieqt.bodymovin.render.thread" ); |
28 | |
29 | BatchRenderer *BatchRenderer::m_rendererInstance = nullptr; |
30 | |
31 | BatchRenderer::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 | |
42 | BatchRenderer::~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 | |
53 | BatchRenderer *BatchRenderer::instance() |
54 | { |
55 | if (!m_rendererInstance) |
56 | m_rendererInstance = new BatchRenderer; |
57 | |
58 | return m_rendererInstance; |
59 | } |
60 | |
61 | void BatchRenderer::deleteInstance() |
62 | { |
63 | delete m_rendererInstance; |
64 | m_rendererInstance = nullptr; |
65 | } |
66 | |
67 | void 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 | |
93 | void 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 | |
108 | bool 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 | |
126 | void 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 | |
160 | BMBase *BatchRenderer::getFrame(LottieAnimation *animator, int ) |
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 | |
171 | void 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 | |
203 | void BatchRenderer::frameRendered(LottieAnimation *animator, int ) |
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 | |
221 | void 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 | |
235 | int 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 | |
281 | QT_END_NAMESPACE |
282 | |