Skip to content

Commit 0d1c8cd

Browse files
fix: remove redundant android adpf thread reporting
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/b363e225-1c7b-4b79-9b10-affbb8444d57 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent dddab34 commit 0d1c8cd

3 files changed

Lines changed: 0 additions & 309 deletions

File tree

osu.Android/Native/OboeAudioBridge.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,6 @@ public void Dispose()
226226
[DllImport(lib_name)] internal static extern byte nSetThreadAffinity(int coreMask);
227227
[DllImport(lib_name)] internal static extern int nGetBigCoreMask();
228228
[DllImport(lib_name)] internal static extern int nTameBackgroundThreads(int littleCoreMask);
229-
[DllImport(lib_name)] internal static extern IntPtr nADPFCreateSession(long targetDurationNanos);
230-
[DllImport(lib_name)] internal static extern void nADPFReportActualDuration(IntPtr sessionPtr, long actualDurationNanos);
231-
[DllImport(lib_name)] internal static extern void nADPFUpdateTargetDuration(IntPtr sessionPtr, long targetDurationNanos);
232-
[DllImport(lib_name)] internal static extern void nADPFCloseSession(IntPtr sessionPtr);
233-
[DllImport(lib_name)] internal static extern void nADPFSetPreferPowerEfficiency(IntPtr sessionPtr, byte preferEfficiency);
234229
[DllImport(lib_name)] internal static extern void nInstallCrashHandler([MarshalAs(UnmanagedType.LPUTF8Str)] string? logPath);
235230
[DllImport(lib_name)] internal static extern void nReinstallCrashHandler();
236231
}

osu.Android/Native/oboe_bridge.cpp

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -801,65 +801,3 @@ OSU_EXPORT int nTameBackgroundThreads(int little_core_mask) {
801801
return demoted;
802802
}
803803
}
804-
805-
#include <android/performance_hint.h>
806-
// Explicit dependency for gettid() used in nADPFCreateSession below — do not
807-
// rely on transitive includes from Oboe / NDK headers, which may change.
808-
#include <unistd.h>
809-
810-
extern "C" {
811-
OSU_EXPORT intptr_t nADPFCreateSession(int64_t targetDurationNanos) {
812-
auto manager = APerformanceHint_getManager();
813-
if (!manager) return 0;
814-
815-
int32_t thread_id = gettid();
816-
return reinterpret_cast<intptr_t>(APerformanceHint_createSession(manager, &thread_id, 1, targetDurationNanos));
817-
}
818-
819-
OSU_EXPORT void nADPFReportActualDuration(intptr_t sessionPtr, int64_t actualDurationNanos) {
820-
if (sessionPtr) {
821-
APerformanceHint_reportActualWorkDuration(reinterpret_cast<APerformanceHintSession*>(sessionPtr), actualDurationNanos);
822-
}
823-
}
824-
825-
OSU_EXPORT void nADPFUpdateTargetDuration(intptr_t sessionPtr, int64_t targetDurationNanos) {
826-
if (sessionPtr) {
827-
APerformanceHint_updateTargetWorkDuration(reinterpret_cast<APerformanceHintSession*>(sessionPtr), targetDurationNanos);
828-
}
829-
}
830-
831-
OSU_EXPORT void nADPFCloseSession(intptr_t sessionPtr) {
832-
if (sessionPtr) {
833-
APerformanceHint_closeSession(reinterpret_cast<APerformanceHintSession*>(sessionPtr));
834-
}
835-
}
836-
837-
// APerformanceHintSession_setPreferPowerEfficiency was added in Android 15 (API 35).
838-
// We resolve it via dlsym so the .so continues to load on Android 12–14 without
839-
// linker errors. On API 35+ devices (Pixel 9, Galaxy S25 series) this tells the
840-
// CPU governor to prioritise performance headroom over efficiency for these threads,
841-
// giving an additional ~5% clock-frequency boost at the cost of slightly higher
842-
// power draw — acceptable for an interactive game session.
843-
// preferEfficiency=0 → prefer performance (disable power-efficiency bias).
844-
//
845-
// P/Invoke boundary uses `byte` instead of `bool` because Mono's P/Invoke marshaller
846-
// does not guarantee a 1-byte ABI for C++ `bool`. The native side receives a `byte`
847-
// and converts it via `preferEfficiency != 0` before forwarding to the API's `bool` param.
848-
OSU_EXPORT void nADPFSetPreferPowerEfficiency(intptr_t sessionPtr, byte preferEfficiency) {
849-
if (!sessionPtr) return;
850-
851-
typedef void (*SetPreferPowerEfficiencyFn)(APerformanceHintSession*, bool);
852-
static SetPreferPowerEfficiencyFn fn = nullptr;
853-
static bool resolved = false;
854-
855-
if (!resolved) {
856-
fn = reinterpret_cast<SetPreferPowerEfficiencyFn>(
857-
dlsym(RTLD_DEFAULT, "APerformanceHintSession_setPreferPowerEfficiency"));
858-
resolved = true;
859-
}
860-
861-
if (fn) {
862-
fn(reinterpret_cast<APerformanceHintSession*>(sessionPtr), preferEfficiency != 0);
863-
}
864-
}
865-
}

