mirror of
https://github.com/flyinghead/flycast.git
synced 2026-11-07 09:16:10 -10:00
android: use gamesdk frame-pacing library
Use games-frame-pacing library (aka Swappy) for OpenGL and Vulkan. Implements proper frame pacing for 30 FPS games and/or 120+ Hz displays. Don't duplicate Vulkan frames.
This commit is contained in:
1 parent
08c63bd39b
commit
b34c4cde8d
16 files changed
+709
-18
No files matched your search
@@ -62,3 +62,6 @@
|
||||
[submodule "core/deps/freetype"]
|
||||
path = core/deps/freetype
|
||||
url = https://gitlab.freedesktop.org/freetype/freetype.git
|
||||
[submodule "core/deps/gamesdk"]
|
||||
path = core/deps/gamesdk
|
||||
url = https://github.com/flyinghead/gamesdk.git
|
||||
@@ -1253,6 +1253,15 @@ if(NOT LIBRETRO)
|
||||
add_subdirectory(shell/android-studio/flycast/src/main/jni/src)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE android log)
|
||||
|
||||
set(GAMESDK_THREAD_CHECKS 0)
|
||||
add_subdirectory(core/deps/gamesdk/games-frame-pacing EXCLUDE_FROM_ALL)
|
||||
target_compile_options(swappy_static PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-Wno-vla-cxx-extension>)
|
||||
target_compile_definitions(swappy_static PRIVATE ANDROIDGAMESDK_NO_BINARY_DEX_LINKAGE)
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE core/deps/gamesdk/games-frame-pacing/include)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE swappy_static)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE SWAPPY)
|
||||
|
||||
elseif(APPLE)
|
||||
string(TIMESTAMP YEAR "%Y")
|
||||
string(REGEX MATCH "^[^-]+" TAG_VERSION ${GIT_VERSION})
|
||||
|
||||
@@ -120,6 +120,7 @@ Option<bool> NativeDepthInterpolation("rend.NativeDepthInterpolation", false);
|
||||
Option<bool> EmulateFramebuffer("rend.EmulateFramebuffer", false);
|
||||
Option<bool> FixUpscaleBleedingEdge("rend.FixUpscaleBleedingEdge", true);
|
||||
Option<bool> CustomGpuDriver("rend.CustomGpuDriver", false);
|
||||
Option<bool> FramePacing("rend.FramePacing", true);
|
||||
#ifdef VIDEO_ROUTING
|
||||
Option<bool, false> VideoRouting("rend.VideoRouting", false);
|
||||
Option<bool, false> VideoRoutingScale("rend.VideoRoutingScale", false);
|
||||
|
||||
@@ -476,6 +476,7 @@ extern Option<bool> NativeDepthInterpolation;
|
||||
extern Option<bool> EmulateFramebuffer;
|
||||
extern Option<bool> FixUpscaleBleedingEdge;
|
||||
extern Option<bool> CustomGpuDriver;
|
||||
extern Option<bool> FramePacing;
|
||||
#ifdef VIDEO_ROUTING
|
||||
extern Option<bool, false> VideoRouting;
|
||||
extern Option<bool, false> VideoRoutingScale;
|
||||
|
||||
Submodule
+1
Submodule core/deps/gamesdk added at 4ac9952e6a.
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2026 flyinghead
|
||||
|
||||
This file is part of Flycast.
|
||||
|
||||
Flycast is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Flycast is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include "vulkan.h"
|
||||
|
||||
namespace swappyvk
|
||||
{
|
||||
#ifdef SWAPPY
|
||||
|
||||
void setQueueFamilyIndex(bool googleTimingSupported, vk::Device device, vk::Queue queue, u32 queueFamilyIndex);
|
||||
void init(vk::PhysicalDevice phyDevice, vk::Device device, vk::SwapchainKHR swapchain);
|
||||
void setSwapInterval(int i);
|
||||
vk::Result present(vk::Queue queue, const vk::PresentInfoKHR& presentInfo);
|
||||
void detachSwapchain();
|
||||
void detachDevice();
|
||||
|
||||
#else
|
||||
|
||||
static inline void setQueueFamilyIndex(bool googleTimingSupported, vk::Device device, vk::Queue queue, u32 queueFamilyIndex) {}
|
||||
static inline void init(vk::PhysicalDevice phyDevice, vk::Device device, vk::SwapchainKHR swapchain) {}
|
||||
static inline void setSwapInterval(int i) {}
|
||||
|
||||
static inline vk::Result present(vk::Queue queue, const vk::PresentInfoKHR& presentInfo) {
|
||||
return queue.presentKHR(presentInfo);
|
||||
}
|
||||
|
||||
static inline void detachSwapchain() {}
|
||||
static inline void detachDevice() {}
|
||||
|
||||
#endif
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "oslib/directory.h"
|
||||
#include "vulkan_driver.h"
|
||||
#include "rend/transform_matrix.h"
|
||||
#include "swappyvk.h"
|
||||
#if defined(__ANDROID__) && HOST_CPU == CPU_ARM64
|
||||
#include "adreno.h"
|
||||
#endif
|
||||
@@ -497,7 +498,13 @@ bool VulkanContext::InitDevice()
|
||||
// Enable VK_EXT_provoking_vertex if available
|
||||
provokingVertexSupported = tryAddDeviceExtension(VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME);
|
||||
}
|
||||
|
||||
bool googleTimingSupported = false;
|
||||
#if defined(SWAPPY)
|
||||
googleTimingSupported = tryAddDeviceExtension(vk::GOOGLEDisplayTimingExtensionName);
|
||||
if (googleTimingSupported)
|
||||
NOTICE_LOG(RENDERER, "VK_GOOGLE_display_timing supported");
|
||||
#endif
|
||||
|
||||
// Get device features
|
||||
|
||||
vk::PhysicalDeviceFeatures2 featuresChain{};
|
||||
@@ -553,6 +560,7 @@ bool VulkanContext::InitDevice()
|
||||
// Queues
|
||||
graphicsQueue = device->getQueue(graphicsQueueIndex, 0);
|
||||
presentQueue = device->getQueue(presentQueueIndex, 0);
|
||||
swappyvk::setQueueFamilyIndex(googleTimingSupported, device.get(), presentQueue, presentQueueIndex);
|
||||
|
||||
// Descriptor pool
|
||||
std::array<vk::DescriptorPoolSize, 11> pool_sizes =
|
||||
@@ -751,14 +759,22 @@ void VulkanContext::CreateSwapChain()
|
||||
INFO_LOG(RENDERER, "Using mailbox present mode");
|
||||
swapchainPresentMode = vk::PresentModeKHR::eMailbox;
|
||||
}
|
||||
#ifndef SWAPPY
|
||||
if (swapOnVSync && config::DupeFrames && settings.display.refreshRate > 60.f)
|
||||
swapInterval = settings.display.refreshRate / 60.f;
|
||||
else
|
||||
#endif
|
||||
swapInterval = 1;
|
||||
|
||||
vk::SurfaceTransformFlagBitsKHR preTransform = (surfaceCapabilities.supportedTransforms & vk::SurfaceTransformFlagBitsKHR::eIdentity) ? vk::SurfaceTransformFlagBitsKHR::eIdentity : surfaceCapabilities.currentTransform;
|
||||
|
||||
u32 imageCount = std::max(3u * swapInterval, surfaceCapabilities.minImageCount);
|
||||
INFO_LOG(RENDERER, "Surface capabilities: minImageCount %d maxImageCount %d swapInterval %d", surfaceCapabilities.minImageCount, surfaceCapabilities.maxImageCount, swapInterval);
|
||||
#ifdef SWAPPY
|
||||
// No need to dupe frames
|
||||
u32 imageCount = 3;
|
||||
#else
|
||||
u32 imageCount = 4;
|
||||
#endif
|
||||
imageCount = std::max(imageCount * swapInterval, surfaceCapabilities.minImageCount);
|
||||
if (surfaceCapabilities.maxImageCount != 0)
|
||||
imageCount = std::min(imageCount, surfaceCapabilities.maxImageCount);
|
||||
vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eColorAttachment;
|
||||
@@ -779,6 +795,7 @@ void VulkanContext::CreateSwapChain()
|
||||
swapChainCreateInfo.pQueueFamilyIndices = queueFamilyIndices;
|
||||
}
|
||||
|
||||
swappyvk::detachSwapchain();
|
||||
swapChain.reset();
|
||||
try {
|
||||
swapChain = device->createSwapchainKHRUnique(swapChainCreateInfo);
|
||||
@@ -792,6 +809,8 @@ void VulkanContext::CreateSwapChain()
|
||||
}
|
||||
while (!swapChain);
|
||||
|
||||
swappyvk::init(physicalDevice, *device, *swapChain);
|
||||
|
||||
std::vector<vk::Image> swapChainImages = device->getSwapchainImagesKHR(*swapChain);
|
||||
|
||||
imageViews.resize(swapChainImages.size());
|
||||
@@ -1012,7 +1031,7 @@ void VulkanContext::Present() noexcept
|
||||
{
|
||||
try {
|
||||
DoSwapAutomation();
|
||||
vk::Result res = presentQueue.presentKHR(vk::PresentInfoKHR(1, &(*renderCompleteSemaphores[currentSemaphore]), 1, &(*swapChain), ¤tImage));
|
||||
vk::Result res = swappyvk::present(presentQueue, vk::PresentInfoKHR(1, &(*renderCompleteSemaphores[currentSemaphore]), 1, &(*swapChain), ¤tImage));
|
||||
#if defined(VK_USE_PLATFORM_METAL_EXT) || defined(__APPLE__)
|
||||
// presentKHR lists eSuboptimalKHR as a success code so it is
|
||||
// returned rather than thrown. Treat it as a request to
|
||||
@@ -1026,9 +1045,12 @@ void VulkanContext::Present() noexcept
|
||||
resized = true;
|
||||
#endif
|
||||
currentSemaphore = (currentSemaphore + 1) % renderCompleteSemaphores.size();
|
||||
|
||||
if (lastFrameView && IsValid() && !gui_is_open())
|
||||
for (int i = 1; i < swapInterval; i++)
|
||||
#ifndef SWAPPY
|
||||
if (lastFrameView && IsValid() && !gui_is_open()
|
||||
&& swapOnVSync && config::DupeFrames)
|
||||
{
|
||||
const int interval = swapInterval * gameSwapInterval;
|
||||
for (int i = 1; i < interval; i++)
|
||||
{
|
||||
PresentFrame(vk::Image(), lastFrameView, lastFrameExtent, lastFrameAR);
|
||||
res = presentQueue.presentKHR(vk::PresentInfoKHR(1, &(*renderCompleteSemaphores[currentSemaphore]), 1, &(*swapChain), ¤tImage));
|
||||
@@ -1042,6 +1064,8 @@ void VulkanContext::Present() noexcept
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} catch (const vk::SystemError& e) {
|
||||
// Happens when resizing the window
|
||||
INFO_LOG(RENDERER, "vk::SystemError %s", e.what());
|
||||
@@ -1234,6 +1258,7 @@ void VulkanContext::term()
|
||||
instance->destroySurfaceKHR(surface.release());
|
||||
#endif
|
||||
pipelineCache.reset();
|
||||
swappyvk::detachDevice();
|
||||
device.reset();
|
||||
#ifdef VK_DEBUG
|
||||
#ifndef __ANDROID__
|
||||
@@ -1574,3 +1599,13 @@ bool VulkanContext::GetLastFrame(std::vector<u8>& data, int& width, int& height)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanContext::setSwapInterval(int interval)
|
||||
{
|
||||
interval = std::min(interval, 2);
|
||||
if (gameSwapInterval != interval) {
|
||||
NOTICE_LOG(RENDERER, "Swap interval changed to %d", swapOnVSync ? swapInterval * interval : 0);
|
||||
swappyvk::setSwapInterval(interval);
|
||||
}
|
||||
gameSwapInterval = interval;
|
||||
}
|
||||
@@ -149,6 +149,7 @@ public:
|
||||
bool isAMD() override {
|
||||
return vendorID == VENDOR_ATI || vendorID == VENDOR_AMD;
|
||||
}
|
||||
void setSwapInterval(int interval) override;
|
||||
vk::Format GetDepthFormat() const { return depthFormat; }
|
||||
static VulkanContext *Instance() { return static_cast<VulkanContext *>(GraphicsContext::Instance()); }
|
||||
bool SupportsSamplerAnisotropy() const { return samplerAnisotropy; }
|
||||
@@ -278,7 +279,7 @@ private:
|
||||
|
||||
std::string driverName;
|
||||
std::string driverVersion;
|
||||
|
||||
int gameSwapInterval = 1;
|
||||
#ifdef VK_DEBUG
|
||||
#ifndef __ANDROID__
|
||||
vk::UniqueDebugUtilsMessengerEXT debugUtilsMessenger;
|
||||
|
||||
@@ -104,6 +104,9 @@ void BaseVulkanRenderer::Process(TA_context* ctx)
|
||||
texCommandBuffer = texCommandPool.Allocate();
|
||||
texCommandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
|
||||
|
||||
if (!ctx->rend.isRTT && ctx->rend.swapInterval > 0)
|
||||
GetContext()->setSwapInterval(ctx->rend.swapInterval);
|
||||
|
||||
ta_parse(ctx, true);
|
||||
|
||||
// TODO can't update fog or palette twice in multi render
|
||||
|
||||
@@ -205,16 +205,18 @@ void gui_settings_video()
|
||||
OptionCheckbox(T("Linear Interpolation"), config::LinearInterpolation, T("Scales the output with linear interpolation. Will use nearest neighbor interpolation otherwise. Disable with integer scaling."));
|
||||
#ifndef TARGET_IPHONE
|
||||
OptionCheckbox(T("VSync"), config::VSync, T("Synchronizes the frame rate with the screen refresh rate. Recommended"));
|
||||
if (isVulkan(config::RendererType))
|
||||
{
|
||||
ImGui::Indent();
|
||||
{
|
||||
DisabledScope scope(!config::VSync);
|
||||
|
||||
OptionCheckbox(T("Duplicate frames"), config::DupeFrames, T("Duplicate frames on high refresh rate monitors (120 Hz and higher)"));
|
||||
}
|
||||
ImGui::Unindent();
|
||||
ImGui::Indent();
|
||||
{
|
||||
DisabledScope scope(!config::VSync);
|
||||
#ifdef __ANDROID__
|
||||
OptionCheckbox(T("Frame Pacing (Experimental)"), config::FramePacing,
|
||||
T("Provide the GPU with presentation timing for each frame to replicate original frame pacing."));
|
||||
#else
|
||||
if (isVulkan(config::RendererType))
|
||||
OptionCheckbox(T("Duplicate frames"), config::DupeFrames, T("Duplicate frames on high refresh rate monitors (120 Hz and higher)"));
|
||||
#endif
|
||||
}
|
||||
ImGui::Unindent();
|
||||
#endif
|
||||
OptionCheckbox(T("Show VMU In-game"), config::FloatVMUs, T("Show the VMU LCD screens while in-game"));
|
||||
OptionCheckbox(T("Full Framebuffer Emulation"), config::EmulateFramebuffer,
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.google.androidgamesdk;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.Choreographer;
|
||||
import android.util.Log;
|
||||
|
||||
|
||||
public class ChoreographerCallback implements Choreographer.FrameCallback {
|
||||
private static final String LOG_TAG = "ChoreographerCallback";
|
||||
private long mCookie;
|
||||
private LooperThread mLooper;
|
||||
|
||||
private class LooperThread extends Thread {
|
||||
public Handler mHandler;
|
||||
|
||||
public void run() {
|
||||
Log.i(LOG_TAG, "Starting looper thread");
|
||||
Looper.prepare();
|
||||
mHandler = new Handler();
|
||||
Looper.loop();
|
||||
Log.i(LOG_TAG, "Terminating looper thread");
|
||||
}
|
||||
}
|
||||
|
||||
public ChoreographerCallback(long cookie) {
|
||||
mCookie = cookie;
|
||||
mLooper = new LooperThread();
|
||||
mLooper.start();
|
||||
}
|
||||
|
||||
public void postFrameCallback() {
|
||||
mLooper.mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Choreographer.getInstance().postFrameCallback(ChoreographerCallback.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void postFrameCallbackDelayed(long delayMillis) {
|
||||
Choreographer.getInstance().postFrameCallbackDelayed(this, delayMillis);
|
||||
}
|
||||
|
||||
public void terminate() {
|
||||
mLooper.mHandler.getLooper().quit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFrame(long frameTimeNanos) {
|
||||
nOnChoreographer(mCookie, frameTimeNanos);
|
||||
}
|
||||
|
||||
public native void nOnChoreographer(long cookie, long frameTimeNanos);
|
||||
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.google.androidgamesdk;
|
||||
|
||||
/** JNI api for getting device information */
|
||||
public class GameSdkDeviceInfoJni {
|
||||
private static Throwable initializationExceptionOrError;
|
||||
|
||||
static {
|
||||
try {
|
||||
System.loadLibrary("game_sdk_device_info_jni");
|
||||
} catch(Exception exception) {
|
||||
// Catch SecurityException, NullPointerException (or any potential unchecked exception)
|
||||
// as we don't want to crash the app if the library failed to load.
|
||||
initializationExceptionOrError = exception;
|
||||
} catch(Error error) {
|
||||
// Catch UnsatisfiedLinkError (or any potential unchecked error)
|
||||
// as we don't want to crash the app if the library failed to load.
|
||||
initializationExceptionOrError = error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a byte array, which is a serialized proto containing device information, or
|
||||
* null if the native library "game_sdk_device_info_jni" could not be loaded.
|
||||
*
|
||||
* @return Optional with the serialized byte array, representing game sdk device info with errors,
|
||||
* or null.
|
||||
*/
|
||||
public static byte[] tryGetProtoSerialized() {
|
||||
if (initializationExceptionOrError != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getProtoSerialized();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the exception or error that was caught when trying to load the library, if any.
|
||||
* Otherwise, returns null.
|
||||
*
|
||||
* @return The caught Throwable or null.
|
||||
*/
|
||||
public static Throwable getInitializationExceptionOrError() {
|
||||
return initializationExceptionOrError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a byte array, which is a serialized proto.
|
||||
*
|
||||
* @return serialized byte array, representing game sdk device info with errors.
|
||||
*/
|
||||
private static native byte[] getProtoSerialized();
|
||||
|
||||
private GameSdkDeviceInfoJni() {}
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
package com.google.androidgamesdk;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static android.app.NativeActivity.META_DATA_LIB_NAME;
|
||||
|
||||
public class SwappyDisplayManager implements DisplayManager.DisplayListener {
|
||||
final private String LOG_TAG = "SwappyDisplayManager";
|
||||
final private boolean DEBUG = false;
|
||||
final private long ONE_MS_IN_NS = 1000000;
|
||||
final private long ONE_S_IN_NS = ONE_MS_IN_NS * 1000;
|
||||
|
||||
private long mCookie;
|
||||
private Activity mActivity;
|
||||
private DisplayManager mDisplayManager;
|
||||
private WindowManager mWindowManager;
|
||||
private Display.Mode mCurrentMode;
|
||||
|
||||
private LooperThread mLooper;
|
||||
|
||||
private class LooperThread extends Thread {
|
||||
public Handler mHandler;
|
||||
private Lock mLock = new ReentrantLock();
|
||||
private Condition mCondition = mLock.newCondition();
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
mLock.lock();
|
||||
super.start();
|
||||
try {
|
||||
mCondition.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
mLock.unlock();
|
||||
|
||||
}
|
||||
|
||||
public void run() {
|
||||
Log.i(LOG_TAG, "Starting looper thread");
|
||||
|
||||
mLock.lock();
|
||||
Looper.prepare();
|
||||
mHandler = new Handler();
|
||||
mCondition.signal();
|
||||
mLock.unlock();
|
||||
|
||||
Looper.loop();
|
||||
|
||||
Log.i(LOG_TAG, "Terminating looper thread");
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
private boolean modeMatchesCurrentResolution(Display.Mode mode) {
|
||||
return mode.getPhysicalHeight() == mCurrentMode.getPhysicalHeight() &&
|
||||
mode.getPhysicalWidth() == mCurrentMode.getPhysicalWidth();
|
||||
|
||||
}
|
||||
|
||||
// Called from native SwappyDisplayManager.cpp
|
||||
public SwappyDisplayManager(long cookie, Activity activity) {
|
||||
// Load the native library for cases where an NDK application is running
|
||||
// without a java componenet
|
||||
try {
|
||||
ActivityInfo ai = activity.getPackageManager().getActivityInfo(
|
||||
activity.getIntent().getComponent(), PackageManager.GET_META_DATA);
|
||||
if (ai.metaData != null) {
|
||||
String nativeLibName = ai.metaData.getString(META_DATA_LIB_NAME);
|
||||
if (nativeLibName != null) {
|
||||
System.loadLibrary(nativeLibName);
|
||||
}
|
||||
}
|
||||
} catch (java.lang.Throwable e) {
|
||||
Log.e(LOG_TAG, e.getMessage());
|
||||
}
|
||||
|
||||
mCookie = cookie;
|
||||
mActivity = activity;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDisplayManager = mActivity.getSystemService(DisplayManager.class);
|
||||
mWindowManager = mActivity.getSystemService(WindowManager.class);
|
||||
Display display = mWindowManager.getDefaultDisplay();
|
||||
mCurrentMode = display.getMode();
|
||||
updateSupportedRefreshRates(display);
|
||||
}
|
||||
|
||||
// Register display listener callbacks
|
||||
synchronized(this) {
|
||||
mLooper = new LooperThread();
|
||||
mLooper.start();
|
||||
mDisplayManager.registerDisplayListener(this, mLooper.mHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
private void updateSupportedRefreshRates(Display display) {
|
||||
Display.Mode[] supportedModes = display.getSupportedModes();
|
||||
int totalModes = 0;
|
||||
for (int i = 0; i < supportedModes.length; i++) {
|
||||
if (!modeMatchesCurrentResolution(supportedModes[i])) {
|
||||
continue;
|
||||
}
|
||||
totalModes++;
|
||||
}
|
||||
|
||||
long[] supportedRefreshPeriods = new long[totalModes];
|
||||
int[] supportedDisplayModeIds = new int[totalModes];
|
||||
totalModes = 0;
|
||||
for (int i = 0; i < supportedModes.length; i++) {
|
||||
if (!modeMatchesCurrentResolution(supportedModes[i])) {
|
||||
continue;
|
||||
}
|
||||
supportedRefreshPeriods[totalModes] =
|
||||
(long) (ONE_S_IN_NS / supportedModes[i].getRefreshRate());
|
||||
supportedDisplayModeIds[totalModes] = supportedModes[i].getModeId();
|
||||
totalModes++;
|
||||
|
||||
}
|
||||
// Call down to native to set the supported refresh rates
|
||||
nSetSupportedRefreshPeriods(mCookie, supportedRefreshPeriods, supportedDisplayModeIds);
|
||||
}
|
||||
|
||||
// Called from native SwappyDisplayManager.cpp
|
||||
public void setPreferredDisplayModeId(final int modeId) {
|
||||
mActivity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Window w = mActivity.getWindow();
|
||||
WindowManager.LayoutParams l = w.getAttributes();
|
||||
if (DEBUG) {
|
||||
Log.v(LOG_TAG, "set preferredDisplayModeId to " + modeId);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
l.preferredDisplayModeId = modeId;
|
||||
}
|
||||
|
||||
|
||||
w.setAttributes(l);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Called from native SwappyDisplayManager.cpp
|
||||
public void terminate() {
|
||||
mDisplayManager.unregisterDisplayListener(this);
|
||||
mLooper.mHandler.getLooper().quit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayAdded(int displayId) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayRemoved(int displayId) {
|
||||
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
@Override
|
||||
public void onDisplayChanged(int displayId) {
|
||||
synchronized(this) {
|
||||
Display display = mWindowManager.getDefaultDisplay();
|
||||
float newRefreshRate = display.getRefreshRate();
|
||||
Display.Mode newMode = display.getMode();
|
||||
boolean resolutionChanged =
|
||||
(newMode.getPhysicalWidth() != mCurrentMode.getPhysicalWidth()) |
|
||||
(newMode.getPhysicalHeight() != mCurrentMode.getPhysicalHeight());
|
||||
boolean refreshRateChanged = (newRefreshRate != mCurrentMode.getRefreshRate());
|
||||
mCurrentMode = newMode;
|
||||
|
||||
if (resolutionChanged) {
|
||||
updateSupportedRefreshRates(display);
|
||||
}
|
||||
|
||||
if (refreshRateChanged) {
|
||||
final long appVsyncOffsetNanos = display.getAppVsyncOffsetNanos();
|
||||
final long vsyncPresentationDeadlineNanos =
|
||||
mWindowManager.getDefaultDisplay().getPresentationDeadlineNanos();
|
||||
|
||||
final long vsyncPeriodNanos = (long)(ONE_S_IN_NS / newRefreshRate);
|
||||
final long sfVsyncOffsetNanos =
|
||||
vsyncPeriodNanos - (vsyncPresentationDeadlineNanos - ONE_MS_IN_NS);
|
||||
|
||||
nOnRefreshPeriodChanged(mCookie,
|
||||
vsyncPeriodNanos,
|
||||
appVsyncOffsetNanos,
|
||||
sfVsyncOffsetNanos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private native void nSetSupportedRefreshPeriods(long cookie,
|
||||
long[] refreshPeriods,
|
||||
int[] modeIds);
|
||||
private native void nOnRefreshPeriodChanged(long cookie,
|
||||
long refreshPeriod,
|
||||
long appOffset,
|
||||
long sfOffset);
|
||||
}
|
||||
@@ -8,4 +8,6 @@ target_sources(${PROJECT_NAME} PRIVATE
|
||||
Android.cpp
|
||||
http_client.h
|
||||
i18n.cpp
|
||||
jni_util.h)
|
||||
jni_util.h
|
||||
swappygl.cpp
|
||||
swappyvk.cpp)
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
Copyright 2026 flyinghead
|
||||
|
||||
This file is part of Flycast.
|
||||
|
||||
Flycast is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Flycast is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "types.h"
|
||||
#include "wsi/egl.h"
|
||||
#include "cfg/option.h"
|
||||
#include "jni_util.h"
|
||||
#ifdef SWAPPY
|
||||
|
||||
#define __eglext_h_ 1 // avoid multiply defined errors
|
||||
#include <swappy/swappyGL.h>
|
||||
#include <swappy/swappyGL_extra.h>
|
||||
|
||||
class SwappyGLGC : public EGLGraphicsContext
|
||||
{
|
||||
public:
|
||||
SwappyGLGC(void *window, void *display);
|
||||
~SwappyGLGC();
|
||||
void swap() override;
|
||||
|
||||
private:
|
||||
void changeSwapInterval();
|
||||
void activate(bool enable);
|
||||
|
||||
bool swappyAvailable = false;
|
||||
bool enabled = false;
|
||||
};
|
||||
|
||||
extern jobject g_activity;
|
||||
|
||||
void EGLGraphicsContext::Create(void *window, void *display) {
|
||||
new SwappyGLGC(window, display);
|
||||
}
|
||||
|
||||
SwappyGLGC::SwappyGLGC(void *window, void *display)
|
||||
: EGLGraphicsContext(window, display)
|
||||
{
|
||||
activate(config::FramePacing);
|
||||
}
|
||||
|
||||
SwappyGLGC::~SwappyGLGC() {
|
||||
activate(false);
|
||||
}
|
||||
|
||||
void SwappyGLGC::activate(bool enable)
|
||||
{
|
||||
if (this->enabled == enable)
|
||||
return;
|
||||
this->enabled = enable;
|
||||
if (enable)
|
||||
{
|
||||
SwappyGL_init(jni::env(), g_activity);
|
||||
swappyAvailable = SwappyGL_isEnabled();
|
||||
if (swappyAvailable)
|
||||
{
|
||||
SwappyGL_setWindow((ANativeWindow *)window);
|
||||
SwappyGL_setSwapIntervalNS(SWAPPY_SWAP_60FPS);
|
||||
SwappyGL_setAutoSwapInterval(false);
|
||||
SwappyGL_setAutoPipelineMode(false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
SwappyGL_destroy();
|
||||
swappyAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SwappyGLGC::swap()
|
||||
{
|
||||
activate(config::FramePacing);
|
||||
do_swap_automation();
|
||||
changeSwapInterval();
|
||||
if (swapOnVSync && swappyAvailable)
|
||||
SwappyGL_swap(display, surface);
|
||||
else
|
||||
eglSwapBuffers(display, surface);
|
||||
}
|
||||
|
||||
void SwappyGLGC::changeSwapInterval()
|
||||
{
|
||||
if (!swappyAvailable) {
|
||||
EGLGraphicsContext::changeSwapInterval();
|
||||
return;
|
||||
}
|
||||
if (!gameSwapIntervalChanged
|
||||
&& swapOnVSync == (!settings.input.fastForwardMode && config::VSync))
|
||||
return;
|
||||
gameSwapIntervalChanged = false;
|
||||
const bool oldSwapOnVSync = swapOnVSync;
|
||||
swapOnVSync = (!settings.input.fastForwardMode && config::VSync);
|
||||
if (swapOnVSync)
|
||||
{
|
||||
if (!oldSwapOnVSync)
|
||||
// put swappy back in control
|
||||
eglSwapInterval(display, 1);
|
||||
currentSwapInterval = gameSwapInterval;
|
||||
if (swappyAvailable) {
|
||||
SwappyGL_setSwapIntervalNS(SWAPPY_SWAP_60FPS * currentSwapInterval);
|
||||
NOTICE_LOG(RENDERER, "Swap interval changed to %d (EGL max %d)", currentSwapInterval, maxSwapInterval);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No way to disable swappy so use EGL to disable sync wait
|
||||
eglSwapInterval(display, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // SWAPPY
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
Copyright 2026 flyinghead
|
||||
|
||||
This file is part of Flycast.
|
||||
|
||||
Flycast is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Flycast is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "rend/vulkan/swappyvk.h"
|
||||
#include "jni_util.h"
|
||||
#include "wsi/context.h"
|
||||
#include "cfg/option.h"
|
||||
#include <swappy/swappyVk.h>
|
||||
|
||||
#ifdef SWAPPY
|
||||
|
||||
extern jobject g_activity;
|
||||
|
||||
namespace swappyvk
|
||||
{
|
||||
static bool googleTimingSupported;
|
||||
static vk::PhysicalDevice phyDevice;
|
||||
static vk::Device device;
|
||||
static vk::SwapchainKHR swapchain;
|
||||
static bool swappyAvailable;
|
||||
static bool enabled;
|
||||
|
||||
void setQueueFamilyIndex(bool googleTimingSupported, vk::Device device, vk::Queue queue, u32 queueFamilyIndex)
|
||||
{
|
||||
swappyvk::googleTimingSupported = googleTimingSupported;
|
||||
if (googleTimingSupported)
|
||||
SwappyVk_setQueueFamilyIndex((VkDevice)device, (VkQueue)queue, queueFamilyIndex);
|
||||
}
|
||||
|
||||
static void activate(bool enable)
|
||||
{
|
||||
if (enabled == enable)
|
||||
return;
|
||||
enabled = enable;
|
||||
swappyAvailable = false;
|
||||
if (enable)
|
||||
{
|
||||
if (settings.input.fastForwardMode || !config::VSync || !googleTimingSupported)
|
||||
return;
|
||||
|
||||
// Fake call to make swappy use google timing for this device
|
||||
VkExtensionProperties prop { { VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME } };
|
||||
u32 requiredExtensionCount = 1;
|
||||
char s[VK_MAX_EXTENSION_NAME_SIZE];
|
||||
char *requiredExtension = s;
|
||||
SwappyVk_determineDeviceExtensions((VkPhysicalDevice)phyDevice, 1, &prop, &requiredExtensionCount, &requiredExtension);
|
||||
|
||||
u64 refreshDuration;
|
||||
SwappyVk_initAndGetRefreshCycleDuration(jni::env(), g_activity, (VkPhysicalDevice)phyDevice, (VkDevice)device,
|
||||
(VkSwapchainKHR)swapchain, &refreshDuration);
|
||||
SwappyVk_isEnabled((VkSwapchainKHR)swapchain, &swappyAvailable);
|
||||
NOTICE_LOG(RENDERER, "SwappyVk status: %s", swappyAvailable ? "enabled" : "disabled");
|
||||
if (!swappyAvailable) {
|
||||
SwappyVk_destroySwapchain((VkDevice)device, (VkSwapchainKHR)swapchain);
|
||||
return;
|
||||
}
|
||||
GraphicsContext *gc = GraphicsContext::Instance();
|
||||
if (gc != nullptr)
|
||||
{
|
||||
void *window, *display;
|
||||
gc->getWindow(&window, &display);
|
||||
SwappyVk_setWindow((VkDevice)device, (VkSwapchainKHR)swapchain, (ANativeWindow *)window);
|
||||
}
|
||||
else {
|
||||
ERROR_LOG(RENDERER, "SwappyVk: Graphics context is null");
|
||||
}
|
||||
SwappyVk_setAutoSwapInterval(false);
|
||||
SwappyVk_setAutoPipelineMode(false);
|
||||
SwappyVk_setSwapIntervalNS((VkDevice)device, (VkSwapchainKHR)swapchain, SWAPPY_SWAP_60FPS);
|
||||
}
|
||||
else {
|
||||
if (swapchain)
|
||||
SwappyVk_destroySwapchain((VkDevice)device, (VkSwapchainKHR)swapchain);
|
||||
}
|
||||
}
|
||||
|
||||
void init(vk::PhysicalDevice phyDevice, vk::Device device, vk::SwapchainKHR swapchain)
|
||||
{
|
||||
swappyvk::phyDevice = phyDevice;
|
||||
swappyvk::device = device;
|
||||
swappyvk::swapchain = swapchain;
|
||||
activate(config::FramePacing);
|
||||
}
|
||||
|
||||
void setSwapInterval(int i) {
|
||||
if (swappyAvailable)
|
||||
SwappyVk_setSwapIntervalNS((VkDevice)device, (VkSwapchainKHR)swapchain, SWAPPY_SWAP_60FPS * i);
|
||||
}
|
||||
|
||||
vk::Result present(vk::Queue queue, const vk::PresentInfoKHR& presentInfo)
|
||||
{
|
||||
activate(config::FramePacing);
|
||||
|
||||
if (!swappyAvailable)
|
||||
return queue.presentKHR(presentInfo);
|
||||
|
||||
bool swapOnVSync = !settings.input.fastForwardMode && config::VSync;
|
||||
SwappyVk_enableFramePacing((VkSwapchainKHR)swapchain, swapOnVSync);
|
||||
SwappyVk_enableBlockingWait((VkSwapchainKHR)swapchain, swapOnVSync);
|
||||
return static_cast<vk::Result>(SwappyVk_queuePresent((VkQueue)queue, (const VkPresentInfoKHR *)presentInfo));
|
||||
}
|
||||
|
||||
void detachSwapchain() {
|
||||
activate(false);
|
||||
}
|
||||
|
||||
void detachDevice()
|
||||
{
|
||||
if (device)
|
||||
{
|
||||
SwappyVk_destroyDevice((VkDevice)device);
|
||||
swapchain = nullptr;
|
||||
device = nullptr;
|
||||
swappyAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace swappyvk
|
||||
#endif // SWAPPY
|
||||
Reference in new issue
Block a user