Writing Portable OpenGL

14/03/2013


Writing Portable OpenGL

So you're making a game or other graphical application with OpenGL. You've heard that OpenGL is portable and that your application will be able to run on Linux, Mac and Windows without issues. That it could even be ported to a mobile phone if you restrict your usage to OpenGL ES.

Sounds easy doesn't it? Unfortunately the opposite is true - OpenGL is far from portable. Direct X is at least portable across Microsoft products. But lets not turn this into one of those rants, because I really like OpenGL, and I feel so sorry for the poor sods roped into writing graphics drivers. We acknowledge that they have a hard job, which leaves us, unfortunately, with a hard job too.

Writing portable OpenGL is more a frame of mind than a technical challenge. You need the ability to guess what may go wrong before you have tried it, to get into the heads of those poor driver writers, predict their bugs before they appear. I can promise you will find driver with bugs, segfaults, crashes, memory leaks and undefined behavior. Your shaders wont compile on anything than your own machine and your state machine will fight itself into submission. No two computers will run your program the same. There is only one truth in OpenGL and that isn't the specification - it is the experience I will now attempt to depart upon you.

We start from the beginning:



Error Checking

Error checking is the first step to writing portable and bug free OpenGL code. Never let errors pass silently. OpenGL provides the fairly rudimentary function glGetError to check for errors. If you can, call this after every OpenGL function. If you can't bare that idea then call it after blocks of calls, and if an error occurs then narrow down the bad function by placing checks inbetween.

OpenGL errors are very tempting to let slide because often the system will still work, displaying the correct visuals. This is just deception. It might work on your computer but for a computer somewhere else in the world, just waiting to run your application, it will be broken.

My advice is to exit hard upon error, reporting line number and errorous function. This forces you to write error free code as you go; otherwise your application will simply not get past rendering a frame.

You can find out about why certain functions call certain errors on their API webpage, but a bit of common sense helps with debugging too, as some functions can throw errors which circumstances are not listed.

OpenGL also provides glCheckFramebufferStatus to check for framebuffer errors. For shaders it provides glGetShaderiv and glGetProgramInfoLog to check for compile and link errors. Use these too!

If you let errors pass silently you may even end up with really cool effects which you can never reproduce again - like that image on the right. Do you want that?!

Never let errors pass silently, even if your application still appears to work.

ProTip: gDEBugger is an incredibly useful tool which will report OpenGL errors including some more complicated ones which pass in normal use. gDEBugger also provides essential tools for profiling and a whole bunch of other features which really let you get into the gritty details of your application. Learn to love it!



Extensions

OpenGL extensions are fundamentally a good way to continue evolution of the standard, but they do also provide a fantastic catch 22. Any renderer worth it's weight will require some number of OpenGL extensions to perform even basic rendering. This means that often displaying an error message to the user when they lack an extension may first require those specific extensions they are missing.

Having a application which requires a certain extension to perform is also a little annoying. If a user is missing this extension they probably just can't run your program, end of story. It isn't worth the development time to formulate alternatives.

Therefore some assumptions must be made about a users hardware and drivers. It seems usually fair to assume DX9ish functionality for PCs. If more extensions are required then check if the user has the extension before attempting to use it. Display a proper error message if you can. Resist the temptation to just crash.

The OpenGL capabilities database is a great resource for finding out how widely supported an extension is. It isn't the perfect data set but works pretty well for a mid-level 3d indie game. As a general rule of thumb any mid or high end card released after a major OpenGL version should support the majority of it's features. Sometimes targeting versions can be easier than specific cards.

Function Pointers

To use OpenGL extensions one must acquire function pointers to the appropriate functions at runtime using dynamic loading. Often people achieve this by using GLEW, but I would recommend doing it by hand.

The reasoning is that it allows you to have exact knowledge of which extensions you are using. It also allows you to display debug information when extensions fail to load.

ProTip: Many cards have extensions listed under the incorrect name. If at first you fail to find an extension make sure to try again but with EXT or ARB on the end. E.G: glGenShaders if not found look for glGenShadersEXT if not found look for glGenShadersARB if not found report completely missing.

ProTip: Extension pointers must be grabbed within the OpenGL context which uses them. If you switch or create a new context remember to reload them - or if you are working with two contexts at once, ensure they are not stored statically.

OS X

On OS X you may have trouble finding some of the common/important/modern extensions used with earlier versions of OpenGL. Apple have decided not to support many of these inside an OpenGL 2.1 context in an attempt to get more people to migrate toward later versions of OpenGL (3.2+).

What this means for you is that you are probably stuck between a rock and a hard place. Either consider dropping those unsupported extensions or upgrading wholesale to a newer version of OpenGL (potentially reducing your user base on the other side). The correct decision depends on your target audience. And if all this talk about versions is just confusing you then...good luck.



State Machine

It can be tempting to be very relaxed about the OpenGL state machine. For example "Obviously I will want depth testing in my application so I'll just glEnable(GL_DEPTH_TEST) at the start and forget about it".

While a single assumption like this may not be trouble, once they stack up it will come to bite you. Please don't take this approach. Always reset the state machine to some known state (I suggest the default state) once you have finished using it. Always match glEnable with glDisable and remember to unbind textures, buffers and shaders when you are done with them.

This is the only way to stay sane while using the state machine. Leaving lingering state will quickly lead to weird errors and unintended behaviour with no clear source. The driver writers can't account for every operation working in every possible state of the machine so often operations will crash when in the incorrect state rather than showing errors.

Treat the state machine like you would malloc and free, always try to ensure you know what state it is in for every line of the program.



Fixed Function

