Skip to content

Commit e1e7974

Browse files
authored
Merge pull request #133 from winnerspiros/winnerspiros/fix-vulkan-oboe-android-58228484722015673
Fix Vulkan and Oboe support on Android
2 parents 0ec34bb + 1ec528c commit e1e7974

12 files changed

Lines changed: 450 additions & 65 deletions

File tree

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ jobs:
1515
steps:
1616
- name: Checkout
1717
uses: actions/checkout@v6
18+
with:
19+
submodules: recursive
1820

1921
- name: Install .NET 10.0.x
2022
uses: actions/setup-dotnet@v5
@@ -77,6 +79,8 @@ jobs:
7779
steps:
7880
- name: Checkout
7981
uses: actions/checkout@v6
82+
with:
83+
submodules: recursive
8084

8185
- name: Install .NET 10.0.x
8286
uses: actions/setup-dotnet@v5
@@ -116,6 +120,8 @@ jobs:
116120
steps:
117121
- name: Checkout
118122
uses: actions/checkout@v6
123+
with:
124+
submodules: recursive
119125

120126
- name: Setup JDK 11
121127
uses: actions/setup-java@v5
@@ -141,6 +147,8 @@ jobs:
141147
steps:
142148
- name: Checkout
143149
uses: actions/checkout@v6
150+
with:
151+
submodules: recursive
144152

145153
- name: Install .NET 10.0.x
146154
uses: actions/setup-dotnet@v5

.github/workflows/release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717
steps:
1818
- name: Checkout
1919
uses: actions/checkout@v6
20+
with:
21+
submodules: recursive
2022

2123
- name: Setup JDK 17
2224
uses: actions/setup-java@v5

.gitmodules

Whitespace-only changes.

osu.Android.slnf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
"osu.Game\\osu.Game.csproj"
1717
]
1818
}
19-
}
19+
}

osu.Android/AndroidNativeBridgeManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ public void StartVulkanProbe()
117117

