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
50 changes: 16 additions & 34 deletions osu.Android.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,23 @@
<NoWarn>$(NoWarn);NU1608;XA4301</NoWarn>
</PropertyGroup>

<!-- Release-only optimisations: AOT for low-latency gameplay, trimming for smaller APK.
Suppress trim analysis warnings because the project uses reflection extensively
(Newtonsoft.Json, Realm, AutoMapper, RuntimeBinder). -->
<!-- Release-only optimisations.
Trimming and AOT were previously enabled here for size/startup, but caused a
reproducible "splash → black screen → crash, no logs" failure on real devices.
Root cause: PublishTrimmed + TrimMode=partial + AndroidEnableProfiledAot, with
SuppressTrimAnalysisWarnings=true masking the build-time signal. Only Microsoft.CSharp
and ppy.Veldrid.SPIRV were rooted — osu.Game, the rulesets, Realm, Newtonsoft.Json,
AutoMapper, Sentry, OsuTK and other reflection-heavy assemblies were not. A trimmed
method/type or an un-AOT'd profiled-AOT call site throws TypeLoadException /
MissingMethodException early in OsuGame construction, before the file logger is open;
the process dies on the SDLThread / .NET ThreadPool thread with only a tombstone, so
the user gets no osu.log and Logcat shows a bare native crash.
Upstream ppy/osu's osu.Android.props enables NEITHER trimming NOR AOT — we now match
that baseline. The remaining release-only knobs (assembly compression, no PDBs) are
safe and unrelated to the startup crash. To re-enable trimming/AOT in the future,
reintroduce them one at a time, root every reflection-using assembly explicitly, and
run with SuppressTrimAnalysisWarnings=false to surface the gaps before shipping. -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
<!-- Do NOT use EnableLLVM with profiled AOT on .NET 10.
The LLVM backend generates internally-inconsistent PLT (Procedure Linkage Table)
entries when only a subset of methods is AOT-compiled (profiled AOT). This causes
'plt_entry not met' assertions in aot-runtime.c at startup (SIGABRT on SDLThread).
The default Mono AOT compiler handles partial-image PLT generation correctly. -->
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
<!-- Keep IL bodies as a fallback for methods not covered by profiled AOT.
AndroidEnableProfiledAot only AOT-compiles methods in the startup profile; the remaining
methods require IL for JIT/interpretation. Stripping IL (AndroidStripILAfterAOT=true)
removes that fallback, causing 'plt_entry not met' assertions in aot-runtime.c when
the runtime encounters an un-AOT'd method (observed on .NET Timer thread at startup).
The ~20-30 MB size saving is not worth the crash risk. -->
<AndroidStripILAfterAOT>false</AndroidStripILAfterAOT>
<!-- Compress managed assemblies inside the APK (LZ4). Android extracts them on first run
but the download/APK size is significantly smaller. -->
<AndroidEnableAssemblyCompression>true</AndroidEnableAssemblyCompression>
Expand All @@ -54,20 +50,6 @@
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>

<ItemGroup Condition="'$(Configuration)' == 'Release'">
<!-- Force Microsoft.CSharp into the trimmer input graph (.NET 10+). -->
<!-- A bare TrimmerRootAssembly is ignored if the assembly is not in the -->
<!-- linker's input set; adding this reference ensures it is included. -->
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" NoWarn="NU1510" />
<TrimmerRootAssembly Include="Microsoft.CSharp" RootMode="all" />
<!-- Preserve ppy.Veldrid.SPIRV from trimming. The trimmer does not trace the
CrossCompileTarget enum usage in osu.Framework's VeldridShader/GLShader correctly
(the reference crosses assembly boundaries via a ProjectReference chain), causing
TypeLoadException: 'Could not resolve type Veldrid.SPIRV.CrossCompileTarget' at
startup when the renderer tries to compile shaders. -->
<TrimmerRootAssembly Include="ppy.Veldrid.SPIRV" RootMode="all" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.420.2" />
<!-- `ppy.osu.Framework.NativeLibs` is a transitive dependency of `ppy.osu.Framework`
Expand Down
48 changes: 44 additions & 4 deletions osu.Android/OsuGameActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@