Don't mix shaders and fixed function.

If you require shaders in your application change all your code to use them correctly. Yes that even means removing all calls to glMatrixMode and passing in your view and projection matricies via uniforms. It means removing calls to glVertexPointer and using attributes instead. It means disabling lighting and doing all the lighting calculations in your shader. It means doing this for the UI too, not just the main viewport!

Other than the fact that using shaders and fixed function lighting is obviously incompatible, there are two very good other reasons for doing this.

The first is stylistic. Ensuring that all rendering is done using shaders, with uniforms and attributes means that your code is neater and conceptually cleaner. Having different functions for texture coordinates and position data makes assumptions about how to use your shaders which aren't needed. Programmable pipeline is just that - it should only make the minimum assumptions about how to be programmed.

The second is that OpenGL ES, OpenGL 3.0, 4.0 and any future OpenGL development does and will embrace programmable pipeline and is slowly fading out fixed-function. This means using programmable pipeline makes your code portable to new platforms and future proofs it against new standards and developments. Remeber that small neat code is portable code.



Shaders

Shaders are the single biggest headache when it comes to writing portable OpenGL. They are flawed from the ground up and so writing portable shaders is a constant and ongoing struggle. The issue is that shaders are compiled from a high level C type language (GLSL) by every computer running the application, at runtime, every time. Unlike DirectX, which uses low level compiled bytecode, OpenGL programs must distribute the C like shader files.

What this means is that shaders can compile vastly differently on different machines. Some compilers consider some things errors which others consider warnings. Some compilers support different features or specifics. There is no real way to verify or Lint your shaders to be confident they will compile.

Different compilers will also perform different optimisations (many not very well). This makes relying on some optimisations like loop unrolling and constant propagation dodgy. Not a good thing when certain operations are only valid on some cards whence a loop has been unrolled (like texture sampling).

The icing on the cake are the numerous bugs across various compilers (how much easier it would have been for the shader writers if they had just had to parse bytecode). All together this creates a mindfield of operations which can cause shaders not to compile on certain platforms.

The best way to solve this problem unfortunatly appears to be to try and limit your shaders to a subset of GLSL without any known issues. This subset is so small that it cannot always be used, so all we provide are things to beware of. Please contact me if you have more.

Use Versions

The first thing you might want to do is put #version 120 (or some other version) at the top of your shader file. Different OpenGL versions have quite extreme different syntax and semantics for shaders. The default feature set for a compiler is broad and varied. Putting some shader version at least gives you a chance of portability. This is much like declaring a standard for your C compilation. Target the minimum version which covers your required feature set and try to target the same version for all of your shaders.

For more info on the difference between versions check here.

Beware of Looping

It is hard to predict if the compiler will unroll loops. In general if you are doing less than 10 iterations it might be best to unroll by hand. Basic loops usually work okay, but texture sampling and indexing into data arrays can cause trouble. Putting function calls inside loops can also be troublesome as it increases the unlikelihood that the function will be inlined and/or the loop unrolled.

Beware Ternary Operator

The ternary operator (a = b ? x : y) has a number of known bugs on ATI drivers. In many cases it will cause incorrect behaviour or crash the compiler. Avoid it and just write the same logic with an if statement.

Beware of Functions

Compilers will often not inline functions. If you can, inline them yourself. If hand inlining is not viable ensure they are as basic as possible and don't contain loops, texture loopups or other things that can cause issues. If this is still unavoidable then cross your fingers.

Beware Shader Libraries

One of the cool ideas behind GLSL was that you could write mutiple source files and link them together after compilation, somewhat akin to C objects. This would allow for the creation of shader libraries, or common code in one location.

While this is quite a cool idea, and something DirectX is missing, unfortunately I've found many ATI cards will simply crash if you try to link together more than one vertex or fragment shader.

Although a nice feature, it just isn't usable when writing portable code, and that makes me sad.

Beware "const"

When defining constants I've come now to use #define. Constants defined using the preprocessor have a much better chance of propagation, meaning your shaders will run faster and you may manage to avoid expensive conditionals and looping.

Beware Complexity

Although its rare to encounter a card these days with an instruction limit, I have had compilers complain at shaders using too many instructions or being too complex in some way. Just be reasonable with your requirements. Don't use hundreds of uniforms or texture samplers, don't loop for a thousand iterations, you should be okay.

Beware of Nvidia

The Nvidia compiles are famous for being much more relaxed than ATI compilers and will often let slide things which are either invalid under ATI or even invalid under the spec. One classic example is float x = 1.0f; compared to float x = 1.0;. The first will compile on Nvidia cards but almost always throws errors on ATI cards. For reference the second version is correct.

Beware of Arrays

On some cards and operating systems arrays are broken. On Snow Leopard trying to declare them simply throws a compiler error. But many other cards have issues indexing into them, in particular combined with the many times aforementioned loop unrolling.

Beware of OpenGL ES

OpenGL ES has a separate shader specification to the standard one. This means lots of features of version 120 may not be available, and there are some other loops to jump through such as precision specifiers.



Oddities

See Also: Common Mistakes OpenGL

Please contact me if you find more and I'll add them to the list.

glGenerateMipMaps

Just don't call it. This function can basically be considered undefined behaviour on ATI cards. Mostly it simply doesn't work, filling the texture with black, but I've also seen it crash the program, crash the drivers or even crash the GPU. People online will suggest that you must first call glEnable(GL_TEXTURE2D) or other tricks, but the truth is this function simply will never work on a bunch of machines. Preprocess your MipMaps or emulate the behaviour with render to texture and shaders. Don't call this function!