@@ -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