mirror of https://github.com/OpenRCT2/OpenRCT2.git
Implement dynamic atlas allocation in texture cache
This commit is contained in:
parent
c506e08ac2
commit
c107101aff
|
@ -71,6 +71,7 @@ static const char * TryLoadAllProcAddresses()
|
|||
SetupOpenGLFunction(glTexSubImage3D);
|
||||
SetupOpenGLFunction(glTexImage3D);
|
||||
SetupOpenGLFunction(glGetIntegerv);
|
||||
SetupOpenGLFunction(glGetTexImage);
|
||||
|
||||
// 2.0+ functions
|
||||
SetupOpenGLFunction(glAttachShader);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue