4

It is generally understood in software engineering that global state is bad. However, OpenGL has been designed very much embracing the concept of global state. many of the things you modify will affect everything in your program (active shading program, uniforms, active buffer, glDepthTest...)

Because of this, I ended up developing a philosophy of "do not fight the global state, embrace it". When I design OpenGL programs, I do so assuming that each function and object must set the state immediately prior to being rendered, and that nothing can be assumed prior to issuing a render command. if you want a specific depth test, set it, if you want a specific shading program, set it, if you want a given uniform value, set it. Obviously this is mostly a heuristic as setting everything every time on any function would be a horrible design.

I however find myself working with some developers that are fully against global state (for good reasons). Their design revolves around defining a small set of objects as singletons and then passing values down as parameters as much as possible. The end result is that sometimes there's 5 or more function calls between the intention (e.g "rendering") and the action (e.g calling glDrawArrays).

For example sometimes it seems, that the best place to me, in terms of easiness, clarity and refactorability, to perform an action, would violate this principle of avoiding global state by calling a function of a global object to change the value of a variable. Rather than trying to pass that variable down, which has lead to some discussions about the best design.

The question is, is it a good idea to folow the general heuristic and avoid global state as much as possible when working with OpenGL, or is this an exception to the rule and due to its design it can be considered that embracing the global state may be better in this one instance?

  • 5
    Stateful singletons do not avoid global state, they are global state. – Goyo Oct 31 '18 at 19:36
  • I know, which is why only a small set of objects are defined as singletons, to handle what could not be fully reduced to parameter passing, and the rest is just passed aroung along functions and objects. – Makogan Oct 31 '18 at 20:09
  • I guess the function call mentioned in the question can be reduced to parameter passing, otherwise you wouldn't be discussing which one is the best design. – Goyo Oct 31 '18 at 20:15
  • We are not talking about a single function, but rather design heuristics when using a really large and widely used api. – Makogan Oct 31 '18 at 20:21
  • I would consider anti-corruption layer – gnat Oct 31 '18 at 21:01
6

OpenGL didn't 'embrace' global state so much as it was one of few sensible solutions to a technical problem. It's expensive to communicate with a video card. Flipping bits in video memory is considerably more expensive than flipping bits in ordinary memory. As an example it's so significant that later versions of OpenGL got rid of immediate mode, because setting up buffers (i.e. retained mode), then rendering from those buffers was so dramatically more efficient it was worth the more complicated setup. It would be impractical to "pass" the entire configuration of the rendering system on every frame because of how much communication would have to take place.

In contrast, in many web server situations, it's not uncommon to pass around a massively complex data structure, encompassing the entire state of the request and whatnot, because all you're really actually passing is an 8 byte pointer.

So, the problem with global state is it can be hard to know who's modifying it and when, and so it can be tricky to reason what it's state is. So, the thing to do is to setup your application to only modify that state in well defined ways, with well defined processes, so it is easy to reason what is happening.

Just compare the rendering state with e.g. a database driven application. In many respects, the database is a big global state. However, the interface to that DB, and often the DB itself, enforces when and how it is modified, and enforces what the data looks like inside it.

One simple way you can implement something similar is to have an interface which keeps track within your application what the state of the renderer is. You know you need the renderer setup a particular way, so you can consult your interface which would know, without needing to ask the graphics card. Likewise, it can also make the decision what has to change to get the renderer into the right state in the most efficient manner.

Building 3d graphics engines is an enormously complicated and broad subject, so I can't give precise pointers or code samples. But the basic gist is you need to come up with a way to sensibly merge the global state of the 3d card with a limited scope programming model on the CPU side, rather than having your application use all global variables.

In your specific example, when you say the other devs are defining several singletons and passing them around I get highly suspicious, but without seeing more of what's going on I can't really comment on that in more detail.

  • Why do you get highly suspicious? – Makogan Oct 31 '18 at 20:20
  • 4
    @Makogan because singletons are usually dodgy except for some specific circumstances. If you are using singletons to bridge the cpu -> video divide I'm doubtful they're being used sensibly. – whatsisname Oct 31 '18 at 20:22
  • 2
    @Makogan: If I may add something: State changes are costly (although different things have different costs), so what you may want to minimize them by organizing your data so that it's processed in batches (e.g. by sorting, or keeping the data in separate arrays by type), which should reduce the frequency of state changes and cache misses. While I'm all for OOP, this is the case where it's better to design the system in a way that avoids a lot of pointer indirection, and prefers coarse-grained objects and a more procedural, data-oriented design for the rendering code. – Filip Milovanović Nov 1 '18 at 13:36
0

