This commit is contained in:
Zeqiang Li 2022-03-06 19:58:46 +08:00
parent 973e244526
commit 2620f24c94
60 changed files with 9570 additions and 0 deletions

View File

@ -51,10 +51,12 @@ if(ANDROID OR OHOS)
include(${CMAKE_CURRENT_LIST_DIR}/pvmp3dec/CMakeLists.txt)
include(${CMAKE_CURRENT_LIST_DIR}/tremolo/CMakeLists.txt)
include(${CMAKE_CURRENT_LIST_DIR}/Swappy/src/swappy/CMakeLists.txt)
list(APPEND CC_EXTERNAL_LIBS
pvmp3dec
vorbisidec
swappy
)
elseif(WINDOWS)

View File

@ -0,0 +1,41 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This is the main interface to the Android Performance Tuner library, also
* known as Tuning Fork.
*
* It is part of the Android Games SDK and produces best results when integrated
* with the Swappy Frame Pacing Library.
*
* See the documentation at
* https://developer.android.com/games/sdk/performance-tuner/custom-engine for
* more information on using this library in a native Android game.
*
*/
#pragma once
// There are separate versions for each GameSDK component that use this format:
#define ANDROID_GAMESDK_PACKED_VERSION(MAJOR, MINOR, BUGFIX) \
((MAJOR << 16) | (MINOR) | (BUGFIX << 8))
// Accessors
#define ANDROID_GAMESDK_MAJOR_VERSION(PACKED) ((PACKED) >> 16)
#define ANDROID_GAMESDK_MINOR_VERSION(PACKED) ((PACKED)&0xff)
#define ANDROID_GAMESDK_BUGFIX_VERSION(PACKED) (((PACKED) >> 8) & 0xff)
#define AGDK_STRING_VERSION(MAJOR, MINOR, BUGFIX, GIT) \
#MAJOR "." #MINOR "." #BUGFIX "." #GIT

View File

@ -0,0 +1 @@
include ../../src/swappy/OWNERS

View File

@ -0,0 +1,138 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @defgroup swappyGL Swappy for OpenGL
* OpenGL part of Swappy.
* @{
*/
#pragma once
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <jni.h>
#include <stdint.h>
#include "swappy_common.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize Swappy, getting the required Android parameters from the
* display subsystem via JNI.
* @param env The JNI environment where Swappy is used
* @param jactivity The activity where Swappy is used
* @return false if Swappy failed to initialize.
* @see SwappyGL_destroy
*/
bool SwappyGL_init(JNIEnv *env, jobject jactivity);
/**
* @brief Check if Swappy was successfully initialized.
* @return false if either the `swappy.disable` system property is not `false`
* or the required OpenGL extensions are not available for Swappy to work.
*/
bool SwappyGL_isEnabled();
/**
* @brief Destroy resources and stop all threads that Swappy has created.
* @see SwappyGL_init
*/
void SwappyGL_destroy();
/**
* @brief Tell Swappy which ANativeWindow to use when calling to ANativeWindow_*
* API.
* @param window ANativeWindow that was used to create the EGLSurface.
* @return true on success, false if Swappy was not initialized.
*/
bool SwappyGL_setWindow(ANativeWindow *window);
/**
* @brief Replace calls to eglSwapBuffers with this. Swappy will wait for the
* previous frame's buffer to be processed by the GPU before actually calling
* eglSwapBuffers.
* @return true on success or false if
* 1) Swappy is not initialized or 2) eglSwapBuffers did not return EGL_TRUE.
* In the latter case, eglGetError can be used to get the error code.
*/
bool SwappyGL_swap(EGLDisplay display, EGLSurface surface);
// Paramter setters:
void SwappyGL_setUseAffinity(bool tf);
/**
* @brief Override the swap interval
*
* By default, Swappy will adjust the swap interval based on actual frame
* rendering time.
*
* If an app wants to override the swap interval calculated by Swappy, it can
* call this function:
*
* * This will temporarily override Swappy's frame timings but, unless
* `SwappyGL_setAutoSwapInterval(false)` is called, the timings will continue
* to be be updated dynamically, so the swap interval may change.
*
* * This set the **minimal** interval to run. For example,
* `SwappyGL_setSwapIntervalNS(SWAPPY_SWAP_30FPS)` will not allow Swappy to swap
* faster, even if auto mode decides that it can. But it can go slower if auto
* mode is on.
*
* @param swap_ns The new swap interval value, in nanoseconds.
*/
void SwappyGL_setSwapIntervalNS(uint64_t swap_ns);
/**
* @brief Set the fence timeout parameter, for devices with faulty
* drivers. Its default value is 50,000,000ns (50ms).
*/
void SwappyGL_setFenceTimeoutNS(uint64_t fence_timeout_ns);
// Parameter getters:
/**
* @brief Get the refresh period value, in nanoseconds.
*/
uint64_t SwappyGL_getRefreshPeriodNanos();
/**
* @brief Get the swap interval value, in nanoseconds.
*/
uint64_t SwappyGL_getSwapIntervalNS();
bool SwappyGL_getUseAffinity();
/**
* @brief Get the fence timeout value, in nanoseconds.
*/
uint64_t SwappyGL_getFenceTimeoutNS();
/**
* @brief Set the number of bad frames to wait before applying a fix for buffer
* stuffing. Set to zero in order to turn off this feature. Default value = 0.
*/
void SwappyGL_setBufferStuffingFixWait(int32_t n_frames);
#ifdef __cplusplus
};
#endif
/** @} */

View File

@ -0,0 +1,171 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @defgroup swappyGL_extra Swappy for OpenGL extras
* Extra utility functions to use Swappy with OpenGL.
* @{
*/
#pragma once
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <jni.h>
#include <stdint.h>
#include "swappy_common.h"
/**
* The longest duration, in refresh periods, represented by the statistics.
* @see SwappyStats
*/
#define MAX_FRAME_BUCKETS 6
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief If an app wishes to use the Android choreographer to provide ticks to
* Swappy, it can call this function.
*
* @warning This function *must* be called before the first `Swappy_swap()`
* call. Afterwards, call this function every choreographer tick.
*/
void SwappyGL_onChoreographer(int64_t frameTimeNanos);
/** @brief Pass callbacks to be called each frame to trace execution. */
void SwappyGL_injectTracer(const SwappyTracer *t);
/**
* @brief Toggle auto-swap interval detection on/off
*
* By default, Swappy will adjust the swap interval based on actual frame
* rendering time. If an app wants to override the swap interval calculated by
* Swappy, it can call `SwappyGL_setSwapIntervalNS`. This will temporarily
* override Swappy's frame timings but, unless
* `SwappyGL_setAutoSwapInterval(false)` is called, the timings will continue to
* be be updated dynamically, so the swap interval may change.
*/
void SwappyGL_setAutoSwapInterval(bool enabled);
/**
* @brief Sets the maximal duration for auto-swap interval in milliseconds.
*
* If Swappy is operating in auto-swap interval and the frame duration is longer
* than `max_swap_ns`, Swappy will not do any pacing and just submit the frame
* as soon as possible.
*/
void SwappyGL_setMaxAutoSwapIntervalNS(uint64_t max_swap_ns);
/**
* @brief Toggle auto-pipeline mode on/off
*
* By default, if auto-swap interval is on, auto-pipelining is on and Swappy
* will try to reduce latency by scheduling cpu and gpu work in the same
* pipeline stage, if it fits.
*/
void SwappyGL_setAutoPipelineMode(bool enabled);
/**
* @brief Toggle statistics collection on/off
*
* By default, stats collection is off and there is no overhead related to
* stats. An app can turn on stats collection by calling
* `SwappyGL_enableStats(true)`. Then, the app is expected to call
* ::SwappyGL_recordFrameStart for each frame before starting to do any CPU
* related work. Stats will be logged to logcat with a 'FrameStatistics' tag. An
* app can get the stats by calling ::SwappyGL_getStats.
*/
void SwappyGL_enableStats(bool enabled);
/**
* @brief Swappy statistics, collected if toggled on with
* ::SwappyGL_enableStats.
* @see SwappyGL_getStats
*/
struct SwappyStats {
/** @brief Total frames swapped by swappy */
uint64_t totalFrames;
/** @brief Histogram of the number of screen refreshes a frame waited in the
* compositor queue after rendering was completed.
*
* For example:
* if a frame waited 2 refresh periods in the compositor queue after
* rendering was done, the frame will be counted in idleFrames[2]
*/
uint64_t idleFrames[MAX_FRAME_BUCKETS];
/** @brief Histogram of the number of screen refreshes passed between the
* requested presentation time and the actual present time.
*
* For example:
* if a frame was presented 2 refresh periods after the requested
* timestamp swappy set, the frame will be counted in lateFrames[2]
*/
uint64_t lateFrames[MAX_FRAME_BUCKETS];
/** @brief Histogram of the number of screen refreshes passed between two
* consecutive frames
*
* For example:
* if frame N was presented 2 refresh periods after frame N-1
* frame N will be counted in offsetFromPreviousFrame[2]
*/
uint64_t offsetFromPreviousFrame[MAX_FRAME_BUCKETS];
/** @brief Histogram of the number of screen refreshes passed between the
* call to Swappy_recordFrameStart and the actual present time.
*
* For example:
* if a frame was presented 2 refresh periods after the call to
* `Swappy_recordFrameStart` the frame will be counted in latencyFrames[2]
*/
uint64_t latencyFrames[MAX_FRAME_BUCKETS];
};
/**
* @brief Should be called if stats have been enabled with SwappyGL_enableStats.
*
* When stats collection is enabled with SwappyGL_enableStats, the app is
* expected to call this function for each frame before starting to do any CPU
* related work.
*
* @see SwappyGL_enableStats.
*/
void SwappyGL_recordFrameStart(EGLDisplay display, EGLSurface surface);
/**
* @brief Returns the stats collected, if statistics collection was toggled on.
*
* @param swappyStats Pointer to a SwappyStats that will be populated with
* collected stats.
* @see SwappyStats
* @see SwappyGL_enableStats
*/
void SwappyGL_getStats(SwappyStats *swappyStats);
/** @brief Remove callbacks that were previously added using
* SwappyGL_injectTracer. */
void SwappyGL_uninjectTracer(const SwappyTracer *t);
#ifdef __cplusplus
};
#endif
/** @} */

View File

@ -0,0 +1,323 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @defgroup swappyVk Swappy for Vulkan
* Vulkan part of Swappy.
* @{
*/
#pragma once
#include "jni.h"
#include "swappy_common.h"
#define VK_NO_PROTOTYPES 1
#include <vulkan/vulkan.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Determine any Vulkan device extensions that must be enabled for a new
* VkDevice.
*
* Swappy-for-Vulkan (SwappyVk) benefits from certain Vulkan device extensions
* (e.g. VK_GOOGLE_display_timing). Before the application calls
* vkCreateDevice, SwappyVk needs to look at the list of available extensions
* (returned by vkEnumerateDeviceExtensionProperties) and potentially identify
* one or more extensions that the application must add to:
*
* - VkDeviceCreateInfo::enabledExtensionCount
* - VkDeviceCreateInfo::ppEnabledExtensionNames
*
* before the application calls vkCreateDevice. For each VkPhysicalDevice that
* the application will call vkCreateDevice for, the application must call this
* function, and then must add the identified extension(s) to the list that are
* enabled for the VkDevice. Similar to many Vulkan functions, this function
* can be called twice, once to identify the number of required extensions, and
* again with application-allocated memory that the function can write into.
*
* @param[in] physicalDevice - The VkPhysicalDevice associated with
* the available extensions.
* @param[in] availableExtensionCount - This is the returned value of
* pPropertyCount from vkEnumerateDeviceExtensionProperties.
* @param[in] pAvailableExtensions - This is the returned value of
* pProperties from vkEnumerateDeviceExtensionProperties.
* @param[inout] pRequiredExtensionCount - If pRequiredExtensions is nullptr,
* the function sets this to the number of extensions that are required. If
* pRequiredExtensions is non-nullptr, this is the number of required extensions
* that the function should write into pRequiredExtensions.
* @param[inout] pRequiredExtensions - If non-nullptr, this is
* application-allocated memory into which the function will write the names of
* required extensions. It is a pointer to an array of
* char* strings (i.e. the same as
* VkDeviceCreateInfo::ppEnabledExtensionNames).
*/
void SwappyVk_determineDeviceExtensions(
VkPhysicalDevice physicalDevice, uint32_t availableExtensionCount,
VkExtensionProperties* pAvailableExtensions,
uint32_t* pRequiredExtensionCount, char** pRequiredExtensions);
/**
* @brief Tell Swappy the queueFamilyIndex used to create a specific VkQueue
*
* Swappy needs to know the queueFamilyIndex used for creating a specific
* VkQueue so it can use it when presenting.
*
* @param[in] device - The VkDevice associated with the queue
* @param[in] queue - A device queue.
* @param[in] queueFamilyIndex - The queue family index used to create the
* VkQueue.
*
*/
void SwappyVk_setQueueFamilyIndex(VkDevice device, VkQueue queue,
uint32_t queueFamilyIndex);
// TBD: For now, SwappyVk assumes only one VkSwapchainKHR per VkDevice, and that
// applications don't re-create swapchains. Is this long-term sufficient?
/**
* Internal init function. Do not call directly.
* See SwappyVk_initAndGetRefreshCycleDuration instead.
* @private
*/
bool SwappyVk_initAndGetRefreshCycleDuration_internal(
JNIEnv* env, jobject jactivity, VkPhysicalDevice physicalDevice,
VkDevice device, VkSwapchainKHR swapchain, uint64_t* pRefreshDuration);
/**
* @brief Initialize SwappyVk for a given device and swapchain, and obtain the
* approximate time duration between vertical-blanking periods.
*
* Uses JNI to query AppVsyncOffset and PresentationDeadline.
*
* If your application presents to more than one swapchain at a time, you must
* call this for each swapchain before calling swappyVkSetSwapInterval() for it.
*
* The duration between vertical-blanking periods (an interval) is expressed as
* the approximate number of nanoseconds between vertical-blanking periods of
* the swapchains physical display.
*
* If the application converts this number to a fraction (e.g. 16,666,666 nsec
* to 0.016666666) and divides one by that fraction, it will be the approximate
* refresh rate of the display (e.g. 16,666,666 nanoseconds corresponds to a
* 60Hz display, 11,111,111 nsec corresponds to a 90Hz display).
*
* @param[in] env - JNIEnv that is assumed to be from AttachCurrentThread
* function
* @param[in] jactivity - NativeActivity object handle, used for JNI
* @param[in] physicalDevice - The VkPhysicalDevice associated with the
* swapchain
* @param[in] device - The VkDevice associated with the swapchain
* @param[in] swapchain - The VkSwapchainKHR the application wants Swappy to
* swap
* @param[out] pRefreshDuration - The returned refresh cycle duration
*
* @return bool - true if the value returned by pRefreshDuration is
* valid, otherwise false if an error.
*/
bool SwappyVk_initAndGetRefreshCycleDuration(JNIEnv* env, jobject jactivity,
VkPhysicalDevice physicalDevice,
VkDevice device,
VkSwapchainKHR swapchain,
uint64_t* pRefreshDuration);
/**
* @brief Tell Swappy which ANativeWindow to use when calling to ANativeWindow_*
* API.
* @param[in] device - The VkDevice associated with the swapchain
* @param[in] swapchain - The VkSwapchainKHR the application wants Swappy to
* swap
* @param[in] window - The ANativeWindow that was used to create the
* VkSwapchainKHR
*/
void SwappyVk_setWindow(VkDevice device, VkSwapchainKHR swapchain,
ANativeWindow* window);
/**
* @brief Tell Swappy the duration of that each presented image should be
* visible.
*
* If your application presents to more than one swapchain at a time, you must
* call this for each swapchain before presenting to it.
*
* @param[in] device - The VkDevice associated with the swapchain
* @param[in] swapchain - The VkSwapchainKHR the application wants Swappy to
* swap
* @param[in] swap_ns - The duration of that each presented image should be
* visible in nanoseconds
*/
void SwappyVk_setSwapIntervalNS(VkDevice device, VkSwapchainKHR swapchain,
uint64_t swap_ns);
/**
* @brief Tell Swappy to present one or more images to corresponding swapchains.
*
* Swappy will call vkQueuePresentKHR for your application. Swappy may insert a
* struct to the pNext-chain of VkPresentInfoKHR, or it may insert other Vulkan
* commands in order to attempt to honor the desired swap interval.
*
* @note If your application presents to more than one swapchain at a time, and
* if you use a different swap interval for each swapchain, Swappy will attempt
* to honor the swap interval for each swapchain (being more successful on
* devices that support an underlying presentation-timing extension, such as
* VK_GOOGLE_display_timing).
*
* @param[in] queue - The VkQueue associated with the device and swapchain
* @param[in] pPresentInfo - A pointer to the VkPresentInfoKHR containing the
* information about what image(s) to present on which
* swapchain(s).
*/
VkResult SwappyVk_queuePresent(VkQueue queue,
const VkPresentInfoKHR* pPresentInfo);
/**
* @brief Destroy the SwappyVk instance associated with a swapchain.
*
* This API is expected to be called before calling vkDestroySwapchainKHR()
* so Swappy can cleanup its internal state.
*
* @param[in] device - The VkDevice associated with SwappyVk
* @param[in] swapchain - The VkSwapchainKHR the application wants Swappy to
* destroy
*/
void SwappyVk_destroySwapchain(VkDevice device, VkSwapchainKHR swapchain);
/**
* @brief Destroy any swapchains associated with the device and clean up the
* device's resources
*
* This function should be called after SwappyVk_destroySwapchain if you no
* longer need the device.
*
* @param[in] device - The VkDevice associated with SwappyVk
*/
void SwappyVk_destroyDevice(VkDevice device);
/**
* @brief Enables Auto-Swap-Interval feature for all instances.
*
* By default this feature is enabled. Changing it is completely
* optional for fine-tuning swappy behaviour.
*
* @param[in] enabled - True means enable, false means disable
*/
void SwappyVk_setAutoSwapInterval(bool enabled);
/**
* @brief Enables Auto-Pipeline-Mode feature for all instances.
*
* By default this feature is enabled. Changing it is completely
* optional for fine-tuning swappy behaviour.
*
* @param[in] enabled - True means enable, false means disable
*/
void SwappyVk_setAutoPipelineMode(bool enabled);
/**
* @brief Sets the maximal swap duration for all instances.
*
* Sets the maximal duration for Auto-Swap-Interval in milliseconds.
* If SwappyVk is operating in Auto-Swap-Interval and the frame duration is
* longer than the provided duration, SwappyVk will not do any pacing and just
* submit the frame as soon as possible.
*
* @param[in] max_swap_ns - maximal swap duration in milliseconds.
*/
void SwappyVk_setMaxAutoSwapIntervalNS(uint64_t max_swap_ns);
/**
* @brief The fence timeout parameter can be set for devices with faulty
* drivers. Its default value is 50,000,000.
*/
void SwappyVk_setFenceTimeoutNS(uint64_t fence_timeout_ns);
/**
* @brief Get the fence timeout parameter, for devices with faulty
* drivers. Its default value is 50,000,000.
*/
uint64_t SwappyVk_getFenceTimeoutNS();
/**
* @brief Inject callback functions to be called each frame.
*
* @param[in] tracer - Collection of callback functions
*/
void SwappyVk_injectTracer(const SwappyTracer* tracer);
/**
* @brief A structure enabling you to provide your own Vulkan function wrappers
* by calling ::SwappyVk_setFunctionProvider.
*
* Usage of this functionality is optional.
*/
typedef struct SwappyVkFunctionProvider {
/**
* @brief Callback to initialize the function provider.
*
* This function is called by Swappy before any functions are requested.
* E.g. so you can call dlopen on the Vulkan library.
*/
bool (*init)();
/**
* @brief Callback to get the address of a function.
*
* This function is called by Swappy to get the address of a Vulkan
* function.
* @param name The null-terminated name of the function.
*/
void* (*getProcAddr)(const char* name);
/**
* @brief Callback to close any resources owned by the function provider.
*
* This function is called by Swappy when no more functions will be
* requested, e.g. so you can call dlclose on the Vulkan library.
*/
void (*close)();
} SwappyVkFunctionProvider;
/**
* @brief Set the Vulkan function provider.
*
* This enables you to provide an object that will be used to look up Vulkan
* functions, e.g. to hook usage of these functions.
*
* To use this functionality, you *must* call this function before any others.
*
* Usage of this function is entirely optional. If you do not use it, the Vulkan
* functions required by Swappy will be dynamically loaded from libvulkan.so.
*
* @param[in] provider - provider object
*/
void SwappyVk_setFunctionProvider(
const SwappyVkFunctionProvider* pSwappyVkFunctionProvider);
/**
* @brief Get the swap interval value, in nanoseconds, for a given swapchain.
*
* @param[in] swapchain - the swapchain to query
*/
uint64_t SwappyVk_getSwapIntervalNS(VkSwapchainKHR swapchain);
#ifdef __cplusplus
} // extern "C"
#endif
/** @} */

View File

@ -0,0 +1,221 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @defgroup swappy_common Swappy common tools
* Tools to be used with Swappy for OpenGL or Swappy for Vulkan.
* @{
*/
#pragma once
#include <android/native_window.h>
#include <stdint.h>
#include "common/gamesdk_common.h"
/** @brief Swap interval for 60fps, in nanoseconds. */
#define SWAPPY_SWAP_60FPS (16666667L)
/** @brief Swap interval for 30fps, in nanoseconds. */
#define SWAPPY_SWAP_30FPS (33333333L)
/** @brief Swap interval for 20fps, in nanoseconds. */
#define SWAPPY_SWAP_20FPS (50000000L)
/** @cond INTERNAL */
#define SWAPPY_SYSTEM_PROP_KEY_DISABLE "swappy.disable"
// Internal macros to track Swappy version, do not use directly.
#define SWAPPY_MAJOR_VERSION 1
#define SWAPPY_MINOR_VERSION 10
#define SWAPPY_BUGFIX_VERSION 0
#define SWAPPY_PACKED_VERSION \
ANDROID_GAMESDK_PACKED_VERSION(SWAPPY_MAJOR_VERSION, SWAPPY_MINOR_VERSION, \
SWAPPY_BUGFIX_VERSION)
// Internal macros to generate a symbol to track Swappy version, do not use
// directly.
#define SWAPPY_VERSION_CONCAT_NX(PREFIX, MAJOR, MINOR, BUGFIX, GITCOMMIT) \
PREFIX##_##MAJOR##_##MINOR##_##BUGFIX##_##GITCOMMIT
#define SWAPPY_VERSION_CONCAT(PREFIX, MAJOR, MINOR, BUGFIX, GITCOMMIT) \
SWAPPY_VERSION_CONCAT_NX(PREFIX, MAJOR, MINOR, BUGFIX, GITCOMMIT)
#define SWAPPY_VERSION_SYMBOL \
SWAPPY_VERSION_CONCAT(Swappy_version, SWAPPY_MAJOR_VERSION, \
SWAPPY_MINOR_VERSION, SWAPPY_BUGFIX_VERSION, \
AGDK_GIT_COMMIT)
/** @endcond */
/** @brief Id of a thread returned by an external thread manager. */
typedef uint64_t SwappyThreadId;
/**
* @brief A structure enabling you to set how Swappy starts and joins threads by
* calling
* ::Swappy_setThreadFunctions.
*
* Usage of this functionality is optional.
*/
typedef struct SwappyThreadFunctions {
/** @brief Thread start callback.
*
* This function is called by Swappy to start thread_func on a new thread.
* @param user_data A value to be passed the thread function.
* If the thread was started, this function should set the thread_id and
* return 0. If the thread was not started, this function should return a
* non-zero value.
*/
int (*start)(SwappyThreadId* thread_id, void* (*thread_func)(void*),
void* user_data);
/** @brief Thread join callback.
*
* This function is called by Swappy to join the thread with given id.
*/
void (*join)(SwappyThreadId thread_id);
/** @brief Thread joinable callback.
*
* This function is called by Swappy to discover whether the thread with the
* given id is joinable.
*/
bool (*joinable)(SwappyThreadId thread_id);
} SwappyThreadFunctions;
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Return the version of the Swappy library at runtime.
*/
uint32_t Swappy_version();
/**
* @brief Call this before any other functions in order to use a custom thread
* manager.
*
* Usage of this function is entirely optional. Swappy uses std::thread by
* default.
*
*/
void Swappy_setThreadFunctions(const SwappyThreadFunctions* thread_functions);
/**
* @brief Return the full version of the Swappy library at runtime, e.g.
* "1.9.0_8a85ab7c46"
*/
const char* Swappy_versionString();
#ifdef __cplusplus
} // extern "C"
#endif
/**
* Pointer to a function that can be attached to SwappyTracer::preWait
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
*/
typedef void (*SwappyPreWaitCallback)(void*);
/**
* Pointer to a function that can be attached to SwappyTracer::postWait.
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
* @param cpu_time_ns Time for CPU processing of this frame in nanoseconds.
* @param gpu_time_ns Time for GPU processing of previous frame in nanoseconds.
*/
typedef void (*SwappyPostWaitCallback)(void*, int64_t cpu_time_ns,
int64_t gpu_time_ns);
/**
* Pointer to a function that can be attached to SwappyTracer::preSwapBuffers.
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
*/
typedef void (*SwappyPreSwapBuffersCallback)(void*);
/**
* Pointer to a function that can be attached to SwappyTracer::postSwapBuffers.
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
* @param desiredPresentationTimeMillis The target time, in milliseconds, at
* which the frame would be presented on screen.
*/
typedef void (*SwappyPostSwapBuffersCallback)(
void*, int64_t desiredPresentationTimeMillis);
/**
* Pointer to a function that can be attached to SwappyTracer::startFrame.
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
* @param desiredPresentationTimeMillis The time, in milliseconds, at which the
* frame is scheduled to be presented.
*/
typedef void (*SwappyStartFrameCallback)(void*, int currentFrame,
int64_t desiredPresentationTimeMillis);
/**
* Pointer to a function that can be attached to
* SwappyTracer::swapIntervalChanged. Call ::SwappyGL_getSwapIntervalNS or
* ::SwappyVk_getSwapIntervalNS to get the latest swapInterval.
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
*/
typedef void (*SwappySwapIntervalChangedCallback)(void*);
/**
* @brief Collection of callbacks to be called each frame to trace execution.
*
* Injection of these is optional.
*/
typedef struct SwappyTracer {
/**
* Callback called before waiting to queue the frame to the composer.
*/
SwappyPreWaitCallback preWait;
/**
* Callback called after wait to queue the frame to the composer is done.
*/
SwappyPostWaitCallback postWait;
/**
* Callback called before calling the function to queue the frame to the
* composer.
*/
SwappyPreSwapBuffersCallback preSwapBuffers;
/**
* Callback called after calling the function to queue the frame to the
* composer.
*/
SwappyPostSwapBuffersCallback postSwapBuffers;
/**
* Callback called at the start of a frame.
*/
SwappyStartFrameCallback startFrame;
/**
* Pointer to some arbitrary data that will be passed as the first argument
* of callbacks.
*/
void* userData;
/**
* Callback called when the swap interval was changed.
*/
SwappySwapIntervalChangedCallback swapIntervalChanged;
} SwappyTracer;
/** @} */

View File

@ -0,0 +1,49 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <android/choreographer.h>
// Declare the types, even if we can't access the functions.
#if __ANDROID_API__ < 24
struct AChoreographer;
typedef struct AChoreographer AChoreographer;
/**
* Prototype of the function that is called when a new frame is being rendered.
* It's passed the time that the frame is being rendered as nanoseconds in the
* CLOCK_MONOTONIC time base, as well as the data pointer provided by the
* application that registered a callback. All callbacks that run as part of
* rendering a frame will observe the same frame time, so it should be used
* whenever events need to be synchronized (e.g. animations).
*/
typedef void (*AChoreographer_frameCallback)(long frameTimeNanos, void* data);
#endif // __ANDROID_API__ < 24
#if __ANDROID_API__ < 30
/**
* Prototype of the function that is called when the display refresh rate
* changes. It's passed the new vsync period in nanoseconds, as well as the data
* pointer provided by the application that registered a callback.
*/
typedef void (*AChoreographer_refreshRateCallback)(int64_t vsyncPeriodNanos,
void* data);
#endif // __ANDROID_API__ < 30

View File

