Skip to content

Commit a4f68b6

Browse files
perf: ADPF for Draw+Update threads, FrameSync v2 migration to ActualUnlimited
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/428d0670-2fa1-4d26-98a0-126ff08e5a7a Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent ac6d98c commit a4f68b6

2 files changed

Lines changed: 126 additions & 11 deletions

File tree

osu.Android/OsuGameAndroid.cs

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,14 @@ public partial class OsuGameAndroid : OsuGame
165165
private global::Android.Content.PM.ScreenOrientation? lastRequestedOrientation;
166166
private int currentRefreshRate;
167167

168+
// ADPF (Android Dynamic Performance Framework) hint sessions for the Draw and Update threads.
169+
// These tell the CPU scheduler to boost the clock frequency so game-loop threads can complete
170+
// their work within the display frame deadline (e.g. 8.33 ms at 120 Hz).
171+
// The sessions are created once after LoadComplete (when thread IDs are stable) and closed on
172+
// Dispose. Target duration is updated whenever the active display refresh rate changes.
173+
private IntPtr adpfDrawSession;
174+
private IntPtr adpfUpdateSession;
175+
168176
// Surface.setFrameRate() compatibility constants from android.view.Surface.
169177
// Hard-coded because the Xamarin/.NET-for-Android bindings do not always expose
170178
// these as named fields across binding versions.
@@ -517,6 +525,50 @@ protected override void LoadComplete()
517525
}
518526
});
519527
}
528+
529+
// ADPF (Android Dynamic Performance Framework) hint sessions for Draw + Update threads.
530+
// These hint sessions tell the CPU governor "these threads need to finish their work
531+
// within one display-frame interval". The kernel then pre-boosts the CPU frequency
532+
// so the threads don't stall mid-frame waiting for a slow core to spin up.
533+
//
534+
// Target duration = 1 / displayRefreshRate. We default to 120 Hz (8.33 ms) and
535+
// update the target when the display refresh rate is confirmed by applyDisplayMode.
536+
//
537+
// nADPFCreateSession() captures gettid() of the *calling* thread, so each Add
538+
// lambda must run on its respective game thread to register the correct TID.
539+
Scheduler.Add(() =>
540+
{
541+
try
542+
{
543+
Host?.DrawThread?.Scheduler.Add(() =>
544+
{
545+
try
546+
{
547+
long targetNs = currentRefreshRate > 0 ? 1_000_000_000L / currentRefreshRate : 8_333_333L;
548+
adpfDrawSession = OboeAudioBridge.nADPFCreateSession(targetNs);
549+
if (adpfDrawSession != IntPtr.Zero)
550+
Logger.Log($"[osu!] ADPF session created for Draw thread (target={targetNs / 1_000_000.0:F2}ms)", LoggingTarget.Performance);
551+
}
552+
catch { }
553+
});
554+
555+
Host?.UpdateThread?.Scheduler.Add(() =>
556+
{
557+
try
558+
{
559+
long targetNs = currentRefreshRate > 0 ? 1_000_000_000L / currentRefreshRate : 8_333_333L;
560+
adpfUpdateSession = OboeAudioBridge.nADPFCreateSession(targetNs);
561+
if (adpfUpdateSession != IntPtr.Zero)
562+
Logger.Log($"[osu!] ADPF session created for Update thread (target={targetNs / 1_000_000.0:F2}ms)", LoggingTarget.Performance);
563+
}
564+
catch { }
565+
});
566+
}
567+
catch (Exception e)
568+
{
569+
Debug.WriteLine($"[osu!] Failed to enqueue ADPF session creation: {e.Message}");
570+
}
571+
});
520572
}
521573
catch (Exception e)
522574
{
@@ -1380,6 +1432,14 @@ private void applyDisplayMode(global::Android.Views.Display display, global::And
13801432
}
13811433

13821434
Logger.Log($"[osu!] Display mode applied: {mode.RefreshRate}Hz (mode {mode.ModeId}, {mode.PhysicalWidth}x{mode.PhysicalHeight})", LoggingTarget.Performance);
1435+
1436+
// Update ADPF target duration to match the new display refresh rate.
1437+
// This keeps the CPU governor hint aligned with the actual frame deadline.
1438+
if (mode.RefreshRate > 0)
1439+
{
1440+
long targetNs = (long)(1_000_000_000.0 / mode.RefreshRate);
1441+
updateAdpfTargetDuration(targetNs);
1442+
}
13831443
}
13841444
catch (Exception e)
13851445
{
@@ -1388,8 +1448,23 @@ private void applyDisplayMode(global::Android.Views.Display display, global::And
13881448
});
13891449
}
13901450

