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
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.526.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.527.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
10 changes: 10 additions & 0 deletions osu.Android/AndroidStartupFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ internal static class AndroidStartupFlags
/// </summary>
public const string FLAG_LAST_NATIVE_CRASH_CONSUMED = "android_last_native_crash_consumed.flag";

/// <summary>
/// Stores the original <c>FrameSync</c> value that was temporarily overwritten to
/// <c>VSync</c> by <see cref="LogManagement.ForceVSyncDuringVulkanColdStart"/> before
/// framework init. After the Draw thread presents its first frame,
/// <see cref="OsuGameAndroid"/> reads this value and restores it via the framework
/// config manager (applying it in-memory AND persisting to <c>framework.ini</c>),
/// then deletes the flag so subsequent launches repeat the same cycle.
/// </summary>
public const string FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE = "android_vulkan_cold_start_frame_sync_restore.flag";

private static string? resolveDir()
{
try
Expand Down
111 changes: 111 additions & 0 deletions osu.Android/LogManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,117 @@ public static bool IsVulkanConfigured()
return renderer != null && string.Equals(renderer, "Vulkan", StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Temporarily forces <c>FrameSync = VSync</c> in <c>framework.ini</c> when
/// Vulkan is configured and the persisted value maps to IMMEDIATE present mode
/// (i.e. <c>ActualUnlimited</c> or <c>Unlimited</c>).
///
/// <para>
/// <b>Why:</b> On Adreno 7xx (Snapdragon 8 Gen 2/3), applying
/// <c>VK_PRESENT_MODE_IMMEDIATE_KHR</c> during the cold-start texture-upload burst
/// triggers a swapchain recreation (FIFO → IMMEDIATE) while hundreds of textures
/// are being uploaded. The Vulkan driver stalls in <c>vkQueuePresentKHR</c>,
/// blocking the Draw thread indefinitely and producing a black screen + ANR.
/// </para>
///
/// <para>
/// <b>How:</b> Before the framework reads <c>framework.ini</c> (in OnCreate,
/// before <c>base.OnCreate</c>), we rewrite <c>FrameSync</c> to <c>VSync</c> so
/// the swapchain is created in FIFO mode (safe). The original value is saved to
/// <see cref="AndroidStartupFlags.FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE"/>
/// and restored by <see cref="OsuGameAndroid"/> after the Draw thread presents
/// its first frame (via <c>FrameworkConfigManager.SetValue</c>, which applies
/// in-memory AND persists to disk).
/// </para>
///
/// <para>
/// <b>Safety:</b> If the process dies before restoration, next launch finds
/// <c>FrameSync = VSync</c> in the ini (safe FIFO cold start) plus the restore
/// flag still on disk, so the same deferred-switch cycle repeats. No user-visible
/// permanent change to the config.
Comment on lines +773 to +776
/// </para>
/// </summary>
public static void ForceVSyncDuringVulkanColdStart()
{
try
{
if (!IsVulkanConfigured())
return;

// If safe-mode is active, ForceOpenGLRendererIfSafeMode already switched
// to OpenGL — no Vulkan swapchain will be created, so no override needed.
if (AndroidStartupSafeMode.IsActive)
return;

string? root = resolveStorageRoot();
if (root == null) return;

string iniPath = Path.Combine(root, "framework.ini");
if (!File.Exists(iniPath)) return;

string[] lines;

try
{
lines = File.ReadAllLines(iniPath);
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] LogManagement: could not read framework.ini for Vulkan cold-start VSync override: {e.Message}");
return;
}

bool changed = false;
string? originalValue = null;

for (int i = 0; i < lines.Length; i++)
{
string line = lines[i];
int eq = line.IndexOf('=');
if (eq <= 0) continue;

string key = line.Substring(0, eq).Trim();
string value = line.Substring(eq + 1).Trim();

if (!string.Equals(key, "FrameSync", StringComparison.Ordinal))
continue;

// Only override values that map to IMMEDIATE present mode.
// VSync and Limit2x use FIFO, which is safe during cold start.
if (string.Equals(value, "ActualUnlimited", StringComparison.Ordinal)
|| string.Equals(value, "Unlimited", StringComparison.Ordinal))
{
originalValue = value;
lines[i] = "FrameSync = VSync";
changed = true;
}

break;
}

if (!changed || originalValue == null) return;

// Save the original value so OsuGameAndroid can restore it after first frame.
AndroidStartupFlags.WriteValue(AndroidStartupFlags.FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE, originalValue);

try
{
File.WriteAllLines(iniPath, lines);
Logger.Log($"[osu!] Vulkan cold-start protection: FrameSync {originalValue} → VSync (FIFO) until first frame presents", LoggingTarget.Performance);
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] LogManagement: could not rewrite framework.ini for Vulkan cold-start VSync override: {e.Message}");
// Clean up the flag since we couldn't apply the override
AndroidStartupFlags.Set(AndroidStartupFlags.FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE, false);
}
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] LogManagement: ForceVSyncDuringVulkanColdStart failed: {e.Message}");
}
}

