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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive

- name: Install .NET 10.0.x
uses: actions/setup-dotnet@v5
Expand Down Expand Up @@ -77,6 +79,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive

- name: Install .NET 10.0.x
uses: actions/setup-dotnet@v5
Expand Down Expand Up @@ -116,6 +120,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive

- name: Setup JDK 11
uses: actions/setup-java@v5
Expand All @@ -141,6 +147,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive

- name: Install .NET 10.0.x
uses: actions/setup-dotnet@v5
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive

- name: Setup JDK 17
uses: actions/setup-java@v5
Expand Down
Empty file added .gitmodules
Empty file.
2 changes: 1 addition & 1 deletion osu.Android.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
"osu.Game\\osu.Game.csproj"
]
}
}
}
2 changes: 2 additions & 0 deletions osu.Android/AndroidNativeBridgeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ public void StartVulkanProbe()

[MethodImpl(MethodImplOptions.NoInlining)]
public bool IsVulkanRecommended() => (vulkanProbe as VulkanProbe)?.IsRecommended ?? false;

public bool IsVulkanAvailable() => (vulkanProbe as VulkanProbe)?.IsAvailable ?? false;
public void StopVulkanProbe()
{
(vulkanProbe as VulkanProbe)?.Dispose();
Expand Down
26 changes: 26 additions & 0 deletions osu.Android/OboeAudioRedirector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand All @@ -12,6 +13,7 @@
using ManagedBass.Mix;
using osu.Framework.Audio;
using osu.Framework.Audio.Mixing;
using osu.Framework.Bindables;

namespace osu.Android
{
Expand Down Expand Up @@ -43,10 +45,16 @@ public void RefreshMixers(int hardwareSampleRate)
addRootMixer(audioManager.TrackMixer);
addRootMixer(audioManager.SampleMixer);

foreach (var mixer in getActiveMixers())
addRootMixer(mixer);

if (mixerHandles.Count == 0)
{
addMixer(audioManager.TrackMixer);
addMixer(audioManager.SampleMixer);

foreach (var mixer in getActiveMixers())
addMixer(mixer);
}

if (mixerHandles.Count == 0)
Expand All @@ -62,6 +70,24 @@ public void RefreshMixers(int hardwareSampleRate)
Console.WriteLine($"[osu!] Oboe redirector initialized: master={masterMixer}, sources={string.Join(',', mixerHandles)}");
}

private IEnumerable<AudioMixer> getActiveMixers()
{
// Use reflection to access the internal activeMixers list in AudioManager.
// In the official framework, it is an internal BindableList<AudioMixer> activeMixers.
FieldInfo? field = typeof(AudioManager).GetField("activeMixers", BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null) yield break;

object? val = field.GetValue(audioManager);
if (val is IEnumerable enumerable)
{
foreach (var item in enumerable)
{
if (item is AudioMixer mixer)
yield return mixer;
}
}
}

private void setupMasterMixer()
{
if (masterMixer != 0)
Expand Down
13 changes: 8 additions & 5 deletions osu.Android/OsuGameActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,15 @@ public IntPtr GetSurfaceGlobalRef()
public void SurfaceCreated(ISurfaceHolder holder)
{
var surface = holder.Surface;
if (surface != null && surface.Handle != IntPtr.Zero)
if (surface != null && surface.IsValid)
{
var handle = surface.Handle;
surfaceGlobalRef = global::Android.Runtime.JNIEnv.NewGlobalRef(handle);
surfaceEvent.Set();
Debug.WriteLine("[osu!] Native surface JNI global reference created");
IntPtr handle = surface.Handle;
if (handle == IntPtr.Zero) return;
{
surfaceGlobalRef = global::Android.Runtime.JNIEnv.NewGlobalRef(handle);
surfaceEvent.Set();
Debug.WriteLine("[osu!] Native surface JNI global reference created");
}
}
}

Expand Down
105 changes: 77 additions & 28 deletions osu.Android/OsuGameAndroid.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using Microsoft.Maui.Devices;
using osu.Android.Performance;
using osu.Framework.Development;

#pragma warning disable CA1422
#pragma warning restore CA1422

using Android.App;
using Android.Content.PM;
using Android.Content;
using Android.Media;
using Android.Views;
using osu.Game.Performance;
using osu.Game.Updater;
using System.Collections.Specialized;
using System;
using System.Collections;
using System.Collections.Generic;
using Debug = System.Diagnostics.Debug;
using Microsoft.Maui.Devices;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System;
using System.Diagnostics;
using Context = global::Android.Content.Context;
using Android.Media;
using Android.OS;
using Android.Views;
using osu.Android.Native;
using osu.Framework.Allocation;
using AudioManager = osu.Framework.Audio.AudioManager;
using osu.Framework.Bindables;
using osu.Framework.Development;
using osu.Framework.Configuration;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game;
using osu.Game.Configuration;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens;
using osu.Game.Updater;
using osu.Game.Utils;
using osu.Game;
using osuTK;
using osu.Game.Performance;
using osu.Android.Performance;
using Vector2 = osuTK.Vector2;

namespace osu.Android
{
public partial class OsuGameAndroid : OsuGame
{
[Cached]
private readonly OsuGameActivity gameActivity;

private readonly object packageInfoLock = new object();

private PackageInfo? packageInfo;
private bool packageInfoChecked;

Expand Down Expand Up @@ -77,6 +84,8 @@ public partial class OsuGameAndroid : OsuGame
private readonly IHighPerformanceSessionManager highPerformanceSessionManager = new AndroidHighPerformanceSessionManager();

private OboeAudioRedirector? audioRedirector;
private Delegate? activeMixersHandler;
private object? activeMixersList;
private IntPtr updateAdpfSession;
private IntPtr renderAdpfSession;

Expand All @@ -102,7 +111,7 @@ public override string Version
get
{
if (!IsDeployedBuild)
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
return @"local " + (osu.Framework.Development.DebugUtils.IsDebugBuild ? @"debug" : @"release");

return getPackageInfo()?.VersionName ?? @"unknown";
}
Expand Down Expand Up @@ -137,6 +146,30 @@ private void load()
LocalConfig.BindWith(OsuSetting.AudioOffset, audioOffset);

audioRedirector = new OboeAudioRedirector(Audio);

try
{
// Use reflection to bind to collection changes of the internal activeMixers list in AudioManager.
FieldInfo? field = typeof(AudioManager).GetField("activeMixers", BindingFlags.Instance | BindingFlags.NonPublic);
if (field != null)
{
activeMixersList = field.GetValue(Audio);
object? val = field.GetValue(Audio);
if (val != null)
{
MethodInfo? bindMethod = val.GetType().GetMethod("BindCollectionChanged", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (bindMethod != null)
{
var del = Delegate.CreateDelegate(bindMethod.GetParameters()[0].ParameterType, this, typeof(OsuGameAndroid).GetMethod(nameof(onActiveMixersChanged), BindingFlags.Instance | BindingFlags.NonPublic)!);
bindMethod.Invoke(val, new object[] { del, true });
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"[osu!] Failed to bind to activeMixers via reflection: {ex.Message}");
}
}

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
Expand Down Expand Up @@ -215,9 +248,9 @@ protected override void LoadComplete()
int hardwareSampleRate = 0;
try
{
if (gameActivity.GetSystemService(Context.AudioService) is AudioManager audioManager)
if (gameActivity.GetSystemService(global::Android.Content.Context.AudioService) is global::Android.Media.AudioManager audioManager)
{
string? rateStr = audioManager.GetProperty(AudioManager.PropertyOutputSampleRate);
string? rateStr = audioManager.GetProperty(global::Android.Media.AudioManager.PropertyOutputSampleRate);

if (!string.IsNullOrEmpty(rateStr))
hardwareSampleRate = int.Parse(rateStr);
Expand Down Expand Up @@ -360,7 +393,11 @@ private void selectHighestRefreshRate()
}
}

public bool IsVulkanRecommended() => (nativeBridges as AndroidNativeBridgeManager)?.IsVulkanRecommended() ?? false;
public override bool IsVulkanRecommended => (nativeBridges as AndroidNativeBridgeManager)?.IsVulkanRecommended() ?? false;

public override bool IsVulkanSupported => (nativeBridges as AndroidNativeBridgeManager)?.IsVulkanAvailable() ?? false;

private void onActiveMixersChanged(object? sender, NotifyCollectionChangedEventArgs args) => Schedule(() => { if (lowLatencyAudio.Value) audioRedirector?.RefreshMixers(0); });

public double GetMeasuredAudioLatencyMs() => getMeasuredAudioLatencyFromBridge();

Expand All @@ -371,9 +408,9 @@ private void startOboeBridge(Action<double> onLatencyMeasured, IntPtr provider,

try
{
if (gameActivity.GetSystemService(Context.AudioService) is AudioManager audioManager)
if (gameActivity.GetSystemService(global::Android.Content.Context.AudioService) is global::Android.Media.AudioManager audioManager)
{
string? rateStr = audioManager.GetProperty(AudioManager.PropertyOutputSampleRate);
string? rateStr = audioManager.GetProperty(global::Android.Media.AudioManager.PropertyOutputSampleRate);

if (!string.IsNullOrEmpty(rateStr))
hardwareSampleRate = int.Parse(rateStr);
Expand Down Expand Up @@ -443,11 +480,11 @@ private void updateOrientation()
switch (orientation)
{
case MobileUtils.Orientation.Locked:
gameActivity.RequestedOrientation = ScreenOrientation.Locked;
gameActivity.RequestedOrientation = global::Android.Content.PM.ScreenOrientation.Locked;
break;

case MobileUtils.Orientation.Portrait:
gameActivity.RequestedOrientation = ScreenOrientation.Portrait;
gameActivity.RequestedOrientation = global::Android.Content.PM.ScreenOrientation.Portrait;
break;

case MobileUtils.Orientation.Default:
Expand Down Expand Up @@ -485,6 +522,18 @@ protected override void Dispose(bool isDisposing)
audioRedirector?.Dispose();
audioRedirector = null;

if (activeMixersList != null && activeMixersHandler != null)
{
try
{
MethodInfo? unbindMethod = activeMixersList.GetType().GetMethod("UnbindCollectionChanged", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
unbindMethod?.Invoke(activeMixersList, new object[] { activeMixersHandler });
}
catch { }
activeMixersList = null;
activeMixersHandler = null;
}

if (nativeBridges != null)
disposeNativeBridges();

Expand Down Expand Up @@ -524,6 +573,6 @@ protected override void UpdateAfterChildren()
internal class AndroidBatteryInfo : BatteryInfo
{
public override double? ChargeLevel => Microsoft.Maui.Devices.Battery.ChargeLevel;
public override bool OnBattery => Microsoft.Maui.Devices.Battery.PowerSource == BatteryPowerSource.Battery;
public override bool OnBattery => Microsoft.Maui.Devices.Battery.PowerSource == global::Microsoft.Maui.Devices.BatteryPowerSource.Battery;
}
}
}
2 changes: 1 addition & 1 deletion osu.Desktop.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj"
]
}
}
}
3 changes: 3 additions & 0 deletions osu.Game/OsuGameBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ public virtual EndpointConfiguration CreateEndpoints() =>
public string VersionHash { get; private set; }

public bool IsDeployedBuild => AssemblyVersion.Major > 0;
public virtual bool IsVulkanRecommended => false;

public virtual bool IsVulkanSupported => false;

public virtual string Version
{
Expand Down
Loading
Loading