1391-
private global::Android.Views.Display? getActiveDisplay()
1451+
/// <summary>
1452+
/// Updates the target work duration on both ADPF hint sessions (Draw + Update thread)
1453+
/// so the CPU governor can pre-boost each thread to meet the new frame deadline.
1454+
/// </summary>
1455+
private void updateAdpfTargetDuration(long targetNs)
13921456
{
1457+
try
1458+
{
1459+
if (adpfDrawSession != IntPtr.Zero)
1460+
OboeAudioBridge.nADPFUpdateTargetDuration(adpfDrawSession, targetNs);
1461+
if (adpfUpdateSession != IntPtr.Zero)
1462+
OboeAudioBridge.nADPFUpdateTargetDuration(adpfUpdateSession, targetNs);
1463+
}
1464+
catch { }
1465+
}
1466+
1467+
private global::Android.Views.Display? getActiveDisplay() {
13931468
if (gameActivity.IsFinishing || gameActivity.IsDestroyed)
13941469
return null;
13951470

@@ -1846,7 +1921,7 @@ private void updateOrientation()
18461921

18471922
/// <summary>
18481923
/// One-shot migration that switches Android-side <see cref="FrameSync"/> from the
1849-
/// framework default of <see cref="FrameSync.Limit2x"/> to <see cref="FrameSync.VSync"/>.
1924+
/// framework default of <see cref="FrameSync.Limit2x"/> to <see cref="FrameSync.ActualUnlimited"/>.
18501925
///
18511926
/// <para>
18521927
/// On a 120Hz Adreno-class display (Snapdragon 8 Gen 2 / S23 Ultra),
@@ -1860,11 +1935,14 @@ private void updateOrientation()
18601935
/// </para>
18611936
///
18621937
/// <para>
1863-
/// <see cref="FrameSync.VSync"/> caps the draw thread to the display refresh and
1864-
/// bounds in-flight frames to one, eliminating the pile-up. The migration runs
1865-
/// exactly once per install (gated by <see cref="OsuSetting.AndroidStartupFrameSyncMigrationApplied"/>)
1866-
/// so a user who later prefers <c>Limit2x</c>/<c>Unlimited</c> from
1867-
/// Settings &gt; Graphics &gt; Renderer is not fought on every launch.
1938+
/// <see cref="FrameSync.ActualUnlimited"/> uses Vulkan IMMEDIATE present mode (VK_PRESENT_MODE_IMMEDIATE_KHR)
1939+
/// which presents each frame as soon as it is ready without waiting for vblank.
1940+
/// Combined with VK_GOOGLE_display_timing (skipping desiredPresentTime in IMMEDIATE mode),
1941+
/// this delivers the lowest possible input-to-display latency while avoiding the
1942+
/// vkAcquireNextImageKHR queue pile-up of Limit2x. The migration runs exactly once per
1943+
/// install (gated by <see cref="OsuSetting.AndroidStartupFrameSyncMigrationApplied"/>)
1944+
/// so a user who later prefers a different mode from Settings → Graphics → Renderer
1945+
/// is not fought on every launch.
18681946
/// </para>
18691947
/// </summary>
18701948
private void applyAndroidFrameSyncMigrationOnce(FrameworkConfigManager frameworkConfig)
@@ -1875,21 +1953,40 @@ private void applyAndroidFrameSyncMigrationOnce(FrameworkConfigManager framework
18751953
if (LocalConfig.Get<bool>(OsuSetting.AndroidStartupFrameSyncMigrationApplied))
18761954
{
18771955
CrashDiagnostics.WriteAliveMarker("applyAndroidFrameSyncMigrationOnce (already applied)");
1956+
1957+
// v2 migration: upgrade users who were previously migrated to VSync (by an older
1958+
// build) to ActualUnlimited. Only applies if:
1959+
// 1. The v2 migration hasn't run yet.
1960+
// 2. The user is currently on VSync (hasn't manually changed it since v1).
1961+
// This gives existing users the lower-latency uncapped mode without overriding
1962+
// deliberate user choices.
1963+
if (!LocalConfig.Get<bool>(OsuSetting.AndroidStartupFrameSyncV2MigrationApplied))
1964+
{
1965+
var frameSync = frameworkConfig.GetBindable<FrameSync>(FrameworkSetting.FrameSync);
1966+
if (frameSync.Value == FrameSync.VSync)
1967+
{
1968+
frameSync.Value = FrameSync.ActualUnlimited;
1969+
Logger.Log("[osu!] Android FrameSync v2 migration: VSync → ActualUnlimited (IMMEDIATE present mode, lower latency)", LoggingTarget.Performance);
1970+
}
1971+
LocalConfig.SetValue(OsuSetting.AndroidStartupFrameSyncV2MigrationApplied, true);
1972+
}
1973+
18781974
return;
18791975
}
18801976

1881-
var frameSync = frameworkConfig.GetBindable<FrameSync>(FrameworkSetting.FrameSync);
1977+
var frameSyncV1 = frameworkConfig.GetBindable<FrameSync>(FrameworkSetting.FrameSync);
18821978

18831979
// Only override the framework default. If the user has already explicitly
18841980
// chosen a different mode (Unlimited / VSync / Custom), respect that —
18851981
// the migration's job is to nudge the *default*, not to overwrite intent.
1886-
if (frameSync.Value == FrameSync.Limit2x)
1982+
if (frameSyncV1.Value == FrameSync.Limit2x)
18871983
{
1888-
frameSync.Value = FrameSync.VSync;
1889-
Logger.Log("[osu!] Android first-launch FrameSync migration: Limit2x → VSync (bounds Vulkan present-queue depth on Adreno)", LoggingTarget.Performance);
1984+
frameSyncV1.Value = FrameSync.ActualUnlimited;
1985+
Logger.Log("[osu!] Android first-launch FrameSync migration: Limit2x → ActualUnlimited (IMMEDIATE present, no vblank stall)", LoggingTarget.Performance);
18901986
}
18911987

18921988
LocalConfig.SetValue(OsuSetting.AndroidStartupFrameSyncMigrationApplied, true);
1989+
LocalConfig.SetValue(OsuSetting.AndroidStartupFrameSyncV2MigrationApplied, true);
18931990
}
18941991
catch (Exception e)
18951992
{
@@ -2342,6 +2439,22 @@ protected override void Dispose(bool isDisposing)
23422439
dexPerformanceSession?.Dispose();
23432440
dexPerformanceSession = null;
23442441

2442+
// Close ADPF hint sessions for game threads.
2443+
try
2444+
{
2445+
if (adpfDrawSession != IntPtr.Zero)
2446+
{
2447+
OboeAudioBridge.nADPFCloseSession(adpfDrawSession);
2448+
adpfDrawSession = IntPtr.Zero;
2449+
}
2450+
if (adpfUpdateSession != IntPtr.Zero)
2451+
{
2452+
OboeAudioBridge.nADPFCloseSession(adpfUpdateSession);
2453+
adpfUpdateSession = IntPtr.Zero;
2454+
}
2455+
}
2456+
catch { }
2457+
23452458
var cst = System.Threading.Interlocked.Exchange(ref coldStartTamingTimer, null);
23462459
try { cst?.Dispose(); }
23472460
catch { /* ignore */ }

osu.Game/Configuration/OsuConfigManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ protected override void InitialiseDefaults()
274274
SetDefault(OsuSetting.AndroidLowLatencyAudio, true);
275275
SetDefault(OsuSetting.AndroidVulkanProbe, false);
276276
SetDefault(OsuSetting.AndroidStartupFrameSyncMigrationApplied, false);
277+
SetDefault(OsuSetting.AndroidStartupFrameSyncV2MigrationApplied, false);
277278

278279
// --- Android startup-safety toggles ---
279280
//
@@ -576,6 +577,7 @@ public enum OsuSetting
576577
AndroidLowLatencyAudio,
577578
AndroidVulkanProbe,
578579
AndroidStartupFrameSyncMigrationApplied,
580+
AndroidStartupFrameSyncV2MigrationApplied,
579581
AndroidCleanupStaleRealmFifos,
580582
AndroidDeferStartupNativeInit,
581583
AndroidStartupFrameSyncMigrationEnabled,

0 commit comments

Comments
 (0)