From 51cc68e1b2d04a7ba3034066775e5ffc643aeb15 Mon Sep 17 00:00:00 2001 From: DrSocalkwe3n Date: Sun, 4 Jan 2026 20:11:14 +0600 Subject: [PATCH] =?UTF-8?q?codex-5.2:=20=D0=9E=D0=B1=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D1=87=D0=B8=D0=BA=20=D1=80=D0=B5=D1=81=D1=83=D1=80?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=20=D0=BD=D0=B0=20=D1=81=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D0=BD=D0=B5=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Src/Client/AssetsManager.cpp | 981 ++++++++ Src/Client/AssetsManager.hpp | 152 ++ Src/Client/ServerSession.cpp | 714 +++--- Src/Client/ServerSession.hpp | 17 +- .../AtlasPipeline/TexturePipelineProgram.hpp | 1815 +-------------- Src/Client/Vulkan/VulkanRenderSession.hpp | 9 +- Src/Common/Abstract.cpp | 4 +- Src/Common/Abstract.hpp | 67 + Src/Common/AssetsPreloader.cpp | 7 + Src/Common/AssetsPreloader.hpp | 2 +- Src/Common/TexturePipelineProgram.hpp | 2067 +++++++++++++++++ 11 files changed, 3614 insertions(+), 2221 deletions(-) create mode 100644 Src/Client/AssetsManager.cpp create mode 100644 Src/Client/AssetsManager.hpp create mode 100644 Src/Common/TexturePipelineProgram.hpp diff --git a/Src/Client/AssetsManager.cpp b/Src/Client/AssetsManager.cpp new file mode 100644 index 0000000..4e80171 --- /dev/null +++ b/Src/Client/AssetsManager.cpp @@ -0,0 +1,981 @@ +#include "AssetsManager.hpp" +#include +#include +#include +#include +#include +#include +#include "Common/Net.hpp" +#include "Common/TexturePipelineProgram.hpp" + +namespace LV::Client { + +namespace { + +static const char* enumAssetsToDirectory(LV::EnumAssets value) { + switch(value) { + case LV::EnumAssets::Nodestate: return "nodestate"; + case LV::EnumAssets::Particle: return "particle"; + case LV::EnumAssets::Animation: return "animation"; + case LV::EnumAssets::Model: return "model"; + case LV::EnumAssets::Texture: return "texture"; + case LV::EnumAssets::Sound: return "sound"; + case LV::EnumAssets::Font: return "font"; + default: + break; + } + + assert(!"Unknown asset type"); + return ""; +} + +static std::u8string 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); + + std::u8string data; + data.resize(static_cast(size)); + if(size > 0) { + file.read(reinterpret_cast(data.data()), size); + if(!file) + throw std::runtime_error("Не удалось прочитать файл: " + path.string()); + } + + return data; +} + +static std::u8string readOptionalMeta(const fs::path& path) { + fs::path metaPath = path; + metaPath += ".meta"; + if(!fs::exists(metaPath) || !fs::is_regular_file(metaPath)) + return {}; + + return readFileBytes(metaPath); +} + +struct PipelineRemapResult { + bool Ok = true; + std::string Error; +}; + +PipelineRemapResult remapTexturePipelineIds(std::vector& code, + const std::function& mapId) +{ + struct Range { + size_t Start = 0; + size_t End = 0; + }; + + enum class SrcKind : uint8_t { TexId = 0, Sub = 1 }; + enum class Op : uint8_t { + End = 0, + Base_Tex = 1, + Base_Fill = 2, + Base_Anim = 3, + Resize = 10, + Transform = 11, + Opacity = 12, + NoAlpha = 13, + MakeAlpha = 14, + Invert = 15, + Brighten = 16, + Contrast = 17, + Multiply = 18, + Screen = 19, + Colorize = 20, + Anim = 21, + Overlay = 30, + Mask = 31, + LowPart = 32, + Combine = 40 + }; + + struct SrcMeta { + SrcKind Kind = SrcKind::TexId; + uint32_t TexId = 0; + uint32_t Off = 0; + uint32_t Len = 0; + size_t TexIdOffset = 0; + }; + + const size_t size = code.size(); + std::vector visited; + + auto read8 = [&](size_t& ip, uint8_t& out) -> bool { + if(ip >= size) + return false; + out = code[ip++]; + return true; + }; + + auto read16 = [&](size_t& ip, uint16_t& out) -> bool { + if(ip + 1 >= size) + return false; + out = uint16_t(code[ip]) | (uint16_t(code[ip + 1]) << 8); + ip += 2; + return true; + }; + + auto read24 = [&](size_t& ip, uint32_t& out) -> bool { + if(ip + 2 >= size) + return false; + out = uint32_t(code[ip]) | + (uint32_t(code[ip + 1]) << 8) | + (uint32_t(code[ip + 2]) << 16); + ip += 3; + return true; + }; + + auto read32 = [&](size_t& ip, uint32_t& out) -> bool { + if(ip + 3 >= size) + return false; + out = uint32_t(code[ip]) | + (uint32_t(code[ip + 1]) << 8) | + (uint32_t(code[ip + 2]) << 16) | + (uint32_t(code[ip + 3]) << 24); + ip += 4; + return true; + }; + + auto readSrc = [&](size_t& ip, SrcMeta& out) -> bool { + uint8_t kind = 0; + if(!read8(ip, kind)) + return false; + out.Kind = static_cast(kind); + if(out.Kind == SrcKind::TexId) { + out.TexIdOffset = ip; + return read24(ip, out.TexId); + } + if(out.Kind == SrcKind::Sub) { + return read24(ip, out.Off) && read24(ip, out.Len); + } + return false; + }; + + auto patchTexId = [&](const SrcMeta& src) -> bool { + if(src.Kind != SrcKind::TexId) + return true; + uint32_t newId = mapId(src.TexId); + if(newId >= (1u << 24)) + return false; + if(src.TexIdOffset + 2 >= code.size()) + return false; + code[src.TexIdOffset + 0] = uint8_t(newId & 0xFFu); + code[src.TexIdOffset + 1] = uint8_t((newId >> 8) & 0xFFu); + code[src.TexIdOffset + 2] = uint8_t((newId >> 16) & 0xFFu); + return true; + }; + + std::move_only_function scan; + scan = [&](size_t start, size_t end) -> bool { + if(start >= end || end > size) + return true; + for(const auto& r : visited) { + if(r.Start == start && r.End == end) + return true; + } + visited.push_back(Range{start, end}); + + size_t ip = start; + while(ip < end) { + uint8_t opByte = 0; + if(!read8(ip, opByte)) + return false; + Op op = static_cast(opByte); + switch(op) { + case Op::End: + return true; + + case Op::Base_Tex: { + SrcMeta src{}; + if(!readSrc(ip, src)) + return false; + if(!patchTexId(src)) + return false; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(!scan(subStart, subEnd)) + return false; + } + } break; + + case Op::Base_Fill: { + uint16_t tmp16 = 0; + uint32_t tmp32 = 0; + if(!read16(ip, tmp16)) + return false; + if(!read16(ip, tmp16)) + return false; + if(!read32(ip, tmp32)) + return false; + } break; + + case Op::Base_Anim: { + SrcMeta src{}; + if(!readSrc(ip, src)) + return false; + if(!patchTexId(src)) + return false; + uint16_t tmp16 = 0; + uint8_t tmp8 = 0; + if(!read16(ip, tmp16)) + return false; + if(!read16(ip, tmp16)) + return false; + if(!read16(ip, tmp16)) + return false; + if(!read16(ip, tmp16)) + return false; + if(!read8(ip, tmp8)) + return false; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(!scan(subStart, subEnd)) + return false; + } + } break; + + case Op::Resize: { + uint16_t tmp16 = 0; + if(!read16(ip, tmp16)) + return false; + if(!read16(ip, tmp16)) + return false; + } break; + + case Op::Transform: + case Op::Opacity: + case Op::Invert: { + uint8_t tmp8 = 0; + if(!read8(ip, tmp8)) + return false; + } break; + + case Op::NoAlpha: + case Op::Brighten: + break; + + case Op::MakeAlpha: + if(ip + 2 >= size) + return false; + ip += 3; + break; + + case Op::Contrast: + if(ip + 1 >= size) + return false; + ip += 2; + break; + + case Op::Multiply: + case Op::Screen: { + uint32_t tmp32 = 0; + if(!read32(ip, tmp32)) + return false; + } break; + + case Op::Colorize: { + uint32_t tmp32 = 0; + uint8_t tmp8 = 0; + if(!read32(ip, tmp32)) + return false; + if(!read8(ip, tmp8)) + return false; + } break; + + case Op::Anim: { + uint16_t tmp16 = 0; + uint8_t tmp8 = 0; + if(!read16(ip, tmp16)) + return false; + if(!read16(ip, tmp16)) + return false; + if(!read16(ip, tmp16)) + return false; + if(!read16(ip, tmp16)) + return false; + if(!read8(ip, tmp8)) + return false; + } break; + + case Op::Overlay: + case Op::Mask: { + SrcMeta src{}; + if(!readSrc(ip, src)) + return false; + if(!patchTexId(src)) + return false; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(!scan(subStart, subEnd)) + return false; + } + } break; + + case Op::LowPart: { + uint8_t tmp8 = 0; + if(!read8(ip, tmp8)) + return false; + SrcMeta src{}; + if(!readSrc(ip, src)) + return false; + if(!patchTexId(src)) + return false; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(!scan(subStart, subEnd)) + return false; + } + } break; + + case Op::Combine: { + uint16_t tmp16 = 0; + if(!read16(ip, tmp16)) + return false; + if(!read16(ip, tmp16)) + return false; + uint16_t count = 0; + if(!read16(ip, count)) + return false; + for(uint16_t i = 0; i < count; ++i) { + if(!read16(ip, tmp16)) + return false; + if(!read16(ip, tmp16)) + return false; + SrcMeta src{}; + if(!readSrc(ip, src)) + return false; + if(!patchTexId(src)) + return false; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(!scan(subStart, subEnd)) + return false; + } + } + } break; + + default: + return false; + } + } + + return true; + }; + + if(!scan(0, size)) + return {false, "Invalid texture pipeline bytecode"}; + + return {}; +} + +static std::vector collectTexturePipelineIds(const std::vector& code) { + std::vector out; + std::unordered_set seen; + + auto addId = [&](uint32_t id) { + if(seen.insert(id).second) + out.push_back(id); + }; + + std::vector copy = code; + auto result = remapTexturePipelineIds(copy, [&](uint32_t id) { + addId(id); + return id; + }); + + if(!result.Ok) + return {}; + + return out; +} + +} // namespace + +AssetsManager::AssetsManager(asio::io_context& ioc, const fs::path& cachePath, + size_t maxCacheDirectorySize, size_t maxLifeTime) + : Cache(AssetsCacheManager::Create(ioc, cachePath, maxCacheDirectorySize, maxLifeTime)) +{ + for(size_t i = 0; i < static_cast(AssetType::MAX_ENUM); ++i) + NextLocalId[i] = 1; +} + +AssetsManager::PackReloadResult AssetsManager::reloadPacks(const PackRegister& reg) { + PackReloadResult result; + auto oldPacks = PackResources; + for(auto& table : PackResources) + table.clear(); + + for(const fs::path& instance : reg.Packs) { + try { + if(fs::is_regular_file(instance)) { + LOG.warn() << "Архивы ресурспаков пока не поддерживаются: " << instance.string(); + continue; + } + if(!fs::is_directory(instance)) { + LOG.warn() << "Неизвестный тип ресурспака: " << instance.string(); + continue; + } + + fs::path assetsRoot = instance; + fs::path assetsCandidate = instance / "assets"; + if(fs::exists(assetsCandidate) && fs::is_directory(assetsCandidate)) + assetsRoot = assetsCandidate; + + 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; + + auto& typeTable = PackResources[type][domain]; + for(auto fbegin = fs::recursive_directory_iterator(assetPath), + fend = fs::recursive_directory_iterator(); + fbegin != fend; ++fbegin) { + if(fbegin->is_directory()) + continue; + fs::path file = fbegin->path(); + if(assetType == AssetType::Texture && file.extension() == ".meta") + continue; + + std::string key = fs::relative(file, assetPath).string(); + if(typeTable.contains(key)) + continue; + + PackResource entry; + entry.Type = assetType; + entry.Domain = domain; + entry.Key = key; + entry.LocalId = getOrCreateLocalId(assetType, entry.Domain, entry.Key); + + try { + if(assetType == AssetType::Nodestate) { + std::u8string data = readFileBytes(file); + std::string_view view(reinterpret_cast(data.data()), data.size()); + js::object obj = js::parse(view).as_object(); + + HeadlessNodeState hns; + auto modelResolver = [&](std::string_view model) -> AssetsModel { + auto [mDomain, mKey] = parseDomainKey(model, entry.Domain); + return getOrCreateLocalId(AssetType::Model, mDomain, mKey); + }; + + entry.Header = hns.parse(obj, modelResolver); + std::u8string compiled = hns.dump(); + entry.Res = Resource(std::move(compiled)); + entry.Hash = entry.Res.hash(); + } else if(assetType == AssetType::Model) { + const std::string ext = file.extension().string(); + if(ext == ".json") { + std::u8string data = readFileBytes(file); + std::string_view view(reinterpret_cast(data.data()), data.size()); + js::object obj = js::parse(view).as_object(); + + HeadlessModel hm; + auto modelResolver = [&](std::string_view model) -> AssetsModel { + auto [mDomain, mKey] = parseDomainKey(model, entry.Domain); + return getOrCreateLocalId(AssetType::Model, mDomain, mKey); + }; + auto textureResolver = [&](std::string_view textureSrc) -> std::vector { + TexturePipelineProgram tpp; + if(!tpp.compile(std::string(textureSrc))) + return {}; + auto textureIdResolver = [&](std::string_view name) -> std::optional { + auto [tDomain, tKey] = parseDomainKey(name, entry.Domain); + return getOrCreateLocalId(AssetType::Texture, tDomain, tKey); + }; + if(!tpp.link(textureIdResolver)) + return {}; + return tpp.toBytes(); + }; + + entry.Header = hm.parse(obj, modelResolver, textureResolver); + std::u8string compiled = hm.dump(); + entry.Res = Resource(std::move(compiled)); + entry.Hash = entry.Res.hash(); + } else { + LOG.warn() << "Не поддерживаемый формат модели: " << file.string(); + continue; + } + } else if(assetType == AssetType::Texture) { + std::u8string data = readFileBytes(file); + entry.Res = Resource(std::move(data)); + entry.Hash = entry.Res.hash(); + entry.Header = readOptionalMeta(file); + } else { + std::u8string data = readFileBytes(file); + entry.Res = Resource(std::move(data)); + entry.Hash = entry.Res.hash(); + } + } catch(const std::exception& exc) { + LOG.warn() << "Ошибка загрузки ресурса " << file.string() << ": " << exc.what(); + continue; + } + + typeTable.emplace(entry.Key, entry); + } + } + } + } catch(const std::exception& exc) { + LOG.warn() << "Ошибка загрузки ресурспака " << instance.string() << ": " << exc.what(); + } + } + + for(size_t type = 0; type < static_cast(AssetType::MAX_ENUM); ++type) { + for(const auto& [domain, keyTable] : PackResources[type]) { + for(const auto& [key, res] : keyTable) { + bool changed = true; + auto oldDomain = oldPacks[type].find(domain); + if(oldDomain != oldPacks[type].end()) { + auto oldKey = oldDomain->second.find(key); + if(oldKey != oldDomain->second.end()) { + changed = oldKey->second.Hash != res.Hash; + } + } + if(changed) + result.ChangeOrAdd[type].push_back(res.LocalId); + } + } + + for(const auto& [domain, keyTable] : oldPacks[type]) { + for(const auto& [key, res] : keyTable) { + auto newDomain = PackResources[type].find(domain); + bool lost = true; + if(newDomain != PackResources[type].end()) { + if(newDomain->second.contains(key)) + lost = false; + } + if(lost) + result.Lost[type].push_back(res.LocalId); + } + } + } + + return result; +} + +AssetsManager::BindResult AssetsManager::bindServerResource(AssetType type, AssetId serverId, + std::string domain, std::string key, const Hash_t& hash, std::vector header) +{ + BindResult result; + AssetId localFromDK = getOrCreateLocalId(type, domain, key); + AssetId localFromServer = ensureServerLocalId(type, serverId); + + unionLocalIds(type, localFromServer, localFromDK, &result.ReboundFrom); + AssetId localId = resolveLocalIdMutable(type, localFromDK); + + auto& map = ServerToLocal[static_cast(type)]; + if(serverId >= map.size()) + map.resize(serverId + 1, 0); + map[serverId] = localId; + + auto& infoList = BindInfos[static_cast(type)]; + if(localId >= infoList.size()) + infoList.resize(localId + 1); + + bool hadBinding = infoList[localId].has_value(); + bool changed = !hadBinding || infoList[localId]->Hash != hash || infoList[localId]->Header != header; + + infoList[localId] = BindInfo{ + .Type = type, + .LocalId = localId, + .Domain = std::move(domain), + .Key = std::move(key), + .Hash = hash, + .Header = std::move(header) + }; + + result.LocalId = localId; + result.Changed = changed; + result.NewBinding = !hadBinding; + return result; +} + +std::optional AssetsManager::unbindServerResource(AssetType type, AssetId serverId) { + auto& map = ServerToLocal[static_cast(type)]; + if(serverId >= map.size()) + return std::nullopt; + AssetId localId = map[serverId]; + map[serverId] = 0; + if(localId == 0) + return std::nullopt; + return resolveLocalIdMutable(type, localId); +} + +void AssetsManager::clearServerBindings() { + for(auto& table : ServerToLocal) + table.clear(); + for(auto& table : BindInfos) + table.clear(); +} + +const AssetsManager::BindInfo* AssetsManager::getBind(AssetType type, AssetId localId) const { + localId = resolveLocalId(type, localId); + const auto& table = BindInfos[static_cast(type)]; + if(localId >= table.size()) + return nullptr; + if(!table[localId]) + return nullptr; + return &*table[localId]; +} + +std::vector AssetsManager::rebindHeader(AssetType type, const std::vector& header, bool serverIds) { + if(header.empty()) + return {}; + + auto mapModelId = [&](AssetId id) -> AssetId { + if(serverIds) + return ensureServerLocalId(AssetType::Model, id); + return resolveLocalIdMutable(AssetType::Model, id); + }; + + auto mapTextureId = [&](AssetId id) -> AssetId { + if(serverIds) + return ensureServerLocalId(AssetType::Texture, id); + return resolveLocalIdMutable(AssetType::Texture, id); + }; + + if(type == AssetType::Nodestate) { + if(header.size() % sizeof(AssetId) != 0) + return header; + std::vector out(header.size()); + const size_t count = header.size() / sizeof(AssetId); + for(size_t i = 0; i < count; ++i) { + AssetId raw = 0; + std::memcpy(&raw, header.data() + i * sizeof(AssetId), sizeof(AssetId)); + AssetId mapped = mapModelId(raw); + std::memcpy(out.data() + i * sizeof(AssetId), &mapped, sizeof(AssetId)); + } + return out; + } + + if(type == AssetType::Model) { + try { + TOS::ByteBuffer buffer(header.size(), header.data()); + auto reader = buffer.reader(); + + uint16_t modelCount = reader.readUInt16(); + std::vector models; + models.reserve(modelCount); + for(uint16_t i = 0; i < modelCount; ++i) { + AssetId id = reader.readUInt32(); + models.push_back(mapModelId(id)); + } + + uint16_t texCount = reader.readUInt16(); + std::vector> pipelines; + pipelines.reserve(texCount); + for(uint16_t i = 0; i < texCount; ++i) { + uint32_t size32 = reader.readUInt32(); + TOS::ByteBuffer pipe; + reader.readBuffer(pipe); + if(pipe.size() != size32) { + LOG.warn() << "Несовпадение длины pipeline: " << size32 << " vs " << pipe.size(); + } + std::vector code(pipe.begin(), pipe.end()); + auto result = remapTexturePipelineIds(code, [&](uint32_t id) { + return mapTextureId(static_cast(id)); + }); + if(!result.Ok) { + LOG.warn() << "Ошибка ребинда pipeline: " << result.Error; + } + pipelines.emplace_back(std::move(code)); + } + + TOS::ByteBuffer::Writer wr; + wr << uint16_t(models.size()); + for(AssetId id : models) + wr << id; + wr << uint16_t(pipelines.size()); + for(const auto& pipe : pipelines) { + wr << uint32_t(pipe.size()); + TOS::ByteBuffer pipeBuff(pipe.begin(), pipe.end()); + wr << pipeBuff; + } + + TOS::ByteBuffer out = wr.complite(); + return std::vector(out.begin(), out.end()); + } catch(const std::exception& exc) { + LOG.warn() << "Ошибка ребинда заголовка модели: " << exc.what(); + return header; + } + } + + return header; +} + +std::optional AssetsManager::parseHeader(AssetType type, const std::vector& header) { + if(header.empty()) + return std::nullopt; + + ParsedHeader result; + result.Type = type; + + if(type == AssetType::Nodestate) { + if(header.size() % sizeof(AssetId) != 0) + return std::nullopt; + const size_t count = header.size() / sizeof(AssetId); + result.ModelDeps.resize(count); + for(size_t i = 0; i < count; ++i) { + AssetId raw = 0; + std::memcpy(&raw, header.data() + i * sizeof(AssetId), sizeof(AssetId)); + result.ModelDeps[i] = raw; + } + return result; + } + + if(type == AssetType::Model) { + try { + TOS::ByteBuffer buffer(header.size(), header.data()); + auto reader = buffer.reader(); + + uint16_t modelCount = reader.readUInt16(); + result.ModelDeps.reserve(modelCount); + for(uint16_t i = 0; i < modelCount; ++i) + result.ModelDeps.push_back(reader.readUInt32()); + + uint16_t texCount = reader.readUInt16(); + result.TexturePipelines.reserve(texCount); + for(uint16_t i = 0; i < texCount; ++i) { + uint32_t size32 = reader.readUInt32(); + TOS::ByteBuffer pipe; + reader.readBuffer(pipe); + if(pipe.size() != size32) { + return std::nullopt; + } + result.TexturePipelines.emplace_back(pipe.begin(), pipe.end()); + } + + std::unordered_set seen; + for(const auto& pipe : result.TexturePipelines) { + for(uint32_t id : collectTexturePipelineIds(pipe)) { + if(seen.insert(id).second) + result.TextureDeps.push_back(id); + } + } + + return result; + } catch(const std::exception&) { + return std::nullopt; + } + } + + return std::nullopt; +} + +void AssetsManager::pushReads(std::vector reads) { + std::vector forCache; + forCache.reserve(reads.size()); + + for(ResourceKey& key : reads) { + std::optional pack = findPackResource(key.Type, key.Domain, key.Key); + if(pack && pack->Hash == key.Hash) { + ReadyReads.emplace_back(std::move(key), pack->Res); + continue; + } + + auto& list = PendingReadsByHash[key.Hash]; + bool isFirst = list.empty(); + list.push_back(std::move(key)); + if(isFirst) + forCache.push_back(list.front().Hash); + } + + if(!forCache.empty()) + Cache->pushReads(std::move(forCache)); +} + +std::vector>> AssetsManager::pullReads() { + std::vector>> out; + out.reserve(ReadyReads.size()); + + for(auto& entry : ReadyReads) + out.emplace_back(std::move(entry)); + ReadyReads.clear(); + + std::vector>> cached = Cache->pullReads(); + for(auto& [hash, res] : cached) { + auto iter = PendingReadsByHash.find(hash); + if(iter == PendingReadsByHash.end()) + continue; + for(ResourceKey& key : iter->second) + out.emplace_back(std::move(key), res); + PendingReadsByHash.erase(iter); + } + + return out; +} + +AssetsManager::AssetId AssetsManager::getOrCreateLocalId(AssetType type, std::string_view domain, std::string_view key) { + auto& table = DKToLocal[static_cast(type)]; + auto iterDomain = table.find(domain); + if(iterDomain == table.end()) { + iterDomain = table.emplace( + std::string(domain), + std::unordered_map{} + ).first; + } + + auto& keyTable = iterDomain->second; + auto iterKey = keyTable.find(key); + if(iterKey != keyTable.end()) { + iterKey->second = resolveLocalIdMutable(type, iterKey->second); + return iterKey->second; + } + + AssetId id = allocateLocalId(type); + keyTable.emplace(std::string(key), id); + + auto& dk = LocalToDK[static_cast(type)]; + if(id >= dk.size()) + dk.resize(id + 1); + dk[id] = DomainKey{std::string(domain), std::string(key), true}; + + return id; +} + +AssetsManager::AssetId AssetsManager::getOrCreateLocalFromServer(AssetType type, AssetId serverId) { + return ensureServerLocalId(type, serverId); +} + +std::optional AssetsManager::getLocalIdFromServer(AssetType type, AssetId serverId) const { + const auto& map = ServerToLocal[static_cast(type)]; + if(serverId >= map.size()) + return std::nullopt; + AssetId local = map[serverId]; + if(local == 0) + return std::nullopt; + return resolveLocalId(type, local); +} + +AssetsManager::AssetId AssetsManager::resolveLocalId(AssetType type, AssetId localId) const { + if(localId == 0) + return 0; + const auto& parents = LocalParent[static_cast(type)]; + if(localId >= parents.size()) + return localId; + AssetId cur = localId; + while(cur < parents.size() && parents[cur] != cur && parents[cur] != 0) + cur = parents[cur]; + return cur; +} + +AssetsManager::AssetId AssetsManager::allocateLocalId(AssetType type) { + auto& next = NextLocalId[static_cast(type)]; + AssetId id = next++; + + auto& parents = LocalParent[static_cast(type)]; + if(id >= parents.size()) + parents.resize(id + 1, 0); + parents[id] = id; + + auto& dk = LocalToDK[static_cast(type)]; + if(id >= dk.size()) + dk.resize(id + 1); + + return id; +} + +AssetsManager::AssetId AssetsManager::ensureServerLocalId(AssetType type, AssetId serverId) { + auto& map = ServerToLocal[static_cast(type)]; + if(serverId >= map.size()) + map.resize(serverId + 1, 0); + if(map[serverId] == 0) + map[serverId] = allocateLocalId(type); + return resolveLocalIdMutable(type, map[serverId]); +} + +AssetsManager::AssetId AssetsManager::resolveLocalIdMutable(AssetType type, AssetId localId) { + if(localId == 0) + return 0; + auto& parents = LocalParent[static_cast(type)]; + if(localId >= parents.size()) + return localId; + AssetId root = localId; + while(root < parents.size() && parents[root] != root && parents[root] != 0) + root = parents[root]; + if(root == localId) + return root; + AssetId cur = localId; + while(cur < parents.size() && parents[cur] != root && parents[cur] != 0) { + AssetId next = parents[cur]; + parents[cur] = root; + cur = next; + } + return root; +} + +void AssetsManager::unionLocalIds(AssetType type, AssetId fromId, AssetId toId, std::optional* reboundFrom) { + AssetId fromRoot = resolveLocalIdMutable(type, fromId); + AssetId toRoot = resolveLocalIdMutable(type, toId); + if(fromRoot == 0 || toRoot == 0 || fromRoot == toRoot) + return; + + auto& parents = LocalParent[static_cast(type)]; + if(fromRoot >= parents.size() || toRoot >= parents.size()) + return; + + parents[fromRoot] = toRoot; + if(reboundFrom) + *reboundFrom = fromRoot; + + auto& dk = LocalToDK[static_cast(type)]; + if(fromRoot < dk.size()) { + const DomainKey& fromDK = dk[fromRoot]; + if(fromDK.Known) { + if(toRoot >= dk.size()) + dk.resize(toRoot + 1); + DomainKey& toDK = dk[toRoot]; + if(!toDK.Known) { + toDK = fromDK; + DKToLocal[static_cast(type)][toDK.Domain][toDK.Key] = toRoot; + } else if(toDK.Domain != fromDK.Domain || toDK.Key != fromDK.Key) { + LOG.warn() << "Конфликт домен/ключ при ребинде: " + << fromDK.Domain << ':' << fromDK.Key << " vs " + << toDK.Domain << ':' << toDK.Key; + } + } + } + + auto& binds = BindInfos[static_cast(type)]; + if(fromRoot < binds.size()) { + if(toRoot >= binds.size()) + binds.resize(toRoot + 1); + if(!binds[toRoot] && binds[fromRoot]) + binds[toRoot] = std::move(binds[fromRoot]); + } +} + +std::optional AssetsManager::findPackResource(AssetType type, + std::string_view domain, std::string_view key) const +{ + const auto& typeTable = PackResources[static_cast(type)]; + auto iterDomain = typeTable.find(domain); + if(iterDomain == typeTable.end()) + return std::nullopt; + auto iterKey = iterDomain->second.find(key); + if(iterKey == iterDomain->second.end()) + return std::nullopt; + return iterKey->second; +} + +} // namespace LV::Client diff --git a/Src/Client/AssetsManager.hpp b/Src/Client/AssetsManager.hpp new file mode 100644 index 0000000..04242b3 --- /dev/null +++ b/Src/Client/AssetsManager.hpp @@ -0,0 +1,152 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Client/AssetsCacheManager.hpp" +#include "Common/Abstract.hpp" +#include "TOSLib.hpp" + +namespace LV::Client { + +namespace fs = std::filesystem; + +class AssetsManager { +public: + using Ptr = std::shared_ptr; + using AssetType = EnumAssets; + using AssetId = ResourceId; + + struct ResourceKey { + Hash_t Hash{}; + AssetType Type{}; + std::string Domain; + std::string Key; + AssetId Id = 0; + }; + + struct BindInfo { + AssetType Type{}; + AssetId LocalId = 0; + std::string Domain; + std::string Key; + Hash_t Hash{}; + std::vector Header; + }; + + struct BindResult { + AssetId LocalId = 0; + bool Changed = false; + bool NewBinding = false; + std::optional ReboundFrom; + }; + + struct PackRegister { + std::vector Packs; + }; + + struct PackResource { + AssetType Type{}; + AssetId LocalId = 0; + std::string Domain; + std::string Key; + Resource Res; + Hash_t Hash{}; + std::u8string Header; + }; + + struct PackReloadResult { + std::array, static_cast(AssetType::MAX_ENUM)> ChangeOrAdd; + std::array, static_cast(AssetType::MAX_ENUM)> Lost; + }; + + struct ParsedHeader { + AssetType Type{}; + std::vector ModelDeps; + std::vector TextureDeps; + std::vector> TexturePipelines; + }; + + static Ptr Create(asio::io_context& ioc, const fs::path& cachePath, + size_t maxCacheDirectorySize = 8 * 1024 * 1024 * 1024ULL, + size_t maxLifeTime = 7 * 24 * 60 * 60) { + return Ptr(new AssetsManager(ioc, cachePath, maxCacheDirectorySize, maxLifeTime)); + } + + PackReloadResult reloadPacks(const PackRegister& reg); + + BindResult bindServerResource(AssetType type, AssetId serverId, std::string domain, std::string key, + const Hash_t& hash, std::vector header); + std::optional unbindServerResource(AssetType type, AssetId serverId); + void clearServerBindings(); + + const BindInfo* getBind(AssetType type, AssetId localId) const; + + std::vector rebindHeader(AssetType type, const std::vector& header, bool serverIds = true); + static std::optional parseHeader(AssetType type, const std::vector& header); + + void pushResources(std::vector resources) { + Cache->pushResources(std::move(resources)); + } + + void pushReads(std::vector reads); + std::vector>> pullReads(); + + AssetId getOrCreateLocalId(AssetType type, std::string_view domain, std::string_view key); + AssetId getOrCreateLocalFromServer(AssetType type, AssetId serverId); + std::optional getLocalIdFromServer(AssetType type, AssetId serverId) const; + +private: + struct DomainKey { + std::string Domain; + std::string Key; + bool Known = false; + }; + + using IdTable = std::unordered_map< + std::string, + std::unordered_map, + detail::TSVHash, + detail::TSVEq>; + + using PackTable = std::unordered_map< + std::string, + std::unordered_map, + detail::TSVHash, + detail::TSVEq>; + + AssetsManager(asio::io_context& ioc, const fs::path& cachePath, + size_t maxCacheDirectorySize, size_t maxLifeTime); + + AssetId allocateLocalId(AssetType type); + AssetId ensureServerLocalId(AssetType type, AssetId serverId); + AssetId resolveLocalIdMutable(AssetType type, AssetId localId); + AssetId resolveLocalId(AssetType type, AssetId localId) const; + void unionLocalIds(AssetType type, AssetId fromId, AssetId toId, std::optional* reboundFrom); + + std::optional findPackResource(AssetType type, std::string_view domain, std::string_view key) const; + + Logger LOG = "Client>AssetsManager"; + AssetsCacheManager::Ptr Cache; + + std::array(AssetType::MAX_ENUM)> DKToLocal; + std::array, static_cast(AssetType::MAX_ENUM)> LocalToDK; + std::array, static_cast(AssetType::MAX_ENUM)> LocalParent; + std::array, static_cast(AssetType::MAX_ENUM)> ServerToLocal; + std::array>, static_cast(AssetType::MAX_ENUM)> BindInfos; + std::array(AssetType::MAX_ENUM)> PackResources; + std::array(AssetType::MAX_ENUM)> NextLocalId{}; + + std::unordered_map> PendingReadsByHash; + std::vector>> ReadyReads; +}; + +} // namespace LV::Client diff --git a/Src/Client/ServerSession.cpp b/Src/Client/ServerSession.cpp index 6c7e805..2902b8f 100644 --- a/Src/Client/ServerSession.cpp +++ b/Src/Client/ServerSession.cpp @@ -364,7 +364,7 @@ void ServerSession::update(GlobalTime gTime, float dTime) { for(AssetEntry& entry : assets) { entry.Hash = entry.Res.hash(); if(const AssetsManager::BindInfo* bind = AM->getBind(entry.Type, entry.Id)) - entry.Dependencies = AM->rebindHeader(bind->Header); + entry.Dependencies = AM->rebindHeader(entry.Type, bind->Header); else entry.Dependencies.clear(); @@ -451,7 +451,7 @@ void ServerSession::update(GlobalTime gTime, float dTime) { std::vector deps; if(const AssetsManager::BindInfo* bind = AM->getBind(key.Type, key.Id)) - deps = AM->rebindHeader(bind->Header); + deps = AM->rebindHeader(key.Type, bind->Header); AssetEntry entry { .Type = key.Type, @@ -575,7 +575,13 @@ void ServerSession::update(GlobalTime gTime, float dTime) { // Под рукой нет ресурса, отправим на проверку в AssetsManager if(needQuery) { AsyncContext.ResourceWait[(int) bind.Type][bind.Domain].emplace_back(bind.Key, bind.Hash); - needToLoad.emplace_back(bind.Hash, bind.Type, bind.Domain, bind.Key, bind.Id); + needToLoad.push_back(AssetsManager::ResourceKey{ + .Hash = bind.Hash, + .Type = bind.Type, + .Domain = bind.Domain, + .Key = bind.Key, + .Id = bind.Id + }); } } @@ -978,7 +984,7 @@ void ServerSession::update(GlobalTime gTime, float dTime) { for(AssetBindEntry& entry : abc.Binds) { std::vector deps; if(!entry.Header.empty()) - deps = AM->rebindHeader(entry.Header); + deps = AM->rebindHeader(entry.Type, entry.Header); MyAssets.ExistBinds[(int) entry.Type].insert(entry.Id); result.Assets_ChangeOrAdd[entry.Type].push_back(entry.Id); @@ -1270,179 +1276,142 @@ void ServerSession::protocolError() { coro<> ServerSession::readPacket(Net::AsyncSocket &sock) { uint8_t first = co_await sock.read(); - switch((ToClient::L1) first) { - case ToClient::L1::System: co_await rP_System(sock); co_return; - case ToClient::L1::Resource: co_await rP_Resource(sock); co_return; - case ToClient::L1::Definition: co_await rP_Definition(sock); co_return; - case ToClient::L1::Content: co_await rP_Content(sock); co_return; - default: - protocolError(); - } -} - -coro<> ServerSession::rP_System(Net::AsyncSocket &sock) { - uint8_t second = co_await sock.read(); - - switch((ToClient::L2System) second) { - case ToClient::L2System::Init: - + switch((ToClient) first) { + case ToClient::Init: co_return; - case ToClient::L2System::Disconnect: - { - EnumDisconnect type = (EnumDisconnect) co_await sock.read(); - std::string reason = co_await sock.read(); - - if(type == EnumDisconnect::ByInterface) - reason = "по запросу интерфейса " + reason; - else if(type == EnumDisconnect::CriticalError) - reason = "на сервере произошла критическая ошибка " + reason; - else if(type == EnumDisconnect::ProtocolError) - reason = "ошибка протокола (сервер) " + reason; - - LOG.info() << "Отключение от сервера: " << reason; - + case ToClient::Disconnect: + co_await rP_Disconnect(sock); co_return; - } - case ToClient::L2System::LinkCameraToEntity: - + case ToClient::AssetsBindDK: + co_await rP_AssetsBindDK(sock); co_return; - case ToClient::L2System::UnlinkCamera: - + case ToClient::AssetsBindHH: + co_await rP_AssetsBindHH(sock); co_return; - case ToClient::L2System::SyncTick: - AsyncContext.TickSequence.lock()->push_back(std::move(AsyncContext.ThisTickEntry)); + case ToClient::AssetsInitSend: + co_await rP_AssetsInitSend(sock); + co_return; + case ToClient::AssetsNextSend: + co_await rP_AssetsNextSend(sock); + co_return; + case ToClient::DefinitionsUpdate: + co_await rP_DefinitionsUpdate(sock); + co_return; + case ToClient::ChunkVoxels: + co_await rP_ChunkVoxels(sock); + co_return; + case ToClient::ChunkNodes: + co_await rP_ChunkNodes(sock); + co_return; + case ToClient::ChunkLightPrism: + co_await rP_ChunkLightPrism(sock); + co_return; + case ToClient::RemoveRegion: + co_await rP_RemoveRegion(sock); + co_return; + case ToClient::Tick: + co_await rP_Tick(sock); + co_return; + case ToClient::TestLinkCameraToEntity: + co_await rP_TestLinkCameraToEntity(sock); + co_return; + case ToClient::TestUnlinkCamera: + co_await rP_TestUnlinkCamera(sock); co_return; default: protocolError(); } } -coro<> ServerSession::rP_Resource(Net::AsyncSocket &sock) { - static std::atomic debugResourceLogCount = 0; - uint8_t second = co_await sock.read(); +coro<> ServerSession::rP_Disconnect(Net::AsyncSocket &sock) { + EnumDisconnect type = (EnumDisconnect) co_await sock.read(); + std::string reason = co_await sock.read(); - switch((ToClient::L2Resource) second) { - case ToClient::L2Resource::Bind: - { - uint32_t count = co_await sock.read(); - std::vector binds; - binds.reserve(count); + if(type == EnumDisconnect::ByInterface) + reason = "по запросу интерфейса " + reason; + else if(type == EnumDisconnect::CriticalError) + reason = "на сервере произошла критическая ошибка " + reason; + else if(type == EnumDisconnect::ProtocolError) + reason = "ошибка протокола (сервер) " + reason; - for(size_t iter = 0; iter < count; iter++) { - uint8_t type = co_await sock.read(); + LOG.info() << "Отключение от сервера: " << reason; + co_return; +} - if(type >= (int) EnumAssets::MAX_ENUM) - protocolError(); +coro<> ServerSession::rP_AssetsBindDK(Net::AsyncSocket &sock) { + std::string compressed = co_await sock.read(); + std::u8string in((const char8_t*) compressed.data(), compressed.size()); + std::u8string data = unCompressLinear(in); + Net::LinearReader lr(data); - uint32_t id = co_await sock.read(); - std::string domain, key; - domain = co_await sock.read(); - key = co_await sock.read(); - Hash_t hash; - co_await sock.read((std::byte*) hash.data(), hash.size()); - uint32_t headerSize = co_await sock.read(); - std::vector header; - if(headerSize > 0) { - header.resize(headerSize); - co_await sock.read((std::byte*) header.data(), header.size()); - } + uint16_t domainsCount = lr.read(); + std::vector domains; + domains.reserve(domainsCount); + for(uint16_t i = 0; i < domainsCount; ++i) + domains.push_back(lr.read()); - AssetsManager::BindResult bindResult = AM->bindServerResource( - (EnumAssets) type, (ResourceId) id, domain, key, hash, header); - - if(!bindResult.Changed) + for(size_t type = 0; type < static_cast(EnumAssets::MAX_ENUM); ++type) { + uint32_t count = lr.read(); + for(uint32_t i = 0; i < count; ++i) { + uint16_t domainId = lr.read(); + std::string key = lr.read(); + if(domainId >= domains.size()) continue; - binds.emplace_back(AssetBindEntry{ - .Type = (EnumAssets) type, - .Id = bindResult.LocalId, - .Domain = std::move(domain), - .Key = std::move(key), - .Hash = hash, - .Header = std::move(header) - }); - - if(binds.back().Domain == "test" - && (binds.back().Type == EnumAssets::Nodestate - || binds.back().Type == EnumAssets::Model - || binds.back().Type == EnumAssets::Texture)) - { - uint32_t idx = debugResourceLogCount.fetch_add(1); - if(idx < 128) { - LOG.debug() << "Bind asset type=" << assetTypeName(binds.back().Type) - << " id=" << binds.back().Id - << " key=" << binds.back().Domain << ':' << binds.back().Key - << " hash=" << int(binds.back().Hash[0]) << '.' - << int(binds.back().Hash[1]) << '.' - << int(binds.back().Hash[2]) << '.' - << int(binds.back().Hash[3]); - } - } - } - - AsyncContext.AssetsBinds.lock()->push_back(AssetsBindsChange(binds, {})); - co_return; - } - case ToClient::L2Resource::BindDK: - { - uint32_t count = co_await sock.read(); - for(size_t iter = 0; iter < count; iter++) { - uint8_t type = co_await sock.read(); - if(type >= (int) EnumAssets::MAX_ENUM) - protocolError(); - - std::string domain = co_await sock.read(); - std::string key = co_await sock.read(); - ResourceId serverId = NextServerId[type]++; auto& table = ServerIdToDK[type]; if(table.size() <= serverId) - table.resize(serverId+1); - table[serverId] = {std::move(domain), std::move(key)}; + table.resize(serverId + 1); + table[serverId] = {domains[domainId], std::move(key)}; } - co_return; } - case ToClient::L2Resource::BindHash: - { + + co_return; +} + +coro<> ServerSession::rP_AssetsBindHH(Net::AsyncSocket &sock) { + static std::atomic debugResourceLogCount = 0; + AssetsBindsChange abc; + + for(size_t typeIndex = 0; typeIndex < static_cast(EnumAssets::MAX_ENUM); ++typeIndex) { uint32_t count = co_await sock.read(); + if(count == 0) + continue; + std::vector binds; binds.reserve(count); - for(size_t iter = 0; iter < count; iter++) { - uint8_t type = co_await sock.read(); - if(type >= (int) EnumAssets::MAX_ENUM) - protocolError(); - + for(size_t iter = 0; iter < count; ++iter) { uint32_t id = co_await sock.read(); Hash_t hash; co_await sock.read((std::byte*) hash.data(), hash.size()); - uint32_t headerSize = co_await sock.read(); - std::vector header; - if(headerSize > 0) { - header.resize(headerSize); - co_await sock.read((std::byte*) header.data(), header.size()); - } + std::string headerStr = co_await sock.read(); + std::vector header(headerStr.begin(), headerStr.end()); - auto& table = ServerIdToDK[type]; + auto& table = ServerIdToDK[typeIndex]; if(id >= table.size()) { - LOG.warn() << "BindHash without domain/key for id=" << id; + LOG.warn() << "AssetsBindHH without domain/key for id=" << id; continue; } const auto& [domain, key] = table[id]; if(domain.empty() && key.empty()) { - LOG.warn() << "BindHash missing domain/key for id=" << id; + LOG.warn() << "AssetsBindHH missing domain/key for id=" << id; continue; } + EnumAssets type = static_cast(typeIndex); AssetsManager::BindResult bindResult = AM->bindServerResource( - (EnumAssets) type, (ResourceId) id, domain, key, hash, header); + type, (ResourceId) id, domain, key, hash, header); - if(!bindResult.Changed) + if(bindResult.ReboundFrom) + abc.Lost[typeIndex].push_back(*bindResult.ReboundFrom); + + if(!bindResult.Changed && !bindResult.ReboundFrom) continue; binds.emplace_back(AssetBindEntry{ - .Type = (EnumAssets) type, + .Type = type, .Id = bindResult.LocalId, .Domain = domain, .Key = key, @@ -1468,307 +1437,264 @@ coro<> ServerSession::rP_Resource(Net::AsyncSocket &sock) { } } - if(!binds.empty()) - AsyncContext.AssetsBinds.lock()->push_back(AssetsBindsChange(binds, {})); + if(!binds.empty()) { + abc.Binds.append_range(binds); + binds.clear(); + } + } + + bool hasLost = false; + for(const auto& list : abc.Lost) { + if(!list.empty()) { + hasLost = true; + break; + } + } + + if(!abc.Binds.empty() || hasLost) + AsyncContext.AssetsBinds.lock()->push_back(std::move(abc)); + else + co_return; + + co_return; +} + +coro<> ServerSession::rP_AssetsInitSend(Net::AsyncSocket &sock) { + static std::atomic debugResourceLogCount = 0; + uint32_t size = co_await sock.read(); + Hash_t hash; + co_await sock.read((std::byte*) hash.data(), hash.size()); + + bool found = false; + EnumAssets type = EnumAssets::Texture; + std::string domain; + std::string key; + + for(int typeIndex = 0; typeIndex < (int) EnumAssets::MAX_ENUM && !found; ++typeIndex) { + auto& waitingByDomain = AsyncContext.ResourceWait[typeIndex]; + for(auto iterDomain = waitingByDomain.begin(); iterDomain != waitingByDomain.end() && !found; ) { + auto& entries = iterDomain->second; + for(size_t i = 0; i < entries.size(); ++i) { + if(entries[i].second == hash) { + type = static_cast(typeIndex); + domain = iterDomain->first; + key = entries[i].first; + entries.erase(entries.begin() + i); + if(entries.empty()) + iterDomain = waitingByDomain.erase(iterDomain); + else + ++iterDomain; + found = true; + break; + } + } + if(!found) + ++iterDomain; + } + } + + if(!found) { + LOG.warn() << "AssetsInitSend for unknown hash " << int(hash[0]) << '.' + << int(hash[1]) << '.' << int(hash[2]) << '.' << int(hash[3]); + AsyncContext.AssetsLoading[hash] = AssetLoading{ + EnumAssets::Texture, 0, {}, {}, + std::u8string(size, '\0'), 0 + }; co_return; } - case ToClient::L2Resource::Lost: + + ResourceId localId = AM->getOrCreateLocalId(type, domain, key); + + if(domain == "test" + && (type == EnumAssets::Nodestate + || type == EnumAssets::Model + || type == EnumAssets::Texture)) { - uint32_t count = co_await sock.read(); - AssetsBindsChange abc; - - for(size_t iter = 0; iter < count; iter++) { - uint8_t type = co_await sock.read(); - uint32_t id = co_await sock.read(); - - if(type >= (int) EnumAssets::MAX_ENUM) - protocolError(); - - auto localId = AM->unbindServerResource((EnumAssets) type, id); - if(!localId) - continue; - - abc.Lost[(int) type].push_back(*localId); + uint32_t idx = debugResourceLogCount.fetch_add(1); + if(idx < 128) { + LOG.debug() << "AssetsInitSend type=" << assetTypeName(type) + << " id=" << localId + << " key=" << domain << ':' << key + << " size=" << size; } + } - AsyncContext.AssetsBinds.lock()->emplace_back(std::move(abc)); + AsyncContext.AssetsLoading[hash] = AssetLoading{ + type, localId, std::move(domain), std::move(key), + std::u8string(size, '\0'), 0 + }; + + co_return; +} + +coro<> ServerSession::rP_AssetsNextSend(Net::AsyncSocket &sock) { + static std::atomic debugResourceLogCount = 0; + Hash_t hash; + co_await sock.read((std::byte*) hash.data(), hash.size()); + uint32_t size = co_await sock.read(); + + if(!AsyncContext.AssetsLoading.contains(hash)) { + std::vector discard(size); + co_await sock.read(discard.data(), size); co_return; } - case ToClient::L2Resource::InitResSend: - { - uint32_t size = co_await sock.read(); - Hash_t hash; - co_await sock.read((std::byte*) hash.data(), hash.size()); - ResourceId id = co_await sock.read(); - EnumAssets type = (EnumAssets) co_await sock.read(); - if(type >= EnumAssets::MAX_ENUM) - protocolError(); + AssetLoading& al = AsyncContext.AssetsLoading.at(hash); + if(al.Data.size() - al.Offset < size) + MAKE_ERROR("Несоответствие ожидаемого размера ресурса"); - std::string domain = co_await sock.read(); - std::string key = co_await sock.read(); - ResourceId localId = 0; - if(auto mapped = AM->getLocalIdFromServer(type, id)) { - localId = *mapped; - } else { - localId = AM->getId(type, domain, key); - AM->bindServerResource(type, id, domain, key, hash, {}); - } + co_await sock.read((std::byte*) al.Data.data() + al.Offset, size); + al.Offset += size; - if(domain == "test" - && (type == EnumAssets::Nodestate - || type == EnumAssets::Model - || type == EnumAssets::Texture)) + if(al.Offset != al.Data.size()) + co_return; + + if(!al.Domain.empty() || !al.Key.empty()) { + if(al.Domain == "test" + && (al.Type == EnumAssets::Nodestate + || al.Type == EnumAssets::Model + || al.Type == EnumAssets::Texture)) { uint32_t idx = debugResourceLogCount.fetch_add(1); if(idx < 128) { - LOG.debug() << "InitResSend type=" << assetTypeName(type) - << " id=" << localId - << " key=" << domain << ':' << key - << " size=" << size; + LOG.debug() << "Resource loaded type=" << assetTypeName(al.Type) + << " id=" << al.Id + << " key=" << al.Domain << ':' << al.Key + << " size=" << al.Data.size(); } } - AsyncContext.AssetsLoading[hash] = AssetLoading{ - type, localId, std::move(domain), std::move(key), - std::u8string(size, '\0'), 0 - }; - - co_return; + AsyncContext.LoadedAssets.lock()->emplace_back(AssetEntry{ + .Type = al.Type, + .Id = al.Id, + .Domain = std::move(al.Domain), + .Key = std::move(al.Key), + .Res = std::move(al.Data), + .Hash = hash + }); } - case ToClient::L2Resource::ChunkSend: - { - Hash_t hash; - co_await sock.read((std::byte*) hash.data(), hash.size()); - try { - uint32_t size = co_await sock.read(); - assert(AsyncContext.AssetsLoading.contains(hash)); - AssetLoading& al = AsyncContext.AssetsLoading.at(hash); - if(al.Data.size()-al.Offset < size) - MAKE_ERROR("Несоответствие ожидаемого размера ресурса"); - co_await sock.read((std::byte*) al.Data.data() + al.Offset, size); - al.Offset += size; + AsyncContext.AssetsLoading.erase(AsyncContext.AssetsLoading.find(hash)); - if(al.Offset == al.Data.size()) { - // Ресурс полностью загружен - if(al.Domain == "test" - && (al.Type == EnumAssets::Nodestate - || al.Type == EnumAssets::Model - || al.Type == EnumAssets::Texture)) - { - uint32_t idx = debugResourceLogCount.fetch_add(1); - if(idx < 128) { - LOG.debug() << "Resource loaded type=" << assetTypeName(al.Type) - << " id=" << al.Id - << " key=" << al.Domain << ':' << al.Key - << " size=" << al.Data.size(); + auto iter = std::lower_bound(AsyncContext.AlreadyLoading.begin(), AsyncContext.AlreadyLoading.end(), hash); + if(iter != AsyncContext.AlreadyLoading.end() && *iter == hash) + AsyncContext.AlreadyLoading.erase(iter); + + co_return; +} + +coro<> ServerSession::rP_DefinitionsUpdate(Net::AsyncSocket &sock) { + static std::atomic debugDefLogCount = 0; + uint32_t typeCount = co_await sock.read(); + typeCount = std::min(typeCount, static_cast(EnumDefContent::MAX_ENUM)); + + for(uint32_t type = 0; type < typeCount; ++type) { + uint32_t count = co_await sock.read(); + for(uint32_t i = 0; i < count; ++i) { + ResourceId id = co_await sock.read(); + std::string dataStr = co_await sock.read(); + (void)dataStr; + + if(type == static_cast(EnumDefContent::Node)) { + DefNode_t def; + def.NodestateId = 0; + def.TexId = id; + AsyncContext.ThisTickEntry.Profile_Node_AddOrChange.emplace_back(id, def); + if(id < 32) { + uint32_t idx = debugDefLogCount.fetch_add(1); + if(idx < 64) { + LOG.debug() << "DefNode id=" << id + << " nodestate=" << def.NodestateId + << " tex=" << def.TexId; } } - - AsyncContext.LoadedAssets.lock()->emplace_back(AssetEntry{ - .Type = al.Type, - .Id = al.Id, - .Domain = std::move(al.Domain), - .Key = std::move(al.Key), - .Res = std::move(al.Data), - .Hash = hash - }); - - AsyncContext.AssetsLoading.erase(AsyncContext.AssetsLoading.find(hash)); - - auto iter = std::lower_bound(AsyncContext.AlreadyLoading.begin(), AsyncContext.AlreadyLoading.end(), hash); - if(iter != AsyncContext.AlreadyLoading.end() && *iter == hash) - AsyncContext.AlreadyLoading.erase(iter); - } - } catch(const std::exception& exc) { - std::string err = exc.what(); - int g = 0; - } - - co_return; - } - default: - protocolError(); - } -} - -coro<> ServerSession::rP_Definition(Net::AsyncSocket &sock) { - static std::atomic debugDefLogCount = 0; - uint8_t second = co_await sock.read(); - - switch((ToClient::L2Definition) second) { - case ToClient::L2Definition::World: { - DefWorldId cdId = co_await sock.read(); - - co_return; - } - case ToClient::L2Definition::FreeWorld: { - DefWorldId cdId = co_await sock.read(); - - co_return; - } - case ToClient::L2Definition::Voxel: { - DefVoxelId cdId = co_await sock.read(); - - co_return; - } - case ToClient::L2Definition::FreeVoxel: { - DefVoxelId cdId = co_await sock.read(); - - co_return; - } - case ToClient::L2Definition::Node: - { - DefNode_t def; - DefNodeId id = co_await sock.read(); - ResourceId serverNodestate = co_await sock.read(); - if(auto localId = AM->getLocalIdFromServer(EnumAssets::Nodestate, serverNodestate)) - def.NodestateId = *localId; - else - def.NodestateId = 0; - def.TexId = id; - - if(id < 32) { - uint32_t idx = debugDefLogCount.fetch_add(1); - if(idx < 64) { - LOG.debug() << "DefNode id=" << id - << " nodestate=" << def.NodestateId - << " tex=" << def.TexId; } } + } - AsyncContext.ThisTickEntry.Profile_Node_AddOrChange.emplace_back(id, def); + uint32_t lostCount = co_await sock.read(); + lostCount = std::min(lostCount, static_cast(EnumDefContent::MAX_ENUM)); + for(uint32_t type = 0; type < lostCount; ++type) { + uint32_t count = co_await sock.read(); + for(uint32_t i = 0; i < count; ++i) { + ResourceId id = co_await sock.read(); + if(type == static_cast(EnumDefContent::Node)) + AsyncContext.ThisTickEntry.Profile_Node_Lost.push_back(id); + } + } - co_return; + uint32_t dkCount = co_await sock.read(); + dkCount = std::min(dkCount, static_cast(EnumDefContent::MAX_ENUM)); + for(uint32_t type = 0; type < dkCount; ++type) { + uint32_t count = co_await sock.read(); + for(uint32_t i = 0; i < count; ++i) { + std::string key = co_await sock.read(); + std::string domain = co_await sock.read(); + (void)key; + (void)domain; + } } - case ToClient::L2Definition::FreeNode: - { - DefNodeId id = co_await sock.read(); - AsyncContext.ThisTickEntry.Profile_Node_Lost.push_back(id); - co_return; - } - case ToClient::L2Definition::Portal: - - co_return; - case ToClient::L2Definition::FreePortal: - - co_return; - case ToClient::L2Definition::Entity: - { - DefEntityId id = co_await sock.read(); - DefEntityInfo def; - AsyncContext.ThisTickEntry.Profile_Entity_AddOrChange.emplace_back(id, def); - co_return; - } - case ToClient::L2Definition::FreeEntity: - { - DefEntityId id = co_await sock.read(); - AsyncContext.ThisTickEntry.Profile_Entity_Lost.push_back(id); - co_return; - } - default: - protocolError(); - } + co_return; } -coro<> ServerSession::rP_Content(Net::AsyncSocket &sock) { - uint8_t second = co_await sock.read(); +coro<> ServerSession::rP_ChunkVoxels(Net::AsyncSocket &sock) { + WorldId_t wcId = co_await sock.read(); + Pos::GlobalChunk pos; + pos.unpack(co_await sock.read()); - switch((ToClient::L2Content) second) { - case ToClient::L2Content::World: { - WorldId_t wId = co_await sock.read(); - AsyncContext.ThisTickEntry.Worlds_AddOrChange.emplace_back(wId, nullptr); - co_return; - } - case ToClient::L2Content::RemoveWorld: { - WorldId_t wId = co_await sock.read(); - AsyncContext.ThisTickEntry.Worlds_Lost.push_back(wId); - co_return; - } - case ToClient::L2Content::Portal: + uint32_t compressedSize = co_await sock.read(); + assert(compressedSize <= std::pow(2, 24)); + std::u8string compressed(compressedSize, '\0'); + co_await sock.read((std::byte*) compressed.data(), compressedSize); - co_return; - case ToClient::L2Content::RemovePortal: - - co_return; - case ToClient::L2Content::Entity: - { - EntityId_t id = co_await sock.read(); - DefEntityId defId = co_await sock.read(); - WorldId_t worldId = co_await sock.read(); + AsyncContext.ThisTickEntry.Chunks_AddOrChange_Voxel[wcId].insert({pos, std::move(compressed)}); + co_return; +} - Pos::Object pos; - pos.x = co_await sock.read(); - pos.y = co_await sock.read(); - pos.z = co_await sock.read(); +coro<> ServerSession::rP_ChunkNodes(Net::AsyncSocket &sock) { + WorldId_t wcId = co_await sock.read(); + Pos::GlobalChunk pos; + pos.unpack(co_await sock.read()); - ToServer::PacketQuat q; - for(int iter = 0; iter < 5; iter++) - q.Data[iter] = co_await sock.read(); + uint32_t compressedSize = co_await sock.read(); + assert(compressedSize <= std::pow(2, 24)); + std::u8string compressed(compressedSize, '\0'); + co_await sock.read((std::byte*) compressed.data(), compressedSize); - EntityInfo info; - info.DefId = defId; - info.WorldId = worldId; - info.Pos = pos; - info.Quat = q.toQuat(); + AsyncContext.ThisTickEntry.Chunks_AddOrChange_Node[wcId].insert({pos, std::move(compressed)}); + co_return; +} - AsyncContext.ThisTickEntry.Entity_AddOrChange.emplace_back(id, info); - co_return; - } - case ToClient::L2Content::RemoveEntity: - { - EntityId_t id = co_await sock.read(); - AsyncContext.ThisTickEntry.Entity_Lost.push_back(id); - co_return; - } - case ToClient::L2Content::ChunkVoxels: - { - WorldId_t wcId = co_await sock.read(); - Pos::GlobalChunk pos; - pos.unpack(co_await sock.read()); +coro<> ServerSession::rP_ChunkLightPrism(Net::AsyncSocket &sock) { + (void)sock; + co_return; +} - uint32_t compressedSize = co_await sock.read(); - assert(compressedSize <= std::pow(2, 24)); - std::u8string compressed(compressedSize, '\0'); - co_await sock.read((std::byte*) compressed.data(), compressedSize); +coro<> ServerSession::rP_RemoveRegion(Net::AsyncSocket &sock) { + WorldId_t wcId = co_await sock.read(); + Pos::GlobalRegion pos; + pos.unpack(co_await sock.read()); - AsyncContext.ThisTickEntry.Chunks_AddOrChange_Node[wcId].insert({pos, std::move(compressed)}); + AsyncContext.ThisTickEntry.Regions_Lost[wcId].push_back(pos); + co_return; +} - co_return; - } +coro<> ServerSession::rP_Tick(Net::AsyncSocket &sock) { + (void)sock; + AsyncContext.TickSequence.lock()->push_back(std::move(AsyncContext.ThisTickEntry)); + AsyncContext.ThisTickEntry = {}; + co_return; +} - case ToClient::L2Content::ChunkNodes: - { - WorldId_t wcId = co_await sock.read(); - Pos::GlobalChunk pos; - pos.unpack(co_await sock.read()); +coro<> ServerSession::rP_TestLinkCameraToEntity(Net::AsyncSocket &sock) { + (void) sock; + co_return; +} - uint32_t compressedSize = co_await sock.read(); - assert(compressedSize <= std::pow(2, 24)); - std::u8string compressed(compressedSize, '\0'); - co_await sock.read((std::byte*) compressed.data(), compressedSize); - - AsyncContext.ThisTickEntry.Chunks_AddOrChange_Node[wcId].insert({pos, std::move(compressed)}); - - co_return; - } - case ToClient::L2Content::ChunkLightPrism: - - co_return; - case ToClient::L2Content::RemoveRegion: { - WorldId_t wcId = co_await sock.read(); - Pos::GlobalRegion pos; - pos.unpack(co_await sock.read()); - - AsyncContext.ThisTickEntry.Regions_Lost[wcId].push_back(pos); - - co_return; - } - default: - protocolError(); - } +coro<> ServerSession::rP_TestUnlinkCamera(Net::AsyncSocket &sock) { + (void) sock; + co_return; } } diff --git a/Src/Client/ServerSession.hpp b/Src/Client/ServerSession.hpp index 3600583..b576d45 100644 --- a/Src/Client/ServerSession.hpp +++ b/Src/Client/ServerSession.hpp @@ -191,10 +191,19 @@ private: coro<> run(AsyncUseControl::Lock); void protocolError(); coro<> readPacket(Net::AsyncSocket &sock); - coro<> rP_System(Net::AsyncSocket &sock); - coro<> rP_Resource(Net::AsyncSocket &sock); - coro<> rP_Definition(Net::AsyncSocket &sock); - coro<> rP_Content(Net::AsyncSocket &sock); + coro<> rP_Disconnect(Net::AsyncSocket &sock); + coro<> rP_AssetsBindDK(Net::AsyncSocket &sock); + coro<> rP_AssetsBindHH(Net::AsyncSocket &sock); + coro<> rP_AssetsInitSend(Net::AsyncSocket &sock); + coro<> rP_AssetsNextSend(Net::AsyncSocket &sock); + coro<> rP_DefinitionsUpdate(Net::AsyncSocket &sock); + coro<> rP_ChunkVoxels(Net::AsyncSocket &sock); + coro<> rP_ChunkNodes(Net::AsyncSocket &sock); + coro<> rP_ChunkLightPrism(Net::AsyncSocket &sock); + coro<> rP_RemoveRegion(Net::AsyncSocket &sock); + coro<> rP_Tick(Net::AsyncSocket &sock); + coro<> rP_TestLinkCameraToEntity(Net::AsyncSocket &sock); + coro<> rP_TestUnlinkCamera(Net::AsyncSocket &sock); // Нужен сокет, на котором только что был согласован игровой протокол (asyncInitGameProtocol) diff --git a/Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp b/Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp index e8e379c..5f31311 100644 --- a/Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp +++ b/Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp @@ -1,1816 +1,3 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// ======================== -// External texture view -// ======================== -struct Texture { - uint32_t Width, Height; - const uint32_t* Pixels; // assumed 0xAARRGGBB -}; - -// ======================== -// Bytecode words are uint8_t (1 byte machine word) -// TexId is u24 (3 bytes, little-endian) -// Subprogram refs use off24/len24 in BYTES (<=65535) -// ======================== -class TexturePipelineProgram { -public: - using Word = uint8_t; - - enum AnimFlags : Word { - AnimSmooth = 1u << 0, - AnimHorizontal = 1u << 1 - }; - - static constexpr uint16_t DefaultAnimFpsQ = uint16_t(8u * 256u); - static constexpr size_t MaxCodeBytes = (1u << 16) + 1u; // 65537 - - struct OwnedTexture { - uint32_t Width = 0, Height = 0; - std::vector Pixels; - Texture view() const { return Texture{Width, Height, Pixels.data()}; } - }; - - using IdResolverFunc = std::function(std::string_view)>; - using TextureProviderFunc = std::function(uint32_t)>; - - // Patch point to 3 consecutive bytes where u24 texId lives (b0,b1,b2) - struct Patch { - size_t ByteIndex0 = 0; // Code_[i], Code_[i+1], Code_[i+2] - std::string Name; - }; - - bool compile(std::string src, std::string* err = nullptr) { - Source_ = std::move(src); - Code_.clear(); - Patches_.clear(); - PendingSub_.clear(); - return _parseProgram(err); - } - - bool link(const IdResolverFunc& resolver, std::string* err = nullptr) { - for (const auto& p : Patches_) { - auto idOpt = resolver(p.Name); - if(!idOpt) { - if(err) *err = "Не удалось разрешить имя текстуры: " + p.Name; - return false; - } - uint32_t id = *idOpt; - if(id >= (1u << 24)) { - if(err) *err = "TexId выходит за 24 бита (u24): " + p.Name + " => " + std::to_string(id); - return false; - } - if(p.ByteIndex0 + 2 >= Code_.size()) { - if(err) *err = "Внутренняя ошибка: применение идентификатора выходит за рамки кода"; - return false; - } - Code_[p.ByteIndex0 + 0] = uint8_t(id & 0xFFu); - Code_[p.ByteIndex0 + 1] = uint8_t((id >> 8) & 0xFFu); - Code_[p.ByteIndex0 + 2] = uint8_t((id >> 16) & 0xFFu); - } - return true; - } - - bool bake(const TextureProviderFunc& provider, OwnedTexture& out, std::string* err = nullptr) const { - return bake(provider, out, 0.0, err); - } - - bool bake(const TextureProviderFunc& provider, OwnedTexture& out, double timeSeconds, std::string* err = nullptr) const { - VM vm(provider); - return vm.run(Code_, out, timeSeconds, err); - } - - const std::vector& words() const { return Code_; } - const std::vector& patches() const { return Patches_; } - - std::vector toBytes() const { return Code_; } - - struct AnimSpec { - uint32_t TexId = 0; - bool HasTexId = false; - uint16_t FrameW = 0; - uint16_t FrameH = 0; - uint16_t FrameCount = 0; - uint16_t FpsQ = 0; - uint16_t Flags = 0; - }; - - static std::vector extractAnimationSpecs(const Word* code, size_t size) { - std::vector specs; - if(!code || size == 0) { - return specs; - } - - struct Range { - size_t Start = 0; - size_t End = 0; - }; - - std::vector visited; - - auto read8 = [&](size_t& ip, uint8_t& out)->bool{ - if(ip >= size) return false; - out = code[ip++]; - return true; - }; - auto read16 = [&](size_t& ip, uint16_t& out)->bool{ - if(ip + 1 >= size) return false; - out = uint16_t(code[ip]) | (uint16_t(code[ip + 1]) << 8); - ip += 2; - return true; - }; - auto read24 = [&](size_t& ip, uint32_t& out)->bool{ - if(ip + 2 >= size) return false; - out = uint32_t(code[ip]) | - (uint32_t(code[ip + 1]) << 8) | - (uint32_t(code[ip + 2]) << 16); - ip += 3; - return true; - }; - auto read32 = [&](size_t& ip, uint32_t& out)->bool{ - if(ip + 3 >= size) return false; - out = uint32_t(code[ip]) | - (uint32_t(code[ip + 1]) << 8) | - (uint32_t(code[ip + 2]) << 16) | - (uint32_t(code[ip + 3]) << 24); - ip += 4; - return true; - }; - - struct SrcMeta { - SrcKind Kind = SrcKind::TexId; - uint32_t TexId = 0; - uint32_t Off = 0; - uint32_t Len = 0; - }; - - auto readSrc = [&](size_t& ip, SrcMeta& out)->bool{ - uint8_t kind = 0; - if(!read8(ip, kind)) return false; - out.Kind = static_cast(kind); - if(out.Kind == SrcKind::TexId) { - return read24(ip, out.TexId); - } - if(out.Kind == SrcKind::Sub) { - return read24(ip, out.Off) && read24(ip, out.Len); - } - return false; - }; - - auto scan = [&](auto&& self, size_t start, size_t end) -> void { - if(start >= end || end > size) { - return; - } - for(const auto& r : visited) { - if(r.Start == start && r.End == end) { - return; - } - } - visited.push_back(Range{start, end}); - - size_t ip = start; - while(ip < end) { - uint8_t opByte = 0; - if(!read8(ip, opByte)) return; - Op op = static_cast(opByte); - switch(op) { - case Op::End: - return; - - case Op::Base_Tex: { - SrcMeta src{}; - if(!readSrc(ip, src)) return; - if(src.Kind == SrcKind::Sub) { - size_t subStart = src.Off; - size_t subEnd = subStart + src.Len; - if(subStart < subEnd && subEnd <= size) { - self(self, subStart, subEnd); - } - } - } break; - - case Op::Base_Fill: { - uint16_t tmp16 = 0; - uint32_t tmp32 = 0; - if(!read16(ip, tmp16)) return; - if(!read16(ip, tmp16)) return; - if(!read32(ip, tmp32)) return; - } break; - - case Op::Base_Anim: { - SrcMeta src{}; - if(!readSrc(ip, src)) return; - uint16_t frameW = 0; - uint16_t frameH = 0; - uint16_t frameCount = 0; - uint16_t fpsQ = 0; - uint8_t flags = 0; - if(!read16(ip, frameW)) return; - if(!read16(ip, frameH)) return; - if(!read16(ip, frameCount)) return; - if(!read16(ip, fpsQ)) return; - if(!read8(ip, flags)) return; - - if(src.Kind == SrcKind::TexId) { - AnimSpec spec{}; - spec.TexId = src.TexId; - spec.HasTexId = true; - spec.FrameW = frameW; - spec.FrameH = frameH; - spec.FrameCount = frameCount; - spec.FpsQ = fpsQ; - spec.Flags = flags; - specs.push_back(spec); - } else if(src.Kind == SrcKind::Sub) { - size_t subStart = src.Off; - size_t subEnd = subStart + src.Len; - if(subStart < subEnd && subEnd <= size) { - self(self, subStart, subEnd); - } - } - } break; - - case Op::Resize: { - uint16_t tmp16 = 0; - if(!read16(ip, tmp16)) return; - if(!read16(ip, tmp16)) return; - } break; - - case Op::Transform: - case Op::Opacity: - case Op::Invert: - if(!read8(ip, opByte)) return; - break; - - case Op::NoAlpha: - case Op::Brighten: - break; - - case Op::MakeAlpha: - if(ip + 2 >= size) return; - ip += 3; - break; - - case Op::Contrast: - if(ip + 1 >= size) return; - ip += 2; - break; - - case Op::Multiply: - case Op::Screen: { - uint32_t tmp32 = 0; - if(!read32(ip, tmp32)) return; - } break; - - case Op::Colorize: { - uint32_t tmp32 = 0; - if(!read32(ip, tmp32)) return; - if(!read8(ip, opByte)) return; - } break; - - case Op::Anim: { - uint16_t frameW = 0; - uint16_t frameH = 0; - uint16_t frameCount = 0; - uint16_t fpsQ = 0; - uint8_t flags = 0; - if(!read16(ip, frameW)) return; - if(!read16(ip, frameH)) return; - if(!read16(ip, frameCount)) return; - if(!read16(ip, fpsQ)) return; - if(!read8(ip, flags)) return; - - AnimSpec spec{}; - spec.HasTexId = false; - spec.FrameW = frameW; - spec.FrameH = frameH; - spec.FrameCount = frameCount; - spec.FpsQ = fpsQ; - spec.Flags = flags; - specs.push_back(spec); - } break; - - case Op::Overlay: - case Op::Mask: { - SrcMeta src{}; - if(!readSrc(ip, src)) return; - if(src.Kind == SrcKind::Sub) { - size_t subStart = src.Off; - size_t subEnd = subStart + src.Len; - if(subStart < subEnd && subEnd <= size) { - self(self, subStart, subEnd); - } - } - } break; - - case Op::LowPart: { - if(!read8(ip, opByte)) return; - SrcMeta src{}; - if(!readSrc(ip, src)) return; - if(src.Kind == SrcKind::Sub) { - size_t subStart = src.Off; - size_t subEnd = subStart + src.Len; - if(subStart < subEnd && subEnd <= size) { - self(self, subStart, subEnd); - } - } - } break; - - case Op::Combine: { - uint16_t w = 0, h = 0, n = 0; - if(!read16(ip, w)) return; - if(!read16(ip, h)) return; - if(!read16(ip, n)) return; - for(uint16_t i = 0; i < n; ++i) { - uint16_t tmp16 = 0; - if(!read16(ip, tmp16)) return; - if(!read16(ip, tmp16)) return; - SrcMeta src{}; - if(!readSrc(ip, src)) return; - if(src.Kind == SrcKind::Sub) { - size_t subStart = src.Off; - size_t subEnd = subStart + src.Len; - if(subStart < subEnd && subEnd <= size) { - self(self, subStart, subEnd); - } - } - } - (void)w; (void)h; - } break; - - default: - return; - } - } - }; - - scan(scan, 0, size); - return specs; - } - - static std::vector extractAnimationSpecs(const std::vector& code) { - return extractAnimationSpecs(code.data(), code.size()); - } - - void fromBytes(std::vector bytes) { - Code_ = std::move(bytes); - Patches_.clear(); - Source_.clear(); - PendingSub_.clear(); - } - -private: - // ======================== - // Byte helpers (little-endian) - // ======================== - static inline uint16_t _rd16(const std::vector& c, size_t& ip) { - uint16_t v = uint16_t(c[ip]) | (uint16_t(c[ip+1]) << 8); - ip += 2; - return v; - } - static inline uint32_t _rd24(const std::vector& c, size_t& ip) { - uint32_t v = uint32_t(c[ip]) | (uint32_t(c[ip+1]) << 8) | (uint32_t(c[ip+2]) << 16); - ip += 3; - return v; - } - static inline uint32_t _rd32(const std::vector& c, size_t& ip) { - uint32_t v = uint32_t(c[ip]) | - (uint32_t(c[ip+1]) << 8) | - (uint32_t(c[ip+2]) << 16) | - (uint32_t(c[ip+3]) << 24); - ip += 4; - return v; - } - - static inline void _wr8 (std::vector& o, uint32_t v){ o.push_back(uint8_t(v & 0xFFu)); } - static inline void _wr16(std::vector& o, uint32_t v){ - o.push_back(uint8_t(v & 0xFFu)); - o.push_back(uint8_t((v >> 8) & 0xFFu)); - } - static inline void _wr24(std::vector& o, uint32_t v){ - o.push_back(uint8_t(v & 0xFFu)); - o.push_back(uint8_t((v >> 8) & 0xFFu)); - o.push_back(uint8_t((v >> 16) & 0xFFu)); - } - static inline void _wr32(std::vector& o, uint32_t v){ - o.push_back(uint8_t(v & 0xFFu)); - o.push_back(uint8_t((v >> 8) & 0xFFu)); - o.push_back(uint8_t((v >> 16) & 0xFFu)); - o.push_back(uint8_t((v >> 24) & 0xFFu)); - } - - // ======================== - // SrcRef encoding in bytes (variable length) - // kind(1) + payload - // TexId: id24(3) => total 4 - // Sub : off16(3) + len16(3) => total 7 - // ======================== - enum class SrcKind : uint8_t { TexId = 0, Sub = 1 }; - - struct SrcRef { - SrcKind Kind{}; - uint32_t TexId24 = 0; // for TexId - uint16_t Off24 = 0; // for Sub - uint16_t Len24 = 0; // for Sub - }; - - // ======================== - // Opcodes (1 byte) - // ======================== - enum class Op : uint8_t { - End = 0, - - Base_Tex = 1, // SrcRef(TexId) - Base_Fill = 2, // w16, h16, color32 - Base_Anim = 3, // SrcRef(TexId), frameW16, frameH16, frames16, fpsQ16, flags8 - - Resize = 10, // w16, h16 - Transform = 11, // t8 - Opacity = 12, // a8 - NoAlpha = 13, // - - MakeAlpha = 14, // rgb24 (3 bytes) RR,GG,BB - Invert = 15, // mask8 - Brighten = 16, // - - Contrast = 17, // cBias8, bBias8 (bias-127) - Multiply = 18, // color32 - Screen = 19, // color32 - Colorize = 20, // color32, ratio8 - Anim = 21, // frameW16, frameH16, frames16, fpsQ16, flags8 - - Overlay = 30, // SrcRef (var) - Mask = 31, // SrcRef (var) - LowPart = 32, // percent8, SrcRef (var) - - Combine = 40 // w16,h16,n16 then n*(x16,y16,SrcRef) (если понадобится — допишем DSL) - }; - - // ======================== - // Pixel helpers (assume 0xAARRGGBB) - // ======================== - static inline uint8_t _a(uint32_t c){ return uint8_t((c >> 24) & 0xFF); } - static inline uint8_t _r(uint32_t c){ return uint8_t((c >> 16) & 0xFF); } - static inline uint8_t _g(uint32_t c){ return uint8_t((c >> 8) & 0xFF); } - static inline uint8_t _b(uint32_t c){ return uint8_t((c >> 0) & 0xFF); } - static inline uint32_t _pack(uint8_t a,uint8_t r,uint8_t g,uint8_t b){ - return (uint32_t(a)<<24)|(uint32_t(r)<<16)|(uint32_t(g)<<8)|(uint32_t(b)); - } - static inline uint8_t _clampu8(int v){ return uint8_t(std::min(255, std::max(0, v))); } - - // ======================== - // VM (executes bytes) - // ======================== - struct Image { - uint32_t W=0,H=0; - std::vector Px; - }; - - class VM { - public: - using TextureProvider = TexturePipelineProgram::TextureProviderFunc; - - explicit VM(TextureProvider provider) : Provider_(std::move(provider)) {} - - bool run(const std::vector& code, OwnedTexture& out, double timeSeconds, std::string* err) { - if(code.empty()) { if(err) *err="Empty bytecode"; return false; } - - Image cur; - std::unordered_map texCache; - std::unordered_map subCache; // key = (off<<24) | len - - size_t ip = 0; - - auto need = [&](size_t n)->bool{ - if(ip + n > code.size()) { if(err) *err="Bytecode truncated"; return false; } - return true; - }; - - while(true) { - if(!need(1)) return false; - Op op = static_cast(code[ip++]); - if(op == Op::End) break; - - switch(op) { - case Op::Base_Tex: { - SrcRef src; - if(!_readSrc(code, ip, src, err)) return false; - if(src.Kind != SrcKind::TexId) return _bad(err, "Base_Tex must be TexId"); - cur = _loadTex(src.TexId24, texCache, err); - if(cur.W == 0) return false; - } break; - - case Op::Base_Fill: { - if(!need(2+2+4)) return false; - uint32_t w = _rd16(code, ip); - uint32_t h = _rd16(code, ip); - uint32_t color = _rd32(code, ip); - cur = _makeSolid(w, h, color); - } break; - - case Op::Base_Anim: { - SrcRef src; - if(!_readSrc(code, ip, src, err)) return false; - if(src.Kind != SrcKind::TexId) return _bad(err, "Base_Anim must be TexId"); - if(!need(2+2+2+2+1)) return false; - - uint32_t frameW = _rd16(code, ip); - uint32_t frameH = _rd16(code, ip); - uint32_t frameCount = _rd16(code, ip); - uint32_t fpsQ = _rd16(code, ip); - uint32_t flags = code[ip++]; - - Image sheet = _loadTex(src.TexId24, texCache, err); - if(sheet.W == 0) return false; - - uint32_t fw = frameW ? frameW : sheet.W; - uint32_t fh = frameH ? frameH : sheet.H; - if(fw == 0 || fh == 0) return _bad(err, "Base_Anim invalid frame size"); - - bool horizontal = (flags & AnimHorizontal) != 0; - if(frameCount == 0) { - uint32_t avail = horizontal ? (sheet.W / fw) : (sheet.H / fh); - frameCount = std::max(1u, avail); - } - - uint32_t fpsQv = fpsQ ? fpsQ : DefaultAnimFpsQ; - double fps = double(fpsQv) / 256.0; - double frameTime = timeSeconds * fps; - if(frameTime < 0.0) frameTime = 0.0; - - uint32_t frameIndex = frameCount ? (uint32_t(frameTime) % frameCount) : 0u; - double frac = frameTime - std::floor(frameTime); - - cur = _cropFrame(sheet, frameIndex, fw, fh, horizontal); - - if(flags & AnimSmooth) { - uint32_t nextIndex = frameCount ? ((frameIndex + 1u) % frameCount) : 0u; - Image next = _cropFrame(sheet, nextIndex, fw, fh, horizontal); - _lerp(cur, next, frac); - } - } break; - - case Op::Anim: { - if(!cur.W || !cur.H) return _bad(err, "Anim requires base image"); - if(!need(2+2+2+2+1)) return false; - - uint32_t frameW = _rd16(code, ip); - uint32_t frameH = _rd16(code, ip); - uint32_t frameCount = _rd16(code, ip); - uint32_t fpsQ = _rd16(code, ip); - uint32_t flags = code[ip++]; - - const Image& sheet = cur; - uint32_t fw = frameW ? frameW : sheet.W; - uint32_t fh = frameH ? frameH : sheet.H; - if(fw == 0 || fh == 0) return _bad(err, "Anim invalid frame size"); - - bool horizontal = (flags & AnimHorizontal) != 0; - if(frameCount == 0) { - uint32_t avail = horizontal ? (sheet.W / fw) : (sheet.H / fh); - frameCount = std::max(1u, avail); - } - - uint32_t fpsQv = fpsQ ? fpsQ : DefaultAnimFpsQ; - double fps = double(fpsQv) / 256.0; - double frameTime = timeSeconds * fps; - if(frameTime < 0.0) frameTime = 0.0; - - uint32_t frameIndex = frameCount ? (uint32_t(frameTime) % frameCount) : 0u; - double frac = frameTime - std::floor(frameTime); - - cur = _cropFrame(sheet, frameIndex, fw, fh, horizontal); - if(flags & AnimSmooth) { - uint32_t nextIndex = frameCount ? ((frameIndex + 1u) % frameCount) : 0u; - Image next = _cropFrame(sheet, nextIndex, fw, fh, horizontal); - _lerp(cur, next, frac); - } - } break; - - case Op::Overlay: { - SrcRef src; - if(!_readSrc(code, ip, src, err)) return false; - Image over = _loadSrc(code, src, texCache, subCache, timeSeconds, err); - if(over.W == 0) return false; - if(!cur.W) { cur = std::move(over); break; } - over = _resizeNN_ifNeeded(over, cur.W, cur.H); - _alphaOver(cur, over); - } break; - - case Op::Mask: { - SrcRef src; - if(!_readSrc(code, ip, src, err)) return false; - Image m = _loadSrc(code, src, texCache, subCache, timeSeconds, err); - if(m.W == 0) return false; - if(!cur.W) return _bad(err, "Mask requires base image"); - m = _resizeNN_ifNeeded(m, cur.W, cur.H); - _applyMask(cur, m); - } break; - - case Op::LowPart: { - if(!need(1)) return false; - uint32_t pct = std::min(100u, uint32_t(code[ip++])); - SrcRef src; - if(!_readSrc(code, ip, src, err)) return false; - Image over = _loadSrc(code, src, texCache, subCache, timeSeconds, err); - if(over.W == 0) return false; - if(!cur.W) return _bad(err, "LowPart requires base image"); - over = _resizeNN_ifNeeded(over, cur.W, cur.H); - _lowpart(cur, over, pct); - } break; - - case Op::Resize: { - if(!cur.W) return _bad(err, "Resize requires base image"); - if(!need(2+2)) return false; - uint32_t w = _rd16(code, ip); - uint32_t h = _rd16(code, ip); - cur = _resizeNN(cur, w, h); - } break; - - case Op::Transform: { - if(!cur.W) return _bad(err, "Transform requires base image"); - if(!need(1)) return false; - uint32_t t = code[ip++] & 7u; - cur = _transform(cur, t); - } break; - - case Op::Opacity: { - if(!cur.W) return _bad(err, "Opacity requires base image"); - if(!need(1)) return false; - uint32_t a = code[ip++] & 0xFFu; - _opacity(cur, uint8_t(a)); - } break; - - case Op::NoAlpha: { - if(!cur.W) return _bad(err, "NoAlpha requires base image"); - _noAlpha(cur); - } break; - - case Op::MakeAlpha: { - if(!cur.W) return _bad(err, "MakeAlpha requires base image"); - if(!need(3)) return false; - uint32_t rr = code[ip++], gg = code[ip++], bb = code[ip++]; - uint32_t rgb24 = (rr << 16) | (gg << 8) | bb; - _makeAlpha(cur, rgb24); - } break; - - case Op::Invert: { - if(!cur.W) return _bad(err, "Invert requires base image"); - if(!need(1)) return false; - uint32_t mask = code[ip++] & 0xFu; - _invert(cur, mask); - } break; - - case Op::Brighten: { - if(!cur.W) return _bad(err, "Brighten requires base image"); - _brighten(cur); - } break; - - case Op::Contrast: { - if(!cur.W) return _bad(err, "Contrast requires base image"); - if(!need(2)) return false; - int c = int(code[ip++]) - 127; - int b = int(code[ip++]) - 127; - _contrast(cur, c, b); - } break; - - case Op::Multiply: { - if(!cur.W) return _bad(err, "Multiply requires base image"); - if(!need(4)) return false; - uint32_t color = _rd32(code, ip); - _multiply(cur, color); - } break; - - case Op::Screen: { - if(!cur.W) return _bad(err, "Screen requires base image"); - if(!need(4)) return false; - uint32_t color = _rd32(code, ip); - _screen(cur, color); - } break; - - case Op::Colorize: { - if(!cur.W) return _bad(err, "Colorize requires base image"); - if(!need(4+1)) return false; - uint32_t color = _rd32(code, ip); - uint32_t ratio = code[ip++] & 0xFFu; - _colorize(cur, color, uint8_t(ratio)); - } break; - - default: - return _bad(err, "Unknown opcode (no skip table in this minimal VM)"); - } - } - - out.Width = cur.W; - out.Height = cur.H; - out.Pixels = std::move(cur.Px); - return true; - } - - private: - TextureProvider Provider_; - - static bool _bad(std::string* err, const char* msg){ if(err) *err = msg; return false; } - - static bool _readSrc(const std::vector& code, size_t& ip, SrcRef& out, std::string* err) { - if(ip >= code.size()) return _bad(err, "Bytecode truncated (SrcRef.kind)"); - out.Kind = static_cast(code[ip++]); - if(out.Kind == SrcKind::TexId) { - if(ip + 3 > code.size()) return _bad(err, "Bytecode truncated (TexId24)"); - out.TexId24 = _rd24(code, ip); - out.Off24 = 0; out.Len24 = 0; - return true; - } - if(out.Kind == SrcKind::Sub) { - if(ip + 6 > code.size()) return _bad(err, "Bytecode truncated (Sub off/len)"); - out.Off24 = _rd24(code, ip); - out.Len24 = _rd24(code, ip); - out.TexId24 = 0; - return true; - } - return _bad(err, "Unknown SrcKind"); - } - - Image _loadTex(uint32_t id, std::unordered_map& cache, std::string* err) { - auto it = cache.find(id); - if(it != cache.end()) return it->second; - - auto t = Provider_(id); - if(!t || !t->Pixels || !t->Width || !t->Height) { - if(err) *err = "Texture id not found: " + std::to_string(id); - return {}; - } - Image img; - img.W = t->Width; img.H = t->Height; - img.Px.assign(t->Pixels, t->Pixels + size_t(img.W)*size_t(img.H)); - cache.emplace(id, img); - return img; - } - - Image _loadSub(const std::vector& code, - uint32_t off, uint32_t len, - std::unordered_map& /*texCache*/, - std::unordered_map& subCache, - double timeSeconds, - std::string* err) { - uint64_t key = (uint32_t(off) << 24) | uint32_t(len); - auto it = subCache.find(key); - if(it != subCache.end()) return it->second; - - size_t start = size_t(off); - size_t end = start + size_t(len); - if(end > code.size()) { if(err) *err="Subprogram out of range"; return {}; } - - std::vector slice(code.begin()+start, code.begin()+end); - OwnedTexture tmp; - VM nested(Provider_); - if(!nested.run(slice, tmp, timeSeconds, err)) return {}; - - Image img; - img.W = tmp.Width; img.H = tmp.Height; img.Px = std::move(tmp.Pixels); - subCache.emplace(key, img); - return img; - } - - Image _loadSrc(const std::vector& code, - const SrcRef& src, - std::unordered_map& texCache, - std::unordered_map& subCache, - double timeSeconds, - std::string* err) { - if(src.Kind == SrcKind::TexId) return _loadTex(src.TexId24, texCache, err); - if(src.Kind == SrcKind::Sub) return _loadSub(code, src.Off24, src.Len24, texCache, subCache, timeSeconds, err); - if(err) *err = "Unknown SrcKind"; - return {}; - } - - // ---- image ops (как в исходнике) ---- - static Image _makeSolid(uint32_t w, uint32_t h, uint32_t color) { - Image img; img.W=w; img.H=h; - img.Px.assign(size_t(w)*size_t(h), color); - return img; - } - - static Image _resizeNN(const Image& src, uint32_t nw, uint32_t nh) { - Image dst; dst.W=nw; dst.H=nh; - dst.Px.resize(size_t(nw)*size_t(nh)); - for(uint32_t y=0;y= sheet.H) continue; - for(uint32_t x = 0; x < fw; ++x) { - uint32_t sx = baseX + x; - if(sx >= sheet.W) continue; - out.Px[size_t(y) * fw + x] = sheet.Px[size_t(sy) * sheet.W + sx]; - } - } - return out; - } - - static void _lerp(Image& base, const Image& over, double t) { - if(t <= 0.0) return; - if(t >= 1.0) { base = over; return; } - if(base.W != over.W || base.H != over.H) return; - - const size_t n = base.Px.size(); - for(size_t i = 0; i < n; ++i) { - uint32_t a = base.Px[i]; - uint32_t b = over.Px[i]; - int ar = _r(a), ag = _g(a), ab = _b(a), aa = _a(a); - int br = _r(b), bg = _g(b), bb = _b(b), ba = _a(b); - - uint8_t rr = _clampu8(int(ar + (br - ar) * t)); - uint8_t rg = _clampu8(int(ag + (bg - ag) * t)); - uint8_t rb = _clampu8(int(ab + (bb - ab) * t)); - uint8_t ra = _clampu8(int(aa + (ba - aa) * t)); - - base.Px[i] = _pack(ra, rr, rg, rb); - } - } - - static void _alphaOver(Image& base, const Image& over) { - const size_t n = base.Px.size(); - for(size_t i=0;i(255, (outRp * 255) / outA)); - outG = uint8_t(std::min(255, (outGp * 255) / outA)); - outB = uint8_t(std::min(255, (outBp * 255) / outA)); - } - base.Px[i] = _pack(uint8_t(outA), outR, outG, outB); - } - } - - static void _applyMask(Image& base, const Image& mask) { - const size_t n = base.Px.size(); - for(size_t i=0;i> 16) & 0xFF); - uint8_t gg = uint8_t((rgb24 >> 8) & 0xFF); - uint8_t bb = uint8_t((rgb24 >> 0) & 0xFF); - for(auto& p : img.Px) { - if(_r(p)==rr && _g(p)==gg && _b(p)==bb) p = _pack(0, _r(p), _g(p), _b(p)); - } - } - static void _invert(Image& img, uint32_t maskBits) { - for(auto& p : img.Px) { - uint8_t a=_a(p), r=_r(p), g=_g(p), b=_b(p); - if(maskBits & 1u) r = 255 - r; - if(maskBits & 2u) g = 255 - g; - if(maskBits & 4u) b = 255 - b; - if(maskBits & 8u) a = 255 - a; - p = _pack(a,r,g,b); - } - } - static void _brighten(Image& img) { - for(auto& p : img.Px) { - int r = _r(p), g = _g(p), b = _b(p); - r = r + (255 - r) / 3; - g = g + (255 - g) / 3; - b = b + (255 - b) / 3; - p = _pack(_a(p), _clampu8(r), _clampu8(g), _clampu8(b)); - } - } - static void _contrast(Image& img, int c, int br) { - double C = double(std::max(-127, std::min(127, c))); - double factor = (259.0 * (C + 255.0)) / (255.0 * (259.0 - C)); - for(auto& p : img.Px) { - int r = int(factor * (int(_r(p)) - 128) + 128) + br; - int g = int(factor * (int(_g(p)) - 128) + 128) + br; - int b = int(factor * (int(_b(p)) - 128) + 128) + br; - p = _pack(_a(p), _clampu8(r), _clampu8(g), _clampu8(b)); - } - } - static void _multiply(Image& img, uint32_t color) { - uint8_t cr=_r(color), cg=_g(color), cb=_b(color); - for(auto& p : img.Px) { - uint8_t r = uint8_t((uint32_t(_r(p)) * cr) / 255); - uint8_t g = uint8_t((uint32_t(_g(p)) * cg) / 255); - uint8_t b = uint8_t((uint32_t(_b(p)) * cb) / 255); - p = _pack(_a(p), r,g,b); - } - } - static void _screen(Image& img, uint32_t color) { - uint8_t cr=_r(color), cg=_g(color), cb=_b(color); - for(auto& p : img.Px) { - uint8_t r = uint8_t(255 - ((255 - _r(p)) * (255 - cr)) / 255); - uint8_t g = uint8_t(255 - ((255 - _g(p)) * (255 - cg)) / 255); - uint8_t b = uint8_t(255 - ((255 - _b(p)) * (255 - cb)) / 255); - p = _pack(_a(p), r,g,b); - } - } - static void _colorize(Image& img, uint32_t color, uint8_t ratio) { - uint8_t cr=_r(color), cg=_g(color), cb=_b(color); - for(auto& p : img.Px) { - int r = (int(_r(p)) * (255 - ratio) + int(cr) * ratio) / 255; - int g = (int(_g(p)) * (255 - ratio) + int(cg) * ratio) / 255; - int b = (int(_b(p)) * (255 - ratio) + int(cb) * ratio) / 255; - p = _pack(_a(p), uint8_t(r), uint8_t(g), uint8_t(b)); - } - } - static void _lowpart(Image& base, const Image& over, uint32_t percent) { - uint32_t startY = base.H - (base.H * percent) / 100; - for(uint32_t y=startY; y(255, (outRp * 255) / outA)); - outG = uint8_t(std::min(255, (outGp * 255) / outA)); - outB = uint8_t(std::min(255, (outBp * 255) / outA)); - } - base.Px[i] = _pack(uint8_t(outA), outR, outG, outB); - } - } - } - - static Image _transform(const Image& src, uint32_t t) { - Image dst; - auto at = [&](uint32_t x, uint32_t y)->uint32_t { return src.Px[size_t(y)*src.W + x]; }; - auto make = [&](uint32_t w, uint32_t h){ - Image d; d.W=w; d.H=h; d.Px.resize(size_t(w)*size_t(h)); - return d; - }; - auto set = [&](Image& im, uint32_t x, uint32_t y, uint32_t v){ - im.Px[size_t(y)*im.W + x] = v; - }; - - switch (t & 7u) { - case 0: return src; - case 1: { dst = make(src.H, src.W); - for(uint32_t y=0;y op(...) - // tex 32x32 "#RRGGBBAA" - // nested only where op expects a texture arg: - // overlay( tex other |> ... ) - // Also supports overlay(other) / mask(other) / lowpart(50, other) - // ======================== - enum class TokKind { End, Ident, Number, String, Pipe, Comma, LParen, RParen, Eq, X }; - - struct Tok { - TokKind Kind = TokKind::End; - std::string Text; - uint32_t U32 = 0; - }; - - struct Lexer { - std::string_view S; - size_t I=0; - - bool HasBuf = false; - Tok Buf; - - static bool isAlpha(char c){ return (c>='a'&&c<='z')||(c>='A'&&c<='Z')||c=='_'; } - static bool isNum(char c){ return (c>='0'&&c<='9'); } - static bool isAlnum(char c){ return isAlpha(c)||isNum(c); } - - void unread(const Tok& t) { - // allow only 1-level unread - HasBuf = true; - Buf = t; - } - - Tok peek() { - Tok t = next(); - unread(t); - return t; - } - - void skipWs() { - while (I < S.size()) { - char c = S[I]; - if(c==' '||c=='\t'||c=='\r'||c=='\n'){ I++; continue; } - if(c=='#'){ while (I= S.size()) return {TokKind::End, {}, 0}; - - if(S[I]=='|' && I+1') { I+=2; return {TokKind::Pipe, "|>",0}; } - char c = S[I]; - if(c==','){ I++; return {TokKind::Comma,",",0}; } - if(c=='('){ I++; return {TokKind::LParen,"(",0}; } - if(c==')'){ I++; return {TokKind::RParen,")",0}; } - if(c=='='){ I++; return {TokKind::Eq,"=",0}; } - if(c=='x' || c=='X'){ I++; return {TokKind::X,"x",0}; } - - if(c=='"') { - I++; - std::string out; - while (I < S.size()) { - char ch = S[I++]; - if(ch=='"') break; - if(ch=='\\' && I Pos; - std::unordered_map Named; - }; - - // ======================== - // Compiler state - // ======================== - std::string Source_; - std::vector Code_; - std::vector Patches_; - - // ---- emit helpers (target = arbitrary out vector) ---- - static inline void _emitOp(std::vector& out, Op op) { _wr8(out, uint8_t(op)); } - static inline void _emitU8(std::vector& out, uint32_t v){ _wr8(out, v); } - static inline void _emitU16(std::vector& out, uint32_t v){ _wr16(out, v); } - static inline void _emitU24(std::vector& out, uint32_t v){ _wr24(out, v); } - static inline void _emitU32(std::vector& out, uint32_t v){ _wr32(out, v); } - - // reserve 3 bytes for u24 texId and register patch (absolute or relative) - struct RelPatch { size_t Rel0; std::string Name; }; - - static void _emitTexPatchU24(std::vector& out, - std::vector* absPatches, - std::vector* relPatches, - const std::string& name) { - const size_t idx = out.size(); - out.push_back(0); out.push_back(0); out.push_back(0); - if(absPatches) absPatches->push_back(Patch{idx, name}); - if(relPatches) relPatches->push_back(RelPatch{idx, name}); - } - - static void _emitSrcTexName(std::vector& out, - std::vector* absPatches, - std::vector* relPatches, - const std::string& name) { - _emitU8(out, uint8_t(SrcKind::TexId)); - _emitTexPatchU24(out, absPatches, relPatches, name); - } - - static void _emitSrcSub(std::vector& out, uint32_t off24, uint32_t len24) { - _emitU8(out, uint8_t(SrcKind::Sub)); - _emitU24(out, off24); - _emitU24(out, len24); - } - - // ======================== - // Color parsing: #RRGGBB or #RRGGBBAA -> 0xAARRGGBB - // ======================== - static bool _parseHexColor(std::string_view s, uint32_t& outARGB) { - if(s.size()!=7 && s.size()!=9) return false; - if(s[0] != '#') return false; - auto hex = [](char c)->int{ - if(c>='0'&&c<='9') return c-'0'; - if(c>='a'&&c<='f') return 10+(c-'a'); - if(c>='A'&&c<='F') return 10+(c-'A'); - return -1; - }; - auto byteAt = [&](size_t idx)->std::optional{ - int hi=hex(s[idx]), lo=hex(s[idx+1]); - if(hi<0||lo<0) return std::nullopt; - return uint8_t((hi<<4)|lo); - }; - auto r = byteAt(1), g = byteAt(3), b = byteAt(5); - if(!r||!g||!b) return false; - uint8_t a = 255; - if(s.size()==9) { - auto aa = byteAt(7); - if(!aa) return false; - a = *aa; - } - outARGB = (uint32_t(a)<<24) | (uint32_t(*r)<<16) | (uint32_t(*g)<<8) | (uint32_t(*b)); - return true; - } - - // ======================== - // Parsing entry: full program - // ======================== - bool _parseProgram(std::string* err) { - Lexer lx{Source_}; - Tok t = lx.next(); - if(!(t.Kind==TokKind::Ident && t.Text=="tex")) { - if(err) *err="Expected 'tex' at start"; - return false; - } - - // compile base into main Code_ - if(!_compileBaseAfterTex(lx, Code_, /*abs*/&Patches_, /*rel*/nullptr, err)) return false; - - // pipeline: |> op ... - Tok nt = lx.next(); - while (nt.Kind == TokKind::Pipe) { - Tok opName = lx.next(); - if(opName.Kind != TokKind::Ident) { if(err) *err="Expected op name after |>"; return false; } - ParsedOp op; op.Name = opName.Text; - - Tok peek = lx.next(); - if(peek.Kind == TokKind::LParen) { - if(!_parseArgListOrTextureExpr(lx, op, err)) return false; - nt = lx.next(); - } else { - nt = peek; // no-arg op - } - - if(!_compileOpInto(lx, op, Code_, /*abs*/&Patches_, /*rel*/nullptr, err)) return false; - } - - _emitOp(Code_, Op::End); - if (Code_.size() > MaxCodeBytes) { - if (err) - *err = "Pipeline bytecode too large: " + std::to_string(Code_.size()) + - " > MaxCodeBytes(" + std::to_string(MaxCodeBytes) + ")"; - return false; - } - - return true; - } - - // ======================== - // Base compilation after 'tex' - // supports: - // 1) tex name - // 2) tex "name(.png/.jpg/.jpeg)" (allowed but normalized) - // 3) tex anim(...) - // 4) tex 32x32 "#RRGGBBAA" - // ======================== - bool _compileBaseAfterTex(Lexer& lx, - std::vector& out, - std::vector* absPatches, - std::vector* relPatches, - std::string* err) { - Tok a = lx.next(); - - if(a.Kind == TokKind::Ident && a.Text == "anim") { - Tok lp = lx.next(); - if(lp.Kind != TokKind::LParen) { if(err) *err="Expected '(' after anim"; return false; } - - ParsedOp op; op.Name="anim"; - if(!_parseArgList(lx, op, err)) return false; - - auto posU = [&](size_t i)->std::optional{ - if(i >= op.Pos.size()) return std::nullopt; - if(op.Pos[i].Kind != ArgVal::ValueKind::U32) return std::nullopt; - return op.Pos[i].U32; - }; - auto posS = [&](size_t i)->std::optional{ - if(i >= op.Pos.size()) return std::nullopt; - return op.Pos[i].S; - }; - auto namedU = [&](std::string_view k)->std::optional{ - auto it = op.Named.find(std::string(k)); - if(it==op.Named.end() || it->second.Kind!=ArgVal::ValueKind::U32) return std::nullopt; - return it->second.U32; - }; - auto namedS = [&](std::string_view k)->std::optional{ - auto it = op.Named.find(std::string(k)); - if(it==op.Named.end()) return std::nullopt; - return it->second.S; - }; - - std::string tex = namedS("tex").value_or(posS(0).value_or("")); - if(tex.empty()) { if(err) *err="anim requires texture name"; return false; } - - uint32_t frameW = namedU("frame_w").value_or(namedU("w").value_or(posU(1).value_or(0))); - uint32_t frameH = namedU("frame_h").value_or(namedU("h").value_or(posU(2).value_or(0))); - uint32_t frames = namedU("frames").value_or(namedU("count").value_or(posU(3).value_or(0))); - uint32_t fps = namedU("fps").value_or(posU(4).value_or(0)); - uint32_t smooth = namedU("smooth").value_or(posU(5).value_or(0)); - - std::string axis = namedS("axis").value_or(""); - bool horizontal = (!axis.empty() && (axis[0] == 'x' || axis[0] == 'h')); - - if(frameW > 65535u || frameH > 65535u || frames > 65535u) { - if(err) *err="anim params must fit uint16"; - return false; - } - - uint32_t fpsQ = fps ? std::min(0xFFFFu, fps * 256u) : DefaultAnimFpsQ; - uint32_t flags = (smooth ? AnimSmooth : 0) | (horizontal ? AnimHorizontal : 0); - - _emitOp(out, Op::Base_Anim); - _emitSrcTexName(out, absPatches, relPatches, tex); - _emitU16(out, frameW); - _emitU16(out, frameH); - _emitU16(out, frames); - _emitU16(out, fpsQ); - _emitU8(out, flags); - return true; - } - - if(a.Kind == TokKind::Ident || a.Kind == TokKind::String) { - // tex name (or tex "name.png" => normalized) - _emitOp(out, Op::Base_Tex); - _emitSrcTexName(out, absPatches, relPatches, a.Text); - return true; - } - - if(a.Kind == TokKind::Number) { - // tex 32x32 "#RRGGBBAA" - Tok xTok = lx.next(); - Tok b = lx.next(); - Tok colTok = lx.next(); - if(xTok.Kind != TokKind::X || b.Kind != TokKind::Number || (colTok.Kind!=TokKind::Ident && colTok.Kind!=TokKind::String)) { - if(err) *err="Expected: tex x <#color>"; - return false; - } - uint32_t w = a.U32, h = b.U32; - uint32_t color = 0; - if(!_parseHexColor(colTok.Text, color)) { - if(err) *err="Bad color literal (use #RRGGBB or #RRGGBBAA)"; - return false; - } - if(w>65535u || h>65535u) { if(err) *err="w/h must fit in uint16"; return false; } - _emitOp(out, Op::Base_Fill); - _emitU16(out, w); - _emitU16(out, h); - _emitU32(out, color); - return true; - } - - if(err) *err="Bad 'tex' base expression"; - return false; - } - - // ======================== - // Args parsing: - // - normal args: (a,b,key=v) - // - OR if first token inside '(' is 'tex' => parse nested program until ')' - // ======================== - bool _parseArgListOrTextureExpr(Lexer& lx, ParsedOp& op, std::string* err) { - Tok first = lx.next(); - - if(first.Kind==TokKind::Ident && first.Text=="tex") { - // marker - ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = "__SUBTEX__"; - op.Pos.push_back(av); - - PendingSubData sub; - if(!_compileSubProgramFromAlreadySawTex(lx, sub, err)) return false; - - Tok end = lx.next(); - if(end.Kind != TokKind::RParen) { if(err) *err="Expected ')' after sub texture expr"; return false; } - - PendingSub_[&op] = std::move(sub); - return true; - } - - // otherwise parse as normal arg list, where `first` is first token inside '(' - Tok t = first; - if(t.Kind == TokKind::RParen) return true; - - while (true) { - if(t.Kind == TokKind::Ident) { - Tok maybeEq = lx.next(); - if(maybeEq.Kind == TokKind::Eq) { - Tok v = lx.next(); - ArgVal av; - if(!_tokToVal(v, av, err)) return false; - op.Named[t.Text] = std::move(av); - t = lx.next(); - } else { - ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = t.Text; - op.Pos.push_back(std::move(av)); - t = maybeEq; - } - } else { - ArgVal av; - if(!_tokToVal(t, av, err)) return false; - op.Pos.push_back(std::move(av)); - t = lx.next(); - } - - if(t.Kind == TokKind::Comma) { t = lx.next(); continue; } - if(t.Kind == TokKind::RParen) return true; - - if(err) *err = "Expected ',' or ')' in argument list"; - return false; - } - } - - bool _parseArgList(Lexer& lx, ParsedOp& op, std::string* err) { - Tok t = lx.next(); - if(t.Kind == TokKind::RParen) return true; - - while (true) { - if(t.Kind == TokKind::Ident) { - Tok maybeEq = lx.next(); - if(maybeEq.Kind == TokKind::Eq) { - Tok v = lx.next(); - ArgVal av; - if(!_tokToVal(v, av, err)) return false; - op.Named[t.Text] = std::move(av); - t = lx.next(); - } else { - ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = t.Text; - op.Pos.push_back(std::move(av)); - t = maybeEq; - } - } else { - ArgVal av; - if(!_tokToVal(t, av, err)) return false; - op.Pos.push_back(std::move(av)); - t = lx.next(); - } - - if(t.Kind == TokKind::Comma) { t = lx.next(); continue; } - if(t.Kind == TokKind::RParen) return true; - - if(err) *err = "Expected ',' or ')' in argument list"; - return false; - } - } - - bool _tokToVal(const Tok& t, ArgVal& out, std::string* err) { - if(t.Kind == TokKind::Number) { out.Kind=ArgVal::ValueKind::U32; out.U32=t.U32; return true; } - if(t.Kind == TokKind::String) { out.Kind=ArgVal::ValueKind::Str; out.S=t.Text; return true; } - if(t.Kind == TokKind::Ident) { out.Kind=ArgVal::ValueKind::Ident; out.S=t.Text; return true; } - if(err) *err = "Expected value token"; - return false; - } - - // ======================== - // Subprogram compilation: - // we already consumed 'tex'. Parse base + pipeline until next token is ')' - // DO NOT consume ')' - // ======================== - struct PendingSubData { - std::vector Bytes; - std::vector RelPatches; - }; - - bool _compileSubProgramFromAlreadySawTex(Lexer& lx, PendingSubData& outSub, std::string* err) { - outSub.Bytes.clear(); - outSub.RelPatches.clear(); - - // base - if(!_compileBaseAfterTex(lx, outSub.Bytes, /*abs*/nullptr, /*rel*/&outSub.RelPatches, err)) - return false; - - // pipeline until ')' - while(true) { - // peek - Tok nt = lx.peek(); - if(nt.Kind == TokKind::RParen) break; - if(nt.Kind != TokKind::Pipe) { if(err) *err="Sub tex: expected '|>' or ')'"; return false; } - - // consume pipe - lx.next(); - Tok opName = lx.next(); - if(opName.Kind != TokKind::Ident) { if(err) *err="Sub tex: expected op name"; return false; } - - ParsedOp op; op.Name = opName.Text; - - Tok lp = lx.next(); - if(lp.Kind == TokKind::LParen) { - if(!_parseArgListOrTextureExpr(lx, op, err)) return false; - } else { - // no-arg op, lp already is next token (pipe or ')'), so we need to "unread" — can't. - // simplest: treat it as next token for outer loop by rewinding lexer state. - // We'll do it by storing the token back via a small hack: rebuild peek? Too heavy. - // Instead: enforce parentheses for ops in subprogram except no-arg ops (brighten/noalpha) which can be without. - // To keep behavior identical to main, we handle no-arg by rewinding I one token is not possible, - // so we accept that in subprogram, no-arg ops must be written as brighten() etc. - if(err) *err="Sub tex: no-arg ops must use parentheses, e.g. brighten()"; - return false; - } - - if(!_compileOpInto(lx, op, outSub.Bytes, /*abs*/nullptr, /*rel*/&outSub.RelPatches, err)) - return false; - } - - // Pipeline until we see ')' - while (true) { - Tok nt = lx.peek(); - if(nt.Kind == TokKind::RParen) break; - if(nt.Kind != TokKind::Pipe) { if(err) *err="Sub tex: expected '|>' or ')'"; return false; } - - // consume pipe - lx.next(); - - Tok opName = lx.next(); - if(opName.Kind != TokKind::Ident) { if(err) *err="Sub tex: expected op name"; return false; } - - ParsedOp op; op.Name = opName.Text; - - // allow both op and op(...) - Tok maybe = lx.peek(); - if(maybe.Kind == TokKind::LParen) { - lx.next(); // consume '(' - if(!_parseArgListOrTextureExpr(lx, op, err)) return false; - } else { - // no-arg op; nothing to parse - } - - if(!_compileOpInto(lx, op, outSub.Bytes, /*abs*/nullptr, /*rel*/&outSub.RelPatches, err)) - return false; - } - - - _emitOp(outSub.Bytes, Op::End); - return true; - } - - // pending subprogram associated with ParsedOp pointer (created during parsing) - mutable std::unordered_map PendingSub_; - - // Append subprogram to `out` and emit SrcRef(Sub, off16, len16), migrating patches properly. - static bool _appendSubprogram(std::vector& out, - PendingSubData&& sub, - std::vector* absPatches, - std::vector* relPatches, - uint32_t& outOff, - uint32_t& outLen, - std::string* err) { - const size_t offset = out.size(); - const size_t len = sub.Bytes.size(); - - if(offset > 0xFFFFFFu || len > 0xFFFFFFu || (offset + len) > 0xFFFFFFu) { - if(err) *err = "Subprogram слишком большой (off/len должны влезать в u24 байт)"; - return false; - } - - if(offset + len > MaxCodeBytes) { - if(err) *err = "Pipeline bytecode too large after sub append: " + - std::to_string(offset + len) + " > MaxCodeBytes(" + std::to_string(MaxCodeBytes) + ")"; - return false; - } - - // migrate patches - if(absPatches) { - for(const auto& rp : sub.RelPatches) { - absPatches->push_back(Patch{offset + rp.Rel0, rp.Name}); - } - } - if(relPatches) { - for(const auto& rp : sub.RelPatches) { - relPatches->push_back(RelPatch{offset + rp.Rel0, rp.Name}); - } - } - - out.insert(out.end(), sub.Bytes.begin(), sub.Bytes.end()); - - outOff = uint32_t(offset); - outLen = uint32_t(len); - return true; - } - - // ======================== - // Compile operations into arbitrary `out` - // absPatches != nullptr => patches recorded as absolute for this buffer - // relPatches != nullptr => patches recorded as relative for this buffer - // ======================== - bool _compileOpInto(Lexer& /*lx*/, - const ParsedOp& op, - std::vector& out, - std::vector* absPatches, - std::vector* relPatches, - std::string* err) { - auto posU = [&](size_t i)->std::optional{ - if(i >= op.Pos.size()) return std::nullopt; - if(op.Pos[i].Kind != ArgVal::ValueKind::U32) return std::nullopt; - return op.Pos[i].U32; - }; - auto posS = [&](size_t i)->std::optional{ - if(i >= op.Pos.size()) return std::nullopt; - return op.Pos[i].S; - }; - auto namedU = [&](std::string_view k)->std::optional{ - auto it = op.Named.find(std::string(k)); - if(it==op.Named.end() || it->second.Kind!=ArgVal::ValueKind::U32) return std::nullopt; - return it->second.U32; - }; - auto namedS = [&](std::string_view k)->std::optional{ - auto it = op.Named.find(std::string(k)); - if(it==op.Named.end()) return std::nullopt; - return it->second.S; - }; - - auto emitSrcFromName = [&](const std::string& n){ - _emitSrcTexName(out, absPatches, relPatches, n); - }; - - auto emitSrcFromPendingSub = [&]()->bool{ - auto it = PendingSub_.find(&op); - if(it == PendingSub_.end()) { if(err) *err="Internal: missing subprogram"; return false; } - uint32_t off=0, len=0; - if(!_appendSubprogram(out, std::move(it->second), absPatches, relPatches, off, len, err)) return false; - PendingSub_.erase(it); - _emitSrcSub(out, off, len); - return true; - }; - - // --- Ops that accept a "texture" argument: overlay/mask/lowpart --- - if(op.Name == "overlay") { - _emitOp(out, Op::Overlay); - if(!op.Pos.empty() && op.Pos[0].S == "__SUBTEX__") return emitSrcFromPendingSub(); - - // allow overlay(name) or overlay(tex=name) - std::string tex = namedS("tex").value_or(posS(0).value_or("")); - if(tex.empty()) { if(err) *err="overlay requires texture arg"; return false; } - emitSrcFromName(tex); - return true; - } - - if(op.Name == "mask") { - _emitOp(out, Op::Mask); - if(!op.Pos.empty() && op.Pos[0].S == "__SUBTEX__") return emitSrcFromPendingSub(); - - std::string tex = namedS("tex").value_or(posS(0).value_or("")); - if(tex.empty()) { if(err) *err="mask requires texture arg"; return false; } - emitSrcFromName(tex); - return true; - } - - if(op.Name == "lowpart") { - uint32_t pct = namedU("percent").value_or(posU(0).value_or(0)); - if(!pct) { if(err) *err="lowpart requires percent"; return false; } - - _emitOp(out, Op::LowPart); - _emitU8(out, std::min(100u, pct)); - - // 2nd arg can be nested subtex or name - if(op.Pos.size() >= 2 && op.Pos[1].S == "__SUBTEX__") return emitSrcFromPendingSub(); - - std::string tex = namedS("tex").value_or(posS(1).value_or("")); - if(tex.empty()) { if(err) *err="lowpart requires texture arg"; return false; } - emitSrcFromName(tex); - return true; - } - - // --- Unary ops --- - if(op.Name == "resize") { - uint32_t w = namedU("w").value_or(posU(0).value_or(0)); - uint32_t h = namedU("h").value_or(posU(1).value_or(0)); - if(!w || !h || w>65535u || h>65535u) { if(err) *err="resize(w,h) must fit uint16"; return false; } - _emitOp(out, Op::Resize); _emitU16(out, w); _emitU16(out, h); - return true; - } - - if(op.Name == "transform") { - uint32_t t = namedU("t").value_or(posU(0).value_or(0)); - _emitOp(out, Op::Transform); _emitU8(out, t & 7u); - return true; - } - - if(op.Name == "opacity") { - uint32_t a = namedU("a").value_or(posU(0).value_or(255)); - _emitOp(out, Op::Opacity); _emitU8(out, a & 0xFFu); - return true; - } - - if(op.Name == "remove_alpha" || op.Name == "noalpha") { - _emitOp(out, Op::NoAlpha); - return true; - } - - if(op.Name == "make_alpha") { - std::string col = namedS("color").value_or(posS(0).value_or("")); - uint32_t argb=0; - if(!_parseHexColor(col, argb)) { if(err) *err="make_alpha requires color #RRGGBB"; return false; } - uint32_t rgb24 = argb & 0x00FFFFFFu; - _emitOp(out, Op::MakeAlpha); - _emitU8(out, (rgb24 >> 16) & 0xFFu); - _emitU8(out, (rgb24 >> 8) & 0xFFu); - _emitU8(out, (rgb24 >> 0) & 0xFFu); - return true; - } - - if(op.Name == "invert") { - std::string ch = namedS("channels").value_or(posS(0).value_or("rgb")); - uint32_t mask=0; - for(char c : ch) { - if(c=='r') mask |= 1; - if(c=='g') mask |= 2; - if(c=='b') mask |= 4; - if(c=='a') mask |= 8; - } - _emitOp(out, Op::Invert); _emitU8(out, mask & 0xFu); - return true; - } - - if(op.Name == "brighten") { - _emitOp(out, Op::Brighten); - return true; - } - - if(op.Name == "contrast") { - int c = int(namedU("value").value_or(posU(0).value_or(0))); - int b = int(namedU("brightness").value_or(posU(1).value_or(0))); - c = std::max(-127, std::min(127, c)); - b = std::max(-127, std::min(127, b)); - _emitOp(out, Op::Contrast); - _emitU8(out, uint32_t(c + 127)); - _emitU8(out, uint32_t(b + 127)); - return true; - } - - auto compileColorOp = [&](Op opcode, bool needsRatio)->bool{ - std::string col = namedS("color").value_or(posS(0).value_or("")); - uint32_t argb=0; - if(!_parseHexColor(col, argb)) { if(err) *err="Bad color literal"; return false; } - _emitOp(out, opcode); - _emitU32(out, argb); - if(needsRatio) { - uint32_t ratio = namedU("ratio").value_or(posU(1).value_or(255)); - _emitU8(out, ratio & 0xFFu); - } - return true; - }; - - if(op.Name == "multiply") return compileColorOp(Op::Multiply, false); - if(op.Name == "screen") return compileColorOp(Op::Screen, false); - if(op.Name == "colorize") return compileColorOp(Op::Colorize, true); - - if(op.Name == "anim") { - uint32_t frameW = namedU("frame_w").value_or(namedU("w").value_or(posU(0).value_or(0))); - uint32_t frameH = namedU("frame_h").value_or(namedU("h").value_or(posU(1).value_or(0))); - uint32_t frames = namedU("frames").value_or(namedU("count").value_or(posU(2).value_or(0))); - uint32_t fps = namedU("fps").value_or(posU(3).value_or(0)); - uint32_t smooth = namedU("smooth").value_or(posU(4).value_or(0)); - - std::string axis = namedS("axis").value_or(""); - bool horizontal = (!axis.empty() && (axis[0] == 'x' || axis[0] == 'h')); - - if(frameW > 65535u || frameH > 65535u || frames > 65535u) { - if(err) *err="anim params must fit uint16"; - return false; - } - - uint32_t fpsQ = fps ? std::min(0xFFFFu, fps * 256u) : DefaultAnimFpsQ; - uint32_t flags = (smooth ? AnimSmooth : 0) | (horizontal ? AnimHorizontal : 0); - - _emitOp(out, Op::Anim); - _emitU16(out, frameW); - _emitU16(out, frameH); - _emitU16(out, frames); - _emitU16(out, fpsQ); - _emitU8(out, flags); - return true; - } - - if(err) *err = "Unknown op: " + op.Name; - return false; - } -}; +#include "Common/TexturePipelineProgram.hpp" diff --git a/Src/Client/Vulkan/VulkanRenderSession.hpp b/Src/Client/Vulkan/VulkanRenderSession.hpp index 7253522..0578c7a 100644 --- a/Src/Client/Vulkan/VulkanRenderSession.hpp +++ b/Src/Client/Vulkan/VulkanRenderSession.hpp @@ -149,11 +149,8 @@ public: ModelObject model; std::string type = "unknown"; std::optional header; - if(deps && !deps->empty()) { - header = AssetsManager::parseHeader(*deps); - if(header && header->Type != EnumAssets::Model) - header.reset(); - } + if(deps && !deps->empty()) + header = AssetsManager::parseHeader(EnumAssets::Model, *deps); const std::vector* textureDeps = header ? &header->TextureDeps : nullptr; auto remapTextureId = [&](uint32_t placeholder) -> uint32_t { if(!textureDeps || placeholder >= textureDeps->size()) @@ -900,7 +897,7 @@ public: } if(deps && !deps->empty()) { - auto header = AssetsManager::parseHeader(*deps); + auto header = AssetsManager::parseHeader(EnumAssets::Model, *deps); if(header && header->Type == EnumAssets::Nodestate) { nodestate.LocalToModel.assign(header->ModelDeps.begin(), header->ModelDeps.end()); } diff --git a/Src/Common/Abstract.cpp b/Src/Common/Abstract.cpp index 3273814..ad748e0 100644 --- a/Src/Common/Abstract.cpp +++ b/Src/Common/Abstract.cpp @@ -1,5 +1,5 @@ #include "Abstract.hpp" -#include "Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp" +#include "Common/TexturePipelineProgram.hpp" #include "Common/Net.hpp" #include "TOSLib.hpp" #include @@ -783,7 +783,7 @@ void unCompressNodes_bit(const std::u8string& compressed, Node* ptr) { } } -void unCompressNodes(const std::u8string& compressed, Node* ptr) { +void unCompressNodes(std::u8string_view compressed, Node* ptr) { const std::u8string& next = unCompressLinear(compressed); const Node *lPtr = (const Node*) next.data(); std::copy(lPtr, lPtr+16*16*16, ptr); diff --git a/Src/Common/Abstract.hpp b/Src/Common/Abstract.hpp index fe3bcc4..82493a0 100644 --- a/Src/Common/Abstract.hpp +++ b/Src/Common/Abstract.hpp @@ -973,6 +973,57 @@ struct HeadlessModel { std::u8string dump() const; }; +struct TexturePipeline { + std::vector BinTextures; + std::vector Pipeline; + + bool operator==(const TexturePipeline& other) const { + return BinTextures == other.BinTextures && Pipeline == other.Pipeline; + } +}; + +struct PreparedModel { + struct SubModel { + std::string Domain; + std::string Key; + }; + + using Cuboid = HeadlessModel::Cuboid; + + std::unordered_map CompiledTextures; + std::vector Cuboids; + std::vector SubModels; + + PreparedModel() = default; + PreparedModel(const std::u8string& data) { load(data); } + PreparedModel(std::u8string_view data) { load(data); } + + void load(std::u8string_view data) { + HeadlessModel model; + model.load(data); + Cuboids = model.Cuboids; + + CompiledTextures.clear(); + CompiledTextures.reserve(model.Textures.size()); + for(const auto& [key, id] : model.Textures) { + TexturePipeline pipe; + pipe.BinTextures.push_back(id); + CompiledTextures.emplace(key, std::move(pipe)); + } + } +}; + +struct PreparedNodeState : public HeadlessNodeState { + using HeadlessNodeState::Model; + using HeadlessNodeState::VectorModel; + + std::vector LocalToModel; + + PreparedNodeState() = default; + PreparedNodeState(std::u8string_view data) { load(data); } + PreparedNodeState(const std::u8string& data) { load(data); } +}; + struct PreparedGLTF { std::vector TextureKey; std::unordered_map Textures; @@ -1072,4 +1123,20 @@ struct hash { return v; } }; + +template <> +struct hash { + std::size_t operator()(const LV::TexturePipeline& pipe) const noexcept { + std::size_t v = 14695981039346656037ULL; + for (uint32_t id : pipe.BinTextures) { + v ^= static_cast(id); + v *= 1099511628211ULL; + } + for (uint8_t byte : pipe.Pipeline) { + v ^= static_cast(byte); + v *= 1099511628211ULL; + } + return v; + } +}; } diff --git a/Src/Common/AssetsPreloader.cpp b/Src/Common/AssetsPreloader.cpp index d9db274..59dccda 100644 --- a/Src/Common/AssetsPreloader.cpp +++ b/Src/Common/AssetsPreloader.cpp @@ -421,4 +421,11 @@ AssetsPreloader::Out_bakeId AssetsPreloader::bakeIdTables() { return result; } +std::tuple, std::vector> +AssetsPreloader::getNodeDependency(const std::string& domain, const std::string& key) { + (void)domain; + (void)key; + return {0, {}, {}}; +} + } diff --git a/Src/Common/AssetsPreloader.hpp b/Src/Common/AssetsPreloader.hpp index 9be9c22..c9715b0 100644 --- a/Src/Common/AssetsPreloader.hpp +++ b/Src/Common/AssetsPreloader.hpp @@ -14,7 +14,7 @@ #include #include #include -#include "Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp" +#include "Common/TexturePipelineProgram.hpp" #include "Common/Abstract.hpp" #include "Common/Async.hpp" #include "TOSAsync.hpp" diff --git a/Src/Common/TexturePipelineProgram.hpp b/Src/Common/TexturePipelineProgram.hpp new file mode 100644 index 0000000..eaa8d14 --- /dev/null +++ b/Src/Common/TexturePipelineProgram.hpp @@ -0,0 +1,2067 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ======================== +// External texture view +// ======================== +struct Texture { + uint32_t Width, Height; + const uint32_t* Pixels; // assumed 0xAARRGGBB +}; + +// ======================== +// Bytecode words are uint8_t (1 byte machine word) +// TexId is u24 (3 bytes, little-endian) +// Subprogram refs use off24/len24 in BYTES (<=65535) +// ======================== +class TexturePipelineProgram { +public: + using Word = uint8_t; + + enum AnimFlags : Word { + AnimSmooth = 1u << 0, + AnimHorizontal = 1u << 1 + }; + + static constexpr uint16_t DefaultAnimFpsQ = uint16_t(8u * 256u); + static constexpr size_t MaxCodeBytes = (1u << 16) + 1u; // 65537 + + struct OwnedTexture { + uint32_t Width = 0, Height = 0; + std::vector Pixels; + Texture view() const { return Texture{Width, Height, Pixels.data()}; } + }; + + using IdResolverFunc = std::function(std::string_view)>; + using TextureProviderFunc = std::function(uint32_t)>; + + // Patch point to 3 consecutive bytes where u24 texId lives (b0,b1,b2) + struct Patch { + size_t ByteIndex0 = 0; // Code_[i], Code_[i+1], Code_[i+2] + std::string Name; + }; + + bool compile(std::string src, std::string* err = nullptr) { + Source_ = std::move(src); + Code_.clear(); + Patches_.clear(); + PendingSub_.clear(); + return _parseProgram(err); + } + + bool link(const IdResolverFunc& resolver, std::string* err = nullptr) { + for (const auto& p : Patches_) { + auto idOpt = resolver(p.Name); + if(!idOpt) { + if(err) *err = "Не удалось разрешить имя текстуры: " + p.Name; + return false; + } + uint32_t id = *idOpt; + if(id >= (1u << 24)) { + if(err) *err = "TexId выходит за 24 бита (u24): " + p.Name + " => " + std::to_string(id); + return false; + } + if(p.ByteIndex0 + 2 >= Code_.size()) { + if(err) *err = "Внутренняя ошибка: применение идентификатора выходит за рамки кода"; + return false; + } + Code_[p.ByteIndex0 + 0] = uint8_t(id & 0xFFu); + Code_[p.ByteIndex0 + 1] = uint8_t((id >> 8) & 0xFFu); + Code_[p.ByteIndex0 + 2] = uint8_t((id >> 16) & 0xFFu); + } + return true; + } + + bool bake(const TextureProviderFunc& provider, OwnedTexture& out, std::string* err = nullptr) const { + return bake(provider, out, 0.0, err); + } + + bool bake(const TextureProviderFunc& provider, OwnedTexture& out, double timeSeconds, std::string* err = nullptr) const { + VM vm(provider); + return vm.run(Code_, out, timeSeconds, err); + } + + const std::vector& words() const { return Code_; } + const std::vector& patches() const { return Patches_; } + + std::vector toBytes() const { return Code_; } + + struct AnimSpec { + uint32_t TexId = 0; + bool HasTexId = false; + uint16_t FrameW = 0; + uint16_t FrameH = 0; + uint16_t FrameCount = 0; + uint16_t FpsQ = 0; + uint16_t Flags = 0; + }; + + static std::vector extractAnimationSpecs(const Word* code, size_t size) { + std::vector specs; + if(!code || size == 0) { + return specs; + } + + struct Range { + size_t Start = 0; + size_t End = 0; + }; + + std::vector visited; + + auto read8 = [&](size_t& ip, uint8_t& out)->bool{ + if(ip >= size) return false; + out = code[ip++]; + return true; + }; + auto read16 = [&](size_t& ip, uint16_t& out)->bool{ + if(ip + 1 >= size) return false; + out = uint16_t(code[ip]) | (uint16_t(code[ip + 1]) << 8); + ip += 2; + return true; + }; + auto read24 = [&](size_t& ip, uint32_t& out)->bool{ + if(ip + 2 >= size) return false; + out = uint32_t(code[ip]) | + (uint32_t(code[ip + 1]) << 8) | + (uint32_t(code[ip + 2]) << 16); + ip += 3; + return true; + }; + auto read32 = [&](size_t& ip, uint32_t& out)->bool{ + if(ip + 3 >= size) return false; + out = uint32_t(code[ip]) | + (uint32_t(code[ip + 1]) << 8) | + (uint32_t(code[ip + 2]) << 16) | + (uint32_t(code[ip + 3]) << 24); + ip += 4; + return true; + }; + + struct SrcMeta { + SrcKind Kind = SrcKind::TexId; + uint32_t TexId = 0; + uint32_t Off = 0; + uint32_t Len = 0; + }; + + auto readSrc = [&](size_t& ip, SrcMeta& out)->bool{ + uint8_t kind = 0; + if(!read8(ip, kind)) return false; + out.Kind = static_cast(kind); + if(out.Kind == SrcKind::TexId) { + return read24(ip, out.TexId); + } + if(out.Kind == SrcKind::Sub) { + return read24(ip, out.Off) && read24(ip, out.Len); + } + return false; + }; + + auto scan = [&](auto&& self, size_t start, size_t end) -> void { + if(start >= end || end > size) { + return; + } + for(const auto& r : visited) { + if(r.Start == start && r.End == end) { + return; + } + } + visited.push_back(Range{start, end}); + + size_t ip = start; + while(ip < end) { + uint8_t opByte = 0; + if(!read8(ip, opByte)) return; + Op op = static_cast(opByte); + switch(op) { + case Op::End: + return; + + case Op::Base_Tex: { + SrcMeta src{}; + if(!readSrc(ip, src)) return; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(subStart < subEnd && subEnd <= size) { + self(self, subStart, subEnd); + } + } + } break; + + case Op::Base_Fill: { + uint16_t tmp16 = 0; + uint32_t tmp32 = 0; + if(!read16(ip, tmp16)) return; + if(!read16(ip, tmp16)) return; + if(!read32(ip, tmp32)) return; + } break; + + case Op::Base_Anim: { + SrcMeta src{}; + if(!readSrc(ip, src)) return; + uint16_t frameW = 0; + uint16_t frameH = 0; + uint16_t frameCount = 0; + uint16_t fpsQ = 0; + uint8_t flags = 0; + if(!read16(ip, frameW)) return; + if(!read16(ip, frameH)) return; + if(!read16(ip, frameCount)) return; + if(!read16(ip, fpsQ)) return; + if(!read8(ip, flags)) return; + + if(src.Kind == SrcKind::TexId) { + AnimSpec spec{}; + spec.TexId = src.TexId; + spec.HasTexId = true; + spec.FrameW = frameW; + spec.FrameH = frameH; + spec.FrameCount = frameCount; + spec.FpsQ = fpsQ; + spec.Flags = flags; + specs.push_back(spec); + } else if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(subStart < subEnd && subEnd <= size) { + self(self, subStart, subEnd); + } + } + } break; + + case Op::Resize: { + uint16_t tmp16 = 0; + if(!read16(ip, tmp16)) return; + if(!read16(ip, tmp16)) return; + } break; + + case Op::Transform: + case Op::Opacity: + case Op::Invert: + if(!read8(ip, opByte)) return; + break; + + case Op::NoAlpha: + case Op::Brighten: + break; + + case Op::MakeAlpha: + if(ip + 2 >= size) return; + ip += 3; + break; + + case Op::Contrast: + if(ip + 1 >= size) return; + ip += 2; + break; + + case Op::Multiply: + case Op::Screen: { + uint32_t tmp32 = 0; + if(!read32(ip, tmp32)) return; + } break; + + case Op::Colorize: { + uint32_t tmp32 = 0; + if(!read32(ip, tmp32)) return; + if(!read8(ip, opByte)) return; + } break; + + case Op::Anim: { + uint16_t frameW = 0; + uint16_t frameH = 0; + uint16_t frameCount = 0; + uint16_t fpsQ = 0; + uint8_t flags = 0; + if(!read16(ip, frameW)) return; + if(!read16(ip, frameH)) return; + if(!read16(ip, frameCount)) return; + if(!read16(ip, fpsQ)) return; + if(!read8(ip, flags)) return; + + AnimSpec spec{}; + spec.HasTexId = false; + spec.FrameW = frameW; + spec.FrameH = frameH; + spec.FrameCount = frameCount; + spec.FpsQ = fpsQ; + spec.Flags = flags; + specs.push_back(spec); + } break; + + case Op::Overlay: + case Op::Mask: { + SrcMeta src{}; + if(!readSrc(ip, src)) return; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(subStart < subEnd && subEnd <= size) { + self(self, subStart, subEnd); + } + } + } break; + + case Op::LowPart: { + if(!read8(ip, opByte)) return; + SrcMeta src{}; + if(!readSrc(ip, src)) return; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(subStart < subEnd && subEnd <= size) { + self(self, subStart, subEnd); + } + } + } break; + + case Op::Combine: { + uint16_t w = 0, h = 0, n = 0; + if(!read16(ip, w)) return; + if(!read16(ip, h)) return; + if(!read16(ip, n)) return; + for(uint16_t i = 0; i < n; ++i) { + uint16_t tmp16 = 0; + if(!read16(ip, tmp16)) return; + if(!read16(ip, tmp16)) return; + SrcMeta src{}; + if(!readSrc(ip, src)) return; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(subStart < subEnd && subEnd <= size) { + self(self, subStart, subEnd); + } + } + } + (void)w; (void)h; + } break; + + default: + return; + } + } + }; + + scan(scan, 0, size); + return specs; + } + + static bool remapTexIds(std::vector& code, const std::vector& remap, std::string* err = nullptr) { + struct Range { + size_t Start = 0; + size_t End = 0; + }; + + struct SrcMeta { + SrcKind Kind = SrcKind::TexId; + uint32_t TexId = 0; + uint32_t Off = 0; + uint32_t Len = 0; + size_t TexIdOffset = 0; + }; + + const size_t size = code.size(); + std::vector visited; + + auto read8 = [&](size_t& ip, uint8_t& out)->bool{ + if(ip >= size) return false; + out = code[ip++]; + return true; + }; + auto read16 = [&](size_t& ip, uint16_t& out)->bool{ + if(ip + 1 >= size) return false; + out = uint16_t(code[ip]) | (uint16_t(code[ip + 1]) << 8); + ip += 2; + return true; + }; + auto read24 = [&](size_t& ip, uint32_t& out)->bool{ + if(ip + 2 >= size) return false; + out = uint32_t(code[ip]) | + (uint32_t(code[ip + 1]) << 8) | + (uint32_t(code[ip + 2]) << 16); + ip += 3; + return true; + }; + auto read32 = [&](size_t& ip, uint32_t& out)->bool{ + if(ip + 3 >= size) return false; + out = uint32_t(code[ip]) | + (uint32_t(code[ip + 1]) << 8) | + (uint32_t(code[ip + 2]) << 16) | + (uint32_t(code[ip + 3]) << 24); + ip += 4; + return true; + }; + + auto readSrc = [&](size_t& ip, SrcMeta& out)->bool{ + uint8_t kind = 0; + if(!read8(ip, kind)) return false; + out.Kind = static_cast(kind); + if(out.Kind == SrcKind::TexId) { + out.TexIdOffset = ip; + return read24(ip, out.TexId); + } + if(out.Kind == SrcKind::Sub) { + return read24(ip, out.Off) && read24(ip, out.Len); + } + return false; + }; + + auto patchTexId = [&](const SrcMeta& src)->bool{ + if(src.Kind != SrcKind::TexId) + return true; + if(src.TexId >= remap.size()) { + if(err) *err = "Texture id out of remap range"; + return false; + } + uint32_t newId = remap[src.TexId]; + if(newId >= (1u << 24)) { + if(err) *err = "TexId выходит за 24 бита (u24)"; + return false; + } + if(src.TexIdOffset + 2 >= code.size()) { + if(err) *err = "Применение идентификатора выходит за рамки кода"; + return false; + } + code[src.TexIdOffset + 0] = uint8_t(newId & 0xFFu); + code[src.TexIdOffset + 1] = uint8_t((newId >> 8) & 0xFFu); + code[src.TexIdOffset + 2] = uint8_t((newId >> 16) & 0xFFu); + return true; + }; + + std::move_only_function scan; + scan = [&](size_t start, size_t end) -> bool { + if(start >= end || end > size) { + return true; + } + for(const auto& r : visited) { + if(r.Start == start && r.End == end) { + return true; + } + } + visited.push_back(Range{start, end}); + + size_t ip = start; + while(ip < end) { + uint8_t opByte = 0; + if(!read8(ip, opByte)) return false; + Op op = static_cast(opByte); + switch(op) { + case Op::End: + return true; + + case Op::Base_Tex: { + SrcMeta src{}; + if(!readSrc(ip, src)) return false; + if(!patchTexId(src)) return false; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(!scan(subStart, subEnd)) return false; + } + } break; + + case Op::Base_Fill: { + uint16_t tmp16 = 0; + uint32_t tmp32 = 0; + if(!read16(ip, tmp16)) return false; + if(!read16(ip, tmp16)) return false; + if(!read32(ip, tmp32)) return false; + } break; + + case Op::Base_Anim: { + SrcMeta src{}; + if(!readSrc(ip, src)) return false; + if(!patchTexId(src)) return false; + uint16_t frameW = 0; + uint16_t frameH = 0; + uint16_t frameCount = 0; + uint16_t fpsQ = 0; + uint8_t flags = 0; + if(!read16(ip, frameW)) return false; + if(!read16(ip, frameH)) return false; + if(!read16(ip, frameCount)) return false; + if(!read16(ip, fpsQ)) return false; + if(!read8(ip, flags)) return false; + (void)frameW; (void)frameH; (void)frameCount; (void)fpsQ; (void)flags; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(!scan(subStart, subEnd)) return false; + } + } break; + + case Op::Resize: { + uint16_t tmp16 = 0; + if(!read16(ip, tmp16)) return false; + if(!read16(ip, tmp16)) return false; + } break; + + case Op::Transform: + case Op::Opacity: + case Op::Invert: + if(!read8(ip, opByte)) return false; + break; + + case Op::NoAlpha: + case Op::Brighten: + break; + + case Op::MakeAlpha: + if(ip + 2 >= size) return false; + ip += 3; + break; + + case Op::Contrast: + if(ip + 1 >= size) return false; + ip += 2; + break; + + case Op::Multiply: + case Op::Screen: { + uint32_t tmp32 = 0; + if(!read32(ip, tmp32)) return false; + } break; + + case Op::Colorize: { + uint32_t tmp32 = 0; + if(!read32(ip, tmp32)) return false; + if(!read8(ip, opByte)) return false; + } break; + + case Op::Anim: { + uint16_t frameW = 0; + uint16_t frameH = 0; + uint16_t frameCount = 0; + uint16_t fpsQ = 0; + uint8_t flags = 0; + if(!read16(ip, frameW)) return false; + if(!read16(ip, frameH)) return false; + if(!read16(ip, frameCount)) return false; + if(!read16(ip, fpsQ)) return false; + if(!read8(ip, flags)) return false; + (void)frameW; (void)frameH; (void)frameCount; (void)fpsQ; (void)flags; + } break; + + case Op::Overlay: + case Op::Mask: { + SrcMeta src{}; + if(!readSrc(ip, src)) return false; + if(!patchTexId(src)) return false; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(!scan(subStart, subEnd)) return false; + } + } break; + + case Op::LowPart: { + if(!read8(ip, opByte)) return false; + SrcMeta src{}; + if(!readSrc(ip, src)) return false; + if(!patchTexId(src)) return false; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(!scan(subStart, subEnd)) return false; + } + } break; + + case Op::Combine: { + uint16_t w = 0, h = 0, n = 0; + if(!read16(ip, w)) return false; + if(!read16(ip, h)) return false; + if(!read16(ip, n)) return false; + for(uint16_t i = 0; i < n; ++i) { + uint16_t tmp16 = 0; + if(!read16(ip, tmp16)) return false; + if(!read16(ip, tmp16)) return false; + SrcMeta src{}; + if(!readSrc(ip, src)) return false; + if(!patchTexId(src)) return false; + if(src.Kind == SrcKind::Sub) { + size_t subStart = src.Off; + size_t subEnd = subStart + src.Len; + if(!scan(subStart, subEnd)) return false; + } + } + (void)w; (void)h; + } break; + + default: + return false; + } + } + return true; + }; + + return scan(0, size); + } + + static std::vector extractAnimationSpecs(const std::vector& code) { + return extractAnimationSpecs(code.data(), code.size()); + } + + void fromBytes(std::vector bytes) { + Code_ = std::move(bytes); + Patches_.clear(); + Source_.clear(); + PendingSub_.clear(); + } + +private: + // ======================== + // Byte helpers (little-endian) + // ======================== + static inline uint16_t _rd16(const std::vector& c, size_t& ip) { + uint16_t v = uint16_t(c[ip]) | (uint16_t(c[ip+1]) << 8); + ip += 2; + return v; + } + static inline uint32_t _rd24(const std::vector& c, size_t& ip) { + uint32_t v = uint32_t(c[ip]) | (uint32_t(c[ip+1]) << 8) | (uint32_t(c[ip+2]) << 16); + ip += 3; + return v; + } + static inline uint32_t _rd32(const std::vector& c, size_t& ip) { + uint32_t v = uint32_t(c[ip]) | + (uint32_t(c[ip+1]) << 8) | + (uint32_t(c[ip+2]) << 16) | + (uint32_t(c[ip+3]) << 24); + ip += 4; + return v; + } + + static inline void _wr8 (std::vector& o, uint32_t v){ o.push_back(uint8_t(v & 0xFFu)); } + static inline void _wr16(std::vector& o, uint32_t v){ + o.push_back(uint8_t(v & 0xFFu)); + o.push_back(uint8_t((v >> 8) & 0xFFu)); + } + static inline void _wr24(std::vector& o, uint32_t v){ + o.push_back(uint8_t(v & 0xFFu)); + o.push_back(uint8_t((v >> 8) & 0xFFu)); + o.push_back(uint8_t((v >> 16) & 0xFFu)); + } + static inline void _wr32(std::vector& o, uint32_t v){ + o.push_back(uint8_t(v & 0xFFu)); + o.push_back(uint8_t((v >> 8) & 0xFFu)); + o.push_back(uint8_t((v >> 16) & 0xFFu)); + o.push_back(uint8_t((v >> 24) & 0xFFu)); + } + + // ======================== + // SrcRef encoding in bytes (variable length) + // kind(1) + payload + // TexId: id24(3) => total 4 + // Sub : off16(3) + len16(3) => total 7 + // ======================== + enum class SrcKind : uint8_t { TexId = 0, Sub = 1 }; + + struct SrcRef { + SrcKind Kind{}; + uint32_t TexId24 = 0; // for TexId + uint16_t Off24 = 0; // for Sub + uint16_t Len24 = 0; // for Sub + }; + + // ======================== + // Opcodes (1 byte) + // ======================== + enum class Op : uint8_t { + End = 0, + + Base_Tex = 1, // SrcRef(TexId) + Base_Fill = 2, // w16, h16, color32 + Base_Anim = 3, // SrcRef(TexId), frameW16, frameH16, frames16, fpsQ16, flags8 + + Resize = 10, // w16, h16 + Transform = 11, // t8 + Opacity = 12, // a8 + NoAlpha = 13, // - + MakeAlpha = 14, // rgb24 (3 bytes) RR,GG,BB + Invert = 15, // mask8 + Brighten = 16, // - + Contrast = 17, // cBias8, bBias8 (bias-127) + Multiply = 18, // color32 + Screen = 19, // color32 + Colorize = 20, // color32, ratio8 + Anim = 21, // frameW16, frameH16, frames16, fpsQ16, flags8 + + Overlay = 30, // SrcRef (var) + Mask = 31, // SrcRef (var) + LowPart = 32, // percent8, SrcRef (var) + + Combine = 40 // w16,h16,n16 then n*(x16,y16,SrcRef) (если понадобится — допишем DSL) + }; + + // ======================== + // Pixel helpers (assume 0xAARRGGBB) + // ======================== + static inline uint8_t _a(uint32_t c){ return uint8_t((c >> 24) & 0xFF); } + static inline uint8_t _r(uint32_t c){ return uint8_t((c >> 16) & 0xFF); } + static inline uint8_t _g(uint32_t c){ return uint8_t((c >> 8) & 0xFF); } + static inline uint8_t _b(uint32_t c){ return uint8_t((c >> 0) & 0xFF); } + static inline uint32_t _pack(uint8_t a,uint8_t r,uint8_t g,uint8_t b){ + return (uint32_t(a)<<24)|(uint32_t(r)<<16)|(uint32_t(g)<<8)|(uint32_t(b)); + } + static inline uint8_t _clampu8(int v){ return uint8_t(std::min(255, std::max(0, v))); } + + // ======================== + // VM (executes bytes) + // ======================== + struct Image { + uint32_t W=0,H=0; + std::vector Px; + }; + + class VM { + public: + using TextureProvider = TexturePipelineProgram::TextureProviderFunc; + + explicit VM(TextureProvider provider) : Provider_(std::move(provider)) {} + + bool run(const std::vector& code, OwnedTexture& out, double timeSeconds, std::string* err) { + if(code.empty()) { if(err) *err="Empty bytecode"; return false; } + + Image cur; + std::unordered_map texCache; + std::unordered_map subCache; // key = (off<<24) | len + + size_t ip = 0; + + auto need = [&](size_t n)->bool{ + if(ip + n > code.size()) { if(err) *err="Bytecode truncated"; return false; } + return true; + }; + + while(true) { + if(!need(1)) return false; + Op op = static_cast(code[ip++]); + if(op == Op::End) break; + + switch(op) { + case Op::Base_Tex: { + SrcRef src; + if(!_readSrc(code, ip, src, err)) return false; + if(src.Kind != SrcKind::TexId) return _bad(err, "Base_Tex must be TexId"); + cur = _loadTex(src.TexId24, texCache, err); + if(cur.W == 0) return false; + } break; + + case Op::Base_Fill: { + if(!need(2+2+4)) return false; + uint32_t w = _rd16(code, ip); + uint32_t h = _rd16(code, ip); + uint32_t color = _rd32(code, ip); + cur = _makeSolid(w, h, color); + } break; + + case Op::Base_Anim: { + SrcRef src; + if(!_readSrc(code, ip, src, err)) return false; + if(src.Kind != SrcKind::TexId) return _bad(err, "Base_Anim must be TexId"); + if(!need(2+2+2+2+1)) return false; + + uint32_t frameW = _rd16(code, ip); + uint32_t frameH = _rd16(code, ip); + uint32_t frameCount = _rd16(code, ip); + uint32_t fpsQ = _rd16(code, ip); + uint32_t flags = code[ip++]; + + Image sheet = _loadTex(src.TexId24, texCache, err); + if(sheet.W == 0) return false; + + uint32_t fw = frameW ? frameW : sheet.W; + uint32_t fh = frameH ? frameH : sheet.H; + if(fw == 0 || fh == 0) return _bad(err, "Base_Anim invalid frame size"); + + bool horizontal = (flags & AnimHorizontal) != 0; + if(frameCount == 0) { + uint32_t avail = horizontal ? (sheet.W / fw) : (sheet.H / fh); + frameCount = std::max(1u, avail); + } + + uint32_t fpsQv = fpsQ ? fpsQ : DefaultAnimFpsQ; + double fps = double(fpsQv) / 256.0; + double frameTime = timeSeconds * fps; + if(frameTime < 0.0) frameTime = 0.0; + + uint32_t frameIndex = frameCount ? (uint32_t(frameTime) % frameCount) : 0u; + double frac = frameTime - std::floor(frameTime); + + cur = _cropFrame(sheet, frameIndex, fw, fh, horizontal); + + if(flags & AnimSmooth) { + uint32_t nextIndex = frameCount ? ((frameIndex + 1u) % frameCount) : 0u; + Image next = _cropFrame(sheet, nextIndex, fw, fh, horizontal); + _lerp(cur, next, frac); + } + } break; + + case Op::Anim: { + if(!cur.W || !cur.H) return _bad(err, "Anim requires base image"); + if(!need(2+2+2+2+1)) return false; + + uint32_t frameW = _rd16(code, ip); + uint32_t frameH = _rd16(code, ip); + uint32_t frameCount = _rd16(code, ip); + uint32_t fpsQ = _rd16(code, ip); + uint32_t flags = code[ip++]; + + const Image& sheet = cur; + uint32_t fw = frameW ? frameW : sheet.W; + uint32_t fh = frameH ? frameH : sheet.H; + if(fw == 0 || fh == 0) return _bad(err, "Anim invalid frame size"); + + bool horizontal = (flags & AnimHorizontal) != 0; + if(frameCount == 0) { + uint32_t avail = horizontal ? (sheet.W / fw) : (sheet.H / fh); + frameCount = std::max(1u, avail); + } + + uint32_t fpsQv = fpsQ ? fpsQ : DefaultAnimFpsQ; + double fps = double(fpsQv) / 256.0; + double frameTime = timeSeconds * fps; + if(frameTime < 0.0) frameTime = 0.0; + + uint32_t frameIndex = frameCount ? (uint32_t(frameTime) % frameCount) : 0u; + double frac = frameTime - std::floor(frameTime); + + cur = _cropFrame(sheet, frameIndex, fw, fh, horizontal); + if(flags & AnimSmooth) { + uint32_t nextIndex = frameCount ? ((frameIndex + 1u) % frameCount) : 0u; + Image next = _cropFrame(sheet, nextIndex, fw, fh, horizontal); + _lerp(cur, next, frac); + } + } break; + + case Op::Overlay: { + SrcRef src; + if(!_readSrc(code, ip, src, err)) return false; + Image over = _loadSrc(code, src, texCache, subCache, timeSeconds, err); + if(over.W == 0) return false; + if(!cur.W) { cur = std::move(over); break; } + over = _resizeNN_ifNeeded(over, cur.W, cur.H); + _alphaOver(cur, over); + } break; + + case Op::Mask: { + SrcRef src; + if(!_readSrc(code, ip, src, err)) return false; + Image m = _loadSrc(code, src, texCache, subCache, timeSeconds, err); + if(m.W == 0) return false; + if(!cur.W) return _bad(err, "Mask requires base image"); + m = _resizeNN_ifNeeded(m, cur.W, cur.H); + _applyMask(cur, m); + } break; + + case Op::LowPart: { + if(!need(1)) return false; + uint32_t pct = std::min(100u, uint32_t(code[ip++])); + SrcRef src; + if(!_readSrc(code, ip, src, err)) return false; + Image over = _loadSrc(code, src, texCache, subCache, timeSeconds, err); + if(over.W == 0) return false; + if(!cur.W) return _bad(err, "LowPart requires base image"); + over = _resizeNN_ifNeeded(over, cur.W, cur.H); + _lowpart(cur, over, pct); + } break; + + case Op::Resize: { + if(!cur.W) return _bad(err, "Resize requires base image"); + if(!need(2+2)) return false; + uint32_t w = _rd16(code, ip); + uint32_t h = _rd16(code, ip); + cur = _resizeNN(cur, w, h); + } break; + + case Op::Transform: { + if(!cur.W) return _bad(err, "Transform requires base image"); + if(!need(1)) return false; + uint32_t t = code[ip++] & 7u; + cur = _transform(cur, t); + } break; + + case Op::Opacity: { + if(!cur.W) return _bad(err, "Opacity requires base image"); + if(!need(1)) return false; + uint32_t a = code[ip++] & 0xFFu; + _opacity(cur, uint8_t(a)); + } break; + + case Op::NoAlpha: { + if(!cur.W) return _bad(err, "NoAlpha requires base image"); + _noAlpha(cur); + } break; + + case Op::MakeAlpha: { + if(!cur.W) return _bad(err, "MakeAlpha requires base image"); + if(!need(3)) return false; + uint32_t rr = code[ip++], gg = code[ip++], bb = code[ip++]; + uint32_t rgb24 = (rr << 16) | (gg << 8) | bb; + _makeAlpha(cur, rgb24); + } break; + + case Op::Invert: { + if(!cur.W) return _bad(err, "Invert requires base image"); + if(!need(1)) return false; + uint32_t mask = code[ip++] & 0xFu; + _invert(cur, mask); + } break; + + case Op::Brighten: { + if(!cur.W) return _bad(err, "Brighten requires base image"); + _brighten(cur); + } break; + + case Op::Contrast: { + if(!cur.W) return _bad(err, "Contrast requires base image"); + if(!need(2)) return false; + int c = int(code[ip++]) - 127; + int b = int(code[ip++]) - 127; + _contrast(cur, c, b); + } break; + + case Op::Multiply: { + if(!cur.W) return _bad(err, "Multiply requires base image"); + if(!need(4)) return false; + uint32_t color = _rd32(code, ip); + _multiply(cur, color); + } break; + + case Op::Screen: { + if(!cur.W) return _bad(err, "Screen requires base image"); + if(!need(4)) return false; + uint32_t color = _rd32(code, ip); + _screen(cur, color); + } break; + + case Op::Colorize: { + if(!cur.W) return _bad(err, "Colorize requires base image"); + if(!need(4+1)) return false; + uint32_t color = _rd32(code, ip); + uint32_t ratio = code[ip++] & 0xFFu; + _colorize(cur, color, uint8_t(ratio)); + } break; + + default: + return _bad(err, "Unknown opcode (no skip table in this minimal VM)"); + } + } + + out.Width = cur.W; + out.Height = cur.H; + out.Pixels = std::move(cur.Px); + return true; + } + + private: + TextureProvider Provider_; + + static bool _bad(std::string* err, const char* msg){ if(err) *err = msg; return false; } + + static bool _readSrc(const std::vector& code, size_t& ip, SrcRef& out, std::string* err) { + if(ip >= code.size()) return _bad(err, "Bytecode truncated (SrcRef.kind)"); + out.Kind = static_cast(code[ip++]); + if(out.Kind == SrcKind::TexId) { + if(ip + 3 > code.size()) return _bad(err, "Bytecode truncated (TexId24)"); + out.TexId24 = _rd24(code, ip); + out.Off24 = 0; out.Len24 = 0; + return true; + } + if(out.Kind == SrcKind::Sub) { + if(ip + 6 > code.size()) return _bad(err, "Bytecode truncated (Sub off/len)"); + out.Off24 = _rd24(code, ip); + out.Len24 = _rd24(code, ip); + out.TexId24 = 0; + return true; + } + return _bad(err, "Unknown SrcKind"); + } + + Image _loadTex(uint32_t id, std::unordered_map& cache, std::string* err) { + auto it = cache.find(id); + if(it != cache.end()) return it->second; + + auto t = Provider_(id); + if(!t || !t->Pixels || !t->Width || !t->Height) { + if(err) *err = "Texture id not found: " + std::to_string(id); + return {}; + } + Image img; + img.W = t->Width; img.H = t->Height; + img.Px.assign(t->Pixels, t->Pixels + size_t(img.W)*size_t(img.H)); + cache.emplace(id, img); + return img; + } + + Image _loadSub(const std::vector& code, + uint32_t off, uint32_t len, + std::unordered_map& /*texCache*/, + std::unordered_map& subCache, + double timeSeconds, + std::string* err) { + uint64_t key = (uint32_t(off) << 24) | uint32_t(len); + auto it = subCache.find(key); + if(it != subCache.end()) return it->second; + + size_t start = size_t(off); + size_t end = start + size_t(len); + if(end > code.size()) { if(err) *err="Subprogram out of range"; return {}; } + + std::vector slice(code.begin()+start, code.begin()+end); + OwnedTexture tmp; + VM nested(Provider_); + if(!nested.run(slice, tmp, timeSeconds, err)) return {}; + + Image img; + img.W = tmp.Width; img.H = tmp.Height; img.Px = std::move(tmp.Pixels); + subCache.emplace(key, img); + return img; + } + + Image _loadSrc(const std::vector& code, + const SrcRef& src, + std::unordered_map& texCache, + std::unordered_map& subCache, + double timeSeconds, + std::string* err) { + if(src.Kind == SrcKind::TexId) return _loadTex(src.TexId24, texCache, err); + if(src.Kind == SrcKind::Sub) return _loadSub(code, src.Off24, src.Len24, texCache, subCache, timeSeconds, err); + if(err) *err = "Unknown SrcKind"; + return {}; + } + + // ---- image ops (как в исходнике) ---- + static Image _makeSolid(uint32_t w, uint32_t h, uint32_t color) { + Image img; img.W=w; img.H=h; + img.Px.assign(size_t(w)*size_t(h), color); + return img; + } + + static Image _resizeNN(const Image& src, uint32_t nw, uint32_t nh) { + Image dst; dst.W=nw; dst.H=nh; + dst.Px.resize(size_t(nw)*size_t(nh)); + for(uint32_t y=0;y= sheet.H) continue; + for(uint32_t x = 0; x < fw; ++x) { + uint32_t sx = baseX + x; + if(sx >= sheet.W) continue; + out.Px[size_t(y) * fw + x] = sheet.Px[size_t(sy) * sheet.W + sx]; + } + } + return out; + } + + static void _lerp(Image& base, const Image& over, double t) { + if(t <= 0.0) return; + if(t >= 1.0) { base = over; return; } + if(base.W != over.W || base.H != over.H) return; + + const size_t n = base.Px.size(); + for(size_t i = 0; i < n; ++i) { + uint32_t a = base.Px[i]; + uint32_t b = over.Px[i]; + int ar = _r(a), ag = _g(a), ab = _b(a), aa = _a(a); + int br = _r(b), bg = _g(b), bb = _b(b), ba = _a(b); + + uint8_t rr = _clampu8(int(ar + (br - ar) * t)); + uint8_t rg = _clampu8(int(ag + (bg - ag) * t)); + uint8_t rb = _clampu8(int(ab + (bb - ab) * t)); + uint8_t ra = _clampu8(int(aa + (ba - aa) * t)); + + base.Px[i] = _pack(ra, rr, rg, rb); + } + } + + static void _alphaOver(Image& base, const Image& over) { + const size_t n = base.Px.size(); + for(size_t i=0;i(255, (outRp * 255) / outA)); + outG = uint8_t(std::min(255, (outGp * 255) / outA)); + outB = uint8_t(std::min(255, (outBp * 255) / outA)); + } + base.Px[i] = _pack(uint8_t(outA), outR, outG, outB); + } + } + + static void _applyMask(Image& base, const Image& mask) { + const size_t n = base.Px.size(); + for(size_t i=0;i> 16) & 0xFF); + uint8_t gg = uint8_t((rgb24 >> 8) & 0xFF); + uint8_t bb = uint8_t((rgb24 >> 0) & 0xFF); + for(auto& p : img.Px) { + if(_r(p)==rr && _g(p)==gg && _b(p)==bb) p = _pack(0, _r(p), _g(p), _b(p)); + } + } + static void _invert(Image& img, uint32_t maskBits) { + for(auto& p : img.Px) { + uint8_t a=_a(p), r=_r(p), g=_g(p), b=_b(p); + if(maskBits & 1u) r = 255 - r; + if(maskBits & 2u) g = 255 - g; + if(maskBits & 4u) b = 255 - b; + if(maskBits & 8u) a = 255 - a; + p = _pack(a,r,g,b); + } + } + static void _brighten(Image& img) { + for(auto& p : img.Px) { + int r = _r(p), g = _g(p), b = _b(p); + r = r + (255 - r) / 3; + g = g + (255 - g) / 3; + b = b + (255 - b) / 3; + p = _pack(_a(p), _clampu8(r), _clampu8(g), _clampu8(b)); + } + } + static void _contrast(Image& img, int c, int br) { + double C = double(std::max(-127, std::min(127, c))); + double factor = (259.0 * (C + 255.0)) / (255.0 * (259.0 - C)); + for(auto& p : img.Px) { + int r = int(factor * (int(_r(p)) - 128) + 128) + br; + int g = int(factor * (int(_g(p)) - 128) + 128) + br; + int b = int(factor * (int(_b(p)) - 128) + 128) + br; + p = _pack(_a(p), _clampu8(r), _clampu8(g), _clampu8(b)); + } + } + static void _multiply(Image& img, uint32_t color) { + uint8_t cr=_r(color), cg=_g(color), cb=_b(color); + for(auto& p : img.Px) { + uint8_t r = uint8_t((uint32_t(_r(p)) * cr) / 255); + uint8_t g = uint8_t((uint32_t(_g(p)) * cg) / 255); + uint8_t b = uint8_t((uint32_t(_b(p)) * cb) / 255); + p = _pack(_a(p), r,g,b); + } + } + static void _screen(Image& img, uint32_t color) { + uint8_t cr=_r(color), cg=_g(color), cb=_b(color); + for(auto& p : img.Px) { + uint8_t r = uint8_t(255 - ((255 - _r(p)) * (255 - cr)) / 255); + uint8_t g = uint8_t(255 - ((255 - _g(p)) * (255 - cg)) / 255); + uint8_t b = uint8_t(255 - ((255 - _b(p)) * (255 - cb)) / 255); + p = _pack(_a(p), r,g,b); + } + } + static void _colorize(Image& img, uint32_t color, uint8_t ratio) { + uint8_t cr=_r(color), cg=_g(color), cb=_b(color); + for(auto& p : img.Px) { + int r = (int(_r(p)) * (255 - ratio) + int(cr) * ratio) / 255; + int g = (int(_g(p)) * (255 - ratio) + int(cg) * ratio) / 255; + int b = (int(_b(p)) * (255 - ratio) + int(cb) * ratio) / 255; + p = _pack(_a(p), uint8_t(r), uint8_t(g), uint8_t(b)); + } + } + static void _lowpart(Image& base, const Image& over, uint32_t percent) { + uint32_t startY = base.H - (base.H * percent) / 100; + for(uint32_t y=startY; y(255, (outRp * 255) / outA)); + outG = uint8_t(std::min(255, (outGp * 255) / outA)); + outB = uint8_t(std::min(255, (outBp * 255) / outA)); + } + base.Px[i] = _pack(uint8_t(outA), outR, outG, outB); + } + } + } + + static Image _transform(const Image& src, uint32_t t) { + Image dst; + auto at = [&](uint32_t x, uint32_t y)->uint32_t { return src.Px[size_t(y)*src.W + x]; }; + auto make = [&](uint32_t w, uint32_t h){ + Image d; d.W=w; d.H=h; d.Px.resize(size_t(w)*size_t(h)); + return d; + }; + auto set = [&](Image& im, uint32_t x, uint32_t y, uint32_t v){ + im.Px[size_t(y)*im.W + x] = v; + }; + + switch (t & 7u) { + case 0: return src; + case 1: { dst = make(src.H, src.W); + for(uint32_t y=0;y op(...) + // tex 32x32 "#RRGGBBAA" + // nested only where op expects a texture arg: + // overlay( tex other |> ... ) + // Also supports overlay(other) / mask(other) / lowpart(50, other) + // ======================== + enum class TokKind { End, Ident, Number, String, Pipe, Comma, LParen, RParen, Eq, X }; + + struct Tok { + TokKind Kind = TokKind::End; + std::string Text; + uint32_t U32 = 0; + }; + + struct Lexer { + std::string_view S; + size_t I=0; + + bool HasBuf = false; + Tok Buf; + + static bool isAlpha(char c){ return (c>='a'&&c<='z')||(c>='A'&&c<='Z')||c=='_'; } + static bool isNum(char c){ return (c>='0'&&c<='9'); } + static bool isAlnum(char c){ return isAlpha(c)||isNum(c); } + + void unread(const Tok& t) { + // allow only 1-level unread + HasBuf = true; + Buf = t; + } + + Tok peek() { + Tok t = next(); + unread(t); + return t; + } + + void skipWs() { + while (I < S.size()) { + char c = S[I]; + if(c==' '||c=='\t'||c=='\r'||c=='\n'){ I++; continue; } + if(c=='#'){ while (I= S.size()) return {TokKind::End, {}, 0}; + + if(S[I]=='|' && I+1') { I+=2; return {TokKind::Pipe, "|>",0}; } + char c = S[I]; + if(c==','){ I++; return {TokKind::Comma,",",0}; } + if(c=='('){ I++; return {TokKind::LParen,"(",0}; } + if(c==')'){ I++; return {TokKind::RParen,")",0}; } + if(c=='='){ I++; return {TokKind::Eq,"=",0}; } + if(c=='x' || c=='X'){ I++; return {TokKind::X,"x",0}; } + + if(c=='"') { + I++; + std::string out; + while (I < S.size()) { + char ch = S[I++]; + if(ch=='"') break; + if(ch=='\\' && I Pos; + std::unordered_map Named; + }; + + // ======================== + // Compiler state + // ======================== + std::string Source_; + std::vector Code_; + std::vector Patches_; + + // ---- emit helpers (target = arbitrary out vector) ---- + static inline void _emitOp(std::vector& out, Op op) { _wr8(out, uint8_t(op)); } + static inline void _emitU8(std::vector& out, uint32_t v){ _wr8(out, v); } + static inline void _emitU16(std::vector& out, uint32_t v){ _wr16(out, v); } + static inline void _emitU24(std::vector& out, uint32_t v){ _wr24(out, v); } + static inline void _emitU32(std::vector& out, uint32_t v){ _wr32(out, v); } + + // reserve 3 bytes for u24 texId and register patch (absolute or relative) + struct RelPatch { size_t Rel0; std::string Name; }; + + static void _emitTexPatchU24(std::vector& out, + std::vector* absPatches, + std::vector* relPatches, + const std::string& name) { + const size_t idx = out.size(); + out.push_back(0); out.push_back(0); out.push_back(0); + if(absPatches) absPatches->push_back(Patch{idx, name}); + if(relPatches) relPatches->push_back(RelPatch{idx, name}); + } + + static void _emitSrcTexName(std::vector& out, + std::vector* absPatches, + std::vector* relPatches, + const std::string& name) { + _emitU8(out, uint8_t(SrcKind::TexId)); + _emitTexPatchU24(out, absPatches, relPatches, name); + } + + static void _emitSrcSub(std::vector& out, uint32_t off24, uint32_t len24) { + _emitU8(out, uint8_t(SrcKind::Sub)); + _emitU24(out, off24); + _emitU24(out, len24); + } + + // ======================== + // Color parsing: #RRGGBB or #RRGGBBAA -> 0xAARRGGBB + // ======================== + static bool _parseHexColor(std::string_view s, uint32_t& outARGB) { + if(s.size()!=7 && s.size()!=9) return false; + if(s[0] != '#') return false; + auto hex = [](char c)->int{ + if(c>='0'&&c<='9') return c-'0'; + if(c>='a'&&c<='f') return 10+(c-'a'); + if(c>='A'&&c<='F') return 10+(c-'A'); + return -1; + }; + auto byteAt = [&](size_t idx)->std::optional{ + int hi=hex(s[idx]), lo=hex(s[idx+1]); + if(hi<0||lo<0) return std::nullopt; + return uint8_t((hi<<4)|lo); + }; + auto r = byteAt(1), g = byteAt(3), b = byteAt(5); + if(!r||!g||!b) return false; + uint8_t a = 255; + if(s.size()==9) { + auto aa = byteAt(7); + if(!aa) return false; + a = *aa; + } + outARGB = (uint32_t(a)<<24) | (uint32_t(*r)<<16) | (uint32_t(*g)<<8) | (uint32_t(*b)); + return true; + } + + // ======================== + // Parsing entry: full program + // ======================== + bool _parseProgram(std::string* err) { + Lexer lx{Source_}; + Tok t = lx.next(); + if(!(t.Kind==TokKind::Ident && t.Text=="tex")) { + if(err) *err="Expected 'tex' at start"; + return false; + } + + // compile base into main Code_ + if(!_compileBaseAfterTex(lx, Code_, /*abs*/&Patches_, /*rel*/nullptr, err)) return false; + + // pipeline: |> op ... + Tok nt = lx.next(); + while (nt.Kind == TokKind::Pipe) { + Tok opName = lx.next(); + if(opName.Kind != TokKind::Ident) { if(err) *err="Expected op name after |>"; return false; } + ParsedOp op; op.Name = opName.Text; + + Tok peek = lx.next(); + if(peek.Kind == TokKind::LParen) { + if(!_parseArgListOrTextureExpr(lx, op, err)) return false; + nt = lx.next(); + } else { + nt = peek; // no-arg op + } + + if(!_compileOpInto(lx, op, Code_, /*abs*/&Patches_, /*rel*/nullptr, err)) return false; + } + + _emitOp(Code_, Op::End); + if (Code_.size() > MaxCodeBytes) { + if (err) + *err = "Pipeline bytecode too large: " + std::to_string(Code_.size()) + + " > MaxCodeBytes(" + std::to_string(MaxCodeBytes) + ")"; + return false; + } + + return true; + } + + // ======================== + // Base compilation after 'tex' + // supports: + // 1) tex name + // 2) tex "name(.png/.jpg/.jpeg)" (allowed but normalized) + // 3) tex anim(...) + // 4) tex 32x32 "#RRGGBBAA" + // ======================== + bool _compileBaseAfterTex(Lexer& lx, + std::vector& out, + std::vector* absPatches, + std::vector* relPatches, + std::string* err) { + Tok a = lx.next(); + + if(a.Kind == TokKind::Ident && a.Text == "anim") { + Tok lp = lx.next(); + if(lp.Kind != TokKind::LParen) { if(err) *err="Expected '(' after anim"; return false; } + + ParsedOp op; op.Name="anim"; + if(!_parseArgList(lx, op, err)) return false; + + auto posU = [&](size_t i)->std::optional{ + if(i >= op.Pos.size()) return std::nullopt; + if(op.Pos[i].Kind != ArgVal::ValueKind::U32) return std::nullopt; + return op.Pos[i].U32; + }; + auto posS = [&](size_t i)->std::optional{ + if(i >= op.Pos.size()) return std::nullopt; + return op.Pos[i].S; + }; + auto namedU = [&](std::string_view k)->std::optional{ + auto it = op.Named.find(std::string(k)); + if(it==op.Named.end() || it->second.Kind!=ArgVal::ValueKind::U32) return std::nullopt; + return it->second.U32; + }; + auto namedS = [&](std::string_view k)->std::optional{ + auto it = op.Named.find(std::string(k)); + if(it==op.Named.end()) return std::nullopt; + return it->second.S; + }; + + std::string tex = namedS("tex").value_or(posS(0).value_or("")); + if(tex.empty()) { if(err) *err="anim requires texture name"; return false; } + + uint32_t frameW = namedU("frame_w").value_or(namedU("w").value_or(posU(1).value_or(0))); + uint32_t frameH = namedU("frame_h").value_or(namedU("h").value_or(posU(2).value_or(0))); + uint32_t frames = namedU("frames").value_or(namedU("count").value_or(posU(3).value_or(0))); + uint32_t fps = namedU("fps").value_or(posU(4).value_or(0)); + uint32_t smooth = namedU("smooth").value_or(posU(5).value_or(0)); + + std::string axis = namedS("axis").value_or(""); + bool horizontal = (!axis.empty() && (axis[0] == 'x' || axis[0] == 'h')); + + if(frameW > 65535u || frameH > 65535u || frames > 65535u) { + if(err) *err="anim params must fit uint16"; + return false; + } + + uint32_t fpsQ = fps ? std::min(0xFFFFu, fps * 256u) : DefaultAnimFpsQ; + uint32_t flags = (smooth ? AnimSmooth : 0) | (horizontal ? AnimHorizontal : 0); + + _emitOp(out, Op::Base_Anim); + _emitSrcTexName(out, absPatches, relPatches, tex); + _emitU16(out, frameW); + _emitU16(out, frameH); + _emitU16(out, frames); + _emitU16(out, fpsQ); + _emitU8(out, flags); + return true; + } + + if(a.Kind == TokKind::Ident || a.Kind == TokKind::String) { + // tex name (or tex "name.png" => normalized) + _emitOp(out, Op::Base_Tex); + _emitSrcTexName(out, absPatches, relPatches, a.Text); + return true; + } + + if(a.Kind == TokKind::Number) { + // tex 32x32 "#RRGGBBAA" + Tok xTok = lx.next(); + Tok b = lx.next(); + Tok colTok = lx.next(); + if(xTok.Kind != TokKind::X || b.Kind != TokKind::Number || (colTok.Kind!=TokKind::Ident && colTok.Kind!=TokKind::String)) { + if(err) *err="Expected: tex x <#color>"; + return false; + } + uint32_t w = a.U32, h = b.U32; + uint32_t color = 0; + if(!_parseHexColor(colTok.Text, color)) { + if(err) *err="Bad color literal (use #RRGGBB or #RRGGBBAA)"; + return false; + } + if(w>65535u || h>65535u) { if(err) *err="w/h must fit in uint16"; return false; } + _emitOp(out, Op::Base_Fill); + _emitU16(out, w); + _emitU16(out, h); + _emitU32(out, color); + return true; + } + + if(err) *err="Bad 'tex' base expression"; + return false; + } + + // ======================== + // Args parsing: + // - normal args: (a,b,key=v) + // - OR if first token inside '(' is 'tex' => parse nested program until ')' + // ======================== + bool _parseArgListOrTextureExpr(Lexer& lx, ParsedOp& op, std::string* err) { + Tok first = lx.next(); + + if(first.Kind==TokKind::Ident && first.Text=="tex") { + // marker + ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = "__SUBTEX__"; + op.Pos.push_back(av); + + PendingSubData sub; + if(!_compileSubProgramFromAlreadySawTex(lx, sub, err)) return false; + + Tok end = lx.next(); + if(end.Kind != TokKind::RParen) { if(err) *err="Expected ')' after sub texture expr"; return false; } + + PendingSub_[&op] = std::move(sub); + return true; + } + + // otherwise parse as normal arg list, where `first` is first token inside '(' + Tok t = first; + if(t.Kind == TokKind::RParen) return true; + + while (true) { + if(t.Kind == TokKind::Ident) { + Tok maybeEq = lx.next(); + if(maybeEq.Kind == TokKind::Eq) { + Tok v = lx.next(); + ArgVal av; + if(!_tokToVal(v, av, err)) return false; + op.Named[t.Text] = std::move(av); + t = lx.next(); + } else { + ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = t.Text; + op.Pos.push_back(std::move(av)); + t = maybeEq; + } + } else { + ArgVal av; + if(!_tokToVal(t, av, err)) return false; + op.Pos.push_back(std::move(av)); + t = lx.next(); + } + + if(t.Kind == TokKind::Comma) { t = lx.next(); continue; } + if(t.Kind == TokKind::RParen) return true; + + if(err) *err = "Expected ',' or ')' in argument list"; + return false; + } + } + + bool _parseArgList(Lexer& lx, ParsedOp& op, std::string* err) { + Tok t = lx.next(); + if(t.Kind == TokKind::RParen) return true; + + while (true) { + if(t.Kind == TokKind::Ident) { + Tok maybeEq = lx.next(); + if(maybeEq.Kind == TokKind::Eq) { + Tok v = lx.next(); + ArgVal av; + if(!_tokToVal(v, av, err)) return false; + op.Named[t.Text] = std::move(av); + t = lx.next(); + } else { + ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = t.Text; + op.Pos.push_back(std::move(av)); + t = maybeEq; + } + } else { + ArgVal av; + if(!_tokToVal(t, av, err)) return false; + op.Pos.push_back(std::move(av)); + t = lx.next(); + } + + if(t.Kind == TokKind::Comma) { t = lx.next(); continue; } + if(t.Kind == TokKind::RParen) return true; + + if(err) *err = "Expected ',' or ')' in argument list"; + return false; + } + } + + bool _tokToVal(const Tok& t, ArgVal& out, std::string* err) { + if(t.Kind == TokKind::Number) { out.Kind=ArgVal::ValueKind::U32; out.U32=t.U32; return true; } + if(t.Kind == TokKind::String) { out.Kind=ArgVal::ValueKind::Str; out.S=t.Text; return true; } + if(t.Kind == TokKind::Ident) { out.Kind=ArgVal::ValueKind::Ident; out.S=t.Text; return true; } + if(err) *err = "Expected value token"; + return false; + } + + // ======================== + // Subprogram compilation: + // we already consumed 'tex'. Parse base + pipeline until next token is ')' + // DO NOT consume ')' + // ======================== + struct PendingSubData { + std::vector Bytes; + std::vector RelPatches; + }; + + bool _compileSubProgramFromAlreadySawTex(Lexer& lx, PendingSubData& outSub, std::string* err) { + outSub.Bytes.clear(); + outSub.RelPatches.clear(); + + // base + if(!_compileBaseAfterTex(lx, outSub.Bytes, /*abs*/nullptr, /*rel*/&outSub.RelPatches, err)) + return false; + + // pipeline until ')' + while(true) { + // peek + Tok nt = lx.peek(); + if(nt.Kind == TokKind::RParen) break; + if(nt.Kind != TokKind::Pipe) { if(err) *err="Sub tex: expected '|>' or ')'"; return false; } + + // consume pipe + lx.next(); + Tok opName = lx.next(); + if(opName.Kind != TokKind::Ident) { if(err) *err="Sub tex: expected op name"; return false; } + + ParsedOp op; op.Name = opName.Text; + + Tok lp = lx.next(); + if(lp.Kind == TokKind::LParen) { + if(!_parseArgListOrTextureExpr(lx, op, err)) return false; + } else { + // no-arg op, lp already is next token (pipe or ')'), so we need to "unread" — can't. + // simplest: treat it as next token for outer loop by rewinding lexer state. + // We'll do it by storing the token back via a small hack: rebuild peek? Too heavy. + // Instead: enforce parentheses for ops in subprogram except no-arg ops (brighten/noalpha) which can be without. + // To keep behavior identical to main, we handle no-arg by rewinding I one token is not possible, + // so we accept that in subprogram, no-arg ops must be written as brighten() etc. + if(err) *err="Sub tex: no-arg ops must use parentheses, e.g. brighten()"; + return false; + } + + if(!_compileOpInto(lx, op, outSub.Bytes, /*abs*/nullptr, /*rel*/&outSub.RelPatches, err)) + return false; + } + + // Pipeline until we see ')' + while (true) { + Tok nt = lx.peek(); + if(nt.Kind == TokKind::RParen) break; + if(nt.Kind != TokKind::Pipe) { if(err) *err="Sub tex: expected '|>' or ')'"; return false; } + + // consume pipe + lx.next(); + + Tok opName = lx.next(); + if(opName.Kind != TokKind::Ident) { if(err) *err="Sub tex: expected op name"; return false; } + + ParsedOp op; op.Name = opName.Text; + + // allow both op and op(...) + Tok maybe = lx.peek(); + if(maybe.Kind == TokKind::LParen) { + lx.next(); // consume '(' + if(!_parseArgListOrTextureExpr(lx, op, err)) return false; + } else { + // no-arg op; nothing to parse + } + + if(!_compileOpInto(lx, op, outSub.Bytes, /*abs*/nullptr, /*rel*/&outSub.RelPatches, err)) + return false; + } + + + _emitOp(outSub.Bytes, Op::End); + return true; + } + + // pending subprogram associated with ParsedOp pointer (created during parsing) + mutable std::unordered_map PendingSub_; + + // Append subprogram to `out` and emit SrcRef(Sub, off16, len16), migrating patches properly. + static bool _appendSubprogram(std::vector& out, + PendingSubData&& sub, + std::vector* absPatches, + std::vector* relPatches, + uint32_t& outOff, + uint32_t& outLen, + std::string* err) { + const size_t offset = out.size(); + const size_t len = sub.Bytes.size(); + + if(offset > 0xFFFFFFu || len > 0xFFFFFFu || (offset + len) > 0xFFFFFFu) { + if(err) *err = "Subprogram слишком большой (off/len должны влезать в u24 байт)"; + return false; + } + + if(offset + len > MaxCodeBytes) { + if(err) *err = "Pipeline bytecode too large after sub append: " + + std::to_string(offset + len) + " > MaxCodeBytes(" + std::to_string(MaxCodeBytes) + ")"; + return false; + } + + // migrate patches + if(absPatches) { + for(const auto& rp : sub.RelPatches) { + absPatches->push_back(Patch{offset + rp.Rel0, rp.Name}); + } + } + if(relPatches) { + for(const auto& rp : sub.RelPatches) { + relPatches->push_back(RelPatch{offset + rp.Rel0, rp.Name}); + } + } + + out.insert(out.end(), sub.Bytes.begin(), sub.Bytes.end()); + + outOff = uint32_t(offset); + outLen = uint32_t(len); + return true; + } + + // ======================== + // Compile operations into arbitrary `out` + // absPatches != nullptr => patches recorded as absolute for this buffer + // relPatches != nullptr => patches recorded as relative for this buffer + // ======================== + bool _compileOpInto(Lexer& /*lx*/, + const ParsedOp& op, + std::vector& out, + std::vector* absPatches, + std::vector* relPatches, + std::string* err) { + auto posU = [&](size_t i)->std::optional{ + if(i >= op.Pos.size()) return std::nullopt; + if(op.Pos[i].Kind != ArgVal::ValueKind::U32) return std::nullopt; + return op.Pos[i].U32; + }; + auto posS = [&](size_t i)->std::optional{ + if(i >= op.Pos.size()) return std::nullopt; + return op.Pos[i].S; + }; + auto namedU = [&](std::string_view k)->std::optional{ + auto it = op.Named.find(std::string(k)); + if(it==op.Named.end() || it->second.Kind!=ArgVal::ValueKind::U32) return std::nullopt; + return it->second.U32; + }; + auto namedS = [&](std::string_view k)->std::optional{ + auto it = op.Named.find(std::string(k)); + if(it==op.Named.end()) return std::nullopt; + return it->second.S; + }; + + auto emitSrcFromName = [&](const std::string& n){ + _emitSrcTexName(out, absPatches, relPatches, n); + }; + + auto emitSrcFromPendingSub = [&]()->bool{ + auto it = PendingSub_.find(&op); + if(it == PendingSub_.end()) { if(err) *err="Internal: missing subprogram"; return false; } + uint32_t off=0, len=0; + if(!_appendSubprogram(out, std::move(it->second), absPatches, relPatches, off, len, err)) return false; + PendingSub_.erase(it); + _emitSrcSub(out, off, len); + return true; + }; + + // --- Ops that accept a "texture" argument: overlay/mask/lowpart --- + if(op.Name == "overlay") { + _emitOp(out, Op::Overlay); + if(!op.Pos.empty() && op.Pos[0].S == "__SUBTEX__") return emitSrcFromPendingSub(); + + // allow overlay(name) or overlay(tex=name) + std::string tex = namedS("tex").value_or(posS(0).value_or("")); + if(tex.empty()) { if(err) *err="overlay requires texture arg"; return false; } + emitSrcFromName(tex); + return true; + } + + if(op.Name == "mask") { + _emitOp(out, Op::Mask); + if(!op.Pos.empty() && op.Pos[0].S == "__SUBTEX__") return emitSrcFromPendingSub(); + + std::string tex = namedS("tex").value_or(posS(0).value_or("")); + if(tex.empty()) { if(err) *err="mask requires texture arg"; return false; } + emitSrcFromName(tex); + return true; + } + + if(op.Name == "lowpart") { + uint32_t pct = namedU("percent").value_or(posU(0).value_or(0)); + if(!pct) { if(err) *err="lowpart requires percent"; return false; } + + _emitOp(out, Op::LowPart); + _emitU8(out, std::min(100u, pct)); + + // 2nd arg can be nested subtex or name + if(op.Pos.size() >= 2 && op.Pos[1].S == "__SUBTEX__") return emitSrcFromPendingSub(); + + std::string tex = namedS("tex").value_or(posS(1).value_or("")); + if(tex.empty()) { if(err) *err="lowpart requires texture arg"; return false; } + emitSrcFromName(tex); + return true; + } + + // --- Unary ops --- + if(op.Name == "resize") { + uint32_t w = namedU("w").value_or(posU(0).value_or(0)); + uint32_t h = namedU("h").value_or(posU(1).value_or(0)); + if(!w || !h || w>65535u || h>65535u) { if(err) *err="resize(w,h) must fit uint16"; return false; } + _emitOp(out, Op::Resize); _emitU16(out, w); _emitU16(out, h); + return true; + } + + if(op.Name == "transform") { + uint32_t t = namedU("t").value_or(posU(0).value_or(0)); + _emitOp(out, Op::Transform); _emitU8(out, t & 7u); + return true; + } + + if(op.Name == "opacity") { + uint32_t a = namedU("a").value_or(posU(0).value_or(255)); + _emitOp(out, Op::Opacity); _emitU8(out, a & 0xFFu); + return true; + } + + if(op.Name == "remove_alpha" || op.Name == "noalpha") { + _emitOp(out, Op::NoAlpha); + return true; + } + + if(op.Name == "make_alpha") { + std::string col = namedS("color").value_or(posS(0).value_or("")); + uint32_t argb=0; + if(!_parseHexColor(col, argb)) { if(err) *err="make_alpha requires color #RRGGBB"; return false; } + uint32_t rgb24 = argb & 0x00FFFFFFu; + _emitOp(out, Op::MakeAlpha); + _emitU8(out, (rgb24 >> 16) & 0xFFu); + _emitU8(out, (rgb24 >> 8) & 0xFFu); + _emitU8(out, (rgb24 >> 0) & 0xFFu); + return true; + } + + if(op.Name == "invert") { + std::string ch = namedS("channels").value_or(posS(0).value_or("rgb")); + uint32_t mask=0; + for(char c : ch) { + if(c=='r') mask |= 1; + if(c=='g') mask |= 2; + if(c=='b') mask |= 4; + if(c=='a') mask |= 8; + } + _emitOp(out, Op::Invert); _emitU8(out, mask & 0xFu); + return true; + } + + if(op.Name == "brighten") { + _emitOp(out, Op::Brighten); + return true; + } + + if(op.Name == "contrast") { + int c = int(namedU("value").value_or(posU(0).value_or(0))); + int b = int(namedU("brightness").value_or(posU(1).value_or(0))); + c = std::max(-127, std::min(127, c)); + b = std::max(-127, std::min(127, b)); + _emitOp(out, Op::Contrast); + _emitU8(out, uint32_t(c + 127)); + _emitU8(out, uint32_t(b + 127)); + return true; + } + + auto compileColorOp = [&](Op opcode, bool needsRatio)->bool{ + std::string col = namedS("color").value_or(posS(0).value_or("")); + uint32_t argb=0; + if(!_parseHexColor(col, argb)) { if(err) *err="Bad color literal"; return false; } + _emitOp(out, opcode); + _emitU32(out, argb); + if(needsRatio) { + uint32_t ratio = namedU("ratio").value_or(posU(1).value_or(255)); + _emitU8(out, ratio & 0xFFu); + } + return true; + }; + + if(op.Name == "multiply") return compileColorOp(Op::Multiply, false); + if(op.Name == "screen") return compileColorOp(Op::Screen, false); + if(op.Name == "colorize") return compileColorOp(Op::Colorize, true); + + if(op.Name == "anim") { + uint32_t frameW = namedU("frame_w").value_or(namedU("w").value_or(posU(0).value_or(0))); + uint32_t frameH = namedU("frame_h").value_or(namedU("h").value_or(posU(1).value_or(0))); + uint32_t frames = namedU("frames").value_or(namedU("count").value_or(posU(2).value_or(0))); + uint32_t fps = namedU("fps").value_or(posU(3).value_or(0)); + uint32_t smooth = namedU("smooth").value_or(posU(4).value_or(0)); + + std::string axis = namedS("axis").value_or(""); + bool horizontal = (!axis.empty() && (axis[0] == 'x' || axis[0] == 'h')); + + if(frameW > 65535u || frameH > 65535u || frames > 65535u) { + if(err) *err="anim params must fit uint16"; + return false; + } + + uint32_t fpsQ = fps ? std::min(0xFFFFu, fps * 256u) : DefaultAnimFpsQ; + uint32_t flags = (smooth ? AnimSmooth : 0) | (horizontal ? AnimHorizontal : 0); + + _emitOp(out, Op::Anim); + _emitU16(out, frameW); + _emitU16(out, frameH); + _emitU16(out, frames); + _emitU16(out, fpsQ); + _emitU8(out, flags); + return true; + } + + if(err) *err = "Unknown op: " + op.Name; + return false; + } +};