Managing OpenGL Resources

Started by
12 comments, last by Hodgman 10 years ago

Good day ladies and gentlemen of the realm.

Towards the end of last year I decided graphics programming is something I really wanted to get into, so as part of the learning process I've been putting together a very basic OpenGL engine. One of the things I've been putting a fair bit of thought into is how to abstract OpenGL functionality, specifically OpenGL resources, in an object-oriented manner (using modern C++).

For me one of the main design considerations when abstracting resources is whether of not these abstractions should exhibit value or reference semantics, i.e. should they represent resource handles or the resources themselves.

It made sense to me, and suited my needs at the time, to follow reference semantics, so every copy of a resource object refers to the same underlying OpenGL resource. Implementation wise went I with smart pointers and custom deleters, with the approach being used for Shader, VAO, FBO and Texture objects.


Shader::Shader()
:
m_programID( new GLuint( glCreateProgram() ), [=]( GLuint* program ){ glDeleteProgram( *program ); } )
{
}

So why am I mentioning all this? Well I want to ask if there valid cases for choosing to go with value semantics, and how things might change if you had classes dedicated to managing resource pools. Also any tips anyone might have for managing OpenGL resources and creating meaningful abstractions.

Advertisement

Sorry, I'm not gonna answer your question tongue.png

But can you point me to some books or articles about that 'value/reference semantics' stuff ?

Or maybe I do not understand you correctly. What is a recource handler in your situation and what it does ?

P.S.

Your constructor seems like a useless lambda monstrocity. It makes eyes bleed. You can't tell what it does at the first glance. Please, don't do like that :) C++ 11 is cool, but it's should be used not for the sake of the C++ 11 itself.

Also, if you're going to manage your RAM usage, I'd recommend to you not to use some automatics and smart/shared pointers at all, but to implement your own memory management mechanics, it will give you more flexibility and control over what's going on.

Do you really need to destroy your Shader object when it goes out of scope? Or it's more handy to load needed shaders, use them system-wise, and free memory manually after you're completely sure, that you'll not gonna use them at all ?

edit:

For resource pooling, I think, it's an effective approach to use your wrapper classes as the resources themselves + use standart containers with custom memory allocators and some memory counting structure for debugging. Depends on what you've meant by 'classes dedicated to managing resource pools'.

edit2:

Hope this would be more constructive.

Your constructor seems like a useless lambda monstrocity. It makes eyes bleed. You can't tell what it does at the first glance. Please, don't do like that C++ 11 is cool, but it's should be used not for the sake of the C++ 11 itself.

Thanks for the reply but I think you'll find that is not quite as useless as you say. Using a custom deleter for a smart pointer is a fairly standard idiom for automatic lifetime management of non-memory resources.

See http://herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers/

There are two main cases where you can’t use make_shared (or allocate_shared) to create an object that you know will be owned by shared_ptrs: (a) if you need a custom deleter, such as because of using shared_ptrs to manage a non-memory resource or an object allocated in a nonstandard memory area, you can’t use make_shared because it doesn’t support specifying a deleter; and (b) if you are adopting a raw pointer to an object being handed to you from other (usually legacy) code, you would construct a shared_ptr from that raw pointer directly.

Also, if you're going to manage your RAM usage, I'd recommend to you not to use some automatics and smart/shared pointers at all, but to implement your own memory management mechanics, it will give you more flexibility and control over what's going on.

Do you really need to destroy your Shader object when it goes out of scope? Or it's more handy to load needed shaders, use them system-wise, and free memory manually after you're completely sure, that you'll not gonna use them at all ?

I totally agree with that, if my ambitions were more grand and involved releasing an actual product then tighter, more manual control of resource lifetimes is definitely required for performance optimisations. That is sometimes I'll keep in the back of my mind and if my little project starts to get big enough I'll definitely look into it. As for now, resources being tied to scope seems to fit my relatively small bill. smile.png

But can you point me to some books or articles about that 'value/reference semantics' stuff ?

Or maybe I do not understand you correctly. What is a recource handler in your situation and what it does ?

The idea of values and references (to values) is a fairly fundamental topic in computer science and essentially what you are talking about is indirection. It's possible it is just the terminology that you aren't familiar with, in C++ references are implemented via pointer types and reference types, and in C# or Java the language explicitly categories types as being either "Reference Types" or "Value/Primitive Types".

See also:

http://en.wikipedia.org/wiki/Value_type

http://en.wikipedia.org/wiki/Reference_type

A succient summary from the wikipedia article "In computer science, the term value type is commonly used to refer to one of two kinds of data types: Types of values or Types of objects with deep copy semantics."

