swappy
This commit is contained in:
parent
973e244526
commit
2620f24c94
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
include ../../src/swappy/OWNERS
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup swappyGL Swappy for OpenGL
|
||||
* OpenGL part of Swappy.
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <jni.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "swappy_common.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Initialize Swappy, getting the required Android parameters from the
|
||||
* display subsystem via JNI.
|
||||
* @param env The JNI environment where Swappy is used
|
||||
* @param jactivity The activity where Swappy is used
|
||||
* @return false if Swappy failed to initialize.
|
||||
* @see SwappyGL_destroy
|
||||
*/
|
||||
bool SwappyGL_init(JNIEnv *env, jobject jactivity);
|
||||
|
||||
/**
|
||||
* @brief Check if Swappy was successfully initialized.
|
||||
* @return false if either the `swappy.disable` system property is not `false`
|
||||
* or the required OpenGL extensions are not available for Swappy to work.
|
||||
*/
|
||||
bool SwappyGL_isEnabled();
|
||||
|
||||
/**
|
||||
* @brief Destroy resources and stop all threads that Swappy has created.
|
||||
* @see SwappyGL_init
|
||||
*/
|
||||
void SwappyGL_destroy();
|
||||
|
||||
/**
|
||||
* @brief Tell Swappy which ANativeWindow to use when calling to ANativeWindow_*
|
||||
* API.
|
||||
* @param window ANativeWindow that was used to create the EGLSurface.
|
||||
* @return true on success, false if Swappy was not initialized.
|
||||
*/
|
||||
bool SwappyGL_setWindow(ANativeWindow *window);
|
||||
|
||||
/**
|
||||
* @brief Replace calls to eglSwapBuffers with this. Swappy will wait for the
|
||||
* previous frame's buffer to be processed by the GPU before actually calling
|
||||
* eglSwapBuffers.
|
||||
* @return true on success or false if
|
||||
* 1) Swappy is not initialized or 2) eglSwapBuffers did not return EGL_TRUE.
|
||||
* In the latter case, eglGetError can be used to get the error code.
|
||||
*/
|
||||
bool SwappyGL_swap(EGLDisplay display, EGLSurface surface);
|
||||
|
||||
// Paramter setters:
|
||||
|
||||
void SwappyGL_setUseAffinity(bool tf);
|
||||
|
||||
/**
|
||||
* @brief Override the swap interval
|
||||
*
|
||||
* By default, Swappy will adjust the swap interval based on actual frame
|
||||
* rendering time.
|
||||
*
|
||||
* If an app wants to override the swap interval calculated by Swappy, it can
|
||||
* call this function:
|
||||
*
|
||||
* * This will temporarily override Swappy's frame timings but, unless
|
||||
* `SwappyGL_setAutoSwapInterval(false)` is called, the timings will continue
|
||||
* to be be updated dynamically, so the swap interval may change.
|
||||
*
|
||||
* * This set the **minimal** interval to run. For example,
|
||||
* `SwappyGL_setSwapIntervalNS(SWAPPY_SWAP_30FPS)` will not allow Swappy to swap
|
||||
* faster, even if auto mode decides that it can. But it can go slower if auto
|
||||
* mode is on.
|
||||
*
|
||||
* @param swap_ns The new swap interval value, in nanoseconds.
|
||||
*/
|
||||
void SwappyGL_setSwapIntervalNS(uint64_t swap_ns);
|
||||
|
||||
/**
|
||||
* @brief Set the fence timeout parameter, for devices with faulty
|
||||
* drivers. Its default value is 50,000,000ns (50ms).
|
||||
*/
|
||||
void SwappyGL_setFenceTimeoutNS(uint64_t fence_timeout_ns);
|
||||
|
||||
// Parameter getters:
|
||||
|
||||
/**
|
||||
* @brief Get the refresh period value, in nanoseconds.
|
||||
*/
|
||||
uint64_t SwappyGL_getRefreshPeriodNanos();
|
||||
|
||||
/**
|
||||
* @brief Get the swap interval value, in nanoseconds.
|
||||
*/
|
||||
uint64_t SwappyGL_getSwapIntervalNS();
|
||||
|
||||
bool SwappyGL_getUseAffinity();
|
||||
|
||||
/**
|
||||
* @brief Get the fence timeout value, in nanoseconds.
|
||||
*/
|
||||
uint64_t SwappyGL_getFenceTimeoutNS();
|
||||
|
||||
/**
|
||||
* @brief Set the number of bad frames to wait before applying a fix for buffer
|
||||
* stuffing. Set to zero in order to turn off this feature. Default value = 0.
|
||||
*/
|
||||
void SwappyGL_setBufferStuffingFixWait(int32_t n_frames);
|
||||
|
||||
#ifdef __cplusplus
|
||||
};
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup swappyGL_extra Swappy for OpenGL extras
|
||||
* Extra utility functions to use Swappy with OpenGL.
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <jni.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "swappy_common.h"
|
||||
|
||||
/**
|
||||
* The longest duration, in refresh periods, represented by the statistics.
|
||||
* @see SwappyStats
|
||||
*/
|
||||
#define MAX_FRAME_BUCKETS 6
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief If an app wishes to use the Android choreographer to provide ticks to
|
||||
* Swappy, it can call this function.
|
||||
*
|
||||
* @warning This function *must* be called before the first `Swappy_swap()`
|
||||
* call. Afterwards, call this function every choreographer tick.
|
||||
*/
|
||||
void SwappyGL_onChoreographer(int64_t frameTimeNanos);
|
||||
|
||||
/** @brief Pass callbacks to be called each frame to trace execution. */
|
||||
void SwappyGL_injectTracer(const SwappyTracer *t);
|
||||
|
||||
/**
|
||||
* @brief Toggle auto-swap interval detection on/off
|
||||
*
|
||||
* By default, Swappy will adjust the swap interval based on actual frame
|
||||
* rendering time. If an app wants to override the swap interval calculated by
|
||||
* Swappy, it can call `SwappyGL_setSwapIntervalNS`. This will temporarily
|
||||
* override Swappy's frame timings but, unless
|
||||
* `SwappyGL_setAutoSwapInterval(false)` is called, the timings will continue to
|
||||
* be be updated dynamically, so the swap interval may change.
|
||||
*/
|
||||
void SwappyGL_setAutoSwapInterval(bool enabled);
|
||||
|
||||
/**
|
||||
* @brief Sets the maximal duration for auto-swap interval in milliseconds.
|
||||
*
|
||||
* If Swappy is operating in auto-swap interval and the frame duration is longer
|
||||
* than `max_swap_ns`, Swappy will not do any pacing and just submit the frame
|
||||
* as soon as possible.
|
||||
*/
|
||||
void SwappyGL_setMaxAutoSwapIntervalNS(uint64_t max_swap_ns);
|
||||
|
||||
/**
|
||||
* @brief Toggle auto-pipeline mode on/off
|
||||
*
|
||||
* By default, if auto-swap interval is on, auto-pipelining is on and Swappy
|
||||
* will try to reduce latency by scheduling cpu and gpu work in the same
|
||||
* pipeline stage, if it fits.
|
||||
*/
|
||||
void SwappyGL_setAutoPipelineMode(bool enabled);
|
||||
|
||||
/**
|
||||
* @brief Toggle statistics collection on/off
|
||||
*
|
||||
* By default, stats collection is off and there is no overhead related to
|
||||
* stats. An app can turn on stats collection by calling
|
||||
* `SwappyGL_enableStats(true)`. Then, the app is expected to call
|
||||
* ::SwappyGL_recordFrameStart for each frame before starting to do any CPU
|
||||
* related work. Stats will be logged to logcat with a 'FrameStatistics' tag. An
|
||||
* app can get the stats by calling ::SwappyGL_getStats.
|
||||
*/
|
||||
void SwappyGL_enableStats(bool enabled);
|
||||
|
||||
/**
|
||||
* @brief Swappy statistics, collected if toggled on with
|
||||
* ::SwappyGL_enableStats.
|
||||
* @see SwappyGL_getStats
|
||||
*/
|
||||
struct SwappyStats {
|
||||
/** @brief Total frames swapped by swappy */
|
||||
uint64_t totalFrames;
|
||||
|
||||
/** @brief Histogram of the number of screen refreshes a frame waited in the
|
||||
* compositor queue after rendering was completed.
|
||||
*
|
||||
* For example:
|
||||
* if a frame waited 2 refresh periods in the compositor queue after
|
||||
* rendering was done, the frame will be counted in idleFrames[2]
|
||||
*/
|
||||
uint64_t idleFrames[MAX_FRAME_BUCKETS];
|
||||
|
||||
/** @brief Histogram of the number of screen refreshes passed between the
|
||||
* requested presentation time and the actual present time.
|
||||
*
|
||||
* For example:
|
||||
* if a frame was presented 2 refresh periods after the requested
|
||||
* timestamp swappy set, the frame will be counted in lateFrames[2]
|
||||
*/
|
||||
uint64_t lateFrames[MAX_FRAME_BUCKETS];
|
||||
|
||||
/** @brief Histogram of the number of screen refreshes passed between two
|
||||
* consecutive frames
|
||||
*
|
||||
* For example:
|
||||
* if frame N was presented 2 refresh periods after frame N-1
|
||||
* frame N will be counted in offsetFromPreviousFrame[2]
|
||||
*/
|
||||
uint64_t offsetFromPreviousFrame[MAX_FRAME_BUCKETS];
|
||||
|
||||
/** @brief Histogram of the number of screen refreshes passed between the
|
||||
* call to Swappy_recordFrameStart and the actual present time.
|
||||
*
|
||||
* For example:
|
||||
* if a frame was presented 2 refresh periods after the call to
|
||||
* `Swappy_recordFrameStart` the frame will be counted in latencyFrames[2]
|
||||
*/
|
||||
uint64_t latencyFrames[MAX_FRAME_BUCKETS];
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Should be called if stats have been enabled with SwappyGL_enableStats.
|
||||
*
|
||||
* When stats collection is enabled with SwappyGL_enableStats, the app is
|
||||
* expected to call this function for each frame before starting to do any CPU
|
||||
* related work.
|
||||
*
|
||||
* @see SwappyGL_enableStats.
|
||||
*/
|
||||
void SwappyGL_recordFrameStart(EGLDisplay display, EGLSurface surface);
|
||||
|
||||
/**
|
||||
* @brief Returns the stats collected, if statistics collection was toggled on.
|
||||
*
|
||||
* @param swappyStats Pointer to a SwappyStats that will be populated with
|
||||
* collected stats.
|
||||
* @see SwappyStats
|
||||
* @see SwappyGL_enableStats
|
||||
*/
|
||||
void SwappyGL_getStats(SwappyStats *swappyStats);
|
||||
|
||||
/** @brief Remove callbacks that were previously added using
|
||||
* SwappyGL_injectTracer. */
|
||||
void SwappyGL_uninjectTracer(const SwappyTracer *t);
|
||||
|
||||
#ifdef __cplusplus
|
||||
};
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup swappyVk Swappy for Vulkan
|
||||
* Vulkan part of Swappy.
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "jni.h"
|
||||
#include "swappy_common.h"
|
||||
|
||||
#define VK_NO_PROTOTYPES 1
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Determine any Vulkan device extensions that must be enabled for a new
|
||||
* VkDevice.
|
||||
*
|
||||
* Swappy-for-Vulkan (SwappyVk) benefits from certain Vulkan device extensions
|
||||
* (e.g. VK_GOOGLE_display_timing). Before the application calls
|
||||
* vkCreateDevice, SwappyVk needs to look at the list of available extensions
|
||||
* (returned by vkEnumerateDeviceExtensionProperties) and potentially identify
|
||||
* one or more extensions that the application must add to:
|
||||
*
|
||||
* - VkDeviceCreateInfo::enabledExtensionCount
|
||||
* - VkDeviceCreateInfo::ppEnabledExtensionNames
|
||||
*
|
||||
* before the application calls vkCreateDevice. For each VkPhysicalDevice that
|
||||
* the application will call vkCreateDevice for, the application must call this
|
||||
* function, and then must add the identified extension(s) to the list that are
|
||||
* enabled for the VkDevice. Similar to many Vulkan functions, this function
|
||||
* can be called twice, once to identify the number of required extensions, and
|
||||
* again with application-allocated memory that the function can write into.
|
||||
*
|
||||
* @param[in] physicalDevice - The VkPhysicalDevice associated with
|
||||
* the available extensions.
|
||||
* @param[in] availableExtensionCount - This is the returned value of
|
||||
* pPropertyCount from vkEnumerateDeviceExtensionProperties.
|
||||
* @param[in] pAvailableExtensions - This is the returned value of
|
||||
* pProperties from vkEnumerateDeviceExtensionProperties.
|
||||
* @param[inout] pRequiredExtensionCount - If pRequiredExtensions is nullptr,
|
||||
* the function sets this to the number of extensions that are required. If
|
||||
* pRequiredExtensions is non-nullptr, this is the number of required extensions
|
||||
* that the function should write into pRequiredExtensions.
|
||||
* @param[inout] pRequiredExtensions - If non-nullptr, this is
|
||||
* application-allocated memory into which the function will write the names of
|
||||
* required extensions. It is a pointer to an array of
|
||||
* char* strings (i.e. the same as
|
||||
* VkDeviceCreateInfo::ppEnabledExtensionNames).
|
||||
*/
|
||||
void SwappyVk_determineDeviceExtensions(
|
||||
VkPhysicalDevice physicalDevice, uint32_t availableExtensionCount,
|
||||
VkExtensionProperties* pAvailableExtensions,
|
||||
uint32_t* pRequiredExtensionCount, char** pRequiredExtensions);
|
||||
|
||||
/**
|
||||
* @brief Tell Swappy the queueFamilyIndex used to create a specific VkQueue
|
||||
*
|
||||
* Swappy needs to know the queueFamilyIndex used for creating a specific
|
||||
* VkQueue so it can use it when presenting.
|
||||
*
|
||||
* @param[in] device - The VkDevice associated with the queue
|
||||
* @param[in] queue - A device queue.
|
||||
* @param[in] queueFamilyIndex - The queue family index used to create the
|
||||
* VkQueue.
|
||||
*
|
||||
*/
|
||||
void SwappyVk_setQueueFamilyIndex(VkDevice device, VkQueue queue,
|
||||
uint32_t queueFamilyIndex);
|
||||
|
||||
// TBD: For now, SwappyVk assumes only one VkSwapchainKHR per VkDevice, and that
|
||||
// applications don't re-create swapchains. Is this long-term sufficient?
|
||||
|
||||
/**
|
||||
* Internal init function. Do not call directly.
|
||||
* See SwappyVk_initAndGetRefreshCycleDuration instead.
|
||||
* @private
|
||||
*/
|
||||
bool SwappyVk_initAndGetRefreshCycleDuration_internal(
|
||||
JNIEnv* env, jobject jactivity, VkPhysicalDevice physicalDevice,
|
||||
VkDevice device, VkSwapchainKHR swapchain, uint64_t* pRefreshDuration);
|
||||
|
||||
/**
|
||||
* @brief Initialize SwappyVk for a given device and swapchain, and obtain the
|
||||
* approximate time duration between vertical-blanking periods.
|
||||
*
|
||||
* Uses JNI to query AppVsyncOffset and PresentationDeadline.
|
||||
*
|
||||
* If your application presents to more than one swapchain at a time, you must
|
||||
* call this for each swapchain before calling swappyVkSetSwapInterval() for it.
|
||||
*
|
||||
* The duration between vertical-blanking periods (an interval) is expressed as
|
||||
* the approximate number of nanoseconds between vertical-blanking periods of
|
||||
* the 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
|
||||
|
||||
/** @} */
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup swappy_common Swappy common tools
|
||||
* Tools to be used with Swappy for OpenGL or Swappy for Vulkan.
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <android/native_window.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "common/gamesdk_common.h"
|
||||
|
||||
/** @brief Swap interval for 60fps, in nanoseconds. */
|
||||
#define SWAPPY_SWAP_60FPS (16666667L)
|
||||
|
||||
/** @brief Swap interval for 30fps, in nanoseconds. */
|
||||
#define SWAPPY_SWAP_30FPS (33333333L)
|
||||
|
||||
/** @brief Swap interval for 20fps, in nanoseconds. */
|
||||
#define SWAPPY_SWAP_20FPS (50000000L)
|
||||
|
||||
/** @cond INTERNAL */
|
||||
|
||||
#define SWAPPY_SYSTEM_PROP_KEY_DISABLE "swappy.disable"
|
||||
|
||||
// Internal macros to track Swappy version, do not use directly.
|
||||
#define SWAPPY_MAJOR_VERSION 1
|
||||
#define SWAPPY_MINOR_VERSION 10
|
||||
#define SWAPPY_BUGFIX_VERSION 0
|
||||
#define SWAPPY_PACKED_VERSION \
|
||||
ANDROID_GAMESDK_PACKED_VERSION(SWAPPY_MAJOR_VERSION, SWAPPY_MINOR_VERSION, \
|
||||
SWAPPY_BUGFIX_VERSION)
|
||||
|
||||
// Internal macros to generate a symbol to track Swappy version, do not use
|
||||
// directly.
|
||||
#define SWAPPY_VERSION_CONCAT_NX(PREFIX, MAJOR, MINOR, BUGFIX, GITCOMMIT) \
|
||||
PREFIX##_##MAJOR##_##MINOR##_##BUGFIX##_##GITCOMMIT
|
||||
#define SWAPPY_VERSION_CONCAT(PREFIX, MAJOR, MINOR, BUGFIX, GITCOMMIT) \
|
||||
SWAPPY_VERSION_CONCAT_NX(PREFIX, MAJOR, MINOR, BUGFIX, GITCOMMIT)
|
||||
#define SWAPPY_VERSION_SYMBOL \
|
||||
SWAPPY_VERSION_CONCAT(Swappy_version, SWAPPY_MAJOR_VERSION, \
|
||||
SWAPPY_MINOR_VERSION, SWAPPY_BUGFIX_VERSION, \
|
||||
AGDK_GIT_COMMIT)
|
||||
|
||||
/** @endcond */
|
||||
|
||||
/** @brief Id of a thread returned by an external thread manager. */
|
||||
typedef uint64_t SwappyThreadId;
|
||||
|
||||
/**
|
||||
* @brief A structure enabling you to set how Swappy starts and joins threads by
|
||||
* calling
|
||||
* ::Swappy_setThreadFunctions.
|
||||
*
|
||||
* Usage of this functionality is optional.
|
||||
*/
|
||||
typedef struct SwappyThreadFunctions {
|
||||
/** @brief Thread start callback.
|
||||
*
|
||||
* This function is called by Swappy to start thread_func on a new thread.
|
||||
* @param user_data A value to be passed the thread function.
|
||||
* If the thread was started, this function should set the thread_id and
|
||||
* return 0. If the thread was not started, this function should return a
|
||||
* non-zero value.
|
||||
*/
|
||||
int (*start)(SwappyThreadId* thread_id, void* (*thread_func)(void*),
|
||||
void* user_data);
|
||||
|
||||
/** @brief Thread join callback.
|
||||
*
|
||||
* This function is called by Swappy to join the thread with given id.
|
||||
*/
|
||||
void (*join)(SwappyThreadId thread_id);
|
||||
|
||||
/** @brief Thread joinable callback.
|
||||
*
|
||||
* This function is called by Swappy to discover whether the thread with the
|
||||
* given id is joinable.
|
||||
*/
|
||||
bool (*joinable)(SwappyThreadId thread_id);
|
||||
} SwappyThreadFunctions;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Return the version of the Swappy library at runtime.
|
||||
*/
|
||||
uint32_t Swappy_version();
|
||||
|
||||
/**
|
||||
* @brief Call this before any other functions in order to use a custom thread
|
||||
* manager.
|
||||
*
|
||||
* Usage of this function is entirely optional. Swappy uses std::thread by
|
||||
* default.
|
||||
*
|
||||
*/
|
||||
void Swappy_setThreadFunctions(const SwappyThreadFunctions* thread_functions);
|
||||
|
||||
/**
|
||||
* @brief Return the full version of the Swappy library at runtime, e.g.
|
||||
* "1.9.0_8a85ab7c46"
|
||||
*/
|
||||
const char* Swappy_versionString();
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Pointer to a function that can be attached to SwappyTracer::preWait
|
||||
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
|
||||
*/
|
||||
typedef void (*SwappyPreWaitCallback)(void*);
|
||||
|
||||
/**
|
||||
* Pointer to a function that can be attached to SwappyTracer::postWait.
|
||||
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
|
||||
* @param cpu_time_ns Time for CPU processing of this frame in nanoseconds.
|
||||
* @param gpu_time_ns Time for GPU processing of previous frame in nanoseconds.
|
||||
*/
|
||||
typedef void (*SwappyPostWaitCallback)(void*, int64_t cpu_time_ns,
|
||||
int64_t gpu_time_ns);
|
||||
|
||||
/**
|
||||
* Pointer to a function that can be attached to SwappyTracer::preSwapBuffers.
|
||||
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
|
||||
*/
|
||||
typedef void (*SwappyPreSwapBuffersCallback)(void*);
|
||||
|
||||
/**
|
||||
* Pointer to a function that can be attached to SwappyTracer::postSwapBuffers.
|
||||
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
|
||||
* @param desiredPresentationTimeMillis The target time, in milliseconds, at
|
||||
* which the frame would be presented on screen.
|
||||
*/
|
||||
typedef void (*SwappyPostSwapBuffersCallback)(
|
||||
void*, int64_t desiredPresentationTimeMillis);
|
||||
|
||||
/**
|
||||
* Pointer to a function that can be attached to SwappyTracer::startFrame.
|
||||
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
|
||||
* @param desiredPresentationTimeMillis The time, in milliseconds, at which the
|
||||
* frame is scheduled to be presented.
|
||||
*/
|
||||
typedef void (*SwappyStartFrameCallback)(void*, int currentFrame,
|
||||
int64_t desiredPresentationTimeMillis);
|
||||
|
||||
/**
|
||||
* Pointer to a function that can be attached to
|
||||
* SwappyTracer::swapIntervalChanged. Call ::SwappyGL_getSwapIntervalNS or
|
||||
* ::SwappyVk_getSwapIntervalNS to get the latest swapInterval.
|
||||
* @param userData Pointer to arbitrary data, see SwappyTracer::userData.
|
||||
*/
|
||||
typedef void (*SwappySwapIntervalChangedCallback)(void*);
|
||||
|
||||
/**
|
||||
* @brief Collection of callbacks to be called each frame to trace execution.
|
||||
*
|
||||
* Injection of these is optional.
|
||||
*/
|
||||
typedef struct SwappyTracer {
|
||||
/**
|
||||
* Callback called before waiting to queue the frame to the composer.
|
||||
*/
|
||||
SwappyPreWaitCallback preWait;
|
||||
|
||||
/**
|
||||
* Callback called after wait to queue the frame to the composer is done.
|
||||
*/
|
||||
SwappyPostWaitCallback postWait;
|
||||
|
||||
/**
|
||||
* Callback called before calling the function to queue the frame to the
|
||||
* composer.
|
||||
*/
|
||||
SwappyPreSwapBuffersCallback preSwapBuffers;
|
||||
|
||||
/**
|
||||
* Callback called after calling the function to queue the frame to the
|
||||
* composer.
|
||||
*/
|
||||
SwappyPostSwapBuffersCallback postSwapBuffers;
|
||||
|
||||
/**
|
||||
* Callback called at the start of a frame.
|
||||
*/
|
||||
SwappyStartFrameCallback startFrame;
|
||||
|
||||
/**
|
||||
* Pointer to some arbitrary data that will be passed as the first argument
|
||||
* of callbacks.
|
||||
*/
|
||||
void* userData;
|
||||
|
||||
/**
|
||||
* Callback called when the swap interval was changed.
|
||||
*/
|
||||
SwappySwapIntervalChangedCallback swapIntervalChanged;
|
||||
} SwappyTracer;
|
||||
|
||||
/** @} */
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <android/choreographer.h>
|
||||
|
||||
// Declare the types, even if we can't access the functions.
|
||||
#if __ANDROID_API__ < 24
|
||||
|
||||
struct AChoreographer;
|
||||
typedef struct AChoreographer AChoreographer;
|
||||
|
||||
/**
|
||||
* Prototype of the function that is called when a new frame is being rendered.
|
||||
* It's passed the time that the frame is being rendered as nanoseconds in the
|
||||
* CLOCK_MONOTONIC time base, as well as the data pointer provided by the
|
||||
* application that registered a callback. All callbacks that run as part of
|
||||
* rendering a frame will observe the same frame time, so it should be used
|
||||
* whenever events need to be synchronized (e.g. animations).
|
||||
*/
|
||||
typedef void (*AChoreographer_frameCallback)(long frameTimeNanos, void* data);
|
||||
|
||||
#endif // __ANDROID_API__ < 24
|
||||
|
||||
#if __ANDROID_API__ < 30
|
||||
|
||||
/**
|
||||
* Prototype of the function that is called when the display refresh rate
|
||||
* changes. It's passed the new vsync period in nanoseconds, as well as the data
|
||||
* pointer provided by the application that registered a callback.
|
||||
*/
|
||||
typedef void (*AChoreographer_refreshRateCallback)(int64_t vsyncPeriodNanos,
|
||||
void* data);
|
||||
|
||||
#endif // __ANDROID_API__ < 30
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "Log.h"
|
||||
|
||||
// The code in this file will dynamically load Java classes from a binary
|
||||
// resource linked to the library. The binary data is in DEX format, accessible
|
||||
// using the _binary_classes_dex_start and _binary_classes_dex_end linker
|
||||
// symbols.
|
||||
|
||||
// If you make AGDK classes available on the Java classpath, e.g. by adding them
|
||||
// to your game/engine's own Java component, you do not need to add the binary
|
||||
// resource and can instead define ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE which
|
||||
// will avoid the linker requiring these symbols.
|
||||
#define ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
|
||||
#ifndef ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
|
||||
extern const char _binary_classes_dex_start;
|
||||
extern const char _binary_classes_dex_end;
|
||||
#endif
|
||||
|
||||
namespace gamesdk {
|
||||
|
||||
#ifndef ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
|
||||
|
||||
static bool saveBytesToFile(std::string fileName, const char* bytes,
|
||||
size_t size) {
|
||||
std::ofstream save_file(fileName, std::ios::binary);
|
||||
if (save_file.good()) {
|
||||
save_file.write(bytes, size);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool deleteFile(std::string fileName) {
|
||||
if (remove(fileName.c_str()) != 0)
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool createTempFile(JNIEnv* env, jobject activity, const char* ext,
|
||||
std::string& tempFileName) {
|
||||
bool result = false;
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
jmethodID getCacheDir =
|
||||
env->GetMethodID(activityClass, "getCacheDir", "()Ljava/io/File;");
|
||||
jobject cacheDir = env->CallObjectMethod(activity, getCacheDir);
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
} else {
|
||||
jclass fileClass = env->FindClass("java/io/File");
|
||||
jmethodID createTempFile =
|
||||
env->GetStaticMethodID(fileClass, "createTempFile",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/"
|
||||
"io/File;)Ljava/io/File;");
|
||||
jstring prefix = env->NewStringUTF("ags");
|
||||
jstring suffix = env->NewStringUTF(ext);
|
||||
jobject tempFile = env->CallStaticObjectMethod(
|
||||
fileClass, createTempFile, prefix, suffix, cacheDir);
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
} else {
|
||||
jmethodID getPath =
|
||||
env->GetMethodID(fileClass, "getPath", "()Ljava/lang/String;");
|
||||
jstring pathString =
|
||||
(jstring)env->CallObjectMethod(tempFile, getPath);
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
} else {
|
||||
const char* path = env->GetStringUTFChars(pathString, NULL);
|
||||
tempFileName.assign(path);
|
||||
env->ReleaseStringUTFChars(pathString, path);
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
env->DeleteLocalRef(prefix);
|
||||
env->DeleteLocalRef(suffix);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif // #ifndef ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
|
||||
|
||||
static jclass loadClass(JNIEnv* env, jobject activity, const char* name,
|
||||
JNINativeMethod* nativeMethods,
|
||||
size_t nativeMethodsSize) {
|
||||
/*
|
||||
* 1. Get a classloader from actvity
|
||||
* 2. Try to create the requested class from the activty classloader
|
||||
* 3. If step 2 not successful then get a classloder for dex bytes (in
|
||||
* memory or file)
|
||||
* 4. If step 3 is successful then register native methods
|
||||
*/
|
||||
if (!env || !activity || !name) {
|
||||
return nullptr;
|
||||
}
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
|
||||
jmethodID getClassLoader = env->GetMethodID(activityClass, "getClassLoader",
|
||||
"()Ljava/lang/ClassLoader;");
|
||||
jobject classLoaderObj = env->CallObjectMethod(activity, getClassLoader);
|
||||
jmethodID loadClass = env->GetMethodID(
|
||||
classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
||||
jstring className = env->NewStringUTF(name);
|
||||
jclass targetClass = static_cast<jclass>(
|
||||
env->CallObjectMethod(classLoaderObj, loadClass, className));
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionClear();
|
||||
|
||||
#ifdef ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
|
||||
ALOGE(
|
||||
"Couldn't find class %s and ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE "
|
||||
"is defined",
|
||||
name);
|
||||
#else
|
||||
jstring dexLoaderClassName =
|
||||
env->NewStringUTF("dalvik/system/InMemoryDexClassLoader");
|
||||
jclass imclassloaderClass = static_cast<jclass>(env->CallObjectMethod(
|
||||
classLoaderObj, loadClass, dexLoaderClassName));
|
||||
env->DeleteLocalRef(dexLoaderClassName);
|
||||
|
||||
if (env->ExceptionCheck() || !imclassloaderClass) {
|
||||
env->ExceptionClear();
|
||||
// For older SDK versions <26, where InMemoryDexClassLoader is not
|
||||
// available
|
||||
dexLoaderClassName =
|
||||
env->NewStringUTF("dalvik/system/PathClassLoader");
|
||||
imclassloaderClass = static_cast<jclass>(env->CallObjectMethod(
|
||||
classLoaderObj, loadClass, dexLoaderClassName));
|
||||
env->DeleteLocalRef(dexLoaderClassName);
|
||||
if (env->ExceptionCheck() || !imclassloaderClass) {
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
ALOGE("Unable to find dalvik/system/PathClassLoader.");
|
||||
targetClass = nullptr;
|
||||
} else {
|
||||
jmethodID constructor = env->GetMethodID(
|
||||
imclassloaderClass, "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/ClassLoader;)V");
|
||||
std::string tempPath;
|
||||
if (!createTempFile(env, activity, ".dex", tempPath)) {
|
||||
ALOGE(
|
||||
"Unable to create a temporary file to store DEX with "
|
||||
"Java classes.");
|
||||
} else {
|
||||
size_t dex_file_size = (size_t)(&_binary_classes_dex_end -
|
||||
&_binary_classes_dex_start);
|
||||
if (!saveBytesToFile(tempPath, &_binary_classes_dex_start,
|
||||
dex_file_size)) {
|
||||
ALOGE("Unable to write to %s file.", tempPath.c_str());
|
||||
} else {
|
||||
jstring dexPathString =
|
||||
env->NewStringUTF(tempPath.c_str());
|
||||
jobject imclassloaderObj =
|
||||
env->NewObject(imclassloaderClass, constructor,
|
||||
dexPathString, classLoaderObj);
|
||||
env->DeleteLocalRef(dexPathString);
|
||||
targetClass = static_cast<jclass>(env->CallObjectMethod(
|
||||
imclassloaderObj, loadClass, className));
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
ALOGE("Unable to find %s class", name);
|
||||
} else {
|
||||
env->RegisterNatives(targetClass, nativeMethods,
|
||||
nativeMethodsSize);
|
||||
ALOGI("Using internal %s class from dex bytes.",
|
||||
name);
|
||||
}
|
||||
if (imclassloaderObj) {
|
||||
env->DeleteLocalRef(imclassloaderObj);
|
||||
}
|
||||
}
|
||||
deleteFile(tempPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
jmethodID constructor = env->GetMethodID(
|
||||
imclassloaderClass, "<init>",
|
||||
"(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V");
|
||||
|
||||
size_t dex_file_size =
|
||||
(size_t)(&_binary_classes_dex_end - &_binary_classes_dex_start);
|
||||
auto byteBuffer = env->NewDirectByteBuffer(
|
||||
(void*)&_binary_classes_dex_start, dex_file_size);
|
||||
jobject imclassloaderObj = env->NewObject(
|
||||
imclassloaderClass, constructor, byteBuffer, classLoaderObj);
|
||||
|
||||
targetClass = static_cast<jclass>(
|
||||
env->CallObjectMethod(imclassloaderObj, loadClass, className));
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
ALOGE("Unable to find %s class", name);
|
||||
} else {
|
||||
env->RegisterNatives(targetClass, nativeMethods,
|
||||
nativeMethodsSize);
|
||||
ALOGI("Using internal %s class from dex bytes.", name);
|
||||
}
|
||||
if (imclassloaderObj) {
|
||||
env->DeleteLocalRef(imclassloaderObj);
|
||||
}
|
||||
}
|
||||
if (imclassloaderClass) {
|
||||
env->DeleteLocalRef(imclassloaderClass);
|
||||
}
|
||||
#endif // #ifdef ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE
|
||||
}
|
||||
env->DeleteLocalRef(className);
|
||||
return targetClass;
|
||||
}
|
||||
|
||||
} // namespace gamesdk
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
|
||||
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define ALOGW_ONCE_IF(cond, ...) \
|
||||
do { \
|
||||
static bool alogw_once##__FILE__##__LINE__##__ = true; \
|
||||
if (cond && alogw_once##__FILE__##__LINE__##__) { \
|
||||
alogw_once##__FILE__##__LINE__##__ = false; \
|
||||
ALOGW(__VA_ARGS__); \
|
||||
} \
|
||||
} while (0)
|
||||
#define ALOGE_ONCE(...) \
|
||||
do { \
|
||||
static bool aloge_once##__FILE__##__LINE__##__ = true; \
|
||||
if (aloge_once##__FILE__##__LINE__##__) { \
|
||||
aloge_once##__FILE__##__LINE__##__ = false; \
|
||||
ALOGE(__VA_ARGS__); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define ALOGV(...) \
|
||||
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
|
||||
#else
|
||||
#define ALOGV(...)
|
||||
#endif
|
||||
|
||||
namespace swappy {
|
||||
|
||||
std::string to_string(int value);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
// These string functions can be needed when compiling using GNU STL.
|
||||
|
||||
#if (defined ANDROID_GNUSTL) || \
|
||||
((defined ANDROID_NDK_VERSION) && ANDROID_NDK_VERSION <= 17)
|
||||
|
||||
namespace std {
|
||||
|
||||
template <typename T>
|
||||
std::string to_string(T value) {
|
||||
std::stringstream os;
|
||||
os << value;
|
||||
return os.str();
|
||||
}
|
||||
template <typename T>
|
||||
std::wstring to_wstring(T value) {
|
||||
std::wstringstream os;
|
||||
os << value;
|
||||
return os.str();
|
||||
}
|
||||
|
||||
} // namespace std
|
||||
|
||||
#endif
|
||||
|
||||
#if (defined ANDROID_GNUSTL)
|
||||
|
||||
namespace std {
|
||||
|
||||
long double stold(const std::string& str, std::size_t* pos = nullptr) {
|
||||
long double d;
|
||||
std::stringstream is(str);
|
||||
auto p0 = is.tellg();
|
||||
is >> d;
|
||||
if (pos != nullptr) {
|
||||
*pos = is.tellg() - p0;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
} // namespace std
|
||||
#endif
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <android/log.h>
|
||||
#include <android/trace.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace gamesdk {
|
||||
|
||||
class Trace {
|
||||
public:
|
||||
using ATrace_beginSection_type = void (*)(const char *sectionName);
|
||||
using ATrace_endSection_type = void (*)();
|
||||
using ATrace_isEnabled_type = bool (*)();
|
||||
using ATrace_setCounter_type = void (*)(const char *counterName,
|
||||
int64_t counterValue);
|
||||
|
||||
Trace() {
|
||||
__android_log_print(ANDROID_LOG_INFO, "Trace",
|
||||
"Unable to load NDK tracing APIs");
|
||||
}
|
||||
|
||||
Trace(ATrace_beginSection_type beginSection,
|
||||
ATrace_endSection_type endSection, ATrace_isEnabled_type isEnabled,
|
||||
ATrace_setCounter_type setCounter)
|
||||
: ATrace_beginSection(beginSection),
|
||||
ATrace_endSection(endSection),
|
||||
ATrace_isEnabled(isEnabled),
|
||||
ATrace_setCounter(setCounter) {}
|
||||
|
||||
static std::unique_ptr<Trace> create() {
|
||||
void *libandroid = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (!libandroid) {
|
||||
return std::make_unique<Trace>();
|
||||
}
|
||||
|
||||
auto beginSection = reinterpret_cast<ATrace_beginSection_type>(
|
||||
dlsym(libandroid, "ATrace_beginSection"));
|
||||
if (!beginSection) {
|
||||
return std::make_unique<Trace>();
|
||||
}
|
||||
|
||||
auto endSection = reinterpret_cast<ATrace_endSection_type>(
|
||||
dlsym(libandroid, "ATrace_endSection"));
|
||||
if (!endSection) {
|
||||
return std::make_unique<Trace>();
|
||||
}
|
||||
|
||||
auto isEnabled = reinterpret_cast<ATrace_isEnabled_type>(
|
||||
dlsym(libandroid, "ATrace_isEnabled"));
|
||||
if (!isEnabled) {
|
||||
return std::make_unique<Trace>();
|
||||
}
|
||||
|
||||
auto setCounter = reinterpret_cast<ATrace_setCounter_type>(
|
||||
dlsym(libandroid, "ATrace_setCounter"));
|
||||
/* ATrace_setCounter was added in API 29, continue even if it is not
|
||||
* available */
|
||||
|
||||
return std::make_unique<Trace>(beginSection, endSection, isEnabled,
|
||||
setCounter);
|
||||
}
|
||||
|
||||
bool isAvailable() const { return ATrace_beginSection != nullptr; }
|
||||
|
||||
bool isEnabled() const {
|
||||
return (ATrace_isEnabled != nullptr) && ATrace_isEnabled();
|
||||
}
|
||||
|
||||
void beginSection(const char *name) const {
|
||||
if (!ATrace_beginSection) {
|
||||
return;
|
||||
}
|
||||
|
||||
ATrace_beginSection(name);
|
||||
}
|
||||
|
||||
void endSection() const {
|
||||
if (!ATrace_endSection) {
|
||||
return;
|
||||
}
|
||||
|
||||
ATrace_endSection();
|
||||
}
|
||||
|
||||
void setCounter(const char *name, int64_t value) {
|
||||
if (!ATrace_setCounter || !isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ATrace_setCounter(name, value);
|
||||
}
|
||||
|
||||
static Trace *getInstance() {
|
||||
static std::unique_ptr<Trace> trace = Trace::create();
|
||||
return trace.get();
|
||||
};
|
||||
|
||||
private:
|
||||
const ATrace_beginSection_type ATrace_beginSection = nullptr;
|
||||
const ATrace_endSection_type ATrace_endSection = nullptr;
|
||||
const ATrace_isEnabled_type ATrace_isEnabled = nullptr;
|
||||
const ATrace_setCounter_type ATrace_setCounter = nullptr;
|
||||
};
|
||||
|
||||
struct ScopedTrace {
|
||||
ScopedTrace(const char *name) {
|
||||
Trace *trace = Trace::getInstance();
|
||||
if (!trace->isAvailable() || !trace->isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
trace->beginSection(name);
|
||||
mIsTracing = true;
|
||||
}
|
||||
|
||||
~ScopedTrace() {
|
||||
if (!mIsTracing) {
|
||||
return;
|
||||
}
|
||||
|
||||
Trace *trace = Trace::getInstance();
|
||||
trace->endSection();
|
||||
}
|
||||
|
||||
private:
|
||||
bool mIsTracing = false;
|
||||
};
|
||||
|
||||
} // namespace gamesdk
|
||||
|
||||
#define PASTE_HELPER_HELPER(a, b) a##b
|
||||
#define PASTE_HELPER(a, b) PASTE_HELPER_HELPER(a, b)
|
||||
#define TRACE_CALL() \
|
||||
gamesdk::ScopedTrace PASTE_HELPER(scopedTrace, \
|
||||
__LINE__)(__PRETTY_FUNCTION__)
|
||||
#define TRACE_INT(name, value) \
|
||||
gamesdk::Trace::getInstance()->setCounter(name, value)
|
||||
#define TRACE_ENABLED() gamesdk::Trace::getInstance()->isEnabled()
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <android/asset_manager.h>
|
||||
#include <android/asset_manager_jni.h>
|
||||
|
||||
#define LOG_TAG "TuningForkUtils"
|
||||
|
||||
#include "Log.h"
|
||||
#include "apk_utils.h"
|
||||
#include "jni/jni_wrap.h"
|
||||
|
||||
namespace apk_utils {
|
||||
|
||||
NativeAsset::NativeAsset(const char* name) {
|
||||
auto java_asset_manager = gamesdk::jni::AppContext().getAssets();
|
||||
AAssetManager* mgr = AAssetManager_fromJava(
|
||||
gamesdk::jni::Env(), (jobject)java_asset_manager.obj_);
|
||||
asset = AAssetManager_open(mgr, name, AASSET_MODE_BUFFER);
|
||||
if (asset == nullptr) {
|
||||
ALOGW("Can't find %s in APK", name);
|
||||
}
|
||||
}
|
||||
NativeAsset::NativeAsset(NativeAsset&& a) : asset(a.asset) {
|
||||
a.asset = nullptr;
|
||||
}
|
||||
NativeAsset& NativeAsset::operator=(NativeAsset&& a) {
|
||||
asset = a.asset;
|
||||
a.asset = nullptr;
|
||||
return *this;
|
||||
}
|
||||
NativeAsset::~NativeAsset() {
|
||||
if (asset != nullptr) {
|
||||
AAsset_close(asset);
|
||||
}
|
||||
}
|
||||
bool NativeAsset::IsValid() { return asset != nullptr; }
|
||||
NativeAsset::operator AAsset*() { return asset; }
|
||||
|
||||
} // namespace apk_utils
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace apk_utils {
|
||||
|
||||
/**
|
||||
* A wrapper class that represents a single asset from the app's assets.
|
||||
* Loads and unloads the asset with the given name to memory.
|
||||
*/
|
||||
class NativeAsset {
|
||||
AAsset* asset;
|
||||
|
||||
public:
|
||||
NativeAsset(const char* name);
|
||||
NativeAsset(NativeAsset&& a);
|
||||
NativeAsset& operator=(NativeAsset&& a);
|
||||
|
||||
NativeAsset(const NativeAsset& a) = delete;
|
||||
NativeAsset& operator=(const NativeAsset& a) = delete;
|
||||
|
||||
~NativeAsset();
|
||||
bool IsValid();
|
||||
operator AAsset*();
|
||||
};
|
||||
|
||||
} // namespace apk_utils
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "jni/jni_helper.h"
|
||||
|
||||
#include "jnictx.h"
|
||||
|
||||
namespace gamesdk {
|
||||
|
||||
namespace jni {
|
||||
|
||||
static jmethodID find_class_;
|
||||
static LocalObject activity_class_loader_;
|
||||
|
||||
void InitActivityClassLoader() {
|
||||
if (activity_class_loader_.IsNull()) {
|
||||
jobject activity = AppContextGlobalRef();
|
||||
jclass activity_clazz = Env()->GetObjectClass(activity);
|
||||
jmethodID get_class_loader = Env()->GetMethodID(
|
||||
activity_clazz, "getClassLoader", "()Ljava/lang/ClassLoader;");
|
||||
activity_class_loader_ =
|
||||
Env()->CallObjectMethod(activity, get_class_loader);
|
||||
|
||||
jclass class_loader = Env()->FindClass("java/lang/ClassLoader");
|
||||
|
||||
find_class_ = Env()->GetMethodID(
|
||||
class_loader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
||||
Env()->DeleteLocalRef(activity_clazz);
|
||||
Env()->DeleteLocalRef(class_loader);
|
||||
}
|
||||
}
|
||||
|
||||
void Init(JNIEnv* env, jobject ctx) { Ctx::Init(env, ctx); }
|
||||
void Destroy() { Ctx::Destroy(); }
|
||||
bool IsValid() {
|
||||
return Ctx::Instance() != nullptr && Ctx::Instance()->IsValid();
|
||||
}
|
||||
JNIEnv* Env() { return Ctx::Instance()->Env(); }
|
||||
void DetachThread() { return Ctx::Instance()->DetachThread(); }
|
||||
jobject AppContextGlobalRef() { return Ctx::Instance()->AppCtx(); }
|
||||
|
||||
jclass FindClass(const char* class_name) {
|
||||
jclass jni_class = Env()->FindClass(class_name);
|
||||
|
||||
if (jni_class == NULL) {
|
||||
InitActivityClassLoader();
|
||||
// FindClass would have thrown.
|
||||
Env()->ExceptionClear();
|
||||
jstring class_jname = Env()->NewStringUTF(class_name);
|
||||
jni_class = (jclass)(Env()->CallObjectMethod(activity_class_loader_,
|
||||
find_class_, class_jname));
|
||||
Env()->DeleteLocalRef(class_jname);
|
||||
}
|
||||
return jni_class;
|
||||
}
|
||||
|
||||
LocalObject NewObjectV(const char* cclz, const char* ctorSig, va_list argptr) {
|
||||
jclass clz = FindClass(cclz);
|
||||
jmethodID constructor = Env()->GetMethodID(clz, "<init>", ctorSig);
|
||||
jobject o = Env()->NewObjectV(clz, constructor, argptr);
|
||||
return LocalObject(o, clz);
|
||||
}
|
||||
LocalObject NewObject(const char* cclz, const char* ctorSig, ...) {
|
||||
va_list argptr;
|
||||
va_start(argptr, ctorSig);
|
||||
auto o = NewObjectV(cclz, ctorSig, argptr);
|
||||
va_end(argptr);
|
||||
return o;
|
||||
}
|
||||
jobject LocalObject::CallObjectMethod(const char* name, const char* sig,
|
||||
...) const {
|
||||
jmethodID mid = Env()->GetMethodID(clz_, name, sig);
|
||||
va_list argptr;
|
||||
va_start(argptr, sig);
|
||||
jobject o = Env()->CallObjectMethodV(obj_, mid, argptr);
|
||||
va_end(argptr);
|
||||
return o;
|
||||
}
|
||||
jobject LocalObject::CallStaticObjectMethod(const char* name, const char* sig,
|
||||
...) const {
|
||||
jmethodID mid = Env()->GetStaticMethodID(clz_, name, sig);
|
||||
va_list argptr;
|
||||
va_start(argptr, sig);
|
||||
jobject o = Env()->CallStaticObjectMethodV(clz_, mid, argptr);
|
||||
va_end(argptr);
|
||||
return o;
|
||||
}
|
||||
String LocalObject::CallStringMethod(const char* name, const char* sig,
|
||||
...) const {
|
||||
jmethodID mid = Env()->GetMethodID(clz_, name, sig);
|
||||
va_list argptr;
|
||||
va_start(argptr, sig);
|
||||
jobject o = Env()->CallObjectMethodV(obj_, mid, argptr);
|
||||
va_end(argptr);
|
||||
String s((jstring)o);
|
||||
return s;
|
||||
}
|
||||
void LocalObject::CallVoidMethod(const char* name, const char* sig, ...) const {
|
||||
jmethodID mid = Env()->GetMethodID(clz_, name, sig);
|
||||
va_list argptr;
|
||||
va_start(argptr, sig);
|
||||
Env()->CallVoidMethodV(obj_, mid, argptr);
|
||||
va_end(argptr);
|
||||
}
|
||||
int LocalObject::CallIntMethod(const char* name, const char* sig, ...) const {
|
||||
jmethodID mid = Env()->GetMethodID(clz_, name, sig);
|
||||
va_list argptr;
|
||||
va_start(argptr, sig);
|
||||
int r = Env()->CallIntMethodV(obj_, mid, argptr);
|
||||
va_end(argptr);
|
||||
return r;
|
||||
}
|
||||
bool LocalObject::CallBooleanMethod(const char* name, const char* sig,
|
||||
...) const {
|
||||
jmethodID mid = Env()->GetMethodID(clz_, name, sig);
|
||||
va_list argptr;
|
||||
va_start(argptr, sig);
|
||||
bool r = Env()->CallBooleanMethodV(obj_, mid, argptr);
|
||||
va_end(argptr);
|
||||
return r;
|
||||
}
|
||||
std::string GetExceptionMessage() {
|
||||
std::string msg;
|
||||
jthrowable exception = Env()->ExceptionOccurred();
|
||||
Env()->ExceptionClear();
|
||||
jclass oclass = FindClass("java/lang/Object");
|
||||
jmethodID toString =
|
||||
Env()->GetMethodID(oclass, "toString", "()Ljava/lang/String;");
|
||||
jstring s = (jstring)Env()->CallObjectMethod(exception, toString);
|
||||
const char* utf = Env()->GetStringUTFChars(s, nullptr);
|
||||
msg = utf;
|
||||
Env()->ReleaseStringUTFChars(s, utf);
|
||||
Env()->DeleteLocalRef(oclass);
|
||||
Env()->DeleteLocalRef(s);
|
||||
Env()->DeleteLocalRef(exception);
|
||||
return msg;
|
||||
}
|
||||
bool CheckForException(std::string& msg) {
|
||||
if (Env()->ExceptionCheck()) {
|
||||
msg = GetExceptionMessage();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
LocalObject LocalObject::GetObjectField(const char* field_name,
|
||||
const char* sig) const {
|
||||
jfieldID fid = Env()->GetFieldID(clz_, field_name, sig);
|
||||
if (!RawExceptionCheck()) {
|
||||
auto out = Env()->GetObjectField(obj_, fid);
|
||||
return LocalObject(out, nullptr);
|
||||
} else {
|
||||
return LocalObject(nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
int LocalObject::GetIntField(const char* field_name) const {
|
||||
jfieldID fid = Env()->GetFieldID(clz_, field_name, "I");
|
||||
if (!RawExceptionCheck())
|
||||
return Env()->GetIntField(obj_, fid);
|
||||
else
|
||||
return BAD_FIELD;
|
||||
}
|
||||
bool LocalObject::GetBooleanField(const char* field_name) const {
|
||||
jfieldID fid = Env()->GetFieldID(clz_, field_name, "Z");
|
||||
if (!RawExceptionCheck())
|
||||
return Env()->GetBooleanField(obj_, fid);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
int64_t LocalObject::GetLongField(const char* field_name) const {
|
||||
jfieldID fid = Env()->GetFieldID(clz_, field_name, "J");
|
||||
if (!RawExceptionCheck())
|
||||
return Env()->GetLongField(obj_, fid);
|
||||
else
|
||||
return BAD_FIELD;
|
||||
}
|
||||
std::vector<unsigned char> GetByteArrayBytesAndDeleteRef(jbyteArray jbs) {
|
||||
jbyte* bs = Env()->GetByteArrayElements(jbs, 0);
|
||||
std::vector<unsigned char> ret(bs, bs + Env()->GetArrayLength(jbs));
|
||||
Env()->ReleaseByteArrayElements(jbs, bs, JNI_ABORT);
|
||||
Env()->DeleteLocalRef(jbs);
|
||||
return ret;
|
||||
}
|
||||
|
||||
jni::String GetStaticStringField(const char* class_name,
|
||||
const char* field_name) {
|
||||
JNIEnv* env = Env();
|
||||
LocalObject obj;
|
||||
obj.Cast(class_name);
|
||||
jclass clz = obj;
|
||||
jfieldID fid = env->GetStaticFieldID(clz, field_name, "Ljava/lang/String;");
|
||||
return (jstring)env->GetStaticObjectField(clz, fid);
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
void DumpLocalRefTable() {
|
||||
JNIEnv* env = Env();
|
||||
jclass vm_class = env->FindClass("dalvik/system/VMDebug");
|
||||
jmethodID dump_mid =
|
||||
env->GetStaticMethodID(vm_class, "dumpReferenceTables", "()V");
|
||||
env->CallStaticVoidMethod(vm_class, dump_mid);
|
||||
env->DeleteLocalRef(vm_class);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace jni
|
||||
|
||||
} // namespace gamesdk
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define CHECK_FOR_JNI_EXCEPTION_AND_RETURN(A) \
|
||||
if (RawExceptionCheck()) { \
|
||||
std::string exception_msg = GetExceptionMessage(); \
|
||||
ALOGW("%s", exception_msg.c_str()); \
|
||||
return A; \
|
||||
}
|
||||
|
||||
namespace gamesdk {
|
||||
|
||||
namespace jni {
|
||||
|
||||
// Management of jni envs and the app context.
|
||||
void Init(JNIEnv* env, jobject ctx);
|
||||
void Destroy();
|
||||
bool IsValid();
|
||||
JNIEnv* Env();
|
||||
void DetachThread();
|
||||
jobject AppContextGlobalRef();
|
||||
// It is the responsibility of the caller to delete the returned local
|
||||
// reference.
|
||||
jclass FindClass(const char* class_name);
|
||||
|
||||
// A wrapper around a jni jstring.
|
||||
// Releases the jstring and any c string pointer generated from it upon
|
||||
// destruction.
|
||||
class String {
|
||||
jstring j_str_;
|
||||
const char* c_str_;
|
||||
|
||||
public:
|
||||
String(const char* s) : j_str_(Env()->NewStringUTF(s)), c_str_(nullptr) {}
|
||||
String(jstring s) : j_str_(s), c_str_(nullptr) {}
|
||||
String(String&& rhs) : j_str_(rhs.j_str_), c_str_(rhs.c_str_) {
|
||||
rhs.j_str_ = nullptr;
|
||||
rhs.c_str_ = nullptr;
|
||||
}
|
||||
String(const String& rhs) : j_str_(rhs.j_str_), c_str_(nullptr) {
|
||||
if (j_str_ != nullptr) {
|
||||
j_str_ = reinterpret_cast<jstring>(Env()->NewLocalRef(j_str_));
|
||||
}
|
||||
}
|
||||
String& operator=(const String& rhs) {
|
||||
if (this != &rhs) {
|
||||
Release();
|
||||
if (rhs.j_str_ != nullptr) {
|
||||
j_str_ =
|
||||
reinterpret_cast<jstring>(Env()->NewLocalRef(rhs.j_str_));
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
jstring J() const { return j_str_; }
|
||||
const char* C() {
|
||||
if (c_str_ == nullptr && j_str_ != nullptr) {
|
||||
c_str_ = Env()->GetStringUTFChars(j_str_, nullptr);
|
||||
}
|
||||
return c_str_;
|
||||
}
|
||||
~String() { Release(); }
|
||||
void Release() {
|
||||
if (c_str_ != nullptr) {
|
||||
Env()->ReleaseStringUTFChars(j_str_, c_str_);
|
||||
c_str_ = nullptr;
|
||||
}
|
||||
if (j_str_ != nullptr) {
|
||||
Env()->DeleteLocalRef(j_str_);
|
||||
j_str_ = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// This class takes ownership of the jni object and jni class and calls
|
||||
// DeleteLocalRef on destruction. The copy constructor and l-value operator= are
|
||||
// deleted to avoid creating and deleting
|
||||
// local references unnecessarily. Use the r-value move versions instead.
|
||||
// NB you cannot share these objects between threads. Create global refs in
|
||||
// order to do that.
|
||||
class LocalObject {
|
||||
jobject obj_;
|
||||
jclass clz_;
|
||||
|
||||
public:
|
||||
static constexpr int BAD_FIELD = -1;
|
||||
|
||||
LocalObject(jobject o = nullptr, jclass c = nullptr) : obj_(o), clz_(c) {}
|
||||
~LocalObject() { Release(); }
|
||||
LocalObject(LocalObject&& o) : obj_(o.obj_), clz_(o.clz_) {
|
||||
o.obj_ = nullptr;
|
||||
o.clz_ = nullptr;
|
||||
}
|
||||
LocalObject(const LocalObject& o) = delete;
|
||||
LocalObject& operator=(const LocalObject& o) = delete;
|
||||
LocalObject& operator=(LocalObject&& o) {
|
||||
if (obj_ != o.obj_) {
|
||||
if (obj_ != nullptr) {
|
||||
Env()->DeleteLocalRef(obj_);
|
||||
}
|
||||
obj_ = o.obj_;
|
||||
}
|
||||
if (clz_ != o.clz_) {
|
||||
if (clz_ != nullptr) {
|
||||
Env()->DeleteLocalRef(clz_);
|
||||
}
|
||||
clz_ = o.clz_;
|
||||
}
|
||||
o.obj_ = nullptr;
|
||||
o.clz_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
jobject ObjNewRef() const {
|
||||
if (obj_ != nullptr) return Env()->NewLocalRef(obj_);
|
||||
return obj_;
|
||||
}
|
||||
jclass ClassNewRef() const {
|
||||
if (clz_ != nullptr) return (jclass)Env()->NewLocalRef(clz_);
|
||||
return clz_;
|
||||
}
|
||||
jobjectArray AsObjectArray() const {
|
||||
return reinterpret_cast<jobjectArray>(obj_);
|
||||
}
|
||||
bool IsNull() const { return obj_ == nullptr; }
|
||||
bool ClassIsNull() const { return clz_ == nullptr; }
|
||||
operator jobject() const { return obj_; }
|
||||
operator jclass() const { return clz_; }
|
||||
void SetObj(jobject o) {
|
||||
if (obj_ != nullptr) Env()->DeleteLocalRef(obj_);
|
||||
obj_ = o;
|
||||
}
|
||||
void SetClass(jclass c) {
|
||||
if (clz_ != nullptr) Env()->DeleteLocalRef(clz_);
|
||||
clz_ = c;
|
||||
}
|
||||
// Set clz_ to the class with the given name or get the class from the
|
||||
// object if clz_to is missing / empty.
|
||||
bool Cast(const std::string& clz_to = "") {
|
||||
jclass c;
|
||||
if (clz_to.empty()) {
|
||||
if (obj_ == nullptr)
|
||||
c = nullptr;
|
||||
else
|
||||
c = Env()->GetObjectClass(obj_);
|
||||
} else {
|
||||
c = FindClass(clz_to.c_str());
|
||||
}
|
||||
if (c == nullptr) return false;
|
||||
SetClass(c);
|
||||
return true;
|
||||
}
|
||||
|
||||
// These methods take a variable number of arguments and have the return the
|
||||
// type indicated. The arguments are passed directly to JNI and it's not
|
||||
// type-safe, so:
|
||||
// All object arguments should be jobjects, NOT LocalObject.
|
||||
// All string arguments should be jstrings, NOT String.
|
||||
jobject CallObjectMethod(const char* name, const char* sig, ...) const;
|
||||
jobject CallStaticObjectMethod(const char* name, const char* sig,
|
||||
...) const;
|
||||
jni::String CallStringMethod(const char* name, const char* sig, ...) const;
|
||||
void CallVoidMethod(const char* name, const char* sig, ...) const;
|
||||
int CallIntMethod(const char* name, const char* sig, ...) const;
|
||||
bool CallBooleanMethod(const char* name, const char* sig, ...) const;
|
||||
|
||||
// Returns a null object if the field could not be found (and exception will
|
||||
// be set)
|
||||
LocalObject GetObjectField(const char* field_name, const char* sig) const;
|
||||
// Returns BAD_FIELD is the field could not be found (and exception is set)
|
||||
int GetIntField(const char* field_name) const;
|
||||
bool GetBooleanField(const char* field_name) const;
|
||||
int64_t GetLongField(const char* field_name) const;
|
||||
|
||||
private:
|
||||
void Release() {
|
||||
if (clz_ != nullptr) {
|
||||
Env()->DeleteLocalRef(clz_);
|
||||
}
|
||||
if (obj_ != nullptr) {
|
||||
Env()->DeleteLocalRef(obj_);
|
||||
}
|
||||
obj_ = nullptr;
|
||||
clz_ = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
LocalObject NewObjectV(const char* cclz, const char* ctorSig, va_list argptr);
|
||||
LocalObject NewObject(const char* cclz, const char* ctorSig, ...);
|
||||
|
||||
inline bool RawExceptionCheck() { return Env()->ExceptionCheck(); }
|
||||
// This will clear the exception and get the exception message.
|
||||
std::string GetExceptionMessage();
|
||||
// Do a RawExceptionCheck and return the result of it, filling in the msg with
|
||||
// the
|
||||
// exception message if one was thrown. Also clears the exception.
|
||||
bool CheckForException(std::string& msg);
|
||||
|
||||
std::vector<unsigned char> GetByteArrayBytesAndDeleteRef(jbyteArray jbs);
|
||||
|
||||
jni::String GetStaticStringField(const char* class_name,
|
||||
const char* field_name);
|
||||
|
||||
// Debugging
|
||||
#ifndef NDEBUG
|
||||
void DumpLocalRefTable();
|
||||
#else
|
||||
inline void DumpLocalRefTable() {}
|
||||
#endif
|
||||
|
||||
} // namespace jni
|
||||
|
||||
} // namespace gamesdk
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,639 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "jni/jni_helper.h"
|
||||
|
||||
namespace gamesdk {
|
||||
|
||||
namespace jni {
|
||||
|
||||
namespace java {
|
||||
|
||||
class Object {
|
||||
public:
|
||||
LocalObject obj_;
|
||||
Object(const char* className, const char* ctorSig, ...) {
|
||||
va_list argptr;
|
||||
va_start(argptr, ctorSig);
|
||||
obj_ = NewObjectV(className, ctorSig, argptr);
|
||||
va_end(argptr);
|
||||
}
|
||||
Object(LocalObject&& o) : obj_(std::move(o)) {}
|
||||
Object(jobject o) {
|
||||
// If there's an exception pending, don't store the reference as it may
|
||||
// be invalid and cause a crash on some devices when released.
|
||||
if (!RawExceptionCheck()) {
|
||||
obj_ = o;
|
||||
obj_.Cast(); // Cast to the object's own class.
|
||||
}
|
||||
}
|
||||
bool valid() const { return !obj_.ClassIsNull(); }
|
||||
void CallVVMethod(const char* name) { obj_.CallVoidMethod(name, "()V"); }
|
||||
void CallIVMethod(const char* name, int a) {
|
||||
obj_.CallVoidMethod(name, "(I)V", a);
|
||||
}
|
||||
int CallVIMethod(const char* name) {
|
||||
return obj_.CallIntMethod(name, "()I");
|
||||
}
|
||||
int CallIIMethod(const char* name, const int a) {
|
||||
return obj_.CallIntMethod(name, "(I)I", a);
|
||||
}
|
||||
void CallZVMethod(const char* name, bool a) {
|
||||
obj_.CallVoidMethod(name, "(Z)V", a);
|
||||
}
|
||||
void CallSVMethod(const char* name, const char* a) {
|
||||
obj_.CallVoidMethod(name, "(Ljava/lang/String;)V", String(a).J());
|
||||
}
|
||||
String CallVSMethod(const char* name) {
|
||||
return obj_.CallStringMethod(name, "()Ljava/lang/String;");
|
||||
}
|
||||
void CallSSVMethod(const char* name, const char* a, const char* b) {
|
||||
obj_.CallVoidMethod(name, "(Ljava/lang/String;Ljava/lang/String;)V",
|
||||
String(a).J(), String(b).J());
|
||||
}
|
||||
Object CallAOMethod(const char* name, const char* returnClass) {
|
||||
std::stringstream str;
|
||||
str << "()[L" << returnClass << ";";
|
||||
jobject o = obj_.CallObjectMethod(name, str.str().c_str());
|
||||
return Object(o);
|
||||
}
|
||||
Object CallVOMethod(const char* name, const char* returnClass) {
|
||||
std::stringstream str;
|
||||
str << "()L" << returnClass << ";";
|
||||
jobject o = obj_.CallObjectMethod(name, str.str().c_str());
|
||||
return Object(o);
|
||||
}
|
||||
Object CallIOMethod(const char* name, int a, const char* returnClass) {
|
||||
std::stringstream str;
|
||||
str << "(I)L" << returnClass << ";";
|
||||
jobject o = obj_.CallObjectMethod(name, str.str().c_str(), a);
|
||||
return Object(o);
|
||||
}
|
||||
void CallOVMethod(const char* name, const char* parameterClassA,
|
||||
const Object& a) {
|
||||
std::stringstream str;
|
||||
str << "(L" << parameterClassA << ";)V";
|
||||
obj_.CallVoidMethod(name, str.str().c_str(), (jobject)a.obj_);
|
||||
}
|
||||
Object CallSIOMethod(const char* name, const char* a, int b,
|
||||
const char* returnClass) {
|
||||
std::stringstream str;
|
||||
str << "(Ljava/lang/String;I)L" << returnClass << ";";
|
||||
jobject o =
|
||||
obj_.CallObjectMethod(name, str.str().c_str(), String(a).J(), b);
|
||||
return Object(o);
|
||||
}
|
||||
Object CallOOOMethod(const char* name, const char* parameterClassA,
|
||||
const Object& a, const char* parameterClassB,
|
||||
const Object& b, const char* returnClass) {
|
||||
std::stringstream str;
|
||||
str << "(L" << parameterClassA << ";L" << parameterClassB << ";)L"
|
||||
<< returnClass << ";";
|
||||
jobject o = obj_.CallObjectMethod(name, str.str().c_str(),
|
||||
(jobject)a.obj_, (jobject)b.obj_);
|
||||
return Object(o);
|
||||
}
|
||||
int CallSIIMethod(const char* name, const char* a, const int b) {
|
||||
std::stringstream str;
|
||||
str << "(Ljava/lang/String;I)I";
|
||||
return obj_.CallIntMethod(name, "(Ljava/lang/String;I)I", String(a).J(),
|
||||
b);
|
||||
}
|
||||
Object CallSIIOMethod(const char* name, const char* a, int b, int c,
|
||||
const char* returnClass) {
|
||||
std::stringstream str;
|
||||
str << "(Ljava/lang/String;II)L" << returnClass << ";";
|
||||
jobject o =
|
||||
obj_.CallObjectMethod(name, str.str().c_str(), String(a).J(), b, c);
|
||||
return Object(o);
|
||||
}
|
||||
Object CallSOMethod(const char* name, const char* a,
|
||||
const char* returnClass) {
|
||||
std::stringstream str;
|
||||
str << "(Ljava/lang/String;)L" << returnClass << ";";
|
||||
jobject o =
|
||||
obj_.CallObjectMethod(name, str.str().c_str(), String(a).J());
|
||||
return Object(o);
|
||||
}
|
||||
bool CallVZMethod(const char* name) {
|
||||
return obj_.CallBooleanMethod(name, "()Z");
|
||||
}
|
||||
bool IsNull() const { return obj_.IsNull(); }
|
||||
};
|
||||
|
||||
namespace io {
|
||||
|
||||
class OutputStream : public Object {
|
||||
public:
|
||||
OutputStream(Object&& o) : Object(std::move(o)) {}
|
||||
void close() { CallVVMethod("close"); }
|
||||
};
|
||||
|
||||
class OutputStreamWriter : public Object {
|
||||
public:
|
||||
OutputStreamWriter(OutputStream& o, const std::string& s)
|
||||
: Object("java/io/OutputStreamWriter",
|
||||
"(Ljava/io/OutputStream;Ljava/lang/String;)V", (jobject)o.obj_,
|
||||
String(s.c_str()).J()) {}
|
||||
};
|
||||
|
||||
class Writer : public Object {
|
||||
public:
|
||||
Writer(Object&& o) : Object(std::move(o)) {}
|
||||
void write(const std::string& s) { CallSVMethod("write", s.c_str()); }
|
||||
void flush() { CallVVMethod("flush"); }
|
||||
void close() { CallVVMethod("close"); }
|
||||
};
|
||||
|
||||
class BufferedWriter : public Writer {
|
||||
public:
|
||||
BufferedWriter(const Writer& w)
|
||||
: Writer(Object("java/io/BufferedWriter", "(Ljava/io/Writer;)V",
|
||||
(jobject)w.obj_)) {}
|
||||
};
|
||||
|
||||
class InputStream : public Object {
|
||||
public:
|
||||
InputStream(Object&& o) : Object(std::move(o)) {}
|
||||
void close() { CallVVMethod("close"); }
|
||||
};
|
||||
|
||||
class Reader : public Object {
|
||||
public:
|
||||
Reader(Object&& o) : Object(std::move(o)) {}
|
||||
jni::String readLine() { return CallVSMethod("readLine"); }
|
||||
void close() { CallVVMethod("close"); }
|
||||
};
|
||||
|
||||
class InputStreamReader : public Reader {
|
||||
public:
|
||||
InputStreamReader(const InputStream& is, const std::string& s)
|
||||
: Reader(Object("java/io/InputStreamReader",
|
||||
"(Ljava/io/InputStream;Ljava/lang/String;)V",
|
||||
(jobject)is.obj_, String(s.c_str()).J())) {}
|
||||
};
|
||||
|
||||
class BufferedReader : public Reader {
|
||||
public:
|
||||
BufferedReader(const Reader& r)
|
||||
: Reader(Object("java/io/BufferedReader", "(Ljava/io/Reader;)V",
|
||||
(jobject)r.obj_)) {}
|
||||
};
|
||||
|
||||
class File : public Object {
|
||||
public:
|
||||
File(Object&& o) : Object(std::move(o)) {}
|
||||
jni::String getPath() { return CallVSMethod("getPath"); }
|
||||
};
|
||||
|
||||
} // namespace io
|
||||
|
||||
namespace util {
|
||||
|
||||
class UUID : public Object {
|
||||
public:
|
||||
UUID(LocalObject&& o) : Object(std::move(o)) {}
|
||||
static UUID randomUUID() {
|
||||
LocalObject obj(nullptr, Env()->FindClass("java/util/UUID"));
|
||||
auto o = obj.CallStaticObjectMethod("randomUUID", "()Ljava/util/UUID;");
|
||||
obj.SetObj(o);
|
||||
return obj;
|
||||
}
|
||||
jni::String toString() { return CallVSMethod("toString"); }
|
||||
};
|
||||
|
||||
class List : public java::Object {
|
||||
public:
|
||||
List(Object&& o) : Object(std::move(o)) {}
|
||||
java::Object get(int index) {
|
||||
return CallIOMethod("get", index, "java/lang/Object");
|
||||
}
|
||||
bool isEmpty() { return CallVZMethod("isEmpty"); }
|
||||
jni::String toString() { return CallVSMethod("toString"); }
|
||||
};
|
||||
|
||||
} // namespace util
|
||||
|
||||
namespace net {
|
||||
|
||||
class URLConnection : public Object {
|
||||
public:
|
||||
URLConnection(Object&& o) : Object(std::move(o)) {}
|
||||
};
|
||||
|
||||
class HttpURLConnection : public URLConnection {
|
||||
public:
|
||||
HttpURLConnection(URLConnection&& u) : URLConnection(std::move(u)) {
|
||||
obj_.Cast("java/net/HttpURLConnection");
|
||||
}
|
||||
void setRequestMethod(const std::string& method) {
|
||||
CallSVMethod("setRequestMethod", method.c_str());
|
||||
}
|
||||
void setConnectTimeout(int t) { CallIVMethod("setConnectTimeout", t); }
|
||||
void setReadTimeout(int timeout) {
|
||||
CallIVMethod("setReadTimeout", timeout);
|
||||
}
|
||||
void setDoOutput(bool d) { CallZVMethod("setDoOutput", d); }
|
||||
void setDoInput(bool d) { CallZVMethod("setDoInput", d); }
|
||||
void setUseCaches(bool d) { CallZVMethod("setUseCaches", d); }
|
||||
void setRequestProperty(const std::string& name, const std::string& value) {
|
||||
CallSSVMethod("setRequestProperty", name.c_str(), value.c_str());
|
||||
}
|
||||
io::OutputStream getOutputStream() {
|
||||
return CallVOMethod("getOutputStream", "java/io/OutputStream");
|
||||
}
|
||||
void connect() { CallVVMethod("connect"); }
|
||||
void disconnect() { CallVVMethod("disconnect"); }
|
||||
int getResponseCode() { return CallVIMethod("getResponseCode"); }
|
||||
jni::String getResponseMessage() {
|
||||
return CallVSMethod("getResponseMessage");
|
||||
}
|
||||
io::InputStream getInputStream() {
|
||||
return CallVOMethod("getInputStream", "java/io/InputStream");
|
||||
}
|
||||
};
|
||||
|
||||
class URL : public Object {
|
||||
public:
|
||||
URL(LocalObject o) : Object(o) {}
|
||||
URL(const std::string& s)
|
||||
: Object("java/net/URL", "(Ljava/lang/String;)V",
|
||||
String(s.c_str()).J()) {}
|
||||
URLConnection openConnection() {
|
||||
return URLConnection(Object(obj_.CallObjectMethod(
|
||||
"openConnection", "()Ljava/net/URLConnection;")));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
|
||||
namespace security {
|
||||
|
||||
class MessageDigest : public java::Object {
|
||||
public:
|
||||
MessageDigest(const std::string& instance) : java::Object(nullptr) {
|
||||
LocalObject temp(nullptr, FindClass("java/security/MessageDigest"));
|
||||
auto o = temp.CallStaticObjectMethod(
|
||||
"getInstance", "(Ljava/lang/String;)Ljava/security/MessageDigest;",
|
||||
String(instance.c_str()).J());
|
||||
temp.SetObj(o);
|
||||
obj_ = std::move(temp);
|
||||
}
|
||||
std::vector<unsigned char> digest(
|
||||
const std::vector<unsigned char>& bs) const {
|
||||
auto env = Env();
|
||||
jbyteArray jbs = env->NewByteArray(bs.size());
|
||||
env->SetByteArrayRegion(jbs, 0, bs.size(),
|
||||
reinterpret_cast<const jbyte*>(bs.data()));
|
||||
jbyteArray out = reinterpret_cast<jbyteArray>(
|
||||
obj_.CallObjectMethod("digest", "([B)[B", jbs));
|
||||
env->DeleteLocalRef(jbs);
|
||||
return GetByteArrayBytesAndDeleteRef(out);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace security
|
||||
|
||||
} // namespace java
|
||||
|
||||
namespace android {
|
||||
|
||||
namespace content {
|
||||
|
||||
namespace pm {
|
||||
|
||||
class FeatureInfo : public java::Object {
|
||||
public:
|
||||
FeatureInfo(java::Object&& o) : java::Object(std::move(o)) {
|
||||
jni::String jname(
|
||||
(jstring)obj_.GetObjectField("name", "Ljava/lang/String;")
|
||||
.ObjNewRef());
|
||||
if (jname.J() != nullptr) name = jname.C();
|
||||
reqGlEsVersion = obj_.GetIntField("reqGlEsVersion");
|
||||
}
|
||||
static constexpr int GL_ES_VERSION_UNDEFINED = 0x0000000;
|
||||
|
||||
std::string name;
|
||||
int reqGlEsVersion;
|
||||
};
|
||||
|
||||
class ApplicationInfo : public java::Object {
|
||||
public:
|
||||
ApplicationInfo(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
int flags() const { return obj_.GetIntField("flags"); }
|
||||
static const int FLAG_DEBUGGABLE = 2;
|
||||
};
|
||||
|
||||
class PackageInfo : public java::Object {
|
||||
public:
|
||||
PackageInfo(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
typedef std::vector<unsigned char> Signature;
|
||||
std::vector<Signature> signatures() const {
|
||||
auto env = Env();
|
||||
auto jsigs = obj_.GetObjectField("signatures",
|
||||
"[Landroid/content/pm/Signature;");
|
||||
jobjectArray sigs = jsigs.AsObjectArray();
|
||||
if (sigs == nullptr) return {};
|
||||
int n = env->GetArrayLength(sigs);
|
||||
if (n > 0) {
|
||||
std::vector<std::vector<unsigned char>> ret;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
Object sig(env->GetObjectArrayElement(sigs, i));
|
||||
jbyteArray bytes = reinterpret_cast<jbyteArray>(
|
||||
sig.obj_.CallObjectMethod("toByteArray", "()[B"));
|
||||
ret.push_back(GetByteArrayBytesAndDeleteRef(bytes));
|
||||
}
|
||||
return ret;
|
||||
} else
|
||||
return {};
|
||||
}
|
||||
int versionCode() const { return obj_.GetIntField("versionCode"); }
|
||||
ApplicationInfo applicationInfo() const {
|
||||
auto appInfo = obj_.GetObjectField(
|
||||
"applicationInfo", "[Landroid/content/pm/ApplicationInfo;");
|
||||
return ApplicationInfo(std::move(appInfo));
|
||||
}
|
||||
};
|
||||
|
||||
class PackageManager : public java::Object {
|
||||
public:
|
||||
PackageManager(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
static constexpr int GET_SIGNATURES = 0x0000040;
|
||||
PackageInfo getPackageInfo(const std::string& name, int flags) {
|
||||
return CallSIOMethod("getPackageInfo", name.c_str(), flags,
|
||||
"android/content/pm/PackageInfo");
|
||||
}
|
||||
std::vector<FeatureInfo> getSystemAvailableFeatures() {
|
||||
auto env = Env();
|
||||
auto jfeatures = CallAOMethod("getSystemAvailableFeatures",
|
||||
"android/content/pm/FeatureInfo");
|
||||
if (jfeatures.obj_.IsNull()) return {};
|
||||
jobjectArray features = jfeatures.obj_.AsObjectArray();
|
||||
int n = env->GetArrayLength(features);
|
||||
std::vector<FeatureInfo> ret;
|
||||
if (n > 0) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
FeatureInfo f(Object(env->GetObjectArrayElement(features, i)));
|
||||
ret.push_back(std::move(f));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pm
|
||||
|
||||
namespace res {
|
||||
|
||||
class AssetManager : public java::Object {
|
||||
public:
|
||||
AssetManager(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
};
|
||||
|
||||
} // namespace res
|
||||
|
||||
class Intent : java::Object {
|
||||
public:
|
||||
static constexpr const char* ACTION_BATTERY_CHANGED =
|
||||
"android.intent.action.BATTERY_CHANGED";
|
||||
Intent(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
int getIntExtra(const char* name, int defaultValue) {
|
||||
return CallSIIMethod("getIntExtra", name, defaultValue);
|
||||
}
|
||||
};
|
||||
|
||||
class IntentFilter : public java::Object {
|
||||
public:
|
||||
IntentFilter(jobject o) : java::Object(o) {}
|
||||
IntentFilter(const char* action)
|
||||
: Object("android/content/IntentFilter", "(Ljava/lang/String;)V",
|
||||
String(action).J()) {}
|
||||
};
|
||||
|
||||
class BroadcastReceiver : public java::Object {
|
||||
public:
|
||||
BroadcastReceiver(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
};
|
||||
|
||||
class Context : public java::Object {
|
||||
public:
|
||||
static constexpr const char* CONNECTIVITY_SERVICE = "connectivity";
|
||||
static constexpr const char* BATTERY_SERVICE = "batterymanager";
|
||||
static constexpr const char* POWER_SERVICE = "power";
|
||||
static constexpr const char* ACTIVITY_SERVICE = "activity";
|
||||
Context(jobject o) : java::Object(o) {}
|
||||
pm::PackageManager getPackageManager() {
|
||||
return CallVOMethod("getPackageManager",
|
||||
"android/content/pm/PackageManager");
|
||||
}
|
||||
jni::String getPackageName() { return CallVSMethod("getPackageName"); }
|
||||
res::AssetManager getAssets() {
|
||||
return CallVOMethod("getAssets", "android/content/res/AssetManager");
|
||||
}
|
||||
java::io::File getCacheDir() {
|
||||
return CallVOMethod("getCacheDir", "java/io/File");
|
||||
}
|
||||
java::Object getSystemService(const char* name) {
|
||||
return CallSOMethod("getSystemService", name, "java/lang/Object");
|
||||
}
|
||||
java::Object registerReceiver(BroadcastReceiver& broadcastReceiver,
|
||||
IntentFilter& intentFilter) {
|
||||
return CallOOOMethod("registerReceiver",
|
||||
"android/content/BroadcastReceiver",
|
||||
broadcastReceiver, "android/content/IntentFilter",
|
||||
intentFilter, "android/content/Intent");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace content
|
||||
|
||||
namespace os {
|
||||
|
||||
class DebugClass {
|
||||
jmethodID getNativeHeapAllocatedSize_method_id_;
|
||||
|
||||
public:
|
||||
static uint64_t getNativeHeapAllocatedSize() {
|
||||
JNIEnv* env = Env();
|
||||
if (env != nullptr) {
|
||||
LocalObject obj;
|
||||
obj.Cast("android/os/Debug");
|
||||
jclass clz = obj;
|
||||
jmethodID method = env->GetStaticMethodID(
|
||||
clz, "getNativeHeapAllocatedSize", "()J");
|
||||
if (method != NULL)
|
||||
return (uint64_t)env->CallStaticLongMethod(clz, method);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t getNativeHeapFreeSize() {
|
||||
JNIEnv* env = Env();
|
||||
if (env != nullptr) {
|
||||
LocalObject obj;
|
||||
obj.Cast("android/os/Debug");
|
||||
jclass clz = obj;
|
||||
jmethodID method =
|
||||
env->GetStaticMethodID(clz, "getNativeHeapFreeSize", "()J");
|
||||
if (method != NULL)
|
||||
return (uint64_t)env->CallStaticLongMethod(clz, method);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t getNativeHeapSize() {
|
||||
JNIEnv* env = Env();
|
||||
if (env != nullptr) {
|
||||
LocalObject obj;
|
||||
obj.Cast("android/os/Debug");
|
||||
jclass clz = obj;
|
||||
jmethodID method =
|
||||
env->GetStaticMethodID(clz, "getNativeHeapSize", "()J");
|
||||
if (method != NULL)
|
||||
return (uint64_t)env->CallStaticLongMethod(clz, method);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t getPss() {
|
||||
JNIEnv* env = Env();
|
||||
if (env != nullptr) {
|
||||
LocalObject obj;
|
||||
obj.Cast("android/os/Debug");
|
||||
jclass clz = obj;
|
||||
jmethodID method = env->GetStaticMethodID(clz, "getPss", "()J");
|
||||
if (method != NULL)
|
||||
return (uint64_t)env->CallStaticLongMethod(clz, method);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}; // class Debug
|
||||
|
||||
class Build {
|
||||
static constexpr const char class_name[] = "android/os/Build";
|
||||
|
||||
public:
|
||||
static jni::String MODEL() {
|
||||
return GetStaticStringField(class_name, "MODEL");
|
||||
}
|
||||
static jni::String BRAND() {
|
||||
return GetStaticStringField(class_name, "BRAND");
|
||||
}
|
||||
static jni::String PRODUCT() {
|
||||
return GetStaticStringField(class_name, "PRODUCT");
|
||||
}
|
||||
static jni::String DEVICE() {
|
||||
return GetStaticStringField(class_name, "DEVICE");
|
||||
}
|
||||
static jni::String FINGERPRINT() {
|
||||
return GetStaticStringField(class_name, "FINGERPRINT");
|
||||
}
|
||||
static jni::String SOC_MODEL() {
|
||||
return GetStaticStringField(class_name, "SOC_MODEL");
|
||||
}
|
||||
static jni::String SOC_MANUFACTURER() {
|
||||
return GetStaticStringField(class_name, "SOC_MANUFACTURER");
|
||||
}
|
||||
}; // Class Build
|
||||
|
||||
class BatteryManager : java::Object {
|
||||
public:
|
||||
static constexpr const char* EXTRA_LEVEL = "level";
|
||||
static constexpr const char* EXTRA_SCALE = "scale";
|
||||
static constexpr const char* EXTRA_PLUGGED = "plugged";
|
||||
static constexpr const int BATTERY_PROPERTY_CHARGE_COUNTER = 1;
|
||||
BatteryManager(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
int getIntProperty(int id) { return CallIIMethod("getIntProperty", id); }
|
||||
};
|
||||
|
||||
class PowerManager : java::Object {
|
||||
public:
|
||||
PowerManager(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
int getCurrentThermalStatus() {
|
||||
return CallVIMethod("getCurrentThermalStatus");
|
||||
}
|
||||
bool isPowerSaveMode() { return CallVZMethod("isPowerSaveMode"); }
|
||||
};
|
||||
|
||||
} // namespace os
|
||||
|
||||
namespace net {
|
||||
|
||||
class ConnectivityManager : java::Object {
|
||||
public:
|
||||
ConnectivityManager(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
// NB This requires Manifest.permission.ACCESS_NETWORK_STATE.
|
||||
bool isActiveNetworkMetered() {
|
||||
return CallVZMethod("isActiveNetworkMetered");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
|
||||
namespace app {
|
||||
|
||||
class MemoryInfo : public java::Object {
|
||||
public:
|
||||
MemoryInfo(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
MemoryInfo()
|
||||
: java::Object("android/app/ActivityManager$MemoryInfo", "()V") {}
|
||||
int64_t threshold() const { return obj_.GetLongField("threshold"); }
|
||||
int64_t availMem() const { return obj_.GetLongField("availMem"); }
|
||||
bool lowMemory() const { return obj_.GetBooleanField("lowMemory"); }
|
||||
int64_t totalMem() const { return obj_.GetLongField("totalMem"); }
|
||||
};
|
||||
|
||||
class ActivityManager : public java::Object {
|
||||
public:
|
||||
ActivityManager(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
void getMemoryInfo(MemoryInfo& memoryInfo) {
|
||||
CallOVMethod("getMemoryInfo", "android/app/ActivityManager$MemoryInfo",
|
||||
memoryInfo);
|
||||
}
|
||||
int32_t getMemoryClass() { return CallVIMethod("getMemoryClass"); }
|
||||
int32_t getLargeMemoryClass() {
|
||||
return CallVIMethod("getLargeMemoryClass");
|
||||
}
|
||||
bool isLowRamDevice() { return CallVZMethod("isLowRamDevice"); }
|
||||
java::util::List getHistoricalProcessExitReasons(std::string& packageName,
|
||||
int pid, int maxNum) {
|
||||
return CallSIIOMethod("getHistoricalProcessExitReasons",
|
||||
packageName.c_str(), pid, maxNum,
|
||||
"java/util/List");
|
||||
}
|
||||
};
|
||||
|
||||
class ApplicationExitInfo : java::Object {
|
||||
public:
|
||||
static constexpr int REASON_LOW_MEMORY = 3;
|
||||
ApplicationExitInfo(java::Object&& o) : java::Object(std::move(o)) {}
|
||||
int getReason() { return CallVIMethod("getReason"); }
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
} // namespace android
|
||||
|
||||
// A local jni reference to the app context
|
||||
android::content::Context AppContext();
|
||||
|
||||
} // namespace jni
|
||||
|
||||
} // namespace gamesdk
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "jnictx.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Log.h"
|
||||
#define LOG_TAG "JniCtx"
|
||||
|
||||
namespace gamesdk {
|
||||
|
||||
namespace jni {
|
||||
|
||||
static std::unique_ptr<Ctx> theCtx;
|
||||
static thread_local JNIEnv* theEnv = nullptr;
|
||||
|
||||
/*static*/ const Ctx* Ctx::Init(JNIEnv* env, jobject ctx) {
|
||||
theCtx = std::unique_ptr<Ctx>(new Ctx(env, ctx, ConstructorTag{}));
|
||||
theEnv = env;
|
||||
return theCtx.get();
|
||||
}
|
||||
|
||||
/*static*/ const Ctx* Ctx::Instance() {
|
||||
if (theCtx.get() == nullptr) {
|
||||
ALOGE("You must call jni::Ctx::Init before using any jni::Ctx methods");
|
||||
}
|
||||
return theCtx.get();
|
||||
}
|
||||
|
||||
/*static*/ void Ctx::Destroy() { theCtx.reset(); }
|
||||
|
||||
Ctx::Ctx(JNIEnv* env, jobject ctx, ConstructorTag) {
|
||||
if (env) {
|
||||
jctx_ = env->NewGlobalRef(ctx);
|
||||
env->GetJavaVM(&jvm_);
|
||||
}
|
||||
}
|
||||
|
||||
Ctx::~Ctx() {
|
||||
if (jctx_) {
|
||||
JNIEnv* env = Env();
|
||||
if (env) {
|
||||
env->DeleteGlobalRef(jctx_);
|
||||
}
|
||||
}
|
||||
}
|
||||
JNIEnv* Ctx::Env() const {
|
||||
if (theEnv == nullptr && jvm_ != nullptr) {
|
||||
jvm_->AttachCurrentThread(&theEnv, NULL);
|
||||
}
|
||||
return theEnv;
|
||||
}
|
||||
|
||||
void Ctx::DetachThread() const {
|
||||
if (jvm_ != nullptr) jvm_->DetachCurrentThread();
|
||||
theEnv = nullptr;
|
||||
}
|
||||
|
||||
} // namespace jni
|
||||
|
||||
} // namespace gamesdk
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
namespace gamesdk {
|
||||
|
||||
namespace jni {
|
||||
|
||||
// Singleton class that stores an app's JVM and context
|
||||
class Ctx {
|
||||
private:
|
||||
// Allows construction with std::unique_ptr from a static method, but
|
||||
// disallows construction outside of the class since no one else can
|
||||
// construct a ConstructorTag
|
||||
struct ConstructorTag {};
|
||||
JavaVM* jvm_;
|
||||
jobject jctx_; // Global reference to the app's context
|
||||
public:
|
||||
static const Ctx* Init(JNIEnv* env, jobject ctx);
|
||||
static void Destroy();
|
||||
static const Ctx* Instance();
|
||||
Ctx(JNIEnv* env, jobject ctx, ConstructorTag);
|
||||
~Ctx();
|
||||
JNIEnv* Env() const;
|
||||
JavaVM* Jvm() const { return jvm_; }
|
||||
jobject AppCtx() const { return jctx_; }
|
||||
bool IsValid() const { return jvm_ != nullptr && jctx_ != nullptr; }
|
||||
void DetachThread() const;
|
||||
Ctx() = delete;
|
||||
Ctx(const Ctx&) = delete;
|
||||
Ctx(Ctx&&) = delete;
|
||||
Ctx& operator=(const Ctx& rhs) = delete;
|
||||
};
|
||||
|
||||
} // namespace jni
|
||||
|
||||
} // namespace gamesdk
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "system_utils.h"
|
||||
|
||||
#include <android/api-level.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/system_properties.h>
|
||||
|
||||
namespace gamesdk {
|
||||
|
||||
#if __ANDROID_API__ >= 26
|
||||
std::string getSystemPropViaCallback(const char* key,
|
||||
const char* default_value = "") {
|
||||
const prop_info* prop = __system_property_find(key);
|
||||
if (prop == nullptr) {
|
||||
return default_value;
|
||||
}
|
||||
std::string return_value;
|
||||
auto thunk = [](void* cookie, const char* /*name*/, const char* value,
|
||||
uint32_t /*serial*/) {
|
||||
if (value != nullptr) {
|
||||
std::string* r = static_cast<std::string*>(cookie);
|
||||
*r = value;
|
||||
}
|
||||
};
|
||||
__system_property_read_callback(prop, thunk, &return_value);
|
||||
return return_value;
|
||||
}
|
||||
#else
|
||||
std::string getSystemPropViaGet(const char* key,
|
||||
const char* default_value = "") {
|
||||
char buffer[PROP_VALUE_MAX + 1] = ""; // +1 for terminator
|
||||
int bufferLen = __system_property_get(key, buffer);
|
||||
if (bufferLen > 0)
|
||||
return buffer;
|
||||
else
|
||||
return "";
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string GetSystemProp(const char* key, const char* default_value) {
|
||||
#if __ANDROID_API__ >= 26
|
||||
return getSystemPropViaCallback(key, default_value);
|
||||
#else
|
||||
return getSystemPropViaGet(key, default_value);
|
||||
#endif
|
||||
}
|
||||
|
||||
int GetSystemPropAsInt(const char* key, int default_value) {
|
||||
std::string prop = GetSystemProp(key);
|
||||
return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10);
|
||||
}
|
||||
|
||||
bool GetSystemPropAsBool(const char* key, bool default_value) {
|
||||
return GetSystemPropAsInt(key, default_value) != 0;
|
||||
}
|
||||
|
||||
} // namespace gamesdk
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
stoza@google.com
|
||||
adyabr@google.com
|
||||
ianelliott@google.com
|
||||
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "CPUTracer.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "../../common/Log.h"
|
||||
#include "../../common/Trace.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
CPUTracer::CPUTracer() {}
|
||||
|
||||
CPUTracer::~CPUTracer() { joinThread(); }
|
||||
|
||||
void CPUTracer::joinThread() {
|
||||
bool join = false;
|
||||
if (mThread && mThread->joinable()) {
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mTrace = false;
|
||||
mRunning = false;
|
||||
mCond.notify_one();
|
||||
join = true;
|
||||
}
|
||||
if (join) {
|
||||
mThread->join();
|
||||
}
|
||||
mThread.reset();
|
||||
}
|
||||
|
||||
void CPUTracer::startTrace() {
|
||||
if (TRACE_ENABLED()) {
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mThread) {
|
||||
mRunning = true;
|
||||
mThread = std::make_unique<Thread>([this]() { threadMain(); });
|
||||
}
|
||||
mTrace = true;
|
||||
mCond.notify_one();
|
||||
} else {
|
||||
joinThread();
|
||||
}
|
||||
}
|
||||
|
||||
void CPUTracer::endTrace() {
|
||||
if (TRACE_ENABLED()) {
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mTrace = false;
|
||||
mCond.notify_one();
|
||||
} else {
|
||||
joinThread();
|
||||
}
|
||||
}
|
||||
|
||||
void CPUTracer::threadMain() NO_THREAD_SAFETY_ANALYSIS {
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
while (mRunning) {
|
||||
if (mTrace) {
|
||||
gamesdk::ScopedTrace trace("Swappy: CPU frame time");
|
||||
mCond.wait(lock);
|
||||
} else {
|
||||
mCond.wait(lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "Thread.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
class CPUTracer {
|
||||
public:
|
||||
CPUTracer();
|
||||
~CPUTracer();
|
||||
|
||||
CPUTracer(CPUTracer&) = delete;
|
||||
|
||||
void startTrace();
|
||||
void endTrace();
|
||||
|
||||
private:
|
||||
void threadMain();
|
||||
void joinThread();
|
||||
|
||||
std::mutex mMutex;
|
||||
std::condition_variable_any mCond GUARDED_BY(mMutex);
|
||||
std::unique_ptr<Thread> mThread;
|
||||
bool mRunning GUARDED_BY(mMutex) = true;
|
||||
bool mTrace GUARDED_BY(mMutex) = false;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "ChoreographerFilter.h"
|
||||
|
||||
#define LOG_TAG "ChoreographerFilter"
|
||||
|
||||
#include <sched.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "Log.h"
|
||||
#include "Settings.h"
|
||||
#include "Thread.h"
|
||||
#include "Trace.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using time_point = std::chrono::steady_clock::time_point;
|
||||
|
||||
namespace {
|
||||
|
||||
class Timer {
|
||||
public:
|
||||
Timer(std::chrono::nanoseconds refreshPeriod,
|
||||
std::chrono::nanoseconds appToSfDelay)
|
||||
: mRefreshPeriod(refreshPeriod), mAppToSfDelay(appToSfDelay) {}
|
||||
|
||||
// Returns false if we have detected that we have received the same
|
||||
// timestamp multiple times so that the caller can wait for fresh timestamps
|
||||
bool addTimestamp(time_point point) {
|
||||
// Keep track of the previous timestamp and how many times we've seen it
|
||||
// to determine if we've stopped receiving Choreographer callbacks,
|
||||
// which would indicate that we should probably stop until we see them
|
||||
// again (e.g., if the app has been moved to the background)
|
||||
if (point == mLastTimestamp) {
|
||||
if (mRepeatCount++ > 5) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mRepeatCount = 0;
|
||||
}
|
||||
mLastTimestamp = point;
|
||||
|
||||
point += mAppToSfDelay;
|
||||
|
||||
bool moreThanOneRefreshPeriodElapsed =
|
||||
mBaseTime + mRefreshPeriod * 1.5 < point;
|
||||
if (moreThanOneRefreshPeriodElapsed) {
|
||||
do {
|
||||
mBaseTime += mRefreshPeriod;
|
||||
} while (mBaseTime + mRefreshPeriod * 1.5 < point);
|
||||
mBaseTime += mRefreshPeriod;
|
||||
// Long waits pollute the filter so don't adjust refreshPeriod.
|
||||
return true;
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds delta = (point - (mBaseTime + mRefreshPeriod));
|
||||
if (delta < -mRefreshPeriod / 2) {
|
||||
// Also ignore short intervals
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exponential smoothing factor = 0.04 avoids roughness.
|
||||
mRefreshPeriod += delta / 25;
|
||||
mBaseTime += mRefreshPeriod;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void sleep(std::chrono::nanoseconds offset) {
|
||||
if (offset < -(mRefreshPeriod / 2) || offset > mRefreshPeriod / 2) {
|
||||
offset = 0ms;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
auto targetTime = mBaseTime + mRefreshPeriod + offset;
|
||||
while (targetTime < now) {
|
||||
targetTime += mRefreshPeriod;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_until(targetTime);
|
||||
}
|
||||
|
||||
private:
|
||||
std::chrono::nanoseconds mRefreshPeriod;
|
||||
const std::chrono::nanoseconds mAppToSfDelay;
|
||||
time_point mBaseTime = std::chrono::steady_clock::now();
|
||||
|
||||
time_point mLastTimestamp = std::chrono::steady_clock::now();
|
||||
int32_t mRepeatCount = 0;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
namespace swappy {
|
||||
|
||||
ChoreographerFilter::ChoreographerFilter(std::chrono::nanoseconds refreshPeriod,
|
||||
std::chrono::nanoseconds appToSfDelay,
|
||||
Worker doWork)
|
||||
: mRefreshPeriod(refreshPeriod),
|
||||
mAppToSfDelay(appToSfDelay),
|
||||
mDoWork(doWork) {
|
||||
Settings::getInstance()->addListener([this]() { onSettingsChanged(); });
|
||||
|
||||
std::lock_guard<std::mutex> lock(mThreadPoolMutex);
|
||||
mUseAffinity = Settings::getInstance()->getUseAffinity();
|
||||
launchThreadsLocked();
|
||||
}
|
||||
|
||||
ChoreographerFilter::~ChoreographerFilter() {
|
||||
std::lock_guard<std::mutex> lock(mThreadPoolMutex);
|
||||
terminateThreadsLocked();
|
||||
}
|
||||
|
||||
void ChoreographerFilter::onChoreographer() {
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mLastTimestamp = std::chrono::steady_clock::now();
|
||||
++mSequenceNumber;
|
||||
mCondition.notify_all();
|
||||
}
|
||||
|
||||
void ChoreographerFilter::launchThreadsLocked() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mIsRunning = true;
|
||||
}
|
||||
|
||||
const int32_t numThreads = getNumCpus() > 2 ? 2 : 1;
|
||||
for (int32_t thread = 0; thread < numThreads; ++thread) {
|
||||
mThreadPool.push_back(
|
||||
Thread([this, thread]() { threadMain(mUseAffinity, thread); }));
|
||||
}
|
||||
}
|
||||
|
||||
void ChoreographerFilter::terminateThreadsLocked() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mIsRunning = false;
|
||||
mCondition.notify_all();
|
||||
}
|
||||
|
||||
for (auto& thread : mThreadPool) {
|
||||
thread.join();
|
||||
}
|
||||
mThreadPool.clear();
|
||||
}
|
||||
|
||||
void ChoreographerFilter::onSettingsChanged() {
|
||||
const bool useAffinity = Settings::getInstance()->getUseAffinity();
|
||||
const Settings::DisplayTimings& displayTimings =
|
||||
Settings::getInstance()->getDisplayTimings();
|
||||
std::lock_guard<std::mutex> lock(mThreadPoolMutex);
|
||||
if (useAffinity == mUseAffinity &&
|
||||
mRefreshPeriod == displayTimings.refreshPeriod) {
|
||||
return;
|
||||
}
|
||||
|
||||
terminateThreadsLocked();
|
||||
mUseAffinity = useAffinity;
|
||||
mRefreshPeriod = displayTimings.refreshPeriod;
|
||||
mAppToSfDelay = displayTimings.sfOffset - displayTimings.appOffset;
|
||||
ALOGV(
|
||||
"onSettingsChanged(): refreshPeriod=%lld, appOffset=%lld, "
|
||||
"sfOffset=%lld",
|
||||
(long long)displayTimings.refreshPeriod.count(),
|
||||
(long long)displayTimings.appOffset.count(),
|
||||
(long long)displayTimings.sfOffset.count());
|
||||
launchThreadsLocked();
|
||||
}
|
||||
|
||||
void ChoreographerFilter::threadMain(bool useAffinity, int32_t thread) {
|
||||
Timer timer(mRefreshPeriod, mAppToSfDelay);
|
||||
|
||||
{
|
||||
int cpu = getNumCpus() - 1 - thread;
|
||||
if (cpu >= 0) {
|
||||
setAffinity(cpu);
|
||||
}
|
||||
}
|
||||
|
||||
std::string threadName = "Filter";
|
||||
threadName += swappy::to_string(thread);
|
||||
pthread_setname_np(pthread_self(), threadName.c_str());
|
||||
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
while (true) {
|
||||
auto timestamp = mLastTimestamp;
|
||||
auto workDuration = mWorkDuration;
|
||||
lock.unlock();
|
||||
|
||||
// If we have received the same timestamp multiple times, it probably
|
||||
// means that the app has stopped sending them to us, which could
|
||||
// indicate that it's no longer running. If we detect that, we stop
|
||||
// until we see a fresh timestamp to avoid spinning forever in the
|
||||
// background.
|
||||
if (!timer.addTimestamp(timestamp)) {
|
||||
lock.lock();
|
||||
mCondition.wait(lock, [=]() {
|
||||
return !mIsRunning || (mLastTimestamp != timestamp);
|
||||
});
|
||||
timestamp = mLastTimestamp;
|
||||
lock.unlock();
|
||||
timer.addTimestamp(timestamp);
|
||||
}
|
||||
|
||||
if (!mIsRunning) break;
|
||||
|
||||
timer.sleep(-workDuration);
|
||||
{
|
||||
std::unique_lock<std::mutex> workLock(mWorkMutex);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - mLastWorkRun > mRefreshPeriod / 2) {
|
||||
// Assume we got here first and there's work to do
|
||||
gamesdk::ScopedTrace trace("doWork");
|
||||
mWorkDuration = mDoWork();
|
||||
mLastWorkRun = now;
|
||||
}
|
||||
}
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "Settings.h"
|
||||
#include "Thread.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
class ChoreographerFilter {
|
||||
public:
|
||||
using Worker = std::function<std::chrono::nanoseconds()>;
|
||||
|
||||
explicit ChoreographerFilter(std::chrono::nanoseconds refreshPeriod,
|
||||
std::chrono::nanoseconds appToSfDelay,
|
||||
Worker doWork);
|
||||
~ChoreographerFilter();
|
||||
|
||||
void onChoreographer();
|
||||
|
||||
private:
|
||||
void launchThreadsLocked();
|
||||
void terminateThreadsLocked();
|
||||
|
||||
void onSettingsChanged();
|
||||
|
||||
void threadMain(bool useAffinity, int32_t thread);
|
||||
|
||||
std::mutex mThreadPoolMutex;
|
||||
bool mUseAffinity = true;
|
||||
std::vector<Thread> mThreadPool;
|
||||
|
||||
std::mutex mMutex;
|
||||
std::condition_variable mCondition;
|
||||
bool mIsRunning = true;
|
||||
int64_t mSequenceNumber = 0;
|
||||
std::chrono::steady_clock::time_point mLastTimestamp;
|
||||
|
||||
std::mutex mWorkMutex;
|
||||
std::chrono::steady_clock::time_point mLastWorkRun;
|
||||
std::chrono::nanoseconds mWorkDuration;
|
||||
|
||||
std::chrono::nanoseconds mRefreshPeriod;
|
||||
std::chrono::nanoseconds mAppToSfDelay;
|
||||
const Worker mDoWork;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,520 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#define LOG_TAG "ChoreographerThread"
|
||||
|
||||
#include "ChoreographerThread.h"
|
||||
|
||||
#include <android/looper.h>
|
||||
#include <jni.h>
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
|
||||
#include "ChoreographerShim.h"
|
||||
#include "CpuInfo.h"
|
||||
#include "JNIUtil.h"
|
||||
#include "Log.h"
|
||||
#include "Settings.h"
|
||||
#include "Thread.h"
|
||||
#include "Trace.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
// AChoreographer is supported from API 24. To allow compilation for minSDK < 24
|
||||
// and still use AChoreographer for SDK >= 24 we need runtime support to call
|
||||
// AChoreographer APIs.
|
||||
|
||||
using PFN_AChoreographer_getInstance = AChoreographer *(*)();
|
||||
|
||||
using PFN_AChoreographer_postFrameCallback =
|
||||
void (*)(AChoreographer *choreographer,
|
||||
AChoreographer_frameCallback callback, void *data);
|
||||
|
||||
using PFN_AChoreographer_postFrameCallbackDelayed = void (*)(
|
||||
AChoreographer *choreographer, AChoreographer_frameCallback callback,
|
||||
void *data, long delayMillis);
|
||||
|
||||
using PFN_AChoreographer_registerRefreshRateCallback =
|
||||
void (*)(AChoreographer *choreographer,
|
||||
AChoreographer_refreshRateCallback callback, void *data);
|
||||
|
||||
using PFN_AChoreographer_unregisterRefreshRateCallback =
|
||||
void (*)(AChoreographer *choreographer,
|
||||
AChoreographer_refreshRateCallback callback, void *data);
|
||||
|
||||
// Forward declaration of the native method of Java Choreographer class
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_google_androidgamesdk_ChoreographerCallback_nOnChoreographer(
|
||||
JNIEnv * /*env*/, jobject /*this*/, jlong cookie, jlong /*frameTimeNanos*/);
|
||||
}
|
||||
|
||||
class NDKChoreographerThread : public ChoreographerThread {
|
||||
public:
|
||||
static constexpr int MIN_SDK_VERSION = 24;
|
||||
|
||||
NDKChoreographerThread(Callback onChoreographer,
|
||||
Callback onRefreshRateChanged);
|
||||
~NDKChoreographerThread() override;
|
||||
|
||||
private:
|
||||
void looperThread();
|
||||
void scheduleNextFrameCallback() override REQUIRES(mWaitingMutex);
|
||||
|
||||
PFN_AChoreographer_getInstance mAChoreographer_getInstance = nullptr;
|
||||
PFN_AChoreographer_postFrameCallback mAChoreographer_postFrameCallback =
|
||||
nullptr;
|
||||
PFN_AChoreographer_postFrameCallbackDelayed
|
||||
mAChoreographer_postFrameCallbackDelayed = nullptr;
|
||||
PFN_AChoreographer_registerRefreshRateCallback
|
||||
mAChoreographer_registerRefreshRateCallback = nullptr;
|
||||
PFN_AChoreographer_unregisterRefreshRateCallback
|
||||
mAChoreographer_unregisterRefreshRateCallback = nullptr;
|
||||
void *mLibAndroid = nullptr;
|
||||
Thread mThread;
|
||||
std::condition_variable mWaitingCondition;
|
||||
ALooper *mLooper GUARDED_BY(mWaitingMutex) = nullptr;
|
||||
bool mThreadRunning GUARDED_BY(mWaitingMutex) = false;
|
||||
AChoreographer *mChoreographer GUARDED_BY(mWaitingMutex) = nullptr;
|
||||
Callback mOnRefreshRateChanged;
|
||||
};
|
||||
|
||||
NDKChoreographerThread::NDKChoreographerThread(Callback onChoreographer,
|
||||
Callback onRefreshRateChanged)
|
||||
: ChoreographerThread(onChoreographer),
|
||||
mOnRefreshRateChanged(onRefreshRateChanged) {
|
||||
mLibAndroid = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (mLibAndroid == nullptr) {
|
||||
ALOGE("FATAL: cannot open libandroid.so: %s", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
mAChoreographer_getInstance =
|
||||
reinterpret_cast<PFN_AChoreographer_getInstance>(
|
||||
dlsym(mLibAndroid, "AChoreographer_getInstance"));
|
||||
|
||||
mAChoreographer_postFrameCallback =
|
||||
reinterpret_cast<PFN_AChoreographer_postFrameCallback>(
|
||||
dlsym(mLibAndroid, "AChoreographer_postFrameCallback"));
|
||||
|
||||
mAChoreographer_postFrameCallbackDelayed =
|
||||
reinterpret_cast<PFN_AChoreographer_postFrameCallbackDelayed>(
|
||||
dlsym(mLibAndroid, "AChoreographer_postFrameCallbackDelayed"));
|
||||
|
||||
mAChoreographer_registerRefreshRateCallback =
|
||||
reinterpret_cast<PFN_AChoreographer_registerRefreshRateCallback>(
|
||||
dlsym(mLibAndroid, "AChoreographer_registerRefreshRateCallback"));
|
||||
|
||||
mAChoreographer_unregisterRefreshRateCallback =
|
||||
reinterpret_cast<PFN_AChoreographer_unregisterRefreshRateCallback>(
|
||||
dlsym(mLibAndroid, "AChoreographer_unregisterRefreshRateCallback"));
|
||||
|
||||
if (!mAChoreographer_getInstance || !mAChoreographer_postFrameCallback ||
|
||||
!mAChoreographer_postFrameCallbackDelayed) {
|
||||
ALOGE("FATAL: cannot get AChoreographer symbols");
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(mWaitingMutex);
|
||||
// create a new ALooper thread to get Choreographer events
|
||||
mThreadRunning = true;
|
||||
mThread = Thread([this]() { looperThread(); });
|
||||
mWaitingCondition.wait(lock, [&]() REQUIRES(mWaitingMutex) {
|
||||
return mChoreographer != nullptr;
|
||||
});
|
||||
|
||||
mInitialized = true;
|
||||
}
|
||||
|
||||
NDKChoreographerThread::~NDKChoreographerThread() {
|
||||
ALOGI("Destroying NDKChoreographerThread");
|
||||
if (mLibAndroid != nullptr) dlclose(mLibAndroid);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mWaitingMutex);
|
||||
if (!mLooper) {
|
||||
return;
|
||||
}
|
||||
ALooper_acquire(mLooper);
|
||||
mThreadRunning = false;
|
||||
ALooper_wake(mLooper);
|
||||
}
|
||||
mThread.join();
|
||||
ALooper_release(mLooper);
|
||||
}
|
||||
|
||||
void NDKChoreographerThread::looperThread() {
|
||||
int outFd, outEvents;
|
||||
void *outData;
|
||||
std::lock_guard<std::mutex> lock(mWaitingMutex);
|
||||
|
||||
mLooper = ALooper_prepare(0);
|
||||
if (!mLooper) {
|
||||
ALOGE("ALooper_prepare failed");
|
||||
return;
|
||||
}
|
||||
|
||||
mChoreographer = mAChoreographer_getInstance();
|
||||
if (!mChoreographer) {
|
||||
ALOGE("AChoreographer_getInstance failed");
|
||||
return;
|
||||
}
|
||||
|
||||
AChoreographer_refreshRateCallback callback = [](int64_t vsyncPeriodNanos,
|
||||
void *data) {
|
||||
reinterpret_cast<NDKChoreographerThread *>(data)
|
||||
->mOnRefreshRateChanged();
|
||||
};
|
||||
|
||||
if (mAChoreographer_registerRefreshRateCallback && mOnRefreshRateChanged) {
|
||||
mAChoreographer_registerRefreshRateCallback(mChoreographer, callback,
|
||||
this);
|
||||
}
|
||||
mWaitingCondition.notify_all();
|
||||
|
||||
const char *name = "SwappyChoreographer";
|
||||
|
||||
CpuInfo cpu;
|
||||
cpu_set_t cpu_set;
|
||||
CPU_ZERO(&cpu_set);
|
||||
CPU_SET(0, &cpu_set);
|
||||
|
||||
if (cpu.getNumberOfCpus() > 0) {
|
||||
ALOGI("Swappy found %d CPUs [%s].", cpu.getNumberOfCpus(),
|
||||
cpu.getHardware().c_str());
|
||||
if (cpu.getNumberOfLittleCores() > 0) {
|
||||
cpu_set = cpu.getLittleCoresMask();
|
||||
}
|
||||
}
|
||||
|
||||
const auto tid = gettid();
|
||||
ALOGI("Setting '%s' thread [%d-0x%x] affinity mask to 0x%x.", name, tid,
|
||||
tid, to_mask(cpu_set));
|
||||
sched_setaffinity(tid, sizeof(cpu_set), &cpu_set);
|
||||
|
||||
pthread_setname_np(pthread_self(), name);
|
||||
|
||||
while (mThreadRunning) {
|
||||
// mutex should be unlocked before sleeping on pollAll
|
||||
mWaitingMutex.unlock();
|
||||
ALooper_pollAll(-1, &outFd, &outEvents, &outData);
|
||||
mWaitingMutex.lock();
|
||||
}
|
||||
if (mAChoreographer_unregisterRefreshRateCallback &&
|
||||
mOnRefreshRateChanged) {
|
||||
mAChoreographer_unregisterRefreshRateCallback(mChoreographer, callback,
|
||||
this);
|
||||
}
|
||||
ALOGI("Terminating Looper thread");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void NDKChoreographerThread::scheduleNextFrameCallback() {
|
||||
AChoreographer_frameCallback frameCallback = [](long frameTimeNanos,
|
||||
void *data) {
|
||||
reinterpret_cast<NDKChoreographerThread *>(data)->onChoreographer();
|
||||
};
|
||||
|
||||
mAChoreographer_postFrameCallbackDelayed(mChoreographer, frameCallback,
|
||||
this, 1);
|
||||
}
|
||||
|
||||
class JavaChoreographerThread : public ChoreographerThread {
|
||||
public:
|
||||
JavaChoreographerThread(JavaVM *vm, jobject jactivity,
|
||||
Callback onChoreographer);
|
||||
~JavaChoreographerThread() override;
|
||||
static void onChoreographer(jlong cookie);
|
||||
void onChoreographer() override { ChoreographerThread::onChoreographer(); };
|
||||
|
||||
private:
|
||||
void scheduleNextFrameCallback() override REQUIRES(mWaitingMutex);
|
||||
|
||||
JavaVM *mJVM;
|
||||
jobject mJobj = nullptr;
|
||||
jmethodID mJpostFrameCallback = nullptr;
|
||||
jmethodID mJterminate = nullptr;
|
||||
};
|
||||
|
||||
JavaChoreographerThread::JavaChoreographerThread(JavaVM *vm, jobject jactivity,
|
||||
Callback onChoreographer)
|
||||
: ChoreographerThread(onChoreographer), mJVM(vm) {
|
||||
if (!vm || !jactivity) {
|
||||
return;
|
||||
}
|
||||
JNIEnv *env;
|
||||
mJVM->AttachCurrentThread(&env, nullptr);
|
||||
|
||||
jclass choreographerCallbackClass = gamesdk::loadClass(
|
||||
env, jactivity, ChoreographerThread::CT_CLASS,
|
||||
(JNINativeMethod *)ChoreographerThread::CTNativeMethods,
|
||||
ChoreographerThread::CTNativeMethodsSize);
|
||||
|
||||
if (!choreographerCallbackClass) return;
|
||||
|
||||
jmethodID constructor =
|
||||
env->GetMethodID(choreographerCallbackClass, "<init>", "(J)V");
|
||||
|
||||
mJpostFrameCallback = env->GetMethodID(choreographerCallbackClass,
|
||||
"postFrameCallback", "()V");
|
||||
|
||||
mJterminate =
|
||||
env->GetMethodID(choreographerCallbackClass, "terminate", "()V");
|
||||
|
||||
jobject choreographerCallback = env->NewObject(
|
||||
choreographerCallbackClass, constructor, reinterpret_cast<jlong>(this));
|
||||
|
||||
mJobj = env->NewGlobalRef(choreographerCallback);
|
||||
|
||||
mInitialized = true;
|
||||
}
|
||||
|
||||
JavaChoreographerThread::~JavaChoreographerThread() {
|
||||
ALOGI("Destroying JavaChoreographerThread");
|
||||
|
||||
if (!mJobj) {
|
||||
return;
|
||||
}
|
||||
|
||||
JNIEnv *env;
|
||||
// Check if we need to attach and only detach if we do.
|
||||
jint result =
|
||||
mJVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_2);
|
||||
if (result != JNI_OK) {
|
||||
if (result == JNI_EVERSION) {
|
||||
result =
|
||||
mJVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_1);
|
||||
}
|
||||
if (result == JNI_EDETACHED) {
|
||||
mJVM->AttachCurrentThread(&env, nullptr);
|
||||
}
|
||||
}
|
||||
env->CallVoidMethod(mJobj, mJterminate);
|
||||
env->DeleteGlobalRef(mJobj);
|
||||
if (result == JNI_EDETACHED) {
|
||||
mJVM->DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
|
||||
void JavaChoreographerThread::scheduleNextFrameCallback() {
|
||||
JNIEnv *env;
|
||||
mJVM->AttachCurrentThread(&env, nullptr);
|
||||
env->CallVoidMethod(mJobj, mJpostFrameCallback);
|
||||
}
|
||||
|
||||
void JavaChoreographerThread::onChoreographer(jlong cookie) {
|
||||
JavaChoreographerThread *me =
|
||||
reinterpret_cast<JavaChoreographerThread *>(cookie);
|
||||
me->onChoreographer();
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_google_androidgamesdk_ChoreographerCallback_nOnChoreographer(
|
||||
JNIEnv * /*env*/, jobject /*this*/, jlong cookie,
|
||||
jlong /*frameTimeNanos*/) {
|
||||
JavaChoreographerThread::onChoreographer(cookie);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
class NoChoreographerThread : public ChoreographerThread {
|
||||
public:
|
||||
NoChoreographerThread(Callback onChoreographer);
|
||||
~NoChoreographerThread();
|
||||
|
||||
private:
|
||||
void postFrameCallbacks() override;
|
||||
void scheduleNextFrameCallback() override REQUIRES(mWaitingMutex);
|
||||
void looperThread();
|
||||
void onSettingsChanged();
|
||||
|
||||
Thread mThread;
|
||||
bool mThreadRunning GUARDED_BY(mWaitingMutex);
|
||||
std::condition_variable_any mWaitingCondition GUARDED_BY(mWaitingMutex);
|
||||
std::chrono::nanoseconds mRefreshPeriod GUARDED_BY(mWaitingMutex);
|
||||
};
|
||||
|
||||
NoChoreographerThread::NoChoreographerThread(Callback onChoreographer)
|
||||
: ChoreographerThread(onChoreographer) {
|
||||
std::lock_guard<std::mutex> lock(mWaitingMutex);
|
||||
Settings::getInstance()->addListener([this]() { onSettingsChanged(); });
|
||||
mThreadRunning = true;
|
||||
mThread = Thread([this]() { looperThread(); });
|
||||
mInitialized = true;
|
||||
}
|
||||
|
||||
NoChoreographerThread::~NoChoreographerThread() {
|
||||
ALOGI("Destroying NoChoreographerThread");
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mWaitingMutex);
|
||||
mThreadRunning = false;
|
||||
}
|
||||
mWaitingCondition.notify_all();
|
||||
mThread.join();
|
||||
}
|
||||
|
||||
void NoChoreographerThread::onSettingsChanged() {
|
||||
const Settings::DisplayTimings &displayTimings =
|
||||
Settings::getInstance()->getDisplayTimings();
|
||||
std::lock_guard<std::mutex> lock(mWaitingMutex);
|
||||
mRefreshPeriod = displayTimings.refreshPeriod;
|
||||
ALOGV("onSettingsChanged(): refreshPeriod=%lld",
|
||||
(long long)displayTimings.refreshPeriod.count());
|
||||
}
|
||||
|
||||
void NoChoreographerThread::looperThread() {
|
||||
const char *name = "SwappyChoreographer";
|
||||
|
||||
CpuInfo cpu;
|
||||
cpu_set_t cpu_set;
|
||||
CPU_ZERO(&cpu_set);
|
||||
CPU_SET(0, &cpu_set);
|
||||
|
||||
if (cpu.getNumberOfCpus() > 0) {
|
||||
ALOGI("Swappy found %d CPUs [%s].", cpu.getNumberOfCpus(),
|
||||
cpu.getHardware().c_str());
|
||||
if (cpu.getNumberOfLittleCores() > 0) {
|
||||
cpu_set = cpu.getLittleCoresMask();
|
||||
}
|
||||
}
|
||||
|
||||
const auto tid = gettid();
|
||||
ALOGI("Setting '%s' thread [%d-0x%x] affinity mask to 0x%x.", name, tid,
|
||||
tid, to_mask(cpu_set));
|
||||
sched_setaffinity(tid, sizeof(cpu_set), &cpu_set);
|
||||
|
||||
pthread_setname_np(pthread_self(), name);
|
||||
|
||||
auto wakeTime = std::chrono::steady_clock::now();
|
||||
|
||||
while (true) {
|
||||
{
|
||||
// mutex should be unlocked before sleeping
|
||||
std::lock_guard<std::mutex> lock(mWaitingMutex);
|
||||
if (!mThreadRunning) {
|
||||
break;
|
||||
}
|
||||
mWaitingCondition.wait(mWaitingMutex);
|
||||
if (!mThreadRunning) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto timePassed = std::chrono::steady_clock::now() - wakeTime;
|
||||
const int intervals = std::floor(timePassed / mRefreshPeriod);
|
||||
wakeTime += (intervals + 1) * mRefreshPeriod;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_until(wakeTime);
|
||||
mCallback();
|
||||
}
|
||||
ALOGI("Terminating choreographer thread");
|
||||
}
|
||||
|
||||
void NoChoreographerThread::postFrameCallbacks() {
|
||||
std::lock_guard<std::mutex> lock(mWaitingMutex);
|
||||
mWaitingCondition.notify_one();
|
||||
}
|
||||
|
||||
void NoChoreographerThread::scheduleNextFrameCallback() {}
|
||||
|
||||
const char *ChoreographerThread::CT_CLASS =
|
||||
"com/google/androidgamesdk/ChoreographerCallback";
|
||||
|
||||
const JNINativeMethod ChoreographerThread::CTNativeMethods[] = {
|
||||
{"nOnChoreographer", "(JJ)V",
|
||||
(void
|
||||
*)&Java_com_google_androidgamesdk_ChoreographerCallback_nOnChoreographer}};
|
||||
|
||||
ChoreographerThread::ChoreographerThread(Callback onChoreographer)
|
||||
: mCallback(onChoreographer) {}
|
||||
|
||||
ChoreographerThread::~ChoreographerThread() = default;
|
||||
|
||||
void ChoreographerThread::postFrameCallbacks() {
|
||||
TRACE_CALL();
|
||||
|
||||
// This method is called before calling to swap buffers
|
||||
// It registers to get MAX_CALLBACKS_BEFORE_IDLE frame callbacks before
|
||||
// going idle so if app goes to idle the thread will not get further frame
|
||||
// callbacks
|
||||
std::lock_guard<std::mutex> lock(mWaitingMutex);
|
||||
if (mCallbacksBeforeIdle == 0) {
|
||||
scheduleNextFrameCallback();
|
||||
}
|
||||
mCallbacksBeforeIdle = MAX_CALLBACKS_BEFORE_IDLE;
|
||||
}
|
||||
|
||||
void ChoreographerThread::onChoreographer() {
|
||||
TRACE_CALL();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mWaitingMutex);
|
||||
mCallbacksBeforeIdle--;
|
||||
|
||||
if (mCallbacksBeforeIdle > 0) {
|
||||
scheduleNextFrameCallback();
|
||||
}
|
||||
}
|
||||
mCallback();
|
||||
}
|
||||
|
||||
std::unique_ptr<ChoreographerThread>
|
||||
ChoreographerThread::createChoreographerThread(Type type, JavaVM *vm,
|
||||
jobject jactivity,
|
||||
Callback onChoreographer,
|
||||
Callback onRefreshRateChanged,
|
||||
SdkVersion sdkVersion) {
|
||||
if (type == Type::App) {
|
||||
ALOGI("Using Application's Choreographer");
|
||||
return std::make_unique<NoChoreographerThread>(onChoreographer);
|
||||
}
|
||||
|
||||
if (vm == nullptr ||
|
||||
sdkVersion.sdkInt >= NDKChoreographerThread::MIN_SDK_VERSION) {
|
||||
ALOGI("Using NDK Choreographer");
|
||||
const auto usingDisplayManager =
|
||||
SwappyDisplayManager::useSwappyDisplayManager(sdkVersion);
|
||||
const auto refreshRateCallback =
|
||||
usingDisplayManager ? Callback() : onRefreshRateChanged;
|
||||
return std::make_unique<NDKChoreographerThread>(onChoreographer,
|
||||
refreshRateCallback);
|
||||
}
|
||||
|
||||
if (vm != nullptr && jactivity != nullptr) {
|
||||
std::unique_ptr<ChoreographerThread> javaChoreographerThread =
|
||||
std::make_unique<JavaChoreographerThread>(vm, jactivity,
|
||||
onChoreographer);
|
||||
if (javaChoreographerThread->isInitialized()) {
|
||||
ALOGI("Using Java Choreographer");
|
||||
return javaChoreographerThread;
|
||||
}
|
||||
}
|
||||
|
||||
ALOGI("Using no Choreographer (Best Effort)");
|
||||
return std::make_unique<NoChoreographerThread>(onChoreographer);
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "SwappyDisplayManager.h"
|
||||
#include "Thread.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
class ChoreographerThread {
|
||||
public:
|
||||
enum class Type {
|
||||
// choreographer ticks are provided by application
|
||||
App,
|
||||
|
||||
// register internally with choreographer
|
||||
Swappy,
|
||||
};
|
||||
|
||||
static const char* CT_CLASS;
|
||||
static const JNINativeMethod CTNativeMethods[];
|
||||
static constexpr int CTNativeMethodsSize = 1;
|
||||
|
||||
using Callback = std::function<void()>;
|
||||
|
||||
static std::unique_ptr<ChoreographerThread> createChoreographerThread(
|
||||
Type type, JavaVM* vm, jobject jactivity, Callback onChoreographer,
|
||||
Callback onRefreshRateChanged, SdkVersion sdkVersion);
|
||||
|
||||
virtual ~ChoreographerThread() = 0;
|
||||
|
||||
virtual void postFrameCallbacks();
|
||||
|
||||
bool isInitialized() { return mInitialized; }
|
||||
|
||||
protected:
|
||||
ChoreographerThread(Callback onChoreographer);
|
||||
virtual void scheduleNextFrameCallback() REQUIRES(mWaitingMutex) = 0;
|
||||
virtual void onChoreographer();
|
||||
|
||||
std::mutex mWaitingMutex;
|
||||
int mCallbacksBeforeIdle GUARDED_BY(mWaitingMutex) = 0;
|
||||
Callback mCallback;
|
||||
bool mInitialized = false;
|
||||
|
||||
static constexpr int MAX_CALLBACKS_BEFORE_IDLE = 10;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#define LOG_TAG "Swappy"
|
||||
|
||||
#include "CpuInfo.h"
|
||||
|
||||
#include <bitset>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
#include "Log.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool startsWith(std::string &mainStr, const char *toMatch) {
|
||||
// std::string::find returns 0 if toMatch is found at beginning
|
||||
return mainStr.find(toMatch) == 0;
|
||||
}
|
||||
|
||||
std::vector<std::string> split(const std::string &s, char c) {
|
||||
std::vector<std::string> v;
|
||||
std::string::size_type i = 0;
|
||||
std::string::size_type j = s.find(c);
|
||||
|
||||
while (j != std::string::npos) {
|
||||
v.push_back(s.substr(i, j - i));
|
||||
i = ++j;
|
||||
j = s.find(c, j);
|
||||
|
||||
if (j == std::string::npos) {
|
||||
v.push_back(s.substr(i, s.length()));
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::string &path) {
|
||||
char buf[10240];
|
||||
FILE *fp = fopen(path.c_str(), "r");
|
||||
if (fp == nullptr) return std::string();
|
||||
|
||||
fgets(buf, 10240, fp);
|
||||
fclose(fp);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
namespace swappy {
|
||||
|
||||
std::string to_string(int n) {
|
||||
constexpr int kBufSize = 12; // strlen("−2147483648")+1
|
||||
static char buf[kBufSize];
|
||||
snprintf(buf, kBufSize, "%d", n);
|
||||
return buf;
|
||||
}
|
||||
|
||||
CpuInfo::CpuInfo() {
|
||||
const auto BUFFER_LENGTH = 10240;
|
||||
|
||||
char buf[BUFFER_LENGTH];
|
||||
FILE *fp = fopen("/proc/cpuinfo", "r");
|
||||
|
||||
if (!fp) {
|
||||
return;
|
||||
}
|
||||
|
||||
long mMaxFrequency = 0;
|
||||
long mMinFrequency = std::numeric_limits<long>::max();
|
||||
|
||||
while (fgets(buf, BUFFER_LENGTH, fp) != NULL) {
|
||||
buf[strlen(buf) - 1] = '\0'; // eat the newline fgets() stores
|
||||
std::string line = buf;
|
||||
|
||||
if (startsWith(line, "processor")) {
|
||||
Cpu core;
|
||||
core.id = mCpus.size();
|
||||
|
||||
auto core_path =
|
||||
std::string("/sys/devices/system/cpu/cpu") + to_string(core.id);
|
||||
|
||||
auto package_id =
|
||||
ReadFile(core_path + "/topology/physical_package_id");
|
||||
auto frequency = ReadFile(core_path + "/cpufreq/cpuinfo_max_freq");
|
||||
|
||||
core.package_id = atol(package_id.c_str());
|
||||
core.frequency = atol(frequency.c_str());
|
||||
|
||||
mMinFrequency = std::min(mMinFrequency, core.frequency);
|
||||
mMaxFrequency = std::max(mMaxFrequency, core.frequency);
|
||||
|
||||
mCpus.push_back(core);
|
||||
} else if (startsWith(line, "Hardware")) {
|
||||
mHardware = split(line, ':')[1];
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
CPU_ZERO(&mLittleCoresMask);
|
||||
CPU_ZERO(&mBigCoresMask);
|
||||
|
||||
for (auto cpu : mCpus) {
|
||||
if (cpu.frequency == mMinFrequency) {
|
||||
++mNumberOfLittleCores;
|
||||
cpu.type = Cpu::Type::Little;
|
||||
CPU_SET(cpu.id, &mLittleCoresMask);
|
||||
} else {
|
||||
++mNumberOfBigCores;
|
||||
cpu.type = Cpu::Type::Big;
|
||||
CPU_SET(cpu.id, &mBigCoresMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int CpuInfo::getNumberOfCpus() const { return mCpus.size(); }
|
||||
|
||||
const std::vector<CpuInfo::Cpu> &CpuInfo::getCpus() const { return mCpus; }
|
||||
|
||||
const std::string CpuInfo::getHardware() const { return mHardware; }
|
||||
|
||||
unsigned int CpuInfo::getNumberOfLittleCores() const {
|
||||
return mNumberOfLittleCores;
|
||||
}
|
||||
|
||||
unsigned int CpuInfo::getNumberOfBigCores() const { return mNumberOfBigCores; }
|
||||
|
||||
cpu_set_t CpuInfo::getLittleCoresMask() const { return mLittleCoresMask; }
|
||||
|
||||
cpu_set_t CpuInfo::getBigCoresMask() const { return mBigCoresMask; }
|
||||
|
||||
unsigned int to_mask(cpu_set_t cpu_set) {
|
||||
std::bitset<32> mask;
|
||||
|
||||
for (int i = 0; i < CPU_SETSIZE; ++i) {
|
||||
if (CPU_ISSET(i, &cpu_set)) mask[i] = 1;
|
||||
}
|
||||
return (int)mask.to_ulong();
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sched.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace swappy {
|
||||
|
||||
class CpuInfo {
|
||||
public:
|
||||
struct Cpu {
|
||||
enum class Type { Little, Big };
|
||||
|
||||
int id;
|
||||
int package_id;
|
||||
long frequency;
|
||||
|
||||
Type type;
|
||||
};
|
||||
|
||||
CpuInfo();
|
||||
|
||||
unsigned int getNumberOfCpus() const;
|
||||
|
||||
const std::vector<Cpu>& getCpus() const;
|
||||
const std::string getHardware() const;
|
||||
|
||||
unsigned int getNumberOfLittleCores() const;
|
||||
unsigned int getNumberOfBigCores() const;
|
||||
|
||||
cpu_set_t getLittleCoresMask() const;
|
||||
cpu_set_t getBigCoresMask() const;
|
||||
|
||||
private:
|
||||
std::vector<Cpu> mCpus;
|
||||
std::string mHardware;
|
||||
|
||||
unsigned int mNumberOfLittleCores = 0;
|
||||
unsigned int mNumberOfBigCores = 0;
|
||||
|
||||
cpu_set_t mLittleCoresMask;
|
||||
cpu_set_t mBigCoresMask;
|
||||
};
|
||||
|
||||
unsigned int to_mask(cpu_set_t cpu_set);
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <swappy/swappyGL_extra.h>
|
||||
|
||||
namespace swappy {
|
||||
|
||||
class FrameStatistics {
|
||||
public:
|
||||
virtual ~FrameStatistics() {}
|
||||
virtual int32_t lastLatencyRecorded() const = 0;
|
||||
// Only the essential latency statistics, not full.
|
||||
virtual bool isEssential() const = 0;
|
||||
virtual SwappyStats getStats() = 0;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "Settings.h"
|
||||
|
||||
#define LOG_TAG "Settings"
|
||||
|
||||
#include "Log.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
std::unique_ptr<Settings> Settings::instance;
|
||||
|
||||
Settings* Settings::getInstance() {
|
||||
if (!instance) {
|
||||
instance = std::make_unique<Settings>(ConstructorTag{});
|
||||
}
|
||||
return instance.get();
|
||||
}
|
||||
|
||||
void Settings::reset() { instance.reset(); }
|
||||
|
||||
void Settings::addListener(Listener listener) {
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mListeners.emplace_back(std::move(listener));
|
||||
}
|
||||
|
||||
void Settings::setDisplayTimings(const DisplayTimings& displayTimings) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mDisplayTimings = displayTimings;
|
||||
}
|
||||
// Notify the listeners without the lock held
|
||||
notifyListeners();
|
||||
}
|
||||
void Settings::setSwapDuration(uint64_t swapNs) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mSwapDuration = std::chrono::nanoseconds(swapNs);
|
||||
}
|
||||
// Notify the listeners without the lock held
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void Settings::setUseAffinity(bool tf) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mUseAffinity = tf;
|
||||
}
|
||||
// Notify the listeners without the lock held
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
const Settings::DisplayTimings& Settings::getDisplayTimings() const {
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mDisplayTimings;
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds Settings::getSwapDuration() const {
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mSwapDuration;
|
||||
}
|
||||
|
||||
bool Settings::getUseAffinity() const {
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mUseAffinity;
|
||||
}
|
||||
|
||||
void Settings::notifyListeners() {
|
||||
// Grab a local copy of the listeners
|
||||
std::vector<Listener> listeners;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
listeners = mListeners;
|
||||
}
|
||||
|
||||
// Call the listeners without the lock held
|
||||
for (const auto& listener : listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Thread.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
class Settings {
|
||||
private:
|
||||
// Allows construction with std::unique_ptr from a static method, but
|
||||
// disallows construction outside of the class since no one else can
|
||||
// construct a ConstructorTag
|
||||
struct ConstructorTag {};
|
||||
|
||||
public:
|
||||
struct DisplayTimings {
|
||||
std::chrono::nanoseconds refreshPeriod{0};
|
||||
std::chrono::nanoseconds appOffset{0};
|
||||
std::chrono::nanoseconds sfOffset{0};
|
||||
};
|
||||
|
||||
explicit Settings(ConstructorTag){};
|
||||
|
||||
static Settings* getInstance();
|
||||
|
||||
static void reset();
|
||||
|
||||
using Listener = std::function<void()>;
|
||||
void addListener(Listener listener);
|
||||
|
||||
void setDisplayTimings(const DisplayTimings& displayTimings);
|
||||
void setSwapDuration(uint64_t swapNs);
|
||||
void setUseAffinity(bool);
|
||||
|
||||
const DisplayTimings& getDisplayTimings() const;
|
||||
std::chrono::nanoseconds getSwapDuration() const;
|
||||
bool getUseAffinity() const;
|
||||
|
||||
private:
|
||||
void notifyListeners();
|
||||
|
||||
static std::unique_ptr<Settings> instance;
|
||||
|
||||
mutable std::mutex mMutex;
|
||||
std::vector<Listener> mListeners GUARDED_BY(mMutex);
|
||||
|
||||
DisplayTimings mDisplayTimings GUARDED_BY(mMutex);
|
||||
std::chrono::nanoseconds mSwapDuration GUARDED_BY(mMutex) =
|
||||
std::chrono::nanoseconds(16'666'667L);
|
||||
bool mUseAffinity GUARDED_BY(mMutex) = true;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,368 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "CPUTracer.h"
|
||||
#include "ChoreographerFilter.h"
|
||||
#include "ChoreographerThread.h"
|
||||
#include "FrameStatistics.h"
|
||||
#include "SwappyDisplayManager.h"
|
||||
#include "Thread.h"
|
||||
#include "swappy/swappyGL.h"
|
||||
#include "swappy/swappyGL_extra.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
// ANativeWindow_setFrameRate is supported from API 30. To allow compilation for
|
||||
// minSDK < 30 we need runtime support to call this API.
|
||||
using PFN_ANativeWindow_setFrameRate = int32_t (*)(ANativeWindow* window,
|
||||
float frameRate,
|
||||
int8_t compatibility);
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
struct SwappyCommonSettings {
|
||||
SdkVersion sdkVersion;
|
||||
|
||||
std::chrono::nanoseconds refreshPeriod;
|
||||
std::chrono::nanoseconds appVsyncOffset;
|
||||
std::chrono::nanoseconds sfVsyncOffset;
|
||||
|
||||
static bool getFromApp(JNIEnv* env, jobject jactivity,
|
||||
SwappyCommonSettings* out);
|
||||
static SdkVersion getSDKVersion(JNIEnv* env);
|
||||
static bool queryDisplayTimings(JNIEnv* env, jobject jactivity,
|
||||
SwappyCommonSettings* out);
|
||||
};
|
||||
|
||||
// Common part between OpenGL and Vulkan implementations.
|
||||
class SwappyCommon {
|
||||
public:
|
||||
enum class PipelineMode { Off, On };
|
||||
|
||||
// callbacks to be called during pre/post swap
|
||||
struct SwapHandlers {
|
||||
std::function<bool()> lastFrameIsComplete;
|
||||
std::function<std::chrono::nanoseconds()> getPrevFrameGpuTime;
|
||||
};
|
||||
|
||||
SwappyCommon(JNIEnv* env, jobject jactivity);
|
||||
|
||||
~SwappyCommon();
|
||||
|
||||
std::chrono::nanoseconds getSwapDuration();
|
||||
|
||||
void onChoreographer(int64_t frameTimeNanos);
|
||||
|
||||
void onPreSwap(const SwapHandlers& h);
|
||||
|
||||
bool needToSetPresentationTime() { return mPresentationTimeNeeded; }
|
||||
|
||||
void onPostSwap(const SwapHandlers& h);
|
||||
|
||||
PipelineMode getCurrentPipelineMode() { return mPipelineMode; }
|
||||
|
||||
template <typename... T>
|
||||
struct Tracer {
|
||||
void (*function)(void*, T...);
|
||||
void* userData;
|
||||
};
|
||||
|
||||
void addTracerCallbacks(const SwappyTracer& tracer);
|
||||
|
||||
void removeTracerCallbacks(const SwappyTracer& tracer);
|
||||
|
||||
void setAutoSwapInterval(bool enabled);
|
||||
void setAutoPipelineMode(bool enabled);
|
||||
|
||||
void setMaxAutoSwapDuration(std::chrono::nanoseconds swapDuration) {
|
||||
mAutoSwapIntervalThreshold = swapDuration;
|
||||
}
|
||||
|
||||
std::chrono::steady_clock::time_point getPresentationTime() {
|
||||
return mPresentationTime;
|
||||
}
|
||||
std::chrono::nanoseconds getRefreshPeriod() const {
|
||||
return mCommonSettings.refreshPeriod;
|
||||
}
|
||||
|
||||
bool isValid() { return mValid; }
|
||||
|
||||
std::chrono::nanoseconds getFenceTimeout() const { return mFenceTimeout; }
|
||||
void setFenceTimeout(std::chrono::nanoseconds t) { mFenceTimeout = t; }
|
||||
|
||||
bool isDeviceUnsupported();
|
||||
|
||||
void setANativeWindow(ANativeWindow* window);
|
||||
|
||||
void setFrameStatistics(
|
||||
const std::shared_ptr<FrameStatistics>& frameStats) {
|
||||
mFrameStatistics = frameStats;
|
||||
}
|
||||
|
||||
void setBufferStuffingFixWait(int32_t nFrames) {
|
||||
mBufferStuffingFixWait = std::max(0, nFrames);
|
||||
}
|
||||
|
||||
protected:
|
||||
// Used for testing
|
||||
SwappyCommon(const SwappyCommonSettings& settings);
|
||||
|
||||
private:
|
||||
class FrameDuration {
|
||||
public:
|
||||
FrameDuration() = default;
|
||||
|
||||
FrameDuration(std::chrono::nanoseconds cpuTime,
|
||||
std::chrono::nanoseconds gpuTime,
|
||||
bool frameMissedDeadline)
|
||||
: mCpuTime(cpuTime),
|
||||
mGpuTime(gpuTime),
|
||||
mFrameMissedDeadline(frameMissedDeadline) {
|
||||
mCpuTime = std::min(mCpuTime, MAX_DURATION);
|
||||
mGpuTime = std::min(mGpuTime, MAX_DURATION);
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds getCpuTime() const { return mCpuTime; }
|
||||
std::chrono::nanoseconds getGpuTime() const { return mGpuTime; }
|
||||
|
||||
bool frameMiss() const { return mFrameMissedDeadline; }
|
||||
|
||||
std::chrono::nanoseconds getTime(PipelineMode pipeline) const {
|
||||
if (mCpuTime == 0ns && mGpuTime == 0ns) {
|
||||
return 0ns;
|
||||
}
|
||||
|
||||
if (pipeline == PipelineMode::On) {
|
||||
return std::max(mCpuTime, mGpuTime) + FRAME_MARGIN;
|
||||
}
|
||||
|
||||
return mCpuTime + mGpuTime + FRAME_MARGIN;
|
||||
}
|
||||
|
||||
FrameDuration& operator+=(const FrameDuration& other) {
|
||||
mCpuTime += other.mCpuTime;
|
||||
mGpuTime += other.mGpuTime;
|
||||
return *this;
|
||||
}
|
||||
|
||||
FrameDuration& operator-=(const FrameDuration& other) {
|
||||
mCpuTime -= other.mCpuTime;
|
||||
mGpuTime -= other.mGpuTime;
|
||||
return *this;
|
||||
}
|
||||
|
||||
friend FrameDuration operator/(FrameDuration lhs, int rhs) {
|
||||
lhs.mCpuTime /= rhs;
|
||||
lhs.mGpuTime /= rhs;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
private:
|
||||
std::chrono::nanoseconds mCpuTime = std::chrono::nanoseconds(0);
|
||||
std::chrono::nanoseconds mGpuTime = std::chrono::nanoseconds(0);
|
||||
bool mFrameMissedDeadline = false;
|
||||
|
||||
static constexpr std::chrono::nanoseconds MAX_DURATION =
|
||||
std::chrono::milliseconds(100);
|
||||
};
|
||||
|
||||
void addFrameDuration(FrameDuration duration);
|
||||
std::chrono::nanoseconds wakeClient();
|
||||
|
||||
bool swapFaster(int newSwapInterval) REQUIRES(mMutex);
|
||||
|
||||
bool swapSlower(const FrameDuration& averageFrameTime,
|
||||
const std::chrono::nanoseconds& upperBound,
|
||||
int newSwapInterval) REQUIRES(mMutex);
|
||||
bool updateSwapInterval();
|
||||
void preSwapBuffersCallbacks();
|
||||
void postSwapBuffersCallbacks();
|
||||
void preWaitCallbacks();
|
||||
void postWaitCallbacks(std::chrono::nanoseconds cpuTime,
|
||||
std::chrono::nanoseconds gpuTime);
|
||||
void startFrameCallbacks();
|
||||
void swapIntervalChangedCallbacks();
|
||||
void onSettingsChanged();
|
||||
void updateMeasuredSwapDuration(std::chrono::nanoseconds duration);
|
||||
void startFrame();
|
||||
void waitUntil(int32_t target);
|
||||
void waitUntilTargetFrame();
|
||||
void waitOneFrame();
|
||||
void setPreferredDisplayModeId(int index);
|
||||
void setPreferredRefreshPeriod(std::chrono::nanoseconds frameTime)
|
||||
REQUIRES(mMutex);
|
||||
int calculateSwapInterval(std::chrono::nanoseconds frameTime,
|
||||
std::chrono::nanoseconds refreshPeriod);
|
||||
void updateDisplayTimings();
|
||||
|
||||
// Waits for the next frame, considering both Choreographer and the prior
|
||||
// frame's completion
|
||||
bool waitForNextFrame(const SwapHandlers& h);
|
||||
|
||||
void onRefreshRateChanged();
|
||||
|
||||
inline bool swapFasterCondition() {
|
||||
return mSwapDuration <=
|
||||
mCommonSettings.refreshPeriod * (mAutoSwapInterval - 1) +
|
||||
DURATION_ROUNDING_MARGIN;
|
||||
}
|
||||
|
||||
const jobject mJactivity;
|
||||
void* mLibAndroid = nullptr;
|
||||
PFN_ANativeWindow_setFrameRate mANativeWindow_setFrameRate = nullptr;
|
||||
|
||||
JavaVM* mJVM = nullptr;
|
||||
|
||||
SwappyCommonSettings mCommonSettings;
|
||||
|
||||
std::unique_ptr<ChoreographerFilter> mChoreographerFilter;
|
||||
|
||||
bool mUsingExternalChoreographer = false;
|
||||
std::unique_ptr<ChoreographerThread> mChoreographerThread;
|
||||
|
||||
std::mutex mWaitingMutex;
|
||||
std::condition_variable mWaitingCondition;
|
||||
std::chrono::steady_clock::time_point mCurrentFrameTimestamp =
|
||||
std::chrono::steady_clock::now();
|
||||
int32_t mCurrentFrame = 0;
|
||||
std::atomic<std::chrono::nanoseconds> mMeasuredSwapDuration;
|
||||
|
||||
std::chrono::steady_clock::time_point mSwapTime;
|
||||
|
||||
std::mutex mMutex;
|
||||
class FrameDurations {
|
||||
public:
|
||||
void add(FrameDuration frameDuration);
|
||||
bool hasEnoughSamples() const;
|
||||
FrameDuration getAverageFrameTime() const;
|
||||
int getMissedFramePercent() const;
|
||||
void clear();
|
||||
|
||||
private:
|
||||
static constexpr std::chrono::nanoseconds
|
||||
FRAME_DURATION_SAMPLE_SECONDS = 2s;
|
||||
|
||||
std::deque<std::pair<std::chrono::time_point<std::chrono::steady_clock>,
|
||||
FrameDuration>>
|
||||
mFrames;
|
||||
FrameDuration mFrameDurationsSum = {};
|
||||
int mMissedFrameCount = 0;
|
||||
};
|
||||
|
||||
FrameDurations mFrameDurations GUARDED_BY(mMutex);
|
||||
|
||||
bool mAutoSwapIntervalEnabled GUARDED_BY(mMutex) = true;
|
||||
bool mPipelineModeAutoMode GUARDED_BY(mMutex) = true;
|
||||
|
||||
static constexpr std::chrono::nanoseconds FRAME_MARGIN = 1ms;
|
||||
static constexpr std::chrono::nanoseconds DURATION_ROUNDING_MARGIN = 1us;
|
||||
static constexpr int NON_PIPELINE_PERCENT = 50; // 50%
|
||||
static constexpr int FRAME_DROP_THRESHOLD = 10; // 10%
|
||||
|
||||
std::chrono::nanoseconds mSwapDuration = 0ns;
|
||||
int32_t mAutoSwapInterval;
|
||||
std::atomic<std::chrono::nanoseconds> mAutoSwapIntervalThreshold = {
|
||||
50ms}; // 20FPS
|
||||
static constexpr std::chrono::nanoseconds REFRESH_RATE_MARGIN = 500ns;
|
||||
|
||||
std::chrono::steady_clock::time_point mStartFrameTime;
|
||||
|
||||
struct SwappyTracerCallbacks {
|
||||
std::list<Tracer<>> preWait;
|
||||
std::list<Tracer<int64_t, int64_t>> postWait;
|
||||
std::list<Tracer<>> preSwapBuffers;
|
||||
std::list<Tracer<int64_t>> postSwapBuffers;
|
||||
std::list<Tracer<int32_t, int64_t>> startFrame;
|
||||
std::list<Tracer<>> swapIntervalChanged;
|
||||
};
|
||||
|
||||
SwappyTracerCallbacks mInjectedTracers;
|
||||
|
||||
int32_t mTargetFrame = 0;
|
||||
std::chrono::steady_clock::time_point mPresentationTime =
|
||||
std::chrono::steady_clock::now();
|
||||
bool mPresentationTimeNeeded;
|
||||
PipelineMode mPipelineMode = PipelineMode::On;
|
||||
|
||||
bool mValid;
|
||||
|
||||
std::chrono::nanoseconds mFenceTimeout = std::chrono::nanoseconds(50ms);
|
||||
|
||||
constexpr static bool USE_DISPLAY_MANAGER = true;
|
||||
std::unique_ptr<SwappyDisplayManager> mDisplayManager;
|
||||
int mNextModeId = -1;
|
||||
|
||||
std::shared_ptr<SwappyDisplayManager::RefreshPeriodMap>
|
||||
mSupportedRefreshPeriods;
|
||||
|
||||
struct TimingSettings {
|
||||
std::chrono::nanoseconds refreshPeriod = {};
|
||||
std::chrono::nanoseconds swapDuration = {};
|
||||
|
||||
static TimingSettings from(const Settings& settings) {
|
||||
TimingSettings timingSettings;
|
||||
|
||||
timingSettings.refreshPeriod =
|
||||
settings.getDisplayTimings().refreshPeriod;
|
||||
timingSettings.swapDuration = settings.getSwapDuration();
|
||||
return timingSettings;
|
||||
}
|
||||
|
||||
bool operator!=(const TimingSettings& other) const {
|
||||
return (refreshPeriod != other.refreshPeriod) ||
|
||||
(swapDuration != other.swapDuration);
|
||||
}
|
||||
|
||||
bool operator==(const TimingSettings& other) const {
|
||||
return !(*this != other);
|
||||
}
|
||||
};
|
||||
TimingSettings mNextTimingSettings GUARDED_BY(mMutex) = {};
|
||||
bool mTimingSettingsNeedUpdate GUARDED_BY(mMutex) = false;
|
||||
|
||||
CPUTracer mCPUTracer;
|
||||
|
||||
ANativeWindow* mWindow GUARDED_BY(mMutex) = nullptr;
|
||||
bool mWindowChanged GUARDED_BY(mMutex) = false;
|
||||
float mLatestFrameRateVote GUARDED_BY(mMutex) = 0.f;
|
||||
static constexpr float FRAME_RATE_VOTE_MARGIN = 1.f; // 1Hz
|
||||
|
||||
// If zero, don't apply the double buffering fix. If non-zero, apply
|
||||
// the fix after this number of bad frames.
|
||||
int mBufferStuffingFixWait = 0;
|
||||
// When zero, buffer stuffing fixing may occur.
|
||||
// After a fix has been applied, this is non-zero and counts down to avoid
|
||||
// consecutive fixes.
|
||||
int mBufferStuffingFixCounter = 0;
|
||||
// Counts the number of consecutive missed frames (as judged by expected
|
||||
// latency).
|
||||
int mMissedFrameCounter = 0;
|
||||
|
||||
std::shared_ptr<FrameStatistics> mFrameStatistics;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#define LOG_TAG "SwappyDisplayManager"
|
||||
|
||||
#include "SwappyDisplayManager.h"
|
||||
|
||||
#include <Log.h>
|
||||
#include <android/looper.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "JNIUtil.h"
|
||||
#include "Settings.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
// Forward declaration of the native methods of Java SwappyDisplayManager class
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_google_androidgamesdk_SwappyDisplayManager_nSetSupportedRefreshPeriods(
|
||||
JNIEnv *env, jobject /* this */, jlong cookie, jlongArray refreshPeriods,
|
||||
jintArray modeIds);
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_google_androidgamesdk_SwappyDisplayManager_nOnRefreshPeriodChanged(
|
||||
JNIEnv *env, jobject /* this */, jlong cookie, jlong refreshPeriod,
|
||||
jlong appOffset, jlong sfOffset);
|
||||
}
|
||||
|
||||
const char *SwappyDisplayManager::SDM_CLASS =
|
||||
"com/google/androidgamesdk/SwappyDisplayManager";
|
||||
|
||||
const JNINativeMethod SwappyDisplayManager::SDMNativeMethods[] = {
|
||||
{"nSetSupportedRefreshPeriods", "(J[J[I)V",
|
||||
(void
|
||||
*)&Java_com_google_androidgamesdk_SwappyDisplayManager_nSetSupportedRefreshPeriods},
|
||||
{"nOnRefreshPeriodChanged", "(JJJJ)V",
|
||||
(void
|
||||
*)&Java_com_google_androidgamesdk_SwappyDisplayManager_nOnRefreshPeriodChanged}};
|
||||
|
||||
bool SwappyDisplayManager::useSwappyDisplayManager(SdkVersion sdkVersion) {
|
||||
// SwappyDisplayManager uses APIs introduced in SDK 23 and we get spurious
|
||||
// window messages for SDK < 28, so restrict here.
|
||||
if (sdkVersion.sdkInt < MIN_SDK_VERSION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SDK 31 and above doesn't need SwappyDisplayManager as it has native
|
||||
// support in NDK. SDK 30 has partial native support
|
||||
// (AChoreographer_registerRefreshRateCallback) but lacks synchronization
|
||||
// with DisplayManager to query app/sf offsets
|
||||
return !(sdkVersion.sdkInt >= 31 ||
|
||||
(sdkVersion.sdkInt == 30 && sdkVersion.previewSdkInt == 1));
|
||||
}
|
||||
|
||||
SwappyDisplayManager::SwappyDisplayManager(JavaVM *vm, jobject mainActivity)
|
||||
: mJVM(vm) {
|
||||
if (!vm || !mainActivity) {
|
||||
return;
|
||||
}
|
||||
|
||||
JNIEnv *env;
|
||||
mJVM->AttachCurrentThread(&env, nullptr);
|
||||
|
||||
jclass swappyDisplayManagerClass = gamesdk::loadClass(
|
||||
env, mainActivity, SwappyDisplayManager::SDM_CLASS,
|
||||
(JNINativeMethod *)SwappyDisplayManager::SDMNativeMethods,
|
||||
SwappyDisplayManager::SDMNativeMethodsSize);
|
||||
|
||||
if (!swappyDisplayManagerClass) return;
|
||||
|
||||
jmethodID constructor = env->GetMethodID(
|
||||
swappyDisplayManagerClass, "<init>", "(JLandroid/app/Activity;)V");
|
||||
mSetPreferredDisplayModeId = env->GetMethodID(
|
||||
swappyDisplayManagerClass, "setPreferredDisplayModeId", "(I)V");
|
||||
mTerminate =
|
||||
env->GetMethodID(swappyDisplayManagerClass, "terminate", "()V");
|
||||
jobject swappyDisplayManager = env->NewObject(
|
||||
swappyDisplayManagerClass, constructor, (jlong)this, mainActivity);
|
||||
mJthis = env->NewGlobalRef(swappyDisplayManager);
|
||||
|
||||
mInitialized = true;
|
||||
}
|
||||
|
||||
SwappyDisplayManager::~SwappyDisplayManager() {
|
||||
JNIEnv *env;
|
||||
mJVM->AttachCurrentThread(&env, nullptr);
|
||||
|
||||
env->CallVoidMethod(mJthis, mTerminate);
|
||||
env->DeleteGlobalRef(mJthis);
|
||||
}
|
||||
|
||||
std::shared_ptr<SwappyDisplayManager::RefreshPeriodMap>
|
||||
SwappyDisplayManager::getSupportedRefreshPeriods() {
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
|
||||
mCondition.wait(
|
||||
lock, [&]() { return mSupportedRefreshPeriods.get() != nullptr; });
|
||||
return mSupportedRefreshPeriods;
|
||||
}
|
||||
|
||||
void SwappyDisplayManager::setPreferredDisplayModeId(int index) {
|
||||
JNIEnv *env;
|
||||
mJVM->AttachCurrentThread(&env, nullptr);
|
||||
|
||||
env->CallVoidMethod(mJthis, mSetPreferredDisplayModeId, index);
|
||||
}
|
||||
|
||||
// Helper class to wrap JNI entry points to SwappyDisplayManager
|
||||
class SwappyDisplayManagerJNI {
|
||||
public:
|
||||
static void onSetSupportedRefreshPeriods(
|
||||
jlong, std::shared_ptr<SwappyDisplayManager::RefreshPeriodMap>);
|
||||
static void onRefreshPeriodChanged(jlong, long, long, long);
|
||||
};
|
||||
|
||||
void SwappyDisplayManagerJNI::onSetSupportedRefreshPeriods(
|
||||
jlong cookie,
|
||||
std::shared_ptr<SwappyDisplayManager::RefreshPeriodMap> refreshPeriods) {
|
||||
auto *sDM = reinterpret_cast<SwappyDisplayManager *>(cookie);
|
||||
|
||||
std::lock_guard<std::mutex> lock(sDM->mMutex);
|
||||
sDM->mSupportedRefreshPeriods = std::move(refreshPeriods);
|
||||
sDM->mCondition.notify_one();
|
||||
}
|
||||
|
||||
void SwappyDisplayManagerJNI::onRefreshPeriodChanged(jlong /*cookie*/,
|
||||
long refreshPeriod,
|
||||
long appOffset,
|
||||
long sfOffset) {
|
||||
ALOGV("onRefreshPeriodChanged: refresh rate: %.0fHz", 1e9f / refreshPeriod);
|
||||
using std::chrono::nanoseconds;
|
||||
Settings::DisplayTimings displayTimings;
|
||||
displayTimings.refreshPeriod = nanoseconds(refreshPeriod);
|
||||
displayTimings.appOffset = nanoseconds(appOffset);
|
||||
displayTimings.sfOffset = nanoseconds(sfOffset);
|
||||
Settings::getInstance()->setDisplayTimings(displayTimings);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_google_androidgamesdk_SwappyDisplayManager_nSetSupportedRefreshPeriods(
|
||||
JNIEnv *env, jobject /* this */, jlong cookie, jlongArray refreshPeriods,
|
||||
jintArray modeIds) {
|
||||
int length = env->GetArrayLength(refreshPeriods);
|
||||
auto refreshPeriodsMap =
|
||||
std::make_shared<SwappyDisplayManager::RefreshPeriodMap>();
|
||||
|
||||
jlong *refreshPeriodsArr = env->GetLongArrayElements(refreshPeriods, 0);
|
||||
jint *modeIdsArr = env->GetIntArrayElements(modeIds, 0);
|
||||
for (int i = 0; i < length; i++) {
|
||||
(*refreshPeriodsMap)[std::chrono::nanoseconds(refreshPeriodsArr[i])] =
|
||||
modeIdsArr[i];
|
||||
}
|
||||
env->ReleaseLongArrayElements(refreshPeriods, refreshPeriodsArr, 0);
|
||||
env->ReleaseIntArrayElements(modeIds, modeIdsArr, 0);
|
||||
|
||||
SwappyDisplayManagerJNI::onSetSupportedRefreshPeriods(cookie,
|
||||
refreshPeriodsMap);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_google_androidgamesdk_SwappyDisplayManager_nOnRefreshPeriodChanged(
|
||||
JNIEnv *env, jobject /* this */, jlong cookie, jlong refreshPeriod,
|
||||
jlong appOffset, jlong sfOffset) {
|
||||
SwappyDisplayManagerJNI::onRefreshPeriodChanged(cookie, refreshPeriod,
|
||||
appOffset, sfOffset);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace swappy {
|
||||
|
||||
struct SdkVersion {
|
||||
int sdkInt; // Build.VERSION.SDK_INT
|
||||
int previewSdkInt; // Build.VERSION.PREVIEW_SDK_INT
|
||||
};
|
||||
|
||||
class SwappyDisplayManager {
|
||||
public:
|
||||
static const char* SDM_CLASS;
|
||||
static const JNINativeMethod SDMNativeMethods[];
|
||||
static constexpr int SDMNativeMethodsSize = 2;
|
||||
static constexpr int MIN_SDK_VERSION = 28;
|
||||
|
||||
static bool useSwappyDisplayManager(SdkVersion sdkVersion);
|
||||
|
||||
SwappyDisplayManager(JavaVM*, jobject mainActivity);
|
||||
~SwappyDisplayManager();
|
||||
|
||||
bool isInitialized() { return mInitialized; }
|
||||
|
||||
// Map from refresh period to display mode id
|
||||
using RefreshPeriodMap = std::map<std::chrono::nanoseconds, int>;
|
||||
|
||||
std::shared_ptr<RefreshPeriodMap> getSupportedRefreshPeriods();
|
||||
|
||||
void setPreferredDisplayModeId(int index);
|
||||
|
||||
private:
|
||||
JavaVM* mJVM;
|
||||
std::mutex mMutex;
|
||||
std::condition_variable mCondition;
|
||||
std::shared_ptr<RefreshPeriodMap> mSupportedRefreshPeriods;
|
||||
jobject mJthis = nullptr;
|
||||
jmethodID mSetPreferredDisplayModeId = nullptr;
|
||||
jmethodID mTerminate = nullptr;
|
||||
bool mInitialized = false;
|
||||
|
||||
friend class SwappyDisplayManagerJNI;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "Thread.h"
|
||||
|
||||
#include <sched.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include "swappy/swappy_common.h"
|
||||
|
||||
#define LOG_TAG "SwappyThread"
|
||||
#include "Log.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
int32_t getNumCpus() {
|
||||
static int32_t sNumCpus = []() {
|
||||
pid_t pid = gettid();
|
||||
cpu_set_t cpuSet;
|
||||
CPU_ZERO(&cpuSet);
|
||||
sched_getaffinity(pid, sizeof(cpuSet), &cpuSet);
|
||||
|
||||
int32_t numCpus = 0;
|
||||
while (CPU_ISSET(numCpus, &cpuSet)) {
|
||||
++numCpus;
|
||||
}
|
||||
|
||||
return numCpus;
|
||||
}();
|
||||
|
||||
return sNumCpus;
|
||||
}
|
||||
|
||||
void setAffinity(int32_t cpu) {
|
||||
cpu_set_t cpuSet;
|
||||
CPU_ZERO(&cpuSet);
|
||||
CPU_SET(cpu, &cpuSet);
|
||||
sched_setaffinity(gettid(), sizeof(cpuSet), &cpuSet);
|
||||
}
|
||||
|
||||
void setAffinity(Affinity affinity) {
|
||||
const int32_t numCpus = getNumCpus();
|
||||
|
||||
cpu_set_t cpuSet;
|
||||
CPU_ZERO(&cpuSet);
|
||||
for (int32_t cpu = 0; cpu < numCpus; ++cpu) {
|
||||
switch (affinity) {
|
||||
case Affinity::None:
|
||||
CPU_SET(cpu, &cpuSet);
|
||||
break;
|
||||
case Affinity::Even:
|
||||
if (cpu % 2 == 0) CPU_SET(cpu, &cpuSet);
|
||||
break;
|
||||
case Affinity::Odd:
|
||||
if (cpu % 2 == 1) CPU_SET(cpu, &cpuSet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sched_setaffinity(gettid(), sizeof(cpuSet), &cpuSet);
|
||||
}
|
||||
|
||||
static const SwappyThreadFunctions* s_ext_thread_manager = nullptr;
|
||||
|
||||
struct ThreadImpl {
|
||||
virtual ~ThreadImpl() {}
|
||||
virtual bool joinable() = 0;
|
||||
virtual void join() = 0;
|
||||
};
|
||||
|
||||
struct ExtThreadImpl : public ThreadImpl {
|
||||
std::function<void()> fn_;
|
||||
SwappyThreadId id_;
|
||||
|
||||
public:
|
||||
ExtThreadImpl(std::function<void()>&& fn) : fn_(std::move(fn)) {
|
||||
if (s_ext_thread_manager->start(&id_, startThread, this) != 0) {
|
||||
ALOGE("Couldn't create thread");
|
||||
}
|
||||
}
|
||||
void join() { s_ext_thread_manager->join(id_); }
|
||||
bool joinable() { return s_ext_thread_manager->joinable(id_); }
|
||||
static void* startThread(void* x) {
|
||||
ExtThreadImpl* impl = (ExtThreadImpl*)x;
|
||||
impl->fn_();
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct StlThreadImpl : public ThreadImpl {
|
||||
std::thread thread_;
|
||||
|
||||
public:
|
||||
StlThreadImpl(std::function<void()>&& fn) : thread_(std::move(fn)) {}
|
||||
void join() { thread_.join(); }
|
||||
bool joinable() { return thread_.joinable(); }
|
||||
};
|
||||
|
||||
Thread::Thread() noexcept {}
|
||||
|
||||
Thread::~Thread() {}
|
||||
|
||||
Thread::Thread(std::function<void()>&& fn) noexcept {
|
||||
if (s_ext_thread_manager != nullptr) {
|
||||
impl_ = std::make_unique<ExtThreadImpl>(std::move(fn));
|
||||
} else {
|
||||
impl_ = std::make_unique<StlThreadImpl>(std::move(fn));
|
||||
}
|
||||
}
|
||||
|
||||
Thread::Thread(Thread&& rhs) noexcept : impl_(std::move(rhs.impl_)) {}
|
||||
|
||||
Thread& Thread::operator=(Thread&& rhs) noexcept {
|
||||
if (&rhs != this) {
|
||||
impl_ = std::move(rhs.impl_);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Thread::join() {
|
||||
if (impl_.get()) {
|
||||
impl_->join();
|
||||
}
|
||||
}
|
||||
|
||||
bool Thread::joinable() {
|
||||
return (impl_.get() != nullptr && impl_->joinable());
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
||||
extern "C" void Swappy_setThreadFunctions(const SwappyThreadFunctions* mgr) {
|
||||
swappy::s_ext_thread_manager = mgr;
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
// Enable thread safety attributes only with clang.
|
||||
// The attributes can be safely erased when compiling with other compilers.
|
||||
#if defined(__clang__) && (!defined(SWIG))
|
||||
#define THREAD_ANNOTATION_ATTRIBUTE__(x) __attribute__((x))
|
||||
#else
|
||||
#define THREAD_ANNOTATION_ATTRIBUTE__(x) // no-op
|
||||
#endif
|
||||
|
||||
#if !defined GAMESDK_THREAD_CHECKS
|
||||
#define GAMESDK_THREAD_CHECKS 1
|
||||
#endif
|
||||
|
||||
#if GAMESDK_THREAD_CHECKS
|
||||
#define GUARDED_BY(x) THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))
|
||||
|
||||
#define REQUIRES(...) \
|
||||
THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__))
|
||||
|
||||
#define NO_THREAD_SAFETY_ANALYSIS \
|
||||
THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis)
|
||||
#else
|
||||
#define GUARDED_BY(x)
|
||||
#define REQUIRES(...)
|
||||
#define NO_THREAD_SAFETY_ANALYSIS
|
||||
#endif
|
||||
|
||||
namespace swappy {
|
||||
|
||||
enum class Affinity { None, Even, Odd };
|
||||
|
||||
int32_t getNumCpus();
|
||||
void setAffinity(int32_t cpu);
|
||||
void setAffinity(Affinity affinity);
|
||||
|
||||
struct ThreadImpl;
|
||||
|
||||
class Thread {
|
||||
std::unique_ptr<ThreadImpl> impl_;
|
||||
|
||||
public:
|
||||
Thread() noexcept;
|
||||
Thread(std::function<void()>&& fn) noexcept;
|
||||
Thread(Thread&& rhs) noexcept;
|
||||
Thread(const Thread&) = delete;
|
||||
Thread& operator=(Thread&& rhs) noexcept;
|
||||
Thread& operator=(const Thread& rhs) = delete;
|
||||
~Thread();
|
||||
void join();
|
||||
bool joinable();
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "EGL.h"
|
||||
|
||||
#include <Trace.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#define LOG_TAG "Swappy::EGL"
|
||||
|
||||
#include "Log.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace swappy {
|
||||
|
||||
std::unique_ptr<EGL> EGL::create(std::chrono::nanoseconds fenceTimeout) {
|
||||
auto eglLib = dlopen("libEGL.so", RTLD_LAZY | RTLD_LOCAL);
|
||||
if (eglLib == nullptr) {
|
||||
ALOGE("Can't load libEGL");
|
||||
return nullptr;
|
||||
}
|
||||
auto eglGetProcAddress = reinterpret_cast<eglGetProcAddress_type>(
|
||||
dlsym(eglLib, "eglGetProcAddress"));
|
||||
if (eglGetProcAddress == nullptr) {
|
||||
ALOGE("Failed to load eglGetProcAddress");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto eglSwapBuffers =
|
||||
reinterpret_cast<eglSwapBuffers_type>(dlsym(eglLib, "eglSwapBuffers"));
|
||||
if (eglSwapBuffers == nullptr) {
|
||||
ALOGE("Failed to load eglSwapBuffers");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto eglPresentationTimeANDROID =
|
||||
reinterpret_cast<eglPresentationTimeANDROID_type>(
|
||||
eglGetProcAddress("eglPresentationTimeANDROID"));
|
||||
if (eglPresentationTimeANDROID == nullptr) {
|
||||
ALOGE("Failed to load eglPresentationTimeANDROID");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto eglCreateSyncKHR = reinterpret_cast<eglCreateSyncKHR_type>(
|
||||
eglGetProcAddress("eglCreateSyncKHR"));
|
||||
if (eglCreateSyncKHR == nullptr) {
|
||||
ALOGE("Failed to load eglCreateSyncKHR");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto eglDestroySyncKHR = reinterpret_cast<eglDestroySyncKHR_type>(
|
||||
eglGetProcAddress("eglDestroySyncKHR"));
|
||||
if (eglDestroySyncKHR == nullptr) {
|
||||
ALOGE("Failed to load eglDestroySyncKHR");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto eglGetSyncAttribKHR = reinterpret_cast<eglGetSyncAttribKHR_type>(
|
||||
eglGetProcAddress("eglGetSyncAttribKHR"));
|
||||
if (eglGetSyncAttribKHR == nullptr) {
|
||||
ALOGE("Failed to load eglGetSyncAttribKHR");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto eglGetError =
|
||||
reinterpret_cast<eglGetError_type>(eglGetProcAddress("eglGetError"));
|
||||
if (eglGetError == nullptr) {
|
||||
ALOGE("Failed to load eglGetError");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto eglSurfaceAttrib = reinterpret_cast<eglSurfaceAttrib_type>(
|
||||
eglGetProcAddress("eglSurfaceAttrib"));
|
||||
if (eglSurfaceAttrib == nullptr) {
|
||||
ALOGE("Failed to load eglSurfaceAttrib");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// stats may not be supported on all versions
|
||||
auto eglGetNextFrameIdANDROID =
|
||||
reinterpret_cast<eglGetNextFrameIdANDROID_type>(
|
||||
eglGetProcAddress("eglGetNextFrameIdANDROID"));
|
||||
if (eglGetNextFrameIdANDROID == nullptr) {
|
||||
ALOGI("Failed to load eglGetNextFrameIdANDROID");
|
||||
}
|
||||
|
||||
auto eglGetFrameTimestampsANDROID =
|
||||
reinterpret_cast<eglGetFrameTimestampsANDROID_type>(
|
||||
eglGetProcAddress("eglGetFrameTimestampsANDROID"));
|
||||
if (eglGetFrameTimestampsANDROID == nullptr) {
|
||||
ALOGI("Failed to load eglGetFrameTimestampsANDROID");
|
||||
}
|
||||
|
||||
auto egl = std::make_unique<EGL>(fenceTimeout, eglGetProcAddress,
|
||||
ConstructorTag{});
|
||||
egl->eglLib = eglLib;
|
||||
egl->eglSwapBuffers = eglSwapBuffers;
|
||||
egl->eglGetProcAddress = eglGetProcAddress;
|
||||
egl->eglPresentationTimeANDROID = eglPresentationTimeANDROID;
|
||||
egl->eglCreateSyncKHR = eglCreateSyncKHR;
|
||||
egl->eglDestroySyncKHR = eglDestroySyncKHR;
|
||||
egl->eglGetSyncAttribKHR = eglGetSyncAttribKHR;
|
||||
egl->eglGetError = eglGetError;
|
||||
egl->eglSurfaceAttrib = eglSurfaceAttrib;
|
||||
egl->eglGetNextFrameIdANDROID = eglGetNextFrameIdANDROID;
|
||||
egl->eglGetFrameTimestampsANDROID = eglGetFrameTimestampsANDROID;
|
||||
return egl;
|
||||
}
|
||||
|
||||
EGL::~EGL() {
|
||||
if (eglLib) {
|
||||
dlclose(eglLib);
|
||||
}
|
||||
}
|
||||
void EGL::resetSyncFence(EGLDisplay display) {
|
||||
std::lock_guard<std::mutex> lock(mSyncFenceMutex);
|
||||
|
||||
if (mFenceWaiter.waitForIdle() && mSyncFence != EGL_NO_SYNC_KHR) {
|
||||
EGLBoolean result = eglDestroySyncKHR(display, mSyncFence);
|
||||
if (result == EGL_FALSE) {
|
||||
ALOGE("Failed to destroy sync fence");
|
||||
}
|
||||
}
|
||||
|
||||
mSyncFence = eglCreateSyncKHR(display, EGL_SYNC_FENCE_KHR, nullptr);
|
||||
|
||||
if (mSyncFence != EGL_NO_SYNC_KHR) {
|
||||
// kick of the thread work to wait for the fence and measure its time
|
||||
mFenceWaiter.onFenceCreation(display, mSyncFence);
|
||||
} else {
|
||||
ALOGE("Failed to create sync fence");
|
||||
}
|
||||
}
|
||||
|
||||
bool EGL::lastFrameIsComplete(EGLDisplay display) {
|
||||
std::lock_guard<std::mutex> lock(mSyncFenceMutex);
|
||||
|
||||
// This will be the case on the first frame
|
||||
if (mSyncFence == EGL_NO_SYNC_KHR) {
|
||||
return true;
|
||||
}
|
||||
|
||||
EGLint status = 0;
|
||||
EGLBoolean result =
|
||||
eglGetSyncAttribKHR(display, mSyncFence, EGL_SYNC_STATUS_KHR, &status);
|
||||
if (result == EGL_FALSE) {
|
||||
ALOGE("Failed to get sync status");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (status == EGL_SIGNALED_KHR) {
|
||||
return true;
|
||||
} else if (status == EGL_UNSIGNALED_KHR) {
|
||||
return false;
|
||||
} else {
|
||||
ALOGE("Unexpected sync status: %d", status);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool EGL::setPresentationTime(EGLDisplay display, EGLSurface surface,
|
||||
std::chrono::steady_clock::time_point time) {
|
||||
eglPresentationTimeANDROID(display, surface,
|
||||
time.time_since_epoch().count());
|
||||
return EGL_TRUE;
|
||||
}
|
||||
|
||||
bool EGL::statsSupported() {
|
||||
return (eglGetNextFrameIdANDROID != nullptr &&
|
||||
eglGetFrameTimestampsANDROID != nullptr);
|
||||
}
|
||||
|
||||
std::pair<bool, EGLuint64KHR> EGL::getNextFrameId(EGLDisplay dpy,
|
||||
EGLSurface surface) const {
|
||||
if (eglGetNextFrameIdANDROID == nullptr) {
|
||||
ALOGE("stats are not supported on this platform");
|
||||
return {false, 0};
|
||||
}
|
||||
|
||||
EGLuint64KHR frameId;
|
||||
EGLBoolean result = eglGetNextFrameIdANDROID(dpy, surface, &frameId);
|
||||
if (result == EGL_FALSE) {
|
||||
ALOGE("Failed to get next frame ID");
|
||||
return {false, 0};
|
||||
}
|
||||
|
||||
return {true, frameId};
|
||||
}
|
||||
|
||||
std::unique_ptr<EGL::FrameTimestamps> EGL::getFrameTimestamps(
|
||||
EGLDisplay dpy, EGLSurface surface, EGLuint64KHR frameId) const {
|
||||
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
|
||||
if (eglGetFrameTimestampsANDROID == nullptr) {
|
||||
ALOGE("stats are not supported on this platform");
|
||||
return nullptr;
|
||||
}
|
||||
const std::vector<EGLint> timestamps = {
|
||||
EGL_REQUESTED_PRESENT_TIME_ANDROID,
|
||||
EGL_RENDERING_COMPLETE_TIME_ANDROID,
|
||||
EGL_COMPOSITION_LATCH_TIME_ANDROID,
|
||||
EGL_DISPLAY_PRESENT_TIME_ANDROID,
|
||||
};
|
||||
|
||||
std::vector<EGLnsecsANDROID> values(timestamps.size());
|
||||
|
||||
EGLBoolean result =
|
||||
eglGetFrameTimestampsANDROID(dpy, surface, frameId, timestamps.size(),
|
||||
timestamps.data(), values.data());
|
||||
if (result == EGL_FALSE) {
|
||||
EGLint reason = eglGetError();
|
||||
if (reason == EGL_BAD_SURFACE) {
|
||||
eglSurfaceAttrib(dpy, surface, EGL_TIMESTAMPS_ANDROID, EGL_TRUE);
|
||||
} else {
|
||||
ALOGE_ONCE("Failed to get timestamps for frame %llu",
|
||||
(unsigned long long)frameId);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// try again if we got some pending stats
|
||||
for (auto i : values) {
|
||||
if (i == EGL_TIMESTAMP_PENDING_ANDROID) return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<EGL::FrameTimestamps> frameTimestamps =
|
||||
std::make_unique<EGL::FrameTimestamps>();
|
||||
frameTimestamps->requested = values[0];
|
||||
frameTimestamps->renderingCompleted = values[1];
|
||||
frameTimestamps->compositionLatched = values[2];
|
||||
frameTimestamps->presented = values[3];
|
||||
|
||||
return frameTimestamps;
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
EGL::FenceWaiter::FenceWaiter(std::chrono::nanoseconds fenceTimeout,
|
||||
eglGetProcAddress_type getProcAddress)
|
||||
: mFenceTimeout(fenceTimeout) {
|
||||
std::unique_lock<std::mutex> lock(mFenceWaiterLock);
|
||||
|
||||
eglClientWaitSyncKHR = reinterpret_cast<eglClientWaitSyncKHR_type>(
|
||||
getProcAddress("eglClientWaitSyncKHR"));
|
||||
if (eglClientWaitSyncKHR == nullptr)
|
||||
ALOGE("Failed to load eglClientWaitSyncKHR");
|
||||
eglDestroySyncKHR = reinterpret_cast<eglDestroySyncKHR_type>(
|
||||
getProcAddress("eglDestroySyncKHR"));
|
||||
if (eglDestroySyncKHR == nullptr) ALOGE("Failed to load eglDestroySyncKHR");
|
||||
|
||||
mFenceWaiter = Thread([this]() { threadMain(); });
|
||||
}
|
||||
|
||||
EGL::FenceWaiter::~FenceWaiter() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mFenceWaiterLock);
|
||||
mFenceWaiterRunning = false;
|
||||
mFenceWaiterCondition.notify_all();
|
||||
}
|
||||
mFenceWaiter.join();
|
||||
}
|
||||
|
||||
bool EGL::FenceWaiter::waitForIdle() {
|
||||
std::lock_guard<std::mutex> lock(mFenceWaiterLock);
|
||||
mFenceWaiterCondition.wait(
|
||||
mFenceWaiterLock,
|
||||
[this]() REQUIRES(mFenceWaiterLock) { return !mFenceWaiterPending; });
|
||||
return mSyncFence != EGL_NO_SYNC_KHR;
|
||||
}
|
||||
|
||||
void EGL::FenceWaiter::onFenceCreation(EGLDisplay display,
|
||||
EGLSyncKHR syncFence) {
|
||||
std::lock_guard<std::mutex> lock(mFenceWaiterLock);
|
||||
mDisplay = display;
|
||||
mSyncFence = syncFence;
|
||||
mFenceWaiterPending = true;
|
||||
mFenceWaiterCondition.notify_all();
|
||||
}
|
||||
|
||||
void EGL::FenceWaiter::threadMain() {
|
||||
std::lock_guard<std::mutex> lock(mFenceWaiterLock);
|
||||
while (mFenceWaiterRunning) {
|
||||
// wait for new fence object
|
||||
mFenceWaiterCondition.wait(
|
||||
mFenceWaiterLock, [this]() REQUIRES(mFenceWaiterLock) {
|
||||
return mFenceWaiterPending || !mFenceWaiterRunning;
|
||||
});
|
||||
|
||||
if (!mFenceWaiterRunning) {
|
||||
break;
|
||||
}
|
||||
|
||||
gamesdk::ScopedTrace tracer("Swappy: GPU frame time");
|
||||
const auto startTime = std::chrono::steady_clock::now();
|
||||
EGLBoolean result = eglClientWaitSyncKHR(mDisplay, mSyncFence, 0,
|
||||
mFenceTimeout.count());
|
||||
switch (result) {
|
||||
case EGL_FALSE:
|
||||
ALOGE("Failed to wait sync");
|
||||
break;
|
||||
case EGL_TIMEOUT_EXPIRED_KHR:
|
||||
ALOGE("Timeout waiting for fence");
|
||||
break;
|
||||
}
|
||||
if (result != EGL_CONDITION_SATISFIED_KHR) {
|
||||
result = eglDestroySyncKHR(mDisplay, mSyncFence);
|
||||
if (result == EGL_FALSE) {
|
||||
ALOGE("Failed to destroy sync fence");
|
||||
}
|
||||
mSyncFence = EGL_NO_SYNC_KHR;
|
||||
}
|
||||
|
||||
mFencePendingTime = std::chrono::steady_clock::now() - startTime;
|
||||
|
||||
mFenceWaiterPending = false;
|
||||
mFenceWaiterCondition.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds EGL::FenceWaiter::getFencePendingTime() const {
|
||||
// return mFencePendingTime without a lock to avoid blocking the main thread
|
||||
// worst case, the time will be of some previous frame
|
||||
return mFencePendingTime.load();
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "Thread.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
class EGL {
|
||||
private:
|
||||
// Allows construction with std::unique_ptr from a static method, but
|
||||
// disallows construction outside of the class since no one else can
|
||||
// construct a ConstructorTag
|
||||
struct ConstructorTag {};
|
||||
|
||||
public:
|
||||
struct FrameTimestamps {
|
||||
EGLnsecsANDROID requested;
|
||||
EGLnsecsANDROID renderingCompleted;
|
||||
EGLnsecsANDROID compositionLatched;
|
||||
EGLnsecsANDROID presented;
|
||||
};
|
||||
|
||||
using eglGetProcAddress_type = void (*(*)(const char *))(void);
|
||||
|
||||
explicit EGL(std::chrono::nanoseconds fenceTimeout,
|
||||
eglGetProcAddress_type getProcAddress, ConstructorTag)
|
||||
: mFenceWaiter(fenceTimeout, getProcAddress) {}
|
||||
~EGL();
|
||||
static std::unique_ptr<EGL> create(std::chrono::nanoseconds fenceTimeout);
|
||||
|
||||
void resetSyncFence(EGLDisplay display);
|
||||
bool lastFrameIsComplete(EGLDisplay display);
|
||||
bool setPresentationTime(EGLDisplay display, EGLSurface surface,
|
||||
std::chrono::steady_clock::time_point time);
|
||||
std::chrono::nanoseconds getFencePendingTime() const {
|
||||
return mFenceWaiter.getFencePendingTime();
|
||||
}
|
||||
|
||||
// for stats
|
||||
bool statsSupported();
|
||||
std::pair<bool, EGLuint64KHR> getNextFrameId(EGLDisplay dpy,
|
||||
EGLSurface surface) const;
|
||||
std::unique_ptr<FrameTimestamps> getFrameTimestamps(
|
||||
EGLDisplay dpy, EGLSurface surface, EGLuint64KHR frameId) const;
|
||||
EGLBoolean swapBuffers(EGLDisplay dpy, EGLSurface surface) {
|
||||
return this->eglSwapBuffers(dpy, surface);
|
||||
}
|
||||
|
||||
private:
|
||||
void *eglLib = nullptr;
|
||||
eglGetProcAddress_type eglGetProcAddress = nullptr;
|
||||
using eglSwapBuffers_type = EGLBoolean (*)(EGLDisplay, EGLSurface);
|
||||
eglSwapBuffers_type eglSwapBuffers = nullptr;
|
||||
using eglPresentationTimeANDROID_type = EGLBoolean (*)(EGLDisplay,
|
||||
EGLSurface,
|
||||
EGLnsecsANDROID);
|
||||
eglPresentationTimeANDROID_type eglPresentationTimeANDROID = nullptr;
|
||||
using eglCreateSyncKHR_type = EGLSyncKHR (*)(EGLDisplay, EGLenum,
|
||||
const EGLint *);
|
||||
eglCreateSyncKHR_type eglCreateSyncKHR = nullptr;
|
||||
using eglDestroySyncKHR_type = EGLBoolean (*)(EGLDisplay, EGLSyncKHR);
|
||||
eglDestroySyncKHR_type eglDestroySyncKHR = nullptr;
|
||||
using eglGetSyncAttribKHR_type = EGLBoolean (*)(EGLDisplay, EGLSyncKHR,
|
||||
EGLint, EGLint *);
|
||||
eglGetSyncAttribKHR_type eglGetSyncAttribKHR = nullptr;
|
||||
|
||||
using eglGetError_type = EGLint (*)(void);
|
||||
eglGetError_type eglGetError = nullptr;
|
||||
using eglSurfaceAttrib_type = EGLBoolean (*)(EGLDisplay, EGLSurface, EGLint,
|
||||
EGLint);
|
||||
eglSurfaceAttrib_type eglSurfaceAttrib = nullptr;
|
||||
using eglGetNextFrameIdANDROID_type = EGLBoolean (*)(EGLDisplay, EGLSurface,
|
||||
EGLuint64KHR *);
|
||||
eglGetNextFrameIdANDROID_type eglGetNextFrameIdANDROID = nullptr;
|
||||
using eglGetFrameTimestampsANDROID_type =
|
||||
EGLBoolean (*)(EGLDisplay, EGLSurface, EGLuint64KHR, EGLint,
|
||||
const EGLint *, EGLnsecsANDROID *);
|
||||
eglGetFrameTimestampsANDROID_type eglGetFrameTimestampsANDROID = nullptr;
|
||||
|
||||
std::mutex mSyncFenceMutex;
|
||||
EGLSyncKHR mSyncFence = EGL_NO_SYNC_KHR;
|
||||
|
||||
class FenceWaiter {
|
||||
public:
|
||||
FenceWaiter(std::chrono::nanoseconds fenceTimeout,
|
||||
EGL::eglGetProcAddress_type getProcAddress);
|
||||
~FenceWaiter();
|
||||
|
||||
void onFenceCreation(EGLDisplay display, EGLSyncKHR syncFence);
|
||||
// Wait and return true if the fence was signalled.
|
||||
// The fence will NOT be destroyed in this case.
|
||||
bool waitForIdle();
|
||||
std::chrono::nanoseconds getFencePendingTime() const;
|
||||
|
||||
private:
|
||||
using eglClientWaitSyncKHR_type = EGLBoolean (*)(EGLDisplay, EGLSyncKHR,
|
||||
EGLint, EGLTimeKHR);
|
||||
eglClientWaitSyncKHR_type eglClientWaitSyncKHR = nullptr;
|
||||
using eglDestroySyncKHR_type = EGLBoolean (*)(EGLDisplay, EGLSyncKHR);
|
||||
eglDestroySyncKHR_type eglDestroySyncKHR = nullptr;
|
||||
|
||||
void threadMain();
|
||||
Thread mFenceWaiter GUARDED_BY(mFenceWaiterLock);
|
||||
std::mutex mFenceWaiterLock;
|
||||
std::condition_variable_any mFenceWaiterCondition;
|
||||
bool mFenceWaiterRunning GUARDED_BY(mFenceWaiterLock) = true;
|
||||
bool mFenceWaiterPending GUARDED_BY(mFenceWaiterLock) = false;
|
||||
std::atomic<std::chrono::nanoseconds> mFencePendingTime;
|
||||
EGLDisplay mDisplay GUARDED_BY(mFenceWaiterLock);
|
||||
EGLSyncKHR mSyncFence GUARDED_BY(mFenceWaiterLock) = EGL_NO_SYNC_KHR;
|
||||
std::chrono::nanoseconds mFenceTimeout;
|
||||
};
|
||||
|
||||
FenceWaiter mFenceWaiter;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "FrameStatisticsGL.h"
|
||||
|
||||
#define LOG_TAG "FrameStatisticsGL"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
#include "EGL.h"
|
||||
#include "Log.h"
|
||||
#include "SwappyCommon.h"
|
||||
#include "Trace.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
int32_t LatencyFrameStatisticsGL::getFrameDelta(EGLnsecsANDROID start,
|
||||
EGLnsecsANDROID end) {
|
||||
const int64_t deltaTimeNano = end - start;
|
||||
|
||||
int32_t numFrames =
|
||||
deltaTimeNano / mSwappyCommon.getRefreshPeriod().count();
|
||||
numFrames = std::max(
|
||||
0, std::min(numFrames, static_cast<int32_t>(MAX_FRAME_BUCKETS) - 1));
|
||||
return numFrames;
|
||||
}
|
||||
|
||||
LatencyFrameStatisticsGL::LatencyFrameStatisticsGL(
|
||||
const EGL& egl, const SwappyCommon& swappyCommon)
|
||||
: mEgl(egl), mSwappyCommon(swappyCommon) {}
|
||||
|
||||
void LatencyFrameStatisticsGL::updateLatency(
|
||||
swappy::EGL::FrameTimestamps& frameStats, TimePoint frameStartTime) {
|
||||
int latency = getFrameDelta(frameStartTime.time_since_epoch().count(),
|
||||
frameStats.compositionLatched);
|
||||
TRACE_INT("FrameLatency", latency);
|
||||
mLastLatency = latency;
|
||||
}
|
||||
|
||||
LatencyFrameStatisticsGL::ThisFrame LatencyFrameStatisticsGL::getThisFrame(
|
||||
EGLDisplay dpy, EGLSurface surface) {
|
||||
const TimePoint frameStartTime = std::chrono::steady_clock::now();
|
||||
|
||||
// first get the next frame id
|
||||
std::pair<bool, EGLuint64KHR> nextFrameId =
|
||||
mEgl.getNextFrameId(dpy, surface);
|
||||
if (nextFrameId.first) {
|
||||
mPendingFrames.push_back(
|
||||
{dpy, surface, nextFrameId.second, frameStartTime});
|
||||
}
|
||||
|
||||
if (mPendingFrames.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
EGLFrame frame = mPendingFrames.front();
|
||||
// make sure we don't lag behind the stats too much
|
||||
if (nextFrameId.first && nextFrameId.second - frame.id > MAX_FRAME_LAG) {
|
||||
while (mPendingFrames.size() > 1)
|
||||
mPendingFrames.erase(mPendingFrames.begin());
|
||||
mPrevFrameTime = 0;
|
||||
frame = mPendingFrames.front();
|
||||
}
|
||||
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 14
|
||||
std::unique_ptr<EGL::FrameTimestamps> frameStats =
|
||||
mEgl.getFrameTimestamps(frame.dpy, frame.surface, frame.id);
|
||||
|
||||
if (!frameStats) {
|
||||
return {frame.startFrameTime};
|
||||
}
|
||||
|
||||
mPendingFrames.erase(mPendingFrames.begin());
|
||||
|
||||
return {frame.startFrameTime, std::move(frameStats)};
|
||||
#else
|
||||
return {frame.startFrameTime};
|
||||
#endif
|
||||
}
|
||||
|
||||
// called once per swap
|
||||
void LatencyFrameStatisticsGL::capture(EGLDisplay dpy, EGLSurface surface) {
|
||||
auto frame = getThisFrame(dpy, surface);
|
||||
if (!frame.stats) return;
|
||||
updateLatency(*frame.stats, frame.startTime);
|
||||
}
|
||||
|
||||
// NB This is only needed for C++14
|
||||
constexpr std::chrono::nanoseconds FullFrameStatisticsGL::LOG_EVERY_N_NS;
|
||||
|
||||
FullFrameStatisticsGL::FullFrameStatisticsGL(const EGL& egl,
|
||||
const SwappyCommon& swappyCommon)
|
||||
: LatencyFrameStatisticsGL(egl, swappyCommon) {}
|
||||
|
||||
int32_t FullFrameStatisticsGL::updateFrames(EGLnsecsANDROID start,
|
||||
EGLnsecsANDROID end,
|
||||
uint64_t stat[]) {
|
||||
int32_t numFrames = getFrameDelta(start, end);
|
||||
stat[numFrames]++;
|
||||
return numFrames;
|
||||
}
|
||||
|
||||
void FullFrameStatisticsGL::updateIdleFrames(EGL::FrameTimestamps& frameStats) {
|
||||
updateFrames(frameStats.renderingCompleted, frameStats.compositionLatched,
|
||||
mStats.idleFrames);
|
||||
}
|
||||
|
||||
void FullFrameStatisticsGL::updateLatencyFrames(
|
||||
swappy::EGL::FrameTimestamps& frameStats, TimePoint frameStartTime) {
|
||||
int latency =
|
||||
updateFrames(frameStartTime.time_since_epoch().count(),
|
||||
frameStats.compositionLatched, mStats.latencyFrames);
|
||||
TRACE_INT("FrameLatency", latency);
|
||||
mLastLatency = latency;
|
||||
}
|
||||
|
||||
void FullFrameStatisticsGL::updateLateFrames(EGL::FrameTimestamps& frameStats) {
|
||||
updateFrames(frameStats.requested, frameStats.presented, mStats.lateFrames);
|
||||
}
|
||||
|
||||
void FullFrameStatisticsGL::updateOffsetFromPreviousFrame(
|
||||
swappy::EGL::FrameTimestamps& frameStats) {
|
||||
if (mPrevFrameTime != 0) {
|
||||
updateFrames(mPrevFrameTime, frameStats.presented,
|
||||
mStats.offsetFromPreviousFrame);
|
||||
}
|
||||
mPrevFrameTime = frameStats.presented;
|
||||
}
|
||||
|
||||
// called once per swap
|
||||
void FullFrameStatisticsGL::capture(EGLDisplay dpy, EGLSurface surface) {
|
||||
auto frame = getThisFrame(dpy, surface);
|
||||
|
||||
if (!frame.stats) return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mStats.totalFrames++;
|
||||
updateIdleFrames(*frame.stats);
|
||||
updateLateFrames(*frame.stats);
|
||||
updateOffsetFromPreviousFrame(*frame.stats);
|
||||
updateLatencyFrames(*frame.stats, frame.startTime);
|
||||
|
||||
logFrames();
|
||||
}
|
||||
|
||||
void FullFrameStatisticsGL::logFrames() {
|
||||
static auto previousLogTime = std::chrono::steady_clock::now();
|
||||
|
||||
if (std::chrono::steady_clock::now() - previousLogTime < LOG_EVERY_N_NS) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string message;
|
||||
ALOGI("== Frame statistics ==");
|
||||
ALOGI("total frames: %" PRIu64, mStats.totalFrames);
|
||||
message += "Buckets: ";
|
||||
for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
|
||||
message += "\t[" + swappy::to_string(i) + "]";
|
||||
ALOGI("%s", message.c_str());
|
||||
|
||||
message = "";
|
||||
message += "idle frames: ";
|
||||
for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
|
||||
message += "\t " + swappy::to_string(mStats.idleFrames[i]);
|
||||
ALOGI("%s", message.c_str());
|
||||
|
||||
message = "";
|
||||
message += "late frames: ";
|
||||
for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
|
||||
message += "\t " + swappy::to_string(mStats.lateFrames[i]);
|
||||
ALOGI("%s", message.c_str());
|
||||
|
||||
message = "";
|
||||
message += "offset from previous frame: ";
|
||||
for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
|
||||
message += "\t " + swappy::to_string(mStats.offsetFromPreviousFrame[i]);
|
||||
ALOGI("%s", message.c_str());
|
||||
|
||||
message = "";
|
||||
message += "frame latency: ";
|
||||
for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
|
||||
message += "\t " + swappy::to_string(mStats.latencyFrames[i]);
|
||||
ALOGI("%s", message.c_str());
|
||||
|
||||
previousLogTime = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
SwappyStats FullFrameStatisticsGL::getStats() {
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mStats;
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "EGL.h"
|
||||
#include "FrameStatistics.h"
|
||||
#include "Thread.h"
|
||||
|
||||
using TimePoint = std::chrono::steady_clock::time_point;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace swappy {
|
||||
|
||||
class SwappyCommon;
|
||||
|
||||
// Just records latency
|
||||
class LatencyFrameStatisticsGL : public FrameStatistics {
|
||||
public:
|
||||
LatencyFrameStatisticsGL(const EGL& egl, const SwappyCommon& swappyCommon);
|
||||
~LatencyFrameStatisticsGL() = default;
|
||||
int32_t lastLatencyRecorded() const override { return mLastLatency; }
|
||||
bool isEssential() const override { return true; }
|
||||
virtual SwappyStats getStats() override { return {}; }
|
||||
|
||||
virtual void capture(EGLDisplay dpy, EGLSurface surface);
|
||||
|
||||
protected:
|
||||
static constexpr int MAX_FRAME_LAG = 10;
|
||||
int32_t getFrameDelta(EGLnsecsANDROID start, EGLnsecsANDROID end);
|
||||
struct ThisFrame {
|
||||
TimePoint startTime;
|
||||
std::unique_ptr<EGL::FrameTimestamps> stats;
|
||||
};
|
||||
ThisFrame getThisFrame(EGLDisplay dpy, EGLSurface surface);
|
||||
void updateLatency(EGL::FrameTimestamps& frameStats,
|
||||
TimePoint frameStartTime);
|
||||
|
||||
const EGL& mEgl;
|
||||
const SwappyCommon& mSwappyCommon;
|
||||
|
||||
struct EGLFrame {
|
||||
EGLDisplay dpy;
|
||||
EGLSurface surface;
|
||||
EGLuint64KHR id;
|
||||
TimePoint startFrameTime;
|
||||
};
|
||||
std::vector<EGLFrame> mPendingFrames;
|
||||
EGLnsecsANDROID mPrevFrameTime = 0;
|
||||
std::atomic<int32_t> mLastLatency = {0};
|
||||
};
|
||||
|
||||
class FullFrameStatisticsGL : public LatencyFrameStatisticsGL {
|
||||
public:
|
||||
FullFrameStatisticsGL(const EGL& egl, const SwappyCommon& swappyCommon);
|
||||
~FullFrameStatisticsGL() = default;
|
||||
|
||||
void capture(EGLDisplay dpy, EGLSurface surface) override;
|
||||
|
||||
SwappyStats getStats() override;
|
||||
|
||||
bool isEssential() const override { return false; }
|
||||
|
||||
private:
|
||||
static constexpr std::chrono::nanoseconds LOG_EVERY_N_NS = 1s;
|
||||
|
||||
int updateFrames(EGLnsecsANDROID start, EGLnsecsANDROID end,
|
||||
uint64_t stat[]);
|
||||
void updateIdleFrames(EGL::FrameTimestamps& frameStats) REQUIRES(mMutex);
|
||||
void updateLateFrames(EGL::FrameTimestamps& frameStats) REQUIRES(mMutex);
|
||||
void updateOffsetFromPreviousFrame(EGL::FrameTimestamps& frameStats)
|
||||
REQUIRES(mMutex);
|
||||
void updateLatencyFrames(EGL::FrameTimestamps& frameStats,
|
||||
TimePoint frameStartTime) REQUIRES(mMutex);
|
||||
void logFrames() REQUIRES(mMutex);
|
||||
|
||||
std::mutex mMutex;
|
||||
SwappyStats mStats GUARDED_BY(mMutex) = {};
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,333 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "SwappyGL.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "Log.h"
|
||||
#include "Thread.h"
|
||||
#include "Trace.h"
|
||||
#include "system_utils.h"
|
||||
|
||||
#define LOG_TAG "Swappy"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
using std::chrono::milliseconds;
|
||||
using std::chrono::nanoseconds;
|
||||
|
||||
std::mutex SwappyGL::sInstanceMutex;
|
||||
std::unique_ptr<SwappyGL> SwappyGL::sInstance;
|
||||
|
||||
bool SwappyGL::init(JNIEnv *env, jobject jactivity) {
|
||||
std::lock_guard<std::mutex> lock(sInstanceMutex);
|
||||
if (sInstance) {
|
||||
ALOGE("Attempted to initialize SwappyGL twice");
|
||||
return false;
|
||||
}
|
||||
sInstance = std::make_unique<SwappyGL>(env, jactivity, ConstructorTag{});
|
||||
if (!sInstance->mEnableSwappy) {
|
||||
ALOGE("Failed to initialize SwappyGL");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SwappyGL::onChoreographer(int64_t frameTimeNanos) {
|
||||
TRACE_CALL();
|
||||
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
return;
|
||||
}
|
||||
|
||||
swappy->mCommonBase.onChoreographer(frameTimeNanos);
|
||||
}
|
||||
|
||||
bool SwappyGL::setWindow(ANativeWindow *window) {
|
||||
TRACE_CALL();
|
||||
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
ALOGE("Failed to get SwappyGL instance in setWindow");
|
||||
return false;
|
||||
}
|
||||
|
||||
swappy->mCommonBase.setANativeWindow(window);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SwappyGL::swap(EGLDisplay display, EGLSurface surface) {
|
||||
TRACE_CALL();
|
||||
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
|
||||
if (swappy->enabled()) {
|
||||
return swappy->swapInternal(display, surface);
|
||||
} else {
|
||||
return swappy->getEgl()->swapBuffers(display, surface) == EGL_TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
bool SwappyGL::lastFrameIsComplete(EGLDisplay display) {
|
||||
if (!getEgl()->lastFrameIsComplete(display)) {
|
||||
gamesdk::ScopedTrace trace("lastFrameIncomplete");
|
||||
ALOGV("lastFrameIncomplete");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SwappyGL::swapInternal(EGLDisplay display, EGLSurface surface) {
|
||||
const SwappyCommon::SwapHandlers handlers = {
|
||||
.lastFrameIsComplete = [&]() { return lastFrameIsComplete(display); },
|
||||
.getPrevFrameGpuTime =
|
||||
[&]() { return getEgl()->getFencePendingTime(); },
|
||||
};
|
||||
|
||||
mCommonBase.onPreSwap(handlers);
|
||||
|
||||
if (mCommonBase.needToSetPresentationTime()) {
|
||||
bool setPresentationTimeResult = setPresentationTime(display, surface);
|
||||
if (!setPresentationTimeResult) {
|
||||
return setPresentationTimeResult;
|
||||
}
|
||||
}
|
||||
|
||||
resetSyncFence(display);
|
||||
|
||||
bool swapBuffersResult =
|
||||
(getEgl()->swapBuffers(display, surface) == EGL_TRUE);
|
||||
|
||||
mCommonBase.onPostSwap(handlers);
|
||||
|
||||
return swapBuffersResult;
|
||||
}
|
||||
|
||||
void SwappyGL::addTracer(const SwappyTracer *tracer) {
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
return;
|
||||
}
|
||||
if (swappy->enabled() && tracer != nullptr)
|
||||
swappy->mCommonBase.addTracerCallbacks(*tracer);
|
||||
}
|
||||
|
||||
void SwappyGL::removeTracer(const SwappyTracer *tracer) {
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
return;
|
||||
}
|
||||
if (swappy->enabled() && tracer != nullptr)
|
||||
swappy->mCommonBase.removeTracerCallbacks(*tracer);
|
||||
}
|
||||
|
||||
nanoseconds SwappyGL::getSwapDuration() {
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy || !swappy->enabled()) {
|
||||
return -1ns;
|
||||
}
|
||||
return swappy->mCommonBase.getSwapDuration();
|
||||
};
|
||||
|
||||
void SwappyGL::setAutoSwapInterval(bool enabled) {
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
return;
|
||||
}
|
||||
if (swappy->enabled()) swappy->mCommonBase.setAutoSwapInterval(enabled);
|
||||
}
|
||||
|
||||
void SwappyGL::setAutoPipelineMode(bool enabled) {
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
return;
|
||||
}
|
||||
if (swappy->enabled()) swappy->mCommonBase.setAutoPipelineMode(enabled);
|
||||
}
|
||||
|
||||
void SwappyGL::setMaxAutoSwapDuration(std::chrono::nanoseconds maxDuration) {
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
return;
|
||||
}
|
||||
if (swappy->enabled())
|
||||
swappy->mCommonBase.setMaxAutoSwapDuration(maxDuration);
|
||||
}
|
||||
|
||||
void SwappyGL::enableStats(bool enabled) {
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!swappy->enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!swappy->getEgl()->statsSupported()) {
|
||||
ALOGI("stats are not suppored on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
if (!swappy->mFrameStatistics ||
|
||||
swappy->mFrameStatistics->isEssential()) {
|
||||
swappy->mFrameStatistics = std::make_shared<FullFrameStatisticsGL>(
|
||||
*swappy->mEgl, swappy->mCommonBase);
|
||||
ALOGI("Enabling stats");
|
||||
} else {
|
||||
ALOGI("Stats already enabled");
|
||||
}
|
||||
} else {
|
||||
swappy->mFrameStatistics = std::make_shared<LatencyFrameStatisticsGL>(
|
||||
*swappy->mEgl, swappy->mCommonBase);
|
||||
ALOGI("Disabling stats");
|
||||
}
|
||||
swappy->mCommonBase.setFrameStatistics(swappy->mFrameStatistics);
|
||||
}
|
||||
|
||||
void SwappyGL::recordFrameStart(EGLDisplay display, EGLSurface surface) {
|
||||
TRACE_CALL();
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (swappy->mFrameStatistics)
|
||||
swappy->mFrameStatistics->capture(display, surface);
|
||||
}
|
||||
|
||||
void SwappyGL::getStats(SwappyStats *stats) {
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (swappy->mFrameStatistics && !swappy->mFrameStatistics->isEssential())
|
||||
*stats = swappy->mFrameStatistics->getStats();
|
||||
}
|
||||
|
||||
SwappyGL *SwappyGL::getInstance() {
|
||||
std::lock_guard<std::mutex> lock(sInstanceMutex);
|
||||
return sInstance.get();
|
||||
}
|
||||
|
||||
bool SwappyGL::isEnabled() {
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
// This is a case of error.
|
||||
// We do not log anything here, so that we do not spam
|
||||
// the user when this function is called each frame.
|
||||
return false;
|
||||
}
|
||||
return swappy->enabled();
|
||||
}
|
||||
|
||||
void SwappyGL::destroyInstance() {
|
||||
std::lock_guard<std::mutex> lock(sInstanceMutex);
|
||||
sInstance.reset();
|
||||
}
|
||||
|
||||
void SwappyGL::setFenceTimeout(std::chrono::nanoseconds t) {
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy || !swappy->enabled()) {
|
||||
return;
|
||||
}
|
||||
swappy->mCommonBase.setFenceTimeout(t);
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds SwappyGL::getFenceTimeout() {
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy || !swappy->enabled()) {
|
||||
return std::chrono::nanoseconds(0);
|
||||
}
|
||||
return swappy->mCommonBase.getFenceTimeout();
|
||||
}
|
||||
|
||||
EGL *SwappyGL::getEgl() {
|
||||
static thread_local EGL *egl = nullptr;
|
||||
if (!egl) {
|
||||
std::lock_guard<std::mutex> lock(mEglMutex);
|
||||
egl = mEgl.get();
|
||||
}
|
||||
return egl;
|
||||
}
|
||||
|
||||
SwappyGL::SwappyGL(JNIEnv *env, jobject jactivity, ConstructorTag)
|
||||
: mFrameStatistics(nullptr), mCommonBase(env, jactivity) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mEglMutex);
|
||||
mEgl = EGL::create(mCommonBase.getFenceTimeout());
|
||||
if (!mEgl) {
|
||||
ALOGE("Failed to load EGL functions");
|
||||
mEnableSwappy = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mCommonBase.isValid()) {
|
||||
ALOGE("SwappyCommon could not initialize correctly.");
|
||||
mEnableSwappy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
mEnableSwappy =
|
||||
!gamesdk::GetSystemPropAsBool(SWAPPY_SYSTEM_PROP_KEY_DISABLE, false);
|
||||
if (!enabled()) {
|
||||
ALOGI("Swappy is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
ALOGI("SwappyGL initialized successfully");
|
||||
}
|
||||
|
||||
void SwappyGL::resetSyncFence(EGLDisplay display) {
|
||||
getEgl()->resetSyncFence(display);
|
||||
}
|
||||
|
||||
bool SwappyGL::setPresentationTime(EGLDisplay display, EGLSurface surface) {
|
||||
TRACE_CALL();
|
||||
|
||||
auto displayTimings = Settings::getInstance()->getDisplayTimings();
|
||||
|
||||
// if we are too close to the vsync, there is no need to set presentation
|
||||
// time
|
||||
if ((mCommonBase.getPresentationTime() - std::chrono::steady_clock::now()) <
|
||||
(mCommonBase.getRefreshPeriod() - displayTimings.sfOffset)) {
|
||||
return EGL_TRUE;
|
||||
}
|
||||
return getEgl()->setPresentationTime(display, surface,
|
||||
mCommonBase.getPresentationTime());
|
||||
}
|
||||
|
||||
void SwappyGL::setBufferStuffingFixWait(int32_t n_frames) {
|
||||
TRACE_CALL();
|
||||
SwappyGL *swappy = getInstance();
|
||||
if (!swappy) {
|
||||
return;
|
||||
}
|
||||
swappy->mCommonBase.setBufferStuffingFixWait(n_frames);
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
|
||||
#include "EGL.h"
|
||||
#include "FrameStatisticsGL.h"
|
||||
#include "SwappyCommon.h"
|
||||
#include "swappy/swappyGL.h"
|
||||
#include "swappy/swappyGL_extra.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
using EGLDisplay = void *;
|
||||
using EGLSurface = void *;
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
class SwappyGL {
|
||||
private:
|
||||
// Allows construction with std::unique_ptr from a static method, but
|
||||
// disallows construction outside of the class since no one else can
|
||||
// construct a ConstructorTag
|
||||
struct ConstructorTag {};
|
||||
|
||||
public:
|
||||
SwappyGL(JNIEnv *env, jobject jactivity, ConstructorTag);
|
||||
static bool init(JNIEnv *env, jobject jactivity);
|
||||
|
||||
static bool setWindow(ANativeWindow *window);
|
||||
|
||||
static void onChoreographer(int64_t frameTimeNanos);
|
||||
|
||||
static bool swap(EGLDisplay display, EGLSurface surface);
|
||||
|
||||
// Pass callbacks for tracing within the swap function
|
||||
static void addTracer(const SwappyTracer *tracer);
|
||||
|
||||
static void removeTracer(const SwappyTracer *tracer);
|
||||
|
||||
static std::chrono::nanoseconds getSwapDuration();
|
||||
|
||||
static void setAutoSwapInterval(bool enabled);
|
||||
|
||||
static void setAutoPipelineMode(bool enabled);
|
||||
|
||||
static void setMaxAutoSwapDuration(std::chrono::nanoseconds maxDuration);
|
||||
|
||||
static void enableStats(bool enabled);
|
||||
static void recordFrameStart(EGLDisplay display, EGLSurface surface);
|
||||
static void getStats(SwappyStats *stats);
|
||||
static bool isEnabled();
|
||||
static void destroyInstance();
|
||||
|
||||
static void setFenceTimeout(std::chrono::nanoseconds t);
|
||||
static std::chrono::nanoseconds getFenceTimeout();
|
||||
|
||||
static void setBufferStuffingFixWait(int32_t n_frames);
|
||||
|
||||
private:
|
||||
static SwappyGL *getInstance();
|
||||
|
||||
bool enabled() const { return mEnableSwappy; }
|
||||
|
||||
EGL *getEgl();
|
||||
|
||||
bool swapInternal(EGLDisplay display, EGLSurface surface);
|
||||
|
||||
bool lastFrameIsComplete(EGLDisplay display);
|
||||
|
||||
// Destroys the previous sync fence (if any) and creates a new one for this
|
||||
// frame
|
||||
void resetSyncFence(EGLDisplay display);
|
||||
|
||||
// Computes the desired presentation time based on the swap interval and
|
||||
// sets it using eglPresentationTimeANDROID
|
||||
bool setPresentationTime(EGLDisplay display, EGLSurface surface);
|
||||
|
||||
bool mEnableSwappy = true;
|
||||
|
||||
static std::mutex sInstanceMutex;
|
||||
static std::unique_ptr<SwappyGL> sInstance GUARDED_BY(sInstanceMutex);
|
||||
|
||||
std::mutex mEglMutex;
|
||||
std::unique_ptr<EGL> mEgl;
|
||||
|
||||
std::shared_ptr<LatencyFrameStatisticsGL> mFrameStatistics;
|
||||
|
||||
SwappyCommon mCommonBase;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// API entry points
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "Settings.h"
|
||||
#include "SwappyGL.h"
|
||||
#include "swappy/swappyGL.h"
|
||||
|
||||
using namespace swappy;
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Internal function to track Swappy version bundled in a binary.
|
||||
void SWAPPY_VERSION_SYMBOL();
|
||||
|
||||
/**
|
||||
* @brief Initialize Swappy, getting the required Android parameters from the
|
||||
* display subsystem via JNI.
|
||||
* @param env The JNI environment where Swappy is used
|
||||
* @param jactivity The activity where Swappy is used
|
||||
* @return false if Swappy failed to initialize.
|
||||
* @see SwappyGL_destroy
|
||||
*/
|
||||
bool SwappyGL_init(JNIEnv *env, jobject jactivity) {
|
||||
// This call ensures that the header and the linked library are from the
|
||||
// same version (if not, a linker error will be triggered because of an
|
||||
// undefined symbolP).
|
||||
SWAPPY_VERSION_SYMBOL();
|
||||
return SwappyGL::init(env, jactivity);
|
||||
}
|
||||
|
||||
void SwappyGL_destroy() { SwappyGL::destroyInstance(); }
|
||||
|
||||
void SwappyGL_onChoreographer(int64_t frameTimeNanos) {
|
||||
SwappyGL::onChoreographer(frameTimeNanos);
|
||||
}
|
||||
|
||||
bool SwappyGL_setWindow(ANativeWindow *window) {
|
||||
return SwappyGL::setWindow(window);
|
||||
}
|
||||
|
||||
bool SwappyGL_swap(EGLDisplay display, EGLSurface surface) {
|
||||
return SwappyGL::swap(display, surface);
|
||||
}
|
||||
|
||||
void SwappyGL_setUseAffinity(bool tf) {
|
||||
Settings::getInstance()->setUseAffinity(tf);
|
||||
}
|
||||
|
||||
void SwappyGL_setSwapIntervalNS(uint64_t swap_ns) {
|
||||
Settings::getInstance()->setSwapDuration(swap_ns);
|
||||
}
|
||||
|
||||
uint64_t SwappyGL_getRefreshPeriodNanos() {
|
||||
return Settings::getInstance()->getDisplayTimings().refreshPeriod.count();
|
||||
}
|
||||
|
||||
bool SwappyGL_getUseAffinity() {
|
||||
return Settings::getInstance()->getUseAffinity();
|
||||
}
|
||||
|
||||
uint64_t SwappyGL_getSwapIntervalNS() {
|
||||
return SwappyGL::getSwapDuration().count();
|
||||
}
|
||||
|
||||
void SwappyGL_injectTracer(const SwappyTracer *t) { SwappyGL::addTracer(t); }
|
||||
|
||||
void SwappyGL_setAutoSwapInterval(bool enabled) {
|
||||
SwappyGL::setAutoSwapInterval(enabled);
|
||||
}
|
||||
|
||||
void SwappyGL_setMaxAutoSwapIntervalNS(uint64_t max_swap_ns) {
|
||||
SwappyGL::setMaxAutoSwapDuration(std::chrono::nanoseconds(max_swap_ns));
|
||||
}
|
||||
|
||||
void SwappyGL_setAutoPipelineMode(bool enabled) {
|
||||
SwappyGL::setAutoPipelineMode(enabled);
|
||||
}
|
||||
|
||||
void SwappyGL_enableStats(bool enabled) { SwappyGL::enableStats(enabled); }
|
||||
|
||||
void SwappyGL_recordFrameStart(EGLDisplay display, EGLSurface surface) {
|
||||
SwappyGL::recordFrameStart(display, surface);
|
||||
}
|
||||
|
||||
void SwappyGL_getStats(SwappyStats *stats) { SwappyGL::getStats(stats); }
|
||||
|
||||
bool SwappyGL_isEnabled() { return SwappyGL::isEnabled(); }
|
||||
|
||||
void SwappyGL_setFenceTimeoutNS(uint64_t t) {
|
||||
SwappyGL::setFenceTimeout(std::chrono::nanoseconds(t));
|
||||
}
|
||||
|
||||
uint64_t SwappyGL_getFenceTimeoutNS() {
|
||||
return SwappyGL::getFenceTimeout().count();
|
||||
}
|
||||
|
||||
void SwappyGL_setBufferStuffingFixWait(int32_t n_frames) {
|
||||
SwappyGL::setBufferStuffingFixWait(n_frames);
|
||||
}
|
||||
|
||||
void SwappyGL_uninjectTracer(const SwappyTracer *t) {
|
||||
SwappyGL::removeTracer(t);
|
||||
}
|
||||
|
||||
} // extern "C" {
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
# Lint as: python3
|
||||
"""Check whether Swappy is working properly.
|
||||
|
||||
This script takes a logcat (list of lines - e.g. from readlines()) and the path
|
||||
to a systrace and applies checks on them to verify whether Swappy is working
|
||||
properly or not. A zero return code means all looks fine, while other codes
|
||||
indicate different problems.
|
||||
"""
|
||||
|
||||
import decimal
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
# Beautiful Soup parsing library
|
||||
import bs4
|
||||
|
||||
def test_swappy_initialized(logcat: List[bytes]) -> str:
|
||||
"""Verify whether Swappy initialized successfully
|
||||
|
||||
Takes logcat data argument and searches for FrameStatistics blocks like this
|
||||
example:
|
||||
|
||||
Args:
|
||||
logcat: Logcat data.
|
||||
|
||||
Returns:
|
||||
The Swappy backend name or an empty string if Swappy is not initialized.
|
||||
"""
|
||||
choreographer_re = re.compile(rb'Using (\S+) Choreographer')
|
||||
choreographer_backend = ''
|
||||
for line in logcat:
|
||||
match = choreographer_re.search(line)
|
||||
if match:
|
||||
choreographer_backend = match.group(1)
|
||||
|
||||
return choreographer_backend
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
def test_swappy_frame_stats(logcat: List[bytes]) -> bool:
|
||||
"""Verify whether Swappy is producing frame statistics.
|
||||
|
||||
Takes logcat data argument and searches for FrameStatistics blocks like this
|
||||
example:
|
||||
|
||||
12-02 00:24:47.630: I/FrameStatistics(15134): == Frame statistics ==
|
||||
12-02 00:24:47.630: I/FrameStatistics(15134): total frames: 123
|
||||
12-02 00:24:47.630: I/FrameStatistics(15134): Buckets: [0] [1] [2] [3] [4] [5]
|
||||
12-02 00:24:47.630: I/FrameStatistics(15134): idle frames: 116 7 0 0 0 0
|
||||
12-02 00:24:47.630: I/FrameStatistics(15134): late frames: 121 1 1 0 0 0
|
||||
12-02 00:24:47.630: I/FrameStatistics(15134): offset from previous frame: 122 0 0 0 0 0
|
||||
12-02 00:24:47.630: I/FrameStatistics(15134): frame latency: 0 121 1 1 0 0
|
||||
|
||||
A basic consistency check is applied: successive "total frames" values should
|
||||
never decrease. If not even a single block is found, Swappy is deemed to be
|
||||
off.
|
||||
|
||||
Args:
|
||||
logcat: Logcat data.
|
||||
|
||||
Returns:
|
||||
True if it looks like Swappy is on
|
||||
"""
|
||||
# pylint: enable=line-too-long
|
||||
frame_stats_re = re.compile(rb'I/FrameStatistics\(\d+\): total frames: (\d+)')
|
||||
total_frames = -1
|
||||
for line in logcat:
|
||||
match = frame_stats_re.search(line)
|
||||
if match:
|
||||
new_total_frames = int(match.group(1))
|
||||
if new_total_frames < total_frames:
|
||||
return False
|
||||
total_frames = new_total_frames
|
||||
|
||||
print(dict(total_frames=total_frames))
|
||||
return True if total_frames > -1 else False
|
||||
|
||||
|
||||
def test_swappy_working(
|
||||
logcat: List[bytes], systrace_path: str, latency_thr_ms: float) -> bool:
|
||||
"""Verify whether Swappy seems to be working correctly.
|
||||
|
||||
For more details on this algorithm please see: http://go/swappy-systrace.
|
||||
|
||||
Args:
|
||||
logcat: Logcat data.
|
||||
systrace_path: Path to the systrace file.
|
||||
latency_thr_ms: Maximum average queue latency expected if Swappy is working.
|
||||
|
||||
Returns:
|
||||
True if it looks like Swappy is working correctly.
|
||||
"""
|
||||
# Finding out surfaceflinger's PID from the logcat is way easier than from the
|
||||
# systrace
|
||||
sf_pid = get_surfaceflinger_pid(logcat)
|
||||
if sf_pid is None:
|
||||
print("Couldn't find surface flinger pid in logcat")
|
||||
|
||||
trace_data = get_trace_data(systrace_path)
|
||||
soup = bs4.BeautifulSoup(trace_data, features='html.parser')
|
||||
script_tags = soup.find_all('script', attrs={'class': 'trace-data'})
|
||||
|
||||
# The number of trace-data script tags apparently depends on device model.
|
||||
# If there's more than one, we look at the longest one because probably
|
||||
# (hopefully) that's where the actual trace is.
|
||||
trace_tag = max([(len(tag.string), tag) for tag in script_tags])[1]
|
||||
data = trace_tag.string.split('\n')
|
||||
data = [line.strip() for line in data if line.strip() and line[0] != '#']
|
||||
|
||||
# We are interested in COUNTER events for the line named:
|
||||
# 'SurfaceView - <PACKAGE>/<ACTIVITY>'
|
||||
# Examples seen so far illustrate the expected possible formats:
|
||||
# pylint: disable=line-too-long
|
||||
# <...>-3194 (-----) [001] ...1 33425.567500: tracing_mark_write: C|3194|SurfaceView - com.gameloft.android.ANMP.GloftA8HM/com.gameloft.android.ANMP.GloftA8HM.MainActivity@85d2511@0#0|0
|
||||
# surfaceflinger-3194 ( 3194) [001] ...1 33662.535536: tracing_mark_write: C|3194|SurfaceView - com.gameloft.android.ANMP.GloftA8HM/com.gameloft.android.ANMP.GloftA8HM.MainActivity@73c2e5f@0#0|0
|
||||
# Binder:486_2-533 ( 486) [000] ...1 69410.718619: tracing_mark_write: C|486|SurfaceView - com.prefabulated.swappy/com.prefabulated.bouncyball.OrbitActivity#0|2
|
||||
# pylint: enable=line-too-long
|
||||
if sf_pid is not None:
|
||||
event_re_str = (
|
||||
r' (?P<time>\d+\.\d{6}): tracing_mark_write: C\|'
|
||||
+ str(sf_pid)
|
||||
+ r'\|SurfaceView - .*\|(?P<value>\d+)')
|
||||
else:
|
||||
event_re_str = (
|
||||
r' (?P<time>\d+\.\d{6}): tracing_mark_write: C\|\d+\|SurfaceView - .*\|(?P<value>\d+)')
|
||||
event_re = re.compile(event_re_str)
|
||||
|
||||
current_time_s = None
|
||||
current_depth = None
|
||||
enqueued_frames = 0
|
||||
total_queuing_time_s = decimal.Decimal(0)
|
||||
for line in data:
|
||||
match = event_re.search(line)
|
||||
if match:
|
||||
new_time_s = decimal.Decimal(match.group('time'))
|
||||
new_depth = int(match.group('value'))
|
||||
if current_depth is not None:
|
||||
if new_depth > current_depth:
|
||||
enqueued_frames += new_depth - current_depth
|
||||
if current_depth > 0:
|
||||
total_queuing_time_s += current_depth * (new_time_s - current_time_s)
|
||||
current_time_s = new_time_s
|
||||
current_depth = new_depth
|
||||
|
||||
total_queuing_time_ms = total_queuing_time_s * 1000
|
||||
avg_queue_latency_ms = total_queuing_time_ms / enqueued_frames
|
||||
print(dict(
|
||||
enqueued_frames=enqueued_frames,
|
||||
total_queuing_time_ms=float(total_queuing_time_ms),
|
||||
latency_thr_ms=latency_thr_ms,
|
||||
avg_queue_latency_ms=float(avg_queue_latency_ms)))
|
||||
return avg_queue_latency_ms < latency_thr_ms
|
||||
|
||||
def get_surfaceflinger_pid(logcat: List[bytes]) -> int:
|
||||
"""Obtain the surfaceflinger PID from the logcat.
|
||||
|
||||
All instances of the PID in the file must be the same, or an AssertionError
|
||||
will be raised.
|
||||
|
||||
Args:
|
||||
logcat: Logcat data.
|
||||
|
||||
Returns:
|
||||
surfaceflinger PID number
|
||||
"""
|
||||
# Look for e.g.
|
||||
# 12-03 12:44:37.667: W/SurfaceFlinger(1234):'
|
||||
sf_pid_re = re.compile(
|
||||
rb'\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d: [WIDE]\/SurfaceFlinger\((\d+)\): ')
|
||||
# Sometimes adb logcat gives a different format, like this:
|
||||
# 12-03 12:44:37.667 602 1641 W SurfaceFlinger'
|
||||
sf_pid_2_re = re.compile(rb'\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\s+(\d+)\s+(\d+)\s+[WIDE]\s+SurfaceFlinger')
|
||||
sf_pid = None
|
||||
for line in logcat:
|
||||
match = sf_pid_re.search(line)
|
||||
if not match:
|
||||
match = sf_pid_2_re.search(line)
|
||||
if match:
|
||||
this_pid = int(match.group(1))
|
||||
if sf_pid is None:
|
||||
sf_pid = this_pid
|
||||
else:
|
||||
assert sf_pid == this_pid
|
||||
return sf_pid
|
||||
|
||||
|
||||
def get_trace_data(systrace_path: str) -> bytes:
|
||||
"""Open file and get just the trace data (ignoring HTML data and etc.)."""
|
||||
with open(systrace_path, 'rb') as f:
|
||||
file_data = f.read()
|
||||
start_marker = b'<!-- BEGIN TRACE -->'
|
||||
end_marker = b'<!-- END TRACE -->'
|
||||
start_pos = file_data.index(start_marker) + len(start_marker)
|
||||
end_pos = file_data.index(end_marker)
|
||||
return file_data[start_pos:end_pos]
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# Lint as: python3
|
||||
"""Check whether Swappy is working properly.
|
||||
|
||||
This script takes paths to a logcat and a systrace and applies checks on them to
|
||||
verify whether Swappy is working properly or not. A zero return code means all
|
||||
looks fine, while other codes indicate different problems.
|
||||
|
||||
To create the logcat file:
|
||||
adb logcat -d > $filename$
|
||||
To create the systrace file:
|
||||
python systrace/systrace.py -e $device ID$ -o $output file$ -t $length in seconds$ gfx
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import argparse
|
||||
|
||||
import lib_test_swappy
|
||||
|
||||
RET_CODE_SWAPPY_NOT_ON = 10
|
||||
RET_CODE_SWAPPY_NOT_WORKING = 11
|
||||
RET_CODE_SWAPPY_NO_FRAME_STATS = 12
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description='Check Swappy logcat and systrace output')
|
||||
parser.add_argument('--logcat', type=str, help='logcat file', required=True)
|
||||
parser.add_argument('--systrace', type=str, help='systrace file')
|
||||
parser.add_argument('--latency_thr_ms', type=float, default=25.0,
|
||||
help='Latency threshold in milliseconds')
|
||||
parser.add_argument('--no_frame_stats', action='store_true', help="Don't check frame statistics")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print('logcat:', args.logcat)
|
||||
print('systrace:', args.systrace)
|
||||
|
||||
with open(args.logcat, 'rb') as logcat_file:
|
||||
logcat = logcat_file.readlines()
|
||||
|
||||
swappy_backend = lib_test_swappy.test_swappy_initialized(logcat)
|
||||
if not swappy_backend:
|
||||
sys.exit(RET_CODE_SWAPPY_NOT_ON)
|
||||
print("Swappy choreographer backend = %s" % swappy_backend)
|
||||
|
||||
if not args.no_frame_stats:
|
||||
if not lib_test_swappy.test_swappy_frame_stats(logcat):
|
||||
sys.exit(RET_CODE_SWAPPY_NO_FRAME_STATS)
|
||||
|
||||
if args.systrace:
|
||||
if not lib_test_swappy.test_swappy_working(logcat, args.systrace, args.latency_thr_ms):
|
||||
sys.exit(RET_CODE_SWAPPY_NOT_WORKING)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "SwappyVk.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
class DefaultSwappyVkFunctionProvider {
|
||||
public:
|
||||
static bool Init() {
|
||||
if (!mLibVulkan) {
|
||||
// This is the first time we've been called
|
||||
mLibVulkan = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (!mLibVulkan) {
|
||||
// If Vulkan doesn't exist, bail-out early:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static void* GetProcAddr(const char* name) {
|
||||
if (!mLibVulkan && !Init()) return nullptr;
|
||||
return dlsym(mLibVulkan, name);
|
||||
}
|
||||
static void Close() {
|
||||
if (mLibVulkan) {
|
||||
dlclose(mLibVulkan);
|
||||
mLibVulkan = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static void* mLibVulkan;
|
||||
};
|
||||
|
||||
void* DefaultSwappyVkFunctionProvider::mLibVulkan = nullptr;
|
||||
|
||||
bool SwappyVk::InitFunctions() {
|
||||
if (pFunctionProvider == nullptr) {
|
||||
static SwappyVkFunctionProvider c_provider;
|
||||
c_provider.init = &DefaultSwappyVkFunctionProvider::Init;
|
||||
c_provider.getProcAddr = &DefaultSwappyVkFunctionProvider::GetProcAddr;
|
||||
c_provider.close = &DefaultSwappyVkFunctionProvider::Close;
|
||||
pFunctionProvider = &c_provider;
|
||||
}
|
||||
if (pFunctionProvider->init()) {
|
||||
LoadVulkanFunctions(pFunctionProvider);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void SwappyVk::SetFunctionProvider(
|
||||
const SwappyVkFunctionProvider* functionProvider) {
|
||||
if (pFunctionProvider != nullptr) pFunctionProvider->close();
|
||||
pFunctionProvider = functionProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic/Singleton implementation of swappyVkDetermineDeviceExtensions.
|
||||
*/
|
||||
void SwappyVk::swappyVkDetermineDeviceExtensions(
|
||||
VkPhysicalDevice physicalDevice, uint32_t availableExtensionCount,
|
||||
VkExtensionProperties* pAvailableExtensions,
|
||||
uint32_t* pRequiredExtensionCount, char** pRequiredExtensions) {
|
||||
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
|
||||
// TODO: Refactor this to be more concise:
|
||||
if (!pRequiredExtensions) {
|
||||
for (uint32_t i = 0; i < availableExtensionCount; i++) {
|
||||
if (!strcmp(VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME,
|
||||
pAvailableExtensions[i].extensionName)) {
|
||||
(*pRequiredExtensionCount)++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
doesPhysicalDeviceHaveGoogleDisplayTiming[physicalDevice] = false;
|
||||
for (uint32_t i = 0, j = 0; i < availableExtensionCount; i++) {
|
||||
if (!strcmp(VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME,
|
||||
pAvailableExtensions[i].extensionName)) {
|
||||
if (j < *pRequiredExtensionCount) {
|
||||
strcpy(pRequiredExtensions[j++],
|
||||
VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME);
|
||||
doesPhysicalDeviceHaveGoogleDisplayTiming[physicalDevice] =
|
||||
true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
doesPhysicalDeviceHaveGoogleDisplayTiming[physicalDevice] = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SwappyVk::SetQueueFamilyIndex(VkDevice device, VkQueue queue,
|
||||
uint32_t queueFamilyIndex) {
|
||||
perQueueFamilyIndex[queue] = {device, queueFamilyIndex};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic/Singleton implementation of swappyVkGetRefreshCycleDuration.
|
||||
*/
|
||||
bool SwappyVk::GetRefreshCycleDuration(JNIEnv* env, jobject jactivity,
|
||||
VkPhysicalDevice physicalDevice,
|
||||
VkDevice device,
|
||||
VkSwapchainKHR swapchain,
|
||||
uint64_t* pRefreshDuration) {
|
||||
auto& pImplementation = perSwapchainImplementation[swapchain];
|
||||
if (!pImplementation) {
|
||||
if (!InitFunctions()) {
|
||||
// If Vulkan doesn't exist, bail-out early
|
||||
return false;
|
||||
}
|
||||
|
||||
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
|
||||
// First, based on whether VK_GOOGLE_display_timing is available
|
||||
// (determined and cached by swappyVkDetermineDeviceExtensions),
|
||||
// determine which derived class to use to implement the rest of the API
|
||||
if (doesPhysicalDeviceHaveGoogleDisplayTiming[physicalDevice]) {
|
||||
pImplementation = std::make_shared<SwappyVkGoogleDisplayTiming>(
|
||||
env, jactivity, physicalDevice, device, pFunctionProvider);
|
||||
ALOGV(
|
||||
"SwappyVk initialized for VkDevice %p using "
|
||||
"VK_GOOGLE_display_timing on Android",
|
||||
device);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
pImplementation = std::make_shared<SwappyVkFallback>(
|
||||
env, jactivity, physicalDevice, device, pFunctionProvider);
|
||||
ALOGV("SwappyVk initialized for VkDevice %p using Android fallback",
|
||||
device);
|
||||
}
|
||||
|
||||
if (!pImplementation) { // should never happen
|
||||
ALOGE(
|
||||
"SwappyVk could not find or create correct implementation for "
|
||||
"the current environment: "
|
||||
"%p, %p",
|
||||
physicalDevice, device);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Now, call that derived class to get the refresh duration to return
|
||||
return pImplementation->doGetRefreshCycleDuration(swapchain,
|
||||
pRefreshDuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic/Singleton implementation of swappyVkSetWindow.
|
||||
*/
|
||||
void SwappyVk::SetWindow(VkDevice device, VkSwapchainKHR swapchain,
|
||||
ANativeWindow* window) {
|
||||
auto& pImplementation = perSwapchainImplementation[swapchain];
|
||||
if (!pImplementation) {
|
||||
return;
|
||||
}
|
||||
pImplementation->doSetWindow(window);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic/Singleton implementation of swappyVkSetSwapInterval.
|
||||
*/
|
||||
void SwappyVk::SetSwapDuration(VkDevice device, VkSwapchainKHR swapchain,
|
||||
uint64_t swapNs) {
|
||||
auto& pImplementation = perSwapchainImplementation[swapchain];
|
||||
if (!pImplementation) {
|
||||
return;
|
||||
}
|
||||
pImplementation->doSetSwapInterval(swapchain, swapNs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic/Singleton implementation of swappyVkQueuePresent.
|
||||
*/
|
||||
VkResult SwappyVk::QueuePresent(VkQueue queue,
|
||||
const VkPresentInfoKHR* pPresentInfo) {
|
||||
if (perQueueFamilyIndex.find(queue) == perQueueFamilyIndex.end()) {
|
||||
ALOGE("Unknown queue %p. Did you call SwappyVkSetQueueFamilyIndex ?",
|
||||
queue);
|
||||
return VK_INCOMPLETE;
|
||||
}
|
||||
|
||||
// This command doesn't have a VkDevice. It should have at least one
|
||||
// VkSwapchainKHR's. For this command, all VkSwapchainKHR's will have the
|
||||
// same VkDevice and VkQueue.
|
||||
if ((pPresentInfo->swapchainCount == 0) || (!pPresentInfo->pSwapchains)) {
|
||||
// This shouldn't happen, but if it does, something is really wrong.
|
||||
return VK_ERROR_DEVICE_LOST;
|
||||
}
|
||||
auto& pImplementation =
|
||||
perSwapchainImplementation[*pPresentInfo->pSwapchains];
|
||||
if (pImplementation) {
|
||||
return pImplementation->doQueuePresent(
|
||||
queue, perQueueFamilyIndex[queue].queueFamilyIndex, pPresentInfo);
|
||||
} else {
|
||||
// This should only happen if the API was used wrong (e.g. they never
|
||||
// called swappyVkGetRefreshCycleDuration).
|
||||
// NOTE: Technically, a Vulkan library shouldn't protect a user from
|
||||
// themselves, but we'll be friendlier
|
||||
return VK_ERROR_DEVICE_LOST;
|
||||
}
|
||||
}
|
||||
|
||||
void SwappyVk::DestroySwapchain(VkDevice /*device*/, VkSwapchainKHR swapchain) {
|
||||
auto swapchain_it = perSwapchainImplementation.find(swapchain);
|
||||
if (swapchain_it == perSwapchainImplementation.end()) return;
|
||||
perSwapchainImplementation.erase(swapchain);
|
||||
}
|
||||
|
||||
void SwappyVk::DestroyDevice(VkDevice device) {
|
||||
{
|
||||
// Erase swapchains
|
||||
auto it = perSwapchainImplementation.begin();
|
||||
while (it != perSwapchainImplementation.end()) {
|
||||
if (it->second->getDevice() == device) {
|
||||
it = perSwapchainImplementation.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
// Erase the device
|
||||
auto it = perQueueFamilyIndex.begin();
|
||||
while (it != perQueueFamilyIndex.end()) {
|
||||
if (it->second.device == device) {
|
||||
it = perQueueFamilyIndex.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SwappyVk::SetAutoSwapInterval(bool enabled) {
|
||||
for (auto i : perSwapchainImplementation) {
|
||||
i.second->setAutoSwapInterval(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
void SwappyVk::SetAutoPipelineMode(bool enabled) {
|
||||
for (auto i : perSwapchainImplementation) {
|
||||
i.second->setAutoPipelineMode(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
void SwappyVk::SetMaxAutoSwapDuration(std::chrono::nanoseconds maxDuration) {
|
||||
for (auto i : perSwapchainImplementation) {
|
||||
i.second->setMaxAutoSwapDuration(maxDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void SwappyVk::SetFenceTimeout(std::chrono::nanoseconds t) {
|
||||
for (auto i : perSwapchainImplementation) {
|
||||
i.second->setFenceTimeout(t);
|
||||
}
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds SwappyVk::GetFenceTimeout() const {
|
||||
auto it = perSwapchainImplementation.begin();
|
||||
if (it != perSwapchainImplementation.end()) {
|
||||
return it->second->getFenceTimeout();
|
||||
}
|
||||
return std::chrono::nanoseconds(0);
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds SwappyVk::GetSwapInterval(VkSwapchainKHR swapchain) {
|
||||
auto it = perSwapchainImplementation.find(swapchain);
|
||||
if (it != perSwapchainImplementation.end())
|
||||
return it->second->getSwapInterval();
|
||||
return std::chrono::nanoseconds(0);
|
||||
}
|
||||
|
||||
void SwappyVk::addTracer(const SwappyTracer* t) {
|
||||
for (auto i : perSwapchainImplementation) {
|
||||
i.second->addTracer(t);
|
||||
}
|
||||
}
|
||||
|
||||
void SwappyVk::removeTracer(const SwappyTracer* t) {
|
||||
for (auto i : perSwapchainImplementation) {
|
||||
i.second->removeTracer(t);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "SwappyVkBase.h"
|
||||
#include "SwappyVkFallback.h"
|
||||
#include "SwappyVkGoogleDisplayTiming.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
// The API functions call methods of the singleton SwappyVk class.
|
||||
// Those methods call virtual methods of the abstract SwappyVkBase class,
|
||||
// which is actually implemented by one of the derived/concrete classes:
|
||||
//
|
||||
// - SwappyVkGoogleDisplayTiming
|
||||
// - SwappyVkFallback
|
||||
|
||||
/***************************************************************************************************
|
||||
*
|
||||
* Singleton class that provides the high-level implementation of the Swappy
|
||||
*entrypoints.
|
||||
*
|
||||
***************************************************************************************************/
|
||||
/**
|
||||
* Singleton class that provides the high-level implementation of the Swappy
|
||||
* entrypoints.
|
||||
*
|
||||
* This class determines which low-level implementation to use for each physical
|
||||
* device, and then calls that class's do-method for the entrypoint.
|
||||
*/
|
||||
class SwappyVk {
|
||||
public:
|
||||
static SwappyVk& getInstance() {
|
||||
static SwappyVk instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
~SwappyVk() {
|
||||
if (pFunctionProvider) {
|
||||
pFunctionProvider->close();
|
||||
}
|
||||
}
|
||||
|
||||
void swappyVkDetermineDeviceExtensions(
|
||||
VkPhysicalDevice physicalDevice, uint32_t availableExtensionCount,
|
||||
VkExtensionProperties* pAvailableExtensions,
|
||||
uint32_t* pRequiredExtensionCount, char** pRequiredExtensions);
|
||||
void SetQueueFamilyIndex(VkDevice device, VkQueue queue,
|
||||
uint32_t queueFamilyIndex);
|
||||
bool GetRefreshCycleDuration(JNIEnv* env, jobject jactivity,
|
||||
VkPhysicalDevice physicalDevice,
|
||||
VkDevice device, VkSwapchainKHR swapchain,
|
||||
uint64_t* pRefreshDuration);
|
||||
void SetWindow(VkDevice device, VkSwapchainKHR swapchain,
|
||||
ANativeWindow* window);
|
||||
void SetSwapDuration(VkDevice device, VkSwapchainKHR swapchain,
|
||||
uint64_t swapNs);
|
||||
VkResult QueuePresent(VkQueue queue, const VkPresentInfoKHR* pPresentInfo);
|
||||
void DestroySwapchain(VkDevice device, VkSwapchainKHR swapchain);
|
||||
void DestroyDevice(VkDevice device);
|
||||
|
||||
void SetAutoSwapInterval(bool enabled);
|
||||
void SetAutoPipelineMode(bool enabled);
|
||||
void SetMaxAutoSwapDuration(std::chrono::nanoseconds maxDuration);
|
||||
void SetFenceTimeout(std::chrono::nanoseconds duration);
|
||||
std::chrono::nanoseconds GetFenceTimeout() const;
|
||||
std::chrono::nanoseconds GetSwapInterval(VkSwapchainKHR swapchain);
|
||||
|
||||
void addTracer(const SwappyTracer* t);
|
||||
void removeTracer(const SwappyTracer* t);
|
||||
|
||||
void SetFunctionProvider(const SwappyVkFunctionProvider* pFunctionProvider);
|
||||
bool InitFunctions();
|
||||
|
||||
private:
|
||||
std::map<VkPhysicalDevice, bool> doesPhysicalDeviceHaveGoogleDisplayTiming;
|
||||
std::map<VkSwapchainKHR, std::shared_ptr<SwappyVkBase>>
|
||||
perSwapchainImplementation;
|
||||
|
||||
struct QueueFamilyIndex {
|
||||
VkDevice device;
|
||||
uint32_t queueFamilyIndex;
|
||||
};
|
||||
std::map<VkQueue, QueueFamilyIndex> perQueueFamilyIndex;
|
||||
|
||||
const SwappyVkFunctionProvider* pFunctionProvider = nullptr;
|
||||
|
||||
private:
|
||||
SwappyVk() {} // Need to implement this constructor
|
||||
|
||||
// Forbid copies.
|
||||
SwappyVk(SwappyVk const&) = delete;
|
||||
void operator=(SwappyVk const&) = delete;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,434 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "SwappyVkBase.h"
|
||||
|
||||
#include "system_utils.h"
|
||||
|
||||
#define LOG_TAG "SwappyVkBase"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
PFN_vkCreateCommandPool vkCreateCommandPool = nullptr;
|
||||
PFN_vkDestroyCommandPool vkDestroyCommandPool = nullptr;
|
||||
PFN_vkCreateFence vkCreateFence = nullptr;
|
||||
PFN_vkDestroyFence vkDestroyFence = nullptr;
|
||||
PFN_vkWaitForFences vkWaitForFences = nullptr;
|
||||
PFN_vkGetFenceStatus vkGetFenceStatus = nullptr;
|
||||
PFN_vkResetFences vkResetFences = nullptr;
|
||||
PFN_vkCreateSemaphore vkCreateSemaphore = nullptr;
|
||||
PFN_vkDestroySemaphore vkDestroySemaphore = nullptr;
|
||||
PFN_vkCreateEvent vkCreateEvent = nullptr;
|
||||
PFN_vkDestroyEvent vkDestroyEvent = nullptr;
|
||||
PFN_vkCmdSetEvent vkCmdSetEvent = nullptr;
|
||||
PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers = nullptr;
|
||||
PFN_vkFreeCommandBuffers vkFreeCommandBuffers = nullptr;
|
||||
PFN_vkBeginCommandBuffer vkBeginCommandBuffer = nullptr;
|
||||
PFN_vkEndCommandBuffer vkEndCommandBuffer = nullptr;
|
||||
PFN_vkQueueSubmit vkQueueSubmit = nullptr;
|
||||
|
||||
void LoadVulkanFunctions(const SwappyVkFunctionProvider* pFunctionProvider) {
|
||||
if (vkCreateCommandPool == nullptr) {
|
||||
vkCreateCommandPool = reinterpret_cast<PFN_vkCreateCommandPool>(
|
||||
pFunctionProvider->getProcAddr("vkCreateCommandPool"));
|
||||
vkDestroyCommandPool = reinterpret_cast<PFN_vkDestroyCommandPool>(
|
||||
pFunctionProvider->getProcAddr("vkDestroyCommandPool"));
|
||||
vkCreateFence = reinterpret_cast<PFN_vkCreateFence>(
|
||||
pFunctionProvider->getProcAddr("vkCreateFence"));
|
||||
vkDestroyFence = reinterpret_cast<PFN_vkDestroyFence>(
|
||||
pFunctionProvider->getProcAddr("vkDestroyFence"));
|
||||
vkWaitForFences = reinterpret_cast<PFN_vkWaitForFences>(
|
||||
pFunctionProvider->getProcAddr("vkWaitForFences"));
|
||||
vkGetFenceStatus = reinterpret_cast<PFN_vkGetFenceStatus>(
|
||||
pFunctionProvider->getProcAddr("vkGetFenceStatus"));
|
||||
vkResetFences = reinterpret_cast<PFN_vkResetFences>(
|
||||
pFunctionProvider->getProcAddr("vkResetFences"));
|
||||
vkCreateSemaphore = reinterpret_cast<PFN_vkCreateSemaphore>(
|
||||
pFunctionProvider->getProcAddr("vkCreateSemaphore"));
|
||||
vkDestroySemaphore = reinterpret_cast<PFN_vkDestroySemaphore>(
|
||||
pFunctionProvider->getProcAddr("vkDestroySemaphore"));
|
||||
vkCreateEvent = reinterpret_cast<PFN_vkCreateEvent>(
|
||||
pFunctionProvider->getProcAddr("vkCreateEvent"));
|
||||
vkDestroyEvent = reinterpret_cast<PFN_vkDestroyEvent>(
|
||||
pFunctionProvider->getProcAddr("vkDestroyEvent"));
|
||||
vkCmdSetEvent = reinterpret_cast<PFN_vkCmdSetEvent>(
|
||||
pFunctionProvider->getProcAddr("vkCmdSetEvent"));
|
||||
vkAllocateCommandBuffers =
|
||||
reinterpret_cast<PFN_vkAllocateCommandBuffers>(
|
||||
pFunctionProvider->getProcAddr("vkAllocateCommandBuffers"));
|
||||
vkFreeCommandBuffers = reinterpret_cast<PFN_vkFreeCommandBuffers>(
|
||||
pFunctionProvider->getProcAddr("vkFreeCommandBuffers"));
|
||||
vkBeginCommandBuffer = reinterpret_cast<PFN_vkBeginCommandBuffer>(
|
||||
pFunctionProvider->getProcAddr("vkBeginCommandBuffer"));
|
||||
vkEndCommandBuffer = reinterpret_cast<PFN_vkEndCommandBuffer>(
|
||||
pFunctionProvider->getProcAddr("vkEndCommandBuffer"));
|
||||
vkQueueSubmit = reinterpret_cast<PFN_vkQueueSubmit>(
|
||||
pFunctionProvider->getProcAddr("vkQueueSubmit"));
|
||||
}
|
||||
}
|
||||
|
||||
SwappyVkBase::SwappyVkBase(JNIEnv* env, jobject jactivity,
|
||||
VkPhysicalDevice physicalDevice, VkDevice device,
|
||||
const SwappyVkFunctionProvider* pFunctionProvider)
|
||||
: mCommonBase(env, jactivity),
|
||||
mPhysicalDevice(physicalDevice),
|
||||
mDevice(device),
|
||||
mpFunctionProvider(pFunctionProvider),
|
||||
mInitialized(false),
|
||||
mEnabled(false) {
|
||||
if (!mCommonBase.isValid()) {
|
||||
ALOGE("SwappyCommon could not initialize correctly.");
|
||||
return;
|
||||
}
|
||||
|
||||
mpfnGetDeviceProcAddr = reinterpret_cast<PFN_vkGetDeviceProcAddr>(
|
||||
mpFunctionProvider->getProcAddr("vkGetDeviceProcAddr"));
|
||||
mpfnQueuePresentKHR = reinterpret_cast<PFN_vkQueuePresentKHR>(
|
||||
mpfnGetDeviceProcAddr(mDevice, "vkQueuePresentKHR"));
|
||||
|
||||
initGoogExtension();
|
||||
|
||||
mEnabled =
|
||||
!gamesdk::GetSystemPropAsBool(SWAPPY_SYSTEM_PROP_KEY_DISABLE, false);
|
||||
}
|
||||
|
||||
void SwappyVkBase::initGoogExtension() {
|
||||
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
|
||||
mpfnGetRefreshCycleDurationGOOGLE =
|
||||
reinterpret_cast<PFN_vkGetRefreshCycleDurationGOOGLE>(
|
||||
mpfnGetDeviceProcAddr(mDevice, "vkGetRefreshCycleDurationGOOGLE"));
|
||||
mpfnGetPastPresentationTimingGOOGLE =
|
||||
reinterpret_cast<PFN_vkGetPastPresentationTimingGOOGLE>(
|
||||
mpfnGetDeviceProcAddr(mDevice,
|
||||
"vkGetPastPresentationTimingGOOGLE"));
|
||||
#endif
|
||||
}
|
||||
|
||||
SwappyVkBase::~SwappyVkBase() { destroyVkSyncObjects(); }
|
||||
|
||||
void SwappyVkBase::doSetWindow(ANativeWindow* window) {
|
||||
mCommonBase.setANativeWindow(window);
|
||||
}
|
||||
|
||||
void SwappyVkBase::doSetSwapInterval(VkSwapchainKHR swapchain,
|
||||
uint64_t swapNs) {
|
||||
Settings::getInstance()->setSwapDuration(swapNs);
|
||||
}
|
||||
|
||||
VkResult SwappyVkBase::initializeVkSyncObjects(VkQueue queue,
|
||||
uint32_t queueFamilyIndex) {
|
||||
if (mCommandPool.find(queue) != mCommandPool.end()) {
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VkSync sync;
|
||||
|
||||
const VkCommandPoolCreateInfo cmd_pool_info = {
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
|
||||
.pNext = NULL,
|
||||
.flags = 0,
|
||||
.queueFamilyIndex = queueFamilyIndex,
|
||||
};
|
||||
|
||||
VkResult res = vkCreateCommandPool(mDevice, &cmd_pool_info, NULL,
|
||||
&mCommandPool[queue]);
|
||||
if (res) {
|
||||
ALOGE("vkCreateCommandPool failed %d", res);
|
||||
return res;
|
||||
}
|
||||
const VkCommandBufferAllocateInfo present_cmd_info = {
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
|
||||
.pNext = NULL,
|
||||
.commandPool = mCommandPool[queue],
|
||||
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
|
||||
.commandBufferCount = 1,
|
||||
};
|
||||
|
||||
for (int i = 0; i < MAX_PENDING_FENCES; i++) {
|
||||
VkFenceCreateInfo fence_ci = {
|
||||
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
|
||||
.pNext = NULL,
|
||||
.flags = VK_FENCE_CREATE_SIGNALED_BIT};
|
||||
res = vkCreateFence(mDevice, &fence_ci, NULL, &sync.fence);
|
||||
if (res) {
|
||||
ALOGE("failed to create fence: %d", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
VkSemaphoreCreateInfo semaphore_ci = {
|
||||
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
|
||||
.pNext = NULL,
|
||||
.flags = 0};
|
||||
res = vkCreateSemaphore(mDevice, &semaphore_ci, NULL, &sync.semaphore);
|
||||
if (res) {
|
||||
ALOGE("failed to create semaphore: %d", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
res =
|
||||
vkAllocateCommandBuffers(mDevice, &present_cmd_info, &sync.command);
|
||||
if (res) {
|
||||
ALOGE("vkAllocateCommandBuffers failed %d", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
const VkCommandBufferBeginInfo cmd_buf_info = {
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.pNext = NULL,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT,
|
||||
.pInheritanceInfo = NULL,
|
||||
};
|
||||
res = vkBeginCommandBuffer(sync.command, &cmd_buf_info);
|
||||
if (res) {
|
||||
ALOGE("vkAllocateCommandBuffers failed %d", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
VkEventCreateInfo event_info = {
|
||||
.sType = VK_STRUCTURE_TYPE_EVENT_CREATE_INFO,
|
||||
.pNext = NULL,
|
||||
.flags = 0,
|
||||
};
|
||||
res = vkCreateEvent(mDevice, &event_info, NULL, &sync.event);
|
||||
if (res) {
|
||||
ALOGE("vkCreateEvent failed %d", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
vkCmdSetEvent(sync.command, sync.event,
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
|
||||
|
||||
res = vkEndCommandBuffer(sync.command);
|
||||
if (res) {
|
||||
ALOGE("vkCreateEvent failed %d", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
mFreeSyncPool[queue].push_back(sync);
|
||||
}
|
||||
|
||||
// Create a thread that will wait for the fences
|
||||
auto emplaceResult =
|
||||
mThreads.emplace(queue, std::make_unique<ThreadContext>(queue));
|
||||
auto& threadContext = emplaceResult.first->second;
|
||||
|
||||
// Start the thread
|
||||
std::lock_guard<std::mutex> lock(threadContext->lock);
|
||||
threadContext->thread =
|
||||
Thread([&]() { waitForFenceThreadMain(*threadContext); });
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
void SwappyVkBase::destroyVkSyncObjects() {
|
||||
// Stop all waiters threads
|
||||
for (auto it = mThreads.begin(); it != mThreads.end(); it++) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(it->second->lock);
|
||||
it->second->running = false;
|
||||
it->second->condition.notify_one();
|
||||
}
|
||||
it->second->thread.join();
|
||||
}
|
||||
|
||||
// Wait for all unsignaled fences to get signlaed
|
||||
for (auto it = mWaitingSyncs.begin(); it != mWaitingSyncs.end(); it++) {
|
||||
auto queue = it->first;
|
||||
auto syncList = it->second;
|
||||
while (syncList.size() > 0) {
|
||||
VkSync sync = syncList.front();
|
||||
syncList.pop_front();
|
||||
vkWaitForFences(mDevice, 1, &sync.fence, VK_TRUE, UINT64_MAX);
|
||||
mSignaledSyncs[queue].push_back(sync);
|
||||
}
|
||||
}
|
||||
|
||||
// Move all signaled fences to the free pool
|
||||
for (auto it = mSignaledSyncs.begin(); it != mSignaledSyncs.end(); it++) {
|
||||
auto queue = it->first;
|
||||
reclaimSignaledFences(queue);
|
||||
}
|
||||
|
||||
// Free all sync objects
|
||||
for (auto it = mFreeSyncPool.begin(); it != mFreeSyncPool.end(); it++) {
|
||||
auto syncList = it->second;
|
||||
while (syncList.size() > 0) {
|
||||
VkSync sync = syncList.front();
|
||||
syncList.pop_front();
|
||||
vkFreeCommandBuffers(mDevice, mCommandPool[it->first], 1,
|
||||
&sync.command);
|
||||
vkDestroyEvent(mDevice, sync.event, NULL);
|
||||
vkDestroySemaphore(mDevice, sync.semaphore, NULL);
|
||||
vkResetFences(mDevice, 1, &sync.fence);
|
||||
vkDestroyFence(mDevice, sync.fence, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// Free destroy the command pools
|
||||
for (auto it = mCommandPool.begin(); it != mCommandPool.end(); it++) {
|
||||
auto commandPool = it->second;
|
||||
vkDestroyCommandPool(mDevice, commandPool, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void SwappyVkBase::reclaimSignaledFences(VkQueue queue) {
|
||||
std::lock_guard<std::mutex> lock(mThreads[queue]->lock);
|
||||
while (!mSignaledSyncs[queue].empty()) {
|
||||
VkSync sync = mSignaledSyncs[queue].front();
|
||||
mSignaledSyncs[queue].pop_front();
|
||||
mFreeSyncPool[queue].push_back(sync);
|
||||
}
|
||||
}
|
||||
|
||||
bool SwappyVkBase::lastFrameIsCompleted(VkQueue queue) {
|
||||
auto pipelineMode = mCommonBase.getCurrentPipelineMode();
|
||||
std::lock_guard<std::mutex> lock(mThreads[queue]->lock);
|
||||
if (pipelineMode == SwappyCommon::PipelineMode::On) {
|
||||
// We are in pipeline mode so we need to check the fence of frame N-1
|
||||
return mWaitingSyncs[queue].size() < 2;
|
||||
}
|
||||
|
||||
// We are not in pipeline mode so we need to check the fence the current
|
||||
// frame. i.e. there are not unsignaled frames
|
||||
return mWaitingSyncs[queue].empty();
|
||||
}
|
||||
|
||||
VkResult SwappyVkBase::injectFence(VkQueue queue,
|
||||
const VkPresentInfoKHR* pPresentInfo,
|
||||
VkSemaphore* pSemaphore) {
|
||||
reclaimSignaledFences(queue);
|
||||
|
||||
// If we cross the swap interval threshold, we don't pace at all.
|
||||
// In this case we might not have a free fence, so just don't use the fence.
|
||||
if (mFreeSyncPool[queue].empty() ||
|
||||
vkGetFenceStatus(mDevice, mFreeSyncPool[queue].front().fence) !=
|
||||
VK_SUCCESS) {
|
||||
*pSemaphore = VK_NULL_HANDLE;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VkSync sync = mFreeSyncPool[queue].front();
|
||||
mFreeSyncPool[queue].pop_front();
|
||||
|
||||
vkResetFences(mDevice, 1, &sync.fence);
|
||||
|
||||
VkPipelineStageFlags pipe_stage_flags;
|
||||
VkSubmitInfo submit_info;
|
||||
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||
submit_info.pNext = NULL;
|
||||
submit_info.pWaitDstStageMask = &pipe_stage_flags;
|
||||
pipe_stage_flags = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
submit_info.waitSemaphoreCount = pPresentInfo->waitSemaphoreCount;
|
||||
submit_info.pWaitSemaphores = pPresentInfo->pWaitSemaphores;
|
||||
submit_info.commandBufferCount = 1;
|
||||
submit_info.pCommandBuffers = &sync.command;
|
||||
submit_info.signalSemaphoreCount = 1;
|
||||
submit_info.pSignalSemaphores = &sync.semaphore;
|
||||
VkResult res = vkQueueSubmit(queue, 1, &submit_info, sync.fence);
|
||||
*pSemaphore = sync.semaphore;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mThreads[queue]->lock);
|
||||
mWaitingSyncs[queue].push_back(sync);
|
||||
mThreads[queue]->hasPendingWork = true;
|
||||
mThreads[queue]->condition.notify_all();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void SwappyVkBase::setAutoSwapInterval(bool enabled) {
|
||||
mCommonBase.setAutoSwapInterval(enabled);
|
||||
}
|
||||
|
||||
void SwappyVkBase::setMaxAutoSwapDuration(std::chrono::nanoseconds swapMaxNS) {
|
||||
mCommonBase.setMaxAutoSwapDuration(swapMaxNS);
|
||||
}
|
||||
|
||||
void SwappyVkBase::setAutoPipelineMode(bool enabled) {
|
||||
mCommonBase.setAutoPipelineMode(enabled);
|
||||
}
|
||||
|
||||
void SwappyVkBase::waitForFenceThreadMain(ThreadContext& thread) {
|
||||
while (true) {
|
||||
bool waitingSyncsEmpty;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thread.lock);
|
||||
// Wait for new fence object
|
||||
thread.condition.wait(thread.lock, [&]() REQUIRES(thread.lock) {
|
||||
return thread.hasPendingWork || !thread.running;
|
||||
});
|
||||
|
||||
thread.hasPendingWork = false;
|
||||
|
||||
if (!thread.running) {
|
||||
break;
|
||||
}
|
||||
|
||||
waitingSyncsEmpty = mWaitingSyncs[thread.queue].empty();
|
||||
}
|
||||
|
||||
while (!waitingSyncsEmpty) {
|
||||
VkSync sync;
|
||||
{ // Get the sync object with a lock
|
||||
std::lock_guard<std::mutex> lock(thread.lock);
|
||||
sync = mWaitingSyncs[thread.queue].front();
|
||||
}
|
||||
|
||||
gamesdk::ScopedTrace tracer("Swappy: GPU frame time");
|
||||
const auto startTime = std::chrono::steady_clock::now();
|
||||
VkResult result =
|
||||
vkWaitForFences(mDevice, 1, &sync.fence, VK_TRUE,
|
||||
mCommonBase.getFenceTimeout().count());
|
||||
if (result) {
|
||||
ALOGE("Failed to wait for fence %d", result);
|
||||
}
|
||||
mLastFenceTime = std::chrono::steady_clock::now() - startTime;
|
||||
|
||||
// Move the sync object to the signaled list
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thread.lock);
|
||||
mWaitingSyncs[thread.queue].pop_front();
|
||||
|
||||
mSignaledSyncs[thread.queue].push_back(sync);
|
||||
waitingSyncsEmpty = mWaitingSyncs[thread.queue].empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds SwappyVkBase::getLastFenceTime(VkQueue queue) {
|
||||
return mLastFenceTime;
|
||||
}
|
||||
|
||||
void SwappyVkBase::setFenceTimeout(std::chrono::nanoseconds duration) {
|
||||
mCommonBase.setFenceTimeout(duration);
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds SwappyVkBase::getFenceTimeout() const {
|
||||
return mCommonBase.getFenceTimeout();
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds SwappyVkBase::getSwapInterval() {
|
||||
return mCommonBase.getSwapDuration();
|
||||
}
|
||||
|
||||
void SwappyVkBase::addTracer(const SwappyTracer* tracer) {
|
||||
if (tracer != nullptr) mCommonBase.addTracerCallbacks(*tracer);
|
||||
}
|
||||
|
||||
void SwappyVkBase::removeTracer(const SwappyTracer* tracer) {
|
||||
if (tracer != nullptr) mCommonBase.removeTracerCallbacks(*tracer);
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
*
|
||||
* Per-Device abstract base class.
|
||||
*
|
||||
***************************************************************************************************/
|
||||
|
||||
/**
|
||||
* Abstract base class that calls the Vulkan API.
|
||||
*
|
||||
* It is expected that one concrete class will be instantiated per VkDevice, and
|
||||
* that all VkSwapchainKHR's for a given VkDevice will share the same instance.
|
||||
*
|
||||
* Base class members are used by the derived classes to unify the behavior
|
||||
* across implementations:
|
||||
* @mThread - Thread used for getting Choreographer events.
|
||||
* @mTreadRunning - Used to signal the tread to exit
|
||||
* @mNextPresentID - unique ID for frame presentation.
|
||||
* @mNextDesiredPresentTime - Holds the time in nanoseconds for the next frame
|
||||
* to be presented.
|
||||
* @mNextPresentIDToCheck - Used to determine whether presentation time needs
|
||||
* to be adjusted.
|
||||
* @mFrameID - Keeps track of how many Choreographer callbacks received.
|
||||
* @mLastframeTimeNanos - Holds the last frame time reported by Choreographer.
|
||||
* @mSumRefreshTime - Used together with @mSamples to calculate refresh rate
|
||||
* based on Choreographer.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <inttypes.h>
|
||||
#include <pthread.h>
|
||||
#include <swappy/swappyVk.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
|
||||
#include "ChoreographerShim.h"
|
||||
#include "Settings.h"
|
||||
#include "SwappyCommon.h"
|
||||
#include "Trace.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
#define ALOGE(...) \
|
||||
__android_log_print(ANDROID_LOG_ERROR, "SwappyVk", __VA_ARGS__)
|
||||
#define ALOGW(...) \
|
||||
__android_log_print(ANDROID_LOG_WARN, "SwappyVk", __VA_ARGS__)
|
||||
#define ALOGI(...) \
|
||||
__android_log_print(ANDROID_LOG_INFO, "SwappyVk", __VA_ARGS__)
|
||||
#define ALOGD(...) \
|
||||
__android_log_print(ANDROID_LOG_DEBUG, "SwappyVk", __VA_ARGS__)
|
||||
#define ALOGV(...) \
|
||||
__android_log_print(ANDROID_LOG_VERBOSE, "SwappyVk", __VA_ARGS__)
|
||||
|
||||
constexpr uint32_t kThousand = 1000;
|
||||
constexpr uint32_t kMillion = 1000000;
|
||||
constexpr uint32_t kBillion = 1000000000;
|
||||
constexpr uint32_t k16_6msec = 16666666;
|
||||
|
||||
constexpr uint32_t kTooCloseToVsyncBoundary = 3000000;
|
||||
constexpr uint32_t kTooFarAwayFromVsyncBoundary = 7000000;
|
||||
constexpr uint32_t kNudgeWithinVsyncBoundaries = 2000000;
|
||||
|
||||
// AChoreographer is supported from API 24. To allow compilation for minSDK < 24
|
||||
// and still use AChoreographer for SDK >= 24 we need runtime support to call
|
||||
// AChoreographer APIs.
|
||||
|
||||
using PFN_AChoreographer_getInstance = AChoreographer* (*)();
|
||||
|
||||
using PFN_AChoreographer_postFrameCallback =
|
||||
void (*)(AChoreographer* choreographer,
|
||||
AChoreographer_frameCallback callback, void* data);
|
||||
|
||||
using PFN_AChoreographer_postFrameCallbackDelayed = void (*)(
|
||||
AChoreographer* choreographer, AChoreographer_frameCallback callback,
|
||||
void* data, long delayMillis);
|
||||
|
||||
extern PFN_vkCreateCommandPool vkCreateCommandPool;
|
||||
extern PFN_vkDestroyCommandPool vkDestroyCommandPool;
|
||||
extern PFN_vkCreateFence vkCreateFence;
|
||||
extern PFN_vkDestroyFence vkDestroyFence;
|
||||
extern PFN_vkWaitForFences vkWaitForFences;
|
||||
extern PFN_vkResetFences vkResetFences;
|
||||
extern PFN_vkCreateSemaphore vkCreateSemaphore;
|
||||
extern PFN_vkDestroySemaphore vkDestroySemaphore;
|
||||
extern PFN_vkCreateEvent vkCreateEvent;
|
||||
extern PFN_vkDestroyEvent vkDestroyEvent;
|
||||
extern PFN_vkCmdSetEvent vkCmdSetEvent;
|
||||
extern PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers;
|
||||
extern PFN_vkFreeCommandBuffers vkFreeCommandBuffers;
|
||||
extern PFN_vkBeginCommandBuffer vkBeginCommandBuffer;
|
||||
extern PFN_vkEndCommandBuffer vkEndCommandBuffer;
|
||||
extern PFN_vkQueueSubmit vkQueueSubmit;
|
||||
|
||||
void LoadVulkanFunctions(const SwappyVkFunctionProvider* pFunctionProvider);
|
||||
|
||||
class SwappyVkBase {
|
||||
public:
|
||||
SwappyVkBase(JNIEnv* env, jobject jactivity,
|
||||
VkPhysicalDevice physicalDevice, VkDevice device,
|
||||
const SwappyVkFunctionProvider* pFunctionProvider);
|
||||
|
||||
virtual ~SwappyVkBase();
|
||||
|
||||
virtual bool doGetRefreshCycleDuration(VkSwapchainKHR swapchain,
|
||||
uint64_t* pRefreshDuration) = 0;
|
||||
|
||||
virtual VkResult doQueuePresent(VkQueue queue, uint32_t queueFamilyIndex,
|
||||
const VkPresentInfoKHR* pPresentInfo) = 0;
|
||||
|
||||
void doSetWindow(ANativeWindow* window);
|
||||
void doSetSwapInterval(VkSwapchainKHR swapchain, uint64_t swapNs);
|
||||
|
||||
VkResult injectFence(VkQueue queue, const VkPresentInfoKHR* pPresentInfo,
|
||||
VkSemaphore* pSemaphore);
|
||||
|
||||
bool isEnabled() { return mEnabled; }
|
||||
|
||||
void setAutoSwapInterval(bool enabled);
|
||||
void setAutoPipelineMode(bool enabled);
|
||||
|
||||
void setMaxAutoSwapDuration(std::chrono::nanoseconds swapMaxNS);
|
||||
|
||||
void setFenceTimeout(std::chrono::nanoseconds duration);
|
||||
std::chrono::nanoseconds getFenceTimeout() const;
|
||||
std::chrono::nanoseconds getSwapInterval();
|
||||
|
||||
void addTracer(const SwappyTracer* tracer);
|
||||
void removeTracer(const SwappyTracer* tracer);
|
||||
|
||||
VkDevice getDevice() const { return mDevice; }
|
||||
|
||||
protected:
|
||||
struct VkSync {
|
||||
VkFence fence;
|
||||
VkSemaphore semaphore;
|
||||
VkCommandBuffer command;
|
||||
VkEvent event;
|
||||
};
|
||||
|
||||
struct ThreadContext {
|
||||
ThreadContext(VkQueue queue) : queue(queue) {}
|
||||
|
||||
Thread thread;
|
||||
bool running GUARDED_BY(lock) = true;
|
||||
bool hasPendingWork GUARDED_BY(lock);
|
||||
std::mutex lock;
|
||||
std::condition_variable_any condition;
|
||||
VkQueue queue GUARDED_BY(lock);
|
||||
};
|
||||
|
||||
SwappyCommon mCommonBase;
|
||||
VkPhysicalDevice mPhysicalDevice;
|
||||
VkDevice mDevice;
|
||||
const SwappyVkFunctionProvider* mpFunctionProvider;
|
||||
bool mInitialized;
|
||||
bool mEnabled;
|
||||
|
||||
uint32_t mNextPresentID = 0;
|
||||
uint32_t mNextPresentIDToCheck = 2;
|
||||
|
||||
PFN_vkGetDeviceProcAddr mpfnGetDeviceProcAddr = nullptr;
|
||||
PFN_vkQueuePresentKHR mpfnQueuePresentKHR = nullptr;
|
||||
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
|
||||
PFN_vkGetRefreshCycleDurationGOOGLE mpfnGetRefreshCycleDurationGOOGLE =
|
||||
nullptr;
|
||||
PFN_vkGetPastPresentationTimingGOOGLE mpfnGetPastPresentationTimingGOOGLE =
|
||||
nullptr;
|
||||
#endif
|
||||
// Holds VKSync objects ready to be used
|
||||
std::map<VkQueue, std::list<VkSync>> mFreeSyncPool;
|
||||
|
||||
// Holds VKSync objects queued and but signaled yet
|
||||
std::map<VkQueue, std::list<VkSync>> mWaitingSyncs;
|
||||
|
||||
// Holds VKSync objects that were signaled
|
||||
std::map<VkQueue, std::list<VkSync>> mSignaledSyncs;
|
||||
|
||||
std::map<VkQueue, VkCommandPool> mCommandPool;
|
||||
std::map<VkQueue, std::unique_ptr<ThreadContext>> mThreads;
|
||||
|
||||
static constexpr int MAX_PENDING_FENCES = 2;
|
||||
|
||||
std::atomic<std::chrono::nanoseconds> mLastFenceTime = {};
|
||||
|
||||
void initGoogExtension();
|
||||
VkResult initializeVkSyncObjects(VkQueue queue, uint32_t queueFamilyIndex);
|
||||
void destroyVkSyncObjects();
|
||||
void reclaimSignaledFences(VkQueue queue);
|
||||
bool lastFrameIsCompleted(VkQueue queue);
|
||||
std::chrono::nanoseconds getLastFenceTime(VkQueue queue);
|
||||
void waitForFenceThreadMain(ThreadContext& thread);
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "SwappyVkFallback.h"
|
||||
|
||||
#define LOG_TAG "SwappyVkFallback"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
SwappyVkFallback::SwappyVkFallback(JNIEnv* env, jobject jactivity,
|
||||
VkPhysicalDevice physicalDevice,
|
||||
VkDevice device,
|
||||
const SwappyVkFunctionProvider* provider)
|
||||
: SwappyVkBase(env, jactivity, physicalDevice, device, provider) {}
|
||||
|
||||
bool SwappyVkFallback::doGetRefreshCycleDuration(VkSwapchainKHR swapchain,
|
||||
uint64_t* pRefreshDuration) {
|
||||
if (!isEnabled()) {
|
||||
ALOGE("Swappy is disabled.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since we don't have presentation timing, we cannot achieve pipelining.
|
||||
mCommonBase.setAutoPipelineMode(false);
|
||||
|
||||
*pRefreshDuration = mCommonBase.getRefreshPeriod().count();
|
||||
|
||||
double refreshRate = 1000000000.0 / *pRefreshDuration;
|
||||
ALOGI("Returning refresh duration of %" PRIu64 " nsec (approx %f Hz)",
|
||||
*pRefreshDuration, refreshRate);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
VkResult SwappyVkFallback::doQueuePresent(
|
||||
VkQueue queue, uint32_t queueFamilyIndex,
|
||||
const VkPresentInfoKHR* pPresentInfo) {
|
||||
if (!isEnabled()) {
|
||||
ALOGE("Swappy is disabled.");
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
|
||||
VkResult result = initializeVkSyncObjects(queue, queueFamilyIndex);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const SwappyCommon::SwapHandlers handlers = {
|
||||
.lastFrameIsComplete =
|
||||
std::bind(&SwappyVkFallback::lastFrameIsCompleted, this, queue),
|
||||
.getPrevFrameGpuTime =
|
||||
std::bind(&SwappyVkFallback::getLastFenceTime, this, queue),
|
||||
};
|
||||
|
||||
// Inject the fence first and wait for it in onPreSwap() as we don't want to
|
||||
// submit a frame before rendering is completed.
|
||||
VkSemaphore semaphore;
|
||||
result = injectFence(queue, pPresentInfo, &semaphore);
|
||||
if (result) {
|
||||
ALOGE("Failed to vkQueueSubmit %d", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t waitSemaphoreCount;
|
||||
const VkSemaphore* pWaitSemaphores;
|
||||
if (semaphore != VK_NULL_HANDLE) {
|
||||
waitSemaphoreCount = 1;
|
||||
pWaitSemaphores = &semaphore;
|
||||
} else {
|
||||
waitSemaphoreCount = pPresentInfo->waitSemaphoreCount;
|
||||
pWaitSemaphores = pPresentInfo->pWaitSemaphores;
|
||||
}
|
||||
|
||||
mCommonBase.onPreSwap(handlers);
|
||||
|
||||
VkPresentInfoKHR replacementPresentInfo = {
|
||||
pPresentInfo->sType, nullptr,
|
||||
waitSemaphoreCount, pWaitSemaphores,
|
||||
pPresentInfo->swapchainCount, pPresentInfo->pSwapchains,
|
||||
pPresentInfo->pImageIndices, pPresentInfo->pResults};
|
||||
|
||||
result = mpfnQueuePresentKHR(queue, &replacementPresentInfo);
|
||||
|
||||
mCommonBase.onPostSwap(handlers);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
*
|
||||
* Per-Device concrete/derived class for the "Android fallback" path (uses
|
||||
* Choreographer to try to get presents to occur at the desired time).
|
||||
*
|
||||
***************************************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "SwappyVkBase.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
class SwappyVkFallback : public SwappyVkBase {
|
||||
public:
|
||||
SwappyVkFallback(JNIEnv* env, jobject jactivity,
|
||||
VkPhysicalDevice physicalDevice, VkDevice device,
|
||||
const SwappyVkFunctionProvider* provider);
|
||||
|
||||
virtual bool doGetRefreshCycleDuration(VkSwapchainKHR swapchain,
|
||||
uint64_t* pRefreshDuration) override;
|
||||
|
||||
virtual VkResult doQueuePresent(
|
||||
VkQueue queue, uint32_t queueFamilyIndex,
|
||||
const VkPresentInfoKHR* pPresentInfo) override;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION >= 15
|
||||
|
||||
#include "SwappyVkGoogleDisplayTiming.h"
|
||||
|
||||
#define LOG_TAG "SwappyVkGoogleDisplayTiming"
|
||||
|
||||
using std::chrono::nanoseconds;
|
||||
|
||||
namespace swappy {
|
||||
|
||||
SwappyVkGoogleDisplayTiming::SwappyVkGoogleDisplayTiming(
|
||||
JNIEnv* env, jobject jactivity, VkPhysicalDevice physicalDevice,
|
||||
VkDevice device, const SwappyVkFunctionProvider* provider)
|
||||
: SwappyVkBase(env, jactivity, physicalDevice, device, provider) {}
|
||||
|
||||
bool SwappyVkGoogleDisplayTiming::doGetRefreshCycleDuration(
|
||||
VkSwapchainKHR swapchain, uint64_t* pRefreshDuration) {
|
||||
if (!isEnabled()) {
|
||||
ALOGE("Swappy is disabled.");
|
||||
return false;
|
||||
}
|
||||
|
||||
VkRefreshCycleDurationGOOGLE refreshCycleDuration;
|
||||
VkResult res = mpfnGetRefreshCycleDurationGOOGLE(mDevice, swapchain,
|
||||
&refreshCycleDuration);
|
||||
if (res != VK_SUCCESS) {
|
||||
ALOGE("mpfnGetRefreshCycleDurationGOOGLE failed %d", res);
|
||||
return false;
|
||||
}
|
||||
|
||||
*pRefreshDuration = mCommonBase.getRefreshPeriod().count();
|
||||
|
||||
double refreshRate = 1000000000.0 / *pRefreshDuration;
|
||||
ALOGI("Returning refresh duration of %" PRIu64 " nsec (approx %f Hz)",
|
||||
*pRefreshDuration, refreshRate);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
VkResult SwappyVkGoogleDisplayTiming::doQueuePresent(
|
||||
VkQueue queue, uint32_t queueFamilyIndex,
|
||||
const VkPresentInfoKHR* pPresentInfo) {
|
||||
if (!isEnabled()) {
|
||||
ALOGE("Swappy is disabled.");
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
|
||||
VkResult res = initializeVkSyncObjects(queue, queueFamilyIndex);
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const SwappyCommon::SwapHandlers handlers = {
|
||||
.lastFrameIsComplete = std::bind(
|
||||
&SwappyVkGoogleDisplayTiming::lastFrameIsCompleted, this, queue),
|
||||
.getPrevFrameGpuTime = std::bind(
|
||||
&SwappyVkGoogleDisplayTiming::getLastFenceTime, this, queue),
|
||||
};
|
||||
|
||||
VkSemaphore semaphore;
|
||||
res = injectFence(queue, pPresentInfo, &semaphore);
|
||||
if (res) {
|
||||
ALOGE("Failed to vkQueueSubmit %d", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
uint32_t waitSemaphoreCount;
|
||||
const VkSemaphore* pWaitSemaphores;
|
||||
if (semaphore != VK_NULL_HANDLE) {
|
||||
waitSemaphoreCount = 1;
|
||||
pWaitSemaphores = &semaphore;
|
||||
} else {
|
||||
waitSemaphoreCount = pPresentInfo->waitSemaphoreCount;
|
||||
pWaitSemaphores = pPresentInfo->pWaitSemaphores;
|
||||
}
|
||||
|
||||
mCommonBase.onPreSwap(handlers);
|
||||
|
||||
VkPresentTimeGOOGLE pPresentTimes[pPresentInfo->swapchainCount];
|
||||
VkPresentInfoKHR replacementPresentInfo;
|
||||
VkPresentTimesInfoGOOGLE presentTimesInfo;
|
||||
if (mCommonBase.needToSetPresentationTime()) {
|
||||
// Setup the new structures to pass:
|
||||
for (uint32_t i = 0; i < pPresentInfo->swapchainCount; i++) {
|
||||
pPresentTimes[i].presentID = mNextPresentID;
|
||||
pPresentTimes[i].desiredPresentTime =
|
||||
mCommonBase.getPresentationTime().time_since_epoch().count();
|
||||
}
|
||||
|
||||
presentTimesInfo = {VK_STRUCTURE_TYPE_PRESENT_TIMES_INFO_GOOGLE,
|
||||
pPresentInfo->pNext, pPresentInfo->swapchainCount,
|
||||
pPresentTimes};
|
||||
|
||||
replacementPresentInfo = {
|
||||
pPresentInfo->sType, &presentTimesInfo,
|
||||
waitSemaphoreCount, pWaitSemaphores,
|
||||
pPresentInfo->swapchainCount, pPresentInfo->pSwapchains,
|
||||
pPresentInfo->pImageIndices, pPresentInfo->pResults};
|
||||
|
||||
} else {
|
||||
replacementPresentInfo = {
|
||||
pPresentInfo->sType, nullptr,
|
||||
waitSemaphoreCount, pWaitSemaphores,
|
||||
pPresentInfo->swapchainCount, pPresentInfo->pSwapchains,
|
||||
pPresentInfo->pImageIndices, pPresentInfo->pResults};
|
||||
}
|
||||
mNextPresentID++;
|
||||
|
||||
res = mpfnQueuePresentKHR(queue, &replacementPresentInfo);
|
||||
mCommonBase.onPostSwap(handlers);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace swappy
|
||||
|
||||
#endif // #if (not defined ANDROID_NDK_VERSION) || ANDROID_NDK_VERSION>=15
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "SwappyVkBase.h"
|
||||
|
||||
namespace swappy {
|
||||
|
||||
/***************************************************************************************************
|
||||
*
|
||||
* Per-Device concrete/derived class for using VK_GOOGLE_display_timing.
|
||||
*
|
||||
* This class uses the VK_GOOGLE_display_timing in order to present frames at a
|
||||
*muliple (the "swap interval") of a fixed refresh-cycle duration (i.e. the time
|
||||
*between successive vsync's).
|
||||
*
|
||||
* In order to reduce complexity, some simplifying assumptions are made:
|
||||
*
|
||||
* - We assume a fixed refresh-rate (FRR) display that's between 60 Hz and 120
|
||||
*Hz.
|
||||
*
|
||||
* - While Vulkan allows applications to create and use multiple
|
||||
*VkSwapchainKHR's per VkDevice, and to re-create VkSwapchainKHR's, we assume
|
||||
*that the application uses a single VkSwapchainKHR, and never re-creates it.
|
||||
*
|
||||
* - The values reported back by the VK_GOOGLE_display_timing extension (which
|
||||
*comes from lower-level Android interfaces) are not precise, and that values
|
||||
*can drift over time. For example, the refresh-cycle duration for a 60 Hz
|
||||
*display should be 16,666,666 nsec; but the value reported back by the
|
||||
*extension won't be precisely this. Also, the differences betweeen the times
|
||||
*of two successive frames won't be an exact multiple of 16,666,666 nsec. This
|
||||
*can make it difficult to precisely predict when a future vsync will be (it can
|
||||
*appear to drift overtime). Therefore, we try to give a desiredPresentTime for
|
||||
*each image that is between 3 and 7 msec before vsync. We look at the
|
||||
*actualPresentTime for previously-presented images, and nudge the future
|
||||
*desiredPresentTime back within those 3-7 msec boundaries.
|
||||
*
|
||||
* - There can be a few frames of latency between when an image is presented and
|
||||
*when the actualPresentTime is available for that image. Therefore, we
|
||||
*initially just pick times based upon CLOCK_MONOTONIC (which is the time domain
|
||||
*for VK_GOOGLE_display_timing). After we get past-present times, we nudge the
|
||||
*desiredPresentTime, we wait for a few presents before looking again to see
|
||||
*whether we need to nudge again.
|
||||
*
|
||||
* - If, for some reason, an application can't keep up with its chosen swap
|
||||
*interval (e.g. it's designed for 30FPS on a premium device and is now running
|
||||
*on a slow device; or it's running on a 120Hz display), this algorithm may not
|
||||
*be able to make up for this (i.e. smooth rendering at a targeted frame rate
|
||||
*may not be possible with an application that can't render fast enough).
|
||||
*
|
||||
***************************************************************************************************/
|
||||
class SwappyVkGoogleDisplayTiming : public SwappyVkBase {
|
||||
public:
|
||||
SwappyVkGoogleDisplayTiming(JNIEnv* env, jobject jactivity,
|
||||
VkPhysicalDevice physicalDevice,
|
||||
VkDevice device,
|
||||
const SwappyVkFunctionProvider* provider);
|
||||
|
||||
virtual bool doGetRefreshCycleDuration(VkSwapchainKHR swapchain,
|
||||
uint64_t* pRefreshDuration) override;
|
||||
|
||||
virtual VkResult doQueuePresent(
|
||||
VkQueue queue, uint32_t queueFamilyIndex,
|
||||
const VkPresentInfoKHR* pPresentInfo) override;
|
||||
};
|
||||
|
||||
} // namespace swappy
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// API entry points
|
||||
|
||||
#include "SwappyVk.h"
|
||||
#include "swappy/swappyVk.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Internal function to track Swappy version bundled in a binary.
|
||||
void SWAPPY_VERSION_SYMBOL();
|
||||
|
||||
void SwappyVk_determineDeviceExtensions(
|
||||
VkPhysicalDevice physicalDevice, uint32_t availableExtensionCount,
|
||||
VkExtensionProperties* pAvailableExtensions,
|
||||
uint32_t* pRequiredExtensionCount, char** pRequiredExtensions) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.swappyVkDetermineDeviceExtensions(
|
||||
physicalDevice, availableExtensionCount, pAvailableExtensions,
|
||||
pRequiredExtensionCount, pRequiredExtensions);
|
||||
}
|
||||
|
||||
void SwappyVk_setQueueFamilyIndex(VkDevice device, VkQueue queue,
|
||||
uint32_t queueFamilyIndex) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.SetQueueFamilyIndex(device, queue, queueFamilyIndex);
|
||||
}
|
||||
|
||||
bool SwappyVk_initAndGetRefreshCycleDuration(JNIEnv* env, jobject jactivity,
|
||||
VkPhysicalDevice physicalDevice,
|
||||
VkDevice device,
|
||||
VkSwapchainKHR swapchain,
|
||||
uint64_t* pRefreshDuration) {
|
||||
SWAPPY_VERSION_SYMBOL();
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
return swappy.GetRefreshCycleDuration(env, jactivity, physicalDevice,
|
||||
device, swapchain, pRefreshDuration);
|
||||
}
|
||||
|
||||
void SwappyVk_setWindow(VkDevice device, VkSwapchainKHR swapchain,
|
||||
ANativeWindow* window) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.SetWindow(device, swapchain, window);
|
||||
}
|
||||
|
||||
void SwappyVk_setSwapIntervalNS(VkDevice device, VkSwapchainKHR swapchain,
|
||||
uint64_t swap_ns) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.SetSwapDuration(device, swapchain, swap_ns);
|
||||
}
|
||||
|
||||
VkResult SwappyVk_queuePresent(VkQueue queue,
|
||||
const VkPresentInfoKHR* pPresentInfo) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
return swappy.QueuePresent(queue, pPresentInfo);
|
||||
}
|
||||
|
||||
void SwappyVk_destroySwapchain(VkDevice device, VkSwapchainKHR swapchain) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.DestroySwapchain(device, swapchain);
|
||||
}
|
||||
|
||||
void SwappyVk_destroyDevice(VkDevice device) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.DestroyDevice(device);
|
||||
}
|
||||
|
||||
void SwappyVk_setAutoSwapInterval(bool enabled) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.SetAutoSwapInterval(enabled);
|
||||
}
|
||||
|
||||
void SwappyVk_setAutoPipelineMode(bool enabled) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.SetAutoPipelineMode(enabled);
|
||||
}
|
||||
|
||||
void SwappyVk_setFenceTimeoutNS(uint64_t fence_timeout_ns) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.SetFenceTimeout(std::chrono::nanoseconds(fence_timeout_ns));
|
||||
}
|
||||
|
||||
void SwappyVk_setMaxAutoSwapIntervalNS(uint64_t max_swap_ns) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.SetMaxAutoSwapDuration(std::chrono::nanoseconds(max_swap_ns));
|
||||
}
|
||||
|
||||
uint64_t SwappyVk_getFenceTimeoutNS() {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
return swappy.GetFenceTimeout().count();
|
||||
}
|
||||
|
||||
void SwappyVk_injectTracer(const SwappyTracer* t) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.addTracer(t);
|
||||
}
|
||||
|
||||
void SwappyVk_uninjectTracer(const SwappyTracer* t) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.removeTracer(t);
|
||||
}
|
||||
|
||||
void SwappyVk_setFunctionProvider(
|
||||
const SwappyVkFunctionProvider* pSwappyVkFunctionProvider) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
swappy.SetFunctionProvider(pSwappyVkFunctionProvider);
|
||||
}
|
||||
|
||||
uint64_t SwappyVk_getSwapIntervalNS(VkSwapchainKHR swapchain) {
|
||||
TRACE_CALL();
|
||||
swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
|
||||
return swappy.GetSwapInterval(swapchain).count();
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
Loading…
Reference in New Issue