2 * Copyright (c) 2023 Samsung Electronics Co., Ltd.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 #include <dali-scene3d/internal/common/image-resource-loader.h>
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>
34 #include <functional> ///< for std::function
35 #include <memory> ///< for std::shared_ptr
38 #include <utility> ///< for std::pair
44 constexpr uint32_t MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL = 5u;
45 constexpr uint32_t GC_PERIOD_MILLISECONDS = 1000u;
48 Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, false, "LOG_IMAGE_RESOURCE_LOADER");
51 struct ImageInformation
53 ImageInformation(const std::string url,
54 const Dali::ImageDimensions dimensions,
55 Dali::FittingMode::Type fittingMode,
56 Dali::SamplingMode::Type samplingMode,
57 bool orientationCorrection)
59 mDimensions(dimensions),
60 mFittingMode(fittingMode),
61 mSamplingMode(samplingMode),
62 mOrientationCorrection(orientationCorrection)
66 bool operator==(const ImageInformation& rhs) const
68 // Check url and orientation correction is enough.
69 return (mUrl == rhs.mUrl) && (mOrientationCorrection == rhs.mOrientationCorrection);
73 Dali::ImageDimensions mDimensions;
74 Dali::FittingMode::Type mFittingMode;
75 Dali::SamplingMode::Type mSamplingMode;
76 bool mOrientationCorrection;
80 std::size_t GenerateHash(const ImageInformation& info)
82 std::vector<std::uint8_t> hashTarget;
83 const uint16_t width = info.mDimensions.GetWidth();
84 const uint16_t height = info.mDimensions.GetHeight();
86 // If either the width or height has been specified, include the resizing options in the hash
87 if(width != 0 || height != 0)
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]);
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;
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);
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';
110 return Dali::CalculateHash(info.mUrl) ^ Dali::CalculateHash(hashTarget);
113 std::size_t GenerateHash(const Dali::PixelData& pixelData, bool mipmapRequired)
115 return reinterpret_cast<std::size_t>(static_cast<void*>(pixelData.GetObjectPtr())) ^ (static_cast<std::size_t>(mipmapRequired) << (sizeof(std::size_t) * 4));
118 std::size_t GenerateHash(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
120 std::size_t result = 0x12345678u + pixelDataList.size();
121 for(const auto& mipmapPixelDataList : pixelDataList)
123 result += (result << 5) + mipmapPixelDataList.size();
124 for(const auto& pixelData : mipmapPixelDataList)
126 result += (result << 5) + GenerateHash(pixelData, false);
130 return result ^ (static_cast<std::size_t>(mipmapRequired) << (sizeof(std::size_t) * 4));
133 // Item Creation functor list
135 Dali::PixelData CreatePixelDataFromImageInfo(const ImageInformation& info, bool /* Not used */)
137 return Dali::Toolkit::SyncImageLoader::Load(info.mUrl, info.mDimensions, info.mFittingMode, info.mSamplingMode, info.mOrientationCorrection);
140 Dali::Texture CreateTextureFromPixelData(const Dali::PixelData& pixelData, bool mipmapRequired)
142 Dali::Texture texture;
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());
149 texture.GenerateMipmaps();
155 Dali::Texture CreateCubeTextureFromPixelDataList(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
157 Dali::Texture texture;
158 if(!pixelDataList.empty() && !pixelDataList[0].empty())
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)
163 auto& side = pixelDataList[iSide];
164 for(size_t iMipLevel = 0u, iEndMipLevel = pixelDataList[0].size(); iMipLevel < iEndMipLevel; ++iMipLevel)
166 texture.Upload(side[iMipLevel], Dali::CubeMapLayer::POSITIVE_X + iSide, iMipLevel, 0u, 0u, side[iMipLevel].GetWidth(), side[iMipLevel].GetHeight());
171 texture.GenerateMipmaps();
178 // Forward declare, for signal connection.
179 void DestroyCacheImpl();
181 class CacheImpl : public Dali::ConnectionTracker
192 mLatestCollectedPixelDataIter{mPixelDataCache.begin()},
193 mLatestCollectedTextureIter{mTextureCache.begin()},
194 mLatestCollectedCubeTextureIter{mCubeTextureCache.begin()},
195 mPixelDataContainerUpdated{false},
196 mTextureContainerUpdated{false},
197 mCubeTextureContainerUpdated{false},
200 mFullCollectRequested{false}
202 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create CacheImpl\n");
204 // We should create CacheImpl at main thread, To ensure delete this cache impl
205 Dali::LifecycleController::Get().TerminateSignal().Connect(DestroyCacheImpl);
213 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Destroy CacheImpl\n");
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
225 mPixelDataCache.clear();
226 mTextureCache.clear();
227 mCubeTextureCache.clear();
234 if(Dali::Adaptor::IsAvailable())
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>>>;
248 * @brief Try to get cached item, or create new handle if there is no item.
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.
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)
261 if constexpr(needMutex)
267 if(DALI_LIKELY(!mDestroyed))
271 auto iter = cacheContainer.lower_bound(hashValue);
272 if((iter == cacheContainer.end()) || (hashValue != iter->first))
274 containerUpdated = true;
276 returnItem = ItemCreationFunction(key, keyFlag);
277 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n");
278 cacheContainer.insert(iter, {hashValue, {{key, returnItem}}});
282 auto& cachePairList = iter->second;
283 for(auto jter = cachePairList.begin(), jterEnd = cachePairList.end(); jter != jterEnd; ++jter)
285 if(jter->first == key)
287 // We found that input pixelData already cached.
288 returnItem = jter->second;
289 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Get cached item\n");
295 // If we fail to found same list, just append.
298 containerUpdated = true;
300 returnItem = ItemCreationFunction(key, keyFlag);
301 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n");
302 cachePairList.emplace_back(key, returnItem);
307 if constexpr(needMutex)
315 * @brief Try to collect garbages, which reference counts are 1.
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
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)
328 if constexpr(needMutex)
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)
337 lastIterator = cacheContainer.begin();
338 containerUpdated = false;
341 for(; lastIterator != cacheContainer.end() && (fullCollect || ++checkedCount <= MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL);)
343 auto& cachePairList = lastIterator->second;
345 for(auto jter = cachePairList.begin(); jter != cachePairList.end();)
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))
351 // This item is garbage! just remove it.
352 jter = cachePairList.erase(jter);
360 if(cachePairList.empty())
362 lastIterator = cacheContainer.erase(lastIterator);
370 if constexpr(needMutex)
375 return (lastIterator != cacheContainer.end());
378 public: // Called by main thread.
380 * @brief Try to get cached texture, or newly create if there is no texture that already cached.
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.
386 Dali::Texture GetOrCreateCachedTexture(const Dali::PixelData& pixelData, bool mipmapRequired)
388 auto hashValue = GenerateHash(pixelData, mipmapRequired);
389 return GetOrCreateCachedItem<false, Dali::PixelData, Dali::Texture, CreateTextureFromPixelData>(mTextureCache, hashValue, pixelData, mipmapRequired, mTextureContainerUpdated);
393 * @brief Try to get cached cube texture, or newly create if there is no cube texture that already cached.
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.
399 Dali::Texture GetOrCreateCachedCubeTexture(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
401 auto hashValue = GenerateHash(pixelDataList, mipmapRequired);
402 return GetOrCreateCachedItem<false, std::vector<std::vector<Dali::PixelData>>, Dali::Texture, CreateCubeTextureFromPixelDataList>(mCubeTextureCache, hashValue, pixelDataList, mipmapRequired, mCubeTextureContainerUpdated);
406 * @brief Request incremental gargabe collect.
408 * @param[in] fullCollect True if we will collect whole items, or incrementally.
410 void RequestGarbageCollect(bool fullCollect)
412 if(DALI_LIKELY(Dali::Adaptor::IsAvailable()))
416 mTimer = Dali::Timer::New(GC_PERIOD_MILLISECONDS);
417 mTimer.TickSignal().Connect(this, &CacheImpl::OnTick);
420 mFullCollectRequested |= fullCollect;
422 if(!mTimer.IsRunning())
424 // Restart container interating.
425 if(!mPixelDataContainerUpdated)
428 mPixelDataContainerUpdated = true;
431 mTextureContainerUpdated = true;
437 public: // Can be called by worker thread
439 * @brief Try to get cached pixel data, or newly create if there is no pixel data that already cached.
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.
444 Dali::PixelData GetOrCreateCachedPixelData(const ImageInformation& info)
446 auto hashValue = GenerateHash(info);
447 return GetOrCreateCachedItem<true, ImageInformation, Dali::PixelData, CreatePixelDataFromImageInfo>(mPixelDataCache, hashValue, info, false, mPixelDataContainerUpdated);
450 private: // Called by main thread
453 // Clear full GC flag
454 const bool fullCollect = mFullCollectRequested;
455 mFullCollectRequested = false;
457 return IncrementalGarbageCollect(fullCollect);
461 * @brief Remove unused cache item incrementally.
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.
466 bool IncrementalGarbageCollect(bool fullCollect)
468 bool continueTimer = false;
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;
475 continueTimer |= CollectGarbages<false>(mCubeTextureCache, fullCollect, mCubeTextureContainerUpdated, mLatestCollectedCubeTextureIter, checkedCount);
478 continueTimer |= CollectGarbages<false>(mTextureCache, fullCollect, mTextureContainerUpdated, mLatestCollectedTextureIter, checkedCount);
480 // GC PixelData. We should lock mutex during GC pixelData.
481 continueTimer |= CollectGarbages<true>(mPixelDataCache, fullCollect, mPixelDataContainerUpdated, mLatestCollectedPixelDataIter, checkedCount);
483 return continueTimer;
487 PixelDataCacheContainer mPixelDataCache;
488 TextureCacheContainer mTextureCache;
489 CubeTextureCacheContainer mCubeTextureCache;
493 // Be used when we garbage collection.
494 PixelDataCacheContainer::iterator mLatestCollectedPixelDataIter;
495 TextureCacheContainer::iterator mLatestCollectedTextureIter;
496 CubeTextureCacheContainer::iterator mLatestCollectedCubeTextureIter;
498 bool mPixelDataContainerUpdated;
499 bool mTextureContainerUpdated;
500 bool mCubeTextureContainerUpdated;
502 std::mutex mDataMutex;
505 bool mFullCollectRequested : 1;
508 static std::shared_ptr<CacheImpl> gCacheImpl{nullptr};
510 std::shared_ptr<CacheImpl> GetCacheImpl()
512 if(DALI_UNLIKELY(!gCacheImpl))
514 gCacheImpl = std::make_shared<CacheImpl>();
519 void DestroyCacheImpl()
526 namespace Dali::Scene3D::Internal
528 namespace ImageResourceLoader
530 // Called by main thread..
531 Dali::PixelData GetEmptyPixelDataWhiteRGB()
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;
537 Dali::Texture GetEmptyTextureWhiteRGB()
539 static Dali::PixelData emptyPixelData = GetEmptyPixelDataWhiteRGB();
540 static Dali::Texture emptyTexture = Dali::Texture();
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());
549 Dali::Texture GetCachedTexture(Dali::PixelData pixelData, bool mipmapRequired)
551 return GetCacheImpl()->GetOrCreateCachedTexture(pixelData, mipmapRequired);
554 Dali::Texture GetCachedCubeTexture(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
556 return GetCacheImpl()->GetOrCreateCachedCubeTexture(pixelDataList, mipmapRequired);
559 void RequestGarbageCollect(bool fullCollect)
561 GetCacheImpl()->RequestGarbageCollect(fullCollect);
564 void EnsureResourceLoaderCreated()
569 // Can be called by worker thread.
570 Dali::PixelData GetCachedPixelData(const std::string& url)
572 return GetCachedPixelData(url, ImageDimensions(), FittingMode::DEFAULT, SamplingMode::BOX_THEN_LINEAR, true);
575 Dali::PixelData GetCachedPixelData(const std::string& url,
576 ImageDimensions dimensions,
577 FittingMode::Type fittingMode,
578 SamplingMode::Type samplingMode,
579 bool orientationCorrection)
581 ImageInformation info(url, dimensions, fittingMode, samplingMode, orientationCorrection);
582 if(gCacheImpl == nullptr)
584 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "CacheImpl not prepared! load PixelData without cache.\n");
585 return CreatePixelDataFromImageInfo(info, false);
589 return GetCacheImpl()->GetOrCreateCachedPixelData(info);
592 } // namespace ImageResourceLoader
593 } // namespace Dali::Scene3D::Internal