同步更新,以及增加asio和独占wasapi的支持

This commit is contained in:
LA
2025-12-28 14:15:09 +08:00
parent 559b9cef88
commit 8527ffddb7
28 changed files with 773 additions and 173 deletions

View File

@@ -0,0 +1,40 @@
# Ensure we're running from the correct directory (location of this file).
Set-Location -LiteralPath $PSScriptRoot
$ErrorActionPreference = 'Stop'
$zipPath = Join-Path $env:TEMP 'bassasio.zip'
try {
Invoke-WebRequest -Uri 'https://www.un4seen.com/stuff/bassasio.zip' -OutFile $zipPath
$destX64 = Join-Path $PSScriptRoot 'runtimes\win-x64\native'
$destX86 = Join-Path $PSScriptRoot 'runtimes\win-x86\native'
New-Item -ItemType Directory -Force -Path $destX64 | Out-Null
New-Item -ItemType Directory -Force -Path $destX86 | Out-Null
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [System.IO.Compression.ZipFile]::OpenRead($zipPath)
try {
foreach ($entry in $archive.Entries) {
switch ($entry.FullName) {
'x64/bassasio.dll' {
$out = Join-Path $destX64 'bassasio.dll'
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $out, $true)
}
'bassasio.dll' {
$out = Join-Path $destX86 'bassasio.dll'
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $out, $true)
}
}
}
}
finally {
$archive.Dispose()
}
}
finally {
if (Test-Path $zipPath) { Remove-Item -Force $zipPath }
}

View File

