2323namespace {
2424
2525 constexpr Logger kLog (" thumbnail" );
26- constexpr int kThumbnailTargetPx = 192 ;
2726 constexpr float kThumbnailWebPQuality = 82 .0f ;
2827 constexpr std::size_t kMinWorkers = 2 ;
2928 constexpr std::size_t kMaxWorkers = 4 ;
@@ -58,7 +57,7 @@ namespace {
5857 return out;
5958 }
6059
61- std::optional<std::filesystem::path> cachePathForSource (const std::string& sourcePath) {
60+ std::optional<std::filesystem::path> cachePathForSource (const std::string& sourcePath, int targetPx ) {
6261 namespace fs = std::filesystem;
6362 std::error_code ec;
6463 const auto size = fs::file_size (sourcePath, ec);
@@ -78,7 +77,7 @@ namespace {
7877 + ' \n '
7978 + std::to_string (static_cast <long long >(ticks))
8079 + ' \n '
81- + std::to_string (kThumbnailTargetPx )
80+ + std::to_string (targetPx )
8281 + ' \n '
8382 + std::string (kThumbnailCacheVersion );
8483 return thumbnailCacheDir () / (hex64 (fnv1a64 (key)) + " .webp" );
@@ -122,13 +121,13 @@ namespace {
122121 return rgba;
123122 }
124123
125- bool resizeThumbnail (std::vector<std::uint8_t >& pixels, int & width, int & height) {
124+ bool resizeThumbnail (std::vector<std::uint8_t >& pixels, int & width, int & height, int targetPx ) {
126125 const int maxDim = std::max (width, height);
127- if (maxDim <= kThumbnailTargetPx || width <= 0 || height <= 0 ) {
126+ if (maxDim <= targetPx || width <= 0 || height <= 0 ) {
128127 return true ;
129128 }
130129
131- const float scale = static_cast <float >(kThumbnailTargetPx ) / static_cast <float >(maxDim);
130+ const float scale = static_cast <float >(targetPx ) / static_cast <float >(maxDim);
132131 const int resizedW = std::max (1 , static_cast <int >(std::lround (static_cast <float >(width) * scale)));
133132 const int resizedH = std::max (1 , static_cast <int >(std::lround (static_cast <float >(height) * scale)));
134133
@@ -147,6 +146,12 @@ namespace {
147146
148147} // namespace
149148
149+ std::size_t ThumbnailService::RequestKeyHash::operator ()(const RequestKey& key) const noexcept {
150+ const std::size_t pathHash = std::hash<std::string>{}(key.path );
151+ const std::size_t sizeHash = std::hash<int >{}(key.targetPx );
152+ return pathHash ^ (sizeHash + 0x9e3779b97f4a7c15ull + (pathHash << 6U ) + (pathHash >> 2U ));
153+ }
154+
150155ThumbnailService::Subscription::Subscription (std::function<void ()> disconnect) : m_disconnect(std::move(disconnect)) {}
151156
152157ThumbnailService::Subscription::~Subscription () { disconnect (); }
@@ -240,19 +245,22 @@ ThumbnailService::Subscription ThumbnailService::subscribePendingUpload(PendingU
240245 });
241246}
242247
243- ThumbnailService::Subscription ThumbnailService::subscribeReady (const std::string& path, ReadyCallback callback) {
248+ ThumbnailService::Subscription
249+ ThumbnailService::subscribeReady (const std::string& path, ReadyCallback callback, int targetPx) {
244250 if (path.empty () || !callback) {
245251 return {};
246252 }
247253
248- const TextureHandle current = peek (path);
254+ const TextureHandle current = peek (path, targetPx );
249255 if (current.id != 0 ) {
250256 callback (path, current);
251257 return {};
252258 }
253259
254260 const std::uint64_t id = m_nextListenerId++;
255- m_readyListeners.emplace (id, ReadyListener{.path = path, .callback = std::move (callback)});
261+ m_readyListeners.emplace (
262+ id, ReadyListener{.key = RequestKey{.path = path, .targetPx = targetPx}, .callback = std::move (callback)}
263+ );
256264
257265 std::weak_ptr<bool > token = m_lifetimeToken;
258266 return Subscription ([this , token, id]() {
@@ -264,31 +272,33 @@ ThumbnailService::Subscription ThumbnailService::subscribeReady(const std::strin
264272 });
265273}
266274
267- TextureHandle ThumbnailService::acquire (const std::string& path) {
275+ TextureHandle ThumbnailService::acquire (const std::string& path, int targetPx ) {
268276 if (path.empty ()) {
269277 return {};
270278 }
271279
272- CacheEntry& entry = m_entries[path];
280+ const RequestKey key{.path = path, .targetPx = targetPx};
281+ CacheEntry& entry = m_entries[key];
273282 ++entry.refCount ;
274283 if (entry.handle .id != 0 || entry.failed ) {
275284 return entry.handle ;
276285 }
277286
278- enqueueDecodeIfNeeded (path );
287+ enqueueDecodeIfNeeded (key );
279288 return {};
280289}
281290
282- TextureHandle ThumbnailService::peek (const std::string& path) const {
283- const auto it = m_entries.find (path);
291+ TextureHandle ThumbnailService::peek (const std::string& path, int targetPx ) const {
292+ const auto it = m_entries.find (RequestKey{. path = path, . targetPx = targetPx} );
284293 if (it == m_entries.end ()) {
285294 return {};
286295 }
287296 return it->second .handle ;
288297}
289298
290- void ThumbnailService::release (const std::string& path) {
291- const auto it = m_entries.find (path);
299+ void ThumbnailService::release (const std::string& path, int targetPx) {
300+ const RequestKey key{.path = path, .targetPx = targetPx};
301+ const auto it = m_entries.find (key);
292302 if (it == m_entries.end ()) {
293303 return ;
294304 }
@@ -308,19 +318,19 @@ void ThumbnailService::release(const std::string& path) {
308318 m_entries.erase (it);
309319
310320 std::lock_guard<std::mutex> lock (m_queueMutex);
311- if (m_inFlight.contains (path )) {
312- m_canceled.insert (path );
321+ if (m_inFlight.contains (key )) {
322+ m_canceled.insert (key );
313323 }
314324}
315325
316- void ThumbnailService::enqueueDecodeIfNeeded (const std::string& path ) {
326+ void ThumbnailService::enqueueDecodeIfNeeded (const RequestKey& key ) {
317327 std::lock_guard<std::mutex> lock (m_queueMutex);
318- m_canceled.erase (path );
319- if (m_inFlight.contains (path )) {
328+ m_canceled.erase (key );
329+ if (m_inFlight.contains (key )) {
320330 return ;
321331 }
322- m_inFlight.insert (path );
323- m_jobQueue.push_back (path );
332+ m_inFlight.insert (key );
333+ m_jobQueue.push_back (key );
324334 m_queueCv.notify_one ();
325335}
326336
@@ -342,8 +352,8 @@ bool ThumbnailService::uploadPending(TextureManager& textures) {
342352 bool dropped = false ;
343353 {
344354 std::lock_guard<std::mutex> lock (m_queueMutex);
345- m_inFlight.erase (job.path );
346- if (auto c = m_canceled.find (job.path ); c != m_canceled.end ()) {
355+ m_inFlight.erase (job.key );
356+ if (auto c = m_canceled.find (job.key ); c != m_canceled.end ()) {
347357 m_canceled.erase (c);
348358 dropped = true ;
349359 }
@@ -352,7 +362,7 @@ bool ThumbnailService::uploadPending(TextureManager& textures) {
352362 continue ;
353363 }
354364
355- auto entryIt = m_entries.find (job.path );
365+ auto entryIt = m_entries.find (job.key );
356366 if (entryIt == m_entries.end () || entryIt->second .refCount == 0 ) {
357367 continue ;
358368 }
@@ -364,7 +374,7 @@ bool ThumbnailService::uploadPending(TextureManager& textures) {
364374
365375 TextureHandle handle = textures.loadFromRgba (job.rgba .data (), job.width , job.height );
366376 if (handle.id == 0 ) {
367- kLog .warn (" failed to upload thumbnail texture for {}" , job.path );
377+ kLog .warn (" failed to upload thumbnail texture for {}" , job.key . path );
368378 entryIt->second .failed = true ;
369379 changed = true ;
370380 continue ;
@@ -376,29 +386,29 @@ bool ThumbnailService::uploadPending(TextureManager& textures) {
376386 entryIt->second .handle = handle;
377387 entryIt->second .failed = false ;
378388 changed = true ;
379- notifyReady (job.path , handle);
389+ notifyReady (job.key , handle);
380390 }
381391 return changed;
382392}
383393
384394void ThumbnailService::invalidateGpuResources (TextureManager& textures) {
385395 m_textureManager = &textures;
386396
387- std::vector<std::string> livePaths ;
388- livePaths .reserve (m_entries.size ());
389- for (auto & [path , entry] : m_entries) {
397+ std::vector<RequestKey> liveKeys ;
398+ liveKeys .reserve (m_entries.size ());
399+ for (auto & [key , entry] : m_entries) {
390400 if (entry.handle .id != 0 ) {
391401 m_textureManager->unload (entry.handle );
392402 }
393403 entry.handle = {};
394404 entry.failed = false ;
395405 if (entry.refCount > 0 ) {
396- livePaths .push_back (path );
406+ liveKeys .push_back (key );
397407 }
398408 }
399409
400- for (const std::string& path : livePaths ) {
401- enqueueDecodeIfNeeded (path );
410+ for (const RequestKey& key : liveKeys ) {
411+ enqueueDecodeIfNeeded (key );
402412 }
403413}
404414
@@ -444,8 +454,8 @@ void ThumbnailService::pushResult(DecodedJob job) {
444454}
445455
446456void ThumbnailService::deleteAllTextures () {
447- for (auto & [path , entry] : m_entries) {
448- (void )path ;
457+ for (auto & [key , entry] : m_entries) {
458+ (void )key ;
449459 if (entry.handle .id != 0 && m_textureManager != nullptr ) {
450460 m_textureManager->unload (entry.handle );
451461 }
@@ -468,10 +478,10 @@ void ThumbnailService::notifyPendingUpload() {
468478 }
469479}
470480
471- void ThumbnailService::notifyReady (const std::string& path , TextureHandle handle) {
481+ void ThumbnailService::notifyReady (const RequestKey& key , TextureHandle handle) {
472482 std::vector<std::pair<std::uint64_t , ReadyCallback>> callbacks;
473483 for (const auto & [id, listener] : m_readyListeners) {
474- if (listener.path == path && listener.callback ) {
484+ if (listener.key == key && listener.callback ) {
475485 callbacks.emplace_back (id, listener.callback );
476486 }
477487 }
@@ -482,27 +492,28 @@ void ThumbnailService::notifyReady(const std::string& path, TextureHandle handle
482492 continue ;
483493 }
484494 m_readyListeners.erase (it);
485- callback (path, handle);
495+ callback (key. path , handle);
486496 }
487497}
488498
489499void ThumbnailService::workerLoop () {
490500 while (true ) {
491- std::string path ;
501+ RequestKey key ;
492502 {
493503 std::unique_lock<std::mutex> lock (m_queueMutex);
494504 m_queueCv.wait (lock, [this ]() { return m_shutdown.load () || !m_jobQueue.empty (); });
495505 if (m_shutdown.load ()) {
496506 return ;
497507 }
498- path = std::move (m_jobQueue.front ());
508+ key = std::move (m_jobQueue.front ());
499509 m_jobQueue.pop_front ();
500510 }
501511
512+ const std::string& path = key.path ;
502513 DecodedJob result;
503- result.path = path ;
514+ result.key = key ;
504515
505- if (const auto cachePath = cachePathForSource (path); cachePath.has_value ()) {
516+ if (const auto cachePath = cachePathForSource (path, key. targetPx ); cachePath.has_value ()) {
506517 auto cachedBytes = FileUtils::readBinaryFile (cachePath->string ());
507518 if (!cachedBytes.empty ()) {
508519 if (auto cached = decodeRasterImage (cachedBytes.data (), cachedBytes.size ())) {
@@ -536,13 +547,13 @@ void ThumbnailService::workerLoop() {
536547 int h = decoded->height ;
537548 auto pixels = rgbaToRgb (decoded->pixels );
538549
539- if (!resizeThumbnail (pixels, w, h)) {
550+ if (!resizeThumbnail (pixels, w, h, key. targetPx )) {
540551 result.failed = true ;
541552 pushResult (std::move (result));
542553 continue ;
543554 }
544555
545- if (const auto cachePath = cachePathForSource (path); cachePath.has_value ()) {
556+ if (const auto cachePath = cachePathForSource (path, key. targetPx ); cachePath.has_value ()) {
546557 std::uint8_t * encoded = nullptr ;
547558 const std::size_t encodedSize = WebPEncodeRGB (pixels.data (), w, h, w * 3 , kThumbnailWebPQuality , &encoded);
548559 if (encoded != nullptr && encodedSize > 0 ) {
0 commit comments