Skip to content

Commit aa50738

Browse files
Copilotwinnerspiros
andcommitted
Add MAILBOX present mode detection and Oboe audio offset auto-suggest
Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/75739364-5690-4028-9d88-98f465b77bbc
1 parent aecf470 commit aa50738

4 files changed

Lines changed: 101 additions & 4 deletions

File tree

osu.Android/Native/VulkanProbe.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,28 @@ public bool HasDedicatedTransferQueue
193193
}
194194
}
195195

196+
/// <summary>
197+
/// Whether the device likely supports MAILBOX present mode for low-latency triple-buffered rendering.
198+
/// Detected via the <c>VK_GOOGLE_display_timing</c> extension, which is present on Android GPUs
199+
/// (Adreno, Mali) that also expose MAILBOX present mode.
200+
/// </summary>
201+
public bool SupportsMailboxPresentMode
202+
{
203+
get
204+
{
205+
if (disposed || nativePtr == 0) return false;
206+
207+
try
208+
{
209+
return nVulkanSupportsMailboxPresentMode(nativePtr) != 0;
210+
}
211+
catch
212+
{
213+
return false;
214+
}
215+
}
216+
}
217+
196218
public void Dispose()
197219
{
198220
if (disposed) return;
@@ -247,5 +269,8 @@ public void Dispose()
247269

248270
[DllImport("osu_native")]
249271
private static extern byte nVulkanHasDedicatedTransferQueue(long ptr);
272+
273+
[DllImport("osu_native")]
274+
private static extern byte nVulkanSupportsMailboxPresentMode(long ptr);
250275
}
251276
}

osu.Android/Native/vulkan_bridge.cpp

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ VulkanProbe::VulkanProbe() {
2020

2121
if (available_) {
2222
LOGI("Vulkan available: %s (API %u.%u.%u, driver %u, VRAM %u MB, "
23-
"queues %u, dedicatedCompute=%d, dedicatedTransfer=%d, swapchain=%d)",
23+
"queues %u, dedicatedCompute=%d, dedicatedTransfer=%d, swapchain=%d, mailbox=%d)",
2424
deviceInfo_.deviceName.c_str(),
2525
VK_VERSION_MAJOR(deviceInfo_.apiVersion),
2626
VK_VERSION_MINOR(deviceInfo_.apiVersion),
@@ -30,7 +30,8 @@ VulkanProbe::VulkanProbe() {
3030
deviceInfo_.queueFamilyCount,
3131
deviceInfo_.hasDedicatedComputeQueue ? 1 : 0,
3232
deviceInfo_.hasDedicatedTransferQueue ? 1 : 0,
33-
deviceInfo_.supportsSwapchain ? 1 : 0);
33+
deviceInfo_.supportsSwapchain ? 1 : 0,
34+
deviceInfo_.supportsMailboxPresentMode ? 1 : 0);
3435
} else {
3536
LOGI("Vulkan not available on this device");
3637
}
@@ -127,6 +128,7 @@ bool VulkanProbe::queryDevice() {
127128
// Query additional performance-relevant capabilities.
128129
queryMemory(selected);
129130
queryQueueFamilies(selected);
131+
queryMailboxSupport(selected);
130132

131133
return true;
132134
}
@@ -175,6 +177,31 @@ void VulkanProbe::queryQueueFamilies(VkPhysicalDevice device) {
175177
}
176178
}
177179