118118
[MethodImpl(MethodImplOptions.NoInlining)]
119119
public bool IsVulkanRecommended() => (vulkanProbe as VulkanProbe)?.IsRecommended ?? false;
120+
121+
public bool IsVulkanAvailable() => (vulkanProbe as VulkanProbe)?.IsAvailable ?? false;
120122
public void StopVulkanProbe()
121123
{
122124
(vulkanProbe as VulkanProbe)?.Dispose();

osu.Android/OboeAudioRedirector.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See the LICENCE file in the repository root for full licence text.
33

44
using System;
5+
using System.Collections;
56
using System.Collections.Generic;
67
using System.Diagnostics;
78
using System.Linq;
@@ -12,6 +13,7 @@
1213
using ManagedBass.Mix;
1314
using osu.Framework.Audio;
1415
using osu.Framework.Audio.Mixing;
16+
using osu.Framework.Bindables;
1517

1618
namespace osu.Android
1719
{
@@ -43,10 +45,16 @@ public void RefreshMixers(int hardwareSampleRate)
4345
addRootMixer(audioManager.TrackMixer);
4446
addRootMixer(audioManager.SampleMixer);
4547

48+
foreach (var mixer in getActiveMixers())
49+
addRootMixer(mixer);
50+
4651
if (mixerHandles.Count == 0)
4752
{
4853
addMixer(audioManager.TrackMixer);
4954
addMixer(audioManager.SampleMixer);
55+
56+
foreach (var mixer in getActiveMixers())
57+
addMixer(mixer);
5058
}
5159

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

73+
private IEnumerable<AudioMixer> getActiveMixers()
74+
{
75+
// Use reflection to access the internal activeMixers list in AudioManager.
76+
// In the official framework, it is an internal BindableList<AudioMixer> activeMixers.
77+
FieldInfo? field = typeof(AudioManager).GetField("activeMixers", BindingFlags.Instance | BindingFlags.NonPublic);
78+
if (field == null) yield break;
79+
80+
object? val = field.GetValue(audioManager);
81+
if (val is IEnumerable enumerable)
82+
{
83+
foreach (var item in enumerable)
84+
{
85+
if (item is AudioMixer mixer)
86+
yield return mixer;
87+
}
88+
}
89+
}
90+
6591
private void setupMasterMixer()
6692
{
6793
if (masterMixer != 0)

osu.Android/OsuGameActivity.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,15 @@ public IntPtr GetSurfaceGlobalRef()
183183
public void SurfaceCreated(ISurfaceHolder holder)
184184
{
185185
var surface = holder.Surface;
186-
if (surface != null && surface.Handle != IntPtr.Zero)
186+
if (surface != null && surface.IsValid)
187187
{
188-
var handle = surface.Handle;
189-
surfaceGlobalRef = global::Android.Runtime.JNIEnv.NewGlobalRef(handle);
190-
surfaceEvent.Set();
191-
Debug.WriteLine("[osu!] Native surface JNI global reference created");
188+
IntPtr handle = surface.Handle;
189+
if (handle == IntPtr.Zero) return;
190+
{
191+
surfaceGlobalRef = global::Android.Runtime.JNIEnv.NewGlobalRef(handle);
192+
surfaceEvent.Set();
193+
Debug.WriteLine("[osu!] Native surface JNI global reference created");
194+
}
192195
}
193196
}
194197

osu.Android/OsuGameAndroid.cs

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,50 @@
11
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22
// See the LICENCE file in the repository root for full licence text.
3+
using System.Diagnostics;
4+
using Microsoft.Maui.Devices;
5+
using osu.Android.Performance;
6+
using osu.Framework.Development;
37

4-
#pragma warning disable CA1422
5-
#pragma warning restore CA1422
6-
7-
using Android.App;
88
using Android.Content.PM;
9-
using Android.Content;
10-
using Android.Media;
11-
using Android.Views;
9+
using osu.Game.Performance;
10+
using osu.Game.Updater;
11+
using System.Collections.Specialized;
12+
using System;
13+
using System.Collections;
14+
using System.Collections.Generic;
1215
using Debug = System.Diagnostics.Debug;
13-
using Microsoft.Maui.Devices;
1416
using System.Linq;
17+
using System.Reflection;
1518
using System.Runtime.CompilerServices;
16-
using System;
17-
using System.Diagnostics;
19+
using Context = global::Android.Content.Context;
20+
using Android.Media;
21+
using Android.OS;
22+
using Android.Views;
1823
using osu.Android.Native;
1924
using osu.Framework.Allocation;
25+
using AudioManager = osu.Framework.Audio.AudioManager;
2026
using osu.Framework.Bindables;
21-
using osu.Framework.Development;
27+
using osu.Framework.Configuration;
28+
using osu.Framework.Extensions.IEnumerableExtensions;
29+
using osu.Framework.Graphics;
30+
using osu.Framework.Input;
2231
using osu.Framework.Platform;
32+
using osu.Framework.Threading;
33+
using osu.Game;
2334
using osu.Game.Configuration;
35+
using osu.Game.Overlays;
36+
using osu.Game.Overlays.Notifications;
2437
using osu.Game.Screens;
25-
using osu.Game.Updater;
2638
using osu.Game.Utils;
27-
using osu.Game;
28-
using osuTK;
29-
using osu.Game.Performance;
30-
using osu.Android.Performance;
39+
using Vector2 = osuTK.Vector2;
3140

3241
namespace osu.Android
3342
{
3443
public partial class OsuGameAndroid : OsuGame
3544
{
36-
[Cached]
3745
private readonly OsuGameActivity gameActivity;
3846

3947
private readonly object packageInfoLock = new object();
40-
4148
private PackageInfo? packageInfo;
4249
private bool packageInfoChecked;
4350

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

7986
private OboeAudioRedirector? audioRedirector;
87+
private Delegate? activeMixersHandler;
88+
private object? activeMixersList;
8089
private IntPtr updateAdpfSession;
8190
private IntPtr renderAdpfSession;
8291

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

107116
return getPackageInfo()?.VersionName ?? @"unknown";
108117
}
@@ -137,6 +146,30 @@ private void load()
137146
LocalConfig.BindWith(OsuSetting.AudioOffset, audioOffset);
138147

139148
audioRedirector = new OboeAudioRedirector(Audio);
149+
150+
try
151+
{
152+
// Use reflection to bind to collection changes of the internal activeMixers list in AudioManager.
153+
FieldInfo? field = typeof(AudioManager).GetField("activeMixers", BindingFlags.Instance | BindingFlags.NonPublic);
154+
if (field != null)
155+
{
156+
activeMixersList = field.GetValue(Audio);
157+
object? val = field.GetValue(Audio);
158+
if (val != null)
159+
{
160+
MethodInfo? bindMethod = val.GetType().GetMethod("BindCollectionChanged", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
161+
if (bindMethod != null)
162+
{
163+
var del = Delegate.CreateDelegate(bindMethod.GetParameters()[0].ParameterType, this, typeof(OsuGameAndroid).GetMethod(nameof(onActiveMixersChanged), BindingFlags.Instance | BindingFlags.NonPublic)!);
164+
bindMethod.Invoke(val, new object[] { del, true });
165+
}
166+
}
167+
}
168+
}
169+
catch (Exception ex)
170+
{
171+
Debug.WriteLine($"[osu!] Failed to bind to activeMixers via reflection: {ex.Message}");
172+
}
140173
}
141174

142175
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
@@ -215,9 +248,9 @@ protected override void LoadComplete()
215248
int hardwareSampleRate = 0;
216249
try
217250
{
218-
if (gameActivity.GetSystemService(Context.AudioService) is AudioManager audioManager)
251+
if (gameActivity.GetSystemService(global::Android.Content.Context.AudioService) is global::Android.Media.AudioManager audioManager)
219252
{
220-
string? rateStr = audioManager.GetProperty(AudioManager.PropertyOutputSampleRate);
253+
string? rateStr = audioManager.GetProperty(global::Android.Media.AudioManager.PropertyOutputSampleRate);
221254

222255
if (!string.IsNullOrEmpty(rateStr))
223256
hardwareSampleRate = int.Parse(rateStr);
@@ -360,7 +393,11 @@ private void selectHighestRefreshRate()
360393
}
361394
}
362395

363-
public bool IsVulkanRecommended() => (nativeBridges as AndroidNativeBridgeManager)?.IsVulkanRecommended() ?? false;
396+
public override bool IsVulkanRecommended => (nativeBridges as AndroidNativeBridgeManager)?.IsVulkanRecommended() ?? false;
397+
398+
public override bool IsVulkanSupported => (nativeBridges as AndroidNativeBridgeManager)?.IsVulkanAvailable() ?? false;
399+
400+
private void onActiveMixersChanged(object? sender, NotifyCollectionChangedEventArgs args) => Schedule(() => { if (lowLatencyAudio.Value) audioRedirector?.RefreshMixers(0); });
364401

365402
public double GetMeasuredAudioLatencyMs() => getMeasuredAudioLatencyFromBridge();
366403

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

372409
try
373410
{
374-
if (gameActivity.GetSystemService(Context.AudioService) is AudioManager audioManager)
411+
if (gameActivity.GetSystemService(global::Android.Content.Context.AudioService) is global::Android.Media.AudioManager audioManager)
375412
{
376-
string? rateStr = audioManager.GetProperty(AudioManager.PropertyOutputSampleRate);
413+
string? rateStr = audioManager.GetProperty(global::Android.Media.AudioManager.PropertyOutputSampleRate);
377414

378415
if (!string.IsNullOrEmpty(rateStr))
379416
hardwareSampleRate = int.Parse(rateStr);
@@ -443,11 +480,11 @@ private void updateOrientation()
443480
switch (orientation)
444481
{
445482
case MobileUtils.Orientation.Locked:
446-
gameActivity.RequestedOrientation = ScreenOrientation.Locked;
483+
gameActivity.RequestedOrientation = global::Android.Content.PM.ScreenOrientation.Locked;
447484
break;
448485

449486
case MobileUtils.Orientation.Portrait:
450-
gameActivity.RequestedOrientation = ScreenOrientation.Portrait;
487+
gameActivity.RequestedOrientation = global::Android.Content.PM.ScreenOrientation.Portrait;
451488
break;
452489

453490
case MobileUtils.Orientation.Default:
@@ -485,6 +522,18 @@ protected override void Dispose(bool isDisposing)
485522
audioRedirector?.Dispose();
486523
audioRedirector = null;
487524

525+
if (activeMixersList != null && activeMixersHandler != null)
526+
{
527+
try
528+
{
529+
MethodInfo? unbindMethod = activeMixersList.GetType().GetMethod("UnbindCollectionChanged", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
530+
unbindMethod?.Invoke(activeMixersList, new object[] { activeMixersHandler });
531+
}
532+
catch { }
533+
activeMixersList = null;
534+
activeMixersHandler = null;
535+
}
536+
488537
if (nativeBridges != null)
489538
disposeNativeBridges();
490539

@@ -524,6 +573,6 @@ protected override void UpdateAfterChildren()
524573
internal class AndroidBatteryInfo : BatteryInfo
525574
{
526575
public override double? ChargeLevel => Microsoft.Maui.Devices.Battery.ChargeLevel;
527-
public override bool OnBattery => Microsoft.Maui.Devices.Battery.PowerSource == BatteryPowerSource.Battery;
576+
public override bool OnBattery => Microsoft.Maui.Devices.Battery.PowerSource == global::Microsoft.Maui.Devices.BatteryPowerSource.Battery;
528577
}
529-
}
578+
}

osu.Desktop.slnf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@
2626
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj"
2727
]
2828
}
29-
}
29+
}

osu.Game/OsuGameBase.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ public virtual EndpointConfiguration CreateEndpoints() =>
118118
public string VersionHash { get; private set; }
119119

120120
public bool IsDeployedBuild => AssemblyVersion.Major > 0;
121+
public virtual bool IsVulkanRecommended => false;
122+
123+
public virtual bool IsVulkanSupported => false;
121124

122125
public virtual string Version
123126
{

0 commit comments

Comments
 (0)