// Sentinel file dropped after a successful one-shot shader-cache wipe.
// Stored alongside the cache itself (not in the cache directory, which
// we delete) so the marker survives the wipe. The file payload is the
Expand Down
10 changes: 10 additions & 0 deletions osu.Android/OsuGameActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,16 @@ protected override void OnCreate(Bundle? savedInstanceState)
LogManagement.ForceOpenGLRendererIfSafeMode();
CrashDiagnostics.WriteAliveMarker("LogManagement.ForceOpenGLRendererIfSafeMode (returned)");

// Vulkan cold-start present-mode deferral: temporarily force VSync (FIFO)
// in framework.ini so the swapchain is created in safe FIFO mode. The
// original IMMEDIATE-mode value is restored by OsuGameAndroid after the
// Draw thread presents its first frame. This prevents the Adreno 7xx
// vkQueuePresentKHR stall during the texture-upload burst that causes
// black screen + ANR on cold start.
CrashDiagnostics.WriteAliveMarker("LogManagement.ForceVSyncDuringVulkanColdStart (about to start)");
LogManagement.ForceVSyncDuringVulkanColdStart();
CrashDiagnostics.WriteAliveMarker("LogManagement.ForceVSyncDuringVulkanColdStart (returned)");

CrashDiagnostics.WriteAliveMarker("LogManagement.WipeShaderCacheOnceForVersion (about to start)");
LogManagement.WipeShaderCacheOnceForVersion();
CrashDiagnostics.WriteAliveMarker("LogManagement.WipeShaderCacheOnceForVersion (returned)");
Expand Down
38 changes: 38 additions & 0 deletions osu.Android/OsuGameAndroid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ public partial class OsuGameAndroid : OsuGame
// wedged renderer.
private volatile bool drawThreadEverPresented;

/// <summary>
/// Cached reference to <see cref="FrameworkConfigManager"/> for use in the
/// draw-thread heartbeat lambda, which restores the original <c>FrameSync</c>
/// value after the first frame presents (see <see cref="LogManagement.ForceVSyncDuringVulkanColdStart"/>).
/// </summary>
private FrameworkConfigManager? cachedFrameworkConfig;

// Set true by the deferred SelectHighestRefreshRate call in LoadComplete; gates
// any earlier OnConfigurationChanged-driven SelectHighestRefreshRate() invocations
// out of the cold-start swapchain bring-up window. See SelectHighestRefreshRate.
Expand Down Expand Up @@ -257,6 +264,8 @@ public override Version AssemblyVersion
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager frameworkConfig)
{
cachedFrameworkConfig = frameworkConfig;

LocalConfig.BindWith(OsuSetting.AndroidPerformanceMode, performanceMode);
LocalConfig.BindWith(OsuSetting.AndroidAudioOutput, audioOutput);
LocalConfig.BindWith(OsuSetting.AndroidVulkanProbe, vulkanProbeEnabled);
Expand Down Expand Up @@ -869,6 +878,35 @@ protected override void LoadComplete()
{
Debug.WriteLine($"[osu!] Could not queue ClearStartupInProgress from Draw-thread heartbeat: {queueEx.Message}");
}

// Restore the original FrameSync value that was temporarily
// forced to VSync by LogManagement.ForceVSyncDuringVulkanColdStart.
// Now that the Draw thread is demonstrably healthy (first frame
// presented), it is safe to switch the swapchain from FIFO to
// IMMEDIATE — the texture-upload burst is past and the GPU can
// handle the recreation without stalling.
try
{
string? savedFrameSync = AndroidStartupFlags.ReadValue(
AndroidStartupFlags.FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE);
Comment on lines +888 to +891

if (savedFrameSync != null && cachedFrameworkConfig != null)
{
if (Enum.TryParse<FrameSync>(savedFrameSync, out var originalFrameSync))
{
cachedFrameworkConfig.SetValue(FrameworkSetting.FrameSync, originalFrameSync);
Logger.Log($"[osu!] Vulkan cold-start: restored FrameSync to {originalFrameSync} after first frame", LoggingTarget.Performance);
}

// Delete the flag regardless of parse success so we don't
// re-attempt restoration on the next Draw-thread tick.
AndroidStartupFlags.Set(AndroidStartupFlags.FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE, false);
}
Comment on lines +890 to +904
}
catch (Exception restoreEx)
{
Debug.WriteLine($"[osu!] Vulkan cold-start FrameSync restore failed: {restoreEx.Message}");
}
});
}
catch (Exception ex)
Expand Down
4 changes: 2 additions & 2 deletions osu.Game/osu.Game.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2026.526.1" />
<PackageReference Include="ppy.osu.Framework" Version="2026.527.1" />
<!--
Explicitly pin `ppy.Veldrid.SPIRV` to the winnerspiros fork build that
`ppy.osu.Framework 2026.526.1` was compiled against. This version is the only
`ppy.osu.Framework 2026.527.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
Expand Down
2 changes: 1 addition & 1 deletion osu.iOS.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.526.1" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.527.1" />
</ItemGroup>
</Project>
Loading