@@ -74,5 +74,10 @@ unzip -qjo bassmix24-android.zip libs/arm64-v8a/* -d ../osu.Framework.Android/ar
unzip -qjo bassmix24-android.zip libs/armeabi-v7a/* -d ../osu.Framework.Android/armeabi-v7a/ unzip -qjo bassmix24-android.zip libs/armeabi-v7a/* -d ../osu.Framework.Android/armeabi-v7a/
unzip -qjo bassmix24-android.zip libs/x86/* -d ../osu.Framework.Android/x86/ unzip -qjo bassmix24-android.zip libs/x86/* -d ../osu.Framework.Android/x86/
# bassasio (Windows only)
curl -Lso bassasio.zip https://www.un4seen.com/stuff/bassasio.zip
unzip -qjo bassasio.zip x64/bassasio.dll -d runtimes/win-x64/native/
unzip -qjo bassasio.zip bassasio.dll -d runtimes/win-x86/native/
# clean up # clean up
rm bass*.zip rm bass*.zip

View File

@@ -25,14 +25,14 @@ namespace osu.Framework.Tests.Audio
private volatile bool simulateLoss; private volatile bool simulateLoss;
protected override bool InitBass(int device) protected override bool InitBass(int device, AudioOutputMode outputMode, int? asioDeviceIndex)
{ {
try try
{ {
if (simulateLoss) if (simulateLoss)
return device == Bass.NoSoundDevice && base.InitBass(device); return device == Bass.NoSoundDevice && base.InitBass(device, outputMode, asioDeviceIndex);
return base.InitBass(device); return base.InitBass(device, outputMode, asioDeviceIndex);
} }
finally finally
{ {

View File

@@ -436,6 +436,7 @@ namespace osu.Framework.Tests.Clocks
[TestCase(1)] [TestCase(1)]
[TestCase(10)] [TestCase(10)]
[TestCase(50)] [TestCase(50)]
[FlakyTest]
public void TestNoDecoupledDrift(int updateRate) public void TestNoDecoupledDrift(int updateRate)
{ {
var stopwatch = new StopwatchClock(); var stopwatch = new StopwatchClock();

View File

@@ -254,6 +254,7 @@ namespace osu.Framework.Tests.Clocks
[TestCase(1)] [TestCase(1)]
[TestCase(10)] [TestCase(10)]
[TestCase(50)] [TestCase(50)]
[FlakyTest]
public void TestNoInterpolationDrift(int updateRate) public void TestNoInterpolationDrift(int updateRate)
{ {
var stopwatch = new StopwatchClock(); var stopwatch = new StopwatchClock();

View File

@@ -108,6 +108,7 @@ namespace osu.Framework.Tests.Graphics
} }
[Test] [Test]
[FlakyTest]
public void TestReadSaturated() public void TestReadSaturated()
{ {
var tripleBuffer = new TripleBuffer<TestObject>(); var tripleBuffer = new TripleBuffer<TestObject>();

View File

@@ -79,7 +79,7 @@ namespace osu.Framework.Tests.Visual.Localisation
{ {
} }
public string? Get(string key) => translations.TryGetValue(key, out string? value) ? value : null; public string? Get(string key) => translations.GetValueOrDefault(key);
public Task<string?> GetAsync(string key, CancellationToken cancellationToken = default) => Task.FromResult(Get(key)); public Task<string?> GetAsync(string key, CancellationToken cancellationToken = default) => Task.FromResult(Get(key));

View File

@@ -17,7 +17,7 @@ namespace osu.Framework.iOS
/// <summary> /// <summary>
/// Base <see cref="UIApplicationDelegate"/> implementation for osu!framework applications. /// Base <see cref="UIApplicationDelegate"/> implementation for osu!framework applications.
/// </summary> /// </summary>
public abstract class GameApplicationDelegate : UIApplicationDelegate public abstract class GameApplicationDelegate : UIResponder, IUIApplicationDelegate
{ {
internal event Action<string>? DragDrop; internal event Action<string>? DragDrop;
@@ -27,7 +27,7 @@ namespace osu.Framework.iOS
public IOSGameHost Host { get; private set; } = null!; public IOSGameHost Host { get; private set; } = null!;
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) public virtual bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{ {
mapLibraryNames(); mapLibraryNames();
@@ -46,7 +46,7 @@ namespace osu.Framework.iOS
return true; return true;
} }
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options) public virtual bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{ {
// copied verbatim from SDL: https://github.com/libsdl-org/SDL/blob/d252a8fe126b998bd1b0f4e4cf52312cd11de378/src/video/uikit/SDL_uikitappdelegate.m#L508-L535 // copied verbatim from SDL: https://github.com/libsdl-org/SDL/blob/d252a8fe126b998bd1b0f4e4cf52312cd11de378/src/video/uikit/SDL_uikitappdelegate.m#L508-L535
// the hope is that the SDL app delegate class does not have such handling exist there, but Apple does not provide a corresponding notification to make that possible. // the hope is that the SDL app delegate class does not have such handling exist there, but Apple does not provide a corresponding notification to make that possible.
@@ -55,6 +55,17 @@ namespace osu.Framework.iOS
return true; return true;
} }
public override void BuildMenu(IUIMenuBuilder builder)
{
base.BuildMenu(builder);
// Remove useless menus on iPadOS. This makes it almost match macOS, displaying only "Window" and "Help".
builder.RemoveMenu(UIMenuIdentifier.File.GetConstant());
builder.RemoveMenu(UIMenuIdentifier.Edit.GetConstant());
builder.RemoveMenu(UIMenuIdentifier.Format.GetConstant());
builder.RemoveMenu(UIMenuIdentifier.View.GetConstant());
}
/// <summary> /// <summary>
/// Creates the <see cref="Game"/> class to launch. /// Creates the <see cref="Game"/> class to launch.
/// </summary> /// </summary>

View File

@@ -38,7 +38,7 @@ namespace osu.Framework.iOS
public override void Create() public override void Create()
{ {
SDL_SetHint(SDL_HINT_IOS_HIDE_HOME_INDICATOR, "2"u8); SDL_SetHint(SDL_HINT_IOS_HIDE_HOME_INDICATOR, "2"u8).LogErrorIfFailed();
base.Create(); base.Create();
@@ -59,7 +59,7 @@ namespace osu.Framework.iOS
// iOS may be a good forward direction if this ever comes up, as a user may see a potentially higher // iOS may be a good forward direction if this ever comes up, as a user may see a potentially higher
// frame rate with multi-threaded mode turned on, but it is going to give them worse input latency // frame rate with multi-threaded mode turned on, but it is going to give them worse input latency
// and higher power usage. // and higher power usage.
SDL_SetiOSAnimationCallback(SDLWindowHandle, 1, &runFrame, ObjectHandle.Handle); SDL_SetiOSAnimationCallback(SDLWindowHandle, 1, &runFrame, ObjectHandle.Handle).ThrowIfFailed();
} }
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]

View File

@@ -0,0 +1,115 @@
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace osu.Framework.Audio.Asio
{
internal static class BassAsio
{
private const string dll = "bassasio";
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
internal delegate int AsioProcedure([MarshalAs(UnmanagedType.Bool)] bool input, int channel, IntPtr buffer, int length, IntPtr user);
[StructLayout(LayoutKind.Sequential)]
private struct AsioDeviceInfoNative
{
public IntPtr Name;
public IntPtr Driver;
public int Flags;
}
internal readonly struct AsioDeviceInfo
{
public AsioDeviceInfo(int index, string name)
{
Index = index;
Name = name;
}
public int Index { get; }
public string Name { get; }
}
[StructLayout(LayoutKind.Sequential)]
internal struct AsioInfo
{
public int Inputs;
public int Outputs;
public int Format;
public double BufferLength;
public double MinBufferLength;
public double MaxBufferLength;
public double PreferredBufferLength;
public double Granularity;
}
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_GetDeviceInfo")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool getDeviceInfo(int device, out AsioDeviceInfoNative info);
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_Init")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool Init(int device, int flags = 0);
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_Free")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool Free();
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_Start")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool Start(int bufferLength, int threads = 0);
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_Stop")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool Stop();
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_GetInfo")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetInfo(out AsioInfo info);
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_GetRate")]
internal static extern double GetRate();
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_SetRate")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetRate(double rate);
[DllImport(dll, CallingConvention = CallingConvention.Winapi, EntryPoint = "BASS_ASIO_ChannelEnable")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool ChannelEnable([MarshalAs(UnmanagedType.Bool)] bool input, int channel, AsioProcedure procedure, IntPtr user);
internal static IEnumerable<AsioDeviceInfo> EnumerateDevices(int maxDevices = 64)
{
for (int i = 0; i < maxDevices; i++)
{
if (!getDeviceInfo(i, out var info))
yield break;
string name = Marshal.PtrToStringAnsi(info.Name);
if (!string.IsNullOrEmpty(name))
yield return new AsioDeviceInfo(i, name);
}
}
internal static bool TryFindDeviceIndexByName(string name, out int index)
{
foreach (var d in EnumerateDevices())
{
if (d.Name == name)
{
index = d.Index;
return true;
}
}
index = -1;
return false;
}
}
}

View File

@@ -13,6 +13,7 @@ using JetBrains.Annotations;
using ManagedBass; using ManagedBass;
using ManagedBass.Fx; using ManagedBass.Fx;
using ManagedBass.Mix; using ManagedBass.Mix;
using osu.Framework.Audio.Asio;
using osu.Framework.Audio.Mixing; using osu.Framework.Audio.Mixing;
using osu.Framework.Audio.Mixing.Bass; using osu.Framework.Audio.Mixing.Bass;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@@ -80,7 +81,7 @@ namespace osu.Framework.Audio
/// Consumers should provide a "Default" audio device entry which sets <see cref="AudioDevice"/> to an empty string. /// Consumers should provide a "Default" audio device entry which sets <see cref="AudioDevice"/> to an empty string.
/// </para> /// </para>
/// </remarks> /// </remarks>
public IEnumerable<string> AudioDeviceNames => audioDeviceNames; public IEnumerable<string> AudioDeviceNames => getAudioDeviceEntries();
/// <summary> /// <summary>
/// Is fired whenever a new audio device is discovered and provides its name. /// Is fired whenever a new audio device is discovered and provides its name.
@@ -153,7 +154,22 @@ namespace osu.Framework.Audio
// Mutated by multiple threads, must be thread safe. // Mutated by multiple threads, must be thread safe.
private ImmutableArray<DeviceInfo> audioDevices = ImmutableArray<DeviceInfo>.Empty; private ImmutableArray<DeviceInfo> audioDevices = ImmutableArray<DeviceInfo>.Empty;
private ImmutableList<string> audioDeviceNames = ImmutableList<string>.Empty; private ImmutableList<string> bassDeviceNames = ImmutableList<string>.Empty;
private const string type_bass = "BASS";
private const string type_wasapi_shared = "WASAPI Shared";
private const string type_wasapi_exclusive = "WASAPI Exclusive";
private const string type_asio = "ASIO";
private bool syncingSelection;
protected enum AudioOutputMode
{
Default,
WasapiShared,
WasapiExclusive,
Asio,
}
private Scheduler scheduler => thread.Scheduler; private Scheduler scheduler => thread.Scheduler;
@@ -196,7 +212,15 @@ namespace osu.Framework.Audio
} }
AudioDevice.ValueChanged += _ => scheduler.AddOnce(initCurrentDevice); AudioDevice.ValueChanged += _ => scheduler.AddOnce(initCurrentDevice);
UseExperimentalWasapi.ValueChanged += _ => scheduler.AddOnce(initCurrentDevice); UseExperimentalWasapi.ValueChanged += _ => scheduler.AddOnce(() =>
{
// Keep checkbox meaningful by mapping it onto the dropdown selection when possible.
bool selectionChanged = syncSelectionFromExperimentalFlag();
// If selection didn't change (eg. Default), we still need to reinitialise.
if (!selectionChanged)
initCurrentDevice();
});
// initCurrentDevice not required for changes to `GlobalMixerHandle` as it is only changed when experimental wasapi is toggled (handled above). // initCurrentDevice not required for changes to `GlobalMixerHandle` as it is only changed when experimental wasapi is toggled (handled above).
GlobalMixerHandle.ValueChanged += handle => usingGlobalMixer.Value = handle.NewValue.HasValue; GlobalMixerHandle.ValueChanged += handle => usingGlobalMixer.Value = handle.NewValue.HasValue;
@@ -267,8 +291,7 @@ namespace osu.Framework.Audio
/// Channels removed from this <see cref="AudioMixer"/> fall back to the global <see cref="SampleMixer"/>. /// Channels removed from this <see cref="AudioMixer"/> fall back to the global <see cref="SampleMixer"/>.
/// </remarks> /// </remarks>
/// <param name="identifier">An identifier displayed on the audio mixer visualiser.</param> /// <param name="identifier">An identifier displayed on the audio mixer visualiser.</param>
public AudioMixer CreateAudioMixer(string identifier = default) => public AudioMixer CreateAudioMixer(string identifier = default) => createAudioMixer(SampleMixer, !string.IsNullOrEmpty(identifier) ? identifier : $"user #{Interlocked.Increment(ref userMixerID)}");
createAudioMixer(SampleMixer, !string.IsNullOrEmpty(identifier) ? identifier : $"user #{Interlocked.Increment(ref userMixerID)}");
private AudioMixer createAudioMixer(AudioMixer fallbackMixer, string identifier) private AudioMixer createAudioMixer(AudioMixer fallbackMixer, string identifier)
{ {
@@ -332,24 +355,61 @@ namespace osu.Framework.Audio
/// </summary> /// </summary>
private void initCurrentDevice() private void initCurrentDevice()
{ {
string deviceName = AudioDevice.Value; var (mode, deviceName, asioIndex) = parseSelection(AudioDevice.Value);
// keep legacy setting and dropdown selection in sync.
if (!syncingSelection)
{
syncingSelection = true;
try
{
switch (mode)
{
case AudioOutputMode.WasapiShared:
UseExperimentalWasapi.Value = true;
break;
case AudioOutputMode.Default:
case AudioOutputMode.WasapiExclusive:
case AudioOutputMode.Asio:
// Exclusive/ASIO are separate modes; keep the experimental flag off to avoid confusion.
UseExperimentalWasapi.Value = false;
break;
}
}
finally
{
syncingSelection = false;
}
}
// try using the specified device // try using the specified device
int deviceIndex = audioDeviceNames.FindIndex(d => d == deviceName); if (mode == AudioOutputMode.Asio)
if (deviceIndex >= 0 && trySetDevice(BASS_INTERNAL_DEVICE_COUNT + deviceIndex)) return; {
// ASIO output still requires BASS to be initialised, but output is performed by BassAsio.
// Use the OS default BASS device as a fallback initialisation target.
if (trySetDevice(bass_default_device, mode, asioIndex)) return;
}
else
{
// try using the specified device
int deviceIndex = bassDeviceNames.FindIndex(d => d == deviceName);
if (deviceIndex >= 0 && trySetDevice(BASS_INTERNAL_DEVICE_COUNT + deviceIndex, mode, asioIndex)) return;
}
// try using the system default if there is any device present. // try using the system default if there is any device present.
// mobiles are an exception as the built-in speakers may not be provided as an audio device name, // mobiles are an exception as the built-in speakers may not be provided as an audio device name,
// but they are still provided by BASS under the internal device name "Default". // but they are still provided by BASS under the internal device name "Default".
if ((audioDeviceNames.Count > 0 || RuntimeInfo.IsMobile) && trySetDevice(bass_default_device)) return; if ((bassDeviceNames.Count > 0 || RuntimeInfo.IsMobile) && trySetDevice(bass_default_device, mode, asioIndex)) return;
// no audio devices can be used, so try using Bass-provided "No sound" device as last resort. // no audio devices can be used, so try using Bass-provided "No sound" device as last resort.
trySetDevice(Bass.NoSoundDevice); trySetDevice(Bass.NoSoundDevice, AudioOutputMode.Default, null);
// we're boned. even "No sound" device won't initialise. // we're boned. even "No sound" device won't initialise.
return; return;
bool trySetDevice(int deviceId) bool trySetDevice(int deviceId, AudioOutputMode outputMode, int? asioDeviceIndex)
{ {
var device = audioDevices.ElementAtOrDefault(deviceId); var device = audioDevices.ElementAtOrDefault(deviceId);
@@ -362,7 +422,7 @@ namespace osu.Framework.Audio
return false; return false;
// initialize new device // initialize new device
if (!InitBass(deviceId)) if (!InitBass(deviceId, outputMode, asioDeviceIndex))
return false; return false;
//we have successfully initialised a new device. //we have successfully initialised a new device.
@@ -372,12 +432,53 @@ namespace osu.Framework.Audio
} }
} }
private bool syncSelectionFromExperimentalFlag()
{
if (syncingSelection)
return false;
// Do not attempt to remap explicit exclusive/ASIO selections.
if (tryParseSuffixed(AudioDevice.Value, type_wasapi_exclusive, out _) || tryParseSuffixed(AudioDevice.Value, type_asio, out _))
return false;
if (string.IsNullOrEmpty(AudioDevice.Value))
return false;
string baseName;
if (tryParseSuffixed(AudioDevice.Value, type_bass, out string parsed))
baseName = parsed;
else if (tryParseSuffixed(AudioDevice.Value, type_wasapi_shared, out parsed))
baseName = parsed;
else
// Legacy un-suffixed device name.
baseName = AudioDevice.Value;
string desired = formatEntry(baseName, UseExperimentalWasapi.Value ? type_wasapi_shared : type_bass);
if (AudioDevice.Value == desired)
return false;
syncingSelection = true;
try
{
AudioDevice.Value = desired;
return true;
}
finally
{
syncingSelection = false;
}
}
/// <summary> /// <summary>
/// This method calls <see cref="Bass.Init(int, int, DeviceInitFlags, IntPtr, IntPtr)"/>. /// This method calls <see cref="Bass.Init(int, int, DeviceInitFlags, IntPtr, IntPtr)"/>.
/// It can be overridden for unit testing. /// It can be overridden for unit testing.
/// </summary> /// </summary>
/// <param name="device">The device to initialise.</param> /// <param name="device">The device to initialise.</param>
protected virtual bool InitBass(int device) /// <param name="outputMode">The output mode to use for playback.</param>
/// <param name="asioDeviceIndex">When <paramref name="outputMode"/> is ASIO, the selected ASIO device index.</param>
protected virtual bool InitBass(int device, AudioOutputMode outputMode, int? asioDeviceIndex)
{ {
// this likely doesn't help us but also doesn't seem to cause any issues or any cpu increase. // this likely doesn't help us but also doesn't seem to cause any issues or any cpu increase.
Bass.UpdatePeriod = 5; Bass.UpdatePeriod = 5;
@@ -402,25 +503,15 @@ namespace osu.Framework.Audio
// - This disables support for ItunSMPB tag parsing to match previous expectations. // - This disables support for ItunSMPB tag parsing to match previous expectations.
// - This also disables a change which assumes a 529 sample (2116 byte in stereo 16-bit) delay if the MP3 file doesn't specify one. // - This also disables a change which assumes a 529 sample (2116 byte in stereo 16-bit) delay if the MP3 file doesn't specify one.
// (That was added in Bass for more consistent results across platforms and standard/mp3-free BASS versions, because OSX/iOS's MP3 decoder always removes 529 samples) // (That was added in Bass for more consistent results across platforms and standard/mp3-free BASS versions, because OSX/iOS's MP3 decoder always removes 529 samples)
Bass.Configure((ManagedBass.Configuration)68, 1); // Bass.Configure((ManagedBass.Configuration)68, 1);
// Disable BASS_CONFIG_DEV_TIMEOUT flag to keep BASS audio output from pausing on device processing timeout. // Disable BASS_CONFIG_DEV_TIMEOUT flag to keep BASS audio output from pausing on device processing timeout.
// See https://www.un4seen.com/forum/?topic=19601 for more information. // See https://www.un4seen.com/forum/?topic=19601 for more information.
Bass.Configure((ManagedBass.Configuration)70, false); Bass.Configure((ManagedBass.Configuration)70, false);
bool success = attemptInit();
if (success || !UseExperimentalWasapi.Value)
return success;
// in the case we're using experimental WASAPI, give a second chance of initialisation by forcefully disabling it.
Logger.Log($"BASS device {device} failed to initialise with experimental WASAPI, disabling", level: LogLevel.Error);
UseExperimentalWasapi.Value = false;
return attemptInit();
bool attemptInit() bool attemptInit()
{ {
bool innerSuccess = thread.InitDevice(device, UseExperimentalWasapi.Value); bool innerSuccess = thread.InitDevice(device, toThreadOutputMode(outputMode), asioDeviceIndex);
bool alreadyInitialised = Bass.LastError == Errors.Already; bool alreadyInitialised = Bass.LastError == Errors.Already;
if (alreadyInitialised) if (alreadyInitialised)
@@ -449,6 +540,26 @@ namespace osu.Framework.Audio
return true; return true;
} }
return attemptInit();
}
private static AudioThread.AudioThreadOutputMode toThreadOutputMode(AudioOutputMode mode)
{
switch (mode)
{
case AudioOutputMode.WasapiShared:
return AudioThread.AudioThreadOutputMode.WasapiShared;
case AudioOutputMode.WasapiExclusive:
return AudioThread.AudioThreadOutputMode.WasapiExclusive;
case AudioOutputMode.Asio:
return AudioThread.AudioThreadOutputMode.Asio;
default:
return AudioThread.AudioThreadOutputMode.Default;
}
} }
private void syncAudioDevices() private void syncAudioDevices()
@@ -458,8 +569,8 @@ namespace osu.Framework.Audio
// Bass should always be providing "No sound" and "Default" device. // Bass should always be providing "No sound" and "Default" device.
Trace.Assert(audioDevices.Length >= BASS_INTERNAL_DEVICE_COUNT, "Bass did not provide any audio devices."); Trace.Assert(audioDevices.Length >= BASS_INTERNAL_DEVICE_COUNT, "Bass did not provide any audio devices.");
var oldDeviceNames = audioDeviceNames; var oldDeviceNames = bassDeviceNames;
var newDeviceNames = audioDeviceNames = audioDevices.Skip(BASS_INTERNAL_DEVICE_COUNT).Where(d => d.IsEnabled).Select(d => d.Name).ToImmutableList(); var newDeviceNames = bassDeviceNames = audioDevices.Skip(BASS_INTERNAL_DEVICE_COUNT).Where(d => d.IsEnabled).Select(d => d.Name).ToImmutableList();
scheduler.Add(() => scheduler.Add(() =>
{ {
@@ -485,6 +596,95 @@ namespace osu.Framework.Audio
} }
} }
private IEnumerable<string> getAudioDeviceEntries()
{
var entries = new List<string>();
// Base BASS devices.
entries.AddRange(bassDeviceNames.Select(d => formatEntry(d, type_bass)));
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
// WASAPI variants for each BASS device.
entries.AddRange(bassDeviceNames.Select(d => formatEntry(d, type_wasapi_shared)));
entries.AddRange(bassDeviceNames.Select(d => formatEntry(d, type_wasapi_exclusive)));
// ASIO drivers.
try
{
foreach (var device in BassAsio.EnumerateDevices())
entries.Add(formatEntry(device.Name, type_asio));
}
catch
{
// ASIO is optional and may not be available.
}
}
return entries;
}
private static string formatEntry(string name, string type) => $"{name} ({type})";
private (AudioOutputMode mode, string deviceName, int? asioDeviceIndex) parseSelection(string selection)
{
// Default device.
if (string.IsNullOrEmpty(selection))
{
return (UseExperimentalWasapi.Value && RuntimeInfo.OS == RuntimeInfo.Platform.Windows
? AudioOutputMode.WasapiShared
: AudioOutputMode.Default, string.Empty, null);
}
if (tryParseSuffixed(selection, type_bass, out string name))
return (AudioOutputMode.Default, name, null);
if (tryParseSuffixed(selection, type_wasapi_shared, out name))
return (AudioOutputMode.WasapiShared, name, null);
if (tryParseSuffixed(selection, type_wasapi_exclusive, out name))
return (AudioOutputMode.WasapiExclusive, name, null);
if (tryParseSuffixed(selection, type_asio, out name))
{
int? index = null;
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
try
{
if (BassAsio.TryFindDeviceIndexByName(name, out int found))
index = found;
}
catch
{
}
}
return (AudioOutputMode.Asio, name, index);
}
// Legacy value (raw BASS device name). Keep old behaviour: the experimental flag decides shared WASAPI.
if (UseExperimentalWasapi.Value && RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
return (AudioOutputMode.WasapiShared, selection, null);
return (AudioOutputMode.Default, selection, null);
}
private static bool tryParseSuffixed(string value, string type, out string baseName)
{
string suffix = $" ({type})";
if (value.EndsWith(suffix, StringComparison.Ordinal))
{
baseName = value[..^suffix.Length];
return true;
}
baseName = string.Empty;
return false;
}
/// <summary> /// <summary>
/// Check whether any audio device changes have occurred. /// Check whether any audio device changes have occurred.
/// ///
@@ -535,7 +735,13 @@ namespace osu.Framework.Audio
protected virtual bool IsCurrentDeviceValid() protected virtual bool IsCurrentDeviceValid()
{ {
var device = audioDevices.ElementAtOrDefault(Bass.CurrentDevice); var device = audioDevices.ElementAtOrDefault(Bass.CurrentDevice);
bool isFallback = string.IsNullOrEmpty(AudioDevice.Value) ? !device.IsDefault : device.Name != AudioDevice.Value; var (mode, selectedName, _) = parseSelection(AudioDevice.Value);
// ASIO output selection does not map to a BASS device name; just ensure we're initialised.
if (mode == AudioOutputMode.Asio)
return device.IsEnabled && device.IsInitialized;
bool isFallback = string.IsNullOrEmpty(selectedName) ? !device.IsDefault : device.Name != selectedName;
return device.IsEnabled && device.IsInitialized && !isFallback; return device.IsEnabled && device.IsInitialized && !isFallback;
} }

View File

@@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO; using System.IO;
using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
@@ -55,7 +56,7 @@ namespace osu.Framework.Graphics.Shaders
/// <param name="fragment">The fragment shader name.</param> /// <param name="fragment">The fragment shader name.</param>
/// <returns>A cached <see cref="IShader"/> instance, if existing.</returns> /// <returns>A cached <see cref="IShader"/> instance, if existing.</returns>
public virtual IShader? GetCachedShader(string vertex, string fragment) public virtual IShader? GetCachedShader(string vertex, string fragment)
=> shaderCache.TryGetValue((vertex, fragment), out IShader? shader) ? shader : null; => shaderCache.GetValueOrDefault((vertex, fragment));
/// <summary> /// <summary>
/// Attempts to retrieve an already-cached shader part. /// Attempts to retrieve an already-cached shader part.
@@ -63,7 +64,7 @@ namespace osu.Framework.Graphics.Shaders
/// <param name="name">The name of the shader part.</param> /// <param name="name">The name of the shader part.</param>
/// <returns>A cached <see cref="IShaderPart"/> instance, if existing.</returns> /// <returns>A cached <see cref="IShaderPart"/> instance, if existing.</returns>
public virtual IShaderPart? GetCachedShaderPart(string name) public virtual IShaderPart? GetCachedShaderPart(string name)
=> partCache.TryGetValue(name, out IShaderPart? part) ? part : null; => partCache.GetValueOrDefault(name);
/// <summary> /// <summary>
/// Attempts to retrieve the raw data for a shader file. /// Attempts to retrieve the raw data for a shader file.

View File

@@ -368,10 +368,7 @@ namespace osu.Framework.Graphics.Transforms
return min; return min;
} }
if (lastAppliedTransformIndices.TryGetValue(targetMember, out int val)) return lastAppliedTransformIndices.GetValueOrDefault(targetMember, 0);
return val;
return 0;
} }
/// <summary> /// <summary>

View File

@@ -12,7 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Input.States; using osu.Framework.Input.States;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Framework.Logging; // using osu.Framework.Logging;
using osuTK; using osuTK;
namespace osu.Framework.Input.Bindings namespace osu.Framework.Input.Bindings

View File

@@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions.TypeExtensions; // using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Input.States; using osu.Framework.Input.States;
using osuTK.Input; using osuTK.Input;

View File

@@ -209,7 +209,7 @@ namespace osu.Framework.Input
/// <param name="button">The button to find the manager for.</param> /// <param name="button">The button to find the manager for.</param>
/// <returns>The <see cref="MouseButtonEventManager"/>.</returns> /// <returns>The <see cref="MouseButtonEventManager"/>.</returns>
public MouseButtonEventManager GetButtonEventManagerFor(MouseButton button) => public MouseButtonEventManager GetButtonEventManagerFor(MouseButton button) =>
mouseButtonEventManagers.TryGetValue(button, out var manager) ? manager : null; mouseButtonEventManagers.GetValueOrDefault(button);
/// <summary> /// <summary>
/// Create a <see cref="KeyEventManager"/> for a specified key. /// Create a <see cref="KeyEventManager"/> for a specified key.

View File

@@ -11,7 +11,7 @@ namespace osu.Framework.Platform.Linux
public SDL3LinuxWindow(GraphicsSurfaceType surfaceType, string appName, bool bypassCompositor) public SDL3LinuxWindow(GraphicsSurfaceType surfaceType, string appName, bool bypassCompositor)
: base(surfaceType, appName) : base(surfaceType, appName)
{ {
SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, bypassCompositor ? "1"u8 : "0"u8); SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, bypassCompositor ? "1"u8 : "0"u8).LogErrorIfFailed();
} }
} }
} }

View File

@@ -42,7 +42,7 @@ namespace osu.Framework.Platform.SDL3
// assume that empty text means no text. // assume that empty text means no text.
public override string? GetText() => SDL_HasClipboardText() ? SDL_GetClipboardText() : null; public override string? GetText() => SDL_HasClipboardText() ? SDL_GetClipboardText() : null;
public override void SetText(string text) => SDL_SetClipboardText(text); public override void SetText(string text) => SDL_SetClipboardText(text).LogErrorIfFailed();
public override Image<TPixel>? GetImage<TPixel>() public override Image<TPixel>? GetImage<TPixel>()
{ {

View File

@@ -17,9 +17,9 @@ namespace osu.Framework.Platform.SDL3
// this reset is required even on changing from one fullscreen resolution to another. // this reset is required even on changing from one fullscreen resolution to another.
// if it is not included, the GL context will not get the correct size. // if it is not included, the GL context will not get the correct size.
// this is mentioned by multiple sources as an SDL issue, which seems to resolve by similar means (see https://discourse.libsdl.org/t/sdl-setwindowsize-does-not-work-in-fullscreen/20711/4). // this is mentioned by multiple sources as an SDL issue, which seems to resolve by similar means (see https://discourse.libsdl.org/t/sdl-setwindowsize-does-not-work-in-fullscreen/20711/4).
SDL_SetWindowBordered(SDLWindowHandle, true); SDL_SetWindowBordered(SDLWindowHandle, true).LogErrorIfFailed();
SDL_SetWindowFullscreen(SDLWindowHandle, false); SDL_SetWindowFullscreen(SDLWindowHandle, false).LogErrorIfFailed();
SDL_RestoreWindow(SDLWindowHandle); SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed();
base.UpdateWindowStateAndSize(state, display, displayMode); base.UpdateWindowStateAndSize(state, display, displayMode);
} }

View File

@@ -0,0 +1,10 @@
// 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;
using static SDL.SDL3;
namespace osu.Framework.Platform.SDL3
{
internal class SDL3Exception(string? expression) : Exception($"{SDL_GetError()} (at {expression})");
}

View File

@@ -1,12 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Drawing; using System.Drawing;
using System.Runtime.CompilerServices;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges;
using osu.Framework.Logging;
using osuTK.Input; using osuTK.Input;
using SDL; using SDL;
using static SDL.SDL3; using static SDL.SDL3;
@@ -1045,7 +1048,7 @@ namespace osu.Framework.Platform.SDL3
{ {
int bpp; int bpp;
uint unused; uint unused;
SDL_GetMasksForPixelFormat(mode.format, &bpp, &unused, &unused, &unused, &unused); SDL_GetMasksForPixelFormat(mode.format, &bpp, &unused, &unused, &unused, &unused).ThrowIfFailed();
return new DisplayMode(SDL_GetPixelFormatName(mode.format), new Size(mode.w, mode.h), bpp, mode.refresh_rate, displayIndex); return new DisplayMode(SDL_GetPixelFormatName(mode.format), new Size(mode.w, mode.h), bpp, mode.refresh_rate, displayIndex);
} }
@@ -1144,5 +1147,100 @@ namespace osu.Framework.Platform.SDL3
SDL_ClearError(); SDL_ClearError();
return error; return error;
} }
private static void logError(string? expression)
{
Logger.Log($"SDL error: {SDL_GetError()}");
if (!string.IsNullOrEmpty(expression))
Logger.Log($"at {expression}");
}
public static SDLBool LogErrorIfFailed(this SDLBool returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (!returnValue)
logError(expression);
return returnValue;
}
public static void ThrowIfFailed(this SDLBool returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (!returnValue)
throw new SDL3Exception(expression);
}
public static SDL_PropertiesID ThrowIfFailed(this SDL_PropertiesID returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == 0)
throw new SDL3Exception(expression);
return returnValue;
}
public static int LogErrorIfFailed(this int returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == -1)
logError(expression);
return returnValue;
}
public static IntPtr ThrowIfFailed(this IntPtr returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == IntPtr.Zero)
throw new SDL3Exception(expression);
return returnValue;
}
public static SDLArray<T>? LogErrorIfFailed<T>(this SDLArray<T>? returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
where T : unmanaged
{
if (returnValue == null)
logError(expression);
return returnValue;
}
public static SDL_PenDeviceType ThrowIfFailed(this SDL_PenDeviceType returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == SDL_PenDeviceType.SDL_PEN_DEVICE_TYPE_INVALID)
throw new SDL3Exception(expression);
return returnValue;
}
public static string? LogErrorIfFailed(this string? returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == null)
logError(expression);
return returnValue;
}
public static SDL_DisplayID LogErrorIfFailed(this SDL_DisplayID returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == 0)
logError(expression);
return returnValue;
}
public static SDL_DisplayID ThrowIfFailed(this SDL_DisplayID returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
{
if (returnValue == 0)
throw new SDL3Exception(expression);
return returnValue;
}
public static unsafe T* LogErrorIfFailed<T>(T* returnValue, [CallerArgumentExpression("returnValue")] string? expression = null)
where T : unmanaged
{
if (returnValue == null)
logError(expression);
return returnValue;
}
} }
} }

View File

@@ -33,12 +33,12 @@ namespace osu.Framework.Platform.SDL3
switch (surfaceType) switch (surfaceType)
{ {
case GraphicsSurfaceType.OpenGL: case GraphicsSurfaceType.OpenGL:
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_RED_SIZE, 8).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_GREEN_SIZE, 8).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_BLUE_SIZE, 8); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_BLUE_SIZE, 8).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_ACCUM_ALPHA_SIZE, 0); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_ACCUM_ALPHA_SIZE, 0).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_DEPTH_SIZE, 16); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_DEPTH_SIZE, 16).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STENCIL_SIZE, 8); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STENCIL_SIZE, 8).ThrowIfFailed();
break; break;
case GraphicsSurfaceType.Vulkan: case GraphicsSurfaceType.Vulkan:
@@ -60,7 +60,7 @@ namespace osu.Framework.Platform.SDL3
public Size GetDrawableSize() public Size GetDrawableSize()
{ {
int width, height; int width, height;
SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height); SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height).ThrowIfFailed();
return new Size(width, height); return new Size(width, height);
} }
@@ -70,19 +70,19 @@ namespace osu.Framework.Platform.SDL3
{ {
if (RuntimeInfo.IsMobile) if (RuntimeInfo.IsMobile)
{ {
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLProfile.SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLProfile.SDL_GL_CONTEXT_PROFILE_ES).ThrowIfFailed();
// Minimum OpenGL version for ES profile: // Minimum OpenGL version for ES profile:
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 0); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 0).ThrowIfFailed();
} }
else else
{ {
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLProfile.SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLProfile.SDL_GL_CONTEXT_PROFILE_CORE).ThrowIfFailed();
// Minimum OpenGL version for core profile: // Minimum OpenGL version for core profile:
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3).ThrowIfFailed();
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 2).ThrowIfFailed();
} }
context = SDL_GL_CreateContext(window.SDLWindowHandle); context = SDL_GL_CreateContext(window.SDLWindowHandle);
@@ -90,7 +90,7 @@ namespace osu.Framework.Platform.SDL3
if (context == null) if (context == null)
throw new InvalidOperationException($"Failed to create an SDL3 GL context ({SDL_GetError()})"); throw new InvalidOperationException($"Failed to create an SDL3 GL context ({SDL_GetError()})");
SDL_GL_MakeCurrent(window.SDLWindowHandle, context); SDL_GL_MakeCurrent(window.SDLWindowHandle, context).ThrowIfFailed();
loadBindings(); loadBindings();
} }
@@ -126,29 +126,13 @@ namespace osu.Framework.Platform.SDL3
string? str = Marshal.PtrToStringAnsi(new IntPtr(ptr)); string? str = Marshal.PtrToStringAnsi(new IntPtr(ptr));
Debug.Assert(str != null); Debug.Assert(str != null);
entryPointsInstance[i] = getProcAddress(str); entryPointsInstance[i] = SDL_GL_GetProcAddress(str);
} }
} }
pointsInfo.SetValue(bindings, entryPointsInstance); pointsInfo.SetValue(bindings, entryPointsInstance);
} }
private IntPtr getProcAddress(string symbol)
{
const SDL_LogCategory error_category = SDL_LogCategory.SDL_LOG_CATEGORY_ERROR;
SDL_LogPriority oldPriority = SDL_GetLogPriority(error_category);
// Prevent logging calls to SDL_GL_GetProcAddress() that fail on systems which don't have the requested symbol (typically macOS).
SDL_SetLogPriority(error_category, SDL_LogPriority.SDL_LOG_PRIORITY_INFO);
IntPtr ret = SDL_GL_GetProcAddress(symbol);
// Reset the logging behaviour.
SDL_SetLogPriority(error_category, oldPriority);
return ret;
}
int? IOpenGLGraphicsSurface.BackbufferFramebuffer int? IOpenGLGraphicsSurface.BackbufferFramebuffer
{ {
get get
@@ -182,11 +166,8 @@ namespace osu.Framework.Platform.SDL3
} }
set set
{ {
if (RuntimeInfo.IsDesktop) SDL_GL_SetSwapInterval(value ? 1 : 0);
{ verticalSync = value;
SDL_GL_SetSwapInterval(value ? 1 : 0);
verticalSync = value;
}
} }
} }
@@ -198,7 +179,7 @@ namespace osu.Framework.Platform.SDL3
void IOpenGLGraphicsSurface.DeleteContext(IntPtr context) => SDL_GL_DestroyContext((SDL_GLContextState*)context); void IOpenGLGraphicsSurface.DeleteContext(IntPtr context) => SDL_GL_DestroyContext((SDL_GLContextState*)context);
void IOpenGLGraphicsSurface.MakeCurrent(IntPtr context) => SDL_GL_MakeCurrent(window.SDLWindowHandle, (SDL_GLContextState*)context); void IOpenGLGraphicsSurface.MakeCurrent(IntPtr context) => SDL_GL_MakeCurrent(window.SDLWindowHandle, (SDL_GLContextState*)context);
void IOpenGLGraphicsSurface.ClearCurrent() => SDL_GL_MakeCurrent(window.SDLWindowHandle, null); void IOpenGLGraphicsSurface.ClearCurrent() => SDL_GL_MakeCurrent(window.SDLWindowHandle, null);
IntPtr IOpenGLGraphicsSurface.GetProcAddress(string symbol) => getProcAddress(symbol); IntPtr IOpenGLGraphicsSurface.GetProcAddress(string symbol) => SDL_GL_GetProcAddress(symbol);
#endregion #endregion
@@ -220,7 +201,7 @@ namespace osu.Framework.Platform.SDL3
#region Android-specific implementation #region Android-specific implementation
[SupportedOSPlatform("android")] [SupportedOSPlatform("android")]
IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL_GetAndroidJNIEnv(); IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL_GetAndroidJNIEnv().ThrowIfFailed();
[SupportedOSPlatform("android")] [SupportedOSPlatform("android")]
IntPtr IAndroidGraphicsSurface.SurfaceHandle => window.SurfaceHandle; IntPtr IAndroidGraphicsSurface.SurfaceHandle => window.SurfaceHandle;

View File

@@ -15,7 +15,7 @@ namespace osu.Framework.Platform.SDL3
protected override unsafe void UpdateWindowStateAndSize(WindowState state, Display display, DisplayMode displayMode) protected override unsafe void UpdateWindowStateAndSize(WindowState state, Display display, DisplayMode displayMode)
{ {
// This sets the status bar to hidden. // This sets the status bar to hidden.
SDL_SetWindowFullscreen(SDLWindowHandle, true); SDL_SetWindowFullscreen(SDLWindowHandle, true).LogErrorIfFailed();
// Don't run base logic at all. Let's keep things simple. // Don't run base logic at all. Let's keep things simple.
} }

View File

@@ -75,7 +75,7 @@ namespace osu.Framework.Platform.SDL3
set set
{ {
title = value; title = value;
ScheduleCommand(() => SDL_SetWindowTitle(SDLWindowHandle, title)); ScheduleCommand(() => SDL_SetWindowTitle(SDLWindowHandle, title).LogErrorIfFailed());
} }
} }
@@ -95,6 +95,8 @@ namespace osu.Framework.Platform.SDL3
return IntPtr.Zero; return IntPtr.Zero;
var props = SDL_GetWindowProperties(SDLWindowHandle); var props = SDL_GetWindowProperties(SDLWindowHandle);
if (props == 0)
return IntPtr.Zero;
switch (RuntimeInfo.OS) switch (RuntimeInfo.OS)
{ {
@@ -134,6 +136,8 @@ namespace osu.Framework.Platform.SDL3
return IntPtr.Zero; return IntPtr.Zero;
var props = SDL_GetWindowProperties(SDLWindowHandle); var props = SDL_GetWindowProperties(SDLWindowHandle);
if (props == 0)
return IntPtr.Zero;
if (IsWayland) if (IsWayland)
return SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, IntPtr.Zero); return SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, IntPtr.Zero);
@@ -161,7 +165,7 @@ namespace osu.Framework.Platform.SDL3
{ {
ObjectHandle = new ObjectHandle<SDL3Window>(this, GCHandleType.Normal); ObjectHandle = new ObjectHandle<SDL3Window>(this, GCHandleType.Normal);
SDL_SetHint(SDL_HINT_APP_NAME, appName); SDL_SetHint(SDL_HINT_APP_NAME, appName).LogErrorIfFailed();
if (!SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO | SDL_InitFlags.SDL_INIT_GAMEPAD)) if (!SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO | SDL_InitFlags.SDL_INIT_GAMEPAD))
{ {
@@ -174,7 +178,6 @@ namespace osu.Framework.Platform.SDL3
SDL3 Revision: {SDL_GetRevision()} SDL3 Revision: {SDL_GetRevision()}
SDL3 Video driver: {SDL_GetCurrentVideoDriver()}"); SDL3 Video driver: {SDL_GetCurrentVideoDriver()}");
SDL_SetLogPriority(SDL_LogCategory.SDL_LOG_CATEGORY_ERROR, SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG);
SDL_SetLogOutputFunction(&logOutput, IntPtr.Zero); SDL_SetLogOutputFunction(&logOutput, IntPtr.Zero);
SDL_SetEventFilter(&eventFilter, ObjectHandle.Handle); SDL_SetEventFilter(&eventFilter, ObjectHandle.Handle);
@@ -212,13 +215,13 @@ namespace osu.Framework.Platform.SDL3
flags |= WindowState.ToFlags(); flags |= WindowState.ToFlags();
flags |= graphicsSurface.Type.ToFlags(); flags |= graphicsSurface.Type.ToFlags();
SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "0"u8); SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "0"u8).LogErrorIfFailed();
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"u8); SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"u8).LogErrorIfFailed();
SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"u8); // disable touch events generating synthetic mouse events on desktop platforms SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"u8).LogErrorIfFailed(); // disable touch events generating synthetic mouse events on desktop platforms
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"u8); // disable mouse events generating synthetic touch events on mobile platforms SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"u8).LogErrorIfFailed(); // disable mouse events generating synthetic touch events on mobile platforms
SDL_SetHint(SDL_HINT_PEN_TOUCH_EVENTS, "0"u8); SDL_SetHint(SDL_HINT_PEN_TOUCH_EVENTS, "0"u8).LogErrorIfFailed();
SDL_SetHint(SDL_HINT_PEN_MOUSE_EVENTS, "0"u8); SDL_SetHint(SDL_HINT_PEN_MOUSE_EVENTS, "0"u8).LogErrorIfFailed();
SDL_SetHint(SDL_HINT_IME_IMPLEMENTED_UI, "composition"u8); SDL_SetHint(SDL_HINT_IME_IMPLEMENTED_UI, "composition"u8).LogErrorIfFailed();
SDLWindowHandle = SDL_CreateWindow(title, Size.Width, Size.Height, flags); SDLWindowHandle = SDL_CreateWindow(title, Size.Width, Size.Height, flags);
@@ -228,7 +231,7 @@ namespace osu.Framework.Platform.SDL3
// we want text input to only be active when SDL3DesktopWindowTextInput is active. // we want text input to only be active when SDL3DesktopWindowTextInput is active.
// SDL activates it by default on some platforms: https://github.com/libsdl-org/SDL/blob/release-2.0.16/src/video/SDL_video.c#L573-L582 // SDL activates it by default on some platforms: https://github.com/libsdl-org/SDL/blob/release-2.0.16/src/video/SDL_video.c#L573-L582
// so we deactivate it on startup. // so we deactivate it on startup.
SDL_StopTextInput(SDLWindowHandle); SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed();
graphicsSurface.Initialise(); graphicsSurface.Initialise();
@@ -241,7 +244,7 @@ namespace osu.Framework.Platform.SDL3
/// </summary> /// </summary>
public virtual void Run() public virtual void Run()
{ {
SDL_AddEventWatch(&eventWatch, ObjectHandle.Handle); SDL_AddEventWatch(&eventWatch, ObjectHandle.Handle).LogErrorIfFailed();
RunMainLoop(); RunMainLoop();
} }
@@ -405,14 +408,14 @@ namespace osu.Framework.Platform.SDL3
var flags = SDL_GetWindowFlags(SDLWindowHandle); var flags = SDL_GetWindowFlags(SDLWindowHandle);
if (flags.HasFlagFast(SDL_WindowFlags.SDL_WINDOW_MINIMIZED)) if (flags.HasFlagFast(SDL_WindowFlags.SDL_WINDOW_MINIMIZED))
SDL_RestoreWindow(SDLWindowHandle); SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed();
SDL_RaiseWindow(SDLWindowHandle); SDL_RaiseWindow(SDLWindowHandle).LogErrorIfFailed();
}); });
public void Hide() => ScheduleCommand(() => public void Hide() => ScheduleCommand(() =>
{ {
SDL_HideWindow(SDLWindowHandle); SDL_HideWindow(SDLWindowHandle).LogErrorIfFailed();
}); });
public void Show() => ScheduleCommand(() => public void Show() => ScheduleCommand(() =>
@@ -430,7 +433,7 @@ namespace osu.Framework.Platform.SDL3
SDL_FlashWindow(SDLWindowHandle, flashUntilFocused SDL_FlashWindow(SDLWindowHandle, flashUntilFocused
? SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED ? SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED
: SDL_FlashOperation.SDL_FLASH_BRIEFLY); : SDL_FlashOperation.SDL_FLASH_BRIEFLY).LogErrorIfFailed();
}); });
public void CancelFlash() => ScheduleCommand(() => public void CancelFlash() => ScheduleCommand(() =>
@@ -438,12 +441,12 @@ namespace osu.Framework.Platform.SDL3
if (!RuntimeInfo.IsDesktop) if (!RuntimeInfo.IsDesktop)
return; return;
SDL_FlashWindow(SDLWindowHandle, SDL_FlashOperation.SDL_FLASH_CANCEL); SDL_FlashWindow(SDLWindowHandle, SDL_FlashOperation.SDL_FLASH_CANCEL).LogErrorIfFailed();
}); });
public void EnableScreenSuspension() => ScheduleCommand(() => SDL_EnableScreenSaver()); public void EnableScreenSuspension() => ScheduleCommand(() => SDL_EnableScreenSaver().LogErrorIfFailed());
public void DisableScreenSuspension() => ScheduleCommand(() => SDL_DisableScreenSaver()); public void DisableScreenSuspension() => ScheduleCommand(() => SDL_DisableScreenSaver().LogErrorIfFailed());
/// <summary> /// <summary>
/// Attempts to set the window's icon to the specified image. /// Attempts to set the window's icon to the specified image.
@@ -463,10 +466,12 @@ namespace osu.Framework.Platform.SDL3
fixed (Rgba32* ptr = pixelSpan) fixed (Rgba32* ptr = pixelSpan)
{ {
var pixelFormat = SDL_GetPixelFormatForMasks(32, 0xff, 0xff00, 0xff0000, 0xff000000); var pixelFormat = SDL_GetPixelFormatForMasks(32, 0xff, 0xff00, 0xff0000, 0xff000000);
surface = SDL_CreateSurfaceFrom(imageSize.Width, imageSize.Height, pixelFormat, new IntPtr(ptr), imageSize.Width * 4); surface = SDL3Extensions.LogErrorIfFailed(SDL_CreateSurfaceFrom(imageSize.Width, imageSize.Height, pixelFormat, new IntPtr(ptr), imageSize.Width * 4));
if (surface == null)
return;
} }
SDL_SetWindowIcon(SDLWindowHandle, surface); SDL_SetWindowIcon(SDLWindowHandle, surface).LogErrorIfFailed();
SDL_DestroySurface(surface); SDL_DestroySurface(surface);
}); });
} }
@@ -495,7 +500,7 @@ namespace osu.Framework.Platform.SDL3
do do
{ {
eventsRead = SDL_PeepEvents(events, SDL_EventAction.SDL_GETEVENT, SDL_EventType.SDL_EVENT_FIRST, SDL_EventType.SDL_EVENT_LAST); eventsRead = SDL_PeepEvents(events, SDL_EventAction.SDL_GETEVENT, SDL_EventType.SDL_EVENT_FIRST, SDL_EventType.SDL_EVENT_LAST).LogErrorIfFailed();
for (int i = 0; i < eventsRead; i++) for (int i = 0; i < eventsRead; i++)
HandleEvent(events[i]); HandleEvent(events[i]);
} while (eventsRead == events_per_peep); } while (eventsRead == events_per_peep);

View File

@@ -51,7 +51,7 @@ namespace osu.Framework.Platform.SDL3
throw new InvalidOperationException($"Cannot set {nameof(RelativeMouseMode)} to true when the cursor is not hidden via {nameof(CursorState)}."); throw new InvalidOperationException($"Cannot set {nameof(RelativeMouseMode)} to true when the cursor is not hidden via {nameof(CursorState)}.");
relativeMouseMode = value; relativeMouseMode = value;
ScheduleCommand(() => SDL_SetWindowRelativeMouseMode(SDLWindowHandle, value)); ScheduleCommand(() => SDL_SetWindowRelativeMouseMode(SDLWindowHandle, value).LogErrorIfFailed());
updateCursorConfinement(); updateCursorConfinement();
} }
} }
@@ -69,7 +69,7 @@ namespace osu.Framework.Platform.SDL3
/// </remarks> /// </remarks>
public bool MouseAutoCapture public bool MouseAutoCapture
{ {
set => ScheduleCommand(() => SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, value ? "1"u8 : "0"u8)); set => ScheduleCommand(() => SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, value ? "1"u8 : "0"u8).LogErrorIfFailed());
} }
/// <summary> /// <summary>
@@ -101,9 +101,9 @@ namespace osu.Framework.Platform.SDL3
ScheduleCommand(() => ScheduleCommand(() =>
{ {
if (cursorVisible) if (cursorVisible)
SDL_ShowCursor(); SDL_ShowCursor().LogErrorIfFailed();
else else
SDL_HideCursor(); SDL_HideCursor().LogErrorIfFailed();
}); });
/// <summary> /// <summary>
@@ -113,7 +113,7 @@ namespace osu.Framework.Platform.SDL3
{ {
bool confined = CursorState.HasFlagFast(CursorState.Confined); bool confined = CursorState.HasFlagFast(CursorState.Confined);
ScheduleCommand(() => SDL_SetWindowMouseGrab(SDLWindowHandle, confined)); ScheduleCommand(() => SDL_SetWindowMouseGrab(SDLWindowHandle, confined).LogErrorIfFailed());
// Don't use SDL_SetWindowMouseRect when relative mode is enabled, as relative mode already confines the OS cursor to the window. // Don't use SDL_SetWindowMouseRect when relative mode is enabled, as relative mode already confines the OS cursor to the window.
// This is fine for our use case, as UserInputManager will clamp the mouse position. // This is fine for our use case, as UserInputManager will clamp the mouse position.
@@ -122,12 +122,12 @@ namespace osu.Framework.Platform.SDL3
ScheduleCommand(() => ScheduleCommand(() =>
{ {
var rect = ((RectangleI)(CursorConfineRect / Scale)).ToSDLRect(); var rect = ((RectangleI)(CursorConfineRect / Scale)).ToSDLRect();
SDL_SetWindowMouseRect(SDLWindowHandle, &rect); SDL_SetWindowMouseRect(SDLWindowHandle, &rect).LogErrorIfFailed();
}); });
} }
else else
{ {
ScheduleCommand(() => SDL_SetWindowMouseRect(SDLWindowHandle, null)); ScheduleCommand(() => SDL_SetWindowMouseRect(SDLWindowHandle, null).LogErrorIfFailed());
} }
} }
@@ -192,25 +192,25 @@ namespace osu.Framework.Platform.SDL3
public virtual void StartTextInput(TextInputProperties properties) => ScheduleCommand(() => public virtual void StartTextInput(TextInputProperties properties) => ScheduleCommand(() =>
{ {
currentTextInputProperties ??= SDL_CreateProperties(); currentTextInputProperties ??= SDL_CreateProperties().ThrowIfFailed();
var props = currentTextInputProperties.Value; var props = currentTextInputProperties.Value;
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_TYPE_NUMBER, (long)properties.Type.ToSDLTextInputType()); SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_TYPE_NUMBER, (long)properties.Type.ToSDLTextInputType()).LogErrorIfFailed();
if (!properties.AutoCapitalisation) if (!properties.AutoCapitalisation)
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER, (long)SDL_Capitalization.SDL_CAPITALIZE_NONE); SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER, (long)SDL_Capitalization.SDL_CAPITALIZE_NONE).LogErrorIfFailed();
else else
SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER); SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER).LogErrorIfFailed();
if (properties.Type == TextInputType.Code) if (properties.Type == TextInputType.Code)
SDL_SetBooleanProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN, false); SDL_SetBooleanProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN, false).LogErrorIfFailed();
else else
SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN); SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN).LogErrorIfFailed();
SDL_StartTextInputWithProperties(SDLWindowHandle, props); SDL_StartTextInputWithProperties(SDLWindowHandle, props).LogErrorIfFailed();
}); });
public void StopTextInput() => ScheduleCommand(() => SDL_StopTextInput(SDLWindowHandle)); public void StopTextInput() => ScheduleCommand(() => SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed());
/// <summary> /// <summary>
/// Resets internal state of the platform-native IME. /// Resets internal state of the platform-native IME.
@@ -218,19 +218,19 @@ namespace osu.Framework.Platform.SDL3
/// </summary> /// </summary>
public virtual void ResetIme() => ScheduleCommand(() => public virtual void ResetIme() => ScheduleCommand(() =>
{ {
SDL_StopTextInput(SDLWindowHandle); SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed();
if (currentTextInputProperties is SDL_PropertiesID props) if (currentTextInputProperties is SDL_PropertiesID props)
SDL_StartTextInputWithProperties(SDLWindowHandle, props); SDL_StartTextInputWithProperties(SDLWindowHandle, props).LogErrorIfFailed();
else else
SDL_StartTextInput(SDLWindowHandle); SDL_StartTextInput(SDLWindowHandle).LogErrorIfFailed();
}); });
public void SetTextInputRect(RectangleF rect) => ScheduleCommand(() => public void SetTextInputRect(RectangleF rect) => ScheduleCommand(() =>
{ {
// TODO: SDL3 allows apps to set cursor position through the third parameter of SDL_SetTextInputArea. // TODO: SDL3 allows apps to set cursor position through the third parameter of SDL_SetTextInputArea.
var sdlRect = ((RectangleI)(rect / Scale)).ToSDLRect(); var sdlRect = ((RectangleI)(rect / Scale)).ToSDLRect();
SDL_SetTextInputArea(SDLWindowHandle, &sdlRect, 0); SDL_SetTextInputArea(SDLWindowHandle, &sdlRect, 0).LogErrorIfFailed();
}); });
#region SDL Event Handling #region SDL Event Handling
@@ -359,11 +359,11 @@ namespace osu.Framework.Platform.SDL3
if (controllers.ContainsKey(instanceID)) if (controllers.ContainsKey(instanceID))
return; return;
SDL_Joystick* joystick = SDL_OpenJoystick(instanceID); SDL_Joystick* joystick = SDL3Extensions.LogErrorIfFailed(SDL_OpenJoystick(instanceID));
SDL_Gamepad* controller = null; SDL_Gamepad* controller = null;
if (SDL_IsGamepad(instanceID)) if (SDL_IsGamepad(instanceID))
controller = SDL_OpenGamepad(instanceID); controller = SDL3Extensions.LogErrorIfFailed(SDL_OpenGamepad(instanceID));
controllers[instanceID] = new SDL3ControllerBindings(joystick, controller); controllers[instanceID] = new SDL3ControllerBindings(joystick, controller);
} }
@@ -373,7 +373,7 @@ namespace osu.Framework.Platform.SDL3
/// </summary> /// </summary>
private void populateJoysticks() private void populateJoysticks()
{ {
using var joysticks = SDL_GetJoysticks(); using var joysticks = SDL_GetJoysticks().LogErrorIfFailed();
if (joysticks == null) if (joysticks == null)
return; return;
@@ -531,7 +531,7 @@ namespace osu.Framework.Platform.SDL3
private void handleKeymapChangedEvent() => KeymapChanged?.Invoke(); private void handleKeymapChangedEvent() => KeymapChanged?.Invoke();
private static TabletPenDeviceType getPenType(SDL_PenID instanceID) => SDL_GetPenDeviceType(instanceID).ToTabletPenDeviceType(); private static TabletPenDeviceType getPenType(SDL_PenID instanceID) => SDL_GetPenDeviceType(instanceID).ThrowIfFailed().ToTabletPenDeviceType();
private void handlePenMotionEvent(SDL_PenMotionEvent evtPenMotion) private void handlePenMotionEvent(SDL_PenMotionEvent evtPenMotion)
{ {

View File

@@ -25,7 +25,7 @@ namespace osu.Framework.Platform.SDL3
config.BindWith(FrameworkSetting.MinimiseOnFocusLossInFullscreen, minimiseOnFocusLoss); config.BindWith(FrameworkSetting.MinimiseOnFocusLossInFullscreen, minimiseOnFocusLoss);
minimiseOnFocusLoss.BindValueChanged(e => minimiseOnFocusLoss.BindValueChanged(e =>
{ {
ScheduleCommand(() => SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, e.NewValue ? "1"u8 : "0"u8)); ScheduleCommand(() => SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, e.NewValue ? "1"u8 : "0"u8).LogErrorIfFailed());
}, true); }, true);
fetchDisplays(); fetchDisplays();
@@ -70,7 +70,7 @@ namespace osu.Framework.Platform.SDL3
if (min.Width > sizeWindowed.MaxValue.Width || min.Height > sizeWindowed.MaxValue.Height) if (min.Width > sizeWindowed.MaxValue.Width || min.Height > sizeWindowed.MaxValue.Height)
throw new InvalidOperationException($"Expected a size less than max window size ({sizeWindowed.MaxValue}), got {min}"); throw new InvalidOperationException($"Expected a size less than max window size ({sizeWindowed.MaxValue}), got {min}");
ScheduleCommand(() => SDL_SetWindowMinimumSize(SDLWindowHandle, min.Width, min.Height)); ScheduleCommand(() => SDL_SetWindowMinimumSize(SDLWindowHandle, min.Width, min.Height).LogErrorIfFailed());
}; };
sizeWindowed.MaxValueChanged += max => sizeWindowed.MaxValueChanged += max =>
@@ -81,7 +81,7 @@ namespace osu.Framework.Platform.SDL3
if (max.Width < sizeWindowed.MinValue.Width || max.Height < sizeWindowed.MinValue.Height) if (max.Width < sizeWindowed.MinValue.Width || max.Height < sizeWindowed.MinValue.Height)
throw new InvalidOperationException($"Expected a size greater than min window size ({sizeWindowed.MinValue}), got {max}"); throw new InvalidOperationException($"Expected a size greater than min window size ({sizeWindowed.MinValue}), got {max}");
ScheduleCommand(() => SDL_SetWindowMaximumSize(SDLWindowHandle, max.Width, max.Height)); ScheduleCommand(() => SDL_SetWindowMaximumSize(SDLWindowHandle, max.Width, max.Height).LogErrorIfFailed());
}; };
config.BindWith(FrameworkSetting.SizeFullscreen, sizeFullscreen); config.BindWith(FrameworkSetting.SizeFullscreen, sizeFullscreen);
@@ -165,7 +165,7 @@ namespace osu.Framework.Platform.SDL3
set set
{ {
position = value; position = value;
ScheduleCommand(() => SDL_SetWindowPosition(SDLWindowHandle, value.X, value.Y)); ScheduleCommand(() => SDL_SetWindowPosition(SDLWindowHandle, value.X, value.Y).LogErrorIfFailed());
} }
} }
@@ -183,7 +183,7 @@ namespace osu.Framework.Platform.SDL3
return; return;
resizable = value; resizable = value;
ScheduleCommand(() => SDL_SetWindowResizable(SDLWindowHandle, value)); ScheduleCommand(() => SDL_SetWindowResizable(SDLWindowHandle, value).LogErrorIfFailed());
} }
} }
@@ -245,9 +245,9 @@ namespace osu.Framework.Platform.SDL3
ScheduleCommand(() => ScheduleCommand(() =>
{ {
if (value) if (value)
SDL_ShowWindow(SDLWindowHandle); SDL_ShowWindow(SDLWindowHandle).LogErrorIfFailed();
else else
SDL_HideWindow(SDLWindowHandle); SDL_HideWindow(SDLWindowHandle).LogErrorIfFailed();
}); });
} }
} }
@@ -392,7 +392,7 @@ namespace osu.Framework.Platform.SDL3
} }
display = new Display(displayIndex, display = new Display(displayIndex,
SDL_GetDisplayName(displayID), SDL_GetDisplayName(displayID).LogErrorIfFailed(),
new Rectangle(rect.x, rect.y, rect.w, rect.h), new Rectangle(rect.x, rect.y, rect.w, rect.h),
new Rectangle(usableBounds.x, usableBounds.y, usableBounds.w, usableBounds.h), new Rectangle(usableBounds.x, usableBounds.y, usableBounds.w, usableBounds.h),
displayModes); displayModes);
@@ -421,7 +421,7 @@ namespace osu.Framework.Platform.SDL3
get get
{ {
SDL_Rect rect; SDL_Rect rect;
SDL_GetDisplayBounds(displayID, &rect); SDL_GetDisplayBounds(displayID, &rect).LogErrorIfFailed();
return new Rectangle(rect.x, rect.y, rect.w, rect.h); return new Rectangle(rect.x, rect.y, rect.w, rect.h);
} }
} }
@@ -460,7 +460,7 @@ namespace osu.Framework.Platform.SDL3
private unsafe void fetchWindowSize(bool storeToConfig = true) private unsafe void fetchWindowSize(bool storeToConfig = true)
{ {
int w, h; int w, h;
SDL_GetWindowSize(SDLWindowHandle, &w, &h); SDL_GetWindowSize(SDLWindowHandle, &w, &h).LogErrorIfFailed();
int drawableW = graphicsSurface.GetDrawableSize().Width; int drawableW = graphicsSurface.GetDrawableSize().Width;
@@ -487,7 +487,7 @@ namespace osu.Framework.Platform.SDL3
case SDL_EventType.SDL_EVENT_WINDOW_MOVED: case SDL_EventType.SDL_EVENT_WINDOW_MOVED:
// explicitly requery as there are occasions where what SDL has provided us with is not up-to-date. // explicitly requery as there are occasions where what SDL has provided us with is not up-to-date.
int x, y; int x, y;
SDL_GetWindowPosition(SDLWindowHandle, &x, &y); SDL_GetWindowPosition(SDLWindowHandle, &x, &y).ThrowIfFailed();
var newPosition = new Point(x, y); var newPosition = new Point(x, y);
if (!newPosition.Equals(Position)) if (!newPosition.Equals(Position))
@@ -605,7 +605,7 @@ namespace osu.Framework.Platform.SDL3
windowMaximised = maximized; windowMaximised = maximized;
} }
var newDisplayID = SDL_GetDisplayForWindow(SDLWindowHandle); var newDisplayID = SDL_GetDisplayForWindow(SDLWindowHandle).ThrowIfFailed();
if (displayID != newDisplayID) if (displayID != newDisplayID)
{ {
@@ -676,9 +676,9 @@ namespace osu.Framework.Platform.SDL3
case WindowState.Normal: case WindowState.Normal:
Size = sizeWindowed.Value; Size = sizeWindowed.Value;
SDL_RestoreWindow(SDLWindowHandle); SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed();
SDL_SetWindowSize(SDLWindowHandle, Size.Width, Size.Height); SDL_SetWindowSize(SDLWindowHandle, Size.Width, Size.Height).LogErrorIfFailed();
SDL_SetWindowResizable(SDLWindowHandle, Resizable); SDL_SetWindowResizable(SDLWindowHandle, Resizable).LogErrorIfFailed();
readWindowPositionFromConfig(state, display); readWindowPositionFromConfig(state, display);
break; break;
@@ -690,8 +690,8 @@ namespace osu.Framework.Platform.SDL3
ensureWindowOnDisplay(display); ensureWindowOnDisplay(display);
SDL_SetWindowFullscreenMode(SDLWindowHandle, &closestMode); SDL_SetWindowFullscreenMode(SDLWindowHandle, &closestMode).LogErrorIfFailed();
SDL_SetWindowFullscreen(SDLWindowHandle, true); SDL_SetWindowFullscreen(SDLWindowHandle, true).LogErrorIfFailed();
break; break;
case WindowState.FullscreenBorderless: case WindowState.FullscreenBorderless:
@@ -699,16 +699,16 @@ namespace osu.Framework.Platform.SDL3
break; break;
case WindowState.Maximised: case WindowState.Maximised:
SDL_RestoreWindow(SDLWindowHandle); SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed();
ensureWindowOnDisplay(display); ensureWindowOnDisplay(display);
SDL_MaximizeWindow(SDLWindowHandle); SDL_MaximizeWindow(SDLWindowHandle).LogErrorIfFailed();
break; break;
case WindowState.Minimised: case WindowState.Minimised:
ensureWindowOnDisplay(display); ensureWindowOnDisplay(display);
SDL_MinimizeWindow(SDLWindowHandle); SDL_MinimizeWindow(SDLWindowHandle).LogErrorIfFailed();
break; break;
} }
} }
@@ -721,7 +721,8 @@ namespace osu.Framework.Platform.SDL3
return false; return false;
} }
var mode = windowState == WindowState.Fullscreen ? SDL_GetWindowFullscreenMode(windowHandle) : SDL_GetDesktopDisplayMode(displayID); SDL_ClearError();
var mode = windowState == WindowState.Fullscreen ? SDL_GetWindowFullscreenMode(windowHandle) : SDL3Extensions.LogErrorIfFailed(SDL_GetDesktopDisplayMode(displayID));
string type = windowState == WindowState.Fullscreen ? "fullscreen" : "desktop"; string type = windowState == WindowState.Fullscreen ? "fullscreen" : "desktop";
if (mode != null) if (mode != null)
@@ -768,7 +769,7 @@ namespace osu.Framework.Platform.SDL3
{ {
if (tryGetDisplayAtIndex(display.Index, out var requestedID)) if (tryGetDisplayAtIndex(display.Index, out var requestedID))
{ {
if (requestedID == SDL_GetDisplayForWindow(SDLWindowHandle)) if (requestedID == SDL_GetDisplayForWindow(SDLWindowHandle).LogErrorIfFailed())
return; return;
} }
@@ -844,8 +845,8 @@ namespace osu.Framework.Platform.SDL3
ensureWindowOnDisplay(display); ensureWindowOnDisplay(display);
// this is a generally sane method of handling borderless, and works well on macOS and linux. // this is a generally sane method of handling borderless, and works well on macOS and linux.
SDL_SetWindowFullscreenMode(SDLWindowHandle, null); SDL_SetWindowFullscreenMode(SDLWindowHandle, null).LogErrorIfFailed();
SDL_SetWindowFullscreen(SDLWindowHandle, true); SDL_SetWindowFullscreen(SDLWindowHandle, true).LogErrorIfFailed();
return display.Bounds.Size; return display.Bounds.Size;
} }

View File

@@ -88,7 +88,7 @@ namespace osu.Framework.Platform.Windows
&& RelativeMouseMode) && RelativeMouseMode)
{ {
var pt = PointToScreen(new Point((int)LastMousePosition.Value.X, (int)LastMousePosition.Value.Y)); var pt = PointToScreen(new Point((int)LastMousePosition.Value.X, (int)LastMousePosition.Value.Y));
SDL_WarpMouseGlobal(pt.X, pt.Y); // this directly calls the SetCursorPos win32 API SDL_WarpMouseGlobal(pt.X, pt.Y).LogErrorIfFailed(); // this directly calls the SetCursorPos win32 API
} }
} }
@@ -121,7 +121,7 @@ namespace osu.Framework.Platform.Windows
protected override unsafe Size SetBorderless(Display display) protected override unsafe Size SetBorderless(Display display)
{ {
SDL_SetWindowBordered(SDLWindowHandle, false); SDL_SetWindowBordered(SDLWindowHandle, false).LogErrorIfFailed();
var newSize = display.Bounds.Size; var newSize = display.Bounds.Size;
@@ -130,7 +130,7 @@ namespace osu.Framework.Platform.Windows
// we also trick the game into thinking the window has normal size: see Size setter override // we also trick the game into thinking the window has normal size: see Size setter override
newSize += new Size(windows_borderless_width_hack, 0); newSize += new Size(windows_borderless_width_hack, 0);
SDL_SetWindowSize(SDLWindowHandle, newSize.Width, newSize.Height); SDL_SetWindowSize(SDLWindowHandle, newSize.Width, newSize.Height).LogErrorIfFailed();
Position = display.Bounds.Location; Position = display.Bounds.Location;
return newSize; return newSize;

View File

@@ -9,6 +9,7 @@ using System.Linq;
using ManagedBass; using ManagedBass;
using ManagedBass.Mix; using ManagedBass.Mix;
using ManagedBass.Wasapi; using ManagedBass.Wasapi;
using osu.Framework.Audio.Asio;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Development; using osu.Framework.Development;
@@ -124,13 +125,15 @@ namespace osu.Framework.Threading
private WasapiProcedure? wasapiProcedure; private WasapiProcedure? wasapiProcedure;
private WasapiNotifyProcedure? wasapiNotifyProcedure; private WasapiNotifyProcedure? wasapiNotifyProcedure;
private BassAsio.AsioProcedure asioProcedure = null!;
/// <summary> /// <summary>
/// If a global mixer is being used, this will be the BASS handle for it. /// If a global mixer is being used, this will be the BASS handle for it.
/// If non-null, all game mixers should be added to this mixer. /// If non-null, all game mixers should be added to this mixer.
/// </summary> /// </summary>
private readonly Bindable<int?> globalMixerHandle = new Bindable<int?>(); private readonly Bindable<int?> globalMixerHandle = new Bindable<int?>();
internal bool InitDevice(int deviceId, bool useExperimentalWasapi) internal bool InitDevice(int deviceId, AudioThreadOutputMode outputMode, int? asioDeviceIndex = null)
{ {
Debug.Assert(ThreadSafety.IsAudioThread); Debug.Assert(ThreadSafety.IsAudioThread);
Trace.Assert(deviceId != -1); // The real device ID should always be used, as the -1 device has special cases which are hard to work with. Trace.Assert(deviceId != -1); // The real device ID should always be used, as the -1 device has special cases which are hard to work with.
@@ -139,10 +142,40 @@ namespace osu.Framework.Threading
if (!Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT if (!Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT
return false; return false;
if (useExperimentalWasapi) switch (outputMode)
attemptWasapiInitialisation(); {
else case AudioThreadOutputMode.Default:
freeWasapi(); freeAsio();
freeWasapi();
break;
case AudioThreadOutputMode.WasapiShared:
freeAsio();
attemptWasapiInitialisation(exclusive: false);
break;
case AudioThreadOutputMode.WasapiExclusive:
freeAsio();
attemptWasapiInitialisation(exclusive: true);
break;
case AudioThreadOutputMode.Asio:
freeWasapi();
if (asioDeviceIndex == null)
{
Logger.Log("ASIO output mode selected but no ASIO device index was provided.", level: LogLevel.Error);
return false;
}
if (!initAsio(asioDeviceIndex.Value))
return false;
break;
}
initialised_devices.Add(deviceId); initialised_devices.Add(deviceId);
return true; return true;
@@ -160,6 +193,7 @@ namespace osu.Framework.Threading
Bass.Free(); Bass.Free();
} }
freeAsio();
freeWasapi(); freeWasapi();
if (selectedDevice != deviceId && canSelectDevice(selectedDevice)) if (selectedDevice != deviceId && canSelectDevice(selectedDevice))
@@ -182,7 +216,9 @@ namespace osu.Framework.Threading
} }
} }
private bool attemptWasapiInitialisation() private bool attemptWasapiInitialisation() => attemptWasapiInitialisation(exclusive: false);
private bool attemptWasapiInitialisation(bool exclusive)
{ {
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
return false; return false;
@@ -219,10 +255,10 @@ namespace osu.Framework.Threading
// To keep things in a sane state let's only keep one device initialised via wasapi. // To keep things in a sane state let's only keep one device initialised via wasapi.
freeWasapi(); freeWasapi();
return initWasapi(wasapiDevice); return initWasapi(wasapiDevice, exclusive);
} }
private bool initWasapi(int wasapiDevice) private bool initWasapi(int wasapiDevice, bool exclusive)
{ {
// This is intentionally initialised inline and stored to a field. // This is intentionally initialised inline and stored to a field.
// If we don't do this, it gets GC'd away. // If we don't do this, it gets GC'd away.
@@ -238,11 +274,15 @@ namespace osu.Framework.Threading
if (notify == WasapiNotificationType.DefaultOutput) if (notify == WasapiNotificationType.DefaultOutput)
{ {
freeWasapi(); freeWasapi();
initWasapi(device); initWasapi(device, exclusive);
} }
}); });
bool initialised = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Flags: WasapiInitFlags.EventDriven | WasapiInitFlags.AutoFormat, Buffer: 0f, Period: float.Epsilon); var flags = WasapiInitFlags.EventDriven | WasapiInitFlags.AutoFormat;
if (exclusive)
flags |= (WasapiInitFlags)16; // WasapiInitFlags.Exclusive (not available in older bindings).
bool initialised = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Flags: flags, Buffer: 0f, Period: float.Epsilon);
Logger.Log($"Initialising BassWasapi for device {wasapiDevice}...{(initialised ? "success!" : "FAILED")}"); Logger.Log($"Initialising BassWasapi for device {wasapiDevice}...{(initialised ? "success!" : "FAILED")}");
if (!initialised) if (!initialised)
@@ -267,6 +307,93 @@ namespace osu.Framework.Threading
globalMixerHandle.Value = null; globalMixerHandle.Value = null;
} }
private bool initAsio(int asioDeviceIndex)
{
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
return false;
Logger.Log($"Attempting BassAsio initialisation for device {asioDeviceIndex}");
freeAsio();
try
{
if (!BassAsio.Init(asioDeviceIndex))
{
Logger.Log($"BassAsio.Init({asioDeviceIndex}) failed", level: LogLevel.Error);
return false;
}
}
catch (DllNotFoundException e)
{
Logger.Log($"bassasio native library not found ({e.Message}). ASIO output will be unavailable.", level: LogLevel.Error);
return false;
}
catch (EntryPointNotFoundException e)
{
Logger.Log($"bassasio native library is incompatible ({e.Message}). ASIO output will be unavailable.", level: LogLevel.Error);
return false;
}
int sampleRate = (int)Math.Round(BassAsio.GetRate());
if (sampleRate <= 0)
sampleRate = 44100;
globalMixerHandle.Value = BassMix.CreateMixerStream(sampleRate, 2, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float);
// If we don't store the procedure, it gets GC'd away.
asioProcedure = (input, channel, buffer, length, user) =>
{
if (globalMixerHandle.Value == null)
return 0;
return Bass.ChannelGetData(globalMixerHandle.Value!.Value, buffer, length);
};
// Enable stereo output (first two output channels) and feed the global mixer stream into it.
// We ignore additional channels for now.
BassAsio.ChannelEnable(false, 0, asioProcedure, IntPtr.Zero);
BassAsio.ChannelEnable(false, 1, asioProcedure, IntPtr.Zero);
BassAsio.GetInfo(out var info);
if (!BassAsio.Start(0))
{
Logger.Log("BassAsio.Start() failed", level: LogLevel.Error);
freeAsio();
return false;
}
Logger.Log($"BassAsio initialised (Rate: {BassAsio.GetRate()}, OutChans: {info.Outputs})");
return true;
}
private void freeAsio()
{
try
{
BassAsio.Stop();
BassAsio.Free();
}
catch
{
}
if (globalMixerHandle.Value != null)
{
Bass.StreamFree(globalMixerHandle.Value.Value);
globalMixerHandle.Value = null;
}
}
internal enum AudioThreadOutputMode
{
Default,
WasapiShared,
WasapiExclusive,
Asio,
}
#endregion #endregion
} }
} }