Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -278,19 +278,16 @@ jobs:

# https://github.com/dotnet/macios/issues/19157
# https://github.com/actions/runner-images/issues/12758
- name: Use Xcode 26.4
- name: Use Xcode 26.5
run: |
# Pin to Xcode 26.4 — the .NET iOS workload (net10.0_26.4) requires exactly Xcode 26.4.
# Using Xcode 26.5 causes a hard build failure: "requires Xcode 26.4, current is 26.5".
# Fix: MacOSX.sdk in Xcode 26.4 is a minimal stub. Replace it with a symlink to the
# Pin to Xcode 26.5 — the .NET iOS workload (net10.0_26.5) requires exactly Xcode 26.5.
# Fix: MacOSX.sdk in Xcode 26.5 is a minimal stub. Replace it with a symlink to the
# real versioned SDK found across Xcode installs. Real SDKs have hundreds of headers;
# stubs have ≤1.
ACTIVE_XCODE="/Applications/Xcode_26.4.app"
sudo xcode-select -switch "$ACTIVE_XCODE"
SDKS_DIR="$ACTIVE_XCODE/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs"
MACOS_SDK="$SDKS_DIR/MacOSX.sdk"
PREFERRED_XCODE="/Applications/Xcode_26.5.app"
REAL_SDK=""
# Search ALL Xcode_26.x.app installs (including active) for versioned MacOSX[N].sdk.
REAL_XCODE=""
# Search ALL Xcode_26.x.app installs for versioned MacOSX[N].sdk.
# Validate by usr/include header count: stubs ≤1 file, real SDKs have hundreds.
for xapp in $(ls -d /Applications/Xcode_26.*.app 2>/dev/null | sort -rV); do
sdk_dir="$xapp/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs"
Expand All @@ -300,6 +297,7 @@ jobs:
hdr_count=$(ls "$cand/usr/include" 2>/dev/null | wc -l | tr -d ' ')
if [ "${hdr_count:-0}" -gt 5 ]; then
REAL_SDK="$cand"
REAL_XCODE="$xapp"
break 2
fi
done
Expand All @@ -315,10 +313,23 @@ jobs:
fi
done
fi
# Switch to preferred Xcode if it exists, otherwise the one that has the real SDK.
if [ -d "$PREFERRED_XCODE" ]; then
sudo xcode-select -switch "$PREFERRED_XCODE"
elif [ -n "$REAL_XCODE" ]; then
sudo xcode-select -switch "$REAL_XCODE"
echo "Note: Xcode_26.5.app not found; using $REAL_XCODE for xcode-select"
fi
Comment on lines +316 to +322
# Replace stub MacOSX.sdk in the active Xcode with a symlink to the real versioned SDK.
if [ -n "$REAL_SDK" ]; then
ACTIVE_DEV="$(xcode-select -p)"
ACTIVE_SDKS_DIR="$ACTIVE_DEV/Platforms/MacOSX.platform/Developer/SDKs"
MACOS_SDK="$ACTIVE_SDKS_DIR/MacOSX.sdk"
# Resolve REAL_SDK to its canonical (symlink-free) path to prevent ELOOP errors.
REAL_SDK_CANON=$(python3 -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$REAL_SDK" 2>/dev/null || echo "$REAL_SDK")
sudo rm -rf "$MACOS_SDK"
sudo ln -sfn "$REAL_SDK" "$MACOS_SDK"
echo "Created MacOSX.sdk symlink -> $REAL_SDK"
sudo ln -sfn "$REAL_SDK_CANON" "$MACOS_SDK"
echo "Created MacOSX.sdk symlink -> $REAL_SDK_CANON"
else
echo "WARNING: no valid macOS SDK found; build may fail"
fi
Expand Down
2 changes: 1 addition & 1 deletion osu.Android.props
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.519.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.520.3" />
<!-- `ppy.osu.Framework.NativeLibs` is a transitive dependency of `ppy.osu.Framework`
that ships desktop-only natives (Linux/macOS/Windows) under `runtimes/<rid>/native/`
— including a bare Linux `libbass.so`/`libbass_fx.so`/`libbassmix.so` for linux-arm64.
Expand Down
38 changes: 38 additions & 0 deletions osu.Android/LogManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,44 @@ public static void ForceOpenGLRendererIfSafeMode()
string? root = resolveStorageRoot();
if (root == null) return;

// The previous launch died (Vulkan ANR or native crash) before the
// shader compilation burst finished. The on-disk pipeline cache can
// contain:
// • SPIR-V blobs compiled against the old GlobalUniformData layout
// (before the UniformPadding12 alignment fix in 2026.519.1) if
// the WipeShaderCacheOnceForVersion sentinel was already written
// but the Vulkan session was killed mid-compile.
// • Partially-written or incomplete pipeline objects from the
// interrupted Vulkan compile pass.
//
// Either case causes visual corruption on the rescue OpenGL session:
// – Argon hit circles render as white rectangles (masking uniform
// at wrong struct offset → CornerRadius clipping broken).
// – TrianglesV2 buttons show the wrong hue (gradient colour data
// at wrong offset → DrawColourInfo.Colour.Interpolate returns
// garbage channel values).
//
// Wipe the shader cache unconditionally here — bypassing the
// version-code sentinel — so the OpenGL rescue session always starts
// from a clean slate. The sentinel is NOT reset: the next normal
// (non-safe-mode) launch will still skip the version wipe and reuse
// the freshly-compiled OpenGL cache from this rescue session.
string shaderCacheDir = Path.Combine(root, "cache", "shaders");

if (Directory.Exists(shaderCacheDir))
{
try
{
Directory.Delete(shaderCacheDir, recursive: true);
Logger.Log("[osu!] Android safe-mode: shader cache wiped to ensure clean OpenGL recompilation.", LoggingTarget.Runtime);
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] LogManagement: safe-mode shader cache wipe failed ({e.Message}); falling back to per-entry sweep");
sweepDirectoryBestEffort(shaderCacheDir);
}
}

