| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- #include <algorithm>
- #include "SpriteSet.h"
- namespace {
- constexpr unsigned nullIndex = -1;
- constexpr unsigned maxChangedPixels = 256 * 256;
- constexpr unsigned splitThreshold = 8;
- template<typename T>
- void initializePool(std::vector<T> &pool) {
- const unsigned
- poolSize = static_cast<unsigned>(pool.size()),
- lastPoolIndex = poolSize - 1;
- for (unsigned i = 0; i != lastPoolIndex; ++i) pool[i].nextIndex = i + 1;
- pool[lastPoolIndex].nextIndex = nullIndex;
- }
- template<typename T>
- unsigned allocateEntry(std::vector<T> &pool, unsigned &nextFreeIndex) {
- if (nextFreeIndex == nullIndex) {
- const unsigned
- oldPoolSize = static_cast<unsigned>(pool.size()),
- newPoolSize = oldPoolSize << 1,
- lastPoolIndex = newPoolSize - 1;
- pool.resize(newPoolSize);
- for (unsigned i = oldPoolSize; i != lastPoolIndex; ++i) pool[i].nextIndex = i + 1;
- pool[lastPoolIndex].nextIndex = nullIndex;
- nextFreeIndex = oldPoolSize;
- }
- const unsigned index = nextFreeIndex;
- nextFreeIndex = pool[index].nextIndex;
- return index;
- }
- template<typename T>
- void freeEntry(std::vector<T> &pool, unsigned &nextFreeIndex, const unsigned index) {
- pool[index].nextIndex = nextFreeIndex;
- nextFreeIndex = index;
- }
- }
- SpriteSet::SpriteSet(const unsigned bytesPerPixel, const unsigned pageSize, const unsigned spritePadding):
- bytesPerPixel(bytesPerPixel), pageSize(pageSize), spritePadding(spritePadding)
- {
- initializePool(pagePool);
- initializePool(shelfPool);
- initializePool(slotPool);
- }
- void SpriteSet::tick() {
- unsigned changedPixels = 0;
- // Try compaction by moving sprites from the last page to the first page.
- while (changedPixels <= maxChangedPixels && firstPageIndex != lastPageIndex) {
- const Page &sourcePage = pagePool[lastPageIndex];
- unsigned sourceShelfIndex = sourcePage.firstShelfIndex;
- while (!shelfPool[sourceShelfIndex].allocated) sourceShelfIndex = shelfPool[sourceShelfIndex].nextIndex;
- const Shelf &sourceShelf = shelfPool[sourceShelfIndex];
- unsigned sourceSlotIndex = sourceShelf.firstSlotIndex;
- while (!slotPool[sourceSlotIndex].allocated) sourceSlotIndex = slotPool[sourceSlotIndex].nextIndex;
- const Slot &sourceSlot = slotPool[sourceSlotIndex];
- const unsigned destinationSlotIndex = tryAllocateInPage(firstPageIndex, sourceSlot.width, sourceSlot.height);
- if (destinationSlotIndex == nullIndex) break;
- const Slot &destinationSlot = slotPool[destinationSlotIndex];
- const Shelf &destinationShelf = shelfPool[destinationSlot.shelfIndex];
- Page &destinationPage = pagePool[firstPageIndex];
- const unsigned
- sourceX = sourceSlot.x, sourceY = sourceShelf.y,
- destinationX = destinationSlot.x, destinationY = destinationShelf.y,
- width = sourceSlot.width, height = sourceSlot.height;
- for (unsigned localY = 0; localY != height; ++localY) {
- const auto dataStart = sourcePage.textureData->begin() + ((sourceY + localY) * pageSize + sourceX);
- std::copy(
- dataStart, dataStart + width,
- destinationPage.textureData->begin() + ((destinationY + localY) * pageSize + destinationX)
- );
- }
- destinationPage.firstDirtyY = std::min(destinationPage.firstDirtyY, destinationY);
- destinationPage.pastLastDirtyY = std::max(destinationPage.pastLastDirtyY, destinationY + height);
- destinationPage.firstDirtyX = std::min(destinationPage.firstDirtyX, destinationX);
- destinationPage.pastLastDirtyX = std::max(destinationPage.pastLastDirtyX, destinationX + width);
- changedPixels += remove(sourceSlotIndex);
- if (migrationCallback) migrationCallback(sourceSlotIndex, {destinationSlotIndex, destinationSlot.epoch});
- }
- }
- SpriteSet::Handle SpriteSet::add(
- const unsigned width, const unsigned height, const unsigned char *const data, const unsigned rowStride
- ) {
- const unsigned paddedWidth = width + spritePadding * 2, paddedHeight = height + spritePadding * 2;
- const unsigned slotIndex = allocate(paddedWidth, paddedHeight);
- const Slot &slot = slotPool[slotIndex];
- const Shelf &shelf = shelfPool[slot.shelfIndex];
- Page &page = pagePool[shelf.pageIndex];
- for (
- unsigned i = 0, topPaddingY = shelf.y, bottomPaddingY = shelf.y + paddedHeight - 1;
- i != spritePadding; ++i, ++topPaddingY, --bottomPaddingY
- ) {
- auto textureStart = page.textureData->begin() + (topPaddingY * pageSize + slot.x) * bytesPerPixel;
- std::fill(textureStart, textureStart + paddedWidth * bytesPerPixel, 0);
- textureStart = page.textureData->begin() + (bottomPaddingY * pageSize + slot.x) * bytesPerPixel;
- std::fill(textureStart, textureStart + paddedWidth * bytesPerPixel, 0);
- }
- const unsigned textureY = shelf.y + spritePadding;
- for (unsigned localY = 0; localY != height; ++localY) {
- const unsigned char *const dataStart = data + localY * rowStride * bytesPerPixel;
- const auto textureStart = page.textureData->begin() + ((textureY + localY) * pageSize + slot.x) * bytesPerPixel;
- std::fill(textureStart, textureStart + spritePadding * bytesPerPixel, 0);
- std::fill(
- textureStart + (spritePadding + width) * bytesPerPixel, textureStart + paddedWidth * bytesPerPixel, 0
- );
- std::copy(dataStart, dataStart + width * bytesPerPixel, textureStart + spritePadding * bytesPerPixel);
- }
- page.firstDirtyY = std::min(page.firstDirtyY, shelf.y);
- page.pastLastDirtyY = std::max(page.pastLastDirtyY, shelf.y + paddedHeight);
- page.firstDirtyX = std::min(page.firstDirtyX, slot.x);
- page.pastLastDirtyX = std::max(page.pastLastDirtyX, slot.x + paddedWidth);
- return {slotIndex, slot.epoch};
- }
- unsigned SpriteSet::allocate(const unsigned width, const unsigned height) {
- // Try to allocate in an existing page.
- if (firstPageIndex != nullIndex) {
- unsigned pageIndex = firstPageIndex;
- while (true) {
- const unsigned slotIndex = tryAllocateInPage(pageIndex, width, height);
- if (slotIndex != nullIndex) return slotIndex;
- if (pageIndex == lastPageIndex) break;
- pageIndex = pagePool[pageIndex].nextIndex;
- }
- }
- // No page could fit, allocate a new page.
- if (firstPageIndex == nullIndex) {
- firstPageIndex = lastPageIndex;
- Page &page = pagePool[lastPageIndex];
- page.previousIndex = nullIndex;
- } else {
- unsigned pageIndex = pagePool[lastPageIndex].nextIndex;
- if (pageIndex == nullIndex) {
- const unsigned
- oldPoolSize = static_cast<unsigned>(pagePool.size()),
- newPoolSize = oldPoolSize << 1,
- lastPoolIndex = newPoolSize - 1;
- pagePool.resize(newPoolSize);
- for (unsigned i = oldPoolSize; i != lastPoolIndex; ++i) pagePool[i].nextIndex = i + 1;
- pagePool[lastPoolIndex].nextIndex = nullIndex;
- pagePool[lastPageIndex].nextIndex = oldPoolSize;
- pageIndex = oldPoolSize;
- }
- pagePool[pageIndex].previousIndex = lastPageIndex;
- lastPageIndex = pageIndex;
- }
- Page &page = pagePool[lastPageIndex];
- page.textureId = pageCount;
- page.textureData = std::make_unique<std::vector<unsigned char>>(pageSize * pageSize * bytesPerPixel);
- page.firstDirtyY = pageSize;
- page.pastLastDirtyY = 0;
- page.firstDirtyX = pageSize;
- page.pastLastDirtyX = 0;
- const unsigned shelfIndex = allocateEntry<Shelf>(shelfPool, nextFreeShelfIndex);
- const unsigned slotIndex = allocateEntry<Slot>(slotPool, nextFreeSlotIndex);
- slotPool[slotIndex] = {shelfIndex, pageCount, 0, 0, pageSize, 0, 0, nullIndex, nullIndex, nullIndex, nullIndex, 0, false};
- shelfPool[shelfIndex] = {lastPageIndex, 0, pageSize, nullIndex, nullIndex, slotIndex, slotIndex, false};
- page.firstShelfIndex = shelfIndex;
- ++pageCount;
- return tryAllocateInPage(lastPageIndex, width, height);
- }
- unsigned SpriteSet::tryAllocateInPage(const unsigned pageIndex, const unsigned width, const unsigned height) {
- Page &page = pagePool[pageIndex];
- unsigned selectedShelfIndex = nullIndex, selectedSlotIndex = nullIndex, selectedShelfHeight = -1;
- for (
- unsigned shelfIndex = page.firstShelfIndex;
- shelfIndex != nullIndex; shelfIndex = shelfPool[shelfIndex].nextIndex
- ) {
- const Shelf &shelf = shelfPool[shelfIndex];
- if (
- shelf.height < height || shelf.height >= selectedShelfHeight
- || (shelf.allocated && shelf.height > height * 3 / 2)
- ) continue;
- bool found = false;
- for (
- unsigned slotIndex = shelf.firstFreeSlotIndex;
- slotIndex != nullIndex; slotIndex = slotPool[slotIndex].nextFreeIndex
- ) {
- const Slot &slot = slotPool[slotIndex];
- if (slot.width < width) continue;
- selectedShelfIndex = shelfIndex;
- selectedSlotIndex = slotIndex;
- selectedShelfHeight = shelf.height;
- found = true;
- break;
- }
- if (found && shelf.height == height) break;
- }
- if (selectedSlotIndex == nullIndex) return nullIndex;
- Shelf *shelf = &shelfPool[selectedShelfIndex];
- if (!shelf->allocated) {
- shelf->allocated = true;
- if (shelf->height - height >= splitThreshold) {
- const unsigned newShelfIndex = allocateEntry<Shelf>(shelfPool, nextFreeShelfIndex);
- const unsigned newSlotIndex = allocateEntry<Slot>(slotPool, nextFreeSlotIndex);
- shelf = &shelfPool[selectedShelfIndex];
- slotPool[newSlotIndex] = {
- newShelfIndex, page.textureId, 0, shelf->y + height, pageSize, 0, 0, nullIndex, nullIndex, nullIndex, nullIndex, 0, false
- };
- shelfPool[newShelfIndex] = {
- pageIndex, shelf->y + height, shelf->height - height,
- selectedShelfIndex, shelf->nextIndex, newSlotIndex, newSlotIndex, false
- };
- if (shelf->nextIndex != nullIndex) shelfPool[shelf->nextIndex].previousIndex = newShelfIndex;
- shelf->nextIndex = newShelfIndex;
- shelf->height = height;
- }
- }
- Slot *slot = &slotPool[selectedSlotIndex];
- if (slot->width - width >= splitThreshold) {
- const unsigned newSlotIndex = allocateEntry<Slot>(slotPool, nextFreeSlotIndex);
- slot = &slotPool[selectedSlotIndex];
- slotPool[newSlotIndex] = {
- selectedShelfIndex, page.textureId, slot->x + width, shelf->y, slot->width - width, 0, 0,
- selectedSlotIndex, slot->nextIndex, slot->previousFreeIndex, slot->nextFreeIndex, 0, false
- };
- if (slot->nextIndex != nullIndex) slotPool[slot->nextIndex].previousIndex = newSlotIndex;
- slot->nextIndex = newSlotIndex;
- if (slot->previousFreeIndex == nullIndex) shelf->firstFreeSlotIndex = newSlotIndex;
- else slotPool[slot->previousFreeIndex].nextFreeIndex = newSlotIndex;
- if (slot->nextFreeIndex != nullIndex) slotPool[slot->nextFreeIndex].previousFreeIndex = newSlotIndex;
- slot->width = width;
- } else {
- if (slot->previousFreeIndex == nullIndex) shelf->firstFreeSlotIndex = slot->nextFreeIndex;
- else slotPool[slot->previousFreeIndex].nextFreeIndex = slot->nextFreeIndex;
- if (slot->nextFreeIndex != nullIndex) slotPool[slot->nextFreeIndex].previousFreeIndex = slot->previousFreeIndex;
- }
- slot->allocated = true;
- slot->actualWidth = width;
- slot->height = height;
- slot->epoch = currentEpoch;
- if (pageIndex == firstPageIndex) firstPageAllocatedPixels += width * shelf->height;
- return selectedSlotIndex;
- }
- unsigned SpriteSet::remove(const unsigned slotIndex) {
- Slot &slot = slotPool[slotIndex];
- Shelf &shelf = shelfPool[slot.shelfIndex];
- const unsigned pageIndex = shelf.pageIndex;
- Page &page = pagePool[pageIndex];
- for (int offsetY = 0; offsetY < slot.height; ++offsetY) {
- for (int offsetX = 0; offsetX < slot.width; ++offsetX) {
- const auto pixel = page.textureData->begin() + ((slot.y + offsetY) * pageSize + slot.x + offsetX) * 4;
- pixel[0] = 0;
- pixel[1] = 0;
- pixel[2] = 255;
- pixel[3] = 255;
- }
- }
- const unsigned slotPixels = slot.width * slot.height;
- slot.allocated = false;
- slot.previousFreeIndex = nullIndex;
- if (shelf.firstFreeSlotIndex == nullIndex) {
- slot.nextFreeIndex = nullIndex;
- } else {
- slot.nextFreeIndex = shelf.firstFreeSlotIndex;
- slotPool[shelf.firstFreeSlotIndex].previousFreeIndex = slotIndex;
- }
- shelf.firstFreeSlotIndex = slotIndex;
- ++slot.epoch;
- // Merge consecutive empty slots.
- if (slot.nextIndex != nullIndex) {
- Slot &nextSlot = slotPool[slot.nextIndex];
- if (!nextSlot.allocated) {
- slot.width += nextSlot.width;
- const unsigned nextIndex = slot.nextIndex;
- slot.nextIndex = nextSlot.nextIndex;
- if (nextSlot.previousFreeIndex != nullIndex)
- slotPool[nextSlot.previousFreeIndex].nextFreeIndex = nextSlot.nextFreeIndex;
- if (nextSlot.nextFreeIndex != nullIndex)
- slotPool[nextSlot.nextFreeIndex].previousFreeIndex = nextSlot.previousFreeIndex;
- freeEntry<Slot>(slotPool, nextFreeSlotIndex, nextIndex);
- if (slot.nextIndex != nullIndex) slotPool[slot.nextIndex].previousIndex = slotIndex;
- }
- }
- if (slot.previousIndex != nullIndex) {
- Slot &previousSlot = slotPool[slot.previousIndex];
- if (!previousSlot.allocated) {
- slot.x -= previousSlot.width;
- slot.width += previousSlot.width;
- const unsigned previousIndex = slot.previousIndex;
- slot.previousIndex = previousSlot.previousIndex;
- if (previousSlot.previousFreeIndex != nullIndex)
- slotPool[previousSlot.previousFreeIndex].nextFreeIndex = previousSlot.nextFreeIndex;
- if (previousSlot.nextFreeIndex != nullIndex)
- slotPool[previousSlot.nextFreeIndex].previousFreeIndex = previousSlot.previousFreeIndex;
- freeEntry<Slot>(slotPool, nextFreeSlotIndex, previousIndex);
- if (slot.previousIndex == nullIndex) {
- shelf.firstSlotIndex = slotIndex;
- if (slot.nextIndex == nullIndex) shelf.allocated = false;
- } else {
- slotPool[slot.previousIndex].nextIndex = slotIndex;
- }
- }
- }
- // Merge consecutive empty shelves.
- if (shelf.allocated) return slotPixels;
- if (shelf.nextIndex != nullIndex) {
- Shelf &nextShelf = shelfPool[shelf.nextIndex];
- if (!nextShelf.allocated) {
- shelf.height += nextShelf.height;
- const unsigned nextIndex = shelf.nextIndex;
- shelf.nextIndex = nextShelf.nextIndex;
- freeEntry<Slot>(slotPool, nextFreeSlotIndex, nextShelf.firstSlotIndex);
- freeEntry<Shelf>(shelfPool, nextFreeShelfIndex, nextIndex);
- if (shelf.nextIndex != nullIndex) shelfPool[shelf.nextIndex].previousIndex = slot.shelfIndex;
- }
- }
- if (shelf.previousIndex != nullIndex) {
- Shelf &previousShelf = shelfPool[shelf.previousIndex];
- if (!previousShelf.allocated) {
- shelf.y -= previousShelf.height;
- shelf.height += previousShelf.height;
- slot.y = shelf.y;
- const unsigned previousIndex = shelf.previousIndex;
- shelf.previousIndex = previousShelf.previousIndex;
- freeEntry<Slot>(slotPool, nextFreeSlotIndex, previousShelf.firstSlotIndex);
- freeEntry<Shelf>(shelfPool, nextFreeShelfIndex, previousIndex);
- if (shelf.previousIndex == nullIndex) page.firstShelfIndex = slot.shelfIndex;
- else shelfPool[shelf.previousIndex].nextIndex = slot.shelfIndex;
- }
- }
- // Deallocate the page if it becomes empty, except when it's the first one.
- if (pageIndex == firstPageIndex) {
- firstPageAllocatedPixels -= slot.width * shelf.height;
- return slotPixels;
- }
- if (shelf.height != pageSize) return slotPixels;
- freeEntry<Slot>(slotPool, nextFreeSlotIndex, slotIndex);
- freeEntry<Shelf>(shelfPool, nextFreeShelfIndex, page.firstShelfIndex);
- page.textureData.reset();
- pagePool[page.previousIndex].nextIndex = page.nextIndex;
- if (pageIndex != lastPageIndex) pagePool[page.nextIndex].previousIndex = page.previousIndex;
- pagePool[lastPageIndex].nextIndex = pageIndex;
- return slotPixels;
- }
- void SpriteSet::remove(const Handle handle) {
- Slot &slot = slotPool[handle.slotIndex];
- if (slot.epoch != handle.epoch) return;
- remove(handle.slotIndex);
- }
- SpriteSet::SpriteData SpriteSet::get(const Handle handle) const {
- const Slot &slot = slotPool[handle.slotIndex];
- return {
- slot.textureId, slot.x + spritePadding, slot.y + spritePadding,
- slot.actualWidth - spritePadding * 2, slot.height - spritePadding * 2
- };
- }
- std::vector<const unsigned char*> SpriteSet::getTextures() const {
- if (firstPageIndex == nullIndex)
- return {};
- std::vector<const unsigned char*> textures;
- unsigned pageIndex = firstPageIndex;
- while (true) {
- const Page &page = pagePool[pageIndex];
- textures.push_back(page.textureData->data());
- if (pageIndex == lastPageIndex) break;
- pageIndex = page.nextIndex;
- }
- return textures;
- }
- void SpriteSet::dump(std::vector<SpriteSet::SpriteData> &sprites) const {
- if (firstPageIndex == nullIndex) return;
- for (
- unsigned shelfIndex = pagePool[firstPageIndex].firstShelfIndex;
- shelfIndex != nullIndex; shelfIndex = shelfPool[shelfIndex].nextIndex
- ) {
- const unsigned shelfY = shelfPool[shelfIndex].y;
- for (
- unsigned slotIndex = shelfPool[shelfIndex].firstSlotIndex;
- slotIndex != nullIndex; slotIndex = slotPool[slotIndex].nextIndex
- ) {
- const Slot &slot = slotPool[slotIndex];
- if (slot.allocated) sprites.push_back({slot.x, shelfY, slot.width, slot.height});
- }
- }
- }
|