Skip to content
Merged
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
40 changes: 22 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
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
2 changes: 1 addition & 1 deletion osu.Game/osu.Game.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
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