Skip to content

Commit baec2e6

Browse files
authored
Merge pull request #294 from winnerspiros/copilot/fix-vulkan-glitching-textures
Android Vulkan: fix 9-screen tiling, flashing textures, and low FPS on Samsung One UI / Adreno 740
2 parents c7b9915 + 66eca72 commit baec2e6

4 files changed

Lines changed: 92 additions & 60 deletions

File tree

.github/workflows/ci.yml

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,21 @@ jobs:
8787
# - { prettyname: macOS, fullname: macos-latest }
8888
- { prettyname: Linux, fullname: ubuntu-latest }
8989
threadingMode: ['MultiThreaded']
90-
timeout-minutes: 120
90+
# Split tests into two parallel groups so Windows runners (which are ~2-3× slower than
91+
# Linux) finish each group in ~60 min rather than timing out on the full suite at 120 min.
92+
testSuite:
93+
- name: game
94+
dlls: >-
95+
osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll
96+
osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll
97+
Templates/**/*.Tests/bin/Debug/**/*.Tests.dll
98+
- name: rulesets
99+
dlls: >-
100+
osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll
101+
osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll
102+
osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll
103+
osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll
104+
timeout-minutes: 90
91105
steps:
92106
- name: Checkout
93107
uses: actions/checkout@v6
@@ -116,14 +130,8 @@ jobs:
116130
continue-on-error: true
117131
run: >
118132
dotnet test
119-
osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll
120-
osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll
121-
osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll
122-
osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll
123-
osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll
124-
osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll
125-
Templates/**/*.Tests/bin/Debug/**/*.Tests.dll
126-
--logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
133+
${{matrix.testSuite.dlls}}
134+
--logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}-${{matrix.testSuite.name}}.trx"
127135
--
128136
NUnit.ConsoleOut=0
129137
@@ -133,8 +141,8 @@ jobs:
133141
uses: actions/upload-artifact@v7
134142
if: ${{ !cancelled() }}
135143
with:
136-
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
137-
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
144+
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}-${{matrix.testSuite.name}}
145+
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}-${{matrix.testSuite.name}}.trx
138146

139147
test-results:
140148
name: Test results

osu.Android/OsuGameActivity.cs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,29 @@ protected override void OnCreate(Bundle? savedInstanceState)
225225
LogManagement.WipeShaderCacheOnceForVersion();
226226
CrashDiagnostics.WriteAliveMarker("LogManagement.WipeShaderCacheOnceForVersion (returned)");
227227

228+
// Stamp RGBA8888 at the Window level BEFORE SDL creates its SurfaceView inside
229+
// base.OnCreate(). Android's default SurfaceView pixel format on many high-density
230+
// Samsung / Qualcomm panels is RGB565. SDL3 only calls SurfaceHolder.setFormat(
231+
// RGBA8888) for the OpenGL path — the Vulkan path inherits the window default.
232+
// Setting the format here, before SDL attaches its SurfaceView, ensures the
233+
// SurfaceView is born with RGBA8888 and eliminates the format-change teardown
234+
// (SurfaceHolder.SetFormat in DecorView.Post) that otherwise fires mid-Vulkan-init
235+
// and can produce the "Draw thread did not acknowledge teardown within 250ms" warning.
236+
// The DecorView.Post call and the SurfaceChanged reactive guard are retained as
237+
// belt-and-braces fallbacks for timing windows or OEM variants where this hint is
238+
// not honoured by the SurfaceView allocation path.
239+
if (LogManagement.IsVulkanConfigured())
240+
{
241+
try
242+
{
243+
Window?.SetFormat(global::Android.Graphics.Format.Rgba8888);
244+
}
245+
catch (Exception e)
246+
{
247+
Debug.WriteLine($"[osu!] Pre-SDL Window.SetFormat(RGBA8888) failed (non-fatal): {e.Message}");
248+
}
249+
}
250+
228251
base.OnCreate(savedInstanceState);
229252

