/*
- * Copyright (c) 2023 Samsung Electronics Co., Ltd.
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
#include <dali-scene3d/internal/common/image-resource-loader.h>
// EXTERNAL INCLUDES
-#include <dali-toolkit/public-api/image-loader/sync-image-loader.h>
+#include <dali/devel-api/adaptor-framework/image-loading.h>
+#include <dali/devel-api/adaptor-framework/lifecycle-controller.h>
+#include <dali/devel-api/adaptor-framework/pixel-buffer.h>
#include <dali/devel-api/common/hash.h>
#include <dali/devel-api/common/map-wrapper.h>
#include <dali/devel-api/threading/mutex.h>
#include <dali/integration-api/adaptor-framework/adaptor.h>
#include <dali/integration-api/debug.h>
+#include <dali/integration-api/pixel-data-integ.h>
#include <dali/public-api/adaptor-framework/timer.h>
#include <dali/public-api/common/vector-wrapper.h>
#include <dali/public-api/object/base-object.h>
#include <dali/public-api/signals/connection-tracker.h>
#include <functional> ///< for std::function
+#include <memory> ///< for std::shared_ptr
#include <mutex>
+#include <sstream>
#include <string>
#include <utility> ///< for std::pair
Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, false, "LOG_IMAGE_RESOURCE_LOADER");
#endif
+bool IsDefaultPixelData(const Dali::PixelData& pixelData)
+{
+ if(pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataWhiteRGB() ||
+ pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataWhiteRGBA() ||
+ pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataZAxisRGB() ||
+ pixelData == Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataZAxisAndAlphaRGBA())
+ {
+ return true;
+ }
+ return false;
+}
+
+bool SupportPixelDataCache(const Dali::PixelData& pixelData)
+{
+ // Check given pixelData support to release data after upload.
+ // This is cause we need to reduce CPU memory usage.
+ if(Dali::Integration::IsPixelDataReleaseAfterUpload(pixelData))
+ {
+ return true;
+ }
+
+ // Check given pixelData is default pixelData.
+ if(IsDefaultPixelData(pixelData))
+ {
+ return true;
+ }
+ return false;
+}
+
struct ImageInformation
{
ImageInformation(const std::string url,
*hashTargetPtr++ = info.mDimensions.GetHeight() & 0xff;
*hashTargetPtr++ = (info.mDimensions.GetHeight() >> 8u) & 0xff;
- // Bit-pack the FittingMode, SamplingMode and atlasing.
+ // Bit-pack the FittingMode, SamplingMode and orientation correction.
// FittingMode=2bits, SamplingMode=3bits, orientationCorrection=1bit
*hashTargetPtr = (info.mFittingMode << 4u) | (info.mSamplingMode << 1) | (info.mOrientationCorrection ? 1 : 0);
}
return reinterpret_cast<std::size_t>(static_cast<void*>(pixelData.GetObjectPtr())) ^ (static_cast<std::size_t>(mipmapRequired) << (sizeof(std::size_t) * 4));
}
-std::size_t GenerateHash(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
-{
- std::size_t result = 0x12345678u + pixelDataList.size();
- for(const auto& mipmapPixelDataList : pixelDataList)
- {
- result += (result << 5) + mipmapPixelDataList.size();
- for(const auto& pixelData : mipmapPixelDataList)
- {
- result += (result << 5) + GenerateHash(pixelData, false);
- }
- }
-
- return result ^ (static_cast<std::size_t>(mipmapRequired) << (sizeof(std::size_t) * 4));
-}
-
// Item Creation functor list
-Dali::PixelData CreatePixelDataFromImageInfo(const ImageInformation& info, bool /* Not used */)
+Dali::PixelData CreatePixelDataFromImageInfo(const ImageInformation& info, bool releasePixelData)
{
- return Dali::Toolkit::SyncImageLoader::Load(info.mUrl, info.mDimensions, info.mFittingMode, info.mSamplingMode, info.mOrientationCorrection);
+ Dali::PixelData pixelData;
+
+ // Load the image synchronously (block the thread here).
+ Dali::Devel::PixelBuffer pixelBuffer = Dali::LoadImageFromFile(info.mUrl, info.mDimensions, info.mFittingMode, info.mSamplingMode, info.mOrientationCorrection);
+ if(pixelBuffer)
+ {
+ pixelData = Dali::Devel::PixelBuffer::Convert(pixelBuffer, releasePixelData);
+ }
+ return pixelData;
}
Dali::Texture CreateTextureFromPixelData(const Dali::PixelData& pixelData, bool mipmapRequired)
return texture;
}
-Dali::Texture CreateCubeTextureFromPixelDataList(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
+// Check function whether we can collect given data as garbage, or not.
+bool PixelDataCacheCollectable(const ImageInformation& info, const Dali::PixelData& pixelData)
{
- Dali::Texture texture;
- if(!pixelDataList.empty() && !pixelDataList[0].empty())
- {
- texture = Dali::Texture::New(Dali::TextureType::TEXTURE_CUBE, pixelDataList[0][0].GetPixelFormat(), pixelDataList[0][0].GetWidth(), pixelDataList[0][0].GetHeight());
- for(size_t iSide = 0u, iEndSize = pixelDataList.size(); iSide < iEndSize; ++iSide)
- {
- auto& side = pixelDataList[iSide];
- for(size_t iMipLevel = 0u, iEndMipLevel = pixelDataList[0].size(); iMipLevel < iEndMipLevel; ++iMipLevel)
- {
- texture.Upload(side[iMipLevel], Dali::CubeMapLayer::POSITIVE_X + iSide, iMipLevel, 0u, 0u, side[iMipLevel].GetWidth(), side[iMipLevel].GetHeight());
- }
- }
- if(mipmapRequired)
- {
- texture.GenerateMipmaps();
- }
- }
+ return pixelData.GetBaseObject().ReferenceCount() <= 1;
+}
- return texture;
+bool TextureCacheCollectable(const Dali::PixelData& pixelData, const Dali::Texture& texture)
+{
+ return !IsDefaultPixelData(pixelData) && ///< If key is not default pixelData
+ pixelData.GetBaseObject().ReferenceCount() <= 2 && ///< And it have reference count as 2 (1 is for the key of this container, and other is PixelData cache.)
+ texture.GetBaseObject().ReferenceCount() <= 1; ///< And nobody use this texture, except this contianer.
}
+
+// Forward declare, for signal connection.
+void DestroyCacheImpl();
+
class CacheImpl : public Dali::ConnectionTracker
{
public:
CacheImpl()
: mPixelDataCache{},
mTextureCache{},
- mCubeTextureCache{},
mTimer{},
mLatestCollectedPixelDataIter{mPixelDataCache.begin()},
mLatestCollectedTextureIter{mTextureCache.begin()},
- mLatestCollectedCubeTextureIter{mCubeTextureCache.begin()},
+ mDataMutex{},
mPixelDataContainerUpdated{false},
mTextureContainerUpdated{false},
- mCubeTextureContainerUpdated{false},
- mDataMutex{},
mDestroyed{false},
mFullCollectRequested{false}
{
+ DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create CacheImpl\n");
+
+ // We should create CacheImpl at main thread, To ensure delete this cache impl
+ Dali::LifecycleController::Get().TerminateSignal().Connect(DestroyCacheImpl);
}
/**
*/
~CacheImpl()
{
+ DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Destroy CacheImpl\n");
{
mDataMutex.lock();
- mDestroyed = true;
- mPixelDataContainerUpdated = false;
- mTextureContainerUpdated = false;
- mCubeTextureContainerUpdated = false;
- mLatestCollectedPixelDataIter = decltype(mLatestCollectedPixelDataIter)(); // Invalidate iterator
- mLatestCollectedTextureIter = decltype(mLatestCollectedTextureIter)(); // Invalidate iterator
- mLatestCollectedCubeTextureIter = decltype(mLatestCollectedCubeTextureIter){}; // Invalidate iterator
+ mDestroyed = true;
+ mPixelDataContainerUpdated = false;
+ mTextureContainerUpdated = false;
+ mLatestCollectedPixelDataIter = decltype(mLatestCollectedPixelDataIter)(); // Invalidate iterator
+ mLatestCollectedTextureIter = decltype(mLatestCollectedTextureIter)(); // Invalidate iterator
mPixelDataCache.clear();
mTextureCache.clear();
- mCubeTextureCache.clear();
mDataMutex.unlock();
}
private: // Unified API for this class
// Let compare with hash first. And then, check detail keys after.
- using PixelDataCacheContainer = std::map<std::size_t, std::vector<std::pair<ImageInformation, Dali::PixelData>>>;
- using TextureCacheContainer = std::map<std::size_t, std::vector<std::pair<Dali::PixelData, Dali::Texture>>>;
- using CubeTextureCacheContainer = std::map<std::size_t, std::vector<std::pair<std::vector<std::vector<Dali::PixelData>>, Dali::Texture>>>;
+ using PixelDataCacheContainer = std::map<std::size_t, std::vector<std::pair<ImageInformation, Dali::PixelData>>>;
+ using TextureCacheContainer = std::map<std::size_t, std::vector<std::pair<Dali::PixelData, Dali::Texture>>>;
/**
* @brief Try to get cached item, or create new handle if there is no item.
bool found = false;
auto iter = cacheContainer.lower_bound(hashValue);
+ DALI_LOG_INFO(gLogFilter, Debug::Verbose, "HashValue : %zu\n", hashValue);
if((iter == cacheContainer.end()) || (hashValue != iter->first))
{
containerUpdated = true;
return returnItem;
}
+
/**
* @brief Try to collect garbages, which reference counts are 1.
*
* @oaram[in, out] checkedCount The number of iteration checked total.
* @return True if we iterate whole container, so we don't need to check anymore. False otherwise
*/
- template<bool needMutex, typename ContainerType, typename Iterator = typename ContainerType::iterator>
- bool CollectGarbages(ContainerType& cacheContainer, bool fullCollect, bool& containerUpdated, Iterator& lastIterator, uint32_t& checkedCount)
+ template<typename KeyType, typename ValueType, bool (*Collectable)(const KeyType&, const ValueType&), typename ContainerType, typename Iterator = typename ContainerType::iterator>
+ bool CollectGarbages(ContainerType& cacheContainer, bool fullCollect, bool& containerUpdated, Iterator& lastIterator, uint32_t& checkedCount, uint32_t& collectedCount)
{
- if constexpr(needMutex)
- {
- mDataMutex.lock();
- }
-
- DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Collect Garbages : %zu\n", cacheContainer.size());
+ DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Collect Garbages : %zu (checkedCount : %d, fullCollect? %d)\n", cacheContainer.size(), checkedCount, fullCollect);
// Container changed. We should re-collect garbage from begin again.
if(fullCollect || containerUpdated)
{
{
auto& item = jter->second;
DALI_LOG_INFO(gLogFilter, Debug::Verbose, "item : %p, ref count : %u\n", item.GetObjectPtr(), (item ? item.GetBaseObject().ReferenceCount() : 0u));
- if(!item || (item.GetBaseObject().ReferenceCount() == 1u))
+ if(!item || Collectable(jter->first, item))
{
+ DALI_LOG_INFO(gLogFilter, Debug::Verbose, "GC!!!\n");
// This item is garbage! just remove it.
+ ++collectedCount;
jter = cachePairList.erase(jter);
}
else
}
}
- if constexpr(needMutex)
- {
- mDataMutex.unlock();
- }
-
return (lastIterator != cacheContainer.end());
}
}
/**
- * @brief Try to get cached cube texture, or newly create if there is no cube texture that already cached.
- *
- * @param[in] pixelDataList The pixelData list of image.
- * @param[in] mipmapRequired True if result texture need to generate mipmap.
- * @return Texture that has been cached. Or empty handle if we fail to found cached item.
- */
- Dali::Texture GetOrCreateCachedCubeTexture(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
- {
- auto hashValue = GenerateHash(pixelDataList, mipmapRequired);
- return GetOrCreateCachedItem<false, std::vector<std::vector<Dali::PixelData>>, Dali::Texture, CreateCubeTextureFromPixelDataList>(mCubeTextureCache, hashValue, pixelDataList, mipmapRequired, mCubeTextureContainerUpdated);
- }
-
- /**
* @brief Request incremental gargabe collect.
*
* @param[in] fullCollect True if we will collect whole items, or incrementally.
*/
void RequestGarbageCollect(bool fullCollect)
{
- if(DALI_LIKELY(!mDestroyed && Dali::Adaptor::IsAvailable()))
+ if(DALI_LIKELY(Dali::Adaptor::IsAvailable()))
{
if(!mTimer)
{
if(!mTimer.IsRunning())
{
// Restart container interating.
- if(!mPixelDataContainerUpdated)
- {
- mDataMutex.lock();
- mPixelDataContainerUpdated = true;
- mDataMutex.unlock();
- }
+ mDataMutex.lock();
+ mPixelDataContainerUpdated = true;
+ mDataMutex.unlock();
mTextureContainerUpdated = true;
mTimer.Start();
}
* @brief Try to get cached pixel data, or newly create if there is no pixel data that already cached.
*
* @param[in] info The informations of image to load.
+ * @param[in] releasePixelData Whether we need to release pixel data after upload, or not.
* @return Texture that has been cached. Or empty handle if we fail to found cached item.
*/
- Dali::PixelData GetOrCreateCachedPixelData(const ImageInformation& info)
+ Dali::PixelData GetOrCreateCachedPixelData(const ImageInformation& info, bool releasePixelData)
{
auto hashValue = GenerateHash(info);
- return GetOrCreateCachedItem<true, ImageInformation, Dali::PixelData, CreatePixelDataFromImageInfo>(mPixelDataCache, hashValue, info, false, mPixelDataContainerUpdated);
+ return GetOrCreateCachedItem<true, ImageInformation, Dali::PixelData, CreatePixelDataFromImageInfo>(mPixelDataCache, hashValue, info, releasePixelData, mPixelDataContainerUpdated);
}
private: // Called by main thread
bool IncrementalGarbageCollect(bool fullCollect)
{
bool continueTimer = false;
+ DALI_LOG_INFO(gLogFilter, Debug::Verbose, "GC start\n");
- if(DALI_LIKELY(!mDestroyed))
- {
- // Try to collect Texture GC first, due to the reference count of pixelData who become key of textures.
- // After all texture GC finished, then check PixelData cache.
- uint32_t checkedCount = 0u;
- // GC Cube Texture
- continueTimer |= CollectGarbages<false>(mCubeTextureCache, fullCollect, mCubeTextureContainerUpdated, mLatestCollectedCubeTextureIter, checkedCount);
+ // Try to collect Texture GC first, due to the reference count of pixelData who become key of textures.
+ // After all texture GC finished, then check PixelData cache.
+ uint32_t checkedCount = 0u;
+ uint32_t collectedCount = 0u;
- // GC Texture
- continueTimer |= CollectGarbages<false>(mTextureCache, fullCollect, mTextureContainerUpdated, mLatestCollectedTextureIter, checkedCount);
+ // We should lock mutex during GC pixelData.
+ mDataMutex.lock();
- // GC PixelData. We should lock mutex during GC pixelData.
- continueTimer |= CollectGarbages<true>(mPixelDataCache, fullCollect, mPixelDataContainerUpdated, mLatestCollectedPixelDataIter, checkedCount);
- }
+ // GC Texture
+ continueTimer |= CollectGarbages<Dali::PixelData, Dali::Texture, TextureCacheCollectable>(mTextureCache, fullCollect, mTextureContainerUpdated, mLatestCollectedTextureIter, checkedCount, collectedCount);
+
+ // GC PixelData last. If there are some collected Texture before, we should full-collect.
+ // (Since most of PixelData use 'ReleaseAfterUpload' flags).
+ continueTimer |= CollectGarbages<ImageInformation, Dali::PixelData, PixelDataCacheCollectable>(mPixelDataCache, fullCollect || (collectedCount > 0u), mPixelDataContainerUpdated, mLatestCollectedPixelDataIter, checkedCount, collectedCount);
+
+ mDataMutex.unlock();
+
+ DALI_LOG_INFO(gLogFilter, Debug::Verbose, "GC finished. checkedCount : %u, continueTimer : %d\n", checkedCount, continueTimer);
return continueTimer;
}
private:
- PixelDataCacheContainer mPixelDataCache;
- TextureCacheContainer mTextureCache;
- CubeTextureCacheContainer mCubeTextureCache;
+ PixelDataCacheContainer mPixelDataCache;
+ TextureCacheContainer mTextureCache;
Dali::Timer mTimer;
// Be used when we garbage collection.
- PixelDataCacheContainer::iterator mLatestCollectedPixelDataIter;
- TextureCacheContainer::iterator mLatestCollectedTextureIter;
- CubeTextureCacheContainer::iterator mLatestCollectedCubeTextureIter;
+ PixelDataCacheContainer::iterator mLatestCollectedPixelDataIter;
+ TextureCacheContainer::iterator mLatestCollectedTextureIter;
+
+ std::mutex mDataMutex;
bool mPixelDataContainerUpdated;
bool mTextureContainerUpdated;
- bool mCubeTextureContainerUpdated;
-
- std::mutex mDataMutex;
bool mDestroyed : 1;
bool mFullCollectRequested : 1;
};
-CacheImpl& GetCacheImpl()
+static std::shared_ptr<CacheImpl> gCacheImpl{nullptr};
+static Dali::Texture gEmptyTextureWhiteRGB{};
+static Dali::Texture gEmptyCubeTextureWhiteRGB{};
+
+std::shared_ptr<CacheImpl> GetCacheImpl()
{
- static CacheImpl gCacheImpl;
+ if(DALI_UNLIKELY(!gCacheImpl))
+ {
+ gCacheImpl = std::make_shared<CacheImpl>();
+ }
return gCacheImpl;
}
+void DestroyCacheImpl()
+{
+ gCacheImpl.reset();
+
+ // Remove texture object when application stopped.
+ gEmptyTextureWhiteRGB.Reset();
+ gEmptyCubeTextureWhiteRGB.Reset();
+}
+
} // namespace
namespace Dali::Scene3D::Internal
namespace ImageResourceLoader
{
// Called by main thread..
-Dali::PixelData GetEmptyPixelDataWhiteRGB()
+Dali::Texture GetEmptyTextureWhiteRGB()
{
- static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[3]{0xff, 0xff, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY);
- return emptyPixelData;
+ if(!gEmptyTextureWhiteRGB)
+ {
+ Dali::PixelData emptyPixelData = GetEmptyPixelDataWhiteRGB();
+ gEmptyTextureWhiteRGB = Texture::New(TextureType::TEXTURE_2D, emptyPixelData.GetPixelFormat(), emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
+ gEmptyTextureWhiteRGB.Upload(emptyPixelData, 0, 0, 0, 0, emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
+ }
+ return gEmptyTextureWhiteRGB;
}
-Dali::Texture GetEmptyTextureWhiteRGB()
+Dali::Texture GetEmptyCubeTextureWhiteRGB()
{
- static Dali::PixelData emptyPixelData = GetEmptyPixelDataWhiteRGB();
- static Dali::Texture emptyTexture = Dali::Texture();
- if(!emptyTexture)
+ if(!gEmptyCubeTextureWhiteRGB)
{
- emptyTexture = Texture::New(TextureType::TEXTURE_2D, emptyPixelData.GetPixelFormat(), emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
- emptyTexture.Upload(emptyPixelData, 0, 0, 0, 0, emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
+ Dali::PixelData emptyPixelData = GetEmptyPixelDataWhiteRGB();
+ gEmptyCubeTextureWhiteRGB = Texture::New(TextureType::TEXTURE_CUBE, emptyPixelData.GetPixelFormat(), emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
+ for(size_t iSide = 0u; iSide < 6; ++iSide)
+ {
+ gEmptyCubeTextureWhiteRGB.Upload(emptyPixelData, CubeMapLayer::POSITIVE_X + iSide, 0u, 0u, 0u, emptyPixelData.GetWidth(), emptyPixelData.GetHeight());
+ }
}
- return emptyTexture;
+ return gEmptyCubeTextureWhiteRGB;
}
Dali::Texture GetCachedTexture(Dali::PixelData pixelData, bool mipmapRequired)
{
- return GetCacheImpl().GetOrCreateCachedTexture(pixelData, mipmapRequired);
+ if(SupportPixelDataCache(pixelData))
+ {
+ return GetCacheImpl()->GetOrCreateCachedTexture(pixelData, mipmapRequired);
+ }
+ else
+ {
+ return CreateTextureFromPixelData(pixelData, mipmapRequired);
+ }
}
-Dali::Texture GetCachedCubeTexture(const std::vector<std::vector<Dali::PixelData>>& pixelDataList, bool mipmapRequired)
+void RequestGarbageCollect(bool fullCollect)
{
- return GetCacheImpl().GetOrCreateCachedCubeTexture(pixelDataList, mipmapRequired);
+ GetCacheImpl()->RequestGarbageCollect(fullCollect);
}
-void RequestGarbageCollect(bool fullCollect)
+void EnsureResourceLoaderCreated()
{
- GetCacheImpl().RequestGarbageCollect(fullCollect);
+ GetCacheImpl();
}
// Can be called by worker thread.
+Dali::PixelData GetEmptyPixelDataWhiteRGB()
+{
+ static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[3]{0xff, 0xff, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY);
+ return emptyPixelData;
+}
+
+Dali::PixelData GetEmptyPixelDataWhiteRGBA()
+{
+ static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[4]{0xff, 0xff, 0xff, 0xff}, 4, 1, 1, Pixel::RGBA8888, PixelData::DELETE_ARRAY);
+ return emptyPixelData;
+}
+
+Dali::PixelData GetEmptyPixelDataZAxisRGB()
+{
+ static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[3]{0x7f, 0x7f, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY);
+ return emptyPixelData;
+}
+
+Dali::PixelData GetEmptyPixelDataZAxisAndAlphaRGBA()
+{
+ static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[4]{0x7f, 0x7f, 0xff, 0xff}, 4, 1, 1, Pixel::RGBA8888, PixelData::DELETE_ARRAY);
+ return emptyPixelData;
+}
+
Dali::PixelData GetCachedPixelData(const std::string& url)
{
return GetCachedPixelData(url, ImageDimensions(), FittingMode::DEFAULT, SamplingMode::BOX_THEN_LINEAR, true);
bool orientationCorrection)
{
ImageInformation info(url, dimensions, fittingMode, samplingMode, orientationCorrection);
- return GetCacheImpl().GetOrCreateCachedPixelData(info);
+ if(gCacheImpl == nullptr)
+ {
+ DALI_LOG_INFO(gLogFilter, Debug::Verbose, "CacheImpl not prepared! load PixelData without cache.\n");
+ return CreatePixelDataFromImageInfo(info, false);
+ }
+ else
+ {
+ return GetCacheImpl()->GetOrCreateCachedPixelData(info, true);
+ }
}
} // namespace ImageResourceLoader
} // namespace Dali::Scene3D::Internal