Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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.1" />
<!-- `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
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
5 changes: 3 additions & 2 deletions osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.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 FruitCompositionTool()
{
}

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

public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
}
Expand Down
5 changes: 3 additions & 2 deletions osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.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 JuiceStreamCompositionTool()
{
}

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

public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
}
Expand Down
5 changes: 3 additions & 2 deletions osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.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.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
Expand All @@ -16,7 +17,7 @@ public HoldNoteCompositionTool()
{
}

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

public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
}
Expand Down
5 changes: 3 additions & 2 deletions osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.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.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
Expand All @@ -17,7 +18,7 @@ public NoteCompositionTool()
{
}

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

public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
}
Expand Down
5 changes: 3 additions & 2 deletions osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.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.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
Expand All @@ -17,7 +18,7 @@ public DrumRollCompositionTool()
{
}

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

public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
}
Expand Down
5 changes: 3 additions & 2 deletions osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.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.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
Expand All @@ -17,7 +18,7 @@ public HitCompositionTool()
{
}

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

public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
}
Expand Down
5 changes: 3 additions & 2 deletions osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.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.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
Expand All @@ -17,7 +18,7 @@ public SwellCompositionTool()
{
}

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

public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
}
Expand Down
32 changes: 32 additions & 0 deletions osu.Game/Graphics/OsuIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ public static class OsuIcon
public static IconUsage EditorHitCircle => get(OsuIconMapping.EditorHitCircle);
public static IconUsage EditorSlider => get(OsuIconMapping.EditorSlider);
public static IconUsage EditorSpinner => get(OsuIconMapping.EditorSpinner);
public static IconUsage EditorHit => get(OsuIconMapping.EditorHit);
public static IconUsage EditorDrumRoll => get(OsuIconMapping.EditorDrumRoll);
public static IconUsage EditorSwell => get(OsuIconMapping.EditorSwell);
public static IconUsage EditorFruit => get(OsuIconMapping.EditorFruit);
public static IconUsage EditorJuiceStream => get(OsuIconMapping.EditorJuiceStream);
public static IconUsage EditorNote => get(OsuIconMapping.EditorNote);
public static IconUsage EditorHoldNote => get(OsuIconMapping.EditorHoldNote);
public static IconUsage EditorBananaShower => get(OsuIconMapping.EditorBananaShower);
public static IconUsage EditorGrid => get(OsuIconMapping.EditorGrid);
public static IconUsage EditorAddControlPoint => get(OsuIconMapping.EditorAddControlPoint);
public static IconUsage EditorConvertToStream => get(OsuIconMapping.EditorConvertToStream);
Expand Down Expand Up @@ -409,6 +417,30 @@ private enum OsuIconMapping
[Description(@"Editor/spinner")]
EditorSpinner,

[Description(@"Editor/hit")]
EditorHit,

[Description(@"Editor/drum-roll")]
EditorDrumRoll,

[Description(@"Editor/swell")]
EditorSwell,

[Description(@"Editor/fruit")]
EditorFruit,

[Description(@"Editor/juice-stream")]
EditorJuiceStream,

[Description(@"Editor/banana-shower")]
EditorBananaShower,

[Description(@"Editor/note")]
EditorNote,

[Description(@"Editor/hold-note")]
EditorHoldNote,

Comment on lines +435 to +443
[Description(@"Editor/grid")]
EditorGrid,

Expand Down
6 changes: 3 additions & 3 deletions osu.Game/osu.Game.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,18 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2026.519.1" />
<PackageReference Include="ppy.osu.Framework" Version="2026.520.1" />
<!--
Explicitly pin `ppy.Veldrid.SPIRV` to the winnerspiros fork build that
`ppy.osu.Framework 2026.519.1` was compiled against. This version is the only
`ppy.osu.Framework 2026.520.1` was compiled against. This version is the only
one whose `runtimes/android-arm64/native/libveldrid-spirv.so` is aligned to 16 KB
pages (required by Android 16+). It lives only as a release asset on
<https://github.com/winnerspiros/veldrid-spirv/releases/tag/1.0> and is vendored
under `local-packages/` (see `local-packages/README.md`). Without this pin NuGet
would silently fall back to the 4 KB-aligned nuget.org build and emit NU1903.
-->
<PackageReference Include="ppy.Veldrid.SPIRV" Version="1.0.15-gb268bf39ea" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.427.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.519.0" />
<PackageReference Include="Sentry" Version="6.5.0" />
<PackageReference Include="SharpCompress" Version="0.48.0" />
<PackageReference Include="NUnit" Version="4.6.0" />
Expand Down
Loading
Loading