1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qpixmapcache.h" |
5 | #include "qobject.h" |
6 | #include "qdebug.h" |
7 | #include "qpixmapcache_p.h" |
8 | #include "qthread.h" |
9 | #include "qcoreapplication.h" |
10 | |
11 | using namespace std::chrono_literals; |
12 | |
13 | QT_BEGIN_NAMESPACE |
14 | |
15 | /*! |
16 | \class QPixmapCache |
17 | \inmodule QtGui |
18 | |
19 | \brief The QPixmapCache class provides an application-wide cache for pixmaps. |
20 | |
21 | This class is a tool for optimized drawing with QPixmap. You can |
22 | use it to store temporary pixmaps that are expensive to generate |
23 | without using more storage space than cacheLimit(). Use insert() |
24 | to insert pixmaps, find() to find them, and clear() to empty the |
25 | cache. |
26 | |
27 | QPixmapCache contains no member data, only static functions to |
28 | access the global pixmap cache. It creates an internal QCache |
29 | object for caching the pixmaps. |
30 | |
31 | The cache associates a pixmap with a user-provided string as a key, |
32 | or with a QPixmapCache::Key that the cache generates. |
33 | Using QPixmapCache::Key for keys is faster than using strings. The string API is |
34 | very convenient for complex keys but the QPixmapCache::Key API will be very |
35 | efficient and convenient for a one-to-one object-to-pixmap mapping - in |
36 | this case, you can store the keys as members of an object. |
37 | |
38 | If two pixmaps are inserted into the cache using equal keys then the |
39 | last pixmap will replace the first pixmap in the cache. This follows the |
40 | behavior of the QHash and QCache classes. |
41 | |
42 | The cache becomes full when the total size of all pixmaps in the |
43 | cache exceeds cacheLimit(). The initial cache limit is 10240 KB (10 MB); |
44 | you can change this by calling setCacheLimit() with the required value. |
45 | A pixmap takes roughly (\e{width} * \e{height} * \e{depth})/8 bytes of |
46 | memory. |
47 | |
48 | The \e{Qt Quarterly} article |
49 | \l{http://doc.qt.io/archives/qq/qq12-qpixmapcache.html}{Optimizing |
50 | with QPixmapCache} explains how to use QPixmapCache to speed up |
51 | applications by caching the results of painting. |
52 | |
53 | \note QPixmapCache is only usable from the application's main thread. |
54 | Access from other threads will be ignored and return failure. |
55 | |
56 | \sa QCache, QPixmap |
57 | */ |
58 | |
59 | static const int cache_limit_default = 10240; // 10 MB cache limit |
60 | |
61 | static inline qsizetype cost(const QPixmap &pixmap) |
62 | { |
63 | // make sure to do a 64bit calculation; qsizetype might be smaller |
64 | const qint64 costKb = static_cast<qint64>(pixmap.width()) |
65 | * pixmap.height() * pixmap.depth() / (8 * 1024); |
66 | const qint64 costMax = std::numeric_limits<qsizetype>::max(); |
67 | // a small pixmap should have at least a cost of 1(kb) |
68 | return static_cast<qsizetype>(qBound(min: 1LL, val: costKb, max: costMax)); |
69 | } |
70 | |
71 | static inline bool qt_pixmapcache_thread_test() |
72 | { |
73 | if (Q_LIKELY(QCoreApplication::instance() && QThread::currentThread() == QCoreApplication::instance()->thread())) |
74 | return true; |
75 | |
76 | return false; |
77 | } |
78 | |
79 | /*! |
80 | \class QPixmapCache::Key |
81 | \brief The QPixmapCache::Key class can be used for efficient access |
82 | to the QPixmapCache. |
83 | \inmodule QtGui |
84 | \since 4.6 |
85 | |
86 | Use QPixmapCache::insert() to receive an instance of Key generated |
87 | by the pixmap cache. You can store the key in your own objects for |
88 | a very efficient one-to-one object-to-pixmap mapping. |
89 | */ |
90 | |
91 | /*! |
92 | Constructs an empty Key object. |
93 | */ |
94 | QPixmapCache::Key::Key() : d(nullptr) |
95 | { |
96 | } |
97 | |
98 | /*! |
99 | \internal |
100 | Constructs a copy of \a other. |
101 | */ |
102 | QPixmapCache::Key::Key(const Key &other) |
103 | { |
104 | if (other.d) |
105 | ++(other.d->ref); |
106 | d = other.d; |
107 | } |
108 | |
109 | /*! |
110 | Destroys the key. |
111 | */ |
112 | QPixmapCache::Key::~Key() |
113 | { |
114 | if (d && --(d->ref) == 0) |
115 | delete d; |
116 | } |
117 | |
118 | /*! |
119 | \internal |
120 | |
121 | Returns \c true if this key is the same as the given \a key; otherwise returns |
122 | false. |
123 | */ |
124 | bool QPixmapCache::Key::operator ==(const Key &key) const |
125 | { |
126 | return (d == key.d); |
127 | } |
128 | |
129 | /*! |
130 | \fn bool QPixmapCache::Key::operator !=(const Key &key) const |
131 | \internal |
132 | */ |
133 | |
134 | /*! |
135 | \fn QPixmapCache::Key::Key(Key &&) |
136 | \internal |
137 | \since 5.6 |
138 | */ |
139 | |
140 | /*! |
141 | \fn QPixmapCache::Key &QPixmapCache::Key::operator=(Key &&) |
142 | \internal |
143 | \since 5.6 |
144 | */ |
145 | |
146 | /*! |
147 | \fn void QPixmapCache::Key::swap(Key &) |
148 | \internal |
149 | \since 5.6 |
150 | */ |
151 | |
152 | /*! |
153 | Returns \c true if there is a cached pixmap associated with this key. |
154 | Otherwise, if pixmap was flushed, the key is no longer valid. |
155 | \since 5.7 |
156 | */ |
157 | bool QPixmapCache::Key::isValid() const noexcept |
158 | { |
159 | return d && d->isValid; |
160 | } |
161 | |
162 | /*! |
163 | \internal |
164 | */ |
165 | QPixmapCache::Key &QPixmapCache::Key::operator =(const Key &other) |
166 | { |
167 | if (d != other.d) { |
168 | if (other.d) |
169 | ++(other.d->ref); |
170 | if (d && --(d->ref) == 0) |
171 | delete d; |
172 | d = other.d; |
173 | } |
174 | return *this; |
175 | } |
176 | |
177 | class QPMCache : public QObject, public QCache<QPixmapCache::Key, QPixmapCacheEntry> |
178 | { |
179 | Q_OBJECT |
180 | public: |
181 | QPMCache(); |
182 | ~QPMCache(); |
183 | |
184 | void timerEvent(QTimerEvent *) override; |
185 | bool insert(const QString& key, const QPixmap &pixmap, int cost); |
186 | QPixmapCache::Key insert(const QPixmap &pixmap, int cost); |
187 | bool remove(const QString &key); |
188 | bool remove(const QPixmapCache::Key &key); |
189 | |
190 | void resizeKeyArray(int size); |
191 | QPixmapCache::Key createKey(); |
192 | void releaseKey(const QPixmapCache::Key &key); |
193 | void clear(); |
194 | |
195 | QPixmap *object(const QString &key) const; |
196 | QPixmap *object(const QPixmapCache::Key &key) const; |
197 | |
198 | static inline QPixmapCache::KeyData *get(const QPixmapCache::Key &key) |
199 | {return key.d;} |
200 | |
201 | static QPixmapCache::KeyData* getKeyData(QPixmapCache::Key *key); |
202 | |
203 | bool flushDetachedPixmaps(bool nt); |
204 | |
205 | private: |
206 | static constexpr auto soon_time = 10s; |
207 | static constexpr auto flush_time = 30s; |
208 | int *keyArray; |
209 | int theid; |
210 | int ps; |
211 | int keyArraySize; |
212 | int freeKey; |
213 | QHash<QString, QPixmapCache::Key> cacheKeys; |
214 | bool t; |
215 | }; |
216 | |
217 | QT_BEGIN_INCLUDE_NAMESPACE |
218 | #include "qpixmapcache.moc" |
219 | QT_END_INCLUDE_NAMESPACE |
220 | |
221 | /*! |
222 | size_t QPixmapCache::qHash(const Key &key, size_t seed = 0); |
223 | \since 6.6 |
224 | |
225 | Returns the hash value for the \a key, using \a seed to seed the calculation. |
226 | */ |
227 | size_t QPixmapCache::Key::hash(size_t seed) const noexcept |
228 | { |
229 | return qHash(key: this->d ? this->d->key : 0, seed); |
230 | } |
231 | |
232 | QPMCache::QPMCache() |
233 | : QObject(nullptr), |
234 | QCache<QPixmapCache::Key, QPixmapCacheEntry>(cache_limit_default), |
235 | keyArray(nullptr), theid(0), ps(0), keyArraySize(0), freeKey(0), t(false) |
236 | { |
237 | } |
238 | QPMCache::~QPMCache() |
239 | { |
240 | clear(); |
241 | free(ptr: keyArray); |
242 | } |
243 | |
244 | /* |
245 | This is supposed to cut the cache size down by about 25% in a |
246 | minute once the application becomes idle, to let any inserted pixmap |
247 | remain in the cache for some time before it becomes a candidate for |
248 | cleaning-up, and to not cut down the size of the cache while the |
249 | cache is in active use. |
250 | |
251 | When the last detached pixmap has been deleted from the cache, kill the |
252 | timer so Qt won't keep the CPU from going into sleep mode. Currently |
253 | the timer is not restarted when the pixmap becomes unused, but it does |
254 | restart once something else is added (i.e. the cache space is actually needed). |
255 | |
256 | Returns \c true if any were removed. |
257 | */ |
258 | bool QPMCache::flushDetachedPixmaps(bool nt) |
259 | { |
260 | auto mc = maxCost(); |
261 | const qsizetype currentTotal = totalCost(); |
262 | const qsizetype oldSize = size(); |
263 | if (currentTotal) |
264 | setMaxCost(nt ? currentTotal * 3 / 4 : currentTotal - 1); |
265 | setMaxCost(mc); |
266 | ps = totalCost(); |
267 | return size() != oldSize; |
268 | } |
269 | |
270 | void QPMCache::timerEvent(QTimerEvent *) |
271 | { |
272 | bool nt = totalCost() == ps; |
273 | if (!flushDetachedPixmaps(nt)) { |
274 | killTimer(id: theid); |
275 | theid = 0; |
276 | } else if (nt != t) { |
277 | killTimer(id: theid); |
278 | theid = startTimer(time: nt ? soon_time : flush_time); |
279 | t = nt; |
280 | } |
281 | } |
282 | |
283 | |
284 | QPixmap *QPMCache::object(const QString &key) const |
285 | { |
286 | if (const auto it = cacheKeys.find(key); it != cacheKeys.cend()) |
287 | return object(key: it.value()); |
288 | return nullptr; |
289 | } |
290 | |
291 | QPixmap *QPMCache::object(const QPixmapCache::Key &key) const |
292 | { |
293 | Q_ASSERT(key.isValid()); |
294 | QPixmap *ptr = QCache<QPixmapCache::Key, QPixmapCacheEntry>::object(key); |
295 | //We didn't find the pixmap in the cache, the key is not valid anymore |
296 | if (!ptr) |
297 | const_cast<QPMCache *>(this)->releaseKey(key); |
298 | return ptr; |
299 | } |
300 | |
301 | bool QPMCache::insert(const QString& key, const QPixmap &pixmap, int cost) |
302 | { |
303 | //If for the same key we add already a pixmap we should delete it |
304 | remove(key); |
305 | |
306 | // this will create a new key; the old one has been removed |
307 | auto k = insert(pixmap, cost); |
308 | if (k.isValid()) { |
309 | k.d->stringKey = key; |
310 | cacheKeys[key] = std::move(k); |
311 | return true; |
312 | } |
313 | return false; |
314 | } |
315 | |
316 | QPixmapCache::Key QPMCache::insert(const QPixmap &pixmap, int cost) |
317 | { |
318 | QPixmapCache::Key cacheKey = createKey(); // invalidated by ~QPixmapCacheEntry on failed insert |
319 | bool success = QCache<QPixmapCache::Key, QPixmapCacheEntry>::insert(key: cacheKey, object: new QPixmapCacheEntry(cacheKey, pixmap), cost); |
320 | Q_ASSERT(success || !cacheKey.isValid()); |
321 | if (success) { |
322 | if (!theid) { |
323 | theid = startTimer(time: flush_time); |
324 | t = false; |
325 | } |
326 | } |
327 | return cacheKey; |
328 | } |
329 | |
330 | bool QPMCache::remove(const QString &key) |
331 | { |
332 | const auto cacheKey = cacheKeys.take(key); |
333 | return cacheKey.isValid() && remove(key: cacheKey); |
334 | } |
335 | |
336 | bool QPMCache::remove(const QPixmapCache::Key &key) |
337 | { |
338 | return QCache<QPixmapCache::Key, QPixmapCacheEntry>::remove(key); |
339 | } |
340 | |
341 | void QPMCache::resizeKeyArray(int size) |
342 | { |
343 | if (size <= keyArraySize || size == 0) |
344 | return; |
345 | keyArray = q_check_ptr(p: static_cast<int *>(realloc(ptr: keyArray, |
346 | size: size * sizeof(int)))); |
347 | for (int i = keyArraySize; i != size; ++i) |
348 | keyArray[i] = i + 1; |
349 | keyArraySize = size; |
350 | } |
351 | |
352 | QPixmapCache::Key QPMCache::createKey() |
353 | { |
354 | if (freeKey == keyArraySize) |
355 | resizeKeyArray(size: keyArraySize ? keyArraySize << 1 : 2); |
356 | int id = freeKey; |
357 | freeKey = keyArray[id]; |
358 | QPixmapCache::Key key; |
359 | QPixmapCache::KeyData *d = QPMCache::getKeyData(key: &key); |
360 | d->key = ++id; |
361 | return key; |
362 | } |
363 | |
364 | void QPMCache::releaseKey(const QPixmapCache::Key &key) |
365 | { |
366 | QPixmapCache::KeyData *keyData = key.d; |
367 | if (!keyData) |
368 | return; |
369 | if (!keyData->stringKey.isNull()) |
370 | cacheKeys.remove(key: keyData->stringKey); |
371 | if (keyData->key > keyArraySize || keyData->key <= 0) |
372 | return; |
373 | keyData->key--; |
374 | keyArray[keyData->key] = freeKey; |
375 | freeKey = keyData->key; |
376 | keyData->isValid = false; |
377 | keyData->key = 0; |
378 | } |
379 | |
380 | void QPMCache::clear() |
381 | { |
382 | free(ptr: keyArray); |
383 | keyArray = nullptr; |
384 | freeKey = 0; |
385 | keyArraySize = 0; |
386 | //Mark all keys as invalid |
387 | const QList<QPixmapCache::Key> keys = QCache<QPixmapCache::Key, QPixmapCacheEntry>::keys(); |
388 | for (const auto &key : keys) { |
389 | if (key.d) |
390 | key.d->isValid = false; |
391 | } |
392 | QCache<QPixmapCache::Key, QPixmapCacheEntry>::clear(); |
393 | // Nothing left to flush; stop the timer |
394 | if (theid) { |
395 | killTimer(id: theid); |
396 | theid = 0; |
397 | } |
398 | } |
399 | |
400 | QPixmapCache::KeyData* QPMCache::getKeyData(QPixmapCache::Key *key) |
401 | { |
402 | if (!key->d) |
403 | key->d = new QPixmapCache::KeyData; |
404 | return key->d; |
405 | } |
406 | |
407 | Q_GLOBAL_STATIC(QPMCache, pm_cache) |
408 | |
409 | int Q_AUTOTEST_EXPORT q_QPixmapCache_keyHashSize() |
410 | { |
411 | return pm_cache()->size(); |
412 | } |
413 | |
414 | QPixmapCacheEntry::~QPixmapCacheEntry() |
415 | { |
416 | pm_cache()->releaseKey(key); |
417 | } |
418 | |
419 | /*! |
420 | Looks for a cached pixmap associated with the given \a key in the cache. |
421 | If the pixmap is found, the function sets \a pixmap to that pixmap and |
422 | returns \c true; otherwise it leaves \a pixmap alone and returns \c false. |
423 | |
424 | \since 4.6 |
425 | |
426 | Example: |
427 | \snippet code/src_gui_image_qpixmapcache.cpp 1 |
428 | */ |
429 | |
430 | bool QPixmapCache::find(const QString &key, QPixmap *pixmap) |
431 | { |
432 | if (key.isEmpty() || !qt_pixmapcache_thread_test()) |
433 | return false; |
434 | QPixmap *ptr = pm_cache()->object(key); |
435 | if (ptr && pixmap) |
436 | *pixmap = *ptr; |
437 | return ptr != nullptr; |
438 | } |
439 | |
440 | /*! |
441 | Looks for a cached pixmap associated with the given \a key in the cache. |
442 | If the pixmap is found, the function sets \a pixmap to that pixmap and |
443 | returns \c true; otherwise it leaves \a pixmap alone and returns \c false. If |
444 | the pixmap is not found, it means that the \a key is no longer valid, |
445 | so it will be released for the next insertion. |
446 | |
447 | \since 4.6 |
448 | */ |
449 | bool QPixmapCache::find(const Key &key, QPixmap *pixmap) |
450 | { |
451 | if (!qt_pixmapcache_thread_test()) |
452 | return false; |
453 | //The key is not valid anymore, a flush happened before probably |
454 | if (!key.d || !key.d->isValid) |
455 | return false; |
456 | QPixmap *ptr = pm_cache()->object(key); |
457 | if (ptr && pixmap) |
458 | *pixmap = *ptr; |
459 | return ptr != nullptr; |
460 | } |
461 | |
462 | /*! |
463 | Inserts a copy of the pixmap \a pixmap associated with the \a key into |
464 | the cache. |
465 | |
466 | All pixmaps inserted by the Qt library have a key starting with |
467 | "$qt", so your own pixmap keys should never begin "$qt". |
468 | |
469 | When a pixmap is inserted and the cache is about to exceed its |
470 | limit, it removes pixmaps until there is enough room for the |
471 | pixmap to be inserted. |
472 | |
473 | The oldest pixmaps (least recently accessed in the cache) are |
474 | deleted when more space is needed. |
475 | |
476 | The function returns \c true if the object was inserted into the |
477 | cache; otherwise it returns \c false. |
478 | |
479 | \sa setCacheLimit() |
480 | */ |
481 | |
482 | bool QPixmapCache::insert(const QString &key, const QPixmap &pixmap) |
483 | { |
484 | if (key.isEmpty() || !qt_pixmapcache_thread_test()) |
485 | return false; |
486 | return pm_cache()->insert(key, pixmap, cost: cost(pixmap)); |
487 | } |
488 | |
489 | /*! |
490 | Inserts a copy of the given \a pixmap into the cache and returns a key |
491 | that can be used to retrieve it. |
492 | |
493 | When a pixmap is inserted and the cache is about to exceed its |
494 | limit, it removes pixmaps until there is enough room for the |
495 | pixmap to be inserted. |
496 | |
497 | The oldest pixmaps (least recently accessed in the cache) are |
498 | deleted when more space is needed. |
499 | |
500 | \sa setCacheLimit(), replace() |
501 | |
502 | \since 4.6 |
503 | */ |
504 | QPixmapCache::Key QPixmapCache::insert(const QPixmap &pixmap) |
505 | { |
506 | if (!qt_pixmapcache_thread_test()) |
507 | return QPixmapCache::Key(); |
508 | return pm_cache()->insert(pixmap, cost: cost(pixmap)); |
509 | } |
510 | |
511 | #if QT_DEPRECATED_SINCE(6, 6) |
512 | /*! |
513 | \fn bool QPixmapCache::replace(const Key &key, const QPixmap &pixmap) |
514 | |
515 | \deprecated [6.6] Use \c{remove(key); key = insert(pixmap);} instead. |
516 | |
517 | Replaces the pixmap associated with the given \a key with the \a pixmap |
518 | specified. Returns \c true if the \a pixmap has been correctly inserted into |
519 | the cache; otherwise returns \c false. |
520 | |
521 | The passed \a key is updated to reference \a pixmap now. Other copies of \a |
522 | key, if any, still refer to the old pixmap, which is, however, removed from |
523 | the cache by this function. |
524 | |
525 | \sa setCacheLimit(), insert() |
526 | |
527 | \since 4.6 |
528 | */ |
529 | #endif // QT_DEPRECATED_SINCE(6, 6) |
530 | |
531 | /*! |
532 | Returns the cache limit (in kilobytes). |
533 | |
534 | The default cache limit is 10240 KB. |
535 | |
536 | \sa setCacheLimit() |
537 | */ |
538 | |
539 | int QPixmapCache::cacheLimit() |
540 | { |
541 | if (!qt_pixmapcache_thread_test()) |
542 | return 0; |
543 | return pm_cache()->maxCost(); |
544 | } |
545 | |
546 | /*! |
547 | Sets the cache limit to \a n kilobytes. |
548 | |
549 | The default setting is 10240 KB. |
550 | |
551 | \sa cacheLimit() |
552 | */ |
553 | |
554 | void QPixmapCache::setCacheLimit(int n) |
555 | { |
556 | if (!qt_pixmapcache_thread_test()) |
557 | return; |
558 | pm_cache()->setMaxCost(n); |
559 | } |
560 | |
561 | /*! |
562 | Removes the pixmap associated with \a key from the cache. |
563 | */ |
564 | void QPixmapCache::remove(const QString &key) |
565 | { |
566 | if (key.isEmpty() || !qt_pixmapcache_thread_test()) |
567 | return; |
568 | pm_cache()->remove(key); |
569 | } |
570 | |
571 | /*! |
572 | Removes the pixmap associated with \a key from the cache and releases |
573 | the key for a future insertion. |
574 | |
575 | \since 4.6 |
576 | */ |
577 | void QPixmapCache::remove(const Key &key) |
578 | { |
579 | if (!qt_pixmapcache_thread_test()) |
580 | return; |
581 | //The key is not valid anymore, a flush happened before probably |
582 | if (!key.d || !key.d->isValid) |
583 | return; |
584 | pm_cache()->remove(key); |
585 | } |
586 | |
587 | /*! |
588 | Removes all pixmaps from the cache. |
589 | */ |
590 | |
591 | void QPixmapCache::clear() |
592 | { |
593 | if (!QCoreApplication::closingDown() && !qt_pixmapcache_thread_test()) |
594 | return; |
595 | QT_TRY { |
596 | if (pm_cache.exists()) |
597 | pm_cache->clear(); |
598 | } QT_CATCH(const std::bad_alloc &) { |
599 | // if we ran out of memory during pm_cache(), it's no leak, |
600 | // so just ignore it. |
601 | } |
602 | } |
603 | |
604 | Q_AUTOTEST_EXPORT void qt_qpixmapcache_flush_detached_pixmaps() // for tst_qpixmapcache |
605 | { |
606 | if (!qt_pixmapcache_thread_test()) |
607 | return; |
608 | pm_cache()->flushDetachedPixmaps(nt: true); |
609 | } |
610 | |
611 | Q_AUTOTEST_EXPORT int qt_qpixmapcache_qpixmapcache_total_used() // for tst_qpixmapcache |
612 | { |
613 | if (!qt_pixmapcache_thread_test()) |
614 | return 0; |
615 | return (pm_cache()->totalCost()+1023) / 1024; |
616 | } |
617 | |
618 | /*! |
619 | \fn QPixmapCache::KeyData::KeyData() |
620 | |
621 | \internal |
622 | */ |
623 | /*! |
624 | \fn QPixmapCache::KeyData::KeyData(const KeyData &other) |
625 | \internal |
626 | */ |
627 | /*! |
628 | \fn QPixmapCache::KeyData::~KeyData() |
629 | |
630 | \internal |
631 | */ |
632 | QT_END_NAMESPACE |
633 | |