diff --git a/osu.Framework.Android/AndroidGameHost.cs b/osu.Framework.Android/AndroidGameHost.cs index b6d3106c35..84f962c1a7 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; @@ -26,6 +27,8 @@ public class AndroidGameHost : SDLGameHost { private readonly AndroidGameActivity activity; + private ChoreographerVsyncWaiter vsyncWaiter = new ChoreographerVsyncWaiter(); + public AndroidGameHost(AndroidGameActivity activity) : base(string.Empty) { @@ -48,6 +51,26 @@ 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(); + + if (Renderer.VerticalSync) + vsyncWaiter.WaitForNextVsync(); + } + else + { + base.Swap(); + } + } + public override bool CanExit => false; public override bool CanSuspendToBackground => true; @@ -173,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(); 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..2bbf0247a1 100644 --- a/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs +++ b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs @@ -206,6 +206,59 @@ 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 } }