@@ -246,25 +246,29 @@ protected override void OnCreate(Bundle? savedInstanceState)
246246
247247 // Stamp RGBA8888 at the Window level BEFORE SDL creates its SurfaceView inside
248248 // base.OnCreate(). Android's default SurfaceView pixel format on many high-density
249- // Samsung / Qualcomm panels is RGB565. SDL3 only calls SurfaceHolder.setFormat(
250- // RGBA8888) for the OpenGL path — the Vulkan path inherits the window default.
251- // Setting the format here, before SDL attaches its SurfaceView, ensures the
252- // SurfaceView is born with RGBA8888 and eliminates the format-change teardown
253- // (SurfaceHolder.SetFormat in DecorView.Post) that otherwise fires mid-Vulkan-init
254- // and can produce the "Draw thread did not acknowledge teardown within 250ms" warning.
255- // The DecorView.Post call and the SurfaceChanged reactive guard are retained as
256- // belt-and-braces fallbacks for timing windows or OEM variants where this hint is
257- // not honoured by the SurfaceView allocation path.
258- if ( LogManagement . IsVulkanConfigured ( ) )
249+ // Samsung / Qualcomm panels is RGB565. Setting RGBA8888 here (before SDL attaches
250+ // its SurfaceView) ensures the SurfaceView is born with full 32-bit colour in both
251+ // Vulkan and OpenGL modes:
252+ // - Vulkan: the Veldrid swapchain can request VK_FORMAT_R8G8B8A8_SRGB / BGRA8888
253+ // directly, but the underlying ANativeWindow must also support RGBA8888 — a
254+ // Window born at RGB565 forces a surface teardown (and the
255+ // "Draw thread did not acknowledge teardown within 250ms" warning) when Veldrid
256+ // later calls ANativeWindow_setBuffersGeometry with RGBA8888.
257+ // - OpenGL safe-mode (after a Vulkan crash): SDL3 does call
258+ // SurfaceHolder.setFormat(RGBA8888) for EGL surfaces, but it only does so AFTER
259+ // the SurfaceView is created. Pre-stamping the Window format here guarantees
260+ // the initial SurfaceView allocation happens at RGBA8888, avoiding a brief
261+ // RGB565 render pass that can leave colour-channel artefacts visible in the
262+ // first few frames.
263+ // Belt-and-braces fallbacks (DecorView.Post watcher, SurfaceChanged reactive guard)
264+ // are retained for OEM variants where this Window-level hint is not honoured.
265+ try
259266 {
260- try
261- {
262- Window ? . SetFormat ( global ::Android . Graphics . Format . Rgba8888 ) ;
263- }
264- catch ( Exception e )
265- {
266- Debug . WriteLine ( $ "[osu!] Pre-SDL Window.SetFormat(RGBA8888) failed (non-fatal): { e . Message } ") ;
267- }
267+ Window ? . SetFormat ( global ::Android . Graphics . Format . Rgba8888 ) ;
268+ }
269+ catch ( Exception e )
270+ {
271+ Debug . WriteLine ( $ "[osu!] Pre-SDL Window.SetFormat(RGBA8888) failed (non-fatal): { e . Message } ") ;
268272 }
269273
270274 // BASS AAudio: if the user opted in, tell BASS to open an AAudio device instead
@@ -963,6 +967,75 @@ public void SurfaceDestroyed(ISurfaceHolder holder)
963967 }
964968 }
965969
970+ protected override void OnPause ( )
971+ {
972+ // Root cause of the recurring Vulkan IMMEDIATE-mode ANR (process-runtime ~50s):
973+ //
974+ // 1. Samsung Game Booster (or any surface-lifecycle event) fires onPause() on the
975+ // Java main thread at ~50 seconds of active Vulkan gameplay.
976+ // 2. SDL3's native onPause() sends SDL_EVENT_DID_ENTER_BACKGROUND through its event
977+ // filter synchronously on the calling (Java main) thread.
978+ // 3. The event filter calls Window.Suspended → GameHost.Suspend() →
979+ // ThreadRunner.Suspend() → DrawThread.Pause() → WaitForState(Paused).
980+ // 4. WaitForState spins: `while (state != Paused) Thread.Sleep(1)` — NO TIMEOUT.
981+ // 5. The draw thread is stuck inside vkQueuePresentKHR (Vulkan IMMEDIATE mode;
982+ // FrameSync=ActualUnlimited) due to an Adreno 7xx driver stall. It can only
983+ // check pauseRequested at the START of the next frame — which never comes.
984+ // 6. Java main thread spins forever → input dispatching times out after 10s → ANR.
985+ //
986+ // Fix: watchdog the OnPause() call. If base.OnPause() hasn't returned within 7 seconds
987+ // (leaving a 3-second margin before the 10-second ANR), the draw thread is conclusively
988+ // stuck in the driver. Kill the process immediately for a clean restart rather than a
989+ // frozen 10-second ANR.
990+ //
991+ // We intentionally do NOT set FLAG_STARTUP_IN_PROGRESS (safe-mode) before killing.
992+ // The startup completed successfully; this is a mid-session driver hang triggered by
993+ // a transient system event (Game Booster first-session overlay). The next launch will
994+ // retry Vulkan normally. Safe-mode is reserved for launch-time hangs where the renderer
995+ // itself cannot initialize.
996+ //
997+ // Only active for Vulkan: OpenGL's eglSwapBuffers cannot stall indefinitely in the way
998+ // vkQueuePresentKHR can, so OpenGL sessions are not at risk of this ANR pattern.
999+ if ( LogManagement . IsVulkanConfigured ( ) )
1000+ {
1001+ var pauseCompleted = new ManualResetEventSlim ( false ) ;
1002+
1003+ ThreadPool . QueueUserWorkItem ( _ =>
1004+ {
1005+ const int watchdog_ms = 7000 ;
1006+
1007+ if ( pauseCompleted . Wait ( watchdog_ms ) )
1008+ return ;
1009+
1010+ // base.OnPause() has not returned — draw thread is conclusively stuck in
1011+ // vkQueuePresentKHR. Write a diagnostic marker and kill cleanly.
1012+ try
1013+ {
1014+ CrashDiagnostics . WriteAliveMarker (
1015+ $ "OnPause watchdog fired after { watchdog_ms } ms: draw thread stuck in vkQueuePresentKHR (Vulkan IMMEDIATE ANR). Killing for clean restart.") ;
1016+ }
1017+ catch { }
1018+
1019+ try
1020+ {
1021+ Debug . WriteLine (
1022+ "[osu!] OnPause watchdog: draw thread stuck in vkQueuePresentKHR >7s — killing for clean Vulkan restart." ) ;
1023+ }
1024+ catch { }
1025+
1026+ try { global ::Android . OS . Process . KillProcess ( global ::Android . OS . Process . MyPid ( ) ) ; }
1027+ catch { }
1028+ } ) ;
1029+
1030+ base . OnPause ( ) ;
1031+ pauseCompleted . Set ( ) ;
1032+ }
1033+ else
1034+ {
1035+ base . OnPause ( ) ;
1036+ }
1037+ }
1038+
9661039 public override void OnConfigurationChanged ( global ::Android . Content . Res . Configuration newConfig )
9671040 {
9681041 base . OnConfigurationChanged ( newConfig ) ;
0 commit comments