@ -0,0 +1,239 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <jni.h>
#include <cstdio>
#include <fstream>
#include <sstream>
#include <string>
#include "Log.h"
// The code in this file will dynamically load Java classes from a binary
// resource linked to the library. The binary data is in DEX format, accessible
// using the _binary_classes_dex_start and _binary_classes_dex_end linker
// symbols.
// If you make AGDK classes available on the Java classpath, e.g. by adding them
// to your game/engine's own Java component, you do not need to add the binary
// resource and can instead define ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE which
// will avoid the linker requiring these symbols.
#define ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
#ifndef ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
extern const char _binary_classes_dex_start;
extern const char _binary_classes_dex_end;
#endif
namespace gamesdk {
#ifndef ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
static bool saveBytesToFile(std::string fileName, const char* bytes,
size_t size) {
std::ofstream save_file(fileName, std::ios::binary);
if (save_file.good()) {
save_file.write(bytes, size);
return true;
}
return false;
}
static bool deleteFile(std::string fileName) {
if (remove(fileName.c_str()) != 0)
return false;
else
return true;
}
static bool createTempFile(JNIEnv* env, jobject activity, const char* ext,
std::string& tempFileName) {
bool result = false;
jclass activityClass = env->GetObjectClass(activity);
jmethodID getCacheDir =
env->GetMethodID(activityClass, "getCacheDir", "()Ljava/io/File;");
jobject cacheDir = env->CallObjectMethod(activity, getCacheDir);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
} else {
jclass fileClass = env->FindClass("java/io/File");
jmethodID createTempFile =
env->GetStaticMethodID(fileClass, "createTempFile",
"(Ljava/lang/String;Ljava/lang/String;Ljava/"
"io/File;)Ljava/io/File;");
jstring prefix = env->NewStringUTF("ags");
jstring suffix = env->NewStringUTF(ext);
jobject tempFile = env->CallStaticObjectMethod(
fileClass, createTempFile, prefix, suffix, cacheDir);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
} else {
jmethodID getPath =
env->GetMethodID(fileClass, "getPath", "()Ljava/lang/String;");
jstring pathString =
(jstring)env->CallObjectMethod(tempFile, getPath);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
} else {
const char* path = env->GetStringUTFChars(pathString, NULL);
tempFileName.assign(path);
env->ReleaseStringUTFChars(pathString, path);
result = true;
}
}
env->DeleteLocalRef(prefix);
env->DeleteLocalRef(suffix);
}
return result;
}
#endif // #ifndef ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
static jclass loadClass(JNIEnv* env, jobject activity, const char* name,
JNINativeMethod* nativeMethods,
size_t nativeMethodsSize) {
/*
* 1. Get a classloader from actvity
* 2. Try to create the requested class from the activty classloader
* 3. If step 2 not successful then get a classloder for dex bytes (in
* memory or file)
* 4. If step 3 is successful then register native methods
*/
if (!env || !activity || !name) {
return nullptr;
}
jclass activityClass = env->GetObjectClass(activity);
jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
jmethodID getClassLoader = env->GetMethodID(activityClass, "getClassLoader",
"()Ljava/lang/ClassLoader;");
jobject classLoaderObj = env->CallObjectMethod(activity, getClassLoader);
jmethodID loadClass = env->GetMethodID(
classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
jstring className = env->NewStringUTF(name);
jclass targetClass = static_cast<jclass>(
env->CallObjectMethod(classLoaderObj, loadClass, className));
if (env->ExceptionCheck()) {
env->ExceptionClear();
#ifdef ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
ALOGE(
"Couldn't find class %s and ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE "
"is defined",
name);
#else
jstring dexLoaderClassName =
env->NewStringUTF("dalvik/system/InMemoryDexClassLoader");
jclass imclassloaderClass = static_cast<jclass>(env->CallObjectMethod(
classLoaderObj, loadClass, dexLoaderClassName));
env->DeleteLocalRef(dexLoaderClassName);
if (env->ExceptionCheck() || !imclassloaderClass) {
env->ExceptionClear();
// For older SDK versions <26, where InMemoryDexClassLoader is not
// available
dexLoaderClassName =
env->NewStringUTF("dalvik/system/PathClassLoader");
imclassloaderClass = static_cast<jclass>(env->CallObjectMethod(
classLoaderObj, loadClass, dexLoaderClassName));
env->DeleteLocalRef(dexLoaderClassName);
if (env->ExceptionCheck() || !imclassloaderClass) {
env->ExceptionDescribe();
env->ExceptionClear();
ALOGE("Unable to find dalvik/system/PathClassLoader.");
targetClass = nullptr;
} else {
jmethodID constructor = env->GetMethodID(
imclassloaderClass, "<init>",
"(Ljava/lang/String;Ljava/lang/ClassLoader;)V");
std::string tempPath;
if (!createTempFile(env, activity, ".dex", tempPath)) {
ALOGE(
"Unable to create a temporary file to store DEX with "
"Java classes.");
} else {
size_t dex_file_size = (size_t)(&_binary_classes_dex_end -
&_binary_classes_dex_start);
if (!saveBytesToFile(tempPath, &_binary_classes_dex_start,
dex_file_size)) {
ALOGE("Unable to write to %s file.", tempPath.c_str());
} else {
jstring dexPathString =
env->NewStringUTF(tempPath.c_str());
jobject imclassloaderObj =
env->NewObject(imclassloaderClass, constructor,
dexPathString, classLoaderObj);
env->DeleteLocalRef(dexPathString);
targetClass = static_cast<jclass>(env->CallObjectMethod(
imclassloaderObj, loadClass, className));
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
ALOGE("Unable to find %s class", name);
} else {
env->RegisterNatives(targetClass, nativeMethods,
nativeMethodsSize);
ALOGI("Using internal %s class from dex bytes.",
name);
}
if (imclassloaderObj) {
env->DeleteLocalRef(imclassloaderObj);
}
}
deleteFile(tempPath);
}
}
} else {
jmethodID constructor = env->GetMethodID(
imclassloaderClass, "<init>",
"(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V");
size_t dex_file_size =
(size_t)(&_binary_classes_dex_end - &_binary_classes_dex_start);
auto byteBuffer = env->NewDirectByteBuffer(
(void*)&_binary_classes_dex_start, dex_file_size);
jobject imclassloaderObj = env->NewObject(
imclassloaderClass, constructor, byteBuffer, classLoaderObj);
targetClass = static_cast<jclass>(
env->CallObjectMethod(imclassloaderObj, loadClass, className));
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
ALOGE("Unable to find %s class", name);
} else {
env->RegisterNatives(targetClass, nativeMethods,
nativeMethodsSize);
ALOGI("Using internal %s class from dex bytes.", name);
}
if (imclassloaderObj) {
env->DeleteLocalRef(imclassloaderObj);
}
}
if (imclassloaderClass) {
env->DeleteLocalRef(imclassloaderClass);
}
#endif // #ifdef ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
}
env->DeleteLocalRef(className);
return targetClass;
}
} // namespace gamesdk

View File

@ -0,0 +1,54 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <android/log.h>
#include <string>
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define ALOGW_ONCE_IF(cond, ...) \
do { \
static bool alogw_once##__FILE__##__LINE__##__ = true; \
if (cond && alogw_once##__FILE__##__LINE__##__) { \
alogw_once##__FILE__##__LINE__##__ = false; \
ALOGW(__VA_ARGS__); \
} \
} while (0)
#define ALOGE_ONCE(...) \
do { \
static bool aloge_once##__FILE__##__LINE__##__ = true; \
if (aloge_once##__FILE__##__LINE__##__) { \
aloge_once##__FILE__##__LINE__##__ = false; \
ALOGE(__VA_ARGS__); \
} \
} while (0)
#ifndef NDEBUG
#define ALOGV(...) \
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#else
#define ALOGV(...)
#endif
namespace swappy {
std::string to_string(int value);
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <string>
// These string functions can be needed when compiling using GNU STL.
#if (defined ANDROID_GNUSTL) || \
((defined ANDROID_NDK_VERSION) && ANDROID_NDK_VERSION <= 17)
namespace std {
template <typename T>
std::string to_string(T value) {
std::stringstream os;
os << value;
return os.str();
}
template <typename T>
std::wstring to_wstring(T value) {
std::wstringstream os;
os << value;
return os.str();
}
} // namespace std
#endif
#if (defined ANDROID_GNUSTL)
namespace std {
long double stold(const std::string& str, std::size_t* pos = nullptr) {
long double d;
std::stringstream is(str);
auto p0 = is.tellg();
is >> d;
if (pos != nullptr) {
*pos = is.tellg() - p0;
}
return d;
}
} // namespace std
#endif

View File

@ -0,0 +1,156 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <android/log.h>
#include <android/trace.h>
#include <dlfcn.h>
#include <memory>
namespace gamesdk {
class Trace {
public:
using ATrace_beginSection_type = void (*)(const char *sectionName);
using ATrace_endSection_type = void (*)();
using ATrace_isEnabled_type = bool (*)();
using ATrace_setCounter_type = void (*)(const char *counterName,
int64_t counterValue);
Trace() {
__android_log_print(ANDROID_LOG_INFO, "Trace",
"Unable to load NDK tracing APIs");
}
Trace(ATrace_beginSection_type beginSection,
ATrace_endSection_type endSection, ATrace_isEnabled_type isEnabled,
ATrace_setCounter_type setCounter)
: ATrace_beginSection(beginSection),
ATrace_endSection(endSection),
ATrace_isEnabled(isEnabled),
ATrace_setCounter(setCounter) {}
static std::unique_ptr<Trace> create() {
void *libandroid = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL);
if (!libandroid) {
return std::make_unique<Trace>();
}
auto beginSection = reinterpret_cast<ATrace_beginSection_type>(
dlsym(libandroid, "ATrace_beginSection"));
if (!beginSection) {
return std::make_unique<Trace>();
}
auto endSection = reinterpret_cast<ATrace_endSection_type>(
dlsym(libandroid, "ATrace_endSection"));
if (!endSection) {
return std::make_unique<Trace>();
}
auto isEnabled = reinterpret_cast<ATrace_isEnabled_type>(
dlsym(libandroid, "ATrace_isEnabled"));
if (!isEnabled) {
return std::make_unique<Trace>();
}
auto setCounter = reinterpret_cast<ATrace_setCounter_type>(
dlsym(libandroid, "ATrace_setCounter"));
/* ATrace_setCounter was added in API 29, continue even if it is not
* available */
return std::make_unique<Trace>(beginSection, endSection, isEnabled,
setCounter);
}
bool isAvailable() const { return ATrace_beginSection != nullptr; }
bool isEnabled() const {
return (ATrace_isEnabled != nullptr) && ATrace_isEnabled();
}
void beginSection(const char *name) const {
if (!ATrace_beginSection) {
return;
}
ATrace_beginSection(name);
}
void endSection() const {
if (!ATrace_endSection) {
return;
}
ATrace_endSection();
}
void setCounter(const char *name, int64_t value) {
if (!ATrace_setCounter || !isEnabled()) {
return;
}
ATrace_setCounter(name, value);
}
static Trace *getInstance() {
static std::unique_ptr<Trace> trace = Trace::create();
return trace.get();
};
private:
const ATrace_beginSection_type ATrace_beginSection = nullptr;
const ATrace_endSection_type ATrace_endSection = nullptr;
const ATrace_isEnabled_type ATrace_isEnabled = nullptr;
const ATrace_setCounter_type ATrace_setCounter = nullptr;
};
struct ScopedTrace {
ScopedTrace(const char *name) {
Trace *trace = Trace::getInstance();
if (!trace->isAvailable() || !trace->isEnabled()) {
return;
}
trace->beginSection(name);
mIsTracing = true;
}
~ScopedTrace() {
if (!mIsTracing) {
return;
}
Trace *trace = Trace::getInstance();
trace->endSection();
}
private:
bool mIsTracing = false;
};
} // namespace gamesdk
#define PASTE_HELPER_HELPER(a, b) a##b
#define PASTE_HELPER(a, b) PASTE_HELPER_HELPER(a, b)
#define TRACE_CALL() \
gamesdk::ScopedTrace PASTE_HELPER(scopedTrace, \
__LINE__)(__PRETTY_FUNCTION__)
#define TRACE_INT(name, value) \
gamesdk::Trace::getInstance()->setCounter(name, value)
#define TRACE_ENABLED() gamesdk::Trace::getInstance()->isEnabled()

View File

@ -0,0 +1,53 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
#define LOG_TAG "TuningForkUtils"
#include "Log.h"
#include "apk_utils.h"
#include "jni/jni_wrap.h"
namespace apk_utils {
NativeAsset::NativeAsset(const char* name) {
auto java_asset_manager = gamesdk::jni::AppContext().getAssets();
AAssetManager* mgr = AAssetManager_fromJava(
gamesdk::jni::Env(), (jobject)java_asset_manager.obj_);
asset = AAssetManager_open(mgr, name, AASSET_MODE_BUFFER);
if (asset == nullptr) {
ALOGW("Can't find %s in APK", name);
}
}
NativeAsset::NativeAsset(NativeAsset&& a) : asset(a.asset) {
a.asset = nullptr;
}
NativeAsset& NativeAsset::operator=(NativeAsset&& a) {
asset = a.asset;
a.asset = nullptr;
return *this;
}
NativeAsset::~NativeAsset() {
if (asset != nullptr) {
AAsset_close(asset);
}
}
bool NativeAsset::IsValid() { return asset != nullptr; }
NativeAsset::operator AAsset*() { return asset; }
} // namespace apk_utils

View File

@ -0,0 +1,43 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <string>
namespace apk_utils {
/**
* A wrapper class that represents a single asset from the app's assets.
* Loads and unloads the asset with the given name to memory.
*/
class NativeAsset {
AAsset* asset;
public:
NativeAsset(const char* name);
NativeAsset(NativeAsset&& a);
NativeAsset& operator=(NativeAsset&& a);
NativeAsset(const NativeAsset& a) = delete;
NativeAsset& operator=(const NativeAsset& a) = delete;
~NativeAsset();
bool IsValid();
operator AAsset*();
};
} // namespace apk_utils

View File

@ -0,0 +1,220 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "jni/jni_helper.h"
#include "jnictx.h"
namespace gamesdk {
namespace jni {
static jmethodID find_class_;
static LocalObject activity_class_loader_;
void InitActivityClassLoader() {
if (activity_class_loader_.IsNull()) {
jobject activity = AppContextGlobalRef();
jclass activity_clazz = Env()->GetObjectClass(activity);
jmethodID get_class_loader = Env()->GetMethodID(
activity_clazz, "getClassLoader", "()Ljava/lang/ClassLoader;");
activity_class_loader_ =
Env()->CallObjectMethod(activity, get_class_loader);
jclass class_loader = Env()->FindClass("java/lang/ClassLoader");
find_class_ = Env()->GetMethodID(
class_loader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
Env()->DeleteLocalRef(activity_clazz);
Env()->DeleteLocalRef(class_loader);
}
}
void Init(JNIEnv* env, jobject ctx) { Ctx::Init(env, ctx); }
void Destroy() { Ctx::Destroy(); }
bool IsValid() {
return Ctx::Instance() != nullptr && Ctx::Instance()->IsValid();
}
JNIEnv* Env() { return Ctx::Instance()->Env(); }
void DetachThread() { return Ctx::Instance()->DetachThread(); }
jobject AppContextGlobalRef() { return Ctx::Instance()->AppCtx(); }
jclass FindClass(const char* class_name) {
jclass jni_class = Env()->FindClass(class_name);
if (jni_class == NULL) {
InitActivityClassLoader();
// FindClass would have thrown.
Env()->ExceptionClear();
jstring class_jname = Env()->NewStringUTF(class_name);
jni_class = (jclass)(Env()->CallObjectMethod(activity_class_loader_,
find_class_, class_jname));
Env()->DeleteLocalRef(class_jname);
}
return jni_class;
}
LocalObject NewObjectV(const char* cclz, const char* ctorSig, va_list argptr) {
jclass clz = FindClass(cclz);
jmethodID constructor = Env()->GetMethodID(clz, "<init>", ctorSig);
jobject o = Env()->NewObjectV(clz, constructor, argptr);
return LocalObject(o, clz);
}
LocalObject NewObject(const char* cclz, const char* ctorSig, ...) {
va_list argptr;
va_start(argptr, ctorSig);
auto o = NewObjectV(cclz, ctorSig, argptr);
va_end(argptr);
return o;
}
jobject LocalObject::CallObjectMethod(const char* name, const char* sig,
...) const {
jmethodID mid = Env()->GetMethodID(clz_, name, sig);
va_list argptr;
va_start(argptr, sig);
jobject o = Env()->CallObjectMethodV(obj_, mid, argptr);
va_end(argptr);
return o;
}
jobject LocalObject::CallStaticObjectMethod(const char* name, const char* sig,
...) const {
jmethodID mid = Env()->GetStaticMethodID(clz_, name, sig);
va_list argptr;
va_start(argptr, sig);
jobject o = Env()->CallStaticObjectMethodV(clz_, mid, argptr);
va_end(argptr);
return o;
}
String LocalObject::CallStringMethod(const char* name, const char* sig,
...) const {
jmethodID mid = Env()->GetMethodID(clz_, name, sig);
va_list argptr;
va_start(argptr, sig);
jobject o = Env()->CallObjectMethodV(obj_, mid, argptr);
va_end(argptr);
String s((jstring)o);
return s;
}
void LocalObject::CallVoidMethod(const char* name, const char* sig, ...) const {
jmethodID mid = Env()->GetMethodID(clz_, name, sig);
va_list argptr;
va_start(argptr, sig);
Env()->CallVoidMethodV(obj_, mid, argptr);
va_end(argptr);
}
int LocalObject::CallIntMethod(const char* name, const char* sig, ...) const {
jmethodID mid = Env()->GetMethodID(clz_, name, sig);
va_list argptr;
va_start(argptr, sig);
int r = Env()->CallIntMethodV(obj_, mid, argptr);
va_end(argptr);
return r;
}
bool LocalObject::CallBooleanMethod(const char* name, const char* sig,
...) const {
jmethodID mid = Env()->GetMethodID(clz_, name, sig);
va_list argptr;
va_start(argptr, sig);
bool r = Env()->CallBooleanMethodV(obj_, mid, argptr);
va_end(argptr);
return r;
}
std::string GetExceptionMessage() {
std::string msg;
jthrowable exception = Env()->ExceptionOccurred();
Env()->ExceptionClear();
jclass oclass = FindClass("java/lang/Object");
jmethodID toString =
Env()->GetMethodID(oclass, "toString", "()Ljava/lang/String;");
jstring s = (jstring)Env()->CallObjectMethod(exception, toString);
const char* utf = Env()->GetStringUTFChars(s, nullptr);
msg = utf;
Env()->ReleaseStringUTFChars(s, utf);
Env()->DeleteLocalRef(oclass);
Env()->DeleteLocalRef(s);
Env()->DeleteLocalRef(exception);
return msg;
}
bool CheckForException(std::string& msg) {
if (Env()->ExceptionCheck()) {
msg = GetExceptionMessage();
return true;
}
return false;
}
LocalObject LocalObject::GetObjectField(const char* field_name,
const char* sig) const {
jfieldID fid = Env()->GetFieldID(clz_, field_name, sig);
if (!RawExceptionCheck()) {
auto out = Env()->GetObjectField(obj_, fid);
return LocalObject(out, nullptr);
} else {
return LocalObject(nullptr, nullptr);
}
}
int LocalObject::GetIntField(const char* field_name) const {
jfieldID fid = Env()->GetFieldID(clz_, field_name, "I");
if (!RawExceptionCheck())
return Env()->GetIntField(obj_, fid);
else
return BAD_FIELD;
}
bool LocalObject::GetBooleanField(const char* field_name) const {
jfieldID fid = Env()->GetFieldID(clz_, field_name, "Z");
if (!RawExceptionCheck())
return Env()->GetBooleanField(obj_, fid);
else
return false;
}
int64_t LocalObject::GetLongField(const char* field_name) const {
jfieldID fid = Env()->GetFieldID(clz_, field_name, "J");
if (!RawExceptionCheck())
return Env()->GetLongField(obj_, fid);
else
return BAD_FIELD;
}
std::vector<unsigned char> GetByteArrayBytesAndDeleteRef(jbyteArray jbs) {
jbyte* bs = Env()->GetByteArrayElements(jbs, 0);
std::vector<unsigned char> ret(bs, bs + Env()->GetArrayLength(jbs));
Env()->ReleaseByteArrayElements(jbs, bs, JNI_ABORT);
Env()->DeleteLocalRef(jbs);
return ret;
}
jni::String GetStaticStringField(const char* class_name,
const char* field_name) {
JNIEnv* env = Env();
LocalObject obj;
obj.Cast(class_name);
jclass clz = obj;
jfieldID fid = env->GetStaticFieldID(clz, field_name, "Ljava/lang/String;");
return (jstring)env->GetStaticObjectField(clz, fid);
}
#ifndef NDEBUG
void DumpLocalRefTable() {
JNIEnv* env = Env();
jclass vm_class = env->FindClass("dalvik/system/VMDebug");
jmethodID dump_mid =
env->GetStaticMethodID(vm_class, "dumpReferenceTables", "()V");
env->CallStaticVoidMethod(vm_class, dump_mid);
env->DeleteLocalRef(vm_class);
}
#endif
} // namespace jni
} // namespace gamesdk

View File

@ -0,0 +1,232 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <jni.h>
#include <string>
#include <vector>
#define CHECK_FOR_JNI_EXCEPTION_AND_RETURN(A) \
if (RawExceptionCheck()) { \
std::string exception_msg = GetExceptionMessage(); \
ALOGW("%s", exception_msg.c_str()); \
return A; \
}
namespace gamesdk {
namespace jni {
// Management of jni envs and the app context.
void Init(JNIEnv* env, jobject ctx);
void Destroy();
bool IsValid();
JNIEnv* Env();
void DetachThread();
jobject AppContextGlobalRef();
// It is the responsibility of the caller to delete the returned local
// reference.
jclass FindClass(const char* class_name);
// A wrapper around a jni jstring.
// Releases the jstring and any c string pointer generated from it upon
// destruction.
class String {
jstring j_str_;
const char* c_str_;
public:
String(const char* s) : j_str_(Env()->NewStringUTF(s)), c_str_(nullptr) {}
String(jstring s) : j_str_(s), c_str_(nullptr) {}
String(String&& rhs) : j_str_(rhs.j_str_), c_str_(rhs.c_str_) {
rhs.j_str_ = nullptr;
rhs.c_str_ = nullptr;
}
String(const String& rhs) : j_str_(rhs.j_str_), c_str_(nullptr) {
if (j_str_ != nullptr) {
j_str_ = reinterpret_cast<jstring>(Env()->NewLocalRef(j_str_));
}
}
String& operator=(const String& rhs) {
if (this != &rhs) {
Release();
if (rhs.j_str_ != nullptr) {
j_str_ =
reinterpret_cast<jstring>(Env()->NewLocalRef(rhs.j_str_));
}
}
return *this;
}
jstring J() const { return j_str_; }
const char* C() {
if (c_str_ == nullptr && j_str_ != nullptr) {
c_str_ = Env()->GetStringUTFChars(j_str_, nullptr);
}
return c_str_;
}
~String() { Release(); }
void Release() {
if (c_str_ != nullptr) {
Env()->ReleaseStringUTFChars(j_str_, c_str_);
c_str_ = nullptr;
}
if (j_str_ != nullptr) {
Env()->DeleteLocalRef(j_str_);
j_str_ = nullptr;
}
}
};
// This class takes ownership of the jni object and jni class and calls
// DeleteLocalRef on destruction. The copy constructor and l-value operator= are
// deleted to avoid creating and deleting
// local references unnecessarily. Use the r-value move versions instead.
// NB you cannot share these objects between threads. Create global refs in
// order to do that.
class LocalObject {
jobject obj_;
jclass clz_;
public:
static constexpr int BAD_FIELD = -1;
LocalObject(jobject o = nullptr, jclass c = nullptr) : obj_(o), clz_(c) {}
~LocalObject() { Release(); }
LocalObject(LocalObject&& o) : obj_(o.obj_), clz_(o.clz_) {
o.obj_ = nullptr;
o.clz_ = nullptr;
}
LocalObject(const LocalObject& o) = delete;
LocalObject& operator=(const LocalObject& o) = delete;
LocalObject& operator=(LocalObject&& o) {
if (obj_ != o.obj_) {
if (obj_ != nullptr) {
Env()->DeleteLocalRef(obj_);
}
obj_ = o.obj_;
}
if (clz_ != o.clz_) {
if (clz_ != nullptr) {
Env()->DeleteLocalRef(clz_);
}
clz_ = o.clz_;
}
o.obj_ = nullptr;
o.clz_ = nullptr;
return *this;
}
jobject ObjNewRef() const {
if (obj_ != nullptr) return Env()->NewLocalRef(obj_);
return obj_;
}
jclass ClassNewRef() const {
if (clz_ != nullptr) return (jclass)Env()->NewLocalRef(clz_);
return clz_;
}
jobjectArray AsObjectArray() const {
return reinterpret_cast<jobjectArray>(obj_);
}
bool IsNull() const { return obj_ == nullptr; }
bool ClassIsNull() const { return clz_ == nullptr; }
operator jobject() const { return obj_; }
operator jclass() const { return clz_; }
void SetObj(jobject o) {
if (obj_ != nullptr) Env()->DeleteLocalRef(obj_);
obj_ = o;
}
void SetClass(jclass c) {
if (clz_ != nullptr) Env()->DeleteLocalRef(clz_);
clz_ = c;
}
// Set clz_ to the class with the given name or get the class from the
// object if clz_to is missing / empty.
bool Cast(const std::string& clz_to = "") {
jclass c;
if (clz_to.empty()) {
if (obj_ == nullptr)
c = nullptr;
else
c = Env()->GetObjectClass(obj_);
} else {
c = FindClass(clz_to.c_str());
}
if (c == nullptr) return false;
SetClass(c);
return true;
}
// These methods take a variable number of arguments and have the return the
// type indicated. The arguments are passed directly to JNI and it's not
// type-safe, so:
// All object arguments should be jobjects, NOT LocalObject.
// All string arguments should be jstrings, NOT String.
jobject CallObjectMethod(const char* name, const char* sig, ...) const;
jobject CallStaticObjectMethod(const char* name, const char* sig,
...) const;
jni::String CallStringMethod(const char* name, const char* sig, ...) const;
void CallVoidMethod(const char* name, const char* sig, ...) const;
int CallIntMethod(const char* name, const char* sig, ...) const;
bool CallBooleanMethod(const char* name, const char* sig, ...) const;
// Returns a null object if the field could not be found (and exception will
// be set)
LocalObject GetObjectField(const char* field_name, const char* sig) const;
// Returns BAD_FIELD is the field could not be found (and exception is set)
int GetIntField(const char* field_name) const;
bool GetBooleanField(const char* field_name) const;
int64_t GetLongField(const char* field_name) const;
private:
void Release() {
if (clz_ != nullptr) {
Env()->DeleteLocalRef(clz_);
}
if (obj_ != nullptr) {
Env()->DeleteLocalRef(obj_);
}
obj_ = nullptr;
clz_ = nullptr;
}
};
LocalObject NewObjectV(const char* cclz, const char* ctorSig, va_list argptr);
LocalObject NewObject(const char* cclz, const char* ctorSig, ...);
inline bool RawExceptionCheck() { return Env()->ExceptionCheck(); }
// This will clear the exception and get the exception message.
std::string GetExceptionMessage();
// Do a RawExceptionCheck and return the result of it, filling in the msg with
// the
// exception message if one was thrown. Also clears the exception.
bool CheckForException(std::string& msg);
std::vector<unsigned char> GetByteArrayBytesAndDeleteRef(jbyteArray jbs);
jni::String GetStaticStringField(const char* class_name,
const char* field_name);
// Debugging
#ifndef NDEBUG
void DumpLocalRefTable();
#else
inline void DumpLocalRefTable() {}
#endif
} // namespace jni
} // namespace gamesdk

View File

@ -0,0 +1,35 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "jni/jni_wrap.h"
namespace gamesdk {
namespace jni {
android::content::Context AppContext() {
return Env()->NewLocalRef(AppContextGlobalRef());
}
namespace android {
namespace os {
constexpr const char Build::class_name[];
}
} // namespace android
} // namespace jni
} // namespace gamesdk

View File

@ -0,0 +1,639 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <sstream>
#include "jni/jni_helper.h"
namespace gamesdk {
namespace jni {
namespace java {
class Object {
public:
LocalObject obj_;
Object(const char* className, const char* ctorSig, ...) {
va_list argptr;
va_start(argptr, ctorSig);
obj_ = NewObjectV(className, ctorSig, argptr);
va_end(argptr);
}
Object(LocalObject&& o) : obj_(std::move(o)) {}
Object(jobject o) {
// If there's an exception pending, don't store the reference as it may
// be invalid and cause a crash on some devices when released.
if (!RawExceptionCheck()) {
obj_ = o;
obj_.Cast(); // Cast to the object's own class.
}
}
bool valid() const { return !obj_.ClassIsNull(); }
void CallVVMethod(const char* name) { obj_.CallVoidMethod(name, "()V"); }
void CallIVMethod(const char* name, int a) {
obj_.CallVoidMethod(name, "(I)V", a);
}
int CallVIMethod(const char* name) {
return obj_.CallIntMethod(name, "()I");
}
int CallIIMethod(const char* name, const int a) {
return obj_.CallIntMethod(name, "(I)I", a);
}
void CallZVMethod(const char* name, bool a) {
obj_.CallVoidMethod(name, "(Z)V", a);
}
void CallSVMethod(const char* name, const char* a) {
obj_.CallVoidMethod(name, "(Ljava/lang/String;)V", String(a).J());
}
String CallVSMethod(const char* name) {
return obj_.CallStringMethod(name, "()Ljava/lang/String;");
}
void CallSSVMethod(const char* name, const char* a, const char* b) {
obj_.CallVoidMethod(name, "(Ljava/lang/String;Ljava/lang/String;)V",
String(a).J(), String(b).J());
}
Object CallAOMethod(const char* name, const char* returnClass) {
std::stringstream str;
str << "()[L" << returnClass << ";";
jobject o = obj_.CallObjectMethod(name, str.str().c_str());
return Object(o);
}
Object CallVOMethod(const char* name, const char* returnClass) {
std::stringstream str;
str << "()L" << returnClass << ";";
jobject o = obj_.CallObjectMethod(name, str.str().c_str());
return Object(o);
}
Object CallIOMethod(const char* name, int a, const char* returnClass) {
std::stringstream str;
str << "(I)L" << returnClass << ";";
jobject o = obj_.CallObjectMethod(name, str.str().c_str(), a);
return Object(o);
}
void CallOVMethod(const char* name, const char* parameterClassA,
const Object& a) {
std::stringstream str;
str << "(L" << parameterClassA << ";)V";
obj_.CallVoidMethod(name, str.str().c_str(), (jobject)a.obj_);
}
Object CallSIOMethod(const char* name, const char* a, int b,
const char* returnClass) {
std::stringstream str;
str << "(Ljava/lang/String;I)L" << returnClass << ";";
jobject o =
obj_.CallObjectMethod(name, str.str().c_str(), String(a).J(), b);
return Object(o);
}
Object CallOOOMethod(const char* name, const char* parameterClassA,
const Object& a, const char* parameterClassB,
const Object& b, const char* returnClass) {
std::stringstream str;
str << "(L" << parameterClassA << ";L" << parameterClassB << ";)L"
<< returnClass << ";";
jobject o = obj_.CallObjectMethod(name, str.str().c_str(),
(jobject)a.obj_, (jobject)b.obj_);
return Object(o);
}
int CallSIIMethod(const char* name, const char* a, const int b) {
std::stringstream str;
str << "(Ljava/lang/String;I)I";
return obj_.CallIntMethod(name, "(Ljava/lang/String;I)I", String(a).J(),
b);
}
Object CallSIIOMethod(const char* name, const char* a, int b, int c,
const char* returnClass) {
std::stringstream str;
str << "(Ljava/lang/String;II)L" << returnClass << ";";
jobject o =
obj_.CallObjectMethod(name, str.str().c_str(), String(a).J(), b, c);
return Object(o);
}
Object CallSOMethod(const char* name, const char* a,
const char* returnClass) {
std::stringstream str;
str << "(Ljava/lang/String;)L" << returnClass << ";";
jobject o =
obj_.CallObjectMethod(name, str.str().c_str(), String(a).J());
return Object(o);
}
bool CallVZMethod(const char* name) {
return obj_.CallBooleanMethod(name, "()Z");
}
bool IsNull() const { return obj_.IsNull(); }
};
namespace io {
class OutputStream : public Object {
public:
OutputStream(Object&& o) : Object(std::move(o)) {}
void close() { CallVVMethod("close"); }
};
class OutputStreamWriter : public Object {
public:
OutputStreamWriter(OutputStream& o, const std::string& s)
: Object("java/io/OutputStreamWriter",
"(Ljava/io/OutputStream;Ljava/lang/String;)V", (jobject)o.obj_,
String(s.c_str()).J()) {}
};
class Writer : public Object {
public:
Writer(Object&& o) : Object(std::move(o)) {}
void write(const std::string& s) { CallSVMethod("write", s.c_str()); }
void flush() { CallVVMethod("flush"); }
void close() { CallVVMethod("close"); }
};
class BufferedWriter : public Writer {
public:
BufferedWriter(const Writer& w)
: Writer(Object("java/io/BufferedWriter", "(Ljava/io/Writer;)V",
(jobject)w.obj_)) {}
};
class InputStream : public Object {
public:
InputStream(Object&& o) : Object(std::move(o)) {}
void close() { CallVVMethod("close"); }
};
class Reader : public Object {
public:
Reader(Object&& o) : Object(std::move(o)) {}
jni::String readLine() { return CallVSMethod("readLine"); }
void close() { CallVVMethod("close"); }
};
class InputStreamReader : public Reader {
public:
InputStreamReader(const InputStream& is, const std::string& s)
: Reader(Object("java/io/InputStreamReader",
"(Ljava/io/InputStream;Ljava/lang/String;)V",
(jobject)is.obj_, String(s.c_str()).J())) {}
};
class BufferedReader : public Reader {
public:
BufferedReader(const Reader& r)
: Reader(Object("java/io/BufferedReader", "(Ljava/io/Reader;)V",
(jobject)r.obj_)) {}
};
class File : public Object {
public:
File(Object&& o) : Object(std::move(o)) {}
jni::String getPath() { return CallVSMethod("getPath"); }
};
} // namespace io
namespace util {
class UUID : public Object {
public:
UUID(LocalObject&& o) : Object(std::move(o)) {}
static UUID randomUUID() {
LocalObject obj(nullptr, Env()->FindClass("java/util/UUID"));
auto o = obj.CallStaticObjectMethod("randomUUID", "()Ljava/util/UUID;");
obj.SetObj(o);
return obj;
}
jni::String toString() { return CallVSMethod("toString"); }
};
class List : public java::Object {
public:
List(Object&& o) : Object(std::move(o)) {}
java::Object get(int index) {
return CallIOMethod("get", index, "java/lang/Object");
}
bool isEmpty() { return CallVZMethod("isEmpty"); }
jni::String toString() { return CallVSMethod("toString"); }
};
} // namespace util
namespace net {
class URLConnection : public Object {
public:
URLConnection(Object&& o) : Object(std::move(o)) {}
};
class HttpURLConnection : public URLConnection {
public:
HttpURLConnection(URLConnection&& u) : URLConnection(std::move(u)) {
obj_.Cast("java/net/HttpURLConnection");
}
void setRequestMethod(const std::string& method) {
CallSVMethod("setRequestMethod", method.c_str());
}
void setConnectTimeout(int t) { CallIVMethod("setConnectTimeout", t); }
void setReadTimeout(int timeout) {
CallIVMethod("setReadTimeout", timeout);
}
void setDoOutput(bool d) { CallZVMethod("setDoOutput", d); }
void setDoInput(bool d) { CallZVMethod("setDoInput", d); }
void setUseCaches(bool d) { CallZVMethod("setUseCaches", d); }
void setRequestProperty(const std::string& name, const std::string& value) {
CallSSVMethod("setRequestProperty", name.c_str(), value.c_str());
}
io::OutputStream getOutputStream() {
return CallVOMethod("getOutputStream", "java/io/OutputStream");
}
void connect() { CallVVMethod("connect"); }
void disconnect() { CallVVMethod("disconnect"); }
int getResponseCode() { return CallVIMethod("getResponseCode"); }
jni::String getResponseMessage() {
return CallVSMethod("getResponseMessage");
}
io::InputStream getInputStream() {
return CallVOMethod("getInputStream", "java/io/InputStream");
}
};
class URL : public Object {
public:
URL(LocalObject o) : Object(o) {}
URL(const std::string& s)
: Object("java/net/URL", "(Ljava/lang/String;)V",
String(s.c_str()).J()) {}
URLConnection openConnection() {
return URLConnection(Object(obj_.CallObjectMethod(
"openConnection", "()Ljava/net/URLConnection;")));
}
};
} // namespace net
namespace security {
class MessageDigest : public java::Object {
public:
MessageDigest(const std::string& instance) : java::Object(nullptr) {
LocalObject temp(nullptr, FindClass("java/security/MessageDigest"));
auto o = temp.CallStaticObjectMethod(
"getInstance", "(Ljava/lang/String;)Ljava/security/MessageDigest;",
String(instance.c_str()).J());
temp.SetObj(o);
obj_ = std::move(temp);
}
std::vector<unsigned char> digest(
const std::vector<unsigned char>& bs) const {
auto env = Env();
jbyteArray jbs = env->NewByteArray(bs.size());
env->SetByteArrayRegion(jbs, 0, bs.size(),
reinterpret_cast<const jbyte*>(bs.data()));
jbyteArray out = reinterpret_cast<jbyteArray>(
obj_.CallObjectMethod("digest", "([B)[B", jbs));
env->DeleteLocalRef(jbs);
return GetByteArrayBytesAndDeleteRef(out);
}
};
} // namespace security
} // namespace java
namespace android {
namespace content {
namespace pm {
class FeatureInfo : public java::Object {
public:
FeatureInfo(java::Object&& o) : java::Object(std::move(o)) {
jni::String jname(
(jstring)obj_.GetObjectField("name", "Ljava/lang/String;")
.ObjNewRef());
if (jname.J() != nullptr) name = jname.C();
reqGlEsVersion = obj_.GetIntField("reqGlEsVersion");
}
static constexpr int GL_ES_VERSION_UNDEFINED = 0x0000000;
std::string name;
int reqGlEsVersion;
};
class ApplicationInfo : public java::Object {
public:
ApplicationInfo(java::Object&& o) : java::Object(std::move(o)) {}
int flags() const { return obj_.GetIntField("flags"); }
static const int FLAG_DEBUGGABLE = 2;
};
class PackageInfo : public java::Object {
public:
PackageInfo(java::Object&& o) : java::Object(std::move(o)) {}
typedef std::vector<unsigned char> Signature;
std::vector<Signature> signatures() const {
auto env = Env();
auto jsigs = obj_.GetObjectField("signatures",
"[Landroid/content/pm/Signature;");
jobjectArray sigs = jsigs.AsObjectArray();
if (sigs == nullptr) return {};
int n = env->GetArrayLength(sigs);
if (n > 0) {
std::vector<std::vector<unsigned char>> ret;
for (int i = 0; i < n; ++i) {
Object sig(env->GetObjectArrayElement(sigs, i));
jbyteArray bytes = reinterpret_cast<jbyteArray>(
sig.obj_.CallObjectMethod("toByteArray", "()[B"));
ret.push_back(GetByteArrayBytesAndDeleteRef(bytes));
}
return ret;
} else
return {};
}
int versionCode() const { return obj_.GetIntField("versionCode"); }
ApplicationInfo applicationInfo() const {
auto appInfo = obj_.GetObjectField(
"applicationInfo", "[Landroid/content/pm/ApplicationInfo;");
return ApplicationInfo(std::move(appInfo));
}
};
class PackageManager : public java::Object {
public:
PackageManager(java::Object&& o) : java::Object(std::move(o)) {}
static constexpr int GET_SIGNATURES = 0x0000040;
PackageInfo getPackageInfo(const std::string& name, int flags) {
return CallSIOMethod("getPackageInfo", name.c_str(), flags,
"android/content/pm/PackageInfo");
}
std::vector<FeatureInfo> getSystemAvailableFeatures() {
auto env = Env();
auto jfeatures = CallAOMethod("getSystemAvailableFeatures",
"android/content/pm/FeatureInfo");
if (jfeatures.obj_.IsNull()) return {};
jobjectArray features = jfeatures.obj_.AsObjectArray();
int n = env->GetArrayLength(features);
std::vector<FeatureInfo> ret;
if (n > 0) {
for (int i = 0; i < n; ++i) {
FeatureInfo f(Object(env->GetObjectArrayElement(features, i)));
ret.push_back(std::move(f));
}
}
return ret;
}
};
} // namespace pm
namespace res {
class AssetManager : public java::Object {
public:
AssetManager(java::Object&& o) : java::Object(std::move(o)) {}
};
} // namespace res
class Intent : java::Object {
public:
static constexpr const char* ACTION_BATTERY_CHANGED =
"android.intent.action.BATTERY_CHANGED";
Intent(java::Object&& o) : java::Object(std::move(o)) {}
int getIntExtra(const char* name, int defaultValue) {
return CallSIIMethod("getIntExtra", name, defaultValue);
}
};
class IntentFilter : public java::Object {
public:
IntentFilter(jobject o) : java::Object(o) {}
IntentFilter(const char* action)
: Object("android/content/IntentFilter", "(Ljava/lang/String;)V",
String(action).J()) {}
};
class BroadcastReceiver : public java::Object {
public:
BroadcastReceiver(java::Object&& o) : java::Object(std::move(o)) {}
};
class Context : public java::Object {
public:
static constexpr const char* CONNECTIVITY_SERVICE = "connectivity";
static constexpr const char* BATTERY_SERVICE = "batterymanager";
static constexpr const char* POWER_SERVICE = "power";
static constexpr const char* ACTIVITY_SERVICE = "activity";
Context(jobject o) : java::Object(o) {}
pm::PackageManager getPackageManager() {
return CallVOMethod("getPackageManager",
"android/content/pm/PackageManager");
}
jni::String getPackageName() { return CallVSMethod("getPackageName"); }
res::AssetManager getAssets() {
return CallVOMethod("getAssets", "android/content/res/AssetManager");
}
java::io::File getCacheDir() {
return CallVOMethod("getCacheDir", "java/io/File");
}
java::Object getSystemService(const char* name) {
return CallSOMethod("getSystemService", name, "java/lang/Object");
}
java::Object registerReceiver(BroadcastReceiver& broadcastReceiver,
IntentFilter& intentFilter) {
return CallOOOMethod("registerReceiver",
"android/content/BroadcastReceiver",
broadcastReceiver, "android/content/IntentFilter",
intentFilter, "android/content/Intent");
}
};
} // namespace content
namespace os {
class DebugClass {
jmethodID getNativeHeapAllocatedSize_method_id_;
public:
static uint64_t getNativeHeapAllocatedSize() {
JNIEnv* env = Env();
if (env != nullptr) {
LocalObject obj;
obj.Cast("android/os/Debug");
jclass clz = obj;
jmethodID method = env->GetStaticMethodID(
clz, "getNativeHeapAllocatedSize", "()J");
if (method != NULL)
return (uint64_t)env->CallStaticLongMethod(clz, method);
}
return 0;
}
static uint64_t getNativeHeapFreeSize() {
JNIEnv* env = Env();
if (env != nullptr) {
LocalObject obj;
obj.Cast("android/os/Debug");
jclass clz = obj;
jmethodID method =
env->GetStaticMethodID(clz, "getNativeHeapFreeSize", "()J");
if (method != NULL)
return (uint64_t)env->CallStaticLongMethod(clz, method);
}
return 0;
}
static uint64_t getNativeHeapSize() {
JNIEnv* env = Env();
if (env != nullptr) {
LocalObject obj;
obj.Cast("android/os/Debug");
jclass clz = obj;
jmethodID method =
env->GetStaticMethodID(clz, "getNativeHeapSize", "()J");
if (method != NULL)
return (uint64_t)env->CallStaticLongMethod(clz, method);
}
return 0;
}
static uint64_t getPss() {
JNIEnv* env = Env();
if (env != nullptr) {
LocalObject obj;
obj.Cast("android/os/Debug");
jclass clz = obj;
jmethodID method = env->GetStaticMethodID(clz, "getPss", "()J");
if (method != NULL)
return (uint64_t)env->CallStaticLongMethod(clz, method);
}
return 0;
}
}; // class Debug
class Build {
static constexpr const char class_name[] = "android/os/Build";
public:
static jni::String MODEL() {
return GetStaticStringField(class_name, "MODEL");
}
static jni::String BRAND() {
return GetStaticStringField(class_name, "BRAND");
}
static jni::String PRODUCT() {
return GetStaticStringField(class_name, "PRODUCT");
}
static jni::String DEVICE() {
return GetStaticStringField(class_name, "DEVICE");
}
static jni::String FINGERPRINT() {
return GetStaticStringField(class_name, "FINGERPRINT");
}
static jni::String SOC_MODEL() {
return GetStaticStringField(class_name, "SOC_MODEL");
}
static jni::String SOC_MANUFACTURER() {
return GetStaticStringField(class_name, "SOC_MANUFACTURER");
}
}; // Class Build
class BatteryManager : java::Object {
public:
static constexpr const char* EXTRA_LEVEL = "level";
static constexpr const char* EXTRA_SCALE = "scale";
static constexpr const char* EXTRA_PLUGGED = "plugged";
static constexpr const int BATTERY_PROPERTY_CHARGE_COUNTER = 1;
BatteryManager(java::Object&& o) : java::Object(std::move(o)) {}
int getIntProperty(int id) { return CallIIMethod("getIntProperty", id); }
};
class PowerManager : java::Object {
public:
PowerManager(java::Object&& o) : java::Object(std::move(o)) {}
int getCurrentThermalStatus() {
return CallVIMethod("getCurrentThermalStatus");
}
bool isPowerSaveMode() { return CallVZMethod("isPowerSaveMode"); }
};
} // namespace os
namespace net {
class ConnectivityManager : java::Object {
public:
ConnectivityManager(java::Object&& o) : java::Object(std::move(o)) {}
// NB This requires Manifest.permission.ACCESS_NETWORK_STATE.
bool isActiveNetworkMetered() {
return CallVZMethod("isActiveNetworkMetered");
}
};
} // namespace net
namespace app {
class MemoryInfo : public java::Object {
public:
MemoryInfo(java::Object&& o) : java::Object(std::move(o)) {}
MemoryInfo()
: java::Object("android/app/ActivityManager$MemoryInfo", "()V") {}
int64_t threshold() const { return obj_.GetLongField("threshold"); }
int64_t availMem() const { return obj_.GetLongField("availMem"); }
bool lowMemory() const { return obj_.GetBooleanField("lowMemory"); }
int64_t totalMem() const { return obj_.GetLongField("totalMem"); }
};
class ActivityManager : public java::Object {
public:
ActivityManager(java::Object&& o) : java::Object(std::move(o)) {}
void getMemoryInfo(MemoryInfo& memoryInfo) {
CallOVMethod("getMemoryInfo", "android/app/ActivityManager$MemoryInfo",
memoryInfo);
}
int32_t getMemoryClass() { return CallVIMethod("getMemoryClass"); }
int32_t getLargeMemoryClass() {
return CallVIMethod("getLargeMemoryClass");
}
bool isLowRamDevice() { return CallVZMethod("isLowRamDevice"); }
java::util::List getHistoricalProcessExitReasons(std::string& packageName,
int pid, int maxNum) {
return CallSIIOMethod("getHistoricalProcessExitReasons",
packageName.c_str(), pid, maxNum,
"java/util/List");
}
};
class ApplicationExitInfo : java::Object {
public:
static constexpr int REASON_LOW_MEMORY = 3;
ApplicationExitInfo(java::Object&& o) : java::Object(std::move(o)) {}
int getReason() { return CallVIMethod("getReason"); }
};
} // namespace app
} // namespace android
// A local jni reference to the app context
android::content::Context AppContext();
} // namespace jni
} // namespace gamesdk

View File

@ -0,0 +1,75 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "jnictx.h"
#include <memory>
#include "Log.h"
#define LOG_TAG "JniCtx"
namespace gamesdk {
namespace jni {
static std::unique_ptr<Ctx> theCtx;
static thread_local JNIEnv* theEnv = nullptr;
/*static*/ const Ctx* Ctx::Init(JNIEnv* env, jobject ctx) {
theCtx = std::unique_ptr<Ctx>(new Ctx(env, ctx, ConstructorTag{}));
theEnv = env;
return theCtx.get();
}
/*static*/ const Ctx* Ctx::Instance() {
if (theCtx.get() == nullptr) {
ALOGE("You must call jni::Ctx::Init before using any jni::Ctx methods");
}
return theCtx.get();
}
/*static*/ void Ctx::Destroy() { theCtx.reset(); }
Ctx::Ctx(JNIEnv* env, jobject ctx, ConstructorTag) {
if (env) {
jctx_ = env->NewGlobalRef(ctx);
env->GetJavaVM(&jvm_);
}
}
Ctx::~Ctx() {
if (jctx_) {
JNIEnv* env = Env();
if (env) {
env->DeleteGlobalRef(jctx_);
}
}
}
JNIEnv* Ctx::Env() const {
if (theEnv == nullptr && jvm_ != nullptr) {
jvm_->AttachCurrentThread(&theEnv, NULL);
}
return theEnv;
}
void Ctx::DetachThread() const {
if (jvm_ != nullptr) jvm_->DetachCurrentThread();
theEnv = nullptr;
}
} // namespace jni
} // namespace gamesdk

View File

@ -0,0 +1,53 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <jni.h>
namespace gamesdk {
namespace jni {
// Singleton class that stores an app's JVM and context
class Ctx {
private:
// Allows construction with std::unique_ptr from a static method, but
// disallows construction outside of the class since no one else can
// construct a ConstructorTag
struct ConstructorTag {};
JavaVM* jvm_;
jobject jctx_; // Global reference to the app's context
public:
static const Ctx* Init(JNIEnv* env, jobject ctx);
static void Destroy();
static const Ctx* Instance();
Ctx(JNIEnv* env, jobject ctx, ConstructorTag);
~Ctx();
JNIEnv* Env() const;
JavaVM* Jvm() const { return jvm_; }
jobject AppCtx() const { return jctx_; }
bool IsValid() const { return jvm_ != nullptr && jctx_ != nullptr; }
void DetachThread() const;
Ctx() = delete;
Ctx(const Ctx&) = delete;
Ctx(Ctx&&) = delete;
Ctx& operator=(const Ctx& rhs) = delete;
};
} // namespace jni
} // namespace gamesdk

View File

@ -0,0 +1,72 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "system_utils.h"
#include <android/api-level.h>
#include <stdlib.h>
#include <sys/system_properties.h>
namespace gamesdk {
#if __ANDROID_API__ >= 26
std::string getSystemPropViaCallback(const char* key,
const char* default_value = "") {
const prop_info* prop = __system_property_find(key);
if (prop == nullptr) {
return default_value;
}
std::string return_value;
auto thunk = [](void* cookie, const char* /*name*/, const char* value,
uint32_t /*serial*/) {
if (value != nullptr) {
std::string* r = static_cast<std::string*>(cookie);
*r = value;
}
};
__system_property_read_callback(prop, thunk, &return_value);
return return_value;
}
#else
std::string getSystemPropViaGet(const char* key,
const char* default_value = "") {
char buffer[PROP_VALUE_MAX + 1] = ""; // +1 for terminator
int bufferLen = __system_property_get(key, buffer);
if (bufferLen > 0)
return buffer;
else
return "";
}
#endif
std::string GetSystemProp(const char* key, const char* default_value) {
#if __ANDROID_API__ >= 26
return getSystemPropViaCallback(key, default_value);
#else
return getSystemPropViaGet(key, default_value);
#endif
}
int GetSystemPropAsInt(const char* key, int default_value) {
std::string prop = GetSystemProp(key);
return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10);
}
bool GetSystemPropAsBool(const char* key, bool default_value) {
return GetSystemPropAsInt(key, default_value) != 0;
}
} // namespace gamesdk