If they're value types, it means they can be copied, and copied fairly easily by the user (as simply as a = b;). Copying a GPU resource can be quite an expensive operation, so I'd see value-references as quite a dangerous API. Also, there's no point in cloning some GPU resources -- e.g. in D3D a shader program is just code (no other state), so there's no need in ever having more than one instance of a particular program hanging around.

Regarding shaders specifically, I don't have functionality to load a single shader - I always load a "shader pack", which is a large file containing a huge collection of shader programs and all the associated reflection data and other structures needed to use them (e.g. default UBO data). Once a pack is loaded, the user can acquire references to individual items inside it... Actually, the user can acquire references to shaders ahead-of-time (e.g. at data-build time), because my references are actually a 32-bit pack name and 32-bit shader index into that pack -- these can exist on disk in other files, such as in a model or material file.


Thanks for the reply but I think you'll find that is not quite as useless as you say.

I didn't say that it's useless, I said that its freaking hard to understand what it does, look : lambda list initializer + not-straightforward name 'm_programID' - clearly a mess at first sight. OR post more code so that everything would be clear :)


The idea of values and references (to values) is a fairly fundamental topic in computer science and essentially what you are talking about is indirection.

Your talks are obscure, as in the first post, which has confused me :)

If they're value types, it means they can be copied, and copied fairly easily by the user (as simply as a = b;). Copying a GPU resource can be quite an expensive operation, so I'd see value-references as quite a dangerous API.

This can't be highlighted enough; I've taken the liberty of underlining part of Hodgman's post above, because it needs to be heavily emphasised.

It's also the case that you need to destroy the copied resource when your function returns, by the way, which can also be an expensive operation. So now you're creating new GPU resources, copying to them, then destroying them, and all for what may be a fairly trivial operation. Expensive stuff.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls.

If they're value types, it means they can be copied, and copied fairly easily by the user (as simply as a = b;). Copying a GPU resource can be quite an expensive operation, so I'd see value-references as quite a dangerous API. Also, there's no point in cloning some GPU resources -- e.g. in D3D a shader program is just code (no other state), so there's no need in ever having more than one instance of a particular program hanging around.

This was my intuition of the situation, where for the most part you want shallow copies and reference semantics. If deep copies are required then that should be offered explicitly and cannot just happen 'by mistake'.

Your shader implementation sounds quite interesting, it's pretty cool what can be done when you're familiar with the domain, especially when it comes to performance optimisations. Considering how important resource management is, I definitely need to spend some more time researching the topic.


Thanks for the reply but I think you'll find that is not quite as useless as you say.

I didn't say that it's useless, I said that its freaking hard to understand what it does, look : lambda list initializer + not-straightforward name 'm_programID' - clearly a mess at first sight. OR post more code so that everything would be clear smile.png

Here is an equivalent example of using a shared_ptr with a custom deleter to manage a D311Device and the same thing for automatically releasing an SDL_Surface. I'm not sure why programID would be a confusing name, it represents the unique ID assigned by OpenGL to that particular shader program. Sure I admit I'm not very good when it comes to naming things but that made sense in my head.

Edit:

Oh and another example for managing files.

I personally chose the reference way, specifically using shared_ptrs. A major reason being that OpenGL specification itself uses language that either directly talks about a reference count or otherwise mentions that deleted resources only actually die when nothing refers to them anymore. So when an index buffer is attached to an vertex array, my vertex array object is given a (wrapped) shared_ptr that points to the index buffer.

Edit: I think some drivers were/are buggy specifically with the relation of index buffers and vertex arrays, that binding a vertex array doesn't cause the related index buffer being bound automatically. When the vertex array object in my code has a reference to the index buffer, it could work around that bug by explicitly binding the buffer.

If they're value types, it means they can be copied, and copied fairly easily by the user (as simply as a = b;). Copying a GPU resource can be quite an expensive operation, so I'd see value-references as quite a dangerous API. Also, there's no point in cloning some GPU resources -- e.g. in D3D a shader program is just code (no other state), so there's no need in ever having more than one instance of a particular program hanging around.

This was my intuition of the situation, where for the most part you want shallow copies and reference semantics. If deep copies are required then that should be offered explicitly and cannot just happen 'by mistake'.

One option that hasn't been discussed is to use non-copyable values. This avoids the issue of expensive copies by out-right preventing them.

You might still use shared_ptrs to share access to these objects - but there is a big difference between "an object that moves with reference semantics" and "an object passed by reference". By still being a value it allows you to also pass them by the cheaper plain reference or raw pointer. It also allows them to be stored more efficiently in contiguous arrays.

The precise way in which access to the object is passed around is a choice for the client code, not enforced by the handle type itself.

This topic is closed to new replies.

Advertisement