// TextureAtlas.hpp #pragma once /* ================================================================================ TextureAtlas — как пользоваться (кратко) 1) Создайте атлас (один раз): TextureAtlas atlas(device, physicalDevice, cfg, callback); 2) Зарегистрируйте текстуру и получите стабильный ID: TextureId id = atlas.registerTexture(); if(id == TextureAtlas::kOverflowId || id == atlas.reservedOverflowId()) { ... } // нет свободных 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 и зарезервированными ID игнорируются (no-op). - ID из начала диапазона зарезервированы под служебные нужды: reservedOverflowId() == 0, reservedLayerId(0) == 1 (первый слой), reservedLayerId(1) == 2 (второй слой) и т.д. - Ошибки ресурсов (нет места/стейджинга/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; } uint32_t maxLayers() const { return Cfg_.MaxLayers; } uint32_t maxTextureId() const { return Cfg_.MaxTextureId; } TextureId reservedOverflowId() const { return 0; } TextureId reservedLayerId(uint32_t layer) const { return 1u + layer; } bool isReservedId(TextureId id) const { if(id >= Cfg_.MaxTextureId) return false; return id < _reservedCount(); } bool isReservedLayerId(TextureId id) const { if(id >= Cfg_.MaxTextureId) return false; return id != reservedOverflowId() && id < _reservedCount(); } bool isInvalidId(TextureId id) const { return id == kOverflowId || isReservedId(id); } void requestLayerCount(uint32_t layers) { _ensureAliveOrThrow(); _scheduleLayerGrow(layers); } /// Общий staging-буфер (может быть задан извне). std::shared_ptr getStagingBuffer() const { return Staging_; } private: void _moveFrom(TextureAtlas&& other) noexcept; uint32_t _reservedCount() const { return Cfg_.MaxLayers + 1; } uint32_t _reservedStart() const { return 0; } uint32_t _allocatableStart() const { return _reservedCount(); } uint32_t _allocatableLimit() const { return Cfg_.MaxTextureId; } void _initReservedEntries() { if(Cfg_.MaxTextureId <= _reservedCount()) { return; } _setEntryInvalid(reservedOverflowId(), /*diagPending*/false, /*diagTooLarge*/false); for(uint32_t layer = 0; layer < Cfg_.MaxLayers; ++layer) { TextureId id = reservedLayerId(layer); Entry& e = EntriesCpu_[id]; e.UVMinMax[0] = 0.0f; e.UVMinMax[1] = 0.0f; e.UVMinMax[2] = 1.0f; e.UVMinMax[3] = 1.0f; e.Layer = layer; e.Flags = ENTRY_VALID; } EntriesDirty_ = true; } // ============================= Ошибки/валидация ============================= struct InputError : std::runtime_error { 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_.MaxTextureId <= (Cfg_.MaxLayers + 1)) { throw _inputError("Config.MaxTextureId must be > MaxLayers + 1 (reserved ids)"); } 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 || isReservedId(id)) { 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_; };