diff --git a/Src/Common/AssetsPreloader.cpp b/Src/Common/AssetsPreloader.cpp new file mode 100644 index 0000000..de0af42 --- /dev/null +++ b/Src/Common/AssetsPreloader.cpp @@ -0,0 +1,424 @@ +#include "AssetsPreloader.hpp" +#include +#include +#include + +namespace LV { + +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)); + if(size > 0) { + file.read(reinterpret_cast(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() { + std::fill(NextId.begin(), NextId.end(), 1); + std::fill(LastSendId.begin(), LastSendId.end(), 1); +} + +AssetsPreloader::Out_reloadResources AssetsPreloader::reloadResources(const AssetsRegister& instances, ReloadStatus* status) { + bool expected = false; + assert(_Reloading.compare_exchange_strong(expected, true) && "Двойной вызов reloadResources"); + struct ReloadGuard { + std::atomic& Flag; + ~ReloadGuard() { Flag.exchange(false); } + } guard{_Reloading}; + + try { + ReloadStatus secondStatus; + return _reloadResources(instances, status ? *status : secondStatus); + } catch(...) { + assert(!"reloadResources: здесь не должно быть ошибок"); + std::unreachable(); + } +} + +AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const AssetsRegister& instances, ReloadStatus& status) { + Out_reloadResources result; + + // 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(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(AssetType::MAX_ENUM); ++type) { + AssetType assetType = static_cast(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(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).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 + }; + } + } + } + } 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 modelResolver + = [&](const std::string_view model) -> uint32_t + { + auto [mDomain, mKey] = parseDomainKey(model, domain); + return getId(AssetType::Model, mDomain, mKey); + }; + + std::function(std::string_view)> textureIdResolver + = [&](std::string_view texture) -> std::optional + { + auto [mDomain, mKey] = parseDomainKey(texture, domain); + return getId(AssetType::Texture, mDomain, mKey); + }; + + std::function(const std::string_view)> textureResolver + = [&](const std::string_view texturePipelineSrc) -> std::vector + { + TexturePipelineProgram tpp; + bool flag = tpp.compile((std::string) 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(file.Data.data()), file.Data.size()); + js::object obj = js::parse(view).as_object(); + + HeadlessNodeState hns; + out.Header = hns.parse(obj, modelResolver); + out.Resource = std::make_shared(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(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 = std::make_shared(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::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::make_shared(std::move(file.Data)); + out.Hash = file.Hash; + out.Header = readOptionalMeta(info.Path); + } else { + ResourceFile file = readFileBytes(info.Path); + out.Resource = std::make_shared(std::move(file.Data)); + out.Hash = file.Hash; + } + + out.Id = getId(type, domain, key); + + return out; + }; + + // 2) Обрабатываться будут только изменённые (новый timestamp) или новые ресурсы + // Определяем каких ресурсов не стало + for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); ++type) { + auto& tableResourcesFirstStage = resourcesFirstStage[type]; + for(const auto& [id, resource] : MediaResources[type]) { + if(tableResourcesFirstStage.empty()) { + result.Lost[type][resource.Domain].push_back(resource.Key); + continue; + } + + auto iterDomain = tableResourcesFirstStage.find(resource.Domain); + if(iterDomain == tableResourcesFirstStage.end()) { + result.Lost[type][resource.Domain].push_back(resource.Key); + continue; + } + + if(!iterDomain->second.contains(resource.Key)) { + result.Lost[type][resource.Domain].push_back(resource.Key); + } + } + } + + // Определение новых или изменённых ресурсов + for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); ++type) { + for(const auto& [domain, table] : resourcesFirstStage[type]) { + auto iterTableDomain = DKToId[type].find(domain); + if(iterTableDomain == DKToId[type].end()) { + // Домен неизвестен движку, все ресурсы в нём новые + for(const auto& [key, info] : table) { + PendingResource resource = buildResource(static_cast(type), domain, key, info); + result.NewOrChange[(int) type][domain].push_back(std::move(resource)); + } + } else { + for(const auto& [key, info] : table) { + bool needsUpdate = true; + if(auto iterKey = iterTableDomain->second.find(key); iterKey != iterTableDomain->second.end()) { + // Идентификатор найден + auto iterRes = MediaResources[type].find(iterKey->second); + // Если нашли ресурс по идентификатору и время изменения не поменялось, то он не новый и не изменился + if(iterRes != MediaResources[type].end() && iterRes->second.Timestamp == info.Timestamp) + needsUpdate = false; + } + + if(!needsUpdate) + continue; + + PendingResource resource = buildResource(static_cast(type), domain, key, info); + result.NewOrChange[(int) type][domain].push_back(std::move(resource)); + } + } + } + } + + return result; +} + +AssetsPreloader::Out_applyResourceChange AssetsPreloader::applyResourceChange(const Out_reloadResources& orr) { + Out_applyResourceChange result; + + // Удаляем ресурсы + /* + Удаляются только ресурсы, при этом за ними остаётся бронь на идентификатор + Уже скомпилированные зависимости к ресурсам не будут + перекомпилироваться для смены идентификатора. + Если нужный ресурс появится, то привязка останется. + Новые клиенты не получат ресурс которого нет, + но он может использоваться + */ + for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); type++) { + for(const auto& [domain, keys] : orr.Lost[type]) { + auto iterDomain = DKToId[type].find(domain); + + // Если уже было решено, что ресурсы были, и стали потерянными, то так и должно быть + assert(iterDomain != DKToId[type].end()); + + for(const auto& key : keys) { + auto iterKey = iterDomain->second.find(key); + + // Ресурс был и должен быть + assert(iterKey != iterDomain->second.end()); + + uint32_t id = iterKey->second; + auto& resType = MediaResources[type]; + auto iterRes = resType.find(id); + if(iterRes == resType.end()) + continue; + + // Ресурс был потерян + result.Lost[type].push_back(id); + // Hash более нам неизвестен + HashToId.erase(iterRes->second.Hash); + // Затираем ресурс + resType.erase(iterRes); + } + } + } + + // Добавляем + for(int type = 0; type < (int) AssetType::MAX_ENUM; type++) { + auto& typeTable = DKToId[type]; + for(const auto& [domain, resources] : orr.NewOrChange[type]) { + auto& domainTable = typeTable[domain]; + for(const PendingResource& pending : resources) { + MediaResource resource { + .Domain = domain, + .Key = std::move(pending.Key), + .Timestamp = pending.Timestamp, + .Resource = std::move(pending.Resource), + .Hash = pending.Hash, + .Header = std::move(pending.Header) + }; + + auto& table = MediaResources[type]; + // Нужно затереть старую ссылку хеша на данный ресурс + if(auto iter = table.find(pending.Id); iter != table.end()) + HashToId.erase(iter->second.Hash); + + // Добавили ресурс + table[pending.Id] = resource; + // Связали с хешем + HashToId[resource.Hash] = {static_cast(type), pending.Id}; + // Осведомили о новом/изменённом ресурсе + result.NewOrChange[type].push_back({pending.Id, std::move(resource)}); + } + } + + // Не должно быть ресурсов, которые были помечены как потерянные + #ifndef NDEBUG + std::unordered_set changed; + for(const auto& [id, _] : result.NewOrChange[type]) + changed.insert(id); + + auto& lost = result.Lost[type]; + for(auto iter : lost) + assert(!changed.contains(iter)); + #endif + } + + return result; +} + +AssetsPreloader::Out_bakeId AssetsPreloader::bakeIdTables() { + #ifndef NDEBUG + + assert(!DKToIdInBakingMode); + DKToIdInBakingMode = true; + struct _tempStruct { + AssetsPreloader* handler; + ~_tempStruct() { handler->DKToIdInBakingMode = false; } + } _lock{this}; + + #endif + + Out_bakeId result; + + for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); ++type) { + // домен+ключ -> id + { + auto lock = NewDKToId[type].lock(); + auto& dkToId = DKToId[type]; + for(auto& [domain, keys] : *lock) { + // Если домен не существует, просто воткнёт новые ключи + auto [iterDomain, inserted] = dkToId.try_emplace(domain, std::move(keys)); + if(!inserted) { + // Домен уже существует, сливаем новые ключи + iterDomain->second.merge(keys); + } + } + + lock->clear(); + } + + // id -> домен+ключ + { + auto lock = NewIdToDK[type].lock(); + + auto& idToDK = IdToDK[type]; + result.IdToDK[type] = std::move(*lock); + lock->clear(); + idToDK.append_range(result.IdToDK[type]); + + // result.LastSendId[type] = LastSendId[type]; + LastSendId[type] = NextId[type]; + } + } + + return result; +} + +} \ No newline at end of file diff --git a/Src/Common/AssetsPreloader.hpp b/Src/Common/AssetsPreloader.hpp index e8fb3fb..faa79f9 100644 --- a/Src/Common/AssetsPreloader.hpp +++ b/Src/Common/AssetsPreloader.hpp @@ -4,27 +4,21 @@ #include #include #include -#include #include #include -#include #include #include -#include #include #include #include #include -#include #include #include #include "Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp" #include "Common/Abstract.hpp" #include "Common/Async.hpp" #include "TOSAsync.hpp" -#include "boost/asio/executor.hpp" -#include "boost/asio/experimental/channel.hpp" -#include "boost/asio/this_coro.hpp" +#include "TOSLib.hpp" #include "sha2.hpp" /* @@ -52,6 +46,44 @@ static constexpr const char* EnumAssetsToDirectory(LV::EnumAssets value) { namespace LV { +namespace detail { + +// Позволяет использовать как std::string так и std::string_view в хэш таблицах +struct TSVHash { + using is_transparent = void; + + size_t operator()(std::string_view sv) const noexcept { + return std::hash{}(sv); + } + + size_t operator()(const std::string& s) const noexcept { + return std::hash{}(s); + } +}; + +// Позволяет использовать как std::string так и std::string_view в хэш таблицах +struct TSVEq { + using is_transparent = void; + + bool operator()(std::string_view a, std::string_view b) const noexcept { + return a == b; + } + + bool operator()(const std::string& a, std::string_view b) const noexcept { + return std::string_view(a) == b; + } + + bool operator()(std::string_view a, const std::string& b) const noexcept { + return a == std::string_view(b); + } + + bool operator()(const std::string& a, const std::string& b) const noexcept { + return a == b; + } +}; + +} + namespace fs = std::filesystem; using AssetType = EnumAssets; @@ -59,26 +91,28 @@ struct ResourceFile { using Hash_t = sha2::sha256_hash; // boost::uuids::detail::sha1::digest_type; Hash_t Hash; - std::vector Data; + std::u8string Data; void calcHash() { - Hash = sha2::sha256(Data.data(), Data.size()); + Hash = sha2::sha256((const uint8_t*) Data.data(), Data.size()); } }; -class AssetsPreloader : public TOS::IAsyncDestructible { +class AssetsPreloader { public: using Ptr = std::shared_ptr; - using IdTable = std::unordered_map< - AssetType, // Тип ресурса + using IdTable = std::unordered_map< std::string, // Domain std::unordered_map< std::string, // Key - uint32_t // ResourceId - > - > - >; + uint32_t, // ResourceId + detail::TSVHash, + detail::TSVEq + >, + detail::TSVHash, + detail::TSVEq + >; // /* @@ -91,29 +125,27 @@ public: fs::file_time_type Timestamp; // Обезличенный ресурс - std::shared_ptr> Resource; + std::shared_ptr Resource; // Хэш ресурса ResourceFile::Hash_t Hash; // Скомпилированный заголовок - std::vector Dependencies; - }; - - struct PendingDependency { - std::string Domain, Key; + std::u8string Header; }; struct PendingResource { - std::string Domain, Key; + uint32_t Id; + std::string Key; fs::file_time_type Timestamp; - std::shared_ptr> Resource; + // Обезличенный ресурс + std::shared_ptr Resource; + // Его хеш ResourceFile::Hash_t Hash; - std::vector ModelDeps; - std::vector TextureDeps; - std::vector Extra; + // Заголовок + std::u8string Header; }; - struct ResourceChangeObj { + struct Out_reloadResources { std::unordered_map> Lost[(int) AssetType::MAX_ENUM]; std::unordered_map> NewOrChange[(int) AssetType::MAX_ENUM]; }; @@ -123,6 +155,11 @@ public: std::vector> NewOrChange[(int) AssetType::MAX_ENUM]; }; + struct Out_bakeId { + // Новые привязки + std::array>, static_cast(AssetType::MAX_ENUM)> IdToDK; + }; + struct ReloadStatus { /// TODO: callback'и для обновления статусов /// TODO: многоуровневый статус std::vector. Этапы/Шаги/Объекты @@ -135,20 +172,23 @@ public: Тот файл, что был загружен раньше и будет использоваться */ std::vector Assets; + /* У этих ресурсов приоритет выше, если их удастся получить, то использоваться будут именно они Domain -> {key + data} */ - std::unordered_map> Custom[(int) AssetType::MAX_ENUM]; + std::array< + std::unordered_map< + std::string, + std::unordered_map + >, + static_cast(AssetType::MAX_ENUM) + > Custom; }; public: - static coro Create(asio::io_context& ioc); - explicit AssetsPreloader(asio::io_context& ioc) - : TOS::IAsyncDestructible(ioc) - { - } + AssetsPreloader(); ~AssetsPreloader() = default; AssetsPreloader(const AssetsPreloader&) = delete; @@ -156,79 +196,55 @@ public: AssetsPreloader& operator=(const AssetsPreloader&) = delete; AssetsPreloader& operator=(AssetsPreloader&&) = delete; - // Пересматривает ресурсы и выдаёт изменения. - // Одновременно можно работать только один такой вызов. - // instances -> пути к директории с assets или архивы с assets внутри. От низшего приоритета к высшему. - // status -> обратный отклик о процессе обновления ресурсов. - // ReloadStatus <- новые и потерянные ресурсы. - coro reloadResources(const std::vector& instances, ReloadStatus* status = nullptr) { - bool expected = false; - assert(Reloading_.compare_exchange_strong(expected, true) && "Двойной вызов reloadResources"); - struct ReloadGuard { - std::atomic& Flag; - ~ReloadGuard() { Flag.exchange(false); } - } guard{Reloading_}; - - try { - ReloadStatus secondStatus; - co_return co_await _reloadResources(instances, status ? *status : secondStatus); - } catch(...) { - assert(!"reloadResources: здесь не должно быть ошибок"); - } - } - /* Перепроверка изменений ресурсов по дате изменения, пересчёт хешей. Обнаруженные изменения должны быть отправлены всем клиентам. Ресурсы будут обработаны в подходящий формат и сохранены в кеше. - Одновременно может выполнятся только одна такая функция - Используется в GameServer + Используется в GameServer. + ! Одновременно можно работать только один такой вызов. + ! Бронирует идентификаторы используя getId(); + + instances -> пути к директории с assets или архивы с assets внутри. От низшего приоритета к высшему. + status -> обратный отклик о процессе обновления ресурсов. + ReloadStatus <- новые и потерянные ресурсы. */ - coro recheckResources(const AssetsRegister& info) { - return reloadResources(info.Assets); - } - - // Синхронный вызов reloadResources - ResourceChangeObj reloadResourcesSync(const std::vector& instances, ReloadStatus* status = nullptr) { - asio::io_context ioc; - std::optional result; - std::exception_ptr error; - - asio::co_spawn(ioc, [this, &instances, status, &result, &error]() -> coro<> { - try { - ReloadStatus localStatus; - result = co_await reloadResources(instances, status ? status : &localStatus); - } catch(...) { - error = std::current_exception(); - } - co_return; - }, asio::detached); - - ioc.run(); - - if (error) - std::rethrow_exception(error); - if (!result) - return {}; - return std::move(*result); - } - - // Синхронный вызов recheckResources - ResourceChangeObj recheckResourcesSync(const AssetsRegister& info, ReloadStatus* status = nullptr) { - return reloadResourcesSync(info.Assets, status); - } + Out_reloadResources reloadResources(const AssetsRegister& instances, ReloadStatus* status = nullptr); /* Применяет расчитанные изменения. - Раздаёт идентификаторы ресурсам и записывает их в таблицу + + Out_applyResourceChange <- Нужно отправить клиентам новые привязки ресурсов + id -> hash+header */ - Out_applyResourceChange applyResourceChange(const ResourceChangeObj& orr); + Out_applyResourceChange applyResourceChange(const Out_reloadResources& orr); /* - Выдаёт идентификатор ресурса, даже если он не существует или был удалён. - resource должен содержать домен и путь + Выдаёт идентификатор ресурса. + Многопоточно. + Иногда нужно вызывать bakeIdTables чтобы оптимизировать таблицы + идентификаторов. При этом никто не должен использовать getId */ - ResourceId getId(AssetType type, const std::string& domain, const std::string& key); + ResourceId getId(AssetType type, std::string_view domain, std::string_view key); + + /* + Оптимизирует таблицы идентификаторов. + Нельзя использовать пока есть вероятность что кто-то использует getId(). + Такжке нельзя при выполнении reloadResources(). + + Out_bakeId <- Нужно отправить подключенным клиентам новые привязки id -> домен+ключ + */ + Out_bakeId bakeIdTables(); + + /* + Выдаёт пакет со всеми текущими привязками id -> домен+ключ. + Используется при подключении новых клиентов. + */ + void makeGlobalLinkagePacket() { + /// TODO: Собрать пакет с IdToDK и сжать его домены и ключи и id -> hash+header + + // Тот же пакет для обновления идентификаторов + std::unreachable(); + } // Выдаёт ресурс по идентификатору const MediaResource* getResource(AssetType type, uint32_t id) const; @@ -241,139 +257,13 @@ public: getNodeDependency(const std::string& domain, const std::string& key); private: - struct ResourceFirstStageInfo { + struct ResourceFindInfo { // Путь к архиву (если есть), и путь до ресурса fs::path ArchivePath, Path; // Время изменения файла fs::file_time_type Timestamp; }; - struct ResourceSecondStageInfo : public ResourceFirstStageInfo { - // Обезличенный ресурс - std::shared_ptr> Resource; - ResourceFile::Hash_t Hash; - // Сырой заголовок - std::vector Dependencies; - }; - - // Текущее состояние reloadResources - std::atomic Reloading_ = false; - - struct HeaderWriter { - std::vector Data; - - void writeU8(uint8_t value) { - Data.push_back(value); - } - - void writeU32(uint32_t value) { - Data.push_back(uint8_t(value & 0xff)); - Data.push_back(uint8_t((value >> 8) & 0xff)); - Data.push_back(uint8_t((value >> 16) & 0xff)); - Data.push_back(uint8_t((value >> 24) & 0xff)); - } - - void writeBytes(const uint8_t* data, size_t size) { - if (size == 0) - return; - const uint8_t* ptr = data; - Data.insert(Data.end(), ptr, ptr + size); - } - }; - - 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)); - if (size > 0) { - file.read(reinterpret_cast(out.Data.data()), size); - if (!file) - throw std::runtime_error("Не удалось прочитать файл: " + path.string()); - } - - out.calcHash(); - return out; - } - - static std::vector toBytes(std::u8string_view data) { - std::vector out(data.size()); - if (!out.empty()) - std::memcpy(out.data(), data.data(), out.size()); - return out; - } - - // Dependency lists are ordered by placeholder index used in the resource data. - static std::vector buildHeader(AssetType type, const std::vector& modelDeps, const std::vector& textureDeps, const std::vector& extra) { - HeaderWriter writer; - writer.writeU8('a'); - writer.writeU8('h'); - writer.writeU8(1); - writer.writeU8(static_cast(type)); - - writer.writeU32(static_cast(modelDeps.size())); - for (uint32_t id : modelDeps) - writer.writeU32(id); - - writer.writeU32(static_cast(textureDeps.size())); - for (uint32_t id : textureDeps) - writer.writeU32(id); - - writer.writeU32(static_cast(extra.size())); - writer.writeBytes(extra.data(), extra.size()); - - return std::move(writer.Data); - } - - static std::vector 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); - } - - static std::optional findId(const IdTable& table, AssetType type, const std::string& domain, const std::string& key) { - auto iterType = table.find(type); - if (iterType == table.end()) - return std::nullopt; - auto iterDomain = iterType->second.find(domain); - if (iterDomain == iterType->second.end()) - return std::nullopt; - auto iterKey = iterDomain->second.find(key); - if (iterKey == iterDomain->second.end()) - return std::nullopt; - return iterKey->second; - } - - void ensureNextId(AssetType type) { - size_t index = static_cast(type); - if (NextIdInitialized[index]) - return; - - uint32_t maxId = 0; - auto iterType = DKToId.find(type); - if (iterType != DKToId.end()) { - for (const auto& [domain, keys] : iterType->second) { - for (const auto& [key, id] : keys) { - maxId = std::max(maxId, id + 1); - } - } - } - NextId[index] = maxId; - NextIdInitialized[index] = true; - } - struct HashHasher { std::size_t operator()(const ResourceFile::Hash_t& hash) const noexcept { std::size_t v = 14695981039346656037ULL; @@ -385,446 +275,119 @@ private: } }; - struct ParsedHeader { - AssetType Type = AssetType::MAX_ENUM; - std::vector ModelDeps; - std::vector TextureDeps; - std::vector Extra; - }; + // Текущее состояние reloadResources + std::atomic _Reloading = false; - static std::optional parseHeader(const std::vector& data) { - size_t pos = 0; - auto readU8 = [&](uint8_t& out) -> bool { - if (pos + 1 > data.size()) - return false; - out = data[pos++]; - return true; - }; - auto readU32 = [&](uint32_t& out) -> bool { - if (pos + 4 > data.size()) - return false; - out = uint32_t(data[pos]) | - (uint32_t(data[pos + 1]) << 8) | - (uint32_t(data[pos + 2]) << 16) | - (uint32_t(data[pos + 3]) << 24); - pos += 4; - return true; - }; + // Если идентификатор не найден в асинхронной таблице, переходим к работе с синхронной + ResourceId _getIdNew(AssetType type, std::string_view domain, std::string_view key); - ParsedHeader out; - uint8_t c0, c1, version, type; - if (!readU8(c0) || !readU8(c1) || !readU8(version) || !readU8(type)) - return std::nullopt; - if (c0 != 'a' || c1 != 'h' || version != 1) - return std::nullopt; - out.Type = static_cast(type); + Out_reloadResources _reloadResources(const AssetsRegister& instances, ReloadStatus& status); - uint32_t count = 0; - if (!readU32(count)) - return std::nullopt; - out.ModelDeps.reserve(count); - for (uint32_t i = 0; i < count; i++) { - uint32_t id; - if (!readU32(id)) - return std::nullopt; - out.ModelDeps.push_back(id); - } + #ifndef NDEBUG + // Для контроля за режимом слияния ключей + bool DKToIdInBakingMode = false; + #endif - if (!readU32(count)) - return std::nullopt; - out.TextureDeps.reserve(count); - for (uint32_t i = 0; i < count; i++) { - uint32_t id; - if (!readU32(id)) - return std::nullopt; - out.TextureDeps.push_back(id); - } + /* + Многопоточная таблица идентификаторов. Новые идентификаторы выделяются в NewDKToId, + и далее вливаются в основную таблицу при вызове bakeIdTables() + */ + std::array(AssetType::MAX_ENUM)> DKToId; + /* + Многопоточная таблица обратного резолва. + Идентификатор -> домен+ключ + */ + std::array>, static_cast(AssetType::MAX_ENUM)> IdToDK; - uint32_t extraSize = 0; - if (!readU32(extraSize)) - return std::nullopt; - if (pos + extraSize > data.size()) - return std::nullopt; - out.Extra.assign(data.begin() + pos, data.begin() + pos + extraSize); - return out; - } + /* + Таблица в которой выделяются новые идентификаторы, которых не нашлось в DKToId. + Данный объект одновременно может работать только с одним потоком. + */ + std::array, static_cast(AssetType::MAX_ENUM)> NewDKToId; + /* + Конец поля идентификаторов, известный клиентам. + Если NextId продвинулся дальше, нужно уведомить клиентов о новых привязках. + */ + std::array(AssetType::MAX_ENUM)> LastSendId; + /* + Списки в которых пишутся новые привязки. Начала спиской исходят из LastSendId. + Id + LastSendId -> домен+ключ + */ + std::array>>, static_cast(AssetType::MAX_ENUM)> NewIdToDK; - // Пересмотр ресурсов - coro _reloadResources(const std::vector& instances, ReloadStatus& status) const { - ResourceChangeObj result; - - // 1) Поиск всех ресурсов и построение конечной карты ресурсов (timestamps, path, name, size) - // Карта найденных ресурсов - std::unordered_map< - AssetType, // Тип ресурса - std::unordered_map< - std::string, // Domain - std::unordered_map< - std::string, // Key - ResourceFirstStageInfo // ResourceInfo - > - > - > resourcesFirstStage; - - for (const fs::path& instance : instances) { - 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; - - /// TODO: выглядит всё не очень асинхронно - co_await asio::post(co_await asio::this_coro::executor); - - fs::path domainPath = begin->path(); - std::string domain = domainPath.filename().string(); - - // Перебираем по типу ресурса - for (int type = 0; type < static_cast(AssetType::MAX_ENUM); type++) { - AssetType assetType = static_cast(type); - fs::path assetPath = domainPath / EnumAssetsToDirectory(assetType); - if (!fs::exists(assetPath) || !fs::is_directory(assetPath)) - continue; - - std::unordered_map< - std::string, // Key - ResourceFirstStageInfo // ResourceInfo - >& firstStage = resourcesFirstStage[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).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] = ResourceFirstStageInfo{ - .Path = file, - .Timestamp = timestamp - }; - } - } - } - } else { - throw std::runtime_error("Неизвестный тип инстанса медиаресурсов"); - } - } catch (const std::exception& exc) { - /// TODO: Логгировать в статусе - - } - } - - auto buildResource = [&](AssetType type, const std::string& domain, const std::string& key, const ResourceFirstStageInfo& info) -> PendingResource { - PendingResource out; - out.Domain = domain; - out.Key = key; - out.Timestamp = info.Timestamp; - - if (type == AssetType::Nodestate) { - ResourceFile file = readFileBytes(info.Path); - std::string_view view(reinterpret_cast(file.Data.data()), file.Data.size()); - js::object obj = js::parse(view).as_object(); - - PreparedNodeState pns(domain, obj); - pns.LocalToModel.reserve(pns.LocalToModelKD.size()); - uint32_t placeholder = 0; - for (const auto& [subDomain, subKey] : pns.LocalToModelKD) { - pns.LocalToModel.push_back(placeholder++); - out.ModelDeps.push_back({subDomain, subKey}); - } - - std::vector data = toBytes(pns.dump()); - out.Resource = std::make_shared>(std::move(data)); - out.Hash = sha2::sha256(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(file.Data.data()), file.Data.size()); - js::object obj = js::parse(view).as_object(); - - PreparedModel pm(domain, obj); - pm.CompiledTextures.clear(); - pm.CompiledTextures.reserve(pm.Textures.size()); - - std::unordered_map textureIndex; - auto getTexturePlaceholder = [&](const std::string& texDomain, const std::string& texKey) -> uint32_t { - std::string token; - token.reserve(texDomain.size() + texKey.size() + 1); - token.append(texDomain); - token.push_back(':'); - token.append(texKey); - - auto iter = textureIndex.find(token); - if (iter != textureIndex.end()) - return iter->second; - - uint32_t placeholderId = static_cast(out.TextureDeps.size()); - textureIndex.emplace(std::move(token), placeholderId); - out.TextureDeps.push_back({texDomain, texKey}); - return placeholderId; - }; - - for (const auto& [tkey, pipeline] : pm.Textures) { - TexturePipeline compiled; - - if (pipeline.IsSource) { - TexturePipelineProgram program; - std::string source(reinterpret_cast(pipeline.Pipeline.data()), pipeline.Pipeline.size()); - std::string err; - if (!program.compile(source, &err)) { - throw std::runtime_error("Ошибка компиляции pipeline: " + err); - } - - auto resolver = [&](std::string_view name) -> std::optional { - auto [texDomain, texKey] = parseDomainKey(std::string(name), domain); - return getTexturePlaceholder(texDomain, texKey); - }; - - if (!program.link(resolver, &err)) { - throw std::runtime_error("Ошибка линковки pipeline: " + err); - } - - const std::vector bytes = program.toBytes(); - compiled.Pipeline.resize(bytes.size()); - if (!bytes.empty()) { - std::memcpy(compiled.Pipeline.data(), bytes.data(), bytes.size()); - } - } else { - compiled.Pipeline = pipeline.Pipeline; - } - - for (const auto& [texDomain, texKey] : pipeline.Assets) { - uint32_t placeholderId = getTexturePlaceholder(texDomain, texKey); - compiled.BinTextures.push_back(placeholderId); - } - - pm.CompiledTextures[tkey] = std::move(compiled); - } - - for (const auto& sub : pm.SubModels) { - out.ModelDeps.push_back({sub.Domain, sub.Key}); - } - - std::vector data = toBytes(pm.dump()); - out.Resource = std::make_shared>(std::move(data)); - out.Hash = sha2::sha256(out.Resource->data(), out.Resource->size()); - } else if (ext == ".gltf" || ext == ".glb") { - ResourceFile file = readFileBytes(info.Path); - out.Resource = std::make_shared>(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::make_shared>(std::move(file.Data)); - out.Hash = file.Hash; - out.Extra = readOptionalMeta(info.Path); - } else { - ResourceFile file = readFileBytes(info.Path); - out.Resource = std::make_shared>(std::move(file.Data)); - out.Hash = file.Hash; - } - return out; - }; - - // 2) Обрабатываться будут только изменённые (новый timestamp) или новые ресурсы - - // Текстуры, шрифты, звуки хранить как есть - // У моделей, состояний нод, анимации, частиц обналичить зависимости - - for (const auto& [type, resources] : MediaResources) { - auto iterType = resourcesFirstStage.find(type); - for (const auto& [id, resource] : resources) { - if (iterType == resourcesFirstStage.end()) { - result.Lost[(int) type][resource.Domain].push_back(resource.Key); - continue; - } - - auto iterDomain = iterType->second.find(resource.Domain); - if (iterDomain == iterType->second.end()) { - result.Lost[(int) type][resource.Domain].push_back(resource.Key); - continue; - } - - if (!iterDomain->second.contains(resource.Key)) { - result.Lost[(int) type][resource.Domain].push_back(resource.Key); - } - } - } - - for (const auto& [type, domains] : resourcesFirstStage) { - for (const auto& [domain, table] : domains) { - for (const auto& [key, info] : table) { - bool needsUpdate = true; - if (auto existingId = findId(DKToId, type, domain, key)) { - auto iterType = MediaResources.find(type); - if (iterType != MediaResources.end()) { - auto iterRes = iterType->second.find(*existingId); - if (iterRes != iterType->second.end() && iterRes->second.Timestamp == info.Timestamp) { - needsUpdate = false; - } - } - } - - if (!needsUpdate) - continue; - - co_await asio::post(co_await asio::this_coro::executor); - PendingResource resource = buildResource(type, domain, key, info); - result.NewOrChange[(int) type][domain].push_back(std::move(resource)); - } - } - } - - co_return result; - } - - IdTable DKToId; - std::unordered_map> MediaResources; - std::unordered_map, HashHasher> HashToId; - std::array(AssetType::MAX_ENUM)> NextId = {}; - std::array(AssetType::MAX_ENUM)> NextIdInitialized = {}; + // Загруженные ресурсы + std::array, static_cast(AssetType::MAX_ENUM)> MediaResources; + // Hash -> ресурс + std::unordered_map, HashHasher> HashToId; + // Для последовательного выделения идентификаторов + std::array(AssetType::MAX_ENUM)> NextId; }; -inline AssetsPreloader::Out_applyResourceChange AssetsPreloader::applyResourceChange(const ResourceChangeObj& orr) { - Out_applyResourceChange result; +inline ResourceId AssetsPreloader::getId(AssetType type, std::string_view domain, std::string_view key) { + #ifndef NDEBUG + assert(!DKToIdInBakingMode); + #endif - // Удаляем ресурсы - /* - Удаляются только ресурсы, при этом за ними остаётся бронь на идентификатор - Уже скомпилированные зависимости к ресурсам не будут - перекомпилироваться для смены идентификатора. Если нужный ресурс - появится, то привязка останется. Новые клиенты не получат ресурс - которого нет, но он может использоваться - */ - for (int type = 0; type < (int) AssetType::MAX_ENUM; type++) { - for (const auto& [domain, keys] : orr.Lost[type]) { - auto iterType = DKToId.find(static_cast(type)); - if (iterType == DKToId.end()) - continue; - auto iterDomain = iterType->second.find(domain); - if (iterDomain == iterType->second.end()) - continue; + const auto& typeTable = DKToId[static_cast(type)]; + auto domainTable = typeTable.find(domain); - for (const auto& key : keys) { - auto iterKey = iterDomain->second.find(key); - if (iterKey == iterDomain->second.end()) - continue; - uint32_t id = iterKey->second; - result.Lost[type].push_back(id); + #ifndef NDEBUG + assert(!DKToIdInBakingMode); + #endif - auto iterResType = MediaResources.find(static_cast(type)); - if (iterResType == MediaResources.end()) - continue; - auto iterRes = iterResType->second.find(id); - if (iterRes == iterResType->second.end()) - continue; + if(domainTable == typeTable.end()) + return _getIdNew(type, domain, key); - HashToId.erase(iterRes->second.Hash); - iterResType->second.erase(iterRes); - } - } - } + auto keyTable = domainTable->second.find(key); - // Добавляем - for (int type = 0; type < (int) AssetType::MAX_ENUM; type++) { - for (const auto& [domain, resources] : orr.NewOrChange[type]) { - for (const PendingResource& pending : resources) { - uint32_t id = getId(static_cast(type), pending.Domain, pending.Key); + if (keyTable == domainTable->second.end()) + return _getIdNew(type, domain, key); - std::vector modelIds; - modelIds.reserve(pending.ModelDeps.size()); - for (const auto& dep : pending.ModelDeps) - modelIds.push_back(getId(AssetType::Model, dep.Domain, dep.Key)); + return keyTable->second; - std::vector textureIds; - textureIds.reserve(pending.TextureDeps.size()); - for (const auto& dep : pending.TextureDeps) - textureIds.push_back(getId(AssetType::Texture, dep.Domain, dep.Key)); - - MediaResource resource; - resource.Domain = pending.Domain; - resource.Key = pending.Key; - resource.Timestamp = pending.Timestamp; - resource.Resource = pending.Resource; - resource.Hash = pending.Hash; - resource.Dependencies = buildHeader(static_cast(type), modelIds, textureIds, pending.Extra); - - auto& table = MediaResources[static_cast(type)]; - if (auto iter = table.find(id); iter != table.end()) - HashToId.erase(iter->second.Hash); - - table[id] = resource; - HashToId[resource.Hash] = {static_cast(type), id}; - - result.NewOrChange[type].push_back({id, std::move(resource)}); - } - } - - std::unordered_set changed; - for (const auto& [id, _] : result.NewOrChange[type]) - changed.insert(id); - - auto& lost = result.Lost[type]; - lost.erase(std::remove_if(lost.begin(), lost.end(), - [&](uint32_t id) { return changed.contains(id); }), lost.end()); - } - - return result; + return 0; } -inline ResourceId AssetsPreloader::getId(AssetType type, const std::string& domain, const std::string& key) { - auto& typeTable = DKToId[type]; - auto& domainTable = typeTable[domain]; - if (auto iter = domainTable.find(key); iter != domainTable.end()) +inline ResourceId AssetsPreloader::_getIdNew(AssetType type, std::string_view domain, std::string_view key) { + auto lock = NewDKToId[static_cast(type)].lock(); + + auto iterDomainNewTable = lock->find(domain); + if(iterDomainNewTable == lock->end()) { + iterDomainNewTable = lock->emplace_hint( + iterDomainNewTable, + (std::string) domain, + std::unordered_map{} + ); + } + + auto& domainNewTable = iterDomainNewTable->second; + + if(auto iter = domainNewTable.find(key); iter != domainNewTable.end()) return iter->second; - ensureNextId(type); - uint32_t id = NextId[static_cast(type)]++; - domainTable[key] = id; + uint32_t id = domainNewTable[(std::string) key] = NextId[static_cast(type)]++; + + auto lock2 = NewIdToDK[static_cast(type)].lock(); + lock.unlock(); + + lock2->emplace_back(domain, key); + return id; } inline const AssetsPreloader::MediaResource* AssetsPreloader::getResource(AssetType type, uint32_t id) const { - auto iterType = MediaResources.find(type); - if (iterType == MediaResources.end()) - return nullptr; - auto iterRes = iterType->second.find(id); - if (iterRes == iterType->second.end()) + auto& iterType = MediaResources[static_cast(type)]; + + auto iterRes = iterType.find(id); + if(iterRes == iterType.end()) return nullptr; + return &iterRes->second; } -inline std::optional> AssetsPreloader::getResource(const ResourceFile::Hash_t& hash) { +inline std::optional> + AssetsPreloader::getResource(const ResourceFile::Hash_t& hash) +{ auto iter = HashToId.find(hash); if (iter == HashToId.end()) return std::nullopt; @@ -835,6 +398,7 @@ inline std::optionalHash != hash) { HashToId.erase(iter); return std::nullopt; @@ -843,52 +407,4 @@ inline std::optional{type, id, res}; } -inline std::tuple, std::vector> -AssetsPreloader::getNodeDependency(const std::string& domain, const std::string& key) { - if (domain == "core" && key == "none") { - return {0, {}, {}}; - } - - AssetsNodestate nodestateId = getId(AssetType::Nodestate, domain, key + ".json"); - const MediaResource* nodestate = getResource(AssetType::Nodestate, nodestateId); - if (!nodestate) - return {nodestateId, {}, {}}; - - auto parsed = parseHeader(nodestate->Dependencies); - if (!parsed) - return {nodestateId, {}, {}}; - - std::vector models; - std::vector textures; - std::unordered_set visited; - std::unordered_set seenTextures; - - std::function visitModel = [&](AssetsModel modelId) { - if (!visited.insert(modelId).second) - return; - - models.push_back(modelId); - - const MediaResource* modelRes = getResource(AssetType::Model, modelId); - if (!modelRes) - return; - auto header = parseHeader(modelRes->Dependencies); - if (!header) - return; - - for (uint32_t texId : header->TextureDeps) { - if (seenTextures.insert(texId).second) - textures.push_back(texId); - } - - for (uint32_t subId : header->ModelDeps) - visitModel(subId); - }; - - for (uint32_t modelId : parsed->ModelDeps) - visitModel(modelId); - - return {nodestateId, std::move(models), std::move(textures)}; -} - }