diff --git a/osu.Android/AndroidNativeBridgeManager.cs b/osu.Android/AndroidNativeBridgeManager.cs index a82598f96b63..01a9d20ab7c3 100644 --- a/osu.Android/AndroidNativeBridgeManager.cs +++ b/osu.Android/AndroidNativeBridgeManager.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.CompilerServices; using osu.Android.Native; +using osu.Framework.Logging; using osu.Framework.Threading; using Debug = System.Diagnostics.Debug; @@ -32,7 +33,7 @@ public void StartOboeBridge(Scheduler scheduler, Action onLatencyMeasure return; } - Debug.WriteLine($"[osu!] Starting Oboe bridge (sampleRate={sampleRate}, hasProvider={provider != IntPtr.Zero})"); + Logger.Log($"[osu!] Starting Oboe bridge (sampleRate={sampleRate}, hasProvider={provider != IntPtr.Zero})"); cachedOboeStatus = null; try @@ -69,7 +70,7 @@ public void StartOboeBridge(Scheduler scheduler, Action onLatencyMeasure if (started) { - Debug.WriteLine("[osu!] Oboe bridge started successfully"); + Logger.Log("[osu!] Oboe bridge started successfully"); logOboeInfo(bridge); onStarted?.Invoke(bridge.SampleRate); @@ -87,17 +88,17 @@ public void StartOboeBridge(Scheduler scheduler, Action onLatencyMeasure else { string error = bridge.GetLastErrorMessage() ?? "Unknown"; - Debug.WriteLine($"[osu!] Oboe bridge created but failed to start: {error}"); + Logger.Log($"[osu!] Oboe bridge created but failed to start: {error}", level: LogLevel.Error); } } else { - Debug.WriteLine("[osu!] Oboe bridge creation failed — native library not loaded or stream open failed"); + Logger.Log("[osu!] Oboe bridge creation failed — native library not loaded or stream open failed", level: LogLevel.Error); } } catch (Exception e) { - Debug.WriteLine($"[osu!] Oboe bridge init failed with exception: {e.Message}"); + Logger.Log($"[osu!] Oboe bridge init failed with exception: {e.Message}", level: LogLevel.Error); } } } @@ -107,11 +108,11 @@ public void StopOboeBridge() { lock (oboeLock) { - Debug.WriteLine("[osu!] Stopping Oboe bridge..."); + Logger.Log("[osu!] Stopping Oboe bridge..."); (oboeBridge as OboeAudioBridge)?.Dispose(); oboeBridge = null; cachedOboeStatus = null; - Debug.WriteLine("[osu!] Oboe bridge stopped"); + Logger.Log("[osu!] Oboe bridge stopped"); } } diff --git a/osu.Android/AndroidStartupFlags.cs b/osu.Android/AndroidStartupFlags.cs index 20ab6a1933d2..2a2d558eec89 100644 --- a/osu.Android/AndroidStartupFlags.cs +++ b/osu.Android/AndroidStartupFlags.cs @@ -42,6 +42,17 @@ internal static class AndroidStartupFlags public const string FLAG_DEFER_NATIVE_INIT_DISABLED = "android_startup_disable_defer_native_init.flag"; public const string FLAG_FRAME_SYNC_MIGRATION_ENABLED = "android_startup_enable_frame_sync_migration.flag"; + /// + /// Verbose-logging opt-in sentinel. Presence ⇒ "user has enabled + /// verbose framework logging". Absence ⇒ "default, quiet logging + /// (Important+ only)". Quiet is the default because the framework's + /// runtime/input log is ~330+ KB per launch on Android (mostly + /// OpenTabletDriver detection + SDL platform chatter) and is not + /// useful in the steady state. Toggle from + /// Settings → Graphics → Android Performance. + /// + public const string FLAG_VERBOSE_LOGGING_ENABLED = "android_startup_enable_verbose_logging.flag"; + /// /// "Startup in progress" sentinel. Dropped near the very top of /// and cleared a few seconds after OsuGame.LoadComplete by . diff --git a/osu.Android/LogManagement.cs b/osu.Android/LogManagement.cs index 7b01135abf2d..f69c176c6ddc 100644 --- a/osu.Android/LogManagement.cs +++ b/osu.Android/LogManagement.cs @@ -58,15 +58,31 @@ internal static class LogManagement /// public static void Apply() { - // NOTE: we used to force Logger.Level = LogLevel.Important here to - // shrink runtime log output during the "log explosion" debugging - // window. That has been reverted at user request — the default - // framework log verbosity is now restored so osu.log captures the - // full per-thread startup narrative we need to diagnose hangs. - // Log size is still bounded by pruneLogDirectory() below - // (MAX_LOG_BYTES cap with oldest-first eviction), so re-enabling - // verbose logging cannot regress the on-disk footprint that the - // 480 MB report originally exposed. + // Verbose-logging toggle (default OFF). The framework writes + // ~330 KB of runtime.log + ~28 KB of input.log per launch at the + // default Verbose level, dominated by OpenTabletDriver per-tablet + // detection and SDL platform-feature probe chatter — useful when + // diagnosing a hang, not useful in the steady state. Default to + // Important so on-disk log volume drops to a few KB per launch + // and audio/render hot paths spend zero time formatting log + // messages. Users can re-enable verbose logging from + // Settings → Graphics → Android Performance to capture a full + // log when they need to share one. + // + // Sentinel-driven (not config-driven) because LogManagement.Apply + // runs in OsuGameActivity.OnCreate, LONG before the + // OsuConfigManager exists — same pattern as the other Android + // startup-safety flags. OsuGameAndroid mirrors the in-game + // bindable into the sentinel via mirrorStartupFlag. + try + { + bool verbose = AndroidStartupFlags.IsSet(AndroidStartupFlags.FLAG_VERBOSE_LOGGING_ENABLED); + Logger.Level = verbose ? LogLevel.Verbose : LogLevel.Important; + } + catch (Exception e) + { + Debug.WriteLine($"[osu!] LogManagement: could not apply Logger.Level: {e.Message}"); + } try { diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 826871444798..580f91886f43 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -85,6 +85,7 @@ public partial class OsuGameAndroid : OsuGame private readonly Bindable cleanupStaleRealmFifos = new Bindable(); private readonly Bindable deferStartupNativeInit = new Bindable(); private readonly Bindable startupFrameSyncMigrationEnabled = new Bindable(); + private readonly Bindable verboseLogging = new Bindable(); [Cached(typeof(IHighPerformanceSessionManager))] private readonly IHighPerformanceSessionManager highPerformanceSessionManager = new AndroidHighPerformanceSessionManager(); @@ -208,6 +209,7 @@ private void load(FrameworkConfigManager frameworkConfig) LocalConfig.BindWith(OsuSetting.AndroidCleanupStaleRealmFifos, cleanupStaleRealmFifos); LocalConfig.BindWith(OsuSetting.AndroidDeferStartupNativeInit, deferStartupNativeInit); LocalConfig.BindWith(OsuSetting.AndroidStartupFrameSyncMigrationEnabled, startupFrameSyncMigrationEnabled); + LocalConfig.BindWith(OsuSetting.AndroidVerboseLogging, verboseLogging); // sentinelOnDisable=true → presence ⇒ "feature disabled". The // safety nets default to ON, so the sentinel is created only @@ -215,9 +217,11 @@ private void load(FrameworkConfigManager frameworkConfig) mirrorStartupFlag(cleanupStaleRealmFifos, AndroidStartupFlags.FLAG_CLEANUP_REALM_FIFOS_DISABLED, sentinelOnDisable: true); mirrorStartupFlag(deferStartupNativeInit, AndroidStartupFlags.FLAG_DEFER_NATIVE_INIT_DISABLED, sentinelOnDisable: true); // sentinelOnDisable=false → presence ⇒ "feature enabled". The - // FrameSync migration defaults to OFF, so the sentinel is - // created only when the user explicitly opts in. + // FrameSync migration and verbose-logging toggles both default + // to OFF, so the sentinel is created only when the user + // explicitly opts in. mirrorStartupFlag(startupFrameSyncMigrationEnabled, AndroidStartupFlags.FLAG_FRAME_SYNC_MIGRATION_ENABLED, sentinelOnDisable: false); + mirrorStartupFlag(verboseLogging, AndroidStartupFlags.FLAG_VERBOSE_LOGGING_ENABLED, sentinelOnDisable: false); } catch (Exception e) { @@ -1211,17 +1215,21 @@ private void handleLowLatencyAudioChanged(ValueChangedEvent e) { double suggested = Math.Clamp(-latency, audioOffset.MinValue, audioOffset.MaxValue); audioOffset.Value = suggested; - Debug.WriteLine($"[osu!] Audio offset auto-suggested: {suggested:F1}ms (hardware latency={latency:F1}ms)"); + Logger.Log($"[osu!] Audio offset auto-suggested: {suggested:F1}ms (hardware latency={latency:F1}ms)"); }, audioRedirector != null ? audioRedirector.Provider : IntPtr.Zero, sampleRate => { audioRedirector?.RefreshMixers(sampleRate); - Debug.WriteLine("[osu!] Audio redirector refreshed with hardware sample rate: " + sampleRate); + Logger.Log("[osu!] Audio redirector refreshed with hardware sample rate: " + sampleRate); }); } catch (Exception ex) { - Debug.WriteLine($"[osu!] Failed to start Oboe bridge: {ex.Message}"); - lowLatencyAudio.Value = false; + // Surface to runtime.log so a user-shared log makes Oboe failures + // diagnosable. Do NOT silently flip lowLatencyAudio.Value back to + // false here — persisting that flip turns a single transient init + // failure into a permanent "Oboe doesn't work" for the user, with + // no indication that the toggle was overridden behind their back. + Logger.Log($"[osu!] Failed to start Oboe bridge: {ex.Message}", level: LogLevel.Error); } } else diff --git a/osu.Android/Performance/AndroidHighPerformanceSessionManager.cs b/osu.Android/Performance/AndroidHighPerformanceSessionManager.cs index da51a0c4cd96..9c7c575ce80c 100644 --- a/osu.Android/Performance/AndroidHighPerformanceSessionManager.cs +++ b/osu.Android/Performance/AndroidHighPerformanceSessionManager.cs @@ -18,6 +18,17 @@ public class AndroidHighPerformanceSessionManager : IHighPerformanceSessionManag private GCLatencyMode originalGCMode; + /// + /// One-shot disable. Mono on Android throws + /// from the setter (and, on some runtimes, the + /// getter). We must not let that exception escape — it would crash the + /// game every time the user enters PlayerLoader, holds a mouse + /// button, or otherwise triggers a high-performance session, since + /// is invoked on the update thread and the + /// throw propagates up through UpdateSubTree. + /// + private static bool gcLatencyModeSupported = true; + public IDisposable BeginSession() { enterSession(); @@ -34,8 +45,23 @@ private void enterSession() Logger.Log("Starting high performance session (Android)"); - originalGCMode = GCSettings.LatencyMode; - GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; + if (!gcLatencyModeSupported) + return; + + try + { + originalGCMode = GCSettings.LatencyMode; + GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; + } + catch (PlatformNotSupportedException) + { + // Mono on Android does not implement GCSettings.LatencyMode. + // Latch off so subsequent sessions skip the throwing call entirely + // (the unhandled-exception allowance is finite and would burn out + // after a few gameplay entries, killing the process). + gcLatencyModeSupported = false; + Logger.Log("GCSettings.LatencyMode unsupported on this runtime; high-performance GC tuning disabled."); + } } private void exitSession() @@ -48,8 +74,18 @@ private void exitSession() Logger.Log("Ending high performance session (Android)"); - if (GCSettings.LatencyMode == GCLatencyMode.SustainedLowLatency) - GCSettings.LatencyMode = originalGCMode; + if (!gcLatencyModeSupported) + return; + + try + { + if (GCSettings.LatencyMode == GCLatencyMode.SustainedLowLatency) + GCSettings.LatencyMode = originalGCMode; + } + catch (PlatformNotSupportedException) + { + gcLatencyModeSupported = false; + } } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index b95f8cadbeff..1ec23e2b4529 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -270,6 +270,7 @@ protected override void InitialiseDefaults() SetDefault(OsuSetting.AndroidCleanupStaleRealmFifos, true); SetDefault(OsuSetting.AndroidDeferStartupNativeInit, true); SetDefault(OsuSetting.AndroidStartupFrameSyncMigrationEnabled, false); + SetDefault(OsuSetting.AndroidVerboseLogging, false); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -532,6 +533,7 @@ public enum OsuSetting AndroidCleanupStaleRealmFifos, AndroidDeferStartupNativeInit, AndroidStartupFrameSyncMigrationEnabled, + AndroidVerboseLogging, RefreshRateFullscreen, } } diff --git a/osu.Game/Graphics/UserInterface/FPSCounter.cs b/osu.Game/Graphics/UserInterface/FPSCounter.cs index cf96426b5fd4..39a1be0531a0 100644 --- a/osu.Game/Graphics/UserInterface/FPSCounter.cs +++ b/osu.Game/Graphics/UserInterface/FPSCounter.cs @@ -59,12 +59,6 @@ public partial class FPSCounter : VisibilityContainer, IHasCustomTooltip [Resolved(canBeNull: true)] private OsuColour colours { get; set; } = null!; - [Resolved(canBeNull: true)] - protected OsuGameBase? Game { get; private set; } = null!; - - [Resolved(canBeNull: true)] - private GameHost? host { get; set; } = null!; - public FPSCounter() { AutoSizeAxes = Axes.Both; @@ -238,23 +232,7 @@ private void requestDisplay() private void updateFpsDisplay() { counterDrawFPS.Colour = getColour(displayedFpsCount / aimDrawFPS); - string status = $"{displayedFpsCount:#,0} fps"; - - if (Game != null) - { - status += $" | {host?.ResolvedRenderer.ToString()}"; - - if (Game.DisplayRefreshRate > 0) - status += $" | {Game.DisplayRefreshRate}Hz"; - - if (!string.IsNullOrEmpty(Game.VulkanStatus)) - status += $" | {Game.VulkanStatus}"; - - if (Game.IsOboeEnabled) - status += $" | Oboe: {Game.OboeStatus}{(Game.IsOboeActive ? $" ({Game.OboeLatency:F1}ms)" : "")}"; - } - - counterDrawFPS.Text = status; + counterDrawFPS.Text = $"{displayedFpsCount:#,0} fps"; } private void updateFrameTimeDisplay() diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/AndroidPerformanceSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/AndroidPerformanceSettings.cs index 34aefd81aaa0..047f051e943d 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/AndroidPerformanceSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/AndroidPerformanceSettings.cs @@ -42,47 +42,46 @@ private void load(OsuConfigManager config, OsuGame? game) new SettingsItemV2(new FormCheckBox { Caption = "Low-latency audio (Oboe)", - HintText = "Uses Google Oboe for AAudio low-latency output and real-time audio latency measurement. Requires native library.", + HintText = + "Routes osu!'s final audio through Google's Oboe library, which prefers AAudio + MMAP fast-mixer paths on modern devices " + + "(falling back to OpenSL ES on older ones). Typical end-to-end output latency drops from ~80–150ms (default Android mixer) " + + "to ~20–40ms on supported hardware. Oboe also reports the real hardware output latency back to the game, which is used to " + + "auto-suggest the audio offset on first measurement (~2s after audio starts). " + + "Requires the bundled native library; if it fails to load or the audio device refuses a low-latency stream the game " + + "stays on the default mixer and an error is written to runtime.log — disable this if you hear crackles, drop-outs, " + + "or wrong-pitch playback on your specific device.", Current = config.GetBindable(OsuSetting.AndroidLowLatencyAudio), }) { Keywords = new[] { @"oboe", @"aaudio", @"latency", @"audio" }, }, + // The following toggles were removed from the UI: + // + // - "GPU detection (Vulkan)" (OsuSetting.AndroidVulkanProbe) — purely + // cosmetic; only ran a Vulkan capabilities probe via the native bridge + // and never touched the renderer. Default OFF in OsuConfigManager. + // - "Clean up stale Realm fifos at startup" (OsuSetting.AndroidCleanupStaleRealmFifos) — + // safety net for a previously-fixed Realm-fifo crash. Default ON; + // not exposed because there is no good reason to disable it. + // - "Defer audio/Vulkan native init at startup" (OsuSetting.AndroidDeferStartupNativeInit) — + // cold-start safety net. Default ON; not exposed for the same reason. + // - "Auto-migrate FrameSync to VSync on first launch" + // (OsuSetting.AndroidStartupFrameSyncMigrationEnabled) — silently + // mutated framework defaults; the original bug it worked around is + // fixed elsewhere. Default OFF; not exposed. + // + // The underlying OsuSetting entries are intentionally kept (with their + // defaults) so OsuGameAndroid's BindWith / sentinel-mirror wiring still + // resolves cleanly without having to thread conditional registration + // through OsuConfigManager. new SettingsItemV2(new FormCheckBox { - Caption = "GPU detection (Vulkan)", - HintText = "Probes Vulkan GPU capabilities at startup. Requires native library.", - Current = config.GetBindable(OsuSetting.AndroidVulkanProbe), + Caption = "Verbose logging", + HintText = "Off by default — only important messages are written to the on-disk log. Enable to capture full per-thread diagnostics when sharing a log to debug an issue. Takes effect on next launch. Quiet mode also avoids string-formatting work in audio/render hot paths.", + Current = config.GetBindable(OsuSetting.AndroidVerboseLogging), }) { - Keywords = new[] { @"vulkan", @"gpu", @"graphics" }, - }, - new SettingsItemV2(new FormCheckBox - { - Caption = "Clean up stale Realm fifos at startup", - HintText = "Removes leftover Realm cross-process notification fifos from a previous crashed process. A stale fifo can block Realm initialisation in native code at startup.", - Current = config.GetBindable(OsuSetting.AndroidCleanupStaleRealmFifos), - }) - { - Keywords = new[] { @"realm", @"fifo", @"startup", @"hang" }, - }, - new SettingsItemV2(new FormCheckBox - { - Caption = "Defer audio/Vulkan native init at startup", - HintText = "Delays Oboe and Vulkan-probe initialisation until after the game has finished loading, so a slow native init cannot stall the cold-start sequence. Disable to revert to immediate init.", - Current = config.GetBindable(OsuSetting.AndroidDeferStartupNativeInit), - }) - { - Keywords = new[] { @"oboe", @"vulkan", @"defer", @"startup" }, - }, - new SettingsItemV2(new FormCheckBox - { - Caption = "Auto-migrate FrameSync to VSync on first launch", - HintText = "If enabled, switches the framework FrameSync default from Limit2x to VSync the first time osu! reaches load completion. Off by default — change FrameSync manually in Renderer settings if you want VSync.", - Current = config.GetBindable(OsuSetting.AndroidStartupFrameSyncMigrationEnabled), - }) - { - Keywords = new[] { @"framesync", @"vsync", @"adreno", @"renderer" }, + Keywords = new[] { @"log", @"debug", @"diagnostic", @"verbose" }, }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index 43c676719fba..bc04dda55b92 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -35,6 +35,11 @@ private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, IDi // The VulkanProbe detects feature support; even if some features are disabled (e.g. on // Adreno 7xx), the renderer itself may still work and provide better performance than // OpenGL ES for some workloads. + // + // Note: the Veldrid Vulkan backend currently produces a black screen on some Adreno + // devices (swapchain bring-up never reaches first present). Until that's fixed + // upstream in osu-framework / Veldrid, picking Vulkan here can leave the user + // unable to launch the game from the UI — they'd need to clear app data to recover. if (RuntimeInfo.OS == RuntimeInfo.Platform.Android) { bool isSupported = game?.IsVulkanSupported ?? false;