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