@@ -170,6 +170,9 @@ public partial class OsuGameAndroid : OsuGame
170170 // their work within the display frame deadline (e.g. 8.33 ms at 120 Hz).
171171 // The sessions are created once after LoadComplete (when thread IDs are stable) and closed on
172172 // Dispose. Target duration is updated whenever the active display refresh rate changes.
173+ // Per-frame actual-duration reporting is done via GameThread.FrameCompleted, which fires on
174+ // the respective game thread at the end of every frame (after clock throttle), giving the
175+ // CPU governor a signal to pre-boost the clock for the next frame.
173176 private IntPtr adpfDrawSession ;
174177 private IntPtr adpfUpdateSession ;
175178
@@ -547,7 +550,13 @@ protected override void LoadComplete()
547550 long targetNs = currentRefreshRate > 0 ? 1_000_000_000L / currentRefreshRate : 8_333_333L ;
548551 adpfDrawSession = OboeAudioBridge . nADPFCreateSession ( targetNs ) ;
549552 if ( adpfDrawSession != IntPtr . Zero )
553+ {
550554 Logger . Log ( $ "[osu!] ADPF session created for Draw thread (target={ targetNs / 1_000_000.0 : F2} ms)", LoggingTarget . Performance ) ;
555+ // Subscribe per-frame reporting now that the session handle is valid.
556+ // FrameCompleted fires on the Draw thread itself, so reading
557+ // Host.DrawThread.Clock.ElapsedFrameTime is thread-safe.
558+ Host ! . DrawThread ! . FrameCompleted += onDrawFrameCompleted ;
559+ }
551560 }
552561 catch { }
553562 } ) ;
@@ -559,7 +568,10 @@ protected override void LoadComplete()
559568 long targetNs = currentRefreshRate > 0 ? 1_000_000_000L / currentRefreshRate : 8_333_333L ;
560569 adpfUpdateSession = OboeAudioBridge . nADPFCreateSession ( targetNs ) ;
561570 if ( adpfUpdateSession != IntPtr . Zero )
571+ {
562572 Logger . Log ( $ "[osu!] ADPF session created for Update thread (target={ targetNs / 1_000_000.0 : F2} ms)", LoggingTarget . Performance ) ;
573+ Host ! . UpdateThread ! . FrameCompleted += onUpdateFrameCompleted ;
574+ }
563575 }
564576 catch { }
565577 } ) ;
@@ -1464,6 +1476,44 @@ private void updateAdpfTargetDuration(long targetNs)
14641476 catch { }
14651477 }
14661478
1479+ /// <summary>
1480+ /// Called by <see cref="osu.Framework.Threading.GameThread.FrameCompleted"/> on the Draw thread.
1481+ /// Reports the actual frame duration to the ADPF session so the CPU governor can adjust clock
1482+ /// frequency for the next frame. <see cref="osu.Framework.Threading.GameThread.Clock"/>
1483+ /// <c>ElapsedFrameTime</c> is in milliseconds; we convert to nanoseconds for the ADPF API.
1484+ /// In <see cref="FrameSync.ActualUnlimited"/> mode there is no throttle sleep, so
1485+ /// ElapsedFrameTime accurately reflects actual GPU+CPU work time.
1486+ /// </summary>
1487+ private void onDrawFrameCompleted ( )
1488+ {
1489+ if ( adpfDrawSession == IntPtr . Zero ) return ;
1490+
1491+ try
1492+ {
1493+ double elapsedMs = Host ? . DrawThread ? . Clock . ElapsedFrameTime ?? 0 ;
1494+ if ( elapsedMs > 0 )
1495+ OboeAudioBridge . nADPFReportActualDuration ( adpfDrawSession , ( long ) ( elapsedMs * 1_000_000.0 ) ) ;
1496+ }
1497+ catch { }
1498+ }
1499+
1500+ /// <summary>
1501+ /// Called by <see cref="osu.Framework.Threading.GameThread.FrameCompleted"/> on the Update thread.
1502+ /// See <see cref="onDrawFrameCompleted"/> for rationale.
1503+ /// </summary>
1504+ private void onUpdateFrameCompleted ( )
1505+ {
1506+ if ( adpfUpdateSession == IntPtr . Zero ) return ;
1507+
1508+ try
1509+ {
1510+ double elapsedMs = Host ? . UpdateThread ? . Clock . ElapsedFrameTime ?? 0 ;
1511+ if ( elapsedMs > 0 )
1512+ OboeAudioBridge . nADPFReportActualDuration ( adpfUpdateSession , ( long ) ( elapsedMs * 1_000_000.0 ) ) ;
1513+ }
1514+ catch { }
1515+ }
1516+
14671517 private global ::Android . Views . Display ? getActiveDisplay ( )
14681518 {
14691519 if ( gameActivity . IsFinishing || gameActivity . IsDestroyed )
@@ -2441,8 +2491,15 @@ protected override void Dispose(bool isDisposing)
24412491 dexPerformanceSession = null ;
24422492
24432493 // Close ADPF hint sessions for game threads.
2494+ // Unsubscribe FrameCompleted FIRST so the callbacks don't fire with a stale
2495+ // (already-closed) session handle during the final frames of teardown.
24442496 try
24452497 {
2498+ if ( Host ? . DrawThread != null )
2499+ Host . DrawThread . FrameCompleted -= onDrawFrameCompleted ;
2500+ if ( Host ? . UpdateThread != null )
2501+ Host . UpdateThread . FrameCompleted -= onUpdateFrameCompleted ;
2502+
24462503 if ( adpfDrawSession != IntPtr . Zero )
24472504 {
24482505 OboeAudioBridge . nADPFCloseSession ( adpfDrawSession ) ;
0 commit comments