// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. #include "Asset.h" #include "Content.h" #include "SoftAssetReference.h" #include "Cache/AssetsCache.h" #include "Loading/ContentLoadingManager.h" #include "Loading/Tasks/LoadAssetTask.h" #include "Engine/Core/Log.h" #include "Engine/Engine/Engine.h" #include "Engine/Threading/Threading.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Threading/MainThreadTask.h" #include "Engine/Threading/ConcurrentTaskQueue.h" #if USE_MONO #include #endif AssetReferenceBase::~AssetReferenceBase() { if (_asset) { _asset->OnLoaded.Unbind(this); _asset->OnUnloaded.Unbind(this); _asset->RemoveReference(); } } String AssetReferenceBase::ToString() const { return _asset ? _asset->ToString() : TEXT(""); } void AssetReferenceBase::OnSet(Asset* asset) { auto e = _asset; if (e != asset) { if (e) { e->OnLoaded.Unbind(this); e->OnUnloaded.Unbind(this); e->RemoveReference(); } _asset = e = asset; if (e) { e->AddReference(); e->OnLoaded.Bind(this); e->OnUnloaded.Bind(this); } Changed(); if (e && e->IsLoaded()) Loaded(); } } void AssetReferenceBase::OnLoaded(Asset* asset) { if (this == nullptr || _asset != asset) return; Loaded(); } void AssetReferenceBase::OnUnloaded(Asset* asset) { if (this == nullptr || _asset != asset) return; Unload(); OnSet(nullptr); } WeakAssetReferenceBase::~WeakAssetReferenceBase() { if (_asset) _asset->OnUnloaded.Unbind(this); } String WeakAssetReferenceBase::ToString() const { return _asset ? _asset->ToString() : TEXT(""); } void WeakAssetReferenceBase::OnSet(Asset* asset) { auto e = _asset; if (e != asset) { if (e) e->OnUnloaded.Unbind(this); _asset = e = asset; if (e) e->OnUnloaded.Bind(this); } } void WeakAssetReferenceBase::OnUnloaded(Asset* asset) { if (_asset != asset) return; Unload(); asset->OnUnloaded.Unbind(this); _asset = nullptr; } String SoftAssetReferenceBase::ToString() const { return _asset ? _asset->ToString() : (_id.IsValid() ? _id.ToString() : TEXT("")); } void SoftAssetReferenceBase::OnSet(Asset* asset) { if (_asset == asset) return; if (_asset) { _asset->OnUnloaded.Unbind(this); _asset->RemoveReference(); } _asset = asset; _id = asset ? asset->GetID() : Guid::Empty; if (asset) { asset->AddReference(); asset->OnUnloaded.Bind(this); } Changed(); } void SoftAssetReferenceBase::OnSet(const Guid& id) { if (_id == id) return; if (_asset) { _asset->OnUnloaded.Unbind(this); _asset->RemoveReference(); } _asset = nullptr; _id = id; Changed(); } void SoftAssetReferenceBase::OnResolve(const ScriptingTypeHandle& type) { ASSERT(!_asset); _asset = ::LoadAsset(_id, type); if (_asset) { _asset->OnUnloaded.Bind(this); _asset->AddReference(); } } void SoftAssetReferenceBase::OnUnloaded(Asset* asset) { ASSERT(_asset == asset); _asset->RemoveReference(); _asset->OnUnloaded.Unbind(this); _asset = nullptr; _id = Guid::Empty; Changed(); } Asset::Asset(const SpawnParams& params, const AssetInfo* info) : ManagedScriptingObject(params) , _refCount(0) , _loadingTask(nullptr) , _isLoaded(false) , _loadFailed(false) , _deleteFileOnUnload(false) , _isVirtual(false) { } int32 Asset::GetReferencesCount() const { return (int32)Platform::AtomicRead(const_cast(&_refCount)); } String Asset::ToString() const { return String::Format(TEXT("{0}, {1}, {2}"), GetTypeName(), GetID(), GetPath()); } void Asset::OnDeleteObject() { ASSERT(IsInMainThread()); // Send event to the gameplay so it can release handle to this asset // This may happen when asset gets removed but sth is still referencing it (eg. in managed code) if (!IsInternalType()) Content::AssetDisposing(this); const bool wasMarkedToDelete = _deleteFileOnUnload != 0; #if USE_EDITOR const String path = wasMarkedToDelete ? GetPath() : String::Empty; #endif const Guid id = GetID(); // Fire unload event (every object referencing this asset or it's data should release reference so later actions are safe) onUnload_MainThread(); // Remove from pool Content::onAssetUnload(this); // Unload asset data (in a safe way to protect asset data) Locker.Lock(); if (_isLoaded) { unload(false); _isLoaded = false; } Locker.Unlock(); #if BUILD_DEBUG // Ensure no object is referencing it (except managed instance if exists) //ASSERT(_refCount == (HasManagedInstance() ? 1 : 0)); #endif // Base (after it `this` is invalid) ManagedScriptingObject::OnDeleteObject(); #if USE_EDITOR if (wasMarkedToDelete) { LOG(Info, "Deleting asset '{0}':{1}.", path, id.ToString()); // Remove from registry Content::GetRegistry()->DeleteAsset(id, nullptr); // Delete file Content::deleteFileSafety(path, id); } #endif } bool Asset::CreateManaged() { // Base if (ManagedScriptingObject::CreateManaged()) return true; // Managed objects holds a reference to this asset until it will be removed by GC AddReference(); return false; } void Asset::DestroyManaged() { // Clear reference if (HasManagedInstance()) { RemoveReference(); } // Base ManagedScriptingObject::DestroyManaged(); } void Asset::OnManagedInstanceDeleted() { // Clear reference RemoveReference(); // Cleanup if (_gcHandle) { #if USE_MONO mono_gchandle_free(_gcHandle); #endif _gcHandle = 0; } // But do not delete itself } void Asset::OnScriptingDispose() { // Delete C# object if (IsRegistered()) UnregisterObject(); DestroyManaged(); // Don't delete C++ object } void Asset::ChangeID(const Guid& newId) { // Don't allow to change asset ids CRASH; } bool Asset::LastLoadFailed() const { return _loadFailed != 0; } #if USE_EDITOR bool Asset::ShouldDeleteFileOnUnload() const { return _deleteFileOnUnload != 0; } #endif void Asset::Reload() { // Virtual assets are memory-only so reloading them makes no sense if (IsVirtual()) return; // It's better to call it from the main thread if (IsInMainThread()) { LOG(Info, "Reloading asset {0}", ToString()); WaitForLoaded(); // Fire event if (!IsInternalType()) Content::AssetReloading(this); OnReloading(this); ScopeLock lock(Locker); if (IsLoaded()) { // Unload current data unload(true); _isLoaded = false; } // Start reloading process startLoading(); } else { Function action; action.Bind(this); Task::StartNew(New(action, this)); } } namespace ContentLoadingManagerImpl { extern ConcurrentTaskQueue Tasks; }; bool Asset::WaitForLoaded(double timeoutInMilliseconds) const { // This function is used many time when some parts of the engine need to wait for asset loading end (it may fail but has to end). // But it cannot be just a simple active-wait loop. // Imagine following situation: // Content Pool has 2 loading threads. // Both start to load layered materials which need to be recompiled (Material Generator work). // Every of these materials is made of a few layers. // To load child layer Material Generator is requesting Content Pool to load it fully. // However this cannot be done because Pool has limited threads and all of them may requesting more loads to be done. // // To solve that problem we have different solutions: // 1) add more loading threads (bad idea) // 2) content loading could use thread pool to enqueue single tasks (still risky due to many tasks queued and limited Thread Pool size (need to build graph of dependencies?)) // 3) content loading could detect asset load calls from content loader threads and load requested asset without stalls // 4) every asset could expose dependencies and content system could load required dependencies earlier // // 3) and 4) are good solutions. 4) would require more work but will be needed in future for building system to gather assets for game packages. // But WaitForLoaded could detect if is called from the Loading Thead and manually load dependent asset. It's fairly easy to do and will work out. // Early out if asset has been already loaded if (IsLoaded()) { // If running on a main thread we can flush asset `Loaded` event if (IsInMainThread()) { Content::tryCallOnLoaded((Asset*)this); } return false; } // Check if has missing loading task Platform::MemoryBarrier(); const auto loadingTask = _loadingTask; if (loadingTask == nullptr) { LOG(Warning, "WaitForLoaded asset \'{0}\' failed. No loading task attached and asset is not loaded.", ToString()); return true; } PROFILE_CPU(); // Check if call is made from the Loading Thread and task has not been taken yet auto thread = ContentLoadingManager::GetCurrentLoadThread(); if (thread != nullptr) { // Note: to reproduce this case just include material into material (use layering). // So during loading first material it will wait for child materials loaded calling this function Task* task = loadingTask; Array> localQueue; while (!Engine::ShouldExit()) { // Try to execute content tasks while (task->IsQueued() && !Engine::ShouldExit()) { // Dequeue task from the loading queue ContentLoadTask* tmp; if (ContentLoadingManagerImpl::Tasks.try_dequeue(tmp)) { if (tmp == task) { if (localQueue.Count() != 0) { // Put back queued tasks ContentLoadingManagerImpl::Tasks.enqueue_bulk(localQueue.Get(), localQueue.Count()); localQueue.Clear(); } thread->Run(tmp); } else { localQueue.Add(tmp); } } else { // No task in queue but it's queued so other thread could have stolen it into own local queue break; } } if (localQueue.Count() != 0) { // Put back queued tasks ContentLoadingManagerImpl::Tasks.enqueue_bulk(localQueue.Get(), localQueue.Count()); localQueue.Clear(); } // Check if task is done if (task->IsEnded()) { // If was fine then wait for the next task if (task->IsFinished()) { task = task->GetContinueWithTask(); if (!task) break; } else { // Failed or cancelled so this wait also fails break; } } } } else { // Wait for task end loadingTask->Wait(timeoutInMilliseconds); } // If running on a main thread we can flush asset `Loaded` event if (IsInMainThread() && IsLoaded()) { Content::tryCallOnLoaded((Asset*)this); } return _isLoaded == 0; } void Asset::InitAsVirtual() { // Be a virtual thing _isVirtual = true; // Be a loaded thing _isLoaded = true; } #if USE_EDITOR void Asset::GetReferences(Array& output) const { // No refs by default } Array Asset::GetReferences() const { Array result; GetReferences(result); return result; } #endif void Asset::DeleteManaged() { if (HasManagedInstance()) { if (IsRegistered()) UnregisterObject(); DestroyManaged(); } } ContentLoadTask* Asset::createLoadingTask() { // Spawn new task and cache pointer return New(this); } void Asset::startLoading() { // Check if is already loaded if (IsLoaded()) return; // Start loading (using async tasks) ASSERT(_loadingTask == nullptr); _loadingTask = createLoadingTask(); ASSERT(_loadingTask != nullptr); _loadingTask->Start(); } void Asset::releaseStorage() { } bool Asset::IsInternalType() const { return false; } bool Asset::onLoad(LoadAssetTask* task) { // It may fail when task is cancelled and new one is created later (don't crash but just end with an error) if (task->Asset.Get() != this || _loadingTask == nullptr) return true; Locker.Lock(); // Load asset LoadResult result; { PROFILE_CPU_ASSET(this); result = loadAsset(); } const bool isLoaded = result == LoadResult::Ok; const bool failed = !isLoaded; _loadFailed = failed; _isLoaded = !failed; if (failed) { LOG(Error, "Loading asset \'{0}\' result: {1}.", ToString(), ToString(result)); } // Unlink task _loadingTask = nullptr; ASSERT(failed || IsLoaded() == isLoaded); Locker.Unlock(); // Send event if (isLoaded) { // Register event `OnLoaded` invoke on main thread // We don't want to fire it here because current thread is content loader. // This allows to reduce mutexes and locks (max one frame delay isn't hurting but provides more safety) Content::onAssetLoaded(this); } return failed; } void Asset::onLoaded() { if (IsInMainThread()) { onLoaded_MainThread(); } else if (OnLoaded.IsBinded()) { Function action; action.Bind(this); Task::StartNew(New(action, this)); } } void Asset::onLoaded_MainThread() { ASSERT(IsInMainThread()); // Send event OnLoaded(this); } void Asset::onUnload_MainThread() { // Note: asset should not be locked now (Locker should be free) so other thread won't be locked. ASSERT(IsInMainThread()); // Send event OnUnloaded(this); // Check if is during loading if (_loadingTask != nullptr) { // Cancel loading auto task = _loadingTask; _loadingTask = nullptr; LOG(Warning, "Cancel loading task for \'{0}\'", ToString()); task->Cancel(); } }