1/***************************************************************************
2 * Copyright 2010-2012 Stefan Majewsky <majewsky@gmx.net> *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU Library General Public License *
6 * version 2 as published by the Free Software Foundation *
7 * *
8 * This program is distributed in the hope that it will be useful, *
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
11 * GNU Library General Public License for more details. *
12 * *
13 * You should have received a copy of the GNU Library General Public *
14 * License along with this program; if not, write to the *
15 * Free Software Foundation, Inc., *
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
17 ***************************************************************************/
18
19#include "kgamerenderer.h"
20#include "kgamerenderer_p.h"
21#include "kgamerendererclient.h"
22#include "colorproxy_p.h"
23#include "kgtheme.h"
24#include "kgthemeprovider.h"
25
26#include <QtCore/QCoreApplication>
27#include <QtCore/QDateTime>
28#include <QtCore/QFileInfo>
29#include <QtCore/QScopedPointer>
30#include <QtGui/QPainter>
31#include <QVariant>
32#include <KDebug>
33
34//TODO: automatically schedule pre-rendering of animation frames
35//TODO: multithreaded SVG loading?
36
37static const QString cacheName(QByteArray theme)
38{
39 const QString appName = QCoreApplication::instance()->applicationName();
40 //e.g. "themes/foobar.desktop" -> "themes/foobar"
41 if (theme.endsWith(QByteArray(".desktop")))
42 theme.truncate(theme.length() - 8); //8 = strlen(".desktop")
43 return QString::fromLatin1("kgamerenderer-%1-%2")
44 .arg(appName).arg(QString::fromUtf8(theme));
45}
46
47KGameRendererPrivate::KGameRendererPrivate(KgThemeProvider* provider, unsigned cacheSize, KGameRenderer* parent)
48 : m_parent(parent)
49 , m_provider(provider)
50 , m_currentTheme(0) //will be loaded on first use
51 , m_frameSuffix(QString::fromLatin1("_%1"))
52 , m_sizePrefix(QString::fromLatin1("%1-%2-"))
53 , m_frameCountPrefix(QString::fromLatin1("fc-"))
54 , m_boundsPrefix(QString::fromLatin1("br-"))
55 //default cache size: 3 MiB = 3 << 20 bytes
56 , m_cacheSize((cacheSize == 0 ? 3 : cacheSize) << 20)
57 , m_strategies(KGameRenderer::UseDiskCache | KGameRenderer::UseRenderingThreads)
58 , m_frameBaseIndex(0)
59 , m_defaultPrimaryView(0)
60 , m_rendererPool(&m_workerPool)
61 , m_imageCache(0)
62{
63 qRegisterMetaType<KGRInternal::Job*>();
64}
65
66KGameRenderer::KGameRenderer(KgThemeProvider* provider, unsigned cacheSize)
67 : d(new KGameRendererPrivate(provider, cacheSize, this))
68{
69 if (!provider->parent())
70 {
71 provider->setParent(this);
72 }
73 connect(provider, SIGNAL(currentThemeChanged(const KgTheme*)), SLOT(_k_setTheme(const KgTheme*)));
74}
75
76static KgThemeProvider* providerForSingleTheme(KgTheme* theme, QObject* parent)
77{
78 KgThemeProvider* prov = new KgThemeProvider(QByteArray(), parent);
79 prov->addTheme(theme);
80 return prov;
81}
82
83KGameRenderer::KGameRenderer(KgTheme* theme, unsigned cacheSize)
84 : d(new KGameRendererPrivate(providerForSingleTheme(theme, this), cacheSize, this))
85{
86}
87
88KGameRenderer::~KGameRenderer()
89{
90 //cleanup clients
91 while (!d->m_clients.isEmpty())
92 {
93 delete d->m_clients.constBegin().key();
94 }
95 //cleanup own stuff
96 d->m_workerPool.waitForDone();
97 delete d->m_imageCache;
98 delete d;
99}
100
101QGraphicsView* KGameRenderer::defaultPrimaryView() const
102{
103 return d->m_defaultPrimaryView;
104}
105
106void KGameRenderer::setDefaultPrimaryView(QGraphicsView* view)
107{
108 d->m_defaultPrimaryView = view;
109}
110
111int KGameRenderer::frameBaseIndex() const
112{
113 return d->m_frameBaseIndex;
114}
115
116void KGameRenderer::setFrameBaseIndex(int frameBaseIndex)
117{
118 d->m_frameBaseIndex = frameBaseIndex;
119}
120
121QString KGameRenderer::frameSuffix() const
122{
123 return d->m_frameSuffix;
124}
125
126void KGameRenderer::setFrameSuffix(const QString& suffix)
127{
128 d->m_frameSuffix = suffix.contains(QLatin1String("%1")) ? suffix : QLatin1String("_%1");
129}
130
131KGameRenderer::Strategies KGameRenderer::strategies() const
132{
133 return d->m_strategies;
134}
135
136void KGameRenderer::setStrategyEnabled(KGameRenderer::Strategy strategy, bool enabled)
137{
138 const bool oldEnabled = d->m_strategies & strategy;
139 if (enabled)
140 {
141 d->m_strategies |= strategy;
142 }
143 else
144 {
145 d->m_strategies &= ~strategy;
146 }
147 if (strategy == KGameRenderer::UseDiskCache && oldEnabled != enabled)
148 {
149 //reload theme
150 const KgTheme* theme = d->m_currentTheme;
151 if (theme)
152 {
153 d->m_currentTheme = 0; //or setTheme() will return immediately
154 d->_k_setTheme(theme);
155 }
156 }
157}
158
159void KGameRendererPrivate::_k_setTheme(const KgTheme* theme)
160{
161 const KgTheme* oldTheme = m_currentTheme;
162 if (oldTheme == theme)
163 {
164 return;
165 }
166 kDebug(11000) << "Setting theme:" << theme->name();
167 if (!setTheme(theme))
168 {
169 const KgTheme* defaultTheme = m_provider->defaultTheme();
170 if (theme != defaultTheme)
171 {
172 kDebug(11000) << "Falling back to default theme:" << defaultTheme->name();
173 setTheme(defaultTheme);
174 m_provider->setCurrentTheme(defaultTheme);
175 }
176 }
177 //announce change to KGameRendererClients
178 QHash<KGameRendererClient*, QString>::iterator it1 = m_clients.begin(), it2 = m_clients.end();
179 for (; it1 != it2; ++it1)
180 {
181 it1.value().clear(); //because the pixmap is outdated
182 it1.key()->d->fetchPixmap();
183 }
184 emit m_parent->themeChanged(m_currentTheme);
185}
186
187bool KGameRendererPrivate::setTheme(const KgTheme* theme)
188{
189 if (!theme)
190 {
191 return false;
192 }
193 //open cache (and SVG file, if necessary)
194 if (m_strategies & KGameRenderer::UseDiskCache)
195 {
196 QScopedPointer<KImageCache> oldCache(m_imageCache);
197 const QString imageCacheName = cacheName(theme->identifier());
198 m_imageCache = new KImageCache(imageCacheName, m_cacheSize);
199 m_imageCache->setPixmapCaching(false); //see big comment in KGRPrivate class declaration
200 //check timestamp of cache vs. last write access to theme/SVG
201 const uint svgTimestamp = qMax(
202 QFileInfo(theme->graphicsPath()).lastModified().toTime_t(),
203 theme->property("_k_themeDescTimestamp").value<uint>()
204 );
205 QByteArray buffer;
206 if (!m_imageCache->find(QString::fromLatin1("kgr_timestamp"), &buffer))
207 buffer = "0";
208 const uint cacheTimestamp = buffer.toInt();
209 //try to instantiate renderer immediately if the cache does not exist or is outdated
210 //FIXME: This logic breaks if the cache evicts the "kgr_timestamp" key. We need additional API in KSharedDataCache to make sure that this key does not get evicted.
211 if (cacheTimestamp < svgTimestamp)
212 {
213 kDebug(11000) << "Theme newer than cache, checking SVG";
214 QScopedPointer<QSvgRenderer> renderer(new QSvgRenderer(theme->graphicsPath()));
215 if (renderer->isValid())
216 {
217 m_rendererPool.setPath(theme->graphicsPath(), renderer.take());
218 m_imageCache->clear();
219 m_imageCache->insert(QString::fromLatin1("kgr_timestamp"), QByteArray::number(svgTimestamp));
220 }
221 else
222 {
223 //The SVG file is broken, so we deny to change the theme without
224 //breaking the previous theme.
225 delete m_imageCache;
226 KSharedDataCache::deleteCache(imageCacheName);
227 m_imageCache = oldCache.take();
228 kDebug(11000) << "Theme change failed: SVG file broken";
229 return false;
230 }
231 }
232 //theme is cached - just delete the old renderer after making sure that no worker threads are using it anymore
233 else if (m_currentTheme != theme)
234 m_rendererPool.setPath(theme->graphicsPath());
235 }
236 else // !(m_strategies & KGameRenderer::UseDiskCache) -> no cache is used
237 {
238 //load SVG file
239 QScopedPointer<QSvgRenderer> renderer(new QSvgRenderer(theme->graphicsPath()));
240 if (renderer->isValid())
241 {
242 m_rendererPool.setPath(theme->graphicsPath(), renderer.take());
243 }
244 else
245 {
246 kDebug(11000) << "Theme change failed: SVG file broken";
247 return false;
248 }
249 //disconnect from disk cache (only needed if changing strategy)
250 delete m_imageCache;
251 m_imageCache = 0;
252 }
253 //clear in-process caches
254 m_pixmapCache.clear();
255 m_frameCountCache.clear();
256 m_boundsCache.clear();
257 //done
258 m_currentTheme = theme;
259 return true;
260}
261
262const KgTheme* KGameRenderer::theme() const
263{
264 //ensure that some theme is loaded
265 if (!d->m_currentTheme)
266 {
267 d->_k_setTheme(d->m_provider->currentTheme());
268 }
269 return d->m_currentTheme;
270}
271
272KgThemeProvider* KGameRenderer::themeProvider() const
273{
274 return d->m_provider;
275}
276
277QString KGameRendererPrivate::spriteFrameKey(const QString& key, int frame, bool normalizeFrameNo) const
278{
279 //fast path for non-animated sprites
280 if (frame < 0)
281 {
282 return key;
283 }
284 //normalize frame number
285 if (normalizeFrameNo)
286 {
287 const int frameCount = m_parent->frameCount(key);
288 if (frameCount <= 0)
289 {
290 //non-animated sprite
291 return key;
292 }
293 else
294 {
295 frame = (frame - m_frameBaseIndex) % frameCount + m_frameBaseIndex;
296 }
297 }
298 return key + m_frameSuffix.arg(frame);
299}
300
301int KGameRenderer::frameCount(const QString& key) const
302{
303 //ensure that some theme is loaded
304 if (!d->m_currentTheme)
305 {
306 d->_k_setTheme(d->m_provider->currentTheme());
307 }
308 //look up in in-process cache
309 QHash<QString, int>::const_iterator it = d->m_frameCountCache.constFind(key);
310 if (it != d->m_frameCountCache.constEnd())
311 {
312 return it.value();
313 }
314 //look up in shared cache (if SVG is not yet loaded)
315 int count = -1;
316 bool countFound = false;
317 const QString cacheKey = d->m_frameCountPrefix + key;
318 if (d->m_rendererPool.hasAvailableRenderers() && (d->m_strategies & KGameRenderer::UseDiskCache))
319 {
320 QByteArray buffer;
321 if (d->m_imageCache->find(cacheKey, &buffer))
322 {
323 count = buffer.toInt();
324 countFound = true;
325 }
326 }
327 //determine from SVG
328 if (!countFound)
329 {
330 QSvgRenderer* renderer = d->m_rendererPool.allocRenderer();
331 //look for animated sprite first
332 count = d->m_frameBaseIndex;
333 while (renderer->elementExists(d->spriteFrameKey(key, count, false)))
334 {
335 ++count;
336 }
337 count -= d->m_frameBaseIndex;
338 //look for non-animated sprite instead
339 if (count == 0)
340 {
341 if (!renderer->elementExists(key))
342 {
343 count = -1;
344 }
345 }
346 d->m_rendererPool.freeRenderer(renderer);
347 //save in shared cache for following requests
348 if (d->m_strategies & KGameRenderer::UseDiskCache)
349 {
350 d->m_imageCache->insert(cacheKey, QByteArray::number(count));
351 }
352 }
353 d->m_frameCountCache.insert(key, count);
354 return count;
355}
356
357QRectF KGameRenderer::boundsOnSprite(const QString& key, int frame) const
358{
359 const QString elementKey = d->spriteFrameKey(key, frame);
360 //ensure that some theme is loaded
361 if (!d->m_currentTheme)
362 {
363 d->_k_setTheme(d->m_provider->currentTheme());
364 }
365 //look up in in-process cache
366 QHash<QString, QRectF>::const_iterator it = d->m_boundsCache.constFind(elementKey);
367 if (it != d->m_boundsCache.constEnd())
368 {
369 return it.value();
370 }
371 //look up in shared cache (if SVG is not yet loaded)
372 QRectF bounds;
373 bool boundsFound = false;
374 const QString cacheKey = d->m_boundsPrefix + elementKey;
375 if (!d->m_rendererPool.hasAvailableRenderers() && (d->m_strategies & KGameRenderer::UseDiskCache))
376 {
377 QByteArray buffer;
378 if (d->m_imageCache->find(cacheKey, &buffer))
379 {
380 QDataStream stream(buffer);
381 stream >> bounds;
382 boundsFound = true;
383 }
384 }
385 //determine from SVG
386 if (!boundsFound)
387 {
388 QSvgRenderer* renderer = d->m_rendererPool.allocRenderer();
389 bounds = renderer->boundsOnElement(elementKey);
390 d->m_rendererPool.freeRenderer(renderer);
391 //save in shared cache for following requests
392 if (d->m_strategies & KGameRenderer::UseDiskCache)
393 {
394 QByteArray buffer;
395 {
396 QDataStream stream(&buffer, QIODevice::WriteOnly);
397 stream << bounds;
398 }
399 d->m_imageCache->insert(cacheKey, buffer);
400 }
401 }
402 d->m_boundsCache.insert(elementKey, bounds);
403 return bounds;
404}
405
406bool KGameRenderer::spriteExists(const QString& key) const
407{
408 return this->frameCount(key) >= 0;
409}
410
411QPixmap KGameRenderer::spritePixmap(const QString& key, const QSize& size, int frame, const QHash<QColor, QColor>& customColors) const
412{
413 QPixmap result;
414 d->requestPixmap(KGRInternal::ClientSpec(key, frame, size, customColors), 0, &result);
415 return result;
416}
417
418//Helper function for KGameRendererPrivate::requestPixmap.
419void KGameRendererPrivate::requestPixmap__propagateResult(const QPixmap& pixmap, KGameRendererClient* client, QPixmap* synchronousResult)
420{
421 if (client)
422 {
423 client->receivePixmap(pixmap);
424 }
425 if (synchronousResult)
426 {
427 *synchronousResult = pixmap;
428 }
429}
430
431void KGameRendererPrivate::requestPixmap(const KGRInternal::ClientSpec& spec, KGameRendererClient* client, QPixmap* synchronousResult)
432{
433 //NOTE: If client == 0, the request is synchronous and must be finished when this method returns. This behavior is used by KGR::spritePixmap(). Instead of KGameRendererClient::receivePixmap, the QPixmap* argument is then used to return the result.
434 //parse request
435 if (spec.size.isEmpty())
436 {
437 requestPixmap__propagateResult(QPixmap(), client, synchronousResult);
438 return;
439 }
440 const QString elementKey = spriteFrameKey(spec.spriteKey, spec.frame);
441 QString cacheKey = m_sizePrefix.arg(spec.size.width()).arg(spec.size.height()) + elementKey;
442 QHash<QColor, QColor>::const_iterator it1 = spec.customColors.constBegin(), it2 = spec.customColors.constEnd();
443 static const QString colorSuffix(QLatin1String( "-%1-%2" ));
444 for (; it1 != it2; ++it1)
445 {
446 cacheKey += colorSuffix.arg(it1.key().rgba()).arg(it1.value().rgba());
447 }
448 //check if update is needed
449 if (client)
450 {
451 if (m_clients.value(client) == cacheKey)
452 {
453 return;
454 }
455 m_clients[client] = cacheKey;
456 }
457 //ensure that some theme is loaded
458 if (!m_currentTheme)
459 {
460 _k_setTheme(m_provider->currentTheme());
461 }
462 //try to serve from high-speed cache
463 QHash<QString, QPixmap>::const_iterator it = m_pixmapCache.constFind(cacheKey);
464 if (it != m_pixmapCache.constEnd())
465 {
466 requestPixmap__propagateResult(it.value(), client, synchronousResult);
467 return;
468 }
469 //try to serve from low-speed cache
470 if (m_strategies & KGameRenderer::UseDiskCache)
471 {
472 QPixmap pix;
473 if (m_imageCache->findPixmap(cacheKey, &pix))
474 {
475 m_pixmapCache.insert(cacheKey, pix);
476 requestPixmap__propagateResult(pix, client, synchronousResult);
477 return;
478 }
479 }
480 //if asynchronous request, is such a rendering job already running?
481 if (client && m_pendingRequests.contains(cacheKey))
482 {
483 return;
484 }
485 //create job
486 KGRInternal::Job* job = new KGRInternal::Job;
487 job->rendererPool = &m_rendererPool;
488 job->cacheKey = cacheKey;
489 job->elementKey = elementKey;
490 job->spec = spec;
491 const bool synchronous = !client;
492 if (synchronous || !(m_strategies & KGameRenderer::UseRenderingThreads))
493 {
494 KGRInternal::Worker worker(job, true, this);
495 worker.run();
496 //if everything worked fine, result is in high-speed cache now
497 const QPixmap result = m_pixmapCache.value(cacheKey);
498 requestPixmap__propagateResult(result, client, synchronousResult);
499 }
500 else
501 {
502 m_workerPool.start(new KGRInternal::Worker(job, !client, this));
503 m_pendingRequests << cacheKey;
504 }
505}
506
507void KGameRendererPrivate::jobFinished(KGRInternal::Job* job, bool isSynchronous)
508{
509 //read job
510 const QString cacheKey = job->cacheKey;
511 const QImage result = job->result;
512 delete job;
513 //check who wanted this pixmap
514 m_pendingRequests.removeAll(cacheKey);
515 const QList<KGameRendererClient*> requesters = m_clients.keys(cacheKey);
516 //put result into image cache
517 if (m_strategies & KGameRenderer::UseDiskCache)
518 {
519 m_imageCache->insertImage(cacheKey, result);
520 //convert result to pixmap (and put into pixmap cache) only if it is needed now
521 //This optimization saves the image-pixmap conversion for intermediate sizes which occur during smooth resize events or window initializations.
522 if (!isSynchronous && requesters.isEmpty())
523 {
524 return;
525 }
526 }
527 const QPixmap pixmap = QPixmap::fromImage(result);
528 m_pixmapCache.insert(cacheKey, pixmap);
529 foreach (KGameRendererClient* requester, requesters)
530 {
531 requester->receivePixmap(pixmap);
532 }
533}
534
535//BEGIN KGRInternal::Job/Worker
536
537KGRInternal::Worker::Worker(KGRInternal::Job* job, bool isSynchronous, KGameRendererPrivate* parent)
538 : m_job(job)
539 , m_synchronous(isSynchronous)
540 , m_parent(parent)
541{
542}
543
544static const uint transparentRgba = QColor(Qt::transparent).rgba();
545
546void KGRInternal::Worker::run()
547{
548 QImage image(m_job->spec.size, QImage::Format_ARGB32_Premultiplied);
549 image.fill(transparentRgba);
550 QPainter* painter = 0;
551 QPaintDeviceColorProxy* proxy = 0;
552 //if no custom colors requested, paint directly onto image
553 if (m_job->spec.customColors.isEmpty())
554 {
555 painter = new QPainter(&image);
556 }
557 else
558 {
559 proxy = new QPaintDeviceColorProxy(&image, m_job->spec.customColors);
560 painter = new QPainter(proxy);
561 }
562
563 //do renderering
564 QSvgRenderer* renderer = m_job->rendererPool->allocRenderer();
565 renderer->render(painter, m_job->elementKey);
566 m_job->rendererPool->freeRenderer(renderer);
567 delete painter;
568 delete proxy;
569
570 //talk back to the main thread
571 m_job->result = image;
572 QMetaObject::invokeMethod(
573 m_parent, "jobFinished", Qt::AutoConnection,
574 Q_ARG(KGRInternal::Job*, m_job), Q_ARG(bool, m_synchronous)
575 );
576 //NOTE: KGR::spritePixmap relies on Qt::DirectConnection when this method is run in the main thread.
577}
578
579//END KGRInternal::Job/Worker
580
581//BEGIN KGRInternal::RendererPool
582
583KGRInternal::RendererPool::RendererPool(QThreadPool* threadPool)
584 : m_valid(Checked_Invalid) //don't try to allocate renderers until given a valid SVG file
585 , m_threadPool(threadPool)
586{
587}
588
589KGRInternal::RendererPool::~RendererPool()
590{
591 //This deletes all renderers.
592 setPath(QString());
593}
594
595void KGRInternal::RendererPool::setPath(const QString& graphicsPath, QSvgRenderer* renderer)
596{
597 QMutexLocker locker(&m_mutex);
598 //delete all renderers
599 m_threadPool->waitForDone();
600 QHash<QSvgRenderer*, QThread*>::const_iterator it1 = m_hash.constBegin(), it2 = m_hash.constEnd();
601 for (; it1 != it2; ++it1)
602 {
603 Q_ASSERT(it1.value() == 0); //nobody may be using our renderers anymore now
604 delete it1.key();
605 }
606 m_hash.clear();
607 //set path
608 m_path = graphicsPath;
609 //existence of a renderer instance is evidence for the validity of the SVG file
610 if (renderer)
611 {
612 m_valid = Checked_Valid;
613 m_hash.insert(renderer, 0);
614 }
615 else
616 {
617 m_valid = Unchecked;
618 }
619}
620
621bool KGRInternal::RendererPool::hasAvailableRenderers() const
622{
623 //look for a renderer which is not associated with a thread
624 QMutexLocker locker(&m_mutex);
625 return m_hash.key(0) != 0;
626}
627
628QSvgRenderer* KGRInternal::RendererPool::allocRenderer()
629{
630 QThread* thread = QThread::currentThread();
631 //look for an available renderer
632 QMutexLocker locker(&m_mutex);
633 QSvgRenderer* renderer = m_hash.key(0);
634 if (!renderer)
635 {
636 //instantiate a new renderer (only if the SVG file has not been found to be invalid yet)
637 if (m_valid == Checked_Invalid)
638 {
639 return 0;
640 }
641 renderer = new QSvgRenderer(m_path);
642 m_valid = renderer->isValid() ? Checked_Valid : Checked_Invalid;
643 }
644 //mark renderer as used
645 m_hash.insert(renderer, thread);
646 return renderer;
647}
648
649void KGRInternal::RendererPool::freeRenderer(QSvgRenderer* renderer)
650{
651 //mark renderer as available
652 QMutexLocker locker(&m_mutex);
653 m_hash.insert(renderer, 0);
654}
655
656//END KGRInternal::RendererPool
657
658#include "kgamerenderer.moc"
659#include "kgamerenderer_p.moc"
660