From 3501a7d28379ce3c7f1d8597158de13bb57894bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 13:44:58 +0000 Subject: [PATCH 1/4] fix: Limit1x FrameSync (prevents GPU thermal throttle), Samsung Game Launcher, audio offset UX Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/62d6e310-b56e-4f11-9f94-fdff8c3c665d Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android/OsuGameActivity.cs | 34 ++++++++++++ osu.Android/OsuGameAndroid.cs | 53 ++++++++++++++----- osu.Game/Configuration/OsuConfigManager.cs | 2 + .../Sections/Audio/AndroidAudioSettings.cs | 18 ++++++- 4 files changed, 93 insertions(+), 14 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 9fd008df7ec0..cd39a19a52e5 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -49,6 +49,14 @@ namespace osu.Android "application/x-osu-replay", })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] + // Samsung Game Launcher / Game Booster discovery for sideloaded APKs. + // Apps installed via Play Store/Galaxy Store are auto-discovered as games via the + // PACKAGE_ADDED broadcast + server-side category database. Sideloaded APKs bypass + // this path entirely. Adding the Samsung game category to an intent-filter on the + // main activity is the supported way to signal to Samsung's Game Launcher package + // scanner that this activity is a game entry point — it scans for activities with + // this category during app install and on periodic rescans. + [IntentFilter(new[] { Intent.ActionMain }, Categories = new[] { "com.samsung.intent.category.GAME" })] public class OsuGameActivity : AndroidGameActivity, ISurfaceHolderCallback { private static readonly string[] osu_url_schemes = { "osu", "osump" }; @@ -458,6 +466,32 @@ protected override void OnCreate(Bundle? savedInstanceState) catch (Exception e) { Debug.WriteLine($"[osu!] Failed to load ruleset assembly {asm}: {e.Message}"); } } + // Samsung Game Launcher self-registration for sideloaded APKs. + // + // Play Store / Galaxy Store installs are auto-discovered by Samsung Game Launcher + // through the PACKAGE_ADDED broadcast it receives at install time, plus its server-side + // game database. Sideloaded APKs bypass both paths entirely — Game Launcher may never + // add the app unless the user manually taps "+" in the Game Launcher UI. + // + // Sending a targeted broadcast to com.samsung.android.game.gameLauncher on every + // launch requests an immediate rescan of our package. Since Android 8.0 implicit + // broadcasts are blocked, we target the package explicitly via setPackage() — the + // broadcast is silently dropped on non-Samsung devices where Game Launcher is absent. + // + // This is a best-effort signal; Game Launcher may still require one manual "Add" + // on very old One UI builds that pre-date the REQUEST_ADD_PACKAGE handler. + try + { + var gameLauncherIntent = new Intent("com.samsung.android.game.gameLauncher.REQUEST_ADD_PACKAGE"); + gameLauncherIntent.SetPackage("com.samsung.android.game.gameLauncher"); + gameLauncherIntent.PutExtra("packageName", PackageName); + SendBroadcast(gameLauncherIntent); + } + catch + { + // Samsung Game Launcher not present (non-Samsung device) or broadcast failed — not an error. + } + CrashDiagnostics.WriteAliveMarker("Activity.OnCreate exit"); } diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 60dce0ab9c99..a65fc4db0efa 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -2171,7 +2171,7 @@ private void updateOrientation() /// /// One-shot migration that switches Android-side from the - /// framework default of to . + /// framework default of to . /// /// /// On a 120Hz Adreno-class display (Snapdragon 8 Gen 2 / S23 Ultra), @@ -2185,11 +2185,20 @@ private void updateOrientation() /// /// /// - /// uses Vulkan IMMEDIATE present mode (VK_PRESENT_MODE_IMMEDIATE_KHR) - /// which presents each frame as soon as it is ready without waiting for vblank. - /// Combined with VK_GOOGLE_display_timing (skipping desiredPresentTime in IMMEDIATE mode), - /// this delivers the lowest possible input-to-display latency while avoiding the - /// vkAcquireNextImageKHR queue pile-up of Limit2x. The migration runs exactly once per + /// (IMMEDIATE present mode) previously addressed + /// the Limit2x stall but presents frames at an unlimited rate — the GPU submits work + /// continuously with no idle window between frames. On Snapdragon 8 Gen 2 / Adreno 740 + /// this drives the SoC to its thermal ceiling within ~30 s of gameplay, tripping the + /// kernel's thermal mitigation and hard-capping the GPU at ~30fps — exactly the + /// "bad FPS" and overheating the user experiences. + /// + /// + /// + /// targets exactly 1× the display refresh rate (e.g. + /// 120fps on a 120Hz panel). Because presents arrive at the same cadence as vblank + /// recycles, vkAcquireNextImageKHR always finds an idle swapchain image + /// (no queue pile-up), AND the GPU gets a natural idle window every frame — keeping + /// thermals stable and preventing the 30fps throttle. The migration runs once per /// install (gated by ) /// so a user who later prefers a different mode from Settings → Graphics → Renderer /// is not fought on every launch. @@ -2205,22 +2214,39 @@ private void applyAndroidFrameSyncMigrationOnce(FrameworkConfigManager framework CrashDiagnostics.WriteAliveMarker("applyAndroidFrameSyncMigrationOnce (already applied)"); // v2 migration: upgrade users who were previously migrated to VSync (by an older - // build) to ActualUnlimited. Only applies if: + // build) to Limit1x. Only applies if: // 1. The v2 migration hasn't run yet. // 2. The user is currently on VSync (hasn't manually changed it since v1). - // This gives existing users the lower-latency uncapped mode without overriding - // deliberate user choices. if (!LocalConfig.Get(OsuSetting.AndroidStartupFrameSyncV2MigrationApplied)) { var frameSync = frameworkConfig.GetBindable(FrameworkSetting.FrameSync); if (frameSync.Value == FrameSync.VSync) { - frameSync.Value = FrameSync.ActualUnlimited; - Logger.Log("[osu!] Android FrameSync v2 migration: VSync → ActualUnlimited (IMMEDIATE present mode, lower latency)", LoggingTarget.Performance); + frameSync.Value = FrameSync.Limit1x; + Logger.Log("[osu!] Android FrameSync v2 migration: VSync → Limit1x (display-rate cap, no thermal stall)", LoggingTarget.Performance); } LocalConfig.SetValue(OsuSetting.AndroidStartupFrameSyncV2MigrationApplied, true); } + // v3 migration: downgrade users who were previously auto-migrated to + // ActualUnlimited (IMMEDIATE present mode) back to Limit1x. ActualUnlimited + // drives the GPU at 100% continuously with no idle window, which overheats + // Adreno 740 / Samsung devices and triggers kernel thermal throttling to ~30fps + // — the exact symptom users experience as "weird Vulkan FPS". Limit1x caps at + // exactly the display refresh rate so the GPU idles between frames, keeping + // thermals stable. We only apply if the user is currently on ActualUnlimited + // (if they manually picked a different mode after v2, leave them alone). + if (!LocalConfig.Get(OsuSetting.AndroidStartupFrameSyncV3MigrationApplied)) + { + var frameSync = frameworkConfig.GetBindable(FrameworkSetting.FrameSync); + if (frameSync.Value == FrameSync.ActualUnlimited) + { + frameSync.Value = FrameSync.Limit1x; + Logger.Log("[osu!] Android FrameSync v3 migration: ActualUnlimited → Limit1x (prevents GPU thermal throttle to 30fps)", LoggingTarget.Performance); + } + LocalConfig.SetValue(OsuSetting.AndroidStartupFrameSyncV3MigrationApplied, true); + } + return; } @@ -2231,12 +2257,13 @@ private void applyAndroidFrameSyncMigrationOnce(FrameworkConfigManager framework // the migration's job is to nudge the *default*, not to overwrite intent. if (frameSyncV1.Value == FrameSync.Limit2x) { - frameSyncV1.Value = FrameSync.ActualUnlimited; - Logger.Log("[osu!] Android first-launch FrameSync migration: Limit2x → ActualUnlimited (IMMEDIATE present, no vblank stall)", LoggingTarget.Performance); + frameSyncV1.Value = FrameSync.Limit1x; + Logger.Log("[osu!] Android first-launch FrameSync migration: Limit2x → Limit1x (display-rate cap, prevents vkAcquireNextImageKHR stall and GPU thermal throttle)", LoggingTarget.Performance); } LocalConfig.SetValue(OsuSetting.AndroidStartupFrameSyncMigrationApplied, true); LocalConfig.SetValue(OsuSetting.AndroidStartupFrameSyncV2MigrationApplied, true); + LocalConfig.SetValue(OsuSetting.AndroidStartupFrameSyncV3MigrationApplied, true); } catch (Exception e) { diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 1056ee9e1df7..0771e110a953 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -278,6 +278,7 @@ protected override void InitialiseDefaults() SetDefault(OsuSetting.AndroidVulkanProbe, false); SetDefault(OsuSetting.AndroidStartupFrameSyncMigrationApplied, false); SetDefault(OsuSetting.AndroidStartupFrameSyncV2MigrationApplied, false); + SetDefault(OsuSetting.AndroidStartupFrameSyncV3MigrationApplied, false); // --- Android startup-safety toggles --- // @@ -582,6 +583,7 @@ public enum OsuSetting AndroidVulkanProbe, AndroidStartupFrameSyncMigrationApplied, AndroidStartupFrameSyncV2MigrationApplied, + AndroidStartupFrameSyncV3MigrationApplied, AndroidCleanupStaleRealmFifos, AndroidDeferStartupNativeInit, AndroidStartupFrameSyncMigrationEnabled, diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AndroidAudioSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AndroidAudioSettings.cs index cebf53d18044..682dfee05eb3 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AndroidAudioSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AndroidAudioSettings.cs @@ -59,10 +59,26 @@ private void load(OsuConfigManager config, OsuGame? game) // // After a resync the restore button below becomes active so users can undo // if the hardware measurement doesn't match their perception. + // + // IMPORTANT — Bluetooth speakers/headphones: + // The AAudio measurement captures the device's internal audio pipeline + // latency (DAC + driver buffer). It does NOT include Bluetooth A2DP + // transmission time, which adds a further ~100–300 ms of device-to-device + // wireless delay that AAudio cannot observe. For BT output, resync will + // give a partially-correct value; you must further adjust the audio offset + // manually (positive = audio arrives later than visuals; negative = earlier) + // until hit sounds and music land where they feel right in your ears. + // + // Note: AudioOffset shifts the ENTIRE gameplay clock — audio track, hit + // object visual timing, and hit sound effects all move together. This keeps + // everything internally consistent regardless of the offset value you choose. new SettingsButtonV2 { Text = "Resync hardware audio offset", - TooltipText = "Measures the device's reported hardware output latency over a 2 s window and applies the median to the audio offset above. Previous offset is saved and can be restored.", + TooltipText = "Measures the device's AAudio pipeline latency over 2 s and applies the median to the audio offset. " + + "NOTE: does NOT include Bluetooth transmission delay (~100–300 ms extra). " + + "For Bluetooth speakers/headphones, resync first, then fine-tune the offset manually until music and hit sounds feel right. " + + "The offset shifts the entire game clock — audio, hit objects, and effects all move together.", Action = () => game?.ResyncHardwareAudioOffset(), Keywords = new[] { @"resync", @"recalibrate", @"offset", @"hardware", @"latency", @"calibration" }, }, From da128dc76a7b49e2d5916e2d5f92c28985d1f8d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 14:00:55 +0000 Subject: [PATCH 2/4] revert: restore ActualUnlimited FrameSync; v3 migration rescues Limit1x users Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/09cd040f-c004-401d-925b-43622580ffdb Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android/OsuGameAndroid.cs | 63 ++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index a65fc4db0efa..debfaff63f9e 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -2171,7 +2171,7 @@ private void updateOrientation() /// /// One-shot migration that switches Android-side from the - /// framework default of to . + /// framework default of to . /// /// /// On a 120Hz Adreno-class display (Snapdragon 8 Gen 2 / S23 Ultra), @@ -2185,24 +2185,25 @@ private void updateOrientation() /// /// /// - /// (IMMEDIATE present mode) previously addressed - /// the Limit2x stall but presents frames at an unlimited rate — the GPU submits work - /// continuously with no idle window between frames. On Snapdragon 8 Gen 2 / Adreno 740 - /// this drives the SoC to its thermal ceiling within ~30 s of gameplay, tripping the - /// kernel's thermal mitigation and hard-capping the GPU at ~30fps — exactly the - /// "bad FPS" and overheating the user experiences. - /// - /// - /// - /// targets exactly 1× the display refresh rate (e.g. - /// 120fps on a 120Hz panel). Because presents arrive at the same cadence as vblank - /// recycles, vkAcquireNextImageKHR always finds an idle swapchain image - /// (no queue pile-up), AND the GPU gets a natural idle window every frame — keeping - /// thermals stable and preventing the 30fps throttle. The migration runs once per + /// uses Vulkan IMMEDIATE present mode (VK_PRESENT_MODE_IMMEDIATE_KHR) + /// which presents each frame as soon as it is ready without waiting for vblank. + /// Combined with VK_GOOGLE_display_timing (skipping desiredPresentTime in IMMEDIATE mode), + /// this delivers the lowest possible input-to-display latency while avoiding the + /// vkAcquireNextImageKHR queue pile-up of Limit2x. The migration runs exactly once per /// install (gated by ) /// so a user who later prefers a different mode from Settings → Graphics → Renderer /// is not fought on every launch. /// + /// + /// + /// Field observations: 27fps at 9ms CPU frame time and 30fps at 1.7ms CPU frame time are + /// both present-queue stall patterns — the CPU is finishing frames quickly but the Vulkan + /// FIFO present queue cannot drain fast enough. IMMEDIATE mode resolves this by bypassing + /// the vblank synchronisation barrier entirely, so every finished frame goes straight to + /// the display engine. VSync or Limit1x (FIFO) amplify the stall during texture-upload + /// storms (100-300 queued uploads logged during song select), making them the wrong choice + /// for this rendering workload. + /// /// private void applyAndroidFrameSyncMigrationOnce(FrameworkConfigManager frameworkConfig) { @@ -2214,35 +2215,35 @@ private void applyAndroidFrameSyncMigrationOnce(FrameworkConfigManager framework CrashDiagnostics.WriteAliveMarker("applyAndroidFrameSyncMigrationOnce (already applied)"); // v2 migration: upgrade users who were previously migrated to VSync (by an older - // build) to Limit1x. Only applies if: + // build) to ActualUnlimited. Only applies if: // 1. The v2 migration hasn't run yet. // 2. The user is currently on VSync (hasn't manually changed it since v1). + // This gives existing users the lower-latency uncapped mode without overriding + // deliberate user choices. if (!LocalConfig.Get(OsuSetting.AndroidStartupFrameSyncV2MigrationApplied)) { var frameSync = frameworkConfig.GetBindable(FrameworkSetting.FrameSync); if (frameSync.Value == FrameSync.VSync) { - frameSync.Value = FrameSync.Limit1x; - Logger.Log("[osu!] Android FrameSync v2 migration: VSync → Limit1x (display-rate cap, no thermal stall)", LoggingTarget.Performance); + frameSync.Value = FrameSync.ActualUnlimited; + Logger.Log("[osu!] Android FrameSync v2 migration: VSync → ActualUnlimited (IMMEDIATE present mode, lower latency)", LoggingTarget.Performance); } LocalConfig.SetValue(OsuSetting.AndroidStartupFrameSyncV2MigrationApplied, true); } - // v3 migration: downgrade users who were previously auto-migrated to - // ActualUnlimited (IMMEDIATE present mode) back to Limit1x. ActualUnlimited - // drives the GPU at 100% continuously with no idle window, which overheats - // Adreno 740 / Samsung devices and triggers kernel thermal throttling to ~30fps - // — the exact symptom users experience as "weird Vulkan FPS". Limit1x caps at - // exactly the display refresh rate so the GPU idles between frames, keeping - // thermals stable. We only apply if the user is currently on ActualUnlimited - // (if they manually picked a different mode after v2, leave them alone). + // v3 migration: restore users who were incorrectly moved to Limit1x (FIFO) by a + // previous build back to ActualUnlimited (IMMEDIATE present mode). Limit1x + // amplifies vkQueuePresentKHR stalls during texture-upload storms (100-300 items + // queued during song-select), producing the 27fps-at-9ms-frame-time pattern + // observed in field logs. We only apply if the user is currently on Limit1x + // (if they manually changed to any other mode, leave them alone). if (!LocalConfig.Get(OsuSetting.AndroidStartupFrameSyncV3MigrationApplied)) { var frameSync = frameworkConfig.GetBindable(FrameworkSetting.FrameSync); - if (frameSync.Value == FrameSync.ActualUnlimited) + if (frameSync.Value == FrameSync.Limit1x) { - frameSync.Value = FrameSync.Limit1x; - Logger.Log("[osu!] Android FrameSync v3 migration: ActualUnlimited → Limit1x (prevents GPU thermal throttle to 30fps)", LoggingTarget.Performance); + frameSync.Value = FrameSync.ActualUnlimited; + Logger.Log("[osu!] Android FrameSync v3 migration: Limit1x → ActualUnlimited (IMMEDIATE present, fixes vkQueuePresentKHR stall during texture-upload storms)", LoggingTarget.Performance); } LocalConfig.SetValue(OsuSetting.AndroidStartupFrameSyncV3MigrationApplied, true); } @@ -2257,8 +2258,8 @@ private void applyAndroidFrameSyncMigrationOnce(FrameworkConfigManager framework // the migration's job is to nudge the *default*, not to overwrite intent. if (frameSyncV1.Value == FrameSync.Limit2x) { - frameSyncV1.Value = FrameSync.Limit1x; - Logger.Log("[osu!] Android first-launch FrameSync migration: Limit2x → Limit1x (display-rate cap, prevents vkAcquireNextImageKHR stall and GPU thermal throttle)", LoggingTarget.Performance); + frameSyncV1.Value = FrameSync.ActualUnlimited; + Logger.Log("[osu!] Android first-launch FrameSync migration: Limit2x → ActualUnlimited (IMMEDIATE present, no vblank stall)", LoggingTarget.Performance); } LocalConfig.SetValue(OsuSetting.AndroidStartupFrameSyncMigrationApplied, true); From 6fc896a1365d50e9d823a0b92d90b15c74fe76ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 17:29:55 +0000 Subject: [PATCH 3/4] bump osu-framework to 2026.504.3 (input spinloop fix + legacy VkRenderPass TBR clear) Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/0bbf55a6-e45a-4dd0-af20-c261afb97c7c Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 4 ++-- osu.iOS.props | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 40fd14d01e0d..dc4895e88c36 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -99,7 +99,7 @@ - +