Advertisement

How to handle OpenGL resources in c++?

Started by March 01, 2022 06:51 PM
8 comments, last by hplus0603 2 years, 9 months ago

Hi everyone, new to the forum so not sure if this is the correct place to ask this question.

I'm writing a simple OpenGL application.

I am trying to create a base class for all the OpenGL resources used in the application so that inheriting from it they are forced to be in sync with the graphics API (be valid/initialized before usage and don't leak memory on the gpu).

Does it make sense to try to force this behavior via inheritance? are there better ways?

I came up with this:

class OGLResource
{

public:
   OGLResource()
       : _id(0) {};

   // No Copy Constructor/Assignment allowed
   OGLResource(const OGLResource&) = delete;
   OGLResource& operator=(const OGLResource&) = delete;

   OGLResource(OGLResource&& other) noexcept
   {
       _id = other._id;
       other._id = 0;
   };

   OGLResource& operator=(OGLResource&& other) noexcept
   {
       if (this != &other)
       {
           FreeResources();
           NotifyDestruction();
           OGLUtils::CheckOGLErrors();

           _id = other._id;
           other._id = 0;
       }
       return *this;
   };

   ~OGLResource()
   {
       if(_id)
           throw "Trying to destroy an OpenGL resource without deleting it first";
   }
   
   void Create()
   {
       _id = InitResources();
       OGLUtils::CheckOGLErrors();

       NotifyCreation();
   }

   void Destroy()
   {
       FreeResources();
       NotifyDestruction();
       OGLUtils::CheckOGLErrors();

       _id = 0;
   }

   unsigned int ID()
   {
       if (!_id)
           throw "Trying to use an uninitialized/deleted OpenGL resource";

       return _id;
   }

protected:
   virtual unsigned int InitResources() = 0;
   virtual void FreeResources() = 0;
   virtual std::string ResourceType() { return "UNKNOWN"; }
   void NotifyCreation() { std::cout << std::endl << "Creating OGL Object: " + ResourceType() + "_" + std::to_string(ID()); }
   void NotifyDestruction() { std::cout << std::endl << "Destroying OGL Object: " + ResourceType() + "_" + std::to_string(ID()); }

private:
   unsigned int _id;
};

The idea is that subclasses must define the behavior of Init/FreeResources(), the base class will be responsible for calling them, and since the resource's ID is private and accessible only via a method in the base class I can be sure the resource is correctly initialized before use (as long as InitResources() is correctly implemented).

Same for the destruction part.

A subclass example:

class OGLVertexShader : public OGLResource
{
private:
   std::string _source;

protected:
   unsigned int  InitResources() override { return glCreateShader(GL_VERTEX_SHADER); }
   void FreeResources() override { glDeleteShader(OGLResource::ID()); }
   std::string ResourceType() override { return "VERTEX_SHADER"; }

public:
   OGLVertexShader(std::string source) : _source{ source }
   {
       OGLResource::Create();

       const char* vsc = source.data();
       glShaderSource(OGLResource::ID(), 1, &vsc, NULL);
       glCompileShader(OGLResource::ID());
       OGLUtils::CheckCompileErrors(OGLResource::ID(), ResourceType());
       OGLUtils::CheckOGLErrors();
   }

   OGLVertexShader(OGLVertexShader&& other) noexcept : OGLResource(std::move(other))
   {
       _source = std::move(other._source);
   };

   OGLVertexShader& operator=(OGLVertexShader&& other) noexcept
   {
       if (this != &other)
       {
           OGLResource::operator=(std::move(other));
           _source = std::move(other._source);
       }
       return *this;
   };

   ~OGLVertexShader()
   {
       OGLResource::Destroy();
   }

};

Does this make any sense? Could you point me in the right direction?

It looks like in your application a subclass could just implement whatever they want in InitResources and FreeResources. Since you know the possible functions to create/destroy resources in OpenGL like (glCreateShader) I would say for this simple case it makes more sense to take in an enum that says, for example, that this is a shader (and an additional field that it is a vertex shader). Then the base class would see that enum and know that it would call glCreateShader on creation and glDeleteShader upon destruction. Basically each enum would define a pair of create/delete functions. However, this solution becomes more tricky if you want to add more resources to a subclass. Then you would need some sort of way to correlate several ids to which resource is being referred to in the subclass. But I don't see any obvious other way to work around that if you allow the subclasses to handle it themselves.

Something else to consider would be a domain specific language that forced the user to do certain things. But that would be a big project to implement and it might not be that user-friendly having to learn some oddball language for this specific use case.

When I've thought of similar problems my realization has been that you cannot make mistakes impossible. But you can make them very hard for a developer who is interested in writing correct code. You do that basically by coming up with rules that says “if you do x, you also have to do y”.

Advertisement

perry_blueberry said:
I would say for this simple case it makes more sense to take in an enum that says, for example, that this is a shader (and an additional field that it is a vertex shader).

I'm definitely going to implement this mechanism, thanks for the suggestion (I'm still trying to figure out a nice way to convert the enum's value to const char* so I could also delete that virtual ResourceType(), suggestions are welcome).

perry_blueberry said:
However, this solution becomes more tricky if you want to add more resources to a subclass. Then you would need some sort of way to correlate several ids to which resource is being referred to in the subclass.

I designed this base class to be the common underlying representation of everything that comes from a glCreate…() so a class that needs to handle more than one should use more than one GLResource's subclass.

How much graphics code have you written before? What specific problems are you trying to solve here?

It's fine to abstract away the graphics system, especially for large projects and across teams. But by doing so you create yet another library boundary, yet another source of bugs and defects to fix, yet another layer to slow you down and cause problems.

If you're doing it to fix an actual problem, or to enforce rules that need to be in your code then go for it. Sources and sinks, using object lifecycles to enforce cleanup, those are fine reasons in specific cases.

If you're doing it to create a structural pattern in your project that will ultimately simplify usage then go for it. Factory patterns and such can make code more approachable and easier to use, at the cost of internal complexity. If it gets reused frequently there is a net benefit, but if everything becomes a once-off it's just a cost, so you'll need to understand what you're doing before going too far down the road.

General advice is to just write solutions to the problems you have. Don't make things into libraries or structural changes until you encounter the same thing multiple times, then merge them, that's your new function, class, or type.

@frob

frob said:
How much graphics code have you written before? What specific problems are you trying to solve here?

Not much really, and not in c++ (as strange as it may sound I work as a developer for a C# graphics library … so a complete c++ beginner). This is a hobby-project with a small scope, so I completely agree with all you've said and I'm trying my best to not over-complicate things. However, writing everything from scratch I'm mostly concerned about two things:

  • Graphics resources management (no leaks, no invalid-state objects)
  • Application - Rendering pipeline state synchronization (current states, currently active/bound resources …)

I decided the latter could be somewhat less important for such a small project, I hope a simple and well-designed drawing routine shouldn't require any form of complex and fool-proof pipeline states management.

So I just wanted to lay down a strong foundation for the most basic building blocks of the application (like resource abstractions for example). The problems I'm trying to solve are on the lines of “I want to make sure resources are properly deleted when out of scope without duplicating code (or at least as little at possible)”, “a Ogl texture's Id can't be shared”, …

Am I missing something? Is this something that could/should be avoided in a small project (I know there's no definitive answer, just collecting opinions).

There are two common options.

Pick up a library that provides the safety, the platform abstraction, and is already debugged. Lots of libraries and engines, from SDL to coco2d-x to Unity and Unreal. They do what you described, taking care of the complexity of the pipeline and the resource management. Even better, they have centuries of combined development time invested in making easy to use, safe systems. Some have had enough experts working on them for enough decades that they're at millennia of combined brainpower all to build a simple, safe, reliable tool.

Or get really good at using sharp, potentially dangerous tools, and become an expert that can contribute to building the things used by others. Learn Vulkan rather than OGL, unless you are just looking for a stepping stone. Become a surgeon or master carver or whatever other parallel fits. Yes, there's a risk that using the tools wrong will cause problems, so master the tools and their usage.

Both are good paths, but know which you are on. Safety scissors are great for most uses, but when you need a scalpel or a chainsaw, it is your own use and skill that determines safety or danger.

Advertisement

@frob I think that learning about the user-facing APIs in frameworks/engines is a great gateway for getting into lower level detail implementations. Then you will have a better understanding of what the tech can be used for. I also think that learning about the lower level details is great for an application developer. Then you have an understanding for why the API looks the way it does and what is possible/not possible/difficult to achieve with the technology. You will also have an easier time moving between frameworks/engines since you know the fundamentals and the important terms to google for. In my view, it only makes sense to choose a path when you have a fairly good grasp of both sides (unless you are short on time and just want to finish a game in which case going for only using an engine makes sense).

@leone9 Similarly to what I've argued in the paragraph above, if you want to create a really good API for this you should use the low level APIs up to the point that you realize what things you find tedious and error-prone. You should also take inspiration from frameworks/engines that are already out there. Obviously that's a lot of work and it will take away time from the actual fun parts which, to me at least, would be implementing the rendering algorithms.

Take a look at https://www.khronos.org/opengl/wiki/Common_Mistakes#The_Object_Oriented_Language_Problem

@clivi Thanks a lot!!! Don't know how I missed it

If you can stay with functions and data instances, then that's usually the best way to go.

If you really do need a C++-level class-wrapper of your different objects, then you could do worse than looking at how D3D11 (or even D3D9) handles this: It uses reference counting, and the “bigger” objects (ID3D11Device) serve as factories for the “smaller” objects (ID3D11Texture2D) and you use CComPtr<> or your other favorite reference-counted pointer to manage lifetimes. Also, the smaller objects keep a reference to the bigger ones, so the big one doesn't go away until all the little ones are dead.

This would, in turn, let you enforce that the particular GL context is active when using (or creating) a sub-resource; you'd have some thread-safe global state for which context is active in the current thread, and bind the appropriate context from the bigger object if it isn't current. Note that this would be slow to do for every bind/render call, so typically you'll end up with a two-pass “issue all the stuff” “render” solution, where all the “issue” just puts objects in lists-of-things-to-do, and the device then walks the big list of things-to-render while it has the right context active.

If you want to do Vulkan or D3D12 or, to some extent, Metal, the rules are different. Not talking about that here.

enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement