From 4aa7c6f41a369d42789dd52b0101c2aa02d50bb0 Mon Sep 17 00:00:00 2001 From: DrSocalkwe3n Date: Thu, 1 Jan 2026 02:13:01 +0600 Subject: [PATCH] =?UTF-8?q?codex-5.2:=20=D1=80=D0=B5=D1=81=D1=83=D1=80?= =?UTF-8?q?=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Src/Client/Vulkan/Abstract.hpp | 14 +- .../AtlasPipeline/PipelinedTextureAtlas.cpp | 198 +++ .../AtlasPipeline/PipelinedTextureAtlas.hpp | 380 +++++ .../AtlasPipeline/SharedStagingBuffer.hpp | 169 ++ .../Vulkan/AtlasPipeline/TextureAtlas.cpp | 477 ++++++ .../Vulkan/AtlasPipeline/TextureAtlas.hpp | 1394 +++++++++++++++++ .../AtlasPipeline/TexturePipelineProgram.hpp | 1271 +++++++++++++++ Src/Client/Vulkan/VertexPool.hpp | 215 +-- Src/Client/Vulkan/Vulkan.cpp | 10 +- Src/Client/Vulkan/VulkanRenderSession.cpp | 625 +++++--- Src/Client/Vulkan/VulkanRenderSession.hpp | 705 ++++----- Src/Common/Abstract.cpp | 55 +- Src/Server/AssetsManager.cpp | 8 +- Src/Server/AssetsManager.hpp | 6 +- Src/Server/GameServer.cpp | 20 +- assets/shaders/chunk/node.vert | 24 +- assets/shaders/chunk/node.vert.bin | Bin 2600 -> 2784 bytes assets/shaders/chunk/node_opaque.frag | 65 +- assets/shaders/chunk/node_opaque.frag.bin | Bin 6812 -> 4276 bytes assets/shaders/chunk/node_transparent.frag | 29 +- .../shaders/chunk/node_transparent.frag.bin | Bin 1448 -> 4156 bytes assets/shaders/chunk/voxel_opaque.frag | 65 +- assets/shaders/chunk/voxel_opaque.frag.bin | Bin 7188 -> 4636 bytes assets/shaders/chunk/voxel_transparent.frag | 49 +- .../shaders/chunk/voxel_transparent.frag.bin | Bin 1288 -> 4604 bytes .../assets/test/model/node/acacia_planks.json | 81 + mods/test/assets/test/model/node/frame.json | 81 + mods/test/assets/test/model/node/grass.json | 81 + .../assets/test/model/node/jungle_planks.json | 81 + .../assets/test/model/node/oak_planks.json | 81 + .../model/node/tropical_rainforest_wood.json | 81 + .../assets/test/model/node/willow_wood.json | 81 + .../test/model/node/xnether_blue_wood.json | 81 + .../test/model/node/xnether_purple_wood.json | 81 + mods/test/assets/test/nodestate/test0.json | 14 + mods/test/assets/test/nodestate/test1.json | 14 + mods/test/assets/test/nodestate/test2.json | 14 + mods/test/assets/test/nodestate/test3.json | 14 + mods/test/assets/test/nodestate/test4.json | 14 + mods/test/assets/test/nodestate/test5.json | 14 + mods/test/assets/test/texture/0.png | Bin 0 -> 14212 bytes .../assets/test/texture/acacia_planks.png | Bin 0 -> 3800 bytes mods/test/assets/test/texture/frame.png | Bin 0 -> 138 bytes mods/test/assets/test/texture/grass.png | Bin 0 -> 10253 bytes .../assets/test/texture/jungle_planks.png | Bin 0 -> 15990 bytes mods/test/assets/test/texture/oak_planks.png | Bin 0 -> 15180 bytes .../test/texture/tropical_rainforest_wood.png | Bin 0 -> 1453 bytes mods/test/assets/test/texture/willow_wood.png | Bin 0 -> 2089 bytes .../assets/test/texture/xnether_blue_wood.png | Bin 0 -> 6101 bytes .../test/texture/xnether_purple_wood.png | Bin 0 -> 5940 bytes mods/test/init.lua | 98 ++ mods/test/mod.json | 9 + 52 files changed, 5787 insertions(+), 912 deletions(-) create mode 100644 Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.cpp create mode 100644 Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.hpp create mode 100644 Src/Client/Vulkan/AtlasPipeline/SharedStagingBuffer.hpp create mode 100644 Src/Client/Vulkan/AtlasPipeline/TextureAtlas.cpp create mode 100644 Src/Client/Vulkan/AtlasPipeline/TextureAtlas.hpp create mode 100644 Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp create mode 100644 mods/test/assets/test/model/node/acacia_planks.json create mode 100644 mods/test/assets/test/model/node/frame.json create mode 100644 mods/test/assets/test/model/node/grass.json create mode 100644 mods/test/assets/test/model/node/jungle_planks.json create mode 100644 mods/test/assets/test/model/node/oak_planks.json create mode 100644 mods/test/assets/test/model/node/tropical_rainforest_wood.json create mode 100644 mods/test/assets/test/model/node/willow_wood.json create mode 100644 mods/test/assets/test/model/node/xnether_blue_wood.json create mode 100644 mods/test/assets/test/model/node/xnether_purple_wood.json create mode 100644 mods/test/assets/test/nodestate/test0.json create mode 100644 mods/test/assets/test/nodestate/test1.json create mode 100644 mods/test/assets/test/nodestate/test2.json create mode 100644 mods/test/assets/test/nodestate/test3.json create mode 100644 mods/test/assets/test/nodestate/test4.json create mode 100644 mods/test/assets/test/nodestate/test5.json create mode 100644 mods/test/assets/test/texture/0.png create mode 100644 mods/test/assets/test/texture/acacia_planks.png create mode 100644 mods/test/assets/test/texture/frame.png create mode 100644 mods/test/assets/test/texture/grass.png create mode 100644 mods/test/assets/test/texture/jungle_planks.png create mode 100644 mods/test/assets/test/texture/oak_planks.png create mode 100644 mods/test/assets/test/texture/tropical_rainforest_wood.png create mode 100644 mods/test/assets/test/texture/willow_wood.png create mode 100644 mods/test/assets/test/texture/xnether_blue_wood.png create mode 100644 mods/test/assets/test/texture/xnether_purple_wood.png create mode 100644 mods/test/init.lua create mode 100644 mods/test/mod.json diff --git a/Src/Client/Vulkan/Abstract.hpp b/Src/Client/Vulkan/Abstract.hpp index 906851b..bc384cd 100644 --- a/Src/Client/Vulkan/Abstract.hpp +++ b/Src/Client/Vulkan/Abstract.hpp @@ -35,12 +35,12 @@ struct VoxelVertexPoint { struct NodeVertexStatic { uint32_t - FX : 9, FY : 9, FZ : 9, // Позиция -224 ~ 288; 64 позиций в одной ноде, 7.5 метров в ряд - N1 : 4, // Не занято - LS : 1, // Масштаб карты освещения (1м/16 или 1м) - Tex : 18, // Текстура - N2 : 14, // Не занято - TU : 16, TV : 16; // UV на текстуре + FX : 11, FY : 11, N1 : 10, // Позиция, 64 позиции на метр, +3.5м запас + FZ : 11, // Позиция + LS : 1, // Масштаб карты освещения (1м/16 или 1м) + Tex : 18, // Текстура + N2 : 2, // Не занято + TU : 16, TV : 16; // UV на текстуре bool operator==(const NodeVertexStatic& other) const { return std::memcmp(this, &other, sizeof(*this)) == 0; @@ -49,4 +49,4 @@ struct NodeVertexStatic { bool operator<=>(const NodeVertexStatic&) const = default; }; -} \ No newline at end of file +} diff --git a/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.cpp b/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.cpp new file mode 100644 index 0000000..ebba2d5 --- /dev/null +++ b/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.cpp @@ -0,0 +1,198 @@ +#include "PipelinedTextureAtlas.hpp" + +PipelinedTextureAtlas::PipelinedTextureAtlas(TextureAtlas&& tk) + : Super(std::move(tk)) {} + +PipelinedTextureAtlas::AtlasTextureId PipelinedTextureAtlas::getByPipeline(const HashedPipeline& pipeline) { + auto iter = _PipeToTexId.find(pipeline); + if (iter == _PipeToTexId.end()) { + AtlasTextureId atlasTexId = Super.registerTexture(); + _PipeToTexId.insert({pipeline, atlasTexId}); + _ChangedPipelines.push_back(pipeline); + + for (uint32_t texId : pipeline.getDependencedTextures()) { + _AddictedTextures[texId].push_back(pipeline); + } + + return atlasTexId; + } + + return iter->second; +} + +void PipelinedTextureAtlas::freeByPipeline(const HashedPipeline& pipeline) { + auto iter = _PipeToTexId.find(pipeline); + if (iter == _PipeToTexId.end()) { + return; + } + + for (uint32_t texId : pipeline.getDependencedTextures()) { + auto iterAT = _AddictedTextures.find(texId); + assert(iterAT != _AddictedTextures.end()); + auto iterATSub = std::find(iterAT->second.begin(), iterAT->second.end(), pipeline); + assert(iterATSub != iterAT->second.end()); + iterAT->second.erase(iterATSub); + } + + Super.removeTexture(iter->second); + _AtlasCpuTextures.erase(iter->second); + _PipeToTexId.erase(iter); +} + +void PipelinedTextureAtlas::updateTexture(uint32_t texId, const StoredTexture& texture) { + _ResToTexture[texId] = texture; + _ChangedTextures.push_back(texId); +} + +void PipelinedTextureAtlas::updateTexture(uint32_t texId, StoredTexture&& texture) { + _ResToTexture[texId] = std::move(texture); + _ChangedTextures.push_back(texId); +} + +void PipelinedTextureAtlas::freeTexture(uint32_t texId) { + auto iter = _ResToTexture.find(texId); + if (iter != _ResToTexture.end()) { + _ResToTexture.erase(iter); + } +} + +bool PipelinedTextureAtlas::getHostTexture(TextureId texId, HostTextureView& out) const { + auto fill = [&](const StoredTexture& tex) -> bool { + if (tex._Pixels.empty() || tex._Widht == 0 || tex._Height == 0) { + return false; + } + out.width = tex._Widht; + out.height = tex._Height; + out.rowPitchBytes = static_cast(tex._Widht) * 4u; + out.pixelsRGBA8 = reinterpret_cast(tex._Pixels.data()); + return true; + }; + + auto it = _ResToTexture.find(texId); + if (it != _ResToTexture.end() && fill(it->second)) { + return true; + } + + auto itAtlas = _AtlasCpuTextures.find(texId); + if (itAtlas != _AtlasCpuTextures.end() && fill(itAtlas->second)) { + return true; + } + return false; +} + +StoredTexture PipelinedTextureAtlas::_generatePipelineTexture(const HashedPipeline& pipeline) { + std::vector words(pipeline._Pipeline.begin(), pipeline._Pipeline.end()); + if (words.empty()) { + if (auto tex = tryCopyFirstDependencyTexture(pipeline)) { + return *tex; + } + return makeSolidColorTexture(0xFFFF00FFu); + } + + TexturePipelineProgram program; + program.fromWords(std::move(words)); + + TexturePipelineProgram::OwnedTexture baked; + auto provider = [this](uint32_t texId) -> std::optional { + auto iter = _ResToTexture.find(texId); + if (iter == _ResToTexture.end()) { + return std::nullopt; + } + const StoredTexture& stored = iter->second; + if (stored._Pixels.empty() || stored._Widht == 0 || stored._Height == 0) { + return std::nullopt; + } + Texture tex{}; + tex.Width = stored._Widht; + tex.Height = stored._Height; + tex.Pixels = stored._Pixels.data(); + return tex; + }; + + if (!program.bake(provider, baked, nullptr)) { + if (auto tex = tryCopyFirstDependencyTexture(pipeline)) { + return *tex; + } + return makeSolidColorTexture(0xFFFF00FFu); + } + + const uint32_t width = baked.Width; + const uint32_t height = baked.Height; + if (width == 0 || height == 0 || + width > std::numeric_limits::max() || + height > std::numeric_limits::max() || + baked.Pixels.size() != static_cast(width) * static_cast(height)) { + if (auto tex = tryCopyFirstDependencyTexture(pipeline)) { + return *tex; + } + return makeSolidColorTexture(0xFFFF00FFu); + } + + return StoredTexture(static_cast(width), + static_cast(height), + std::move(baked.Pixels)); +} + +void PipelinedTextureAtlas::flushNewPipelines() { + std::vector changedTextures = std::move(_ChangedTextures); + + std::sort(changedTextures.begin(), changedTextures.end()); + changedTextures.erase(std::unique(changedTextures.begin(), changedTextures.end()), changedTextures.end()); + + std::vector changedPipelineTextures; + for (uint32_t texId : changedTextures) { + auto iter = _AddictedTextures.find(texId); + if (iter == _AddictedTextures.end()) { + continue; + } + + changedPipelineTextures.append_range(iter->second); + } + + changedPipelineTextures.append_range(std::move(_ChangedPipelines)); + changedTextures.clear(); + + std::sort(changedPipelineTextures.begin(), changedPipelineTextures.end()); + changedPipelineTextures.erase(std::unique(changedPipelineTextures.begin(), changedPipelineTextures.end()), + changedPipelineTextures.end()); + + for (const HashedPipeline& pipeline : changedPipelineTextures) { + auto iterPTTI = _PipeToTexId.find(pipeline); + assert(iterPTTI != _PipeToTexId.end()); + + StoredTexture texture = _generatePipelineTexture(pipeline); + AtlasTextureId atlasTexId = iterPTTI->second; + auto& stored = _AtlasCpuTextures[atlasTexId]; + stored = std::move(texture); + if (!stored._Pixels.empty()) { + Super.setTextureData(atlasTexId, + stored._Widht, + stored._Height, + stored._Pixels.data(), + stored._Widht * 4u); + } + } +} + +TextureAtlas::DescriptorOut PipelinedTextureAtlas::flushUploadsAndBarriers(VkCommandBuffer cmdBuffer) { + return Super.flushUploadsAndBarriers(cmdBuffer); +} + +void PipelinedTextureAtlas::notifyGpuFinished() { + Super.notifyGpuFinished(); +} + +std::optional PipelinedTextureAtlas::tryCopyFirstDependencyTexture(const HashedPipeline& pipeline) const { + auto deps = pipeline.getDependencedTextures(); + if (!deps.empty()) { + auto iter = _ResToTexture.find(deps.front()); + if (iter != _ResToTexture.end()) { + return iter->second; + } + } + return std::nullopt; +} + +StoredTexture PipelinedTextureAtlas::makeSolidColorTexture(uint32_t rgba) { + return StoredTexture(1, 1, std::vector{rgba}); +} diff --git a/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.hpp b/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.hpp new file mode 100644 index 0000000..e1d5b9e --- /dev/null +++ b/Src/Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.hpp @@ -0,0 +1,380 @@ +#pragma once + +#include "TextureAtlas.hpp" +#include "TexturePipelineProgram.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include "boost/container/small_vector.hpp" + +using TextureId = uint32_t; + +namespace detail { + +using Word = TexturePipelineProgram::Word; + +enum class Op16 : Word { + End = 0, + Base_Tex = 1, + Base_Fill = 2, + Resize = 10, + Transform = 11, + Opacity = 12, + NoAlpha = 13, + MakeAlpha = 14, + Invert = 15, + Brighten = 16, + Contrast = 17, + Multiply = 18, + Screen = 19, + Colorize = 20, + Overlay = 30, + Mask = 31, + LowPart = 32, + Combine = 40 +}; + +enum class SrcKind16 : Word { TexId = 0, Sub = 1 }; + +struct SrcRef16 { + SrcKind16 kind{SrcKind16::TexId}; + Word a = 0; + Word b = 0; +}; + +inline uint32_t makeU32(Word lo, Word hi) { + return uint32_t(lo) | (uint32_t(hi) << 16); +} + +inline void addUniqueDep(boost::container::small_vector& deps, uint32_t id) { + if (id == TextureAtlas::kOverflowId) { + return; + } + if (std::find(deps.begin(), deps.end(), id) == deps.end()) { + deps.push_back(id); + } +} + +inline bool readSrc(const std::vector& words, size_t end, size_t& ip, SrcRef16& out) { + if (ip + 2 >= end) { + return false; + } + out.kind = static_cast(words[ip++]); + out.a = words[ip++]; + out.b = words[ip++]; + return true; +} + +inline void extractPipelineDependencies(const std::vector& words, + size_t start, + size_t end, + boost::container::small_vector& deps, + std::vector>& visited) { + if (start >= end || end > words.size()) { + return; + } + const std::pair key{start, end}; + if (std::find(visited.begin(), visited.end(), key) != visited.end()) { + return; + } + visited.push_back(key); + + size_t ip = start; + 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)); + return; + } + if (src.kind == SrcKind16::Sub) { + size_t subStart = static_cast(src.a); + size_t subEnd = subStart + static_cast(src.b); + if (subStart < subEnd && subEnd <= words.size()) { + extractPipelineDependencies(words, subStart, subEnd, deps, visited); + } + } + }; + + while (ip < end) { + if (!need(1)) break; + Op16 op = static_cast(words[ip++]); + switch (op) { + case Op16::End: + 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::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; + 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::Transform: + case Op16::Opacity: + if (!need(1)) return; + ip += 1; + break; + + case Op16::NoAlpha: + case Op16::Brighten: + break; + + case Op16::MakeAlpha: + if (!need(2)) return; + ip += 2; + break; + + case Op16::Invert: + if (!need(1)) return; + ip += 1; + break; + + case Op16::Contrast: + if (!need(2)) return; + ip += 2; + break; + + case Op16::Multiply: + case Op16::Screen: + if (!need(2)) return; + ip += 2; + break; + + case Op16::Colorize: + if (!need(3)) return; + ip += 3; + break; + + case Op16::Combine: { + if (!need(3)) return; + ip += 2; // skip w,h + uint32_t n = words[ip++]; + for (uint32_t i = 0; i < n; ++i) { + if (!need(2 + 3)) return; + ip += 2; // x, y + SrcRef16 src{}; + if (!readSrc(words, end, ip, src)) return; + handleSrc(src); + } + } break; + + default: + return; + } + } +} + +inline boost::container::small_vector extractPipelineDependencies(const std::vector& words) { + boost::container::small_vector deps; + std::vector> visited; + extractPipelineDependencies(words, 0, words.size(), deps, visited); + return deps; +} + +inline boost::container::small_vector extractPipelineDependencies(const boost::container::small_vector& words) { + boost::container::small_vector deps; + std::vector> visited; + std::vector copy(words.begin(), words.end()); + extractPipelineDependencies(copy, 0, copy.size(), deps, visited); + return deps; +} + +} // namespace detail + +// Структура нехешированного пайплайна +struct Pipeline { + std::vector _Pipeline; + + Pipeline() = default; + + explicit Pipeline(const TexturePipelineProgram& program) + : _Pipeline(program.words().begin(), program.words().end()) + { + } + + Pipeline(TextureId texId) { + _Pipeline = { + static_cast(detail::Op16::Base_Tex), + static_cast(detail::SrcKind16::TexId), + static_cast(texId & 0xFFFFu), + static_cast((texId >> 16) & 0xFFFFu), + static_cast(detail::Op16::End) + }; + } +}; + +// Структура хешированного текстурного пайплайна +struct HashedPipeline { + // Предвычисленный хеш + std::size_t _Hash; + boost::container::small_vector _Pipeline; + + HashedPipeline() = default; + HashedPipeline(const Pipeline& pipeline) noexcept + : _Pipeline(pipeline._Pipeline.begin(), pipeline._Pipeline.end()) + { + reComputeHash(); + } + + // Перевычисляет хеш + void reComputeHash() noexcept { + std::size_t hash = 14695981039346656037ull; + 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 *= prime; + } + + _Hash = hash; + } + + // Выдаёт список зависимых текстур, на основе которых строится эта + boost::container::small_vector getDependencedTextures() const { + return detail::extractPipelineDependencies(_Pipeline); + } + + bool operator==(const HashedPipeline& obj) const noexcept { + return _Hash == obj._Hash && _Pipeline == obj._Pipeline; + } + + bool operator<(const HashedPipeline& obj) const noexcept { + return _Hash < obj._Hash || (_Hash == obj._Hash && _Pipeline < obj._Pipeline); + } +}; + +struct StoredTexture { + uint16_t _Widht = 0; + uint16_t _Height = 0; + std::vector _Pixels; + + StoredTexture() = default; + StoredTexture(uint16_t w, uint16_t h, std::vector pixels) + : _Widht(w), _Height(h), _Pixels(std::move(pixels)) + { + } +}; + + +// Пайплайновый текстурный атлас +class PipelinedTextureAtlas { +public: + using AtlasTextureId = uint32_t; + struct HostTextureView { + uint32_t width = 0; + uint32_t height = 0; + uint32_t rowPitchBytes = 0; + const uint8_t* pixelsRGBA8 = nullptr; + }; + +private: + // Функтор хеша + struct HashedPipelineKeyHash { + std::size_t operator()(const HashedPipeline& k) const noexcept { + return k._Hash; + } + }; + + // Функтор равенства + struct HashedPipelineKeyEqual { + bool operator()(const HashedPipeline& a, const HashedPipeline& b) const noexcept { + return a._Pipeline == b._Pipeline; + } + }; + + // Текстурный атлас + TextureAtlas Super; + // Пустой пайплайн (указывающий на одну текстуру) ссылается на простой идентификатор (ResToAtlas) + std::unordered_map _PipeToTexId; + // Загруженные текстуры + std::unordered_map _ResToTexture; + std::unordered_map _AtlasCpuTextures; + // Список зависимых пайплайнов от текстур (при изменении текстуры, нужно перерисовать пайплайны) + std::unordered_map> _AddictedTextures; + // Изменённые простые текстуры (для последующего массового обновление пайплайнов) + std::vector _ChangedTextures; + // Необходимые к созданию/обновлению пайплайны + std::vector _ChangedPipelines; + +public: + PipelinedTextureAtlas(TextureAtlas&& tk); + + uint32_t atlasSide() const { + return Super.atlasSide(); + } + + uint32_t atlasLayers() const { + return Super.atlasLayers(); + } + + uint32_t AtlasSide() const { + return atlasSide(); + } + + uint32_t AtlasLayers() const { + return atlasLayers(); + } + + // Должны всегда бронировать идентификатор, либо отдавать kOverflowId. При этом запись tex+pipeline остаётся + // Выдаёт стабильный идентификатор, привязанный к пайплайну + AtlasTextureId getByPipeline(const HashedPipeline& pipeline); + + // Уведомить что текстура+pipeline более не используются (идентификатор будет освобождён) + // Освобождать можно при потере ресурсов + void freeByPipeline(const HashedPipeline& pipeline); + + void updateTexture(uint32_t texId, const StoredTexture& texture); + void updateTexture(uint32_t texId, StoredTexture&& texture); + + void freeTexture(uint32_t texId); + + bool getHostTexture(TextureId texId, HostTextureView& out) const; + + // Генерация текстуры пайплайна + StoredTexture _generatePipelineTexture(const HashedPipeline& pipeline); + + // Обновляет пайплайны по необходимости + void flushNewPipelines(); + + TextureAtlas::DescriptorOut flushUploadsAndBarriers(VkCommandBuffer cmdBuffer); + + void notifyGpuFinished(); + +private: + std::optional tryCopyFirstDependencyTexture(const HashedPipeline& pipeline) const; + + static StoredTexture makeSolidColorTexture(uint32_t rgba); +}; diff --git a/Src/Client/Vulkan/AtlasPipeline/SharedStagingBuffer.hpp b/Src/Client/Vulkan/AtlasPipeline/SharedStagingBuffer.hpp new file mode 100644 index 0000000..3952547 --- /dev/null +++ b/Src/Client/Vulkan/AtlasPipeline/SharedStagingBuffer.hpp @@ -0,0 +1,169 @@ +#pragma once + +#include + +#include +#include +#include +#include + +/* + Межкадровый промежуточный буфер. + Для модели рендера Один за одним. + После окончания рендера кадра считается синхронизированным + и может заполняться по новой. +*/ + +class SharedStagingBuffer { +public: + static constexpr VkDeviceSize kDefaultSize = 64ull * 1024ull * 1024ull; + + SharedStagingBuffer(VkDevice device, + VkPhysicalDevice physicalDevice, + VkDeviceSize sizeBytes = kDefaultSize) + : device_(device), + physicalDevice_(physicalDevice), + size_(sizeBytes) { + if (!device_ || !physicalDevice_) { + throw std::runtime_error("SharedStagingBuffer: null device/physicalDevice"); + } + if (size_ == 0) { + throw std::runtime_error("SharedStagingBuffer: size must be > 0"); + } + + VkBufferCreateInfo bi{ + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .size = size_, + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr + }; + + if (vkCreateBuffer(device_, &bi, nullptr, &buffer_) != VK_SUCCESS) { + throw std::runtime_error("SharedStagingBuffer: vkCreateBuffer failed"); + } + + VkMemoryRequirements mr{}; + vkGetBufferMemoryRequirements(device_, buffer_, &mr); + + VkMemoryAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + ai.allocationSize = mr.size; + ai.memoryTypeIndex = FindMemoryType_(mr.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + + if (vkAllocateMemory(device_, &ai, nullptr, &memory_) != VK_SUCCESS) { + vkDestroyBuffer(device_, buffer_, nullptr); + buffer_ = VK_NULL_HANDLE; + throw std::runtime_error("SharedStagingBuffer: vkAllocateMemory failed"); + } + + vkBindBufferMemory(device_, buffer_, memory_, 0); + + if (vkMapMemory(device_, memory_, 0, VK_WHOLE_SIZE, 0, &mapped_) != VK_SUCCESS) { + vkFreeMemory(device_, memory_, nullptr); + vkDestroyBuffer(device_, buffer_, nullptr); + buffer_ = VK_NULL_HANDLE; + memory_ = VK_NULL_HANDLE; + throw std::runtime_error("SharedStagingBuffer: vkMapMemory failed"); + } + } + + ~SharedStagingBuffer() { Destroy_(); } + + SharedStagingBuffer(const SharedStagingBuffer&) = delete; + SharedStagingBuffer& operator=(const SharedStagingBuffer&) = delete; + + SharedStagingBuffer(SharedStagingBuffer&& other) noexcept { + *this = std::move(other); + } + + SharedStagingBuffer& operator=(SharedStagingBuffer&& other) noexcept { + if (this != &other) { + Destroy_(); + device_ = other.device_; + physicalDevice_ = other.physicalDevice_; + buffer_ = other.buffer_; + memory_ = other.memory_; + mapped_ = other.mapped_; + size_ = other.size_; + offset_ = other.offset_; + + other.device_ = VK_NULL_HANDLE; + other.physicalDevice_ = VK_NULL_HANDLE; + other.buffer_ = VK_NULL_HANDLE; + other.memory_ = VK_NULL_HANDLE; + other.mapped_ = nullptr; + other.size_ = 0; + other.offset_ = 0; + } + return *this; + } + + VkBuffer Buffer() const { return buffer_; } + void* Mapped() const { return mapped_; } + VkDeviceSize Size() const { return size_; } + + std::optional Allocate(VkDeviceSize bytes, VkDeviceSize alignment) { + VkDeviceSize off = Align_(offset_, alignment); + if (off + bytes > size_) { + return std::nullopt; + } + offset_ = off + bytes; + return off; + } + + void Reset() { offset_ = 0; } + +private: + uint32_t FindMemoryType_(uint32_t typeBits, VkMemoryPropertyFlags properties) const { + VkPhysicalDeviceMemoryProperties mp{}; + vkGetPhysicalDeviceMemoryProperties(physicalDevice_, &mp); + for (uint32_t i = 0; i < mp.memoryTypeCount; ++i) { + if ((typeBits & (1u << i)) && + (mp.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + throw std::runtime_error("SharedStagingBuffer: no suitable memory type"); + } + + static VkDeviceSize Align_(VkDeviceSize value, VkDeviceSize alignment) { + if (alignment == 0) return value; + return (value + alignment - 1) & ~(alignment - 1); + } + + void Destroy_() { + if (device_ == VK_NULL_HANDLE) { + return; + } + if (mapped_) { + vkUnmapMemory(device_, memory_); + mapped_ = nullptr; + } + if (buffer_) { + vkDestroyBuffer(device_, buffer_, nullptr); + buffer_ = VK_NULL_HANDLE; + } + if (memory_) { + vkFreeMemory(device_, memory_, nullptr); + memory_ = VK_NULL_HANDLE; + } + size_ = 0; + offset_ = 0; + device_ = VK_NULL_HANDLE; + physicalDevice_ = VK_NULL_HANDLE; + } + + VkDevice device_ = VK_NULL_HANDLE; + VkPhysicalDevice physicalDevice_ = VK_NULL_HANDLE; + VkBuffer buffer_ = VK_NULL_HANDLE; + VkDeviceMemory memory_ = VK_NULL_HANDLE; + void* mapped_ = nullptr; + VkDeviceSize size_ = 0; + VkDeviceSize offset_ = 0; +}; diff --git a/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.cpp b/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.cpp new file mode 100644 index 0000000..599eb61 --- /dev/null +++ b/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.cpp @@ -0,0 +1,477 @@ +#include "TextureAtlas.hpp" + +TextureAtlas::TextureAtlas(VkDevice device, + VkPhysicalDevice physicalDevice, + const Config& cfg, + EventCallback cb, + std::shared_ptr staging) + : Device_(device), + Phys_(physicalDevice), + Cfg_(cfg), + OnEvent_(std::move(cb)), + Staging_(std::move(staging)) { + if(!Device_ || !Phys_) { + throw std::runtime_error("TextureAtlas: device/physicalDevice == null"); + } + _validateConfigOrThrow(); + + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(Phys_, &props); + CopyOffsetAlignment_ = std::max(4, props.limits.optimalBufferCopyOffsetAlignment); + + if(!Staging_) { + Staging_ = std::make_shared(Device_, Phys_, kStagingSizeBytes); + } + _validateStagingCapacityOrThrow(); + + _createEntriesBufferOrThrow(); + _createAtlasOrThrow(Cfg_.InitialSide, 1); + + EntriesCpu_.resize(Cfg_.MaxTextureId); + std::memset(EntriesCpu_.data(), 0, EntriesCpu_.size() * sizeof(Entry)); + EntriesDirty_ = true; + + Slots_.resize(Cfg_.MaxTextureId); + FreeIds_.reserve(Cfg_.MaxTextureId); + PendingInQueue_.assign(Cfg_.MaxTextureId, false); + + if(Cfg_.ExternalSampler != VK_NULL_HANDLE) { + Sampler_ = Cfg_.ExternalSampler; + OwnsSampler_ = false; + } else { + _createSamplerOrThrow(); + OwnsSampler_ = true; + } + + _rebuildPackersFromPlacements(); + Alive_ = true; +} + +TextureAtlas::~TextureAtlas() { _shutdownNoThrow(); } + +TextureAtlas::TextureAtlas(TextureAtlas&& other) noexcept { + _moveFrom(std::move(other)); +} + +TextureAtlas& TextureAtlas::operator=(TextureAtlas&& other) noexcept { + if(this != &other) { + _shutdownNoThrow(); + _moveFrom(std::move(other)); + } + return *this; +} + +void TextureAtlas::shutdown() { + _ensureAliveOrThrow(); + _shutdownNoThrow(); +} + +TextureAtlas::TextureId TextureAtlas::registerTexture() { + _ensureAliveOrThrow(); + + TextureId id = kOverflowId; + if(!FreeIds_.empty()) { + id = FreeIds_.back(); + FreeIds_.pop_back(); + } else if(NextId_ < Cfg_.MaxTextureId) { + id = NextId_++; + } else { + return kOverflowId; + } + + Slot& s = Slots_[id]; + s = Slot{}; + s.InUse = true; + s.StateValue = State::REGISTERED; + s.Generation = 1; + + _setEntryInvalid(id, /*diagPending*/false, /*diagTooLarge*/false); + EntriesDirty_ = true; + return id; +} + +void TextureAtlas::setTextureData(TextureId id, + uint32_t w, + uint32_t h, + const void* pixelsRGBA8, + uint32_t rowPitchBytes) { + _ensureAliveOrThrow(); + if(id == kOverflowId) return; + _ensureRegisteredIdOrThrow(id); + + if(w == 0 || h == 0) { + throw _inputError("setTextureData: w/h must be > 0"); + } + if(w > Cfg_.MaxTextureSize || h > Cfg_.MaxTextureSize) { + _handleTooLarge(id); + throw _inputError("setTextureData: texture is TOO_LARGE (>2048)"); + } + if(!pixelsRGBA8) { + throw _inputError("setTextureData: pixelsRGBA8 == null"); + } + + if(rowPitchBytes == 0) { + rowPitchBytes = w * 4; + } + if(rowPitchBytes < w * 4) { + throw _inputError("setTextureData: rowPitchBytes < w*4"); + } + + Slot& s = Slots_[id]; + + const bool sizeChanged = (s.HasCpuData && (s.W != w || s.H != h)); + if(sizeChanged) { + _freePlacement(id); + _setEntryInvalid(id, /*diagPending*/true, /*diagTooLarge*/false); + EntriesDirty_ = true; + } + + s.W = w; + s.H = h; + + s.CpuPixels = static_cast(pixelsRGBA8); + s.CpuRowPitchBytes = rowPitchBytes; + s.HasCpuData = true; + s.StateValue = State::PENDING_UPLOAD; + s.Generation++; + + if(!sizeChanged && s.HasPlacement && s.StateWasValid) { + // keep entry valid + } else if(!s.HasPlacement) { + _setEntryInvalid(id, /*diagPending*/true, /*diagTooLarge*/false); + EntriesDirty_ = true; + } + + _enqueuePending(id); + + if(Repack_.Active && Repack_.Plan.count(id) != 0) { + _enqueueRepackPending(id); + } +} + +void TextureAtlas::clearTextureData(TextureId id) { + _ensureAliveOrThrow(); + if(id == kOverflowId) return; + _ensureRegisteredIdOrThrow(id); + + Slot& s = Slots_[id]; + s.CpuPixels = nullptr; + s.CpuRowPitchBytes = 0; + s.HasCpuData = false; + + _freePlacement(id); + s.StateValue = State::REGISTERED; + s.StateWasValid = false; + + _removeFromPending(id); + _removeFromRepackPending(id); + + _setEntryInvalid(id, /*diagPending*/false, /*diagTooLarge*/false); + EntriesDirty_ = true; +} + +void TextureAtlas::removeTexture(TextureId id) { + _ensureAliveOrThrow(); + if(id == kOverflowId) return; + _ensureRegisteredIdOrThrow(id); + + Slot& s = Slots_[id]; + + clearTextureData(id); + + s.InUse = false; + s.StateValue = State::REMOVED; + + FreeIds_.push_back(id); + + _setEntryInvalid(id, /*diagPending*/false, /*diagTooLarge*/false); + EntriesDirty_ = true; +} + +void TextureAtlas::requestFullRepack(RepackMode mode) { + _ensureAliveOrThrow(); + Repack_.Requested = true; + Repack_.Mode = mode; +} + +TextureAtlas::DescriptorOut TextureAtlas::flushUploadsAndBarriers(VkCommandBuffer cmdBuffer) { + _ensureAliveOrThrow(); + if(cmdBuffer == VK_NULL_HANDLE) { + throw _inputError("flushUploadsAndBarriers: cmdBuffer == null"); + } + + if(Repack_.SwapReady) { + _swapToRepackedAtlas(); + } + if(Repack_.Requested && !Repack_.Active) { + _startRepackIfPossible(); + } + + _processPendingLayerGrow(cmdBuffer); + + bool willTouchEntries = EntriesDirty_; + + auto collectQueue = [this](std::deque& queue, + std::vector& inQueue, + std::vector& out) { + while (!queue.empty()) { + TextureId id = queue.front(); + queue.pop_front(); + if(id == kOverflowId || id >= inQueue.size()) { + continue; + } + if(!inQueue[id]) { + continue; + } + inQueue[id] = false; + out.push_back(id); + } + }; + + std::vector pendingNow; + pendingNow.reserve(Pending_.size()); + collectQueue(Pending_, PendingInQueue_, pendingNow); + + std::vector repackPending; + if(Repack_.Active) { + if(Repack_.InPending.empty()) { + Repack_.InPending.assign(Cfg_.MaxTextureId, false); + } + collectQueue(Repack_.Pending, Repack_.InPending, repackPending); + } + + auto processPlacement = [&](TextureId id, Slot& s) -> bool { + if(s.HasPlacement) return true; + const uint32_t wP = s.W + 2u * Cfg_.PaddingPx; + const uint32_t hP = s.H + 2u * Cfg_.PaddingPx; + if(!_tryPlaceWithGrow(id, wP, hP, cmdBuffer)) { + return false; + } + willTouchEntries = true; + return true; + }; + + bool outOfSpace = false; + for(TextureId id : pendingNow) { + if(id == kOverflowId) continue; + if(id >= Slots_.size()) continue; + Slot& s = Slots_[id]; + if(!s.InUse || !s.HasCpuData) continue; + if(!processPlacement(id, s)) { + outOfSpace = true; + _enqueuePending(id); + } + } + if(outOfSpace) { + _emitEventOncePerFlush(AtlasEvent::AtlasOutOfSpace); + } + + bool anyAtlasWrites = false; + bool anyRepackWrites = false; + + auto uploadTextureIntoAtlas = [&](Slot& s, + const Placement& pp, + ImageRes& targetAtlas, + bool isRepackTarget) { + const uint32_t wP = pp.WP; + const uint32_t hP = pp.HP; + const VkDeviceSize bytes = static_cast(wP) * hP * 4u; + auto stagingOff = Staging_->Allocate(bytes, CopyOffsetAlignment_); + if(!stagingOff) { + _emitEventOncePerFlush(AtlasEvent::StagingOverflow); + return false; + } + + uint8_t* dst = static_cast(Staging_->Mapped()) + *stagingOff; + if(!s.CpuPixels) { + return false; + } + _writePaddedRGBA8(dst, wP * 4u, s.W, s.H, Cfg_.PaddingPx, + s.CpuPixels, s.CpuRowPitchBytes); + + _ensureImageLayoutForTransferDst(cmdBuffer, targetAtlas, + isRepackTarget ? anyRepackWrites : anyAtlasWrites); + + VkBufferImageCopy region{}; + region.bufferOffset = *stagingOff; + region.bufferRowLength = wP; + region.bufferImageHeight = hP; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.mipLevel = 0; + region.imageSubresource.baseArrayLayer = pp.Layer; + region.imageSubresource.layerCount = 1; + region.imageOffset = { static_cast(pp.X), + static_cast(pp.Y), 0 }; + region.imageExtent = { wP, hP, 1 }; + + vkCmdCopyBufferToImage(cmdBuffer, Staging_->Buffer(), targetAtlas.Image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + return true; + }; + + for(TextureId id : pendingNow) { + if(id == kOverflowId) continue; + Slot& s = Slots_[id]; + if(!s.InUse || !s.HasCpuData || !s.HasPlacement) continue; + if(!uploadTextureIntoAtlas(s, s.Place, Atlas_, false)) { + _enqueuePending(id); + continue; + } + s.StateValue = State::VALID; + s.StateWasValid = true; + _setEntryValid(id); + EntriesDirty_ = true; + } + + if(Repack_.Active) { + for(TextureId id : repackPending) { + if(Repack_.Plan.count(id) == 0) continue; + Slot& s = Slots_[id]; + if(!s.InUse || !s.HasCpuData) continue; + const PlannedPlacement& pp = Repack_.Plan[id]; + Placement place{pp.X, pp.Y, pp.WP, pp.HP, pp.Layer}; + if(!uploadTextureIntoAtlas(s, place, Repack_.Atlas, true)) { + _enqueueRepackPending(id); + continue; + } + Repack_.WroteSomethingThisFlush = true; + } + } + + if(willTouchEntries || EntriesDirty_) { + const VkDeviceSize entriesBytes = static_cast(EntriesCpu_.size()) * sizeof(Entry); + auto off = Staging_->Allocate(entriesBytes, CopyOffsetAlignment_); + if(!off) { + _emitEventOncePerFlush(AtlasEvent::StagingOverflow); + } else { + std::memcpy(static_cast(Staging_->Mapped()) + *off, + EntriesCpu_.data(), + static_cast(entriesBytes)); + + VkBufferCopy c{}; + c.srcOffset = *off; + c.dstOffset = 0; + c.size = entriesBytes; + vkCmdCopyBuffer(cmdBuffer, Staging_->Buffer(), Entries_.Buffer, 1, &c); + + VkBufferMemoryBarrier b{}; + b.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.buffer = Entries_.Buffer; + b.offset = 0; + b.size = VK_WHOLE_SIZE; + + vkCmdPipelineBarrier(cmdBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, 0, nullptr, 1, &b, 0, nullptr); + EntriesDirty_ = false; + } + } + + if(anyAtlasWrites) { + _transitionImage(cmdBuffer, Atlas_, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + } else if(Atlas_.Layout == VK_IMAGE_LAYOUT_UNDEFINED) { + _transitionImage(cmdBuffer, Atlas_, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + 0, VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + } + + if(anyRepackWrites) { + _transitionImage(cmdBuffer, Repack_.Atlas, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + } + + if(Repack_.Active) { + if(Repack_.Pending.empty()) { + Repack_.WaitingGpuForReady = true; + } + Repack_.WroteSomethingThisFlush = false; + } + + return _buildDescriptorOut(); +} + +void TextureAtlas::notifyGpuFinished() { + _ensureAliveOrThrow(); + + for(auto& img : DeferredImages_) { + _destroyImage(img); + } + DeferredImages_.clear(); + + if(Staging_) { + Staging_->Reset(); + } + FlushEventMask_ = 0; + + if(Repack_.Active && Repack_.WaitingGpuForReady && Repack_.Pending.empty()) { + Repack_.SwapReady = true; + Repack_.WaitingGpuForReady = false; + } +} + +void TextureAtlas::_moveFrom(TextureAtlas&& other) noexcept { + Device_ = other.Device_; + Phys_ = other.Phys_; + Cfg_ = other.Cfg_; + OnEvent_ = std::move(other.OnEvent_); + Alive_ = other.Alive_; + CopyOffsetAlignment_ = other.CopyOffsetAlignment_; + Staging_ = std::move(other.Staging_); + Entries_ = other.Entries_; + Atlas_ = other.Atlas_; + Sampler_ = other.Sampler_; + OwnsSampler_ = other.OwnsSampler_; + EntriesCpu_ = std::move(other.EntriesCpu_); + EntriesDirty_ = other.EntriesDirty_; + Slots_ = std::move(other.Slots_); + FreeIds_ = std::move(other.FreeIds_); + NextId_ = other.NextId_; + Pending_ = std::move(other.Pending_); + PendingInQueue_ = std::move(other.PendingInQueue_); + Packers_ = std::move(other.Packers_); + DeferredImages_ = std::move(other.DeferredImages_); + FlushEventMask_ = other.FlushEventMask_; + GrewThisFlush_ = other.GrewThisFlush_; + Repack_ = std::move(other.Repack_); + + other.Device_ = VK_NULL_HANDLE; + other.Phys_ = VK_NULL_HANDLE; + other.OnEvent_ = {}; + other.Alive_ = false; + other.CopyOffsetAlignment_ = 0; + other.Staging_.reset(); + other.Entries_ = {}; + other.Atlas_ = {}; + other.Sampler_ = VK_NULL_HANDLE; + other.OwnsSampler_ = false; + other.EntriesCpu_.clear(); + other.EntriesDirty_ = false; + other.Slots_.clear(); + other.FreeIds_.clear(); + other.NextId_ = 0; + other.Pending_.clear(); + other.PendingInQueue_.clear(); + other.Packers_.clear(); + other.DeferredImages_.clear(); + other.FlushEventMask_ = 0; + other.GrewThisFlush_ = false; + other.Repack_ = RepackState{}; +} diff --git a/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.hpp b/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.hpp new file mode 100644 index 0000000..e3beb18 --- /dev/null +++ b/Src/Client/Vulkan/AtlasPipeline/TextureAtlas.hpp @@ -0,0 +1,1394 @@ +// TextureAtlas.hpp +#pragma once + +/* +================================================================================ +TextureAtlas — как пользоваться (кратко) + +1) Создайте атлас (один раз): + TextureAtlas atlas(device, physicalDevice, cfg, callback); + +2) Зарегистрируйте текстуру и получите стабильный ID: + TextureId id = atlas.registerTexture(); + if(id == TextureAtlas::kOverflowId) { ... } // нет свободных ID + +3) Задайте данные (RGBA8), можно много раз — ID не меняется: + atlas.setTextureData(id, w, h, pixels, rowPitchBytes); + +4) Каждый кадр (или когда нужно) запишите команды в cmdBuffer: + auto desc = atlas.flushUploadsAndBarriers(cmdBuffer); + + - desc.ImageInfo (sampler+view) используйте как sampled image (2D array). + - desc.EntriesInfo (SSBO) используйте как storage buffer с Entry[]. + - В шейдере ОБЯЗАТЕЛЬНО делайте: + uv = clamp(uv, entry.uvMinMax.xy, entry.uvMinMax.zw); + +5) Затем пользователь САМ делает submit и ждёт завершения GPU (fence вне ТЗ). + После того как GPU точно закончил команды Flush — вызовите: + atlas.notifyGpuFinished(); + +6) Удаление: + atlas.clearTextureData(id); // убрать данные + освободить место + REGISTERED + atlas.removeTexture(id); // освободить ID (после этого использовать нельзя) + +Примечания: +- Вызовы API с kOverflowId игнорируются (no-op). +- Ошибки ресурсов (нет места/стейджинга/oom) НЕ бросают исключения — дают события. +- Исключения только за неверный ввод/неверное использование (см. ТЗ). +- Класс не thread-safe: синхронизацию обеспечивает пользователь. +================================================================================ +*/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "SharedStagingBuffer.hpp" + +class TextureAtlas final { +public: + using TextureId = uint32_t; + static constexpr TextureId kOverflowId = 0xFFFFFFFFu; + + // ----------------------------- Конфигурация ----------------------------- + + struct Config { + uint32_t MaxTextureId = 4096; // Размер SSBO: MaxTextureId * sizeof(Entry) + uint32_t InitialSide = 1024; // {1024, 2048, 4096} + uint32_t MaxLayers = 16; // <= 16 + uint32_t PaddingPx = 2; // фиксированный padding (edge-extend) + uint32_t MaxTextureSize = 2048; // w,h <= 2048 + VkFilter SamplerFilter = VK_FILTER_LINEAR; + bool SamplerAnisotropyEnable = false; + + // Если хотите — можно задать внешний sampler (тогда класс его НЕ уничтожает) + VkSampler ExternalSampler = VK_NULL_HANDLE; + }; + + // ----------------------------- События ----------------------------- + + enum class AtlasEvent { + StagingOverflow, + AtlasOutOfSpace, + GpuOutOfMemory, + RepackStarted, + RepackFinished + }; + + using EventCallback = std::function; + + // ----------------------------- Shader entry ----------------------------- + // Важно: layout должен соответствовать std430 на стороне шейдера. + struct alignas(16) Entry { + // (uMin, vMin, uMax, vMax) + float UVMinMax[4]; + uint32_t Layer; + uint32_t Flags; + uint32_t _Pad0; + uint32_t _Pad1; + }; + static_assert(sizeof(Entry) % 16 == 0, "Entry должен быть кратен 16 байтам"); + + enum EntryFlags : uint32_t { + ENTRY_VALID = 1u << 0, + // опционально под диагностику: + ENTRY_DIAG_PENDING = 1u << 1, + ENTRY_DIAG_TOO_LARGE = 1u << 2 + }; + + // ----------------------------- Выход для дескрипторов ----------------------------- + + struct DescriptorOut { + VkImage AtlasImage = VK_NULL_HANDLE; + VkImageView AtlasView = VK_NULL_HANDLE; + VkSampler Sampler = VK_NULL_HANDLE; + + VkBuffer EntriesBuffer = VK_NULL_HANDLE; + + VkDescriptorImageInfo ImageInfo{}; + VkDescriptorBufferInfo EntriesInfo{}; + + uint32_t AtlasSide = 0; + uint32_t AtlasLayers = 0; + }; + + // ----------------------------- Full repack ----------------------------- + + enum class RepackMode { + Tightest, + KeepCurrentCapacity, + AllowGrow + }; + + // ----------------------------- Жизненный цикл ----------------------------- + + /// Создаёт GPU-ресурсы: atlas image, entries SSBO, staging ring (64MB, если внешняя staging не передана), sampler. + TextureAtlas(VkDevice device, + VkPhysicalDevice physicalDevice, + const Config& cfg, + EventCallback cb = {}, + std::shared_ptr staging = {}); + TextureAtlas(const TextureAtlas&) = delete; + TextureAtlas& operator=(const TextureAtlas&) = delete; + TextureAtlas(TextureAtlas&& other) noexcept; + TextureAtlas& operator=(TextureAtlas&& other) noexcept; + + /// Уничтожает ресурсы (если не уничтожены раньше). + ~TextureAtlas(); + + /// Явно освобождает все ресурсы. После этого любые вызовы (кроме деструктора) — ошибка. + void shutdown(); + + // ----------------------------- API из ТЗ ----------------------------- + + /// Регистрирует новую текстуру и возвращает стабильный TextureId (или kOverflowId). + TextureId registerTexture(); + + /// Устанавливает пиксели (RGBA8), переводит в PENDING_UPLOAD, при смене размера освобождает размещение. + /// Внимание: pixelsRGBA8 должен оставаться валидным, пока текстура не будет очищена или заменена. + void setTextureData(TextureId id, + uint32_t w, + uint32_t h, + const void* pixelsRGBA8, + uint32_t rowPitchBytes); + + /// Сбрасывает данные, освобождает размещение, переводит в REGISTERED. + void clearTextureData(TextureId id); + + /// Удаляет текстуру: освобождает размещение, удаляет данные, удаляет pending, возвращает ID в пул. + void removeTexture(TextureId id); + + /// Запрашивает полный репак (выполняется по частям через Flush, с событиями). + void requestFullRepack(RepackMode mode); + + // ----------------------------- Flush/Notify из ТЗ ----------------------------- + + /// Записывает команды Vulkan: рост/репак/копии pending/обновление entries/барьеры. Submit делает пользователь. + DescriptorOut flushUploadsAndBarriers(VkCommandBuffer cmdBuffer); + + /// Пользователь вызывает ПОСЛЕ завершения GPU-команд Flush: освобождает deferred ресурсы, сбрасывает staging и т.п. + void notifyGpuFinished(); + +// ----------------------------- Дополнительно ----------------------------- + + /// Текущая сторона атласа. + uint32_t atlasSide() const { return Atlas_.Side; } + + /// Текущее число слоёв атласа. + uint32_t atlasLayers() const { return Atlas_.Layers; } + + /// Общий staging-буфер (может быть задан извне). + std::shared_ptr getStagingBuffer() const { return Staging_; } + +private: + void _moveFrom(TextureAtlas&& other) noexcept; + // ============================= Ошибки/валидация ============================= + + struct InputError : std::runtime_error { + using std::runtime_error::runtime_error; + }; + + static InputError _inputError(const std::string& msg) { return InputError(msg); } + + void _validateConfigOrThrow() { + auto isPow2Allowed = [](uint32_t s) { + return s == 1024u || s == 2048u || s == 4096u; + }; + + if(!isPow2Allowed(Cfg_.InitialSide)) { + throw _inputError("Config.InitialSide must be 1024/2048/4096"); + } + + if(Cfg_.MaxLayers == 0 || Cfg_.MaxLayers > 16) { + throw _inputError("Config.MaxLayers must be 1..16"); + } + + if(Cfg_.PaddingPx > 64) { + throw _inputError("Config.PaddingPx looks too large"); + } + + if(Cfg_.MaxTextureId == 0) { + throw _inputError("Config.MaxTextureId must be > 0"); + } + + if(Cfg_.MaxTextureSize != 2048) { + /// TODO: + } + } + + void _validateStagingCapacityOrThrow() const { + if(!Staging_) { + throw std::runtime_error("TextureAtlas: staging buffer not initialized"); + } + const VkDeviceSize entriesBytes = static_cast(Cfg_.MaxTextureId) * sizeof(Entry); + if(entriesBytes > Staging_->Size()) { + throw _inputError("Config.MaxTextureId слишком большой: entries не влезают в staging buffer"); + } + } + + void _ensureAliveOrThrow() const { + if(!Alive_) throw _inputError("TextureAtlas: used after shutdown"); + } + + void _ensureRegisteredIdOrThrow(TextureId id) const { + if(id >= Cfg_.MaxTextureId) { + throw _inputError("TextureId out of range"); + } + if(!Slots_[id].InUse || Slots_[id].StateValue == State::REMOVED) { + throw _inputError("Using unregistered or removed TextureId"); + } + } + + // ============================= Состояния/слоты ============================= + + enum class State { + REGISTERED, + PENDING_UPLOAD, + VALID, + NOT_LOADED_TOO_LARGE, + REMOVED + }; + + struct Placement { + uint32_t X = 0, Y = 0; + uint32_t WP = 0, HP = 0; + uint32_t Layer = 0; + }; + + struct Slot { + bool InUse = false; + + State StateValue = State::REMOVED; + bool HasCpuData = false; + bool TooLarge = false; + + uint32_t W = 0, H = 0; + const uint8_t* CpuPixels = nullptr; + uint32_t CpuRowPitchBytes = 0; + + bool HasPlacement = false; + Placement Place{}; + + bool StateWasValid = false; + uint64_t Generation = 0; + }; + + // ============================= Vulkan ресурсы ============================= + + static constexpr VkDeviceSize kStagingSizeBytes = 64ull * 1024ull * 1024ull; + + struct BufferRes { + VkBuffer Buffer = VK_NULL_HANDLE; + VkDeviceMemory Memory = VK_NULL_HANDLE; + VkDeviceSize Size = 0; + }; + + struct ImageRes { + VkImage Image = VK_NULL_HANDLE; + VkDeviceMemory Memory = VK_NULL_HANDLE; + VkImageView View = VK_NULL_HANDLE; + VkImageLayout Layout = VK_IMAGE_LAYOUT_UNDEFINED; + uint32_t Side = 0; + uint32_t Layers = 0; + }; + + // ============================= MaxRects packer ============================= + + struct Rect { + uint32_t X = 0, Y = 0, W = 0, H = 0; + }; + + struct MaxRectsBin { + uint32_t Width = 0; + uint32_t Height = 0; + std::vector FreeRects; + + void reset(uint32_t w, uint32_t h) { + Width = w; Height = h; + FreeRects.clear(); + FreeRects.push_back(Rect{0,0,w,h}); + } + + static bool _contains(const Rect& a, const Rect& b) { + return b.X >= a.X && b.Y >= a.Y && + (b.X + b.W) <= (a.X + a.W) && + (b.Y + b.H) <= (a.Y + a.H); + } + + static bool _intersects(const Rect& a, const Rect& b) { + return !(b.X >= a.X + a.W || b.X + b.W <= a.X || + b.Y >= a.Y + a.H || b.Y + b.H <= a.Y); + } + + void _prune() { + // удаляем прямоугольники, которые содержатся в других + for(size_t i = 0; i < FreeRects.size(); ++i) { + for(size_t j = i + 1; j < FreeRects.size();) { + if(_contains(FreeRects[i], FreeRects[j])) { + FreeRects.erase(FreeRects.begin() + j); + } else if(_contains(FreeRects[j], FreeRects[i])) { + FreeRects.erase(FreeRects.begin() + i); + --i; + break; + } else { + ++j; + } + } + } + } + + std::optional insert(uint32_t w, uint32_t h) { + // Best Area Fit + size_t bestIdx = std::numeric_limits::max(); + uint64_t bestAreaWaste = std::numeric_limits::max(); + uint32_t bestShortSide = std::numeric_limits::max(); + + for(size_t i = 0; i < FreeRects.size(); ++i) { + const Rect& r = FreeRects[i]; + if(w <= r.W && h <= r.H) { + const uint64_t waste = static_cast(r.W) * r.H - static_cast(w) * h; + const uint32_t shortSide = std::min(r.W - w, r.H - h); + if(waste < bestAreaWaste || (waste == bestAreaWaste && shortSide < bestShortSide)) { + bestAreaWaste = waste; + bestShortSide = shortSide; + bestIdx = i; + } + } + } + + if(bestIdx == std::numeric_limits::max()) { + return std::nullopt; + } + + Rect placed{ FreeRects[bestIdx].X, FreeRects[bestIdx].Y, w, h }; + _splitFree(placed); + _prune(); + return placed; + } + + void _splitFree(const Rect& used) { + std::vector newFree; + newFree.reserve(FreeRects.size() * 2); + + for(const Rect& fr : FreeRects) { + if(!_intersects(fr, used)) { + newFree.push_back(fr); + continue; + } + + // сверху + if(used.Y > fr.Y) { + newFree.push_back(Rect{ fr.X, fr.Y, fr.W, used.Y - fr.Y }); + } + // снизу + if(used.Y + used.H < fr.Y + fr.H) { + newFree.push_back(Rect{ fr.X, used.Y + used.H, fr.W, + (fr.Y + fr.H) - (used.Y + used.H) }); + } + // слева + if(used.X > fr.X) { + const uint32_t x = fr.X; + const uint32_t y = std::max(fr.Y, used.Y); + const uint32_t h = std::min(fr.Y + fr.H, used.Y + used.H) - y; + newFree.push_back(Rect{ x, y, used.X - fr.X, h }); + } + // справа + if(used.X + used.W < fr.X + fr.W) { + const uint32_t x = used.X + used.W; + const uint32_t y = std::max(fr.Y, used.Y); + const uint32_t h = std::min(fr.Y + fr.H, used.Y + used.H) - y; + newFree.push_back(Rect{ x, y, (fr.X + fr.W) - (used.X + used.W), h }); + } + } + + // удаляем нулевые + FreeRects.clear(); + FreeRects.reserve(newFree.size()); + for(const Rect& r : newFree) { + if(r.W > 0 && r.H > 0) + FreeRects.push_back(r); + } + } + + void free(const Rect& r) { + FreeRects.push_back(r); + _mergeAdjacent(); + _prune(); + } + + void _mergeAdjacent() { + bool merged = true; + while (merged) { + merged = false; + for(size_t i = 0; i < FreeRects.size() && !merged; ++i) { + for(size_t j = i + 1; j < FreeRects.size(); ++j) { + Rect a = FreeRects[i]; + Rect b = FreeRects[j]; + + // vertical merge + if(a.X == b.X && a.W == b.W) { + if(a.Y + a.H == b.Y) { + FreeRects[i] = Rect{ a.X, a.Y, a.W, a.H + b.H }; + FreeRects.erase(FreeRects.begin() + j); + merged = true; + break; + } else if(b.Y + b.H == a.Y) { + FreeRects[i] = Rect{ b.X, b.Y, b.W, b.H + a.H }; + FreeRects.erase(FreeRects.begin() + j); + merged = true; + break; + } + } + + // horizontal merge + if(a.Y == b.Y && a.H == b.H) { + if(a.X + a.W == b.X) { + FreeRects[i] = Rect{ a.X, a.Y, a.W + b.W, a.H }; + FreeRects.erase(FreeRects.begin() + j); + merged = true; + break; + } else if(b.X + b.W == a.X) { + FreeRects[i] = Rect{ b.X, b.Y, b.W + a.W, b.H }; + FreeRects.erase(FreeRects.begin() + j); + merged = true; + break; + } + } + } + } + } + } + }; + + // ============================= Repack state ============================= + + struct PlannedPlacement { + uint32_t X = 0, Y = 0; + uint32_t WP = 0, HP = 0; + uint32_t Layer = 0; + }; + + struct RepackState { + bool Requested = false; + bool Active = false; + bool SwapReady = false; + bool WaitingGpuForReady = false; + + RepackMode Mode = RepackMode::Tightest; + + ImageRes Atlas{}; // новый атлас (пока не активный) + + std::unordered_map Plan; + + std::deque Pending; // что ещё нужно залить/перезалить + std::vector InPending; // чтобы не дублировать + bool WroteSomethingThisFlush = false; + }; + + // ============================= Внутренняя логика ============================= + + void _emitEventOncePerFlush(AtlasEvent e) { + const uint32_t bit = 1u << static_cast(e); + if((FlushEventMask_ & bit) != 0) + return; + FlushEventMask_ |= bit; + if(OnEvent_) + OnEvent_(e); + } + + DescriptorOut _buildDescriptorOut() const { + DescriptorOut out{}; + out.AtlasImage = Atlas_.Image; + out.AtlasView = Atlas_.View; + out.Sampler = Sampler_; + out.EntriesBuffer = Entries_.Buffer; + out.AtlasSide = Atlas_.Side; + out.AtlasLayers = Atlas_.Layers; + + out.ImageInfo.sampler = Sampler_; + out.ImageInfo.imageView = Atlas_.View; + out.ImageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + out.EntriesInfo.buffer = Entries_.Buffer; + out.EntriesInfo.offset = 0; + out.EntriesInfo.range = VK_WHOLE_SIZE; + return out; + } + + void _handleTooLarge(TextureId id) { + Slot& s = Slots_[id]; + if(s.StateValue == State::VALID && s.HasPlacement) { + _freePlacement(id); + } else { + _freePlacement(id); + } + + s.CpuPixels = nullptr; + s.CpuRowPitchBytes = 0; + s.HasCpuData = false; + + s.TooLarge = true; + s.StateValue = State::NOT_LOADED_TOO_LARGE; + s.StateWasValid = false; + + _removeFromPending(id); + _removeFromRepackPending(id); + + _setEntryInvalid(id, /*diagPending*/false, /*diagTooLarge*/true); + EntriesDirty_ = true; + } + + void _enqueuePending(TextureId id) { + Slot& s = Slots_[id]; + if(!PendingInQueue_[id]) { + Pending_.push_back(id); + PendingInQueue_[id] = true; + } + } + + void _removeFromPending(TextureId id) { + if(id >= PendingInQueue_.size() || !PendingInQueue_[id]) + return; + PendingInQueue_[id] = false; + // ленивое удаление из Pending_ делаем в Flush (чтобы не O(n) на каждый вызов) + } + + void _enqueueRepackPending(TextureId id) { + if(!Repack_.Active) + return; + + if(id >= Repack_.InPending.size()) + return; + + if(!Repack_.InPending[id]) { + Repack_.Pending.push_back(id); + Repack_.InPending[id] = true; + } + } + + void _removeFromRepackPending(TextureId id) { + if(!Repack_.Active) + return; + if(id >= Repack_.InPending.size()) + return; + Repack_.InPending[id] = false; + // очередь Repack_.Pending лениво отфильтруется в Flush + } + + void _setEntryInvalid(TextureId id, bool diagPending, bool diagTooLarge) { + Entry& e = EntriesCpu_[id]; + e.UVMinMax[0] = e.UVMinMax[1] = e.UVMinMax[2] = e.UVMinMax[3] = 0.0f; + e.Layer = 0; + e.Flags = 0; + if(diagPending) + e.Flags |= ENTRY_DIAG_PENDING; + if(diagTooLarge) + e.Flags |= ENTRY_DIAG_TOO_LARGE; + } + + void _setEntryValid(TextureId id) { + Slot& s = Slots_[id]; + if(!s.HasPlacement) { + _setEntryInvalid(id, /*diagPending*/true, /*diagTooLarge*/false); + return; + } + + const float S = static_cast(Atlas_.Side); + const float x = static_cast(s.Place.X); + const float y = static_cast(s.Place.Y); + const float wP = static_cast(s.Place.WP); + const float hP = static_cast(s.Place.HP); + + Entry& e = EntriesCpu_[id]; + e.UVMinMax[0] = (x + 0.5f) / S; + e.UVMinMax[1] = (y + 0.5f) / S; + e.UVMinMax[2] = (x + wP - 0.5f) / S; + e.UVMinMax[3] = (y + hP - 0.5f) / S; + e.Layer = s.Place.Layer; + e.Flags = ENTRY_VALID; + } + + void _scheduleLayerGrow(uint32_t targetLayers) { + if(targetLayers > Cfg_.MaxLayers) { + targetLayers = Cfg_.MaxLayers; + } + PendingLayerGrow_ = std::max(PendingLayerGrow_, targetLayers); + } + + void _processPendingLayerGrow(VkCommandBuffer cmdBuffer) { + if(PendingLayerGrow_ == 0) + return; + if(PendingLayerGrow_ <= Atlas_.Layers) { + PendingLayerGrow_ = 0; + return; + } + if(cmdBuffer == VK_NULL_HANDLE) + return; + if(_tryGrowAtlas(Atlas_.Side, PendingLayerGrow_, cmdBuffer)) { + PendingLayerGrow_ = 0; + } + } + + // ============================= Размещение/packer ============================= + + bool _tryPlaceWithGrow(TextureId id, uint32_t wP, uint32_t hP, VkCommandBuffer cmdBuffer) { + // 1) текущие слои + if(_tryPlaceInExistingLayers(id, wP, hP)) return true; + + // 2) добавляем новые слои до тех пор, пока есть лимит + while (Atlas_.Layers < Cfg_.MaxLayers) { + if(!_tryAddLayer(cmdBuffer)) { + // рост слоя не удался (например, OOM) — дальше увеличивать сторону нельзя + return false; + } + if(_tryPlaceInExistingLayers(id, wP, hP)) return true; + } + + // 3) увеличить размер атласа 1024→2048→4096 (только когда достигли лимита по слоям) + if(Atlas_.Layers >= Cfg_.MaxLayers) { + const uint32_t nextSide = _nextAllowedSide(Atlas_.Side); + if(nextSide != Atlas_.Side) { + if(_tryGrowAtlas(nextSide, Atlas_.Layers, cmdBuffer)) { + if(_tryPlaceInExistingLayers(id, wP, hP)) return true; + } else { + // OOM — событие уже отправили + return false; + } + } + } + + // 4) невозможно + return false; + } + + bool _tryPlaceInExistingLayers(TextureId id, uint32_t wP, uint32_t hP) { + for(uint32_t layer = 0; layer < Atlas_.Layers; ++layer) { + auto placed = Packers_[layer].insert(wP, hP); + if(placed.has_value()) { + Slot& s = Slots_[id]; + s.HasPlacement = true; + s.Place = Placement{ placed->X, placed->Y, wP, hP, layer }; + // entry пока не VALID (станет VALID после копии), но UV уже можно пересчитать + return true; + } + } + return false; + } + + bool _tryAddLayer(VkCommandBuffer cmdBuffer) { + if(Atlas_.Layers >= Cfg_.MaxLayers) return false; + const uint32_t newLayers = Atlas_.Layers + 1; + if(cmdBuffer == VK_NULL_HANDLE) { + _scheduleLayerGrow(newLayers); + return false; + } + return _tryGrowAtlas(Atlas_.Side, newLayers, cmdBuffer); + } + + uint32_t _nextAllowedSide(uint32_t s) const { + if(s <= 1024u) return 2048u; + if(s <= 2048u) return 4096u; + return s; + } + + bool _tryGrowAtlas(uint32_t newSide, uint32_t newLayers, VkCommandBuffer cmdBuffer) { + // Создаём новый image+view + ImageRes newAtlas{}; + if(!_createAtlasNoThrow(newSide, newLayers, newAtlas)) { + _emitEventOncePerFlush(AtlasEvent::GpuOutOfMemory); + return false; + } + + // Если cmdBuffer == null — не можем записать copy сейчас. + // Поэтому этот путь (рост layers) предполагает, что пользователь вызовет Flush, + // и мы записываем копию только когда cmdBuffer валиден. + // Чтобы не усложнять, если cmdBuffer == null — считаем, что нет роста (fail). + if(cmdBuffer == VK_NULL_HANDLE) { + _destroyImage(newAtlas); + return false; + } + + // Переводим layouts и копируем старый атлас в новый + _transitionImage(cmdBuffer, Atlas_, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + (Atlas_.Layout == VK_IMAGE_LAYOUT_UNDEFINED ? 0 : VK_ACCESS_SHADER_READ_BIT), + VK_ACCESS_TRANSFER_READ_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + + _transitionImage(cmdBuffer, newAtlas, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 0, + VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + + const uint32_t layersToCopy = std::min(Atlas_.Layers, newAtlas.Layers); + VkImageCopy copy{}; + copy.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + copy.srcSubresource.mipLevel = 0; + copy.srcSubresource.baseArrayLayer = 0; + copy.srcSubresource.layerCount = layersToCopy; + copy.dstSubresource = copy.srcSubresource; + copy.extent = { Atlas_.Side, Atlas_.Side, 1 }; + + vkCmdCopyImage(cmdBuffer, + Atlas_.Image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + newAtlas.Image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, ©); + + // Новый делаем читаемым (активным станет сразу после Flush) + _transitionImage(cmdBuffer, newAtlas, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + // Старый вернём в читаемый тоже (на всякий случай) + _transitionImage(cmdBuffer, Atlas_, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + // deferred destroy старого (после Notify) + DeferredImages_.push_back(Atlas_); + + // переключаемся на новый + Atlas_ = newAtlas; + + // После роста пересчитываем entries для всех VALID (S изменился) + for(TextureId id = 0; id < Cfg_.MaxTextureId; ++id) { + Slot& s = Slots_[id]; + if(!s.InUse) + continue; + + if(s.HasPlacement && s.StateValue == State::VALID) { + _setEntryValid(id); + } else if(s.HasPlacement && s.StateValue == State::PENDING_UPLOAD && s.StateWasValid) { + // Был VALID, а теперь ждёт перезаливки — UV всё равно должны соответствовать новому S + // (данные лежат по тем же пиксельным координатам, но нормализация изменилась). + _setEntryValid(id); + } + } + EntriesDirty_ = true; + + // Перестраиваем packer по текущим placements и новому размеру/слоям + _rebuildPackersFromPlacements(); + + GrewThisFlush_ = true; + return true; + } + + void _freePlacement(TextureId id) { + Slot& s = Slots_[id]; + if(!s.HasPlacement) return; + + if(s.Place.Layer < Packers_.size()) { + Packers_[s.Place.Layer].free(Rect{s.Place.X, s.Place.Y, s.Place.WP, s.Place.HP}); + } + s.HasPlacement = false; + s.Place = Placement{}; + s.StateWasValid = false; + } + + void _rebuildPackersFromPlacements() { + Packers_.clear(); + Packers_.resize(Atlas_.Layers); + for(uint32_t l = 0; l < Atlas_.Layers; ++l) { + Packers_[l].reset(Atlas_.Side, Atlas_.Side); + } + + // Занимаем прямоугольники: проще "вставить" их, временно сбросив FreeRects и применив split. + // Здесь делаем упрощённо: для каждого placement просто "split free" через Insert (в том же месте нельзя), + // поэтому вместо этого: удаляем из free списка все пересечения и добавляем остатки — сложно. + // Практичный компромисс: строим "новый packer" через последовательные Insert в порядке убывания площади, + // сохраняя уже существующие координаты нельзя. Поэтому делаем корректнее: + // - Создадим маску занятости списком usedRects, а затем выведем FreeRects как "полосы" — это сложно. + // + // В этой реализации: после роста/свапа мы допускаем, что packer не обязан идеально отражать текущее заполнение, + // и рекомендуем пользователю при сильной фрагментации вызывать requestFullRepack(). + // + // Однако, чтобы не потерять место полностью, добавим занятые как "запрещённые" через SplitFree_ вручную. + for(TextureId id = 0; id < Cfg_.MaxTextureId; ++id) { + const Slot& s = Slots_[id]; + if(!s.InUse || !s.HasPlacement) continue; + if(s.Place.Layer >= Packers_.size()) continue; + // имитируем занятие used (не проверяем пересечения — считаем, что данных не битые) + MaxRectsBin& bin = Packers_[s.Place.Layer]; + bin._splitFree(Rect{s.Place.X, s.Place.Y, s.Place.WP, s.Place.HP}); + bin._prune(); + } + } + + // ============================= Padding edge-extend ============================= + + static void _writePaddedRGBA8(uint8_t* dst, + uint32_t dstRowPitch, + uint32_t w, + uint32_t h, + uint32_t p, + const uint8_t* src, + uint32_t srcRowPitch) { + const uint32_t wP = w + 2u * p; + const uint32_t hP = h + 2u * p; + + // 1) центр + for(uint32_t y = 0; y < h; ++y) { + uint8_t* row = dst + static_cast(y + p) * dstRowPitch; + std::memcpy(row + static_cast(p) * 4u, + src + static_cast(y) * srcRowPitch, + static_cast(w) * 4u); + } + + // 2) расширение по X для строк центра + for(uint32_t y = 0; y < h; ++y) { + uint8_t* row = dst + static_cast(y + p) * dstRowPitch; + + const uint8_t* firstPx = row + static_cast(p) * 4u; + const uint8_t* lastPx = row + static_cast(p + w - 1) * 4u; + + for(uint32_t x = 0; x < p; ++x) { + std::memcpy(row + static_cast(x) * 4u, firstPx, 4u); + } + for(uint32_t x = 0; x < p; ++x) { + std::memcpy(row + static_cast(p + w + x) * 4u, lastPx, 4u); + } + } + + // 3) верхние p строк = первая строка центра + const uint8_t* topSrc = dst + static_cast(p) * dstRowPitch; + for(uint32_t y = 0; y < p; ++y) { + std::memcpy(dst + static_cast(y) * dstRowPitch, + topSrc, + static_cast(wP) * 4u); + } + + // 4) нижние p строк = последняя строка центра + const uint8_t* botSrc = dst + static_cast(p + h - 1) * dstRowPitch; + for(uint32_t y = 0; y < p; ++y) { + std::memcpy(dst + static_cast(p + h + y) * dstRowPitch, + botSrc, + static_cast(wP) * 4u); + } + + (void)hP; + } + + bool _tryPackWithRectpack(uint32_t side, + uint32_t layers, + const std::vector& ids, + std::unordered_map& outPlan) + { + if(side == 0 || layers == 0) + return false; + + std::vector bins; + bins.reserve(layers); + for(uint32_t l = 0; l < layers; ++l) + bins.emplace_back(static_cast(side), static_cast(side), false); + + outPlan.clear(); + outPlan.reserve(ids.size()); + + for(TextureId id : ids) { + const Slot& s = Slots_[id]; + const uint32_t wP = s.W + 2u * Cfg_.PaddingPx; + const uint32_t hP = s.H + 2u * Cfg_.PaddingPx; + + bool placed = false; + for(uint32_t layer = 0; layer < layers; ++layer) { + rbp::Rect rect = bins[layer].Insert( + static_cast(wP), + static_cast(hP), + rbp::MaxRectsBinPack::RectBestShortSideFit); + + if(rect.width > 0 && rect.height > 0) { + outPlan[id] = PlannedPlacement{ + static_cast(rect.x), + static_cast(rect.y), + wP, + hP, + layer}; + placed = true; + break; + } + } + + if(!placed) { + outPlan.clear(); + return false; + } + } + + return true; + } + + // ============================= Repack ============================= + + void _startRepackIfPossible() { + // Собираем кандидаты: все с доступными данными, кроме TOO_LARGE + std::vector ids; + ids.reserve(Cfg_.MaxTextureId); + + for(TextureId id = 0; id < Cfg_.MaxTextureId; ++id) { + const Slot& s = Slots_[id]; + if(!s.InUse) continue; + if(!s.HasCpuData) continue; + if(s.TooLarge) continue; + ids.push_back(id); + } + + if(ids.empty()) { + Repack_.Requested = false; + return; + } + + // сортировка по площади убыванию (wP*hP) + std::sort(ids.begin(), ids.end(), [&](TextureId a, TextureId b) { + const Slot& A = Slots_[a]; + const Slot& B = Slots_[b]; + const uint64_t areaA = uint64_t(A.W + 2u*Cfg_.PaddingPx) * uint64_t(A.H + 2u*Cfg_.PaddingPx); + const uint64_t areaB = uint64_t(B.W + 2u*Cfg_.PaddingPx) * uint64_t(B.H + 2u*Cfg_.PaddingPx); + return areaA > areaB; + }); + + // Выбираем capacity по mode + uint32_t targetSide = Atlas_.Side; + uint32_t targetLayers = Atlas_.Layers; + + std::unordered_map plan; + + if(Repack_.Mode == RepackMode::KeepCurrentCapacity) { + if(!_tryPackWithRectpack(targetSide, targetLayers, ids, plan)) { + _emitEventOncePerFlush(AtlasEvent::AtlasOutOfSpace); + Repack_.Requested = false; + return; + } + } else if(Repack_.Mode == RepackMode::Tightest) { + // Минимальный side/layers из допустимых + const std::array sides{1024u, 2048u, 4096u}; + bool ok = false; + for(uint32_t s : sides) { + for(uint32_t l = 1; l <= Cfg_.MaxLayers; ++l) { + if(_tryPackWithRectpack(s, l, ids, plan)) { + targetSide = s; + targetLayers = l; + ok = true; + break; + } + } + if(ok) break; + } + if(!ok) { + _emitEventOncePerFlush(AtlasEvent::AtlasOutOfSpace); + Repack_.Requested = false; + return; + } + } else { // AllowGrow + // Сначала текущая capacity, потом растим layers, потом side. + bool ok = _tryPackWithRectpack(targetSide, targetLayers, ids, plan); + if(!ok) { + // grow layers + for(uint32_t l = targetLayers + 1; l <= Cfg_.MaxLayers && !ok; ++l) { + if(_tryPackWithRectpack(targetSide, l, ids, plan)) { + targetLayers = l; + ok = true; + } + } + } + if(!ok) { + // grow side + const std::array sides{1024u, 2048u, 4096u}; + for(uint32_t s : sides) { + if(s < targetSide) continue; + for(uint32_t l = 1; l <= Cfg_.MaxLayers; ++l) { + if(_tryPackWithRectpack(s, l, ids, plan)) { + targetSide = s; + targetLayers = l; + ok = true; + break; + } + } + if(ok) break; + } + } + if(!ok) { + _emitEventOncePerFlush(AtlasEvent::AtlasOutOfSpace); + Repack_.Requested = false; + return; + } + } + + // Создаём новый atlas (пока не активный) + ImageRes newAtlas{}; + if(!_createAtlasNoThrow(targetSide, targetLayers, newAtlas)) { + _emitEventOncePerFlush(AtlasEvent::GpuOutOfMemory); + // оставляем requested=true, чтобы попробовать снова в следующий Flush + return; + } + + // Ставим repack active + Repack_.Active = true; + Repack_.Requested = false; + Repack_.SwapReady = false; + Repack_.WaitingGpuForReady = false; + Repack_.Atlas = newAtlas; + Repack_.Plan = std::move(plan); + + Repack_.Pending.clear(); + Repack_.InPending.assign(Cfg_.MaxTextureId, false); + + // Заполняем очередь аплоада всеми текстурами из плана + Repack_.Pending.resize(0); + for(TextureId id : ids) { + if(Repack_.Plan.count(id) == 0) continue; + Repack_.Pending.push_back(id); + Repack_.InPending[id] = true; + } + + _emitEventOncePerFlush(AtlasEvent::RepackStarted); + } + + void _swapToRepackedAtlas() { + // Переключаем текущий atlas на Repack_.Atlas, а старый — в deferred destroy + DeferredImages_.push_back(Atlas_); + Atlas_ = Repack_.Atlas; + Repack_.Atlas = ImageRes{}; + + // Применяем placements из плана + for(TextureId id = 0; id < Cfg_.MaxTextureId; ++id) { + Slot& s = Slots_[id]; + if(!s.InUse) continue; + + auto it = Repack_.Plan.find(id); + if(it != Repack_.Plan.end() && s.HasCpuData && !s.TooLarge) { + const PlannedPlacement& pp = it->second; + s.HasPlacement = true; + s.Place = Placement{pp.X, pp.Y, pp.WP, pp.HP, pp.Layer}; + s.StateValue = State::VALID; + s.StateWasValid = true; + _setEntryValid(id); + } else { + // Если данные доступны, но в план не попала — оставим PENDING_UPLOAD без placement + if(s.HasCpuData && !s.TooLarge) { + s.StateValue = State::PENDING_UPLOAD; + s.StateWasValid = false; + s.HasPlacement = false; + _enqueuePending(id); + _setEntryInvalid(id, /*diagPending*/true, /*diagTooLarge*/false); + } else { + // REGISTERED/TOO_LARGE и т.п. + s.HasPlacement = false; + s.StateWasValid = false; + if(s.TooLarge) { + _setEntryInvalid(id, /*diagPending*/false, /*diagTooLarge*/true); + } else { + _setEntryInvalid(id, /*diagPending*/false, /*diagTooLarge*/false); + } + } + } + } + + EntriesDirty_ = true; + + // Перестроить packer по placements нового атласа + _rebuildPackersFromPlacements(); + + // Сброс repack state + Repack_.Active = false; + Repack_.SwapReady = false; + Repack_.WaitingGpuForReady = false; + Repack_.Plan.clear(); + Repack_.Pending.clear(); + Repack_.InPending.clear(); + + _emitEventOncePerFlush(AtlasEvent::RepackFinished); + } + + // ============================= Layout/барьеры ============================= + + void _ensureImageLayoutForTransferDst(VkCommandBuffer cmd, ImageRes& img, bool& anyWritesFlag) { + if(img.Layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) { + _transitionImage(cmd, img, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + (img.Layout == VK_IMAGE_LAYOUT_UNDEFINED ? 0 : VK_ACCESS_SHADER_READ_BIT), + VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + } + anyWritesFlag = true; + } + + void _transitionImage(VkCommandBuffer cmd, + ImageRes& img, + VkImageLayout newLayout, + VkAccessFlags srcAccess, + VkAccessFlags dstAccess, + VkPipelineStageFlags srcStage, + VkPipelineStageFlags dstStage) { + if(img.Image == VK_NULL_HANDLE) return; + if(img.Layout == newLayout) return; + + VkImageMemoryBarrier b{}; + b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + b.oldLayout = img.Layout; + b.newLayout = newLayout; + b.srcAccessMask = srcAccess; + b.dstAccessMask = dstAccess; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.image = img.Image; + b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + b.subresourceRange.baseMipLevel = 0; + b.subresourceRange.levelCount = 1; + b.subresourceRange.baseArrayLayer = 0; + b.subresourceRange.layerCount = img.Layers; + + vkCmdPipelineBarrier(cmd, srcStage, dstStage, 0, 0, nullptr, 0, nullptr, 1, &b); + img.Layout = newLayout; + } + + // ============================= Vulkan create/destroy ============================= + + uint32_t _findMemoryType(uint32_t typeBits, VkMemoryPropertyFlags props) { + VkPhysicalDeviceMemoryProperties mp{}; + vkGetPhysicalDeviceMemoryProperties(Phys_, &mp); + for(uint32_t i = 0; i < mp.memoryTypeCount; ++i) { + if((typeBits & (1u << i)) && (mp.memoryTypes[i].propertyFlags & props) == props) { + return i; + } + } + throw std::runtime_error("TextureAtlas: no suitable memory type"); + } + + void _createEntriesBufferOrThrow() { + Entries_.Size = static_cast(Cfg_.MaxTextureId) * sizeof(Entry); + + VkBufferCreateInfo bi{}; + bi.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bi.size = Entries_.Size; + bi.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + bi.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if(vkCreateBuffer(Device_, &bi, nullptr, &Entries_.Buffer) != VK_SUCCESS) { + throw std::runtime_error("TextureAtlas: vkCreateBuffer(entries) failed"); + } + + VkMemoryRequirements mr{}; + vkGetBufferMemoryRequirements(Device_, Entries_.Buffer, &mr); + + VkMemoryAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + ai.allocationSize = mr.size; + ai.memoryTypeIndex = _findMemoryType(mr.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + + if(vkAllocateMemory(Device_, &ai, nullptr, &Entries_.Memory) != VK_SUCCESS) { + throw std::runtime_error("TextureAtlas: vkAllocateMemory(entries) failed"); + } + + vkBindBufferMemory(Device_, Entries_.Buffer, Entries_.Memory, 0); + } + + void _createSamplerOrThrow() { + VkSamplerCreateInfo si{}; + si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + si.magFilter = Cfg_.SamplerFilter; + si.minFilter = Cfg_.SamplerFilter; + si.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + si.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + si.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + si.mipLodBias = 0.0f; + si.anisotropyEnable = Cfg_.SamplerAnisotropyEnable ? VK_TRUE : VK_FALSE; + si.maxAnisotropy = Cfg_.SamplerAnisotropyEnable ? 4.0f : 1.0f; + si.compareEnable = VK_FALSE; + si.minLod = 0.0f; + si.maxLod = 0.0f; // mipLevels=1 + si.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; + si.unnormalizedCoordinates = VK_FALSE; + + if(vkCreateSampler(Device_, &si, nullptr, &Sampler_) != VK_SUCCESS) { + throw std::runtime_error("TextureAtlas: vkCreateSampler failed"); + } + } + + void _createAtlasOrThrow(uint32_t side, uint32_t layers) { + if(!_createAtlasNoThrow(side, layers, Atlas_)) { + throw std::runtime_error("TextureAtlas: create atlas failed (OOM?)"); + } + } + + bool _createAtlasNoThrow(uint32_t side, uint32_t layers, ImageRes& out) { + VkImageCreateInfo ii{}; + ii.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + ii.imageType = VK_IMAGE_TYPE_2D; + ii.format = VK_FORMAT_R8G8B8A8_UNORM; + ii.extent = { side, side, 1 }; + ii.mipLevels = 1; + ii.arrayLayers = layers; + ii.samples = VK_SAMPLE_COUNT_1_BIT; + ii.tiling = VK_IMAGE_TILING_OPTIMAL; + ii.usage = VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + ii.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + ii.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VkImage img = VK_NULL_HANDLE; + if(vkCreateImage(Device_, &ii, nullptr, &img) != VK_SUCCESS) { + return false; + } + + VkMemoryRequirements mr{}; + vkGetImageMemoryRequirements(Device_, img, &mr); + + VkMemoryAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + ai.allocationSize = mr.size; + uint32_t memType = 0; + try { + memType = _findMemoryType(mr.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + } catch (...) { + vkDestroyImage(Device_, img, nullptr); + return false; + } + ai.memoryTypeIndex = memType; + + VkDeviceMemory mem = VK_NULL_HANDLE; + if(vkAllocateMemory(Device_, &ai, nullptr, &mem) != VK_SUCCESS) { + vkDestroyImage(Device_, img, nullptr); + return false; + } + + vkBindImageMemory(Device_, img, mem, 0); + + VkImageViewCreateInfo vi{}; + vi.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + vi.image = img; + vi.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; + vi.format = VK_FORMAT_R8G8B8A8_UNORM; + vi.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + vi.subresourceRange.baseMipLevel = 0; + vi.subresourceRange.levelCount = 1; + vi.subresourceRange.baseArrayLayer = 0; + vi.subresourceRange.layerCount = layers; + + VkImageView view = VK_NULL_HANDLE; + if(vkCreateImageView(Device_, &vi, nullptr, &view) != VK_SUCCESS) { + vkFreeMemory(Device_, mem, nullptr); + vkDestroyImage(Device_, img, nullptr); + return false; + } + + out.Image = img; + out.Memory = mem; + out.View = view; + out.Layout = VK_IMAGE_LAYOUT_UNDEFINED; + out.Side = side; + out.Layers = layers; + return true; + } + + void _destroyImage(ImageRes& img) { + if(img.View) vkDestroyImageView(Device_, img.View, nullptr); + if(img.Image) vkDestroyImage(Device_, img.Image, nullptr); + if(img.Memory) vkFreeMemory(Device_, img.Memory, nullptr); + img = ImageRes{}; + } + + void _destroyBuffer(BufferRes& b) { + if(b.Buffer) + vkDestroyBuffer(Device_, b.Buffer, nullptr); + if(b.Memory) + vkFreeMemory(Device_, b.Memory, nullptr); + + b = BufferRes{}; + } + + void _shutdownNoThrow() { + if(!Alive_) return; + + // deferred + for(auto& img : DeferredImages_) _destroyImage(img); + DeferredImages_.clear(); + + // repack atlas (если был) + if(Repack_.Atlas.Image) { + _destroyImage(Repack_.Atlas); + } + + _destroyImage(Atlas_); + _destroyBuffer(Entries_); + + Staging_.reset(); + + if(OwnsSampler_ && Sampler_) { + vkDestroySampler(Device_, Sampler_, nullptr); + } + Sampler_ = VK_NULL_HANDLE; + + Alive_ = false; + } + + // ============================= Данные/поля ============================= + + VkDevice Device_ = VK_NULL_HANDLE; + VkPhysicalDevice Phys_ = VK_NULL_HANDLE; + Config Cfg_{}; + EventCallback OnEvent_; + + bool Alive_ = false; + + VkDeviceSize CopyOffsetAlignment_ = 4; + + std::shared_ptr Staging_; + BufferRes Entries_{}; + ImageRes Atlas_{}; + VkSampler Sampler_ = VK_NULL_HANDLE; + bool OwnsSampler_ = false; + + std::vector EntriesCpu_; + bool EntriesDirty_ = false; + + std::vector Slots_; + std::vector FreeIds_; + TextureId NextId_ = 0; + + // pending очередь (ленивое удаление) + std::deque Pending_; + std::vector PendingInQueue_ = std::vector(Cfg_.MaxTextureId, false); + + // packer по слоям + std::vector Packers_; + + // deferred destroy старых атласов + std::vector DeferredImages_; + + // события "один раз за Flush" + uint32_t FlushEventMask_ = 0; + uint32_t PendingLayerGrow_ = 0; + + // рост индикатор + bool GrewThisFlush_ = false; + + // repack state + RepackState Repack_; +}; diff --git a/Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp b/Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp new file mode 100644 index 0000000..0166ff5 --- /dev/null +++ b/Src/Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp @@ -0,0 +1,1271 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ======================== +// External texture view +// ======================== +struct Texture { + uint32_t Width, Height; + const uint32_t* Pixels; // assumed 0xAARRGGBB +}; + +// ======================== +// Bytecode words are uint16_t +// ======================== +class TexturePipelineProgram { +public: + using Word = uint16_t; + + struct OwnedTexture { + uint32_t Width = 0, Height = 0; + std::vector Pixels; + 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)>; + + // Patch points to two consecutive u16 words where uint32 texId lives (lo, hi) + struct Patch { + size_t WordIndexLo = 0; // Code_[lo], Code_[lo+1] is hi + std::string Name; + }; + + // ---- compile / link / bake ---- + bool compile(std::string src, std::string* err = nullptr) { + Source_ = std::move(src); + Code_.clear(); + Patches_.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; + return false; + } + if(p.WordIndexLo + 1 >= Code_.size()) { + if(err) *err = "Internal error: patch out of range"; + return false; + } + Code_[p.WordIndexLo + 0] = _lo16(*id); + Code_[p.WordIndexLo + 1] = _hi16(*id); + } + return true; + } + + bool bake(const TextureProvider& provider, OwnedTexture& out, std::string* err = nullptr) const { + VM vm(provider); + return vm.run(Code_, out, 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; + } + + void fromWords(std::vector words) { + Code_ = std::move(words); + Patches_.clear(); + Source_.clear(); + } + +private: + // ======================== + // Word helpers + // ======================== + static constexpr uint32_t _make_u32(uint16_t lo, uint16_t hi) { + return uint32_t(lo) | (uint32_t(hi) << 16); + } + 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 + // ======================== + enum class SrcKind : Word { TexId = 0, Sub = 1 }; + + struct SrcRef { + SrcKind Kind; + Word A; + Word B; + }; + + // ======================== + // Opcodes (fixed-length headers; some are variable like Combine) + // ======================== + enum class Op : Word { + 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 + + // 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) + + // 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) + + // Variable example (optional): Combine + // Combine: w,h, n, then n times: x,y, SrcRef (x,y,kind,a,b) + Combine = 40 + }; + + // ======================== + // Pixel helpers (assume 0xAARRGGBB) + // ======================== + static inline uint8_t _a(uint32_t c){ return uint8_t((c >> 24) & 0xFF); } + static inline uint8_t _r(uint32_t c){ return uint8_t((c >> 16) & 0xFF); } + static inline uint8_t _g(uint32_t c){ return uint8_t((c >> 8) & 0xFF); } + static inline uint8_t _b(uint32_t c){ return uint8_t((c >> 0) & 0xFF); } + static inline uint32_t _pack(uint8_t a,uint8_t r,uint8_t g,uint8_t b){ + return (uint32_t(a)<<24)|(uint32_t(r)<<16)|(uint32_t(g)<<8)|(uint32_t(b)); + } + static inline uint8_t _clampu8(int v){ return uint8_t(std::min(255, std::max(0, v))); } + + // ======================== + // VM (executes u16 words) + // ======================== + struct Image { + uint32_t W=0,H=0; + std::vector Px; + }; + + class VM { + public: + explicit VM(TextureProvider provider) : Provider_(std::move(provider)) {} + + bool run(const std::vector& code, OwnedTexture& out, 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) + + size_t ip = 0; + auto need = [&](size_t n)->bool{ + if(ip + n > code.size()) { if(err) *err="Bytecode truncated"; return false; } + return true; + }; + + while (true) { + if(!need(1)) return false; + Op op = static_cast(code[ip++]); + if(op == Op::End) break; + + switch (op) { + case Op::Base_Tex: { + if(!need(3)) return false; + SrcRef src = _readSrc(code, ip); + if(src.Kind != SrcKind::TexId) return _bad(err, "Base_Tex must be TexId"); + cur = _loadTex(_make_u32(src.A, src.B), 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); + cur = _makeSolid(w, h, color); + } break; + + case Op::Overlay: { + if(!need(3)) return false; + SrcRef src = _readSrc(code, ip); + Image over = _loadSrc(code, src, texCache, subCache, err); + if(over.W == 0) return false; + if(!cur.W) { cur = std::move(over); break; } // if no base, adopt + 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); + if(m.W == 0) return false; + if(!cur.W) return _bad(err, "Mask requires base image"); + m = _resizeNN_ifNeeded(m, cur.W, cur.H); + _applyMask(cur, m); + } break; + + case Op::LowPart: { + if(!need(1+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(over.W == 0) return false; + if(!cur.W) return _bad(err, "LowPart requires base image"); + over = _resizeNN_ifNeeded(over, cur.W, cur.H); + _lowpart(cur, over, pct); + } break; + + case Op::Resize: { + if(!need(2)) return false; + uint32_t w = code[ip++], h = code[ip++]; + if(!cur.W) return _bad(err, "Resize requires base image"); + cur = _resizeNN(cur, w, h); + } break; + + case Op::Transform: { + 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(!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; + + case Op::NoAlpha: { + if(!cur.W) return _bad(err, "NoAlpha requires base image"); + _noAlpha(cur); + } 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); + } break; + + case Op::Invert: { + 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; + + case Op::Brighten: { + if(!cur.W) return _bad(err, "Brighten requires base image"); + _brighten(cur); + } break; + + case Op::Contrast: { + 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"); + _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"); + _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"); + _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; + } + + Image _loadTex(uint32_t id, std::unordered_map& cache, std::string* err) { + auto it = cache.find(id); + if(it != cache.end()) return it->second; + auto t = Provider_(id); + if(!t || !t->Pixels || !t->Width || !t->Height) { + if(err) *err = "Texture id not found: " + std::to_string(id); + return {}; + } + Image img; + img.W = t->Width; img.H = t->Height; + img.Px.assign(t->Pixels, t->Pixels + size_t(img.W)*size_t(img.H)); + cache.emplace(id, img); + return img; + } + + Image _loadSub(const std::vector& code, + 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); + auto it = subCache.find(key); + if(it != subCache.end()) return it->second; + + size_t start = size_t(off); + size_t end = start + size_t(len); + if(end > code.size()) { if(err) *err="Subprogram out of range"; return {}; } + + // Run subprogram slice by copying minimal (simple + safe). + std::vector slice(code.begin()+start, code.begin()+end); + OwnedTexture tmp; + VM nested(Provider_); + if(!nested.run(slice, tmp, err)) return {}; + + Image img; + img.W = tmp.Width; img.H = tmp.Height; img.Px = std::move(tmp.Pixels); + subCache.emplace(key, img); + return img; + } + + Image _loadSrc(const std::vector& code, + const SrcRef& src, + std::unordered_map& texCache, + std::unordered_map& subCache, + 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); + } + if(err) *err = "Unknown SrcKind"; + return {}; + } + + // ---- image ops ---- + static Image _makeSolid(uint32_t w, uint32_t h, uint32_t color) { + Image img; img.W=w; img.H=h; + img.Px.assign(size_t(w)*size_t(h), color); + return img; + } + + static Image _resizeNN(const Image& src, uint32_t nw, uint32_t nh) { + Image dst; dst.W=nw; dst.H=nh; + dst.Px.resize(size_t(nw)*size_t(nh)); + for(uint32_t y=0;y(255, (outRp * 255) / outA)); + outG = uint8_t(std::min(255, (outGp * 255) / outA)); + outB = uint8_t(std::min(255, (outBp * 255) / outA)); + } + base.Px[i] = _pack(uint8_t(outA), outR, outG, outB); + } + } + + static void _overlayAt(Image& dst, const Image& src, int ox, int oy) { + for(uint32_t y=0;y= 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> 16) & 0xFF); + uint8_t gg = uint8_t((rgb24 >> 8) & 0xFF); + uint8_t bb = uint8_t((rgb24 >> 0) & 0xFF); + for(auto& p : img.Px) { + if(_r(p)==rr && _g(p)==gg && _b(p)==bb) p = _pack(0, _r(p), _g(p), _b(p)); + } + } + static void _invert(Image& img, uint32_t maskBits) { + for(auto& p : img.Px) { + uint8_t a=_a(p), r=_r(p), g=_g(p), b=_b(p); + if(maskBits & 1u) r = 255 - r; + if(maskBits & 2u) g = 255 - g; + if(maskBits & 4u) b = 255 - b; + if(maskBits & 8u) a = 255 - a; + p = _pack(a,r,g,b); + } + } + static void _brighten(Image& img) { + for(auto& p : img.Px) { + int r = _r(p), g = _g(p), b = _b(p); + r = r + (255 - r) / 3; + g = g + (255 - g) / 3; + b = b + (255 - b) / 3; + p = _pack(_a(p), _clampu8(r), _clampu8(g), _clampu8(b)); + } + } + static void _contrast(Image& img, int c, int br) { + double C = double(std::max(-127, std::min(127, c))); + double factor = (259.0 * (C + 255.0)) / (255.0 * (259.0 - C)); + for(auto& p : img.Px) { + int r = int(factor * (int(_r(p)) - 128) + 128) + br; + int g = int(factor * (int(_g(p)) - 128) + 128) + br; + int b = int(factor * (int(_b(p)) - 128) + 128) + br; + p = _pack(_a(p), _clampu8(r), _clampu8(g), _clampu8(b)); + } + } + static void _multiply(Image& img, uint32_t color) { + uint8_t cr=_r(color), cg=_g(color), cb=_b(color); + for(auto& p : img.Px) { + uint8_t r = uint8_t((uint32_t(_r(p)) * cr) / 255); + uint8_t g = uint8_t((uint32_t(_g(p)) * cg) / 255); + uint8_t b = uint8_t((uint32_t(_b(p)) * cb) / 255); + p = _pack(_a(p), r,g,b); + } + } + static void _screen(Image& img, uint32_t color) { + uint8_t cr=_r(color), cg=_g(color), cb=_b(color); + for(auto& p : img.Px) { + uint8_t r = uint8_t(255 - ((255 - _r(p)) * (255 - cr)) / 255); + uint8_t g = uint8_t(255 - ((255 - _g(p)) * (255 - cg)) / 255); + uint8_t b = uint8_t(255 - ((255 - _b(p)) * (255 - cb)) / 255); + p = _pack(_a(p), r,g,b); + } + } + static void _colorize(Image& img, uint32_t color, uint8_t ratio) { + uint8_t cr=_r(color), cg=_g(color), cb=_b(color); + for(auto& p : img.Px) { + int r = (int(_r(p)) * (255 - ratio) + int(cr) * ratio) / 255; + int g = (int(_g(p)) * (255 - ratio) + int(cg) * ratio) / 255; + int b = (int(_b(p)) * (255 - ratio) + int(cb) * ratio) / 255; + p = _pack(_a(p), uint8_t(r), uint8_t(g), uint8_t(b)); + } + } + static void _lowpart(Image& base, const Image& over, uint32_t percent) { + uint32_t startY = base.H - (base.H * percent) / 100; + for(uint32_t y=startY; y(255, (outRp * 255) / outA)); + outG = uint8_t(std::min(255, (outGp * 255) / outA)); + outB = uint8_t(std::min(255, (outBp * 255) / outA)); + } + base.Px[i] = _pack(uint8_t(outA), outR, outG, outB); + } + } + } + + static Image _transform(const Image& src, uint32_t t) { + Image dst; + auto at = [&](uint32_t x, uint32_t y)->uint32_t { return src.Px[size_t(y)*src.W + x]; }; + auto make = [&](uint32_t w, uint32_t h){ + Image d; d.W=w; d.H=h; d.Px.resize(size_t(w)*size_t(h)); + return d; + }; + auto set = [&](Image& im, uint32_t x, uint32_t y, uint32_t v){ + im.Px[size_t(y)*im.W + x] = v; + }; + + switch (t & 7u) { + case 0: return src; + case 1: { dst = make(src.H, src.W); + for(uint32_t y=0;y op(args...) + // tex 32x32 "#RRGGBBAA" |> ... + // Grouping (subprogram) only where an op expects a texture arg: + // overlay( tex "b" |> ... ) + // ======================== + enum class TokKind { End, Ident, Number, String, Pipe, Comma, LParen, RParen, Eq, X }; + + struct Tok { + TokKind Kind = TokKind::End; + std::string Text; + uint32_t U32 = 0; + }; + + struct Lexer { + std::string_view S; + size_t I=0; + + 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 skipWs() { + while (I < S.size()) { + char c = S[I]; + if(c==' '||c=='\t'||c=='\r'||c=='\n'){ I++; continue; } + if(c=='#'){ while (I= S.size()) return {TokKind::End, {}, 0}; + + if(S[I]=='|' && I+1') { I+=2; return {TokKind::Pipe, "|>",0}; } + char c = S[I]; + if(c==','){ I++; return {TokKind::Comma,",",0}; } + if(c=='('){ I++; return {TokKind::LParen,"(",0}; } + if(c==')'){ I++; return {TokKind::RParen,")",0}; } + if(c=='='){ I++; return {TokKind::Eq,"=",0}; } + if(c=='x' || c=='X'){ I++; return {TokKind::X,"x",0}; } + + if(c=='"') { + I++; + std::string out; + while (I < S.size()) { + char ch = S[I++]; + if(ch=='"') break; + if(ch=='\\' && I Pos; + std::unordered_map Named; + // 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 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)); } + + 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}); + } + + void _emitSrcRef(const SrcRef& r) { + Code_.push_back(Word(r.Kind)); + Code_.push_back(r.A); + Code_.push_back(r.B); + } + + // ======================== + // Color parsing: #RRGGBB or #RRGGBBAA + // Stored as 0xAARRGGBB + // ======================== + static bool _parseHexColor(std::string_view s, uint32_t& outARGB) { + if(s.size()!=7 && s.size()!=9) return false; + if(s[0] != '#') return false; + auto hex = [](char c)->int{ + if(c>='0'&&c<='9') return c-'0'; + if(c>='a'&&c<='f') return 10+(c-'a'); + if(c>='A'&&c<='F') return 10+(c-'A'); + return -1; + }; + auto byteAt = [&](size_t idx)->std::optional{ + int hi=hex(s[idx]), lo=hex(s[idx+1]); + if(hi<0||lo<0) return std::nullopt; + return uint8_t((hi<<4)|lo); + }; + auto r = byteAt(1), g = byteAt(3), b = byteAt(5); + if(!r||!g||!b) return false; + uint8_t a = 255; + if(s.size()==9) { + auto aa = byteAt(7); + if(!aa) return false; + a = *aa; + } + outARGB = (uint32_t(a)<<24) | (uint32_t(*r)<<16) | (uint32_t(*g)<<8) | (uint32_t(*b)); + return true; + } + + // ======================== + // Parsing entry: full program + // ======================== + bool _parseProgram(std::string* err) { + Lexer lx{Source_}; + Tok t = lx.next(); + if(!(t.Kind==TokKind::Ident && t.Text=="tex")) { + if(err) *err="Expected 'tex' at start"; + return false; + } + + // Parse base expression after tex: + // 1) "name" + // 2) Number X Number Ident(color) + // 3) (future) png("...") + 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) { + // tex 32x32 "#RRGGBBAA" + Tok xTok = lx.next(); + Tok b = lx.next(); + Tok colTok = lx.next(); + if(xTok.Kind != TokKind::X || b.Kind != TokKind::Number || (colTok.Kind!=TokKind::Ident && colTok.Kind!=TokKind::String)) { + if(err) *err="Expected: tex x <#color>"; + return false; + } + uint32_t w = a.U32, h = b.U32; + uint32_t color = 0; + if(!_parseHexColor(colTok.Text, color)) { + if(err) *err="Bad color literal (use #RRGGBB or #RRGGBBAA)"; + return false; + } + if(w>65535u || h>65535u) { if(err) *err="w/h must fit in uint16"; return false; } + _emit(Op::Base_Fill); + _emitW(w); _emitW(h); + _emitU32(color); + } else { + if(err) *err="Bad 'tex' base expression"; + 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 { + // 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; + } + + // Parses either: + // - normal args list: (a,b,key=v) + // - OR for ops that take texture, allow: ( tex ... |> ... ) as the *first* positional "special" + 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(!_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 '(' + Tok t = first; + if(t.Kind == TokKind::RParen) return true; + + while (true) { + if(t.Kind == TokKind::Ident) { + Tok maybeEq = lx.next(); + if(maybeEq.Kind == TokKind::Eq) { + Tok v = lx.next(); + ArgVal av; + if(!_tokToVal(v, av, err)) return false; + op.Named[t.Text] = std::move(av); + t = lx.next(); + } else { + ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = t.Text; + op.Pos.push_back(std::move(av)); + t = maybeEq; + } + } else { + ArgVal av; + if(!_tokToVal(t, av, err)) return false; + op.Pos.push_back(std::move(av)); + t = lx.next(); + } + + if(t.Kind == TokKind::Comma) { t = lx.next(); continue; } + if(t.Kind == TokKind::RParen) return true; + + if(err) *err = "Expected ',' or ')' in argument list"; + return false; + } + } + + bool _tokToVal(const Tok& t, ArgVal& out, std::string* err) { + if(t.Kind == TokKind::Number) { out.Kind=ArgVal::ValueKind::U32; out.U32=t.U32; return true; } + if(t.Kind == TokKind::String) { out.Kind=ArgVal::ValueKind::Str; out.S=t.Text; return true; } + if(t.Kind == TokKind::Ident) { out.Kind=ArgVal::ValueKind::Ident; out.S=t.Text; return true; } + if(err) *err = "Expected value token"; + return false; + } + + // ======================== + // Subprogram compilation + // We already consumed 'tex' token. Now we parse base + pipeline until we hit ')' + // Strategy: + // - compile into `sub` vector + // - stop when next token would be ')' + // - 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 + }; + + 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"; + 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(); + 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 + } + + // compile op into `sub` by temporarily swapping buffers + if(!_compileOpInto(lx, op, sub, emitS, emitSW, emitSU32, emitSTexName, err)) return false; + } + + emitS(Op::End); + return true; + } + + // Temporary relative patches inside subprogram being built + struct RelPatch { size_t RelLo; std::string Name; }; + mutable std::vector SubPatchesTemp_; + + // Stash compiled subprogram per op pointer (simplifies this one-file example) + mutable std::unordered_map> PendingSub_; + + // 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}); + } + SubPatchesTemp_.clear(); + + Code_.insert(Code_.end(), sub.begin(), sub.end()); + + SrcRef r; + r.Kind = SrcKind::Sub; + r.A = Word(offset & 0xFFFFu); + r.B = Word(len & 0xFFFFu); + return r; + } + + // ======================== + // compile operations + // ======================== + 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) { + auto posU = [&](size_t i)->std::optional{ + if(i >= op.Pos.size()) return std::nullopt; + if(op.Pos[i].Kind != ArgVal::ValueKind::U32) return std::nullopt; + return op.Pos[i].U32; + }; + auto posS = [&](size_t i)->std::optional{ + if(i >= op.Pos.size()) return std::nullopt; + return op.Pos[i].S; + }; + auto namedU = [&](std::string_view k)->std::optional{ + auto it = op.Named.find(std::string(k)); + if(it==op.Named.end() || it->second.Kind!=ArgVal::ValueKind::U32) return std::nullopt; + return it->second.U32; + }; + auto namedS = [&](std::string_view k)->std::optional{ + auto it = op.Named.find(std::string(k)); + if(it==op.Named.end()) return std::nullopt; + return it->second.S; + }; + + auto emitSrcTexName = [&](const std::string& texName){ + // SrcRef(TexId): kind + id(lo/hi) + out.push_back(Word(SrcKind::TexId)); + emitTexNameFn(texName); + }; + + 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)); + return true; + }; + + // --- Ops that accept a "texture" argument: overlay/mask/lowpart/combine parts --- + 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(); + } + 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); + return true; + } + + if(op.Name == "mask") { + emitOpFn(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); + 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)); + 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); + return true; + } + + // --- Unary ops --- + if(op.Name == "resize") { + uint32_t w = namedU("w").value_or(posU(0).value_or(0)); + uint32_t h = namedU("h").value_or(posU(1).value_or(0)); + if(!w || !h || w>65535u || h>65535u) { if(err) *err="resize(w,h) must fit uint16"; return false; } + emitOpFn(Op::Resize); emitWFn(w); emitWFn(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); + return true; + } + + if(op.Name == "opacity") { + uint32_t a = namedU("a").value_or(posU(0).value_or(255)); + emitOpFn(Op::Opacity); emitWFn(a & 0xFFu); + return true; + } + + if(op.Name == "remove_alpha" || op.Name == "noalpha") { + emitOpFn(Op::NoAlpha); + return true; + } + + if(op.Name == "make_alpha") { + std::string col = namedS("color").value_or(posS(0).value_or("")); + uint32_t argb=0; + if(!_parseHexColor(col, argb)) { if(err) *err="make_alpha requires color #RRGGBB"; return false; } + uint32_t rgb24 = argb & 0x00FFFFFFu; + // pack rgb24 into two u16: lo=0xRRGG, hi=0x00BB + emitOpFn(Op::MakeAlpha); + emitWFn((rgb24 >> 8) & 0xFFFFu); // RR GG + emitWFn(rgb24 & 0x00FFu); // BB + return true; + } + + if(op.Name == "invert") { + std::string ch = namedS("channels").value_or(posS(0).value_or("rgb")); + uint32_t mask=0; + for(char c : ch) { + if(c=='r') mask |= 1; + if(c=='g') mask |= 2; + if(c=='b') mask |= 4; + if(c=='a') mask |= 8; + } + emitOpFn(Op::Invert); emitWFn(mask); + return true; + } + + if(op.Name == "brighten") { + emitOpFn(Op::Brighten); + return true; + } + + if(op.Name == "contrast") { + int c = int(namedU("value").value_or(posU(0).value_or(0))); + int b = int(namedU("brightness").value_or(posU(1).value_or(0))); + c = std::max(-127, std::min(127, c)); + b = std::max(-127, std::min(127, b)); + emitOpFn(Op::Contrast); + emitWFn(uint32_t(c + 127)); + emitWFn(uint32_t(b + 127)); + return true; + } + + auto compileColorOp = [&](Op opcode, bool needsRatio)->bool{ + std::string col = namedS("color").value_or(posS(0).value_or("")); + uint32_t argb=0; + if(!_parseHexColor(col, argb)) { if(err) *err="Bad color literal"; return false; } + emitOpFn(opcode); + emitU32Fn(argb); + if(needsRatio) { + uint32_t ratio = namedU("ratio").value_or(posU(1).value_or(255)); + emitWFn(ratio & 0xFFu); + } + return true; + }; + + if(op.Name == "multiply") return compileColorOp(Op::Multiply, false); + if(op.Name == "screen") return compileColorOp(Op::Screen, false); + if(op.Name == "colorize") return compileColorOp(Op::Colorize, true); + + if(err) *err = "Unknown op: " + op.Name; + return false; + } +}; diff --git a/Src/Client/Vulkan/VertexPool.hpp b/Src/Client/Vulkan/VertexPool.hpp index 25e4071..12bee19 100644 --- a/Src/Client/Vulkan/VertexPool.hpp +++ b/Src/Client/Vulkan/VertexPool.hpp @@ -1,12 +1,43 @@ #pragma once #include "Vulkan.hpp" +#include "Client/Vulkan/AtlasPipeline/SharedStagingBuffer.hpp" +#include #include +#include +#include +#include +#include +#include #include namespace LV::Client::VK { +inline std::weak_ptr& globalVertexStaging() { + static std::weak_ptr staging; + return staging; +} + +inline std::shared_ptr getOrCreateVertexStaging(Vulkan* inst) { + auto& staging = globalVertexStaging(); + std::shared_ptr shared = staging.lock(); + if(!shared) { + shared = std::make_shared( + inst->Graphics.Device, + inst->Graphics.PhysicalDevice + ); + staging = shared; + } + return shared; +} + +inline void resetVertexStaging() { + auto& staging = globalVertexStaging(); + if(auto shared = staging.lock()) + shared->Reset(); +} + /* Память на устройстве выделяется пулами Для массивов вершин память выделяется блоками по PerBlock вершин в каждом @@ -22,10 +53,8 @@ class VertexPool { Vulkan *Inst; // Память, доступная для обмена с устройством - Buffer HostCoherent; - Vertex *HCPtr = nullptr; - VkFence Fence = nullptr; - size_t WritePos = 0; + std::shared_ptr Staging; + VkDeviceSize CopyOffsetAlignment = 4; struct Pool { // Память на устройстве @@ -47,7 +76,6 @@ class VertexPool { struct Task { std::vector Data; - size_t Pos = -1; // Если данные уже записаны, то будет указана позиция в буфере общения uint8_t PoolId; // Куда потом направить uint16_t BlockId; // И в какой блок }; @@ -61,46 +89,21 @@ class VertexPool { private: void pushData(std::vector&& data, uint8_t poolId, uint16_t blockId) { - if(HC_Buffer_Size-WritePos >= data.size()) { - // Пишем в общий буфер, TasksWait - Vertex *ptr = HCPtr+WritePos; - std::copy(data.begin(), data.end(), ptr); - size_t count = data.size(); - TasksWait.push({std::move(data), WritePos, poolId, blockId}); - WritePos += count; - } else { - // Отложим запись на следующий такт - TasksPostponed.push(Task(std::move(data), -1, poolId, blockId)); - } + TasksWait.push({std::move(data), poolId, blockId}); } public: VertexPool(Vulkan* inst) - : Inst(inst), - HostCoherent(inst, - sizeof(Vertex)*HC_Buffer_Size+4 /* Для vkCmdFillBuffer */, - VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) + : Inst(inst) { Pools.reserve(16); - HCPtr = (Vertex*) HostCoherent.mapMemory(); - - const VkFenceCreateInfo info = { - .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, - .pNext = nullptr, - .flags = 0 - }; - - vkAssert(!vkCreateFence(inst->Graphics.Device, &info, nullptr, &Fence)); + Staging = getOrCreateVertexStaging(inst); + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(inst->Graphics.PhysicalDevice, &props); + CopyOffsetAlignment = std::max(4, props.limits.optimalBufferCopyOffsetAlignment); } ~VertexPool() { - if(HCPtr) - HostCoherent.unMapMemory(); - - if(Fence) { - vkDestroyFence(Inst->Graphics.Device, Fence, nullptr); - } } @@ -229,44 +232,65 @@ public: } /* - Должно вызываться после приёма всех данных и перед рендером + Должно вызываться после приёма всех данных, до начала рендера в командном буфере */ - void update(VkCommandPool commandPool) { + void flushUploadsAndBarriers(VkCommandBuffer commandBuffer) { if(TasksWait.empty()) return; - assert(WritePos); - - VkCommandBufferAllocateInfo allocInfo { - VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - nullptr, - commandPool, - VK_COMMAND_BUFFER_LEVEL_PRIMARY, - 1 + struct CopyTask { + VkBuffer DstBuffer; + VkDeviceSize SrcOffset; + VkDeviceSize DstOffset; + VkDeviceSize Size; + uint8_t PoolId; }; - VkCommandBuffer commandBuffer; - vkAllocateCommandBuffers(Inst->Graphics.Device, &allocInfo, &commandBuffer); + std::vector copies; + copies.reserve(TasksWait.size()); + std::vector touchedPools(Pools.size(), 0); - VkCommandBufferBeginInfo beginInfo { - VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - nullptr, - VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, - nullptr - }; + while(!TasksWait.empty()) { + Task task = std::move(TasksWait.front()); + TasksWait.pop(); - vkBeginCommandBuffer(commandBuffer, &beginInfo); + VkDeviceSize bytes = task.Data.size()*sizeof(Vertex); + std::optional stagingOffset = Staging->Allocate(bytes, CopyOffsetAlignment); + if(!stagingOffset) { + TasksPostponed.push(std::move(task)); + while(!TasksWait.empty()) { + TasksPostponed.push(std::move(TasksWait.front())); + TasksWait.pop(); + } + break; + } - VkBufferMemoryBarrier barrier = { + std::memcpy(static_cast(Staging->Mapped()) + *stagingOffset, + task.Data.data(), bytes); + + copies.push_back({ + Pools[task.PoolId].DeviceBuff.getBuffer(), + *stagingOffset, + task.BlockId*sizeof(Vertex)*size_t(PerBlock), + bytes, + task.PoolId + }); + touchedPools[task.PoolId] = 1; + } + + if(copies.empty()) + return; + + VkBufferMemoryBarrier stagingBarrier = { VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, nullptr, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, - HostCoherent.getBuffer(), + Staging->Buffer(), 0, - WritePos*sizeof(Vertex) + Staging->Size() }; vkCmdPipelineBarrier( @@ -275,53 +299,60 @@ public: VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, - 1, &barrier, + 1, &stagingBarrier, 0, nullptr ); - while(!TasksWait.empty()) { - Task& task = TasksWait.front(); - + for(const CopyTask& copy : copies) { VkBufferCopy copyRegion { - task.Pos*sizeof(Vertex), - task.BlockId*sizeof(Vertex)*size_t(PerBlock), - task.Data.size()*sizeof(Vertex) + copy.SrcOffset, + copy.DstOffset, + copy.Size }; - assert(copyRegion.dstOffset+copyRegion.size < sizeof(Vertex)*PerBlock*PerPool); + assert(copyRegion.dstOffset+copyRegion.size <= Pools[copy.PoolId].DeviceBuff.getSize()); - vkCmdCopyBuffer(commandBuffer, HostCoherent.getBuffer(), Pools[task.PoolId].DeviceBuff.getBuffer(), - 1, ©Region); - - TasksWait.pop(); + vkCmdCopyBuffer(commandBuffer, Staging->Buffer(), copy.DstBuffer, 1, ©Region); } - vkEndCommandBuffer(commandBuffer); + std::vector dstBarriers; + dstBarriers.reserve(Pools.size()); + for(size_t poolId = 0; poolId < Pools.size(); poolId++) { + if(!touchedPools[poolId]) + continue; - VkSubmitInfo submitInfo { - VK_STRUCTURE_TYPE_SUBMIT_INFO, - nullptr, - 0, nullptr, - nullptr, - 1, - &commandBuffer, - 0, - nullptr - }; - { - auto lockQueue = Inst->Graphics.DeviceQueueGraphic.lock(); - vkAssert(!vkQueueSubmit(*lockQueue, 1, &submitInfo, Fence)); + VkBufferMemoryBarrier barrier = { + VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, + nullptr, + VK_ACCESS_TRANSFER_WRITE_BIT, + IsIndex ? VK_ACCESS_INDEX_READ_BIT : VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, + VK_QUEUE_FAMILY_IGNORED, + VK_QUEUE_FAMILY_IGNORED, + Pools[poolId].DeviceBuff.getBuffer(), + 0, + Pools[poolId].DeviceBuff.getSize() + }; + dstBarriers.push_back(barrier); } - vkAssert(!vkWaitForFences(Inst->Graphics.Device, 1, &Fence, VK_TRUE, UINT64_MAX)); - vkAssert(!vkResetFences(Inst->Graphics.Device, 1, &Fence)); - vkFreeCommandBuffers(Inst->Graphics.Device, commandPool, 1, &commandBuffer); + if(!dstBarriers.empty()) { + vkCmdPipelineBarrier( + commandBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, + 0, + 0, nullptr, + static_cast(dstBarriers.size()), + dstBarriers.data(), + 0, nullptr + ); + } + } + + void notifyGpuFinished() { std::queue postponed = std::move(TasksPostponed); - WritePos = 0; - while(!postponed.empty()) { - Task& task = postponed.front(); - pushData(std::move(task.Data), task.PoolId, task.BlockId); + TasksWait.push(std::move(postponed.front())); postponed.pop(); } } @@ -330,4 +361,4 @@ public: template using IndexPool = VertexPool; -} \ No newline at end of file +} diff --git a/Src/Client/Vulkan/Vulkan.cpp b/Src/Client/Vulkan/Vulkan.cpp index a62ac14..c2d4a1e 100644 --- a/Src/Client/Vulkan/Vulkan.cpp +++ b/Src/Client/Vulkan/Vulkan.cpp @@ -275,10 +275,6 @@ void Vulkan::run() // if(CallBeforeDraw) // CallBeforeDraw(this); - if(Game.RSession) { - Game.RSession->beforeDraw(); - } - glfwPollEvents(); VkResult err; @@ -314,6 +310,10 @@ void Vulkan::run() vkAssert(!vkBeginCommandBuffer(Graphics.CommandBufferRender, &cmd_buf_info)); } + if(Game.RSession) { + Game.RSession->beforeDraw(); + } + { VkImageMemoryBarrier image_memory_barrier = { @@ -602,6 +602,8 @@ void Vulkan::run() // Насильно ожидаем завершения рендера кадра vkWaitForFences(Graphics.Device, 1, &drawEndFence, true, -1); vkResetFences(Graphics.Device, 1, &drawEndFence); + if(Game.RSession) + Game.RSession->onGpuFinished(); } { diff --git a/Src/Client/Vulkan/VulkanRenderSession.cpp b/Src/Client/Vulkan/VulkanRenderSession.cpp index 9038c27..1a61513 100644 --- a/Src/Client/Vulkan/VulkanRenderSession.cpp +++ b/Src/Client/Vulkan/VulkanRenderSession.cpp @@ -211,7 +211,7 @@ void ChunkMeshGenerator::run(uint8_t id) { } else { for(int z = 0; z < 16; z++) for(int y = 0; y < 16; y++) - fullNodes[17][y+1][z+1] = 1; + fullNodes[17][y+1][z+1] = 0; } if(chunks[1]) { @@ -223,7 +223,7 @@ void ChunkMeshGenerator::run(uint8_t id) { } else { for(int z = 0; z < 16; z++) for(int y = 0; y < 16; y++) - fullNodes[0][y+1][z+1] = 1; + fullNodes[0][y+1][z+1] = 0; } if(chunks[2]) { @@ -235,7 +235,7 @@ void ChunkMeshGenerator::run(uint8_t id) { } else { for(int z = 0; z < 16; z++) for(int x = 0; x < 16; x++) - fullNodes[x+1][17][z+1] = 1; + fullNodes[x+1][17][z+1] = 0; } if(chunks[3]) { @@ -247,7 +247,7 @@ void ChunkMeshGenerator::run(uint8_t id) { } else { for(int z = 0; z < 16; z++) for(int x = 0; x < 16; x++) - fullNodes[x+1][0][z+1] = 1; + fullNodes[x+1][0][z+1] = 0; } if(chunks[4]) { @@ -259,7 +259,7 @@ void ChunkMeshGenerator::run(uint8_t id) { } else { for(int y = 0; y < 16; y++) for(int x = 0; x < 16; x++) - fullNodes[x+1][y+1][17] = 1; + fullNodes[x+1][y+1][17] = 0; } if(chunks[5]) { @@ -271,7 +271,7 @@ void ChunkMeshGenerator::run(uint8_t id) { } else { for(int y = 0; y < 16; y++) for(int x = 0; x < 16; x++) - fullNodes[x+0][y+1][0] = 1; + fullNodes[x+0][y+1][0] = 0; } } else goto end; @@ -307,6 +307,72 @@ void ChunkMeshGenerator::run(uint8_t id) { NodeVertexStatic v; std::memset(&v, 0, sizeof(v)); + struct ModelCacheEntry { + std::vector>>>> Routes; + }; + + std::unordered_map modelCache; + std::unordered_map baseTextureCache; + + std::vector metaStatesInfo; + { + NodeStateInfo info; + info.Name = "meta"; + info.Variations = 256; + metaStatesInfo.push_back(std::move(info)); + } + + auto isFaceCovered = [&](EnumFace face, int covered) -> bool { + switch(face) { + case EnumFace::Up: return covered & (1 << 2); + case EnumFace::Down: return covered & (1 << 3); + case EnumFace::East: return covered & (1 << 0); + case EnumFace::West: return covered & (1 << 1); + case EnumFace::South: return covered & (1 << 4); + case EnumFace::North: return covered & (1 << 5); + default: return false; + } + }; + + auto pickVariant = [&](const std::vector>>>& variants, uint32_t seed) + -> const std::unordered_map>* + { + if(variants.empty()) + return nullptr; + + float total = 0.0f; + for(const auto& entry : variants) + total += std::max(0.0f, entry.first); + + if(total <= 0.0f) + return &variants.front().second; + + float r = (seed % 10000u) / 10000.0f * total; + float accum = 0.0f; + for(const auto& entry : variants) { + accum += std::max(0.0f, entry.first); + if(r <= accum) + return &entry.second; + } + + return &variants.back().second; + }; + + auto appendModel = [&](const std::unordered_map>& faces, int covered, int x, int y, int z) { + for(const auto& [face, verts] : faces) { + if(face != EnumFace::None && isFaceCovered(face, covered)) + continue; + + for(const NodeVertexStatic& baseVert : verts) { + NodeVertexStatic vert = baseVert; + vert.FX = uint32_t(vert.FX + x * 64); + vert.FY = uint32_t(vert.FY + y * 64); + vert.FZ = uint32_t(vert.FZ + z * 64); + result.NodeVertexs.push_back(vert); + } + } + }; + // Сбор вершин for(int z = 0; z < 16; z++) for(int y = 0; y < 16; y++) @@ -323,208 +389,259 @@ void ChunkMeshGenerator::run(uint8_t id) { if(fullCovered == 0b111111) continue; - const DefNode_t* node = getNodeProfile((*chunk)[x+y*16+z*16*16].NodeId); + const Node& nodeData = (*chunk)[x+y*16+z*16*16]; + const DefNode_t* node = getNodeProfile(nodeData.NodeId); - v.Tex = node->TexId; + bool usedModel = false; + + if(NSP && node->NodestateId != 0) { + auto iterCache = modelCache.find(nodeData.Data); + if(iterCache == modelCache.end()) { + std::unordered_map states; + states.emplace("meta", nodeData.Meta); + + ModelCacheEntry entry; + entry.Routes = NSP->getModelsForNode(node->NodestateId, metaStatesInfo, states); + iterCache = modelCache.emplace(nodeData.Data, std::move(entry)).first; + } + + if(!iterCache->second.Routes.empty()) { + uint32_t seed = uint32_t(nodeData.Data) * 2654435761u; + seed ^= uint32_t(x) * 73856093u; + seed ^= uint32_t(y) * 19349663u; + seed ^= uint32_t(z) * 83492791u; + + for(size_t routeIndex = 0; routeIndex < iterCache->second.Routes.size(); routeIndex++) { + const auto& variants = iterCache->second.Routes[routeIndex]; + const auto* faces = pickVariant(variants, seed + uint32_t(routeIndex) * 374761393u); + if(faces) + appendModel(*faces, fullCovered, x, y, z); + } + + usedModel = true; + } + } + + if(usedModel) + continue; + + if(NSP && node->TexId != 0) { + auto iterTex = baseTextureCache.find(node->TexId); + if(iterTex != baseTextureCache.end()) { + v.Tex = iterTex->second; + } else { + uint16_t resolvedTex = NSP->getTextureId(node->TexId); + v.Tex = resolvedTex; + baseTextureCache.emplace(node->TexId, resolvedTex); + } + } else { + v.Tex = node->TexId; + } if(v.Tex == 0) continue; // Рендерим обычный кубоид + // XZ+Y if(!(fullCovered & 0b000100)) { - v.FX = 224+x*16; - v.FY = 224+y*16+16; - v.FZ = 224+z*16+16; + v.FX = 224+x*64; + v.FY = 224+y*64+64; + v.FZ = 224+z*64+64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FX += 16; + v.FX += 64; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FZ -= 16; + v.FZ -= 64; v.TV = 65535; result.NodeVertexs.push_back(v); - v.FX = 224+x*16; - v.FZ = 224+z*16+16; + v.FX = 224+x*64; + v.FZ = 224+z*64+64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FX += 16; - v.FZ -= 16; + v.FX += 64; + v.FZ -= 64; v.TV = 65535; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FX -= 16; + v.FX -= 64; v.TU = 0; result.NodeVertexs.push_back(v); } + // XZ-Y if(!(fullCovered & 0b001000)) { - v.FX = 224+x*16; - v.FY = 224+y*16; - v.FZ = 224+z*16+16; + v.FX = 224+x*64; + v.FY = 224+y*64; + v.FZ = 224+z*64+64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FZ -= 16; + v.FZ -= 64; v.TV = 65535; result.NodeVertexs.push_back(v); - v.FX += 16; + v.FX += 64; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FX = 224+x*16; - v.FZ = 224+z*16+16; + v.FX = 224+x*64; + v.FZ = 224+z*64+64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FX += 16; - v.FZ -= 16; + v.FX += 64; + v.FZ -= 64; v.TV = 65535; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FZ += 16; + v.FZ += 64; v.TV = 0; result.NodeVertexs.push_back(v); } + //YZ+X if(!(fullCovered & 0b000001)) { - v.FX = 224+x*16+16; - v.FY = 224+y*16; - v.FZ = 224+z*16+16; + v.FX = 224+x*64+64; + v.FY = 224+y*64; + v.FZ = 224+z*64+64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FZ -= 16; + v.FZ -= 64; v.TV = 65535; result.NodeVertexs.push_back(v); - v.FY += 16; + v.FY += 64; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FY = 224+y*16; - v.FZ = 224+z*16+16; + v.FY = 224+y*64; + v.FZ = 224+z*64+64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FY += 16; - v.FZ -= 16; + v.FY += 64; + v.FZ -= 64; v.TV = 65535; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FZ += 16; + v.FZ += 64; v.TV = 0; result.NodeVertexs.push_back(v); } + //YZ-X if(!(fullCovered & 0b000010)) { - v.FX = 224+x*16; - v.FY = 224+y*16; - v.FZ = 224+z*16+16; + v.FX = 224+x*64; + v.FY = 224+y*64; + v.FZ = 224+z*64+64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FY += 16; + v.FY += 64; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FZ -= 16; + v.FZ -= 64; v.TV = 65535; result.NodeVertexs.push_back(v); - v.FY = 224+y*16; - v.FZ = 224+z*16+16; + v.FY = 224+y*64; + v.FZ = 224+z*64+64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FY += 16; - v.FZ -= 16; + v.FY += 64; + v.FZ -= 64; v.TV = 65535; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FY -= 16; + v.FY -= 64; v.TU = 0; result.NodeVertexs.push_back(v); } + //XY+Z if(!(fullCovered & 0b010000)) { - v.FX = 224+x*16; - v.FY = 224+y*16; - v.FZ = 224+z*16+16; + v.FX = 224+x*64; + v.FY = 224+y*64; + v.FZ = 224+z*64+64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FX += 16; + v.FX += 64; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FY += 16; + v.FY += 64; v.TV = 65535; result.NodeVertexs.push_back(v); - v.FX = 224+x*16; - v.FY = 224+y*16; + v.FX = 224+x*64; + v.FY = 224+y*64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FX += 16; - v.FY += 16; + v.FX += 64; + v.FY += 64; v.TV = 65535; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FX -= 16; + v.FX -= 64; v.TU = 0; result.NodeVertexs.push_back(v); } + // XY-Z if(!(fullCovered & 0b100000)) { - v.FX = 224+x*16; - v.FY = 224+y*16; - v.FZ = 224+z*16; + v.FX = 224+x*64; + v.FY = 224+y*64; + v.FZ = 224+z*64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FY += 16; + v.FY += 64; v.TV = 65535; result.NodeVertexs.push_back(v); - v.FX += 16; + v.FX += 64; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FX = 224+x*16; - v.FY = 224+y*16; + v.FX = 224+x*64; + v.FY = 224+y*64; v.TU = 0; v.TV = 0; result.NodeVertexs.push_back(v); - v.FX += 16; - v.FY += 16; + v.FX += 64; + v.FY += 64; v.TV = 65535; v.TU = 65535; result.NodeVertexs.push_back(v); - v.FY -= 16; + v.FY -= 64; v.TV = 0; result.NodeVertexs.push_back(v); } @@ -563,7 +680,6 @@ void ChunkMeshGenerator::run(uint8_t id) { } } } - end: Output.lock()->emplace_back(std::move(result)); @@ -588,10 +704,74 @@ void ChunkPreparator::tickSync(const TickSyncData& data) { // Пересчёт соседних чанков // Проверить необходимость пересчёта чанков при изменении профилей + std::unordered_map> changedChunks = data.ChangedChunks; + + if(!data.ChangedNodes.empty()) { + std::unordered_set changedNodes(data.ChangedNodes.begin(), data.ChangedNodes.end()); + + for(const auto& [wId, regions] : ChunksMesh) { + for(const auto& [rPos, chunks] : regions) { + Pos::GlobalChunk base = Pos::GlobalChunk(rPos) << 2; + + for(size_t index = 0; index < chunks.size(); index++) { + const ChunkObj_t& chunk = chunks[index]; + if(chunk.Nodes.empty()) + continue; + + bool hit = false; + for(DefNodeId nodeId : chunk.Nodes) { + if(changedNodes.contains(nodeId)) { + hit = true; + break; + } + } + + if(!hit) + continue; + + Pos::bvec4u localPos; + localPos.unpack(index); + changedChunks[wId].push_back(base + Pos::GlobalChunk(localPos)); + } + } + } + } + + if(!data.ChangedVoxels.empty()) { + std::unordered_set changedVoxels(data.ChangedVoxels.begin(), data.ChangedVoxels.end()); + + for(const auto& [wId, regions] : ChunksMesh) { + for(const auto& [rPos, chunks] : regions) { + Pos::GlobalChunk base = Pos::GlobalChunk(rPos) << 2; + + for(size_t index = 0; index < chunks.size(); index++) { + const ChunkObj_t& chunk = chunks[index]; + if(chunk.Voxels.empty()) + continue; + + bool hit = false; + for(DefVoxelId voxelId : chunk.Voxels) { + if(changedVoxels.contains(voxelId)) { + hit = true; + break; + } + } + + if(!hit) + continue; + + Pos::bvec4u localPos; + localPos.unpack(index); + changedChunks[wId].push_back(base + Pos::GlobalChunk(localPos)); + } + } + } + } + // Добавляем к изменёным чанкам пересчёт соседей { std::vector> toBuild; - for(auto& [wId, chunks] : data.ChangedChunks) { + for(auto& [wId, chunks] : changedChunks) { std::vector list; for(const Pos::GlobalChunk& pos : chunks) { list.push_back(pos); @@ -683,11 +863,6 @@ void ChunkPreparator::tickSync(const TickSyncData& data) { } } - VertexPool_Voxels.update(CMDPool); - VertexPool_Nodes.update(CMDPool); - IndexPool_Nodes_16.update(CMDPool); - IndexPool_Nodes_32.update(CMDPool); - CMG.endTickSync(); } @@ -824,7 +999,7 @@ VulkanRenderSession::VulkanRenderSession(Vulkan *vkInst, IServerSession *serverS : VkInst(vkInst), ServerSession(serverSession), CP(vkInst, serverSession), - MainTest(vkInst), LightDummy(vkInst), + LightDummy(vkInst), TestQuad(vkInst, sizeof(NodeVertexStatic)*6*3*2) { assert(vkInst); @@ -849,49 +1024,9 @@ VulkanRenderSession::VulkanRenderSession(Vulkan *vkInst, IServerSession *serverS &DescriptorPool)); } - { - std::vector shaderLayoutBindings = - { - { - .binding = 0, - .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - .descriptorCount = 1, - .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, - .pImmutableSamplers = nullptr - }, { - .binding = 1, - .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, - .descriptorCount = 1, - .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, - .pImmutableSamplers = nullptr - } - }; - - const VkDescriptorSetLayoutCreateInfo descriptorLayout = - { - .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .bindingCount = (uint32_t) shaderLayoutBindings.size(), - .pBindings = shaderLayoutBindings.data() - }; - - vkAssert(!vkCreateDescriptorSetLayout( - VkInst->Graphics.Device, &descriptorLayout, nullptr, &MainAtlasDescLayout)); - } - - { - VkDescriptorSetAllocateInfo ciAllocInfo = - { - .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, - .pNext = nullptr, - .descriptorPool = DescriptorPool, - .descriptorSetCount = 1, - .pSetLayouts = &MainAtlasDescLayout - }; - - vkAssert(!vkAllocateDescriptorSets(VkInst->Graphics.Device, &ciAllocInfo, &MainAtlasDescriptor)); - } + TP = std::make_unique(VkInst, DescriptorPool); + NSP = std::make_unique(MP, *TP); + CP.setNodestateProvider(NSP.get()); { std::vector shaderLayoutBindings = @@ -937,39 +1072,11 @@ VulkanRenderSession::VulkanRenderSession(Vulkan *vkInst, IServerSession *serverS } - MainTest.atlasAddCallbackOnUniformChange([this]() -> bool { - updateDescriptor_MainAtlas(); - return true; - }); - LightDummy.atlasAddCallbackOnUniformChange([this]() -> bool { updateDescriptor_VoxelsLight(); return true; }); - { - uint16_t texId = MainTest.atlasAddTexture(2, 2); - uint32_t colors[4] = {0xfffffffful, 0x00fffffful, 0xffffff00ul, 0xff00fffful}; - MainTest.atlasChangeTextureData(texId, (const uint32_t*) colors); - } - - { - int width, height; - bool hasAlpha; - for(const char *path : { - "grass.png", - "willow_wood.png", - "tropical_rainforest_wood.png", - "xnether_blue_wood.png", - "xnether_purple_wood.png", - "frame.png" - }) { - ByteBuffer image = VK::loadPNG(getResource(std::string("textures/") + path)->makeStream().Stream, width, height, hasAlpha); - uint16_t texId = MainTest.atlasAddTexture(width, height); - MainTest.atlasChangeTextureData(texId, (const uint32_t*) image.data()); - } - } - /* x left -1 ~ right 1 y up 1 ~ down -1 @@ -981,47 +1088,47 @@ VulkanRenderSession::VulkanRenderSession(Vulkan *vkInst, IServerSession *serverS { NodeVertexStatic *array = (NodeVertexStatic*) TestQuad.mapMemory(); - array[0] = {135, 135, 135, 0, 0, 0, 0, 65535, 0}; - array[1] = {135, 135+16, 135, 0, 0, 0, 0, 0, 65535}; - array[2] = {135+16, 135+16, 135, 0, 0, 0, 0, 0, 65535}; - array[3] = {135, 135, 135, 0, 0, 0, 0, 65535, 0}; - array[4] = {135+16, 135+16, 135, 0, 0, 0, 0, 0, 65535}; - array[5] = {135+16, 135, 135, 0, 0, 0, 0, 0, 0}; + array[0] = {224, 224, 0, 224, 0, 0, 0, 65535, 0}; + array[1] = {224, 224+64, 0, 224, 0, 0, 0, 0, 65535}; + array[2] = {224+64, 224+64, 0, 224, 0, 0, 0, 0, 65535}; + array[3] = {224, 224, 0, 224, 0, 0, 0, 65535, 0}; + array[4] = {224+64, 224+64, 0, 224, 0, 0, 0, 0, 65535}; + array[5] = {224+64, 224, 0, 224, 0, 0, 0, 0, 0}; - array[6] = {135, 135, 135+16, 0, 0, 0, 0, 0, 0}; - array[7] = {135+16, 135, 135+16, 0, 0, 0, 0, 65535, 0}; - array[8] = {135+16, 135+16, 135+16, 0, 0, 0, 0, 65535, 65535}; - array[9] = {135, 135, 135+16, 0, 0, 0, 0, 0, 0}; - array[10] = {135+16, 135+16, 135+16, 0, 0, 0, 0, 65535, 65535}; - array[11] = {135, 135+16, 135+16, 0, 0, 0, 0, 0, 65535}; + array[6] = {224, 224, 0, 224+64, 0, 0, 0, 0, 0}; + array[7] = {224+64, 224, 0, 224+64, 0, 0, 0, 65535, 0}; + array[8] = {224+64, 224+64, 0, 224+64, 0, 0, 0, 65535, 65535}; + array[9] = {224, 224, 0, 224+64, 0, 0, 0, 0, 0}; + array[10] = {224+64, 224+64, 0, 224+64, 0, 0, 0, 65535, 65535}; + array[11] = {224, 224+64, 0, 224+64, 0, 0, 0, 0, 65535}; - array[12] = {135, 135, 135, 0, 0, 0, 0, 0, 0}; - array[13] = {135, 135, 135+16, 0, 0, 0, 0, 65535, 0}; - array[14] = {135, 135+16, 135+16, 0, 0, 0, 0, 65535, 65535}; - array[15] = {135, 135, 135, 0, 0, 0, 0, 0, 0}; - array[16] = {135, 135+16, 135+16, 0, 0, 0, 0, 65535, 65535}; - array[17] = {135, 135+16, 135, 0, 0, 0, 0, 0, 65535}; + array[12] = {224, 224, 0, 224, 0, 0, 0, 0, 0}; + array[13] = {224, 224, 0, 224+64, 0, 0, 0, 65535, 0}; + array[14] = {224, 224+64, 0, 224+64, 0, 0, 0, 65535, 65535}; + array[15] = {224, 224, 0, 224, 0, 0, 0, 0, 0}; + array[16] = {224, 224+64, 0, 224+64, 0, 0, 0, 65535, 65535}; + array[17] = {224, 224+64, 0, 224, 0, 0, 0, 0, 65535}; - array[18] = {135+16, 135, 135+16, 0, 0, 0, 0, 0, 0}; - array[19] = {135+16, 135, 135, 0, 0, 0, 0, 65535, 0}; - array[20] = {135+16, 135+16, 135, 0, 0, 0, 0, 65535, 65535}; - array[21] = {135+16, 135, 135+16, 0, 0, 0, 0, 0, 0}; - array[22] = {135+16, 135+16, 135, 0, 0, 0, 0, 65535, 65535}; - array[23] = {135+16, 135+16, 135+16, 0, 0, 0, 0, 0, 65535}; + array[18] = {224+64, 224, 0, 224+64, 0, 0, 0, 0, 0}; + array[19] = {224+64, 224, 0, 224, 0, 0, 0, 65535, 0}; + array[20] = {224+64, 224+64, 0, 224, 0, 0, 0, 65535, 65535}; + array[21] = {224+64, 224, 0, 224+64, 0, 0, 0, 0, 0}; + array[22] = {224+64, 224+64, 0, 224, 0, 0, 0, 65535, 65535}; + array[23] = {224+64, 224+64, 0, 224+64, 0, 0, 0, 0, 65535}; - array[24] = {135, 135, 135, 0, 0, 0, 0, 0, 0}; - array[25] = {135+16, 135, 135, 0, 0, 0, 0, 65535, 0}; - array[26] = {135+16, 135, 135+16, 0, 0, 0, 0, 65535, 65535}; - array[27] = {135, 135, 135, 0, 0, 0, 0, 0, 0}; - array[28] = {135+16, 135, 135+16, 0, 0, 0, 0, 65535, 65535}; - array[29] = {135, 135, 135+16, 0, 0, 0, 0, 0, 65535}; + array[24] = {224, 224, 0, 224, 0, 0, 0, 0, 0}; + array[25] = {224+64, 224, 0, 224, 0, 0, 0, 65535, 0}; + array[26] = {224+64, 224, 0, 224+64, 0, 0, 0, 65535, 65535}; + array[27] = {224, 224, 0, 224, 0, 0, 0, 0, 0}; + array[28] = {224+64, 224, 0, 224+64, 0, 0, 0, 65535, 65535}; + array[29] = {224, 224, 0, 224+64, 0, 0, 0, 0, 65535}; - array[30] = {135, 135+16, 135+16, 0, 0, 0, 0, 0, 0}; - array[31] = {135+16, 135+16, 135+16, 0, 0, 0, 0, 65535, 0}; - array[32] = {135+16, 135+16, 135, 0, 0, 0, 0, 65535, 65535}; - array[33] = {135, 135+16, 135+16, 0, 0, 0, 0, 0, 0}; - array[34] = {135+16, 135+16, 135, 0, 0, 0, 0, 65535, 65535}; - array[35] = {135, 135+16, 135, 0, 0, 0, 0, 0, 65535}; + array[30] = {224, 224+64, 0, 224+64, 0, 0, 0, 0, 0}; + array[31] = {224+64, 224+64, 0, 224+64, 0, 0, 0, 65535, 0}; + array[32] = {224+64, 224+64, 0, 224, 0, 0, 0, 65535, 65535}; + array[33] = {224, 224+64, 0, 224+64, 0, 0, 0, 0, 0}; + array[34] = {224+64, 224+64, 0, 224, 0, 0, 0, 65535, 65535}; + array[35] = {224, 224+64, 0, 224, 0, 0, 0, 0, 65535}; for(int iter = 0; iter < 36; iter++) { array[iter].Tex = 6; @@ -1067,7 +1174,6 @@ VulkanRenderSession::VulkanRenderSession(Vulkan *vkInst, IServerSession *serverS } } - updateDescriptor_MainAtlas(); updateDescriptor_VoxelsLight(); updateDescriptor_ChunksLight(); @@ -1084,7 +1190,7 @@ VulkanRenderSession::VulkanRenderSession(Vulkan *vkInst, IServerSession *serverS std::vector layouts = { - MainAtlasDescLayout, + TP ? TP->getDescriptorLayout() : VK_NULL_HANDLE, VoxelLightMapDescLayout }; @@ -1433,9 +1539,7 @@ VulkanRenderSession::~VulkanRenderSession() { if(MainAtlas_LightMap_PipelineLayout) vkDestroyPipelineLayout(VkInst->Graphics.Device, MainAtlas_LightMap_PipelineLayout, nullptr); - - if(MainAtlasDescLayout) - vkDestroyDescriptorSetLayout(VkInst->Graphics.Device, MainAtlasDescLayout, nullptr); + if(VoxelLightMapDescLayout) vkDestroyDescriptorSetLayout(VkInst->Graphics.Device, VoxelLightMapDescLayout, nullptr); @@ -1459,36 +1563,93 @@ void VulkanRenderSession::tickSync(const TickSyncData& data) { ChunkPreparator::TickSyncData mcpData; mcpData.ChangedChunks = data.Chunks_ChangeOrAdd; mcpData.LostRegions = data.Chunks_Lost; - CP.tickSync(mcpData); - { - std::vector> resources; - std::vector lost; + if(auto iter = data.Profiles_ChangeOrAdd.find(EnumDefContent::Node); iter != data.Profiles_ChangeOrAdd.end()) + mcpData.ChangedNodes.insert(mcpData.ChangedNodes.end(), iter->second.begin(), iter->second.end()); + if(auto iter = data.Profiles_Lost.find(EnumDefContent::Node); iter != data.Profiles_Lost.end()) + mcpData.ChangedNodes.insert(mcpData.ChangedNodes.end(), iter->second.begin(), iter->second.end()); + if(auto iter = data.Profiles_ChangeOrAdd.find(EnumDefContent::Voxel); iter != data.Profiles_ChangeOrAdd.end()) + mcpData.ChangedVoxels.insert(mcpData.ChangedVoxels.end(), iter->second.begin(), iter->second.end()); + if(auto iter = data.Profiles_Lost.find(EnumDefContent::Voxel); iter != data.Profiles_Lost.end()) + mcpData.ChangedVoxels.insert(mcpData.ChangedVoxels.end(), iter->second.begin(), iter->second.end()); - for(const auto& [type, ids] : data.Assets_ChangeOrAdd) { - if(type != EnumAssets::Model) + std::vector> modelResources; + std::vector modelLost; + if(auto iter = data.Assets_ChangeOrAdd.find(EnumAssets::Model); iter != data.Assets_ChangeOrAdd.end()) { + const auto& list = ServerSession->Assets[EnumAssets::Model]; + for(ResourceId id : iter->second) { + auto entryIter = list.find(id); + if(entryIter == list.end()) continue; - const auto& list = ServerSession->Assets[type]; - for(ResourceId id : ids) { - auto iter = list.find(id); - if(iter == list.end()) + modelResources.emplace_back(id, entryIter->second.Res); + } + } + if(auto iter = data.Assets_Lost.find(EnumAssets::Model); iter != data.Assets_Lost.end()) + modelLost.insert(modelLost.end(), iter->second.begin(), iter->second.end()); + + std::vector changedModels; + if(!modelResources.empty() || !modelLost.empty()) + changedModels = MP.onModelChanges(std::move(modelResources), std::move(modelLost)); + + if(TP) { + std::vector> textureResources; + std::vector textureLost; + + if(auto iter = data.Assets_ChangeOrAdd.find(EnumAssets::Texture); iter != data.Assets_ChangeOrAdd.end()) { + const auto& list = ServerSession->Assets[EnumAssets::Texture]; + for(ResourceId id : iter->second) { + auto entryIter = list.find(id); + if(entryIter == list.end()) continue; - resources.emplace_back(id, iter->second.Res); + textureResources.emplace_back(id, entryIter->second.Res); } } - for(const auto& [type, ids] : data.Assets_Lost) { - if(type != EnumAssets::Model) - continue; + if(auto iter = data.Assets_Lost.find(EnumAssets::Texture); iter != data.Assets_Lost.end()) + textureLost.insert(textureLost.end(), iter->second.begin(), iter->second.end()); - lost.append_range(ids); + if(!textureResources.empty() || !textureLost.empty()) + TP->onTexturesChanges(std::move(textureResources), std::move(textureLost)); + } + + std::vector changedNodestates; + if(NSP) { + std::vector> nodestateResources; + std::vector nodestateLost; + + if(auto iter = data.Assets_ChangeOrAdd.find(EnumAssets::Nodestate); iter != data.Assets_ChangeOrAdd.end()) { + const auto& list = ServerSession->Assets[EnumAssets::Nodestate]; + for(ResourceId id : iter->second) { + auto entryIter = list.find(id); + if(entryIter == list.end()) + continue; + + nodestateResources.emplace_back(id, entryIter->second.Res); + } } - if(!resources.empty() || !lost.empty()) - MP.onModelChanges(std::move(resources), std::move(lost)); + if(auto iter = data.Assets_Lost.find(EnumAssets::Nodestate); iter != data.Assets_Lost.end()) + nodestateLost.insert(nodestateLost.end(), iter->second.begin(), iter->second.end()); + + if(!nodestateResources.empty() || !nodestateLost.empty() || !changedModels.empty()) + changedNodestates = NSP->onNodestateChanges(std::move(nodestateResources), std::move(nodestateLost), changedModels); } + + if(!changedNodestates.empty()) { + std::unordered_set changed; + changed.reserve(changedNodestates.size()); + for(AssetsNodestate id : changedNodestates) + changed.insert(id); + + for(const auto& [nodeId, def] : ServerSession->Profiles.DefNode) { + if(changed.contains(def.NodestateId)) + mcpData.ChangedNodes.push_back(nodeId); + } + } + + CP.tickSync(mcpData); } void VulkanRenderSession::setCameraPos(WorldId_t worldId, Pos::Object pos, glm::quat quat) { @@ -1502,8 +1663,14 @@ void VulkanRenderSession::setCameraPos(WorldId_t worldId, Pos::Object pos, glm:: } void VulkanRenderSession::beforeDraw() { - MainTest.atlasUpdateDynamicData(); + if(TP) + TP->update(); LightDummy.atlasUpdateDynamicData(); + CP.flushUploadsAndBarriers(VkInst->Graphics.CommandBufferRender); +} + +void VulkanRenderSession::onGpuFinished() { + CP.notifyGpuFinished(); } void VulkanRenderSession::drawWorld(GlobalTime gTime, float dTime, VkCommandBuffer drawCmd) { @@ -1643,7 +1810,7 @@ void VulkanRenderSession::drawWorld(GlobalTime gTime, float dTime, VkCommandBuff 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[]) {MainAtlasDescriptor, VoxelLightMapDescriptor}, 0, nullptr); + (const VkDescriptorSet[]) {TP ? TP->getDescriptorSet() : VK_NULL_HANDLE, VoxelLightMapDescriptor}, 0, nullptr); { // glm::vec4 offset = glm::inverse(Quat)*glm::vec4(0, 0, -64, 1); @@ -1662,13 +1829,13 @@ void VulkanRenderSession::drawWorld(GlobalTime gTime, float dTime, VkCommandBuff auto [voxelVertexs, nodeVertexs] = CP.getChunksForRender(WorldId, Pos, 1, PCO.ProjView, x64offset_region); - { - static uint32_t l = TOS::Time::getSeconds(); - if(l != TOS::Time::getSeconds()) { - l = TOS::Time::getSeconds(); - TOS::Logger("Test").debug() << nodeVertexs.size(); - } - } + // { + // static uint32_t l = TOS::Time::getSeconds(); + // if(l != TOS::Time::getSeconds()) { + // l = TOS::Time::getSeconds(); + // TOS::Logger("Test").debug() << nodeVertexs.size(); + // } + // } size_t count = 0; @@ -1789,36 +1956,6 @@ std::vector VulkanRenderSession::generateMeshForVoxelChunks(co return out; } -void VulkanRenderSession::updateDescriptor_MainAtlas() { - VkDescriptorBufferInfo bufferInfo = MainTest; - VkDescriptorImageInfo imageInfo = MainTest; - - std::vector ciDescriptorSet = - { - { - .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .pNext = nullptr, - .dstSet = MainAtlasDescriptor, - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - .pImageInfo = &imageInfo - }, { - .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .pNext = nullptr, - .dstSet = MainAtlasDescriptor, - .dstBinding = 1, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, - .pBufferInfo = &bufferInfo - } - }; - - vkUpdateDescriptorSets(VkInst->Graphics.Device, ciDescriptorSet.size(), ciDescriptorSet.data(), 0, nullptr); -} - void VulkanRenderSession::updateDescriptor_VoxelsLight() { VkDescriptorBufferInfo bufferInfo = LightDummy; VkDescriptorImageInfo imageInfo = LightDummy; diff --git a/Src/Client/Vulkan/VulkanRenderSession.hpp b/Src/Client/Vulkan/VulkanRenderSession.hpp index a385b3e..ceec9b1 100644 --- a/Src/Client/Vulkan/VulkanRenderSession.hpp +++ b/Src/Client/Vulkan/VulkanRenderSession.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include "Client/Vulkan/AtlasPipeline/PipelinedTextureAtlas.hpp" #include "Abstract.hpp" #include "TOSLib.hpp" #include "VertexPool.hpp" @@ -243,7 +245,7 @@ public: continue; } - Models.insert({key, std::move(model)}); + Models.insert_or_assign(key, std::move(model)); } std::sort(result.begin(), result.end()); @@ -388,63 +390,11 @@ private: Хранить все текстуры в оперативке */ class TextureProvider { - // Хедер для атласа перед описанием текстур - struct alignas(16) UniformInfo { - uint32_t - // Количество текстур - SubsCount, - // Счётчик времени с разрешением 8 бит в секунду - Counter, - // Размер атласа - Size; - - // Дальше в шейдере массив на описания текстур - // std::vector SubsInfo; - }; - - // Описание текстуры на стороне шейдера - struct alignas(16) InfoSubTexture { - uint32_t isExist = 0; - uint32_t - // Точная позиция в атласе - PosX = 0, PosY = 0, PosZ = 0, - // Размер текстуры в атласе - Width = 0, Height = 0; - - struct { - uint16_t Enabled : 1 = 0, Frames : 15 = 0; - uint16_t TimePerFrame = 0; - } Animation; - }; - public: TextureProvider(Vulkan* inst, VkDescriptorPool descPool) : Inst(inst), DescPool(descPool) { - { - const VkSamplerCreateInfo ciSampler = - { - .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .magFilter = VK_FILTER_NEAREST, - .minFilter = VK_FILTER_NEAREST, - .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST, - .addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT, - .addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT, - .addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT, - .mipLodBias = 0.0f, - .anisotropyEnable = VK_FALSE, - .maxAnisotropy = 1, - .compareEnable = 0, - .compareOp = VK_COMPARE_OP_NEVER, - .minLod = 0.0f, - .maxLod = 0.0f, - .borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE, - .unnormalizedCoordinates = VK_FALSE - }; - vkAssert(!vkCreateSampler(inst->Graphics.Device, &ciSampler, nullptr, &Sampler)); - } + assert(inst); { std::vector shaderLayoutBindings = @@ -476,391 +426,251 @@ public: vkAssert(!vkCreateDescriptorSetLayout( Inst->Graphics.Device, &descriptorLayout, nullptr, &DescLayout)); } - + { - Atlases.resize(BackupAtlasCount); - VkDescriptorSetAllocateInfo ciAllocInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, .pNext = nullptr, - .descriptorPool = descPool, - .descriptorSetCount = (uint32_t) Atlases.size(), + .descriptorPool = DescPool, + .descriptorSetCount = 1, .pSetLayouts = &DescLayout }; - std::vector descriptors; - descriptors.resize(Atlases.size()); - vkAssert(!vkAllocateDescriptorSets(inst->Graphics.Device, &ciAllocInfo, descriptors.data())); - - for(auto& atlas : Atlases) { - atlas.recreate(Inst, true); - atlas.Descriptor = descriptors.back(); - descriptors.pop_back(); - } + vkAssert(!vkAllocateDescriptorSets(Inst->Graphics.Device, &ciAllocInfo, &Descriptor)); } { - VkSemaphoreCreateInfo semaphoreCreateInfo = { - .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, - .pNext = nullptr, + TextureAtlas::Config cfg; + cfg.MaxTextureId = 1 << 18; + AtlasStaging = std::make_shared( + Inst->Graphics.Device, + Inst->Graphics.PhysicalDevice + ); + Atlas = std::make_unique( + TextureAtlas(Inst->Graphics.Device, Inst->Graphics.PhysicalDevice, cfg, {}, AtlasStaging) + ); + } + + { + const VkFenceCreateInfo info = { + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .pNext = nullptr, .flags = 0 }; - vkAssert(!vkCreateSemaphore(Inst->Graphics.Device, &semaphoreCreateInfo, nullptr, &SendChanges)); + vkAssert(!vkCreateFence(Inst->Graphics.Device, &info, nullptr, &UpdateFence)); } - { - const VkCommandBufferAllocateInfo infoCmd = - { - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - .pNext = nullptr, - .commandPool = Inst->Graphics.Pool, - .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, - .commandBufferCount = 1 - }; - - vkAssert(!vkAllocateCommandBuffers(Inst->Graphics.Device, &infoCmd, &CMD)); - } - - Cache.recreate(Inst, false); - - AtlasTextureUnusedId.all(); + NeedsUpload = true; } ~TextureProvider() { + if(UpdateFence) + vkDestroyFence(Inst->Graphics.Device, UpdateFence, nullptr); + if(DescLayout) vkDestroyDescriptorSetLayout(Inst->Graphics.Device, DescLayout, nullptr); - - if(Sampler) { - vkDestroySampler(Inst->Graphics.Device, Sampler, nullptr); - Sampler = nullptr; - } + } - for(auto& atlas : Atlases) { - atlas.destroy(Inst); - } + VkDescriptorSetLayout getDescriptorLayout() const { + return DescLayout; + } - Atlases.clear(); - - Cache.destroy(Inst); - Cache.unMap(Inst); - - if(SendChanges) { - vkDestroySemaphore(Inst->Graphics.Device, SendChanges, nullptr); - SendChanges = nullptr; - } - - if(CMD) { - vkFreeCommandBuffers(Inst->Graphics.Device, Inst->Graphics.Pool, 1, &CMD); - CMD = nullptr; - } + VkDescriptorSet getDescriptorSet() const { + return Descriptor; } uint16_t getTextureId(const TexturePipeline& pipe) { - return 0; - } + std::lock_guard lock(Mutex); + auto iter = PipelineToAtlas.find(pipe); + if(iter != PipelineToAtlas.end()) + return iter->second; - // Устанавливает новый размер единицы в массиве текстур атласа - enum class EnumAtlasSize { - _2048 = 2048, _4096 = 4096, _8192 = 8192, _16_384 = 16'384 - }; - void setAtlasSize(EnumAtlasSize size) { - ReferenceSize = size; - } + ::HashedPipeline hashed = makeHashedPipeline(pipe); + uint32_t atlasId = Atlas->getByPipeline(hashed); - // Максимальный размер выделенный под атласы в памяти устройства - void setDeviceMemorySize(size_t size) { - std::unreachable(); + uint16_t result = 0; + if(atlasId <= std::numeric_limits::max()) + result = static_cast(atlasId); + else + LOG.warn() << "Atlas texture id overflow: " << atlasId; + + PipelineToAtlas.emplace(pipe, result); + NeedsUpload = true; + return result; } // Применяет изменения, возвращая все затронутые модели std::vector onTexturesChanges(std::vector> newOrChanged, std::vector lost) { + std::lock_guard lock(Mutex); std::vector result; for(const auto& [key, res] : newOrChanged) { result.push_back(key); - ChangedOrAdded.push_back(key); - TextureEntry entry; iResource sres((const uint8_t*) res.data(), res.size()); iBinaryStream stream = sres.makeStream(); png::image img(stream.Stream); - entry.Width = img.get_width(); - entry.Height = img.get_height(); - entry.RGBA.resize(4*entry.Width*entry.Height); + uint32_t width = img.get_width(); + uint32_t height = img.get_height(); - for(int i = 0; i < entry.Height; i++) { - std::copy( - ((const uint32_t*) &img.get_pixbuf().operator [](i)[0]), - ((const uint32_t*) &img.get_pixbuf().operator [](i)[0])+entry.Width, - ((uint32_t*) entry.RGBA.data())+entry.Width*(false ? entry.Height-i-1 : i) - ); + std::vector pixels; + pixels.resize(width*height); + + for(uint32_t y = 0; y < height; y++) { + const auto& row = img.get_pixbuf().operator [](y); + for(uint32_t x = 0; x < width; x++) { + const auto& px = row[x]; + uint32_t rgba = (uint32_t(px.alpha) << 24) + | (uint32_t(px.red) << 16) + | (uint32_t(px.green) << 8) + | uint32_t(px.blue); + pixels[x + y * width] = rgba; + } } - Textures[key] = std::move(entry); + Atlas->updateTexture(key, StoredTexture( + static_cast(width), + static_cast(height), + std::move(pixels) + )); + + NeedsUpload = true; } for(AssetsTexture key : lost) { result.push_back(key); - Lost.push_back(key); + Atlas->freeTexture(key); + NeedsUpload = true; } - { - std::sort(result.begin(), result.end()); - auto eraseIter = std::unique(result.begin(), result.end()); - result.erase(eraseIter, result.end()); - } - - { - std::sort(ChangedOrAdded.begin(), ChangedOrAdded.end()); - auto eraseIter = std::unique(ChangedOrAdded.begin(), ChangedOrAdded.end()); - ChangedOrAdded.erase(eraseIter, ChangedOrAdded.end()); - } - - { - std::sort(Lost.begin(), Lost.end()); - auto eraseIter = std::unique(Lost.begin(), Lost.end()); - Lost.erase(eraseIter, Lost.end()); - } + std::sort(result.begin(), result.end()); + auto eraseIter = std::unique(result.begin(), result.end()); + result.erase(eraseIter, result.end()); return result; } void update() { - // Подготовить обновления атласа - // Если предыдущий освободился, то записать изменения в него + std::lock_guard lock(Mutex); + if(!NeedsUpload || !Atlas) + return; - // Держать на стороне хоста полную версию атласа и все изменения писать туда - // Когерентная память сама разберётся что отсылать на устройство - // Синхронизировать всё из внутреннего буфера в атлас - // При пересоздании хостового буфера, скопировать всё из старого. + Atlas->flushNewPipelines(); - // Оптимизации копирования при указании конкретных изменённых слоёв? + VkCommandBufferAllocateInfo allocInfo { + VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + nullptr, + Inst->Graphics.Pool, + VK_COMMAND_BUFFER_LEVEL_PRIMARY, + 1 + }; + VkCommandBuffer commandBuffer; + vkAssert(!vkAllocateCommandBuffers(Inst->Graphics.Device, &allocInfo, &commandBuffer)); + VkCommandBufferBeginInfo beginInfo { + VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + nullptr, + VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, + nullptr + }; + + vkAssert(!vkBeginCommandBuffer(commandBuffer, &beginInfo)); + + TextureAtlas::DescriptorOut desc = Atlas->flushUploadsAndBarriers(commandBuffer); + + vkAssert(!vkEndCommandBuffer(commandBuffer)); + + VkSubmitInfo submitInfo { + VK_STRUCTURE_TYPE_SUBMIT_INFO, + nullptr, + 0, nullptr, + nullptr, + 1, + &commandBuffer, + 0, + nullptr + }; + + { + auto lockQueue = Inst->Graphics.DeviceQueueGraphic.lock(); + vkAssert(!vkQueueSubmit(*lockQueue, 1, &submitInfo, UpdateFence)); + } + + vkAssert(!vkWaitForFences(Inst->Graphics.Device, 1, &UpdateFence, VK_TRUE, UINT64_MAX)); + vkAssert(!vkResetFences(Inst->Graphics.Device, 1, &UpdateFence)); + + vkFreeCommandBuffers(Inst->Graphics.Device, Inst->Graphics.Pool, 1, &commandBuffer); + + Atlas->notifyGpuFinished(); + updateDescriptor(desc); + + NeedsUpload = false; } - VkDescriptorSet getDescriptor() { - return Atlases[ActiveAtlas].Descriptor; +private: + ::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(pipeline._Pipeline.empty()) { + if(!pipe.BinTextures.empty()) + pipeline = ::Pipeline(pipe.BinTextures.front()); + } + + return ::HashedPipeline(pipeline); } - void pushFrame() { - for(auto& atlas : Atlases) - if(atlas.NotUsedFrames < 100) - atlas.NotUsedFrames++; + void updateDescriptor(const TextureAtlas::DescriptorOut& desc) { + VkWriteDescriptorSet writes[2] = {}; - Atlases[ActiveAtlas].NotUsedFrames = 0; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = Descriptor; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[0].pImageInfo = &desc.ImageInfo; - // Если есть новые текстуры или они поменялись - // + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = Descriptor; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[1].pBufferInfo = &desc.EntriesInfo; + + vkUpdateDescriptorSets(Inst->Graphics.Device, 2, writes, 0, nullptr); } - + private: Vulkan* Inst = nullptr; VkDescriptorPool DescPool = VK_NULL_HANDLE; VkDescriptorSetLayout DescLayout = VK_NULL_HANDLE; - // Для всех атласов - VkSampler Sampler = VK_NULL_HANDLE; - // Ожидание завершения работы с хостовым буфером - VkSemaphore SendChanges = VK_NULL_HANDLE; - // - VkCommandBuffer CMD = VK_NULL_HANDLE; + VkDescriptorSet Descriptor = VK_NULL_HANDLE; + VkFence UpdateFence = VK_NULL_HANDLE; - // Размер, которому должны соответствовать все атласы - EnumAtlasSize ReferenceSize = EnumAtlasSize::_2048; + std::shared_ptr AtlasStaging; + std::unique_ptr Atlas; + std::unordered_map PipelineToAtlas; - struct TextureEntry { - uint16_t Width, Height; - std::vector RGBA; - - // Идентификатор текстуры в атласе - uint16_t InAtlasId = uint16_t(-1); - }; - - // Текстуры, загруженные с файлов - std::unordered_map Textures; - - struct TextureFromPipeline { - - }; - - std::unordered_map Pipelines; - - struct AtlasTextureEntry { - uint16_t PosX, PosY, PosZ, Width, Height; - - }; - - std::bitset<1 << 16> AtlasTextureUnusedId; - std::unordered_map AtlasTextureInfo; - - std::vector ChangedOrAdded, Lost; - - struct VkAtlasInfo { - VkImage Image = VK_NULL_HANDLE; - VkImageLayout ImageLayout = VK_IMAGE_LAYOUT_MAX_ENUM; - - VkDeviceMemory Memory = VK_NULL_HANDLE; - VkImageView View = VK_NULL_HANDLE; - - VkDescriptorSet Descriptor; - EnumAtlasSize Size = EnumAtlasSize::_2048; - uint16_t Depth = 1; - - // Сколько кадров уже не используется атлас - int NotUsedFrames = 0; - - void destroy(Vulkan* inst) { - if(View) { - vkDestroyImageView(inst->Graphics.Device, View, nullptr); - View = nullptr; - } - - if(Image) { - vkDestroyImage(inst->Graphics.Device, Image, nullptr); - Image = nullptr; - } - - if(Memory) { - vkFreeMemory(inst->Graphics.Device, Memory, nullptr); - Memory = nullptr; - } - } - - void recreate(Vulkan* inst, bool deviceLocal) { - // Уничтожаем то, что не понадобится - if(View) { - vkDestroyImageView(inst->Graphics.Device, View, nullptr); - View = nullptr; - } - - if(Image) { - vkDestroyImage(inst->Graphics.Device, Image, nullptr); - Image = nullptr; - } - - if(Memory) { - vkFreeMemory(inst->Graphics.Device, Memory, nullptr); - Memory = nullptr; - } - - // Создаём атлас - uint32_t size = uint32_t(Size); - - VkImageCreateInfo infoImageCreate = - { - .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .imageType = VK_IMAGE_TYPE_2D, - .format = VK_FORMAT_B8G8R8A8_UNORM, - .extent = { size, size, 1 }, - .mipLevels = 1, - .arrayLayers = Depth, - .samples = VK_SAMPLE_COUNT_1_BIT, - .tiling = VK_IMAGE_TILING_MAX_ENUM, - .usage = - static_cast(deviceLocal - ? VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT - : VK_IMAGE_USAGE_TRANSFER_SRC_BIT), - .sharingMode = VK_SHARING_MODE_EXCLUSIVE, - .queueFamilyIndexCount = 0, - .pQueueFamilyIndices = 0, - .initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED - }; - - VkFormatProperties props; - vkGetPhysicalDeviceFormatProperties(inst->Graphics.PhysicalDevice, infoImageCreate.format, &props); - - if (props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) - infoImageCreate.tiling = VK_IMAGE_TILING_OPTIMAL; - else if (props.linearTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) - infoImageCreate.tiling = VK_IMAGE_TILING_LINEAR; - else - vkAssert(!"No support for B8G8R8A8_UNORM as texture image format"); - - vkAssert(!vkCreateImage(inst->Graphics.Device, &infoImageCreate, nullptr, &Image)); - - // Выделяем память - VkMemoryRequirements memoryReqs; - vkGetImageMemoryRequirements(inst->Graphics.Device, Image, &memoryReqs); - - VkMemoryAllocateInfo memoryAlloc - { - .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, - .pNext = nullptr, - .allocationSize = memoryReqs.size, - .memoryTypeIndex = inst->memoryTypeFromProperties(memoryReqs.memoryTypeBits, - deviceLocal - ? VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT - : VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT - ) - }; - - vkAssert(!vkAllocateMemory(inst->Graphics.Device, &memoryAlloc, nullptr, &Memory)); - vkAssert(!vkBindImageMemory(inst->Graphics.Device, Image, Memory, 0)); - - // Порядок пикселей и привязка к картинке - VkImageViewCreateInfo ciView = - { - .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .image = Image, - .viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY, - .format = infoImageCreate.format, - .components = - { - VK_COMPONENT_SWIZZLE_B, - VK_COMPONENT_SWIZZLE_G, - VK_COMPONENT_SWIZZLE_R, - VK_COMPONENT_SWIZZLE_A - }, - .subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 } - }; - - vkAssert(!vkCreateImageView(inst->Graphics.Device, &ciView, nullptr, &View)); - } - }; - - struct HostCache : public VkAtlasInfo { - std::vector Layers; - std::vector Layouts; - std::vector Packs; - - void map(Vulkan* inst) { - Layers.resize(Depth); - Layouts.resize(Depth); - - for(uint32_t layer = 0; layer < Depth; layer++) { - const VkImageSubresource memorySubres = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .mipLevel = 0, .arrayLayer = layer, }; - vkGetImageSubresourceLayout(inst->Graphics.Device, Image, &memorySubres, &Layouts[layer]); - - vkAssert(!vkMapMemory(inst->Graphics.Device, Memory, Layouts[layer].offset, Layouts[layer].size, 0, (void**) &Layers[layer])); - } - } - - void unMap(Vulkan* inst) { - vkUnmapMemory(inst->Graphics.Device, Memory); - - Layers.clear(); - Layouts.clear(); - } - }; - - HostCache Cache; - - static constexpr size_t BackupAtlasCount = 2; - - // Атласы, используемые в кадре. - // Изменения пишутся в не используемый в данный момент атлас - // и изменённый атлас становится активным. Новые изменения - // можно писать по прошествии нескольких кадров. - std::vector Atlases; - int ActiveAtlas = 0; + bool NeedsUpload = false; + Logger LOG = "Client>TextureProvider"; + mutable std::mutex Mutex; }; + /* Хранит информацию о моделях при различных состояниях нод */ @@ -904,7 +714,23 @@ public: continue; } - Nodestates.insert({key, std::move(nodestate)}); + Nodestates.insert_or_assign(key, std::move(nodestate)); + } + + if(!changedModels.empty()) { + std::unordered_set changed; + changed.reserve(changedModels.size()); + for(AssetsModel modelId : changedModels) + changed.insert(modelId); + + for(const auto& [nodestateId, nodestate] : Nodestates) { + for(AssetsModel modelId : nodestate.LocalToModel) { + if(changed.contains(modelId)) { + result.push_back(nodestateId); + break; + } + } + } } std::sort(result.begin(), result.end()); @@ -922,51 +748,78 @@ public: if(iterNodestate == Nodestates.end()) return {}; - std::vector routes = iterNodestate->second.getModelsForState(statesInfo, states); + PreparedNodeState& nodestate = iterNodestate->second; + std::vector routes = nodestate.getModelsForState(statesInfo, states); std::vector>>>> result; std::unordered_map pipelineResolveCache; - for(uint16_t routeId : routes) { - std::vector>>> routeModels; - const auto& route = iterNodestate->second.Routes[routeId]; - for(const auto& [w, m] : route.second) { - if(const PreparedNodeState::Model* ptr = std::get_if(&m)) { - ModelProvider::Model model = MP.getModel(ptr->Id); - Transformations trf(ptr->Transforms); - std::unordered_map> out; + auto appendModel = [&](AssetsModel modelId, const std::vector& transforms, std::unordered_map>& out) { + ModelProvider::Model model = MP.getModel(modelId); + Transformations trf{transforms}; - for(auto& [l, r] : model.Vertecies) { - trf.apply(r); + for(auto& [l, r] : model.Vertecies) { + trf.apply(r); - // Позиция -224 ~ 288; 64 позиций в одной ноде, 7.5 метров в ряд - for(const Vertex& v : r) { - NodeVertexStatic vert; + // Позиция -224 ~ 288; 64 позиций в одной ноде, 7.5 метров в ряд + for(const Vertex& v : r) { + NodeVertexStatic vert; - vert.FX = (v.Pos.x+0.5)*64+224; - vert.FY = (v.Pos.y+0.5)*64+224; - vert.FZ = (v.Pos.z+0.5)*64+224; + vert.FX = (v.Pos.x+0.5f)*64+224; + vert.FY = (v.Pos.y+0.5f)*64+224; + vert.FZ = (v.Pos.z+0.5f)*64+224; - vert.TU = std::clamp(v.UV.x * (1 << 16), 0, (1 << 16)); - vert.TV = std::clamp(v.UV.y * (1 << 16), 0, (1 << 16)); + vert.TU = std::clamp(v.UV.x * (1 << 16), 0, (1 << 16) - 1); + vert.TV = std::clamp(v.UV.y * (1 << 16), 0, (1 << 16) - 1); - const TexturePipeline& pipe = model.TextureMap[model.TextureKeys[v.TexId]]; - if(auto iterPipe = pipelineResolveCache.find(pipe); iterPipe != pipelineResolveCache.end()) { - vert.Tex = iterPipe->second; - } else { - vert.Tex = TP.getTextureId(pipe); - pipelineResolveCache[pipe] = vert.Tex; - } - - out[l].push_back(vert); - } + const TexturePipeline& pipe = model.TextureMap[model.TextureKeys[v.TexId]]; + if(auto iterPipe = pipelineResolveCache.find(pipe); iterPipe != pipelineResolveCache.end()) { + vert.Tex = iterPipe->second; + } else { + vert.Tex = TP.getTextureId(pipe); + pipelineResolveCache[pipe] = vert.Tex; } - /// TODO: uvlock - - routeModels.emplace_back(w, std::move(out)); + out[l].push_back(vert); } } + }; + + auto resolveModelId = [&](uint16_t localId, AssetsModel& outId) -> bool { + if(localId >= nodestate.LocalToModel.size()) + return false; + outId = nodestate.LocalToModel[localId]; + return true; + }; + + for(uint16_t routeId : routes) { + if(routeId >= nodestate.Routes.size()) + continue; + + std::vector>>> routeModels; + const auto& route = nodestate.Routes[routeId]; + for(const auto& [w, m] : route.second) { + std::unordered_map> out; + + if(const PreparedNodeState::Model* ptr = std::get_if(&m)) { + AssetsModel modelId; + if(resolveModelId(ptr->Id, modelId)) + appendModel(modelId, ptr->Transforms, out); + } else if(const PreparedNodeState::VectorModel* ptr = std::get_if(&m)) { + for(const auto& sub : ptr->Models) { + AssetsModel modelId; + if(!resolveModelId(sub.Id, modelId)) + continue; + + std::vector transforms = sub.Transforms; + transforms.insert(transforms.end(), ptr->Transforms.begin(), ptr->Transforms.end()); + appendModel(modelId, transforms, out); + } + } + + /// TODO: uvlock + routeModels.emplace_back(w, std::move(out)); + } result.push_back(std::move(routeModels)); } @@ -974,6 +827,15 @@ public: return result; } + uint16_t getTextureId(AssetsTexture texId) { + if(texId == 0) + return 0; + + TexturePipeline pipe; + pipe.BinTextures.push_back(texId); + return TP.getTextureId(pipe); + } + private: Logger LOG = "Client>NodestateProvider"; ModelProvider& MP; @@ -1027,6 +889,10 @@ public: // Меняет количество обрабатывающих потоков void changeThreadsCount(uint8_t threads); + void setNodestateProvider(NodestateProvider* provider) { + NSP = provider; + } + void prepareTickSync() { Sync.Stop = true; } @@ -1051,6 +917,7 @@ private: } Sync; IServerSession *SS; + NodestateProvider* NSP = nullptr; std::vector Threads; void run(uint8_t id); @@ -1083,23 +950,10 @@ public: assert(serverSession); CMG.changeThreadsCount(1); - - const VkCommandPoolCreateInfo infoCmdPool = - { - .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, - .pNext = nullptr, - .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, - .queueFamilyIndex = VkInst->getSettings().QueueGraphics - }; - - vkAssert(!vkCreateCommandPool(VkInst->Graphics.Device, &infoCmdPool, nullptr, &CMDPool)); } ~ChunkPreparator() { CMG.changeThreadsCount(0); - - if(CMDPool) - vkDestroyCommandPool(VkInst->Graphics.Device, CMDPool, nullptr); } @@ -1111,7 +965,24 @@ public: CMG.pushStageTickSync(); } + void setNodestateProvider(NodestateProvider* provider) { + CMG.setNodestateProvider(provider); + } + void tickSync(const TickSyncData& data); + void notifyGpuFinished() { + resetVertexStaging(); + VertexPool_Voxels.notifyGpuFinished(); + VertexPool_Nodes.notifyGpuFinished(); + IndexPool_Nodes_16.notifyGpuFinished(); + IndexPool_Nodes_32.notifyGpuFinished(); + } + void flushUploadsAndBarriers(VkCommandBuffer commandBuffer) { + VertexPool_Voxels.flushUploadsAndBarriers(commandBuffer); + VertexPool_Nodes.flushUploadsAndBarriers(commandBuffer); + IndexPool_Nodes_16.flushUploadsAndBarriers(commandBuffer); + IndexPool_Nodes_32.flushUploadsAndBarriers(commandBuffer); + } // Готовность кадров определяет когда можно удалять ненужные ресурсы, которые ещё используются в рендере void pushFrame(); @@ -1126,7 +997,6 @@ private: static constexpr uint8_t FRAME_COUNT_RESOURCE_LATENCY = 6; Vulkan* VkInst; - VkCommandPool CMDPool = nullptr; // Генератор вершин чанков ChunkMeshGenerator CMG; @@ -1193,8 +1063,10 @@ class VulkanRenderSession : public IRenderSession { ChunkPreparator CP; ModelProvider MP; + std::unique_ptr TP; + std::unique_ptr NSP; - AtlasImage MainTest, LightDummy; + AtlasImage LightDummy; Buffer TestQuad; std::optional TestVoxel; @@ -1206,8 +1078,6 @@ class VulkanRenderSession : public IRenderSession { .binding = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, Данные к атласу */ - VkDescriptorSetLayout MainAtlasDescLayout = VK_NULL_HANDLE; - VkDescriptorSet MainAtlasDescriptor = VK_NULL_HANDLE; /* .binding = 2, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, Воксельная карта освещения @@ -1232,7 +1102,6 @@ class VulkanRenderSession : public IRenderSession { NodeStaticOpaquePipeline = VK_NULL_HANDLE, NodeStaticTransparentPipeline = VK_NULL_HANDLE; - std::map ServerToAtlas; public: WorldPCO PCO; @@ -1254,13 +1123,13 @@ public: } void beforeDraw(); + void onGpuFinished(); void drawWorld(GlobalTime gTime, float dTime, VkCommandBuffer drawCmd); void pushStage(EnumRenderStage stage); static std::vector generateMeshForVoxelChunks(const std::vector& cubes); private: - void updateDescriptor_MainAtlas(); void updateDescriptor_VoxelsLight(); void updateDescriptor_ChunksLight(); }; diff --git a/Src/Common/Abstract.cpp b/Src/Common/Abstract.cpp index a29807d..7785ba0 100644 --- a/Src/Common/Abstract.cpp +++ b/Src/Common/Abstract.cpp @@ -948,8 +948,11 @@ PreparedNodeState::PreparedNodeState(const std::u8string_view data) { for(int counter4 = 0; counter4 < transformsSize; counter4++) { Transformation tr; tr.Op = Transformation::EnumTransform(lr.read()); + tr.Value = lr.read(); mod2.Transforms.push_back(tr); } + + mod.Models.push_back(std::move(mod2)); } mod.UVLock = lr.read(); @@ -961,6 +964,7 @@ PreparedNodeState::PreparedNodeState(const std::u8string_view data) { for(int counter3 = 0; counter3 < transformsSize; counter3++) { Transformation tr; tr.Op = Transformation::EnumTransform(lr.read()); + tr.Value = lr.read(); mod.Transforms.push_back(tr); } @@ -976,12 +980,15 @@ PreparedNodeState::PreparedNodeState(const std::u8string_view data) { for(int counter3 = 0; counter3 < transformsSize; counter3++) { Transformation tr; tr.Op = Transformation::EnumTransform(lr.read()); + tr.Value = lr.read(); mod.Transforms.push_back(tr); } variants.emplace_back(weight, std::move(mod)); } } + + Routes.emplace_back(nodeId, std::move(variants)); } lr.checkUnreaded(); @@ -1088,9 +1095,9 @@ uint16_t PreparedNodeState::parseCondition(const std::string_view expression) { char c = expression[pos]; // Числа - if(std::isdigit(c)) { + if(std::isdigit(static_cast(c))) { ssize_t npos = pos; - for(; npos < expression.size() && std::isdigit(expression[npos]); npos++); + for(; npos < expression.size() && std::isdigit(static_cast(expression[npos])); npos++); int value; std::string_view value_view = expression.substr(pos, npos-pos); auto [partial_ptr, partial_ec] = std::from_chars(value_view.data(), value_view.data() + value_view.size(), value); @@ -1102,15 +1109,20 @@ uint16_t PreparedNodeState::parseCondition(const std::string_view expression) { } tokens.push_back(value); + pos = npos - 1; continue; } // Переменные - if(std::isalpha(c) || c == ':') { + if(std::isalpha(static_cast(c)) || c == '_' || c == ':') { ssize_t npos = pos; - for(; npos < expression.size() && std::isalpha(expression[npos]); npos++); + for(; npos < expression.size(); npos++) { + char ch = expression[npos]; + if(!std::isalnum(static_cast(ch)) && ch != '_' && ch != ':') + break; + } std::string_view value = expression.substr(pos, npos-pos); - pos += value.size(); + pos = npos - 1; if(value == "true") tokens.push_back(1); else if(value == "false") @@ -1121,7 +1133,7 @@ uint16_t PreparedNodeState::parseCondition(const std::string_view expression) { } // Двойные операторы - if(pos-1 < expression.size()) { + if(pos + 1 < expression.size()) { char n = expression[pos+1]; if(c == '<' && n == '=') { @@ -1145,21 +1157,22 @@ uint16_t PreparedNodeState::parseCondition(const std::string_view expression) { // Операторы switch(c) { - case '(': tokens.push_back(EnumTokenKind::LParen); - case ')': tokens.push_back(EnumTokenKind::RParen); - case '+': tokens.push_back(EnumTokenKind::Plus); - case '-': tokens.push_back(EnumTokenKind::Minus); - case '*': tokens.push_back(EnumTokenKind::Star); - case '/': tokens.push_back(EnumTokenKind::Slash); - case '%': tokens.push_back(EnumTokenKind::Percent); - case '!': tokens.push_back(EnumTokenKind::Not); - case '&': tokens.push_back(EnumTokenKind::And); - case '|': tokens.push_back(EnumTokenKind::Or); - case '<': tokens.push_back(EnumTokenKind::LT); - case '>': tokens.push_back(EnumTokenKind::GT); + case '(': tokens.push_back(EnumTokenKind::LParen); break; + case ')': tokens.push_back(EnumTokenKind::RParen); break; + case '+': tokens.push_back(EnumTokenKind::Plus); break; + case '-': tokens.push_back(EnumTokenKind::Minus); break; + case '*': tokens.push_back(EnumTokenKind::Star); break; + case '/': tokens.push_back(EnumTokenKind::Slash); break; + case '%': tokens.push_back(EnumTokenKind::Percent); break; + case '!': tokens.push_back(EnumTokenKind::Not); break; + case '&': tokens.push_back(EnumTokenKind::And); break; + case '|': tokens.push_back(EnumTokenKind::Or); break; + case '<': tokens.push_back(EnumTokenKind::LT); break; + case '>': tokens.push_back(EnumTokenKind::GT); break; + default: + MAKE_ERROR("Недопустимый символ: " << c); } - - MAKE_ERROR("Недопустимый символ: " << c); + continue; } @@ -2100,4 +2113,4 @@ Resource Resource::convertToMem() const { auto Resource::operator<=>(const Resource&) const = default; -} \ No newline at end of file +} diff --git a/Src/Server/AssetsManager.cpp b/Src/Server/AssetsManager.cpp index 8bad1b8..92cc2aa 100644 --- a/Src/Server/AssetsManager.cpp +++ b/Src/Server/AssetsManager.cpp @@ -499,15 +499,15 @@ AssetsManager::Out_applyResourceChange AssetsManager::applyResourceChange(const PreparedNodeState nodestate = _nodestate; // Ресолвим модели - for(const auto& [lKey, lDomain] : nodestate.LocalToModelKD) { - nodestate.LocalToModel.push_back(lock->getId(EnumAssets::Nodestate, lDomain, lKey)); + for(const auto& [lDomain, lKey] : nodestate.LocalToModelKD) { + nodestate.LocalToModel.push_back(lock->getId(EnumAssets::Model, lDomain, lKey)); } // Сдампим для отправки клиенту (Кеш в пролёте?) Resource res(nodestate.dump()); // На оповещение - result.NewOrChange[(int) EnumAssets::Model].push_back({resId, res}); + result.NewOrChange[(int) EnumAssets::Nodestate].push_back({resId, res}); // Запись в таблице ресурсов data.emplace(ftt, res, domain, key); @@ -717,4 +717,4 @@ AssetsManager::Out_applyResourceChange AssetsManager::applyResourceChange(const return result; } -} \ No newline at end of file +} diff --git a/Src/Server/AssetsManager.hpp b/Src/Server/AssetsManager.hpp index e840fd3..b5614c9 100644 --- a/Src/Server/AssetsManager.hpp +++ b/Src/Server/AssetsManager.hpp @@ -258,6 +258,10 @@ public: std::tuple, std::vector> getNodeDependency(const std::string& domain, const std::string& key) { + if(domain == "core" && key == "none") { + return {0, {}, {}}; + } + auto lock = LocalObj.lock(); AssetsNodestate nodestateId = lock->getId(EnumAssets::Nodestate, domain, key+".json"); @@ -301,4 +305,4 @@ private: }; -} \ No newline at end of file +} diff --git a/Src/Server/GameServer.cpp b/Src/Server/GameServer.cpp index 2a22ee0..f578c2a 100644 --- a/Src/Server/GameServer.cpp +++ b/Src/Server/GameServer.cpp @@ -862,7 +862,6 @@ void GameServer::BackingAsyncLua_t::run(int id) { Pos::bvec64u nodePos(x, y, z); auto &node = out.Nodes[Pos::bvec4u(nodePos >> 4).pack()][Pos::bvec16u(nodePos & 0xf).pack()]; node.NodeId = id; - node.Meta = 0; if(x == 0 && z == 0) node.NodeId = 1; @@ -876,7 +875,7 @@ void GameServer::BackingAsyncLua_t::run(int id) { else if(x == 0 && y == 1) node.NodeId = 0; - // node.Meta = 0; + node.Meta = uint8_t((x + y + z + int(node.NodeId)) & 0x3); } } // else { @@ -1445,12 +1444,7 @@ void GameServer::init(fs::path worldPath) { { sol::table t = LuaMainState.create_table(); - Content.CM.registerBase(EnumDefContent::Node, "test", "test0", t); - Content.CM.registerBase(EnumDefContent::Node, "test", "test1", t); - Content.CM.registerBase(EnumDefContent::Node, "test", "test2", t); - Content.CM.registerBase(EnumDefContent::Node, "test", "test3", t); - Content.CM.registerBase(EnumDefContent::Node, "test", "test4", t); - Content.CM.registerBase(EnumDefContent::Node, "test", "test5", t); + Content.CM.registerBase(EnumDefContent::Node, "core", "none", t); Content.CM.registerBase(EnumDefContent::World, "test", "devel_world", t); } @@ -2364,7 +2358,9 @@ void GameServer::stepSyncContent() { auto region = Expanse.Worlds[0]->Regions.find(rPos); if(region != Expanse.Worlds[0]->Regions.end()) { - region->second->Nodes[cPos.pack()][nPos.pack()].NodeId = 4; + Node& n = region->second->Nodes[cPos.pack()][nPos.pack()]; + n.NodeId = 4; + n.Meta = uint8_t((int(nPos.x) + int(nPos.y) + int(nPos.z)) & 0x3); region->second->IsChunkChanged_Nodes |= 1ull << cPos.pack(); } } @@ -2379,7 +2375,9 @@ void GameServer::stepSyncContent() { auto region = Expanse.Worlds[0]->Regions.find(rPos); if(region != Expanse.Worlds[0]->Regions.end()) { - region->second->Nodes[cPos.pack()][nPos.pack()].NodeId = 0; + Node& n = region->second->Nodes[cPos.pack()][nPos.pack()]; + n.NodeId = 0; + n.Meta = 0; region->second->IsChunkChanged_Nodes |= 1ull << cPos.pack(); } } @@ -2493,4 +2491,4 @@ void GameServer::stepSyncContent() { } -} \ No newline at end of file +} diff --git a/assets/shaders/chunk/node.vert b/assets/shaders/chunk/node.vert index 21927e2..fe40d92 100644 --- a/assets/shaders/chunk/node.vert +++ b/assets/shaders/chunk/node.vert @@ -16,25 +16,29 @@ layout(push_constant) uniform UniformBufferObject { // struct NodeVertexStatic { // uint32_t -// FX : 9, FY : 9, FZ : 9, // Позиция -224 ~ 288; 64 позиций в одной ноде, 7.5 метров в ряд -// N1 : 4, // Не занято -// LS : 1, // Масштаб карты освещения (1м/16 или 1м) -// Tex : 18, // Текстура -// N2 : 14, // Не занято -// TU : 16, TV : 16; // UV на текстуре +// FX : 11, FY : 11, N1 : 10, // Позиция, 64 позиции на метр, +3.5м запас +// FZ : 11, // Позиция +// LS : 1, // Масштаб карты освещения (1м/16 или 1м) +// Tex : 18, // Текстура +// N2 : 2, // Не занято +// TU : 16, TV : 16; // UV на текстуре // }; void main() { + uint fx = Vertex.x & 0x7ffu; + uint fy = (Vertex.x >> 11) & 0x7ffu; + uint fz = Vertex.y & 0x7ffu; + vec4 baseVec = ubo.model*vec4( - float(Vertex.x & 0x1ff) / 64.f - 3.5f, - float((Vertex.x >> 9) & 0x1ff) / 64.f - 3.5f, - float((Vertex.x >> 18) & 0x1ff) / 64.f - 3.5f, + float(fx) / 64.f - 3.5f, + float(fy) / 64.f - 3.5f, + float(fz) / 64.f - 3.5f, 1 ); Geometry.GeoPos = baseVec.xyz; - Geometry.Texture = Vertex.y & 0x3ffff; + Geometry.Texture = (Vertex.y >> 12) & 0x3ffffu; Geometry.UV = vec2( float(Vertex.z & 0xffff) / pow(2, 16), float((Vertex.z >> 16) & 0xffff) / pow(2, 16) diff --git a/assets/shaders/chunk/node.vert.bin b/assets/shaders/chunk/node.vert.bin index b78d2661b7ead08e9470464fcf1abb2f865fe94d..2e53f239da4b97129018469011d4f9f86fe326c1 100644 GIT binary patch literal 2784 zcmZ9M+jmn{5XN_#CPgYBAQu&C+HzB*q5`5^8d~sF2_(>q7r-=4PT+K>3_6f%I$YSxfWC^ zLHS;-(d*R)@m@QuwWCg0YexOB5l1gW;`U|YqE4edY-R)UipbW?tHwUnXuWa{A3bb_ z{W$b?&K<$GhcD+o0KZP+=7hKFgK#r!$T#g4WN&n$R=2;qJZ!bXer0<{J!ujJ?~~j0 z`rVzEQTQ9UpjE747Q5YM*p{XS`yXz1h4b!~a1eHP!??evbbbvSkN9eLkmP|Q&wBVO z9`-}UP5HIVZex?Yc|TLjC0b0u)uo@eYt^uCJ0@Sk6N9fih~lW*5j*YUspI*m6R${K52OpECAYGL36lJp)1pb6<-YhU{@b8zVF8qzuwq7pECiYzk z5Ag@2iHA2oGaji3^^XY`CB)#zJ}x1r#eXP_|G2tfRz(TDeJqX5{-{-yrg!+0vgcEb ze^&N_G#o@8{5aHg}WT9>wS z+!97uw=K;&{OlXMk#hK7q?onW^j^>}=gh;pf5q^gvX(o+cNuLq-{P#z_t@;B@LX!& zQ5Nnn?<;qbIo?uE=5eW52qY^zpXB)31>Nu3XkV_UmW~jX^s=ZaJ&PS_oOhq z<)wEx%lnZqd6}v8c1jrDa-0^1vmERfjvVKuFG#ri=OpBqm;OWoZ#h2|W=}o&Im3$* zxIdHw`%=pBPW+y7pQk+Uh2^;{%pNSy7s7Cs=SyL@Y@V-V!)NnckcqJ}-$>|_8u)o<|5X3@zLhWsVmMRo1sr?9e@6mW(PVh%mQxO%XF|f= zWX>zncO^@bl7xKN$(rPz-IG0jmfwkkw>z>b3>Qf3e18yz=iR2(ed)4^_~jDQLn1K@@;u0p$~lYz;~x6VgG#3*grOL__^~<33t9EVgK{ep#ZSyE{U2~V!# zg;)L(FTC)Bc%jTW_ufgVbX}ji`|F;bp6MP>jL%Pp(pWegWof2@_eMF-OC5nLf$y-d5Vum<6Z6Rq!L2;BOrH4+|#4OsG`rm0GP* zy<2Z|I`v-vX)~!e(^gX7NxMm-pFU2EJ2VuRwi?aB&Tv49h~Yi8tz>(vmuw`BPzux3 zAHsSo-EDXGmIu4LNw>EBi2R&tD)ZqEoo@Tl<23nIeTr4C@mzcDour8@QM>0EY`4)T zvvX}E-G1^!UnzWouadO)l79Co>9ZVie(--GZIY;@v0X(^h{i{f*mnP406)e7Bov=T6$| zZ?ziMlKbqczCpA3%;)*-pZj1AkY#Y1D9@#Be3{=vNI7u^d#2F!&taQeP8k0@(%F{3R@ly@+(~TDTn1y{ zLSY{*Y}eZhW6fv$31k`gS>KCDIqNt6B+^{+?x*d0j`@u}4g8EhgVZPQ{>oRt0i?EP z&wH`I9PKh(2%Af>-t$=28147aUnp$Xe*{9zNPXtfZ?2obb4Gg^{or6Ci@$?ze7pmDat~NjjIWH?Utk{` zIO?&sps6kgl<&IW*@t&Y3>px*w3P)9<;p^Z&`Yf2;7j zc9FZYUgyxyb^pTnPF?HV+9P9c!CKpK;4C_C*NJh)?3=tX&c;i?`Eh29@s3^w@_!Sf zeYW77n+`Fa?bU++gBb1C3eH*jz2MFjd>I+{dmVjrzj-+Mzl#0dK$mmwVhwMi%MaJ^ z7Pfq>;caxe;Tqn-mM?=?!@KCCHC%v`k2SoHF6XSr8Wzyyhimu%TRzrs5nXP$h7Ymj z%V3ob*`tqv_hWtfeM5h8etnmKeKO_-owl!X_DBCUAXlU7zJbpQPTp?}I4hplw{inq z1->EkX}c!g#}c~t;k?EfxP`t1;@NMb%PnWJ6ns;6fq9)T^J?dD`E0FVo3{dD-p|qH zW8VAda=y1XTUB)Vc&A^X%lQUl%{BDV+^cZ%v4>xy%RK;bzcqCE;r$+B%g6oJ(dFE4 zl{;KSZUF1hXAQpjZ-M8(2Ry&Far%!Te*&|>d-wb^$e)3H>}4I@UdG;Uq8lS`o-O1y R@c%;WpY|sBk4|3!{{uQ9z@7jA diff --git a/assets/shaders/chunk/node_opaque.frag b/assets/shaders/chunk/node_opaque.frag index 3300e4f..b32a60c 100644 --- a/assets/shaders/chunk/node_opaque.frag +++ b/assets/shaders/chunk/node_opaque.frag @@ -11,65 +11,36 @@ layout(location = 0) in FragmentObj { layout(location = 0) out vec4 Frame; -struct InfoSubTexture { - uint Flags; // 1 isExist - uint PosXY, WidthHeight; - - uint AnimationFrames_AnimationTimePerFrame; +struct AtlasEntry { + vec4 UVMinMax; + uint Layer; + uint Flags; + uint _Pad0; + uint _Pad1; }; -uniform layout(set = 0, binding = 0) sampler2D MainAtlas; -layout(set = 0, binding = 1) readonly buffer MainAtlasLayoutObj { - uint SubsCount; - uint Counter; - uint WidthHeight; +const uint ATLAS_ENTRY_VALID = 1u; - InfoSubTexture SubTextures[]; +uniform layout(set = 0, binding = 0) sampler2DArray MainAtlas; +layout(set = 0, binding = 1) readonly buffer MainAtlasLayoutObj { + AtlasEntry Entries[]; } MainAtlasLayout; -uniform layout(set = 1, binding = 0) sampler2D LightMap; +uniform layout(set = 1, binding = 0) sampler2DArray LightMap; layout(set = 1, binding = 1) readonly buffer LightMapLayoutObj { vec3 Color; } LightMapLayout; vec4 atlasColor(uint texId, vec2 uv) { - uint flags = (texId & 0xffff0000) >> 16; - texId &= 0xffff; - vec4 color = vec4(uv, 0, 1); - - if((flags & (2 | 4)) > 0) - { - if((flags & 2) > 0) - color = vec4(1, 1, 1, 1); - else if((flags & 4) > 0) - { - color = vec4(1); - } - - } - else if(texId >= uint(MainAtlasLayout.SubsCount)) - return vec4(((int(gl_FragCoord.x / 128) + int(gl_FragCoord.y / 128)) % 2 ) * vec3(0, 1, 1), 1); - else { - InfoSubTexture texInfo = MainAtlasLayout.SubTextures[texId]; - if(texInfo.Flags == 0) - return vec4(((int(gl_FragCoord.x / 128) + int(gl_FragCoord.y / 128)) % 2 ) * vec3(1, 0, 1), 1); + AtlasEntry entry = MainAtlasLayout.Entries[texId]; + if((entry.Flags & ATLAS_ENTRY_VALID) == 0u) + return vec4(((int(gl_FragCoord.x / 128) + int(gl_FragCoord.y / 128)) % 2) * vec3(1, 0, 1), 1); - uint posX = texInfo.PosXY & 0xffff; - uint posY = (texInfo.PosXY >> 16) & 0xffff; - uint width = texInfo.WidthHeight & 0xffff; - uint height = (texInfo.WidthHeight >> 16) & 0xffff; - uint awidth = MainAtlasLayout.WidthHeight & 0xffff; - uint aheight = (MainAtlasLayout.WidthHeight >> 16) & 0xffff; - - if((flags & 1) > 0) - color = texture(MainAtlas, vec2((posX+0.5f+uv.x*(width-1))/awidth, (posY+0.5f+(1-uv.y)*(height-1))/aheight)); - else - color = texture(MainAtlas, vec2((posX+uv.x*width)/awidth, (posY+(1-uv.y)*height)/aheight)); - } - - - return color; + vec2 baseUV = vec2(uv.x, 1.0f - uv.y); + vec2 atlasUV = mix(entry.UVMinMax.xy, entry.UVMinMax.zw, baseUV); + atlasUV = clamp(atlasUV, entry.UVMinMax.xy, entry.UVMinMax.zw); + return texture(MainAtlas, vec3(atlasUV, entry.Layer)); } vec3 blendOverlay(vec3 base, vec3 blend) { diff --git a/assets/shaders/chunk/node_opaque.frag.bin b/assets/shaders/chunk/node_opaque.frag.bin index 780834d27fd89d5a19478523f154435a51ab4102..007905cb453f3e2afae90de63890e8746e00385e 100644 GIT binary patch literal 4276 zcmZvdX>(Ln5QcATcW0hs5M#;{Tx--l4wrU zfZ7^0$1T8j!uSM?BT3VtNaQ(b(d_ET}O;rXiUa8jZ zp34>$@{`os9KU~^f6O}*qs8pea&EM*R4Uu@_^y4(i@C}C*>hF<1a|yh5Wd3@_Je%r z_Q$y*(HJ8U`;@Bo=on42IQuH+O6N<1B_1Gf_U2@%JeDgym2qwEx#BTJ+DpHg zNByJd7(2d`YkgHb;JlmMVi5eio18I$^KNo}tAX>~w70-dWjp1x7Ir@FbTxfgtu`#TKGqrbmT-XZis+wVuNgS!1*tXyMF5l_J^^(??Jym!aje${b1<#HSDWEKljZ0 zIntWKyIRjpP&+^0BtD^kcd%W~@?E4gnd?WSvnZ#pw%@qiF1{~w`mMP<^FB-yQeWTP z{}D=Uzd!#+X!qh}zovja?F0w#vDRrI-$O>@we`23vp|3QuD`u~8_4T#e(k+v^1a^y zi-39D)5o`$H>Yp#J}~DnFsFC_0Lb6Q$3A|XaQ3^68_0bE8o);2yXs@S=Y9%2X8=$0 zeU@+%i10Os)9K9$sQ16^O2chN^K#C(Zv%z6-FzCt&~GmX(l zF2sC|Zp=*(Vs4=uV~xh>BNt-6MK{Jd^3HRS-vRkwsdWSLcEb5v@FzKbK)U|I-@WgF zoHL{THx;VO{e!*-IAiwHH*`67yUFDYEl70dY9TOoG16IEid+Wt3$xc(XP-!H>v!(f z0CV`zGIAW6Y@FG4U7x#vN_?e6}W@wa?W<(4ketkJIE_IKfSc?5BVZ6mwV2OGbv}@46+|MFV38OxAv1jet^4}V<6$=UEV#! z52Dw{pMsMQ@h_pv344?Ro@MUSz?#~4740*?{LYK#8mo`BpFp}FV$Y(>g<8&`*Vl3$ zPCnG~GP;~_)!yY5?E3gu;p9X7Yv^(!+$GfhI-L2sfwg&-{-JjFt^EQr2Zj>cdtU_M zE-mOIz*>fZF@CpsAU}ca3|~sPFo&bq=5Yn?GP;~~4U%OqvHSt5 literal 6812 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 Mh<}5K{Xf%x0VEN*0ssI2 diff --git a/assets/shaders/chunk/node_transparent.frag b/assets/shaders/chunk/node_transparent.frag index 81b14c2..f74d24e 100644 --- a/assets/shaders/chunk/node_transparent.frag +++ b/assets/shaders/chunk/node_transparent.frag @@ -9,9 +9,19 @@ layout(location = 0) in FragmentObj { layout(location = 0) out vec4 Frame; +struct AtlasEntry { + vec4 UVMinMax; + uint Layer; + uint Flags; + uint _Pad0; + uint _Pad1; +}; + +const uint ATLAS_ENTRY_VALID = 1u; + uniform layout(set = 0, binding = 0) sampler2DArray MainAtlas; layout(set = 0, binding = 1) readonly buffer MainAtlasLayoutObj { - vec3 Color; + AtlasEntry Entries[]; } MainAtlasLayout; uniform layout(set = 1, binding = 0) sampler2DArray LightMap; @@ -19,6 +29,19 @@ layout(set = 1, binding = 1) readonly buffer LightMapLayoutObj { vec3 Color; } LightMapLayout; -void main() { - Frame = vec4(Fragment.GeoPos, 1); +vec4 atlasColor(uint texId, vec2 uv) +{ + AtlasEntry entry = MainAtlasLayout.Entries[texId]; + if((entry.Flags & ATLAS_ENTRY_VALID) == 0u) + return vec4(((int(gl_FragCoord.x / 128) + int(gl_FragCoord.y / 128)) % 2) * vec3(1, 0, 1), 1); + + vec2 baseUV = vec2(uv.x, 1.0f - uv.y); + vec2 atlasUV = mix(entry.UVMinMax.xy, entry.UVMinMax.zw, baseUV); + atlasUV = clamp(atlasUV, entry.UVMinMax.xy, entry.UVMinMax.zw); + return texture(MainAtlas, vec3(atlasUV, entry.Layer)); +} + +void main() { + Frame = atlasColor(Fragment.Texture, Fragment.UV); + Frame.xyz *= max(0.2f, dot(Fragment.Normal, normalize(vec3(0.5, 1, 0.8)))); } diff --git a/assets/shaders/chunk/node_transparent.frag.bin b/assets/shaders/chunk/node_transparent.frag.bin index 6c6c733875e190360348104db0281a99dc1a2147..e7259ae1e404c96e32c09bf82a96db8a80693230 100644 GIT binary patch literal 4156 zcmZvddvlXj5QpEArUhE=Hvy?BD56Dfaz_fC0s%>Bk(*c2(1sEwZEBj<)@v0nD2k}{ zOZXA|0{(-aN5&Z)pXbecAY?q9-|X)1?C#mKXWulnXj^-fZizagWznxueXWWXMJ=cu zQClptxl6fSQ?=1u`w#5VV|kQ{9ralebw;g7Fl+ER{gM-=J>HNs#WPYkPS1#turHNvGv{WsQ)Jii&{T4U)l_o~Y)1!?J zX*?QhNpsQ)&M4H%g{l5Zxl-LRz4zG6&3(tBNZ)1b)rzy{Mv-aiS&p2ZQBOy!(EE%# zJyEO9MRcPr=Aff1R|iTH1BF@jw#3(dso^>){l*AS&Q9Vi=3;u7Ww8{72cZG1i#tW4JiZ``ie?w-pQ zm5Ni;+7`cmlYh)R6JzE4nQCFIzf!5%^7yX($Qy;J;+3m)`#g61UJ$;+F!tko==R6C zBGEV_5&Kl?_SiU0vpD-I=gJlG8@x?04qHIB76^_tT0 z3|qMQq2n5S)5!uwB=!eS+39%ZbEUCcwSmGUTg0?SKO+Oz(8{I*%_ zj565%HVXOmcrK3lDB8f@5_D~2^<9e8SKfVl=)P~+&75>1<@C|F3(em(IrBHde5>K~ z)kj}v=Kp-RP?z)Jeiyp$uWgPENY9l!me}@3&iigeW{+X;S#G9+>+t#+pVx^c=e@Ltfy=t= z?+7rD{{B9BhtLOYzaP0S>h^oFc404_CFA_jGKc$~qwTjO7tX1on@9gzvS$wM*~B(q z=-0>Se?H9hy8lVC@3~=r9)0}a@4dnPJhqQM?)Mki*B-4s5&C@xdmiZLo_W7TT2pve z>$wk{pU^MAd6%>6H*F2Z{(^M&$H43O_7qw(7M+rK%WzkSx}Jke#;k=I_{zC>+&x8$c32i(T&*#Ld*~7#(1VN`pAWtAJL7u4?@fX zbYra17=7gaAvBd8;pf{fOLX6RIdCRcBAtQN$TdK}FcbYv_W8sP^ROA) zoPE5hzlB?Xzn`0cd;0m?*ah5s(9#`>@SpO(D;#=<*@YessCUJO{Al=Q+!~2Z6ce?~zS8#s%fbsgD1pbfsm)^SOKak?x1sE9i2emaFK^ zwOoUf54F6BE+<@#cXQzm6<`;l%dd zH$b>cJNhWFmJwi#-)#}dPh$IB-AuSJhhy00aRu%cx}0?lk>v=o1oU;!9M_nI4&-fM MUG6)p+Wsc~2WjC=DF6Tf literal 1448 zcmZ9LOK;Oa6on^=)0Bq3C~YZ);1F0LRV)A@AwU%Zcj1yE9^1$yRxxtoC~*+o@dNom zY>+tL*ppb?==#3SoI7)8(x~@Zrdcy>vuPfhLUv8v)a16!nzcdrF+7>)qmw63AA{I3 z4GRRbEtDsVj-zBk;hJOP3s%ez@L)^gL-Z?lLm$MGcZfBmi# z7CEI&5N8+J+$?jP?|n8KN9iiZo>h#=~8uRP*k^W`3<>}{BMtSf@9^u z!FLCcgdF=J=8Yg7VR-}e0((!EbC3f|>`TeHwO#oLYq<}%wkIDPIk3dsA-QLIYs`(i zK$!6#Wty_5`f}zyDRW{LFNK8nrj+~#VL0> 16; - texId &= 0xffff; - vec4 color = vec4(uv, 0, 1); - - if((flags & (2 | 4)) > 0) - { - if((flags & 2) > 0) - color = vec4(1, 1, 1, 1); - else if((flags & 4) > 0) - { - color = vec4(1); - } - - } - else if(texId >= uint(MainAtlasLayout.SubsCount)) - return vec4(((int(gl_FragCoord.x / 128) + int(gl_FragCoord.y / 128)) % 2 ) * vec3(0, 1, 1), 1); - else { - InfoSubTexture texInfo = MainAtlasLayout.SubTextures[texId]; - if(texInfo.Flags == 0) - return vec4(((int(gl_FragCoord.x / 128) + int(gl_FragCoord.y / 128)) % 2 ) * vec3(1, 0, 1), 1); + AtlasEntry entry = MainAtlasLayout.Entries[texId]; + if((entry.Flags & ATLAS_ENTRY_VALID) == 0u) + return vec4(((int(gl_FragCoord.x / 128) + int(gl_FragCoord.y / 128)) % 2) * vec3(1, 0, 1), 1); - uint posX = texInfo.PosXY & 0xffff; - uint posY = (texInfo.PosXY >> 16) & 0xffff; - uint width = texInfo.WidthHeight & 0xffff; - uint height = (texInfo.WidthHeight >> 16) & 0xffff; - uint awidth = MainAtlasLayout.WidthHeight & 0xffff; - uint aheight = (MainAtlasLayout.WidthHeight >> 16) & 0xffff; - - if((flags & 1) > 0) - color = texture(MainAtlas, vec2((posX+0.5f+uv.x*(width-1))/awidth, (posY+0.5f+(1-uv.y)*(height-1))/aheight)); - else - color = texture(MainAtlas, vec2((posX+uv.x*width)/awidth, (posY+(1-uv.y)*height)/aheight)); - } - - - return color; + vec2 baseUV = vec2(uv.x, 1.0f - uv.y); + vec2 atlasUV = mix(entry.UVMinMax.xy, entry.UVMinMax.zw, baseUV); + atlasUV = clamp(atlasUV, entry.UVMinMax.xy, entry.UVMinMax.zw); + return texture(MainAtlas, vec3(atlasUV, entry.Layer)); } void main() { diff --git a/assets/shaders/chunk/voxel_opaque.frag.bin b/assets/shaders/chunk/voxel_opaque.frag.bin index e1322479c173cd9b5e356ec6292b7da56bd284ff..5df2a7963b17bbfad614b7627370a086087bd44b 100644 GIT binary patch literal 4636 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 DhVjxy diff --git a/assets/shaders/chunk/voxel_transparent.frag b/assets/shaders/chunk/voxel_transparent.frag index d5aa336..a14f394 100644 --- a/assets/shaders/chunk/voxel_transparent.frag +++ b/assets/shaders/chunk/voxel_transparent.frag @@ -9,9 +9,19 @@ layout(location = 0) in Fragment { layout(location = 0) out vec4 Frame; +struct AtlasEntry { + vec4 UVMinMax; + uint Layer; + uint Flags; + uint _Pad0; + uint _Pad1; +}; + +const uint ATLAS_ENTRY_VALID = 1u; + uniform layout(set = 0, binding = 0) sampler2DArray MainAtlas; layout(set = 0, binding = 1) readonly buffer MainAtlasLayoutObj { - vec3 Color; + AtlasEntry Entries[]; } MainAtlasLayout; uniform layout(set = 1, binding = 0) sampler2DArray LightMap; @@ -19,6 +29,39 @@ layout(set = 1, binding = 1) readonly buffer LightMapLayoutObj { vec3 Color; } LightMapLayout; -void main() { - Frame = vec4(1); +vec4 atlasColor(uint texId, vec2 uv) +{ + uv = mod(uv, 1); + + AtlasEntry entry = MainAtlasLayout.Entries[texId]; + if((entry.Flags & ATLAS_ENTRY_VALID) == 0u) + return vec4(((int(gl_FragCoord.x / 128) + int(gl_FragCoord.y / 128)) % 2) * vec3(1, 0, 1), 1); + + vec2 baseUV = vec2(uv.x, 1.0f - uv.y); + vec2 atlasUV = mix(entry.UVMinMax.xy, entry.UVMinMax.zw, baseUV); + atlasUV = clamp(atlasUV, entry.UVMinMax.xy, entry.UVMinMax.zw); + return texture(MainAtlas, vec3(atlasUV, entry.Layer)); +} + +void main() { + vec2 uv; + + switch(fragment.Place) { + case 0: + uv = fragment.GeoPos.xz; break; + case 1: + uv = fragment.GeoPos.xy; break; + case 2: + uv = fragment.GeoPos.zy; break; + case 3: + uv = fragment.GeoPos.xz*vec2(-1, -1); break; + case 4: + uv = fragment.GeoPos.xy*vec2(-1, 1); break; + case 5: + uv = fragment.GeoPos.zy*vec2(-1, 1); break; + default: + uv = vec2(0); + } + + Frame = atlasColor(fragment.VoxMTL, uv); } diff --git a/assets/shaders/chunk/voxel_transparent.frag.bin b/assets/shaders/chunk/voxel_transparent.frag.bin index bb402063dbd959962970d407676654e7d79887f3..7ef98fe8c002c16ea479bbd6abe6c76b0816b758 100644 GIT binary patch literal 4604 zcmZve`;(N_5r)t1%q}PhsE8V61u^jgB6wGVfJ6p$mR&Dkyd>+eJG+hR(HSMr%#_g{moJv+_EAK4Ww1+ zg7ih|uM5*)8bDo@h6~wfKGNL2usF5-*4uU%u_o1uKx5XX)v1mY$6B3P9an;zz$BOl zr@^P-bFhxR#;yQ0{tY6(B2hh^pBl}H#@JY+dEdn3+}y;%;>m7%qT88mPfT^@+mnl( z<89+smf||Ilij7Ma=;J~PQO?z9_r`lP+}_IBizB@gqmMS_>IG*nn&gid z`!L>ZO)_G|nZDt4DZjJvGF*Q)>cvcR{;__1J$-_2t_4x-uctHT*h9tqH#;-WERME{ zGM&rMhzYLL)}khTu3}`7_SNs_-1w9Z;_=N~Tl(TG=e_0Dfr#_oa_dW6Epz6HIA_2( zzvTu!yBs+THWhZQmt3mI~qu*TWn}PL(eKopE+c{YU{BE@;k;c2i z{yx5T_C59aZX-oxKlXIR_IIe9OWR!iT583c3}Sn)c!RiC*yVfqCUGxybN6%Ai}#A$ z7xVR*cQd`HA=NM8jOZz5+Jx=zqLAM}2&A;UX9SPGamILF8Rp*r=U!us^`8GPb|ZBxn0@z z{pGy>M&u?i0IW|t-p~E|j5T-ouR*s5+SYt6i2d5v1J`<@Za|u6JJJ~Iw+?OROiurf ztlOXH(=F(%{q}Y@iay^i{P&={wo!%tJ;2_|E6v+L+C%qhk7e6BF6ZT*M(VQ;S5;oY zi7oi2f%}glU2@(-ds2cG{h3BL&j@w-V`FXh#W_)$=a)!h^qWW9Z$~cj_&3Y@8t;#+ zzSzHiHGY`kn{Y^#ZU)_uWH;_1Uj) z;LNShT-LLUd<9s~2(TXe@+y!Y<3_&Asm#f{JU?>lE9bVp$o(36HTSRKUn>JlaKTMgf1ss-X+d^4_&|CN}MNGKF_&`^WI0Vp7#Nq ze4O_ox}1pK#d#mW=`WurS3b{K^E_w%ufX%BLCpN$GH<-`|0AtM5#<@8U~Y zh5iq)7Pv>BF{hF8|Dh{)0>4{hjdS^Z-$9(;r~iLf_uE_p{2taJ{eCwfF9pWM_kEyZ zAIf&rdLg!X#`D`>jDAtZo0(gWZJa*qHC`@yaTU6C?cndz_dfK!z!luR=zGCHiQAvK zLF72E;0*o*xZimF&c=V~oxDA?m)^aBJP4ePau4s%e8fDEW6J$`5KjJ7uD6NqIr1@= zqv+<+=e#)Aa^@XFjsxc<{tj6CLz$1?goiUH@AB@k{}J@+{zu{DWB*}vIbn}#z&Ab* zc?4L~Y9>YdF<}1fz;oSejI|#^>W{rYMVE_O9!Ia%@&ufG)bb>{oN$%z@)UM;|Igs$ zWBiYM+2J-&SC4o@IR0uHV{Q$SE+HZSUO%ahDb7Gr(G=fqRCL&j9(kJj0#L p#cV!{Z5~%}zd)C>u5q$B>s?^1K65fgc5QaCg-IRv5l+u<`h*Ek4RU7~zA%PHr^x!C!fL#Vad(PVaJ-uc{h&^&3GsA1Y>(>yR0?U<%%=-4)ER`%i#@#&%*pFVx|1jLpJ zED+4LP@yC|NwO)98{!@Dp7^f#P<&r}OL5>@;y}Nqet$Vm^I?AucJ|Ggp?*pA)-p`YX!cC;#cBNlV(rKxqCai3n zduefAELJw=yC{D2KgQTX8%xjer;9p{O`PY+$mM75FY~0%PfhmF*KXdFd7|{Pw9LM5 zi<9f(svLa&R@KG_y)E)$E*YsBd#rO*c-bSn+c>-YTK1C}G334{>1VV=Mh(tp|2qc6X*&Hy<;dsbi;~czAp;wUFfIKAgVF z9oilEwRvy&D|}%0;cC12U2UiPwb_TO&DpPw?sD(y1(WyKzGh?3`#P`5))gP9ciuWW z@3O1^bxGa~_LobYH;V0%bg&y@V}8DVEEKr57@N=Jz9}1cV)vwPN_NE%{w+!T^w`mf zw@(Z>d=);_cUkQUKRuyyzYX!BzM$KZx1?g#&Ky({Ks?+`osElVW%N*X(7uYWYn3-Mnh C6lv7} diff --git a/mods/test/assets/test/model/node/acacia_planks.json b/mods/test/assets/test/model/node/acacia_planks.json new file mode 100644 index 0000000..e815d04 --- /dev/null +++ b/mods/test/assets/test/model/node/acacia_planks.json @@ -0,0 +1,81 @@ +{ + "textures": { + "default": "acacia_planks.png" + }, + "cuboids": [ + { + "from": [ + -0.5, + -0.5, + -0.5 + ], + "to": [ + 0.5, + 0.5, + 0.5 + ], + "faces": { + "down": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "down" + }, + "up": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "up" + }, + "north": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "north" + }, + "south": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "south" + }, + "west": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "west" + }, + "east": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "east" + } + } + } + ] +} diff --git a/mods/test/assets/test/model/node/frame.json b/mods/test/assets/test/model/node/frame.json new file mode 100644 index 0000000..3b71dbd --- /dev/null +++ b/mods/test/assets/test/model/node/frame.json @@ -0,0 +1,81 @@ +{ + "textures": { + "default": "frame.png" + }, + "cuboids": [ + { + "from": [ + -0.5, + -0.5, + -0.5 + ], + "to": [ + 0.5, + 0.5, + 0.5 + ], + "faces": { + "down": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "down" + }, + "up": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "up" + }, + "north": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "north" + }, + "south": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "south" + }, + "west": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "west" + }, + "east": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "east" + } + } + } + ] +} diff --git a/mods/test/assets/test/model/node/grass.json b/mods/test/assets/test/model/node/grass.json new file mode 100644 index 0000000..c8cdff5 --- /dev/null +++ b/mods/test/assets/test/model/node/grass.json @@ -0,0 +1,81 @@ +{ + "textures": { + "default": "grass.png" + }, + "cuboids": [ + { + "from": [ + -0.5, + -0.5, + -0.5 + ], + "to": [ + 0.5, + 0.5, + 0.5 + ], + "faces": { + "down": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "down" + }, + "up": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "up" + }, + "north": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "north" + }, + "south": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "south" + }, + "west": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "west" + }, + "east": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "east" + } + } + } + ] +} diff --git a/mods/test/assets/test/model/node/jungle_planks.json b/mods/test/assets/test/model/node/jungle_planks.json new file mode 100644 index 0000000..60c5c69 --- /dev/null +++ b/mods/test/assets/test/model/node/jungle_planks.json @@ -0,0 +1,81 @@ +{ + "textures": { + "default": "jungle_planks.png" + }, + "cuboids": [ + { + "from": [ + -0.5, + -0.5, + -0.5 + ], + "to": [ + 0.5, + 0.5, + 0.5 + ], + "faces": { + "down": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "down" + }, + "up": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "up" + }, + "north": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "north" + }, + "south": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "south" + }, + "west": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "west" + }, + "east": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "east" + } + } + } + ] +} diff --git a/mods/test/assets/test/model/node/oak_planks.json b/mods/test/assets/test/model/node/oak_planks.json new file mode 100644 index 0000000..b882064 --- /dev/null +++ b/mods/test/assets/test/model/node/oak_planks.json @@ -0,0 +1,81 @@ +{ + "textures": { + "default": "oak_planks.png" + }, + "cuboids": [ + { + "from": [ + -0.5, + -0.5, + -0.5 + ], + "to": [ + 0.5, + 0.5, + 0.5 + ], + "faces": { + "down": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "down" + }, + "up": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "up" + }, + "north": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "north" + }, + "south": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "south" + }, + "west": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "west" + }, + "east": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "east" + } + } + } + ] +} diff --git a/mods/test/assets/test/model/node/tropical_rainforest_wood.json b/mods/test/assets/test/model/node/tropical_rainforest_wood.json new file mode 100644 index 0000000..68e601b --- /dev/null +++ b/mods/test/assets/test/model/node/tropical_rainforest_wood.json @@ -0,0 +1,81 @@ +{ + "textures": { + "default": "tropical_rainforest_wood.png" + }, + "cuboids": [ + { + "from": [ + -0.5, + -0.5, + -0.5 + ], + "to": [ + 0.5, + 0.5, + 0.5 + ], + "faces": { + "down": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "down" + }, + "up": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "up" + }, + "north": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "north" + }, + "south": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "south" + }, + "west": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "west" + }, + "east": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "east" + } + } + } + ] +} diff --git a/mods/test/assets/test/model/node/willow_wood.json b/mods/test/assets/test/model/node/willow_wood.json new file mode 100644 index 0000000..0b96be6 --- /dev/null +++ b/mods/test/assets/test/model/node/willow_wood.json @@ -0,0 +1,81 @@ +{ + "textures": { + "default": "willow_wood.png" + }, + "cuboids": [ + { + "from": [ + -0.5, + -0.5, + -0.5 + ], + "to": [ + 0.5, + 0.5, + 0.5 + ], + "faces": { + "down": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "down" + }, + "up": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "up" + }, + "north": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "north" + }, + "south": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "south" + }, + "west": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "west" + }, + "east": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "east" + } + } + } + ] +} diff --git a/mods/test/assets/test/model/node/xnether_blue_wood.json b/mods/test/assets/test/model/node/xnether_blue_wood.json new file mode 100644 index 0000000..271ead2 --- /dev/null +++ b/mods/test/assets/test/model/node/xnether_blue_wood.json @@ -0,0 +1,81 @@ +{ + "textures": { + "default": "xnether_blue_wood.png" + }, + "cuboids": [ + { + "from": [ + -0.5, + -0.5, + -0.5 + ], + "to": [ + 0.5, + 0.5, + 0.5 + ], + "faces": { + "down": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "down" + }, + "up": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "up" + }, + "north": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "north" + }, + "south": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "south" + }, + "west": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "west" + }, + "east": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "east" + } + } + } + ] +} diff --git a/mods/test/assets/test/model/node/xnether_purple_wood.json b/mods/test/assets/test/model/node/xnether_purple_wood.json new file mode 100644 index 0000000..9127a8b --- /dev/null +++ b/mods/test/assets/test/model/node/xnether_purple_wood.json @@ -0,0 +1,81 @@ +{ + "textures": { + "default": "xnether_purple_wood.png" + }, + "cuboids": [ + { + "from": [ + -0.5, + -0.5, + -0.5 + ], + "to": [ + 0.5, + 0.5, + 0.5 + ], + "faces": { + "down": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "down" + }, + "up": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "up" + }, + "north": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "north" + }, + "south": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "south" + }, + "west": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "west" + }, + "east": { + "uv": [ + 0, + 0, + 1, + 1 + ], + "texture": "default", + "cullface": "east" + } + } + } + ] +} diff --git a/mods/test/assets/test/nodestate/test0.json b/mods/test/assets/test/nodestate/test0.json new file mode 100644 index 0000000..4085598 --- /dev/null +++ b/mods/test/assets/test/nodestate/test0.json @@ -0,0 +1,14 @@ +{ + "meta==0": { + "model": "node/grass.json" + }, + "meta==1": { + "model": "node/oak_planks.json" + }, + "meta==2": { + "model": "node/jungle_planks.json" + }, + "meta==3": { + "model": "node/acacia_planks.json" + } +} diff --git a/mods/test/assets/test/nodestate/test1.json b/mods/test/assets/test/nodestate/test1.json new file mode 100644 index 0000000..e560473 --- /dev/null +++ b/mods/test/assets/test/nodestate/test1.json @@ -0,0 +1,14 @@ +{ + "meta==0": { + "model": "node/tropical_rainforest_wood.json" + }, + "meta==1": { + "model": "node/willow_wood.json" + }, + "meta==2": { + "model": "node/xnether_blue_wood.json" + }, + "meta==3": { + "model": "node/xnether_purple_wood.json" + } +} diff --git a/mods/test/assets/test/nodestate/test2.json b/mods/test/assets/test/nodestate/test2.json new file mode 100644 index 0000000..5a7029f --- /dev/null +++ b/mods/test/assets/test/nodestate/test2.json @@ -0,0 +1,14 @@ +{ + "meta==0": { + "model": "node/frame.json" + }, + "meta==1": { + "model": "node/grass.json" + }, + "meta==2": { + "model": "node/oak_planks.json" + }, + "meta==3": { + "model": "node/acacia_planks.json" + } +} diff --git a/mods/test/assets/test/nodestate/test3.json b/mods/test/assets/test/nodestate/test3.json new file mode 100644 index 0000000..c7f595e --- /dev/null +++ b/mods/test/assets/test/nodestate/test3.json @@ -0,0 +1,14 @@ +{ + "meta==0": { + "model": "node/jungle_planks.json" + }, + "meta==1": { + "model": "node/tropical_rainforest_wood.json" + }, + "meta==2": { + "model": "node/willow_wood.json" + }, + "meta==3": { + "model": "node/xnether_blue_wood.json" + } +} diff --git a/mods/test/assets/test/nodestate/test4.json b/mods/test/assets/test/nodestate/test4.json new file mode 100644 index 0000000..b925d58 --- /dev/null +++ b/mods/test/assets/test/nodestate/test4.json @@ -0,0 +1,14 @@ +{ + "meta==0": { + "model": "node/oak_planks.json" + }, + "meta==1": { + "model": "node/jungle_planks.json" + }, + "meta==2": { + "model": "node/acacia_planks.json" + }, + "meta==3": { + "model": "node/willow_wood.json" + } +} diff --git a/mods/test/assets/test/nodestate/test5.json b/mods/test/assets/test/nodestate/test5.json new file mode 100644 index 0000000..eb652da --- /dev/null +++ b/mods/test/assets/test/nodestate/test5.json @@ -0,0 +1,14 @@ +{ + "meta==0": { + "model": "node/grass.json" + }, + "meta==1": { + "model": "node/frame.json" + }, + "meta==2": { + "model": "node/xnether_purple_wood.json" + }, + "meta==3": { + "model": "node/tropical_rainforest_wood.json" + } +} diff --git a/mods/test/assets/test/texture/0.png b/mods/test/assets/test/texture/0.png new file mode 100644 index 0000000000000000000000000000000000000000..4b48a99b87611000f95bb0cf6dd09a4aebf35e37 GIT binary patch literal 14212 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 literal 0 HcmV?d00001 diff --git a/mods/test/assets/test/texture/acacia_planks.png b/mods/test/assets/test/texture/acacia_planks.png new file mode 100644 index 0000000000000000000000000000000000000000..01d81c99b89f2f703138f255db11db7e6d6cf0cd GIT binary patch literal 3800 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 literal 0 HcmV?d00001 diff --git a/mods/test/assets/test/texture/frame.png b/mods/test/assets/test/texture/frame.png new file mode 100644 index 0000000000000000000000000000000000000000..4a57b06c0a84f365f8026e93faa12aad0c297660 GIT binary patch literal 138 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 T2w=;e00000NkvXXu0mjfFoh(3 literal 0 HcmV?d00001 diff --git a/mods/test/assets/test/texture/jungle_planks.png b/mods/test/assets/test/texture/jungle_planks.png new file mode 100644 index 0000000000000000000000000000000000000000..1ecc2e641c9a90d7d0a704679a029c0ccfe33327 GIT binary patch literal 15990 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 T`)`360Fe4EFIFdF82o<#3>^3s literal 0 HcmV?d00001 diff --git a/mods/test/assets/test/texture/oak_planks.png b/mods/test/assets/test/texture/oak_planks.png new file mode 100644 index 0000000000000000000000000000000000000000..73ffcbdd23b8bc1d1d321de4561731b03b6e5836 GIT binary patch literal 15180 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

<7&10ID?v$*G>;Zb{7sQ}^7)(;Dpc-5 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 literal 0 HcmV?d00001 diff --git a/mods/test/assets/test/texture/tropical_rainforest_wood.png b/mods/test/assets/test/texture/tropical_rainforest_wood.png new file mode 100644 index 0000000000000000000000000000000000000000..af57e116b29c54ac1866ab4041091b68d8dcec6b GIT binary patch literal 1453 zcmV;e1ycHnP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00k9EL_t(Y$4ypSjx0+Jj0n4W z<^V#v3t*RQg{`nA{*VA&&UB?C_;Ae;_0`HQS7ye`=HLJJ*H>L6DRkF)ABoqil_5#u zzK;#I0LCHkzP|{)uIrq)gCKE8h)iToKdAcncqN{h*a5@_WSFAJfC0KI_SZdo3j#zESAZmd_Kze0fFBo;ovI-?#pqH<9alB}M|2B$6}7K}k+X5|I!n z5#k_rYybwBBryO0qyjkVtB4q}#N-6u2xk%s>Fz9VNe}@n5%~UMK!HT)c+SB}o*_h{ z|MAb?_w@u4i07dg2ZUCT9v~FNUOoUUnMvRTA(fZ_RC#X9{?m)csS=X98o|oFkWk+> z1lk?|AgV;dW08^)lLQEaA*KKP_g%`2dyjMVD#zMGg-F6jg-J*ZsJ`Ghb2}tSP2w}~ zbkzh1kO8Jjy#OWs`uJEV?R=xNWL7*`tU<&c;{kmD7y&?V$-vf!j<443kVUacFbSio z1q1+iamgef-=YvFAPW={APyuF2r%akwy2$4g|QuX5{dD89isk^yx^DRsy0a|1+bl< z_FY00yA8$$f!H>JF17DBgsyU;0F+~ZRXS%V#d%-@D1l`B>h@m3D=;SzX)+EIus{NA zKGMu}2`rU}24M%h1fyhI!aXdo#d87%9v~%-olxAwMo6Lpf~us)~NcHov@{4-mr{vM4)?(10TCoc`@cQzQk1`|FAI1%^&U1}G7rL!c8gAhRk+ zwmMrv^ch1%swd!BM3V6VP!eJQR7nIdWD*Z0oikpgWqVYrw0%g+5c9+!SVViKM8_gt zt?#5z0x|-yIwUC+Qy0ec7(~@0*^rs4Uyx=a1K{hi_qJhxL zr#ur3fnv^gM_RSoPeVn}fBq~yJPpYz0g1;+$u>f$x64K3Qwgl$fk;AfpILxKRf(j* zndyO$2|3Q3$xVDyEzWkG9Bf-%8^!6?Zbk|?8>tCY0=%&cvAY`QC#9Klb$`C3D%*)k z&73FXvq8^s1e*Lhv5nl-rDtRl65h6F9S3lXMt>Nnt7hyN25gFiyRUdWCxF3LwIBhi zotbtjwS64Kud8iKrBa~S6naca_e^HTkOQE*OZ0sUNifNrv5`fbo-aJ9(#P%S| z;E<>yfo@TXx>Azp83HPLOfm+mI}v-w+r$97x?6T;=ALM4ZQDD&Rq?*lg|;*S$#d>Y zbv%h_Fi0$^SzzMBXGYSF0)VbpmpvYGhgbr5-|r0@P`C3xEV62Em3akY00000NkvXX Hu0mjf;RJOC literal 0 HcmV?d00001 diff --git a/mods/test/assets/test/texture/willow_wood.png b/mods/test/assets/test/texture/willow_wood.png new file mode 100644 index 0000000000000000000000000000000000000000..6107044abdf71dcb7dd08e93001b800a04b1cddc GIT binary patch literal 2089 zcmV+^2-f$BP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O1bvw&N%a{bv=u1SBzt5ldK!h^Fr+lj?|33LTPOjgybCe_vsIgSQ+mYkb7aH@XF*@BtXCh5>J6_WR670qXM4{L(1Z~00Q!!8@yyE5&({JpBvvq8e`e>RHJ4A z2rmIkO94;@Rt|*?{E1G%q3p#w?|tymt4}@$2`=~$LJYn5T9U6s;Q~w#!VXP zL<=o8ZK>srHc)AEo8Q6~H{H^fcTjDY-R*u4d)#$Tdp=PkGhuzXKSzxY)OaA(2gVaM zd>Z3nf_gde9Wx-tj(~XV2!PPsF|$g}vFXrt%xprL6VLF{cW@eY#DJjNi2Y6{c3;Rn z;bxrsBi#5v&UEPh26CoDcZ1w3Zf{U)eYdGr%s{9w!=6+tSbq3iGi%-j-#8xazQ0Wg z`t^qMlp7w%OUic3Y`U7XYx?GMCtDtdu(qb1B1~o20j{IZpybX$rxdu{TP1>E`xwpt zKb5&E>M)X2uQA0oMrfev3AV-26Q)b%(UmZ}Htd&YZfZWwWTxP<&vjE zcuY!WocQ8mX}x%F>|I!QvifOMtG4MvcCc@5COBo7URll4Pmg*t*OGO*>c6MBjWU9R z4i3Xpd9KlOmyor5i_l}4$*atG8O%3*z{jY+D>5sg#)gS8Hh2zh@-8QfEq=VEX>4Tx0C=2zkv&MmKpe$i(@I4u3U&~6$WWauh>AFB6^c+H)C#RSm|XfHG-*gu zTpR`0f`cE6RR=bb;{*sk16O*>U#SDrpQP7X zTI>ku*#<7ITbi;5T;Y5-i%B}Z<9Duprtz_jda zt(%q&kOjw~#Ia4{P-do7re%}*)3pd?K)>DOX7-XDctMC^S{Q(#L2J9VmdpPzI3M=- ziz216*8+&ECoiij52{LezW~^)uWFRG$#~-7?wA;+wXrMO=-4exMiq~B%H|;dtWD$# z2>|--X2~2W#iJh(t8&6fDW?8{PzH!W$KmdMJvc-A;sY@C7u-JkobEdaVGzoILB~P6 z7rMNg=OhjqE7FFp7k;B$MvNz3cI&3 literal 0 HcmV?d00001 diff --git a/mods/test/assets/test/texture/xnether_blue_wood.png b/mods/test/assets/test/texture/xnether_blue_wood.png new file mode 100644 index 0000000000000000000000000000000000000000..cd668004c66ad57fc03cd7baf18c1c5847bd27ae GIT binary patch literal 6101 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 literal 0 HcmV?d00001 diff --git a/mods/test/init.lua b/mods/test/init.lua new file mode 100644 index 0000000..ac4d4bd --- /dev/null +++ b/mods/test/init.lua @@ -0,0 +1,98 @@ +-- parent = default:air +-- +-- hasHalfTransparency +-- collideBox = {} +-- plantLike = {} +-- nodebox = {} + +local node_template = { + parent = "default:normal" or node_template, + render = { + has_half_transparency = false + }, + collision = { + + }, + events = { + + }, + node_advancement_factory = function(world_id, node_pos) + local node_advancement = { + onLoad = function(data) + + end, + onSave = function() + return {} + end + } + + return node_advancement + end + +} + +local instance = {} + +--[[ +Движок автоматически подгружает ассеты из папки assets +В этом методе можно зарегистрировать ассеты из иных источников +Состояния нод, частицы, анимации, модели, текстуры, звуки, шрифты +]]-- +function instance.assetsInit() +end + +--[[ +*preInit. События для регистрации определений игрового контента +Ноды, воксели, миры, порталы, сущности, предметы +]]-- +function instance.lowPreInit() +end + +--[[ +До вызова preInit будет выполнена регистрация +контента из файлов в папке content +]]-- +function instance.preInit() +local node_air = {} + +node_air.hasHalfTransparency = false +node_air.collideBox = nil +node_air.render = nil + +core.register_node('test0', {}) +core.register_node('test1', {}) +core.register_node('test2', {}) +core.register_node('test3', {}) +core.register_node('test4', {}) +core.register_node('test5', {}) +end + +function instance.highPreInit() +end + +--[[ +На этом этапе можно наложить изменения +на зарегистрированный другими модами контент +]]-- +function instance.init() +end + +function instance.postInit() +end + +function instance.preDeInit() +end + +function instance.deInit() +end + +function instance.postDeInit() +core.unregister_node('test0') +core.unregister_node('test1') +core.unregister_node('test2') +core.unregister_node('test3') +core.unregister_node('test4') +core.unregister_node('test5') +end + +return instance diff --git a/mods/test/mod.json b/mods/test/mod.json new file mode 100644 index 0000000..43bbf32 --- /dev/null +++ b/mods/test/mod.json @@ -0,0 +1,9 @@ +{ + "id": "test", + "name": "Test Mod", + "description": "Это тестовый мод", + "depends": [], + "optional_depends": [], + "author": "DrSocalkwe3n", + "version": [0, 0, 0, 1] +}