diff --git a/Common/System/NativeApp.h b/Common/System/NativeApp.h index 02841f87e50e..9b912279e203 100644 --- a/Common/System/NativeApp.h +++ b/Common/System/NativeApp.h @@ -65,6 +65,9 @@ void NativeFrame(GraphicsContext *graphicsContext); // The app must fill the buffer completely, doing its own internal buffering if needed. void NativeMix(short *audio, int num_samples, int sampleRateHz, void *userdata); +// Runs if System_GetProperty(SYSPROP_HAS_VSYNC_CALLBACK) is supported. +void NativeVSync(int64_t vsyncId, double frameTime, double expectedPresentationTime); + // Called when it's time to shutdown. After this has been called, // no more calls to any other function will be made from the framework // before process exit. diff --git a/Common/System/System.h b/Common/System/System.h index e7135a2175c6..1103459a6de6 100644 --- a/Common/System/System.h +++ b/Common/System/System.h @@ -236,6 +236,8 @@ enum SystemProperty { SYSPROP_USE_IAP, SYSPROP_USE_APP_STORE, SYSPROP_SUPPORTS_SHARE_TEXT, + + SYSPROP_HAS_VSYNC_CALLBACK, }; enum class SystemNotification { diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index ff8064c997c0..df5a63f2cd2c 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -1527,6 +1527,10 @@ void NativeResized() { resized = true; } +void NativeVSync(int64_t vsyncId, double frameTime, double expectedPresentationTime) { + // TODO: Make use of this. +} + void NativeSetRestarting() { restarting = true; } diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index a9ba4f314fd6..c4b08f33a099 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -477,6 +477,8 @@ bool System_GetPropertyBool(SystemProperty prop) { return false; // Update if we add support in FileUtil.cpp: OpenFileInEditor case SYSPROP_SUPPORTS_SHARE_TEXT: return true; + case SYSPROP_HAS_VSYNC_CALLBACK: + return true; case SYSPROP_APP_GOLD: #ifdef GOLD return true; @@ -622,6 +624,12 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_audioConfig optimalSampleRate = optimalSR; } +// Allow the app to intercept the back button. +extern "C" void Java_org_ppsspp_ppsspp_NativeApp_vsync(JNIEnv *env, jclass, long long frameTimeNanos, long long vsyncId, long long expectedPresentationTimeNanos) { + // The frame times should match the raw times we get from the system. + NativeVSync(vsyncId, frameTimeNanos > 0 ? from_time_raw(frameTimeNanos) : -1.0, expectedPresentationTimeNanos > 0 ? from_time_raw(expectedPresentationTimeNanos) : -1.0); +} + // Easy way for the Java side to ask the C++ side for configuration options, such as // the rotation lock which must be controlled from Java on Android. static std::string QueryConfig(std::string_view query) { diff --git a/android/src/org/ppsspp/ppsspp/NativeApp.java b/android/src/org/ppsspp/ppsspp/NativeApp.java index 916046e9d521..0777bd32fc64 100644 --- a/android/src/org/ppsspp/ppsspp/NativeApp.java +++ b/android/src/org/ppsspp/ppsspp/NativeApp.java @@ -74,6 +74,9 @@ public class NativeApp { public static native void setSatInfoAndroid(short index, short id, short elevation, short azimuth, short snr, short good); public static native void pushCameraImageAndroid(byte[] image); + // From the choreographer. + public static native void vsync(long frameTimeNanos, long vsyncId, long expectedPresentationTimeNanos); + // Wrappers public static void reportException(Exception e, String data) { StringBuilder str = new StringBuilder(e.toString() + "\n" + e.getMessage() + "\n"); diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index e49c19f55dfc..f038b3d29a60 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -40,6 +40,7 @@ import android.text.InputType; import android.util.Log; import android.database.Cursor; +import android.view.Choreographer; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.InputDevice; @@ -90,6 +91,12 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList // Lifecycle tracker, to detect erroneous states. private final LifeCycle lifeCycle = new LifeCycle(); + private final Choreographer choreographer = Choreographer.getInstance(); + + // Callback objects (we’ll reuse the same instance so we can remove them cleanly) + private Choreographer.FrameCallback frameCallback; + private Choreographer.VsyncCallback vsyncCallback; + // Graphics and audio interfaces for Vulkan (javaGL = false) private NativeSurfaceView mSurfaceView; private Surface mSurface; @@ -623,6 +630,40 @@ private void updateSystemUiVisibility() { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + vsyncCallback = new Choreographer.VsyncCallback() { + @Override + public void onVsync(Choreographer.FrameData frameData) { + // API 33+ path + long frameTimeNanos = frameData.getFrameTimeNanos(); + Choreographer.FrameTimeline timeline = frameData.getPreferredFrameTimeline(); + long vsyncId = timeline.getVsyncId(); + long expectedPresentationTimeNanos = timeline.getExpectedPresentationTimeNanos(); + + // Call your native framework: + NativeApp.vsync(frameTimeNanos, vsyncId, expectedPresentationTimeNanos); + + // Re-post for next vsync + choreographer.postVsyncCallback(this); + } + }; + } else { + frameCallback = new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + // This is the fallback path (pre-API 33) + // Convert to e.g. milliseconds if needed: + long frameTimeMs = frameTimeNanos / 1_000_000L; + + // Call your native framework, e.g. nativeOnVsync(frameTimeNanos, -1, -1); + NativeApp.vsync(frameTimeNanos, /*vsyncId=*/-1, /*expectedPresentationTimeNanos=*/-1); + + // Re-post for next frame + choreographer.postFrameCallback(this); + } + }; + } + if (m_hasNoNativeBinary) { new Thread() { @Override @@ -969,6 +1010,13 @@ protected void onPause() { super.onPause(); lifeCycle.onPause(); + // Remove callbacks so we stop receiving events + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + choreographer.removeVsyncCallback(vsyncCallback); + } else { + choreographer.removeFrameCallback(frameCallback); + } + if (!javaGL) { Log.i(TAG, "Joining render thread..."); joinRenderLoopThread(); @@ -1014,6 +1062,12 @@ protected void onResume() { } else if (mGLSurfaceView != null) { mGLSurfaceView.onResume(); } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + choreographer.postVsyncCallback(vsyncCallback); + } else { + choreographer.postFrameCallback(frameCallback); + } Log.i(TAG, "onResume end"); } @@ -1880,6 +1934,7 @@ public String getDebugString(String str) { return "bad debug string: " + str; } } + private static String exitReasonToString(int reason) { switch (reason) { case android.app.ApplicationExitInfo.REASON_ANR: diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 139a26d18baa..f721b4639522 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -74,7 +74,8 @@ bool System_AudioRecordingState() { return false; } // Temporary hacks around annoying linking errors. void NativeFrame(GraphicsContext *graphicsContext) { } -void NativeResized() { } +void NativeResized() {} +void NativeVSync(int64_t vsyncId, double frameTime, double expectedPresentationTime) {} std::string System_GetProperty(SystemProperty prop) { return ""; } std::vector System_GetPropertyStringVec(SystemProperty prop) { return std::vector(); } diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp index 1c506b538037..5f071000da27 100644 --- a/libretro/libretro.cpp +++ b/libretro/libretro.cpp @@ -2012,6 +2012,7 @@ void System_PostUIMessage(UIMessage message, std::string_view param) {} void System_RunOnMainThread(std::function) {} void NativeFrame(GraphicsContext *graphicsContext) {} void NativeResized() {} +void NativeVSync(int64_t vsyncId, double frameTime, double expectedPresentationTime) {} void System_Toast(std::string_view str) {} diff --git a/unittest/JitHarness.cpp b/unittest/JitHarness.cpp index 7dbf9eef7d3b..36f7aa52cd33 100644 --- a/unittest/JitHarness.cpp +++ b/unittest/JitHarness.cpp @@ -39,8 +39,9 @@ #include "Core/HLE/HLE.h" // Temporary hacks around annoying linking errors. Copied from Headless. -void NativeFrame(GraphicsContext *graphicsContext) { } -void NativeResized() { } +void NativeFrame(GraphicsContext *graphicsContext) {} +void NativeResized() {} +void NativeVSync(int64_t vsyncId, double frameTime, double expectedPresentationTime) {} bool System_MakeRequest(SystemRequestType type, int requestId, const std::string ¶m1, const std::string ¶m2, int64_t param3, int64_t param4) { return false; } void System_InputBoxGetString(const std::string &title, const std::string &defaultValue, std::function cb) { cb(false, ""); }