string iniPath = Path.Combine(root, "framework.ini");

if (!File.Exists(iniPath))
Expand Down
109 changes: 91 additions & 18 deletions osu.Android/OsuGameActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,25 +246,29 @@ protected override void OnCreate(Bundle? savedInstanceState)

// 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())
// Samsung / Qualcomm panels is RGB565. Setting RGBA8888 here (before SDL attaches
// its SurfaceView) ensures the SurfaceView is born with full 32-bit colour in both
// Vulkan and OpenGL modes:
// - Vulkan: the Veldrid swapchain can request VK_FORMAT_R8G8B8A8_SRGB / BGRA8888
// directly, but the underlying ANativeWindow must also support RGBA8888 — a
// Window born at RGB565 forces a surface teardown (and the
// "Draw thread did not acknowledge teardown within 250ms" warning) when Veldrid
// later calls ANativeWindow_setBuffersGeometry with RGBA8888.
// - OpenGL safe-mode (after a Vulkan crash): SDL3 does call
// SurfaceHolder.setFormat(RGBA8888) for EGL surfaces, but it only does so AFTER
// the SurfaceView is created. Pre-stamping the Window format here guarantees
// the initial SurfaceView allocation happens at RGBA8888, avoiding a brief
// RGB565 render pass that can leave colour-channel artefacts visible in the
// first few frames.
// Belt-and-braces fallbacks (DecorView.Post watcher, SurfaceChanged reactive guard)
// are retained for OEM variants where this Window-level hint is not honoured.
try
{
try
{
Window?.SetFormat(global::Android.Graphics.Format.Rgba8888);
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] Pre-SDL Window.SetFormat(RGBA8888) failed (non-fatal): {e.Message}");
}
Window?.SetFormat(global::Android.Graphics.Format.Rgba8888);
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] Pre-SDL Window.SetFormat(RGBA8888) failed (non-fatal): {e.Message}");
}

// BASS AAudio: if the user opted in, tell BASS to open an AAudio device instead
Expand Down Expand Up @@ -963,6 +967,75 @@ public void SurfaceDestroyed(ISurfaceHolder holder)
}
}

protected override void OnPause()
{
// Root cause of the recurring Vulkan IMMEDIATE-mode ANR (process-runtime ~50s):
//
// 1. Samsung Game Booster (or any surface-lifecycle event) fires onPause() on the
// Java main thread at ~50 seconds of active Vulkan gameplay.
// 2. SDL3's native onPause() sends SDL_EVENT_DID_ENTER_BACKGROUND through its event
// filter synchronously on the calling (Java main) thread.
// 3. The event filter calls Window.Suspended → GameHost.Suspend() →
// ThreadRunner.Suspend() → DrawThread.Pause() → WaitForState(Paused).
// 4. WaitForState spins: `while (state != Paused) Thread.Sleep(1)` — NO TIMEOUT.
// 5. The draw thread is stuck inside vkQueuePresentKHR (Vulkan IMMEDIATE mode;
// FrameSync=ActualUnlimited) due to an Adreno 7xx driver stall. It can only
// check pauseRequested at the START of the next frame — which never comes.
// 6. Java main thread spins forever → input dispatching times out after 10s → ANR.
//
// Fix: watchdog the OnPause() call. If base.OnPause() hasn't returned within 7 seconds
// (leaving a 3-second margin before the 10-second ANR), the draw thread is conclusively
// stuck in the driver. Kill the process immediately for a clean restart rather than a
// frozen 10-second ANR.
//
// We intentionally do NOT set FLAG_STARTUP_IN_PROGRESS (safe-mode) before killing.
// The startup completed successfully; this is a mid-session driver hang triggered by
// a transient system event (Game Booster first-session overlay). The next launch will
// retry Vulkan normally. Safe-mode is reserved for launch-time hangs where the renderer
// itself cannot initialize.
//
// Only active for Vulkan: OpenGL's eglSwapBuffers cannot stall indefinitely in the way
// vkQueuePresentKHR can, so OpenGL sessions are not at risk of this ANR pattern.
if (LogManagement.IsVulkanConfigured())
{
var pauseCompleted = new ManualResetEventSlim(false);

ThreadPool.QueueUserWorkItem(_ =>
{
const int watchdog_ms = 7000;

if (pauseCompleted.Wait(watchdog_ms))
return;

// base.OnPause() has not returned — draw thread is conclusively stuck in
// vkQueuePresentKHR. Write a diagnostic marker and kill cleanly.
try
{
CrashDiagnostics.WriteAliveMarker(
$"OnPause watchdog fired after {watchdog_ms}ms: draw thread stuck in vkQueuePresentKHR (Vulkan IMMEDIATE ANR). Killing for clean restart.");
}
catch { }

try
{
Debug.WriteLine(
"[osu!] OnPause watchdog: draw thread stuck in vkQueuePresentKHR >7s — killing for clean Vulkan restart.");
}
catch { }

try { global::Android.OS.Process.KillProcess(global::Android.OS.Process.MyPid()); }
catch { }
});

base.OnPause();
pauseCompleted.Set();
}
Comment on lines +999 to +1032
else
{
base.OnPause();
}
}

