2 * Copyright (c) 2024 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/devel-api/adaptor-framework/image-loading.h>
23 #include <dali/devel-api/adaptor-framework/lifecycle-controller.h>
24 #include <dali/devel-api/adaptor-framework/pixel-buffer.h>
25 #include <dali/devel-api/common/hash.h>
26 #include <dali/devel-api/common/map-wrapper.h>
27 #include <dali/devel-api/threading/mutex.h>
28 #include <dali/integration-api/adaptor-framework/adaptor.h>
29 #include <dali/integration-api/debug.h>
30 #include <dali/integration-api/pixel-data-integ.h>
31 #include <dali/public-api/adaptor-framework/timer.h>
32 #include <dali/public-api/common/vector-wrapper.h>
33 #include <dali/public-api/object/base-object.h>
34 #include <dali/public-api/signals/connection-tracker.h>
36 #include <functional> ///< for std::function
37 #include <memory> ///< for std::shared_ptr
40 #include <utility> ///< for std::pair
46 constexpr uint32_t MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL = 5u;
47 constexpr uint32_t GC_PERIOD_MILLISECONDS = 1000u;
50 Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, false, "LOG_IMAGE_RESOURCE_LOADER");
53 bool SupportPixelDataCache(Dali::PixelData pixelData)
55 // Check given pixelData support to release data after upload.
56 // This is cause we need to reduce CPU memory usage.
57 if(Dali::Integration::IsPixelDataReleaseAfterUpload(pixelData))
62 // Check given pixelData is default pixelData.
63 if(pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataWhiteRGB() ||
64 pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataWhiteRGBA() ||
65 pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataZAxisRGB() ||
66 pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataZAxisAndAlphaRGBA())
73 bool SupportPixelDataListCache(const std::vector<std::vector<Dali::PixelData>>& pixelDataList)
75 for(const auto& pixelDataListLevel0 : pixelDataList)
77 for(const auto& pixelData : pixelDataListLevel0)
79 if(!SupportPixelDataCache(pixelData))
88 struct ImageInformation
90 ImageInformation(const std::string url,
91 const Dali::ImageDimensions dimensions,
92 Dali::FittingMode::Type fittingMode,
93 Dali::SamplingMode::Type samplingMode,
94 bool orientationCorrection)
96 mDimensions(dimensions),
97 mFittingMode(fittingMode),
98 mSamplingMode(samplingMode),
99 mOrientationCorrection(orientationCorrection)
103 bool operator==(const ImageInformation& rhs) const
105 // Check url and orientation correction is enough.
106 return (mUrl == rhs.mUrl) && (mOrientationCorrection == rhs.mOrientationCorrection);
110 Dali::ImageDimensions mDimensions;
111 Dali::FittingMode::Type mFittingMode;
112 Dali::SamplingMode::Type mSamplingMode;
113 bool mOrientationCorrection;
117 std::size_t GenerateHash(const ImageInformation& info)
119 std::vector<std::uint8_t> hashTarget;
120 const uint16_t width = info.mDimensions.GetWidth();
121 const uint16_t height = info.mDimensions.GetHeight();
123 // If either the width or height has been specified, include the resizing options in the hash
124 if(width != 0 || height != 0)
126 // We are appending 5 bytes to the URL to form the hash input.
127 hashTarget.resize(5u);
128 std::uint8_t* hashTargetPtr = &(hashTarget[0u]);
130 // Pack the width and height (4 bytes total).
131 *hashTargetPtr++ = info.mDimensions.GetWidth() & 0xff;
132 *hashTargetPtr++ = (info.mDimensions.GetWidth() >> 8u) & 0xff;
133 *hashTargetPtr++ = info.mDimensions.GetHeight() & 0xff;
134 *hashTargetPtr++ = (info.mDimensions.GetHeight() >> 8u) & 0xff;
136 // Bit-pack the FittingMode, SamplingMode and orientation correction.
137 // FittingMode=2bits, SamplingMode=3bits, orientationCorrection=1bit
138 *hashTargetPtr = (info.mFittingMode << 4u) | (info.mSamplingMode << 1) | (info.mOrientationCorrection ? 1 : 0);
142 // We are not including sizing information, but we still need an extra byte for orientationCorrection.
143 hashTarget.resize(1u);
144 hashTarget[0u] = info.mOrientationCorrection ? 't' : 'f';
147 return Dali::CalculateHash(info.mUrl) ^ Dali::CalculateHash(hashTarget);
150 std::size_t GenerateHash(const Dali::PixelData& pixelData, bool mipmapRequired)
152 return reinterpret_cast<std::size_t>(static_cast<void*>(pixelData.GetObjectPtr())) ^ (static_cast<std::size_t>(mipmapRequired) << (sizeof(std::size_t) * 4));
155 std::size_t GenerateHash(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
157 std::size_t result = 0x12345678u + pixelDataList.size();
158 for(const auto& mipmapPixelDataList : pixelDataList)
160 result += (result << 5) + mipmapPixelDataList.size();
161 for(const auto& pixelData : mipmapPixelDataList)
163 result += (result << 5) + GenerateHash(pixelData, false);
167 return result ^ (static_cast<std::size_t>(mipmapRequired) << (sizeof(std::size_t) * 4));
170 // Item Creation functor list
172 Dali::PixelData CreatePixelDataFromImageInfo(const ImageInformation& info, bool releasePixelData)
174 Dali::PixelData pixelData;
176 // Load the image synchronously (block the thread here).
177 Dali::Devel::PixelBuffer pixelBuffer = Dali::LoadImageFromFile(info.mUrl, info.mDimensions, info.mFittingMode, info.mSamplingMode, info.mOrientationCorrection);
180 pixelData = Dali::Devel::PixelBuffer::Convert(pixelBuffer, releasePixelData);
185 Dali::Texture CreateTextureFromPixelData(const Dali::PixelData& pixelData, bool mipmapRequired)
187 Dali::Texture texture;
190 texture = Dali::Texture::New(Dali::TextureType::TEXTURE_2D, pixelData.GetPixelFormat(), pixelData.GetWidth(), pixelData.GetHeight());
191 texture.Upload(pixelData, 0, 0, 0, 0, pixelData.GetWidth(), pixelData.GetHeight());
194 texture.GenerateMipmaps();
200 Dali::Texture CreateCubeTextureFromPixelDataList(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
202 Dali::Texture texture;
203 if(!pixelDataList.empty() && !pixelDataList[0].empty())
205 texture = Dali::Texture::New(Dali::TextureType::TEXTURE_CUBE, pixelDataList[0][0].GetPixelFormat(), pixelDataList[0][0].GetWidth(), pixelDataList[0][0].GetHeight());
206 for(size_t iSide = 0u, iEndSize = pixelDataList.size(); iSide < iEndSize; ++iSide)
208 auto& side = pixelDataList[iSide];
209 for(size_t iMipLevel = 0u, iEndMipLevel = pixelDataList[0].size(); iMipLevel < iEndMipLevel; ++iMipLevel)
211 texture.Upload(side[iMipLevel], Dali::CubeMapLayer::POSITIVE_X + iSide, iMipLevel, 0u, 0u, side[iMipLevel].GetWidth(), side[iMipLevel].GetHeight());
216 texture.GenerateMipmaps();
223 // Forward declare, for signal connection.
224 void DestroyCacheImpl();
226 class CacheImpl : public Dali::ConnectionTracker
237 mLatestCollectedPixelDataIter{mPixelDataCache.begin()},
238 mLatestCollectedTextureIter{mTextureCache.begin()},
239 mLatestCollectedCubeTextureIter{mCubeTextureCache.begin()},
240 mPixelDataContainerUpdated{false},
241 mTextureContainerUpdated{false},
242 mCubeTextureContainerUpdated{false},
245 mFullCollectRequested{false}
247 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create CacheImpl\n");
249 // We should create CacheImpl at main thread, To ensure delete this cache impl
250 Dali::LifecycleController::Get().TerminateSignal().Connect(DestroyCacheImpl);
258 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Destroy CacheImpl\n");
263 mPixelDataContainerUpdated = false;
264 mTextureContainerUpdated = false;
265 mCubeTextureContainerUpdated = false;
266 mLatestCollectedPixelDataIter = decltype(mLatestCollectedPixelDataIter)(); // Invalidate iterator
267 mLatestCollectedTextureIter = decltype(mLatestCollectedTextureIter)(); // Invalidate iterator
268 mLatestCollectedCubeTextureIter = decltype(mLatestCollectedCubeTextureIter){}; // Invalidate iterator
270 mPixelDataCache.clear();
271 mTextureCache.clear();
272 mCubeTextureCache.clear();
279 if(Dali::Adaptor::IsAvailable())
286 private: // Unified API for this class
287 // Let compare with hash first. And then, check detail keys after.
288 using PixelDataCacheContainer = std::map<std::size_t, std::vector<std::pair<ImageInformation, Dali::PixelData>>>;
289 using TextureCacheContainer = std::map<std::size_t, std::vector<std::pair<Dali::PixelData, Dali::Texture>>>;
290 using CubeTextureCacheContainer = std::map<std::size_t, std::vector<std::pair<std::vector<std::vector<Dali::PixelData>>, Dali::Texture>>>;
293 * @brief Try to get cached item, or create new handle if there is no item.
295 * @tparam needMutex Whether we need to lock the mutex during this operation, or not.
296 * @param[in] cacheContainer The container of key / item pair.
297 * @param[in] hashValue The hash value of key.
298 * @param[in] key The key of cache item.
299 * @param[in] keyFlag The additional flags when we need to create new item.
300 * @param[out] containerUpdate True whether container changed or not.
301 * @return Item that has been cached. Or newly created.
303 template<bool needMutex, typename KeyType, typename ItemType, ItemType (*ItemCreationFunction)(const KeyType&, bool), typename ContainerType>
304 ItemType GetOrCreateCachedItem(ContainerType& cacheContainer, std::size_t hashValue, const KeyType& key, bool keyFlag, bool& containerUpdated)
306 if constexpr(needMutex)
312 if(DALI_LIKELY(!mDestroyed))
316 auto iter = cacheContainer.lower_bound(hashValue);
317 if((iter == cacheContainer.end()) || (hashValue != iter->first))
319 containerUpdated = true;
321 returnItem = ItemCreationFunction(key, keyFlag);
322 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n");
323 cacheContainer.insert(iter, {hashValue, {{key, returnItem}}});
327 auto& cachePairList = iter->second;
328 for(auto jter = cachePairList.begin(), jterEnd = cachePairList.end(); jter != jterEnd; ++jter)
330 if(jter->first == key)
332 // We found that input pixelData already cached.
333 returnItem = jter->second;
334 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Get cached item\n");
340 // If we fail to found same list, just append.
343 containerUpdated = true;
345 returnItem = ItemCreationFunction(key, keyFlag);
346 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n");
347 cachePairList.emplace_back(key, returnItem);
352 if constexpr(needMutex)
360 * @brief Try to collect garbages, which reference counts are 1.
362 * @tparam needMutex Whether we need to lock the mutex during this operation, or not.
363 * @param[in] cacheContainer The container of key / item pair.
364 * @param[in] fullCollect True if we need to collect whole container.
365 * @param[in, out] containerUpdated True if container information changed. lastIterator will be begin of container when we start collect garbages.
366 * @param[in, out] lastIterator The last iterator of container.
367 * @oaram[in, out] checkedCount The number of iteration checked total.
368 * @return True if we iterate whole container, so we don't need to check anymore. False otherwise
370 template<bool needMutex, typename ContainerType, typename Iterator = typename ContainerType::iterator>
371 bool CollectGarbages(ContainerType& cacheContainer, bool fullCollect, bool& containerUpdated, Iterator& lastIterator, uint32_t& checkedCount)
373 if constexpr(needMutex)
378 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Collect Garbages : %zu\n", cacheContainer.size());
379 // Container changed. We should re-collect garbage from begin again.
380 if(fullCollect || containerUpdated)
382 lastIterator = cacheContainer.begin();
383 containerUpdated = false;
386 for(; lastIterator != cacheContainer.end() && (fullCollect || ++checkedCount <= MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL);)
388 auto& cachePairList = lastIterator->second;
390 for(auto jter = cachePairList.begin(); jter != cachePairList.end();)
392 auto& item = jter->second;
393 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "item : %p, ref count : %u\n", item.GetObjectPtr(), (item ? item.GetBaseObject().ReferenceCount() : 0u));
394 if(!item || (item.GetBaseObject().ReferenceCount() == 1u))
396 // This item is garbage! just remove it.
397 jter = cachePairList.erase(jter);
405 if(cachePairList.empty())
407 lastIterator = cacheContainer.erase(lastIterator);
415 if constexpr(needMutex)
420 return (lastIterator != cacheContainer.end());
423 public: // Called by main thread.
425 * @brief Try to get cached texture, or newly create if there is no texture that already cached.
427 * @param[in] pixelData The pixelData of image.
428 * @param[in] mipmapRequired True if result texture need to generate mipmap.
429 * @return Texture that has been cached. Or empty handle if we fail to found cached item.
431 Dali::Texture GetOrCreateCachedTexture(const Dali::PixelData& pixelData, bool mipmapRequired)
433 auto hashValue = GenerateHash(pixelData, mipmapRequired);
434 return GetOrCreateCachedItem<false, Dali::PixelData, Dali::Texture, CreateTextureFromPixelData>(mTextureCache, hashValue, pixelData, mipmapRequired, mTextureContainerUpdated);
438 * @brief Try to get cached cube texture, or newly create if there is no cube texture that already cached.
440 * @param[in] pixelDataList The pixelData list of image.
441 * @param[in] mipmapRequired True if result texture need to generate mipmap.
442 * @return Texture that has been cached. Or empty handle if we fail to found cached item.
444 Dali::Texture GetOrCreateCachedCubeTexture(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
446 auto hashValue = GenerateHash(pixelDataList, mipmapRequired);
447 return GetOrCreateCachedItem<false, std::vector<std::vector<Dali::PixelData>>, Dali::Texture, CreateCubeTextureFromPixelDataList>(mCubeTextureCache, hashValue, pixelDataList, mipmapRequired, mCubeTextureContainerUpdated);
451 * @brief Request incremental gargabe collect.
453 * @param[in] fullCollect True if we will collect whole items, or incrementally.
455 void RequestGarbageCollect(bool fullCollect)
457 if(DALI_LIKELY(Dali::Adaptor::IsAvailable()))
461 mTimer = Dali::Timer::New(GC_PERIOD_MILLISECONDS);
462 mTimer.TickSignal().Connect(this, &CacheImpl::OnTick);
465 mFullCollectRequested |= fullCollect;
467 if(!mTimer.IsRunning())
469 // Restart container interating.
470 if(!mPixelDataContainerUpdated)
473 mPixelDataContainerUpdated = true;
476 mTextureContainerUpdated = true;
482 public: // Can be called by worker thread
484 * @brief Try to get cached pixel data, or newly create if there is no pixel data that already cached.
486 * @param[in] info The informations of image to load.
487 * @param[in] releasePixelData Whether we need to release pixel data after upload, or not.
488 * @return Texture that has been cached. Or empty handle if we fail to found cached item.
490 Dali::PixelData GetOrCreateCachedPixelData(const ImageInformation& info, bool releasePixelData)
492 auto hashValue = GenerateHash(info);
493 return GetOrCreateCachedItem<true, ImageInformation, Dali::PixelData, CreatePixelDataFromImageInfo>(mPixelDataCache, hashValue, info, releasePixelData, mPixelDataContainerUpdated);
496 private: // Called by main thread
499 // Clear full GC flag
500 const bool fullCollect = mFullCollectRequested;
501 mFullCollectRequested = false;
503 return IncrementalGarbageCollect(fullCollect);
507 * @brief Remove unused cache item incrementally.
509 * @param[in] fullCollect True if we will collect whole items, or incrementally.
510 * @return True if there still exist what we need to check clean. False when whole cached items are using now.
512 bool IncrementalGarbageCollect(bool fullCollect)
514 bool continueTimer = false;
516 // Try to collect Texture GC first, due to the reference count of pixelData who become key of textures.
517 // After all texture GC finished, then check PixelData cache.
518 uint32_t checkedCount = 0u;
521 continueTimer |= CollectGarbages<false>(mCubeTextureCache, fullCollect, mCubeTextureContainerUpdated, mLatestCollectedCubeTextureIter, checkedCount);
524 continueTimer |= CollectGarbages<false>(mTextureCache, fullCollect, mTextureContainerUpdated, mLatestCollectedTextureIter, checkedCount);
526 // GC PixelData. We should lock mutex during GC pixelData.
527 continueTimer |= CollectGarbages<true>(mPixelDataCache, fullCollect, mPixelDataContainerUpdated, mLatestCollectedPixelDataIter, checkedCount);
529 return continueTimer;
533 PixelDataCacheContainer mPixelDataCache;
534 TextureCacheContainer mTextureCache;
535 CubeTextureCacheContainer mCubeTextureCache;
539 // Be used when we garbage collection.
540 PixelDataCacheContainer::iterator mLatestCollectedPixelDataIter;
541 TextureCacheContainer::iterator mLatestCollectedTextureIter;
542 CubeTextureCacheContainer::iterator mLatestCollectedCubeTextureIter;
544 bool mPixelDataContainerUpdated;
545 bool mTextureContainerUpdated;
546 bool mCubeTextureContainerUpdated;
548 std::mutex mDataMutex;
551 bool mFullCollectRequested : 1;
554 static std::shared_ptr<CacheImpl> gCacheImpl{nullptr};
555 static Dali::Texture gEmptyTextureWhiteRGB{};
557 std::shared_ptr<CacheImpl> GetCacheImpl()
559 if(DALI_UNLIKELY(!gCacheImpl))
561 gCacheImpl = std::make_shared<CacheImpl>();
566 void DestroyCacheImpl()
570 // Remove texture object when application stopped.
571 gEmptyTextureWhiteRGB.Reset();
576 namespace Dali::Scene3D::Internal
578 namespace ImageResourceLoader
580 // Called by main thread..
581 Dali::Texture GetEmptyTextureWhiteRGB()
583 if(!gEmptyTextureWhiteRGB)
585 Dali::PixelData emptyPixelData = GetEmptyPixelDataWhiteRGB();
586 gEmptyTextureWhiteRGB = Texture::New(TextureType::TEXTURE_2D, emptyPixelData.GetPixelFormat(), emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
587 gEmptyTextureWhiteRGB.Upload(emptyPixelData, 0, 0, 0, 0, emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
589 return gEmptyTextureWhiteRGB;
592 Dali::Texture GetCachedTexture(Dali::PixelData pixelData, bool mipmapRequired)
594 if(SupportPixelDataCache(pixelData))
596 return GetCacheImpl()->GetOrCreateCachedTexture(pixelData, mipmapRequired);
600 return CreateTextureFromPixelData(pixelData, mipmapRequired);
604 Dali::Texture GetCachedCubeTexture(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
606 if(SupportPixelDataListCache(pixelDataList))
608 return GetCacheImpl()->GetOrCreateCachedCubeTexture(pixelDataList, mipmapRequired);
612 return CreateCubeTextureFromPixelDataList(pixelDataList, mipmapRequired);
616 void RequestGarbageCollect(bool fullCollect)
618 GetCacheImpl()->RequestGarbageCollect(fullCollect);
621 void EnsureResourceLoaderCreated()
626 // Can be called by worker thread.
627 Dali::PixelData GetEmptyPixelDataWhiteRGB()
629 static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[3]{0xff, 0xff, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY);
630 return emptyPixelData;
633 Dali::PixelData GetEmptyPixelDataWhiteRGBA()
635 static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[4]{0xff, 0xff, 0xff, 0xff}, 4, 1, 1, Pixel::RGBA8888, PixelData::DELETE_ARRAY);
636 return emptyPixelData;
639 Dali::PixelData GetEmptyPixelDataZAxisRGB()
641 static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[3]{0x7f, 0x7f, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY);
642 return emptyPixelData;
645 Dali::PixelData GetEmptyPixelDataZAxisAndAlphaRGBA()
647 static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[4]{0x7f, 0x7f, 0xff, 0xff}, 4, 1, 1, Pixel::RGBA8888, PixelData::DELETE_ARRAY);
648 return emptyPixelData;
651 Dali::PixelData GetCachedPixelData(const std::string& url)
653 return GetCachedPixelData(url, ImageDimensions(), FittingMode::DEFAULT, SamplingMode::BOX_THEN_LINEAR, true);
656 Dali::PixelData GetCachedPixelData(const std::string& url,
657 ImageDimensions dimensions,
658 FittingMode::Type fittingMode,
659 SamplingMode::Type samplingMode,
660 bool orientationCorrection)
662 ImageInformation info(url, dimensions, fittingMode, samplingMode, orientationCorrection);
663 if(gCacheImpl == nullptr)
665 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "CacheImpl not prepared! load PixelData without cache.\n");
666 return CreatePixelDataFromImageInfo(info, false);
670 return GetCacheImpl()->GetOrCreateCachedPixelData(info, true);
673 } // namespace ImageResourceLoader
674 } // namespace Dali::Scene3D::Internal