Skip to content

Commit 517a7af

Browse files
authored
Merge pull request #359 from winnerspiros/copilot/fix-vulkan-black-screen-another-one
Merge ppy/master + bump framework to winnerspiros 2026.527.1
2 parents 1d04468 + c15646e commit 517a7af

7 files changed

Lines changed: 173 additions & 4 deletions

File tree

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.526.1" />
55+
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.527.1" />
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/AndroidStartupFlags.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ internal static class AndroidStartupFlags
9393
/// </summary>
9494
public const string FLAG_LAST_NATIVE_CRASH_CONSUMED = "android_last_native_crash_consumed.flag";
9595

96+
/// <summary>
97+
/// Stores the original <c>FrameSync</c> value that was temporarily overwritten to
98+
/// <c>VSync</c> by <see cref="LogManagement.ForceVSyncDuringVulkanColdStart"/> before
99+
/// framework init. After the Draw thread presents its first frame,
100+
/// <see cref="OsuGameAndroid"/> reads this value and restores it via the framework
101+
/// config manager (applying it in-memory AND persisting to <c>framework.ini</c>),
102+
/// then deletes the flag so subsequent launches repeat the same cycle.
103+
/// </summary>
104+
public const string FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE = "android_vulkan_cold_start_frame_sync_restore.flag";
105+
96106
private static string? resolveDir()
97107
{
98108
try

osu.Android/LogManagement.cs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,117 @@ public static bool IsVulkanConfigured()
746746
return renderer != null && string.Equals(renderer, "Vulkan", StringComparison.OrdinalIgnoreCase);
747747
}
748748

749+
/// <summary>
750+
/// Temporarily forces <c>FrameSync = VSync</c> in <c>framework.ini</c> when
751+
/// Vulkan is configured and the persisted value maps to IMMEDIATE present mode
752+
/// (i.e. <c>ActualUnlimited</c> or <c>Unlimited</c>).
753+
///
754+
/// <para>
755+
/// <b>Why:</b> On Adreno 7xx (Snapdragon 8 Gen 2/3), applying
756+
/// <c>VK_PRESENT_MODE_IMMEDIATE_KHR</c> during the cold-start texture-upload burst
757+
/// triggers a swapchain recreation (FIFO → IMMEDIATE) while hundreds of textures
758+
/// are being uploaded. The Vulkan driver stalls in <c>vkQueuePresentKHR</c>,
759+
/// blocking the Draw thread indefinitely and producing a black screen + ANR.
760+
/// </para>
761+
///
762+
/// <para>
763+
/// <b>How:</b> Before the framework reads <c>framework.ini</c> (in OnCreate,
764+
/// before <c>base.OnCreate</c>), we rewrite <c>FrameSync</c> to <c>VSync</c> so
765+
/// the swapchain is created in FIFO mode (safe). The original value is saved to
766+
/// <see cref="AndroidStartupFlags.FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE"/>
767+
/// and restored by <see cref="OsuGameAndroid"/> after the Draw thread presents
768+
/// its first frame (via <c>FrameworkConfigManager.SetValue</c>, which applies
769+
/// in-memory AND persists to disk).
770+
/// </para>
771+
///
772+
/// <para>
773+
/// <b>Safety:</b> If the process dies before restoration, next launch finds
774+
/// <c>FrameSync = VSync</c> in the ini (safe FIFO cold start) plus the restore
775+
/// flag still on disk, so the same deferred-switch cycle repeats. No user-visible
776+
/// permanent change to the config.
777+
/// </para>
778+
/// </summary>
779+
public static void ForceVSyncDuringVulkanColdStart()
780+
{
781+
try
782+
{
783+
if (!IsVulkanConfigured())
784+
return;
785+
786+
// If safe-mode is active, ForceOpenGLRendererIfSafeMode already switched
787+
// to OpenGL — no Vulkan swapchain will be created, so no override needed.
788+
if (AndroidStartupSafeMode.IsActive)
789+
return;
790+
791+
string? root = resolveStorageRoot();
792+
if (root == null) return;
793+
794+
string iniPath = Path.Combine(root, "framework.ini");
795+
if (!File.Exists(iniPath)) return;
796+
797+
string[] lines;
798+
799+
try
800+
{
801+
lines = File.ReadAllLines(iniPath);
802+
}
803+
catch (Exception e)
804+
{
805+
Debug.WriteLine($"[osu!] LogManagement: could not read framework.ini for Vulkan cold-start VSync override: {e.Message}");
806+
return;
807+
}
808+
809+
bool changed = false;
810+
string? originalValue = null;
811+
812+
for (int i = 0; i < lines.Length; i++)
813+
{
814+
string line = lines[i];
815+
int eq = line.IndexOf('=');
816+
if (eq <= 0) continue;
817+
818+
string key = line.Substring(0, eq).Trim();
819+
string value = line.Substring(eq + 1).Trim();
820+
821+
if (!string.Equals(key, "FrameSync", StringComparison.Ordinal))
822+
continue;
823+
824+
// Only override values that map to IMMEDIATE present mode.
825+
// VSync and Limit2x use FIFO, which is safe during cold start.
826+
if (string.Equals(value, "ActualUnlimited", StringComparison.Ordinal)
827+
|| string.Equals(value, "Unlimited", StringComparison.Ordinal))
828+
{
829+
originalValue = value;
830+
lines[i] = "FrameSync = VSync";
831+
changed = true;
832+
}
833+
834+
break;
835+
}
836+
837+
if (!changed || originalValue == null) return;
838+
839+
// Save the original value so OsuGameAndroid can restore it after first frame.
840+
AndroidStartupFlags.WriteValue(AndroidStartupFlags.FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE, originalValue);
841+
842+
try
843+
{
844+
File.WriteAllLines(iniPath, lines);
845+
Logger.Log($"[osu!] Vulkan cold-start protection: FrameSync {originalValue} → VSync (FIFO) until first frame presents", LoggingTarget.Performance);
846+
}
847+
catch (Exception e)
848+
{
849+
Debug.WriteLine($"[osu!] LogManagement: could not rewrite framework.ini for Vulkan cold-start VSync override: {e.Message}");
850+
// Clean up the flag since we couldn't apply the override
851+
AndroidStartupFlags.Set(AndroidStartupFlags.FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE, false);
852+
}
853+
}
854+
catch (Exception e)
855+
{
856+
Debug.WriteLine($"[osu!] LogManagement: ForceVSyncDuringVulkanColdStart failed: {e.Message}");
857+
}
858+
}
859+
749860
// Sentinel file dropped after a successful one-shot shader-cache wipe.
750861
// Stored alongside the cache itself (not in the cache directory, which
751862
// we delete) so the marker survives the wipe. The file payload is the