namespace osu.Android
{
[Activity(ResizeableActivity = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode | ConfigChanges.SmallestScreenSize | ConfigChanges.ScreenLayout | ConfigChanges.ColorMode | ConfigChanges.Density | ConfigChanges.Touchscreen | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden | ConfigChanges.Navigation, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
// Declare ScreenOrientation in the manifest (rather than only assigning RequestedOrientation
// at runtime in OnCreate) so Android creates the activity in landscape from the very first
// frame — the SurfaceView is sized correctly on creation and there is no orientation-change
// event during startup. This is defensive hardening alongside the main fix in osu.Android.props
// (disabling trimming + profiled AOT, which was the actual cause of the startup crash).
[Activity(ResizeableActivity = true, ScreenOrientation = ScreenOrientation.SensorLandscape, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode | ConfigChanges.SmallestScreenSize | ConfigChanges.ScreenLayout | ConfigChanges.ColorMode | ConfigChanges.Density | ConfigChanges.Touchscreen | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden | ConfigChanges.Navigation, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\.osr", DataHost = "*", DataMimeType = "*/*")]
Expand Down Expand Up @@ -81,9 +86,35 @@ protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);

Microsoft.Maui.ApplicationModel.Platform.Init(this, savedInstanceState);
// Wrap Platform.Init defensively: MAUI Essentials pulls in workload-version-sensitive
// initialisation code, and a mismatch between the build-time workload and the device's
// runtime can throw TypeLoadException/MissingMethodException on the UI thread before
// the managed logger is up — users would see only a native tombstone with no osu.log.
try
{
Microsoft.Maui.ApplicationModel.Platform.Init(this, savedInstanceState);
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] MAUI Platform.Init failed (non-fatal): {e.Message}");
}

updateDeXStatus(null);
Window?.DecorView.Post(() => GetSurface()?.Holder?.AddCallback(this));

// Posting the surface-callback registration onto the UI thread loop is intentional
// (the SurfaceView may not be attached yet at OnCreate time). Guard the body of the
// lambda — a later race with activity teardown can make AddCallback throw.
Window?.DecorView.Post(() =>
{
try
{
GetSurface()?.Holder?.AddCallback(this);
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] Failed to register SurfaceHolder callback: {e.Message}");
}
});

handleIntent(Intent);

Expand Down Expand Up @@ -122,7 +153,16 @@ protected override void OnCreate(Bundle? savedInstanceState)
if (Resources?.Configuration != null)
IsTablet = Resources.Configuration.SmallestScreenWidthDp >= 600;

RequestedOrientation = DefaultOrientation = IsTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
// Phones: manifest already requests SensorLandscape; do not re-assign at runtime —
// a no-op assignment is harmless on most devices but a redundant RequestedOrientation
// write can still nudge the SurfaceView into a recreate cycle on some OEMs while the
// SDL draw thread is mid-Vulkan-init. Tablets get a more permissive policy applied
// here; the SurfaceView is already up by this point and the framework handles
// post-init surface resize cleanly.
if (IsTablet)
RequestedOrientation = DefaultOrientation = ScreenOrientation.FullUser;
else
DefaultOrientation = ScreenOrientation.SensorLandscape;

foreach (string asm in new[] { "osu.Game.Rulesets.Osu", "osu.Game.Rulesets.Taiko", "osu.Game.Rulesets.Catch", "osu.Game.Rulesets.Mania" })
{
Expand Down
42 changes: 27 additions & 15 deletions osu.Android/OsuGameAndroid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,25 +227,37 @@ protected override void LoadComplete()

Scheduler.Add(() =>
{
Host.DrawThread.Scheduler.Add(() =>
try
{
try
Host?.DrawThread?.Scheduler.Add(() =>
{
Comment on lines +230 to 233
if (OboeAudioBridge.nSetThreadAffinity(mask) != 0) Logger.Log("[osu!] Render thread pinned to big cores", LoggingTarget.Performance);
global::Android.OS.Process.SetThreadPriority(global::Android.OS.ThreadPriority.UrgentDisplay);
}
catch { }
});
try
{
if (OboeAudioBridge.nSetThreadAffinity(mask) != 0) Logger.Log("[osu!] Render thread pinned to big cores", LoggingTarget.Performance);
global::Android.OS.Process.SetThreadPriority(global::Android.OS.ThreadPriority.UrgentDisplay);
}
catch { }
});

Host.InputThread.Scheduler.Add(() =>
{
try
Host?.InputThread?.Scheduler.Add(() =>
{
if (OboeAudioBridge.nSetThreadAffinity(mask) != 0) Logger.Log("[osu!] Input thread pinned to big cores", LoggingTarget.Performance);
global::Android.OS.Process.SetThreadPriority(global::Android.OS.ThreadPriority.UrgentDisplay);
}
catch { }
});
try
{
if (OboeAudioBridge.nSetThreadAffinity(mask) != 0) Logger.Log("[osu!] Input thread pinned to big cores", LoggingTarget.Performance);
global::Android.OS.Process.SetThreadPriority(global::Android.OS.ThreadPriority.UrgentDisplay);
}
catch { }
});
}
catch (Exception e)
{
// The enclosing try/catch only covers the Scheduler.Add call — not the
// lambda body, which runs later on the update thread. Guard here so an
// NRE from Host.DrawThread/Host.InputThread being null (or a Host
// teardown race during startup) can't escape as an unhandled update-
// thread exception and kill the framework.
Debug.WriteLine($"[osu!] Failed to enqueue thread-affinity pinning for render/input threads: {e.Message}");
}
});
}
catch (Exception e)
Expand Down
Loading