public override void OnConfigurationChanged(global::Android.Content.Res.Configuration newConfig)
{
base.OnConfigurationChanged(newConfig);
Expand Down
43 changes: 32 additions & 11 deletions osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@
using System.Numerics;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning;
Expand All @@ -23,23 +30,26 @@

namespace osu.Game.Rulesets.Catch.Tests
{
public partial class TestSceneHyperDashColouring : OsuTestScene
public partial class TestSceneHyperDashColouring : OsuTestScene, IStorageResourceProvider
{
[Resolved]
private SkinManager skins { get; set; }

[Resolved]
private GameHost host { get; set; } = null!;

[Test]
public void TestDefaultCatcherColour()
{
var skin = new TestSkin();
var skin = new TestSkin(this);

checkHyperDashCatcherColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
}

[Test]
public void TestCustomCatcherColour()
{
var skin = new TestSkin
var skin = new TestSkin(this)
{
HyperDashColour = Colour4.Goldenrod
};
Expand All @@ -50,7 +60,7 @@ public void TestCustomCatcherColour()
[Test]
public void TestCustomAfterImageColour()
{
var skin = new TestSkin
var skin = new TestSkin(this)
{
HyperDashAfterImageColour = Colour4.Lime
};
Expand All @@ -61,7 +71,7 @@ public void TestCustomAfterImageColour()
[Test]
public void TestCustomAfterImageColourPriority()
{
var skin = new TestSkin
var skin = new TestSkin(this)
{
HyperDashColour = Colour4.Goldenrod,
HyperDashAfterImageColour = Colour4.Lime
Expand All @@ -73,15 +83,15 @@ public void TestCustomAfterImageColourPriority()
[Test]
public void TestDefaultFruitColour()
{
var skin = new TestSkin();
var skin = new TestSkin(this);

checkHyperDashFruitColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
}

[Test]
public void TestCustomFruitColour()
{
var skin = new TestSkin
var skin = new TestSkin(this)
{
HyperDashFruitColour = Colour4.Cyan
};
Expand All @@ -92,7 +102,7 @@ public void TestCustomFruitColour()
[Test]
public void TestCustomFruitColourPriority()
{
var skin = new TestSkin
var skin = new TestSkin(this)
{
HyperDashColour = Colour4.Goldenrod,
HyperDashFruitColour = Colour4.Cyan
Expand All @@ -104,7 +114,7 @@ public void TestCustomFruitColourPriority()
[Test]
public void TestFruitColourFallback()
{
var skin = new TestSkin
var skin = new TestSkin(this)
{
HyperDashColour = Colour4.Goldenrod
};
Expand Down Expand Up @@ -209,10 +219,21 @@ public Colour4 HyperDashFruitColour
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashFruit)] = value;
}

public TestSkin()
: base(new SkinInfo(), null, null, string.Empty)
public TestSkin(IStorageResourceProvider resources)
: base(new SkinInfo(), resources, new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy"), string.Empty)
{
}
}

#region IStorageResourceProvider

IRenderer IStorageResourceProvider.Renderer => host.Renderer;
AudioManager IStorageResourceProvider.AudioManager => Audio;
IResourceStore<byte[]> IStorageResourceProvider.Files => null!;
IResourceStore<byte[]> IStorageResourceProvider.Resources => base.Resources;
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host.CreateTextureLoaderStore(underlyingStore);
RealmAccess IStorageResourceProvider.RealmAccess => null!;
Comment on lines +230 to +235

#endregion
}
}
5 changes: 3 additions & 2 deletions osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// See the LICENCE file in the repository root for full licence text.

using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
Expand All @@ -17,7 +18,7 @@ public BananaShowerCompositionTool()
{
}

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

public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
}
Expand Down
Loading
Loading