@@ -522,6 +522,11 @@ protected override void OnCreate(Bundle? savedInstanceState)
522522 }
523523
524524 CrashDiagnostics . WriteAliveMarker ( "Activity.OnCreate exit" ) ;
525+
526+ // Arm the general Vulkan UI-thread watchdog LAST in OnCreate, after all
527+ // surface setup and base initialisation is complete. The 7-second initial
528+ // sleep in the watchdog thread provides additional grace.
529+ startVulkanUiWatchdog ( ) ;
525530 }
526531
527532 protected override void OnNewIntent ( Intent ? intent ) => handleIntent ( intent ) ;
@@ -967,6 +972,138 @@ public void SurfaceDestroyed(ISurfaceHolder holder)
967972 }
968973 }
969974
975+ /// <summary>
976+ /// Arms a background watchdog that monitors the Java main thread (UI thread) for
977+ /// responsiveness during Vulkan sessions.
978+ ///
979+ /// <para>
980+ /// Root cause this addresses: SDL3 on Android serialises certain surface-lifecycle
981+ /// and window-focus events (surfaceDestroyed, nativeWindowFocusChanged, …) through
982+ /// an internal Java <c>synchronized</c> block that blocks the Java main thread until
983+ /// the SDL game/draw thread acknowledges the event. If the draw thread is stuck
984+ /// inside a long kernel GPU driver call (e.g. <c>vkQueuePresentKHR</c> on an
985+ /// Adreno 7xx under high load — visible as 70–80% kernel CPU time), the
986+ /// acknowledgement is delayed and the main thread can block for the full 10-second
987+ /// Android ANR window. The Samsung Game Optimizing Service (GOS) toolbar, ADPF
988+ /// display-mode changes, and other system overlays are typical triggers around
989+ /// 25–35 s of gameplay.
990+ /// </para>
991+ ///
992+ /// <para>
993+ /// The <see cref="OnPause"/> watchdog already covers the <c>OnPause</c> path.
994+ /// This watchdog covers every other blocking entry point on the UI thread by
995+ /// posting a 1-second repeating no-op to the main looper and measuring how long
996+ /// ago the last no-op was executed. If the gap exceeds 7 seconds (leaving a 3-
997+ /// second margin before the 10-second ANR deadline), the process is killed for a
998+ /// clean restart rather than an ANR.
999+ /// </para>
1000+ ///
1001+ /// <para>
1002+ /// Safe-mode is deliberately NOT set: this watchdog fires during mid-session
1003+ /// driver stalls, not at startup. The next launch should retry Vulkan normally.
1004+ /// </para>
1005+ /// </summary>
1006+ private void startVulkanUiWatchdog ( )
1007+ {
1008+ if ( ! LogManagement . IsVulkanConfigured ( ) )
1009+ return ;
1010+
1011+ const int watchdog_threshold_ms = 7000 ;
1012+ const int ping_interval_ms = 1000 ;
1013+
1014+ global ::Android . OS . Handler ? pingHandler ;
1015+
1016+ try
1017+ {
1018+ pingHandler = new global ::Android . OS . Handler ( global ::Android . OS . Looper . MainLooper ! ) ;
1019+ }
1020+ catch ( Exception e )
1021+ {
1022+ Debug . WriteLine ( $ "[osu!] VulkanUiWatchdog: failed to create Handler — watchdog disabled ({ e . Message } )") ;
1023+ return ;
1024+ }
1025+
1026+ // Shared field written by the UI-thread pong and read by the watchdog thread.
1027+ // Interlocked/Volatile access: the pong runs on the UI thread, the reader runs
1028+ // on the watchdog thread. The field is a reference so it can be captured by
1029+ // both lambdas without a ref capture.
1030+ long [ ] lastPongMonotonicMs = { System . Environment . TickCount64 } ;
1031+
1032+ // Self-rescheduling pong: posts itself every ping_interval_ms on the UI thread.
1033+ // Capturing pingHandler via a local ref rather than the outer variable so the
1034+ // lambda holds no hard reference to the activity (GC-safety: the Handler is
1035+ // bound to the process-lifetime main looper, not to this activity instance).
1036+ Action ? pong = null ;
1037+ pong = ( ) =>
1038+ {
1039+ System . Threading . Volatile . Write ( ref lastPongMonotonicMs [ 0 ] , System . Environment . TickCount64 ) ;
1040+
1041+ try { pingHandler . PostDelayed ( pong ! , ping_interval_ms ) ; }
1042+ catch { /* handler gone (process teardown) — stop rescheduling */ }
1043+ } ;
1044+
1045+ try
1046+ {
1047+ pingHandler . Post ( pong ) ;
1048+ }
1049+ catch ( Exception e )
1050+ {
1051+ Debug . WriteLine ( $ "[osu!] VulkanUiWatchdog: failed to post initial pong — watchdog disabled ({ e . Message } )") ;
1052+ return ;
1053+ }
1054+
1055+ var watchdog = new System . Threading . Thread ( ( ) =>
1056+ {
1057+ // Initial grace period equal to the threshold: the first pong may not
1058+ // have executed yet, and early-startup UI-thread work (surface format
1059+ // stamp, DecorView.Post lambda, Samsung Game Launcher broadcast) all
1060+ // runs during this window.
1061+ System . Threading . Thread . Sleep ( watchdog_threshold_ms ) ;
1062+
1063+ while ( true )
1064+ {
1065+ long age = System . Environment . TickCount64 - System . Threading . Volatile . Read ( ref lastPongMonotonicMs [ 0 ] ) ;
1066+
1067+ if ( age > watchdog_threshold_ms )
1068+ {
1069+ // UI thread has not executed the pong for > 7 s. The draw thread
1070+ // is conclusively stuck in vkQueuePresentKHR (or a similar SDL
1071+ // surface-event wait), holding the Java main thread. Kill now to
1072+ // avoid the impending ANR and let the user's launcher restart cleanly.
1073+ try
1074+ {
1075+ CrashDiagnostics . WriteAliveMarker (
1076+ $ "VulkanUiWatchdog fired after { age } ms: "
1077+ + "main thread blocked in SDL surface-event wait — "
1078+ + "draw thread stuck in vkQueuePresentKHR (Vulkan KGSL stall). "
1079+ + "Killing for clean restart rather than Android ANR." ) ;
1080+ }
1081+ catch { }
1082+
1083+ try
1084+ {
1085+ Debug . WriteLine (
1086+ $ "[osu!] VulkanUiWatchdog: main thread blocked { age } ms >7s — killing for clean Vulkan restart.") ;
1087+ }
1088+ catch { }
1089+
1090+ try { global ::Android . OS . Process . KillProcess ( global ::Android . OS . Process . MyPid ( ) ) ; }
1091+ catch { }
1092+
1093+ return ;
1094+ }
1095+
1096+ System . Threading . Thread . Sleep ( ping_interval_ms ) ;
1097+ }
1098+ } )
1099+ {
1100+ IsBackground = true ,
1101+ Name = "VulkanUiWatchdog" ,
1102+ } ;
1103+
1104+ watchdog . Start ( ) ;
1105+ }
1106+
9701107 protected override void OnPause ( )
9711108 {
9721109 // Root cause of the recurring Vulkan IMMEDIATE-mode ANR (process-runtime ~50s):
0 commit comments