From c3d7b38ee1b61787753bdd51780b4774d89023c0 Mon Sep 17 00:00:00 2001 From: Shiumano Date: Thu, 27 Aug 2026 23:16:48 +0900 Subject: [PATCH 1/4] Add a function to set the desired display time on Android --- .../Platform/IAndroidGraphicsSurface.cs | 6 +++ .../Platform/SDL3/SDL3GraphicsSurface.cs | 48 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/osu.Framework/Platform/IAndroidGraphicsSurface.cs b/osu.Framework/Platform/IAndroidGraphicsSurface.cs index 66ceb4358e..88c2d6c191 100644 --- a/osu.Framework/Platform/IAndroidGraphicsSurface.cs +++ b/osu.Framework/Platform/IAndroidGraphicsSurface.cs @@ -17,5 +17,11 @@ public interface IAndroidGraphicsSurface /// /// https://developer.android.com/reference/android/view/Surface.html IntPtr SurfaceHandle { get; } + + /// + /// Set the desired display time for GLES rendering. + /// + /// https://developer.android.com/reference/android/opengl/EGLExt + void SetPresentationTime(long presentationTimeNanos); } } diff --git a/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs index 56ca31116a..f202904d80 100644 --- a/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs +++ b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs @@ -206,6 +206,54 @@ bool IOpenGLGraphicsSurface.VerticalSync [SupportedOSPlatform("android")] IntPtr IAndroidGraphicsSurface.SurfaceHandle => window.SurfaceHandle; + [SupportedOSPlatform("android")] + void IAndroidGraphicsSurface.SetPresentationTime(long presentationTimeNanos) + { + IntPtr sdlEglDisplay = SDL_EGL_GetCurrentDisplay(); + IntPtr sdlEglSurface = getCurrentDrawSurface(); + + if (sdlEglDisplay == IntPtr.Zero || sdlEglSurface == IntPtr.Zero) return; + + setPresentationTime(sdlEglDisplay, sdlEglSurface, presentationTimeNanos); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate IntPtr eglGetCurrentSurfaceDelegate(int readdraw); + private static eglGetCurrentSurfaceDelegate? eglGetCurrentSurface; + + private static IntPtr getCurrentDrawSurface() + { + const int egl_draw = 0x3059; + if (eglGetCurrentSurface == null) + { + IntPtr proc = SDL_EGL_GetProcAddress("eglGetCurrentSurface"); + if (proc != IntPtr.Zero) + { + eglGetCurrentSurface = Marshal.GetDelegateForFunctionPointer(proc); + } + } + + return eglGetCurrentSurface?.Invoke(egl_draw) ?? IntPtr.Zero; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate bool eglPresentationTimeANDROIDDelegate(IntPtr dpy, IntPtr surface, long time); + private static eglPresentationTimeANDROIDDelegate? eglPresentationTimeANDROID; + + private static bool setPresentationTime(IntPtr eglDisplay, IntPtr eglSurface, long presentationTimeNanos) + { + if (eglPresentationTimeANDROID == null) + { + IntPtr proc = SDL_EGL_GetProcAddress("eglPresentationTimeANDROID"); + if (proc != IntPtr.Zero) + { + eglPresentationTimeANDROID = Marshal.GetDelegateForFunctionPointer(proc); + } + } + + return eglPresentationTimeANDROID?.Invoke(eglDisplay, eglSurface, presentationTimeNanos) ?? false; + } + #endregion } } From ec1a4f56ef9f94737de822a127655096d0df0526 Mon Sep 17 00:00:00 2001 From: Shiumano Date: Thu, 27 Aug 2026 23:17:49 +0900 Subject: [PATCH 2/4] Set the desired display time on Android --- osu.Framework.Android/AndroidGameHost.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Framework.Android/AndroidGameHost.cs b/osu.Framework.Android/AndroidGameHost.cs index b6d3106c35..fbcbc4037b 100644 --- a/osu.Framework.Android/AndroidGameHost.cs +++ b/osu.Framework.Android/AndroidGameHost.cs @@ -17,6 +17,7 @@ using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; using Stream = System.IO.Stream; using Uri = Android.Net.Uri; @@ -48,6 +49,23 @@ protected override void DrawFrame() base.DrawFrame(); } + protected override void Swap() + { + if (Window.GraphicsSurface.Type == GraphicsSurfaceType.OpenGL + && Window.GraphicsSurface is IAndroidGraphicsSurface androidGraphics) + { + long nowNanoTime = TimeProvider.System.GetTimestamp(); + + androidGraphics.SetPresentationTime(nowNanoTime); + + base.Swap(); + } + else + { + base.Swap(); + } + } + public override bool CanExit => false; public override bool CanSuspendToBackground => true; From 2624dc576c43a33a7b4dcf10731e00b7ccd88571 Mon Sep 17 00:00:00 2001 From: Shiumano Date: Thu, 27 Aug 2026 23:19:17 +0900 Subject: [PATCH 3/4] Using Choreographer for vsync on Android Calling eglPresentationTimeANDROID caused GL vsync to stop working. --- osu.Framework.Android/AndroidGameHost.cs | 14 ++++ .../ChoreographerVsyncWaiter.cs | 70 +++++++++++++++++++ osu.Framework/Platform/GameHost.cs | 2 +- 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 osu.Framework.Android/ChoreographerVsyncWaiter.cs diff --git a/osu.Framework.Android/AndroidGameHost.cs b/osu.Framework.Android/AndroidGameHost.cs index fbcbc4037b..84f962c1a7 100644 --- a/osu.Framework.Android/AndroidGameHost.cs +++ b/osu.Framework.Android/AndroidGameHost.cs @@ -27,6 +27,8 @@ public class AndroidGameHost : SDLGameHost { private readonly AndroidGameActivity activity; + private ChoreographerVsyncWaiter vsyncWaiter = new ChoreographerVsyncWaiter(); + public AndroidGameHost(AndroidGameActivity activity) : base(string.Empty) { @@ -59,6 +61,9 @@ protected override void Swap() androidGraphics.SetPresentationTime(nowNanoTime); base.Swap(); + + if (Renderer.VerticalSync) + vsyncWaiter.WaitForNextVsync(); } else { @@ -191,5 +196,14 @@ public override bool SuspendToBackground() { return activity.MoveTaskToBack(true); } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + vsyncWaiter.Dispose(); + } + base.Dispose(disposing); + } } } diff --git a/osu.Framework.Android/ChoreographerVsyncWaiter.cs b/osu.Framework.Android/ChoreographerVsyncWaiter.cs new file mode 100644 index 0000000000..88397a8f99 --- /dev/null +++ b/osu.Framework.Android/ChoreographerVsyncWaiter.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Android.OS; +using Android.Views; +using System; +using System.Threading; + +namespace osu.Framework.Android +{ + public sealed class ChoreographerVsyncWaiter : Java.Lang.Object, Choreographer.IFrameCallback + { + private readonly HandlerThread thread; + private readonly Handler handler; + private readonly ManualResetEventSlim vsyncEvent = new ManualResetEventSlim(false); + + private Choreographer choreographer = null!; + private bool disposed; + + public ChoreographerVsyncWaiter() + { + thread = new HandlerThread("ChoreographerVsync"); + thread.Start(); + + handler = new Handler(thread.Looper!); + + using var ready = new ManualResetEventSlim(false); + + handler.Post(() => + { + choreographer = Choreographer.Instance!; + ready.Set(); + }); + + ready.Wait(30000); + ready.Dispose(); + } + + public void WaitForNextVsync() + { + ObjectDisposedException.ThrowIf(disposed, this); + + vsyncEvent.Reset(); + + handler.Post(() => + { + choreographer.PostFrameCallback(this); + }); + + vsyncEvent.Wait(30000); + } + + public void DoFrame(long _) => vsyncEvent.Set(); + + public new void Dispose() + { + if (disposed) + return; + + disposed = true; + + thread.QuitSafely(); + thread.Join(); + + vsyncEvent.Dispose(); + + base.Dispose(); + } + } +} diff --git a/osu.Framework/Platform/GameHost.cs b/osu.Framework/Platform/GameHost.cs index c53880481e..c151136c7c 100644 --- a/osu.Framework/Platform/GameHost.cs +++ b/osu.Framework/Platform/GameHost.cs @@ -579,7 +579,7 @@ protected virtual void Swap() { Renderer.SwapBuffers(); - if (Window.GraphicsSurface.Type == GraphicsSurfaceType.OpenGL && Renderer.VerticalSync) + if (Window.GraphicsSurface.Type == GraphicsSurfaceType.OpenGL && Renderer.VerticalSync && RuntimeInfo.OS != RuntimeInfo.Platform.Android) // without waiting (i.e. glFinish), vsync is basically unplayable due to the extra latency introduced. // we will likely want to give the user control over this in the future as an advanced setting. Renderer.WaitUntilIdle(); From 1af7948089cae52c6c22e210276e57eef4b5d082 Mon Sep 17 00:00:00 2001 From: Shiumano Date: Fri, 28 Aug 2026 02:41:27 +0900 Subject: [PATCH 4/4] fix code analysis warnings --- .../Platform/SDL3/SDL3GraphicsSurface.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs index f202904d80..2bbf0247a1 100644 --- a/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs +++ b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs @@ -218,18 +218,21 @@ void IAndroidGraphicsSurface.SetPresentationTime(long presentationTimeNanos) } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate IntPtr eglGetCurrentSurfaceDelegate(int readdraw); - private static eglGetCurrentSurfaceDelegate? eglGetCurrentSurface; + private delegate IntPtr EglGetCurrentSurfaceDelegate(int readdraw); + + private static EglGetCurrentSurfaceDelegate? eglGetCurrentSurface; private static IntPtr getCurrentDrawSurface() { const int egl_draw = 0x3059; + if (eglGetCurrentSurface == null) { IntPtr proc = SDL_EGL_GetProcAddress("eglGetCurrentSurface"); + if (proc != IntPtr.Zero) { - eglGetCurrentSurface = Marshal.GetDelegateForFunctionPointer(proc); + eglGetCurrentSurface = Marshal.GetDelegateForFunctionPointer(proc); } } @@ -237,21 +240,23 @@ private static IntPtr getCurrentDrawSurface() } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate bool eglPresentationTimeANDROIDDelegate(IntPtr dpy, IntPtr surface, long time); - private static eglPresentationTimeANDROIDDelegate? eglPresentationTimeANDROID; + private delegate bool EglPresentationTimeAndroidDelegate(IntPtr dpy, IntPtr surface, long time); + + private static EglPresentationTimeAndroidDelegate? eglPresentationTimeAndroid; private static bool setPresentationTime(IntPtr eglDisplay, IntPtr eglSurface, long presentationTimeNanos) { - if (eglPresentationTimeANDROID == null) + if (eglPresentationTimeAndroid == null) { IntPtr proc = SDL_EGL_GetProcAddress("eglPresentationTimeANDROID"); + if (proc != IntPtr.Zero) { - eglPresentationTimeANDROID = Marshal.GetDelegateForFunctionPointer(proc); + eglPresentationTimeAndroid = Marshal.GetDelegateForFunctionPointer(proc); } } - return eglPresentationTimeANDROID?.Invoke(eglDisplay, eglSurface, presentationTimeNanos) ?? false; + return eglPresentationTimeAndroid?.Invoke(eglDisplay, eglSurface, presentationTimeNanos) ?? false; } #endregion