400 lines
17 KiB
C++
400 lines
17 KiB
C++
#include "AssetsPreloader.hpp"
|
||
#include "Common/Abstract.hpp"
|
||
#include "Common/TexturePipelineProgram.hpp"
|
||
#include "sha2.hpp"
|
||
#include <atomic>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include <unordered_set>
|
||
#include <utility>
|
||
|
||
namespace LV {
|
||
|
||
static TOS::Logger LOG = "AssetsPreloader";
|
||
|
||
static ResourceFile readFileBytes(const fs::path& path) {
|
||
std::ifstream file(path, std::ios::binary);
|
||
if(!file)
|
||
throw std::runtime_error("Не удалось открыть файл: " + path.string());
|
||
|
||
file.seekg(0, std::ios::end);
|
||
std::streamoff size = file.tellg();
|
||
if(size < 0)
|
||
size = 0;
|
||
file.seekg(0, std::ios::beg);
|
||
|
||
ResourceFile out;
|
||
out.Data.resize(static_cast<size_t>(size));
|
||
if(size > 0) {
|
||
file.read(reinterpret_cast<char*>(out.Data.data()), size);
|
||
if (!file)
|
||
throw std::runtime_error("Не удалось прочитать файл: " + path.string());
|
||
}
|
||
|
||
out.calcHash();
|
||
return out;
|
||
}
|
||
|
||
static std::u8string readOptionalMeta(const fs::path& path) {
|
||
fs::path metaPath = path;
|
||
metaPath += ".meta";
|
||
if(!fs::exists(metaPath) || !fs::is_regular_file(metaPath))
|
||
return {};
|
||
|
||
ResourceFile meta = readFileBytes(metaPath);
|
||
return std::move(meta.Data);
|
||
}
|
||
|
||
AssetsPreloader::AssetsPreloader() {
|
||
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); type++) {
|
||
ResourceLinks[type].emplace_back(
|
||
ResourceFile::Hash_t{0},
|
||
ResourceHeader(),
|
||
fs::file_time_type(),
|
||
fs::path{""},
|
||
false
|
||
);
|
||
}
|
||
}
|
||
|
||
AssetsPreloader::Out_checkAndPrepareResourcesUpdate AssetsPreloader::checkAndPrepareResourcesUpdate(
|
||
const AssetsRegister& instances,
|
||
const std::function<ResourceId(EnumAssets type, std::string_view domain, std::string_view key)>& idResolver,
|
||
const std::function<void(std::u8string&& resource, ResourceFile::Hash_t hash, fs::path resPath)>& onNewResourceParsed,
|
||
ReloadStatus* status
|
||
) {
|
||
assert(idResolver);
|
||
|
||
#ifndef NDEBUG
|
||
bool expected = false;
|
||
assert(_Reloading.compare_exchange_strong(expected, true) && "Двойной вызов reloadResources");
|
||
struct ReloadGuard {
|
||
std::atomic<bool>& Flag;
|
||
~ReloadGuard() { Flag.exchange(false); }
|
||
} guard{_Reloading};
|
||
#endif
|
||
|
||
try {
|
||
ReloadStatus secondStatus;
|
||
return _checkAndPrepareResourcesUpdate(instances, idResolver, onNewResourceParsed, status ? *status : secondStatus);
|
||
} catch(const std::exception& exc) {
|
||
LOG.error() << exc.what();
|
||
assert(!"reloadResources: здесь не должно быть ошибок");
|
||
std::unreachable();
|
||
} catch(...) {
|
||
assert(!"reloadResources: здесь не должно быть ошибок");
|
||
std::unreachable();
|
||
}
|
||
}
|
||
|
||
AssetsPreloader::Out_checkAndPrepareResourcesUpdate AssetsPreloader::_checkAndPrepareResourcesUpdate(
|
||
const AssetsRegister& instances,
|
||
const std::function<ResourceId(EnumAssets type, std::string_view domain, std::string_view key)>& idResolver,
|
||
const std::function<void(std::u8string&& resource, ResourceFile::Hash_t hash, fs::path resPath)>& onNewResourceParsed,
|
||
ReloadStatus& status
|
||
) {
|
||
// 1) Поиск всех ресурсов и построение конечной карты ресурсов (timestamps, path, name, size)
|
||
// Карта найденных ресурсов
|
||
std::array<
|
||
std::unordered_map<
|
||
std::string, // Domain
|
||
std::unordered_map<
|
||
std::string,
|
||
ResourceFindInfo,
|
||
detail::TSVHash,
|
||
detail::TSVEq
|
||
>,
|
||
detail::TSVHash,
|
||
detail::TSVEq
|
||
>,
|
||
static_cast<size_t>(AssetType::MAX_ENUM)
|
||
> resourcesFirstStage;
|
||
|
||
for(const fs::path& instance : instances.Assets) {
|
||
try {
|
||
if(fs::is_regular_file(instance)) {
|
||
// Может архив
|
||
/// TODO: пока не поддерживается
|
||
} else if(fs::is_directory(instance)) {
|
||
// Директория
|
||
fs::path assetsRoot = instance;
|
||
fs::path assetsCandidate = instance / "assets";
|
||
if (fs::exists(assetsCandidate) && fs::is_directory(assetsCandidate))
|
||
assetsRoot = assetsCandidate;
|
||
|
||
// Директория assets существует, перебираем домены в ней
|
||
for(auto begin = fs::directory_iterator(assetsRoot), end = fs::directory_iterator(); begin != end; begin++) {
|
||
if(!begin->is_directory())
|
||
continue;
|
||
|
||
fs::path domainPath = begin->path();
|
||
std::string domain = domainPath.filename().string();
|
||
|
||
// Перебираем по типу ресурса
|
||
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); ++type) {
|
||
AssetType assetType = static_cast<AssetType>(type);
|
||
fs::path assetPath = domainPath / EnumAssetsToDirectory(assetType);
|
||
if (!fs::exists(assetPath) || !fs::is_directory(assetPath))
|
||
continue;
|
||
|
||
std::unordered_map<
|
||
std::string, // Key
|
||
ResourceFindInfo, // ResourceInfo,
|
||
detail::TSVHash,
|
||
detail::TSVEq
|
||
>& firstStage = resourcesFirstStage[static_cast<size_t>(assetType)][domain];
|
||
|
||
// Исследуем все ресурсы одного типа
|
||
for(auto begin = fs::recursive_directory_iterator(assetPath), end = fs::recursive_directory_iterator(); begin != end; begin++) {
|
||
if(begin->is_directory())
|
||
continue;
|
||
|
||
fs::path file = begin->path();
|
||
if(assetType == AssetType::Texture && file.extension() == ".meta")
|
||
continue;
|
||
|
||
std::string key = fs::relative(file, assetPath).generic_string();
|
||
if(firstStage.contains(key))
|
||
continue;
|
||
|
||
fs::file_time_type timestamp = fs::last_write_time(file);
|
||
if(assetType == AssetType::Texture) {
|
||
fs::path metaPath = file;
|
||
metaPath += ".meta";
|
||
if (fs::exists(metaPath) && fs::is_regular_file(metaPath)) {
|
||
auto metaTime = fs::last_write_time(metaPath);
|
||
if (metaTime > timestamp)
|
||
timestamp = metaTime;
|
||
}
|
||
}
|
||
|
||
// Работаем с ресурсом
|
||
firstStage[key] = ResourceFindInfo{
|
||
.Path = file,
|
||
.Timestamp = timestamp,
|
||
.Id = idResolver(assetType, domain, key)
|
||
};
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
throw std::runtime_error("Неизвестный тип инстанса медиаресурсов");
|
||
}
|
||
} catch (const std::exception& exc) {
|
||
/// TODO: Логгировать в статусе
|
||
}
|
||
}
|
||
|
||
// Функция парсинга ресурсов
|
||
auto buildResource = [&](AssetType type, std::string_view domain, std::string_view key, const ResourceFindInfo& info) -> PendingResource {
|
||
PendingResource out;
|
||
out.Key = key;
|
||
out.Timestamp = info.Timestamp;
|
||
|
||
std::function<uint32_t(const std::string_view)> modelResolver
|
||
= [&](const std::string_view model) -> uint32_t
|
||
{
|
||
auto [mDomain, mKey] = parseDomainKey(model, domain);
|
||
return idResolver(AssetType::Model, mDomain, mKey);
|
||
};
|
||
|
||
std::function<std::optional<uint32_t>(std::string_view)> textureIdResolver
|
||
= [&](std::string_view texture) -> std::optional<uint32_t>
|
||
{
|
||
auto [mDomain, mKey] = parseDomainKey(texture, domain);
|
||
return idResolver(AssetType::Texture, mDomain, mKey);
|
||
};
|
||
|
||
std::function<std::vector<uint8_t>(const std::string_view)> textureResolver
|
||
= [&](const std::string_view texturePipelineSrc) -> std::vector<uint8_t>
|
||
{
|
||
TexturePipelineProgram tpp;
|
||
bool flag = tpp.compile(texturePipelineSrc);
|
||
if(!flag)
|
||
return {};
|
||
|
||
tpp.link(textureIdResolver);
|
||
|
||
return tpp.toBytes();
|
||
};
|
||
|
||
if (type == AssetType::Nodestate) {
|
||
ResourceFile file = readFileBytes(info.Path);
|
||
std::string_view view(reinterpret_cast<const char*>(file.Data.data()), file.Data.size());
|
||
js::object obj = js::parse(view).as_object();
|
||
|
||
HeadlessNodeState hns;
|
||
out.Header = hns.parse(obj, modelResolver);
|
||
out.Resource = hns.dump();
|
||
out.Hash = sha2::sha256((const uint8_t*) out.Resource.data(), out.Resource.size());
|
||
} else if (type == AssetType::Model) {
|
||
const std::string ext = info.Path.extension().string();
|
||
if (ext == ".json") {
|
||
ResourceFile file = readFileBytes(info.Path);
|
||
std::string_view view(reinterpret_cast<const char*>(file.Data.data()), file.Data.size());
|
||
js::object obj = js::parse(view).as_object();
|
||
|
||
HeadlessModel hm;
|
||
out.Header = hm.parse(obj, modelResolver, textureResolver);
|
||
out.Resource = hm.dump();
|
||
out.Hash = sha2::sha256((const uint8_t*) out.Resource.data(), out.Resource.size());
|
||
// } else if (ext == ".gltf" || ext == ".glb") {
|
||
// /// TODO: добавить поддержку gltf
|
||
// ResourceFile file = readFileBytes(info.Path);
|
||
// out.Resource = std::make_shared<std::vector<uint8_t>>(std::move(file.Data));
|
||
// out.Hash = file.Hash;
|
||
} else {
|
||
throw std::runtime_error("Не поддерживаемый формат модели: " + info.Path.string());
|
||
}
|
||
} else if (type == AssetType::Texture) {
|
||
ResourceFile file = readFileBytes(info.Path);
|
||
out.Resource = std::move(file.Data);
|
||
out.Hash = file.Hash;
|
||
out.Header = readOptionalMeta(info.Path);
|
||
} else {
|
||
ResourceFile file = readFileBytes(info.Path);
|
||
out.Resource = std::move(file.Data);
|
||
out.Hash = file.Hash;
|
||
}
|
||
|
||
out.Id = idResolver(type, domain, key);
|
||
|
||
return out;
|
||
};
|
||
|
||
// 2) Определяем какие ресурсы изменились (новый timestamp) или новые ресурсы
|
||
Out_checkAndPrepareResourcesUpdate result;
|
||
|
||
// Собираем идентификаторы, чтобы потом определить какие ресурсы пропали
|
||
std::array<
|
||
std::unordered_set<ResourceId>,
|
||
static_cast<size_t>(EnumAssets::MAX_ENUM)
|
||
> uniqueExists;
|
||
|
||
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); ++type) {
|
||
auto& uniqueExistsTypes = uniqueExists[type];
|
||
const auto& resourceLinksTyped = ResourceLinks[type];
|
||
result.MaxNewSize[type] = resourceLinksTyped.size();
|
||
|
||
{
|
||
size_t allIds = 0;
|
||
for(const auto& [domain, keys] : resourcesFirstStage[type])
|
||
allIds += keys.size();
|
||
|
||
uniqueExistsTypes.reserve(allIds);
|
||
}
|
||
|
||
for(const auto& [domain, keys] : resourcesFirstStage[type]) {
|
||
for(const auto& [key, res] : keys) {
|
||
uniqueExistsTypes.insert(res.Id);
|
||
|
||
if(res.Id >= resourceLinksTyped.size() || !resourceLinksTyped[res.Id].IsExist)
|
||
{ // Если идентификатора нет в таблице или ресурс не привязан
|
||
PendingResource resource = buildResource(static_cast<AssetType>(type), domain, key, res);
|
||
if(onNewResourceParsed)
|
||
onNewResourceParsed(std::move(resource.Resource), resource.Hash, res.Path);
|
||
result.HashToPathNew[resource.Hash].push_back(res.Path);
|
||
|
||
if(res.Id >= result.MaxNewSize[type])
|
||
result.MaxNewSize[type] = res.Id+1;
|
||
|
||
result.ResourceUpdates[type].emplace_back(res.Id, resource.Hash, std::move(resource.Header), resource.Timestamp, res.Path);
|
||
} else if(resourceLinksTyped[res.Id].Path != res.Path
|
||
|| resourceLinksTyped[res.Id].LastWrite != res.Timestamp
|
||
) { // Если ресурс теперь берётся с другого места или изменилось время изменения файла
|
||
const auto& lastResource = resourceLinksTyped[res.Id];
|
||
PendingResource resource = buildResource(static_cast<AssetType>(type), domain, key, res);
|
||
|
||
if(lastResource.Hash != resource.Hash) {
|
||
// Хэш изменился
|
||
// Сообщаем о новом ресурсе
|
||
if(onNewResourceParsed)
|
||
onNewResourceParsed(std::move(resource.Resource), resource.Hash, res.Path);
|
||
// Старый хэш более не доступен по этому расположению.
|
||
result.HashToPathLost[lastResource.Hash].push_back(resourceLinksTyped[res.Id].Path);
|
||
// Новый хеш стал доступен по этому расположению.
|
||
result.HashToPathNew[resource.Hash].push_back(res.Path);
|
||
} else if(resourceLinksTyped[res.Id].Path != res.Path) {
|
||
// Изменился конечный путь.
|
||
// Хэш более не доступен по этому расположению.
|
||
result.HashToPathLost[resource.Hash].push_back(resourceLinksTyped[res.Id].Path);
|
||
// Хеш теперь доступен по этому расположению.
|
||
result.HashToPathNew[resource.Hash].push_back(res.Path);
|
||
} else {
|
||
// Ресурс без заголовка никак не изменился.
|
||
}
|
||
|
||
// Чтобы там не поменялось, мог поменятся заголовок. Уведомляем о новой привязке.
|
||
result.ResourceUpdates[type].emplace_back(res.Id, resource.Hash, std::move(resource.Header), resource.Timestamp, res.Path);
|
||
} else {
|
||
// Ресурс не изменился
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3) Определяем какие ресурсы пропали
|
||
for(size_t type = 0; type < static_cast<size_t>(AssetType::MAX_ENUM); ++type) {
|
||
const auto& resourceLinksTyped = ResourceLinks[type];
|
||
|
||
size_t counter = 0;
|
||
for(const auto& [hash, header, timestamp, path, isExist] : resourceLinksTyped) {
|
||
size_t id = counter++;
|
||
if(!isExist)
|
||
continue;
|
||
|
||
if(uniqueExists[type].contains(id))
|
||
continue;
|
||
|
||
// Ресурс потерян
|
||
// Хэш более не доступен по этому расположению.
|
||
result.HashToPathLost[hash].push_back(path);
|
||
result.LostLinks[type].push_back(id);
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
AssetsPreloader::Out_applyResourcesUpdate AssetsPreloader::applyResourcesUpdate(const Out_checkAndPrepareResourcesUpdate& orr) {
|
||
Out_applyResourcesUpdate result;
|
||
|
||
for(size_t type = 0; type < static_cast<size_t>(EnumAssets::MAX_ENUM); ++type) {
|
||
// Затираем потерянные
|
||
for(ResourceId id : orr.LostLinks[type]) {
|
||
assert(id < ResourceLinks[type].size());
|
||
auto& [hash, header, timestamp, path, isExist] = ResourceLinks[type][id];
|
||
hash = {0};
|
||
header = {};
|
||
timestamp = fs::file_time_type();
|
||
path.clear();
|
||
isExist = false;
|
||
|
||
result.NewOrUpdates[type].emplace_back(id, hash, header);
|
||
}
|
||
|
||
// Увеличиваем размер, если необходимо
|
||
if(orr.MaxNewSize[type] > ResourceLinks[type].size()) {
|
||
ResourceLink def{
|
||
ResourceFile::Hash_t{0},
|
||
ResourceHeader(),
|
||
fs::file_time_type(),
|
||
fs::path{""},
|
||
false
|
||
};
|
||
|
||
ResourceLinks[type].resize(orr.MaxNewSize[type], def);
|
||
}
|
||
|
||
// Обновляем / добавляем
|
||
for(auto& [id, hash, header, timestamp, path] : orr.ResourceUpdates[type]) {
|
||
ResourceLinks[type][id] = {hash, std::move(header), timestamp, std::move(path), true};
|
||
result.NewOrUpdates[type].emplace_back(id, hash, header);
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
}
|