From 776e9bfaca01045d57fcfae55d99745ea8ebc397 Mon Sep 17 00:00:00 2001 From: DrSocalkwe3n Date: Sat, 3 Jan 2026 00:41:09 +0600 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BF=D0=B0=D0=B9=D0=BF=D0=BB=D0=B0=D0=B9=D0=BD?= =?UTF-8?q?=20=D0=BC=D0=B0=D1=88=D0=B8=D0=BD=D1=8B=20(=D1=82=D1=80=D0=B5?= =?UTF-8?q?=D0=B1=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=81=D0=BC=D0=BE=D1=82=D1=80=20=D1=82=D0=B5=D1=85=D0=BD=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 4 +- Src/Client/Abstract.hpp | 8 +- Src/Client/ServerSession.cpp | 136 +- Src/Client/ServerSession.hpp | 16 +- .../AtlasPipeline/PipelinedTextureAtlas.cpp | 113 +- .../AtlasPipeline/PipelinedTextureAtlas.hpp | 196 ++- .../Vulkan/AtlasPipeline/TextureAtlas.cpp | 24 +- .../Vulkan/AtlasPipeline/TextureAtlas.hpp | 62 +- .../AtlasPipeline/TexturePipelineProgram.hpp | 1395 ++++++++++++----- Src/Client/Vulkan/Vulkan.cpp | 19 +- Src/Client/Vulkan/VulkanRenderSession.cpp | 169 +- Src/Client/Vulkan/VulkanRenderSession.hpp | 182 ++- Src/Common/Abstract.cpp | 52 + Src/Common/Abstract.hpp | 12 +- Src/Common/AssetsPreloader.hpp | 253 +++ Src/Server/Abstract.cpp | 14 +- Src/Server/AssetsManager.cpp | 30 +- Src/Server/ContentManager.cpp | 12 + Src/Server/ContentManager.hpp | 5 + Src/Server/GameServer.cpp | 302 +++- Src/Server/GameServer.hpp | 1 + Src/Server/RemoteClient.cpp | 58 +- Src/Server/RemoteClient.hpp | 5 + Src/Server/World.cpp | 40 +- Src/Server/World.hpp | 6 +- Src/main.cpp | 5 +- assets/shaders/chunk/node_opaque.frag | 2 +- assets/shaders/chunk/node_opaque.frag.bin | Bin 4276 -> 4288 bytes assets/shaders/chunk/node_transparent.frag | 3 + .../shaders/chunk/node_transparent.frag.bin | Bin 4156 -> 4276 bytes docs/assets_definitions.md | 161 ++ 31 files changed, 2684 insertions(+), 601 deletions(-) create mode 100644 Src/Common/AssetsPreloader.hpp create mode 100644 docs/assets_definitions.md diff --git a/CMakeLists.txt b/CMakeLists.txt index be0a9f3..8ad8a9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,8 +13,8 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdata-sections -ffunction-sections -DGL set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") # -rdynamic # gprof -# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg") -# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg") # sanitizer # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer") diff --git a/Src/Client/Abstract.hpp b/Src/Client/Abstract.hpp index 0dab321..446bc4a 100644 --- a/Src/Client/Abstract.hpp +++ b/Src/Client/Abstract.hpp @@ -112,7 +112,6 @@ struct DefPortalInfo { }; struct DefEntityInfo { - }; struct DefFuncEntityInfo { @@ -138,7 +137,10 @@ struct PortalInfo { }; struct EntityInfo { - + DefEntityId DefId = 0; + WorldId_t WorldId = 0; + Pos::Object Pos = Pos::Object(0); + glm::quat Quat = glm::quat(1.f, 0.f, 0.f, 0.f); }; struct FuncEntityInfo { @@ -232,4 +234,4 @@ public: virtual ~ISurfaceEventListener(); }; -} \ No newline at end of file +} diff --git a/Src/Client/ServerSession.cpp b/Src/Client/ServerSession.cpp index 48e349d..e83286e 100644 --- a/Src/Client/ServerSession.cpp +++ b/Src/Client/ServerSession.cpp @@ -606,10 +606,12 @@ void ServerSession::update(GlobalTime gTime, float dTime) { std::vector profile_World_Lost; std::unordered_map profile_Portal_AddOrChange; std::vector profile_Portal_Lost; - std::unordered_map profile_Entity_AddOrChange; + std::unordered_map profile_Entity_AddOrChange; std::vector profile_Entity_Lost; std::unordered_map profile_Item_AddOrChange; std::vector profile_Item_Lost; + std::unordered_map entity_AddOrChange; + std::vector entity_Lost; { for(TickData& data : ticks) { @@ -726,6 +728,25 @@ void ServerSession::update(GlobalTime gTime, float dTime) { auto eraseIter = std::unique(profile_Item_Lost.begin(), profile_Item_Lost.end()); profile_Item_Lost.erase(eraseIter, profile_Item_Lost.end()); } + + { + for(auto& [id, info] : data.Entity_AddOrChange) { + auto iter = std::lower_bound(entity_Lost.begin(), entity_Lost.end(), id); + if(iter != entity_Lost.end() && *iter == id) + entity_Lost.erase(iter); + + entity_AddOrChange[id] = info; + } + + for(EntityId_t id : data.Entity_Lost) { + entity_AddOrChange.erase(id); + } + + entity_Lost.insert(entity_Lost.end(), data.Entity_Lost.begin(), data.Entity_Lost.end()); + std::sort(entity_Lost.begin(), entity_Lost.end()); + auto eraseIter = std::unique(entity_Lost.begin(), entity_Lost.end()); + entity_Lost.erase(eraseIter, entity_Lost.end()); + } } for(auto& [id, _] : profile_Voxel_AddOrChange) @@ -829,6 +850,11 @@ void ServerSession::update(GlobalTime gTime, float dTime) { auto& c = chunks_Changed[wId]; for(auto& [pos, val] : list) { + auto& sizes = VisibleChunkCompressed[wId][pos]; + VisibleChunkCompressedBytes -= sizes.Voxels; + sizes.Voxels = val.size(); + VisibleChunkCompressedBytes += sizes.Voxels; + caocvr[pos] = unCompressVoxels(val); c.push_back(pos); } @@ -839,6 +865,11 @@ void ServerSession::update(GlobalTime gTime, float dTime) { auto& c = chunks_Changed[wId]; for(auto& [pos, val] : list) { + auto& sizes = VisibleChunkCompressed[wId][pos]; + VisibleChunkCompressedBytes -= sizes.Nodes; + sizes.Nodes = val.size(); + VisibleChunkCompressedBytes += sizes.Nodes; + auto& chunkNodes = caocvr[pos]; unCompressNodes(val, chunkNodes.data()); debugCheckGeneratedChunkNodes(wId, pos, chunkNodes); @@ -985,20 +1016,41 @@ void ServerSession::update(GlobalTime gTime, float dTime) { for(auto& [resId, def] : profile_Node_AddOrChange) { Profiles.DefNode[resId] = def; } + for(auto& [resId, def] : profile_Entity_AddOrChange) { + Profiles.DefEntity[resId] = def; + } } // Чанки { for(auto& [wId, lost] : regions_Lost_Result) { auto iterWorld = Content.Worlds.find(wId); - if(iterWorld == Content.Worlds.end()) + auto iterSizesWorld = VisibleChunkCompressed.find(wId); + if(iterWorld != Content.Worlds.end()) { + for(const Pos::GlobalRegion& rPos : lost) { + auto iterRegion = iterWorld->second.Regions.find(rPos); + if(iterRegion != iterWorld->second.Regions.end()) + iterWorld->second.Regions.erase(iterRegion); + } + } + + if(iterSizesWorld == VisibleChunkCompressed.end()) continue; for(const Pos::GlobalRegion& rPos : lost) { - auto iterRegion = iterWorld->second.Regions.find(rPos); - if(iterRegion != iterWorld->second.Regions.end()) - iterWorld->second.Regions.erase(iterRegion); + for(auto iter = iterSizesWorld->second.begin(); iter != iterSizesWorld->second.end(); ) { + if(Pos::GlobalRegion(iter->first >> 2) == rPos) { + VisibleChunkCompressedBytes -= iter->second.Voxels; + VisibleChunkCompressedBytes -= iter->second.Nodes; + iter = iterSizesWorld->second.erase(iter); + } else { + ++iter; + } + } } + + if(iterSizesWorld->second.empty()) + VisibleChunkCompressed.erase(iterSizesWorld); } for(auto& [wId, voxels] : chunks_AddOrChange_Voxel_Result) { @@ -1019,6 +1071,43 @@ void ServerSession::update(GlobalTime gTime, float dTime) { } + // Сущности + { + for(auto& [entityId, info] : entity_AddOrChange) { + auto iter = Content.Entityes.find(entityId); + if(iter != Content.Entityes.end() && iter->second.WorldId != info.WorldId) { + auto iterWorld = Content.Worlds.find(iter->second.WorldId); + if(iterWorld != Content.Worlds.end()) { + auto &list = iterWorld->second.Entitys; + list.erase(std::remove(list.begin(), list.end(), entityId), list.end()); + } + } + + Content.Entityes[entityId] = info; + + auto &list = Content.Worlds[info.WorldId].Entitys; + if(std::find(list.begin(), list.end(), entityId) == list.end()) + list.push_back(entityId); + } + + for(EntityId_t entityId : entity_Lost) { + auto iter = Content.Entityes.find(entityId); + if(iter != Content.Entityes.end()) { + auto iterWorld = Content.Worlds.find(iter->second.WorldId); + if(iterWorld != Content.Worlds.end()) { + auto &list = iterWorld->second.Entitys; + list.erase(std::remove(list.begin(), list.end(), entityId), list.end()); + } + Content.Entityes.erase(iter); + } else { + for(auto& [wId, worldInfo] : Content.Worlds) { + auto &list = worldInfo.Entitys; + list.erase(std::remove(list.begin(), list.end(), entityId), list.end()); + } + } + } + } + if(RS) RS->tickSync(result); } @@ -1402,11 +1491,18 @@ coro<> ServerSession::rP_Definition(Net::AsyncSocket &sock) { 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(); } @@ -1433,11 +1529,35 @@ coro<> ServerSession::rP_Content(Net::AsyncSocket &sock) { 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(); + Pos::Object pos; + pos.x = co_await sock.read(); + pos.y = co_await sock.read(); + pos.z = co_await sock.read(); + + ToServer::PacketQuat q; + for(int iter = 0; iter < 5; iter++) + q.Data[iter] = co_await sock.read(); + + EntityInfo info; + info.DefId = defId; + info.WorldId = worldId; + info.Pos = pos; + info.Quat = q.toQuat(); + + 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(); diff --git a/Src/Client/ServerSession.hpp b/Src/Client/ServerSession.hpp index 6d5aba8..31c3ece 100644 --- a/Src/Client/ServerSession.hpp +++ b/Src/Client/ServerSession.hpp @@ -42,6 +42,10 @@ public: return Socket->isAlive() && IsConnected; } + uint64_t getVisibleCompressedChunksBytes() const { + return VisibleChunkCompressedBytes; + } + // ISurfaceEventListener virtual void onResize(uint32_t width, uint32_t height) override; @@ -99,7 +103,7 @@ private: std::vector Profile_World_Lost; std::vector> Profile_Portal_AddOrChange; std::vector Profile_Portal_Lost; - std::vector> Profile_Entity_AddOrChange; + std::vector> Profile_Entity_AddOrChange; std::vector Profile_Entity_Lost; std::vector> Profile_Item_AddOrChange; std::vector Profile_Item_Lost; @@ -110,6 +114,13 @@ private: std::unordered_map> Chunks_AddOrChange_Voxel; std::unordered_map> Chunks_AddOrChange_Node; std::unordered_map> Regions_Lost; + std::vector> Entity_AddOrChange; + std::vector Entity_Lost; + }; + + struct ChunkCompressedSize { + uint32_t Voxels = 0; + uint32_t Nodes = 0; }; struct AssetsBindsChange { @@ -169,6 +180,9 @@ private: GlobalTime LastSendPYR_POS; + std::unordered_map> VisibleChunkCompressed; + uint64_t VisibleChunkCompressedBytes = 0; + // Приём данных с сокета coro<> run(AsyncUseControl::Lock); void protocolError(); diff --git a/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.cpp b/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.cpp index ebba2d5..4035acd 100644 --- a/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.cpp +++ b/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.cpp @@ -14,6 +14,34 @@ PipelinedTextureAtlas::AtlasTextureId PipelinedTextureAtlas::getByPipeline(const _AddictedTextures[texId].push_back(pipeline); } + { + std::vector animMeta = + TexturePipelineProgram::extractAnimationSpecs(pipeline._Pipeline.data(), pipeline._Pipeline.size()); + if (!animMeta.empty()) { + AnimatedPipelineState entry; + entry.Specs.reserve(animMeta.size()); + for (const auto& spec : animMeta) { + detail::AnimSpec16 outSpec{}; + outSpec.TexId = spec.HasTexId ? spec.TexId : TextureAtlas::kOverflowId; + outSpec.FrameW = spec.FrameW; + outSpec.FrameH = spec.FrameH; + outSpec.FrameCount = spec.FrameCount; + outSpec.FpsQ = spec.FpsQ; + outSpec.Flags = spec.Flags; + entry.Specs.push_back(outSpec); + } + entry.LastFrames.resize(entry.Specs.size(), std::numeric_limits::max()); + entry.Smooth = false; + for (const auto& spec : entry.Specs) { + if (spec.Flags & detail::AnimSmooth) { + entry.Smooth = true; + break; + } + } + _AnimatedPipelines.emplace(pipeline, std::move(entry)); + } + } + return atlasTexId; } @@ -37,6 +65,7 @@ void PipelinedTextureAtlas::freeByPipeline(const HashedPipeline& pipeline) { Super.removeTexture(iter->second); _AtlasCpuTextures.erase(iter->second); _PipeToTexId.erase(iter); + _AnimatedPipelines.erase(pipeline); } void PipelinedTextureAtlas::updateTexture(uint32_t texId, const StoredTexture& texture) { @@ -90,7 +119,7 @@ StoredTexture PipelinedTextureAtlas::_generatePipelineTexture(const HashedPipeli } TexturePipelineProgram program; - program.fromWords(std::move(words)); + program.fromBytes(std::move(words)); TexturePipelineProgram::OwnedTexture baked; auto provider = [this](uint32_t texId) -> std::optional { @@ -109,7 +138,7 @@ StoredTexture PipelinedTextureAtlas::_generatePipelineTexture(const HashedPipeli return tex; }; - if (!program.bake(provider, baked, nullptr)) { + if (!program.bake(provider, baked, _AnimTimeSeconds, nullptr)) { if (auto tex = tryCopyFirstDependencyTexture(pipeline)) { return *tex; } @@ -135,6 +164,7 @@ StoredTexture PipelinedTextureAtlas::_generatePipelineTexture(const HashedPipeli void PipelinedTextureAtlas::flushNewPipelines() { std::vector changedTextures = std::move(_ChangedTextures); + _ChangedTextures.clear(); std::sort(changedTextures.begin(), changedTextures.end()); changedTextures.erase(std::unique(changedTextures.begin(), changedTextures.end()), changedTextures.end()); @@ -150,6 +180,7 @@ void PipelinedTextureAtlas::flushNewPipelines() { } changedPipelineTextures.append_range(std::move(_ChangedPipelines)); + _ChangedPipelines.clear(); changedTextures.clear(); std::sort(changedPipelineTextures.begin(), changedPipelineTextures.end()); @@ -165,6 +196,18 @@ void PipelinedTextureAtlas::flushNewPipelines() { auto& stored = _AtlasCpuTextures[atlasTexId]; stored = std::move(texture); if (!stored._Pixels.empty()) { + // Смена порядка пикселей + for (uint32_t& pixel : stored._Pixels) { + union { + struct { uint8_t r, g, b, a; } color; + uint32_t data; + }; + + data = pixel; + std::swap(color.r, color.b); + pixel = data; + } + Super.setTextureData(atlasTexId, stored._Widht, stored._Height, @@ -182,6 +225,72 @@ void PipelinedTextureAtlas::notifyGpuFinished() { Super.notifyGpuFinished(); } +bool PipelinedTextureAtlas::updateAnimatedPipelines(double timeSeconds) { + _AnimTimeSeconds = timeSeconds; + if (_AnimatedPipelines.empty()) { + return false; + } + + bool changed = false; + for (auto& [pipeline, entry] : _AnimatedPipelines) { + if (entry.Specs.empty()) { + continue; + } + + if (entry.Smooth) { + _ChangedPipelines.push_back(pipeline); + changed = true; + continue; + } + + if (entry.LastFrames.size() != entry.Specs.size()) + entry.LastFrames.assign(entry.Specs.size(), std::numeric_limits::max()); + + bool pipelineChanged = false; + for (size_t i = 0; i < entry.Specs.size(); ++i) { + const auto& spec = entry.Specs[i]; + + uint32_t fpsQ = spec.FpsQ ? spec.FpsQ : TexturePipelineProgram::DefaultAnimFpsQ; + double fps = double(fpsQ) / 256.0; + double frameTime = timeSeconds * fps; + if (frameTime < 0.0) + frameTime = 0.0; + + uint32_t frameCount = spec.FrameCount; + // Авторасчёт количества кадров + if (frameCount == 0) { + auto iterTex = _ResToTexture.find(spec.TexId); + if (iterTex != _ResToTexture.end()) { + uint32_t fw = spec.FrameW ? spec.FrameW : iterTex->second._Widht; + uint32_t fh = spec.FrameH ? spec.FrameH : iterTex->second._Widht; + if (fw > 0 && fh > 0) { + if (spec.Flags & detail::AnimHorizontal) + frameCount = iterTex->second._Widht / fw; + else + frameCount = iterTex->second._Height / fh; + } + } + } + + if (frameCount == 0) + frameCount = 1; + + uint32_t frameIndex = frameCount ? (uint32_t(frameTime) % frameCount) : 0u; + if (entry.LastFrames[i] != frameIndex) { + entry.LastFrames[i] = frameIndex; + pipelineChanged = true; + } + } + + if (pipelineChanged) { + _ChangedPipelines.push_back(pipeline); + changed = true; + } + } + + return changed; +} + std::optional PipelinedTextureAtlas::tryCopyFirstDependencyTexture(const HashedPipeline& pipeline) const { auto deps = pipeline.getDependencedTextures(); if (!deps.empty()) { diff --git a/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.hpp b/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.hpp index e1d5b9e..c966650 100644 --- a/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.hpp +++ b/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ enum class Op16 : Word { End = 0, Base_Tex = 1, Base_Fill = 2, + Base_Anim = 3, Resize = 10, Transform = 11, Opacity = 12, @@ -33,6 +35,7 @@ enum class Op16 : Word { Multiply = 18, Screen = 19, Colorize = 20, + Anim = 21, Overlay = 30, Mask = 31, LowPart = 32, @@ -43,13 +46,24 @@ enum class SrcKind16 : Word { TexId = 0, Sub = 1 }; struct SrcRef16 { SrcKind16 kind{SrcKind16::TexId}; - Word a = 0; - Word b = 0; + uint32_t TexId = 0; + uint32_t Off = 0; + uint32_t Len = 0; }; -inline uint32_t makeU32(Word lo, Word hi) { - return uint32_t(lo) | (uint32_t(hi) << 16); -} +enum AnimFlags16 : Word { + AnimSmooth = 1 << 0, + AnimHorizontal = 1 << 1 +}; + +struct AnimSpec16 { + uint32_t TexId = 0; + uint16_t FrameW = 0; + uint16_t FrameH = 0; + uint16_t FrameCount = 0; + uint16_t FpsQ = 0; + uint16_t Flags = 0; +}; inline void addUniqueDep(boost::container::small_vector& deps, uint32_t id) { if (id == TextureAtlas::kOverflowId) { @@ -60,16 +74,52 @@ inline void addUniqueDep(boost::container::small_vector& deps, uint } } -inline bool readSrc(const std::vector& words, size_t end, size_t& ip, SrcRef16& out) { +inline bool read16(const std::vector& words, size_t end, size_t& ip, uint16_t& out) { + if (ip + 1 >= end) { + return false; + } + out = uint16_t(words[ip]) | (uint16_t(words[ip + 1]) << 8); + ip += 2; + return true; +} + +inline bool read24(const std::vector& words, size_t end, size_t& ip, uint32_t& out) { if (ip + 2 >= end) { return false; } - out.kind = static_cast(words[ip++]); - out.a = words[ip++]; - out.b = words[ip++]; + out = uint32_t(words[ip]) | + (uint32_t(words[ip + 1]) << 8) | + (uint32_t(words[ip + 2]) << 16); + ip += 3; return true; } +inline bool read32(const std::vector& words, size_t end, size_t& ip, uint32_t& out) { + if (ip + 3 >= end) { + return false; + } + out = uint32_t(words[ip]) | + (uint32_t(words[ip + 1]) << 8) | + (uint32_t(words[ip + 2]) << 16) | + (uint32_t(words[ip + 3]) << 24); + ip += 4; + return true; +} + +inline bool readSrc(const std::vector& words, size_t end, size_t& ip, SrcRef16& out) { + if (ip >= end) { + return false; + } + out.kind = static_cast(words[ip++]); + if (out.kind == SrcKind16::TexId) { + return read24(words, end, ip, out.TexId); + } + if (out.kind == SrcKind16::Sub) { + return read24(words, end, ip, out.Off) && read24(words, end, ip, out.Len); + } + return false; +} + inline void extractPipelineDependencies(const std::vector& words, size_t start, size_t end, @@ -88,12 +138,12 @@ inline void extractPipelineDependencies(const std::vector& words, auto need = [&](size_t n) { return ip + n <= end; }; auto handleSrc = [&](const SrcRef16& src) { if (src.kind == SrcKind16::TexId) { - addUniqueDep(deps, makeU32(src.a, src.b)); + addUniqueDep(deps, src.TexId); return; } if (src.kind == SrcKind16::Sub) { - size_t subStart = static_cast(src.a); - size_t subEnd = subStart + static_cast(src.b); + size_t subStart = static_cast(src.Off); + size_t subEnd = subStart + static_cast(src.Len); if (subStart < subEnd && subEnd <= words.size()) { extractPipelineDependencies(words, subStart, subEnd, deps, visited); } @@ -108,37 +158,54 @@ inline void extractPipelineDependencies(const std::vector& words, return; case Op16::Base_Tex: { - if (!need(3)) return; SrcRef16 src{}; if (!readSrc(words, end, ip, src)) return; handleSrc(src); } break; - case Op16::Base_Fill: - if (!need(4)) return; - ip += 4; - break; + case Op16::Base_Anim: { + SrcRef16 src{}; + if (!readSrc(words, end, ip, src)) return; + handleSrc(src); + uint16_t tmp16 = 0; + uint8_t tmp8 = 0; + if (!read16(words, end, ip, tmp16)) return; + if (!read16(words, end, ip, tmp16)) return; + if (!read16(words, end, ip, tmp16)) return; + if (!read16(words, end, ip, tmp16)) return; + if (!need(1)) return; + tmp8 = words[ip++]; + (void)tmp8; + } break; + + case Op16::Base_Fill: { + uint16_t tmp16 = 0; + uint32_t tmp32 = 0; + if (!read16(words, end, ip, tmp16)) return; + if (!read16(words, end, ip, tmp16)) return; + if (!read32(words, end, ip, tmp32)) return; + } break; case Op16::Overlay: case Op16::Mask: { - if (!need(3)) return; SrcRef16 src{}; if (!readSrc(words, end, ip, src)) return; handleSrc(src); } break; case Op16::LowPart: { - if (!need(1 + 3)) return; + if (!need(1)) return; ip += 1; // percent SrcRef16 src{}; if (!readSrc(words, end, ip, src)) return; handleSrc(src); } break; - case Op16::Resize: - if (!need(2)) return; - ip += 2; - break; + case Op16::Resize: { + uint16_t tmp16 = 0; + if (!read16(words, end, ip, tmp16)) return; + if (!read16(words, end, ip, tmp16)) return; + } break; case Op16::Transform: case Op16::Opacity: @@ -151,8 +218,8 @@ inline void extractPipelineDependencies(const std::vector& words, break; case Op16::MakeAlpha: - if (!need(2)) return; - ip += 2; + if (!need(3)) return; + ip += 3; break; case Op16::Invert: @@ -166,27 +233,42 @@ inline void extractPipelineDependencies(const std::vector& words, break; case Op16::Multiply: - case Op16::Screen: - if (!need(2)) return; - ip += 2; - break; + case Op16::Screen: { + uint32_t tmp32 = 0; + if (!read32(words, end, ip, tmp32)) return; + } break; - case Op16::Colorize: - if (!need(3)) return; - ip += 3; - break; + case Op16::Colorize: { + uint32_t tmp32 = 0; + if (!read32(words, end, ip, tmp32)) return; + if (!need(1)) return; + ip += 1; + } break; + + case Op16::Anim: { + uint16_t tmp16 = 0; + if (!read16(words, end, ip, tmp16)) return; + if (!read16(words, end, ip, tmp16)) return; + if (!read16(words, end, ip, tmp16)) return; + if (!read16(words, end, ip, tmp16)) return; + if (!need(1)) return; + ip += 1; + } break; case Op16::Combine: { - if (!need(3)) return; - ip += 2; // skip w,h - uint32_t n = words[ip++]; + uint16_t w = 0, h = 0, n = 0; + if (!read16(words, end, ip, w)) return; + if (!read16(words, end, ip, h)) return; + if (!read16(words, end, ip, n)) return; for (uint32_t i = 0; i < n; ++i) { - if (!need(2 + 3)) return; - ip += 2; // x, y + uint16_t tmp16 = 0; + if (!read16(words, end, ip, tmp16)) return; // x + if (!read16(words, end, ip, tmp16)) return; // y SrcRef16 src{}; if (!readSrc(words, end, ip, src)) return; handleSrc(src); } + (void)w; (void)h; } break; default: @@ -227,8 +309,9 @@ struct Pipeline { _Pipeline = { static_cast(detail::Op16::Base_Tex), static_cast(detail::SrcKind16::TexId), - static_cast(texId & 0xFFFFu), - static_cast((texId >> 16) & 0xFFFFu), + static_cast(texId & 0xFFu), + static_cast((texId >> 8) & 0xFFu), + static_cast((texId >> 16) & 0xFFu), static_cast(detail::Op16::End) }; } @@ -253,9 +336,7 @@ struct HashedPipeline { constexpr std::size_t prime = 1099511628211ull; for(detail::Word w : _Pipeline) { - hash ^= static_cast(w & 0xFF); - hash *= prime; - hash ^= static_cast((w >> 8) & 0xFF); + hash ^= static_cast(w); hash *= prime; } @@ -328,6 +409,13 @@ private: std::vector _ChangedTextures; // Необходимые к созданию/обновлению пайплайны std::vector _ChangedPipelines; + struct AnimatedPipelineState { + std::vector Specs; + std::vector LastFrames; + bool Smooth = false; + }; + std::unordered_map _AnimatedPipelines; + double _AnimTimeSeconds = 0.0; public: PipelinedTextureAtlas(TextureAtlas&& tk); @@ -348,6 +436,26 @@ public: return atlasLayers(); } + uint32_t maxLayers() const { + return Super.maxLayers(); + } + + uint32_t maxTextureId() const { + return Super.maxTextureId(); + } + + TextureAtlas::TextureId reservedOverflowId() const { + return Super.reservedOverflowId(); + } + + TextureAtlas::TextureId reservedLayerId(uint32_t layer) const { + return Super.reservedLayerId(layer); + } + + void requestLayerCount(uint32_t layers) { + Super.requestLayerCount(layers); + } + // Должны всегда бронировать идентификатор, либо отдавать kOverflowId. При этом запись tex+pipeline остаётся // Выдаёт стабильный идентификатор, привязанный к пайплайну AtlasTextureId getByPipeline(const HashedPipeline& pipeline); @@ -373,6 +481,8 @@ public: void notifyGpuFinished(); + bool updateAnimatedPipelines(double timeSeconds); + private: std::optional tryCopyFirstDependencyTexture(const HashedPipeline& pipeline) const; diff --git a/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.cpp b/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.cpp index 599eb61..1290665 100644 --- a/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.cpp +++ b/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.cpp @@ -29,11 +29,13 @@ TextureAtlas::TextureAtlas(VkDevice device, EntriesCpu_.resize(Cfg_.MaxTextureId); std::memset(EntriesCpu_.data(), 0, EntriesCpu_.size() * sizeof(Entry)); + _initReservedEntries(); EntriesDirty_ = true; Slots_.resize(Cfg_.MaxTextureId); FreeIds_.reserve(Cfg_.MaxTextureId); PendingInQueue_.assign(Cfg_.MaxTextureId, false); + NextId_ = _allocatableStart(); if(Cfg_.ExternalSampler != VK_NULL_HANDLE) { Sampler_ = Cfg_.ExternalSampler; @@ -70,13 +72,19 @@ TextureAtlas::TextureId TextureAtlas::registerTexture() { _ensureAliveOrThrow(); TextureId id = kOverflowId; + if(NextId_ < _allocatableStart()) { + NextId_ = _allocatableStart(); + } + while(!FreeIds_.empty() && isReservedId(FreeIds_.back())) { + FreeIds_.pop_back(); + } if(!FreeIds_.empty()) { id = FreeIds_.back(); FreeIds_.pop_back(); - } else if(NextId_ < Cfg_.MaxTextureId) { + } else if(NextId_ < _allocatableLimit()) { id = NextId_++; } else { - return kOverflowId; + return reservedOverflowId(); } Slot& s = Slots_[id]; @@ -96,7 +104,7 @@ void TextureAtlas::setTextureData(TextureId id, const void* pixelsRGBA8, uint32_t rowPitchBytes) { _ensureAliveOrThrow(); - if(id == kOverflowId) return; + if(isInvalidId(id)) return; _ensureRegisteredIdOrThrow(id); if(w == 0 || h == 0) { @@ -151,7 +159,7 @@ void TextureAtlas::setTextureData(TextureId id, void TextureAtlas::clearTextureData(TextureId id) { _ensureAliveOrThrow(); - if(id == kOverflowId) return; + if(isInvalidId(id)) return; _ensureRegisteredIdOrThrow(id); Slot& s = Slots_[id]; @@ -172,7 +180,7 @@ void TextureAtlas::clearTextureData(TextureId id) { void TextureAtlas::removeTexture(TextureId id) { _ensureAliveOrThrow(); - if(id == kOverflowId) return; + if(isInvalidId(id)) return; _ensureRegisteredIdOrThrow(id); Slot& s = Slots_[id]; @@ -217,7 +225,7 @@ TextureAtlas::DescriptorOut TextureAtlas::flushUploadsAndBarriers(VkCommandBuffe while (!queue.empty()) { TextureId id = queue.front(); queue.pop_front(); - if(id == kOverflowId || id >= inQueue.size()) { + if(isInvalidId(id) || id >= inQueue.size()) { continue; } if(!inQueue[id]) { @@ -253,7 +261,7 @@ TextureAtlas::DescriptorOut TextureAtlas::flushUploadsAndBarriers(VkCommandBuffe bool outOfSpace = false; for(TextureId id : pendingNow) { - if(id == kOverflowId) continue; + if(isInvalidId(id)) continue; if(id >= Slots_.size()) continue; Slot& s = Slots_[id]; if(!s.InUse || !s.HasCpuData) continue; @@ -310,7 +318,7 @@ TextureAtlas::DescriptorOut TextureAtlas::flushUploadsAndBarriers(VkCommandBuffe }; for(TextureId id : pendingNow) { - if(id == kOverflowId) continue; + if(isInvalidId(id)) continue; Slot& s = Slots_[id]; if(!s.InUse || !s.HasCpuData || !s.HasPlacement) continue; if(!uploadTextureIntoAtlas(s, s.Place, Atlas_, false)) { diff --git a/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.hpp b/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.hpp index e3beb18..c3ce775 100644 --- a/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.hpp +++ b/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.hpp @@ -10,7 +10,7 @@ TextureAtlas — как пользоваться (кратко) 2) Зарегистрируйте текстуру и получите стабильный ID: TextureId id = atlas.registerTexture(); - if(id == TextureAtlas::kOverflowId) { ... } // нет свободных ID + if(id == TextureAtlas::kOverflowId || id == atlas.reservedOverflowId()) { ... } // нет свободных ID 3) Задайте данные (RGBA8), можно много раз — ID не меняется: atlas.setTextureData(id, w, h, pixels, rowPitchBytes); @@ -32,7 +32,11 @@ TextureAtlas — как пользоваться (кратко) atlas.removeTexture(id); // освободить ID (после этого использовать нельзя) Примечания: -- Вызовы API с kOverflowId игнорируются (no-op). +- Вызовы API с kOverflowId и зарезервированными ID игнорируются (no-op). +- ID из начала диапазона зарезервированы под служебные нужды: + reservedOverflowId() == 0, + reservedLayerId(0) == 1 (первый слой), + reservedLayerId(1) == 2 (второй слой) и т.д. - Ошибки ресурсов (нет места/стейджинга/oom) НЕ бросают исключения — дают события. - Исключения только за неверный ввод/неверное использование (см. ТЗ). - Класс не thread-safe: синхронизацию обеспечивает пользователь. @@ -192,11 +196,60 @@ public: /// Текущее число слоёв атласа. uint32_t atlasLayers() const { return Atlas_.Layers; } + uint32_t maxLayers() const { return Cfg_.MaxLayers; } + uint32_t maxTextureId() const { return Cfg_.MaxTextureId; } + + TextureId reservedOverflowId() const { return 0; } + TextureId reservedLayerId(uint32_t layer) const { return 1u + layer; } + + bool isReservedId(TextureId id) const { + if(id >= Cfg_.MaxTextureId) return false; + return id < _reservedCount(); + } + + bool isReservedLayerId(TextureId id) const { + if(id >= Cfg_.MaxTextureId) return false; + return id != reservedOverflowId() && id < _reservedCount(); + } + + bool isInvalidId(TextureId id) const { + return id == kOverflowId || isReservedId(id); + } + + void requestLayerCount(uint32_t layers) { + _ensureAliveOrThrow(); + _scheduleLayerGrow(layers); + } + /// Общий staging-буфер (может быть задан извне). std::shared_ptr getStagingBuffer() const { return Staging_; } private: void _moveFrom(TextureAtlas&& other) noexcept; + uint32_t _reservedCount() const { return Cfg_.MaxLayers + 1; } + uint32_t _reservedStart() const { return 0; } + uint32_t _allocatableStart() const { return _reservedCount(); } + uint32_t _allocatableLimit() const { return Cfg_.MaxTextureId; } + + void _initReservedEntries() { + if(Cfg_.MaxTextureId <= _reservedCount()) { + return; + } + + _setEntryInvalid(reservedOverflowId(), /*diagPending*/false, /*diagTooLarge*/false); + for(uint32_t layer = 0; layer < Cfg_.MaxLayers; ++layer) { + TextureId id = reservedLayerId(layer); + Entry& e = EntriesCpu_[id]; + e.UVMinMax[0] = 0.0f; + e.UVMinMax[1] = 0.0f; + e.UVMinMax[2] = 1.0f; + e.UVMinMax[3] = 1.0f; + e.Layer = layer; + e.Flags = ENTRY_VALID; + } + EntriesDirty_ = true; + } + // ============================= Ошибки/валидация ============================= struct InputError : std::runtime_error { @@ -225,6 +278,9 @@ private: if(Cfg_.MaxTextureId == 0) { throw _inputError("Config.MaxTextureId must be > 0"); } + if(Cfg_.MaxTextureId <= (Cfg_.MaxLayers + 1)) { + throw _inputError("Config.MaxTextureId must be > MaxLayers + 1 (reserved ids)"); + } if(Cfg_.MaxTextureSize != 2048) { /// TODO: @@ -246,7 +302,7 @@ private: } void _ensureRegisteredIdOrThrow(TextureId id) const { - if(id >= Cfg_.MaxTextureId) { + if(id >= Cfg_.MaxTextureId || isReservedId(id)) { throw _inputError("TextureId out of range"); } if(!Slots_[id].InUse || Slots_[id].StateValue == State::REMOVED) { diff --git a/Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp b/Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp index 0166ff5..e8e379c 100644 --- a/Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp +++ b/Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include // ======================== @@ -19,11 +20,21 @@ struct Texture { }; // ======================== -// Bytecode words are uint16_t +// 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 = uint16_t; + 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; @@ -31,118 +42,417 @@ public: Texture view() const { return Texture{Width, Height, Pixels.data()}; } }; - // name -> uint32 id - using IdResolver = std::function(std::string_view)>; - // id -> Texture view - using TextureProvider = std::function(uint32_t)>; + using IdResolverFunc = std::function(std::string_view)>; + using TextureProviderFunc = std::function(uint32_t)>; - // Patch points to two consecutive u16 words where uint32 texId lives (lo, hi) + // Patch point to 3 consecutive bytes where u24 texId lives (b0,b1,b2) struct Patch { - size_t WordIndexLo = 0; // Code_[lo], Code_[lo+1] is hi + size_t ByteIndex0 = 0; // Code_[i], Code_[i+1], Code_[i+2] std::string Name; }; - // ---- compile / link / bake ---- 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 IdResolver& resolver, std::string* err = nullptr) { - for(const auto& p : Patches_) { - auto id = resolver(p.Name); - if(!id) { - if(err) *err = "Unresolved texture name: " + p.Name; + 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; } - if(p.WordIndexLo + 1 >= Code_.size()) { - if(err) *err = "Internal error: patch out of range"; + uint32_t id = *idOpt; + if(id >= (1u << 24)) { + if(err) *err = "TexId выходит за 24 бита (u24): " + p.Name + " => " + std::to_string(id); return false; } - Code_[p.WordIndexLo + 0] = _lo16(*id); - Code_[p.WordIndexLo + 1] = _hi16(*id); + 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 TextureProvider& provider, OwnedTexture& out, std::string* err = nullptr) const { + 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, err); + return vm.run(Code_, out, timeSeconds, err); } const std::vector& words() const { return Code_; } const std::vector& patches() const { return Patches_; } - // Serialize words to bytes (little-endian) - std::vector toBytes() const { - std::vector bytes(Code_.size() * sizeof(Word)); - std::memcpy(bytes.data(), Code_.data(), bytes.size()); - return bytes; + 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; } - void fromWords(std::vector words) { - Code_ = std::move(words); + 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: // ======================== - // Word helpers + // Byte helpers (little-endian) // ======================== - static constexpr uint32_t _make_u32(uint16_t lo, uint16_t hi) { - return uint32_t(lo) | (uint32_t(hi) << 16); + 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)); } - static constexpr uint16_t _lo16(uint32_t v) { return uint16_t(v & 0xFFFFu); } - static constexpr uint16_t _hi16(uint32_t v) { return uint16_t((v >> 16) & 0xFFFFu); } // ======================== - // SrcRef encoding in u16 words - // kind + a + b (3 words) - // kind=0 TexId: a=_lo16(id), b=_hi16(id) - // kind=1 Sub : a=offsetWords, b=lenWords + // SrcRef encoding in bytes (variable length) + // kind(1) + payload + // TexId: id24(3) => total 4 + // Sub : off16(3) + len16(3) => total 7 // ======================== - enum class SrcKind : Word { TexId = 0, Sub = 1 }; + enum class SrcKind : uint8_t { TexId = 0, Sub = 1 }; struct SrcRef { - SrcKind Kind; - Word A; - Word B; + SrcKind Kind{}; + uint32_t TexId24 = 0; // for TexId + uint16_t Off24 = 0; // for Sub + uint16_t Len24 = 0; // for Sub }; // ======================== - // Opcodes (fixed-length headers; some are variable like Combine) + // Opcodes (1 byte) // ======================== - enum class Op : Word { + enum class Op : uint8_t { End = 0, - // Base producers (top-level expression must start with one of these) - Base_Tex = 1, // args: SrcRef(TexId) -> kind,lo,hi - Base_Fill = 2, // args: w, h, color_lo, color_hi + Base_Tex = 1, // SrcRef(TexId) + Base_Fill = 2, // w16, h16, color32 + Base_Anim = 3, // SrcRef(TexId), frameW16, frameH16, frames16, fpsQ16, flags8 - // Unary ops on current image - Resize = 10, // w, h - Transform = 11, // t(0..7) - Opacity = 12, // a(0..255) - NoAlpha = 13, // - - MakeAlpha = 14, // rgb_lo(0xRRGG), rgb_hi(0x00BB) packed as 24-bit in 2 words - Invert = 15, // mask bits (r=1 g=2 b=4 a=8) - Brighten = 16, // - - Contrast = 17, // contrast_bias(0..254), bright_bias(0..254) where v = bias-127 - Multiply = 18, // color_lo, color_hi (0xAARRGGBB) - Screen = 19, // color_lo, color_hi - Colorize = 20, // color_lo, color_hi, ratio(0..255) + 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 - // Ops that consume a SrcRef (TexId or Sub) - Overlay = 30, // SrcRef (3 words) - Mask = 31, // SrcRef - LowPart = 32, // percent(0..100), SrcRef (1 + 3 words) + Overlay = 30, // SrcRef (var) + Mask = 31, // SrcRef (var) + LowPart = 32, // percent8, SrcRef (var) - // Variable example (optional): Combine - // Combine: w,h, n, then n times: x,y, SrcRef (x,y,kind,a,b) - Combine = 40 + Combine = 40 // w16,h16,n16 then n*(x16,y16,SrcRef) (если понадобится — допишем DSL) }; // ======================== @@ -158,7 +468,7 @@ private: static inline uint8_t _clampu8(int v){ return uint8_t(std::min(255, std::max(0, v))); } // ======================== - // VM (executes u16 words) + // VM (executes bytes) // ======================== struct Image { uint32_t W=0,H=0; @@ -167,58 +477,139 @@ private: class VM { public: + using TextureProvider = TexturePipelineProgram::TextureProviderFunc; + explicit VM(TextureProvider provider) : Provider_(std::move(provider)) {} - bool run(const std::vector& code, OwnedTexture& out, std::string* err) { + 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 = (offset<<16)|len (fits if <=65535) + 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) { + while(true) { if(!need(1)) return false; Op op = static_cast(code[ip++]); if(op == Op::End) break; - switch (op) { + switch(op) { case Op::Base_Tex: { - if(!need(3)) return false; - SrcRef src = _readSrc(code, ip); + 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(_make_u32(src.A, src.B), texCache, err); + cur = _loadTex(src.TexId24, texCache, err); if(cur.W == 0) return false; } break; case Op::Base_Fill: { - if(!need(4)) return false; - uint32_t w = code[ip++], h = code[ip++]; - uint32_t colorLo = code[ip++]; - uint32_t colorHi = code[ip++]; - uint32_t color = _make_u32(colorLo, colorHi); + 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: { - if(!need(3)) return false; - SrcRef src = _readSrc(code, ip); - Image over = _loadSrc(code, src, texCache, subCache, err); + 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; } // if no base, adopt + if(!cur.W) { cur = std::move(over); break; } over = _resizeNN_ifNeeded(over, cur.W, cur.H); _alphaOver(cur, over); } break; case Op::Mask: { - if(!need(3)) return false; - SrcRef src = _readSrc(code, ip); - Image m = _loadSrc(code, src, texCache, subCache, err); + 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); @@ -226,10 +617,11 @@ private: } break; case Op::LowPart: { - if(!need(1+3)) return false; - uint32_t pct = std::min(100u, code[ip++]); - SrcRef src = _readSrc(code, ip); - Image over = _loadSrc(code, src, texCache, subCache, err); + 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); @@ -237,23 +629,24 @@ private: } break; case Op::Resize: { - if(!need(2)) return false; - uint32_t w = code[ip++], h = code[ip++]; 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; - if(!cur.W) return _bad(err, "Transform requires base image"); 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; - if(!cur.W) return _bad(err, "Opacity requires base image"); _opacity(cur, uint8_t(a)); } break; @@ -263,17 +656,17 @@ private: } break; case Op::MakeAlpha: { - if(!need(2)) return false; - uint32_t rgb24 = (uint32_t(code[ip+1]) << 16) | uint32_t(code[ip]); // lo has RR GG, hi has 00 BB - ip += 2; if(!cur.W) return _bad(err, "MakeAlpha requires base image"); - _makeAlpha(cur, rgb24 & 0x00FFFFFFu); + 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; - if(!cur.W) return _bad(err, "Invert requires base image"); _invert(cur, mask); } break; @@ -283,59 +676,35 @@ private: } 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; - if(!cur.W) return _bad(err, "Contrast requires base image"); _contrast(cur, c, b); } break; case Op::Multiply: { - if(!need(2)) return false; - uint32_t colorLo = code[ip++]; - uint32_t colorHi = code[ip++]; - uint32_t color = _make_u32(colorLo, colorHi); 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(!need(2)) return false; - uint32_t colorLo = code[ip++]; - uint32_t colorHi = code[ip++]; - uint32_t color = _make_u32(colorLo, colorHi); 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(!need(3)) return false; - uint32_t colorLo = code[ip++]; - uint32_t colorHi = code[ip++]; - uint32_t color = _make_u32(colorLo, colorHi); - uint32_t ratio = code[ip++] & 0xFFu; 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; - case Op::Combine: { - // variable length: - // w,h,n then for each: x,y, SrcRef(3) - if(!need(3)) return false; - uint32_t w = code[ip++], h = code[ip++], n = code[ip++]; - Image outImg; outImg.W=w; outImg.H=h; outImg.Px.assign(size_t(w)*size_t(h), 0u); - for(uint32_t i=0;i& code, size_t& ip) { - SrcRef r; - r.Kind = static_cast(code[ip++]); - r.A = code[ip++]; - r.B = code[ip++]; - return r; + 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); @@ -375,12 +756,13 @@ private: return img; } - Image _loadSub(const std::vector& code, - Word off, Word len, - std::unordered_map& texCache, - std::unordered_map& subCache, - std::string* err) { - uint32_t key = (uint32_t(off) << 16) | uint32_t(len); + 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; @@ -388,11 +770,10 @@ private: size_t end = start + size_t(len); if(end > code.size()) { if(err) *err="Subprogram out of range"; return {}; } - // Run subprogram slice by copying minimal (simple + safe). - std::vector slice(code.begin()+start, code.begin()+end); + std::vector slice(code.begin()+start, code.begin()+end); OwnedTexture tmp; VM nested(Provider_); - if(!nested.run(slice, tmp, err)) return {}; + if(!nested.run(slice, tmp, timeSeconds, err)) return {}; Image img; img.W = tmp.Width; img.H = tmp.Height; img.Px = std::move(tmp.Pixels); @@ -400,22 +781,19 @@ private: return img; } - Image _loadSrc(const std::vector& code, - const SrcRef& src, - std::unordered_map& texCache, - std::unordered_map& subCache, - std::string* err) { - if(src.Kind == SrcKind::TexId) { - return _loadTex(_make_u32(src.A, src.B), texCache, err); - } - if(src.Kind == SrcKind::Sub) { - return _loadSub(code, src.A, src.B, texCache, subCache, err); - } + 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 ---- + // ---- 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); @@ -440,6 +818,48 @@ private: return _resizeNN(img, w, h); } + static Image _cropFrame(const Image& sheet, uint32_t index, uint32_t fw, uint32_t fh, bool horizontal) { + Image out; + out.W = fw; + out.H = fh; + out.Px.assign(size_t(fw) * size_t(fh), 0u); + + uint32_t baseX = horizontal ? (index * fw) : 0u; + uint32_t baseY = horizontal ? 0u : (index * fh); + + for(uint32_t y = 0; y < fh; ++y) { + uint32_t sy = baseY + y; + if(sy >= 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= int(dst.H)) continue; - for(uint32_t x=0;x= int(dst.W)) continue; - size_t di = size_t(dy)*dst.W + uint32_t(dx); - uint32_t b = dst.Px[di], o = src.Px[size_t(y)*src.W + x]; - - uint8_t ba=_a(b), br=_r(b), bg=_g(b), bb=_b(b); - uint8_t oa=_a(o), or_=_r(o), og=_g(o), ob=_b(o); - - uint32_t brp = (uint32_t(br) * ba) / 255; - uint32_t bgp = (uint32_t(bg) * ba) / 255; - uint32_t bbp = (uint32_t(bb) * ba) / 255; - - uint32_t orp = (uint32_t(or_) * oa) / 255; - uint32_t ogp = (uint32_t(og) * oa) / 255; - uint32_t obp = (uint32_t(ob) * oa) / 255; - - uint32_t inv = 255 - oa; - uint32_t outA = oa + (uint32_t(ba) * inv) / 255; - uint32_t outRp = orp + (brp * inv) / 255; - uint32_t outGp = ogp + (bgp * inv) / 255; - uint32_t outBp = obp + (bbp * inv) / 255; - - uint8_t outR=0,outG=0,outB=0; - if(outA) { - outR = uint8_t(std::min(255, (outRp * 255) / outA)); - outG = uint8_t(std::min(255, (outGp * 255) / outA)); - outB = uint8_t(std::min(255, (outBp * 255) / outA)); - } - dst.Px[di] = _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 op(args...) - // tex 32x32 "#RRGGBBAA" |> ... - // Grouping (subprogram) only where an op expects a texture arg: - // overlay( tex "b" |> ... ) + // now supports: + // tex name |> 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 }; @@ -692,21 +1074,38 @@ private: 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}; @@ -748,7 +1147,11 @@ private: if(isAlpha(c) || c=='#') { size_t start=I; I++; - while (I Pos; std::unordered_map Named; - // For ops that accept texture expression, we allow first positional arg to be "subexpr marker" - // but we handle that at compile-time by parsing texture expr inside parentheses. }; // ======================== // Compiler state // ======================== std::string Source_; - std::vector Code_; + std::vector Code_; std::vector Patches_; - // ---- _emit helpers ---- - void _emit(Op op) { Code_.push_back(Word(op)); } - void _emitW(uint32_t v) { Code_.push_back(Word(v & 0xFFFFu)); } - void _emitU32(uint32_t v) { Code_.push_back(_lo16(v)); Code_.push_back(_hi16(v)); } + // ---- 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); } - void _emitTexRefName(const std::string& name) { - // reserve lo+hi for uint32 texId - size_t lo = Code_.size(); - Code_.push_back(0); - Code_.push_back(0); - Patches_.push_back(Patch{lo, name}); + // 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}); } - void _emitSrcRef(const SrcRef& r) { - Code_.push_back(Word(r.Kind)); - Code_.push_back(r.A); - Code_.push_back(r.B); + 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 - // Stored as 0xAARRGGBB + // 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; @@ -839,19 +1255,118 @@ private: return false; } - // Parse base expression after tex: - // 1) "name" - // 2) Number X Number Ident(color) - // 3) (future) png("...") + // 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::String || a.Kind == TokKind::Ident) { - // tex "name.png" - _emit(Op::Base_Tex); - // SrcRef(TexId): kind + id(lo/hi) - Code_.push_back(Word(SrcKind::TexId)); - _emitTexRefName(a.Text); // lo+hi patched later - } else if(a.Kind == TokKind::Number) { + 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(); @@ -867,65 +1382,41 @@ private: return false; } if(w>65535u || h>65535u) { if(err) *err="w/h must fit in uint16"; return false; } - _emit(Op::Base_Fill); - _emitW(w); _emitW(h); - _emitU32(color); - } else { - if(err) *err="Bad 'tex' base expression"; - return false; + _emitOp(out, Op::Base_Fill); + _emitU16(out, w); + _emitU16(out, h); + _emitU32(out, color); + return true; } - // 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 { - // no-arg op (like brighten) must be followed by next |> or end - nt = peek; - } - - if(!_compileOp(lx, op, err)) return false; - } - - _emit(Op::End); - return true; + if(err) *err="Bad 'tex' base expression"; + return false; } - // Parses either: - // - normal args list: (a,b,key=v) - // - OR for ops that take texture, allow: ( tex ... |> ... ) as the *first* positional "special" + // ======================== + // 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) { - // Lookahead: if next token is 'tex' => parse sub texture expression until ')' Tok first = lx.next(); - if(first.Kind==TokKind::Ident && first.Text=="tex") { - // We parse a full texture expression (starting after 'tex') into a subprogram bytecode vector. - // We'll store a marker in op.Named["_subtex"] with special string "" - // But easier: store the subprogram words immediately as a pseudo-arg in op.Pos[0].S = "" - ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = "__SUBTEX__"; - op.Pos.push_back(std::move(av)); - // compile subprogram into vector sub - std::vector sub; + 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; - // Expect ')' Tok end = lx.next(); if(end.Kind != TokKind::RParen) { if(err) *err="Expected ')' after sub texture expr"; return false; } - // Stash the subprogram into an internal buffer attached to this op (hack: store in a map) PendingSub_[&op] = std::move(sub); return true; } - // Otherwise parse normal arg list, where `first` is first token inside '(' + // otherwise parse as normal arg list, where `first` is first token inside '(' Tok t = first; if(t.Kind == TokKind::RParen) return true; @@ -958,6 +1449,39 @@ private: } } + 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; } @@ -967,139 +1491,142 @@ private: } // ======================== - // Subprogram compilation - // We already consumed 'tex' token. Now we parse base + pipeline until we hit ')' - // Strategy: - // - compile into `sub` vector - // - stop when next token would be ')' - // - do NOT consume ')' + // Subprogram compilation: + // we already consumed 'tex'. Parse base + pipeline until next token is ')' + // DO NOT consume ')' // ======================== - bool _compileSubProgramFromAlreadySawTex(Lexer& lx, std::vector& sub, std::string* err) { - // We reuse a mini-compiler that writes into `sub` instead of Code_ - auto emitS = [&](Op op){ sub.push_back(Word(op)); }; - auto emitSW = [&](uint32_t v){ sub.push_back(Word(v & 0xFFFFu)); }; - auto emitSU32 = [&](uint32_t v){ sub.push_back(_lo16(v)); sub.push_back(_hi16(v)); }; - auto emitSTexName = [&](const std::string& name){ - // IMPORTANT: patches must point into main Code_, not sub. - // Solution: subprogram words are appended into main Code_ later, so we can patch after append. - // Here we place placeholder lo/hi and store a *relative patch* into SubPatchesTemp_. - size_t lo = sub.size(); - sub.push_back(0); sub.push_back(0); - SubPatchesTemp_.push_back({lo, name}); // relative to sub start - }; + struct PendingSubData { + std::vector Bytes; + std::vector RelPatches; + }; - Tok a = lx.next(); - if(a.Kind == TokKind::String || a.Kind == TokKind::Ident) { - emitS(Op::Base_Tex); - sub.push_back(Word(SrcKind::TexId)); - emitSTexName(a.Text); - } else if(a.Kind == TokKind::Number) { - 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="Sub tex: expected x <#color>"; - return false; - } - uint32_t w = a.U32, h = b.U32; - uint32_t color=0; - if(!_parseHexColor(colTok.Text, color)) { if(err) *err="Sub tex: bad color"; return false; } - if(w>65535u || h>65535u) { if(err) *err="Sub tex: w/h must fit uint16"; return false; } - emitS(Op::Base_Fill); - emitSW(w); emitSW(h); - emitSU32(color); - } else { - if(err) *err="Sub tex: bad base"; + 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 we see ')' lookahead (we can’t unread, so we detect by peeking in a copy) - while (true) { - // Peek next non-ws token without consuming by copying lexer - Lexer peek = lx; - Tok nt = peek.next(); + // 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 + // 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; } - // compile op into `sub` by temporarily swapping buffers - if(!_compileOpInto(lx, op, sub, emitS, emitSW, emitSU32, emitSTexName, err)) 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; } - emitS(Op::End); + + _emitOp(outSub.Bytes, Op::End); return true; } - // Temporary relative patches inside subprogram being built - struct RelPatch { size_t RelLo; std::string Name; }; - mutable std::vector SubPatchesTemp_; + // pending subprogram associated with ParsedOp pointer (created during parsing) + mutable std::unordered_map PendingSub_; - // Stash compiled subprogram per op pointer (simplifies this one-file example) - 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(); - // Append a subprogram to main Code_, returning SrcRef(Sub, offset,len) and migrating its patches - SrcRef _appendSubprogram(std::vector&& sub) { - // offset/len must fit u16 - size_t offset = Code_.size(); - size_t len = sub.size(); - - // migrate relative patches -> absolute patches into main Code_ - // Each rel patch points to lo word within sub vector. - for(const auto& rp : SubPatchesTemp_) { - size_t absLo = offset + rp.RelLo; - Patches_.push_back(Patch{absLo, rp.Name}); + if(offset > 0xFFFFFFu || len > 0xFFFFFFu || (offset + len) > 0xFFFFFFu) { + if(err) *err = "Subprogram слишком большой (off/len должны влезать в u24 байт)"; + return false; } - SubPatchesTemp_.clear(); - Code_.insert(Code_.end(), sub.begin(), sub.end()); + 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; + } - SrcRef r; - r.Kind = SrcKind::Sub; - r.A = Word(offset & 0xFFFFu); - r.B = Word(len & 0xFFFFu); - return r; + // 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 + // Compile operations into arbitrary `out` + // absPatches != nullptr => patches recorded as absolute for this buffer + // relPatches != nullptr => patches recorded as relative for this buffer // ======================== - bool _compileOp(Lexer& lx, const ParsedOp& op, std::string* err) { - // Normal compile into main Code_ - auto it = PendingSub_.find(&op); - const bool hasSub = (it != PendingSub_.end()); - return _compileOpInto( - lx, op, Code_, - [&](Op o){ _emit(o); }, - [&](uint32_t v){ _emitW(v); }, - [&](uint32_t v){ _emitU32(v); }, - [&](const std::string& name){ _emitTexRefName(name); }, - err, - hasSub ? &it->second : nullptr - ); - } - - // Core compiler that can target either main `Code_` or a `sub` vector. - template bool _compileOpInto(Lexer& /*lx*/, - const ParsedOp& op, - std::vector& out, - EmitOp emitOpFn, - EmitWFn emitWFn, - EmitU32Fn emitU32Fn, - EmitTexNameFn emitTexNameFn, - std::string* err, - std::vector* pendingSub = nullptr) { + 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; @@ -1120,64 +1647,55 @@ private: return it->second.S; }; - auto emitSrcTexName = [&](const std::string& texName){ - // SrcRef(TexId): kind + id(lo/hi) - out.push_back(Word(SrcKind::TexId)); - emitTexNameFn(texName); + auto emitSrcFromName = [&](const std::string& n){ + _emitSrcTexName(out, absPatches, relPatches, n); }; auto emitSrcFromPendingSub = [&]()->bool{ - if(!pendingSub) { if(err) *err="Internal: missing subprogram"; return false; } - // move pendingSub into main Code_ ONLY (grouping only makes sense there) - // If we're compiling inside a subprogram and we see another nested subprogram, - // this demo keeps it simple: it will still append into the *same* vector (out), - // so we can just inline by "append here". For production, you likely want a - // separate sub-table or a more structured approach. - // - // For simplicity: we append nested subprogram right into `out` and reference it by offset/len. - size_t offset = out.size(); - size_t len = pendingSub->size(); - out.insert(out.end(), pendingSub->begin(), pendingSub->end()); - out.push_back(Word(SrcKind::Sub)); - out.push_back(Word(offset & 0xFFFFu)); - out.push_back(Word(len & 0xFFFFu)); + 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/combine parts --- + // --- Ops that accept a "texture" argument: overlay/mask/lowpart --- if(op.Name == "overlay") { - emitOpFn(Op::Overlay); - if(!op.Pos.empty() && op.Pos[0].S == "__SUBTEX__") { - // Subprogram source - // In main compile path, we prefer storing subprograms at end and referencing by offset/len. - // Here we already have the compiled sub in pendingSub; we append + _emit SrcRef(Sub,...). - // For main program, we use _appendSubprogram() outside; in this generic function we inline. - return emitSrcFromPendingSub(); - } + _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; } - emitSrcTexName(tex); + emitSrcFromName(tex); return true; } if(op.Name == "mask") { - emitOpFn(Op::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; } - emitSrcTexName(tex); + 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; } - emitOpFn(Op::LowPart); - emitWFn(std::min(100u, pct)); + + _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 tex"; return false; } - emitSrcTexName(tex); + if(tex.empty()) { if(err) *err="lowpart requires texture arg"; return false; } + emitSrcFromName(tex); return true; } @@ -1186,24 +1704,24 @@ private: 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; } - emitOpFn(Op::Resize); emitWFn(w); emitWFn(h); + _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)); - emitOpFn(Op::Transform); emitWFn(t & 7u); + _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)); - emitOpFn(Op::Opacity); emitWFn(a & 0xFFu); + _emitOp(out, Op::Opacity); _emitU8(out, a & 0xFFu); return true; } if(op.Name == "remove_alpha" || op.Name == "noalpha") { - emitOpFn(Op::NoAlpha); + _emitOp(out, Op::NoAlpha); return true; } @@ -1212,10 +1730,10 @@ private: uint32_t argb=0; if(!_parseHexColor(col, argb)) { if(err) *err="make_alpha requires color #RRGGBB"; return false; } uint32_t rgb24 = argb & 0x00FFFFFFu; - // pack rgb24 into two u16: lo=0xRRGG, hi=0x00BB - emitOpFn(Op::MakeAlpha); - emitWFn((rgb24 >> 8) & 0xFFFFu); // RR GG - emitWFn(rgb24 & 0x00FFu); // BB + _emitOp(out, Op::MakeAlpha); + _emitU8(out, (rgb24 >> 16) & 0xFFu); + _emitU8(out, (rgb24 >> 8) & 0xFFu); + _emitU8(out, (rgb24 >> 0) & 0xFFu); return true; } @@ -1228,12 +1746,12 @@ private: if(c=='b') mask |= 4; if(c=='a') mask |= 8; } - emitOpFn(Op::Invert); emitWFn(mask); + _emitOp(out, Op::Invert); _emitU8(out, mask & 0xFu); return true; } if(op.Name == "brighten") { - emitOpFn(Op::Brighten); + _emitOp(out, Op::Brighten); return true; } @@ -1242,9 +1760,9 @@ private: 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)); - emitOpFn(Op::Contrast); - emitWFn(uint32_t(c + 127)); - emitWFn(uint32_t(b + 127)); + _emitOp(out, Op::Contrast); + _emitU8(out, uint32_t(c + 127)); + _emitU8(out, uint32_t(b + 127)); return true; } @@ -1252,11 +1770,11 @@ private: 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; } - emitOpFn(opcode); - emitU32Fn(argb); + _emitOp(out, opcode); + _emitU32(out, argb); if(needsRatio) { uint32_t ratio = namedU("ratio").value_or(posU(1).value_or(255)); - emitWFn(ratio & 0xFFu); + _emitU8(out, ratio & 0xFFu); } return true; }; @@ -1265,6 +1783,33 @@ private: 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; } diff --git a/Src/Client/Vulkan/Vulkan.cpp b/Src/Client/Vulkan/Vulkan.cpp index 4ee5861..8945557 100644 --- a/Src/Client/Vulkan/Vulkan.cpp +++ b/Src/Client/Vulkan/Vulkan.cpp @@ -311,7 +311,7 @@ void Vulkan::run() } if(Game.RSession) { - Game.RSession->beforeDraw(); + Game.RSession->beforeDraw(double(gTime)); } { @@ -624,12 +624,6 @@ void Vulkan::run() err = vkQueuePresentKHR(*lockQueue, &present); } - { - auto lockQueue = Graphics.DeviceQueueGraphic.lock(); - vkDeviceWaitIdle(Graphics.Device); - lockQueue.unlock(); - } - if (err == VK_ERROR_OUT_OF_DATE_KHR) { freeSwapchains(); @@ -651,12 +645,6 @@ void Vulkan::run() Screen.State = DrawState::End; } - { - auto lockQueue = Graphics.DeviceQueueGraphic.lock(); - vkDeviceWaitIdle(Graphics.Device); - lockQueue.unlock(); - } - for(int iter = 0; iter < 4; iter++) { vkDestroySemaphore(Graphics.Device, SemaphoreImageAcquired[iter], nullptr); vkDestroySemaphore(Graphics.Device, SemaphoreDrawComplete[iter], nullptr); @@ -688,8 +676,6 @@ uint32_t Vulkan::memoryTypeFromProperties(uint32_t bitsOfAcceptableTypes, VkFlag void Vulkan::freeSwapchains() { - //vkDeviceWaitIdle(Screen.Device); - if(Graphics.Instance && Graphics.Device) { std::vector oldViews; @@ -2302,6 +2288,9 @@ void Vulkan::gui_ConnectedToServer() { (int) Game.RSession->PlayerPos.x >> 6, (int) Game.RSession->PlayerPos.y >> 6, (int) Game.RSession->PlayerPos.z >> 6 ); + double chunksKb = double(Game.Session->getVisibleCompressedChunksBytes()) / 1024.0; + ImGui::Text("chunks compressed: %.1f KB", chunksKb); + if(ImGui::Button("Delimeter")) LOG.debug(); diff --git a/Src/Client/Vulkan/VulkanRenderSession.cpp b/Src/Client/Vulkan/VulkanRenderSession.cpp index a46661f..3ec3509 100644 --- a/Src/Client/Vulkan/VulkanRenderSession.cpp +++ b/Src/Client/Vulkan/VulkanRenderSession.cpp @@ -323,7 +323,7 @@ void ChunkMeshGenerator::run(uint8_t id) { }; std::unordered_map modelCache; - std::unordered_map baseTextureCache; + std::unordered_map baseTextureCache; std::vector metaStatesInfo; { @@ -451,7 +451,7 @@ void ChunkMeshGenerator::run(uint8_t id) { if(iterTex != baseTextureCache.end()) { v.Tex = iterTex->second; } else { - uint16_t resolvedTex = NSP->getTextureId(node->TexId); + uint32_t resolvedTex = NSP->getTextureId(node->TexId); v.Tex = resolvedTex; baseTextureCache.emplace(node->TexId, resolvedTex); } @@ -1670,7 +1670,7 @@ void VulkanRenderSession::tickSync(const TickSyncData& data) { } if(TP) { - std::vector> textureResources; + std::vector textureResources; std::vector textureLost; if(auto iter = data.Assets_ChangeOrAdd.find(EnumAssets::Texture); iter != data.Assets_ChangeOrAdd.end()) { @@ -1680,7 +1680,12 @@ void VulkanRenderSession::tickSync(const TickSyncData& data) { if(entryIter == list.end()) continue; - textureResources.emplace_back(id, entryIter->second.Res); + textureResources.push_back({ + .Id = id, + .Res = entryIter->second.Res, + .Domain = entryIter->second.Domain, + .Key = entryIter->second.Key + }); } } @@ -1739,9 +1744,9 @@ void VulkanRenderSession::setCameraPos(WorldId_t worldId, Pos::Object pos, glm:: PlayerPos /= float(Pos::Object_t::BS); } -void VulkanRenderSession::beforeDraw() { +void VulkanRenderSession::beforeDraw(double timeSeconds) { if(TP) - TP->update(); + TP->update(timeSeconds); LightDummy.atlasUpdateDynamicData(); CP.flushUploadsAndBarriers(VkInst->Graphics.CommandBufferRender); } @@ -1901,6 +1906,7 @@ void VulkanRenderSession::drawWorld(GlobalTime gTime, float dTime, VkCommandBuff vkCmdDraw(drawCmd, 6*3*2, 1, 0, 0); { + PCO.Model = glm::mat4(1.0f); Pos::GlobalChunk x64offset = X64Offset >> Pos::Object_t::BS_Bit >> 4; Pos::GlobalRegion x64offset_region = x64offset >> 2; @@ -1940,9 +1946,160 @@ void VulkanRenderSession::drawWorld(GlobalTime gTime, float dTime, VkCommandBuff PCO.Model = orig; } + vkCmdBindPipeline(drawCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, NodeStaticTransparentPipeline); + vkCmdPushConstants(drawCmd, MainAtlas_LightMap_PipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_GEOMETRY_BIT, 0, sizeof(WorldPCO), &PCO); + vkCmdBindDescriptorSets(drawCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + MainAtlas_LightMap_PipelineLayout, 0, 2, + (const VkDescriptorSet[]) {TP ? TP->getDescriptorSet() : VK_NULL_HANDLE, VoxelLightMapDescriptor}, 0, nullptr); + + ensureAtlasLayerPreview(); + if(AtlasLayersPreview) { + glm::mat4 previewModel = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 64.0f, 0.0f)-glm::vec3(X64Offset >> Pos::Object_t::BS_Bit)); + vkCmdPushConstants(drawCmd, MainAtlas_LightMap_PipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_GEOMETRY_BIT, offsetof(WorldPCO, Model), sizeof(WorldPCO::Model), &previewModel); + VkBuffer previewBuffer = *AtlasLayersPreview; + VkDeviceSize previewOffset = 0; + vkCmdBindVertexBuffers(drawCmd, 0, 1, &previewBuffer, &previewOffset); + vkCmdDraw(drawCmd, AtlasLayersPreviewCount * 6, 1, 0, 0); + } + + if(false) { + ensureEntityTexture(); + + if(!ServerSession->Content.Entityes.empty()) { + VkBuffer entityBuffer = TestQuad; + VkDeviceSize entityOffset = 0; + vkCmdBindVertexBuffers(drawCmd, 0, 1, &entityBuffer, &entityOffset); + + glm::mat4 orig = PCO.Model; + for(const auto& pair : ServerSession->Content.Entityes) { + const auto& info = pair.second; + if(info.WorldId != WorldId) + continue; + + glm::vec3 entityPos = Pos::Object_t::asFloatVec(info.Pos - X64Offset); + entityPos.y -= 1.6f; // Camera position arrives as eye height. + + glm::mat4 model = glm::translate(glm::mat4(1.0f), entityPos); + model = model * glm::mat4(info.Quat); + model = glm::scale(model, glm::vec3(0.6f, 1.8f, 0.6f)); + + PCO.Model = model; + vkCmdPushConstants(drawCmd, MainAtlas_LightMap_PipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_GEOMETRY_BIT, offsetof(WorldPCO, Model), sizeof(WorldPCO::Model), &PCO.Model); + vkCmdDraw(drawCmd, 6*3*2, 1, 0, 0); + } + + PCO.Model = orig; + } + } + CP.pushFrame(); } +void VulkanRenderSession::updateTestQuadTexture(uint32_t texId) { + if(EntityTextureReady && EntityTextureId == texId) + return; + + auto *array = reinterpret_cast(TestQuad.mapMemory()); + const size_t vertexCount = TestQuad.getSize() / sizeof(NodeVertexStatic); + for(size_t iter = 0; iter < vertexCount; ++iter) + array[iter].Tex = texId; + TestQuad.unMapMemory(); + + EntityTextureId = texId; + EntityTextureReady = true; +} + +void VulkanRenderSession::ensureEntityTexture() { + if(EntityTextureReady || !TP || !NSP) + return; + + auto iter = ServerSession->Assets.find(EnumAssets::Texture); + if(iter == ServerSession->Assets.end() || iter->second.empty()) + return; + + const AssetEntry* picked = nullptr; + for(const auto& [id, entry] : iter->second) { + if(entry.Key == "default.png") { + picked = &entry; + break; + } + } + if(!picked) { + for(const auto& [id, entry] : iter->second) { + if(entry.Key == "grass.png") { + picked = &entry; + break; + } + } + } + if(!picked) + picked = &iter->second.begin()->second; + + updateTestQuadTexture(NSP->getTextureId(picked->Id)); +} + +void VulkanRenderSession::ensureAtlasLayerPreview() { + if(!TP) + return; + + const uint32_t maxLayers = TP->getAtlasMaxLayers(); + if(maxLayers == 0) + return; + + if(AtlasLayersPreview && AtlasLayersPreviewCount == maxLayers) + return; + + TP->requestAtlasLayerCount(maxLayers); + + const uint32_t vertsPerQuad = 6; + const uint32_t totalVerts = maxLayers * vertsPerQuad; + std::vector verts(totalVerts); + + const uint32_t columns = 4; + const uint32_t base = 224; + const uint32_t step = 320; + const uint32_t size = 256; + const uint32_t z = base; + + auto makeVert = [&](uint32_t fx, uint32_t fy, uint32_t tex, uint32_t tu, uint32_t tv) -> NodeVertexStatic { + NodeVertexStatic v{}; + v.FX = fx; + v.FY = fy; + v.FZ = z; + v.LS = 0; + v.Tex = tex; + v.TU = tu; + v.TV = tv; + return v; + }; + + for(uint32_t layer = 0; layer < maxLayers; ++layer) { + const uint32_t col = layer % columns; + const uint32_t row = layer / columns; + const uint32_t x0 = base + col * step; + const uint32_t y0 = base + row * step; + const uint32_t x1 = x0 + size; + const uint32_t y1 = y0 + size; + const uint32_t tex = TP->getAtlasLayerId(layer); + + const size_t start = static_cast(layer) * vertsPerQuad; + verts[start + 0] = makeVert(x0, y0, tex, 0, 0); + verts[start + 1] = makeVert(x0, y1, tex, 0, 65535); + verts[start + 2] = makeVert(x1, y1, tex, 65535, 65535); + verts[start + 3] = makeVert(x0, y0, tex, 0, 0); + verts[start + 4] = makeVert(x1, y1, tex, 65535, 65535); + verts[start + 5] = makeVert(x1, y0, tex, 65535, 0); + } + + AtlasLayersPreview.emplace(VkInst, verts.size() * sizeof(NodeVertexStatic)); + std::memcpy(AtlasLayersPreview->mapMemory(), verts.data(), verts.size() * sizeof(NodeVertexStatic)); + AtlasLayersPreview->unMapMemory(); + AtlasLayersPreviewCount = maxLayers; +} + void VulkanRenderSession::pushStage(EnumRenderStage stage) { } diff --git a/Src/Client/Vulkan/VulkanRenderSession.hpp b/Src/Client/Vulkan/VulkanRenderSession.hpp index 872b9b9..1d3978f 100644 --- a/Src/Client/Vulkan/VulkanRenderSession.hpp +++ b/Src/Client/Vulkan/VulkanRenderSession.hpp @@ -418,6 +418,13 @@ private: */ class TextureProvider { public: + struct TextureUpdate { + AssetsTexture Id = 0; + Resource Res; + std::string Domain; + std::string Key; + }; + TextureProvider(Vulkan* inst, VkDescriptorPool descPool) : Inst(inst), DescPool(descPool) { @@ -508,32 +515,58 @@ public: return Descriptor; } - uint16_t getTextureId(const TexturePipeline& pipe) { + uint32_t getTextureId(const TexturePipeline& pipe) { std::lock_guard lock(Mutex); - auto iter = PipelineToAtlas.find(pipe); - if(iter != PipelineToAtlas.end()) - return iter->second; + bool animated = isAnimatedPipeline(pipe); + if(!animated) { + auto iter = PipelineToAtlas.find(pipe); + if(iter != PipelineToAtlas.end()) + return iter->second; + } ::HashedPipeline hashed = makeHashedPipeline(pipe); uint32_t atlasId = Atlas->getByPipeline(hashed); - uint16_t result = 0; - if(atlasId <= std::numeric_limits::max()) - result = static_cast(atlasId); - else + uint32_t result = atlasId; + if(Atlas && result >= Atlas->maxTextureId()) { LOG.warn() << "Atlas texture id overflow: " << atlasId; + result = Atlas->reservedOverflowId(); + } - PipelineToAtlas.emplace(pipe, result); + if(!animated) + PipelineToAtlas.emplace(pipe, result); NeedsUpload = true; return result; } + uint32_t getAtlasMaxLayers() const { + std::lock_guard lock(Mutex); + return Atlas ? Atlas->maxLayers() : 0u; + } + + uint32_t getAtlasLayerId(uint32_t layer) const { + std::lock_guard lock(Mutex); + if(!Atlas) + return TextureAtlas::kOverflowId; + if(layer >= Atlas->maxLayers()) + return Atlas->reservedOverflowId(); + return Atlas->reservedLayerId(layer); + } + + void requestAtlasLayerCount(uint32_t layers) { + std::lock_guard lock(Mutex); + if(Atlas) + Atlas->requestLayerCount(layers); + } + // Применяет изменения, возвращая все затронутые модели - std::vector onTexturesChanges(std::vector> newOrChanged, std::vector lost) { + std::vector onTexturesChanges(std::vector newOrChanged, std::vector lost) { std::lock_guard lock(Mutex); std::vector result; - for(const auto& [key, res] : newOrChanged) { + for(const auto& update : newOrChanged) { + const AssetsTexture key = update.Id; + const Resource& res = update.Res; result.push_back(key); iResource sres((const uint8_t*) res.data(), res.size()); @@ -563,12 +596,19 @@ public: std::move(pixels) )); + if(auto anim = getDefaultAnimation(update.Key, width, height)) { + AnimatedSources[key] = *anim; + } else { + AnimatedSources.erase(key); + } + NeedsUpload = true; } for(AssetsTexture key : lost) { result.push_back(key); Atlas->freeTexture(key); + AnimatedSources.erase(key); NeedsUpload = true; } @@ -579,9 +619,15 @@ public: return result; } - void update() { + void update(double timeSeconds) { std::lock_guard lock(Mutex); - if(!NeedsUpload || !Atlas) + if(!Atlas) + return; + + if(Atlas->updateAnimatedPipelines(timeSeconds)) + NeedsUpload = true; + + if(!NeedsUpload) return; Atlas->flushNewPipelines(); @@ -638,24 +684,94 @@ public: } private: + struct AnimatedSource { + uint16_t FrameW = 0; + uint16_t FrameH = 0; + uint16_t FrameCount = 0; + uint16_t FpsQ = 0; + uint16_t Flags = 0; + }; + + static std::optional getDefaultAnimation(std::string_view key, uint32_t width, uint32_t height) { + if(auto slash = key.find_last_of('/'); slash != std::string_view::npos) + key = key.substr(slash + 1); + + if(key == "fire_0.png") { + AnimatedSource anim; + anim.FrameW = static_cast(width); + anim.FrameH = static_cast(width); + anim.FrameCount = static_cast(width ? height / width : 0); + anim.FpsQ = static_cast(12 * 256); + anim.Flags = 0; + return anim; + } + + if(key == "lava_still.png") { + AnimatedSource anim; + anim.FrameW = static_cast(width); + anim.FrameH = static_cast(width); + anim.FrameCount = static_cast(width ? height / width : 0); + anim.FpsQ = static_cast(8 * 256); + anim.Flags = 0; + return anim; + } + + if(key == "water_still.png") { + AnimatedSource anim; + anim.FrameW = static_cast(width); + anim.FrameH = static_cast(width); + anim.FrameCount = static_cast(width ? height / width : 0); + anim.FpsQ = static_cast(8 * 256); + anim.Flags = TexturePipelineProgram::AnimSmooth; + return anim; + } + + return std::nullopt; + } + + bool isAnimatedPipeline(const TexturePipeline& pipe) const { + if(!pipe.Pipeline.empty()) + return false; + if(pipe.BinTextures.size() != 1) + return false; + return AnimatedSources.contains(pipe.BinTextures.front()); + } + ::HashedPipeline makeHashedPipeline(const TexturePipeline& pipe) const { ::Pipeline pipeline; - if(!pipe.Pipeline.empty() && (pipe.Pipeline.size() % 2u) == 0u) { - std::vector words; - words.reserve(pipe.Pipeline.size() / 2u); - const uint8_t* bytes = reinterpret_cast(pipe.Pipeline.data()); - for(size_t i = 0; i < pipe.Pipeline.size(); i += 2) { - uint16_t lo = bytes[i]; - uint16_t hi = bytes[i + 1]; - words.push_back(static_cast(lo | (hi << 8))); - } - pipeline._Pipeline.assign(words.begin(), words.end()); + if(!pipe.Pipeline.empty()) { + const auto* bytes = reinterpret_cast(pipe.Pipeline.data()); + pipeline._Pipeline.assign(bytes, bytes + pipe.Pipeline.size()); } if(pipeline._Pipeline.empty()) { - if(!pipe.BinTextures.empty()) - pipeline = ::Pipeline(pipe.BinTextures.front()); + if(!pipe.BinTextures.empty()) { + AssetsTexture texId = pipe.BinTextures.front(); + auto animIter = AnimatedSources.find(texId); + if(animIter != AnimatedSources.end()) { + const auto& anim = animIter->second; + pipeline._Pipeline.clear(); + pipeline._Pipeline.reserve(1 + 1 + 3 + 2 + 2 + 2 + 2 + 1 + 1); + auto emit16 = [&](uint16_t v) { + pipeline._Pipeline.push_back(static_cast<::detail::Word>(v & 0xFFu)); + pipeline._Pipeline.push_back(static_cast<::detail::Word>((v >> 8) & 0xFFu)); + }; + pipeline._Pipeline.push_back(static_cast<::detail::Word>(::detail::Op16::Base_Anim)); + pipeline._Pipeline.push_back(static_cast<::detail::Word>(::detail::SrcKind16::TexId)); + pipeline._Pipeline.push_back(static_cast<::detail::Word>(texId & 0xFFu)); + pipeline._Pipeline.push_back(static_cast<::detail::Word>((texId >> 8) & 0xFFu)); + pipeline._Pipeline.push_back(static_cast<::detail::Word>((texId >> 16) & 0xFFu)); + emit16(anim.FrameW); + emit16(anim.FrameH); + emit16(anim.FrameCount); + emit16(anim.FpsQ); + pipeline._Pipeline.push_back(static_cast<::detail::Word>(anim.Flags & 0xFFu)); + pipeline._Pipeline.push_back(static_cast<::detail::Word>(::detail::Op16::End)); + } else { + pipeline = ::Pipeline(texId); + } + } } return ::HashedPipeline(pipeline); @@ -690,7 +806,8 @@ private: std::shared_ptr AtlasStaging; std::unique_ptr Atlas; - std::unordered_map PipelineToAtlas; + std::unordered_map PipelineToAtlas; + std::unordered_map AnimatedSources; bool NeedsUpload = false; Logger LOG = "Client>TextureProvider"; @@ -811,7 +928,7 @@ public: } std::vector>>>> result; - std::unordered_map pipelineResolveCache; + std::unordered_map pipelineResolveCache; auto appendModel = [&](AssetsModel modelId, const std::vector& transforms, std::unordered_map>& out) { ModelProvider::Model model = MP.getModel(modelId); @@ -886,7 +1003,7 @@ public: return result; } - uint16_t getTextureId(AssetsTexture texId) { + uint32_t getTextureId(AssetsTexture texId) { if(texId == 0) return 0; @@ -1134,6 +1251,10 @@ class VulkanRenderSession : public IRenderSession { AtlasImage LightDummy; Buffer TestQuad; std::optional TestVoxel; + std::optional AtlasLayersPreview; + uint32_t AtlasLayersPreviewCount = 0; + uint32_t EntityTextureId = 0; + bool EntityTextureReady = false; VkDescriptorPool DescriptorPool = VK_NULL_HANDLE; @@ -1187,7 +1308,7 @@ public: return glm::translate(glm::mat4(quat), camOffset); } - void beforeDraw(); + void beforeDraw(double timeSeconds); void onGpuFinished(); void drawWorld(GlobalTime gTime, float dTime, VkCommandBuffer drawCmd); void pushStage(EnumRenderStage stage); @@ -1195,6 +1316,9 @@ public: static std::vector generateMeshForVoxelChunks(const std::vector& cubes); private: + void updateTestQuadTexture(uint32_t texId); + void ensureEntityTexture(); + void ensureAtlasLayerPreview(); void updateDescriptor_VoxelsLight(); void updateDescriptor_ChunksLight(); }; diff --git a/Src/Common/Abstract.cpp b/Src/Common/Abstract.cpp index 5ce7114..ac7c32d 100644 --- a/Src/Common/Abstract.cpp +++ b/Src/Common/Abstract.cpp @@ -1,4 +1,5 @@ #include "Abstract.hpp" +#include "Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp" #include "Common/Net.hpp" #include "TOSLib.hpp" #include @@ -6,6 +7,8 @@ #include "boost/json.hpp" #include "sha2.hpp" #include +#include +#include #include #include #include @@ -15,6 +18,7 @@ #include #include #include +#include #include @@ -22,6 +26,50 @@ namespace LV { namespace fs = std::filesystem; +PrecompiledTexturePipeline compileTexturePipeline(const std::string &cmd, std::string_view defaultDomain) { + PrecompiledTexturePipeline result; + + std::string_view view(cmd); + const size_t trimPos = view.find_first_not_of(" \t\r\n"); + if(trimPos == std::string_view::npos) + MAKE_ERROR("Пустая текстурная команда"); + + view = view.substr(trimPos); + + const bool isPipeline = view.size() >= 3 + && view.compare(0, 3, "tex") == 0 + && (view.size() == 3 || std::isspace(static_cast(view[3]))); + + if(!isPipeline) { + auto [domain, key] = parseDomainKey(std::string(view), defaultDomain); + result.Assets.emplace_back(std::move(domain), std::move(key)); + return result; + } + + TexturePipelineProgram program; + std::string err; + if(!program.compile(std::string(view), &err)) { + MAKE_ERROR("Ошибка разбора pipeline: " << err); + } + + result.IsSource = true; + result.Pipeline.assign(reinterpret_cast(view.data()), view.size()); + + std::unordered_set seen; + for(const auto& patch : program.patches()) { + auto [domain, key] = parseDomainKey(patch.Name, defaultDomain); + std::string token; + token.reserve(domain.size() + key.size() + 1); + token.append(domain); + token.push_back(':'); + token.append(key); + if(seen.insert(token).second) + result.Assets.emplace_back(std::move(domain), std::move(key)); + } + + return result; +} + CompressedVoxels compressVoxels_byte(const std::vector& voxels) { std::u8string compressed; @@ -1089,6 +1137,10 @@ uint16_t PreparedNodeState::parseCondition(const std::string_view expression) { }; std::vector> tokens; + + if(expression.empty()) + tokens.push_back(int(1)); + ssize_t pos = 0; auto skipWS = [&](){ while(pos> Assets; // Чистый код текстурных преобразований, локальные идентификаторы связаны с Assets std::u8string Pipeline; + // Pipeline содержит исходный текст (tex ...), нужен для компиляции на сервере + bool IsSource = false; }; struct TexturePipeline { @@ -530,15 +532,7 @@ struct TexturePipeline { }; // Компилятор текстурных потоков -inline PrecompiledTexturePipeline compileTexturePipeline(const std::string &cmd, const std::string_view defaultDomain = "core") { - PrecompiledTexturePipeline result; - - auto [domain, key] = parseDomainKey(cmd, defaultDomain); - - result.Assets.emplace_back(domain, key); - - return result; -} +PrecompiledTexturePipeline compileTexturePipeline(const std::string &cmd, std::string_view defaultDomain = "core"); struct NodestateEntry { std::string Name; diff --git a/Src/Common/AssetsPreloader.hpp b/Src/Common/AssetsPreloader.hpp new file mode 100644 index 0000000..272c491 --- /dev/null +++ b/Src/Common/AssetsPreloader.hpp @@ -0,0 +1,253 @@ +#pragma once + +#include +#include +#include +#include +#include "Common/Async.hpp" +#include "TOSAsync.hpp" +#include "boost/asio/executor.hpp" +#include "boost/asio/experimental/channel.hpp" +#include "boost/asio/this_coro.hpp" +#include "sha2.hpp" + +/* + Класс отвечает за отслеживание изменений и подгрузки медиаресурсов в указанных директориях. + Медиаресурсы, собранные из папки assets или зарегистрированные модами. + Хранит все данные в оперативной памяти. +*/ + + +enum class EnumAssets : int { + Nodestate, Particle, Animation, Model, Texture, Sound, Font, MAX_ENUM +}; + +using AssetsNodestate = uint32_t; +using AssetsParticle = uint32_t; +using AssetsAnimation = uint32_t; +using AssetsModel = uint32_t; +using AssetsTexture = uint32_t; +using AssetsSound = uint32_t; +using AssetsFont = uint32_t; + +static constexpr const char* EnumAssetsToDirectory(EnumAssets value) { + switch(value) { + case EnumAssets::Nodestate: return "nodestate"; + case EnumAssets::Particle: return "particles"; + case EnumAssets::Animation: return "animations"; + case EnumAssets::Model: return "models"; + case EnumAssets::Texture: return "textures"; + case EnumAssets::Sound: return "sounds"; + case EnumAssets::Font: return "fonts"; + default: + } + + assert(!"Неизвестный тип медиаресурса"); +} + +namespace LV { + +namespace fs = std::filesystem; + +struct ResourceFile { + using Hash_t = sha2::sha256_hash; // boost::uuids::detail::sha1::digest_type; + + Hash_t Hash; + std::vector Data; + + void calcHash() { + Hash = sha2::sha256((const uint8_t*) Data.data(), Data.size()); + } +}; + +class AssetsPreloader : public TOS::IAsyncDestructible { +public: + using Ptr = std::shared_ptr; + + // + struct ReloadResult { + }; + + struct ReloadStatus { + /// TODO: callback'и для обновления статусов + /// TODO: многоуровневый статус std::vector. Этапы/Шаги/Объекты + }; + +public: + static coro Create(asio::io_context& ioc); + ~AssetsPreloader() = default; + + AssetsPreloader(const AssetsPreloader&) = delete; + AssetsPreloader(AssetsPreloader&&) = delete; + AssetsPreloader& operator=(const AssetsPreloader&) = delete; + AssetsPreloader& operator=(AssetsPreloader&&) = delete; + + // Пересматривает ресурсы и выдаёт изменения. + // Одновременно можно работать только один такой вызов. + // instances -> пути к директории с assets или архивы с assets внутри. От низшего приоритета к высшему. + // status -> обратный отклик о процессе обновления ресурсов. + // ReloadStatus <- новые и потерянные ресурсы. + coro reloadResources(const std::vector& instances, ReloadStatus* status = nullptr) { + bool expected = false; + assert(Reloading_.compare_exchange_strong(expected, true) && "Двойной вызов reloadResources"); + + try { + ReloadStatus secondStatus; + co_return _reloadResources(instances, status ? *status : secondStatus); + } catch(...) { + assert(!"reloadResources: здесь не должно быть ошибок"); + } + + Reloading_.exchange(false); + } + +private: + struct ResourceFirstStageInfo { + // Путь к архиву (если есть), и путь до ресурса + fs::path ArchivePath, Path; + // Время изменения файла + fs::file_time_type Timestamp; + }; + + struct ResourceSecondStageInfo : public ResourceFirstStageInfo { + // Обезличенный ресурс + std::shared_ptr> Resource; + ResourceFile::Hash_t Hash; + // Сырой заголовок + std::vector Dependencies; + }; + + /* + Ресурс имеет бинарную часть, из который вырезаны все зависимости. + Вторая часть это заголовок, которые всегда динамично передаётся с сервера. + В заголовке хранятся зависимости от ресурсов. + */ + struct MediaResource { + std::string Domain, Key; + + fs::file_time_type Timestamp; + // Обезличенный ресурс + std::shared_ptr> Resource; + // Хэш ресурса + ResourceFile::Hash_t Hash; + + // Скомпилированный заголовок + std::vector Dependencies; + }; + + AssetsPreloader(asio::io_context& ioc) + : TOS::IAsyncDestructible(ioc) + { + + } + + // Текущее состояние reloadResources + std::atomic Reloading_ = false; + + // Пересмотр ресурсов + coro _reloadResources(const std::vector& instances, ReloadStatus& status) const { + // 1) Поиск всех ресурсов и построение конечной карты ресурсов (timestamps, path, name, size) + // Карта найденных ресурсов + std::unordered_map< + EnumAssets, // Тип ресурса + std::unordered_map< + std::string, // Domain + std::unordered_map< + std::string, // Key + ResourceFirstStageInfo // ResourceInfo + > + > + > resourcesFirstStage; + + for (const fs::path& instance : instances) { + try { + if (fs::is_regular_file(instance)) { + // Может архив + /// TODO: пока не поддерживается + } else if (fs::is_directory(instance)) { + // Директория + fs::path assets = instance / "assets"; + if (fs::exists(assets) && fs::is_directory(assets)) { + // Директорию assets существует, перебираем домены в ней + for (auto begin = fs::directory_iterator(assets), end = fs::directory_iterator(); begin != end; begin++) { + if (!begin->is_directory()) + continue; + + /// TODO: выглядит всё не очень асинхронно + co_await asio::post(co_await asio::this_coro::executor); + + fs::path domainPath = begin->path(); + std::string domain = domainPath.filename(); + + // Перебираем по типу ресурса + for (EnumAssets assetType = EnumAssets(0); assetType < EnumAssets::MAX_ENUM; ((int&) assetType)++) { + fs::path assetPath = domainPath / EnumAssetsToDirectory(assetType); + + std::unordered_map< + std::string, // Key + ResourceFirstStageInfo // ResourceInfo + >& firstStage = resourcesFirstStage[assetType][domain]; + + // Исследуем все ресурсы одного типа + for (auto begin = fs::recursive_directory_iterator(assetPath), end = fs::recursive_directory_iterator(); begin != end; begin++) { + if (begin->is_directory()) + continue; + + fs::path file = begin->path(); + std::string key = fs::relative(file, domainPath).string(); + + // Работаем с ресурсом + firstStage[key] = ResourceFirstStageInfo{ + .Path = file, + .Timestamp = fs::last_write_time(file) + }; + } + } + } + } + } else { + throw std::runtime_error("Неизвестный тип инстанса медиаресурсов"); + } + } catch (const std::exception& exc) { + /// TODO: Логгировать в статусе + + } + } + + // 2) Обрабатываться будут только изменённые (новый timestamp) или новые ресурсы + // .meta + + // Текстуры, шрифты, звуки хранить как есть + // У моделей, состояний нод, анимации, частиц обналичить зависимости + // Мета влияет только на хедер + + /// TODO: реализовать реформатирование новых и изменённых ресурсов во внутренний обезличенный формат + + co_await asio::post(co_await asio::this_coro::executor); + + asio::experimental::channel ch(IOC, 8); + + co_return ReloadResult{}; + } + + std::unordered_map< + EnumAssets, // Тип ресурса + std::unordered_map< + std::string, // Domain + std::unordered_map< + std::string, // Key + uint32_t // ResourceId + > + > + > DKToId; + + std::unordered_map< + EnumAssets, // Тип ресурса + std::unordered_map< + uint32_t, + MediaResource // ResourceInfo + > + > MediaResources; +}; + +} \ No newline at end of file diff --git a/Src/Server/Abstract.cpp b/Src/Server/Abstract.cpp index f2b7caf..1b4459e 100644 --- a/Src/Server/Abstract.cpp +++ b/Src/Server/Abstract.cpp @@ -4,6 +4,18 @@ namespace LV::Server { +Entity::Entity(DefEntityId defId) + : DefId(defId) +{ + ABBOX = {Pos::Object_t::BS, Pos::Object_t::BS, Pos::Object_t::BS}; + WorldId = 0; + Pos = Pos::Object(0); + Speed = Pos::Object(0); + Acceleration = Pos::Object(0); + Quat = glm::quat(1.f, 0.f, 0.f, 0.f); + InRegionPos = Pos::GlobalRegion(0); +} + } namespace std { @@ -14,4 +26,4 @@ struct hash { return std::hash()(obj.WorldId) ^ std::hash()(obj.ObjectPos); } }; -} \ No newline at end of file +} diff --git a/Src/Server/AssetsManager.cpp b/Src/Server/AssetsManager.cpp index f119dec..2576ec9 100644 --- a/Src/Server/AssetsManager.cpp +++ b/Src/Server/AssetsManager.cpp @@ -1,8 +1,10 @@ #include "AssetsManager.hpp" #include "Common/Abstract.hpp" +#include "Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp" #include "boost/json.hpp" #include "png++/rgb_pixel.hpp" #include +#include #include #include #include @@ -560,10 +562,34 @@ AssetsManager::Out_applyResourceChange AssetsManager::applyResourceChange(const // Ресолвим текстуры std::variant model = _model; - std::visit([&lock](auto& val) { + std::visit([&lock, &domain](auto& val) { for(const auto& [key, pipeline] : val.Textures) { TexturePipeline pipe; - pipe.Pipeline = pipeline.Pipeline; + if(pipeline.IsSource) { + std::string source(reinterpret_cast(pipeline.Pipeline.data()), pipeline.Pipeline.size()); + TexturePipelineProgram program; + std::string err; + if(!program.compile(source, &err)) { + MAKE_ERROR("Ошибка компиляции pipeline: " << err); + } + + auto resolver = [&](std::string_view name) -> std::optional { + auto [texDomain, texKey] = parseDomainKey(std::string(name), domain); + return lock->getId(EnumAssets::Texture, texDomain, texKey); + }; + + if(!program.link(resolver, &err)) { + MAKE_ERROR("Ошибка линковки pipeline: " << err); + } + + const std::vector bytes = program.toBytes(); + pipe.Pipeline.resize(bytes.size()); + if(!bytes.empty()) { + std::memcpy(pipe.Pipeline.data(), bytes.data(), bytes.size()); + } + } else { + pipe.Pipeline = pipeline.Pipeline; + } for(const auto& [domain, key] : pipeline.Assets) { ResourceId texId = lock->getId(EnumAssets::Texture, domain, key); diff --git a/Src/Server/ContentManager.cpp b/Src/Server/ContentManager.cpp index 9d63d77..eac0961 100644 --- a/Src/Server/ContentManager.cpp +++ b/Src/Server/ContentManager.cpp @@ -80,6 +80,16 @@ void ContentManager::registerBase_World(ResourceId id, const std::string& domain world.emplace(); } +void ContentManager::registerBase_Entity(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile) { + std::optional& entity = getEntry_Entity(id); + if(!entity) + entity.emplace(); + + DefEntity& def = *entity; + def.Domain = domain; + def.Key = key; +} + void ContentManager::registerBase(EnumDefContent type, const std::string& domain, const std::string& key, const sol::table& profile) { ResourceId id = getId(type, domain, key); @@ -89,6 +99,8 @@ void ContentManager::registerBase(EnumDefContent type, const std::string& domain registerBase_Node(id, domain, key, profile); else if(type == EnumDefContent::World) registerBase_World(id, domain, key, profile); + else if(type == EnumDefContent::Entity) + registerBase_Entity(id, domain, key, profile); else MAKE_ERROR("Не реализовано"); } diff --git a/Src/Server/ContentManager.hpp b/Src/Server/ContentManager.hpp index e12c581..143486f 100644 --- a/Src/Server/ContentManager.hpp +++ b/Src/Server/ContentManager.hpp @@ -132,6 +132,7 @@ class ContentManager { void registerBase_Node(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile); void registerBase_World(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile); + void registerBase_Entity(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile); public: ContentManager(AssetsManager &am); @@ -208,6 +209,10 @@ public: return std::nullopt; } + ResourceId getContentId(EnumDefContent type, const std::string& domain, const std::string& key) { + return getId(type, domain, key); + } + private: TOS::Logger LOG = "Server>ContentManager"; AssetsManager& AM; diff --git a/Src/Server/GameServer.cpp b/Src/Server/GameServer.cpp index 1231b76..9881da7 100644 --- a/Src/Server/GameServer.cpp +++ b/Src/Server/GameServer.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -853,31 +854,159 @@ void GameServer::BackingAsyncLua_t::run(int id) { lock->pop(); } - //if(key.RegionPos == Pos::GlobalRegion(0, 0, 0)) + out.Voxels.clear(); + out.Entityes.clear(); + { - float *ptr = noise.data(); - for(int z = 0; z < 64; z++) - for(int y = 0; y < 64; y++) - for(int x = 0; x < 64; x++, ptr++) { - DefVoxelId id = std::clamp(*ptr, 0.f, 1.f) * 3; //> 0.9 ? 1 : 0; + constexpr DefNodeId kNodeAir = 0; + constexpr DefNodeId kNodeGrass = 2; + constexpr uint8_t kMetaGrass = 1; + constexpr DefNodeId kNodeDirt = 3; + constexpr DefNodeId kNodeStone = 4; + constexpr DefNodeId kNodeWood = 1; + constexpr DefNodeId kNodeLeaves = 5; + constexpr DefNodeId kNodeLava = 7; + constexpr DefNodeId kNodeWater = 8; + constexpr DefNodeId kNodeFire = 9; + + auto hash32 = [](uint32_t x) { + x ^= x >> 16; + x *= 0x7feb352dU; + x ^= x >> 15; + x *= 0x846ca68bU; + x ^= x >> 16; + return x; + }; + + Pos::GlobalNode regionBase = key.RegionPos; + regionBase <<= 6; + + std::array heights; + for(int z = 0; z < 64; z++) { + for(int x = 0; x < 64; x++) { + int32_t gx = regionBase.x + x; + int32_t gz = regionBase.z + z; + float fx = float(gx); + float fz = float(gz); + + float base = glm::perlin(glm::vec2(fx * 0.005f, fz * 0.005f)); + float detail = glm::perlin(glm::vec2(fx * 0.02f, fz * 0.02f)) * 0.35f; + float ridge = glm::perlin(glm::vec2(fx * 0.0015f, fz * 0.0015f)); + float ridged = 1.f - std::abs(ridge); + float mountains = ridged * ridged; + float noiseDetail = noise[(z * 64) + x]; + + float height = 18.f + (base + detail) * 8.f + mountains * 32.f + noiseDetail * 3.f; + int h = std::clamp(int(height + 0.5f), -256, 256); + heights[z * 64 + x] = h; + } + } + + for(int z = 0; z < 64; z++) { + for(int x = 0; x < 64; x++) { + int surface = heights[z * 64 + x]; + int32_t gx = regionBase.x + x; + int32_t gz = regionBase.z + z; + uint32_t seed = hash32(uint32_t(gx) * 73856093u ^ uint32_t(gz) * 19349663u); + + for(int y = 0; y < 64; y++) { + int32_t gy = regionBase.y + y; Pos::bvec64u nodePos(x, y, z); auto &node = out.Nodes[Pos::bvec4u(nodePos >> 4).pack()][Pos::bvec16u(nodePos & 0xf).pack()]; - node.NodeId = id; - - if(x == 0 && z == 0) - node.NodeId = 1; - else if(y == 0 && z == 0) - node.NodeId = 2; - else if(x == 0 && y == 0) - node.NodeId = 3; - if(y == 1 && z == 0) - node.NodeId = 0; - else if(x == 0 && y == 1) - node.NodeId = 0; - - node.Meta = uint8_t((x + y + z + int(node.NodeId)) & 0x3); + if(gy <= surface) { + if(gy == surface) { + node.NodeId = kNodeGrass; + node.Meta = kMetaGrass; + } else if(gy >= surface - 3) { + node.NodeId = kNodeDirt; + node.Meta = uint8_t((seed + gy) & 0x3); + } else { + node.NodeId = kNodeStone; + node.Meta = uint8_t((seed + gy + 1) & 0x3); + } + } else { + node.Data = kNodeAir; + } } + } + } + + auto setNode = [&](int x, int y, int z, DefNodeId id, uint8_t meta, bool onlyAir) { + if(x < 0 || x >= 64 || y < 0 || y >= 64 || z < 0 || z >= 64) + return; + + Pos::bvec64u nodePos(x, y, z); + auto &node = out.Nodes[Pos::bvec4u(nodePos >> 4).pack()][Pos::bvec16u(nodePos & 0xf).pack()]; + if(onlyAir && node.Data != 0) + return; + + node.NodeId = id; + node.Meta = meta; + }; + + for(int z = 1; z < 63; z++) { + for(int x = 1; x < 63; x++) { + int surface = heights[z * 64 + x]; + int localY = surface - regionBase.y; + if(localY < 1 || localY >= 63) + continue; + + int32_t gx = regionBase.x + x; + int32_t gz = regionBase.z + z; + uint32_t seed = hash32(uint32_t(gx) * 83492791u ^ uint32_t(gz) * 2971215073u); + + int treeHeight = 4 + int(seed % 3); + if(localY + treeHeight + 2 >= 64) + continue; + + if((seed % 97) >= 2) + continue; + + int diff = surface - heights[z * 64 + (x - 1)]; + if(diff > 2 || diff < -2) + continue; + diff = surface - heights[z * 64 + (x + 1)]; + if(diff > 2 || diff < -2) + continue; + diff = surface - heights[(z - 1) * 64 + x]; + if(diff > 2 || diff < -2) + continue; + diff = surface - heights[(z + 1) * 64 + x]; + if(diff > 2 || diff < -2) + continue; + + uint8_t woodMeta = uint8_t((seed >> 2) & 0x3); + uint8_t leafMeta = uint8_t((seed >> 4) & 0x3); + + for(int i = 1; i <= treeHeight; i++) { + setNode(x, localY + i, z, kNodeWood, woodMeta, false); + } + + int topY = localY + treeHeight; + for(int dy = -2; dy <= 2; dy++) { + for(int dz = -2; dz <= 2; dz++) { + for(int dx = -2; dx <= 2; dx++) { + int dist2 = dx * dx + dz * dz + dy * dy; + if(dist2 > 5) + continue; + + setNode(x + dx, topY + dy, z + dz, kNodeLeaves, leafMeta, true); + } + } + } + } + } + + if(regionBase.x == 0 && regionBase.z == 0) { + constexpr int kTestGlobalY = 64; + if(regionBase.y <= kTestGlobalY && (regionBase.y + 63) >= kTestGlobalY) { + int localY = kTestGlobalY - regionBase.y; + setNode(2, localY, 2, kNodeLava, 0, false); + setNode(4, localY, 2, kNodeWater, 0, false); + setNode(6, localY, 2, kNodeFire, 0, false); + } + } } // else { // Node *ptr = (Node*) &out.Nodes[0][0]; @@ -1447,6 +1576,8 @@ void GameServer::init(fs::path worldPath) { sol::table t = LuaMainState.create_table(); // Content.CM.registerBase(EnumDefContent::Node, "core", "none", t); Content.CM.registerBase(EnumDefContent::World, "test", "devel_world", t); + Content.CM.registerBase(EnumDefContent::Entity, "core", "player", t); + PlayerEntityDefId = Content.CM.getContentId(EnumDefContent::Entity, "core", "player"); } initLuaPre(); @@ -1706,7 +1837,27 @@ void GameServer::stepConnections() { auto wIter = Expanse.Worlds.find(wPair.first); assert(wIter != Expanse.Worlds.end()); - wIter->second->onRemoteClient_RegionsLost(cec, wPair.second); + wIter->second->onRemoteClient_RegionsLost(wPair.first, cec, wPair.second); + } + + if(cec->PlayerEntity) { + ServerEntityId_t entityId = *cec->PlayerEntity; + auto [worldId, regionPos, entityIndex] = entityId; + auto iterWorld = Expanse.Worlds.find(worldId); + if(iterWorld != Expanse.Worlds.end()) { + auto iterRegion = iterWorld->second->Regions.find(regionPos); + if(iterRegion != iterWorld->second->Regions.end()) { + Region& region = *iterRegion->second; + if(entityIndex < region.Entityes.size()) + region.Entityes[entityIndex].IsRemoved = true; + + std::vector removed = {entityId}; + for(const std::shared_ptr& observer : region.RMs) { + observer->prepareEntitiesRemove(removed); + } + } + } + cec->clearPlayerEntity(); } std::string username = cec->Username; @@ -1811,7 +1962,7 @@ IWorldSaveBackend::TickSyncInfo_Out GameServer::stepDatabaseSync() { auto iterWorld = Expanse.Worlds.find(worldId); assert(iterWorld != Expanse.Worlds.end()); - std::vector notLoaded = iterWorld->second->onRemoteClient_RegionsEnter(remoteClient, regions); + std::vector notLoaded = iterWorld->second->onRemoteClient_RegionsEnter(worldId, remoteClient, regions); if(!notLoaded.empty()) { // Добавляем к списку на загрузку std::vector &tl = toDB.Load[worldId]; @@ -1824,7 +1975,7 @@ IWorldSaveBackend::TickSyncInfo_Out GameServer::stepDatabaseSync() { auto iterWorld = Expanse.Worlds.find(worldId); assert(iterWorld != Expanse.Worlds.end()); - iterWorld->second->onRemoteClient_RegionsLost(remoteClient, regions); + iterWorld->second->onRemoteClient_RegionsLost(worldId, remoteClient, regions); } } } @@ -1929,13 +2080,116 @@ void GameServer::stepGeneratorAndLuaAsync(IWorldSaveBackend::TickSyncInfo_Out db iterWorld->second->pushRegions(std::move(regions)); for(auto& [cec, poses] : toSubscribe) { - iterWorld->second->onRemoteClient_RegionsEnter(cec, poses); + iterWorld->second->onRemoteClient_RegionsEnter(worldId, cec, poses); } } } void GameServer::stepPlayerProceed() { + auto iterWorld = Expanse.Worlds.find(0); + if(iterWorld == Expanse.Worlds.end()) + return; + World& world = *iterWorld->second; + + for(std::shared_ptr& remoteClient : Game.RemoteClients) { + if(!remoteClient) + continue; + + Pos::Object pos = remoteClient->CameraPos; + Pos::GlobalRegion regionPos = Pos::Object_t::asRegionsPos(pos); + glm::quat quat = remoteClient->CameraQuat.toQuat(); + + if(!remoteClient->PlayerEntity) { + auto iterRegion = world.Regions.find(regionPos); + if(iterRegion == world.Regions.end()) + continue; + + Entity entity(PlayerEntityDefId); + entity.WorldId = iterWorld->first; + entity.Pos = pos; + entity.Quat = quat; + entity.InRegionPos = regionPos; + + Region& region = *iterRegion->second; + RegionEntityId_t entityIndex = region.pushEntity(entity); + if(entityIndex == RegionEntityId_t(-1)) + continue; + + ServerEntityId_t entityId = {iterWorld->first, regionPos, entityIndex}; + remoteClient->setPlayerEntity(entityId); + + std::vector> updates; + updates.emplace_back(entityId, ®ion.Entityes[entityIndex]); + for(const std::shared_ptr& observer : region.RMs) { + observer->prepareEntitiesUpdate(updates); + } + + continue; + } + + ServerEntityId_t entityId = *remoteClient->PlayerEntity; + auto [worldId, prevRegion, entityIndex] = entityId; + auto iterRegion = world.Regions.find(prevRegion); + if(iterRegion == world.Regions.end()) { + remoteClient->clearPlayerEntity(); + continue; + } + + Region& region = *iterRegion->second; + if(entityIndex >= region.Entityes.size() || region.Entityes[entityIndex].IsRemoved) { + remoteClient->clearPlayerEntity(); + continue; + } + + Entity& entity = region.Entityes[entityIndex]; + Pos::GlobalRegion nextRegion = Pos::Object_t::asRegionsPos(pos); + if(nextRegion != prevRegion) { + entity.IsRemoved = true; + std::vector removed = {entityId}; + for(const std::shared_ptr& observer : region.RMs) { + observer->prepareEntitiesRemove(removed); + } + + remoteClient->clearPlayerEntity(); + + auto iterNewRegion = world.Regions.find(nextRegion); + if(iterNewRegion == world.Regions.end()) + continue; + + Entity nextEntity(PlayerEntityDefId); + nextEntity.WorldId = iterWorld->first; + nextEntity.Pos = pos; + nextEntity.Quat = quat; + nextEntity.InRegionPos = nextRegion; + + Region& newRegion = *iterNewRegion->second; + RegionEntityId_t nextIndex = newRegion.pushEntity(nextEntity); + if(nextIndex == RegionEntityId_t(-1)) + continue; + + ServerEntityId_t nextId = {iterWorld->first, nextRegion, nextIndex}; + remoteClient->setPlayerEntity(nextId); + + std::vector> updates; + updates.emplace_back(nextId, &newRegion.Entityes[nextIndex]); + for(const std::shared_ptr& observer : newRegion.RMs) { + observer->prepareEntitiesUpdate(updates); + } + continue; + } + + entity.Pos = pos; + entity.Quat = quat; + entity.WorldId = iterWorld->first; + entity.InRegionPos = prevRegion; + + std::vector> updates; + updates.emplace_back(entityId, &entity); + for(const std::shared_ptr& observer : region.RMs) { + observer->prepareEntitiesUpdate(updates); + } + } } void GameServer::stepWorldPhysic() { diff --git a/Src/Server/GameServer.hpp b/Src/Server/GameServer.hpp index 7e59eec..c721061 100644 --- a/Src/Server/GameServer.hpp +++ b/Src/Server/GameServer.hpp @@ -267,6 +267,7 @@ class GameServer : public AsyncObject { // Идентификатор текущегго мода, находящевося в обработке std::string CurrentModId; AssetsManager::AssetsRegister AssetsInit; + DefEntityId PlayerEntityDefId = 0; public: GameServer(asio::io_context &ioc, fs::path worldPath); diff --git a/Src/Server/RemoteClient.cpp b/Src/Server/RemoteClient.cpp index 0a6b723..06900d9 100644 --- a/Src/Server/RemoteClient.cpp +++ b/Src/Server/RemoteClient.cpp @@ -366,10 +366,37 @@ void RemoteClient::NetworkAndResource_t::prepareEntitiesUpdate(const std::vector } } - // TODO: отправить клиенту + ResUses_t::RefEntity_t refEntity; + refEntity.Profile = entity->getDefId(); + if(!ResUses.RefDefEntity.contains(refEntity.Profile)) + ResUses.RefDefEntity[refEntity.Profile] = {}; + + checkPacketBorder(32); + NextPacket << (uint8_t) ToClient::L1::Content + << (uint8_t) ToClient::L2Content::Entity + << ceId + << (uint32_t) refEntity.Profile + << (uint32_t) entity->WorldId + << entity->Pos.x + << entity->Pos.y + << entity->Pos.z; + + { + ToServer::PacketQuat q; + q.fromQuat(entity->Quat); + for(int iter = 0; iter < 5; iter++) + NextPacket << q.Data[iter]; + } + + ResUses.RefEntity[entityId] = std::move(refEntity); } } +void RemoteClient::NetworkAndResource_t::prepareEntitiesUpdate_Dynamic(const std::vector>& entities) +{ + prepareEntitiesUpdate(entities); +} + void RemoteClient::NetworkAndResource_t::prepareEntitySwap(ServerEntityId_t prev, ServerEntityId_t next) { ReMapEntities.rebindClientKey(prev, next); @@ -400,6 +427,8 @@ void RemoteClient::NetworkAndResource_t::prepareEntitiesRemove(const std::vector ResUses.RefDefEntity.erase(iterProfileRef); ResUses.DefEntity.erase(iterProfile); } + + ResUses.RefEntity.erase(iterEntity); } checkPacketBorder(16); @@ -489,7 +518,12 @@ void RemoteClient::NetworkAndResource_t::prepareWorldRemove(WorldId_t worldId) // void RemoteClient::NetworkAndResource_t::preparePortalRemove(PortalId portalId) {} void RemoteClient::prepareCameraSetEntity(ServerEntityId_t entityId) { - + auto lock = NetworkAndResource.lock(); + ClientEntityId_t cId = lock->ReMapEntities.toClient(entityId); + lock->checkPacketBorder(8); + lock->NextPacket << (uint8_t) ToClient::L1::System + << (uint8_t) ToClient::L2System::LinkCameraToEntity + << cId; } ResourceRequest RemoteClient::pushPreparedPackets() { @@ -679,15 +713,19 @@ void RemoteClient::NetworkAndResource_t::informateDefPortal(const std::vector>& entityes) { - // for(auto pair : entityes) { - // DefEntityId_t id = pair.first; - // if(!ResUses.DefEntity.contains(id)) - // continue; + for(auto pair : entityes) { + DefEntityId id = pair.first; + if(!ResUses.DefEntity.contains(id)) + continue; - // NextPacket << (uint8_t) ToClient::L1::Definition - // << (uint8_t) ToClient::L2Definition::Entity - // << id; - // } + checkPacketBorder(8); + NextPacket << (uint8_t) ToClient::L1::Definition + << (uint8_t) ToClient::L2Definition::Entity + << id; + + if(!ResUses.RefDefEntity.contains(id)) + ResUses.RefDefEntity[id] = {}; + } } void RemoteClient::NetworkAndResource_t::informateDefItem(const std::vector>& items) diff --git a/Src/Server/RemoteClient.hpp b/Src/Server/RemoteClient.hpp index 9ea0102..cac0c8d 100644 --- a/Src/Server/RemoteClient.hpp +++ b/Src/Server/RemoteClient.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -336,6 +337,7 @@ public: // Если игрок пересекал границы региона (для перерасчёта ContentViewState) bool CrossedRegion = true; std::queue Build, Break; + std::optional PlayerEntity; public: RemoteClient(asio::io_context &ioc, tcp::socket socket, const std::string username, GameServer* server) @@ -347,6 +349,9 @@ public: coro<> run(); void shutdown(EnumDisconnect type, const std::string reason); bool isConnected() { return IsConnected; } + void setPlayerEntity(ServerEntityId_t id) { PlayerEntity = id; } + std::optional getPlayerEntity() const { return PlayerEntity; } + void clearPlayerEntity() { PlayerEntity.reset(); } void pushPackets(std::vector *simplePackets, std::vector *smartPackets = nullptr) { if(IsGoingShutdown) diff --git a/Src/Server/World.cpp b/Src/Server/World.cpp index fe09ee7..6b518d8 100644 --- a/Src/Server/World.cpp +++ b/Src/Server/World.cpp @@ -16,7 +16,7 @@ World::~World() { } -std::vector World::onRemoteClient_RegionsEnter(std::shared_ptr cec, const std::vector& enter) { +std::vector World::onRemoteClient_RegionsEnter(WorldId_t worldId, std::shared_ptr cec, const std::vector& enter) { std::vector out; for(const Pos::GlobalRegion &pos : enter) { @@ -43,18 +43,49 @@ std::vector World::onRemoteClient_RegionsEnter(std::shared_pt nodes[Pos::bvec4u(x, y, z)] = region.Nodes[Pos::bvec4u(x, y, z).pack()].data(); } - + if(!region.Entityes.empty()) { + std::vector> updates; + updates.reserve(region.Entityes.size()); + + for(size_t iter = 0; iter < region.Entityes.size(); iter++) { + const Entity& entity = region.Entityes[iter]; + if(entity.IsRemoved) + continue; + + ServerEntityId_t entityId = {worldId, pos, static_cast(iter)}; + updates.emplace_back(entityId, &entity); + } + + if(!updates.empty()) + cec->prepareEntitiesUpdate(updates); + } } return out; } -void World::onRemoteClient_RegionsLost(std::shared_ptr cec, const std::vector &lost) { +void World::onRemoteClient_RegionsLost(WorldId_t worldId, std::shared_ptr cec, const std::vector &lost) { for(const Pos::GlobalRegion &pos : lost) { auto region = Regions.find(pos); if(region == Regions.end()) continue; + if(!region->second->Entityes.empty()) { + std::vector removed; + removed.reserve(region->second->Entityes.size()); + + for(size_t iter = 0; iter < region->second->Entityes.size(); iter++) { + const Entity& entity = region->second->Entityes[iter]; + if(entity.IsRemoved) + continue; + + removed.emplace_back(worldId, pos, static_cast(iter)); + } + + if(!removed.empty()) + cec->prepareEntitiesRemove(removed); + } + std::vector> &CECs = region->second->RMs; for(size_t iter = 0; iter < CECs.size(); iter++) { if(CECs[iter] == cec) { @@ -74,6 +105,7 @@ void World::pushRegions(std::vector> regi Region ®ion = *(Regions[key] = std::make_unique()); region.Voxels = std::move(value.Voxels); region.Nodes = value.Nodes; + region.Entityes = std::move(value.Entityes); } } @@ -81,4 +113,4 @@ void World::onUpdate(GameServer *server, float dtime) { } -} \ No newline at end of file +} diff --git a/Src/Server/World.hpp b/Src/Server/World.hpp index 26bb496..1f23114 100644 --- a/Src/Server/World.hpp +++ b/Src/Server/World.hpp @@ -146,8 +146,8 @@ public: Возвращает список не загруженных регионов, на которые соответственно игрока не получилось подписать При подписи происходит отправка всех чанков и сущностей региона */ - std::vector onRemoteClient_RegionsEnter(std::shared_ptr cec, const std::vector &enter); - void onRemoteClient_RegionsLost(std::shared_ptr cec, const std::vector& lost); + std::vector onRemoteClient_RegionsEnter(WorldId_t worldId, std::shared_ptr cec, const std::vector &enter); + void onRemoteClient_RegionsLost(WorldId_t worldId, std::shared_ptr cec, const std::vector& lost); struct SaveUnloadInfo { std::vector ToUnload; std::vector> ToSave; @@ -176,4 +176,4 @@ public: -} \ No newline at end of file +} diff --git a/Src/main.cpp b/Src/main.cpp index 658dbb0..0f4a692 100644 --- a/Src/main.cpp +++ b/Src/main.cpp @@ -1,8 +1,11 @@ #include "Common/Abstract.hpp" +#include "boost/asio/awaitable.hpp" +#include #include #include #include #include +#include namespace LV { @@ -37,6 +40,4 @@ int main() { std::cout << "Hello world!" << std::endl; return LV::main(); - - return 0; } diff --git a/assets/shaders/chunk/node_opaque.frag b/assets/shaders/chunk/node_opaque.frag index 0bb5042..70bc08d 100644 --- a/assets/shaders/chunk/node_opaque.frag +++ b/assets/shaders/chunk/node_opaque.frag @@ -1,6 +1,6 @@ #version 460 -// layout(early_fragment_tests) in; +layout(early_fragment_tests) in; layout(location = 0) in FragmentObj { vec3 GeoPos; // Реальная позиция в мире diff --git a/assets/shaders/chunk/node_opaque.frag.bin b/assets/shaders/chunk/node_opaque.frag.bin index 007905cb453f3e2afae90de63890e8746e00385e..ef897bccce4d5550deba4b8178615e4ae003367c 100644 GIT binary patch delta 20 bcmdm@ctCMN4W|GDGXo0)0|V#AN^1cCHvt4; delta 10 RcmX@0xJ7Y7&BhK30RS1|1VjJ; diff --git a/assets/shaders/chunk/node_transparent.frag b/assets/shaders/chunk/node_transparent.frag index f74d24e..91c78ba 100644 --- a/assets/shaders/chunk/node_transparent.frag +++ b/assets/shaders/chunk/node_transparent.frag @@ -44,4 +44,7 @@ vec4 atlasColor(uint texId, vec2 uv) void main() { Frame = atlasColor(Fragment.Texture, Fragment.UV); Frame.xyz *= max(0.2f, dot(Fragment.Normal, normalize(vec3(0.5, 1, 0.8)))); + + if(Frame.w == 0) + discard; } diff --git a/assets/shaders/chunk/node_transparent.frag.bin b/assets/shaders/chunk/node_transparent.frag.bin index e7259ae1e404c96e32c09bf82a96db8a80693230..007905cb453f3e2afae90de63890e8746e00385e 100644 GIT binary patch delta 367 zcmXw!KTASk7{#A^-(D^3P9X$gX<_-4;JWT*tnX zV^I$Mj@bi;^PKbid*6@ztPr{IAZ^}edLIN!?M$N~IH&f%W_zhU%S(PM7o+58 zTDi9UO{0^V@43?>N-69u{jkf-(vZvuJhm5!6*#!qm2%;TSs1JD9m rS*2wi9s>C@dJf^y%B#SioEXE3Z~9ex2J*i6q4EXqRz6O0nLYmtDuFXV delta 249 zcmdm@xJQAPnMs+Qft8Vgn}L^M>O|gX)`<)Z3{y7dzGo5zv3xSqGfI3D3xG@pRt9#4 zNs|kir9~%!xIo~XpOaq%q*)o*7^ZAq$Q;aQ3(}+rk^=&F1{MYoUkQjA!F*;0kX9xj z_5jMkjFABIS%HQdvI@&i0rEg1Al)D~$TWy/ + nodestate/ *.json + model/ *.json | *.gltf | *.glb + texture/ *.png | *.jpg (jpeg) + particle/ (загрузка из файлов пока не реализована) + animation/ (загрузка из файлов пока не реализована) + sound/ (загрузка из файлов пока не реализована) + font/ (загрузка из файлов пока не реализована) +``` + +Пример: `assets/core/nodestate/stone.json` имеет `domain=core`, `key=stone.json`. +При обращении к nodestate из логики нод используется ключ без суффикса `.json` +(сервер дописывает расширение автоматически). + +## Nodestate (JSON) +Файл nodestate — это JSON-объект, где ключи — условия, а значения — описание модели +или список вариантов моделей. + +### Условия +Условие — строковое выражение. Поддерживаются: +- числа, `true`, `false` +- переменные: `state` или `state:value` (двоеточие — часть имени) +- операторы: `+ - * / %`, `!`, `&`, `|`, `< <= > >= == !=` +- скобки + +Пустая строка условия трактуется как `true`. + +### Формат варианта модели +Объект варианта: +- `model`: строка `domain:key` **или** массив объектов моделей +- `weight`: число (вес при случайном выборе), по умолчанию `1` +- `uvlock`: bool (используется для векторных моделей; для одиночной модели игнорируется) +- `transformations`: массив строк `"key=value"` для трансформаций + +Если `model` — строка, это одиночная модель. +Если `model` — массив, это векторная модель: набор объектов вида: +``` +{ "model": "domain:key", "uvlock": false, "transformations": ["x=0", "ry=1.57"] } +``` +Для векторной модели также могут задаваться `uvlock` и `transformations` на верхнем уровне +(они применяются к группе). + +Трансформации поддерживают ключи: +`x`, `y`, `z`, `rx`, `ry`, `rz` (сдвиг и поворот). + +Домен в строке `domain:key` можно опустить — тогда используется домен файла nodestate. + +### Пример +```json +{ + "": { "model": "core:stone" }, + "variant == 1": [ + { "model": "core:stone_alt", "weight": 2 }, + { "model": "core:stone_alt_2", "weight": 1, "transformations": ["ry=1.57"] } + ], + "facing:north": { + "model": [ + { "model": "core:stone", "transformations": ["ry=3.14"] }, + { "model": "core:stone_detail", "transformations": ["x=0.5"] } + ], + "uvlock": true + } +} +``` + +## Model (JSON) +Формат описывает геометрию и текстуры. + +### Верхний уровень +- `gui_light`: строка (сейчас используется только `default`) +- `ambient_occlusion`: bool +- `display`: объект с наборами `rotation`/`translation`/`scale` (все — массивы из 3 чисел) +- `textures`: объект `name -> string` (ссылка на текстуру или pipeline) +- `cuboids`: массив геометрических блоков +- `sub_models`: массив подмоделей + +### Текстуры +В `textures` значение: +- либо строка `domain:key` (прямая ссылка на текстуру), +- либо pipeline-строка, начинающаяся с `tex` (компилируется `TexturePipelineProgram`). + +Если домен не указан, используется домен файла модели. + +### Cuboids +Элемент `cuboids`: +- `shade`: bool (по умолчанию `true`) +- `from`: `[x, y, z]` +- `to`: `[x, y, z]` +- `faces`: объект граней (`down|up|north|south|west|east`) +- `transformations`: массив `"key=value"` (ключи как у nodestate) + +Грань (`faces.`) может содержать: +- `uv`: `[u0, v0, u1, v1]` +- `texture`: строка (ключ из `textures`) +- `cullface`: `down|up|north|south|west|east` +- `tintindex`: int +- `rotation`: int16 + +### Sub-models +`sub_models` допускает: +- строку `domain:key` +- объект `{ "model": "domain:key", "scene": 0 }` +- объект `{ "path": "domain:key", "scene": 0 }` + +Поле `scene` опционально. + +### Пример +```json +{ + "ambient_occlusion": true, + "textures": { + "all": "core:stone" + }, + "cuboids": [ + { + "from": [0, 0, 0], + "to": [16, 16, 16], + "faces": { + "north": { "uv": [0, 0, 16, 16], "texture": "#all" } + } + } + ], + "sub_models": [ + "core:stone_detail", + { "model": "core:stone_variant", "scene": 1 } + ] +} +``` + +## Model (glTF / GLB) +Файлы моделей могут быть: +- `.gltf` (JSON glTF) +- `.glb` (binary glTF) + +Оба формата конвертируются в `PreparedGLTF`. + +## Texture +Поддерживаются только PNG и JPEG. +Формат определяется по сигнатуре файла. + +## Прочие типы ресурсов +Для `particle`, `animation`, `sound`, `font` загрузка из файловой системы +в серверном загрузчике пока не реализована (`std::unreachable()`), но возможна +регистрация из Lua через `path` (сырые бинарные данные).