Written by Max Röhrbein-Kling and Johannes Kuhlmann
Having frequently posted here on Gamasutra over the past couple of weeks, we have now reached part four of our series of blogs about our experience with bringing Galaxy on Fire 3 - Manticore to Vulkan.
Our posts follow this structure:
In case you have not read our previous posts yet, here is our disclaimer 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 own experiences with implementing a Vulkan renderer and getting it to work on different devices, in particular on Android devices. So, we are mainly going to talk about the interesting aspects of our implementation and then dive into what we learned along the way.
First and foremost, the focus of our Vulkan renderer was to ship a game. That means it is more pragmatic than perfect and we have mainly done what has worked for us. We are not using any fancy stuff like custom allocators, parallel command generation, reusing command buffers, etc., etc. We do believe, though, that our implementation is still reasonably versatile and well done.
This fourth post covers the problems we encountered that are specific to Vulkan on Android.
While proper Vulkan support was only added in Android 7 (or Android N, or Nougat, or API level 24), there are a few devices out there that already had Vulkan support on Android 6 (or Android M, or Marshmallow, or API level 23). Some of these devices are, for example, the Samsung Galaxy S7 (Edge) and Nvidia Shield Tablet.
It is possible to support such devices with a bit of extra effort. The problem is that you cannot depend on the Vulkan header and library being part of the Android SDK/NDK. Instead, you have to provide your own header file and load the library dynamically at runtime. Google's Vulkan samples have a convenient wrapper for this that spares you all the typing.
Note, however, that just because a given device can support Vulkan on Android 6, that does not mean all devices of that type support Vulkan on Android 6. The Vulkan support can vary with minor updates and even the extend of the support may vary. For example, we have found one implementation reporting its API version as being 0.0.1 and not supporting the swapchain extension or validation layers at all. Another device told us it did not want to work with us by reporting the
So, make sure to check that the Vulkan implementation that a device provides is actually one you can work with and that it supports all the features you need. Otherwise, fail gracefully.
A challenge that is rather unique to Android is that you have to handle the case when your application is sent to the background. The player can pause and resume the application at pretty much any time by pressing the home button, for example. You have to handle this on the CPU side in order to not eat up all CPU cycles in the background. This would annoy the user by slowing down the phone and draining the battery.
Unfortunately, we could not find any documentation on what you have to do for Vulkan when this happens. Therefore, we had to figure this out ourselves by experimenting. We already knew that with OpenGL ES you have to be careful to not destroy your complete context (which means you will have to recreate all your textures and buffers, and so on). If you are careful, you can get away with only having your surface destroyed and recreated.
It is actually the same case with Vulkan. When the application is paused, your surface is destroyed. When it is resumed, you get a new surface which you will have to render into from then on. This is all a bit tricky as you have to be careful with synchronization and timing. Do not destroy the surface while still rendering into it, for example.
Destroying and recreating the surface also means that you will have to recreate your swapchain and its framebuffers. When all of that is done, you should have a smooth and quick pause and resume cycle.
Note, however, that you cannot pass in the old swapchain into
vkCreateSwapchainKHR(). You have to destroy it independently and create a completely new one. We assume this is due to the old swapchain already being invalid because the surface was destroyed.
We started with implementing the Vulkan renderer on Windows. There, RenderDoc had our back when our rendered frames looked wrong and the validation layers did not provide enough insights.
For Android development, there are various tools aiming to satisfy your Vulkan debugging needs. RenderDoc also supports capturing from an Android app. But it is harder to set up. The major GPU vendors also provide their own tools:
Google is currently working on extracting the graphics debugger from Android Studio into a standalone tool.
Sadly, when we really wanted a frame capture, it almost always was on a device where the validation layers were not working. And all of the tools we tried require you to load a special validation layer for the capture. As a result, none of these tools provided a great deal of help. We therefore either tried to reproduce the problem on Windows or took a more manual approach by selectively disabling certain kinds of draw calls to track down the problem.
Implementing a Vulkan renderer is already a complex undertaking in itself. But from our point view, there are even more pitfalls on Android. This is mainly caused by two factors: First, the absence of simple-to-use tools. And second, the presence of additional difficulties such as the application lifecycle and different versions of Android.
Interestingly, different GPUs from the same vendor often have the same manifestations of bugs. So, if you want to reproduce a problem, make sure to use a device with the exact same GPU or at least with one from the same vendor. This can be difficult in some cases as, for example, Samsung likes to ship different GPUs in different regions of the world.
In the next (and final) post, we will talk about select statistics and numbers that we collected from our Vulkan implementation.