Skip to content

Commit 3acf7c6

Browse files
author
Ubuntu
committed
fix: tick native watchdog before SetFormat to prevent black screen
When the surface is born as RGB565 on Vulkan, the RGB565 guard in SurfaceChanged calls SetFormat(RGBA8888) which triggers a synchronous surface teardown on the UI thread. This blocks the UI thread for hundreds of milliseconds, preventing the Update thread from sending heartbeats to the native watchdog. The native watchdog (10s default) then fires, killing the process and producing a black screen. The fix calls NativeWatchdog.Heartbeat() right before SetFormat to reset the watchdog timer from the UI thread. The native watchdog checks g_lastHeartbeatMonotonicSec which is updated by a simple atomic store — safe to call from any thread and never throws. This is safer than the previous approach of setting surfaceEvent before SetFormat, which introduced a data race between the draw thread reading surfaceGlobalRef and SurfaceDestroyed freeing it.
1 parent 90dc644 commit 3acf7c6

1 file changed

Lines changed: 19 additions & 11 deletions

File tree

osu.Android/OsuGameActivity.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
using osu.Framework.Android;
2222
using osu.Game.Database;
2323
using osu.Framework.Logging;
24+
using osu.Android.Native;
2425

2526
namespace osu.Android
2627
{
@@ -899,18 +900,25 @@ public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Forma
899900
Logger.Log(rgb565Message, LoggingTarget.Runtime, LogLevel.Important);
900901
Logger.Log(rgb565Message, LoggingTarget.Performance, LogLevel.Important);
901902

902-
// Set the surface event BEFORE calling SetFormat so the draw thread can
903-
// proceed with the current (soon-to-be-recreated) surface. Without this,
904-
// the draw thread blocks on surfaceEvent.Wait(5000) while SetFormat triggers
905-
// a synchronous surface teardown on the UI thread, blocking the entire
906-
// managed runtime from sending heartbeats. The native watchdog then fires
907-
// at 10s because no managed heartbeat is observed.
908-
// The new surface will trigger another SurfaceChanged which will set
909-
// surfaceEvent again with the correct RGBA8888 format.
910-
if (width > 0 && height > 0)
903+
// Tick the native watchdog BEFORE calling SetFormat. SetFormat triggers a
904+
// synchronous surface teardown on the UI thread that can block for hundreds
905+
// of milliseconds. During this window the Update thread may not get a chance
906+
// to send a heartbeat (especially during startup when the runtime is under
907+
// heavy load), and the native watchdog (10s default) would fire, killing the
908+
// process and producing a black screen.
909+
//
910+
// Calling Heartbeat() here resets the watchdog timer from the UI thread,
911+
// giving SetFormat time to complete without triggering a false positive.
912+
// The native watchdog checks g_lastHeartbeatMonotonicSec which is updated
913+
// by a simple atomic store — safe to call from any thread.
914+
try
915+
{
916+
NativeWatchdog.Heartbeat();
917+
Debug.WriteLine("[osu!] Native watchdog heartbeat ticked before SetFormat(RGBA8888)");
918+
}
919+
catch (Exception e)
911920
{
912-
surfaceEvent.Set();
913-
Debug.WriteLine($"[osu!] Native surface signal set before format change (size: {width}x{height})");
921+
Debug.WriteLine($"[osu!] NativeWatchdog.Heartbeat failed (non-fatal): {e.Message}");
914922
}
915923

916924
try

0 commit comments

Comments
 (0)