Implement dynamic atlas allocation in texture cache

This commit is contained in:
Alexander Overvoorde 2016-07-23 20:31:41 +02:00
parent c506e08ac2
commit c107101aff
5 changed files with 116 additions and 66 deletions

View File

@ -71,6 +71,7 @@ static const char * TryLoadAllProcAddresses()
SetupOpenGLFunction(glTexSubImage3D);
SetupOpenGLFunction(glTexImage3D);
SetupOpenGLFunction(glGetIntegerv);
SetupOpenGLFunction(glGetTexImage);
// 2.0+ functions
SetupOpenGLFunction(glAttachShader);

View File

@ -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;

View File

@ -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<DrawImageInstance> instances;
instances.reserve(_commandBuffers.images.size());

View File

@ -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<void>(&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<char> 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 */

View File

@ -17,7 +17,8 @@
#pragma once
#include <unordered_map>
#include <array>
#include <vector>
#include <algorithm>
#include <SDL_pixels.h>
#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<GLuint> _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<Atlas, 4> _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<Atlas> _atlases;
std::unordered_map<uint32, CachedTextureInfo> _imageTextureMap;
std::unordered_map<GlyphId, CachedTextureInfo, GlyphId::Hash, GlyphId::Equal> _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);