View File

@ -0,0 +1,32 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "string"
namespace gamesdk {
// Get the value of the given system property
std::string GetSystemProp(const char* key, const char* default_value = "");
// Get the value of the given system property as an integer
int GetSystemPropAsInt(const char* key, int default_value = 0);
// Get the value of the given system property as a bool
bool GetSystemPropAsBool(const char* key, bool default_value = false);
} // namespace gamesdk

View File

@ -0,0 +1,121 @@
project(swappy C CXX)
set(CMAKE_CXX_STANDARD 14)
set(IgnoreOldToolchainWarning "${ANDROID_UNIFIED_HEADERS}")
# set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror -Wthread-safety" )
# set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D _LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS -O3 -fPIC" )
# set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions" )
# set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti" )
# set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffunction-sections -fdata-sections" )
# set( CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -g0")
# if ( DEFINED GAMESDK_THREAD_CHECKS )
# set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DGAMESDK_THREAD_CHECKS=${GAMESDK_THREAD_CHECKS}" )
# endif()
# set( CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections" )
# set( CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-s" )
# set( CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--hash-style=both" )
set ( SOURCE_LOCATION ${CMAKE_CURRENT_LIST_DIR})
set ( SOURCE_LOCATION_COMMON "${SOURCE_LOCATION}/common" )
set ( SOURCE_LOCATION_OPENGL "${SOURCE_LOCATION}/opengl" )
set ( SOURCE_LOCATION_VULKAN "${SOURCE_LOCATION}/vulkan" )
include_directories( ${CMAKE_CURRENT_LIST_DIR}/../../include )
include_directories( ${CMAKE_CURRENT_LIST_DIR}/../common )
include_directories( ${CMAKE_CURRENT_LIST_DIR}/common )
include_directories( ${ANDROID_NDK}/sources/third_party/vulkan/src/common )
if(CC_USE_GLES3 or CC_USE_GLES2)
include_directories( ${CMAKE_CURRENT_LIST_DIR}/opengl )
endif()
if(CC_USE_VULKAN)
include_directories( ${CMAKE_CURRENT_LIST_DIR}/vulkan )
endif()
message( STATUS "Building swappy_static to ${CMAKE_CURRENT_BINARY_DIR}/build" )
# Dex linking requires an extra option for later versions of clang lld
if (ANDROID_NDK_MAJOR GREATER 22)
if ( ANDROID_NDK_ABI_NAME MATCHES "armeabi-v7a")
set (LINKER_TARGET_EMULATION_OPTION "-m" "armelf_linux_eabi")
elseif(ANDROID_NDK_ABI_NAME MATCHES "arm64-v8a")
set (LINKER_TARGET_EMULATION_OPTION "-m" "aarch64linux")
elseif(ANDROID_NDK_ABI_NAME MATCHES "x86_64")
set (LINKER_TARGET_EMULATION_OPTION "-m" "elf_x86_64")
elseif(ANDROID_NDK_ABI_NAME MATCHES "x86")
set (LINKER_TARGET_EMULATION_OPTION "-m" "elf_i386")
endif()
endif()
add_custom_command(OUTPUT classes_dex.o
COMMAND cd ../intermediates/dex/release/mergeDexRelease/out && ${CMAKE_LINKER} ${LINKER_TARGET_EMULATION_OPTION} -r -b binary -o ${CMAKE_CURRENT_BINARY_DIR}/classes_dex.o classes.dex
WORKING_DIRECTORY ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}
)
set_source_files_properties(
classes_dex.o
PROPERTIES
EXTERNAL_OBJECT true
GENERATED true
)
set(SRC_FILE
${SOURCE_LOCATION_COMMON}/ChoreographerFilter.cpp
${SOURCE_LOCATION_COMMON}/ChoreographerThread.cpp
${SOURCE_LOCATION_COMMON}/CpuInfo.cpp
${SOURCE_LOCATION_COMMON}/Settings.cpp
${SOURCE_LOCATION_COMMON}/Thread.cpp
${SOURCE_LOCATION_COMMON}/SwappyCommon.cpp
${SOURCE_LOCATION_COMMON}/swappy_c.cpp
${SOURCE_LOCATION_COMMON}/SwappyDisplayManager.cpp
${SOURCE_LOCATION_COMMON}/CPUTracer.cpp
${SOURCE_LOCATION_OPENGL}/FrameStatisticsGL.cpp
${SOURCE_LOCATION}/../common/system_utils.cpp)
if(CC_USE_GLES3 or CC_USE_GLES2)
list(APPEND SRC_FILE
${SOURCE_LOCATION_OPENGL}/EGL.cpp
${SOURCE_LOCATION_OPENGL}/swappyGL_c.cpp
${SOURCE_LOCATION_OPENGL}/SwappyGL.cpp)
endif()
if(CC_USE_VULKAN)
list(APPEND SRC_FILE
${SOURCE_LOCATION_VULKAN}/swappyVk_c.cpp
${SOURCE_LOCATION_VULKAN}/SwappyVk.cpp
${SOURCE_LOCATION_VULKAN}/SwappyVkBase.cpp
${SOURCE_LOCATION_VULKAN}/SwappyVkFallback.cpp
${SOURCE_LOCATION_VULKAN}/SwappyVkGoogleDisplayTiming.cpp)
endif()
add_library( swappy_static
STATIC
${SRC_FILE}
# ${CMAKE_CURRENT_BINARY_DIR}/classes_dex.o
# Add new source files here
)
set_target_properties( swappy_static PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/build )
add_library( swappy
SHARED
${SOURCE_LOCATION_COMMON}/swappy_c.cpp
${SOURCE_LOCATION_OPENGL}/swappyGL_c.cpp
${SOURCE_LOCATION_VULKAN}/swappyVk_c.cpp)
target_link_libraries( swappy
swappy_static
android
GLESv2
log
atomic)

View File

@ -0,0 +1,4 @@
stoza@google.com
adyabr@google.com
ianelliott@google.com

View File

@ -0,0 +1,81 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "CPUTracer.h"
#include <memory>
#include "../../common/Log.h"
#include "../../common/Trace.h"
namespace swappy {
CPUTracer::CPUTracer() {}
CPUTracer::~CPUTracer() { joinThread(); }
void CPUTracer::joinThread() {
bool join = false;
if (mThread && mThread->joinable()) {
std::lock_guard<std::mutex> lock(mMutex);
mTrace = false;
mRunning = false;
mCond.notify_one();
join = true;
}
if (join) {
mThread->join();
}
mThread.reset();
}
void CPUTracer::startTrace() {
if (TRACE_ENABLED()) {
std::lock_guard<std::mutex> lock(mMutex);
if (!mThread) {
mRunning = true;
mThread = std::make_unique<Thread>([this]() { threadMain(); });
}
mTrace = true;
mCond.notify_one();
} else {
joinThread();
}
}
void CPUTracer::endTrace() {
if (TRACE_ENABLED()) {
std::lock_guard<std::mutex> lock(mMutex);
mTrace = false;
mCond.notify_one();
} else {
joinThread();
}
}
void CPUTracer::threadMain() NO_THREAD_SAFETY_ANALYSIS {
std::unique_lock<std::mutex> lock(mMutex);
while (mRunning) {
if (mTrace) {
gamesdk::ScopedTrace trace("Swappy: CPU frame time");
mCond.wait(lock);
} else {
mCond.wait(lock);
}
}
}
} // namespace swappy

View File

@ -0,0 +1,48 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <condition_variable>
#include <memory>
#include <mutex>
#include "Thread.h"
namespace swappy {
class CPUTracer {
public:
CPUTracer();
~CPUTracer();
CPUTracer(CPUTracer&) = delete;
void startTrace();
void endTrace();
private:
void threadMain();
void joinThread();
std::mutex mMutex;
std::condition_variable_any mCond GUARDED_BY(mMutex);
std::unique_ptr<Thread> mThread;
bool mRunning GUARDED_BY(mMutex) = true;
bool mTrace GUARDED_BY(mMutex) = false;
};
} // namespace swappy

View File

@ -0,0 +1,239 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ChoreographerFilter.h"
#define LOG_TAG "ChoreographerFilter"
#include <sched.h>
#include <unistd.h>
#include <deque>
#include <string>
#include <thread>
#include "Log.h"
#include "Settings.h"
#include "Thread.h"
#include "Trace.h"
using namespace std::chrono_literals;
using time_point = std::chrono::steady_clock::time_point;
namespace {
class Timer {
public:
Timer(std::chrono::nanoseconds refreshPeriod,
std::chrono::nanoseconds appToSfDelay)
: mRefreshPeriod(refreshPeriod), mAppToSfDelay(appToSfDelay) {}
// Returns false if we have detected that we have received the same
// timestamp multiple times so that the caller can wait for fresh timestamps
bool addTimestamp(time_point point) {
// Keep track of the previous timestamp and how many times we've seen it
// to determine if we've stopped receiving Choreographer callbacks,
// which would indicate that we should probably stop until we see them
// again (e.g., if the app has been moved to the background)
if (point == mLastTimestamp) {
if (mRepeatCount++ > 5) {
return false;
}
} else {
mRepeatCount = 0;
}
mLastTimestamp = point;
point += mAppToSfDelay;
bool moreThanOneRefreshPeriodElapsed =
mBaseTime + mRefreshPeriod * 1.5 < point;
if (moreThanOneRefreshPeriodElapsed) {
do {
mBaseTime += mRefreshPeriod;
} while (mBaseTime + mRefreshPeriod * 1.5 < point);
mBaseTime += mRefreshPeriod;
// Long waits pollute the filter so don't adjust refreshPeriod.
return true;
}
std::chrono::nanoseconds delta = (point - (mBaseTime + mRefreshPeriod));
if (delta < -mRefreshPeriod / 2) {
// Also ignore short intervals
return true;
}
// Exponential smoothing factor = 0.04 avoids roughness.
mRefreshPeriod += delta / 25;
mBaseTime += mRefreshPeriod;
return true;
}
void sleep(std::chrono::nanoseconds offset) {
if (offset < -(mRefreshPeriod / 2) || offset > mRefreshPeriod / 2) {
offset = 0ms;
}
const auto now = std::chrono::steady_clock::now();
auto targetTime = mBaseTime + mRefreshPeriod + offset;
while (targetTime < now) {
targetTime += mRefreshPeriod;
}
std::this_thread::sleep_until(targetTime);
}
private:
std::chrono::nanoseconds mRefreshPeriod;
const std::chrono::nanoseconds mAppToSfDelay;
time_point mBaseTime = std::chrono::steady_clock::now();
time_point mLastTimestamp = std::chrono::steady_clock::now();
int32_t mRepeatCount = 0;
};
} // anonymous namespace
namespace swappy {
ChoreographerFilter::ChoreographerFilter(std::chrono::nanoseconds refreshPeriod,
std::chrono::nanoseconds appToSfDelay,
Worker doWork)
: mRefreshPeriod(refreshPeriod),
mAppToSfDelay(appToSfDelay),
mDoWork(doWork) {
Settings::getInstance()->addListener([this]() { onSettingsChanged(); });
std::lock_guard<std::mutex> lock(mThreadPoolMutex);
mUseAffinity = Settings::getInstance()->getUseAffinity();
launchThreadsLocked();
}
ChoreographerFilter::~ChoreographerFilter() {
std::lock_guard<std::mutex> lock(mThreadPoolMutex);
terminateThreadsLocked();
}
void ChoreographerFilter::onChoreographer() {
std::lock_guard<std::mutex> lock(mMutex);
mLastTimestamp = std::chrono::steady_clock::now();
++mSequenceNumber;
mCondition.notify_all();
}
void ChoreographerFilter::launchThreadsLocked() {
{
std::lock_guard<std::mutex> lock(mMutex);
mIsRunning = true;
}
const int32_t numThreads = getNumCpus() > 2 ? 2 : 1;
for (int32_t thread = 0; thread < numThreads; ++thread) {
mThreadPool.push_back(
Thread([this, thread]() { threadMain(mUseAffinity, thread); }));
}
}
void ChoreographerFilter::terminateThreadsLocked() {
{
std::lock_guard<std::mutex> lock(mMutex);
mIsRunning = false;
mCondition.notify_all();
}
for (auto& thread : mThreadPool) {
thread.join();
}
mThreadPool.clear();
}
void ChoreographerFilter::onSettingsChanged() {
const bool useAffinity = Settings::getInstance()->getUseAffinity();
const Settings::DisplayTimings& displayTimings =
Settings::getInstance()->getDisplayTimings();
std::lock_guard<std::mutex> lock(mThreadPoolMutex);
if (useAffinity == mUseAffinity &&
mRefreshPeriod == displayTimings.refreshPeriod) {
return;
}
terminateThreadsLocked();
mUseAffinity = useAffinity;
mRefreshPeriod = displayTimings.refreshPeriod;
mAppToSfDelay = displayTimings.sfOffset - displayTimings.appOffset;
ALOGV(
"onSettingsChanged(): refreshPeriod=%lld, appOffset=%lld, "
"sfOffset=%lld",
(long long)displayTimings.refreshPeriod.count(),
(long long)displayTimings.appOffset.count(),
(long long)displayTimings.sfOffset.count());
launchThreadsLocked();
}
void ChoreographerFilter::threadMain(bool useAffinity, int32_t thread) {
Timer timer(mRefreshPeriod, mAppToSfDelay);
{
int cpu = getNumCpus() - 1 - thread;
if (cpu >= 0) {
setAffinity(cpu);
}
}
std::string threadName = "Filter";
threadName += swappy::to_string(thread);
pthread_setname_np(pthread_self(), threadName.c_str());
std::unique_lock<std::mutex> lock(mMutex);
while (true) {
auto timestamp = mLastTimestamp;
auto workDuration = mWorkDuration;
lock.unlock();
// If we have received the same timestamp multiple times, it probably
// means that the app has stopped sending them to us, which could
// indicate that it's no longer running. If we detect that, we stop
// until we see a fresh timestamp to avoid spinning forever in the
// background.
if (!timer.addTimestamp(timestamp)) {
lock.lock();
mCondition.wait(lock, [=]() {
return !mIsRunning || (mLastTimestamp != timestamp);
});
timestamp = mLastTimestamp;
lock.unlock();
timer.addTimestamp(timestamp);
}
if (!mIsRunning) break;
timer.sleep(-workDuration);
{
std::unique_lock<std::mutex> workLock(mWorkMutex);
const auto now = std::chrono::steady_clock::now();
if (now - mLastWorkRun > mRefreshPeriod / 2) {
// Assume we got here first and there's work to do
gamesdk::ScopedTrace trace("doWork");
mWorkDuration = mDoWork();
mLastWorkRun = now;
}
}
lock.lock();
}
}
} // namespace swappy

View File

@ -0,0 +1,66 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <condition_variable>
#include <mutex>
#include <vector>
#include "Settings.h"
#include "Thread.h"
namespace swappy {
class ChoreographerFilter {
public:
using Worker = std::function<std::chrono::nanoseconds()>;
explicit ChoreographerFilter(std::chrono::nanoseconds refreshPeriod,
std::chrono::nanoseconds appToSfDelay,
Worker doWork);
~ChoreographerFilter();
void onChoreographer();
private:
void launchThreadsLocked();
void terminateThreadsLocked();
void onSettingsChanged();
void threadMain(bool useAffinity, int32_t thread);
std::mutex mThreadPoolMutex;
bool mUseAffinity = true;
std::vector<Thread> mThreadPool;
std::mutex mMutex;
std::condition_variable mCondition;
bool mIsRunning = true;
int64_t mSequenceNumber = 0;
std::chrono::steady_clock::time_point mLastTimestamp;
std::mutex mWorkMutex;
std::chrono::steady_clock::time_point mLastWorkRun;
std::chrono::nanoseconds mWorkDuration;
std::chrono::nanoseconds mRefreshPeriod;
std::chrono::nanoseconds mAppToSfDelay;
const Worker mDoWork;
};
} // namespace swappy

View File

@ -0,0 +1,520 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "ChoreographerThread"
#include "ChoreographerThread.h"
#include <android/looper.h>
#include <jni.h>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>
#include <cmath>
#include <condition_variable>
#include <cstdlib>
#include <cstring>
#include <thread>
#include "ChoreographerShim.h"
#include "CpuInfo.h"
#include "JNIUtil.h"
#include "Log.h"
#include "Settings.h"
#include "Thread.h"
#include "Trace.h"
namespace swappy {
// AChoreographer is supported from API 24. To allow compilation for minSDK < 24
// and still use AChoreographer for SDK >= 24 we need runtime support to call
// AChoreographer APIs.
using PFN_AChoreographer_getInstance = AChoreographer *(*)();
using PFN_AChoreographer_postFrameCallback =
void (*)(AChoreographer *choreographer,
AChoreographer_frameCallback callback, void *data);
using PFN_AChoreographer_postFrameCallbackDelayed = void (*)(
AChoreographer *choreographer, AChoreographer_frameCallback callback,
void *data, long delayMillis);
using PFN_AChoreographer_registerRefreshRateCallback =
void (*)(AChoreographer *choreographer,
AChoreographer_refreshRateCallback callback, void *data);
using PFN_AChoreographer_unregisterRefreshRateCallback =
void (*)(AChoreographer *choreographer,
AChoreographer_refreshRateCallback callback, void *data);
// Forward declaration of the native method of Java Choreographer class
extern "C" {
JNIEXPORT void JNICALL
Java_com_google_androidgamesdk_ChoreographerCallback_nOnChoreographer(
JNIEnv * /*env*/, jobject /*this*/, jlong cookie, jlong /*frameTimeNanos*/);
}
class NDKChoreographerThread : public ChoreographerThread {
public:
static constexpr int MIN_SDK_VERSION = 24;
NDKChoreographerThread(Callback onChoreographer,
Callback onRefreshRateChanged);
~NDKChoreographerThread() override;
private:
void looperThread();
void scheduleNextFrameCallback() override REQUIRES(mWaitingMutex);
PFN_AChoreographer_getInstance mAChoreographer_getInstance = nullptr;
PFN_AChoreographer_postFrameCallback mAChoreographer_postFrameCallback =
nullptr;
PFN_AChoreographer_postFrameCallbackDelayed
mAChoreographer_postFrameCallbackDelayed = nullptr;
PFN_AChoreographer_registerRefreshRateCallback
mAChoreographer_registerRefreshRateCallback = nullptr;
PFN_AChoreographer_unregisterRefreshRateCallback
mAChoreographer_unregisterRefreshRateCallback = nullptr;
void *mLibAndroid = nullptr;
Thread mThread;
std::condition_variable mWaitingCondition;
ALooper *mLooper GUARDED_BY(mWaitingMutex) = nullptr;
bool mThreadRunning GUARDED_BY(mWaitingMutex) = false;
AChoreographer *mChoreographer GUARDED_BY(mWaitingMutex) = nullptr;
Callback mOnRefreshRateChanged;
};
NDKChoreographerThread::NDKChoreographerThread(Callback onChoreographer,
Callback onRefreshRateChanged)
: ChoreographerThread(onChoreographer),
mOnRefreshRateChanged(onRefreshRateChanged) {
mLibAndroid = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL);
if (mLibAndroid == nullptr) {
ALOGE("FATAL: cannot open libandroid.so: %s", strerror(errno));
return;
}
mAChoreographer_getInstance =
reinterpret_cast<PFN_AChoreographer_getInstance>(
dlsym(mLibAndroid, "AChoreographer_getInstance"));
mAChoreographer_postFrameCallback =
reinterpret_cast<PFN_AChoreographer_postFrameCallback>(
dlsym(mLibAndroid, "AChoreographer_postFrameCallback"));
mAChoreographer_postFrameCallbackDelayed =
reinterpret_cast<PFN_AChoreographer_postFrameCallbackDelayed>(
dlsym(mLibAndroid, "AChoreographer_postFrameCallbackDelayed"));
mAChoreographer_registerRefreshRateCallback =
reinterpret_cast<PFN_AChoreographer_registerRefreshRateCallback>(
dlsym(mLibAndroid, "AChoreographer_registerRefreshRateCallback"));
mAChoreographer_unregisterRefreshRateCallback =
reinterpret_cast<PFN_AChoreographer_unregisterRefreshRateCallback>(
dlsym(mLibAndroid, "AChoreographer_unregisterRefreshRateCallback"));
if (!mAChoreographer_getInstance || !mAChoreographer_postFrameCallback ||
!mAChoreographer_postFrameCallbackDelayed) {
ALOGE("FATAL: cannot get AChoreographer symbols");
return;
}
std::unique_lock<std::mutex> lock(mWaitingMutex);
// create a new ALooper thread to get Choreographer events
mThreadRunning = true;
mThread = Thread([this]() { looperThread(); });
mWaitingCondition.wait(lock, [&]() REQUIRES(mWaitingMutex) {
return mChoreographer != nullptr;
});
mInitialized = true;
}
NDKChoreographerThread::~NDKChoreographerThread() {
ALOGI("Destroying NDKChoreographerThread");
if (mLibAndroid != nullptr) dlclose(mLibAndroid);
{
std::lock_guard<std::mutex> lock(mWaitingMutex);
if (!mLooper) {
return;
}
ALooper_acquire(mLooper);
mThreadRunning = false;
ALooper_wake(mLooper);
}
mThread.join();
ALooper_release(mLooper);
}
void NDKChoreographerThread::looperThread() {
int outFd, outEvents;
void *outData;
std::lock_guard<std::mutex> lock(mWaitingMutex);
mLooper = ALooper_prepare(0);
if (!mLooper) {
ALOGE("ALooper_prepare failed");
return;
}
mChoreographer = mAChoreographer_getInstance();
if (!mChoreographer) {
ALOGE("AChoreographer_getInstance failed");
return;
}
AChoreographer_refreshRateCallback callback = [](int64_t vsyncPeriodNanos,
void *data) {
reinterpret_cast<NDKChoreographerThread *>(data)
->mOnRefreshRateChanged();
};
if (mAChoreographer_registerRefreshRateCallback && mOnRefreshRateChanged) {
mAChoreographer_registerRefreshRateCallback(mChoreographer, callback,
this);
}
mWaitingCondition.notify_all();
const char *name = "SwappyChoreographer";
CpuInfo cpu;
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
if (cpu.getNumberOfCpus() > 0) {
ALOGI("Swappy found %d CPUs [%s].", cpu.getNumberOfCpus(),
cpu.getHardware().c_str());
if (cpu.getNumberOfLittleCores() > 0) {
cpu_set = cpu.getLittleCoresMask();
}
}
const auto tid = gettid();
ALOGI("Setting '%s' thread [%d-0x%x] affinity mask to 0x%x.", name, tid,
tid, to_mask(cpu_set));
sched_setaffinity(tid, sizeof(cpu_set), &cpu_set);
pthread_setname_np(pthread_self(), name);
while (mThreadRunning) {
// mutex should be unlocked before sleeping on pollAll
mWaitingMutex.unlock();
ALooper_pollAll(-1, &outFd, &outEvents, &outData);
mWaitingMutex.lock();
}
if (mAChoreographer_unregisterRefreshRateCallback &&
mOnRefreshRateChanged) {
mAChoreographer_unregisterRefreshRateCallback(mChoreographer, callback,
this);
}
ALOGI("Terminating Looper thread");
return;
}
void NDKChoreographerThread::scheduleNextFrameCallback() {
AChoreographer_frameCallback frameCallback = [](long frameTimeNanos,
void *data) {
reinterpret_cast<NDKChoreographerThread *>(data)->onChoreographer();
};
mAChoreographer_postFrameCallbackDelayed(mChoreographer, frameCallback,
this, 1);
}
class JavaChoreographerThread : public ChoreographerThread {
public:
JavaChoreographerThread(JavaVM *vm, jobject jactivity,
Callback onChoreographer);
~JavaChoreographerThread() override;
static void onChoreographer(jlong cookie);
void onChoreographer() override { ChoreographerThread::onChoreographer(); };
private:
void scheduleNextFrameCallback() override REQUIRES(mWaitingMutex);
JavaVM *mJVM;
jobject mJobj = nullptr;
jmethodID mJpostFrameCallback = nullptr;
jmethodID mJterminate = nullptr;
};
JavaChoreographerThread::JavaChoreographerThread(JavaVM *vm, jobject jactivity,
Callback onChoreographer)
: ChoreographerThread(onChoreographer), mJVM(vm) {
if (!vm || !jactivity) {
return;
}
JNIEnv *env;
mJVM->AttachCurrentThread(&env, nullptr);
jclass choreographerCallbackClass = gamesdk::loadClass(
env, jactivity, ChoreographerThread::CT_CLASS,
(JNINativeMethod *)ChoreographerThread::CTNativeMethods,
ChoreographerThread::CTNativeMethodsSize);
if (!choreographerCallbackClass) return;
jmethodID constructor =
env->GetMethodID(choreographerCallbackClass, "<init>", "(J)V");
mJpostFrameCallback = env->GetMethodID(choreographerCallbackClass,
"postFrameCallback", "()V");
mJterminate =
env->GetMethodID(choreographerCallbackClass, "terminate", "()V");
jobject choreographerCallback = env->NewObject(
choreographerCallbackClass, constructor, reinterpret_cast<jlong>(this));
mJobj = env->NewGlobalRef(choreographerCallback);
mInitialized = true;
}
JavaChoreographerThread::~JavaChoreographerThread() {
ALOGI("Destroying JavaChoreographerThread");
if (!mJobj) {
return;
}
JNIEnv *env;
// Check if we need to attach and only detach if we do.
jint result =
mJVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_2);
if (result != JNI_OK) {
if (result == JNI_EVERSION) {
result =
mJVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_1);
}
if (result == JNI_EDETACHED) {
mJVM->AttachCurrentThread(&env, nullptr);
}
}
env->CallVoidMethod(mJobj, mJterminate);
env->DeleteGlobalRef(mJobj);
if (result == JNI_EDETACHED) {
mJVM->DetachCurrentThread();
}
}
void JavaChoreographerThread::scheduleNextFrameCallback() {
JNIEnv *env;
mJVM->AttachCurrentThread(&env, nullptr);
env->CallVoidMethod(mJobj, mJpostFrameCallback);
}
void JavaChoreographerThread::onChoreographer(jlong cookie) {
JavaChoreographerThread *me =
reinterpret_cast<JavaChoreographerThread *>(cookie);
me->onChoreographer();
}
extern "C" {
JNIEXPORT void JNICALL
Java_com_google_androidgamesdk_ChoreographerCallback_nOnChoreographer(
JNIEnv * /*env*/, jobject /*this*/, jlong cookie,
jlong /*frameTimeNanos*/) {
JavaChoreographerThread::onChoreographer(cookie);
}
} // extern "C"
class NoChoreographerThread : public ChoreographerThread {
public:
NoChoreographerThread(Callback onChoreographer);
~NoChoreographerThread();
private:
void postFrameCallbacks() override;
void scheduleNextFrameCallback() override REQUIRES(mWaitingMutex);
void looperThread();
void onSettingsChanged();
Thread mThread;
bool mThreadRunning GUARDED_BY(mWaitingMutex);
std::condition_variable_any mWaitingCondition GUARDED_BY(mWaitingMutex);
std::chrono::nanoseconds mRefreshPeriod GUARDED_BY(mWaitingMutex);
};
NoChoreographerThread::NoChoreographerThread(Callback onChoreographer)
: ChoreographerThread(onChoreographer) {
std::lock_guard<std::mutex> lock(mWaitingMutex);
Settings::getInstance()->addListener([this]() { onSettingsChanged(); });
mThreadRunning = true;
mThread = Thread([this]() { looperThread(); });
mInitialized = true;
}
NoChoreographerThread::~NoChoreographerThread() {
ALOGI("Destroying NoChoreographerThread");
{
std::lock_guard<std::mutex> lock(mWaitingMutex);
mThreadRunning = false;
}
mWaitingCondition.notify_all();
mThread.join();
}
void NoChoreographerThread::onSettingsChanged() {
const Settings::DisplayTimings &displayTimings =
Settings::getInstance()->getDisplayTimings();
std::lock_guard<std::mutex> lock(mWaitingMutex);
mRefreshPeriod = displayTimings.refreshPeriod;
ALOGV("onSettingsChanged(): refreshPeriod=%lld",
(long long)displayTimings.refreshPeriod.count());
}
void NoChoreographerThread::looperThread() {
const char *name = "SwappyChoreographer";
CpuInfo cpu;
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
if (cpu.getNumberOfCpus() > 0) {
ALOGI("Swappy found %d CPUs [%s].", cpu.getNumberOfCpus(),
cpu.getHardware().c_str());
if (cpu.getNumberOfLittleCores() > 0) {
cpu_set = cpu.getLittleCoresMask();
}
}
const auto tid = gettid();
ALOGI("Setting '%s' thread [%d-0x%x] affinity mask to 0x%x.", name, tid,
tid, to_mask(cpu_set));
sched_setaffinity(tid, sizeof(cpu_set), &cpu_set);
pthread_setname_np(pthread_self(), name);
auto wakeTime = std::chrono::steady_clock::now();
while (true) {
{
// mutex should be unlocked before sleeping
std::lock_guard<std::mutex> lock(mWaitingMutex);
if (!mThreadRunning) {
break;
}
mWaitingCondition.wait(mWaitingMutex);
if (!mThreadRunning) {
break;
}
const auto timePassed = std::chrono::steady_clock::now() - wakeTime;
const int intervals = std::floor(timePassed / mRefreshPeriod);
wakeTime += (intervals + 1) * mRefreshPeriod;
}
std::this_thread::sleep_until(wakeTime);
mCallback();
}
ALOGI("Terminating choreographer thread");
}
void NoChoreographerThread::postFrameCallbacks() {
std::lock_guard<std::mutex> lock(mWaitingMutex);
mWaitingCondition.notify_one();
}
void NoChoreographerThread::scheduleNextFrameCallback() {}
const char *ChoreographerThread::CT_CLASS =
"com/google/androidgamesdk/ChoreographerCallback";
const JNINativeMethod ChoreographerThread::CTNativeMethods[] = {
{"nOnChoreographer", "(JJ)V",
(void
*)&Java_com_google_androidgamesdk_ChoreographerCallback_nOnChoreographer}};
ChoreographerThread::ChoreographerThread(Callback onChoreographer)
: mCallback(onChoreographer) {}
ChoreographerThread::~ChoreographerThread() = default;
void ChoreographerThread::postFrameCallbacks() {
TRACE_CALL();
// This method is called before calling to swap buffers
// It registers to get MAX_CALLBACKS_BEFORE_IDLE frame callbacks before
// going idle so if app goes to idle the thread will not get further frame
// callbacks
std::lock_guard<std::mutex> lock(mWaitingMutex);
if (mCallbacksBeforeIdle == 0) {
scheduleNextFrameCallback();
}
mCallbacksBeforeIdle = MAX_CALLBACKS_BEFORE_IDLE;
}
void ChoreographerThread::onChoreographer() {
TRACE_CALL();
{
std::lock_guard<std::mutex> lock(mWaitingMutex);
mCallbacksBeforeIdle--;
if (mCallbacksBeforeIdle > 0) {
scheduleNextFrameCallback();
}
}
mCallback();
}
std::unique_ptr<ChoreographerThread>
ChoreographerThread::createChoreographerThread(Type type, JavaVM *vm,
jobject jactivity,
Callback onChoreographer,
Callback onRefreshRateChanged,
SdkVersion sdkVersion) {
if (type == Type::App) {
ALOGI("Using Application's Choreographer");
return std::make_unique<NoChoreographerThread>(onChoreographer);
}
if (vm == nullptr ||
sdkVersion.sdkInt >= NDKChoreographerThread::MIN_SDK_VERSION) {
ALOGI("Using NDK Choreographer");
const auto usingDisplayManager =
SwappyDisplayManager::useSwappyDisplayManager(sdkVersion);
const auto refreshRateCallback =
usingDisplayManager ? Callback() : onRefreshRateChanged;
return std::make_unique<NDKChoreographerThread>(onChoreographer,
refreshRateCallback);
}
if (vm != nullptr && jactivity != nullptr) {
std::unique_ptr<ChoreographerThread> javaChoreographerThread =
std::make_unique<JavaChoreographerThread>(vm, jactivity,
onChoreographer);
if (javaChoreographerThread->isInitialized()) {
ALOGI("Using Java Choreographer");
return javaChoreographerThread;
}
}
ALOGI("Using no Choreographer (Best Effort)");
return std::make_unique<NoChoreographerThread>(onChoreographer);
}
} // namespace swappy

View File

@ -0,0 +1,67 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <jni.h>
#include <mutex>
#include "SwappyDisplayManager.h"
#include "Thread.h"
namespace swappy {
class ChoreographerThread {
public:
enum class Type {
// choreographer ticks are provided by application
App,
// register internally with choreographer
Swappy,
};
static const char* CT_CLASS;
static const JNINativeMethod CTNativeMethods[];
static constexpr int CTNativeMethodsSize = 1;
using Callback = std::function<void()>;
static std::unique_ptr<ChoreographerThread> createChoreographerThread(
Type type, JavaVM* vm, jobject jactivity, Callback onChoreographer,
Callback onRefreshRateChanged, SdkVersion sdkVersion);
virtual ~ChoreographerThread() = 0;
virtual void postFrameCallbacks();
bool isInitialized() { return mInitialized; }
protected:
ChoreographerThread(Callback onChoreographer);
virtual void scheduleNextFrameCallback() REQUIRES(mWaitingMutex) = 0;
virtual void onChoreographer();
std::mutex mWaitingMutex;
int mCallbacksBeforeIdle GUARDED_BY(mWaitingMutex) = 0;
Callback mCallback;
bool mInitialized = false;
static constexpr int MAX_CALLBACKS_BEFORE_IDLE = 10;
};
} // namespace swappy

View File

@ -0,0 +1,155 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "Swappy"
#include "CpuInfo.h"
#include <bitset>
#include <cstdlib>
#include <cstring>
#include <limits>
#include "Log.h"
namespace {
bool startsWith(std::string &mainStr, const char *toMatch) {
// std::string::find returns 0 if toMatch is found at beginning
return mainStr.find(toMatch) == 0;
}
std::vector<std::string> split(const std::string &s, char c) {
std::vector<std::string> v;
std::string::size_type i = 0;
std::string::size_type j = s.find(c);
while (j != std::string::npos) {
v.push_back(s.substr(i, j - i));
i = ++j;
j = s.find(c, j);
if (j == std::string::npos) {
v.push_back(s.substr(i, s.length()));
}
}
return v;
}
std::string ReadFile(const std::string &path) {
char buf[10240];
FILE *fp = fopen(path.c_str(), "r");
if (fp == nullptr) return std::string();
fgets(buf, 10240, fp);
fclose(fp);
return std::string(buf);
}
} // anonymous namespace
namespace swappy {
std::string to_string(int n) {
constexpr int kBufSize = 12; // strlen("2147483648")+1
static char buf[kBufSize];
snprintf(buf, kBufSize, "%d", n);
return buf;
}
CpuInfo::CpuInfo() {
const auto BUFFER_LENGTH = 10240;
char buf[BUFFER_LENGTH];
FILE *fp = fopen("/proc/cpuinfo", "r");
if (!fp) {
return;
}
long mMaxFrequency = 0;
long mMinFrequency = std::numeric_limits<long>::max();
while (fgets(buf, BUFFER_LENGTH, fp) != NULL) {
buf[strlen(buf) - 1] = '\0'; // eat the newline fgets() stores
std::string line = buf;
if (startsWith(line, "processor")) {
Cpu core;
core.id = mCpus.size();
auto core_path =
std::string("/sys/devices/system/cpu/cpu") + to_string(core.id);
auto package_id =
ReadFile(core_path + "/topology/physical_package_id");
auto frequency = ReadFile(core_path + "/cpufreq/cpuinfo_max_freq");
core.package_id = atol(package_id.c_str());
core.frequency = atol(frequency.c_str());
mMinFrequency = std::min(mMinFrequency, core.frequency);
mMaxFrequency = std::max(mMaxFrequency, core.frequency);
mCpus.push_back(core);
} else if (startsWith(line, "Hardware")) {
mHardware = split(line, ':')[1];
}
}
fclose(fp);
CPU_ZERO(&mLittleCoresMask);
CPU_ZERO(&mBigCoresMask);
for (auto cpu : mCpus) {
if (cpu.frequency == mMinFrequency) {
++mNumberOfLittleCores;
cpu.type = Cpu::Type::Little;
CPU_SET(cpu.id, &mLittleCoresMask);
} else {
++mNumberOfBigCores;
cpu.type = Cpu::Type::Big;
CPU_SET(cpu.id, &mBigCoresMask);
}
}
}
unsigned int CpuInfo::getNumberOfCpus() const { return mCpus.size(); }
const std::vector<CpuInfo::Cpu> &CpuInfo::getCpus() const { return mCpus; }
const std::string CpuInfo::getHardware() const { return mHardware; }
unsigned int CpuInfo::getNumberOfLittleCores() const {
return mNumberOfLittleCores;
}
unsigned int CpuInfo::getNumberOfBigCores() const { return mNumberOfBigCores; }
cpu_set_t CpuInfo::getLittleCoresMask() const { return mLittleCoresMask; }
cpu_set_t CpuInfo::getBigCoresMask() const { return mBigCoresMask; }
unsigned int to_mask(cpu_set_t cpu_set) {
std::bitset<32> mask;
for (int i = 0; i < CPU_SETSIZE; ++i) {
if (CPU_ISSET(i, &cpu_set)) mask[i] = 1;
}
return (int)mask.to_ulong();
}
} // namespace swappy

View File

@ -0,0 +1,65 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <sched.h>
#include <map>
#include <string>
#include <vector>
namespace swappy {
class CpuInfo {
public:
struct Cpu {
enum class Type { Little, Big };
int id;
int package_id;
long frequency;
Type type;
};
CpuInfo();
unsigned int getNumberOfCpus() const;
const std::vector<Cpu>& getCpus() const;
const std::string getHardware() const;
unsigned int getNumberOfLittleCores() const;
unsigned int getNumberOfBigCores() const;
cpu_set_t getLittleCoresMask() const;
cpu_set_t getBigCoresMask() const;
private:
std::vector<Cpu> mCpus;
std::string mHardware;
unsigned int mNumberOfLittleCores = 0;
unsigned int mNumberOfBigCores = 0;
cpu_set_t mLittleCoresMask;
cpu_set_t mBigCoresMask;
};
unsigned int to_mask(cpu_set_t cpu_set);
} // namespace swappy

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <swappy/swappyGL_extra.h>
namespace swappy {
class FrameStatistics {
public:
virtual ~FrameStatistics() {}
virtual int32_t lastLatencyRecorded() const = 0;
// Only the essential latency statistics, not full.
virtual bool isEssential() const = 0;
virtual SwappyStats getStats() = 0;
};
} // namespace swappy

View File

@ -0,0 +1,96 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "Settings.h"
#define LOG_TAG "Settings"
#include "Log.h"
namespace swappy {
std::unique_ptr<Settings> Settings::instance;
Settings* Settings::getInstance() {
if (!instance) {
instance = std::make_unique<Settings>(ConstructorTag{});
}
return instance.get();
}
void Settings::reset() { instance.reset(); }
void Settings::addListener(Listener listener) {
std::lock_guard<std::mutex> lock(mMutex);
mListeners.emplace_back(std::move(listener));
}
void Settings::setDisplayTimings(const DisplayTimings& displayTimings) {
{
std::lock_guard<std::mutex> lock(mMutex);
mDisplayTimings = displayTimings;
}
// Notify the listeners without the lock held
notifyListeners();
}
void Settings::setSwapDuration(uint64_t swapNs) {
{
std::lock_guard<std::mutex> lock(mMutex);
mSwapDuration = std::chrono::nanoseconds(swapNs);
}
// Notify the listeners without the lock held
notifyListeners();
}
void Settings::setUseAffinity(bool tf) {
{
std::lock_guard<std::mutex> lock(mMutex);
mUseAffinity = tf;
}
// Notify the listeners without the lock held
notifyListeners();
}
const Settings::DisplayTimings& Settings::getDisplayTimings() const {
std::lock_guard<std::mutex> lock(mMutex);
return mDisplayTimings;
}
std::chrono::nanoseconds Settings::getSwapDuration() const {
std::lock_guard<std::mutex> lock(mMutex);
return mSwapDuration;
}
bool Settings::getUseAffinity() const {
std::lock_guard<std::mutex> lock(mMutex);
return mUseAffinity;
}
void Settings::notifyListeners() {
// Grab a local copy of the listeners
std::vector<Listener> listeners;
{
std::lock_guard<std::mutex> lock(mMutex);
listeners = mListeners;
}
// Call the listeners without the lock held
for (const auto& listener : listeners) {
listener();
}
}
} // namespace swappy

View File

@ -0,0 +1,75 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <chrono>
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "Thread.h"
namespace swappy {
class Settings {
private:
// Allows construction with std::unique_ptr from a static method, but
// disallows construction outside of the class since no one else can
// construct a ConstructorTag
struct ConstructorTag {};
public:
struct DisplayTimings {
std::chrono::nanoseconds refreshPeriod{0};
std::chrono::nanoseconds appOffset{0};
std::chrono::nanoseconds sfOffset{0};
};
explicit Settings(ConstructorTag){};
static Settings* getInstance();
static void reset();
using Listener = std::function<void()>;
void addListener(Listener listener);
void setDisplayTimings(const DisplayTimings& displayTimings);
void setSwapDuration(uint64_t swapNs);
void setUseAffinity(bool);
const DisplayTimings& getDisplayTimings() const;
std::chrono::nanoseconds getSwapDuration() const;
bool getUseAffinity() const;
private:
void notifyListeners();
static std::unique_ptr<Settings> instance;
mutable std::mutex mMutex;
std::vector<Listener> mListeners GUARDED_BY(mMutex);
DisplayTimings mDisplayTimings GUARDED_BY(mMutex);
std::chrono::nanoseconds mSwapDuration GUARDED_BY(mMutex) =
std::chrono::nanoseconds(16'666'667L);
bool mUseAffinity GUARDED_BY(mMutex) = true;
};
} // namespace swappy

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,368 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <jni.h>
#include <atomic>
#include <chrono>
#include <deque>
#include <list>
#include <memory>
#include <mutex>
#include "CPUTracer.h"
#include "ChoreographerFilter.h"
#include "ChoreographerThread.h"
#include "FrameStatistics.h"
#include "SwappyDisplayManager.h"
#include "Thread.h"
#include "swappy/swappyGL.h"
#include "swappy/swappyGL_extra.h"
namespace swappy {
// ANativeWindow_setFrameRate is supported from API 30. To allow compilation for
// minSDK < 30 we need runtime support to call this API.
using PFN_ANativeWindow_setFrameRate = int32_t (*)(ANativeWindow* window,
float frameRate,
int8_t compatibility);
using namespace std::chrono_literals;
struct SwappyCommonSettings {
SdkVersion sdkVersion;
std::chrono::nanoseconds refreshPeriod;
std::chrono::nanoseconds appVsyncOffset;
std::chrono::nanoseconds sfVsyncOffset;
static bool getFromApp(JNIEnv* env, jobject jactivity,
SwappyCommonSettings* out);
static SdkVersion getSDKVersion(JNIEnv* env);
static bool queryDisplayTimings(JNIEnv* env, jobject jactivity,
SwappyCommonSettings* out);
};
// Common part between OpenGL and Vulkan implementations.
class SwappyCommon {
public:
enum class PipelineMode { Off, On };
// callbacks to be called during pre/post swap
struct SwapHandlers {
std::function<bool()> lastFrameIsComplete;
std::function<std::chrono::nanoseconds()> getPrevFrameGpuTime;
};
SwappyCommon(JNIEnv* env, jobject jactivity);
~SwappyCommon();
std::chrono::nanoseconds getSwapDuration();
void onChoreographer(int64_t frameTimeNanos);
void onPreSwap(const SwapHandlers& h);
bool needToSetPresentationTime() { return mPresentationTimeNeeded; }
void onPostSwap(const SwapHandlers& h);
PipelineMode getCurrentPipelineMode() { return mPipelineMode; }
template <typename... T>
struct Tracer {
void (*function)(void*, T...);
void* userData;
};
void addTracerCallbacks(const SwappyTracer& tracer);
void removeTracerCallbacks(const SwappyTracer& tracer);
void setAutoSwapInterval(bool enabled);
void setAutoPipelineMode(bool enabled);
void setMaxAutoSwapDuration(std::chrono::nanoseconds swapDuration) {
mAutoSwapIntervalThreshold = swapDuration;
}
std::chrono::steady_clock::time_point getPresentationTime() {
return mPresentationTime;
}
std::chrono::nanoseconds getRefreshPeriod() const {
return mCommonSettings.refreshPeriod;
}
bool isValid() { return mValid; }
std::chrono::nanoseconds getFenceTimeout() const { return mFenceTimeout; }
void setFenceTimeout(std::chrono::nanoseconds t) { mFenceTimeout = t; }
bool isDeviceUnsupported();
void setANativeWindow(ANativeWindow* window);
void setFrameStatistics(
const std::shared_ptr<FrameStatistics>& frameStats) {
mFrameStatistics = frameStats;
}
void setBufferStuffingFixWait(int32_t nFrames) {
mBufferStuffingFixWait = std::max(0, nFrames);
}
protected:
// Used for testing
SwappyCommon(const SwappyCommonSettings& settings);
private:
class FrameDuration {
public:
FrameDuration() = default;
FrameDuration(std::chrono::nanoseconds cpuTime,
std::chrono::nanoseconds gpuTime,
bool frameMissedDeadline)
: mCpuTime(cpuTime),
mGpuTime(gpuTime),
mFrameMissedDeadline(frameMissedDeadline) {
mCpuTime = std::min(mCpuTime, MAX_DURATION);
mGpuTime = std::min(mGpuTime, MAX_DURATION);
}
std::chrono::nanoseconds getCpuTime() const { return mCpuTime; }
std::chrono::nanoseconds getGpuTime() const { return mGpuTime; }
bool frameMiss() const { return mFrameMissedDeadline; }
std::chrono::nanoseconds getTime(PipelineMode pipeline) const {
if (mCpuTime == 0ns && mGpuTime == 0ns) {
return 0ns;
}
if (pipeline == PipelineMode::On) {
return std::max(mCpuTime, mGpuTime) + FRAME_MARGIN;
}
return mCpuTime + mGpuTime + FRAME_MARGIN;
}
FrameDuration& operator+=(const FrameDuration& other) {
mCpuTime += other.mCpuTime;
mGpuTime += other.mGpuTime;
return *this;
}
FrameDuration& operator-=(const FrameDuration& other) {
mCpuTime -= other.mCpuTime;
mGpuTime -= other.mGpuTime;
return *this;
}
friend FrameDuration operator/(FrameDuration lhs, int rhs) {
lhs.mCpuTime /= rhs;
lhs.mGpuTime /= rhs;
return lhs;
}
private:
std::chrono::nanoseconds mCpuTime = std::chrono::nanoseconds(0);
std::chrono::nanoseconds mGpuTime = std::chrono::nanoseconds(0);
bool mFrameMissedDeadline = false;
static constexpr std::chrono::nanoseconds MAX_DURATION =
std::chrono::milliseconds(100);
};
void addFrameDuration(FrameDuration duration);
std::chrono::nanoseconds wakeClient();
bool swapFaster(int newSwapInterval) REQUIRES(mMutex);
bool swapSlower(const FrameDuration& averageFrameTime,
const std::chrono::nanoseconds& upperBound,
int newSwapInterval) REQUIRES(mMutex);
bool updateSwapInterval();
void preSwapBuffersCallbacks();
void postSwapBuffersCallbacks();
void preWaitCallbacks();
void postWaitCallbacks(std::chrono::nanoseconds cpuTime,
std::chrono::nanoseconds gpuTime);
void startFrameCallbacks();
void swapIntervalChangedCallbacks();
void onSettingsChanged();
void updateMeasuredSwapDuration(std::chrono::nanoseconds duration);
void startFrame();
void waitUntil(int32_t target);
void waitUntilTargetFrame();
void waitOneFrame();
void setPreferredDisplayModeId(int index);
void setPreferredRefreshPeriod(std::chrono::nanoseconds frameTime)
REQUIRES(mMutex);
int calculateSwapInterval(std::chrono::nanoseconds frameTime,
std::chrono::nanoseconds refreshPeriod);
void updateDisplayTimings();
// Waits for the next frame, considering both Choreographer and the prior
// frame's completion
bool waitForNextFrame(const SwapHandlers& h);
void onRefreshRateChanged();
inline bool swapFasterCondition() {
return mSwapDuration <=
mCommonSettings.refreshPeriod * (mAutoSwapInterval - 1) +
DURATION_ROUNDING_MARGIN;
}
const jobject mJactivity;
void* mLibAndroid = nullptr;
PFN_ANativeWindow_setFrameRate mANativeWindow_setFrameRate = nullptr;
JavaVM* mJVM = nullptr;
SwappyCommonSettings mCommonSettings;
std::unique_ptr<ChoreographerFilter> mChoreographerFilter;
bool mUsingExternalChoreographer = false;
std::unique_ptr<ChoreographerThread> mChoreographerThread;
std::mutex mWaitingMutex;
std::condition_variable mWaitingCondition;
std::chrono::steady_clock::time_point mCurrentFrameTimestamp =
std::chrono::steady_clock::now();
int32_t mCurrentFrame = 0;
std::atomic<std::chrono::nanoseconds> mMeasuredSwapDuration;
std::chrono::steady_clock::time_point mSwapTime;
std::mutex mMutex;
class FrameDurations {
public:
void add(FrameDuration frameDuration);
bool hasEnoughSamples() const;
FrameDuration getAverageFrameTime() const;
int getMissedFramePercent() const;
void clear();
private:
static constexpr std::chrono::nanoseconds
FRAME_DURATION_SAMPLE_SECONDS = 2s;
std::deque<std::pair<std::chrono::time_point<std::chrono::steady_clock>,
FrameDuration>>
mFrames;
FrameDuration mFrameDurationsSum = {};
int mMissedFrameCount = 0;
};
FrameDurations mFrameDurations GUARDED_BY(mMutex);
bool mAutoSwapIntervalEnabled GUARDED_BY(mMutex) = true;
bool mPipelineModeAutoMode GUARDED_BY(mMutex) = true;
static constexpr std::chrono::nanoseconds FRAME_MARGIN = 1ms;
static constexpr std::chrono::nanoseconds DURATION_ROUNDING_MARGIN = 1us;
static constexpr int NON_PIPELINE_PERCENT = 50; // 50%
static constexpr int FRAME_DROP_THRESHOLD = 10; // 10%
std::chrono::nanoseconds mSwapDuration = 0ns;
int32_t mAutoSwapInterval;
std::atomic<std::chrono::nanoseconds> mAutoSwapIntervalThreshold = {
50ms}; // 20FPS
static constexpr std::chrono::nanoseconds REFRESH_RATE_MARGIN = 500ns;
std::chrono::steady_clock::time_point mStartFrameTime;
struct SwappyTracerCallbacks {
std::list<Tracer<>> preWait;
std::list<Tracer<int64_t, int64_t>> postWait;
std::list<Tracer<>> preSwapBuffers;
std::list<Tracer<int64_t>> postSwapBuffers;
std::list<Tracer<int32_t, int64_t>> startFrame;
std::list<Tracer<>> swapIntervalChanged;
};
SwappyTracerCallbacks mInjectedTracers;
int32_t mTargetFrame = 0;
std::chrono::steady_clock::time_point mPresentationTime =
std::chrono::steady_clock::now();
bool mPresentationTimeNeeded;
PipelineMode mPipelineMode = PipelineMode::On;
bool mValid;
std::chrono::nanoseconds mFenceTimeout = std::chrono::nanoseconds(50ms);
constexpr static bool USE_DISPLAY_MANAGER = true;
std::unique_ptr<SwappyDisplayManager> mDisplayManager;
int mNextModeId = -1;
std::shared_ptr<SwappyDisplayManager::RefreshPeriodMap>
mSupportedRefreshPeriods;
struct TimingSettings {
std::chrono::nanoseconds refreshPeriod = {};
std::chrono::nanoseconds swapDuration = {};
static TimingSettings from(const Settings& settings) {
TimingSettings timingSettings;
timingSettings.refreshPeriod =
settings.getDisplayTimings().refreshPeriod;
timingSettings.swapDuration = settings.getSwapDuration();
return timingSettings;
}
bool operator!=(const TimingSettings& other) const {
return (refreshPeriod != other.refreshPeriod) ||
(swapDuration != other.swapDuration);
}
bool operator==(const TimingSettings& other) const {
return !(*this != other);
}
};
TimingSettings mNextTimingSettings GUARDED_BY(mMutex) = {};
bool mTimingSettingsNeedUpdate GUARDED_BY(mMutex) = false;
CPUTracer mCPUTracer;
ANativeWindow* mWindow GUARDED_BY(mMutex) = nullptr;
bool mWindowChanged GUARDED_BY(mMutex) = false;
float mLatestFrameRateVote GUARDED_BY(mMutex) = 0.f;
static constexpr float FRAME_RATE_VOTE_MARGIN = 1.f; // 1Hz
// If zero, don't apply the double buffering fix. If non-zero, apply
// the fix after this number of bad frames.
int mBufferStuffingFixWait = 0;
// When zero, buffer stuffing fixing may occur.
// After a fix has been applied, this is non-zero and counts down to avoid
// consecutive fixes.
int mBufferStuffingFixCounter = 0;
// Counts the number of consecutive missed frames (as judged by expected
// latency).
int mMissedFrameCounter = 0;
std::shared_ptr<FrameStatistics> mFrameStatistics;
};
} // namespace swappy

View File

@ -0,0 +1,189 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "SwappyDisplayManager"
#include "SwappyDisplayManager.h"
#include <Log.h>
#include <android/looper.h>
#include <jni.h>
#include <map>
#include "JNIUtil.h"
#include "Settings.h"
namespace swappy {
// Forward declaration of the native methods of Java SwappyDisplayManager class
extern "C" {
JNIEXPORT void JNICALL
Java_com_google_androidgamesdk_SwappyDisplayManager_nSetSupportedRefreshPeriods(
JNIEnv *env, jobject /* this */, jlong cookie, jlongArray refreshPeriods,
jintArray modeIds);
JNIEXPORT void JNICALL
Java_com_google_androidgamesdk_SwappyDisplayManager_nOnRefreshPeriodChanged(
JNIEnv *env, jobject /* this */, jlong cookie, jlong refreshPeriod,
jlong appOffset, jlong sfOffset);
}
const char *SwappyDisplayManager::SDM_CLASS =
"com/google/androidgamesdk/SwappyDisplayManager";
const JNINativeMethod SwappyDisplayManager::SDMNativeMethods[] = {
{"nSetSupportedRefreshPeriods", "(J[J[I)V",
(void
*)&Java_com_google_androidgamesdk_SwappyDisplayManager_nSetSupportedRefreshPeriods},
{"nOnRefreshPeriodChanged", "(JJJJ)V",
(void
*)&Java_com_google_androidgamesdk_SwappyDisplayManager_nOnRefreshPeriodChanged}};
bool SwappyDisplayManager::useSwappyDisplayManager(SdkVersion sdkVersion) {
// SwappyDisplayManager uses APIs introduced in SDK 23 and we get spurious
// window messages for SDK < 28, so restrict here.
if (sdkVersion.sdkInt < MIN_SDK_VERSION) {
return false;
}
// SDK 31 and above doesn't need SwappyDisplayManager as it has native
// support in NDK. SDK 30 has partial native support
// (AChoreographer_registerRefreshRateCallback) but lacks synchronization
// with DisplayManager to query app/sf offsets
return !(sdkVersion.sdkInt >= 31 ||
(sdkVersion.sdkInt == 30 && sdkVersion.previewSdkInt == 1));
}
SwappyDisplayManager::SwappyDisplayManager(JavaVM *vm, jobject mainActivity)
: mJVM(vm) {
if (!vm || !mainActivity) {
return;
}
JNIEnv *env;
mJVM->AttachCurrentThread(&env, nullptr);
jclass swappyDisplayManagerClass = gamesdk::loadClass(
env, mainActivity, SwappyDisplayManager::SDM_CLASS,
(JNINativeMethod *)SwappyDisplayManager::SDMNativeMethods,
SwappyDisplayManager::SDMNativeMethodsSize);
if (!swappyDisplayManagerClass) return;
jmethodID constructor = env->GetMethodID(
swappyDisplayManagerClass, "<init>", "(JLandroid/app/Activity;)V");
mSetPreferredDisplayModeId = env->GetMethodID(
swappyDisplayManagerClass, "setPreferredDisplayModeId", "(I)V");
mTerminate =
env->GetMethodID(swappyDisplayManagerClass, "terminate", "()V");
jobject swappyDisplayManager = env->NewObject(
swappyDisplayManagerClass, constructor, (jlong)this, mainActivity);
mJthis = env->NewGlobalRef(swappyDisplayManager);
mInitialized = true;
}
SwappyDisplayManager::~SwappyDisplayManager() {
JNIEnv *env;
mJVM->AttachCurrentThread(&env, nullptr);
env->CallVoidMethod(mJthis, mTerminate);
env->DeleteGlobalRef(mJthis);
}
std::shared_ptr<SwappyDisplayManager::RefreshPeriodMap>
SwappyDisplayManager::getSupportedRefreshPeriods() {
std::unique_lock<std::mutex> lock(mMutex);
mCondition.wait(
lock, [&]() { return mSupportedRefreshPeriods.get() != nullptr; });
return mSupportedRefreshPeriods;
}
void SwappyDisplayManager::setPreferredDisplayModeId(int index) {
JNIEnv *env;
mJVM->AttachCurrentThread(&env, nullptr);
env->CallVoidMethod(mJthis, mSetPreferredDisplayModeId, index);
}
// Helper class to wrap JNI entry points to SwappyDisplayManager
class SwappyDisplayManagerJNI {
public:
static void onSetSupportedRefreshPeriods(
jlong, std::shared_ptr<SwappyDisplayManager::RefreshPeriodMap>);
static void onRefreshPeriodChanged(jlong, long, long, long);
};
void SwappyDisplayManagerJNI::onSetSupportedRefreshPeriods(
jlong cookie,
std::shared_ptr<SwappyDisplayManager::RefreshPeriodMap> refreshPeriods) {
auto *sDM = reinterpret_cast<SwappyDisplayManager *>(cookie);
std::lock_guard<std::mutex> lock(sDM->mMutex);
sDM->mSupportedRefreshPeriods = std::move(refreshPeriods);
sDM->mCondition.notify_one();
}
void SwappyDisplayManagerJNI::onRefreshPeriodChanged(jlong /*cookie*/,
long refreshPeriod,
long appOffset,
long sfOffset) {
ALOGV("onRefreshPeriodChanged: refresh rate: %.0fHz", 1e9f / refreshPeriod);
using std::chrono::nanoseconds;
Settings::DisplayTimings displayTimings;
displayTimings.refreshPeriod = nanoseconds(refreshPeriod);
displayTimings.appOffset = nanoseconds(appOffset);
displayTimings.sfOffset = nanoseconds(sfOffset);
Settings::getInstance()->setDisplayTimings(displayTimings);
}
extern "C" {
JNIEXPORT void JNICALL
Java_com_google_androidgamesdk_SwappyDisplayManager_nSetSupportedRefreshPeriods(
JNIEnv *env, jobject /* this */, jlong cookie, jlongArray refreshPeriods,
jintArray modeIds) {
int length = env->GetArrayLength(refreshPeriods);
auto refreshPeriodsMap =
std::make_shared<SwappyDisplayManager::RefreshPeriodMap>();
jlong *refreshPeriodsArr = env->GetLongArrayElements(refreshPeriods, 0);
jint *modeIdsArr = env->GetIntArrayElements(modeIds, 0);
for (int i = 0; i < length; i++) {
(*refreshPeriodsMap)[std::chrono::nanoseconds(refreshPeriodsArr[i])] =
modeIdsArr[i];
}
env->ReleaseLongArrayElements(refreshPeriods, refreshPeriodsArr, 0);
env->ReleaseIntArrayElements(modeIds, modeIdsArr, 0);
SwappyDisplayManagerJNI::onSetSupportedRefreshPeriods(cookie,
refreshPeriodsMap);
}
JNIEXPORT void JNICALL
Java_com_google_androidgamesdk_SwappyDisplayManager_nOnRefreshPeriodChanged(
JNIEnv *env, jobject /* this */, jlong cookie, jlong refreshPeriod,
jlong appOffset, jlong sfOffset) {
SwappyDisplayManagerJNI::onRefreshPeriodChanged(cookie, refreshPeriod,
appOffset, sfOffset);
}
} // extern "C"
} // namespace swappy

View File

@ -0,0 +1,68 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <jni.h>
#include <condition_variable>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
namespace swappy {
struct SdkVersion {
int sdkInt; // Build.VERSION.SDK_INT
int previewSdkInt; // Build.VERSION.PREVIEW_SDK_INT
};
class SwappyDisplayManager {
public:
static const char* SDM_CLASS;
static const JNINativeMethod SDMNativeMethods[];
static constexpr int SDMNativeMethodsSize = 2;
static constexpr int MIN_SDK_VERSION = 28;
static bool useSwappyDisplayManager(SdkVersion sdkVersion);
SwappyDisplayManager(JavaVM*, jobject mainActivity);
~SwappyDisplayManager();
bool isInitialized() { return mInitialized; }
// Map from refresh period to display mode id
using RefreshPeriodMap = std::map<std::chrono::nanoseconds, int>;
std::shared_ptr<RefreshPeriodMap> getSupportedRefreshPeriods();
void setPreferredDisplayModeId(int index);
private:
JavaVM* mJVM;
std::mutex mMutex;
std::condition_variable mCondition;
std::shared_ptr<RefreshPeriodMap> mSupportedRefreshPeriods;
jobject mJthis = nullptr;
jmethodID mSetPreferredDisplayModeId = nullptr;
jmethodID mTerminate = nullptr;
bool mInitialized = false;
friend class SwappyDisplayManagerJNI;
};
} // namespace swappy

View File

@ -0,0 +1,149 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "Thread.h"
#include <sched.h>
#include <unistd.h>
#include <thread>
#include "swappy/swappy_common.h"
#define LOG_TAG "SwappyThread"
#include "Log.h"
namespace swappy {
int32_t getNumCpus() {
static int32_t sNumCpus = []() {
pid_t pid = gettid();
cpu_set_t cpuSet;
CPU_ZERO(&cpuSet);
sched_getaffinity(pid, sizeof(cpuSet), &cpuSet);
int32_t numCpus = 0;
while (CPU_ISSET(numCpus, &cpuSet)) {
++numCpus;
}
return numCpus;
}();
return sNumCpus;
}
void setAffinity(int32_t cpu) {
cpu_set_t cpuSet;
CPU_ZERO(&cpuSet);
CPU_SET(cpu, &cpuSet);
sched_setaffinity(gettid(), sizeof(cpuSet), &cpuSet);
}
void setAffinity(Affinity affinity) {
const int32_t numCpus = getNumCpus();
cpu_set_t cpuSet;
CPU_ZERO(&cpuSet);
for (int32_t cpu = 0; cpu < numCpus; ++cpu) {
switch (affinity) {
case Affinity::None:
CPU_SET(cpu, &cpuSet);
break;
case Affinity::Even:
if (cpu % 2 == 0) CPU_SET(cpu, &cpuSet);
break;
case Affinity::Odd:
if (cpu % 2 == 1) CPU_SET(cpu, &cpuSet);
break;
}
}
sched_setaffinity(gettid(), sizeof(cpuSet), &cpuSet);
}
static const SwappyThreadFunctions* s_ext_thread_manager = nullptr;
struct ThreadImpl {
virtual ~ThreadImpl() {}
virtual bool joinable() = 0;
virtual void join() = 0;
};
struct ExtThreadImpl : public ThreadImpl {
std::function<void()> fn_;
SwappyThreadId id_;
public:
ExtThreadImpl(std::function<void()>&& fn) : fn_(std::move(fn)) {
if (s_ext_thread_manager->start(&id_, startThread, this) != 0) {
ALOGE("Couldn't create thread");
}
}
void join() { s_ext_thread_manager->join(id_); }
bool joinable() { return s_ext_thread_manager->joinable(id_); }
static void* startThread(void* x) {
ExtThreadImpl* impl = (ExtThreadImpl*)x;
impl->fn_();
return nullptr;
}
};
struct StlThreadImpl : public ThreadImpl {
std::thread thread_;
public:
StlThreadImpl(std::function<void()>&& fn) : thread_(std::move(fn)) {}
void join() { thread_.join(); }
bool joinable() { return thread_.joinable(); }
};
Thread::Thread() noexcept {}
Thread::~Thread() {}
Thread::Thread(std::function<void()>&& fn) noexcept {
if (s_ext_thread_manager != nullptr) {
impl_ = std::make_unique<ExtThreadImpl>(std::move(fn));
} else {
impl_ = std::make_unique<StlThreadImpl>(std::move(fn));
}
}
Thread::Thread(Thread&& rhs) noexcept : impl_(std::move(rhs.impl_)) {}
Thread& Thread::operator=(Thread&& rhs) noexcept {
if (&rhs != this) {
impl_ = std::move(rhs.impl_);
}
return *this;
}
void Thread::join() {
if (impl_.get()) {
impl_->join();
}
}
bool Thread::joinable() {
return (impl_.get() != nullptr && impl_->joinable());
}
} // namespace swappy
extern "C" void Swappy_setThreadFunctions(const SwappyThreadFunctions* mgr) {
swappy::s_ext_thread_manager = mgr;
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <cstdint>
#include <functional>
#include <memory>
// Enable thread safety attributes only with clang.
// The attributes can be safely erased when compiling with other compilers.
#if defined(__clang__) && (!defined(SWIG))
#define THREAD_ANNOTATION_ATTRIBUTE__(x) __attribute__((x))
#else
#define THREAD_ANNOTATION_ATTRIBUTE__(x) // no-op
#endif
#if !defined GAMESDK_THREAD_CHECKS
#define GAMESDK_THREAD_CHECKS 1
#endif
#if GAMESDK_THREAD_CHECKS
#define GUARDED_BY(x) THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))
#define REQUIRES(...) \
THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__))
#define NO_THREAD_SAFETY_ANALYSIS \
THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis)
#else
#define GUARDED_BY(x)
#define REQUIRES(...)
#define NO_THREAD_SAFETY_ANALYSIS
#endif
namespace swappy {
enum class Affinity { None, Even, Odd };
int32_t getNumCpus();
void setAffinity(int32_t cpu);
void setAffinity(Affinity affinity);
struct ThreadImpl;
class Thread {
std::unique_ptr<ThreadImpl> impl_;
public:
Thread() noexcept;
Thread(std::function<void()>&& fn) noexcept;
Thread(Thread&& rhs) noexcept;
Thread(const Thread&) = delete;
Thread& operator=(Thread&& rhs) noexcept;
Thread& operator=(const Thread& rhs) = delete;
~Thread();
void join();
bool joinable();
};
} // namespace swappy

View File

@ -0,0 +1,37 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// API entry points for both OpenGL and Vulkan
#include "swappy/swappy_common.h"
extern "C" {
void SWAPPY_VERSION_SYMBOL() {
// Intentionally empty: this function is needed in order to interrogate
// shared libraries for the Swappy version.
}
uint32_t Swappy_version() { return SWAPPY_PACKED_VERSION; }
const char* Swappy_versionString() {
static const char version[] =
AGDK_STRING_VERSION(SWAPPY_MAJOR_VERSION, SWAPPY_MINOR_VERSION,
SWAPPY_BUGFIX_VERSION, AGDK_GIT_COMMIT);
return version;
}
} // extern "C"

View File

@ -0,0 +1,342 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "EGL.h"
#include <Trace.h>
#include <dlfcn.h>
#include <vector>
#define LOG_TAG "Swappy::EGL"
#include "Log.h"
using namespace std::chrono_literals;
namespace swappy {
std::unique_ptr<EGL> EGL::create(std::chrono::nanoseconds fenceTimeout) {
auto eglLib = dlopen("libEGL.so", RTLD_LAZY | RTLD_LOCAL);
if (eglLib == nullptr) {
ALOGE("Can't load libEGL");
return nullptr;
}
auto eglGetProcAddress = reinterpret_cast<eglGetProcAddress_type>(
dlsym(eglLib, "eglGetProcAddress"));
if (eglGetProcAddress == nullptr) {
ALOGE("Failed to load eglGetProcAddress");
return nullptr;
}
auto eglSwapBuffers =
reinterpret_cast<eglSwapBuffers_type>(dlsym(eglLib, "eglSwapBuffers"));
if (eglSwapBuffers == nullptr) {
ALOGE("Failed to load eglSwapBuffers");
return nullptr;
}
auto eglPresentationTimeANDROID =
reinterpret_cast<eglPresentationTimeANDROID_type>(
eglGetProcAddress("eglPresentationTimeANDROID"));
if (eglPresentationTimeANDROID == nullptr) {
ALOGE("Failed to load eglPresentationTimeANDROID");
return nullptr;
}
auto eglCreateSyncKHR = reinterpret_cast<eglCreateSyncKHR_type>(
eglGetProcAddress("eglCreateSyncKHR"));
if (eglCreateSyncKHR == nullptr) {
ALOGE("Failed to load eglCreateSyncKHR");
return nullptr;
}
auto eglDestroySyncKHR = reinterpret_cast<eglDestroySyncKHR_type>(
eglGetProcAddress("eglDestroySyncKHR"));
if (eglDestroySyncKHR == nullptr) {
ALOGE("Failed to load eglDestroySyncKHR");
return nullptr;
}
auto eglGetSyncAttribKHR = reinterpret_cast<eglGetSyncAttribKHR_type>(
eglGetProcAddress("eglGetSyncAttribKHR"));
if (eglGetSyncAttribKHR == nullptr) {
ALOGE("Failed to load eglGetSyncAttribKHR");
return nullptr;
}
auto eglGetError =
reinterpret_cast<eglGetError_type>(eglGetProcAddress("eglGetError"));
if (eglGetError == nullptr) {
ALOGE("Failed to load eglGetError");
return nullptr;
}
auto eglSurfaceAttrib = reinterpret_cast<eglSurfaceAttrib_type>(
eglGetProcAddress("eglSurfaceAttrib"));
if (eglSurfaceAttrib == nullptr) {
ALOGE("Failed to load eglSurfaceAttrib");
return nullptr;
}
// stats may not be supported on all versions
auto eglGetNextFrameIdANDROID =
reinterpret_cast<eglGetNextFrameIdANDROID_type>(
eglGetProcAddress("eglGetNextFrameIdANDROID"));
if (eglGetNextFrameIdANDROID == nullptr) {
ALOGI("Failed to load eglGetNextFrameIdANDROID");
}
auto eglGetFrameTimestampsANDROID =
reinterpret_cast<eglGetFrameTimestampsANDROID_type>(
eglGetProcAddress("eglGetFrameTimestampsANDROID"));
if (eglGetFrameTimestampsANDROID == nullptr) {
ALOGI("Failed to load eglGetFrameTimestampsANDROID");
}
auto egl = std::make_unique<EGL>(fenceTimeout, eglGetProcAddress,
ConstructorTag{});
egl->eglLib = eglLib;
egl->eglSwapBuffers = eglSwapBuffers;
egl->eglGetProcAddress = eglGetProcAddress;
egl->eglPresentationTimeANDROID = eglPresentationTimeANDROID;
egl->eglCreateSyncKHR = eglCreateSyncKHR;
egl->eglDestroySyncKHR = eglDestroySyncKHR;
egl->eglGetSyncAttribKHR = eglGetSyncAttribKHR;
egl->eglGetError = eglGetError;
egl->eglSurfaceAttrib = eglSurfaceAttrib;
egl->eglGetNextFrameIdANDROID = eglGetNextFrameIdANDROID;
egl->eglGetFrameTimestampsANDROID = eglGetFrameTimestampsANDROID;
return egl;
}
EGL::~EGL() {
if (eglLib) {
dlclose(eglLib);
}
}
void EGL::resetSyncFence(EGLDisplay display) {
std::lock_guard<std::mutex> lock(mSyncFenceMutex);
if (mFenceWaiter.waitForIdle() && mSyncFence != EGL_NO_SYNC_KHR) {
EGLBoolean result = eglDestroySyncKHR(display, mSyncFence);
if (result == EGL_FALSE) {
ALOGE("Failed to destroy sync fence");
}
}
mSyncFence = eglCreateSyncKHR(display, EGL_SYNC_FENCE_KHR, nullptr);
if (mSyncFence != EGL_NO_SYNC_KHR) {
// kick of the thread work to wait for the fence and measure its time
mFenceWaiter.onFenceCreation(display, mSyncFence);
} else {
ALOGE("Failed to create sync fence");
}
}
bool EGL::lastFrameIsComplete(EGLDisplay display) {
std::lock_guard<std::mutex> lock(mSyncFenceMutex);
// This will be the case on the first frame
if (mSyncFence == EGL_NO_SYNC_KHR) {
return true;
}
EGLint status = 0;
EGLBoolean result =
eglGetSyncAttribKHR(display, mSyncFence, EGL_SYNC_STATUS_KHR, &status);
if (result == EGL_FALSE) {
ALOGE("Failed to get sync status");
return true;
}
if (status == EGL_SIGNALED_KHR) {
return true;
} else if (status == EGL_UNSIGNALED_KHR) {
return false;
} else {
ALOGE("Unexpected sync status: %d", status);
return true;
}
}
bool EGL::setPresentationTime(EGLDisplay display, EGLSurface surface,
std::chrono::steady_clock::time_point time) {
eglPresentationTimeANDROID(display, surface,
time.time_since_epoch().count());
return EGL_TRUE;
}
bool EGL::statsSupported() {
return (eglGetNextFrameIdANDROID != nullptr &&
eglGetFrameTimestampsANDROID != nullptr);
}
std::pair<bool, EGLuint64KHR> EGL::getNextFrameId(EGLDisplay dpy,
EGLSurface surface) const {
if (eglGetNextFrameIdANDROID == nullptr) {
ALOGE("stats are not supported on this platform");
return {false, 0};
}
EGLuint64KHR frameId;
EGLBoolean result = eglGetNextFrameIdANDROID(dpy, surface, &frameId);
if (result == EGL_FALSE) {
ALOGE("Failed to get next frame ID");
return {false, 0};
}
return {true, frameId};
}
std::unique_ptr<EGL::FrameTimestamps> EGL::getFrameTimestamps(
EGLDisplay dpy, EGLSurface surface, EGLuint64KHR frameId) const {
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
if (eglGetFrameTimestampsANDROID == nullptr) {
ALOGE("stats are not supported on this platform");
return nullptr;
}
const std::vector<EGLint> timestamps = {
EGL_REQUESTED_PRESENT_TIME_ANDROID,
EGL_RENDERING_COMPLETE_TIME_ANDROID,
EGL_COMPOSITION_LATCH_TIME_ANDROID,
EGL_DISPLAY_PRESENT_TIME_ANDROID,
};
std::vector<EGLnsecsANDROID> values(timestamps.size());
EGLBoolean result =
eglGetFrameTimestampsANDROID(dpy, surface, frameId, timestamps.size(),
timestamps.data(), values.data());
if (result == EGL_FALSE) {
EGLint reason = eglGetError();
if (reason == EGL_BAD_SURFACE) {
eglSurfaceAttrib(dpy, surface, EGL_TIMESTAMPS_ANDROID, EGL_TRUE);
} else {
ALOGE_ONCE("Failed to get timestamps for frame %llu",
(unsigned long long)frameId);
}
return nullptr;
}
// try again if we got some pending stats
for (auto i : values) {
if (i == EGL_TIMESTAMP_PENDING_ANDROID) return nullptr;
}
std::unique_ptr<EGL::FrameTimestamps> frameTimestamps =
std::make_unique<EGL::FrameTimestamps>();
frameTimestamps->requested = values[0];
frameTimestamps->renderingCompleted = values[1];
frameTimestamps->compositionLatched = values[2];
frameTimestamps->presented = values[3];
return frameTimestamps;
#else
return nullptr;
#endif
}
EGL::FenceWaiter::FenceWaiter(std::chrono::nanoseconds fenceTimeout,
eglGetProcAddress_type getProcAddress)
: mFenceTimeout(fenceTimeout) {
std::unique_lock<std::mutex> lock(mFenceWaiterLock);
eglClientWaitSyncKHR = reinterpret_cast<eglClientWaitSyncKHR_type>(
getProcAddress("eglClientWaitSyncKHR"));
if (eglClientWaitSyncKHR == nullptr)
ALOGE("Failed to load eglClientWaitSyncKHR");
eglDestroySyncKHR = reinterpret_cast<eglDestroySyncKHR_type>(
getProcAddress("eglDestroySyncKHR"));
if (eglDestroySyncKHR == nullptr) ALOGE("Failed to load eglDestroySyncKHR");
mFenceWaiter = Thread([this]() { threadMain(); });
}
EGL::FenceWaiter::~FenceWaiter() {
{
std::lock_guard<std::mutex> lock(mFenceWaiterLock);
mFenceWaiterRunning = false;
mFenceWaiterCondition.notify_all();
}
mFenceWaiter.join();
}
bool EGL::FenceWaiter::waitForIdle() {
std::lock_guard<std::mutex> lock(mFenceWaiterLock);
mFenceWaiterCondition.wait(
mFenceWaiterLock,
[this]() REQUIRES(mFenceWaiterLock) { return !mFenceWaiterPending; });
return mSyncFence != EGL_NO_SYNC_KHR;
}
void EGL::FenceWaiter::onFenceCreation(EGLDisplay display,
EGLSyncKHR syncFence) {
std::lock_guard<std::mutex> lock(mFenceWaiterLock);
mDisplay = display;
mSyncFence = syncFence;
mFenceWaiterPending = true;
mFenceWaiterCondition.notify_all();
}
void EGL::FenceWaiter::threadMain() {
std::lock_guard<std::mutex> lock(mFenceWaiterLock);
while (mFenceWaiterRunning) {
// wait for new fence object
mFenceWaiterCondition.wait(
mFenceWaiterLock, [this]() REQUIRES(mFenceWaiterLock) {
return mFenceWaiterPending || !mFenceWaiterRunning;
});
if (!mFenceWaiterRunning) {
break;
}
gamesdk::ScopedTrace tracer("Swappy: GPU frame time");
const auto startTime = std::chrono::steady_clock::now();
EGLBoolean result = eglClientWaitSyncKHR(mDisplay, mSyncFence, 0,
mFenceTimeout.count());
switch (result) {
case EGL_FALSE:
ALOGE("Failed to wait sync");
break;
case EGL_TIMEOUT_EXPIRED_KHR:
ALOGE("Timeout waiting for fence");
break;
}
if (result != EGL_CONDITION_SATISFIED_KHR) {
result = eglDestroySyncKHR(mDisplay, mSyncFence);
if (result == EGL_FALSE) {
ALOGE("Failed to destroy sync fence");
}
mSyncFence = EGL_NO_SYNC_KHR;
}
mFencePendingTime = std::chrono::steady_clock::now() - startTime;
mFenceWaiterPending = false;
mFenceWaiterCondition.notify_all();
}
}
std::chrono::nanoseconds EGL::FenceWaiter::getFencePendingTime() const {
// return mFencePendingTime without a lock to avoid blocking the main thread
// worst case, the time will be of some previous frame
return mFencePendingTime.load();
}
} // namespace swappy

View File

@ -0,0 +1,141 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include "Thread.h"
namespace swappy {
class EGL {
private:
// Allows construction with std::unique_ptr from a static method, but
// disallows construction outside of the class since no one else can
// construct a ConstructorTag
struct ConstructorTag {};
public:
struct FrameTimestamps {
EGLnsecsANDROID requested;
EGLnsecsANDROID renderingCompleted;
EGLnsecsANDROID compositionLatched;
EGLnsecsANDROID presented;
};
using eglGetProcAddress_type = void (*(*)(const char *))(void);
explicit EGL(std::chrono::nanoseconds fenceTimeout,
eglGetProcAddress_type getProcAddress, ConstructorTag)
: mFenceWaiter(fenceTimeout, getProcAddress) {}
~EGL();
static std::unique_ptr<EGL> create(std::chrono::nanoseconds fenceTimeout);
void resetSyncFence(EGLDisplay display);
bool lastFrameIsComplete(EGLDisplay display);
bool setPresentationTime(EGLDisplay display, EGLSurface surface,
std::chrono::steady_clock::time_point time);
std::chrono::nanoseconds getFencePendingTime() const {
return mFenceWaiter.getFencePendingTime();
}
// for stats
bool statsSupported();
std::pair<bool, EGLuint64KHR> getNextFrameId(EGLDisplay dpy,
EGLSurface surface) const;
std::unique_ptr<FrameTimestamps> getFrameTimestamps(
EGLDisplay dpy, EGLSurface surface, EGLuint64KHR frameId) const;
EGLBoolean swapBuffers(EGLDisplay dpy, EGLSurface surface) {
return this->eglSwapBuffers(dpy, surface);
}
private:
void *eglLib = nullptr;
eglGetProcAddress_type eglGetProcAddress = nullptr;
using eglSwapBuffers_type = EGLBoolean (*)(EGLDisplay, EGLSurface);
eglSwapBuffers_type eglSwapBuffers = nullptr;
using eglPresentationTimeANDROID_type = EGLBoolean (*)(EGLDisplay,
EGLSurface,
EGLnsecsANDROID);
eglPresentationTimeANDROID_type eglPresentationTimeANDROID = nullptr;
using eglCreateSyncKHR_type = EGLSyncKHR (*)(EGLDisplay, EGLenum,
const EGLint *);
eglCreateSyncKHR_type eglCreateSyncKHR = nullptr;
using eglDestroySyncKHR_type = EGLBoolean (*)(EGLDisplay, EGLSyncKHR);
eglDestroySyncKHR_type eglDestroySyncKHR = nullptr;
using eglGetSyncAttribKHR_type = EGLBoolean (*)(EGLDisplay, EGLSyncKHR,
EGLint, EGLint *);
eglGetSyncAttribKHR_type eglGetSyncAttribKHR = nullptr;
using eglGetError_type = EGLint (*)(void);
eglGetError_type eglGetError = nullptr;
using eglSurfaceAttrib_type = EGLBoolean (*)(EGLDisplay, EGLSurface, EGLint,
EGLint);
eglSurfaceAttrib_type eglSurfaceAttrib = nullptr;
using eglGetNextFrameIdANDROID_type = EGLBoolean (*)(EGLDisplay, EGLSurface,
EGLuint64KHR *);
eglGetNextFrameIdANDROID_type eglGetNextFrameIdANDROID = nullptr;
using eglGetFrameTimestampsANDROID_type =
EGLBoolean (*)(EGLDisplay, EGLSurface, EGLuint64KHR, EGLint,
const EGLint *, EGLnsecsANDROID *);
eglGetFrameTimestampsANDROID_type eglGetFrameTimestampsANDROID = nullptr;
std::mutex mSyncFenceMutex;
EGLSyncKHR mSyncFence = EGL_NO_SYNC_KHR;
class FenceWaiter {
public:
FenceWaiter(std::chrono::nanoseconds fenceTimeout,
EGL::eglGetProcAddress_type getProcAddress);
~FenceWaiter();
void onFenceCreation(EGLDisplay display, EGLSyncKHR syncFence);
// Wait and return true if the fence was signalled.
// The fence will NOT be destroyed in this case.
bool waitForIdle();
std::chrono::nanoseconds getFencePendingTime() const;
private:
using eglClientWaitSyncKHR_type = EGLBoolean (*)(EGLDisplay, EGLSyncKHR,
EGLint, EGLTimeKHR);
eglClientWaitSyncKHR_type eglClientWaitSyncKHR = nullptr;
using eglDestroySyncKHR_type = EGLBoolean (*)(EGLDisplay, EGLSyncKHR);
eglDestroySyncKHR_type eglDestroySyncKHR = nullptr;
void threadMain();
Thread mFenceWaiter GUARDED_BY(mFenceWaiterLock);
std::mutex mFenceWaiterLock;
std::condition_variable_any mFenceWaiterCondition;
bool mFenceWaiterRunning GUARDED_BY(mFenceWaiterLock) = true;
bool mFenceWaiterPending GUARDED_BY(mFenceWaiterLock) = false;
std::atomic<std::chrono::nanoseconds> mFencePendingTime;
EGLDisplay mDisplay GUARDED_BY(mFenceWaiterLock);
EGLSyncKHR mSyncFence GUARDED_BY(mFenceWaiterLock) = EGL_NO_SYNC_KHR;
std::chrono::nanoseconds mFenceTimeout;
};
FenceWaiter mFenceWaiter;
};
} // namespace swappy

View File

@ -0,0 +1,208 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "FrameStatisticsGL.h"
#define LOG_TAG "FrameStatisticsGL"
#include <inttypes.h>
#include <cmath>
#include <string>
#include "EGL.h"
#include "Log.h"
#include "SwappyCommon.h"
#include "Trace.h"
namespace swappy {
int32_t LatencyFrameStatisticsGL::getFrameDelta(EGLnsecsANDROID start,
EGLnsecsANDROID end) {
const int64_t deltaTimeNano = end - start;
int32_t numFrames =
deltaTimeNano / mSwappyCommon.getRefreshPeriod().count();
numFrames = std::max(
0, std::min(numFrames, static_cast<int32_t>(MAX_FRAME_BUCKETS) - 1));
return numFrames;
}
LatencyFrameStatisticsGL::LatencyFrameStatisticsGL(
const EGL& egl, const SwappyCommon& swappyCommon)
: mEgl(egl), mSwappyCommon(swappyCommon) {}
void LatencyFrameStatisticsGL::updateLatency(
swappy::EGL::FrameTimestamps& frameStats, TimePoint frameStartTime) {
int latency = getFrameDelta(frameStartTime.time_since_epoch().count(),
frameStats.compositionLatched);
TRACE_INT("FrameLatency", latency);
mLastLatency = latency;
}
LatencyFrameStatisticsGL::ThisFrame LatencyFrameStatisticsGL::getThisFrame(
EGLDisplay dpy, EGLSurface surface) {
const TimePoint frameStartTime = std::chrono::steady_clock::now();
// first get the next frame id
std::pair<bool, EGLuint64KHR> nextFrameId =
mEgl.getNextFrameId(dpy, surface);
if (nextFrameId.first) {
mPendingFrames.push_back(
{dpy, surface, nextFrameId.second, frameStartTime});
}
if (mPendingFrames.empty()) {
return {};
}
EGLFrame frame = mPendingFrames.front();
// make sure we don't lag behind the stats too much
if (nextFrameId.first && nextFrameId.second - frame.id > MAX_FRAME_LAG) {
while (mPendingFrames.size() > 1)
mPendingFrames.erase(mPendingFrames.begin());
mPrevFrameTime = 0;
frame = mPendingFrames.front();
}
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 14
std::unique_ptr<EGL::FrameTimestamps> frameStats =
mEgl.getFrameTimestamps(frame.dpy, frame.surface, frame.id);
if (!frameStats) {
return {frame.startFrameTime};
}
mPendingFrames.erase(mPendingFrames.begin());
return {frame.startFrameTime, std::move(frameStats)};
#else
return {frame.startFrameTime};
#endif
}
// called once per swap
void LatencyFrameStatisticsGL::capture(EGLDisplay dpy, EGLSurface surface) {
auto frame = getThisFrame(dpy, surface);
if (!frame.stats) return;
updateLatency(*frame.stats, frame.startTime);
}
// NB This is only needed for C++14
constexpr std::chrono::nanoseconds FullFrameStatisticsGL::LOG_EVERY_N_NS;
FullFrameStatisticsGL::FullFrameStatisticsGL(const EGL& egl,
const SwappyCommon& swappyCommon)
: LatencyFrameStatisticsGL(egl, swappyCommon) {}
int32_t FullFrameStatisticsGL::updateFrames(EGLnsecsANDROID start,
EGLnsecsANDROID end,
uint64_t stat[]) {
int32_t numFrames = getFrameDelta(start, end);
stat[numFrames]++;
return numFrames;
}
void FullFrameStatisticsGL::updateIdleFrames(EGL::FrameTimestamps& frameStats) {
updateFrames(frameStats.renderingCompleted, frameStats.compositionLatched,
mStats.idleFrames);
}
void FullFrameStatisticsGL::updateLatencyFrames(
swappy::EGL::FrameTimestamps& frameStats, TimePoint frameStartTime) {
int latency =
updateFrames(frameStartTime.time_since_epoch().count(),
frameStats.compositionLatched, mStats.latencyFrames);
TRACE_INT("FrameLatency", latency);
mLastLatency = latency;
}
void FullFrameStatisticsGL::updateLateFrames(EGL::FrameTimestamps& frameStats) {
updateFrames(frameStats.requested, frameStats.presented, mStats.lateFrames);
}
void FullFrameStatisticsGL::updateOffsetFromPreviousFrame(
swappy::EGL::FrameTimestamps& frameStats) {
if (mPrevFrameTime != 0) {
updateFrames(mPrevFrameTime, frameStats.presented,
mStats.offsetFromPreviousFrame);
}
mPrevFrameTime = frameStats.presented;
}
// called once per swap
void FullFrameStatisticsGL::capture(EGLDisplay dpy, EGLSurface surface) {
auto frame = getThisFrame(dpy, surface);
if (!frame.stats) return;
std::lock_guard<std::mutex> lock(mMutex);
mStats.totalFrames++;
updateIdleFrames(*frame.stats);
updateLateFrames(*frame.stats);
updateOffsetFromPreviousFrame(*frame.stats);
updateLatencyFrames(*frame.stats, frame.startTime);
logFrames();
}
void FullFrameStatisticsGL::logFrames() {
static auto previousLogTime = std::chrono::steady_clock::now();
if (std::chrono::steady_clock::now() - previousLogTime < LOG_EVERY_N_NS) {
return;
}
std::string message;
ALOGI("== Frame statistics ==");
ALOGI("total frames: %" PRIu64, mStats.totalFrames);
message += "Buckets: ";
for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
message += "\t[" + swappy::to_string(i) + "]";
ALOGI("%s", message.c_str());
message = "";
message += "idle frames: ";
for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
message += "\t " + swappy::to_string(mStats.idleFrames[i]);
ALOGI("%s", message.c_str());
message = "";
message += "late frames: ";
for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
message += "\t " + swappy::to_string(mStats.lateFrames[i]);
ALOGI("%s", message.c_str());
message = "";
message += "offset from previous frame: ";
for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
message += "\t " + swappy::to_string(mStats.offsetFromPreviousFrame[i]);
ALOGI("%s", message.c_str());
message = "";
message += "frame latency: ";
for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
message += "\t " + swappy::to_string(mStats.latencyFrames[i]);
ALOGI("%s", message.c_str());
previousLogTime = std::chrono::steady_clock::now();
}
SwappyStats FullFrameStatisticsGL::getStats() {
std::lock_guard<std::mutex> lock(mMutex);
return mStats;
}
} // namespace swappy

View File

@ -0,0 +1,99 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <array>
#include <atomic>
#include <map>
#include <vector>
#include "EGL.h"
#include "FrameStatistics.h"
#include "Thread.h"
using TimePoint = std::chrono::steady_clock::time_point;
using namespace std::chrono_literals;
namespace swappy {
class SwappyCommon;
// Just records latency
class LatencyFrameStatisticsGL : public FrameStatistics {
public:
LatencyFrameStatisticsGL(const EGL& egl, const SwappyCommon& swappyCommon);
~LatencyFrameStatisticsGL() = default;
int32_t lastLatencyRecorded() const override { return mLastLatency; }
bool isEssential() const override { return true; }
virtual SwappyStats getStats() override { return {}; }
virtual void capture(EGLDisplay dpy, EGLSurface surface);
protected:
static constexpr int MAX_FRAME_LAG = 10;
int32_t getFrameDelta(EGLnsecsANDROID start, EGLnsecsANDROID end);
struct ThisFrame {
TimePoint startTime;
std::unique_ptr<EGL::FrameTimestamps> stats;
};
ThisFrame getThisFrame(EGLDisplay dpy, EGLSurface surface);
void updateLatency(EGL::FrameTimestamps& frameStats,
TimePoint frameStartTime);
const EGL& mEgl;
const SwappyCommon& mSwappyCommon;
struct EGLFrame {
EGLDisplay dpy;
EGLSurface surface;
EGLuint64KHR id;
TimePoint startFrameTime;
};
std::vector<EGLFrame> mPendingFrames;
EGLnsecsANDROID mPrevFrameTime = 0;
std::atomic<int32_t> mLastLatency = {0};
};
class FullFrameStatisticsGL : public LatencyFrameStatisticsGL {
public:
FullFrameStatisticsGL(const EGL& egl, const SwappyCommon& swappyCommon);
~FullFrameStatisticsGL() = default;
void capture(EGLDisplay dpy, EGLSurface surface) override;
SwappyStats getStats() override;
bool isEssential() const override { return false; }
private:
static constexpr std::chrono::nanoseconds LOG_EVERY_N_NS = 1s;
int updateFrames(EGLnsecsANDROID start, EGLnsecsANDROID end,
uint64_t stat[]);
void updateIdleFrames(EGL::FrameTimestamps& frameStats) REQUIRES(mMutex);
void updateLateFrames(EGL::FrameTimestamps& frameStats) REQUIRES(mMutex);
void updateOffsetFromPreviousFrame(EGL::FrameTimestamps& frameStats)
REQUIRES(mMutex);
void updateLatencyFrames(EGL::FrameTimestamps& frameStats,
TimePoint frameStartTime) REQUIRES(mMutex);
void logFrames() REQUIRES(mMutex);
std::mutex mMutex;
SwappyStats mStats GUARDED_BY(mMutex) = {};
};
} // namespace swappy

View File

@ -0,0 +1,333 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SwappyGL.h"
#include <cinttypes>
#include <cmath>
#include <cstdlib>
#include "Log.h"
#include "Thread.h"
#include "Trace.h"
#include "system_utils.h"
#define LOG_TAG "Swappy"
namespace swappy {
using std::chrono::milliseconds;
using std::chrono::nanoseconds;
std::mutex SwappyGL::sInstanceMutex;
std::unique_ptr<SwappyGL> SwappyGL::sInstance;
bool SwappyGL::init(JNIEnv *env, jobject jactivity) {
std::lock_guard<std::mutex> lock(sInstanceMutex);
if (sInstance) {
ALOGE("Attempted to initialize SwappyGL twice");
return false;
}
sInstance = std::make_unique<SwappyGL>(env, jactivity, ConstructorTag{});
if (!sInstance->mEnableSwappy) {
ALOGE("Failed to initialize SwappyGL");
return false;
}
return true;
}
void SwappyGL::onChoreographer(int64_t frameTimeNanos) {
TRACE_CALL();
SwappyGL *swappy = getInstance();
if (!swappy) {
return;
}
swappy->mCommonBase.onChoreographer(frameTimeNanos);
}
bool SwappyGL::setWindow(ANativeWindow *window) {
TRACE_CALL();
SwappyGL *swappy = getInstance();
if (!swappy) {
ALOGE("Failed to get SwappyGL instance in setWindow");
return false;
}
swappy->mCommonBase.setANativeWindow(window);
return true;
}
bool SwappyGL::swap(EGLDisplay display, EGLSurface surface) {
TRACE_CALL();
SwappyGL *swappy = getInstance();
if (!swappy) {
return EGL_FALSE;
}
if (swappy->enabled()) {
return swappy->swapInternal(display, surface);
} else {
return swappy->getEgl()->swapBuffers(display, surface) == EGL_TRUE;
}
}
bool SwappyGL::lastFrameIsComplete(EGLDisplay display) {
if (!getEgl()->lastFrameIsComplete(display)) {
gamesdk::ScopedTrace trace("lastFrameIncomplete");
ALOGV("lastFrameIncomplete");
return false;
}
return true;
}
bool SwappyGL::swapInternal(EGLDisplay display, EGLSurface surface) {
const SwappyCommon::SwapHandlers handlers = {
.lastFrameIsComplete = [&]() { return lastFrameIsComplete(display); },
.getPrevFrameGpuTime =
[&]() { return getEgl()->getFencePendingTime(); },
};
mCommonBase.onPreSwap(handlers);
if (mCommonBase.needToSetPresentationTime()) {
bool setPresentationTimeResult = setPresentationTime(display, surface);
if (!setPresentationTimeResult) {
return setPresentationTimeResult;
}
}
resetSyncFence(display);
bool swapBuffersResult =
(getEgl()->swapBuffers(display, surface) == EGL_TRUE);
mCommonBase.onPostSwap(handlers);
return swapBuffersResult;
}
void SwappyGL::addTracer(const SwappyTracer *tracer) {
SwappyGL *swappy = getInstance();
if (!swappy) {
return;
}
if (swappy->enabled() && tracer != nullptr)
swappy->mCommonBase.addTracerCallbacks(*tracer);
}
void SwappyGL::removeTracer(const SwappyTracer *tracer) {
SwappyGL *swappy = getInstance();
if (!swappy) {
return;
}
if (swappy->enabled() && tracer != nullptr)
swappy->mCommonBase.removeTracerCallbacks(*tracer);
}
nanoseconds SwappyGL::getSwapDuration() {
SwappyGL *swappy = getInstance();
if (!swappy || !swappy->enabled()) {
return -1ns;
}
return swappy->mCommonBase.getSwapDuration();
};
void SwappyGL::setAutoSwapInterval(bool enabled) {
SwappyGL *swappy = getInstance();
if (!swappy) {
return;
}
if (swappy->enabled()) swappy->mCommonBase.setAutoSwapInterval(enabled);
}
void SwappyGL::setAutoPipelineMode(bool enabled) {
SwappyGL *swappy = getInstance();
if (!swappy) {
return;
}
if (swappy->enabled()) swappy->mCommonBase.setAutoPipelineMode(enabled);
}
void SwappyGL::setMaxAutoSwapDuration(std::chrono::nanoseconds maxDuration) {
SwappyGL *swappy = getInstance();
if (!swappy) {
return;
}
if (swappy->enabled())
swappy->mCommonBase.setMaxAutoSwapDuration(maxDuration);
}
void SwappyGL::enableStats(bool enabled) {
SwappyGL *swappy = getInstance();
if (!swappy) {
return;
}
if (!swappy->enabled()) {
return;
}
if (!swappy->getEgl()->statsSupported()) {
ALOGI("stats are not suppored on this platform");
return;
}
if (enabled) {
if (!swappy->mFrameStatistics ||
swappy->mFrameStatistics->isEssential()) {
swappy->mFrameStatistics = std::make_shared<FullFrameStatisticsGL>(
*swappy->mEgl, swappy->mCommonBase);
ALOGI("Enabling stats");
} else {
ALOGI("Stats already enabled");
}
} else {
swappy->mFrameStatistics = std::make_shared<LatencyFrameStatisticsGL>(
*swappy->mEgl, swappy->mCommonBase);
ALOGI("Disabling stats");
}
swappy->mCommonBase.setFrameStatistics(swappy->mFrameStatistics);
}
void SwappyGL::recordFrameStart(EGLDisplay display, EGLSurface surface) {
TRACE_CALL();
SwappyGL *swappy = getInstance();
if (!swappy) {
return;
}
if (swappy->mFrameStatistics)
swappy->mFrameStatistics->capture(display, surface);
}
void SwappyGL::getStats(SwappyStats *stats) {
SwappyGL *swappy = getInstance();
if (!swappy) {
return;
}
if (swappy->mFrameStatistics && !swappy->mFrameStatistics->isEssential())
*stats = swappy->mFrameStatistics->getStats();
}
SwappyGL *SwappyGL::getInstance() {
std::lock_guard<std::mutex> lock(sInstanceMutex);
return sInstance.get();
}
bool SwappyGL::isEnabled() {
SwappyGL *swappy = getInstance();
if (!swappy) {
// This is a case of error.
// We do not log anything here, so that we do not spam
// the user when this function is called each frame.
return false;
}
return swappy->enabled();
}
void SwappyGL::destroyInstance() {
std::lock_guard<std::mutex> lock(sInstanceMutex);
sInstance.reset();
}
void SwappyGL::setFenceTimeout(std::chrono::nanoseconds t) {
SwappyGL *swappy = getInstance();
if (!swappy || !swappy->enabled()) {
return;
}
swappy->mCommonBase.setFenceTimeout(t);
}
std::chrono::nanoseconds SwappyGL::getFenceTimeout() {
SwappyGL *swappy = getInstance();
if (!swappy || !swappy->enabled()) {
return std::chrono::nanoseconds(0);
}
return swappy->mCommonBase.getFenceTimeout();
}
EGL *SwappyGL::getEgl() {
static thread_local EGL *egl = nullptr;
if (!egl) {
std::lock_guard<std::mutex> lock(mEglMutex);
egl = mEgl.get();
}
return egl;
}
SwappyGL::SwappyGL(JNIEnv *env, jobject jactivity, ConstructorTag)
: mFrameStatistics(nullptr), mCommonBase(env, jactivity) {
{
std::lock_guard<std::mutex> lock(mEglMutex);
mEgl = EGL::create(mCommonBase.getFenceTimeout());
if (!mEgl) {
ALOGE("Failed to load EGL functions");
mEnableSwappy = false;
return;
}
}
if (!mCommonBase.isValid()) {
ALOGE("SwappyCommon could not initialize correctly.");
mEnableSwappy = false;
return;
}
mEnableSwappy =
!gamesdk::GetSystemPropAsBool(SWAPPY_SYSTEM_PROP_KEY_DISABLE, false);
if (!enabled()) {
ALOGI("Swappy is disabled");
return;
}
ALOGI("SwappyGL initialized successfully");
}
void SwappyGL::resetSyncFence(EGLDisplay display) {
getEgl()->resetSyncFence(display);
}
bool SwappyGL::setPresentationTime(EGLDisplay display, EGLSurface surface) {
TRACE_CALL();
auto displayTimings = Settings::getInstance()->getDisplayTimings();
// if we are too close to the vsync, there is no need to set presentation
// time
if ((mCommonBase.getPresentationTime() - std::chrono::steady_clock::now()) <
(mCommonBase.getRefreshPeriod() - displayTimings.sfOffset)) {
return EGL_TRUE;
}
return getEgl()->setPresentationTime(display, surface,
mCommonBase.getPresentationTime());
}
void SwappyGL::setBufferStuffingFixWait(int32_t n_frames) {
TRACE_CALL();
SwappyGL *swappy = getInstance();
if (!swappy) {
return;
}
swappy->mCommonBase.setBufferStuffingFixWait(n_frames);
}
} // namespace swappy

View File

@ -0,0 +1,110 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <jni.h>
#include <chrono>
#include <mutex>
#include "EGL.h"
#include "FrameStatisticsGL.h"
#include "SwappyCommon.h"
#include "swappy/swappyGL.h"
#include "swappy/swappyGL_extra.h"
namespace swappy {
using EGLDisplay = void *;
using EGLSurface = void *;
using namespace std::chrono_literals;
class SwappyGL {
private:
// Allows construction with std::unique_ptr from a static method, but
// disallows construction outside of the class since no one else can
// construct a ConstructorTag
struct ConstructorTag {};
public:
SwappyGL(JNIEnv *env, jobject jactivity, ConstructorTag);
static bool init(JNIEnv *env, jobject jactivity);
static bool setWindow(ANativeWindow *window);
static void onChoreographer(int64_t frameTimeNanos);
static bool swap(EGLDisplay display, EGLSurface surface);
// Pass callbacks for tracing within the swap function
static void addTracer(const SwappyTracer *tracer);
static void removeTracer(const SwappyTracer *tracer);
static std::chrono::nanoseconds getSwapDuration();
static void setAutoSwapInterval(bool enabled);
static void setAutoPipelineMode(bool enabled);
static void setMaxAutoSwapDuration(std::chrono::nanoseconds maxDuration);
static void enableStats(bool enabled);
static void recordFrameStart(EGLDisplay display, EGLSurface surface);
static void getStats(SwappyStats *stats);
static bool isEnabled();
static void destroyInstance();
static void setFenceTimeout(std::chrono::nanoseconds t);
static std::chrono::nanoseconds getFenceTimeout();
static void setBufferStuffingFixWait(int32_t n_frames);
private:
static SwappyGL *getInstance();
bool enabled() const { return mEnableSwappy; }
EGL *getEgl();
bool swapInternal(EGLDisplay display, EGLSurface surface);
bool lastFrameIsComplete(EGLDisplay display);
// Destroys the previous sync fence (if any) and creates a new one for this
// frame
void resetSyncFence(EGLDisplay display);
// Computes the desired presentation time based on the swap interval and
// sets it using eglPresentationTimeANDROID
bool setPresentationTime(EGLDisplay display, EGLSurface surface);
bool mEnableSwappy = true;
static std::mutex sInstanceMutex;
static std::unique_ptr<SwappyGL> sInstance GUARDED_BY(sInstanceMutex);
std::mutex mEglMutex;
std::unique_ptr<EGL> mEgl;
std::shared_ptr<LatencyFrameStatisticsGL> mFrameStatistics;
SwappyCommon mCommonBase;
};
} // namespace swappy

View File

@ -0,0 +1,122 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// API entry points
#include <chrono>
#include "Settings.h"
#include "SwappyGL.h"
#include "swappy/swappyGL.h"
using namespace swappy;
extern "C" {
// Internal function to track Swappy version bundled in a binary.
void SWAPPY_VERSION_SYMBOL();
/**
* @brief Initialize Swappy, getting the required Android parameters from the
* display subsystem via JNI.
* @param env The JNI environment where Swappy is used
* @param jactivity The activity where Swappy is used
* @return false if Swappy failed to initialize.
* @see SwappyGL_destroy
*/
bool SwappyGL_init(JNIEnv *env, jobject jactivity) {
// This call ensures that the header and the linked library are from the
// same version (if not, a linker error will be triggered because of an
// undefined symbolP).
SWAPPY_VERSION_SYMBOL();
return SwappyGL::init(env, jactivity);
}
void SwappyGL_destroy() { SwappyGL::destroyInstance(); }
void SwappyGL_onChoreographer(int64_t frameTimeNanos) {
SwappyGL::onChoreographer(frameTimeNanos);
}
bool SwappyGL_setWindow(ANativeWindow *window) {
return SwappyGL::setWindow(window);
}
bool SwappyGL_swap(EGLDisplay display, EGLSurface surface) {
return SwappyGL::swap(display, surface);
}
void SwappyGL_setUseAffinity(bool tf) {
Settings::getInstance()->setUseAffinity(tf);
}
void SwappyGL_setSwapIntervalNS(uint64_t swap_ns) {
Settings::getInstance()->setSwapDuration(swap_ns);
}
uint64_t SwappyGL_getRefreshPeriodNanos() {
return Settings::getInstance()->getDisplayTimings().refreshPeriod.count();
}
bool SwappyGL_getUseAffinity() {
return Settings::getInstance()->getUseAffinity();
}
uint64_t SwappyGL_getSwapIntervalNS() {
return SwappyGL::getSwapDuration().count();
}
void SwappyGL_injectTracer(const SwappyTracer *t) { SwappyGL::addTracer(t); }
void SwappyGL_setAutoSwapInterval(bool enabled) {
SwappyGL::setAutoSwapInterval(enabled);
}
void SwappyGL_setMaxAutoSwapIntervalNS(uint64_t max_swap_ns) {
SwappyGL::setMaxAutoSwapDuration(std::chrono::nanoseconds(max_swap_ns));
}
void SwappyGL_setAutoPipelineMode(bool enabled) {
SwappyGL::setAutoPipelineMode(enabled);
}
void SwappyGL_enableStats(bool enabled) { SwappyGL::enableStats(enabled); }
void SwappyGL_recordFrameStart(EGLDisplay display, EGLSurface surface) {
SwappyGL::recordFrameStart(display, surface);
}
void SwappyGL_getStats(SwappyStats *stats) { SwappyGL::getStats(stats); }
bool SwappyGL_isEnabled() { return SwappyGL::isEnabled(); }
void SwappyGL_setFenceTimeoutNS(uint64_t t) {
SwappyGL::setFenceTimeout(std::chrono::nanoseconds(t));
}
uint64_t SwappyGL_getFenceTimeoutNS() {
return SwappyGL::getFenceTimeout().count();
}
void SwappyGL_setBufferStuffingFixWait(int32_t n_frames) {
SwappyGL::setBufferStuffingFixWait(n_frames);
}
void SwappyGL_uninjectTracer(const SwappyTracer *t) {
SwappyGL::removeTracer(t);
}
} // extern "C" {

View File

@ -0,0 +1,194 @@
# Lint as: python3
"""Check whether Swappy is working properly.
This script takes a logcat (list of lines - e.g. from readlines()) and the path
to a systrace and applies checks on them to verify whether Swappy is working
properly or not. A zero return code means all looks fine, while other codes
indicate different problems.
"""
import decimal
import re
from typing import List
# Beautiful Soup parsing library
import bs4
def test_swappy_initialized(logcat: List[bytes]) -> str:
"""Verify whether Swappy initialized successfully
Takes logcat data argument and searches for FrameStatistics blocks like this
example:
Args:
logcat: Logcat data.
Returns:
The Swappy backend name or an empty string if Swappy is not initialized.
"""
choreographer_re = re.compile(rb'Using (\S+) Choreographer')
choreographer_backend = ''
for line in logcat:
match = choreographer_re.search(line)
if match:
choreographer_backend = match.group(1)
return choreographer_backend
# pylint: disable=line-too-long
def test_swappy_frame_stats(logcat: List[bytes]) -> bool:
"""Verify whether Swappy is producing frame statistics.
Takes logcat data argument and searches for FrameStatistics blocks like this
example:
12-02 00:24:47.630: I/FrameStatistics(15134): == Frame statistics ==
12-02 00:24:47.630: I/FrameStatistics(15134): total frames: 123
12-02 00:24:47.630: I/FrameStatistics(15134): Buckets: [0] [1] [2] [3] [4] [5]
12-02 00:24:47.630: I/FrameStatistics(15134): idle frames: 116 7 0 0 0 0
12-02 00:24:47.630: I/FrameStatistics(15134): late frames: 121 1 1 0 0 0
12-02 00:24:47.630: I/FrameStatistics(15134): offset from previous frame: 122 0 0 0 0 0
12-02 00:24:47.630: I/FrameStatistics(15134): frame latency: 0 121 1 1 0 0
A basic consistency check is applied: successive "total frames" values should
never decrease. If not even a single block is found, Swappy is deemed to be
off.
Args:
logcat: Logcat data.
Returns:
True if it looks like Swappy is on
"""
# pylint: enable=line-too-long
frame_stats_re = re.compile(rb'I/FrameStatistics\(\d+\): total frames: (\d+)')
total_frames = -1
for line in logcat:
match = frame_stats_re.search(line)
if match:
new_total_frames = int(match.group(1))
if new_total_frames < total_frames:
return False
total_frames = new_total_frames
print(dict(total_frames=total_frames))
return True if total_frames > -1 else False
def test_swappy_working(
logcat: List[bytes], systrace_path: str, latency_thr_ms: float) -> bool:
"""Verify whether Swappy seems to be working correctly.
For more details on this algorithm please see: http://go/swappy-systrace.
Args:
logcat: Logcat data.
systrace_path: Path to the systrace file.
latency_thr_ms: Maximum average queue latency expected if Swappy is working.
Returns:
True if it looks like Swappy is working correctly.
"""
# Finding out surfaceflinger's PID from the logcat is way easier than from the
# systrace
sf_pid = get_surfaceflinger_pid(logcat)
if sf_pid is None:
print("Couldn't find surface flinger pid in logcat")
trace_data = get_trace_data(systrace_path)
soup = bs4.BeautifulSoup(trace_data, features='html.parser')
script_tags = soup.find_all('script', attrs={'class': 'trace-data'})
# The number of trace-data script tags apparently depends on device model.
# If there's more than one, we look at the longest one because probably
# (hopefully) that's where the actual trace is.
trace_tag = max([(len(tag.string), tag) for tag in script_tags])[1]
data = trace_tag.string.split('\n')
data = [line.strip() for line in data if line.strip() and line[0] != '#']
# We are interested in COUNTER events for the line named:
# 'SurfaceView - <PACKAGE>/<ACTIVITY>'
# Examples seen so far illustrate the expected possible formats:
# pylint: disable=line-too-long
# <...>-3194 (-----) [001] ...1 33425.567500: tracing_mark_write: C|3194|SurfaceView - com.gameloft.android.ANMP.GloftA8HM/com.gameloft.android.ANMP.GloftA8HM.MainActivity@85d2511@0#0|0
# surfaceflinger-3194 ( 3194) [001] ...1 33662.535536: tracing_mark_write: C|3194|SurfaceView - com.gameloft.android.ANMP.GloftA8HM/com.gameloft.android.ANMP.GloftA8HM.MainActivity@73c2e5f@0#0|0
# Binder:486_2-533 ( 486) [000] ...1 69410.718619: tracing_mark_write: C|486|SurfaceView - com.prefabulated.swappy/com.prefabulated.bouncyball.OrbitActivity#0|2
# pylint: enable=line-too-long
if sf_pid is not None:
event_re_str = (
r' (?P<time>\d+\.\d{6}): tracing_mark_write: C\|'
+ str(sf_pid)
+ r'\|SurfaceView - .*\|(?P<value>\d+)')
else:
event_re_str = (
r' (?P<time>\d+\.\d{6}): tracing_mark_write: C\|\d+\|SurfaceView - .*\|(?P<value>\d+)')
event_re = re.compile(event_re_str)
current_time_s = None
current_depth = None
enqueued_frames = 0
total_queuing_time_s = decimal.Decimal(0)
for line in data:
match = event_re.search(line)
if match:
new_time_s = decimal.Decimal(match.group('time'))
new_depth = int(match.group('value'))
if current_depth is not None:
if new_depth > current_depth:
enqueued_frames += new_depth - current_depth
if current_depth > 0:
total_queuing_time_s += current_depth * (new_time_s - current_time_s)
current_time_s = new_time_s
current_depth = new_depth
total_queuing_time_ms = total_queuing_time_s * 1000
avg_queue_latency_ms = total_queuing_time_ms / enqueued_frames
print(dict(
enqueued_frames=enqueued_frames,
total_queuing_time_ms=float(total_queuing_time_ms),
latency_thr_ms=latency_thr_ms,
avg_queue_latency_ms=float(avg_queue_latency_ms)))
return avg_queue_latency_ms < latency_thr_ms
def get_surfaceflinger_pid(logcat: List[bytes]) -> int:
"""Obtain the surfaceflinger PID from the logcat.
All instances of the PID in the file must be the same, or an AssertionError
will be raised.
Args:
logcat: Logcat data.
Returns:
surfaceflinger PID number
"""
# Look for e.g.
# 12-03 12:44:37.667: W/SurfaceFlinger(1234):'
sf_pid_re = re.compile(
rb'\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d: [WIDE]\/SurfaceFlinger\((\d+)\): ')
# Sometimes adb logcat gives a different format, like this:
# 12-03 12:44:37.667 602 1641 W SurfaceFlinger'
sf_pid_2_re = re.compile(rb'\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\s+(\d+)\s+(\d+)\s+[WIDE]\s+SurfaceFlinger')
sf_pid = None
for line in logcat:
match = sf_pid_re.search(line)
if not match:
match = sf_pid_2_re.search(line)
if match:
this_pid = int(match.group(1))
if sf_pid is None:
sf_pid = this_pid
else:
assert sf_pid == this_pid
return sf_pid
def get_trace_data(systrace_path: str) -> bytes:
"""Open file and get just the trace data (ignoring HTML data and etc.)."""
with open(systrace_path, 'rb') as f:
file_data = f.read()
start_marker = b'<!-- BEGIN TRACE -->'
end_marker = b'<!-- END TRACE -->'
start_pos = file_data.index(start_marker) + len(start_marker)
end_pos = file_data.index(end_marker)
return file_data[start_pos:end_pos]

View File

@ -0,0 +1,57 @@
# Lint as: python3
"""Check whether Swappy is working properly.
This script takes paths to a logcat and a systrace and applies checks on them to
verify whether Swappy is working properly or not. A zero return code means all
looks fine, while other codes indicate different problems.
To create the logcat file:
adb logcat -d > $filename$
To create the systrace file:
python systrace/systrace.py -e $device ID$ -o $output file$ -t $length in seconds$ gfx
"""
import sys
import argparse
import lib_test_swappy
RET_CODE_SWAPPY_NOT_ON = 10
RET_CODE_SWAPPY_NOT_WORKING = 11
RET_CODE_SWAPPY_NO_FRAME_STATS = 12
def main():
"""
Main function
"""
parser = argparse.ArgumentParser(description='Check Swappy logcat and systrace output')
parser.add_argument('--logcat', type=str, help='logcat file', required=True)
parser.add_argument('--systrace', type=str, help='systrace file')
parser.add_argument('--latency_thr_ms', type=float, default=25.0,
help='Latency threshold in milliseconds')
parser.add_argument('--no_frame_stats', action='store_true', help="Don't check frame statistics")
args = parser.parse_args()
print('logcat:', args.logcat)
print('systrace:', args.systrace)
with open(args.logcat, 'rb') as logcat_file:
logcat = logcat_file.readlines()
swappy_backend = lib_test_swappy.test_swappy_initialized(logcat)
if not swappy_backend:
sys.exit(RET_CODE_SWAPPY_NOT_ON)
print("Swappy choreographer backend = %s" % swappy_backend)
if not args.no_frame_stats:
if not lib_test_swappy.test_swappy_frame_stats(logcat):
sys.exit(RET_CODE_SWAPPY_NO_FRAME_STATS)
if args.systrace:
if not lib_test_swappy.test_swappy_working(logcat, args.systrace, args.latency_thr_ms):
sys.exit(RET_CODE_SWAPPY_NOT_WORKING)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,300 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SwappyVk.h"
namespace swappy {
class DefaultSwappyVkFunctionProvider {
public:
static bool Init() {
if (!mLibVulkan) {
// This is the first time we've been called
mLibVulkan = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
if (!mLibVulkan) {
// If Vulkan doesn't exist, bail-out early:
return false;
}
}
return true;
}
static void* GetProcAddr(const char* name) {
if (!mLibVulkan && !Init()) return nullptr;
return dlsym(mLibVulkan, name);
}
static void Close() {
if (mLibVulkan) {
dlclose(mLibVulkan);
mLibVulkan = nullptr;
}
}
private:
static void* mLibVulkan;
};
void* DefaultSwappyVkFunctionProvider::mLibVulkan = nullptr;
bool SwappyVk::InitFunctions() {
if (pFunctionProvider == nullptr) {
static SwappyVkFunctionProvider c_provider;
c_provider.init = &DefaultSwappyVkFunctionProvider::Init;
c_provider.getProcAddr = &DefaultSwappyVkFunctionProvider::GetProcAddr;
c_provider.close = &DefaultSwappyVkFunctionProvider::Close;
pFunctionProvider = &c_provider;
}
if (pFunctionProvider->init()) {
LoadVulkanFunctions(pFunctionProvider);
return true;
} else {
return false;
}
}
void SwappyVk::SetFunctionProvider(
const SwappyVkFunctionProvider* functionProvider) {
if (pFunctionProvider != nullptr) pFunctionProvider->close();
pFunctionProvider = functionProvider;
}
/**
* Generic/Singleton implementation of swappyVkDetermineDeviceExtensions.
*/
void SwappyVk::swappyVkDetermineDeviceExtensions(
VkPhysicalDevice physicalDevice, uint32_t availableExtensionCount,
VkExtensionProperties* pAvailableExtensions,
uint32_t* pRequiredExtensionCount, char** pRequiredExtensions) {
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
// TODO: Refactor this to be more concise:
if (!pRequiredExtensions) {
for (uint32_t i = 0; i < availableExtensionCount; i++) {
if (!strcmp(VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME,
pAvailableExtensions[i].extensionName)) {
(*pRequiredExtensionCount)++;
}
}
} else {
doesPhysicalDeviceHaveGoogleDisplayTiming[physicalDevice] = false;
for (uint32_t i = 0, j = 0; i < availableExtensionCount; i++) {
if (!strcmp(VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME,
pAvailableExtensions[i].extensionName)) {
if (j < *pRequiredExtensionCount) {
strcpy(pRequiredExtensions[j++],
VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME);
doesPhysicalDeviceHaveGoogleDisplayTiming[physicalDevice] =
true;
}
}
}
}
#else
doesPhysicalDeviceHaveGoogleDisplayTiming[physicalDevice] = false;
#endif
}
void SwappyVk::SetQueueFamilyIndex(VkDevice device, VkQueue queue,
uint32_t queueFamilyIndex) {
perQueueFamilyIndex[queue] = {device, queueFamilyIndex};
}
/**
* Generic/Singleton implementation of swappyVkGetRefreshCycleDuration.
*/
bool SwappyVk::GetRefreshCycleDuration(JNIEnv* env, jobject jactivity,
VkPhysicalDevice physicalDevice,
VkDevice device,
VkSwapchainKHR swapchain,
uint64_t* pRefreshDuration) {
auto& pImplementation = perSwapchainImplementation[swapchain];
if (!pImplementation) {
if (!InitFunctions()) {
// If Vulkan doesn't exist, bail-out early
return false;
}
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
// First, based on whether VK_GOOGLE_display_timing is available
// (determined and cached by swappyVkDetermineDeviceExtensions),
// determine which derived class to use to implement the rest of the API
if (doesPhysicalDeviceHaveGoogleDisplayTiming[physicalDevice]) {
pImplementation = std::make_shared<SwappyVkGoogleDisplayTiming>(
env, jactivity, physicalDevice, device, pFunctionProvider);
ALOGV(
"SwappyVk initialized for VkDevice %p using "
"VK_GOOGLE_display_timing on Android",
device);
} else
#endif
{
pImplementation = std::make_shared<SwappyVkFallback>(
env, jactivity, physicalDevice, device, pFunctionProvider);
ALOGV("SwappyVk initialized for VkDevice %p using Android fallback",
device);
}
if (!pImplementation) { // should never happen
ALOGE(
"SwappyVk could not find or create correct implementation for "
"the current environment: "
"%p, %p",
physicalDevice, device);
return false;
}
}
// Now, call that derived class to get the refresh duration to return
return pImplementation->doGetRefreshCycleDuration(swapchain,
pRefreshDuration);
}
/**
* Generic/Singleton implementation of swappyVkSetWindow.
*/
void SwappyVk::SetWindow(VkDevice device, VkSwapchainKHR swapchain,
ANativeWindow* window) {
auto& pImplementation = perSwapchainImplementation[swapchain];
if (!pImplementation) {
return;
}
pImplementation->doSetWindow(window);
}
/**
* Generic/Singleton implementation of swappyVkSetSwapInterval.
*/
void SwappyVk::SetSwapDuration(VkDevice device, VkSwapchainKHR swapchain,
uint64_t swapNs) {
auto& pImplementation = perSwapchainImplementation[swapchain];
if (!pImplementation) {
return;
}
pImplementation->doSetSwapInterval(swapchain, swapNs);
}
/**
* Generic/Singleton implementation of swappyVkQueuePresent.
*/
VkResult SwappyVk::QueuePresent(VkQueue queue,
const VkPresentInfoKHR* pPresentInfo) {
if (perQueueFamilyIndex.find(queue) == perQueueFamilyIndex.end()) {
ALOGE("Unknown queue %p. Did you call SwappyVkSetQueueFamilyIndex ?",
queue);
return VK_INCOMPLETE;
}
// This command doesn't have a VkDevice. It should have at least one
// VkSwapchainKHR's. For this command, all VkSwapchainKHR's will have the
// same VkDevice and VkQueue.
if ((pPresentInfo->swapchainCount == 0) || (!pPresentInfo->pSwapchains)) {
// This shouldn't happen, but if it does, something is really wrong.
return VK_ERROR_DEVICE_LOST;
}
auto& pImplementation =
perSwapchainImplementation[*pPresentInfo->pSwapchains];
if (pImplementation) {
return pImplementation->doQueuePresent(
queue, perQueueFamilyIndex[queue].queueFamilyIndex, pPresentInfo);
} else {
// This should only happen if the API was used wrong (e.g. they never
// called swappyVkGetRefreshCycleDuration).
// NOTE: Technically, a Vulkan library shouldn't protect a user from
// themselves, but we'll be friendlier
return VK_ERROR_DEVICE_LOST;
}
}
void SwappyVk::DestroySwapchain(VkDevice /*device*/, VkSwapchainKHR swapchain) {
auto swapchain_it = perSwapchainImplementation.find(swapchain);
if (swapchain_it == perSwapchainImplementation.end()) return;
perSwapchainImplementation.erase(swapchain);
}
void SwappyVk::DestroyDevice(VkDevice device) {
{
// Erase swapchains
auto it = perSwapchainImplementation.begin();
while (it != perSwapchainImplementation.end()) {
if (it->second->getDevice() == device) {
it = perSwapchainImplementation.erase(it);
} else {
++it;
}
}
}
{
// Erase the device
auto it = perQueueFamilyIndex.begin();
while (it != perQueueFamilyIndex.end()) {
if (it->second.device == device) {
it = perQueueFamilyIndex.erase(it);
} else {
++it;
}
}
}
}
void SwappyVk::SetAutoSwapInterval(bool enabled) {
for (auto i : perSwapchainImplementation) {
i.second->setAutoSwapInterval(enabled);
}
}
void SwappyVk::SetAutoPipelineMode(bool enabled) {
for (auto i : perSwapchainImplementation) {
i.second->setAutoPipelineMode(enabled);
}
}
void SwappyVk::SetMaxAutoSwapDuration(std::chrono::nanoseconds maxDuration) {
for (auto i : perSwapchainImplementation) {
i.second->setMaxAutoSwapDuration(maxDuration);
}
}
void SwappyVk::SetFenceTimeout(std::chrono::nanoseconds t) {
for (auto i : perSwapchainImplementation) {
i.second->setFenceTimeout(t);
}
}
std::chrono::nanoseconds SwappyVk::GetFenceTimeout() const {
auto it = perSwapchainImplementation.begin();
if (it != perSwapchainImplementation.end()) {
return it->second->getFenceTimeout();
}
return std::chrono::nanoseconds(0);
}
std::chrono::nanoseconds SwappyVk::GetSwapInterval(VkSwapchainKHR swapchain) {
auto it = perSwapchainImplementation.find(swapchain);
if (it != perSwapchainImplementation.end())
return it->second->getSwapInterval();
return std::chrono::nanoseconds(0);
}
void SwappyVk::addTracer(const SwappyTracer* t) {
for (auto i : perSwapchainImplementation) {
i.second->addTracer(t);
}
}
void SwappyVk::removeTracer(const SwappyTracer* t) {
for (auto i : perSwapchainImplementation) {
i.second->removeTracer(t);
}
}
} // namespace swappy

View File

@ -0,0 +1,110 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "SwappyVkBase.h"
#include "SwappyVkFallback.h"
#include "SwappyVkGoogleDisplayTiming.h"
namespace swappy {
// The API functions call methods of the singleton SwappyVk class.
// Those methods call virtual methods of the abstract SwappyVkBase class,
// which is actually implemented by one of the derived/concrete classes:
//
// - SwappyVkGoogleDisplayTiming
// - SwappyVkFallback
/***************************************************************************************************
*
* Singleton class that provides the high-level implementation of the Swappy
*entrypoints.
*
***************************************************************************************************/
/**
* Singleton class that provides the high-level implementation of the Swappy
* entrypoints.
*
* This class determines which low-level implementation to use for each physical
* device, and then calls that class's do-method for the entrypoint.
*/
class SwappyVk {
public:
static SwappyVk& getInstance() {
static SwappyVk instance;
return instance;
}
~SwappyVk() {
if (pFunctionProvider) {
pFunctionProvider->close();
}
}
void swappyVkDetermineDeviceExtensions(
VkPhysicalDevice physicalDevice, uint32_t availableExtensionCount,
VkExtensionProperties* pAvailableExtensions,
uint32_t* pRequiredExtensionCount, char** pRequiredExtensions);
void SetQueueFamilyIndex(VkDevice device, VkQueue queue,
uint32_t queueFamilyIndex);
bool GetRefreshCycleDuration(JNIEnv* env, jobject jactivity,
VkPhysicalDevice physicalDevice,
VkDevice device, VkSwapchainKHR swapchain,
uint64_t* pRefreshDuration);
void SetWindow(VkDevice device, VkSwapchainKHR swapchain,
ANativeWindow* window);
void SetSwapDuration(VkDevice device, VkSwapchainKHR swapchain,
uint64_t swapNs);
VkResult QueuePresent(VkQueue queue, const VkPresentInfoKHR* pPresentInfo);
void DestroySwapchain(VkDevice device, VkSwapchainKHR swapchain);
void DestroyDevice(VkDevice device);
void SetAutoSwapInterval(bool enabled);
void SetAutoPipelineMode(bool enabled);
void SetMaxAutoSwapDuration(std::chrono::nanoseconds maxDuration);
void SetFenceTimeout(std::chrono::nanoseconds duration);
std::chrono::nanoseconds GetFenceTimeout() const;
std::chrono::nanoseconds GetSwapInterval(VkSwapchainKHR swapchain);
void addTracer(const SwappyTracer* t);
void removeTracer(const SwappyTracer* t);
void SetFunctionProvider(const SwappyVkFunctionProvider* pFunctionProvider);
bool InitFunctions();
private:
std::map<VkPhysicalDevice, bool> doesPhysicalDeviceHaveGoogleDisplayTiming;
std::map<VkSwapchainKHR, std::shared_ptr<SwappyVkBase>>
perSwapchainImplementation;
struct QueueFamilyIndex {
VkDevice device;
uint32_t queueFamilyIndex;
};
std::map<VkQueue, QueueFamilyIndex> perQueueFamilyIndex;
const SwappyVkFunctionProvider* pFunctionProvider = nullptr;
private:
SwappyVk() {} // Need to implement this constructor
// Forbid copies.
SwappyVk(SwappyVk const&) = delete;
void operator=(SwappyVk const&) = delete;
};
} // namespace swappy

View File

@ -0,0 +1,434 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SwappyVkBase.h"
#include "system_utils.h"
#define LOG_TAG "SwappyVkBase"
namespace swappy {
PFN_vkCreateCommandPool vkCreateCommandPool = nullptr;
PFN_vkDestroyCommandPool vkDestroyCommandPool = nullptr;
PFN_vkCreateFence vkCreateFence = nullptr;
PFN_vkDestroyFence vkDestroyFence = nullptr;
PFN_vkWaitForFences vkWaitForFences = nullptr;
PFN_vkGetFenceStatus vkGetFenceStatus = nullptr;
PFN_vkResetFences vkResetFences = nullptr;
PFN_vkCreateSemaphore vkCreateSemaphore = nullptr;
PFN_vkDestroySemaphore vkDestroySemaphore = nullptr;
PFN_vkCreateEvent vkCreateEvent = nullptr;
PFN_vkDestroyEvent vkDestroyEvent = nullptr;
PFN_vkCmdSetEvent vkCmdSetEvent = nullptr;
PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers = nullptr;
PFN_vkFreeCommandBuffers vkFreeCommandBuffers = nullptr;
PFN_vkBeginCommandBuffer vkBeginCommandBuffer = nullptr;
PFN_vkEndCommandBuffer vkEndCommandBuffer = nullptr;
PFN_vkQueueSubmit vkQueueSubmit = nullptr;
void LoadVulkanFunctions(const SwappyVkFunctionProvider* pFunctionProvider) {
if (vkCreateCommandPool == nullptr) {
vkCreateCommandPool = reinterpret_cast<PFN_vkCreateCommandPool>(
pFunctionProvider->getProcAddr("vkCreateCommandPool"));
vkDestroyCommandPool = reinterpret_cast<PFN_vkDestroyCommandPool>(
pFunctionProvider->getProcAddr("vkDestroyCommandPool"));
vkCreateFence = reinterpret_cast<PFN_vkCreateFence>(
pFunctionProvider->getProcAddr("vkCreateFence"));
vkDestroyFence = reinterpret_cast<PFN_vkDestroyFence>(
pFunctionProvider->getProcAddr("vkDestroyFence"));
vkWaitForFences = reinterpret_cast<PFN_vkWaitForFences>(
pFunctionProvider->getProcAddr("vkWaitForFences"));
vkGetFenceStatus = reinterpret_cast<PFN_vkGetFenceStatus>(
pFunctionProvider->getProcAddr("vkGetFenceStatus"));
vkResetFences = reinterpret_cast<PFN_vkResetFences>(
pFunctionProvider->getProcAddr("vkResetFences"));
vkCreateSemaphore = reinterpret_cast<PFN_vkCreateSemaphore>(
pFunctionProvider->getProcAddr("vkCreateSemaphore"));
vkDestroySemaphore = reinterpret_cast<PFN_vkDestroySemaphore>(
pFunctionProvider->getProcAddr("vkDestroySemaphore"));
vkCreateEvent = reinterpret_cast<PFN_vkCreateEvent>(
pFunctionProvider->getProcAddr("vkCreateEvent"));
vkDestroyEvent = reinterpret_cast<PFN_vkDestroyEvent>(
pFunctionProvider->getProcAddr("vkDestroyEvent"));
vkCmdSetEvent = reinterpret_cast<PFN_vkCmdSetEvent>(
pFunctionProvider->getProcAddr("vkCmdSetEvent"));
vkAllocateCommandBuffers =
reinterpret_cast<PFN_vkAllocateCommandBuffers>(
pFunctionProvider->getProcAddr("vkAllocateCommandBuffers"));
vkFreeCommandBuffers = reinterpret_cast<PFN_vkFreeCommandBuffers>(
pFunctionProvider->getProcAddr("vkFreeCommandBuffers"));
vkBeginCommandBuffer = reinterpret_cast<PFN_vkBeginCommandBuffer>(
pFunctionProvider->getProcAddr("vkBeginCommandBuffer"));
vkEndCommandBuffer = reinterpret_cast<PFN_vkEndCommandBuffer>(
pFunctionProvider->getProcAddr("vkEndCommandBuffer"));
vkQueueSubmit = reinterpret_cast<PFN_vkQueueSubmit>(
pFunctionProvider->getProcAddr("vkQueueSubmit"));
}
}
SwappyVkBase::SwappyVkBase(JNIEnv* env, jobject jactivity,
VkPhysicalDevice physicalDevice, VkDevice device,
const SwappyVkFunctionProvider* pFunctionProvider)
: mCommonBase(env, jactivity),
mPhysicalDevice(physicalDevice),
mDevice(device),
mpFunctionProvider(pFunctionProvider),
mInitialized(false),
mEnabled(false) {
if (!mCommonBase.isValid()) {
ALOGE("SwappyCommon could not initialize correctly.");
return;
}
mpfnGetDeviceProcAddr = reinterpret_cast<PFN_vkGetDeviceProcAddr>(
mpFunctionProvider->getProcAddr("vkGetDeviceProcAddr"));
mpfnQueuePresentKHR = reinterpret_cast<PFN_vkQueuePresentKHR>(
mpfnGetDeviceProcAddr(mDevice, "vkQueuePresentKHR"));
initGoogExtension();
mEnabled =
!gamesdk::GetSystemPropAsBool(SWAPPY_SYSTEM_PROP_KEY_DISABLE, false);
}
void SwappyVkBase::initGoogExtension() {
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
mpfnGetRefreshCycleDurationGOOGLE =
reinterpret_cast<PFN_vkGetRefreshCycleDurationGOOGLE>(
mpfnGetDeviceProcAddr(mDevice, "vkGetRefreshCycleDurationGOOGLE"));
mpfnGetPastPresentationTimingGOOGLE =
reinterpret_cast<PFN_vkGetPastPresentationTimingGOOGLE>(
mpfnGetDeviceProcAddr(mDevice,
"vkGetPastPresentationTimingGOOGLE"));
#endif
}
SwappyVkBase::~SwappyVkBase() { destroyVkSyncObjects(); }
void SwappyVkBase::doSetWindow(ANativeWindow* window) {
mCommonBase.setANativeWindow(window);
}
void SwappyVkBase::doSetSwapInterval(VkSwapchainKHR swapchain,
uint64_t swapNs) {
Settings::getInstance()->setSwapDuration(swapNs);
}
VkResult SwappyVkBase::initializeVkSyncObjects(VkQueue queue,
uint32_t queueFamilyIndex) {
if (mCommandPool.find(queue) != mCommandPool.end()) {
return VK_SUCCESS;
}
VkSync sync;
const VkCommandPoolCreateInfo cmd_pool_info = {
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.pNext = NULL,
.flags = 0,
.queueFamilyIndex = queueFamilyIndex,
};
VkResult res = vkCreateCommandPool(mDevice, &cmd_pool_info, NULL,
&mCommandPool[queue]);
if (res) {
ALOGE("vkCreateCommandPool failed %d", res);
return res;
}
const VkCommandBufferAllocateInfo present_cmd_info = {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.pNext = NULL,
.commandPool = mCommandPool[queue],
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1,
};
for (int i = 0; i < MAX_PENDING_FENCES; i++) {
VkFenceCreateInfo fence_ci = {
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
.pNext = NULL,
.flags = VK_FENCE_CREATE_SIGNALED_BIT};
res = vkCreateFence(mDevice, &fence_ci, NULL, &sync.fence);
if (res) {
ALOGE("failed to create fence: %d", res);
return res;
}
VkSemaphoreCreateInfo semaphore_ci = {
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
.pNext = NULL,
.flags = 0};
res = vkCreateSemaphore(mDevice, &semaphore_ci, NULL, &sync.semaphore);
if (res) {
ALOGE("failed to create semaphore: %d", res);
return res;
}
res =
vkAllocateCommandBuffers(mDevice, &present_cmd_info, &sync.command);
if (res) {
ALOGE("vkAllocateCommandBuffers failed %d", res);
return res;
}
const VkCommandBufferBeginInfo cmd_buf_info = {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.pNext = NULL,
.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT,
.pInheritanceInfo = NULL,
};
res = vkBeginCommandBuffer(sync.command, &cmd_buf_info);
if (res) {
ALOGE("vkAllocateCommandBuffers failed %d", res);
return res;
}
VkEventCreateInfo event_info = {
.sType = VK_STRUCTURE_TYPE_EVENT_CREATE_INFO,
.pNext = NULL,
.flags = 0,
};
res = vkCreateEvent(mDevice, &event_info, NULL, &sync.event);
if (res) {
ALOGE("vkCreateEvent failed %d", res);
return res;
}
vkCmdSetEvent(sync.command, sync.event,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
res = vkEndCommandBuffer(sync.command);
if (res) {
ALOGE("vkCreateEvent failed %d", res);
return res;
}
mFreeSyncPool[queue].push_back(sync);
}
// Create a thread that will wait for the fences
auto emplaceResult =
mThreads.emplace(queue, std::make_unique<ThreadContext>(queue));
auto& threadContext = emplaceResult.first->second;
// Start the thread
std::lock_guard<std::mutex> lock(threadContext->lock);
threadContext->thread =
Thread([&]() { waitForFenceThreadMain(*threadContext); });
return VK_SUCCESS;
}
void SwappyVkBase::destroyVkSyncObjects() {
// Stop all waiters threads
for (auto it = mThreads.begin(); it != mThreads.end(); it++) {
{
std::lock_guard<std::mutex> lock(it->second->lock);
it->second->running = false;
it->second->condition.notify_one();
}
it->second->thread.join();
}
// Wait for all unsignaled fences to get signlaed
for (auto it = mWaitingSyncs.begin(); it != mWaitingSyncs.end(); it++) {
auto queue = it->first;
auto syncList = it->second;
while (syncList.size() > 0) {
VkSync sync = syncList.front();
syncList.pop_front();
vkWaitForFences(mDevice, 1, &sync.fence, VK_TRUE, UINT64_MAX);
mSignaledSyncs[queue].push_back(sync);
}
}
// Move all signaled fences to the free pool
for (auto it = mSignaledSyncs.begin(); it != mSignaledSyncs.end(); it++) {
auto queue = it->first;
reclaimSignaledFences(queue);
}
// Free all sync objects
for (auto it = mFreeSyncPool.begin(); it != mFreeSyncPool.end(); it++) {
auto syncList = it->second;
while (syncList.size() > 0) {
VkSync sync = syncList.front();
syncList.pop_front();
vkFreeCommandBuffers(mDevice, mCommandPool[it->first], 1,
&sync.command);
vkDestroyEvent(mDevice, sync.event, NULL);
vkDestroySemaphore(mDevice, sync.semaphore, NULL);
vkResetFences(mDevice, 1, &sync.fence);
vkDestroyFence(mDevice, sync.fence, NULL);
}
}
// Free destroy the command pools
for (auto it = mCommandPool.begin(); it != mCommandPool.end(); it++) {
auto commandPool = it->second;
vkDestroyCommandPool(mDevice, commandPool, NULL);
}
}
void SwappyVkBase::reclaimSignaledFences(VkQueue queue) {
std::lock_guard<std::mutex> lock(mThreads[queue]->lock);
while (!mSignaledSyncs[queue].empty()) {
VkSync sync = mSignaledSyncs[queue].front();
mSignaledSyncs[queue].pop_front();
mFreeSyncPool[queue].push_back(sync);
}
}
bool SwappyVkBase::lastFrameIsCompleted(VkQueue queue) {
auto pipelineMode = mCommonBase.getCurrentPipelineMode();
std::lock_guard<std::mutex> lock(mThreads[queue]->lock);
if (pipelineMode == SwappyCommon::PipelineMode::On) {
// We are in pipeline mode so we need to check the fence of frame N-1
return mWaitingSyncs[queue].size() < 2;
}
// We are not in pipeline mode so we need to check the fence the current
// frame. i.e. there are not unsignaled frames
return mWaitingSyncs[queue].empty();
}
VkResult SwappyVkBase::injectFence(VkQueue queue,
const VkPresentInfoKHR* pPresentInfo,
VkSemaphore* pSemaphore) {
reclaimSignaledFences(queue);
// If we cross the swap interval threshold, we don't pace at all.
// In this case we might not have a free fence, so just don't use the fence.
if (mFreeSyncPool[queue].empty() ||
vkGetFenceStatus(mDevice, mFreeSyncPool[queue].front().fence) !=
VK_SUCCESS) {
*pSemaphore = VK_NULL_HANDLE;
return VK_SUCCESS;
}
VkSync sync = mFreeSyncPool[queue].front();
mFreeSyncPool[queue].pop_front();
vkResetFences(mDevice, 1, &sync.fence);
VkPipelineStageFlags pipe_stage_flags;
VkSubmitInfo submit_info;
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info.pNext = NULL;
submit_info.pWaitDstStageMask = &pipe_stage_flags;
pipe_stage_flags = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
submit_info.waitSemaphoreCount = pPresentInfo->waitSemaphoreCount;
submit_info.pWaitSemaphores = pPresentInfo->pWaitSemaphores;
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &sync.command;
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &sync.semaphore;
VkResult res = vkQueueSubmit(queue, 1, &submit_info, sync.fence);
*pSemaphore = sync.semaphore;
std::lock_guard<std::mutex> lock(mThreads[queue]->lock);
mWaitingSyncs[queue].push_back(sync);
mThreads[queue]->hasPendingWork = true;
mThreads[queue]->condition.notify_all();
return res;
}
void SwappyVkBase::setAutoSwapInterval(bool enabled) {
mCommonBase.setAutoSwapInterval(enabled);
}
void SwappyVkBase::setMaxAutoSwapDuration(std::chrono::nanoseconds swapMaxNS) {
mCommonBase.setMaxAutoSwapDuration(swapMaxNS);
}
void SwappyVkBase::setAutoPipelineMode(bool enabled) {
mCommonBase.setAutoPipelineMode(enabled);
}
void SwappyVkBase::waitForFenceThreadMain(ThreadContext& thread) {
while (true) {
bool waitingSyncsEmpty;
{
std::lock_guard<std::mutex> lock(thread.lock);
// Wait for new fence object
thread.condition.wait(thread.lock, [&]() REQUIRES(thread.lock) {
return thread.hasPendingWork || !thread.running;
});
thread.hasPendingWork = false;
if (!thread.running) {
break;
}
waitingSyncsEmpty = mWaitingSyncs[thread.queue].empty();
}
while (!waitingSyncsEmpty) {
VkSync sync;
{ // Get the sync object with a lock
std::lock_guard<std::mutex> lock(thread.lock);
sync = mWaitingSyncs[thread.queue].front();
}
gamesdk::ScopedTrace tracer("Swappy: GPU frame time");
const auto startTime = std::chrono::steady_clock::now();
VkResult result =
vkWaitForFences(mDevice, 1, &sync.fence, VK_TRUE,
mCommonBase.getFenceTimeout().count());
if (result) {
ALOGE("Failed to wait for fence %d", result);
}
mLastFenceTime = std::chrono::steady_clock::now() - startTime;
// Move the sync object to the signaled list
{
std::lock_guard<std::mutex> lock(thread.lock);
mWaitingSyncs[thread.queue].pop_front();
mSignaledSyncs[thread.queue].push_back(sync);
waitingSyncsEmpty = mWaitingSyncs[thread.queue].empty();
}
}
}
}
std::chrono::nanoseconds SwappyVkBase::getLastFenceTime(VkQueue queue) {
return mLastFenceTime;
}
void SwappyVkBase::setFenceTimeout(std::chrono::nanoseconds duration) {
mCommonBase.setFenceTimeout(duration);
}
std::chrono::nanoseconds SwappyVkBase::getFenceTimeout() const {
return mCommonBase.getFenceTimeout();
}
std::chrono::nanoseconds SwappyVkBase::getSwapInterval() {
return mCommonBase.getSwapDuration();
}
void SwappyVkBase::addTracer(const SwappyTracer* tracer) {
if (tracer != nullptr) mCommonBase.addTracerCallbacks(*tracer);
}
void SwappyVkBase::removeTracer(const SwappyTracer* tracer) {
if (tracer != nullptr) mCommonBase.removeTracerCallbacks(*tracer);
}
} // namespace swappy

View File

@ -0,0 +1,217 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/***************************************************************************************************
*
* Per-Device abstract base class.
*
***************************************************************************************************/
/**
* Abstract base class that calls the Vulkan API.
*
* It is expected that one concrete class will be instantiated per VkDevice, and
* that all VkSwapchainKHR's for a given VkDevice will share the same instance.
*
* Base class members are used by the derived classes to unify the behavior
* across implementations:
* @mThread - Thread used for getting Choreographer events.
* @mTreadRunning - Used to signal the tread to exit
* @mNextPresentID - unique ID for frame presentation.
* @mNextDesiredPresentTime - Holds the time in nanoseconds for the next frame
* to be presented.
* @mNextPresentIDToCheck - Used to determine whether presentation time needs
* to be adjusted.
* @mFrameID - Keeps track of how many Choreographer callbacks received.
* @mLastframeTimeNanos - Holds the last frame time reported by Choreographer.
* @mSumRefreshTime - Used together with @mSamples to calculate refresh rate
* based on Choreographer.
*/
#pragma once
#include <dlfcn.h>
#include <inttypes.h>
#include <pthread.h>
#include <swappy/swappyVk.h>
#include <unistd.h>
#include <condition_variable>
#include <cstdlib>
#include <cstring>
#include <list>
#include <map>
#include <mutex>
#include "ChoreographerShim.h"
#include "Settings.h"
#include "SwappyCommon.h"
#include "Trace.h"
namespace swappy {
#define ALOGE(...) \
__android_log_print(ANDROID_LOG_ERROR, "SwappyVk", __VA_ARGS__)
#define ALOGW(...) \
__android_log_print(ANDROID_LOG_WARN, "SwappyVk", __VA_ARGS__)
#define ALOGI(...) \
__android_log_print(ANDROID_LOG_INFO, "SwappyVk", __VA_ARGS__)
#define ALOGD(...) \
__android_log_print(ANDROID_LOG_DEBUG, "SwappyVk", __VA_ARGS__)
#define ALOGV(...) \
__android_log_print(ANDROID_LOG_VERBOSE, "SwappyVk", __VA_ARGS__)
constexpr uint32_t kThousand = 1000;
constexpr uint32_t kMillion = 1000000;
constexpr uint32_t kBillion = 1000000000;
constexpr uint32_t k16_6msec = 16666666;
constexpr uint32_t kTooCloseToVsyncBoundary = 3000000;
constexpr uint32_t kTooFarAwayFromVsyncBoundary = 7000000;
constexpr uint32_t kNudgeWithinVsyncBoundaries = 2000000;
// AChoreographer is supported from API 24. To allow compilation for minSDK < 24
// and still use AChoreographer for SDK >= 24 we need runtime support to call
// AChoreographer APIs.
using PFN_AChoreographer_getInstance = AChoreographer* (*)();
using PFN_AChoreographer_postFrameCallback =
void (*)(AChoreographer* choreographer,
AChoreographer_frameCallback callback, void* data);
using PFN_AChoreographer_postFrameCallbackDelayed = void (*)(
AChoreographer* choreographer, AChoreographer_frameCallback callback,
void* data, long delayMillis);
extern PFN_vkCreateCommandPool vkCreateCommandPool;
extern PFN_vkDestroyCommandPool vkDestroyCommandPool;
extern PFN_vkCreateFence vkCreateFence;
extern PFN_vkDestroyFence vkDestroyFence;
extern PFN_vkWaitForFences vkWaitForFences;
extern PFN_vkResetFences vkResetFences;
extern PFN_vkCreateSemaphore vkCreateSemaphore;
extern PFN_vkDestroySemaphore vkDestroySemaphore;
extern PFN_vkCreateEvent vkCreateEvent;
extern PFN_vkDestroyEvent vkDestroyEvent;
extern PFN_vkCmdSetEvent vkCmdSetEvent;
extern PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers;
extern PFN_vkFreeCommandBuffers vkFreeCommandBuffers;
extern PFN_vkBeginCommandBuffer vkBeginCommandBuffer;
extern PFN_vkEndCommandBuffer vkEndCommandBuffer;
extern PFN_vkQueueSubmit vkQueueSubmit;
void LoadVulkanFunctions(const SwappyVkFunctionProvider* pFunctionProvider);
class SwappyVkBase {
public:
SwappyVkBase(JNIEnv* env, jobject jactivity,
VkPhysicalDevice physicalDevice, VkDevice device,
const SwappyVkFunctionProvider* pFunctionProvider);
virtual ~SwappyVkBase();
virtual bool doGetRefreshCycleDuration(VkSwapchainKHR swapchain,
uint64_t* pRefreshDuration) = 0;
virtual VkResult doQueuePresent(VkQueue queue, uint32_t queueFamilyIndex,
const VkPresentInfoKHR* pPresentInfo) = 0;
void doSetWindow(ANativeWindow* window);
void doSetSwapInterval(VkSwapchainKHR swapchain, uint64_t swapNs);
VkResult injectFence(VkQueue queue, const VkPresentInfoKHR* pPresentInfo,
VkSemaphore* pSemaphore);
bool isEnabled() { return mEnabled; }
void setAutoSwapInterval(bool enabled);
void setAutoPipelineMode(bool enabled);
void setMaxAutoSwapDuration(std::chrono::nanoseconds swapMaxNS);
void setFenceTimeout(std::chrono::nanoseconds duration);
std::chrono::nanoseconds getFenceTimeout() const;
std::chrono::nanoseconds getSwapInterval();
void addTracer(const SwappyTracer* tracer);
void removeTracer(const SwappyTracer* tracer);
VkDevice getDevice() const { return mDevice; }
protected:
struct VkSync {
VkFence fence;
VkSemaphore semaphore;
VkCommandBuffer command;
VkEvent event;
};
struct ThreadContext {
ThreadContext(VkQueue queue) : queue(queue) {}
Thread thread;
bool running GUARDED_BY(lock) = true;
bool hasPendingWork GUARDED_BY(lock);
std::mutex lock;
std::condition_variable_any condition;
VkQueue queue GUARDED_BY(lock);
};
SwappyCommon mCommonBase;
VkPhysicalDevice mPhysicalDevice;
VkDevice mDevice;
const SwappyVkFunctionProvider* mpFunctionProvider;
bool mInitialized;
bool mEnabled;
uint32_t mNextPresentID = 0;
uint32_t mNextPresentIDToCheck = 2;
PFN_vkGetDeviceProcAddr mpfnGetDeviceProcAddr = nullptr;
PFN_vkQueuePresentKHR mpfnQueuePresentKHR = nullptr;
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
PFN_vkGetRefreshCycleDurationGOOGLE mpfnGetRefreshCycleDurationGOOGLE =
nullptr;
PFN_vkGetPastPresentationTimingGOOGLE mpfnGetPastPresentationTimingGOOGLE =
nullptr;
#endif
// Holds VKSync objects ready to be used
std::map<VkQueue, std::list<VkSync>> mFreeSyncPool;
// Holds VKSync objects queued and but signaled yet
std::map<VkQueue, std::list<VkSync>> mWaitingSyncs;
// Holds VKSync objects that were signaled
std::map<VkQueue, std::list<VkSync>> mSignaledSyncs;
std::map<VkQueue, VkCommandPool> mCommandPool;
std::map<VkQueue, std::unique_ptr<ThreadContext>> mThreads;
static constexpr int MAX_PENDING_FENCES = 2;
std::atomic<std::chrono::nanoseconds> mLastFenceTime = {};
void initGoogExtension();
VkResult initializeVkSyncObjects(VkQueue queue, uint32_t queueFamilyIndex);
void destroyVkSyncObjects();
void reclaimSignaledFences(VkQueue queue);
bool lastFrameIsCompleted(VkQueue queue);
std::chrono::nanoseconds getLastFenceTime(VkQueue queue);
void waitForFenceThreadMain(ThreadContext& thread);
};
} // namespace swappy

View File

@ -0,0 +1,102 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SwappyVkFallback.h"
#define LOG_TAG "SwappyVkFallback"
namespace swappy {
SwappyVkFallback::SwappyVkFallback(JNIEnv* env, jobject jactivity,
VkPhysicalDevice physicalDevice,
VkDevice device,
const SwappyVkFunctionProvider* provider)
: SwappyVkBase(env, jactivity, physicalDevice, device, provider) {}
bool SwappyVkFallback::doGetRefreshCycleDuration(VkSwapchainKHR swapchain,
uint64_t* pRefreshDuration) {
if (!isEnabled()) {
ALOGE("Swappy is disabled.");
return false;
}
// Since we don't have presentation timing, we cannot achieve pipelining.
mCommonBase.setAutoPipelineMode(false);
*pRefreshDuration = mCommonBase.getRefreshPeriod().count();
double refreshRate = 1000000000.0 / *pRefreshDuration;
ALOGI("Returning refresh duration of %" PRIu64 " nsec (approx %f Hz)",
*pRefreshDuration, refreshRate);
return true;
}
VkResult SwappyVkFallback::doQueuePresent(
VkQueue queue, uint32_t queueFamilyIndex,
const VkPresentInfoKHR* pPresentInfo) {
if (!isEnabled()) {
ALOGE("Swappy is disabled.");
return VK_ERROR_INITIALIZATION_FAILED;
}
VkResult result = initializeVkSyncObjects(queue, queueFamilyIndex);
if (result) {
return result;
}
const SwappyCommon::SwapHandlers handlers = {
.lastFrameIsComplete =
std::bind(&SwappyVkFallback::lastFrameIsCompleted, this, queue),
.getPrevFrameGpuTime =
std::bind(&SwappyVkFallback::getLastFenceTime, this, queue),
};
// Inject the fence first and wait for it in onPreSwap() as we don't want to
// submit a frame before rendering is completed.
VkSemaphore semaphore;
result = injectFence(queue, pPresentInfo, &semaphore);
if (result) {
ALOGE("Failed to vkQueueSubmit %d", result);
return result;
}
uint32_t waitSemaphoreCount;
const VkSemaphore* pWaitSemaphores;
if (semaphore != VK_NULL_HANDLE) {
waitSemaphoreCount = 1;
pWaitSemaphores = &semaphore;
} else {
waitSemaphoreCount = pPresentInfo->waitSemaphoreCount;
pWaitSemaphores = pPresentInfo->pWaitSemaphores;
}
mCommonBase.onPreSwap(handlers);
VkPresentInfoKHR replacementPresentInfo = {
pPresentInfo->sType, nullptr,
waitSemaphoreCount, pWaitSemaphores,
pPresentInfo->swapchainCount, pPresentInfo->pSwapchains,
pPresentInfo->pImageIndices, pPresentInfo->pResults};
result = mpfnQueuePresentKHR(queue, &replacementPresentInfo);
mCommonBase.onPostSwap(handlers);
return result;
}
} // namespace swappy

View File

@ -0,0 +1,44 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/***************************************************************************************************
*
* Per-Device concrete/derived class for the "Android fallback" path (uses
* Choreographer to try to get presents to occur at the desired time).
*
***************************************************************************************************/
#pragma once
#include "SwappyVkBase.h"
namespace swappy {
class SwappyVkFallback : public SwappyVkBase {
public:
SwappyVkFallback(JNIEnv* env, jobject jactivity,
VkPhysicalDevice physicalDevice, VkDevice device,
const SwappyVkFunctionProvider* provider);
virtual bool doGetRefreshCycleDuration(VkSwapchainKHR swapchain,
uint64_t* pRefreshDuration) override;
virtual VkResult doQueuePresent(
VkQueue queue, uint32_t queueFamilyIndex,
const VkPresentInfoKHR* pPresentInfo) override;
};
} // namespace swappy

View File

@ -0,0 +1,133 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
#include "SwappyVkGoogleDisplayTiming.h"
#define LOG_TAG "SwappyVkGoogleDisplayTiming"
using std::chrono::nanoseconds;
namespace swappy {
SwappyVkGoogleDisplayTiming::SwappyVkGoogleDisplayTiming(
JNIEnv* env, jobject jactivity, VkPhysicalDevice physicalDevice,
VkDevice device, const SwappyVkFunctionProvider* provider)
: SwappyVkBase(env, jactivity, physicalDevice, device, provider) {}
bool SwappyVkGoogleDisplayTiming::doGetRefreshCycleDuration(
VkSwapchainKHR swapchain, uint64_t* pRefreshDuration) {
if (!isEnabled()) {
ALOGE("Swappy is disabled.");
return false;
}
VkRefreshCycleDurationGOOGLE refreshCycleDuration;
VkResult res = mpfnGetRefreshCycleDurationGOOGLE(mDevice, swapchain,
&refreshCycleDuration);
if (res != VK_SUCCESS) {
ALOGE("mpfnGetRefreshCycleDurationGOOGLE failed %d", res);
return false;
}
*pRefreshDuration = mCommonBase.getRefreshPeriod().count();
double refreshRate = 1000000000.0 / *pRefreshDuration;
ALOGI("Returning refresh duration of %" PRIu64 " nsec (approx %f Hz)",
*pRefreshDuration, refreshRate);
return true;
}
VkResult SwappyVkGoogleDisplayTiming::doQueuePresent(
VkQueue queue, uint32_t queueFamilyIndex,
const VkPresentInfoKHR* pPresentInfo) {
if (!isEnabled()) {
ALOGE("Swappy is disabled.");
return VK_ERROR_INITIALIZATION_FAILED;
}
VkResult res = initializeVkSyncObjects(queue, queueFamilyIndex);
if (res) {
return res;
}
const SwappyCommon::SwapHandlers handlers = {
.lastFrameIsComplete = std::bind(
&SwappyVkGoogleDisplayTiming::lastFrameIsCompleted, this, queue),
.getPrevFrameGpuTime = std::bind(
&SwappyVkGoogleDisplayTiming::getLastFenceTime, this, queue),
};
VkSemaphore semaphore;
res = injectFence(queue, pPresentInfo, &semaphore);
if (res) {
ALOGE("Failed to vkQueueSubmit %d", res);
return res;
}
uint32_t waitSemaphoreCount;
const VkSemaphore* pWaitSemaphores;
if (semaphore != VK_NULL_HANDLE) {
waitSemaphoreCount = 1;
pWaitSemaphores = &semaphore;
} else {
waitSemaphoreCount = pPresentInfo->waitSemaphoreCount;
pWaitSemaphores = pPresentInfo->pWaitSemaphores;
}
mCommonBase.onPreSwap(handlers);
VkPresentTimeGOOGLE pPresentTimes[pPresentInfo->swapchainCount];
VkPresentInfoKHR replacementPresentInfo;
VkPresentTimesInfoGOOGLE presentTimesInfo;
if (mCommonBase.needToSetPresentationTime()) {
// Setup the new structures to pass:
for (uint32_t i = 0; i < pPresentInfo->swapchainCount; i++) {
pPresentTimes[i].presentID = mNextPresentID;
pPresentTimes[i].desiredPresentTime =
mCommonBase.getPresentationTime().time_since_epoch().count();
}
presentTimesInfo = {VK_STRUCTURE_TYPE_PRESENT_TIMES_INFO_GOOGLE,
pPresentInfo->pNext, pPresentInfo->swapchainCount,
pPresentTimes};
replacementPresentInfo = {
pPresentInfo->sType, &presentTimesInfo,
waitSemaphoreCount, pWaitSemaphores,
pPresentInfo->swapchainCount, pPresentInfo->pSwapchains,
pPresentInfo->pImageIndices, pPresentInfo->pResults};
} else {
replacementPresentInfo = {
pPresentInfo->sType, nullptr,
waitSemaphoreCount, pWaitSemaphores,
pPresentInfo->swapchainCount, pPresentInfo->pSwapchains,
pPresentInfo->pImageIndices, pPresentInfo->pResults};
}
mNextPresentID++;
res = mpfnQueuePresentKHR(queue, &replacementPresentInfo);
mCommonBase.onPostSwap(handlers);
return res;
}
} // namespace swappy
#endif // #if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION>=15

View File

@ -0,0 +1,81 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "SwappyVkBase.h"
namespace swappy {
/***************************************************************************************************
*
* Per-Device concrete/derived class for using VK_GOOGLE_display_timing.
*
* This class uses the VK_GOOGLE_display_timing in order to present frames at a
*muliple (the "swap interval") of a fixed refresh-cycle duration (i.e. the time
*between successive vsync's).
*
* In order to reduce complexity, some simplifying assumptions are made:
*
* - We assume a fixed refresh-rate (FRR) display that's between 60 Hz and 120
*Hz.
*
* - While Vulkan allows applications to create and use multiple
*VkSwapchainKHR's per VkDevice, and to re-create VkSwapchainKHR's, we assume
*that the application uses a single VkSwapchainKHR, and never re-creates it.
*
* - The values reported back by the VK_GOOGLE_display_timing extension (which
*comes from lower-level Android interfaces) are not precise, and that values
*can drift over time. For example, the refresh-cycle duration for a 60 Hz
*display should be 16,666,666 nsec; but the value reported back by the
*extension won't be precisely this. Also, the differences betweeen the times
*of two successive frames won't be an exact multiple of 16,666,666 nsec. This
*can make it difficult to precisely predict when a future vsync will be (it can
*appear to drift overtime). Therefore, we try to give a desiredPresentTime for
*each image that is between 3 and 7 msec before vsync. We look at the
*actualPresentTime for previously-presented images, and nudge the future
*desiredPresentTime back within those 3-7 msec boundaries.
*
* - There can be a few frames of latency between when an image is presented and
*when the actualPresentTime is available for that image. Therefore, we
*initially just pick times based upon CLOCK_MONOTONIC (which is the time domain
*for VK_GOOGLE_display_timing). After we get past-present times, we nudge the
*desiredPresentTime, we wait for a few presents before looking again to see
*whether we need to nudge again.
*
* - If, for some reason, an application can't keep up with its chosen swap
*interval (e.g. it's designed for 30FPS on a premium device and is now running
*on a slow device; or it's running on a 120Hz display), this algorithm may not
*be able to make up for this (i.e. smooth rendering at a targeted frame rate
*may not be possible with an application that can't render fast enough).
*
***************************************************************************************************/
class SwappyVkGoogleDisplayTiming : public SwappyVkBase {
public:
SwappyVkGoogleDisplayTiming(JNIEnv* env, jobject jactivity,
VkPhysicalDevice physicalDevice,
VkDevice device,
const SwappyVkFunctionProvider* provider);
virtual bool doGetRefreshCycleDuration(VkSwapchainKHR swapchain,
uint64_t* pRefreshDuration) override;
virtual VkResult doQueuePresent(
VkQueue queue, uint32_t queueFamilyIndex,
const VkPresentInfoKHR* pPresentInfo) override;
};
} // namespace swappy

View File

@ -0,0 +1,145 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// API entry points
#include "SwappyVk.h"
#include "swappy/swappyVk.h"
extern "C" {
// Internal function to track Swappy version bundled in a binary.
void SWAPPY_VERSION_SYMBOL();
void SwappyVk_determineDeviceExtensions(
VkPhysicalDevice physicalDevice, uint32_t availableExtensionCount,
VkExtensionProperties* pAvailableExtensions,
uint32_t* pRequiredExtensionCount, char** pRequiredExtensions) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.swappyVkDetermineDeviceExtensions(
physicalDevice, availableExtensionCount, pAvailableExtensions,
pRequiredExtensionCount, pRequiredExtensions);
}
void SwappyVk_setQueueFamilyIndex(VkDevice device, VkQueue queue,
uint32_t queueFamilyIndex) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.SetQueueFamilyIndex(device, queue, queueFamilyIndex);
}
bool SwappyVk_initAndGetRefreshCycleDuration(JNIEnv* env, jobject jactivity,
VkPhysicalDevice physicalDevice,
VkDevice device,
VkSwapchainKHR swapchain,
uint64_t* pRefreshDuration) {
SWAPPY_VERSION_SYMBOL();
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
return swappy.GetRefreshCycleDuration(env, jactivity, physicalDevice,
device, swapchain, pRefreshDuration);
}
void SwappyVk_setWindow(VkDevice device, VkSwapchainKHR swapchain,
ANativeWindow* window) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.SetWindow(device, swapchain, window);
}
void SwappyVk_setSwapIntervalNS(VkDevice device, VkSwapchainKHR swapchain,
uint64_t swap_ns) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.SetSwapDuration(device, swapchain, swap_ns);
}
VkResult SwappyVk_queuePresent(VkQueue queue,
const VkPresentInfoKHR* pPresentInfo) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
return swappy.QueuePresent(queue, pPresentInfo);
}
void SwappyVk_destroySwapchain(VkDevice device, VkSwapchainKHR swapchain) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.DestroySwapchain(device, swapchain);
}
void SwappyVk_destroyDevice(VkDevice device) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.DestroyDevice(device);
}
void SwappyVk_setAutoSwapInterval(bool enabled) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.SetAutoSwapInterval(enabled);
}
void SwappyVk_setAutoPipelineMode(bool enabled) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.SetAutoPipelineMode(enabled);
}
void SwappyVk_setFenceTimeoutNS(uint64_t fence_timeout_ns) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.SetFenceTimeout(std::chrono::nanoseconds(fence_timeout_ns));
}
void SwappyVk_setMaxAutoSwapIntervalNS(uint64_t max_swap_ns) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.SetMaxAutoSwapDuration(std::chrono::nanoseconds(max_swap_ns));
}
uint64_t SwappyVk_getFenceTimeoutNS() {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
return swappy.GetFenceTimeout().count();
}
void SwappyVk_injectTracer(const SwappyTracer* t) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.addTracer(t);
}
void SwappyVk_uninjectTracer(const SwappyTracer* t) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.removeTracer(t);
}
void SwappyVk_setFunctionProvider(
const SwappyVkFunctionProvider* pSwappyVkFunctionProvider) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
swappy.SetFunctionProvider(pSwappyVkFunctionProvider);
}
uint64_t SwappyVk_getSwapIntervalNS(VkSwapchainKHR swapchain) {
TRACE_CALL();
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
return swappy.GetSwapInterval(swapchain).count();
}
} // extern "C"