Skip to content

Commit

Permalink
Use array of textures rather than GL_TEXTURE_2D_ARRAY for atlas texture
Browse files Browse the repository at this point in the history
Would be simpler with much better performance if a proper 2D texture array (GL_TEXTURE_2D_ARRAY) was used. I couldn't quite get it to work, parts of the Nuklear GUI would be missing if the array texture was allocated with > 1 depth (layers)...
Tidy up added rendering code and gl shaders
  • Loading branch information
grantramsay committed Mar 2, 2019
1 parent 17cd795 commit cb9676f
Show file tree
Hide file tree
Showing 13 changed files with 488 additions and 378 deletions.
39 changes: 36 additions & 3 deletions apps/freeablo/farender/spritecache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace FARender
return ret;
}

SpriteCache::SpriteCache(uint32_t size) : mNextCacheIndex(1), mCurrentSize(0), mMaxSize(size) { (void)mMaxSize; }
SpriteCache::SpriteCache(uint32_t size) : mNextCacheIndex(1), mCurrentSize(0), mMaxSize(size) {}

SpriteCache::~SpriteCache()
{
Expand Down Expand Up @@ -100,6 +100,9 @@ namespace FARender

void SpriteCache::directInsert(Render::SpriteGroup* sprite, uint32_t cacheIndex)
{
if (mCurrentSize >= mMaxSize)
evict();

mUsedList.push_front(cacheIndex);
mCache[cacheIndex] = CacheEntry(sprite, mUsedList.begin(), true);

Expand All @@ -110,6 +113,9 @@ namespace FARender
{
if (!mCache.count(index))
{
if (mCurrentSize >= mMaxSize)
evict();

Render::SpriteGroup* newSprite = NULL;

if (mCacheToStr.count(index))
Expand Down Expand Up @@ -251,12 +257,39 @@ namespace FARender

void SpriteCache::evict()
{
// Sprites can no longer be removed from texture atlas.
if (!Render::SpriteGroup::canDelete())
return;

std::list<uint32_t>::reverse_iterator it;

for (it = mUsedList.rbegin(); it != mUsedList.rend(); it++)
{
if (!mCache[*it].immortal)
break;
}

release_assert(it != mUsedList.rend() && "no evictable slots found. This should never happen");

CacheEntry toEvict = mCache[*it];

toEvict.sprite->destroy();
delete toEvict.sprite;

mCache.erase(*it);
mUsedList.erase(--(it.base()));
mCurrentSize--;
}

void SpriteCache::clear()
{
// Sprites can no longer be removed from texture atlas.
if (!Render::SpriteGroup::canDelete())
return;

for (std::list<uint32_t>::iterator it = mUsedList.begin(); it != mUsedList.end(); it++)
{
mCache[*it].sprite->destroy();
delete mCache[*it].sprite;
}
}

std::string SpriteCache::getPathForIndex(uint32_t index)
Expand Down
2 changes: 2 additions & 0 deletions components/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ set(RenderFiles
render/nuklear_sdl_gl3.cpp
render/nuklear_sdl_gl3.h
render/misc.h
render/atlastexture.cpp
render/atlastexture.h
../extern/RectangleBinPack/MaxRectsBinPack.cpp
../extern/RectangleBinPack/MaxRectsBinPack.h
../extern/RectangleBinPack/Rect.cpp
Expand Down
154 changes: 154 additions & 0 deletions components/render/atlastexture.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/* Stores many small textures into a large texture (or array of textures)
* to allow batched drawing commands that increase performance.
* Currently implemented as an array of 2D textures, would be simpler with much
* better performance if a proper 2D texture array (GL_TEXTURE_2D_ARRAY) was
* used. Author couldn't quite get it to work, parts of the Nuklear GUI would
* be missing if the array texture was allocated with > 1 depth (layers)... */

#include "atlastexture.h"
#include "../../extern/RectangleBinPack/MaxRectsBinPack.h"

#include <boost/make_unique.hpp>
#include <misc/assert.h>

namespace Render
{
AtlasTexture::AtlasTexture()
{
GLint maxTextureSize;
GLint maxTextures = 8; // Hardcoded in fragment shader, GL 3.x minimum is 16.
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);

mTextureWidth = maxTextureSize;
mTextureHeight = maxTextureSize;

// Limit number of textures to a reasonable level (measured from testing).
// Note: Increasing this has a severe impact on performance.
GLint estimatedRequiredTextures = (1uLL << 29) / (mTextureWidth * mTextureHeight);
mTextureLayers = std::min(estimatedRequiredTextures, maxTextures);

// Atlas texture is currently packed so reduce alignment requirements.
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

mTextureArrayIds.resize(mTextureLayers);
glGenTextures(mTextureLayers, &mTextureArrayIds[0]);

for (int32_t layer = 0; layer < mTextureLayers; layer++)
mBinPacker.push_back(boost::make_unique<rbp::MaxRectsBinPack>(mTextureWidth, mTextureHeight, false));

GLuint fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);

for (int i = 0; i < mTextureLayers; i++)
{
bind(i);

// Allocate memory for texture array (by passing NULL).
// NOTE: For GL_COMPRESSED_RGBA image dimensions need to be padded to a
// multiple/alignment of 4: https://forums.khronos.org/showthread.php/77554
// Also may need to clear texture if there are
glTexImage2D(GL_TEXTURE_2D,
0,
/*GL_RGBA8*/ GL_RGB5_A1 /*GL_COMPRESSED_RGBA*/,
mTextureWidth,
mTextureHeight,
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
NULL);

// Clear the texture, it is undefined upon initialisation and we want any
// unused padded areas to be transparent (especially for highlighting edges).
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, mTextureArrayIds[i], 0);
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}

glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);

#ifdef DEBUG_ATLAS_TEXTURE
GLint internalFormat = 0;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat);
printf("MaxTextureSize %d, used (%d, %d, %d), 0x%04X\n", maxTextureSize, mTextureWidth, mTextureHeight, mTextureLayers, internalFormat);
#endif
}

size_t AtlasTexture::addTexture(int32_t width, int32_t height, const void* data)
{
// Alignment of 4 is required for default pixel store alignment or for using compressed textures.
// Padding is useful for avoiding artifacts when highlighting edges in a packed atlas texture.
// Only the bottom and right need be padded when adding an image.
// e.g. adding two 3x3 images beside one another (I=image, P=padding, A=alignment padding):
// PADDING = 1 PADDING = 2 PADDING = 1 PADDING = 2
// ALIGNMENT = 1 ALIGNMENT = 1 ALIGNMENT = 4 ALIGNMENT = 4
// I I I P I I I P I I I P P I I I P P I I I P I I I P I I I P P A A A I I I P P A A A
// I I I P I I I P I I I P P I I I P P I I I P I I I P I I I P P A A A I I I P P A A A
// I I I P I I I P I I I P P I I I P P I I I P I I I P I I I P P A A A I I I P P A A A
// P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P A A A P P P P P A A A
// P P P P P P P P P P P P P P P A A A P P P P P A A A
// A A A A A A A A A A A A A A A A
// A A A A A A A A A A A A A A A A
// A A A A A A A A A A A A A A A A
static const int32_t PADDING = 1; // Must be >= 0
static const int32_t ALIGNMENT = 1; // Must be >= 1
int32_t paddedWidth = (width + PADDING + ALIGNMENT - 1) / ALIGNMENT * ALIGNMENT;
int32_t paddedHeight = (height + PADDING + ALIGNMENT - 1) / ALIGNMENT * ALIGNMENT;

// This assert may cause trouble as one of the Diablo 1 images is 10752 pixels high.
// However ~46% (https://feedback.wildfiregames.com/report/opengl/feature/GL_MAX_TEXTURE_SIZE)
// of graphics cards are limited to 8192, not quite sure how it ever worked..
release_assert(paddedWidth <= mTextureWidth && paddedHeight <= mTextureHeight); // Texture size too small...

rbp::Rect packedPos;
int32_t layer;
for (layer = 0; layer < mTextureLayers; layer++)
{
packedPos = mBinPacker[layer]->Insert(paddedWidth, paddedHeight, rbp::MaxRectsBinPack::RectBestAreaFit);
release_assert(packedPos.height != 0);
if (packedPos.height != 0)
break;
}
release_assert(layer < mTextureLayers); // Run out of layers...

bind(layer);
glTexSubImage2D(GL_TEXTURE_2D, 0, packedPos.x, packedPos.y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);

// NOTE: when using compressed textures sometimes adding a
// glFlush/glFinish here fixes tiles a bit, not sure why..

auto id = mNextTextureId++;
mLookupMap[id] = AtlasTextureEntry(packedPos.x, packedPos.y, layer, width, height);

return id;
}

void AtlasTexture::bind(GLuint layer) const
{
glActiveTexture(GL_TEXTURE0 + layer);
glBindTexture(GL_TEXTURE_2D, mTextureArrayIds[layer]);
}

void AtlasTexture::bind() const
{
for (int i = 0; i < mTextureLayers; i++)
bind(i);
}

void AtlasTexture::free() { glDeleteTextures(mTextureLayers, &mTextureArrayIds[0]); }

float AtlasTexture::getOccupancy() const
{
float summedOccupancy = 0;
for (auto& bp : mBinPacker)
summedOccupancy += bp->Occupancy();
return summedOccupancy / mBinPacker.size() * 100;
}
}
53 changes: 53 additions & 0 deletions components/render/atlastexture.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#pragma once

