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/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>
33 #include <functional> ///< for std::function
36 #include <utility> ///< for std::pair
42 constexpr uint32_t MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL = 5u;
43 constexpr uint32_t GC_PERIOD_MILLISECONDS = 1000u;
46 Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, false, "LOG_IMAGE_RESOURCE_LOADER");
49 struct ImageInformation
51 ImageInformation(const std::string url,
52 const Dali::ImageDimensions dimensions,
53 Dali::FittingMode::Type fittingMode,
54 Dali::SamplingMode::Type samplingMode,
55 bool orientationCorrection)
57 mDimensions(dimensions),
58 mFittingMode(fittingMode),
59 mSamplingMode(samplingMode),
60 mOrientationCorrection(orientationCorrection)
64 bool operator==(const ImageInformation& rhs) const
66 // Check url and orientation correction is enough.
67 return (mUrl == rhs.mUrl) && (mOrientationCorrection == rhs.mOrientationCorrection);
71 Dali::ImageDimensions mDimensions;
72 Dali::FittingMode::Type mFittingMode;
73 Dali::SamplingMode::Type mSamplingMode;
74 bool mOrientationCorrection;
78 std::size_t GenerateHash(const ImageInformation& info)
80 std::vector<std::uint8_t> hashTarget;
81 const uint16_t width = info.mDimensions.GetWidth();
82 const uint16_t height = info.mDimensions.GetHeight();
84 // If either the width or height has been specified, include the resizing options in the hash
85 if(width != 0 || height != 0)
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]);
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;
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);
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';
108 return Dali::CalculateHash(info.mUrl) ^ Dali::CalculateHash(hashTarget);
111 std::size_t GenerateHash(const Dali::PixelData& pixelData, bool mipmapRequired)
113 return reinterpret_cast<std::size_t>(static_cast<void*>(pixelData.GetObjectPtr())) ^ (static_cast<std::size_t>(mipmapRequired) << (sizeof(std::size_t) * 4));
116 std::size_t GenerateHash(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
118 std::size_t result = 0x12345678u + pixelDataList.size();
119 for(const auto& mipmapPixelDataList : pixelDataList)
121 result += (result << 5) + mipmapPixelDataList.size();
122 for(const auto& pixelData : mipmapPixelDataList)
124 result += (result << 5) + GenerateHash(pixelData, false);
128 return result ^ (static_cast<std::size_t>(mipmapRequired) << (sizeof(std::size_t) * 4));
131 // Item Creation functor list
133 Dali::PixelData CreatePixelDataFromImageInfo(const ImageInformation& info, bool /* Not used */)
135 return Dali::Toolkit::SyncImageLoader::Load(info.mUrl, info.mDimensions, info.mFittingMode, info.mSamplingMode, info.mOrientationCorrection);
138 Dali::Texture CreateTextureFromPixelData(const Dali::PixelData& pixelData, bool mipmapRequired)
140 Dali::Texture texture;
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());
147 texture.GenerateMipmaps();
153 Dali::Texture CreateCubeTextureFromPixelDataList(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
155 Dali::Texture texture;
156 if(!pixelDataList.empty() && !pixelDataList[0].empty())
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)
161 auto& side = pixelDataList[iSide];
162 for(size_t iMipLevel = 0u, iEndMipLevel = pixelDataList[0].size(); iMipLevel < iEndMipLevel; ++iMipLevel)
164 texture.Upload(side[iMipLevel], Dali::CubeMapLayer::POSITIVE_X + iSide, iMipLevel, 0u, 0u, side[iMipLevel].GetWidth(), side[iMipLevel].GetHeight());
169 texture.GenerateMipmaps();
175 class CacheImpl : public Dali::ConnectionTracker
186 mLatestCollectedPixelDataIter{mPixelDataCache.begin()},
187 mLatestCollectedTextureIter{mTextureCache.begin()},
188 mLatestCollectedCubeTextureIter{mCubeTextureCache.begin()},
189 mPixelDataContainerUpdated{false},
190 mTextureContainerUpdated{false},
191 mCubeTextureContainerUpdated{false},
194 mFullCollectRequested{false}
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
214 mPixelDataCache.clear();
215 mTextureCache.clear();
216 mCubeTextureCache.clear();
223 if(Dali::Adaptor::IsAvailable())
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>>>;
237 * @brief Try to get cached item, or create new handle if there is no item.
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.
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)
250 if constexpr(needMutex)
256 if(DALI_LIKELY(!mDestroyed))
260 auto iter = cacheContainer.lower_bound(hashValue);
261 if((iter == cacheContainer.end()) || (hashValue != iter->first))
263 containerUpdated = true;
265 returnItem = ItemCreationFunction(key, keyFlag);
266 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n");
267 cacheContainer.insert(iter, {hashValue, {{key, returnItem}}});
271 auto& cachePairList = iter->second;
272 for(auto jter = cachePairList.begin(), jterEnd = cachePairList.end(); jter != jterEnd; ++jter)
274 if(jter->first == key)
276 // We found that input pixelData already cached.
277 returnItem = jter->second;
278 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Get cached item\n");
284 // If we fail to found same list, just append.
287 containerUpdated = true;
289 returnItem = ItemCreationFunction(key, keyFlag);
290 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n");
291 cachePairList.emplace_back(key, returnItem);
296 if constexpr(needMutex)
304 * @brief Try to collect garbages, which reference counts are 1.
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
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)
317 if constexpr(needMutex)
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)
326 lastIterator = cacheContainer.begin();
327 containerUpdated = false;
330 for(; lastIterator != cacheContainer.end() && (fullCollect || ++checkedCount <= MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL);)
332 auto& cachePairList = lastIterator->second;
334 for(auto jter = cachePairList.begin(); jter != cachePairList.end();)
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))
340 // This item is garbage! just remove it.
341 jter = cachePairList.erase(jter);
349 if(cachePairList.empty())
351 lastIterator = cacheContainer.erase(lastIterator);
359 if constexpr(needMutex)
364 return (lastIterator != cacheContainer.end());
367 public: // Called by main thread.
369 * @brief Try to get cached texture, or newly create if there is no texture that already cached.
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.
375 Dali::Texture GetOrCreateCachedTexture(const Dali::PixelData& pixelData, bool mipmapRequired)
377 auto hashValue = GenerateHash(pixelData, mipmapRequired);
378 return GetOrCreateCachedItem<false, Dali::PixelData, Dali::Texture, CreateTextureFromPixelData>(mTextureCache, hashValue, pixelData, mipmapRequired, mTextureContainerUpdated);
382 * @brief Try to get cached cube texture, or newly create if there is no cube texture that already cached.
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.
388 Dali::Texture GetOrCreateCachedCubeTexture(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
390 auto hashValue = GenerateHash(pixelDataList, mipmapRequired);
391 return GetOrCreateCachedItem<false, std::vector<std::vector<Dali::PixelData>>, Dali::Texture, CreateCubeTextureFromPixelDataList>(mCubeTextureCache, hashValue, pixelDataList, mipmapRequired, mCubeTextureContainerUpdated);
395 * @brief Request incremental gargabe collect.
397 * @param[in] fullCollect True if we will collect whole items, or incrementally.
399 void RequestGarbageCollect(bool fullCollect)
401 if(DALI_LIKELY(!mDestroyed && Dali::Adaptor::IsAvailable()))
405 mTimer = Dali::Timer::New(GC_PERIOD_MILLISECONDS);
406 mTimer.TickSignal().Connect(this, &CacheImpl::OnTick);
409 mFullCollectRequested |= fullCollect;
411 if(!mTimer.IsRunning())
413 // Restart container interating.
414 if(!mPixelDataContainerUpdated)
417 mPixelDataContainerUpdated = true;
420 mTextureContainerUpdated = true;
426 public: // Can be called by worker thread
428 * @brief Try to get cached pixel data, or newly create if there is no pixel data that already cached.
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.
433 Dali::PixelData GetOrCreateCachedPixelData(const ImageInformation& info)
435 auto hashValue = GenerateHash(info);
436 return GetOrCreateCachedItem<true, ImageInformation, Dali::PixelData, CreatePixelDataFromImageInfo>(mPixelDataCache, hashValue, info, false, mPixelDataContainerUpdated);
439 private: // Called by main thread
442 // Clear full GC flag
443 const bool fullCollect = mFullCollectRequested;
444 mFullCollectRequested = false;
446 return IncrementalGarbageCollect(fullCollect);
450 * @brief Remove unused cache item incrementally.
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.
455 bool IncrementalGarbageCollect(bool fullCollect)
457 bool continueTimer = false;
459 if(DALI_LIKELY(!mDestroyed))
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;
465 continueTimer |= CollectGarbages<false>(mCubeTextureCache, fullCollect, mCubeTextureContainerUpdated, mLatestCollectedCubeTextureIter, checkedCount);
468 continueTimer |= CollectGarbages<false>(mTextureCache, fullCollect, mTextureContainerUpdated, mLatestCollectedTextureIter, checkedCount);
470 // GC PixelData. We should lock mutex during GC pixelData.
471 continueTimer |= CollectGarbages<true>(mPixelDataCache, fullCollect, mPixelDataContainerUpdated, mLatestCollectedPixelDataIter, checkedCount);
474 return continueTimer;
478 PixelDataCacheContainer mPixelDataCache;
479 TextureCacheContainer mTextureCache;
480 CubeTextureCacheContainer mCubeTextureCache;
484 // Be used when we garbage collection.
485 PixelDataCacheContainer::iterator mLatestCollectedPixelDataIter;
486 TextureCacheContainer::iterator mLatestCollectedTextureIter;
487 CubeTextureCacheContainer::iterator mLatestCollectedCubeTextureIter;
489 bool mPixelDataContainerUpdated;
490 bool mTextureContainerUpdated;
491 bool mCubeTextureContainerUpdated;
493 std::mutex mDataMutex;
496 bool mFullCollectRequested : 1;
499 CacheImpl& GetCacheImpl()
501 static CacheImpl gCacheImpl;
507 namespace Dali::Scene3D::Internal
509 namespace ImageResourceLoader
511 // Called by main thread..
512 Dali::PixelData GetEmptyPixelDataWhiteRGB()
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;
518 Dali::Texture GetEmptyTextureWhiteRGB()
520 static Dali::PixelData emptyPixelData = GetEmptyPixelDataWhiteRGB();
521 static Dali::Texture emptyTexture = Dali::Texture();
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());
530 Dali::Texture GetCachedTexture(Dali::PixelData pixelData, bool mipmapRequired)
532 return GetCacheImpl().GetOrCreateCachedTexture(pixelData, mipmapRequired);
535 Dali::Texture GetCachedCubeTexture(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
537 return GetCacheImpl().GetOrCreateCachedCubeTexture(pixelDataList, mipmapRequired);
540 void RequestGarbageCollect(bool fullCollect)
542 GetCacheImpl().RequestGarbageCollect(fullCollect);
545 // Can be called by worker thread.
546 Dali::PixelData GetCachedPixelData(const std::string& url)
548 return GetCachedPixelData(url, ImageDimensions(), FittingMode::DEFAULT, SamplingMode::BOX_THEN_LINEAR, true);
551 Dali::PixelData GetCachedPixelData(const std::string& url,
552 ImageDimensions dimensions,
553 FittingMode::Type fittingMode,
554 SamplingMode::Type samplingMode,
555 bool orientationCorrection)
557 ImageInformation info(url, dimensions, fittingMode, samplingMode, orientationCorrection);
558 return GetCacheImpl().GetOrCreateCachedPixelData(info);
560 } // namespace ImageResourceLoader
561 } // namespace Dali::Scene3D::Internal