(Scene3D) Ensure remove image-resource-loader cache when app terminated
[platform/core/uifw/dali-toolkit.git] / dali-scene3d / internal / common / image-resource-loader.cpp
1 /*
2  * Copyright (c) 2023 Samsung Electronics Co., Ltd.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  */
17
18 // CLASS HEADER
19 #include <dali-scene3d/internal/common/image-resource-loader.h>
20
21 // EXTERNAL INCLUDES
22 #include <dali-toolkit/public-api/image-loader/sync-image-loader.h>
23 #include <dali/devel-api/adaptor-framework/lifecycle-controller.h>
24 #include <dali/devel-api/common/hash.h>
25 #include <dali/devel-api/common/map-wrapper.h>
26 #include <dali/devel-api/threading/mutex.h>
27 #include <dali/integration-api/adaptor-framework/adaptor.h>
28 #include <dali/integration-api/debug.h>
29 #include <dali/public-api/adaptor-framework/timer.h>
30 #include <dali/public-api/common/vector-wrapper.h>
31 #include <dali/public-api/object/base-object.h>
32 #include <dali/public-api/signals/connection-tracker.h>
33
34 #include <functional> ///< for std::function
35 #include <memory> ///< for std::shared_ptr
36 #include <mutex>
37 #include <string>
38 #include <utility> ///< for std::pair
39
40 // INTERNAL INCLUDES
41
42 namespace
43 {
44 constexpr uint32_t MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL = 5u;
45 constexpr uint32_t GC_PERIOD_MILLISECONDS                     = 1000u;
46
47 #ifdef DEBUG_ENABLED
48 Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, false, "LOG_IMAGE_RESOURCE_LOADER");
49 #endif
50
51 struct ImageInformation
52 {
53   ImageInformation(const std::string           url,
54                    const Dali::ImageDimensions dimensions,
55                    Dali::FittingMode::Type     fittingMode,
56                    Dali::SamplingMode::Type    samplingMode,
57                    bool                        orientationCorrection)
58   : mUrl(url),
59     mDimensions(dimensions),
60     mFittingMode(fittingMode),
61     mSamplingMode(samplingMode),
62     mOrientationCorrection(orientationCorrection)
63   {
64   }
65
66   bool operator==(const ImageInformation& rhs) const
67   {
68     // Check url and orientation correction is enough.
69     return (mUrl == rhs.mUrl) && (mOrientationCorrection == rhs.mOrientationCorrection);
70   }
71
72   std::string              mUrl;
73   Dali::ImageDimensions    mDimensions;
74   Dali::FittingMode::Type  mFittingMode;
75   Dali::SamplingMode::Type mSamplingMode;
76   bool                     mOrientationCorrection;
77 };
78
79 // Hash functor list
80 std::size_t GenerateHash(const ImageInformation& info)
81 {
82   std::vector<std::uint8_t> hashTarget;
83   const uint16_t            width  = info.mDimensions.GetWidth();
84   const uint16_t            height = info.mDimensions.GetHeight();
85
86   // If either the width or height has been specified, include the resizing options in the hash
87   if(width != 0 || height != 0)
88   {
89     // We are appending 5 bytes to the URL to form the hash input.
90     hashTarget.resize(5u);
91     std::uint8_t* hashTargetPtr = &(hashTarget[0u]);
92
93     // Pack the width and height (4 bytes total).
94     *hashTargetPtr++ = info.mDimensions.GetWidth() & 0xff;
95     *hashTargetPtr++ = (info.mDimensions.GetWidth() >> 8u) & 0xff;
96     *hashTargetPtr++ = info.mDimensions.GetHeight() & 0xff;
97     *hashTargetPtr++ = (info.mDimensions.GetHeight() >> 8u) & 0xff;
98
99     // Bit-pack the FittingMode, SamplingMode and orientation correction.
100     // FittingMode=2bits, SamplingMode=3bits, orientationCorrection=1bit
101     *hashTargetPtr = (info.mFittingMode << 4u) | (info.mSamplingMode << 1) | (info.mOrientationCorrection ? 1 : 0);
102   }
103   else
104   {
105     // We are not including sizing information, but we still need an extra byte for orientationCorrection.
106     hashTarget.resize(1u);
107     hashTarget[0u] = info.mOrientationCorrection ? 't' : 'f';
108   }
109
110   return Dali::CalculateHash(info.mUrl) ^ Dali::CalculateHash(hashTarget);
111 }
112
113 std::size_t GenerateHash(const Dali::PixelData& pixelData, bool mipmapRequired)
114 {
115   return reinterpret_cast<std::size_t>(static_cast<void*>(pixelData.GetObjectPtr())) ^ (static_cast<std::size_t>(mipmapRequired) << (sizeof(std::size_t) * 4));
116 }
117
118 std::size_t GenerateHash(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
119 {
120   std::size_t result = 0x12345678u + pixelDataList.size();
121   for(const auto& mipmapPixelDataList : pixelDataList)
122   {
123     result += (result << 5) + mipmapPixelDataList.size();
124     for(const auto& pixelData : mipmapPixelDataList)
125     {
126       result += (result << 5) + GenerateHash(pixelData, false);
127     }
128   }
129
130   return result ^ (static_cast<std::size_t>(mipmapRequired) << (sizeof(std::size_t) * 4));
131 }
132
133 // Item Creation functor list
134
135 Dali::PixelData CreatePixelDataFromImageInfo(const ImageInformation& info, bool /* Not used */)
136 {
137   return Dali::Toolkit::SyncImageLoader::Load(info.mUrl, info.mDimensions, info.mFittingMode, info.mSamplingMode, info.mOrientationCorrection);
138 }
139
140 Dali::Texture CreateTextureFromPixelData(const Dali::PixelData& pixelData, bool mipmapRequired)
141 {
142   Dali::Texture texture;
143   if(pixelData)
144   {
145     texture = Dali::Texture::New(Dali::TextureType::TEXTURE_2D, pixelData.GetPixelFormat(), pixelData.GetWidth(), pixelData.GetHeight());
146     texture.Upload(pixelData, 0, 0, 0, 0, pixelData.GetWidth(), pixelData.GetHeight());
147     if(mipmapRequired)
148     {
149       texture.GenerateMipmaps();
150     }
151   }
152   return texture;
153 }
154
155 Dali::Texture CreateCubeTextureFromPixelDataList(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
156 {
157   Dali::Texture texture;
158   if(!pixelDataList.empty() && !pixelDataList[0].empty())
159   {
160     texture = Dali::Texture::New(Dali::TextureType::TEXTURE_CUBE, pixelDataList[0][0].GetPixelFormat(), pixelDataList[0][0].GetWidth(), pixelDataList[0][0].GetHeight());
161     for(size_t iSide = 0u, iEndSize = pixelDataList.size(); iSide < iEndSize; ++iSide)
162     {
163       auto& side = pixelDataList[iSide];
164       for(size_t iMipLevel = 0u, iEndMipLevel = pixelDataList[0].size(); iMipLevel < iEndMipLevel; ++iMipLevel)
165       {
166         texture.Upload(side[iMipLevel], Dali::CubeMapLayer::POSITIVE_X + iSide, iMipLevel, 0u, 0u, side[iMipLevel].GetWidth(), side[iMipLevel].GetHeight());
167       }
168     }
169     if(mipmapRequired)
170     {
171       texture.GenerateMipmaps();
172     }
173   }
174
175   return texture;
176 }
177
178 // Forward declare, for signal connection.
179 void DestroyCacheImpl();
180
181 class CacheImpl : public Dali::ConnectionTracker
182 {
183 public:
184   /**
185    * @brief Constructor
186    */
187   CacheImpl()
188   : mPixelDataCache{},
189     mTextureCache{},
190     mCubeTextureCache{},
191     mTimer{},
192     mLatestCollectedPixelDataIter{mPixelDataCache.begin()},
193     mLatestCollectedTextureIter{mTextureCache.begin()},
194     mLatestCollectedCubeTextureIter{mCubeTextureCache.begin()},
195     mPixelDataContainerUpdated{false},
196     mTextureContainerUpdated{false},
197     mCubeTextureContainerUpdated{false},
198     mDataMutex{},
199     mDestroyed{false},
200     mFullCollectRequested{false}
201   {
202     DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create CacheImpl\n");
203
204     // We should create CacheImpl at main thread, To ensure delete this cache impl
205     Dali::LifecycleController::Get().TerminateSignal().Connect(DestroyCacheImpl);
206   }
207
208   /**
209    * @brief Destructor
210    */
211   ~CacheImpl()
212   {
213     DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Destroy CacheImpl\n");
214     {
215       mDataMutex.lock();
216
217       mDestroyed                      = true;
218       mPixelDataContainerUpdated      = false;
219       mTextureContainerUpdated        = false;
220       mCubeTextureContainerUpdated    = false;
221       mLatestCollectedPixelDataIter   = decltype(mLatestCollectedPixelDataIter)();   // Invalidate iterator
222       mLatestCollectedTextureIter     = decltype(mLatestCollectedTextureIter)();     // Invalidate iterator
223       mLatestCollectedCubeTextureIter = decltype(mLatestCollectedCubeTextureIter){}; // Invalidate iterator
224
225       mPixelDataCache.clear();
226       mTextureCache.clear();
227       mCubeTextureCache.clear();
228
229       mDataMutex.unlock();
230     }
231
232     if(mTimer)
233     {
234       if(Dali::Adaptor::IsAvailable())
235       {
236         mTimer.Stop();
237       }
238     }
239   }
240
241 private: // Unified API for this class
242   // Let compare with hash first. And then, check detail keys after.
243   using PixelDataCacheContainer   = std::map<std::size_t, std::vector<std::pair<ImageInformation, Dali::PixelData>>>;
244   using TextureCacheContainer     = std::map<std::size_t, std::vector<std::pair<Dali::PixelData, Dali::Texture>>>;
245   using CubeTextureCacheContainer = std::map<std::size_t, std::vector<std::pair<std::vector<std::vector<Dali::PixelData>>, Dali::Texture>>>;
246
247   /**
248    * @brief Try to get cached item, or create new handle if there is no item.
249    *
250    * @tparam needMutex Whether we need to lock the mutex during this operation, or not.
251    * @param[in] cacheContainer The container of key / item pair.
252    * @param[in] hashValue The hash value of key.
253    * @param[in] key The key of cache item.
254    * @param[in] keyFlag The additional flags when we need to create new item.
255    * @param[out] containerUpdate True whether container changed or not.
256    * @return Item that has been cached. Or newly created.
257    */
258   template<bool needMutex, typename KeyType, typename ItemType, ItemType (*ItemCreationFunction)(const KeyType&, bool), typename ContainerType>
259   ItemType GetOrCreateCachedItem(ContainerType& cacheContainer, std::size_t hashValue, const KeyType& key, bool keyFlag, bool& containerUpdated)
260   {
261     if constexpr(needMutex)
262     {
263       mDataMutex.lock();
264     }
265     ItemType returnItem;
266
267     if(DALI_LIKELY(!mDestroyed))
268     {
269       bool found = false;
270
271       auto iter = cacheContainer.lower_bound(hashValue);
272       if((iter == cacheContainer.end()) || (hashValue != iter->first))
273       {
274         containerUpdated = true;
275
276         returnItem = ItemCreationFunction(key, keyFlag);
277         DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n");
278         cacheContainer.insert(iter, {hashValue, {{key, returnItem}}});
279       }
280       else
281       {
282         auto& cachePairList = iter->second;
283         for(auto jter = cachePairList.begin(), jterEnd = cachePairList.end(); jter != jterEnd; ++jter)
284         {
285           if(jter->first == key)
286           {
287             // We found that input pixelData already cached.
288             returnItem = jter->second;
289             DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Get cached item\n");
290             found = true;
291             break;
292           }
293         }
294
295         // If we fail to found same list, just append.
296         if(!found)
297         {
298           containerUpdated = true;
299
300           returnItem = ItemCreationFunction(key, keyFlag);
301           DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n");
302           cachePairList.emplace_back(key, returnItem);
303         }
304       }
305     }
306
307     if constexpr(needMutex)
308     {
309       mDataMutex.unlock();
310     }
311
312     return returnItem;
313   }
314   /**
315    * @brief Try to collect garbages, which reference counts are 1.
316    *
317    * @tparam needMutex Whether we need to lock the mutex during this operation, or not.
318    * @param[in] cacheContainer The container of key / item pair.
319    * @param[in] fullCollect True if we need to collect whole container.
320    * @param[in, out] containerUpdated True if container information changed. lastIterator will be begin of container when we start collect garbages.
321    * @param[in, out] lastIterator The last iterator of container.
322    * @oaram[in, out] checkedCount The number of iteration checked total.
323    * @return True if we iterate whole container, so we don't need to check anymore. False otherwise
324    */
325   template<bool needMutex, typename ContainerType, typename Iterator = typename ContainerType::iterator>
326   bool CollectGarbages(ContainerType& cacheContainer, bool fullCollect, bool& containerUpdated, Iterator& lastIterator, uint32_t& checkedCount)
327   {
328     if constexpr(needMutex)
329     {
330       mDataMutex.lock();
331     }
332
333     DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Collect Garbages : %zu\n", cacheContainer.size());
334     // Container changed. We should re-collect garbage from begin again.
335     if(fullCollect || containerUpdated)
336     {
337       lastIterator     = cacheContainer.begin();
338       containerUpdated = false;
339     }
340
341     for(; lastIterator != cacheContainer.end() && (fullCollect || ++checkedCount <= MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL);)
342     {
343       auto& cachePairList = lastIterator->second;
344
345       for(auto jter = cachePairList.begin(); jter != cachePairList.end();)
346       {
347         auto& item = jter->second;
348         DALI_LOG_INFO(gLogFilter, Debug::Verbose, "item : %p, ref count : %u\n", item.GetObjectPtr(), (item ? item.GetBaseObject().ReferenceCount() : 0u));
349         if(!item || (item.GetBaseObject().ReferenceCount() == 1u))
350         {
351           // This item is garbage! just remove it.
352           jter = cachePairList.erase(jter);
353         }
354         else
355         {
356           ++jter;
357         }
358       }
359
360       if(cachePairList.empty())
361       {
362         lastIterator = cacheContainer.erase(lastIterator);
363       }
364       else
365       {
366         ++lastIterator;
367       }
368     }
369
370     if constexpr(needMutex)
371     {
372       mDataMutex.unlock();
373     }
374
375     return (lastIterator != cacheContainer.end());
376   }
377
378 public: // Called by main thread.
379   /**
380    * @brief Try to get cached texture, or newly create if there is no texture that already cached.
381    *
382    * @param[in] pixelData The pixelData of image.
383    * @param[in] mipmapRequired True if result texture need to generate mipmap.
384    * @return Texture that has been cached. Or empty handle if we fail to found cached item.
385    */
386   Dali::Texture GetOrCreateCachedTexture(const Dali::PixelData& pixelData, bool mipmapRequired)
387   {
388     auto hashValue = GenerateHash(pixelData, mipmapRequired);
389     return GetOrCreateCachedItem<false, Dali::PixelData, Dali::Texture, CreateTextureFromPixelData>(mTextureCache, hashValue, pixelData, mipmapRequired, mTextureContainerUpdated);
390   }
391
392   /**
393    * @brief Try to get cached cube texture, or newly create if there is no cube texture that already cached.
394    *
395    * @param[in] pixelDataList The pixelData list of image.
396    * @param[in] mipmapRequired True if result texture need to generate mipmap.
397    * @return Texture that has been cached. Or empty handle if we fail to found cached item.
398    */
399   Dali::Texture GetOrCreateCachedCubeTexture(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
400   {
401     auto hashValue = GenerateHash(pixelDataList, mipmapRequired);
402     return GetOrCreateCachedItem<false, std::vector<std::vector<Dali::PixelData>>, Dali::Texture, CreateCubeTextureFromPixelDataList>(mCubeTextureCache, hashValue, pixelDataList, mipmapRequired, mCubeTextureContainerUpdated);
403   }
404
405   /**
406    * @brief Request incremental gargabe collect.
407    *
408    * @param[in] fullCollect True if we will collect whole items, or incrementally.
409    */
410   void RequestGarbageCollect(bool fullCollect)
411   {
412     if(DALI_LIKELY(Dali::Adaptor::IsAvailable()))
413     {
414       if(!mTimer)
415       {
416         mTimer = Dali::Timer::New(GC_PERIOD_MILLISECONDS);
417         mTimer.TickSignal().Connect(this, &CacheImpl::OnTick);
418       }
419
420       mFullCollectRequested |= fullCollect;
421
422       if(!mTimer.IsRunning())
423       {
424         // Restart container interating.
425         if(!mPixelDataContainerUpdated)
426         {
427           mDataMutex.lock();
428           mPixelDataContainerUpdated = true;
429           mDataMutex.unlock();
430         }
431         mTextureContainerUpdated = true;
432         mTimer.Start();
433       }
434     }
435   }
436
437 public: // Can be called by worker thread
438   /**
439    * @brief Try to get cached pixel data, or newly create if there is no pixel data that already cached.
440    *
441    * @param[in] info The informations of image to load.
442    * @return Texture that has been cached. Or empty handle if we fail to found cached item.
443    */
444   Dali::PixelData GetOrCreateCachedPixelData(const ImageInformation& info)
445   {
446     auto hashValue = GenerateHash(info);
447     return GetOrCreateCachedItem<true, ImageInformation, Dali::PixelData, CreatePixelDataFromImageInfo>(mPixelDataCache, hashValue, info, false, mPixelDataContainerUpdated);
448   }
449
450 private: // Called by main thread
451   bool OnTick()
452   {
453     // Clear full GC flag
454     const bool fullCollect = mFullCollectRequested;
455     mFullCollectRequested  = false;
456
457     return IncrementalGarbageCollect(fullCollect);
458   }
459
460   /**
461    * @brief Remove unused cache item incrementally.
462    *
463    * @param[in] fullCollect True if we will collect whole items, or incrementally.
464    * @return True if there still exist what we need to check clean. False when whole cached items are using now.
465    */
466   bool IncrementalGarbageCollect(bool fullCollect)
467   {
468     bool continueTimer = false;
469
470     // Try to collect Texture GC first, due to the reference count of pixelData who become key of textures.
471     // After all texture GC finished, then check PixelData cache.
472     uint32_t checkedCount = 0u;
473
474     // GC Cube Texture
475     continueTimer |= CollectGarbages<false>(mCubeTextureCache, fullCollect, mCubeTextureContainerUpdated, mLatestCollectedCubeTextureIter, checkedCount);
476
477     // GC Texture
478     continueTimer |= CollectGarbages<false>(mTextureCache, fullCollect, mTextureContainerUpdated, mLatestCollectedTextureIter, checkedCount);
479
480     // GC PixelData. We should lock mutex during GC pixelData.
481     continueTimer |= CollectGarbages<true>(mPixelDataCache, fullCollect, mPixelDataContainerUpdated, mLatestCollectedPixelDataIter, checkedCount);
482
483     return continueTimer;
484   }
485
486 private:
487   PixelDataCacheContainer   mPixelDataCache;
488   TextureCacheContainer     mTextureCache;
489   CubeTextureCacheContainer mCubeTextureCache;
490
491   Dali::Timer mTimer;
492
493   // Be used when we garbage collection.
494   PixelDataCacheContainer::iterator   mLatestCollectedPixelDataIter;
495   TextureCacheContainer::iterator     mLatestCollectedTextureIter;
496   CubeTextureCacheContainer::iterator mLatestCollectedCubeTextureIter;
497
498   bool mPixelDataContainerUpdated;
499   bool mTextureContainerUpdated;
500   bool mCubeTextureContainerUpdated;
501
502   std::mutex mDataMutex;
503
504   bool mDestroyed : 1;
505   bool mFullCollectRequested : 1;
506 };
507
508 static std::shared_ptr<CacheImpl> gCacheImpl{nullptr};
509
510 std::shared_ptr<CacheImpl> GetCacheImpl()
511 {
512   if(DALI_UNLIKELY(!gCacheImpl))
513   {
514     gCacheImpl = std::make_shared<CacheImpl>();
515   }
516   return gCacheImpl;
517 }
518
519 void DestroyCacheImpl()
520 {
521   gCacheImpl.reset();
522 }
523
524 } // namespace
525
526 namespace Dali::Scene3D::Internal
527 {
528 namespace ImageResourceLoader
529 {
530 // Called by main thread..
531 Dali::PixelData GetEmptyPixelDataWhiteRGB()
532 {
533   static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[3]{0xff, 0xff, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY);
534   return emptyPixelData;
535 }
536
537 Dali::Texture GetEmptyTextureWhiteRGB()
538 {
539   static Dali::PixelData emptyPixelData = GetEmptyPixelDataWhiteRGB();
540   static Dali::Texture   emptyTexture   = Dali::Texture();
541   if(!emptyTexture)
542   {
543     emptyTexture = Texture::New(TextureType::TEXTURE_2D, emptyPixelData.GetPixelFormat(), emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
544     emptyTexture.Upload(emptyPixelData, 0, 0, 0, 0, emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
545   }
546   return emptyTexture;
547 }
548
549 Dali::Texture GetCachedTexture(Dali::PixelData pixelData, bool mipmapRequired)
550 {
551   return GetCacheImpl()->GetOrCreateCachedTexture(pixelData, mipmapRequired);
552 }
553
554 Dali::Texture GetCachedCubeTexture(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
555 {
556   return GetCacheImpl()->GetOrCreateCachedCubeTexture(pixelDataList, mipmapRequired);
557 }
558
559 void RequestGarbageCollect(bool fullCollect)
560 {
561   GetCacheImpl()->RequestGarbageCollect(fullCollect);
562 }
563
564 void EnsureResourceLoaderCreated()
565 {
566   GetCacheImpl();
567 }
568
569 // Can be called by worker thread.
570 Dali::PixelData GetCachedPixelData(const std::string& url)
571 {
572   return GetCachedPixelData(url, ImageDimensions(), FittingMode::DEFAULT, SamplingMode::BOX_THEN_LINEAR, true);
573 }
574
575 Dali::PixelData GetCachedPixelData(const std::string& url,
576                                    ImageDimensions    dimensions,
577                                    FittingMode::Type  fittingMode,
578                                    SamplingMode::Type samplingMode,
579                                    bool               orientationCorrection)
580 {
581   ImageInformation info(url, dimensions, fittingMode, samplingMode, orientationCorrection);
582   if(gCacheImpl == nullptr)
583   {
584     DALI_LOG_INFO(gLogFilter, Debug::Verbose, "CacheImpl not prepared! load PixelData without cache.\n");
585     return CreatePixelDataFromImageInfo(info, false);
586   }
587   else
588   {
589     return GetCacheImpl()->GetOrCreateCachedPixelData(info);
590   }
591 }
592 } // namespace ImageResourceLoader
593 } // namespace Dali::Scene3D::Internal