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
41 #include <utility> ///< for std::pair
47 constexpr uint32_t MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL = 5u;
48 constexpr uint32_t GC_PERIOD_MILLISECONDS = 1000u;
51 Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, false, "LOG_IMAGE_RESOURCE_LOADER");
54 bool IsDefaultPixelData(const Dali::PixelData& pixelData)
56 if(pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataWhiteRGB() ||
57 pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataWhiteRGBA() ||
58 pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataZAxisRGB() ||
59 pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataZAxisAndAlphaRGBA())
66 bool SupportPixelDataCache(const Dali::PixelData& pixelData)
68 // Check given pixelData support to release data after upload.
69 // This is cause we need to reduce CPU memory usage.
70 if(Dali::Integration::IsPixelDataReleaseAfterUpload(pixelData))
75 // Check given pixelData is default pixelData.
76 if(IsDefaultPixelData(pixelData))
83 struct ImageInformation
85 ImageInformation(const std::string url,
86 const Dali::ImageDimensions dimensions,
87 Dali::FittingMode::Type fittingMode,
88 Dali::SamplingMode::Type samplingMode,
89 bool orientationCorrection)
91 mDimensions(dimensions),
92 mFittingMode(fittingMode),
93 mSamplingMode(samplingMode),
94 mOrientationCorrection(orientationCorrection)
98 bool operator==(const ImageInformation& rhs) const
100 // Check url and orientation correction is enough.
101 return (mUrl == rhs.mUrl) && (mOrientationCorrection == rhs.mOrientationCorrection);
105 Dali::ImageDimensions mDimensions;
106 Dali::FittingMode::Type mFittingMode;
107 Dali::SamplingMode::Type mSamplingMode;
108 bool mOrientationCorrection;
112 std::size_t GenerateHash(const ImageInformation& info)
114 std::vector<std::uint8_t> hashTarget;
115 const uint16_t width = info.mDimensions.GetWidth();
116 const uint16_t height = info.mDimensions.GetHeight();
118 // If either the width or height has been specified, include the resizing options in the hash
119 if(width != 0 || height != 0)
121 // We are appending 5 bytes to the URL to form the hash input.
122 hashTarget.resize(5u);
123 std::uint8_t* hashTargetPtr = &(hashTarget[0u]);
125 // Pack the width and height (4 bytes total).
126 *hashTargetPtr++ = info.mDimensions.GetWidth() & 0xff;
127 *hashTargetPtr++ = (info.mDimensions.GetWidth() >> 8u) & 0xff;
128 *hashTargetPtr++ = info.mDimensions.GetHeight() & 0xff;
129 *hashTargetPtr++ = (info.mDimensions.GetHeight() >> 8u) & 0xff;
131 // Bit-pack the FittingMode, SamplingMode and orientation correction.
132 // FittingMode=2bits, SamplingMode=3bits, orientationCorrection=1bit
133 *hashTargetPtr = (info.mFittingMode << 4u) | (info.mSamplingMode << 1) | (info.mOrientationCorrection ? 1 : 0);
137 // We are not including sizing information, but we still need an extra byte for orientationCorrection.
138 hashTarget.resize(1u);
139 hashTarget[0u] = info.mOrientationCorrection ? 't' : 'f';
142 return Dali::CalculateHash(info.mUrl) ^ Dali::CalculateHash(hashTarget);
145 std::size_t GenerateHash(const Dali::PixelData& pixelData, bool mipmapRequired)
147 return reinterpret_cast<std::size_t>(static_cast<void*>(pixelData.GetObjectPtr())) ^ (static_cast<std::size_t>(mipmapRequired) << (sizeof(std::size_t) * 4));
150 // Item Creation functor list
152 Dali::PixelData CreatePixelDataFromImageInfo(const ImageInformation& info, bool releasePixelData)
154 Dali::PixelData pixelData;
156 // Load the image synchronously (block the thread here).
157 Dali::Devel::PixelBuffer pixelBuffer = Dali::LoadImageFromFile(info.mUrl, info.mDimensions, info.mFittingMode, info.mSamplingMode, info.mOrientationCorrection);
160 pixelData = Dali::Devel::PixelBuffer::Convert(pixelBuffer, releasePixelData);
165 Dali::Texture CreateTextureFromPixelData(const Dali::PixelData& pixelData, bool mipmapRequired)
167 Dali::Texture texture;
170 texture = Dali::Texture::New(Dali::TextureType::TEXTURE_2D, pixelData.GetPixelFormat(), pixelData.GetWidth(), pixelData.GetHeight());
171 texture.Upload(pixelData, 0, 0, 0, 0, pixelData.GetWidth(), pixelData.GetHeight());
174 texture.GenerateMipmaps();
180 // Check function whether we can collect given data as garbage, or not.
181 bool PixelDataCacheCollectable(const ImageInformation& info, const Dali::PixelData& pixelData)
183 return pixelData.GetBaseObject().ReferenceCount() <= 1;
186 bool TextureCacheCollectable(const Dali::PixelData& pixelData, const Dali::Texture& texture)
188 return !IsDefaultPixelData(pixelData) && ///< If key is not default pixelData
189 pixelData.GetBaseObject().ReferenceCount() <= 2 && ///< And it have reference count as 2 (1 is for the key of this container, and other is PixelData cache.)
190 texture.GetBaseObject().ReferenceCount() <= 1; ///< And nobody use this texture, except this contianer.
193 // Forward declare, for signal connection.
194 void DestroyCacheImpl();
196 class CacheImpl : public Dali::ConnectionTracker
206 mLatestCollectedPixelDataIter{mPixelDataCache.begin()},
207 mLatestCollectedTextureIter{mTextureCache.begin()},
209 mPixelDataContainerUpdated{false},
210 mTextureContainerUpdated{false},
212 mFullCollectRequested{false}
214 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create CacheImpl\n");
216 // We should create CacheImpl at main thread, To ensure delete this cache impl
217 Dali::LifecycleController::Get().TerminateSignal().Connect(DestroyCacheImpl);
225 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Destroy CacheImpl\n");
230 mPixelDataContainerUpdated = false;
231 mTextureContainerUpdated = false;
232 mLatestCollectedPixelDataIter = decltype(mLatestCollectedPixelDataIter)(); // Invalidate iterator
233 mLatestCollectedTextureIter = decltype(mLatestCollectedTextureIter)(); // Invalidate iterator
235 mPixelDataCache.clear();
236 mTextureCache.clear();
243 if(Dali::Adaptor::IsAvailable())
250 private: // Unified API for this class
251 // Let compare with hash first. And then, check detail keys after.
252 using PixelDataCacheContainer = std::map<std::size_t, std::vector<std::pair<ImageInformation, Dali::PixelData>>>;
253 using TextureCacheContainer = std::map<std::size_t, std::vector<std::pair<Dali::PixelData, Dali::Texture>>>;
256 * @brief Try to get cached item, or create new handle if there is no item.
258 * @tparam needMutex Whether we need to lock the mutex during this operation, or not.
259 * @param[in] cacheContainer The container of key / item pair.
260 * @param[in] hashValue The hash value of key.
261 * @param[in] key The key of cache item.
262 * @param[in] keyFlag The additional flags when we need to create new item.
263 * @param[out] containerUpdate True whether container changed or not.
264 * @return Item that has been cached. Or newly created.
266 template<bool needMutex, typename KeyType, typename ItemType, ItemType (*ItemCreationFunction)(const KeyType&, bool), typename ContainerType>
267 ItemType GetOrCreateCachedItem(ContainerType& cacheContainer, std::size_t hashValue, const KeyType& key, bool keyFlag, bool& containerUpdated)
269 if constexpr(needMutex)
275 if(DALI_LIKELY(!mDestroyed))
279 auto iter = cacheContainer.lower_bound(hashValue);
280 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "HashValue : %zu\n", hashValue);
281 if((iter == cacheContainer.end()) || (hashValue != iter->first))
283 containerUpdated = true;
285 returnItem = ItemCreationFunction(key, keyFlag);
286 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n");
287 cacheContainer.insert(iter, {hashValue, {{key, returnItem}}});
291 auto& cachePairList = iter->second;
292 for(auto jter = cachePairList.begin(), jterEnd = cachePairList.end(); jter != jterEnd; ++jter)
294 if(jter->first == key)
296 // We found that input pixelData already cached.
297 returnItem = jter->second;
298 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Get cached item\n");
304 // If we fail to found same list, just append.
307 containerUpdated = true;
309 returnItem = ItemCreationFunction(key, keyFlag);
310 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n");
311 cachePairList.emplace_back(key, returnItem);
316 if constexpr(needMutex)
325 * @brief Try to collect garbages, which reference counts are 1.
327 * @tparam needMutex Whether we need to lock the mutex during this operation, or not.
328 * @param[in] cacheContainer The container of key / item pair.
329 * @param[in] fullCollect True if we need to collect whole container.
330 * @param[in, out] containerUpdated True if container information changed. lastIterator will be begin of container when we start collect garbages.
331 * @param[in, out] lastIterator The last iterator of container.
332 * @oaram[in, out] checkedCount The number of iteration checked total.
333 * @return True if we iterate whole container, so we don't need to check anymore. False otherwise
335 template<typename KeyType, typename ValueType, bool (*Collectable)(const KeyType&, const ValueType&), typename ContainerType, typename Iterator = typename ContainerType::iterator>
336 bool CollectGarbages(ContainerType& cacheContainer, bool fullCollect, bool& containerUpdated, Iterator& lastIterator, uint32_t& checkedCount, uint32_t& collectedCount)
338 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Collect Garbages : %zu (checkedCount : %d, fullCollect? %d)\n", cacheContainer.size(), checkedCount, fullCollect);
339 // Container changed. We should re-collect garbage from begin again.
340 if(fullCollect || containerUpdated)
342 lastIterator = cacheContainer.begin();
343 containerUpdated = false;
346 for(; lastIterator != cacheContainer.end() && (fullCollect || ++checkedCount <= MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL);)
348 auto& cachePairList = lastIterator->second;
350 for(auto jter = cachePairList.begin(); jter != cachePairList.end();)
352 auto& item = jter->second;
353 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "item : %p, ref count : %u\n", item.GetObjectPtr(), (item ? item.GetBaseObject().ReferenceCount() : 0u));
354 if(!item || Collectable(jter->first, item))
356 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "GC!!!\n");
357 // This item is garbage! just remove it.
359 jter = cachePairList.erase(jter);
367 if(cachePairList.empty())
369 lastIterator = cacheContainer.erase(lastIterator);
377 return (lastIterator != cacheContainer.end());
380 public: // Called by main thread.
382 * @brief Try to get cached texture, or newly create if there is no texture that already cached.
384 * @param[in] pixelData The pixelData 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 GetOrCreateCachedTexture(const Dali::PixelData& pixelData, bool mipmapRequired)
390 auto hashValue = GenerateHash(pixelData, mipmapRequired);
391 return GetOrCreateCachedItem<false, Dali::PixelData, Dali::Texture, CreateTextureFromPixelData>(mTextureCache, hashValue, pixelData, mipmapRequired, mTextureContainerUpdated);
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(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.
415 mPixelDataContainerUpdated = true;
417 mTextureContainerUpdated = true;
423 public: // Can be called by worker thread
425 * @brief Try to get cached pixel data, or newly create if there is no pixel data that already cached.
427 * @param[in] info The informations of image to load.
428 * @param[in] releasePixelData Whether we need to release pixel data after upload, or not.
429 * @return Texture that has been cached. Or empty handle if we fail to found cached item.
431 Dali::PixelData GetOrCreateCachedPixelData(const ImageInformation& info, bool releasePixelData)
433 auto hashValue = GenerateHash(info);
434 return GetOrCreateCachedItem<true, ImageInformation, Dali::PixelData, CreatePixelDataFromImageInfo>(mPixelDataCache, hashValue, info, releasePixelData, mPixelDataContainerUpdated);
437 private: // Called by main thread
440 // Clear full GC flag
441 const bool fullCollect = mFullCollectRequested;
442 mFullCollectRequested = false;
444 return IncrementalGarbageCollect(fullCollect);
448 * @brief Remove unused cache item incrementally.
450 * @param[in] fullCollect True if we will collect whole items, or incrementally.
451 * @return True if there still exist what we need to check clean. False when whole cached items are using now.
453 bool IncrementalGarbageCollect(bool fullCollect)
455 bool continueTimer = false;
456 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "GC start\n");
458 // Try to collect Texture GC first, due to the reference count of pixelData who become key of textures.
459 // After all texture GC finished, then check PixelData cache.
460 uint32_t checkedCount = 0u;
461 uint32_t collectedCount = 0u;
463 // We should lock mutex during GC pixelData.
467 continueTimer |= CollectGarbages<Dali::PixelData, Dali::Texture, TextureCacheCollectable>(mTextureCache, fullCollect, mTextureContainerUpdated, mLatestCollectedTextureIter, checkedCount, collectedCount);
469 // GC PixelData last. If there are some collected Texture before, we should full-collect.
470 // (Since most of PixelData use 'ReleaseAfterUpload' flags).
471 continueTimer |= CollectGarbages<ImageInformation, Dali::PixelData, PixelDataCacheCollectable>(mPixelDataCache, fullCollect || (collectedCount > 0u), mPixelDataContainerUpdated, mLatestCollectedPixelDataIter, checkedCount, collectedCount);
475 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "GC finished. checkedCount : %u, continueTimer : %d\n", checkedCount, continueTimer);
477 return continueTimer;
481 PixelDataCacheContainer mPixelDataCache;
482 TextureCacheContainer mTextureCache;
486 // Be used when we garbage collection.
487 PixelDataCacheContainer::iterator mLatestCollectedPixelDataIter;
488 TextureCacheContainer::iterator mLatestCollectedTextureIter;
490 std::mutex mDataMutex;
492 bool mPixelDataContainerUpdated;
493 bool mTextureContainerUpdated;
496 bool mFullCollectRequested : 1;
499 static std::shared_ptr<CacheImpl> gCacheImpl{nullptr};
500 static Dali::Texture gEmptyTextureWhiteRGB{};
501 static Dali::Texture gEmptyCubeTextureWhiteRGB{};
503 std::shared_ptr<CacheImpl> GetCacheImpl()
505 if(DALI_UNLIKELY(!gCacheImpl))
507 gCacheImpl = std::make_shared<CacheImpl>();
512 void DestroyCacheImpl()
516 // Remove texture object when application stopped.
517 gEmptyTextureWhiteRGB.Reset();
518 gEmptyCubeTextureWhiteRGB.Reset();
523 namespace Dali::Scene3D::Internal
525 namespace ImageResourceLoader
527 // Called by main thread..
528 Dali::Texture GetEmptyTextureWhiteRGB()
530 if(!gEmptyTextureWhiteRGB)
532 Dali::PixelData emptyPixelData = GetEmptyPixelDataWhiteRGB();
533 gEmptyTextureWhiteRGB = Texture::New(TextureType::TEXTURE_2D, emptyPixelData.GetPixelFormat(), emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
534 gEmptyTextureWhiteRGB.Upload(emptyPixelData, 0, 0, 0, 0, emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
536 return gEmptyTextureWhiteRGB;
539 Dali::Texture GetEmptyCubeTextureWhiteRGB()
541 if(!gEmptyCubeTextureWhiteRGB)
543 Dali::PixelData emptyPixelData = GetEmptyPixelDataWhiteRGB();
544 gEmptyCubeTextureWhiteRGB = Texture::New(TextureType::TEXTURE_CUBE, emptyPixelData.GetPixelFormat(), emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
545 for(size_t iSide = 0u; iSide < 6; ++iSide)
547 gEmptyCubeTextureWhiteRGB.Upload(emptyPixelData, CubeMapLayer::POSITIVE_X + iSide, 0u, 0u, 0u, emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
550 return gEmptyCubeTextureWhiteRGB;
553 Dali::Texture GetCachedTexture(Dali::PixelData pixelData, bool mipmapRequired)
555 if(Dali::Adaptor::IsAvailable() && SupportPixelDataCache(pixelData))
557 return GetCacheImpl()->GetOrCreateCachedTexture(pixelData, mipmapRequired);
561 return CreateTextureFromPixelData(pixelData, mipmapRequired);
565 void RequestGarbageCollect(bool fullCollect)
567 if(DALI_LIKELY(Dali::Adaptor::IsAvailable()))
569 GetCacheImpl()->RequestGarbageCollect(fullCollect);
573 void EnsureResourceLoaderCreated()
575 if(DALI_LIKELY(Dali::Adaptor::IsAvailable()))
581 // Can be called by worker thread.
582 Dali::PixelData GetEmptyPixelDataWhiteRGB()
584 static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[3]{0xff, 0xff, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY);
585 return emptyPixelData;
588 Dali::PixelData GetEmptyPixelDataWhiteRGBA()
590 static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[4]{0xff, 0xff, 0xff, 0xff}, 4, 1, 1, Pixel::RGBA8888, PixelData::DELETE_ARRAY);
591 return emptyPixelData;
594 Dali::PixelData GetEmptyPixelDataZAxisRGB()
596 static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[3]{0x7f, 0x7f, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY);
597 return emptyPixelData;
600 Dali::PixelData GetEmptyPixelDataZAxisAndAlphaRGBA()
602 static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[4]{0x7f, 0x7f, 0xff, 0xff}, 4, 1, 1, Pixel::RGBA8888, PixelData::DELETE_ARRAY);
603 return emptyPixelData;
606 Dali::PixelData GetCachedPixelData(const std::string& url)
608 return GetCachedPixelData(url, ImageDimensions(), FittingMode::DEFAULT, SamplingMode::BOX_THEN_LINEAR, true);
611 Dali::PixelData GetCachedPixelData(const std::string& url,
612 ImageDimensions dimensions,
613 FittingMode::Type fittingMode,
614 SamplingMode::Type samplingMode,
615 bool orientationCorrection)
617 ImageInformation info(url, dimensions, fittingMode, samplingMode, orientationCorrection);
618 if(gCacheImpl == nullptr)
620 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "CacheImpl not prepared! load PixelData without cache.\n");
621 return CreatePixelDataFromImageInfo(info, false);
625 return GetCacheImpl()->GetOrCreateCachedPixelData(info, true);
628 } // namespace ImageResourceLoader
629 } // namespace Dali::Scene3D::Internal