230253
// Wrap Platform.Init defensively: MAUI Essentials pulls in workload-version-sensitive
@@ -660,13 +683,16 @@ public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Forma
660683
// picks up the new ANativeWindow and negotiates a proper BGRA/RGBA 8-bit swapchain.
661684
if (format == global::Android.Graphics.Format.Rgb565 && LogManagement.IsVulkanConfigured())
662685
{
663-
Logger.Log(
664-
"[osu!] Android surface pixel format RGB565 is incompatible with the Vulkan rendering pipeline " +
665-
"— requesting RGBA8888 and triggering a surface recreate. " +
666-
"This is the root cause of the Vulkan black-screen crash on Adreno (SDL_PIXELFORMAT_RGB565 in runtime log). " +
667-
"The next SurfaceChanged will carry the corrected format.",
668-
LoggingTarget.Performance,
669-
LogLevel.Important);
686+
// Log to Runtime so the mid-session RGB565 reset is visible in the main log
687+
// (and therefore in the notification overlay). Performance log gets the same
688+
// entry for correlation with display-mode and frame-timing data.
689+
string rgb565Message =
690+
"[osu!] Android surface pixel format RGB565 detected (Vulkan path) — " +
691+
"requesting RGBA8888 and triggering a surface recreate. " +
692+
"If this fires after startup an OEM display-mode change has reset the surface format, " +
693+
"which would cause a mid-session swapchain rebuild at wrong dimensions.";
694+
Logger.Log(rgb565Message, LoggingTarget.Runtime, LogLevel.Important);
695+
Logger.Log(rgb565Message, LoggingTarget.Performance, LogLevel.Important);
670696

671697
try
672698
{

osu.Android/OsuGameAndroid.cs

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,8 @@ protected override void LoadComplete()
379379
//
380380
// Pinning Update + Draw + Input to a 5-core subset (mask 0xF8 on SD8G2) is the
381381
// ONLY unconditional Android-specific synchronous mutation we still perform
382-
// during the cold-start window — every other customisation (SustainedPerformanceMode,
383-
// RequestUnbufferedDispatch, refresh-rate selection, Oboe / Vulkan-probe init,
382+
// during the cold-start window — every other customisation (RequestUnbufferedDispatch,
383+
// refresh-rate selection, Oboe / Vulkan-probe init,
384384
// performance-mode GC-latency flip) is already deferred behind the
385385
// refreshRateDelayMs scheduler below. Field logs.zip on v2026.423.176 show both
386386
// a normal launch and a safe-mode launch dying silently mid-Toolbar load
@@ -569,21 +569,28 @@ protected override void LoadComplete()
569569
Debug.WriteLine($"[osu!] TameBackgroundThreads (initial) failed: {e.Message}");
570570
}
571571

572-
// Sustained performance mode is applied LATER, together with the deferred
573-
// display-mode / GC-latency work below. See the Scheduler.AddDelayed block
574-
// further down (after base.LoadComplete()) that schedules the first apply
575-
// on a refreshRateDelayMs timer. Running
576-
// Window.SetSustainedPerformanceMode(true) synchronously here — during the
577-
// Toolbar cold-start texture-upload burst and the Vulkan swapchain bring-up —
578-
// has been observed to race the Draw thread on Samsung One UI / Adreno panels:
579-
// the window-flag mutation round-trips through ViewRootImpl.setPrivateFlags
580-
// and can partially reconfigure the Surface while vkAcquireNextImageKHR is in
581-
// flight, stalling the present queue. Update keeps ticking (so neither the
582-
// managed nor the native watchdog ever dumps), the screen never updates, and
583-
// ~10 s later Android raises a MotionEvent input-dispatch ANR — the exact
584-
// cold-start "black screen → no touch → ANR" fingerprint reported across
585-
// multiple v174 launches in logs.zip. Deferring to the same window used by
586-
// SelectHighestRefreshRate moves the mutation behind the texture-upload burst.
572+
// Window.SetSustainedPerformanceMode is intentionally NOT called anywhere.
573+
//
574+
// On Samsung One UI / Adreno devices, calling SetSustainedPerformanceMode(true)
575+
// triggers a non-seamless display-mode transition (even when deferred behind the
576+
// texture-upload burst). The transition momentarily destroys the SurfaceView,
577+
// which resets the surface pixel format back to the Android default (RGB565 on
578+
// high-density Samsung panels). Our SurfaceChanged reactive guard then calls
579+
// SurfaceHolder.SetFormat(RGBA8888), causing a second surface-destroy/recreate
580+
// cycle. During this second cycle the ANativeWindow transiently reports the
581+
// display's scaled (dp) dimensions — 1029×480 on a 3088×1440 3×-density panel —
582+
// instead of the physical pixel dimensions. Veldrid reads those dimensions from
583+
// vkGetPhysicalDeviceSurfaceCapabilitiesKHR during its VkSurfaceKHR-loss
584+
// recovery, creates a permanent swapchain at 1029×480, and SurfaceFlinger tiles
585+
// that sub-screen image 3×3 to fill the display. The result is the "9 screens"
586+
// artifact, blurry/flashing textures, and a sustained FPS drop observed on
587+
// Galaxy S24 Ultra (Adreno 740, One UI 7, Android 15) with Vulkan enabled.
588+
//
589+
// Removing the call eliminates the mid-session surface teardown. ADPF performance
590+
// hinting is already provided by Oboe's setPerformanceHintEnabled(true) (set
591+
// during stream open in oboe_bridge.cpp), and GC low-latency is handled by
592+
// AndroidHighPerformanceSessionManager (SustainedLowLatency GCSettings) which
593+
// covers the same thermal/responsiveness goals without touching the Surface.
587594

588595
base.LoadComplete();
589596

@@ -688,27 +695,6 @@ protected override void LoadComplete()
688695
Debug.WriteLine($"[osu!] Deferred SelectHighestRefreshRate failed: {ex.Message}");
689696
}
690697

