Comparing OpenGL ES and Vulkan

A quick overview of the contrasting design choices between OpenGL ES and Vulkan

When considering a big transition such as changing rendering API, it is important to understand how this change will affect the development process.

This section will focus on how developing with Vulkan compares to developing with OpenGL ES. The core takeaway of this comparison is how much more control a developer has over the GPU when developing with Vulkan. This has both advantages and disadvantages, for instance, an application can be optimised much more effectively because a developer can specify more precisely what they want the GPU to do, however the Vulkan driver does a lot less hand holding. This means Vulkan applications generally take longer to initially write and it is easier for things to go wrong.

One of the main points of difference between OpenGL ES and Vulkan is:

Vulkan Gives Explicit Control Over the Application

When using OpenGL ES, the graphics driver can often seem like a black box to developers - everything is hidden and abstracted away. This can be useful for new graphics developers who do not want to overwhelmed straight away, as they can simply instruct the API to do a specific task and the graphics driver will decide how to do it. However, for more experienced developers who are looking to get the best performance out of their application this disconnect from the hardware and lack of control limits how far optimisation can ultimately go.

Vulkan was designed from the beginning to approach this very differently, giving as much control over the hardware to the developer as possible. The driver layer is significantly slimmer and everything is exposed. The developer tells the driver exactly what they want to happen and the driver has to do minimal guesswork.

Here are three key examples of how this design philosophy is implemented in Vulkan and how this differs in OpenGL ES.

API Objects/States

In OpenGL ES, there is a single global render state that can be modified by the developer. Based on this state the driver has to determine on-the-fly what commands need to be sent to the GPU. This approach is very error prone, as the developer has to ensure, when issuing draw calls, that the render state is correct and exactly how they want it to be.

In Vulkan, the new way of doing this is to prepare beforehand all the API objects which describe what actions to take. This way the graphic driver does not have to decide anything and can simply translate the actions into machine code and execute it on the GPU. This approach is much less error prone as the API objects usually do not change during runtime. The only thing a developer has to make sure that the right API object is bound and that this object has the correct content.

Synchronisation

Synchronisation in software engineering can be a challenging problem. That is why in OpenGL ES it is mostly hidden from the developer and only coarse-grained synchronisation is possible in some places.

In some cases the driver creates synchronisation point between the CPU and the GPU, where one would wait for the other to finish a particular operation. Unfortunately, the application developer has no idea these points exist, meaning performance could be poor for no obvious reason.

In Vulkan, synchronisation is explicitly specified in a fine-grained manner.

This is supported by fences, semaphores, events and barriers:
  • Fences are used to signal to the CPU when a particular GPU operation has been completed. A very common example would be when an application needs to wait for a specific set of commands to finish before it can continue. A function called vkWaitForFences() is used to wait for any number of fences to signal.
  • Semaphores can be used to marshal ownership of shared data. They are used to signal between GPU operations, as Vulkan offers very few guarantees about the execution order of GPU commands.
  • Events provide fine-grained synchronisation primitives that can be signalled and waited upon by specific operations within a set of commands during execution.
  • Barriers provide execution and memory synchronisation between sets of commands.

This does make development more difficult, but it also enables the developer to do things that were previously impossible due to the black box driver model. It is now possible to do precise and efficient synchronisation, which again could help application performance.

Image Layout Transitions

Image layouts specify how the driver should organise pixels in memory. Due to the way GPUs work, storing pixels in a linear fashion does not give an optimal performance, and instead pixels are stored in a zig-zag pattern.

In OpenGL ES, the driver determines if an image layout transition is needed and always tried to keep images (textures) in the best possible layout for operations like sampling. This helps to improve performance but unfortunately, sometimes this resulted in unnecessary transitions.

In Vulkan, the developer can explicitly specify when and what kind of image layout transitions will occur. It is still the driver’s task to perform the transitions though. This means in a well-designed Vulkan application images will always be the most performant layout but unnecessary transitions are avoided.

The rest of this section will go over a few more of the important differences between the design of OpenGL ES and Vulkan.