osu.Android/OsuGameAndroid.cs

Lines changed: 0 additions & 242 deletions
Original file line numberDiff line numberDiff line change
@@ -168,37 +168,6 @@ public partial class OsuGameAndroid : OsuGame
168168
private global::Android.Content.PM.ScreenOrientation? lastRequestedOrientation;
169169
private int currentRefreshRate;
170170

171-
// ADPF (Android Dynamic Performance Framework) hint sessions for the Draw, Update, and Input threads.
172-
// These tell the CPU scheduler to boost the clock frequency so game-loop threads can complete
173-
// their work within the display frame deadline (e.g. 8.33 ms at 120 Hz).
174-
// The sessions are created once after LoadComplete (when thread IDs are stable) and closed on
175-
// Dispose. Target duration is updated whenever the active display refresh rate changes.
176-
// Per-frame actual-duration reporting is done via GameThread.FrameCompleted, which fires on
177-
// the respective game thread at the end of every frame (after clock throttle), giving the
178-
// CPU governor a signal to pre-boost the clock for the next frame.
179-
private IntPtr adpfDrawSession;
180-
private IntPtr adpfUpdateSession;
181-
182-
// Cached GameThread references stored when ADPF sessions are created. Avoids walking
183-
// Host?.DrawThread? / Host?.UpdateThread? / Host?.InputThread? on every frame callback
184-
// (3 levels of nullable dereference at 120 Hz × 3 threads = 360 null checks/s → 0).
185-
private osu.Framework.Threading.GameThread? adpfDrawThread;
186-
private osu.Framework.Threading.GameThread? adpfUpdateThread;
187-
private osu.Framework.Threading.GameThread? adpfInputThread;
188-
189-
// Input thread ADPF session. The input thread may poll at ~100 kHz (one cycle ≈ 10 µs),
190-
// which is far finer than the ADPF reporting granularity. Instead of calling
191-
// reportActualWorkDuration once per poll (which would flood the ADPF API), we accumulate
192-
// total input-processing time and report once per display frame (~8 ms at 120 Hz).
193-
// The session target is set to the display frame period, which is the correct deadline:
194-
// "input for the next rendered frame must be ready within one frame interval".
195-
private IntPtr adpfInputSession;
196-
private double inputAdpfAccumulatedMs;
197-
private long inputAdpfLastReportMs; // Environment.TickCount64 ms timestamp of last ADPF report
198-
// Cached display-frame period in ms. Recomputed in applyDisplayMode so the hot
199-
// input callback (which can fire at ~100 kHz) reads a plain field, not Math.Round.
200-
private long adpfInputIntervalMs = 8L;
201-
202171
// One-shot System.Threading.Timer that runs a burst of background-thread taming passes
203172
// when the user transitions into active gameplay. Cancelled and replaced on each new
204173
// gameplay entry so repeated pause/resume cycles don't stack timers.
@@ -583,89 +552,6 @@ protected override void LoadComplete()
583552
});
584553
}
585554

