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
15 changes: 8 additions & 7 deletions osu.Android/AndroidNativeBridgeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -32,7 +33,7 @@ public void StartOboeBridge(Scheduler scheduler, Action<double> 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
Expand Down Expand Up @@ -69,7 +70,7 @@ public void StartOboeBridge(Scheduler scheduler, Action<double> onLatencyMeasure

if (started)
{
Debug.WriteLine("[osu!] Oboe bridge started successfully");
Logger.Log("[osu!] Oboe bridge started successfully");
logOboeInfo(bridge);

onStarted?.Invoke(bridge.SampleRate);
Expand All @@ -87,17 +88,17 @@ public void StartOboeBridge(Scheduler scheduler, Action<double> 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);
}
}
}
Expand All @@ -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");
}
}

Expand Down
11 changes: 11 additions & 0 deletions osu.Android/AndroidStartupFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/// <summary>
/// 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.
/// </summary>
public const string FLAG_VERBOSE_LOGGING_ENABLED = "android_startup_enable_verbose_logging.flag";

/// <summary>
/// "Startup in progress" sentinel. Dropped near the very top of <see cref="OsuGameActivity.OnCreate"/>
/// and cleared a few seconds after <c>OsuGame.LoadComplete</c> by <see cref="OsuGameAndroid"/>.
Expand Down
34 changes: 25 additions & 9 deletions osu.Android/LogManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,31 @@ internal static class LogManagement
/// </summary>
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
{
Expand Down
20 changes: 14 additions & 6 deletions osu.Android/OsuGameAndroid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public partial class OsuGameAndroid : OsuGame
private readonly Bindable<bool> cleanupStaleRealmFifos = new Bindable<bool>();
private readonly Bindable<bool> deferStartupNativeInit = new Bindable<bool>();
private readonly Bindable<bool> startupFrameSyncMigrationEnabled = new Bindable<bool>();
private readonly Bindable<bool> verboseLogging = new Bindable<bool>();

[Cached(typeof(IHighPerformanceSessionManager))]
private readonly IHighPerformanceSessionManager highPerformanceSessionManager = new AndroidHighPerformanceSessionManager();
Expand Down Expand Up @@ -208,16 +209,19 @@ 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
// when the user explicitly disables them.
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)
{
Expand Down Expand Up @@ -1211,17 +1215,21 @@ private void handleLowLatencyAudioChanged(ValueChangedEvent<bool> 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
Expand Down
44 changes: 40 additions & 4 deletions osu.Android/Performance/AndroidHighPerformanceSessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ public class AndroidHighPerformanceSessionManager : IHighPerformanceSessionManag

private GCLatencyMode originalGCMode;

/// <summary>
/// One-shot disable. Mono on Android throws <see cref="PlatformNotSupportedException"/>
/// from the <see cref="GCSettings.LatencyMode"/> setter (and, on some runtimes, the
/// getter). We must not let that exception escape — it would crash the
/// game every time the user enters <c>PlayerLoader</c>, holds a mouse
/// button, or otherwise triggers a high-performance session, since
/// <see cref="BeginSession"/> is invoked on the update thread and the
/// throw propagates up through <c>UpdateSubTree</c>.
/// </summary>
private static bool gcLatencyModeSupported = true;

public IDisposable BeginSession()
{
enterSession();
Expand All @@ -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()
Expand All @@ -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;
}
}
}
}
2 changes: 2 additions & 0 deletions osu.Game/Configuration/OsuConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -532,6 +533,7 @@ public enum OsuSetting
AndroidCleanupStaleRealmFifos,
AndroidDeferStartupNativeInit,
AndroidStartupFrameSyncMigrationEnabled,
AndroidVerboseLogging,
RefreshRateFullscreen,
}
}
24 changes: 1 addition & 23 deletions osu.Game/Graphics/UserInterface/FPSCounter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading