Written by Max Röhrbein-Kling and Johannes Kuhlmann
This is the fifth and last part of our series of blog posts about our experience with bringing Galaxy on Fire 3 - Manticore to Vulkan. Thanks for accompanying us on the last four legs of our journey. We hope you will find the final part entertaining and useful as well.
Our posts follow this structure:
- Introduction and Fundamentals
- Handling Resources and Assets
- What We have Learned
- Vulkan on Android
- Stats & Summary (this post)
In case you have not (yet) read any of our preceding articles, here is a little summary of our initial situation once more: When we started working on the Android version of our game, we decided to use Vulkan for rendering (there is also an OpenGL ES version, but that is not of interest here). This series is about our experience with implementing a Vulkan renderer and getting it to work on different devices, in particular on those running Android. Consequently, we mainly talk about the interesting aspects of our own implementation and dive into the learnings we made along the way.
Please keep in mind that the focus of our Vulkan renderer was to ship a game. Thus, it is more pragmatic than perfect and we have mainly done what has worked for us. We have not used any fancy stuff like custom allocators, parallel command generation, reusing command buffers, etc. While not every solution we found will be as optimal for others as it was for us, we do still believe that our implementation is reasonably versatile and well done.
This last blog post in the series presents some numbers we got from comparing our implementations for different graphics APIs.
Before starting a new renderer implementation, it is usually difficult to say what effort it will require and what the pay-off in the end will be. Unfortunately, we cannot provide you with any direct help in that area. But at least, we can tell you how much we had to type and what benefits Vulkan brought us.
When we compare our different renderer implementations and count the lines of code for each implementation, we get the numbers shown in the chart below. Of course, this is only a very rough measure. Yet, it still makes sense in a way. Our implementations have feature-parity and it might give you a sense of how much effort you will have to invest. These numbers only cover the actual code specific to the graphics API; no generic code for culling, sorting, uniforms calculations, or anything like that.
If you compare the OpenGL/OpenGL ES implementation and the Vulkan implementation, you will see that Vulkan involves roughly 40% more typing - a fact that will most likely not come as a surprise to you. Vulkan is meant to be verbose and does a good job at making explicit whatever you want it to do. Instead of simply assuming something, it will have you specify what you actually want.
Next, let us take a look at the performance improvements we gained from using Vulkan. Vulkan is meant to have a lower overhead on the CPU side and that is what we are going to look at. What we are measuring here is only the time used for emitting our draw calls using OpenGL ES 3 and comparing that to Vulkan. We are looking at a specific scene where we come in at around 1,400 draw calls being emitted each frame. You can see the results for three different devices in the chart below.
In our use case, Vulkan is about three to four times faster at emitting draw calls. The differences vary a little across the different devices, ranging from 2.7 times faster on the Nexus 5X to 4.0 times faster on the Google Pixel XL. These results showed us that we could target the 5X as a low-spec device and theoretically ship it with 60 FPS support on the Pixel XL.
When we set out on our Vulkan adventure, we knew we would have to use APIs and SDKs not many developers had used before. And while this sounded bad at first, it turned out to be not too much of a hindrance after all. Actually, the implementation of the renderer was rather straight-forward. The existing documentations and tutorials are very good. All in all, there are only a few things that you do not already know from other graphics APIs (DescriptorSets, for example). For obvious reasons, these may be difficult to get your head wrapped around. But even that turned out to be quite doable.
In the end, implementing Vulkan support took longer than we had anticipated, but this was mainly due to some oversights on our side ("bugs") as well as the range of different devices and GPU vendors we wanted to cover. A selection of rather young drivers did not help either to get things done more quickly.
We realized during our work on the Vulkan implementation that pretty much everyone is new to this. There are not many other engine/game developers who have already worked with Vulkan. The device makers are looking for developers who can report back with experience on working with the new API and their drivers. The ecosystem on the web is not that big and often you cannot find the specific piece of information you are looking for.
Despite the issues caused by its novelty, supporting Vulkan in your game or engine is certainly worth it. It does deliver on the promises of increased performance, portability and having a clean API. The support for Vulkan is steadily increasing. There are more and more devices being released that run Vulkan. Khronos is doing a great job pushing this new standard for graphics. If you can get the contact information of the right persons, the device makers are also very helpful. They are eager to get their hands on any game running Vulkan and providing support wherever possible.
Here are some links that we used frequently during our implementation: