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:
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
- For the C interface, include one of the following:
swappy/swappy.hfor integration with OpenGL ES.
swappy/swappyVk.hfor integration with Vulkan.
In your linker library paths, add a path of the form architecture_sdk-version_ndk-version_stl-version. For example:
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:
-lswappyif you're using a shared library.
-lgamesdkif 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);
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);
Swappy_setSwapIntervalNS(), pass in the duration that a frame
should be presented. In most cases, you can use one of the following constants:
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
The following method checks whether Android Frame Pacing is enabled:
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
In either of these situations,
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
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
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).
To learn more about how to achieve frame pacing in your game, see the following additional resources: