diff --git a/sources/CMakeLists.txt b/sources/CMakeLists.txt index b8a2021d..3ce8042f 100644 --- a/sources/CMakeLists.txt +++ b/sources/CMakeLists.txt @@ -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) diff --git a/sources/Swappy/include/common/gamesdk_common.h b/sources/Swappy/include/common/gamesdk_common.h new file mode 100644 index 00000000..8512262c --- /dev/null +++ b/sources/Swappy/include/common/gamesdk_common.h @@ -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 diff --git a/sources/Swappy/include/swappy/OWNERS b/sources/Swappy/include/swappy/OWNERS new file mode 100644 index 00000000..d79d8611 --- /dev/null +++ b/sources/Swappy/include/swappy/OWNERS @@ -0,0 +1 @@ +include ../../src/swappy/OWNERS diff --git a/sources/Swappy/include/swappy/swappyGL.h b/sources/Swappy/include/swappy/swappyGL.h new file mode 100644 index 00000000..4ad9a4c0 --- /dev/null +++ b/sources/Swappy/include/swappy/swappyGL.h @@ -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 +#include +#include +#include + +#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 + +/** @} */ diff --git a/sources/Swappy/include/swappy/swappyGL_extra.h b/sources/Swappy/include/swappy/swappyGL_extra.h new file mode 100644 index 00000000..155268fc --- /dev/null +++ b/sources/Swappy/include/swappy/swappyGL_extra.h @@ -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 +#include +#include +#include + +#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 + +/** @} */ diff --git a/sources/Swappy/include/swappy/swappyVk.h b/sources/Swappy/include/swappy/swappyVk.h new file mode 100644 index 00000000..1ec50c80 --- /dev/null +++ b/sources/Swappy/include/swappy/swappyVk.h @@ -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 + +#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 swapchain’s 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 + +/** @} */ diff --git a/sources/Swappy/include/swappy/swappy_common.h b/sources/Swappy/include/swappy/swappy_common.h new file mode 100644 index 00000000..ab7a1333 --- /dev/null +++ b/sources/Swappy/include/swappy/swappy_common.h @@ -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 +#include + +#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; + +/** @} */ diff --git a/sources/Swappy/src/common/ChoreographerShim.h b/sources/Swappy/src/common/ChoreographerShim.h new file mode 100644 index 00000000..981ecab4 --- /dev/null +++ b/sources/Swappy/src/common/ChoreographerShim.h @@ -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 + +// 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 \ No newline at end of file diff --git a/sources/Swappy/src/common/JNIUtil.h b/sources/Swappy/src/common/JNIUtil.h new file mode 100644 index 00000000..18d98559 --- /dev/null +++ b/sources/Swappy/src/common/JNIUtil.h @@ -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 + +#include +#include +#include +#include + +#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( + 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(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(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, "", + "(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(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, "", + "(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( + 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 diff --git a/sources/Swappy/src/common/Log.h b/sources/Swappy/src/common/Log.h new file mode 100644 index 00000000..cb93297f --- /dev/null +++ b/sources/Swappy/src/common/Log.h @@ -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 + +#include + +#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); + +} diff --git a/sources/Swappy/src/common/StringShim.h b/sources/Swappy/src/common/StringShim.h new file mode 100644 index 00000000..d32e61cd --- /dev/null +++ b/sources/Swappy/src/common/StringShim.h @@ -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 + +// 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 +std::string to_string(T value) { + std::stringstream os; + os << value; + return os.str(); +} +template +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 diff --git a/sources/Swappy/src/common/Trace.h b/sources/Swappy/src/common/Trace.h new file mode 100644 index 00000000..9929b1ad --- /dev/null +++ b/sources/Swappy/src/common/Trace.h @@ -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 +#include +#include + +#include + +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 create() { + void *libandroid = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL); + if (!libandroid) { + return std::make_unique(); + } + + auto beginSection = reinterpret_cast( + dlsym(libandroid, "ATrace_beginSection")); + if (!beginSection) { + return std::make_unique(); + } + + auto endSection = reinterpret_cast( + dlsym(libandroid, "ATrace_endSection")); + if (!endSection) { + return std::make_unique(); + } + + auto isEnabled = reinterpret_cast( + dlsym(libandroid, "ATrace_isEnabled")); + if (!isEnabled) { + return std::make_unique(); + } + + auto setCounter = reinterpret_cast( + dlsym(libandroid, "ATrace_setCounter")); + /* ATrace_setCounter was added in API 29, continue even if it is not + * available */ + + return std::make_unique(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::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() diff --git a/sources/Swappy/src/common/apk_utils.cpp b/sources/Swappy/src/common/apk_utils.cpp new file mode 100644 index 00000000..b54a25b3 --- /dev/null +++ b/sources/Swappy/src/common/apk_utils.cpp @@ -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 +#include + +#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 \ No newline at end of file diff --git a/sources/Swappy/src/common/apk_utils.h b/sources/Swappy/src/common/apk_utils.h new file mode 100644 index 00000000..44184f40 --- /dev/null +++ b/sources/Swappy/src/common/apk_utils.h @@ -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 + +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 \ No newline at end of file diff --git a/sources/Swappy/src/common/jni/jni_helper.cpp b/sources/Swappy/src/common/jni/jni_helper.cpp new file mode 100644 index 00000000..f3e8b0ae --- /dev/null +++ b/sources/Swappy/src/common/jni/jni_helper.cpp @@ -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, "", 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 GetByteArrayBytesAndDeleteRef(jbyteArray jbs) { + jbyte* bs = Env()->GetByteArrayElements(jbs, 0); + std::vector 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 diff --git a/sources/Swappy/src/common/jni/jni_helper.h b/sources/Swappy/src/common/jni/jni_helper.h new file mode 100644 index 00000000..859f87a9 --- /dev/null +++ b/sources/Swappy/src/common/jni/jni_helper.h @@ -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 + +#include +#include + +#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(Env()->NewLocalRef(j_str_)); + } + } + String& operator=(const String& rhs) { + if (this != &rhs) { + Release(); + if (rhs.j_str_ != nullptr) { + j_str_ = + reinterpret_cast(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(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 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 diff --git a/sources/Swappy/src/common/jni/jni_wrap.cpp b/sources/Swappy/src/common/jni/jni_wrap.cpp new file mode 100644 index 00000000..b1f56f2a --- /dev/null +++ b/sources/Swappy/src/common/jni/jni_wrap.cpp @@ -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 diff --git a/sources/Swappy/src/common/jni/jni_wrap.h b/sources/Swappy/src/common/jni/jni_wrap.h new file mode 100644 index 00000000..95e548d5 --- /dev/null +++ b/sources/Swappy/src/common/jni/jni_wrap.h @@ -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 + +#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 digest( + const std::vector& bs) const { + auto env = Env(); + jbyteArray jbs = env->NewByteArray(bs.size()); + env->SetByteArrayRegion(jbs, 0, bs.size(), + reinterpret_cast(bs.data())); + jbyteArray out = reinterpret_cast( + 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 Signature; + std::vector 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> ret; + for (int i = 0; i < n; ++i) { + Object sig(env->GetObjectArrayElement(sigs, i)); + jbyteArray bytes = reinterpret_cast( + 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 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 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 diff --git a/sources/Swappy/src/common/jni/jnictx.cpp b/sources/Swappy/src/common/jni/jnictx.cpp new file mode 100644 index 00000000..a3871f4c --- /dev/null +++ b/sources/Swappy/src/common/jni/jnictx.cpp @@ -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 + +#include "Log.h" +#define LOG_TAG "JniCtx" + +namespace gamesdk { + +namespace jni { + +static std::unique_ptr theCtx; +static thread_local JNIEnv* theEnv = nullptr; + +/*static*/ const Ctx* Ctx::Init(JNIEnv* env, jobject ctx) { + theCtx = std::unique_ptr(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 diff --git a/sources/Swappy/src/common/jni/jnictx.h b/sources/Swappy/src/common/jni/jnictx.h new file mode 100644 index 00000000..d8297904 --- /dev/null +++ b/sources/Swappy/src/common/jni/jnictx.h @@ -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 + +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 diff --git a/sources/Swappy/src/common/system_utils.cpp b/sources/Swappy/src/common/system_utils.cpp new file mode 100644 index 00000000..efc5b743 --- /dev/null +++ b/sources/Swappy/src/common/system_utils.cpp @@ -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 +#include +#include + +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(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 \ No newline at end of file diff --git a/sources/Swappy/src/common/system_utils.h b/sources/Swappy/src/common/system_utils.h new file mode 100644 index 00000000..488374bc --- /dev/null +++ b/sources/Swappy/src/common/system_utils.h @@ -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 \ No newline at end of file diff --git a/sources/Swappy/src/swappy/CMakeLists.txt b/sources/Swappy/src/swappy/CMakeLists.txt new file mode 100644 index 00000000..ae6d9f2b --- /dev/null +++ b/sources/Swappy/src/swappy/CMakeLists.txt @@ -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) diff --git a/sources/Swappy/src/swappy/OWNERS b/sources/Swappy/src/swappy/OWNERS new file mode 100644 index 00000000..a4adbc51 --- /dev/null +++ b/sources/Swappy/src/swappy/OWNERS @@ -0,0 +1,4 @@ +stoza@google.com +adyabr@google.com +ianelliott@google.com + diff --git a/sources/Swappy/src/swappy/common/CPUTracer.cpp b/sources/Swappy/src/swappy/common/CPUTracer.cpp new file mode 100644 index 00000000..f91de8bd --- /dev/null +++ b/sources/Swappy/src/swappy/common/CPUTracer.cpp @@ -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 + +#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 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 lock(mMutex); + if (!mThread) { + mRunning = true; + mThread = std::make_unique([this]() { threadMain(); }); + } + mTrace = true; + mCond.notify_one(); + } else { + joinThread(); + } +} + +void CPUTracer::endTrace() { + if (TRACE_ENABLED()) { + std::lock_guard lock(mMutex); + mTrace = false; + mCond.notify_one(); + } else { + joinThread(); + } +} + +void CPUTracer::threadMain() NO_THREAD_SAFETY_ANALYSIS { + std::unique_lock lock(mMutex); + while (mRunning) { + if (mTrace) { + gamesdk::ScopedTrace trace("Swappy: CPU frame time"); + mCond.wait(lock); + } else { + mCond.wait(lock); + } + } +} + +} // namespace swappy diff --git a/sources/Swappy/src/swappy/common/CPUTracer.h b/sources/Swappy/src/swappy/common/CPUTracer.h new file mode 100644 index 00000000..8bb89015 --- /dev/null +++ b/sources/Swappy/src/swappy/common/CPUTracer.h @@ -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 +#include +#include + +#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 mThread; + bool mRunning GUARDED_BY(mMutex) = true; + bool mTrace GUARDED_BY(mMutex) = false; +}; + +} // namespace swappy diff --git a/sources/Swappy/src/swappy/common/ChoreographerFilter.cpp b/sources/Swappy/src/swappy/common/ChoreographerFilter.cpp new file mode 100644 index 00000000..371eed7b --- /dev/null +++ b/sources/Swappy/src/swappy/common/ChoreographerFilter.cpp @@ -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 +#include + +#include +#include +#include + +#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 lock(mThreadPoolMutex); + mUseAffinity = Settings::getInstance()->getUseAffinity(); + launchThreadsLocked(); +} + +ChoreographerFilter::~ChoreographerFilter() { + std::lock_guard lock(mThreadPoolMutex); + terminateThreadsLocked(); +} + +void ChoreographerFilter::onChoreographer() { + std::lock_guard lock(mMutex); + mLastTimestamp = std::chrono::steady_clock::now(); + ++mSequenceNumber; + mCondition.notify_all(); +} + +void ChoreographerFilter::launchThreadsLocked() { + { + std::lock_guard 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 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 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 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 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 diff --git a/sources/Swappy/src/swappy/common/ChoreographerFilter.h b/sources/Swappy/src/swappy/common/ChoreographerFilter.h new file mode 100644 index 00000000..9c3163d6 --- /dev/null +++ b/sources/Swappy/src/swappy/common/ChoreographerFilter.h @@ -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 +#include +#include + +#include "Settings.h" +#include "Thread.h" + +namespace swappy { + +class ChoreographerFilter { + public: + using Worker = std::function; + + 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 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 diff --git a/sources/Swappy/src/swappy/common/ChoreographerThread.cpp b/sources/Swappy/src/swappy/common/ChoreographerThread.cpp new file mode 100644 index 00000000..ef5fbad2 --- /dev/null +++ b/sources/Swappy/src/swappy/common/ChoreographerThread.cpp @@ -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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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( + dlsym(mLibAndroid, "AChoreographer_getInstance")); + + mAChoreographer_postFrameCallback = + reinterpret_cast( + dlsym(mLibAndroid, "AChoreographer_postFrameCallback")); + + mAChoreographer_postFrameCallbackDelayed = + reinterpret_cast( + dlsym(mLibAndroid, "AChoreographer_postFrameCallbackDelayed")); + + mAChoreographer_registerRefreshRateCallback = + reinterpret_cast( + dlsym(mLibAndroid, "AChoreographer_registerRefreshRateCallback")); + + mAChoreographer_unregisterRefreshRateCallback = + reinterpret_cast( + dlsym(mLibAndroid, "AChoreographer_unregisterRefreshRateCallback")); + + if (!mAChoreographer_getInstance || !mAChoreographer_postFrameCallback || + !mAChoreographer_postFrameCallbackDelayed) { + ALOGE("FATAL: cannot get AChoreographer symbols"); + return; + } + + std::unique_lock 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 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 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(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(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, "", "(J)V"); + + mJpostFrameCallback = env->GetMethodID(choreographerCallbackClass, + "postFrameCallback", "()V"); + + mJterminate = + env->GetMethodID(choreographerCallbackClass, "terminate", "()V"); + + jobject choreographerCallback = env->NewObject( + choreographerCallbackClass, constructor, reinterpret_cast(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(&env), JNI_VERSION_1_2); + if (result != JNI_OK) { + if (result == JNI_EVERSION) { + result = + mJVM->GetEnv(reinterpret_cast(&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(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 lock(mWaitingMutex); + Settings::getInstance()->addListener([this]() { onSettingsChanged(); }); + mThreadRunning = true; + mThread = Thread([this]() { looperThread(); }); + mInitialized = true; +} + +NoChoreographerThread::~NoChoreographerThread() { + ALOGI("Destroying NoChoreographerThread"); + { + std::lock_guard lock(mWaitingMutex); + mThreadRunning = false; + } + mWaitingCondition.notify_all(); + mThread.join(); +} + +void NoChoreographerThread::onSettingsChanged() { + const Settings::DisplayTimings &displayTimings = + Settings::getInstance()->getDisplayTimings(); + std::lock_guard 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 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 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 lock(mWaitingMutex); + if (mCallbacksBeforeIdle == 0) { + scheduleNextFrameCallback(); + } + mCallbacksBeforeIdle = MAX_CALLBACKS_BEFORE_IDLE; +} + +void ChoreographerThread::onChoreographer() { + TRACE_CALL(); + + { + std::lock_guard lock(mWaitingMutex); + mCallbacksBeforeIdle--; + + if (mCallbacksBeforeIdle > 0) { + scheduleNextFrameCallback(); + } + } + mCallback(); +} + +std::unique_ptr +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(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(onChoreographer, + refreshRateCallback); + } + + if (vm != nullptr && jactivity != nullptr) { + std::unique_ptr javaChoreographerThread = + std::make_unique(vm, jactivity, + onChoreographer); + if (javaChoreographerThread->isInitialized()) { + ALOGI("Using Java Choreographer"); + return javaChoreographerThread; + } + } + + ALOGI("Using no Choreographer (Best Effort)"); + return std::make_unique(onChoreographer); +} + +} // namespace swappy diff --git a/sources/Swappy/src/swappy/common/ChoreographerThread.h b/sources/Swappy/src/swappy/common/ChoreographerThread.h new file mode 100644 index 00000000..a823e33d --- /dev/null +++ b/sources/Swappy/src/swappy/common/ChoreographerThread.h @@ -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 + +#include + +#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; + + static std::unique_ptr 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 diff --git a/sources/Swappy/src/swappy/common/CpuInfo.cpp b/sources/Swappy/src/swappy/common/CpuInfo.cpp new file mode 100644 index 00000000..2345644c --- /dev/null +++ b/sources/Swappy/src/swappy/common/CpuInfo.cpp @@ -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 +#include +#include +#include + +#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 split(const std::string &s, char c) { + std::vector 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::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::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 diff --git a/sources/Swappy/src/swappy/common/CpuInfo.h b/sources/Swappy/src/swappy/common/CpuInfo.h new file mode 100644 index 00000000..2f463cc6 --- /dev/null +++ b/sources/Swappy/src/swappy/common/CpuInfo.h @@ -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 + +#include +#include +#include + +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& 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 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 diff --git a/sources/Swappy/src/swappy/common/FrameStatistics.h b/sources/Swappy/src/swappy/common/FrameStatistics.h new file mode 100644 index 00000000..10e5ef8a --- /dev/null +++ b/sources/Swappy/src/swappy/common/FrameStatistics.h @@ -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 + +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 diff --git a/sources/Swappy/src/swappy/common/Settings.cpp b/sources/Swappy/src/swappy/common/Settings.cpp new file mode 100644 index 00000000..ba0d9420 --- /dev/null +++ b/sources/Swappy/src/swappy/common/Settings.cpp @@ -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::instance; + +Settings* Settings::getInstance() { + if (!instance) { + instance = std::make_unique(ConstructorTag{}); + } + return instance.get(); +} + +void Settings::reset() { instance.reset(); } + +void Settings::addListener(Listener listener) { + std::lock_guard lock(mMutex); + mListeners.emplace_back(std::move(listener)); +} + +void Settings::setDisplayTimings(const DisplayTimings& displayTimings) { + { + std::lock_guard lock(mMutex); + mDisplayTimings = displayTimings; + } + // Notify the listeners without the lock held + notifyListeners(); +} +void Settings::setSwapDuration(uint64_t swapNs) { + { + std::lock_guard lock(mMutex); + mSwapDuration = std::chrono::nanoseconds(swapNs); + } + // Notify the listeners without the lock held + notifyListeners(); +} + +void Settings::setUseAffinity(bool tf) { + { + std::lock_guard lock(mMutex); + mUseAffinity = tf; + } + // Notify the listeners without the lock held + notifyListeners(); +} + +const Settings::DisplayTimings& Settings::getDisplayTimings() const { + std::lock_guard lock(mMutex); + return mDisplayTimings; +} + +std::chrono::nanoseconds Settings::getSwapDuration() const { + std::lock_guard lock(mMutex); + return mSwapDuration; +} + +bool Settings::getUseAffinity() const { + std::lock_guard lock(mMutex); + return mUseAffinity; +} + +void Settings::notifyListeners() { + // Grab a local copy of the listeners + std::vector listeners; + { + std::lock_guard lock(mMutex); + listeners = mListeners; + } + + // Call the listeners without the lock held + for (const auto& listener : listeners) { + listener(); + } +} + +} // namespace swappy diff --git a/sources/Swappy/src/swappy/common/Settings.h b/sources/Swappy/src/swappy/common/Settings.h new file mode 100644 index 00000000..579c8cbe --- /dev/null +++ b/sources/Swappy/src/swappy/common/Settings.h @@ -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 +#include +#include +#include +#include +#include + +#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 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 instance; + + mutable std::mutex mMutex; + std::vector 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 diff --git a/sources/Swappy/src/swappy/common/SwappyCommon.cpp b/sources/Swappy/src/swappy/common/SwappyCommon.cpp new file mode 100644 index 00000000..e9ae0612 --- /dev/null +++ b/sources/Swappy/src/swappy/common/SwappyCommon.cpp @@ -0,0 +1,1034 @@ +/* + * 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 "SwappyCommon.h" + +#include +#include +#include + +#include "Log.h" +#include "Settings.h" +#include "Thread.h" +#include "Trace.h" + +#define LOG_TAG "SwappyCommon" + +namespace swappy { + +using std::chrono::milliseconds; +using std::chrono::nanoseconds; + +// NB These are only needed for C++14 +constexpr nanoseconds SwappyCommon::FrameDuration::MAX_DURATION; +constexpr nanoseconds SwappyCommon::FRAME_MARGIN; +constexpr nanoseconds SwappyCommon::DURATION_ROUNDING_MARGIN; +constexpr nanoseconds SwappyCommon::REFRESH_RATE_MARGIN; +constexpr int SwappyCommon::NON_PIPELINE_PERCENT; +constexpr int SwappyCommon::FRAME_DROP_THRESHOLD; +constexpr std::chrono::nanoseconds + SwappyCommon::FrameDurations::FRAME_DURATION_SAMPLE_SECONDS; + +#if __ANDROID_API__ < 30 +// Define ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_* to allow compilation on older +// versions +enum { + /** + * There are no inherent restrictions on the frame rate of this window. + */ + ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT = 0, + /** + * This window is being used to display content with an inherently fixed + * frame rate, e.g. a video that has a specific frame rate. When the system + * selects a frame rate other than what the app requested, the app will need + * to do pull down or use some other technique to adapt to the system's + * frame rate. The user experience is likely to be worse (e.g. more frame + * stuttering) than it would be if the system had chosen the app's requested + * frame rate. + */ + ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE = 1 +}; +#endif + +bool SwappyCommonSettings::getFromApp(JNIEnv* env, jobject jactivity, + SwappyCommonSettings* out) { + if (out == nullptr) return false; + + ALOGI("Swappy version %d.%d", SWAPPY_MAJOR_VERSION, SWAPPY_MINOR_VERSION); + + out->sdkVersion = getSDKVersion(env); + + jclass activityClass = env->FindClass("android/app/NativeActivity"); + jclass windowManagerClass = env->FindClass("android/view/WindowManager"); + jclass displayClass = env->FindClass("android/view/Display"); + + jmethodID getWindowManager = env->GetMethodID( + activityClass, "getWindowManager", "()Landroid/view/WindowManager;"); + + jmethodID getDefaultDisplay = env->GetMethodID( + windowManagerClass, "getDefaultDisplay", "()Landroid/view/Display;"); + + jobject wm = env->CallObjectMethod(jactivity, getWindowManager); + jobject display = env->CallObjectMethod(wm, getDefaultDisplay); + + jmethodID getRefreshRate = + env->GetMethodID(displayClass, "getRefreshRate", "()F"); + + const float refreshRateHz = env->CallFloatMethod(display, getRefreshRate); + + jmethodID getAppVsyncOffsetNanos = + env->GetMethodID(displayClass, "getAppVsyncOffsetNanos", "()J"); + + // getAppVsyncOffsetNanos was only added in API 21. + // Return gracefully if this device doesn't support it. + if (getAppVsyncOffsetNanos == 0 || env->ExceptionOccurred()) { + ALOGE("Error while getting method: getAppVsyncOffsetNanos"); + env->ExceptionClear(); + return false; + } + const long appVsyncOffsetNanos = + env->CallLongMethod(display, getAppVsyncOffsetNanos); + + jmethodID getPresentationDeadlineNanos = + env->GetMethodID(displayClass, "getPresentationDeadlineNanos", "()J"); + + if (getPresentationDeadlineNanos == 0 || env->ExceptionOccurred()) { + ALOGE("Error while getting method: getPresentationDeadlineNanos"); + return false; + } + + const long vsyncPresentationDeadlineNanos = + env->CallLongMethod(display, getPresentationDeadlineNanos); + + const long ONE_MS_IN_NS = 1000 * 1000; + const long ONE_S_IN_NS = ONE_MS_IN_NS * 1000; + + const long vsyncPeriodNanos = + static_cast(ONE_S_IN_NS / refreshRateHz); + const long sfVsyncOffsetNanos = + vsyncPeriodNanos - (vsyncPresentationDeadlineNanos - ONE_MS_IN_NS); + + using std::chrono::nanoseconds; + out->refreshPeriod = nanoseconds(vsyncPeriodNanos); + out->appVsyncOffset = nanoseconds(appVsyncOffsetNanos); + out->sfVsyncOffset = nanoseconds(sfVsyncOffsetNanos); + + return true; +} + +SwappyCommon::SwappyCommon(JNIEnv* env, jobject jactivity) + : mJactivity(env->NewGlobalRef(jactivity)), + mMeasuredSwapDuration(nanoseconds(0)), + mAutoSwapInterval(1), + mValid(false) { + mLibAndroid = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL); + if (mLibAndroid == nullptr) { + ALOGE("FATAL: cannot open libandroid.so: %s", strerror(errno)); + return; + } + + mANativeWindow_setFrameRate = + reinterpret_cast( + dlsym(mLibAndroid, "ANativeWindow_setFrameRate")); + + if (!SwappyCommonSettings::getFromApp(env, mJactivity, &mCommonSettings)) + return; + + env->GetJavaVM(&mJVM); + + if (isDeviceUnsupported()) { + ALOGE("Device is unsupported"); + return; + } + + mChoreographerFilter = std::make_unique( + mCommonSettings.refreshPeriod, + mCommonSettings.sfVsyncOffset - mCommonSettings.appVsyncOffset, + [this]() { return wakeClient(); }); + + mChoreographerThread = ChoreographerThread::createChoreographerThread( + ChoreographerThread::Type::Swappy, mJVM, jactivity, + [this] { mChoreographerFilter->onChoreographer(); }, + [this] { onRefreshRateChanged(); }, mCommonSettings.sdkVersion); + if (!mChoreographerThread->isInitialized()) { + ALOGE("failed to initialize ChoreographerThread"); + return; + } + if (USE_DISPLAY_MANAGER && SwappyDisplayManager::useSwappyDisplayManager( + mCommonSettings.sdkVersion)) { + mDisplayManager = + std::make_unique(mJVM, jactivity); + + if (!mDisplayManager->isInitialized()) { + mDisplayManager = nullptr; + ALOGE("failed to initialize DisplayManager"); + return; + } + } + + Settings::getInstance()->addListener([this]() { onSettingsChanged(); }); + Settings::getInstance()->setDisplayTimings({mCommonSettings.refreshPeriod, + mCommonSettings.appVsyncOffset, + mCommonSettings.sfVsyncOffset}); + + ALOGI( + "Initialized Swappy with vsyncPeriod=%lld, appOffset=%lld, " + "sfOffset=%lld", + (long long)mCommonSettings.refreshPeriod.count(), + (long long)mCommonSettings.appVsyncOffset.count(), + (long long)mCommonSettings.sfVsyncOffset.count()); + mValid = true; +} + +// Used by tests +SwappyCommon::SwappyCommon(const SwappyCommonSettings& settings) + : mJactivity(nullptr), + mCommonSettings(settings), + mMeasuredSwapDuration(nanoseconds(0)), + mAutoSwapInterval(1), + mValid(true) { + mChoreographerFilter = std::make_unique( + mCommonSettings.refreshPeriod, + mCommonSettings.sfVsyncOffset - mCommonSettings.appVsyncOffset, + [this]() { return wakeClient(); }); + mUsingExternalChoreographer = true; + mChoreographerThread = ChoreographerThread::createChoreographerThread( + ChoreographerThread::Type::App, nullptr, nullptr, + [this] { mChoreographerFilter->onChoreographer(); }, [] {}, + mCommonSettings.sdkVersion); + + Settings::getInstance()->addListener([this]() { onSettingsChanged(); }); + Settings::getInstance()->setDisplayTimings({mCommonSettings.refreshPeriod, + mCommonSettings.appVsyncOffset, + mCommonSettings.sfVsyncOffset}); + + ALOGI( + "Initialized Swappy with vsyncPeriod=%lld, appOffset=%lld, " + "sfOffset=%lld", + (long long)mCommonSettings.refreshPeriod.count(), + (long long)mCommonSettings.appVsyncOffset.count(), + (long long)mCommonSettings.sfVsyncOffset.count()); +} + +SwappyCommon::~SwappyCommon() { + // destroy all threads first before the other members of this class + mChoreographerThread.reset(); + mChoreographerFilter.reset(); + + Settings::reset(); + + if (mJactivity != nullptr) { + JNIEnv* env; + mJVM->AttachCurrentThread(&env, nullptr); + + env->DeleteGlobalRef(mJactivity); + } +} + +void SwappyCommon::onRefreshRateChanged() { + JNIEnv* env; + mJVM->AttachCurrentThread(&env, nullptr); + + ALOGV("onRefreshRateChanged"); + + SwappyCommonSettings settings; + if (!SwappyCommonSettings::getFromApp(env, mJactivity, &settings)) { + ALOGE("failed to query display timings"); + return; + } + + Settings::getInstance()->setDisplayTimings({settings.refreshPeriod, + settings.appVsyncOffset, + settings.sfVsyncOffset}); + ALOGV("onRefreshRateChanged: refresh rate: %.0fHz", + 1e9f / settings.refreshPeriod.count()); +} + +nanoseconds SwappyCommon::wakeClient() { + std::lock_guard lock(mWaitingMutex); + ++mCurrentFrame; + + // We're attempting to align with SurfaceFlinger's vsync, but it's always + // better to be a little late than a little early (since a little early + // could cause our frame to be picked up prematurely), so we pad by an + // additional millisecond. + mCurrentFrameTimestamp = + std::chrono::steady_clock::now() + mMeasuredSwapDuration.load() + 1ms; + mWaitingCondition.notify_all(); + return mMeasuredSwapDuration; +} + +void SwappyCommon::onChoreographer(int64_t frameTimeNanos) { + TRACE_CALL(); + + if (!mUsingExternalChoreographer) { + mUsingExternalChoreographer = true; + mChoreographerThread = ChoreographerThread::createChoreographerThread( + ChoreographerThread::Type::App, nullptr, nullptr, + [this] { mChoreographerFilter->onChoreographer(); }, + [this] { onRefreshRateChanged(); }, mCommonSettings.sdkVersion); + } + + mChoreographerThread->postFrameCallbacks(); +} + +bool SwappyCommon::waitForNextFrame(const SwapHandlers& h) { + int lateFrames = 0; + bool presentationTimeIsNeeded; + + const nanoseconds cpuTime = + (mStartFrameTime.time_since_epoch().count() == 0) + ? 0ns + : std::chrono::steady_clock::now() - mStartFrameTime; + mCPUTracer.endTrace(); + + preWaitCallbacks(); + + // if we are running slower than the threshold there is no point to sleep, + // just let the app run as fast as it can + if (mCommonSettings.refreshPeriod * mAutoSwapInterval <= + mAutoSwapIntervalThreshold.load()) { + waitUntilTargetFrame(); + + // wait for the previous frame to be rendered + while (!h.lastFrameIsComplete()) { + lateFrames++; + waitOneFrame(); + } + + mPresentationTime += lateFrames * mCommonSettings.refreshPeriod; + presentationTimeIsNeeded = true; + } else { + presentationTimeIsNeeded = false; + } + + const nanoseconds gpuTime = h.getPrevFrameGpuTime(); + addFrameDuration({cpuTime, gpuTime, mCurrentFrame > mTargetFrame}); + + postWaitCallbacks(cpuTime, gpuTime); + + return presentationTimeIsNeeded; +} + +void SwappyCommon::updateDisplayTimings() { + // grab a pointer to the latest supported refresh rates + if (mDisplayManager) { + mSupportedRefreshPeriods = + mDisplayManager->getSupportedRefreshPeriods(); + } + + std::lock_guard lock(mMutex); + ALOGW_ONCE_IF(!mWindow, + "ANativeWindow not configured, frame rate will not be " + "reported to Android platform"); + + if (!mTimingSettingsNeedUpdate && !mWindowChanged) { + return; + } + + mTimingSettingsNeedUpdate = false; + + if (!mWindowChanged && + mCommonSettings.refreshPeriod == mNextTimingSettings.refreshPeriod && + mSwapDuration == mNextTimingSettings.swapDuration) { + return; + } + + mWindowChanged = false; + mCommonSettings.refreshPeriod = mNextTimingSettings.refreshPeriod; + + const auto pipelineFrameTime = + mFrameDurations.getAverageFrameTime().getTime(PipelineMode::On); + const auto swapDuration = + pipelineFrameTime != 0ns ? pipelineFrameTime : mSwapDuration; + mAutoSwapInterval = + calculateSwapInterval(swapDuration, mCommonSettings.refreshPeriod); + mPipelineMode = PipelineMode::On; + + const bool swapIntervalValid = + mNextTimingSettings.refreshPeriod * mAutoSwapInterval >= + mNextTimingSettings.swapDuration; + const bool swapIntervalChangedBySettings = + mSwapDuration != mNextTimingSettings.swapDuration; + + mSwapDuration = mNextTimingSettings.swapDuration; + if (!mAutoSwapIntervalEnabled || swapIntervalChangedBySettings || + !swapIntervalValid) { + mAutoSwapInterval = + calculateSwapInterval(mSwapDuration, mCommonSettings.refreshPeriod); + mPipelineMode = PipelineMode::On; + setPreferredRefreshPeriod(mSwapDuration); + } + + if (mNextModeId == -1 && mLatestFrameRateVote == 0) { + setPreferredRefreshPeriod(mSwapDuration); + } + + mFrameDurations.clear(); + + TRACE_INT("mSwapDuration", int(mSwapDuration.count())); + TRACE_INT("mAutoSwapInterval", mAutoSwapInterval); + TRACE_INT("mCommonSettings.refreshPeriod", + mCommonSettings.refreshPeriod.count()); + TRACE_INT("mPipelineMode", static_cast(mPipelineMode)); +} + +void SwappyCommon::onPreSwap(const SwapHandlers& h) { + if (!mUsingExternalChoreographer) { + mChoreographerThread->postFrameCallbacks(); + } + + // for non pipeline mode where both cpu and gpu work is done at the same + // stage wait for next frame will happen after swap + if (mPipelineMode == PipelineMode::On) { + mPresentationTimeNeeded = waitForNextFrame(h); + } else { + mPresentationTimeNeeded = + (mCommonSettings.refreshPeriod * mAutoSwapInterval <= + mAutoSwapIntervalThreshold.load()); + } + + mSwapTime = std::chrono::steady_clock::now(); + preSwapBuffersCallbacks(); +} + +void SwappyCommon::onPostSwap(const SwapHandlers& h) { + postSwapBuffersCallbacks(); + + updateMeasuredSwapDuration(std::chrono::steady_clock::now() - mSwapTime); + + if (mPipelineMode == PipelineMode::Off) { + waitForNextFrame(h); + } + + if (updateSwapInterval()) { + swapIntervalChangedCallbacks(); + TRACE_INT("mPipelineMode", static_cast(mPipelineMode)); + TRACE_INT("mAutoSwapInterval", mAutoSwapInterval); + } + + updateDisplayTimings(); + + startFrame(); +} + +void SwappyCommon::updateMeasuredSwapDuration(nanoseconds duration) { + // TODO: The exponential smoothing factor here is arbitrary + mMeasuredSwapDuration = + (mMeasuredSwapDuration.load() * 4 / 5) + duration / 5; + + // Clamp the swap duration to half the refresh period + // + // We do this since the swap duration can be a bit noisy during periods such + // as app startup, which can cause some stuttering as the smoothing catches + // up with the actual duration. By clamping, we reduce the maximum error + // which reduces the calibration time. + if (mMeasuredSwapDuration.load() > (mCommonSettings.refreshPeriod / 2)) { + mMeasuredSwapDuration.store(mCommonSettings.refreshPeriod / 2); + } +} + +nanoseconds SwappyCommon::getSwapDuration() { + std::lock_guard lock(mMutex); + return mAutoSwapInterval * mCommonSettings.refreshPeriod; +}; + +void SwappyCommon::FrameDurations::add(FrameDuration frameDuration) { + const auto now = std::chrono::steady_clock::now(); + mFrames.push_back({now, frameDuration}); + mFrameDurationsSum += frameDuration; + if (frameDuration.frameMiss()) { + mMissedFrameCount++; + } + + while (mFrames.size() >= 2 && + now - (mFrames.begin() + 1)->first > FRAME_DURATION_SAMPLE_SECONDS) { + mFrameDurationsSum -= mFrames.front().second; + if (mFrames.front().second.frameMiss()) { + mMissedFrameCount--; + } + mFrames.pop_front(); + } +} + +bool SwappyCommon::FrameDurations::hasEnoughSamples() const { + return (!mFrames.empty()) && (mFrames.back().first - mFrames.front().first > + FRAME_DURATION_SAMPLE_SECONDS); +} + +SwappyCommon::FrameDuration SwappyCommon::FrameDurations::getAverageFrameTime() + const { + if (hasEnoughSamples()) { + return mFrameDurationsSum / mFrames.size(); + } + + return {}; +} + +int SwappyCommon::FrameDurations::getMissedFramePercent() const { + return round(mMissedFrameCount * 100.0f / mFrames.size()); +} + +void SwappyCommon::FrameDurations::clear() { + mFrames.clear(); + mFrameDurationsSum = {}; + mMissedFrameCount = 0; +} + +void SwappyCommon::addFrameDuration(FrameDuration duration) { + ALOGV("cpuTime = %.2f", duration.getCpuTime().count() / 1e6f); + ALOGV("gpuTime = %.2f", duration.getGpuTime().count() / 1e6f); + ALOGV("frame %s", duration.frameMiss() ? "MISS" : "on time"); + + std::lock_guard lock(mMutex); + mFrameDurations.add(duration); +} + +bool SwappyCommon::swapSlower(const FrameDuration& averageFrameTime, + const nanoseconds& upperBound, + int newSwapInterval) { + bool swappedSlower = false; + ALOGV("Rendering takes too much time for the given config"); + + const auto frameFitsUpperBound = + averageFrameTime.getTime(PipelineMode::On) <= upperBound; + const auto swapDurationWithinThreshold = + mCommonSettings.refreshPeriod * mAutoSwapInterval <= + mAutoSwapIntervalThreshold.load() + FRAME_MARGIN; + + // Check if turning on pipeline is not enough + if ((mPipelineMode == PipelineMode::On || !frameFitsUpperBound) && + swapDurationWithinThreshold) { + int originalAutoSwapInterval = mAutoSwapInterval; + if (newSwapInterval > mAutoSwapInterval) { + mAutoSwapInterval = newSwapInterval; + } else { + mAutoSwapInterval++; + } + if (mAutoSwapInterval != originalAutoSwapInterval) { + ALOGV("Changing Swap interval to %d from %d", mAutoSwapInterval, + originalAutoSwapInterval); + swappedSlower = true; + } + } + + if (mPipelineMode == PipelineMode::Off) { + ALOGV("turning on pipelining"); + mPipelineMode = PipelineMode::On; + } + + return swappedSlower; +} + +bool SwappyCommon::swapFaster(int newSwapInterval) { + bool swappedFaster = false; + int originalAutoSwapInterval = mAutoSwapInterval; + while (newSwapInterval < mAutoSwapInterval && swapFasterCondition()) { + mAutoSwapInterval--; + } + + if (mAutoSwapInterval != originalAutoSwapInterval) { + ALOGV("Rendering is much shorter for the given config"); + ALOGV("Changing Swap interval to %d from %d", mAutoSwapInterval, + originalAutoSwapInterval); + // since we changed the swap interval, we may need to turn on pipeline + // mode + ALOGV("Turning on pipelining"); + mPipelineMode = PipelineMode::On; + swappedFaster = true; + } + + return swappedFaster; +} + +bool SwappyCommon::updateSwapInterval() { + std::lock_guard lock(mMutex); + if (!mAutoSwapIntervalEnabled) return false; + + if (!mFrameDurations.hasEnoughSamples()) return false; + + const auto averageFrameTime = mFrameDurations.getAverageFrameTime(); + const auto pipelineFrameTime = averageFrameTime.getTime(PipelineMode::On); + const auto nonPipelineFrameTime = + averageFrameTime.getTime(PipelineMode::Off); + + // calculate the new swap interval based on average frame time assume we are + // in pipeline mode (prefer higher swap interval rather than turning off + // pipeline mode) + const int newSwapInterval = + calculateSwapInterval(pipelineFrameTime, mCommonSettings.refreshPeriod); + + // Define upper and lower bounds based on the swap duration + const nanoseconds upperBoundForThisRefresh = + mCommonSettings.refreshPeriod * mAutoSwapInterval; + const nanoseconds lowerBoundForThisRefresh = + mCommonSettings.refreshPeriod * (mAutoSwapInterval - 1) - FRAME_MARGIN; + + const int missedFramesPercent = mFrameDurations.getMissedFramePercent(); + + ALOGV("mPipelineMode = %d", static_cast(mPipelineMode)); + ALOGV("Average cpu frame time = %.2f", + (averageFrameTime.getCpuTime().count()) / 1e6f); + ALOGV("Average gpu frame time = %.2f", + (averageFrameTime.getGpuTime().count()) / 1e6f); + ALOGV("upperBound = %.2f", upperBoundForThisRefresh.count() / 1e6f); + ALOGV("lowerBound = %.2f", lowerBoundForThisRefresh.count() / 1e6f); + ALOGV("frame missed = %d%%", missedFramesPercent); + + bool configChanged = false; + ALOGV("pipelineFrameTime = %.2f", pipelineFrameTime.count() / 1e6f); + const auto nonPipelinePercent = (100.f + NON_PIPELINE_PERCENT) / 100.f; + + // Make sure the frame time fits in the current config to avoid missing + // frames + if (missedFramesPercent > FRAME_DROP_THRESHOLD) { + if (swapSlower(averageFrameTime, upperBoundForThisRefresh, + newSwapInterval)) + configChanged = true; + } + + // So we shouldn't miss any frames with this config but maybe we can go + // faster ? we check the pipeline frame time here as we prefer lower swap + // interval than no pipelining + else if (missedFramesPercent == 0 && swapFasterCondition() && + pipelineFrameTime < lowerBoundForThisRefresh) { + if (swapFaster(newSwapInterval)) configChanged = true; + } + + // If we reached to this condition it means that we fit into the boundaries. + // However we might be in pipeline mode and we could turn it off if we still + // fit. To be very conservative, switch to non-pipeline if frame time * 50% + // fits + else if (mPipelineModeAutoMode && mPipelineMode == PipelineMode::On && + nonPipelineFrameTime * nonPipelinePercent < + upperBoundForThisRefresh) { + ALOGV( + "Rendering time fits the current swap interval without pipelining"); + mPipelineMode = PipelineMode::Off; + configChanged = true; + } + + if (configChanged) { + mFrameDurations.clear(); + } + + setPreferredRefreshPeriod(pipelineFrameTime); + + return configChanged; +} + +template +void addToTracers(Tracers& tracers, Func func, void* userData) { + if (func != nullptr) { + tracers.push_back({func, userData}); + } +} + +template +void removeFromTracers(Tracers& tracers, Func func) { + if (func != nullptr) { + for (auto it = tracers.begin(); it != tracers.end();) { + auto jt = it; + it++; + if (jt->function == func) { + tracers.erase(jt); + } + } + } +} + +void SwappyCommon::addTracerCallbacks(const SwappyTracer& tracer) { + addToTracers(mInjectedTracers.preWait, tracer.preWait, tracer.userData); + addToTracers(mInjectedTracers.postWait, tracer.postWait, tracer.userData); + addToTracers(mInjectedTracers.preSwapBuffers, tracer.preSwapBuffers, + tracer.userData); + addToTracers(mInjectedTracers.postSwapBuffers, tracer.postSwapBuffers, + tracer.userData); + addToTracers(mInjectedTracers.startFrame, tracer.startFrame, + tracer.userData); + addToTracers(mInjectedTracers.swapIntervalChanged, + tracer.swapIntervalChanged, tracer.userData); +} + +void SwappyCommon::removeTracerCallbacks(const SwappyTracer& tracer) { + removeFromTracers(mInjectedTracers.preWait, tracer.preWait); + removeFromTracers(mInjectedTracers.postWait, tracer.postWait); + removeFromTracers(mInjectedTracers.preSwapBuffers, tracer.preSwapBuffers); + removeFromTracers(mInjectedTracers.postSwapBuffers, tracer.postSwapBuffers); + removeFromTracers(mInjectedTracers.startFrame, tracer.startFrame); + removeFromTracers(mInjectedTracers.swapIntervalChanged, + tracer.swapIntervalChanged); +} + +template +void executeTracers(T& tracers, Args... args) { + for (const auto& tracer : tracers) { + tracer.function(tracer.userData, std::forward(args)...); + } +} + +void SwappyCommon::preSwapBuffersCallbacks() { + executeTracers(mInjectedTracers.preSwapBuffers); +} + +void SwappyCommon::postSwapBuffersCallbacks() { + executeTracers(mInjectedTracers.postSwapBuffers, + (int64_t)mPresentationTime.time_since_epoch().count()); +} + +void SwappyCommon::preWaitCallbacks() { + executeTracers(mInjectedTracers.preWait); +} + +void SwappyCommon::postWaitCallbacks(nanoseconds cpuTime, nanoseconds gpuTime) { + executeTracers(mInjectedTracers.postWait, cpuTime.count(), gpuTime.count()); +} + +void SwappyCommon::startFrameCallbacks() { + executeTracers(mInjectedTracers.startFrame, mCurrentFrame, + (int64_t)mPresentationTime.time_since_epoch().count()); +} + +void SwappyCommon::swapIntervalChangedCallbacks() { + executeTracers(mInjectedTracers.swapIntervalChanged); +} + +void SwappyCommon::setAutoSwapInterval(bool enabled) { + std::lock_guard lock(mMutex); + mAutoSwapIntervalEnabled = enabled; + + // non pipeline mode is not supported when auto mode is disabled + if (!enabled) { + mPipelineMode = PipelineMode::On; + TRACE_INT("mPipelineMode", static_cast(mPipelineMode)); + } +} + +void SwappyCommon::setAutoPipelineMode(bool enabled) { + std::lock_guard lock(mMutex); + mPipelineModeAutoMode = enabled; + TRACE_INT("mPipelineModeAutoMode", mPipelineModeAutoMode); + if (!enabled) { + mPipelineMode = PipelineMode::On; + TRACE_INT("mPipelineMode", static_cast(mPipelineMode)); + } +} + +void SwappyCommon::setPreferredDisplayModeId(int modeId) { + if (!mDisplayManager || modeId < 0 || mNextModeId == modeId) { + return; + } + + mNextModeId = modeId; + mDisplayManager->setPreferredDisplayModeId(modeId); + ALOGV("setPreferredDisplayModeId set to %d", modeId); +} + +int SwappyCommon::calculateSwapInterval(nanoseconds frameTime, + nanoseconds refreshPeriod) { + if (frameTime < refreshPeriod) { + return 1; + } + + auto div_result = div(frameTime.count(), refreshPeriod.count()); + auto framesPerRefresh = div_result.quot; + auto framesPerRefreshRemainder = div_result.rem; + + return (framesPerRefresh + + (framesPerRefreshRemainder > REFRESH_RATE_MARGIN.count() ? 1 : 0)); +} + +void SwappyCommon::setPreferredRefreshPeriod(nanoseconds frameTime) { + if (mANativeWindow_setFrameRate && mWindow) { + auto frameRate = 1e9f / frameTime.count(); + + frameRate = std::min(frameRate, 1e9f / (mSwapDuration).count()); + if (std::abs(mLatestFrameRateVote - frameRate) > + FRAME_RATE_VOTE_MARGIN) { + mLatestFrameRateVote = frameRate; + ALOGV("ANativeWindow_setFrameRate(%.2f)", frameRate); + mANativeWindow_setFrameRate( + mWindow, frameRate, + ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT); + } + + TRACE_INT("preferredRefreshPeriod", (int)frameRate); + } else { + if (!mDisplayManager || !mSupportedRefreshPeriods) { + return; + } + // Loop across all supported refresh periods to find the best refresh + // period. Best refresh period means: + // Shortest swap period that can still accommodate the frame time + // and that has the longest refresh period possible to optimize + // power consumption. + std::pair bestRefreshConfig; + nanoseconds minSwapDuration = 1s; + for (const auto& refreshConfig : *mSupportedRefreshPeriods) { + const auto period = refreshConfig.first; + const int swapIntervalForPeriod = + calculateSwapInterval(frameTime, period); + const nanoseconds swapDuration = period * swapIntervalForPeriod; + + // Don't allow swapping faster than mSwapDuration (see public + // header) + if (swapDuration + FRAME_MARGIN < mSwapDuration) { + continue; + } + + // We iterate in ascending order of refresh period, so accepting any + // better or equal-within-margin duration here chooses the longest + // refresh period possible. + if (swapDuration < minSwapDuration + FRAME_MARGIN) { + minSwapDuration = swapDuration; + bestRefreshConfig = refreshConfig; + } + } + + // Switch if we have a potentially better refresh rate + { + TRACE_INT("preferredRefreshPeriod", + bestRefreshConfig.first.count()); + setPreferredDisplayModeId(bestRefreshConfig.second); + } + } +} + +void SwappyCommon::onSettingsChanged() { + std::lock_guard lock(mMutex); + + TimingSettings timingSettings = + TimingSettings::from(*Settings::getInstance()); + + // If display timings has changed, cache the update and apply them on the + // next frame + if (timingSettings != mNextTimingSettings) { + mNextTimingSettings = timingSettings; + mTimingSettingsNeedUpdate = true; + } +} + +void SwappyCommon::startFrame() { + TRACE_CALL(); + + int32_t currentFrame; + std::chrono::steady_clock::time_point currentFrameTimestamp; + { + std::unique_lock lock(mWaitingMutex); + currentFrame = mCurrentFrame; + currentFrameTimestamp = mCurrentFrameTimestamp; + } + + // Whether to add a wait to fix buffer stuffing. + bool waitFrame = false; + + const int intervals = (mPipelineMode == PipelineMode::On) ? 2 : 1; + + // Use frame statistics to fix any buffer stuffing + if (mBufferStuffingFixWait > 0 && mFrameStatistics) { + int32_t lastLatency = mFrameStatistics->lastLatencyRecorded(); + int expectedLatency = mAutoSwapInterval * intervals; + TRACE_INT("ExpectedLatency", expectedLatency); + if (mBufferStuffingFixCounter == 0) { + if (lastLatency > expectedLatency) { + mMissedFrameCounter++; + if (mMissedFrameCounter >= mBufferStuffingFixWait) { + waitFrame = true; + mBufferStuffingFixCounter = 2 * lastLatency; + TRACE_INT("BufferStuffingFix", mBufferStuffingFixCounter); + } + } else { + mMissedFrameCounter = 0; + } + } else { + --mBufferStuffingFixCounter; + TRACE_INT("BufferStuffingFix", mBufferStuffingFixCounter); + } + } + mTargetFrame = currentFrame + mAutoSwapInterval; + if (waitFrame) mTargetFrame += 1; + + // We compute the target time as now + // + the time the buffer will be on the GPU and in the queue to the + // compositor (1 swap period) + mPresentationTime = + currentFrameTimestamp + + (mAutoSwapInterval * intervals) * mCommonSettings.refreshPeriod; + + mStartFrameTime = std::chrono::steady_clock::now(); + mCPUTracer.startTrace(); + + startFrameCallbacks(); +} + +void SwappyCommon::waitUntil(int32_t target) { + TRACE_CALL(); + std::unique_lock lock(mWaitingMutex); + mWaitingCondition.wait(lock, [&]() { + if (mCurrentFrame < target) { + if (!mUsingExternalChoreographer) { + mChoreographerThread->postFrameCallbacks(); + } + return false; + } + return true; + }); +} + +void SwappyCommon::waitUntilTargetFrame() { waitUntil(mTargetFrame); } + +void SwappyCommon::waitOneFrame() { waitUntil(mCurrentFrame + 1); } + +SdkVersion SwappyCommonSettings::getSDKVersion(JNIEnv* env) { + const jclass buildClass = env->FindClass("android/os/Build$VERSION"); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + ALOGE("Failed to get Build.VERSION class"); + return SdkVersion{0, 0}; + } + + const jfieldID sdkInt = env->GetStaticFieldID(buildClass, "SDK_INT", "I"); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + ALOGE("Failed to get Build.VERSION.SDK_INT field"); + return SdkVersion{0, 0}; + } + + const jint sdk = env->GetStaticIntField(buildClass, sdkInt); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + ALOGE("Failed to get SDK version"); + return SdkVersion{0, 0}; + } + + jint sdkPreview = 0; + if (sdk >= 23) { + const jfieldID previewSdkInt = + env->GetStaticFieldID(buildClass, "PREVIEW_SDK_INT", "I"); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + ALOGE("Failed to get Build.VERSION.PREVIEW_SDK_INT field"); + } + + sdkPreview = env->GetStaticIntField(buildClass, previewSdkInt); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + ALOGE("Failed to get preview SDK version"); + } + } + + ALOGI("SDK version = %d preview = %d", sdk, sdkPreview); + return SdkVersion{sdk, sdkPreview}; +} + +void SwappyCommon::setANativeWindow(ANativeWindow* window) { + std::lock_guard lock(mMutex); + if (mWindow == window) { + return; + } + + if (mWindow != nullptr) { + ANativeWindow_release(mWindow); + } + + mWindow = window; + if (mWindow != nullptr) { + ANativeWindow_acquire(mWindow); + mWindowChanged = true; + mLatestFrameRateVote = 0; + } +} + +namespace { + +static std::string GetStaticStringField(JNIEnv* env, jclass clz, + const char* name) { + const jfieldID fieldId = + env->GetStaticFieldID(clz, name, "Ljava/lang/String;"); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + ALOGE("Failed to get string field %s", name); + return ""; + } + + const jstring jstr = (jstring)env->GetStaticObjectField(clz, fieldId); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + ALOGE("Failed to get string %s", name); + return ""; + } + auto cstr = env->GetStringUTFChars(jstr, nullptr); + auto length = env->GetStringUTFLength(jstr); + std::string retValue(cstr, length); + env->ReleaseStringUTFChars(jstr, cstr); + env->DeleteLocalRef(jstr); + return retValue; +} + +struct DeviceIdentifier { + std::string manufacturer; + std::string model; + std::string display; + // Empty fields match against any value and we match the beginning of the + // input, e.g. + // A37 matches A37f, A37fw, etc. + bool match(const std::string& manufacturer_in, const std::string& model_in, + const std::string& display_in) { + if (!matchStartOfString(manufacturer, manufacturer_in)) return false; + if (!matchStartOfString(model, model_in)) return false; + if (!matchStartOfString(display, display_in)) return false; + return true; + } + bool matchStartOfString(const std::string& start, + const std::string& sample) { + return start.empty() || start == sample.substr(0, start.length()); + } +}; + +} // anonymous namespace + +bool SwappyCommon::isDeviceUnsupported() { + JNIEnv* env; + mJVM->AttachCurrentThread(&env, nullptr); + + // List of unsupported models + static std::vector unsupportedDevices = { + {"OPPO", "A37", ""}}; + + const jclass buildClass = env->FindClass("android/os/Build"); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + ALOGE("Failed to get Build class"); + return false; + } + + auto manufacturer = GetStaticStringField(env, buildClass, "MANUFACTURER"); + if (manufacturer.empty()) return false; + + auto model = GetStaticStringField(env, buildClass, "MODEL"); + if (model.empty()) return false; + + auto display = GetStaticStringField(env, buildClass, "DISPLAY"); + if (display.empty()) return false; + + for (auto& device : unsupportedDevices) { + if (device.match(manufacturer, model, display)) return true; + } + + return false; +} + +} // namespace swappy diff --git a/sources/Swappy/src/swappy/common/SwappyCommon.h b/sources/Swappy/src/swappy/common/SwappyCommon.h new file mode 100644 index 00000000..a9496ab9 --- /dev/null +++ b/sources/Swappy/src/swappy/common/SwappyCommon.h @@ -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 + +#include +#include +#include +#include +#include +#include + +#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 lastFrameIsComplete; + std::function 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 + 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& 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 mChoreographerFilter; + + bool mUsingExternalChoreographer = false; + std::unique_ptr 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 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, + 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 mAutoSwapIntervalThreshold = { + 50ms}; // 20FPS + static constexpr std::chrono::nanoseconds REFRESH_RATE_MARGIN = 500ns; + + std::chrono::steady_clock::time_point mStartFrameTime; + + struct SwappyTracerCallbacks { + std::list> preWait; + std::list> postWait; + std::list> preSwapBuffers; + std::list> postSwapBuffers; + std::list> startFrame; + std::list> 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 mDisplayManager; + int mNextModeId = -1; + + std::shared_ptr + 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 mFrameStatistics; +}; + +} // namespace swappy diff --git a/sources/Swappy/src/swappy/common/SwappyDisplayManager.cpp b/sources/Swappy/src/swappy/common/SwappyDisplayManager.cpp new file mode 100644 index 00000000..a675610b --- /dev/null +++ b/sources/Swappy/src/swappy/common/SwappyDisplayManager.cpp @@ -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 +#include +#include + +#include + +#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, "", "(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::getSupportedRefreshPeriods() { + std::unique_lock 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); + static void onRefreshPeriodChanged(jlong, long, long, long); +}; + +void SwappyDisplayManagerJNI::onSetSupportedRefreshPeriods( + jlong cookie, + std::shared_ptr refreshPeriods) { + auto *sDM = reinterpret_cast(cookie); + + std::lock_guard 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(); + + 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 diff --git a/sources/Swappy/src/swappy/common/SwappyDisplayManager.h b/sources/Swappy/src/swappy/common/SwappyDisplayManager.h new file mode 100644 index 00000000..783b3758 --- /dev/null +++ b/sources/Swappy/src/swappy/common/SwappyDisplayManager.h @@ -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 + +#include +#include +#include +#include +#include + +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::shared_ptr getSupportedRefreshPeriods(); + + void setPreferredDisplayModeId(int index); + + private: + JavaVM* mJVM; + std::mutex mMutex; + std::condition_variable mCondition; + std::shared_ptr mSupportedRefreshPeriods; + jobject mJthis = nullptr; + jmethodID mSetPreferredDisplayModeId = nullptr; + jmethodID mTerminate = nullptr; + bool mInitialized = false; + + friend class SwappyDisplayManagerJNI; +}; + +} // namespace swappy diff --git a/sources/Swappy/src/swappy/common/Thread.cpp b/sources/Swappy/src/swappy/common/Thread.cpp new file mode 100644 index 00000000..cf4da82d --- /dev/null +++ b/sources/Swappy/src/swappy/common/Thread.cpp @@ -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 +#include + +#include + +#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 fn_; + SwappyThreadId id_; + + public: + ExtThreadImpl(std::function&& 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&& fn) : thread_(std::move(fn)) {} + void join() { thread_.join(); } + bool joinable() { return thread_.joinable(); } +}; + +Thread::Thread() noexcept {} + +Thread::~Thread() {} + +Thread::Thread(std::function&& fn) noexcept { + if (s_ext_thread_manager != nullptr) { + impl_ = std::make_unique(std::move(fn)); + } else { + impl_ = std::make_unique(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; +} diff --git a/sources/Swappy/src/swappy/common/Thread.h b/sources/Swappy/src/swappy/common/Thread.h new file mode 100644 index 00000000..bf0f8245 --- /dev/null +++ b/sources/Swappy/src/swappy/common/Thread.h @@ -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 +#include +#include + +// 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 impl_; + + public: + Thread() noexcept; + Thread(std::function&& 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 diff --git a/sources/Swappy/src/swappy/common/swappy_c.cpp b/sources/Swappy/src/swappy/common/swappy_c.cpp new file mode 100644 index 00000000..cf9db4d4 --- /dev/null +++ b/sources/Swappy/src/swappy/common/swappy_c.cpp @@ -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" diff --git a/sources/Swappy/src/swappy/opengl/EGL.cpp b/sources/Swappy/src/swappy/opengl/EGL.cpp new file mode 100644 index 00000000..cbbfd2b5 --- /dev/null +++ b/sources/Swappy/src/swappy/opengl/EGL.cpp @@ -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 +#include + +#include + +#define LOG_TAG "Swappy::EGL" + +#include "Log.h" + +using namespace std::chrono_literals; + +namespace swappy { + +std::unique_ptr 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( + dlsym(eglLib, "eglGetProcAddress")); + if (eglGetProcAddress == nullptr) { + ALOGE("Failed to load eglGetProcAddress"); + return nullptr; + } + + auto eglSwapBuffers = + reinterpret_cast(dlsym(eglLib, "eglSwapBuffers")); + if (eglSwapBuffers == nullptr) { + ALOGE("Failed to load eglSwapBuffers"); + return nullptr; + } + + auto eglPresentationTimeANDROID = + reinterpret_cast( + eglGetProcAddress("eglPresentationTimeANDROID")); + if (eglPresentationTimeANDROID == nullptr) { + ALOGE("Failed to load eglPresentationTimeANDROID"); + return nullptr; + } + + auto eglCreateSyncKHR = reinterpret_cast( + eglGetProcAddress("eglCreateSyncKHR")); + if (eglCreateSyncKHR == nullptr) { + ALOGE("Failed to load eglCreateSyncKHR"); + return nullptr; + } + + auto eglDestroySyncKHR = reinterpret_cast( + eglGetProcAddress("eglDestroySyncKHR")); + if (eglDestroySyncKHR == nullptr) { + ALOGE("Failed to load eglDestroySyncKHR"); + return nullptr; + } + + auto eglGetSyncAttribKHR = reinterpret_cast( + eglGetProcAddress("eglGetSyncAttribKHR")); + if (eglGetSyncAttribKHR == nullptr) { + ALOGE("Failed to load eglGetSyncAttribKHR"); + return nullptr; + } + + auto eglGetError = + reinterpret_cast(eglGetProcAddress("eglGetError")); + if (eglGetError == nullptr) { + ALOGE("Failed to load eglGetError"); + return nullptr; + } + + auto eglSurfaceAttrib = reinterpret_cast( + eglGetProcAddress("eglSurfaceAttrib")); + if (eglSurfaceAttrib == nullptr) { + ALOGE("Failed to load eglSurfaceAttrib"); + return nullptr; + } + + // stats may not be supported on all versions + auto eglGetNextFrameIdANDROID = + reinterpret_cast( + eglGetProcAddress("eglGetNextFrameIdANDROID")); + if (eglGetNextFrameIdANDROID == nullptr) { + ALOGI("Failed to load eglGetNextFrameIdANDROID"); + } + + auto eglGetFrameTimestampsANDROID = + reinterpret_cast( + eglGetProcAddress("eglGetFrameTimestampsANDROID")); + if (eglGetFrameTimestampsANDROID == nullptr) { + ALOGI("Failed to load eglGetFrameTimestampsANDROID"); + } + + auto egl = std::make_unique(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 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 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 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::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 timestamps = { + EGL_REQUESTED_PRESENT_TIME_ANDROID, + EGL_RENDERING_COMPLETE_TIME_ANDROID, + EGL_COMPOSITION_LATCH_TIME_ANDROID, + EGL_DISPLAY_PRESENT_TIME_ANDROID, + }; + + std::vector 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 frameTimestamps = + std::make_unique(); + 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 lock(mFenceWaiterLock); + + eglClientWaitSyncKHR = reinterpret_cast( + getProcAddress("eglClientWaitSyncKHR")); + if (eglClientWaitSyncKHR == nullptr) + ALOGE("Failed to load eglClientWaitSyncKHR"); + eglDestroySyncKHR = reinterpret_cast( + getProcAddress("eglDestroySyncKHR")); + if (eglDestroySyncKHR == nullptr) ALOGE("Failed to load eglDestroySyncKHR"); + + mFenceWaiter = Thread([this]() { threadMain(); }); +} + +EGL::FenceWaiter::~FenceWaiter() { + { + std::lock_guard lock(mFenceWaiterLock); + mFenceWaiterRunning = false; + mFenceWaiterCondition.notify_all(); + } + mFenceWaiter.join(); +} + +bool EGL::FenceWaiter::waitForIdle() { + std::lock_guard 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 lock(mFenceWaiterLock); + mDisplay = display; + mSyncFence = syncFence; + mFenceWaiterPending = true; + mFenceWaiterCondition.notify_all(); +} + +void EGL::FenceWaiter::threadMain() { + std::lock_guard 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 diff --git a/sources/Swappy/src/swappy/opengl/EGL.h b/sources/Swappy/src/swappy/opengl/EGL.h new file mode 100644 index 00000000..98fc7328 --- /dev/null +++ b/sources/Swappy/src/swappy/opengl/EGL.h @@ -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 +#include + +#include +#include +#include +#include +#include + +#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 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 getNextFrameId(EGLDisplay dpy, + EGLSurface surface) const; + std::unique_ptr 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 mFencePendingTime; + EGLDisplay mDisplay GUARDED_BY(mFenceWaiterLock); + EGLSyncKHR mSyncFence GUARDED_BY(mFenceWaiterLock) = EGL_NO_SYNC_KHR; + std::chrono::nanoseconds mFenceTimeout; + }; + + FenceWaiter mFenceWaiter; +}; + +} // namespace swappy diff --git a/sources/Swappy/src/swappy/opengl/FrameStatisticsGL.cpp b/sources/Swappy/src/swappy/opengl/FrameStatisticsGL.cpp new file mode 100644 index 00000000..f90d31d9 --- /dev/null +++ b/sources/Swappy/src/swappy/opengl/FrameStatisticsGL.cpp @@ -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 + +#include +#include + +#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(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 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 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 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 lock(mMutex); + return mStats; +} + +} // namespace swappy diff --git a/sources/Swappy/src/swappy/opengl/FrameStatisticsGL.h b/sources/Swappy/src/swappy/opengl/FrameStatisticsGL.h new file mode 100644 index 00000000..8e734133 --- /dev/null +++ b/sources/Swappy/src/swappy/opengl/FrameStatisticsGL.h @@ -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 +#include +#include +#include + +#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 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 mPendingFrames; + EGLnsecsANDROID mPrevFrameTime = 0; + std::atomic 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 diff --git a/sources/Swappy/src/swappy/opengl/SwappyGL.cpp b/sources/Swappy/src/swappy/opengl/SwappyGL.cpp new file mode 100644 index 00000000..1cea55ad --- /dev/null +++ b/sources/Swappy/src/swappy/opengl/SwappyGL.cpp @@ -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 +#include +#include + +#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::sInstance; + +bool SwappyGL::init(JNIEnv *env, jobject jactivity) { + std::lock_guard lock(sInstanceMutex); + if (sInstance) { + ALOGE("Attempted to initialize SwappyGL twice"); + return false; + } + sInstance = std::make_unique(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( + *swappy->mEgl, swappy->mCommonBase); + ALOGI("Enabling stats"); + } else { + ALOGI("Stats already enabled"); + } + } else { + swappy->mFrameStatistics = std::make_shared( + *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 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 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 lock(mEglMutex); + egl = mEgl.get(); + } + return egl; +} + +SwappyGL::SwappyGL(JNIEnv *env, jobject jactivity, ConstructorTag) + : mFrameStatistics(nullptr), mCommonBase(env, jactivity) { + { + std::lock_guard 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 diff --git a/sources/Swappy/src/swappy/opengl/SwappyGL.h b/sources/Swappy/src/swappy/opengl/SwappyGL.h new file mode 100644 index 00000000..169ed567 --- /dev/null +++ b/sources/Swappy/src/swappy/opengl/SwappyGL.h @@ -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 + +#include +#include + +#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 sInstance GUARDED_BY(sInstanceMutex); + + std::mutex mEglMutex; + std::unique_ptr mEgl; + + std::shared_ptr mFrameStatistics; + + SwappyCommon mCommonBase; +}; + +} // namespace swappy diff --git a/sources/Swappy/src/swappy/opengl/swappyGL_c.cpp b/sources/Swappy/src/swappy/opengl/swappyGL_c.cpp new file mode 100644 index 00000000..e0f89a63 --- /dev/null +++ b/sources/Swappy/src/swappy/opengl/swappyGL_c.cpp @@ -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 + +#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" { diff --git a/sources/Swappy/src/swappy/tools/lib_test_swappy.py b/sources/Swappy/src/swappy/tools/lib_test_swappy.py new file mode 100644 index 00000000..8759695b --- /dev/null +++ b/sources/Swappy/src/swappy/tools/lib_test_swappy.py @@ -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 - /' + # 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