Skip to content

Commit 76887f6

Browse files
authored
Merge pull request #343 from winnerspiros/copilot/fix-ui-issues-vulkan-crash
fix: Argon skin corruption + ppy cherry-pick + surface format + safe-mode shader wipe
2 parents c41b07c + ccc4b89 commit 76887f6

17 files changed

Lines changed: 247 additions & 64 deletions

File tree

.github/workflows/ci.yml

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -278,19 +278,16 @@ jobs:
278278

279279
# https://github.com/dotnet/macios/issues/19157
280280
# https://github.com/actions/runner-images/issues/12758
281-
- name: Use Xcode 26.4
281+
- name: Use Xcode 26.5
282282
run: |
283-
# Pin to Xcode 26.4 — the .NET iOS workload (net10.0_26.4) requires exactly Xcode 26.4.
284-
# Using Xcode 26.5 causes a hard build failure: "requires Xcode 26.4, current is 26.5".
285-
# Fix: MacOSX.sdk in Xcode 26.4 is a minimal stub. Replace it with a symlink to the
283+
# Pin to Xcode 26.5 — the .NET iOS workload (net10.0_26.5) requires exactly Xcode 26.5.
284+
# Fix: MacOSX.sdk in Xcode 26.5 is a minimal stub. Replace it with a symlink to the
286285
# real versioned SDK found across Xcode installs. Real SDKs have hundreds of headers;
287286
# stubs have ≤1.
288-
ACTIVE_XCODE="/Applications/Xcode_26.4.app"
289-
sudo xcode-select -switch "$ACTIVE_XCODE"
290-
SDKS_DIR="$ACTIVE_XCODE/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs"
291-
MACOS_SDK="$SDKS_DIR/MacOSX.sdk"
287+
PREFERRED_XCODE="/Applications/Xcode_26.5.app"
292288
REAL_SDK=""
293-
# Search ALL Xcode_26.x.app installs (including active) for versioned MacOSX[N].sdk.
289+
REAL_XCODE=""
290+
# Search ALL Xcode_26.x.app installs for versioned MacOSX[N].sdk.
294291
# Validate by usr/include header count: stubs ≤1 file, real SDKs have hundreds.
295292
for xapp in $(ls -d /Applications/Xcode_26.*.app 2>/dev/null | sort -rV); do
296293
sdk_dir="$xapp/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs"
@@ -300,6 +297,7 @@ jobs:
300297
hdr_count=$(ls "$cand/usr/include" 2>/dev/null | wc -l | tr -d ' ')
301298
if [ "${hdr_count:-0}" -gt 5 ]; then
302299
REAL_SDK="$cand"
300+
REAL_XCODE="$xapp"
303301
break 2
304302
fi
305303
done
@@ -315,10 +313,23 @@ jobs:
315313
fi
316314
done
317315
fi
316+
# Switch to preferred Xcode if it exists, otherwise the one that has the real SDK.
317+
if [ -d "$PREFERRED_XCODE" ]; then
318+
sudo xcode-select -switch "$PREFERRED_XCODE"
319+
elif [ -n "$REAL_XCODE" ]; then
320+
sudo xcode-select -switch "$REAL_XCODE"
321+
echo "Note: Xcode_26.5.app not found; using $REAL_XCODE for xcode-select"
322+
fi
323+
# Replace stub MacOSX.sdk in the active Xcode with a symlink to the real versioned SDK.
318324
if [ -n "$REAL_SDK" ]; then
325+
ACTIVE_DEV="$(xcode-select -p)"
326+
ACTIVE_SDKS_DIR="$ACTIVE_DEV/Platforms/MacOSX.platform/Developer/SDKs"
327+
MACOS_SDK="$ACTIVE_SDKS_DIR/MacOSX.sdk"
328+
# Resolve REAL_SDK to its canonical (symlink-free) path to prevent ELOOP errors.
329+
REAL_SDK_CANON=$(python3 -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$REAL_SDK" 2>/dev/null || echo "$REAL_SDK")
319330
sudo rm -rf "$MACOS_SDK"
320-
sudo ln -sfn "$REAL_SDK" "$MACOS_SDK"
321-
echo "Created MacOSX.sdk symlink -> $REAL_SDK"
331+
sudo ln -sfn "$REAL_SDK_CANON" "$MACOS_SDK"
332+
echo "Created MacOSX.sdk symlink -> $REAL_SDK_CANON"
322333
else
323334
echo "WARNING: no valid macOS SDK found; build may fail"
324335
fi

osu.Android.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
</PropertyGroup>
5353

5454
<ItemGroup>
55-
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.519.1" />
55+
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.520.3" />
5656
<!-- `ppy.osu.Framework.NativeLibs` is a transitive dependency of `ppy.osu.Framework`
5757
that ships desktop-only natives (Linux/macOS/Windows) under `runtimes/<rid>/native/`
5858
— including a bare Linux `libbass.so`/`libbass_fx.so`/`libbassmix.so` for linux-arm64.

osu.Android/LogManagement.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,44 @@ public static void ForceOpenGLRendererIfSafeMode()
481481
string? root = resolveStorageRoot();
482482
if (root == null) return;
483483

484+
// The previous launch died (Vulkan ANR or native crash) before the
485+
// shader compilation burst finished. The on-disk pipeline cache can
486+
// contain:
487+
// • SPIR-V blobs compiled against the old GlobalUniformData layout
488+
// (before the UniformPadding12 alignment fix in 2026.519.1) if
489+
// the WipeShaderCacheOnceForVersion sentinel was already written
490+
// but the Vulkan session was killed mid-compile.
491+
// • Partially-written or incomplete pipeline objects from the
492+
// interrupted Vulkan compile pass.
493+
//
494+
// Either case causes visual corruption on the rescue OpenGL session:
495+
// – Argon hit circles render as white rectangles (masking uniform
496+
// at wrong struct offset → CornerRadius clipping broken).
497+
// – TrianglesV2 buttons show the wrong hue (gradient colour data
498+
// at wrong offset → DrawColourInfo.Colour.Interpolate returns
499+
// garbage channel values).
500+
//
501+
// Wipe the shader cache unconditionally here — bypassing the
502+
// version-code sentinel — so the OpenGL rescue session always starts
503+
// from a clean slate. The sentinel is NOT reset: the next normal
504+
// (non-safe-mode) launch will still skip the version wipe and reuse
505+
// the freshly-compiled OpenGL cache from this rescue session.
506+
string shaderCacheDir = Path.Combine(root, "cache", "shaders");
507+
508+
if (Directory.Exists(shaderCacheDir))
509+
{
510+
try
511+
{
512+
Directory.Delete(shaderCacheDir, recursive: true);
513+
Logger.Log("[osu!] Android safe-mode: shader cache wiped to ensure clean OpenGL recompilation.", LoggingTarget.Runtime);
514+
}
515+
catch (Exception e)
516+
{
517+
Debug.WriteLine($"[osu!] LogManagement: safe-mode shader cache wipe failed ({e.Message}); falling back to per-entry sweep");
518+
sweepDirectoryBestEffort(shaderCacheDir);
519+
}
520+
}
521+
484522
string iniPath = Path.Combine(root, "framework.ini");
485523

486524
if (!File.Exists(iniPath))

osu.Android/OsuGameActivity.cs

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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);

osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@
77
using System.Numerics;
88
using NUnit.Framework;
99
using osu.Framework.Allocation;
10+
using osu.Framework.Audio;
1011
using osu.Framework.Graphics;
1112
using osu.Framework.Graphics.Containers;
13+
using osu.Framework.Graphics.Rendering;
1214
using osu.Framework.Graphics.Sprites;
15+
using osu.Framework.Graphics.Textures;
16+
using osu.Framework.IO.Stores;
17+
using osu.Framework.Platform;
1318
using osu.Framework.Testing;
1419
using osu.Game.Beatmaps;
1520
using osu.Game.Beatmaps.ControlPoints;
21+
using osu.Game.Database;
22+
using osu.Game.IO;
1623
using osu.Game.Rulesets.Catch.Objects;
1724
using osu.Game.Rulesets.Catch.Objects.Drawables;
1825
using osu.Game.Rulesets.Catch.Skinning;
@@ -23,23 +30,26 @@
2330

2431
namespace osu.Game.Rulesets.Catch.Tests
2532
{
26-
public partial class TestSceneHyperDashColouring : OsuTestScene
33+
public partial class TestSceneHyperDashColouring : OsuTestScene, IStorageResourceProvider
2734
{
2835
[Resolved]
2936
private SkinManager skins { get; set; }
3037

38+
[Resolved]
39+
private GameHost host { get; set; } = null!;
40+
3141
[Test]
3242
public void TestDefaultCatcherColour()
3343
{
34-
var skin = new TestSkin();
44+
var skin = new TestSkin(this);
3545

3646
checkHyperDashCatcherColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
3747
}
3848

3949
[Test]
4050
public void TestCustomCatcherColour()
4151
{
42-
var skin = new TestSkin
52+
var skin = new TestSkin(this)
4353
{
4454
HyperDashColour = Colour4.Goldenrod
4555
};
@@ -50,7 +60,7 @@ public void TestCustomCatcherColour()
5060
[Test]
5161
public void TestCustomAfterImageColour()
5262
{
53-
var skin = new TestSkin
63+
var skin = new TestSkin(this)
5464
{
5565
HyperDashAfterImageColour = Colour4.Lime
5666
};
@@ -61,7 +71,7 @@ public void TestCustomAfterImageColour()
6171
[Test]
6272
public void TestCustomAfterImageColourPriority()
6373
{
64-
var skin = new TestSkin
74+
var skin = new TestSkin(this)
6575
{
6676
HyperDashColour = Colour4.Goldenrod,
6777
HyperDashAfterImageColour = Colour4.Lime
@@ -73,15 +83,15 @@ public void TestCustomAfterImageColourPriority()
7383
[Test]
7484
public void TestDefaultFruitColour()
7585
{
76-
var skin = new TestSkin();
86+
var skin = new TestSkin(this);
7787

7888
checkHyperDashFruitColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
7989
}
8090

8191
[Test]
8292
public void TestCustomFruitColour()
8393
{
84-
var skin = new TestSkin
94+
var skin = new TestSkin(this)
8595
{
8696
HyperDashFruitColour = Colour4.Cyan
8797
};
@@ -92,7 +102,7 @@ public void TestCustomFruitColour()
92102
[Test]
93103
public void TestCustomFruitColourPriority()
94104
{
95-
var skin = new TestSkin
105+
var skin = new TestSkin(this)
96106
{
97107
HyperDashColour = Colour4.Goldenrod,
98108
HyperDashFruitColour = Colour4.Cyan
@@ -104,7 +114,7 @@ public void TestCustomFruitColourPriority()
104114
[Test]
105115
public void TestFruitColourFallback()
106116
{
107-
var skin = new TestSkin
117+
var skin = new TestSkin(this)
108118
{
109119
HyperDashColour = Colour4.Goldenrod
110120
};
@@ -209,10 +219,21 @@ public Colour4 HyperDashFruitColour
209219
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashFruit)] = value;
210220
}
211221

212-
public TestSkin()
213-
: base(new SkinInfo(), null, null, string.Empty)
222+
public TestSkin(IStorageResourceProvider resources)
223+
: base(new SkinInfo(), resources, new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy"), string.Empty)
214224
{
215225
}
216226
}
227+
228+
#region IStorageResourceProvider
229+
230+
IRenderer IStorageResourceProvider.Renderer => host.Renderer;
231+
AudioManager IStorageResourceProvider.AudioManager => Audio;
232+
IResourceStore<byte[]> IStorageResourceProvider.Files => null!;
233+
IResourceStore<byte[]> IStorageResourceProvider.Resources => base.Resources;
234+
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host.CreateTextureLoaderStore(underlyingStore);
235+
RealmAccess IStorageResourceProvider.RealmAccess => null!;
236+
237+
#endregion
217238
}
218239
}

osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
// See the LICENCE file in the repository root for full licence text.
33

44
using osu.Framework.Graphics;
5-
using osu.Game.Beatmaps;
5+
using osu.Framework.Graphics.Sprites;
6+
using osu.Game.Graphics;
67
using osu.Game.Rulesets.Catch.Edit.Blueprints;
78
using osu.Game.Rulesets.Catch.Objects;
89
using osu.Game.Rulesets.Edit;
@@ -17,7 +18,7 @@ public BananaShowerCompositionTool()
1718
{
1819
}
1920

20-
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
21+
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorBananaShower };
2122

2223
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
2324
}

0 commit comments

Comments
 (0)