Files
FlaxEngine/Source/Engine/Content/BinaryAsset.cpp

645 lines
18 KiB
C++

// Copyright (c) Wojciech Figat. All rights reserved.
#include "BinaryAsset.h"
#include "Cache/AssetsCache.h"
#include "Storage/ContentStorageManager.h"
#include "Loading/Tasks/LoadAssetDataTask.h"
#include "Factories/BinaryAssetFactory.h"
#include "Engine/ContentImporters/AssetsImportingManager.h"
#include "Engine/Content/Content.h"
#include "Engine/Serialization/JsonTools.h"
#include "Engine/Debug/Exceptions/JsonParseException.h"
#include "Engine/Threading/ThreadPoolTask.h"
#include "Engine/Profiler/ProfilerMemory.h"
#if USE_EDITOR
#include "Engine/Platform/FileSystem.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Engine/Globals.h"
#endif
REGISTER_BINARY_ASSET_ABSTRACT(BinaryAsset, "FlaxEngine.BinaryAsset");
BinaryAsset::BinaryAsset(const SpawnParams& params, const AssetInfo* info)
: Asset(params, info)
, _storageRef(nullptr) // We link storage container later
, _isSaving(false)
, Storage(nullptr)
{
}
BinaryAsset::~BinaryAsset()
{
#if USE_EDITOR
if (Storage)
Storage->OnReloaded.Unbind<BinaryAsset, &BinaryAsset::OnStorageReloaded>(this);
#endif
}
bool BinaryAsset::Init(const FlaxStorageReference& storage, AssetHeader& header)
{
// We allow to init asset only once like that
ASSERT(Storage == nullptr && _header.ID.IsValid() == false);
// Block initialization with a different storage
bool isChanged = _storageRef != storage;
if (Storage != nullptr && isChanged)
{
LOG(Error, "Asset \'{0}\' has been already initialized.", GetPath());
return true;
}
// Get data
_storageRef = storage;
Storage = storage.Get();
_header = header;
#if USE_EDITOR
// Link for storage reload event
if (Storage && isChanged)
Storage->OnReloaded.Bind<BinaryAsset, &BinaryAsset::OnStorageReloaded>(this);
#endif
return false;
}
bool BinaryAsset::Init(AssetInitData& initData)
{
// Validate serialized version
if (initData.SerializedVersion != GetSerializedVersion())
{
LOG(Error, "Asset \'{0}\' is using different serialized version. Loaded: {1}, Runtime: {2}.", GetPath(), initData.SerializedVersion, GetSerializedVersion());
return true;
}
// Get asset data
_header = initData.Header;
#if USE_EDITOR
Metadata.Copy(initData.Metadata);
ClearDependencies();
Dependencies = initData.Dependencies;
for (auto& e : Dependencies)
{
auto asset = Cast<BinaryAsset>(Content::GetAsset(e.First));
if (asset)
{
asset->_dependantAssets.Add(this);
}
}
#endif
return init(initData);
}
bool BinaryAsset::InitVirtual(AssetInitData& initData)
{
// Be virtual
_isVirtual = true;
return Init(initData);
}
#if USE_EDITOR
#if COMPILE_WITH_ASSETS_IMPORTER
void BinaryAsset::Reimport() const
{
const String importPath = GetImportPath();
if (importPath.HasChars())
{
AssetsImportingManager::Import(importPath, GetPath());
}
}
#endif
void BinaryAsset::GetImportMetadata(String& path, String& username) const
{
if (Metadata.IsInvalid())
return;
// Parse metadata and try to get import info
rapidjson_flax::Document document;
document.Parse((const char*)Metadata.Get(), Metadata.Length());
if (document.HasParseError() == false)
{
path = JsonTools::GetString(document, "ImportPath");
username = JsonTools::GetString(document, "ImportUsername");
if (path.HasChars() && FileSystem::IsRelative(path))
{
// Convert path back to thr absolute (eg. if stored in relative format)
path = Globals::ProjectFolder / path;
StringUtils::PathRemoveRelativeParts(path);
}
}
else
{
Log::JsonParseException(document.GetParseError(), document.GetErrorOffset(), GetPath());
}
}
String BinaryAsset::GetImportPath() const
{
String path, username;
GetImportMetadata(path, username);
return path;
}
void BinaryAsset::ClearDependencies()
{
for (auto& e : Dependencies)
{
auto asset = Cast<BinaryAsset>(Content::GetAsset(e.First));
if (asset)
asset->_dependantAssets.Remove(this);
}
Dependencies.Clear();
}
void BinaryAsset::AddDependency(BinaryAsset* asset)
{
ASSERT_LOW_LAYER(asset);
const Guid id = asset->GetID();
for (auto& e : Dependencies)
{
if (e.First == id)
return;
}
ASSERT(!asset->_dependantAssets.Contains(asset));
Dependencies.Add(ToPair(id, FileSystem::GetFileLastEditTime(asset->GetPath())));
asset->_dependantAssets.Add(this);
}
bool BinaryAsset::HasDependenciesModified() const
{
AssetInfo info;
for (const auto& e : Dependencies)
{
if (Content::GetAssetInfo(e.First, info))
{
const auto editTime = FileSystem::GetFileLastEditTime(info.Path);
if (editTime > e.Second)
{
LOG(Info, "Asset {0} was modified - dependency of {1}", info.Path, GetPath());
return true;
}
}
}
return false;
}
#endif
FlaxChunk* BinaryAsset::GetOrCreateChunk(int32 index) const
{
if (IsVirtual()) // Virtual assets don't own storage container
return nullptr;
ASSERT(Math::IsInRange(index, 0, ASSET_FILE_DATA_CHUNKS - 1));
// Try get
auto chunk = _header.Chunks[index];
if (chunk)
{
chunk->RegisterUsage();
return chunk;
}
// Allocate
ASSERT(Storage);
const_cast<BinaryAsset*>(this)->_header.Chunks[index] = chunk = Storage->AllocateChunk();
if (chunk)
chunk->RegisterUsage();
return chunk;
}
void BinaryAsset::SetChunk(int32 index, const Span<byte>& data)
{
auto chunk = GetOrCreateChunk(index);
if (chunk)
chunk->Data.Copy(data.Get(), data.Length());
}
void BinaryAsset::ReleaseChunks() const
{
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
ReleaseChunk(i);
}
void BinaryAsset::ReleaseChunk(int32 index) const
{
auto chunk = GetChunk(index);
if (chunk)
chunk->Data.Release();
}
ContentLoadTask* BinaryAsset::RequestChunkDataAsync(int32 index)
{
auto chunk = GetChunk(index);
if (chunk != nullptr && chunk->IsLoaded())
{
// Data already here
chunk->RegisterUsage();
return nullptr;
}
// Spawn loading task
return New<LoadAssetDataTask>(this, GET_CHUNK_FLAG(index));
}
void BinaryAsset::GetChunkData(int32 index, BytesContainer& data) const
{
//ScopeLock lock(Locker);
// Check if has data missing
if (!HasChunkLoaded(index))
{
// Missing data
data.Release();
return;
}
// Get data
auto chunk = GetChunk(index);
data.Link(chunk->Data);
}
bool BinaryAsset::LoadChunk(int32 chunkIndex) const
{
ASSERT(Storage);
const auto chunk = _header.Chunks[chunkIndex];
if (chunk != nullptr
&& chunk->IsMissing()
&& chunk->ExistsInFile())
{
if (Storage->LoadAssetChunk(chunk))
return true;
}
return false;
}
bool BinaryAsset::LoadChunks(AssetChunksFlag chunks) const
{
if (chunks == 0)
return false;
ASSERT(Storage);
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
{
auto chunk = _header.Chunks[i];
if (chunk != nullptr
&& chunks & GET_CHUNK_FLAG(i)
&& chunk->IsMissing()
&& chunk->ExistsInFile())
{
if (Storage->LoadAssetChunk(chunk))
return true;
}
}
return false;
}
#if USE_EDITOR
bool BinaryAsset::SaveAsset(AssetInitData& data, bool silentMode) const
{
return SaveAsset(GetPath(), data, silentMode);
}
bool BinaryAsset::SaveAsset(const StringView& path, AssetInitData& data, bool silentMode) const
{
data.Header = _header;
data.Metadata.Link(Metadata);
data.Dependencies = Dependencies;
return SaveToAsset(path, data, silentMode);
}
bool BinaryAsset::SaveToAsset(const StringView& path, AssetInitData& data, bool silentMode)
{
PROFILE_CPU();
// Ensure path is in a valid format
String pathNorm(path);
ContentStorageManager::FormatPath(pathNorm);
const StringView filePath = pathNorm;
// Find target storage container and the asset
auto storage = ContentStorageManager::TryGetStorage(filePath);
auto asset = Content::GetAsset(filePath);
auto binaryAsset = dynamic_cast<BinaryAsset*>(asset);
if (asset && !binaryAsset)
{
LOG(Warning, "Cannot write to the non-binary asset location.");
return true;
}
if (!binaryAsset && !storage && FileSystem::FileExists(filePath))
{
// Force-resolve storage (asset at that path could be not yet loaded into registry)
storage = ContentStorageManager::GetStorage(filePath);
}
// Check if can perform write operation to the asset container
if (storage && !storage->AllowDataModifications())
{
LOG(Warning, "Cannot write to the asset storage container.");
return true;
}
// Initialize data container
ASSERT(data.SerializedVersion > 0);
if (binaryAsset)
{
// Use the same asset ID
data.Header.ID = binaryAsset->GetID();
}
else if (storage && storage->GetEntriesCount())
{
// Use the same file ID
data.Header.ID = storage->GetEntry(0).ID;
}
else
{
// Randomize ID
data.Header.ID = Guid::New();
}
// Save (set flag to lock reloads on storage modified)
if (binaryAsset)
binaryAsset->_isSaving = true;
bool result;
if (storage)
{
// HACK: file is locked by some tasks (e.g material asset loaded some data and is updating the asset)
// Let's hide these locks just for the saving
const auto locks = storage->_chunksLock;
storage->_chunksLock = 0;
result = storage->Save(data, silentMode);
ASSERT(storage->_chunksLock == 0);
storage->_chunksLock = locks;
}
else
{
ASSERT(filePath.HasChars());
result = FlaxStorage::Create(filePath, data, silentMode);
}
if (binaryAsset)
binaryAsset->_isSaving = false;
if (binaryAsset)
{
// Inform dependant asset (use cloned version because it might be modified by assets when they got reloaded)
auto dependantAssets = binaryAsset->_dependantAssets;
for (auto& e : dependantAssets)
{
e->OnDependencyModified(binaryAsset);
}
}
return result;
}
void BinaryAsset::OnStorageReloaded(FlaxStorage* storage, bool failed)
{
ASSERT(Storage != nullptr && Storage == storage);
// Clear header (prevent from using old chunks)
auto oldHeader = _header;
Platform::MemoryClear(_header.Chunks, sizeof(_header.Chunks));
// Check if reload failed
if (failed)
{
LOG(Error, "Asset storage reloading failed. Asset: \'{0}\'.", ToString());
return;
}
// Gather updated asset init data
AssetInitData initData;
if (Storage->LoadAssetHeader(GetID(), initData))
{
LOG(Error, "Asset header loading failed. Asset: \'{0}\'.", ToString());
return;
}
if (oldHeader.ID != initData.Header.ID || oldHeader.TypeName != initData.Header.TypeName)
{
LOG(Warning, "Asset reloading data mismatch. Old ID:{0},TypeName:{1}, New ID:{2},TypeName:{3}. Asset: \'{4}\'.", oldHeader.ID, oldHeader.TypeName, initData.Header.ID, initData.Header.TypeName, GetPath());
// Unload asset (file contains different asset data)
// For eg. texture has been changed into sprite atlas on reimport
Content::UnloadAsset(this);
// Delete managed object now because it way fail when we recreate the asset object and want to register the new managed object (IDs will overlap)
DeleteManaged();
return;
}
// Reinitialize (file may modify some data so it needs to be flushed)
if (Init(initData))
{
LOG(Error, "Asset reloading failed. Asset: \'{0}\'.", ToString());
}
// Don't reload on save
if (_isSaving == false)
{
Reload();
}
// Inform dependant asset (use cloned version because it might be modified by assets when they got reloaded)
auto dependantAssets = _dependantAssets;
for (auto& e : dependantAssets)
{
e->OnDependencyModified(this);
}
}
void BinaryAsset::OnDeleteObject()
{
// Clear dependencies stuff
ClearDependencies();
_dependantAssets.Clear();
Asset::OnDeleteObject();
}
#endif
const String& BinaryAsset::GetPath() const
{
#if USE_EDITOR
return Storage ? Storage->GetPath() : String::Empty;
#else
// In build all assets are packed into packages so use ID for original path lookup
return Content::GetRegistry()->GetEditorAssetPath(_id);
#endif
}
uint64 BinaryAsset::GetMemoryUsage() const
{
Locker.Lock();
uint64 result = Asset::GetMemoryUsage();
result += sizeof(BinaryAsset) - sizeof(Asset);
result += _dependantAssets.Capacity() * sizeof(BinaryAsset*);
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
{
auto chunk = _header.Chunks[i];
if (chunk != nullptr && chunk->IsLoaded())
result += chunk->Size();
}
Locker.Unlock();
return result;
}
/// <summary>
/// Helper task used to initialize binary asset and upgrade it if need to in background.
/// </summary>
/// <seealso cref="ContentLoadTask" />
class InitAssetTask : public ContentLoadTask
{
private:
WeakAssetReference<BinaryAsset> _asset;
FlaxStorage::LockData _dataLock;
public:
/// <summary>
/// Initializes a new instance of the <see cref="InitAssetTask"/> class.
/// </summary>
/// <param name="asset">The asset.</param>
InitAssetTask(BinaryAsset* asset)
: _asset(asset)
, _dataLock(asset->Storage->Lock())
{
}
public:
// [ContentLoadTask]
bool HasReference(Object* obj) const override
{
return obj == _asset;
}
protected:
// [ContentLoadTask]
Result run() override
{
AssetReference<BinaryAsset> ref = _asset.Get();
if (ref == nullptr)
return Result::MissingReferences;
auto storage = ref->Storage;
auto factory = (BinaryAssetFactoryBase*)Content::GetAssetFactory(ref->GetTypeName());
ASSERT(factory);
PROFILE_MEM(ContentAssets);
// Here we should open storage and extract AssetInitData
// This would also allow to convert/upgrade data
if (!storage->IsLoaded() && storage->Load())
return Result::AssetLoadError;
if (factory->Init(ref.Get()))
return Result::AssetLoadError;
return Result::Ok;
}
void OnEnd() override
{
_dataLock.Release();
_asset = nullptr;
ContentLoadTask::OnEnd();
}
};
ContentLoadTask* BinaryAsset::createLoadingTask()
{
ContentLoadTask* loadTask = Asset::createLoadingTask();
// Check if asset need any just to be preloaded
auto chunksToPreload = getChunksToPreload();
if (chunksToPreload != 0)
{
// Inject loading chunks task
auto preLoadChunksTask = New<LoadAssetDataTask>(this, chunksToPreload);
preLoadChunksTask->ContinueWith(loadTask);
loadTask = preLoadChunksTask;
}
// Before asset loading we have to initialize storage
// TODO: maybe in build game we could do it in place?
// This step is only for opening asset files in background and upgrading them
// In build game we have only a few packages which are ready to use
auto initTask = New<InitAssetTask>(this);
initTask->ContinueWith(loadTask);
loadTask = initTask;
return loadTask;
}
Asset::LoadResult BinaryAsset::loadAsset()
{
// Ensure that asset has been initialized
ASSERT(Storage && _header.ID.IsValid() && _header.TypeName.HasChars());
auto lock = Storage->Lock();
auto chunksToPreload = getChunksToPreload();
if (chunksToPreload != 0)
{
// Ensure that any chunks that were requested before are loaded in memory (in case streaming flushed them out after timeout)
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
{
const auto chunk = _header.Chunks[i];
if (GET_CHUNK_FLAG(i) & chunksToPreload && chunk && chunk->IsMissing())
Storage->LoadAssetChunk(chunk);
}
}
const LoadResult result = load();
#if !BUILD_RELEASE
if (result == LoadResult::MissingDataChunk)
{
// Provide more insights on potentially missing asset data chunk
Char chunksBitMask[ASSET_FILE_DATA_CHUNKS + 1];
Char chunksExistBitMask[ASSET_FILE_DATA_CHUNKS + 1];
Char chunksLoadBitMask[ASSET_FILE_DATA_CHUNKS + 1];
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
{
if (const FlaxChunk* chunk = _header.Chunks[i])
{
chunksBitMask[i] = '1';
chunksExistBitMask[i] = chunk->ExistsInFile() ? '1' : '0';
chunksLoadBitMask[i] = chunk->IsLoaded() ? '1' : '0';
}
else
{
chunksBitMask[i] = chunksExistBitMask[i] = chunksLoadBitMask[i] = '0';
}
}
chunksBitMask[ASSET_FILE_DATA_CHUNKS] = chunksExistBitMask[ASSET_FILE_DATA_CHUNKS] = chunksLoadBitMask[ASSET_FILE_DATA_CHUNKS] = 0;
LOG(Warning, "Asset reports missing data chunk. Chunks bitmask: {}, existing chunks: {} loaded chunks: {}. '{}'", chunksBitMask, chunksExistBitMask, chunksLoadBitMask, ToString());
}
#endif
return result;
}
void BinaryAsset::releaseStorage()
{
#if USE_EDITOR
// Close file
if (Storage)
Storage->CloseFileHandles();
#endif
}
#if USE_EDITOR
void BinaryAsset::onRename(const StringView& newPath)
{
ScopeLock lock(Locker);
// We don't support packages now
ASSERT(!Storage->IsPackage() && Storage->AllowDataModifications() && Storage->GetEntriesCount() == 1);
// Rename storage
Storage->OnRename(newPath);
}
#endif