From 754496fe06a840f46ed359029d0d2ceed26364a2 Mon Sep 17 00:00:00 2001 From: kearwood Date: Tue, 11 Sep 2012 04:32:04 +0000 Subject: [PATCH] Implemented texture and VBO swapping system that keeps GPU memory utilization within limits --HG-- extra : convert_revision : svn%3A7752d6cf-9f14-4ad2-affc-04f1e67b81a5/trunk%4091 --- KREngine/KREngine.xcodeproj/project.pbxproj | 12 +++++++++++- KREngine/KREngine/Classes/KRCamera.cpp | 1 + KREngine/KREngine/Classes/KREngine.mm | 2 +- KREngine/KREngine/Classes/KRModelManager.cpp | 12 ++++++++++-- KREngine/KREngine/Classes/KRModelManager.h | 5 ++++- KREngine/KREngine/Classes/KRScene.cpp | 1 - KREngine/KREngine/Classes/KRTexture.cpp | 18 ++++++++++++++---- KREngine/KREngine/Classes/KRTexture.h | 7 +++++-- .../KREngine/Classes/KRTextureManager.cpp | 12 +++++++++--- KREngine/KREngine/Classes/KRTextureManager.h | 7 ++++++- KREngine/KREngine/Shaders/font.pvr | Bin 0 -> 43828 bytes objview/KRObjView.xcodeproj/project.pbxproj | 4 ++++ 12 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 KREngine/KREngine/Shaders/font.pvr diff --git a/KREngine/KREngine.xcodeproj/project.pbxproj b/KREngine/KREngine.xcodeproj/project.pbxproj index 08d8a53..30853e2 100644 --- a/KREngine/KREngine.xcodeproj/project.pbxproj +++ b/KREngine/KREngine.xcodeproj/project.pbxproj @@ -296,6 +296,7 @@ E4BBBB961512A46700F43B5B /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.6.sdk/System/Library/Frameworks/AppKit.framework; sourceTree = DEVELOPER_DIR; }; E4BBBB981512A47500F43B5B /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.6.sdk/System/Library/Frameworks/CoreData.framework; sourceTree = DEVELOPER_DIR; }; E4BBBB9A1512A48200F43B5B /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.6.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + E4CE184815FEEDA200F80870 /* font.pvr */ = {isa = PBXFileReference; lastKnownFileType = file; name = font.pvr; path = Shaders/font.pvr; sourceTree = ""; }; E4D133B91538F7480070068C /* light_directional.fsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; name = light_directional.fsh; path = Shaders/light_directional.fsh; sourceTree = ""; }; E4D133BB1538F7560070068C /* light_directional.vsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; name = light_directional.vsh; path = Shaders/light_directional.vsh; sourceTree = ""; }; E4F711A41512BB56007EE923 /* libfbxsdk-2012.2-static.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libfbxsdk-2012.2-static.a"; path = "../../../../../../../../Applications/Autodesk/FBXSDK20122/lib/gcc4/ub/libfbxsdk-2012.2-static.a"; sourceTree = ""; }; @@ -547,12 +548,12 @@ E491016613C99B9E0098455B /* KREngine */ = { isa = PBXGroup; children = ( + E4CE184615FEED6800F80870 /* Standard Assets */, E491016E13C99BAE0098455B /* Classes */, E4924C2415EE95E700B965C6 /* KROctree.cpp */, E4924C2515EE95E800B965C6 /* KROctree.h */, E4924C2915EE96AA00B965C6 /* KROctreeNode.cpp */, E4924C2A15EE96AA00B965C6 /* KROctreeNode.h */, - E45772E313C99F160037BEEA /* Shaders */, E491016713C99B9E0098455B /* Supporting Files */, ); path = KREngine; @@ -627,6 +628,15 @@ name = "OSX Frameworks"; sourceTree = ""; }; + E4CE184615FEED6800F80870 /* Standard Assets */ = { + isa = PBXGroup; + children = ( + E4CE184815FEEDA200F80870 /* font.pvr */, + E45772E313C99F160037BEEA /* Shaders */, + ); + name = "Standard Assets"; + sourceTree = ""; + }; E4F9753815362A5200FD60B2 /* 3rdparty */ = { isa = PBXGroup; children = ( diff --git a/KREngine/KREngine/Classes/KRCamera.cpp b/KREngine/KREngine/Classes/KRCamera.cpp index 9cc8288..6957a20 100644 --- a/KREngine/KREngine/Classes/KRCamera.cpp +++ b/KREngine/KREngine/Classes/KRCamera.cpp @@ -416,6 +416,7 @@ void KRCamera::renderFrame(KRScene &scene, KRMat4 &viewMatrix, KRVector3 &lightD glDepthMask(GL_TRUE); + fprintf(stderr, "VBO Mem: %i Kbyte Texture Mem: %i Kbyte\n", (int)m_pContext->getModelManager()->getMemUsed() / 1024, (int)m_pContext->getTextureManager()->getMemUsed() / 1024); } diff --git a/KREngine/KREngine/Classes/KREngine.mm b/KREngine/KREngine/Classes/KREngine.mm index 873c1b6..3af9c1c 100644 --- a/KREngine/KREngine/Classes/KREngine.mm +++ b/KREngine/KREngine/Classes/KREngine.mm @@ -122,7 +122,7 @@ double const PI = 3.141592653589793f; NSFileManager* fileManager = [NSFileManager defaultManager]; NSString *bundle_directory = [[NSBundle mainBundle] bundlePath]; for (NSString* fileName in [fileManager contentsOfDirectoryAtPath: bundle_directory error:nil]) { - if([fileName hasSuffix: @".vsh"] || [fileName hasSuffix: @".fsh"]) { + if([fileName hasSuffix: @".vsh"] || [fileName hasSuffix: @".fsh"] || [fileName isEqualToString:@"font.pvr"]) { NSString* path = [NSString stringWithFormat:@"%@/%@", bundle_directory, fileName]; _context->loadResource([path UTF8String]); } diff --git a/KREngine/KREngine/Classes/KRModelManager.cpp b/KREngine/KREngine/Classes/KRModelManager.cpp index 2346db4..b9731ee 100644 --- a/KREngine/KREngine/Classes/KRModelManager.cpp +++ b/KREngine/KREngine/Classes/KRModelManager.cpp @@ -36,6 +36,7 @@ KRModelManager::KRModelManager(KRContext &context) : KRContextObject(context) { m_currentVBO.handle = 0; m_currentVBO.data = NULL; + m_vboMemUsed = 0; } KRModelManager::~KRModelManager() { @@ -74,20 +75,23 @@ void KRModelManager::bindVBO(const GLvoid *data, GLsizeiptr size) { assert(m_currentVBO.size == size); glBindBuffer(GL_ARRAY_BUFFER, m_currentVBO.handle); } else { + m_vboMemUsed += size; - while(m_vbos.size() >= KRENGINE_MAX_VBO_HANDLES) { + while(m_vbos.size() >= KRENGINE_MAX_VBO_HANDLES || m_vboMemUsed >= KRENGINE_MAX_VBO_MEM) { // TODO - This should maintain a max size limit for VBO's rather than a limit on the number of VBO's. As meshes are split to multiple small VBO's, this is not too bad, but still not optimial. std::map::iterator first_itr = m_vbos.begin(); vbo_info_type firstVBO = first_itr->second; glDeleteBuffers(1, &firstVBO.handle); + m_vboMemUsed -= firstVBO.size; m_vbos.erase(first_itr); - fprintf(stderr, "VBO Swapping..."); + fprintf(stderr, "VBO Swapping...\n"); } m_currentVBO.handle = -1; glGenBuffers(1, &m_currentVBO.handle); glBindBuffer(GL_ARRAY_BUFFER, m_currentVBO.handle); glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW); + m_currentVBO.size = size; m_currentVBO.data = data; @@ -96,3 +100,7 @@ void KRModelManager::bindVBO(const GLvoid *data, GLsizeiptr size) { } } +long KRModelManager::getMemUsed() +{ + return m_vboMemUsed; +} diff --git a/KREngine/KREngine/Classes/KRModelManager.h b/KREngine/KREngine/Classes/KRModelManager.h index 8eaeb59..b7aa9cd 100644 --- a/KREngine/KREngine/Classes/KRModelManager.h +++ b/KREngine/KREngine/Classes/KRModelManager.h @@ -32,7 +32,8 @@ #ifndef KRMODELMANAGER_H #define KRMODELMANAGER_H -#define KRENGINE_MAX_VBO_HANDLES 50 +#define KRENGINE_MAX_VBO_HANDLES 1000 +#define KRENGINE_MAX_VBO_MEM 100000000 #import "KREngine-common.h" #import "KRContextObject.h" @@ -59,6 +60,7 @@ public: void bindVBO(const GLvoid *data, GLsizeiptr size); + long getMemUsed(); private: std::map m_models; @@ -69,6 +71,7 @@ private: const GLvoid *data; } vbo_info_type; + long m_vboMemUsed; vbo_info_type m_currentVBO; std::map m_vbos; diff --git a/KREngine/KREngine/Classes/KRScene.cpp b/KREngine/KREngine/Classes/KRScene.cpp index 5ae3dc3..59bb531 100644 --- a/KREngine/KREngine/Classes/KRScene.cpp +++ b/KREngine/KREngine/Classes/KRScene.cpp @@ -274,7 +274,6 @@ KRScene *KRScene::Load(KRContext &context, const std::string &name, KRDataBlock { data->append((void *)"\0", 1); // Ensure data is null terminated, to read as a string safely tinyxml2::XMLDocument doc; - fprintf(stderr, "Scene Content: %s\n", data->getStart()); doc.Parse((char *)data->getStart()); KRScene *new_scene = new KRScene(context, name); diff --git a/KREngine/KREngine/Classes/KRTexture.cpp b/KREngine/KREngine/Classes/KRTexture.cpp index ff3c64b..4658750 100644 --- a/KREngine/KREngine/Classes/KRTexture.cpp +++ b/KREngine/KREngine/Classes/KRTexture.cpp @@ -77,7 +77,8 @@ KRTexture::KRTexture(KRDataBlock *data, KRTextureManager *manager) { } KRTexture::~KRTexture() { - releaseHandle(); + long textureMemFreed = 0; + releaseHandle(textureMemFreed); delete m_pData; } @@ -205,10 +206,14 @@ bool KRTexture::createGLTexture() { return true; } -GLuint KRTexture::getHandle() { +GLuint KRTexture::getHandle(long &textureMemUsed) { if(m_iName == 0) { if(!createGLTexture()) { - createGLTexture(); // FINDME - HACK! The first texture fails with 0x501 return code but loads on second try + if(createGLTexture()) { // FINDME - HACK! The first texture fails with 0x501 return code but loads on second try + textureMemUsed += getMemSize(); + } + } else { + textureMemUsed += getMemSize(); } createGLTexture(); @@ -216,9 +221,14 @@ GLuint KRTexture::getHandle() { return m_iName; } -void KRTexture::releaseHandle() { +void KRTexture::releaseHandle(long &textureMemUsed) { if(m_iName != 0) { + textureMemUsed -= getMemSize(); glDeleteTextures(1, &m_iName); m_iName = 0; } } + +long KRTexture::getMemSize() { + return m_pData->getSize(); // TODO - This is not 100% accurate, as loaded format may differ in size while in GPU memory +} diff --git a/KREngine/KREngine/Classes/KRTexture.h b/KREngine/KREngine/Classes/KRTexture.h index 16e1e03..21e69ae 100644 --- a/KREngine/KREngine/Classes/KRTexture.h +++ b/KREngine/KREngine/Classes/KRTexture.h @@ -49,8 +49,10 @@ public: ~KRTexture(); bool createGLTexture(); - GLuint getHandle(); - void releaseHandle(); + GLuint getHandle(long &textureMemUsed); + void releaseHandle(long &textureMemUsed); + + long getMemSize(); private: @@ -72,6 +74,7 @@ private: bool load(); KRTextureManager *m_pManager; + }; #endif diff --git a/KREngine/KREngine/Classes/KRTextureManager.cpp b/KREngine/KREngine/Classes/KRTextureManager.cpp index ba7f9cb..9782302 100644 --- a/KREngine/KREngine/Classes/KRTextureManager.cpp +++ b/KREngine/KREngine/Classes/KRTextureManager.cpp @@ -33,6 +33,7 @@ #include KRTextureManager::KRTextureManager(KRContext &context) : KRContextObject(context) { + m_textureMemUsed = 0; for(int iTexture=0; iTexturegetHandle()); + glBindTexture(GL_TEXTURE_2D, pTexture->getHandle(m_textureMemUsed)); // TODO - These texture parameters should be assigned by the material or texture parameters glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 1.0f); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); @@ -91,6 +92,7 @@ void KRTextureManager::selectTexture(int iTextureUnit, KRTexture *pTexture) { } if(m_activeTextures[iTextureUnit] != NULL) { KRTexture *unloadedTexture = m_activeTextures[iTextureUnit]; + m_activeTextures[iTextureUnit] = NULL; bool bActive = false; for(int iTexture=0; iTexture < KRENGINE_MAX_TEXTURE_UNITS; iTexture++) { if(m_activeTextures[iTexture] == unloadedTexture) { @@ -101,10 +103,10 @@ void KRTextureManager::selectTexture(int iTextureUnit, KRTexture *pTexture) { // Only return a texture to the cache when the last texture unit referencing it is re-assigned to a different texture if(m_textureCache.find(unloadedTexture) == m_textureCache.end()) { m_textureCache.insert(unloadedTexture); - while(m_textureCache.size() > KRENGINE_MAX_TEXTURE_HANDLES) { + while(m_textureCache.size() > KRENGINE_MAX_TEXTURE_HANDLES || m_textureMemUsed > KRENGINE_MAX_TEXTURE_MEM) { // Keep texture size within limits KRTexture *droppedTexture = (*m_textureCache.begin()); - droppedTexture->releaseHandle(); + droppedTexture->releaseHandle(m_textureMemUsed); m_textureCache.erase(droppedTexture); fprintf(stderr, "Texture Swapping...\n"); } @@ -115,3 +117,7 @@ void KRTextureManager::selectTexture(int iTextureUnit, KRTexture *pTexture) { } } +long KRTextureManager::getMemUsed() { + return m_textureMemUsed; +} + diff --git a/KREngine/KREngine/Classes/KRTextureManager.h b/KREngine/KREngine/Classes/KRTextureManager.h index 0f929e0..be5c593 100644 --- a/KREngine/KREngine/Classes/KRTextureManager.h +++ b/KREngine/KREngine/Classes/KRTextureManager.h @@ -30,7 +30,8 @@ // #define KRENGINE_MAX_TEXTURE_UNITS 8 -#define KRENGINE_MAX_TEXTURE_HANDLES 20 +#define KRENGINE_MAX_TEXTURE_HANDLES 1000 +#define KRENGINE_MAX_TEXTURE_MEM 100000000 #ifndef KRTEXTUREMANAGER_H #define KRTEXTUREMANAGER_H @@ -58,11 +59,15 @@ public: KRTexture *getTexture(const char *szFile); + long getMemUsed(); + private: std::map m_textures; KRTexture *m_activeTextures[KRENGINE_MAX_TEXTURE_UNITS]; std::set m_textureCache; + + long m_textureMemUsed; }; #endif diff --git a/KREngine/KREngine/Shaders/font.pvr b/KREngine/KREngine/Shaders/font.pvr new file mode 100644 index 0000000000000000000000000000000000000000..7d91f4e36d1420643a22621dc48b7916ac60b87e GIT binary patch literal 43828 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 f)AxU@_X)y%@elvY;oGIS-~RCa