#include "sdl_gl_funcs.h"

#include <map>
#include <stdint.h>
#include <vector>

//#define DEBUG_ATLAS_TEXTURE

namespace rbp
{
class MaxRectsBinPack;
}

namespace Render
{
class AtlasTextureEntry
{
public:
AtlasTextureEntry() = default;
AtlasTextureEntry(int32_t x, int32_t y, int32_t layer, int32_t width, int32_t height) : mX(x), mY(y), mLayer(layer), mWidth(width), mHeight(height) {}

int32_t mX = 0, mY = 0, mLayer = 0, mWidth = 0, mHeight = 0;
};

typedef std::map<size_t, AtlasTextureEntry> AtlasTextureLookupMap;

class AtlasTexture
{
public:
AtlasTexture();

size_t addTexture(int32_t width, int32_t height, const void* data);
void bind() const;
void free();
GLint getTextureWidth() const { return mTextureWidth; }
GLint getTextureHeight() const { return mTextureHeight; }
const AtlasTextureLookupMap& getLookupMap() const { return mLookupMap; }
float getOccupancy() const;

private:
void bind(GLuint layer) const;

std::vector<GLuint> mTextureArrayIds;
GLint mTextureWidth;
GLint mTextureHeight;
GLint mTextureLayers;
AtlasTextureLookupMap mLookupMap;
size_t mNextTextureId = 1;
std::vector<std::unique_ptr<rbp::MaxRectsBinPack>> mBinPacker;
};
}
1 change: 1 addition & 0 deletions components/render/misc.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace Render
public:
SpriteGroup(const std::string& path);
SpriteGroup(const std::vector<Sprite> sprites) : mSprites(sprites), mAnimLength(sprites.size()) {}
static bool canDelete();
void destroy();

Sprite& operator[](size_t index);
Expand Down
Loading

0 comments on commit cb9676f

Please sign in to comment.