The glium library

Initialization and context management

Initialization

One of the biggest challenges when writing raw OpenGL is initializating the context. The OpenGL API supposes that a context has previously been made “current” and doesn’t mention anything about initialization. To initialize the context you have to use another API instead, like WGL, GLX or EGL.

let display = glutin::WindowBuilder::new().build_glium().unwrap();

Compatibility

During the creation phase, glium will parse the version and list of OpenGL extensions provided by the backend.

The glium teapot example running on a Raspberry Pi

Context management

The OpenGL API was created at times where everything was single-threaded, and one of its design decisions is that OpenGL contexts have to be binded to a thread before they can be used. This leads to the fact that even today most OpenGL applications are single threaded.

  • None of the structs that represent OpenGL objects implement the Send trait, so they can never leave the current thread.
  • Whenever you call a function, glium calls wglGetCurrentContext/glXGetCurrentContext or equivalent to make sure that the context is still the current one. If it’s not the case, it calls MakeCurrent.
As shown here, around 0.08% of the CPU usage comes from the calls to GetCurrentContext.

Buffers

The main buffer handling struct is Buffer. In order to enforce safety and correctness, buffers must have a fixed size and have a template parameter indicating their content. You don’t just manipulate a Buffer, but a Buffer<[u8]> or a Buffer<Foo> for example. Common operations such as reading or writing are very easy to do with methods such as read, map or write.

The “untextured objects” example of AZDO: drawing 64x64x64 individual moving objects. The glium version runs at approximately the same speed as the original pure OpenGL example.

Uniform buffers and SSBOs

One of the characteristics of buffers is that you can read their content (and even modify them) from inside your shaders.

Textures and framebuffer objects

Handling textures is one of the trickiest part of OpenGL.

Reading the content of a texture. This can’t be easier.

sRGB

However there is still something that you can get wrong: sRGB. For historical reasons, the data format of screens and pictures are actually not in linear RGB but in the sRGB format. This is reflected in glium by a difference between sRGB textures and non-sRGB textures.

On the left: glium’s hello triangle. On the right: an OpenGL hello triangle without correct sRGB handling enabled.

Render to texture

One of the most useful feature of OpenGL is render-to-texture, which consists in drawing to a texture instead of drawing to the window. This is where framebuffer objects come into play.

  • Glium framebuffers hold a borrow of their attachments, so it would be too annoying to keep them alive between frames. Instead you can just recreate the same framebuffer at each frame without suffering from a performance issue.
  • Some operations such as switching between windowed and fullscreen mode requires a context rebuild by creating a new context that shares lists with the old context. In this situation, all framebuffer objects, vertex array objects, program pipelines and transform feedback objects become invalid. All these objects are handled internally by glium in order to avoid issues related to this.
All glium tests use render to texture to avoid the pixel ownership test.

Drawing, uniforms, and the state machine

The state machine

One of the major problems of OpenGL today is that it is a giant state machine. In other words, its functions have a different behavior depending on the functions you called previously.

Example of the list of OpenGL function calls during an entire frame. There is no garbage.

Drawing

One example of this stateless API comes when drawing. Drawing is usually a very complex process, involving lots of steps that are very confusing and error-prone even for experts. Instead, glium provides one universal function:

draw(vertex_source, index_source, program, uniforms, parameters)
-> Result<(), DrawError>
  • Reuse or create a new vertex array object for the given vertex source. If vertex array objects are not supported by the OpenGL implementation (which is the case of OpenGL ES 2), then it will call glVertexAttribPointer & glEnableVertexAttribArray.
  • Check whether the vertex source matches the attributes expected by the program, returning an error if some are missing.
  • Call glMemoryFence if necessary for the various buffers used by the command.
  • Synchronize the parameters with the OpenGL state machine, returning an error if some of them are not supported. It also does some logic-related tests, such as returning an error if you enable depth testing without having a depth buffer available (something that is normally not an error according to the OpenGL specs).
  • Bind the program and its uniform values. Uniform values are cached so that they aren’t updated if it’s not necessary. For textures, glium will try to use all texture slots (one different texture per slot), and will only change the texture bindings if not enough slots are available.
  • Create or reuse the framebuffer object.
  • Check the layout of uniform buffers and SSBOs, returning an error if they don’t match.
  • Create sync fences for persistent mapped buffers that are used by this call.
  • Call one of the eight possible glDraw* functions. If you draw with slices of vertex buffers, glium will try to use the BaseVertex variants but will fall back to creating/reusing a VAO if they are not supported.

Safety

Prior to OpenGL 4 and OpenGL ES 3.1, everything was memory safe. All operations had the same effect as if they were executed immediately. Since OpenGL 4 and OpenGL ES 3.1, this is no longer totally the case and you in some situations you have to manually handle synchronization, flushing the cache, and passing around raw pointers that can crash the program if misused.

OpenGL errors

But glium takes some liberties with the definition of “safe” and is in fact stricter. In addition to memory safety, glium also enforces the lack of OpenGL errors.

GL_KHR_no_error

Performing runtime checks to avoid OpenGL errors may seem inefficient, after all the driver already performs the exact same checks. The exact cost of using glium compared to raw OpenGL functions remains to be calculated, but I’m confident that it is low. Even if you find the cost too high, glium has an ace up its sleeve: the GL_KHR_no_error and EGL_KHR_create_context_no_error extensions.

The status of glium

Glium currently has 1,968 commits, 28,686 lines of code in the src directory, 6,964 lines of code in the tests directory, and a build script of around 1.5kloc. For some comparison: hyper has around 13kloc, cargo around 15kloc, image around 12kloc, serde around 16kloc, rustc around 360kloc, and servo around 204kloc.

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store