Vulkan on Android 3 — Render Nothing with Lots of Hidden Details Part 6

Jin-Long Wu
4 min readOct 19, 2018

--

We have introduced all the Vulkan components simply to draw a colored background. Now let’s put them all together.

Renderer

Renderer class includes all drawing commands used by an application and it contains all the Vulkan components we have seen so far.

Scene is our base class which only contains a Renderer and a timer. Renderer will be subclassed and the timer in Update reports frames per second per two seconds.

EmptyScene inherits from Scene and assigns all its custom implementations to functions of EventLoop. We defer the Renderer creation to the APP_CMD_INIT_WINDOW event as ANativeWindow is invalid until then. On APP_CMD_CONFIG_CHANGED event ANativeWindow_getWidth and ANativeWindow_getHeight return the old width and height before orientation change so we swap them as the new resolution.

EmptySceneRenderer inherits Renderer and initialize and assembles all the Vulkan components to make our scene work properly.

How all the components are constructed and interact with each other is specified in the constructor, like layers and extensions to use and dependencies of objects, etc. ColorDestinationRenderPass is chosen for our simple purpose and resettable command pools are created since we are going to record and reset(implicitly) command buffers every frame and therefore, we should allocate command buffers from pools with VK_COMMAND_POOL_CREATE_TRANSIENT_BIT flag bit.

To get better performance, RenderImpl have multiple frames being rendered. The picture lists cases of a various number of concurrent frames.

The pipelined effect becomes minor when the application is CPU bottleneck. While the more application is GPU bottleneck, the more concurrent frame count we would choose to be. However, it is not always the right choice. The more concurrent frames, the more input delays. For instance, we may make keypress cause the model to play some animation. Though the keypress effects on the model apply to the offscreen frame with no delay it will be several frames later to be presented.

The actual workflow is a little bit of complex so we split it into the following parts.

  • We only deal with the case where queue families of presentation and graphics are identical which is also a much simpler and more performant way.
  • If device orientation changes, we must recreate the swapchain as widow surface and resolution become invalid.
  • vkWaitForFences waits for the fence at position currentFrameIndex to complete and vkResetFences resets it to unsignaled state.
    In the beginning n loop: fences are created at signaled state so vkWaitForFences does not wait at all. (n is swapchain->ConcurrentFramesCount())
    Others: It’ll wait until the corresponding command is completed.
  • currentFrameToImageindex keeps currentFrameIndex to imageIndex mappings.
    In the first loop: It is empty so the conditional expression is false.
    Others: Release the corresponding framebuffer since we are going to create a new one for the current frame and erase the mapping, then.
  • vkAcquireNextImageKHR acquire an image from the swapchain, along with the position(imageIndex) in swapchain at which the image resides, and signals imageAvailableSemaphores[currentFrameIndex] when it completes.
  • imageIndexToCurrentFrame keeps imageIndex to currentFrameIndex mappings.
    In the first loop: It is empty so the conditional expression is false.
    Others: A concept I didn’t notice is that the image acquired by vkAcquireNextImageKHR is not necessarily to be totally free. It may still be being rendered at the time being returned by vkAcquireNextImageKHR so we have to check the corresponding fence as well.
  • Once retrieving imageIndex we create VkFramebuffer of framebuffers[imageIndex] with swapchain->ImageViews()[imageIndex] and the render pass. Then we keep a mapping between currentFrameIndex and imageIndex. What we want to do is that we want to know what framebuffer to deal with when we find a particular fence is completed in the future frames.
  • Build a command buffer at the position imageIndex of commandBuffers.
  • Submit the command buffer created to the graphics queue. The submission will wait at stage VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT-_BIT for imageAvailableSemaphores[currentFrameIndex] to be signaled, and signal commandsCompleteSemaphores[currentFrameIndex] and multiFrameFences[currentFrameIndex] when it completes.
  • Wait for commandsCompleteSemaphores[currentFrameIndex] to be signaled and present the image to the screen.
  • Make currentFrameIndex cycle around the range [0, swapchain->ConcurrentFramesCount()] to update the semaphore and fences to use in the next loop.

Note

We might have seen some Vulkan sample create and record command buffer and framebuffer before render loop begins and that is totally fine as long as our rendering data does not change during loops, like only changes of transformation matrices. However, we want to present a more generic way and check what the performance will be.

--

--