Jump to content

  • Log In with Google      Sign In   
  • Create Account

We need your help!

We need 1 more developer from Canada and 12 more from Australia to help us complete a research survey.

Support our site by taking a quick sponsored survey and win a chance at a $50 Amazon gift card. Click here to get started!


Managing OpenGL Resources


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
13 replies to this topic

#1 Lantre   Members   -  Reputation: 229

Like
0Likes
Like

Posted 03 April 2014 - 01:30 PM

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.



Sponsor:

#2 syskrank   Members   -  Reputation: 224

Like
-2Likes
Like

Posted 06 April 2014 - 02:04 AM

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.


Edited by syskrank, 06 April 2014 - 04:00 AM.


#3 Lantre   Members   -  Reputation: 229

Like
0Likes
Like

Posted 06 April 2014 - 07:31 AM

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."



#4 Hodgman   Moderators   -  Reputation: 42456

Like
4Likes
Like

Posted 06 April 2014 - 08:20 AM

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.



#5 syskrank   Members   -  Reputation: 224

Like
0Likes
Like

Posted 06 April 2014 - 09:18 AM


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 :) 



#6 mhagain   Crossbones+   -  Reputation: 9957

Like
0Likes
Like

Posted 06 April 2014 - 09:20 AM

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.


It appears that the gentleman thought C++ was extremely difficult and he was overjoyed that the machine was absorbing it; he understood that good C++ is difficult but the best C++ is well-nigh unintelligible.


#7 Lantre   Members   -  Reputation: 229

Like
0Likes
Like

Posted 06 April 2014 - 09:42 AM

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.



#8 Lantre   Members   -  Reputation: 229

Like
0Likes
Like

Posted 06 April 2014 - 09:57 AM

 


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.


Edited by Lantre, 06 April 2014 - 09:58 AM.


#9 Ubik   Members   -  Reputation: 889

Like
0Likes
Like

Posted 06 April 2014 - 01:04 PM

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.

Edited by Ubik, 06 April 2014 - 01:08 PM.


#10 dmatter   Crossbones+   -  Reputation: 3614

Like
4Likes
Like

Posted 06 April 2014 - 01:39 PM

 

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.



#11 Lantre   Members   -  Reputation: 229

Like
0Likes
Like

Posted 06 April 2014 - 01:58 PM

 

 

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 makes a lot of sense especially if you only have one point of control, e.g. a resource manager/resource cache, which owns the 'real' resource. Client code can then request weak/non-owning references to these resources.

 

This is something I'll probably be playing around with pretty soon for meshes and materials. At the moment I have each object naively loading (and owning) its own textures and meshes, which while convenient is not particularly clever.



#12 Hodgman   Moderators   -  Reputation: 42456

Like
1Likes
Like

Posted 06 April 2014 - 10:24 PM

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.

That's actually what I do for resources, but the value is only used inside the render-device implementation biggrin.png They're owned by an RAII value, and then references are given out to the user, e.g.

//hidden internally, not seen by the user
struct NativeBuffer : NonCopyable { /*GL, D3D, etc buffer handle member variable*/
  NativeBuffer( ... ) { /*acquire handle*/ }
  ~NativeBuffer() { /* release handle*/ }
};

//what the user gets:
typedef Pool<NativeBuffer>::Handle BufferHandle;

class Device
{
public:
  BufferHandle CreateBuffer(...);
  void         ReleaseBuffer( BufferHandle );
  ResourceLock MapBuffer( BufferHandle );
  void         UnmapBuffer( ResourceLock );
private:
  Pool<NativeBuffer> m_Buffers;
}

A system to allow shared ownership is implemented on top of this -- e.g. you might have a model-asset that owns some buffers, and then many model-instances that all share the one model-asset.


Edited by Hodgman, 07 April 2014 - 01:05 AM.


#13 Lantre   Members   -  Reputation: 229

Like
0Likes
Like

Posted 07 April 2014 - 04:03 AM

 

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.

That's actually what I do for resources, but the value is only used inside the render-device implementation biggrin.png They're owned by an RAII value, and then references are given out to the user, e.g.

//hidden internally, not seen by the user
struct NativeBuffer : NonCopyable { /*GL, D3D, etc buffer handle member variable*/
  NativeBuffer( ... ) { /*acquire handle*/ }
  ~NativeBuffer() { /* release handle*/ }
};

//what the user gets:
typedef Pool<NativeBuffer>::Handle BufferHandle;

class Device
{
public:
  BufferHandle CreateBuffer(...);
  void         ReleaseBuffer( BufferHandle );
  ResourceLock MapBuffer( BufferHandle );
  void         UnmapBuffer( ResourceLock );
private:
  Pool<NativeBuffer> m_Buffers;
}

A system to allow shared ownership is implemented on top of this -- e.g. you might have a model-asset that owns some buffers, and then many model-instances that all share the one model-asset.

 

 

Once you give out references to their respective users what mechanism do you use to track usage in order to determine what should or shouldn't be cached? If that is even a relevant question for your framework.



#14 Hodgman   Moderators   -  Reputation: 42456

Like
2Likes
Like

Posted 07 April 2014 - 04:38 AM

Once you give out references to their respective users what mechanism do you use to track usage in order to determine what should or shouldn't be cached? If that is even a relevant question for your framework.

Creating/destroying a GL buffer is equivalent to calling malloc/free, except that you're dealing with GPU-addressable RAM. So, at this level I don't do any advanced management or caching.
 
Instead, the next level of the architecture can do those things. You have objects that are composed of GL resources, such as buffers -- e.g. a model asset loaded from disk. The model asset can have one-to-one "value" ownership over the buffers/resources. When the asset is created/destroyed, the resources are created/destroyed.
 
A model instance can then have shared ownership (e.g. reference counting / shared_ptr / etc) of a model asset. A file system can deal with reading bytes from disk, and a model factory can deal with converting those streams of bytes into model assets. An asset cache can perform the caching, e.g. by having a map/dictionary member that associates asset names with model asset objects. When the reference count on a model asset is zero, it can be deleted from the asset cache (which will free up the GL resources). When creating a model instance, it can use the asset cache, file system and model factory to either fetch an existing model asset or create a new one.






Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS