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 b78d266..2e53f23 100644 Binary files a/assets/shaders/chunk/node.vert.bin and b/assets/shaders/chunk/node.vert.bin differ 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 780834d..007905c 100644 Binary files a/assets/shaders/chunk/node_opaque.frag.bin and b/assets/shaders/chunk/node_opaque.frag.bin differ 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 6c6c733..e7259ae 100644 Binary files a/assets/shaders/chunk/node_transparent.frag.bin and b/assets/shaders/chunk/node_transparent.frag.bin differ diff --git a/assets/shaders/chunk/voxel_opaque.frag b/assets/shaders/chunk/voxel_opaque.frag index 658860e..8988a28 100644 --- a/assets/shaders/chunk/voxel_opaque.frag +++ b/assets/shaders/chunk/voxel_opaque.frag @@ -9,23 +9,22 @@ 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; @@ -35,42 +34,14 @@ vec4 atlasColor(uint texId, vec2 uv) { uv = mod(uv, 1); - 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)); } void main() { diff --git a/assets/shaders/chunk/voxel_opaque.frag.bin b/assets/shaders/chunk/voxel_opaque.frag.bin index e132247..5df2a79 100644 Binary files a/assets/shaders/chunk/voxel_opaque.frag.bin and b/assets/shaders/chunk/voxel_opaque.frag.bin differ 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 bb40206..7ef98fe 100644 Binary files a/assets/shaders/chunk/voxel_transparent.frag.bin and b/assets/shaders/chunk/voxel_transparent.frag.bin differ 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 0000000..4b48a99 Binary files /dev/null and b/mods/test/assets/test/texture/0.png differ 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 0000000..01d81c9 Binary files /dev/null and b/mods/test/assets/test/texture/acacia_planks.png differ diff --git a/mods/test/assets/test/texture/frame.png b/mods/test/assets/test/texture/frame.png new file mode 100644 index 0000000..4a57b06 Binary files /dev/null and b/mods/test/assets/test/texture/frame.png differ diff --git a/mods/test/assets/test/texture/grass.png b/mods/test/assets/test/texture/grass.png new file mode 100644 index 0000000..3ec069c Binary files /dev/null and b/mods/test/assets/test/texture/grass.png differ 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 0000000..1ecc2e6 Binary files /dev/null and b/mods/test/assets/test/texture/jungle_planks.png differ 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 0000000..73ffcbd Binary files /dev/null and b/mods/test/assets/test/texture/oak_planks.png differ 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 0000000..af57e11 Binary files /dev/null and b/mods/test/assets/test/texture/tropical_rainforest_wood.png differ 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 0000000..6107044 Binary files /dev/null and b/mods/test/assets/test/texture/willow_wood.png differ 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 0000000..cd66800 Binary files /dev/null and b/mods/test/assets/test/texture/xnether_blue_wood.png differ diff --git a/mods/test/assets/test/texture/xnether_purple_wood.png b/mods/test/assets/test/texture/xnether_purple_wood.png new file mode 100644 index 0000000..6a989b8 Binary files /dev/null and b/mods/test/assets/test/texture/xnether_purple_wood.png differ 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] +}