osu.Android/OsuGameActivity.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,16 @@ protected override void OnCreate(Bundle? savedInstanceState)
240240
LogManagement.ForceOpenGLRendererIfSafeMode();
241241
CrashDiagnostics.WriteAliveMarker("LogManagement.ForceOpenGLRendererIfSafeMode (returned)");
242242

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

osu.Android/OsuGameAndroid.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ public partial class OsuGameAndroid : OsuGame
152152
// wedged renderer.
153153
private volatile bool drawThreadEverPresented;
154154

155+
/// <summary>
156+
/// Cached reference to <see cref="FrameworkConfigManager"/> for use in the
157+
/// draw-thread heartbeat lambda, which restores the original <c>FrameSync</c>
158+
/// value after the first frame presents (see <see cref="LogManagement.ForceVSyncDuringVulkanColdStart"/>).
159+
/// </summary>
160+
private FrameworkConfigManager? cachedFrameworkConfig;
161+
155162
// Set true by the deferred SelectHighestRefreshRate call in LoadComplete; gates
156163
// any earlier OnConfigurationChanged-driven SelectHighestRefreshRate() invocations
157164
// out of the cold-start swapchain bring-up window. See SelectHighestRefreshRate.
@@ -257,6 +264,8 @@ public override Version AssemblyVersion
257264
[BackgroundDependencyLoader]
258265
private void load(FrameworkConfigManager frameworkConfig)
259266
{
267+
cachedFrameworkConfig = frameworkConfig;
268+
260269
LocalConfig.BindWith(OsuSetting.AndroidPerformanceMode, performanceMode);
261270
LocalConfig.BindWith(OsuSetting.AndroidAudioOutput, audioOutput);
262271
LocalConfig.BindWith(OsuSetting.AndroidVulkanProbe, vulkanProbeEnabled);
@@ -869,6 +878,35 @@ protected override void LoadComplete()
869878
{
870879
Debug.WriteLine($"[osu!] Could not queue ClearStartupInProgress from Draw-thread heartbeat: {queueEx.Message}");
871880
}
881+
882+
// Restore the original FrameSync value that was temporarily
883+
// forced to VSync by LogManagement.ForceVSyncDuringVulkanColdStart.
884+
// Now that the Draw thread is demonstrably healthy (first frame
885+
// presented), it is safe to switch the swapchain from FIFO to
886+
// IMMEDIATE — the texture-upload burst is past and the GPU can
887+
// handle the recreation without stalling.
888+
try
889+
{
890+
string? savedFrameSync = AndroidStartupFlags.ReadValue(
891+
AndroidStartupFlags.FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE);
892+
893+
if (savedFrameSync != null && cachedFrameworkConfig != null)
894+
{
895+
if (Enum.TryParse<FrameSync>(savedFrameSync, out var originalFrameSync))
896+
{
897+
cachedFrameworkConfig.SetValue(FrameworkSetting.FrameSync, originalFrameSync);
898+
Logger.Log($"[osu!] Vulkan cold-start: restored FrameSync to {originalFrameSync} after first frame", LoggingTarget.Performance);
899+
}
900+
901+
// Delete the flag regardless of parse success so we don't
902+
// re-attempt restoration on the next Draw-thread tick.
903+
AndroidStartupFlags.Set(AndroidStartupFlags.FLAG_VULKAN_COLD_START_FRAME_SYNC_RESTORE, false);
904+
}
905+
}
906+
catch (Exception restoreEx)
907+
{
908+
Debug.WriteLine($"[osu!] Vulkan cold-start FrameSync restore failed: {restoreEx.Message}");
909+
}
872910
});
873911
}
874912
catch (Exception ex)

osu.Game/osu.Game.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@
3838
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3939
</PackageReference>
4040
<PackageReference Include="Realm" Version="20.1.0" />
41-
<PackageReference Include="ppy.osu.Framework" Version="2026.526.1" />
41+
<PackageReference Include="ppy.osu.Framework" Version="2026.527.1" />
4242
<!--
4343
Explicitly pin `ppy.Veldrid.SPIRV` to the winnerspiros fork build that
44-
`ppy.osu.Framework 2026.526.1` was compiled against. This version is the only
44+
`ppy.osu.Framework 2026.527.1` was compiled against. This version is the only
4545
one whose `runtimes/android-arm64/native/libveldrid-spirv.so` is aligned to 16 KB
4646
pages (required by Android 16+). It lives only as a release asset on
4747
<https://github.com/winnerspiros/veldrid-spirv/releases/tag/1.0> and is vendored

osu.iOS.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@
3333
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
3434
</PropertyGroup>
3535
<ItemGroup>
36-
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.526.1" />
36+
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.527.1" />
3737
</ItemGroup>
3838
</Project>

0 commit comments

Comments
 (0)