691-
// Deferred sustained-performance-mode apply. See the comment block
692-
// before base.LoadComplete() above for the rationale (Samsung One UI /
693-
// Adreno Surface reconfigure race with vkAcquireNextImageKHR during the
694-
// cold-start texture-upload burst). By the time this fires the
695-
// swapchain has long since stabilised.
696-
try
697-
{
698-
gameActivity.RunOnUiThread(() =>
699-
{
700-
try { gameActivity.Window?.SetSustainedPerformanceMode(true); }
701-
catch (Exception e)
702-
{
703-
Debug.WriteLine($"[osu!] Failed to enable sustained performance mode: {e.Message}");
704-
}
705-
});
706-
}
707-
catch (Exception e)
708-
{
709-
Debug.WriteLine($"[osu!] Failed to dispatch sustained performance mode toggle to UI thread: {e.Message}");
710-
}
711-
712698
// Deferred initial application of the user's performance-mode setting.
713699
// The BindValueChanged registration below is WITHOUT the immediate-fire
714700
// flag, so the very first apply (which may flip GCSettings.LatencyMode
@@ -1033,8 +1019,7 @@ protected override void LoadComplete()
10331019
// the BDL load thread, in the silent cold-start window — exactly
10341020
// when we are debugging a startup hang. Deferring the initial
10351021
// fire via Scheduler.AddDelayed onto the same refreshRateDelayMs
1036-
// timer that gates SustainedPerformanceMode / the initial refresh-
1037-
// rate apply / the initial performance-mode apply keeps the cold-
1022+
// timer that gates the initial refresh-rate apply / performance-mode apply keeps the cold-
10381023
// start path free of synchronous native init even when a saved-
10391024
// true setting would otherwise force it, AND ensures the native
10401025
// init actually lands AFTER the cold-start Toolbar texture-upload
@@ -1149,8 +1134,9 @@ private void applyPerformanceOptimizations(bool enabled)
11491134
{
11501135
try
11511136
{
1152-
// Sustained performance mode is always on (set in LoadComplete).
11531137
// The performance toggle controls the high-perf GC session only.
1138+
// (Window.SetSustainedPerformanceMode is intentionally not called —
1139+
// see the comment before base.LoadComplete() for the full rationale.)
11541140
if (enabled)
11551141
{
11561142
highPerformanceSession ??= highPerformanceSessionManager.BeginSession();

osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,19 @@ private async Task fetchFinalScores()
125125
};
126126

127127
// Should complete instantaneously due to prior lookups.
128-
APIBeatmap beatmap = (await beatmapLookupCache.GetBeatmapAsync(globalBeatmap.Value.BeatmapInfo.OnlineID).ConfigureAwait(false))!;
128+
// GetBeatmapAsync can return null if the online ID is unknown (e.g. in tests or
129+
// when the API is unavailable); fall back to a placeholder rather than crashing.
130+
APIBeatmap? beatmap = await beatmapLookupCache.GetBeatmapAsync(globalBeatmap.Value.BeatmapInfo.OnlineID).ConfigureAwait(false);
131+
beatmap ??= new APIBeatmap
132+
{
133+
BeatmapSet = new APIBeatmapSet
134+
{
135+
Title = "unknown beatmap",
136+
TitleUnicode = "unknown beatmap",
137+
Artist = "unknown artist",
138+
ArtistUnicode = "unknown artist",
139+
}
140+
};
129141

130142
Schedule(() =>
131143
{

0 commit comments

Comments
 (0)