478 lines
14 KiB
C++
478 lines
14 KiB
C++
#include "TextureAtlas.hpp"
|
|
|
|
TextureAtlas::TextureAtlas(VkDevice device,
|
|
VkPhysicalDevice physicalDevice,
|
|
const Config& cfg,
|
|
EventCallback cb,
|
|
std::shared_ptr<SharedStagingBuffer> 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<VkDeviceSize>(4, props.limits.optimalBufferCopyOffsetAlignment);
|
|
|
|
if(!Staging_) {
|
|
Staging_ = std::make_shared<SharedStagingBuffer>(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<const uint8_t*>(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<TextureId>& queue,
|
|
std::vector<bool>& inQueue,
|
|
std::vector<TextureId>& 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<TextureId> pendingNow;
|
|
pendingNow.reserve(Pending_.size());
|
|
collectQueue(Pending_, PendingInQueue_, pendingNow);
|
|
|
|
std::vector<TextureId> 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<VkDeviceSize>(wP) * hP * 4u;
|
|
auto stagingOff = Staging_->Allocate(bytes, CopyOffsetAlignment_);
|
|
if(!stagingOff) {
|
|
_emitEventOncePerFlush(AtlasEvent::StagingOverflow);
|
|
return false;
|
|
}
|
|
|
|
uint8_t* dst = static_cast<uint8_t*>(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<int32_t>(pp.X),
|
|
static_cast<int32_t>(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<VkDeviceSize>(EntriesCpu_.size()) * sizeof(Entry);
|
|
auto off = Staging_->Allocate(entriesBytes, CopyOffsetAlignment_);
|
|
if(!off) {
|
|
_emitEventOncePerFlush(AtlasEvent::StagingOverflow);
|
|
} else {
|
|
std::memcpy(static_cast<uint8_t*>(Staging_->Mapped()) + *off,
|
|
EntriesCpu_.data(),
|
|
static_cast<size_t>(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{};
|
|
}
|