180+
void VulkanProbe::queryMailboxSupport(VkPhysicalDevice device) {
181+
// MAILBOX present mode requires a VkSurface to query definitively, but we detect
182+
// it using VK_GOOGLE_display_timing — a device extension that is present exclusively
183+
// on Android GPUs (Adreno, Mali) that also expose MAILBOX present mode support.
184+
// This gives us a reliable indication without needing an active surface.
185+
deviceInfo_.supportsMailboxPresentMode = false;
186+
187+
uint32_t extCount = 0;
188+
189+
if (vkEnumerateDeviceExtensionProperties(device, nullptr, &extCount, nullptr) != VK_SUCCESS || extCount == 0)
190+
return;
191+
192+
std::vector<VkExtensionProperties> extensions(extCount);
193+
194+
if (vkEnumerateDeviceExtensionProperties(device, nullptr, &extCount, extensions.data()) != VK_SUCCESS)
195+
return;
196+
197+
for (const auto& ext : extensions) {
198+
if (strcmp(ext.extensionName, "VK_GOOGLE_display_timing") == 0) {
199+
deviceInfo_.supportsMailboxPresentMode = true;
200+
return;
201+
}
202+
}
203+
}
204+
178205
void VulkanProbe::cleanup() {
179206
if (instance_ != VK_NULL_HANDLE) {
180207
vkDestroyInstance(instance_, nullptr);
@@ -231,4 +258,9 @@ unsigned char nVulkanHasDedicatedTransferQueue(long ptr) {
231258
return (probe && probe->getDeviceInfo().hasDedicatedTransferQueue) ? 1 : 0;
232259
}
233260

261+
unsigned char nVulkanSupportsMailboxPresentMode(long ptr) {
262+
auto* probe = reinterpret_cast<VulkanProbe*>(ptr);
263+
return (probe && probe->getDeviceInfo().supportsMailboxPresentMode) ? 1 : 0;
264+
}
265+
234266
} // extern "C"

osu.Android/Native/vulkan_bridge.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ class VulkanProbe {
2727
bool hasDedicatedComputeQueue;
2828
/// Whether the device has a dedicated transfer queue.
2929
bool hasDedicatedTransferQueue;
30+
/// Whether the device likely supports VK_PRESENT_MODE_MAILBOX_KHR for low-latency rendering.
31+
/// Detected via the VK_GOOGLE_display_timing device extension, which is present on
32+
/// Android GPUs (Adreno, Mali) that also expose MAILBOX present mode.
33+
bool supportsMailboxPresentMode;
3034
};
3135

3236
VulkanProbe();
@@ -47,5 +51,6 @@ class VulkanProbe {
4751
bool queryDevice();
4852
void queryMemory(VkPhysicalDevice device);
4953
void queryQueueFamilies(VkPhysicalDevice device);
54+
void queryMailboxSupport(VkPhysicalDevice device);
5055
void cleanup();
5156
};

osu.Android/OsuGameAndroid.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public partial class OsuGameAndroid : OsuGame
3434
private readonly Bindable<bool> performanceMode = new Bindable<bool>();
3535
private readonly Bindable<bool> lowLatencyAudio = new Bindable<bool>();
3636
private readonly Bindable<bool> vulkanProbeEnabled = new Bindable<bool>();
37+
private readonly BindableDouble audioOffset = new BindableDouble();
3738

3839
private OboeAudioBridge? oboeBridge;
3940
private VulkanProbe? vulkanProbe;
@@ -64,6 +65,7 @@ private void load(OsuConfigManager config)
6465
config.BindWith(OsuSetting.AndroidPerformanceMode, performanceMode);
6566
config.BindWith(OsuSetting.AndroidLowLatencyAudio, lowLatencyAudio);
6667
config.BindWith(OsuSetting.AndroidVulkanProbe, vulkanProbeEnabled);
68+
config.BindWith(OsuSetting.AudioOffset, audioOffset);
6769
}
6870

6971
protected override void LoadComplete()
@@ -114,7 +116,13 @@ private void startOboeBridge()
114116

115117
if (started)
116118
{
119+
// Log basic stream info immediately.
117120
logOboeInfo();
121+
122+
// Latency is measured asynchronously by the audio callback.
123+
// Schedule a check after a short warm-up period to get a stable reading
124+
// and apply the auto-suggested audio offset if appropriate.
125+
Scheduler.AddDelayed(applyMeasuredLatencyOffset, 2000);
118126
}
119127
else
120128
{
@@ -173,6 +181,7 @@ private void logVulkanInfo()
173181
Debug.WriteLine($"[osu!] Vulkan GPU: available={vulkanProbe.IsAvailable}, "
174182
+ $"API={major}.{minor}.{patch}, "
175183
+ $"swapchain={vulkanProbe.SupportsSwapchain}, "
184+
+ $"mailbox={vulkanProbe.SupportsMailboxPresentMode}, "
176185
+ $"VRAM={vulkanProbe.DeviceLocalMemoryMB}MB, "
177186
+ $"queueFamilies={vulkanProbe.QueueFamilyCount}, "
178187
+ $"dedicatedCompute={vulkanProbe.HasDedicatedComputeQueue}, "
@@ -187,8 +196,34 @@ private void logOboeInfo()
187196
+ $"api={(oboeBridge.IsAAudio ? "AAudio" : "OpenSLES")}, "
188197
+ $"sampleRate={oboeBridge.SampleRate}Hz, "
189198
+ $"burst={oboeBridge.FramesPerBurst}frames, "
190-
+ $"bufferSize={oboeBridge.BufferSizeInFrames}frames, "
191-
+ $"latency={oboeBridge.GetOutputLatencyMs():F1}ms");
199+
+ $"bufferSize={oboeBridge.BufferSizeInFrames}frames");
200+
}
201+
202+
/// <summary>
203+
/// Called after a warm-up delay to read the stable measured latency and apply it
204+
/// as an auto-suggested audio offset when the user hasn't set a manual value.
205+
/// </summary>
206+
private void applyMeasuredLatencyOffset()
207+
{
208+
if (oboeBridge == null) return;
209+
210+
double latency = oboeBridge.GetOutputLatencyMs();
211+
212+
Debug.WriteLine($"[osu!] Oboe measured latency after warm-up: {latency:F1}ms");
213+
214+
if (latency <= 0)
215+
return;
216+
217+
// Only auto-suggest when the user hasn't already configured a manual offset.
218+
// Use a small epsilon to safely compare against the default value of 0.
219+
if (Math.Abs(audioOffset.Value) >= 0.01)
220+
return;
221+
222+
// The audio offset compensates for hardware output delay: if audio arrives
223+
// 20 ms late, we need to set the offset to -20 ms so osu! plays notes earlier.
224+
double suggested = Math.Clamp(-latency, audioOffset.MinValue, audioOffset.MaxValue);
225+
audioOffset.Value = suggested;
226+
Debug.WriteLine($"[osu!] Audio offset auto-suggested: {suggested:F1}ms (hardware latency={latency:F1}ms)");
192227
}
193228

194229
/// <summary>

0 commit comments

Comments
 (0)