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