From f468cc5c89b4fd8d958715fa45937ea7f44fcbe5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 02:29:02 +0000 Subject: [PATCH 1/4] Fix Vulkan 9-screen tiling and flashing textures on Android (Samsung One UI / Adreno 740) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: Window.SetSustainedPerformanceMode(true) triggered a Samsung One UI display-mode transition that recreated the SurfaceView as RGB565. The reactive SurfaceChanged guard called SetFormat(RGBA8888) causing a second teardown. During that teardown, the ANativeWindow transiently reported scaled (dp) dimensions (1029×480 on a 3088×1440 3×-density panel). Veldrid baked those as the permanent swapchain size → 3×3 tiling, blurry textures, and ~40fps. Fixes: - Remove SetSustainedPerformanceMode(true) entirely; ADPF is covered by Oboe's setPerformanceHintEnabled and GC latency by AndroidHighPerformanceSessionManager - Add proactive Window.SetFormat(RGBA8888) before base.OnCreate() so SDL's SurfaceView is born with RGBA8888, eliminating the startup teardown - Upgrade reactive SurfaceChanged guard to also log to Runtime (not just Performance) so any future mid-session RGB565 resets are immediately visible - Update all stale comments that referenced SustainedPerformanceMode Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/3c1fa36e-ab75-466a-9437-b218eeb9fa8e Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android/OsuGameActivity.cs | 30 ++++++++++++++++ osu.Android/OsuGameAndroid.cs | 65 ++++++++++++++-------------------- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 89d3941afc4f..fb6aa9c7bfaa 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -225,6 +225,29 @@ protected override void OnCreate(Bundle? savedInstanceState) LogManagement.WipeShaderCacheOnceForVersion(); CrashDiagnostics.WriteAliveMarker("LogManagement.WipeShaderCacheOnceForVersion (returned)"); + // Stamp RGBA8888 at the Window level BEFORE SDL creates its SurfaceView inside + // base.OnCreate(). Android's default SurfaceView pixel format on many high-density + // Samsung / Qualcomm panels is RGB565. SDL3 only calls SurfaceHolder.setFormat( + // RGBA8888) for the OpenGL path — the Vulkan path inherits the window default. + // Setting the format here, before SDL attaches its SurfaceView, ensures the + // SurfaceView is born with RGBA8888 and eliminates the format-change teardown + // (SurfaceHolder.SetFormat in DecorView.Post) that otherwise fires mid-Vulkan-init + // and can produce the "Draw thread did not acknowledge teardown within 250ms" warning. + // The DecorView.Post call and the SurfaceChanged reactive guard are retained as + // belt-and-braces fallbacks for timing windows or OEM variants where this hint is + // not honoured by the SurfaceView allocation path. + if (LogManagement.IsVulkanConfigured()) + { + try + { + Window?.SetFormat(global::Android.Graphics.Format.Rgba8888); + } + catch (Exception e) + { + Debug.WriteLine($"[osu!] Pre-SDL Window.SetFormat(RGBA8888) failed (non-fatal): {e.Message}"); + } + } + base.OnCreate(savedInstanceState); // Wrap Platform.Init defensively: MAUI Essentials pulls in workload-version-sensitive @@ -660,6 +683,13 @@ public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Forma // picks up the new ANativeWindow and negotiates a proper BGRA/RGBA 8-bit swapchain. if (format == global::Android.Graphics.Format.Rgb565 && LogManagement.IsVulkanConfigured()) { + Logger.Log( + "[osu!] Android surface pixel format RGB565 detected mid-session (Vulkan path). " + + "Requesting RGBA8888 and triggering a surface recreate. " + + "If this fires after startup it indicates an OEM display-mode change " + + "(e.g. SetSustainedPerformanceMode) reset the surface format.", + LoggingTarget.Runtime, + LogLevel.Important); Logger.Log( "[osu!] Android surface pixel format RGB565 is incompatible with the Vulkan rendering pipeline " + "— requesting RGBA8888 and triggering a surface recreate. " + diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 10f9e37654fa..dc89f9a08638 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -379,7 +379,7 @@ protected override void LoadComplete() // // Pinning Update + Draw + Input to a 5-core subset (mask 0xF8 on SD8G2) is the // ONLY unconditional Android-specific synchronous mutation we still perform - // during the cold-start window — every other customisation (SustainedPerformanceMode, + // during the cold-start window — every other customisation ( // RequestUnbufferedDispatch, refresh-rate selection, Oboe / Vulkan-probe init, // performance-mode GC-latency flip) is already deferred behind the // refreshRateDelayMs scheduler below. Field logs.zip on v2026.423.176 show both @@ -569,21 +569,28 @@ protected override void LoadComplete() Debug.WriteLine($"[osu!] TameBackgroundThreads (initial) failed: {e.Message}"); } - // Sustained performance mode is applied LATER, together with the deferred - // display-mode / GC-latency work below. See the Scheduler.AddDelayed block - // further down (after base.LoadComplete()) that schedules the first apply - // on a refreshRateDelayMs timer. Running - // Window.SetSustainedPerformanceMode(true) synchronously here — during the - // Toolbar cold-start texture-upload burst and the Vulkan swapchain bring-up — - // has been observed to race the Draw thread on Samsung One UI / Adreno panels: - // the window-flag mutation round-trips through ViewRootImpl.setPrivateFlags - // and can partially reconfigure the Surface while vkAcquireNextImageKHR is in - // flight, stalling the present queue. Update keeps ticking (so neither the - // managed nor the native watchdog ever dumps), the screen never updates, and - // ~10 s later Android raises a MotionEvent input-dispatch ANR — the exact - // cold-start "black screen → no touch → ANR" fingerprint reported across - // multiple v174 launches in logs.zip. Deferring to the same window used by - // SelectHighestRefreshRate moves the mutation behind the texture-upload burst. + // Window.SetSustainedPerformanceMode is intentionally NOT called anywhere. + // + // On Samsung One UI / Adreno devices, calling SetSustainedPerformanceMode(true) + // triggers a non-seamless display-mode transition (even when deferred behind the + // texture-upload burst). The transition momentarily destroys the SurfaceView, + // which resets the surface pixel format back to the Android default (RGB565 on + // high-density Samsung panels). Our SurfaceChanged reactive guard then calls + // SurfaceHolder.SetFormat(RGBA8888), causing a second surface-destroy/recreate + // cycle. During this second cycle the ANativeWindow transiently reports the + // display's scaled (dp) dimensions — 1029×480 on a 3088×1440 3×-density panel — + // instead of the physical pixel dimensions. Veldrid reads those dimensions from + // vkGetPhysicalDeviceSurfaceCapabilitiesKHR during its VkSurfaceKHR-loss + // recovery, creates a permanent swapchain at 1029×480, and SurfaceFlinger tiles + // that sub-screen image 3×3 to fill the display. The result is the "9 screens" + // artifact, blurry/flashing textures, and a sustained FPS drop observed on + // Galaxy S24 Ultra (Adreno 740, One UI 7, Android 15) with Vulkan enabled. + // + // Removing the call eliminates the mid-session surface teardown. ADPF performance + // hinting is already provided by Oboe's setPerformanceHintEnabled(true) (set + // during stream open in oboe_bridge.cpp), and GC low-latency is handled by + // AndroidHighPerformanceSessionManager (SustainedLowLatency GCSettings) which + // covers the same thermal/responsiveness goals without touching the Surface. base.LoadComplete(); @@ -688,27 +695,6 @@ protected override void LoadComplete() Debug.WriteLine($"[osu!] Deferred SelectHighestRefreshRate failed: {ex.Message}"); } - // Deferred sustained-performance-mode apply. See the comment block - // before base.LoadComplete() above for the rationale (Samsung One UI / - // Adreno Surface reconfigure race with vkAcquireNextImageKHR during the - // cold-start texture-upload burst). By the time this fires the - // swapchain has long since stabilised. - try - { - gameActivity.RunOnUiThread(() => - { - try { gameActivity.Window?.SetSustainedPerformanceMode(true); } - catch (Exception e) - { - Debug.WriteLine($"[osu!] Failed to enable sustained performance mode: {e.Message}"); - } - }); - } - catch (Exception e) - { - Debug.WriteLine($"[osu!] Failed to dispatch sustained performance mode toggle to UI thread: {e.Message}"); - } - // Deferred initial application of the user's performance-mode setting. // The BindValueChanged registration below is WITHOUT the immediate-fire // flag, so the very first apply (which may flip GCSettings.LatencyMode @@ -1033,7 +1019,7 @@ protected override void LoadComplete() // the BDL load thread, in the silent cold-start window — exactly // when we are debugging a startup hang. Deferring the initial // fire via Scheduler.AddDelayed onto the same refreshRateDelayMs - // timer that gates SustainedPerformanceMode / the initial refresh- + // timer that gates the initial refresh- // rate apply / the initial performance-mode apply keeps the cold- // start path free of synchronous native init even when a saved- // true setting would otherwise force it, AND ensures the native @@ -1149,8 +1135,9 @@ private void applyPerformanceOptimizations(bool enabled) { try { - // Sustained performance mode is always on (set in LoadComplete). // The performance toggle controls the high-perf GC session only. + // (Window.SetSustainedPerformanceMode is intentionally not called — + // see the comment before base.LoadComplete() for the full rationale.) if (enabled) { highPerformanceSession ??= highPerformanceSessionManager.BeginSession(); From c949d8733c3b710f741907205a8c25c067551343 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 02:30:35 +0000 Subject: [PATCH 2/4] Address review feedback: fix comment formatting and consolidate duplicate Logger.Log calls Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/3c1fa36e-ab75-466a-9437-b218eeb9fa8e Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android/OsuGameActivity.cs | 24 ++++++++++-------------- osu.Android/OsuGameAndroid.cs | 7 +++---- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index fb6aa9c7bfaa..6331259f52f0 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -683,20 +683,16 @@ public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Forma // picks up the new ANativeWindow and negotiates a proper BGRA/RGBA 8-bit swapchain. if (format == global::Android.Graphics.Format.Rgb565 && LogManagement.IsVulkanConfigured()) { - Logger.Log( - "[osu!] Android surface pixel format RGB565 detected mid-session (Vulkan path). " + - "Requesting RGBA8888 and triggering a surface recreate. " + - "If this fires after startup it indicates an OEM display-mode change " + - "(e.g. SetSustainedPerformanceMode) reset the surface format.", - LoggingTarget.Runtime, - LogLevel.Important); - Logger.Log( - "[osu!] Android surface pixel format RGB565 is incompatible with the Vulkan rendering pipeline " + - "— requesting RGBA8888 and triggering a surface recreate. " + - "This is the root cause of the Vulkan black-screen crash on Adreno (SDL_PIXELFORMAT_RGB565 in runtime log). " + - "The next SurfaceChanged will carry the corrected format.", - LoggingTarget.Performance, - LogLevel.Important); + // Log to Runtime so the mid-session RGB565 reset is visible in the main log + // (and therefore in the notification overlay). Performance log gets the same + // entry for correlation with display-mode and frame-timing data. + string rgb565Message = + "[osu!] Android surface pixel format RGB565 detected (Vulkan path) — " + + "requesting RGBA8888 and triggering a surface recreate. " + + "If this fires after startup an OEM display-mode change reset the surface format, " + + "which would cause a mid-session swapchain rebuild at wrong dimensions."; + Logger.Log(rgb565Message, LoggingTarget.Runtime, LogLevel.Important); + Logger.Log(rgb565Message, LoggingTarget.Performance, LogLevel.Important); try { diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index dc89f9a08638..e28b3f9926de 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -379,8 +379,8 @@ protected override void LoadComplete() // // Pinning Update + Draw + Input to a 5-core subset (mask 0xF8 on SD8G2) is the // ONLY unconditional Android-specific synchronous mutation we still perform - // during the cold-start window — every other customisation ( - // RequestUnbufferedDispatch, refresh-rate selection, Oboe / Vulkan-probe init, + // during the cold-start window — every other customisation (RequestUnbufferedDispatch, + // refresh-rate selection, Oboe / Vulkan-probe init, // performance-mode GC-latency flip) is already deferred behind the // refreshRateDelayMs scheduler below. Field logs.zip on v2026.423.176 show both // a normal launch and a safe-mode launch dying silently mid-Toolbar load @@ -1019,8 +1019,7 @@ protected override void LoadComplete() // the BDL load thread, in the silent cold-start window — exactly // when we are debugging a startup hang. Deferring the initial // fire via Scheduler.AddDelayed onto the same refreshRateDelayMs - // timer that gates the initial refresh- - // rate apply / the initial performance-mode apply keeps the cold- + // timer that gates the initial refresh-rate apply / performance-mode apply keeps the cold- // start path free of synchronous native init even when a saved- // true setting would otherwise force it, AND ensures the native // init actually lands AFTER the cold-start Toolbar texture-upload From 59a03d82e68aacdd620818e664b4dd000aa9054b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 09:01:27 +0000 Subject: [PATCH 3/4] Fix TestMissingScores NullReferenceException and parallelize CI test matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ResultsScreen.cs: GetBeatmapAsync can return null when the online ID is unknown (e.g. in the TestMissingScores test which returns empty scores). Replace the null-forgiving ! with a proper ??= placeholder matching the SubScreenBeatmapSelect pattern. Fixes the Linux test-results reporter failure. ci.yml: Split the test matrix into two parallel groups per OS (game: osu.Game.Tests + Tournament + Templates; rulesets: 4 ruleset DLLs). Each group runs in ~half the total time, so Windows runners (~2-3× slower than Linux) complete each group in ~60 min instead of exceeding the 120 min wall-clock budget for the full suite. Per-job timeout reduced from 120 → 90 min (right-sized for a single group). Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/4f0e240e-6ec5-4f03-abaf-c0a6e6e7a448 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- .github/workflows/ci.yml | 30 ++++++++++++------- .../Matchmaking/RankedPlay/ResultsScreen.cs | 14 ++++++++- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d36849248797..dc6a4e41691c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,21 @@ jobs: # - { prettyname: macOS, fullname: macos-latest } - { prettyname: Linux, fullname: ubuntu-latest } threadingMode: ['MultiThreaded'] - timeout-minutes: 120 + # Split tests into two parallel groups so Windows runners (which are ~2-3× slower than + # Linux) finish each group in ~60 min rather than timing out on the full suite at 120 min. + testSuite: + - name: game + dlls: >- + osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll + osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll + Templates/**/*.Tests/bin/Debug/**/*.Tests.dll + - name: rulesets + dlls: >- + osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll + osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll + osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll + osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll + timeout-minutes: 90 steps: - name: Checkout uses: actions/checkout@v6 @@ -116,14 +130,8 @@ jobs: continue-on-error: true run: > dotnet test - osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll - osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll - osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll - osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll - osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll - osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll - Templates/**/*.Tests/bin/Debug/**/*.Tests.dll - --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" + ${{matrix.testSuite.dlls}} + --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}-${{matrix.testSuite.name}}.trx" -- NUnit.ConsoleOut=0 @@ -133,8 +141,8 @@ jobs: uses: actions/upload-artifact@v7 if: ${{ !cancelled() }} with: - name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}} - path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx + name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}-${{matrix.testSuite.name}} + path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}-${{matrix.testSuite.name}}.trx test-results: name: Test results diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs index 48149e5054b7..310e2559dcc8 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs @@ -125,7 +125,19 @@ private async Task fetchFinalScores() }; // Should complete instantaneously due to prior lookups. - APIBeatmap beatmap = (await beatmapLookupCache.GetBeatmapAsync(globalBeatmap.Value.BeatmapInfo.OnlineID).ConfigureAwait(false))!; + // GetBeatmapAsync can return null if the online ID is unknown (e.g. in tests or + // when the API is unavailable); fall back to a placeholder rather than crashing. + APIBeatmap? beatmap = await beatmapLookupCache.GetBeatmapAsync(globalBeatmap.Value.BeatmapInfo.OnlineID).ConfigureAwait(false); + beatmap ??= new APIBeatmap + { + BeatmapSet = new APIBeatmapSet + { + Title = "unknown beatmap", + TitleUnicode = "unknown beatmap", + Artist = "unknown artist", + ArtistUnicode = "unknown artist", + } + }; Schedule(() => { From 66eca72e6b73c452dfd78002054a06b7cfa83de6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 09:02:42 +0000 Subject: [PATCH 4/4] =?UTF-8?q?Fix=20grammar=20in=20OsuGameActivity=20RGB5?= =?UTF-8?q?65=20log=20message=20(reset=20=E2=86=92=20has=20reset)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/4f0e240e-6ec5-4f03-abaf-c0a6e6e7a448 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android/OsuGameActivity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 6331259f52f0..394080d04c78 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -689,7 +689,7 @@ public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Forma string rgb565Message = "[osu!] Android surface pixel format RGB565 detected (Vulkan path) — " + "requesting RGBA8888 and triggering a surface recreate. " + - "If this fires after startup an OEM display-mode change reset the surface format, " + + "If this fires after startup an OEM display-mode change has reset the surface format, " + "which would cause a mid-session swapchain rebuild at wrong dimensions."; Logger.Log(rgb565Message, LoggingTarget.Runtime, LogLevel.Important); Logger.Log(rgb565Message, LoggingTarget.Performance, LogLevel.Important);