586-
// ADPF (Android Dynamic Performance Framework) hint sessions for Draw + Update threads.
587-
// These hint sessions tell the CPU governor "these threads need to finish their work
588-
// within one display-frame interval". The kernel then pre-boosts the CPU frequency
589-
// so the threads don't stall mid-frame waiting for a slow core to spin up.
590-
//
591-
// Target duration = 1 / displayRefreshRate. We default to 120 Hz (8.33 ms) and
592-
// update the target when the display refresh rate is confirmed by applyDisplayMode.
593-
//
594-
// nADPFCreateSession() captures gettid() of the *calling* thread, so each Add
595-
// lambda must run on its respective game thread to register the correct TID.
596-
Scheduler.Add(() =>
597-
{
598-
try
599-
{
600-
Host?.DrawThread?.Scheduler.Add(() =>
601-
{
602-
try
603-
{
604-
long targetNs = currentRefreshRate > 0 ? 1_000_000_000L / currentRefreshRate : 8_333_333L;
605-
adpfDrawSession = OboeAudioBridge.nADPFCreateSession(targetNs);
606-
if (adpfDrawSession != IntPtr.Zero)
607-
{
608-
// API 35 (Android 15)+: disable power-efficiency bias so the
609-
// CPU governor targets performance headroom for this thread.
610-
// Silent no-op on API < 35 (resolved via dlsym).
611-
OboeAudioBridge.nADPFSetPreferPowerEfficiency(adpfDrawSession, 0);
612-
Logger.Log($"[osu!] ADPF session created for Draw thread (target={targetNs / 1_000_000.0:F2}ms)", LoggingTarget.Performance);
613-
// Cache the thread reference now (we're already on the Draw thread)
614-
// so per-frame callbacks avoid the Host?.DrawThread? nullable chain.
615-
adpfDrawThread = Host!.DrawThread;
616-
// Subscribe per-frame reporting now that the session handle is valid.
617-
// FrameCompleted fires on the Draw thread itself, so reading
618-
// Host.DrawThread.Clock.ElapsedFrameTime is thread-safe.
619-
Host!.DrawThread!.FrameCompleted += onDrawFrameCompleted;
620-
}
621-
}
622-
catch { }
623-
});
624-
625-
Host?.UpdateThread?.Scheduler.Add(() =>
626-
{
627-
try
628-
{
629-
long targetNs = currentRefreshRate > 0 ? 1_000_000_000L / currentRefreshRate : 8_333_333L;
630-
adpfUpdateSession = OboeAudioBridge.nADPFCreateSession(targetNs);
631-
if (adpfUpdateSession != IntPtr.Zero)
632-
{
633-
OboeAudioBridge.nADPFSetPreferPowerEfficiency(adpfUpdateSession, 0);
634-
Logger.Log($"[osu!] ADPF session created for Update thread (target={targetNs / 1_000_000.0:F2}ms)", LoggingTarget.Performance);
635-
adpfUpdateThread = Host!.UpdateThread;
636-
Host!.UpdateThread!.FrameCompleted += onUpdateFrameCompleted;
637-
}
638-
}
639-
catch { }
640-
});
641-
642-
// Input thread ADPF session. The input thread runs at up to ~100 kHz
643-
// on high-end devices (one poll ≈ 10 µs) — far too fast to call
644-
// reportActualWorkDuration on every cycle. We register the session
645-
// here (capturing the Input thread's TID via gettid()) and let
646-
// onInputFrameCompleted accumulate and report once per display frame.
647-
Host?.InputThread?.Scheduler.Add(() =>
648-
{
649-
try
650-
{
651-
long targetNs = currentRefreshRate > 0 ? 1_000_000_000L / currentRefreshRate : 8_333_333L;
652-
adpfInputSession = OboeAudioBridge.nADPFCreateSession(targetNs);
653-
if (adpfInputSession != IntPtr.Zero)
654-
{
655-
OboeAudioBridge.nADPFSetPreferPowerEfficiency(adpfInputSession, 0);
656-
Logger.Log($"[osu!] ADPF session created for Input thread (target={targetNs / 1_000_000.0:F2}ms)", LoggingTarget.Performance);
657-
adpfInputThread = Host!.InputThread;
658-
Host!.InputThread!.FrameCompleted += onInputFrameCompleted;
659-
}
660-
}
661-
catch { }
662-
});
663-
}
664-
catch (Exception e)
665-
{
666-
Debug.WriteLine($"[osu!] Failed to enqueue ADPF session creation: {e.Message}");
667-
}
668-
});
669555
}
670556
catch (Exception e)
671557
{
@@ -1537,8 +1423,6 @@ private void applyDisplayMode(global::Android.Views.Display display, global::And
15371423
try
15381424
{
15391425
currentRefreshRate = (int)mode.RefreshRate;
1540-
// Cache the interval so the input hot-path avoids Math.Round on every poll.
1541-
adpfInputIntervalMs = currentRefreshRate > 0 ? (long)Math.Round(1000.0 / currentRefreshRate) : 8L;
15421426

15431427
// Request the refresh rate via Surface.setFrameRate() ONLY.
15441428
//
@@ -1571,14 +1455,6 @@ private void applyDisplayMode(global::Android.Views.Display display, global::And
15711455
}
15721456

15731457
Logger.Log($"[osu!] Display mode applied: {mode.RefreshRate}Hz (mode {mode.ModeId}, {mode.PhysicalWidth}x{mode.PhysicalHeight})", LoggingTarget.Performance);
1574-
1575-
// Update ADPF target duration to match the new display refresh rate.
1576-
// This keeps the CPU governor hint aligned with the actual frame deadline.
1577-
if (mode.RefreshRate > 0)
1578-
{
1579-
long targetNs = (long)(1_000_000_000.0 / mode.RefreshRate);
1580-
updateAdpfTargetDuration(targetNs);
1581-
}
15821458
}
15831459
catch (Exception e)
15841460
{
@@ -1587,94 +1463,6 @@ private void applyDisplayMode(global::Android.Views.Display display, global::And
15871463
});
15881464
}
15891465

1590-
/// <summary>
1591-
/// Updates the target work duration on all three ADPF hint sessions (Draw, Update, and Input thread)
1592-
/// so the CPU governor can pre-boost each thread to meet the new frame deadline.
1593-
/// </summary>
1594-
private void updateAdpfTargetDuration(long targetNs)
1595-
{
1596-
try
1597-
{
1598-
if (adpfDrawSession != IntPtr.Zero)
1599-
OboeAudioBridge.nADPFUpdateTargetDuration(adpfDrawSession, targetNs);
1600-
if (adpfUpdateSession != IntPtr.Zero)
1601-
OboeAudioBridge.nADPFUpdateTargetDuration(adpfUpdateSession, targetNs);
1602-
if (adpfInputSession != IntPtr.Zero)
1603-
OboeAudioBridge.nADPFUpdateTargetDuration(adpfInputSession, targetNs);
1604-
}
1605-
catch { }
1606-
}
1607-
1608-
/// <summary>
1609-
/// Called by <see cref="osu.Framework.Threading.GameThread.FrameCompleted"/> on the Draw thread.
1610-
/// Reports the actual frame duration to the ADPF session so the CPU governor can adjust clock
1611-
/// frequency for the next frame. <see cref="osu.Framework.Threading.GameThread.Clock"/>
1612-
/// <c>ElapsedFrameTime</c> is in milliseconds; we convert to nanoseconds for the ADPF API.
1613-
/// In <see cref="FrameSync.ActualUnlimited"/> mode there is no throttle sleep, so
1614-
/// ElapsedFrameTime accurately reflects actual GPU+CPU work time.
1615-
/// </summary>
1616-
private void onDrawFrameCompleted()
1617-
{
1618-
if (adpfDrawSession == IntPtr.Zero) return;
1619-
1620-
try
1621-
{
1622-
double elapsedMs = adpfDrawThread?.Clock.ElapsedFrameTime ?? 0;
1623-
if (elapsedMs > 0)
1624-
OboeAudioBridge.nADPFReportActualDuration(adpfDrawSession, (long)(elapsedMs * 1_000_000.0));
1625-
}
1626-
catch { }
1627-
}
1628-
1629-
/// <summary>
1630-
/// Called by <see cref="osu.Framework.Threading.GameThread.FrameCompleted"/> on the Update thread.
1631-
/// See <see cref="onDrawFrameCompleted"/> for rationale.
1632-
/// </summary>
1633-
private void onUpdateFrameCompleted()
1634-
{
1635-
if (adpfUpdateSession == IntPtr.Zero) return;
1636-
1637-
try
1638-
{
1639-
double elapsedMs = adpfUpdateThread?.Clock.ElapsedFrameTime ?? 0;
1640-
if (elapsedMs > 0)
1641-
OboeAudioBridge.nADPFReportActualDuration(adpfUpdateSession, (long)(elapsedMs * 1_000_000.0));
1642-
}
1643-
catch { }
1644-
}
1645-
1646-
/// <summary>
1647-
/// Called by <see cref="osu.Framework.Threading.GameThread.FrameCompleted"/> on the Input thread.
1648-
/// The input thread may poll at ~100 kHz so we accumulate total CPU time across many polls
1649-
/// and report to ADPF at most once per display-frame interval (~8 ms at 120 Hz).
1650-
/// This avoids flooding the ADPF API while still giving the CPU governor a meaningful
1651-
/// signal about how heavily the input core is loaded.
1652-
/// </summary>
1653-
private void onInputFrameCompleted()
1654-
{
1655-
if (adpfInputSession == IntPtr.Zero) return;
1656-
1657-
try
1658-
{
1659-
double elapsedMs = adpfInputThread?.Clock.ElapsedFrameTime ?? 0;
1660-
if (elapsedMs <= 0) return;
1661-
1662-
inputAdpfAccumulatedMs += elapsedMs;
1663-
1664-
// Report once per display frame period. `adpfInputIntervalMs` is pre-computed
1665-
// in applyDisplayMode() so this callback reads a plain field instead of calling Math.Round.
1666-
long nowMs = System.Environment.TickCount64;
1667-
1668-
if (nowMs - inputAdpfLastReportMs >= adpfInputIntervalMs)
1669-
{
1670-
OboeAudioBridge.nADPFReportActualDuration(adpfInputSession, (long)(inputAdpfAccumulatedMs * 1_000_000.0));
1671-
inputAdpfAccumulatedMs = 0;
1672-
inputAdpfLastReportMs = nowMs;
1673-
}
1674-
}
1675-
catch { }
1676-
}
1677-
16781466
/// <summary>
16791467
/// Signals to the Android GameStateManager (API 33) that this activity has
16801468
/// transitioned into a new game state.
@@ -2887,36 +2675,6 @@ protected override void Dispose(bool isDisposing)
28872675
dexPerformanceSession?.Dispose();
28882676
dexPerformanceSession = null;
28892677

2890-
// Close ADPF hint sessions for game threads.
2891-
// Unsubscribe FrameCompleted FIRST so the callbacks don't fire with a stale
2892-
// (already-closed) session handle during the final frames of teardown.
2893-
try
2894-
{
2895-
if (Host?.DrawThread != null)
2896-
Host.DrawThread.FrameCompleted -= onDrawFrameCompleted;
2897-
if (Host?.UpdateThread != null)
2898-
Host.UpdateThread.FrameCompleted -= onUpdateFrameCompleted;
2899-
if (Host?.InputThread != null)
2900-
Host.InputThread.FrameCompleted -= onInputFrameCompleted;
2901-
2902-
if (adpfDrawSession != IntPtr.Zero)
2903-
{
2904-
OboeAudioBridge.nADPFCloseSession(adpfDrawSession);
2905-
adpfDrawSession = IntPtr.Zero;
2906-
}
2907-
if (adpfUpdateSession != IntPtr.Zero)
2908-
{
2909-
OboeAudioBridge.nADPFCloseSession(adpfUpdateSession);
2910-
adpfUpdateSession = IntPtr.Zero;
2911-
}
2912-
if (adpfInputSession != IntPtr.Zero)
2913-
{
2914-
OboeAudioBridge.nADPFCloseSession(adpfInputSession);
2915-
adpfInputSession = IntPtr.Zero;
2916-
}
2917-
}
2918-
catch { }
2919-
29202678
var cst = System.Threading.Interlocked.Exchange(ref coldStartTamingTimer, null);
29212679
try { cst?.Dispose(); }
29222680
catch { /* ignore */ }

0 commit comments

Comments
 (0)