Because of this, I ended up developing a philosophy of "do not fight the global state, embrace it". When I design OpenGL programs, I do so assuming that each function and object must set the state immediately prior to being rendered, and that nothing can be assumed prior to issuing a render command

Actually I interpret the idea of "embracing global state" the totally opposite way from you. What you're trying to do, I believe, is minimize side effects, work towards referential transparency, loosen temporal coupling, something of this sort. Very crudely it's like you want to make sure when your colleague introduces some new type of thing to render, it doesn't mess with the global state in a way that causes other things to start rendering incorrectly (that's usually the sort of tripping points and bugs associated with OpenGL state mismanagement).

The way I interpret it is closer to this if I can borrow from Filip Milovanović's comment:

[...] State changes are costly (although different things have different costs), so what you may want to minimize them by organizing your data so that it's processed in batches (e.g. by sorting, or keeping the data in separate arrays by type) [...]

To put it another way, I'm trying to move as far away as I can from managing GL state as locally as the function to render a single object, and instead work more towards the concept of setting states in the analogical notion of a "rendering pass". A given pass might use a particular shader and set of textures and some particular states for depth testing, and then I try to render as many relevant objects as I can within that particular rendering pass which can all utilize the same state.

That does require a bit more thought and collaboration upfront about the rendering needs of particular objects you anticipate upfront as well as possibly data structures which organize and group such things based on identical state requirements. But if you can structure your software like this, then I think that leads to the ideal balance of both a performant solution and minimal state (mis)management. There's also considerably less global state to manage in the first place the further you move away from the fixed pipeline (if you haven't already).

[...] find myself working with some developers that are fully against global state (for good reasons). Their design revolves around defining a small set of objects as singletons and then passing values down as parameters as much as possible.

The first couple of statements don't compute for me. Any singletons with state are global state, so unless they're stateless and can't be mutated, I don't see why on earth anyone against global state would happily introduce more singletons into the mix. Parameter passing and DI makes sense to me as an alternative to globals.

Rather than trying to pass that variable down, which has lead to some discussions about the best design.

Passing things down would generally be more ideal from a clean coding standpoint if it was a strict alternative to globals. But with OpenGL you've got the performance issues of mutating the state combined with the fact that it's inevitably global, so there's at least quite a bit of redundancy there in terms of work if you're going to pass parameters down the callstack only to ultimately use them to set global states prior to rendering. It might still be a reasonable thing if correctness is your utmost concern regardless of the design's overall cost to performance, though I don't understand why singletons are involved here.

If you're working in a very sub-optimal scenario like the work to arrive at the more idealized solution mentioned before is too difficult, the collaboration between the team is rather loose/poor, and combined with punishing deadlines (the combination of which I'm unfortunately rather used to), then there is a solution I encountered before which wasn't too bad in practice.

I worked in a codebase almost a couple of decades ago with a huge legacy, pure fixed-pipeline GL code, and the OpenGL code was everywhere: there were OpenGL calls in the generalized utility libraries, in GUI callbacks, in the material library, in the geometry library, all over the place. I don't even know how the original developers managed to get anything to render correctly given the chaos except that maybe they just kept patching bugs and fiddling with states until it coincidentally happened to work. But there introducing any new rendering code was fraught with landmines, and trying to fudge the massive codebase towards a more idealized solution seemed borderline hopeless, and so my colleagues arrived at a fairly practical solution that relied on RAII with C++.

They did things like this:

struct GlLineWidth: noncopyble
{
    GlLineWidth(float new_width) {glLineWidth(new_width);}
    ~GlLineWidth()               {glLineWidth(1.0f);}
};

Then they replaced calls like:

glLineWidth(3.0f);

... with this:

GlLineWidth line_width(3.0f);

You could also alternatively use glGet* and capture the state in the constructor and restore the previous state in the destructor (that would probably be more robust, especially in nested cases), but I think my colleagues chose to design it that way as a way to try to force the system to constantly be in some default state (ex: always being able to expect the line width to be 1 for wireframe rendering unless very temporarily set otherwise).

And it's not the prettiest but once that object went out of scope, the line width would be set back to 1. And they did that for all the relevant GL states the fixed pipeline was fiddling with, and that made the codebase a lot more predictable in terms of what sort of state you could expect when writing new code and also having any state changes it made be reverted when exiting the function (either normally or as the result of an exception).

That's a sub-optimal solution for a worst-case scenario which immediately does away with the notion of minimizing state changes, but it did actually improve the predictability of the code (and its changes) considerably, reduced the amount of human error, and it might still involve less state changes than trying to pass a boatload of parameters (or an object indicating many of them) which ultimately get used to change the global state.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.