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.