From c107101aff7382218d67db3f03e338727339746c Mon Sep 17 00:00:00 2001 From: Alexander Overvoorde Date: Sat, 23 Jul 2016 20:31:41 +0200 Subject: [PATCH] Implement dynamic atlas allocation in texture cache --- src/drawing/engines/opengl/OpenGLAPI.cpp | 1 + src/drawing/engines/opengl/OpenGLAPI.h | 4 + .../engines/opengl/OpenGLDrawingEngine.cpp | 2 +- src/drawing/engines/opengl/TextureCache.cpp | 94 +++++++++++++------ src/drawing/engines/opengl/TextureCache.h | 81 +++++++++------- 5 files changed, 116 insertions(+), 66 deletions(-) diff --git a/src/drawing/engines/opengl/OpenGLAPI.cpp b/src/drawing/engines/opengl/OpenGLAPI.cpp index ca698fcca0..22a8388404 100644 --- a/src/drawing/engines/opengl/OpenGLAPI.cpp +++ b/src/drawing/engines/opengl/OpenGLAPI.cpp @@ -71,6 +71,7 @@ static const char * TryLoadAllProcAddresses() SetupOpenGLFunction(glTexSubImage3D); SetupOpenGLFunction(glTexImage3D); SetupOpenGLFunction(glGetIntegerv); + SetupOpenGLFunction(glGetTexImage); // 2.0+ functions SetupOpenGLFunction(glAttachShader); diff --git a/src/drawing/engines/opengl/OpenGLAPI.h b/src/drawing/engines/opengl/OpenGLAPI.h index 19d8e5dd7e..9fa2363d85 100644 --- a/src/drawing/engines/opengl/OpenGLAPI.h +++ b/src/drawing/engines/opengl/OpenGLAPI.h @@ -43,6 +43,7 @@ #define glTexSubImage3D __static__glTexSubImage3D #define glTexImage3D __static__glTexImage3D #define glGetIntegerv __static__glGetIntegerv +#define glGetTexImage __static__glGetTexImage #endif @@ -73,6 +74,7 @@ #undef glTexSubImage3D #undef glTexImage3D #undef glGetIntegerv +#undef glGetTexImage // 1.1 function signatures typedef void (APIENTRYP PFNGLBEGINPROC )(GLenum mode); @@ -96,6 +98,7 @@ typedef void (APIENTRYP PFNGLVIEWPORTPROC )(GLint x, GLint y, GLsizei wid typedef void (APIENTRYP PFNGLTEXSUBIMAGE3DPROC )(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const GLvoid* data); typedef void (APIENTRYP PFNGLTEXIMAGE3DPROC )(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, const GLvoid * data); typedef void (APIENTRYP PFNGLGETINTERGERVPROC )(GLenum pname, GLint * data); +typedef void (APIENTRYP PFNGLGETTEXIMAGEPROC )(GLenum target, GLint level, GLenum format, GLenum type, GLvoid * img); #ifdef NO_EXTERN_GLAPI // Defines the function pointers @@ -130,6 +133,7 @@ GLAPI_DECL PFNGLVIEWPORTPROC glViewport GLAP GLAPI_DECL PFNGLTEXSUBIMAGE3DPROC glTexSubImage3D GLAPI_SET; GLAPI_DECL PFNGLTEXIMAGE3DPROC glTexImage3D GLAPI_SET; GLAPI_DECL PFNGLGETINTERGERVPROC glGetIntegerv GLAPI_SET; +GLAPI_DECL PFNGLGETTEXIMAGEPROC glGetTexImage GLAPI_SET; // 2.0+ function pointers GLAPI_DECL PFNGLATTACHSHADERPROC glAttachShader GLAPI_SET; diff --git a/src/drawing/engines/opengl/OpenGLDrawingEngine.cpp b/src/drawing/engines/opengl/OpenGLDrawingEngine.cpp index 5659337ea4..668aa91a67 100644 --- a/src/drawing/engines/opengl/OpenGLDrawingEngine.cpp +++ b/src/drawing/engines/opengl/OpenGLDrawingEngine.cpp @@ -943,7 +943,7 @@ void OpenGLDrawingContext::FlushLines() { void OpenGLDrawingContext::FlushImages() { if (_commandBuffers.images.size() == 0) return; - OpenGLAPI::SetTexture(0, GL_TEXTURE_2D_ARRAY, _textureCache->GetAtlasTextureArray()); + OpenGLAPI::SetTexture(0, GL_TEXTURE_2D_ARRAY, _textureCache->GetAtlasesTexture()); std::vector instances; instances.reserve(_commandBuffers.images.size()); diff --git a/src/drawing/engines/opengl/TextureCache.cpp b/src/drawing/engines/opengl/TextureCache.cpp index 973ab3d1c6..fd8b9e0561 100644 --- a/src/drawing/engines/opengl/TextureCache.cpp +++ b/src/drawing/engines/opengl/TextureCache.cpp @@ -42,8 +42,6 @@ void TextureCache::SetPalette(const SDL_Color * palette) void TextureCache::InvalidateImage(uint32 image) { - InitialiseAtlases(); - auto kvp = _imageTextureMap.find(image); if (kvp != _imageTextureMap.end()) { @@ -54,8 +52,6 @@ void TextureCache::InvalidateImage(uint32 image) CachedTextureInfo TextureCache::GetOrLoadImageTexture(uint32 image) { - InitialiseAtlases(); - auto kvp = _imageTextureMap.find(image & 0x7FFFF); if (kvp != _imageTextureMap.end()) { @@ -70,8 +66,6 @@ CachedTextureInfo TextureCache::GetOrLoadImageTexture(uint32 image) CachedTextureInfo TextureCache::GetOrLoadGlyphTexture(uint32 image, uint8 * palette) { - InitialiseAtlases(); - GlyphId glyphId; glyphId.Image = image; Memory::Copy(&glyphId.Palette, palette, sizeof(glyphId.Palette)); @@ -88,37 +82,55 @@ CachedTextureInfo TextureCache::GetOrLoadGlyphTexture(uint32 image, uint8 * pale return cacheInfo; } -void TextureCache::InitialiseAtlases() { - if (!_atlasInitialised) { +void TextureCache::CreateAtlasesTexture() { + if (!_atlasesTextureInitialised) { // Determine width and height to use for texture atlases - GLint maxSize; - glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxSize); - if (maxSize > TEXTURE_CACHE_MAX_ATLAS_SIZE) maxSize = TEXTURE_CACHE_MAX_ATLAS_SIZE; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &_atlasesTextureDimensions); + if (_atlasesTextureDimensions > TEXTURE_CACHE_MAX_ATLAS_SIZE) { + _atlasesTextureDimensions = TEXTURE_CACHE_MAX_ATLAS_SIZE; + } + + // Determine maximum number of atlases (minimum of size and array limit) + glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &_atlasesTextureIndicesLimit); + if (_atlasesTextureDimensions < _atlasesTextureIndicesLimit) _atlasesTextureIndicesLimit = _atlasesTextureDimensions; // Create an array texture to hold all of the atlases - glGenTextures(1, &_atlasTextureArray); - glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasTextureArray); - glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_R8UI, maxSize, maxSize, _atlases.size(), 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, nullptr); + glGenTextures(1, &_atlasesTexture); + glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasesTexture); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - // Initialise atlases - for (auto& atlas : _atlases) { - atlas.Initialise(maxSize, maxSize); - } - - _atlasInitialised = true; + _atlasesTextureInitialised = true; + _atlasesTextureIndices = 0; } } +void TextureCache::EnlargeAtlasesTexture(GLuint newEntries) { + CreateAtlasesTexture(); + + GLuint newIndices = _atlasesTextureIndices + newEntries; + + // Retrieve current array data + std::vector oldPixels(_atlasesTextureDimensions * _atlasesTextureDimensions * _atlasesTextureIndices); + glGetTexImage(GL_TEXTURE_2D_ARRAY, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, oldPixels.data()); + + // Reallocate array + glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_R8UI, _atlasesTextureDimensions, _atlasesTextureDimensions, newIndices, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, nullptr); + + // Restore old data + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, _atlasesTextureDimensions, _atlasesTextureDimensions, _atlasesTextureIndices, GL_RED_INTEGER, GL_UNSIGNED_BYTE, oldPixels.data()); + + _atlasesTextureIndices = newIndices; +} + CachedTextureInfo TextureCache::LoadImageTexture(uint32 image) { rct_drawpixelinfo * dpi = GetImageAsDPI(image, 0); - auto cacheInfo = AllocateFromAppropriateAtlas(dpi->width, dpi->height); + auto cacheInfo = AllocateImage(dpi->width, dpi->height); - glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasTextureArray); + glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasesTexture); glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, cacheInfo.bounds.x, cacheInfo.bounds.y, cacheInfo.index, dpi->width, dpi->height, 1, GL_RED_INTEGER, GL_UNSIGNED_BYTE, dpi->bits); DeleteDPI(dpi); @@ -130,9 +142,9 @@ CachedTextureInfo TextureCache::LoadGlyphTexture(uint32 image, uint8 * palette) { rct_drawpixelinfo * dpi = GetGlyphAsDPI(image, palette); - auto cacheInfo = AllocateFromAppropriateAtlas(dpi->width, dpi->height); + auto cacheInfo = AllocateImage(dpi->width, dpi->height); - glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasTextureArray); + glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasesTexture); glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, cacheInfo.bounds.x, cacheInfo.bounds.y, cacheInfo.index, dpi->width, dpi->height, 1, GL_RED_INTEGER, GL_UNSIGNED_BYTE, dpi->bits); DeleteDPI(dpi); @@ -156,14 +168,36 @@ void * TextureCache::GetImageAsARGB(uint32 image, uint32 tertiaryColour, uint32 return pixels32; } -CachedTextureInfo TextureCache::AllocateFromAppropriateAtlas(int imageWidth, int imageHeight) { +CachedTextureInfo TextureCache::AllocateImage(int imageWidth, int imageHeight) { + CreateAtlasesTexture(); + + // Find an atlas that fits this image for (Atlas& atlas : _atlases) { - if (atlas.GetFreeSlots() > 0 && atlas.SupportsImage(imageWidth, imageHeight)) { + if (atlas.GetFreeSlots() > 0 && atlas.IsImageSuitable(imageWidth, imageHeight)) { return atlas.Allocate(imageWidth, imageHeight); } } - throw std::runtime_error("no atlas with free slots left that supports image!"); + // If there is no such atlas, then create a new one + if ((int) _atlases.size() >= _atlasesTextureIndicesLimit) { + throw std::runtime_error("more texture atlases required, but device limit reached!"); + } + + int atlasIndex = (int) _atlases.size(); + int atlasSize = (int) powf(2, (float) Atlas::CalculateImageSizeOrder(imageWidth, imageHeight)); + +#ifdef DEBUG + printf("new texture atlas #%d (size %d) allocated\n", atlasIndex, atlasSize); +#endif + + _atlases.push_back(std::move(Atlas(atlasIndex, atlasSize))); + _atlases.back().Initialise(_atlasesTextureDimensions, _atlasesTextureDimensions); + + // Enlarge texture array to support new atlas + EnlargeAtlasesTexture(1); + + // And allocate from the new atlas + return _atlases.back().Allocate(imageWidth, imageHeight); } rct_drawpixelinfo * TextureCache::GetImageAsDPI(uint32 image, uint32 tertiaryColour) @@ -236,7 +270,7 @@ void * TextureCache::ConvertDPIto32bpp(const rct_drawpixelinfo * dpi) void TextureCache::FreeTextures() { // Free array texture - glDeleteTextures(1, &_atlasTextureArray); + glDeleteTextures(1, &_atlasesTexture); } rct_drawpixelinfo * TextureCache::CreateDPI(sint32 width, sint32 height) @@ -262,8 +296,8 @@ void TextureCache::DeleteDPI(rct_drawpixelinfo* dpi) delete dpi; } -GLuint TextureCache::GetAtlasTextureArray() { - return _atlasTextureArray; +GLuint TextureCache::GetAtlasesTexture() { + return _atlasesTexture; } #endif /* DISABLE_OPENGL */ diff --git a/src/drawing/engines/opengl/TextureCache.h b/src/drawing/engines/opengl/TextureCache.h index e2ca9889b7..f662663aeb 100644 --- a/src/drawing/engines/opengl/TextureCache.h +++ b/src/drawing/engines/opengl/TextureCache.h @@ -17,7 +17,8 @@ #pragma once #include -#include +#include +#include #include #include "../../../common.h" #include "OpenGLAPI.h" @@ -52,12 +53,14 @@ struct GlyphId }; }; -// TODO: Handle no more slots remaining (allocate more atlases?) -// TODO: Handle images larger than 256x256 - -// This is the maximum width and height of each atlas (2048 -> 4 MB) +// This is the maximum width and height of each atlas, basically the +// granularity at which new atlases are allocated (2048 -> 4 MB of VRAM) constexpr int TEXTURE_CACHE_MAX_ATLAS_SIZE = 2048; +// Pixel dimensions of smallest supported slots in texture atlases +// Must be a power of 2! +constexpr int TEXTURE_CACHE_SMALLEST_SLOT = 32; + // Location of an image (texture atlas index, slot and normalized coordinates) struct CachedTextureInfo { GLuint index; @@ -68,28 +71,28 @@ struct CachedTextureInfo { // Represents a texture atlas that images of a given maximum size can be allocated from // Atlases are all stored in the same 2D texture array, occupying the specified index +// Slots in atlases are always squares. class Atlas { private: GLuint _index; - int _imageWidth, _imageHeight; + int _imageSize; int _atlasWidth, _atlasHeight; std::vector _freeSlots; int _cols, _rows; public: - Atlas(GLuint index, int imageWidth, int imageHeight) { + Atlas(GLuint index, int imageSize) { _index = index; - _imageWidth = imageWidth; - _imageHeight = imageHeight; + _imageSize = imageSize; } void Initialise(int atlasWidth, int atlasHeight) { _atlasWidth = atlasWidth; _atlasHeight = atlasHeight; - _cols = _atlasWidth / _imageWidth; - _rows = _atlasHeight / _imageHeight; + _cols = _atlasWidth / _imageSize; + _rows = _atlasHeight / _imageSize; _freeSlots.resize(_cols * _rows); for (size_t i = 0; i < _freeSlots.size(); i++) { @@ -105,10 +108,6 @@ public: auto bounds = GetSlotCoordinates(slot, actualWidth, actualHeight); -#ifdef DEBUG - printf("texture atlas (%d, %d) has %d slots left\n", _imageWidth, _imageHeight, GetFreeSlots()); -#endif - return {_index, slot, bounds, NormalizeCoordinates(bounds)}; } @@ -118,24 +117,39 @@ public: _freeSlots.push_back(info.slot); } - bool SupportsImage(int actualWidth, int actualHeight) const { - return actualWidth <= _imageWidth && actualHeight <= _imageHeight; + // Checks if specified image would be tightly packed in this atlas + // by checking if it is within the right power of 2 range + bool IsImageSuitable(int actualWidth, int actualHeight) const { + int imageOrder = CalculateImageSizeOrder(actualWidth, actualHeight); + int atlasOrder = (int) log2(_imageSize); + + return imageOrder == atlasOrder; } int GetFreeSlots() const { return (int) _freeSlots.size(); } + static int CalculateImageSizeOrder(int actualWidth, int actualHeight) { + int actualSize = std::max(actualWidth, actualHeight); + + if (actualSize < TEXTURE_CACHE_SMALLEST_SLOT) { + actualSize = TEXTURE_CACHE_SMALLEST_SLOT; + } + + return (int) ceil(log2f((float) actualSize)); + } + private: vec4i GetSlotCoordinates(GLuint slot, int actualWidth, int actualHeight) const { int row = slot / _cols; int col = slot % _cols; return vec4i{ - _imageWidth * col, - _imageHeight * row, - _imageWidth * col + actualWidth, - _imageHeight * row + actualHeight, + _imageSize * col, + _imageSize * row, + _imageSize * col + actualWidth, + _imageSize * row + actualHeight, }; } @@ -152,17 +166,13 @@ private: class TextureCache { private: - bool _atlasInitialised = false; + bool _atlasesTextureInitialised = false; - GLuint _atlasTextureArray; - - // Atlases should be ordered from small to large image support - std::array _atlases = { - Atlas{0, 32, 32}, - Atlas{1, 64, 64}, - Atlas{2, 128, 128}, - Atlas{3, 256, 256} - }; + GLuint _atlasesTexture; + GLint _atlasesTextureDimensions; + GLuint _atlasesTextureIndices; + GLint _atlasesTextureIndicesLimit; + std::vector _atlases; std::unordered_map _imageTextureMap; std::unordered_map _glyphTextureMap; @@ -176,14 +186,15 @@ public: void InvalidateImage(uint32 image); CachedTextureInfo GetOrLoadImageTexture(uint32 image); CachedTextureInfo GetOrLoadGlyphTexture(uint32 image, uint8 * palette); - - GLuint GetAtlasTextureArray(); + + GLuint GetAtlasesTexture(); private: - void InitialiseAtlases(); + void CreateAtlasesTexture(); + void EnlargeAtlasesTexture(GLuint newEntries); CachedTextureInfo LoadImageTexture(uint32 image); CachedTextureInfo LoadGlyphTexture(uint32 image, uint8 * palette); - CachedTextureInfo AllocateFromAppropriateAtlas(int imageWidth, int imageHeight); + CachedTextureInfo AllocateImage(int imageWidth, int imageHeight); void * GetImageAsARGB(uint32 image, uint32 tertiaryColour, uint32 * outWidth, uint32 * outHeight); rct_drawpixelinfo * GetImageAsDPI(uint32 image, uint32 tertiaryColour); void * GetGlyphAsARGB(uint32 image, uint8 * palette, uint32 * outWidth, uint32 * outHeight);