Register now for Android Dev Summit 2019!

Achieve proper frame pacing

When running a game, it's important to maintain synchronization between the game engine's rendering process and the platform's display pipeline. This alignment—known as frame pacing—allows games to maintain a steady framerate and low input latency.

The following examples demonstrate the poor user experience that can occur if you don't follow this best practice:

  • Inconsistent frame times. If a game running at 30 FPS attempts to render on a device that natively supports 60 FPS, the game render loop doesn't realize that a repeated frame remains on the screen for an extra 16 milliseconds. This disconnect usually creates substantial inconsistency in frame times, such as: 49 milliseconds, 16 milliseconds, 33 milliseconds. Overly complex scenes further compound this problem, as they cause missed frames to occur.
  • Over-stuffing internal buffers. Some games submit frames as quickly as the rendering API allows. When you apply this approach to Android's tearing-resistant display pipeline, your game often "runs ahead" of the display pipeline by continuing to fill the internal buffers. This situation, known as over-stuffing, causes the render API to stall, often outside of a call to glClear(). The renderer process doesn't realize what's going on, so framerame inconsistency and input latency both get worse.

Android provides a support library, Android Frame Pacing, that helps you execute proper frame pacing in your game.

This document describes how to integrate Android Frame Pacing into your toolchain, how to use the functions that the library provides, and how to verify that your frame pacing has improved.

Integrate Android Frame Pacing into your renderer

Android Frame Pacing is available as a static library from the Android Open Source Project.

The core interface resides in one of the following header files, depending on the API that your renderer uses:

  • OpenGL ES: /include/swappy/swappy.h
  • Vulkan: /include/swappyVk/swappyVk.h

In most cases, the header file contains all the functions you need to integrate the library into your engine. More in-depth explanations of these functions appear in the corresponding OpenGL ES and Vulkan sections.

Update your build settings

After you've downloaded the library onto your machine and have checked it into your source control system, make the following changes to your project's build settings:

  • In your compiler include paths, add your-games-sdk-path/include.
  • For the C interface, include one of the following:
    • swappy/swappy.h for integration with OpenGL ES.
    • swappy/swappyVk.h for integration with Vulkan.
  • In your linker library paths, add a path of the form architecture_sdk-version_ndk-version_stl-version. For example: arm64-v8a_SDK28_NDKr17_c++_static.

    For instructions on generating library output files, see the README document in the Android Frame Pacing API source.

  • When running your linker command, add one of the following flags:
    • -lswappy if you're using a shared library.
    • -lgamesdk if you're using a static library.

Add Android Frame Pacing functions for OpenGL ES renderers

Use the following functions to use Android Frame Pacing with a rendering engine that uses the OpenGL ES API.

Initialize and destroy

Use the following functions to initialize and destroy an instance of Android Frame Pacing, respectively:

  • void Swappy_init(JNIEnv *env, jobject jactivity);
  • void Swappy_destroy();

In general, you should call Swappy_init() as early as possible during the engine startup sequence, and you should call Swappy_destroy() when the game is exiting. You shouldn't need to call these methods at any other time.

Configure swap interval and refresh period

Use the following functions to configure an instance of Android Frame Pacing:

  • void Swappy_setSwapIntervalNS(uint64_t swap_ns);
  • void Swappy_setRefreshPeriod(uint64_t period_ns);
  • void Swappy_setUseAffinity(bool tf);

When calling Swappy_setSwapIntervalNS(), pass in the duration that a frame should be presented. In most cases, you can use one of the following constants: SWAPPY_SWAP_60FPS, SWAPPY_SWAP_30FPS, or SWAPPY_SWAP_20FPS.

In general, you should call these methods directly after a call to Swappy_init(). However, you might also need to call these methods at other times during your game's execution.

Perform per-frame swap

During each rendering frame, call bool Swappy_swap(EGLDisplay display, EGLSurface surface);. This method wraps the eglSwapBuffers() method from Open GL ES, so you should replace all instances of eglSwapBuffers() in your game with Swappy_swap().

Utility functions

The following method checks whether Android Frame Pacing is enabled:

  • bool Swappy_isEnabled();

It's possible that an instance of Android Frame Pacing isn't able to initialize itself for any of the following reasons:

  • The necessary EGL functions are missing on the device.
  • The system has set the swappy.disable property.

In either of these situations, Swappy_isEnabled() returns false, and it's best for you to implement an alternative frame-pacing strategy.

Add Android Frame Pacing functions for Vulkan renderers

Use the following functions to use Android Frame Pacing with a rendering engine that uses the Vulkan API.

Identify required extensions for creation

To gather the set of extensions necessary to create an instance of Android Frame Pacing when using Vulkan, complete the steps shown in the following code snippet:

VkPhysicalDevice physicalDevice;
const char* layerName;
uint32_t numAvailableExtensions;
VkExtensionProperties availableExtensions;
uint32_t numRequiredExtensions;
char** requiredExtensions;

// Determine the number of extensions available for this device.
vkEnumerateDeviceExtensionProperties(physicalDevice, layerName,
        &numAvailableExtensions, &availableExtensions);

// Determine the number of required extensions.
SwappyVk_determineDeviceExtensions(physicalDevice, numAvailableExtensions,
        &availableExtensions, &numRequiredExtensions, nullptr);

// Determine the required extensions.
requiredExtensions = (char**)malloc(numRequiredExtensions *
        (VK_MAX_EXTENSION_SIZE + 1) * sizeof(char*));
SwappyVk_determineDeviceExtensions(physicalDevice, numAvailableExtensions,
        &availableExtensions, &numRequiredExtensions, requiredExtensions);

You can then start Android Frame Pacing by calling vkCreateDevice(). The 2nd argument, a struct of type VkDeviceCreateInfo*, should have its enabledExtensionCount and ppEnabledExtensionNames members set to the number of required extensions and the array of required extension names, respectively.

Identify queue family

To present the correct display queue, Android Frame Pacing needs to know which queue family Vulkan is using. To determine the correct family, complete the steps shown in the following code snippet:

// Reusing local variables from previous snippets:
// VkPhysicalDevice physicalDevice;

const VkDeviceCreateInfo createInfo;
const VkAllocationCallbacks allocator;
VkDevice device;
uint32_t queueFamilyIndex;
uint32_t queueIndex;
VkQueue deviceQueue;

// Values of "device" and "deviceQueue" set in the 1st and 2nd function calls,
// respectively.
vkCreateDevice(physicalDevice, &createInfo, &allocator, &device);
vkGetDeviceQueue(device, queueFamilyIndex, queueIndex, &deviceQueue);
SwappyVk_setQueueFamilyIndex(device, deviceQueue, queueFamilyIndex);

Define framerate for swapchain

To initialize Android Frame Pacing for a given physical device and swapchain, complete the steps shown in the following code snippet:

// Reusing local variables from previous snippets:
// VkPhysicalDevice physicalDevice;
// VkDevice device;

// Assume that swapchain is already known.
VkSwapchainKHR swapchain;
uint64_t refreshDuration; // in nanoseconds

// Determine duration between vertical-blanking periods.
// Example: 60 FPS sets "refreshDuration" to 16,666,666.
SwappyVk_initAndGetRefreshCycleDuration(physicalDevice, device, swapchain,
        &refreshDuration);

// Declare number of vertical-blanking periods that should elapse before
// refreshing one image with the next image. This example shows what to do
// when you want to render your game at 30 FPS on a device with a 60-Hz display.
SwappyVk_setSwapInterval(device, swapchain, 2);

Present a frame

To present a frame of your game to Android Frame Pacing, call SwappyVk_queuePresent(). This function calls vkQueuePresentKHR() on behalf of your game.

Destroy the swapchain

To destroy the SwappyVk data associated with a given swapchain, complete the steps shown in the following code snippet:

// Reusing local variables from previous snippets:
// VkDevice device;
// VkSwapchainKHR swapchain;
// const VkAllocationCallbacks allocator;

SwappyVk_destroySwapchain(device, swapchain);
vkDestroySwapchainKHR(device, swapchain, &allocator);

Verify frame pacing improvement

After integrating Android Frame Pacing into your game, it's important to verify that its functionality has provided more consistent frame pacing. The best tool for confirming this improvement is the systrace command-line program. In particular, it's best to capture a trace for your game using the following set of parameters:

python systrace.py -a your-app-package-name -o mygametrace.html \
  sched freq idle am wm gfx view sync binder_driver hal input aidl

When viewing the output HTML report, you can find information relevant to frame-pacing in the SurfaceView channel. This channel reports the number of frames that are buffered within the display pipeline. By successfully integrating Android Frame Pacing into your game, you should see any inconsistencies in this counter (Figure 1) become stabilized (Figure 2).

Systrace
    report showing SurfaceView channel
Figure 1. Inconsistent frame-pacing before Android Frame Pacing integration

Systrace report
    showing SurfaceView channel
Figure 2. Consistent frame-pacing after Android Frame Pacing integration

Additional resources

To learn more about how to achieve frame pacing in your game, see the following additional resources:

Samples

  • Bouncyball: Example project integrating Android Frame Pacing into a game that uses OpenGL ES for rendering
  • Cube: Example project integrating Android Frame Pacing into a game that uses Vulkan for rendering