Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions osu.Framework.Android/AndroidGameHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -26,6 +27,8 @@ public class AndroidGameHost : SDLGameHost
{
private readonly AndroidGameActivity activity;

private ChoreographerVsyncWaiter vsyncWaiter = new ChoreographerVsyncWaiter();

public AndroidGameHost(AndroidGameActivity activity)
: base(string.Empty)
{
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
70 changes: 70 additions & 0 deletions osu.Framework.Android/ChoreographerVsyncWaiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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();
}
}
}
2 changes: 1 addition & 1 deletion osu.Framework/Platform/GameHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions osu.Framework/Platform/IAndroidGraphicsSurface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,11 @@ public interface IAndroidGraphicsSurface
/// </summary>
/// <remarks>https://developer.android.com/reference/android/view/Surface.html</remarks>
IntPtr SurfaceHandle { get; }

/// <summary>
/// Set the desired display time for GLES rendering.
/// </summary>
/// <remarks>https://developer.android.com/reference/android/opengl/EGLExt</remarks>
void SetPresentationTime(long presentationTimeNanos);
}
}
53 changes: 53 additions & 0 deletions osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
{
IntPtr proc = SDL_EGL_GetProcAddress("eglGetCurrentSurface");

if (proc != IntPtr.Zero)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
{
eglGetCurrentSurface = Marshal.GetDelegateForFunctionPointer<EglGetCurrentSurfaceDelegate>(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)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
{
eglPresentationTimeAndroid = Marshal.GetDelegateForFunctionPointer<EglPresentationTimeAndroidDelegate>(proc);
}
}

return eglPresentationTimeAndroid?.Invoke(eglDisplay, eglSurface, presentationTimeNanos) ?? false;
}

#endregion
}
}
Loading