Files
LuaVox/Src/Common/AssetsPreloader.cpp

400 lines
17 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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;
}
}