diff --git a/osu.Framework.NativeLibs/update-bass.ps1 b/osu.Framework.NativeLibs/update-bass.ps1 new file mode 100644 index 000000000..e342732b7 --- /dev/null +++ b/osu.Framework.NativeLibs/update-bass.ps1 @@ -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 } +} diff --git a/osu.Framework.NativeLibs/update-bass.sh b/osu.Framework.NativeLibs/update-bass.sh index b8bac17f7..6ad6efc70 100755 --- a/osu.Framework.NativeLibs/update-bass.sh +++ b/osu.Framework.NativeLibs/update-bass.sh @@ -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/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 rm bass*.zip diff --git a/osu.Framework.Tests/Audio/AudioManagerWithDeviceLoss.cs b/osu.Framework.Tests/Audio/AudioManagerWithDeviceLoss.cs index 4c757384b..567254c14 100644 --- a/osu.Framework.Tests/Audio/AudioManagerWithDeviceLoss.cs +++ b/osu.Framework.Tests/Audio/AudioManagerWithDeviceLoss.cs @@ -25,14 +25,14 @@ namespace osu.Framework.Tests.Audio private volatile bool simulateLoss; - protected override bool InitBass(int device) + protected override bool InitBass(int device, AudioOutputMode outputMode, int? asioDeviceIndex) { try { 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 { diff --git a/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs b/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs index 0fcdb2fe3..e37ebea9b 100644 --- a/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs +++ b/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs @@ -436,6 +436,7 @@ namespace osu.Framework.Tests.Clocks [TestCase(1)] [TestCase(10)] [TestCase(50)] + [FlakyTest] public void TestNoDecoupledDrift(int updateRate) { var stopwatch = new StopwatchClock(); diff --git a/osu.Framework.Tests/Clocks/InterpolatingFramedClockTest.cs b/osu.Framework.Tests/Clocks/InterpolatingFramedClockTest.cs index 46671653c..f78dbda50 100644 --- a/osu.Framework.Tests/Clocks/InterpolatingFramedClockTest.cs +++ b/osu.Framework.Tests/Clocks/InterpolatingFramedClockTest.cs @@ -254,6 +254,7 @@ namespace osu.Framework.Tests.Clocks [TestCase(1)] [TestCase(10)] [TestCase(50)] + [FlakyTest] public void TestNoInterpolationDrift(int updateRate) { var stopwatch = new StopwatchClock(); diff --git a/osu.Framework.Tests/Graphics/TripleBufferTest.cs b/osu.Framework.Tests/Graphics/TripleBufferTest.cs index d14e0faec..14919a041 100644 --- a/osu.Framework.Tests/Graphics/TripleBufferTest.cs +++ b/osu.Framework.Tests/Graphics/TripleBufferTest.cs @@ -108,6 +108,7 @@ namespace osu.Framework.Tests.Graphics } [Test] + [FlakyTest] public void TestReadSaturated() { var tripleBuffer = new TripleBuffer(); diff --git a/osu.Framework.Tests/Visual/Localisation/LocalisationTestScene.cs b/osu.Framework.Tests/Visual/Localisation/LocalisationTestScene.cs index bf60eb909..80f830694 100644 --- a/osu.Framework.Tests/Visual/Localisation/LocalisationTestScene.cs +++ b/osu.Framework.Tests/Visual/Localisation/LocalisationTestScene.cs @@ -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 GetAsync(string key, CancellationToken cancellationToken = default) => Task.FromResult(Get(key)); diff --git a/osu.Framework.iOS/GameApplicationDelegate.cs b/osu.Framework.iOS/GameApplicationDelegate.cs index c6bfddf11..757d96b07 100644 --- a/osu.Framework.iOS/GameApplicationDelegate.cs +++ b/osu.Framework.iOS/GameApplicationDelegate.cs @@ -17,7 +17,7 @@ namespace osu.Framework.iOS /// /// Base implementation for osu!framework applications. /// - public abstract class GameApplicationDelegate : UIApplicationDelegate + public abstract class GameApplicationDelegate : UIResponder, IUIApplicationDelegate { internal event Action? DragDrop; @@ -27,7 +27,7 @@ namespace osu.Framework.iOS public IOSGameHost Host { get; private set; } = null!; - public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) + public virtual bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) { mapLibraryNames(); @@ -46,7 +46,7 @@ namespace osu.Framework.iOS 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 // 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; } + 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()); + } + /// /// Creates the class to launch. /// diff --git a/osu.Framework.iOS/IOSWindow.cs b/osu.Framework.iOS/IOSWindow.cs index 78341f038..194ecf084 100644 --- a/osu.Framework.iOS/IOSWindow.cs +++ b/osu.Framework.iOS/IOSWindow.cs @@ -38,7 +38,7 @@ namespace osu.Framework.iOS 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(); @@ -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 // frame rate with multi-threaded mode turned on, but it is going to give them worse input latency // and higher power usage. - SDL_SetiOSAnimationCallback(SDLWindowHandle, 1, &runFrame, ObjectHandle.Handle); + SDL_SetiOSAnimationCallback(SDLWindowHandle, 1, &runFrame, ObjectHandle.Handle).ThrowIfFailed(); } [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] diff --git a/osu.Framework/Audio/Asio/BassAsio.cs b/osu.Framework/Audio/Asio/BassAsio.cs new file mode 100644 index 000000000..1c4464d62 --- /dev/null +++ b/osu.Framework/Audio/Asio/BassAsio.cs @@ -0,0 +1,115 @@ +// Copyright (c) ppy Pty Ltd . 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 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; + } + } +} diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs index 616c26f11..e0f83a87d 100644 --- a/osu.Framework/Audio/AudioManager.cs +++ b/osu.Framework/Audio/AudioManager.cs @@ -13,6 +13,7 @@ using JetBrains.Annotations; using ManagedBass; using ManagedBass.Fx; using ManagedBass.Mix; +using osu.Framework.Audio.Asio; using osu.Framework.Audio.Mixing; using osu.Framework.Audio.Mixing.Bass; using osu.Framework.Audio.Sample; @@ -80,7 +81,7 @@ namespace osu.Framework.Audio /// Consumers should provide a "Default" audio device entry which sets to an empty string. /// /// - public IEnumerable AudioDeviceNames => audioDeviceNames; + public IEnumerable AudioDeviceNames => getAudioDeviceEntries(); /// /// 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. private ImmutableArray audioDevices = ImmutableArray.Empty; - private ImmutableList audioDeviceNames = ImmutableList.Empty; + private ImmutableList bassDeviceNames = ImmutableList.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; @@ -196,7 +212,15 @@ namespace osu.Framework.Audio } 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). GlobalMixerHandle.ValueChanged += handle => usingGlobalMixer.Value = handle.NewValue.HasValue; @@ -267,8 +291,7 @@ namespace osu.Framework.Audio /// Channels removed from this fall back to the global . /// /// An identifier displayed on the audio mixer visualiser. - public AudioMixer CreateAudioMixer(string identifier = default) => - createAudioMixer(SampleMixer, !string.IsNullOrEmpty(identifier) ? identifier : $"user #{Interlocked.Increment(ref userMixerID)}"); + public AudioMixer CreateAudioMixer(string identifier = default) => createAudioMixer(SampleMixer, !string.IsNullOrEmpty(identifier) ? identifier : $"user #{Interlocked.Increment(ref userMixerID)}"); private AudioMixer createAudioMixer(AudioMixer fallbackMixer, string identifier) { @@ -332,24 +355,61 @@ namespace osu.Framework.Audio /// 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 - int deviceIndex = audioDeviceNames.FindIndex(d => d == deviceName); - if (deviceIndex >= 0 && trySetDevice(BASS_INTERNAL_DEVICE_COUNT + deviceIndex)) return; + if (mode == AudioOutputMode.Asio) + { + // 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. // 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". - 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. - trySetDevice(Bass.NoSoundDevice); + trySetDevice(Bass.NoSoundDevice, AudioOutputMode.Default, null); // we're boned. even "No sound" device won't initialise. return; - bool trySetDevice(int deviceId) + bool trySetDevice(int deviceId, AudioOutputMode outputMode, int? asioDeviceIndex) { var device = audioDevices.ElementAtOrDefault(deviceId); @@ -362,7 +422,7 @@ namespace osu.Framework.Audio return false; // initialize new device - if (!InitBass(deviceId)) + if (!InitBass(deviceId, outputMode, asioDeviceIndex)) return false; //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; + } + } + /// /// This method calls . /// It can be overridden for unit testing. /// /// The device to initialise. - protected virtual bool InitBass(int device) + /// The output mode to use for playback. + /// When is ASIO, the selected ASIO device index. + 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. Bass.UpdatePeriod = 5; @@ -402,25 +503,15 @@ namespace osu.Framework.Audio // - 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. // (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. // See https://www.un4seen.com/forum/?topic=19601 for more information. 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 innerSuccess = thread.InitDevice(device, UseExperimentalWasapi.Value); + bool innerSuccess = thread.InitDevice(device, toThreadOutputMode(outputMode), asioDeviceIndex); bool alreadyInitialised = Bass.LastError == Errors.Already; if (alreadyInitialised) @@ -449,6 +540,26 @@ namespace osu.Framework.Audio 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() @@ -458,8 +569,8 @@ namespace osu.Framework.Audio // 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."); - var oldDeviceNames = audioDeviceNames; - var newDeviceNames = audioDeviceNames = audioDevices.Skip(BASS_INTERNAL_DEVICE_COUNT).Where(d => d.IsEnabled).Select(d => d.Name).ToImmutableList(); + var oldDeviceNames = bassDeviceNames; + var newDeviceNames = bassDeviceNames = audioDevices.Skip(BASS_INTERNAL_DEVICE_COUNT).Where(d => d.IsEnabled).Select(d => d.Name).ToImmutableList(); scheduler.Add(() => { @@ -485,6 +596,95 @@ namespace osu.Framework.Audio } } + private IEnumerable getAudioDeviceEntries() + { + var entries = new List(); + + // 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; + } + /// /// Check whether any audio device changes have occurred. /// @@ -535,7 +735,13 @@ namespace osu.Framework.Audio protected virtual bool IsCurrentDeviceValid() { 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; } diff --git a/osu.Framework/Graphics/Shaders/ShaderManager.cs b/osu.Framework/Graphics/Shaders/ShaderManager.cs index 5536e5aa5..423178e1a 100644 --- a/osu.Framework/Graphics/Shaders/ShaderManager.cs +++ b/osu.Framework/Graphics/Shaders/ShaderManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using osu.Framework.Graphics.Rendering; using osu.Framework.IO.Stores; @@ -55,7 +56,7 @@ namespace osu.Framework.Graphics.Shaders /// The fragment shader name. /// A cached instance, if existing. public virtual IShader? GetCachedShader(string vertex, string fragment) - => shaderCache.TryGetValue((vertex, fragment), out IShader? shader) ? shader : null; + => shaderCache.GetValueOrDefault((vertex, fragment)); /// /// Attempts to retrieve an already-cached shader part. @@ -63,7 +64,7 @@ namespace osu.Framework.Graphics.Shaders /// The name of the shader part. /// A cached instance, if existing. public virtual IShaderPart? GetCachedShaderPart(string name) - => partCache.TryGetValue(name, out IShaderPart? part) ? part : null; + => partCache.GetValueOrDefault(name); /// /// Attempts to retrieve the raw data for a shader file. diff --git a/osu.Framework/Graphics/Transforms/TargetGroupingTransformTracker.cs b/osu.Framework/Graphics/Transforms/TargetGroupingTransformTracker.cs index 5ef26d617..235c80288 100644 --- a/osu.Framework/Graphics/Transforms/TargetGroupingTransformTracker.cs +++ b/osu.Framework/Graphics/Transforms/TargetGroupingTransformTracker.cs @@ -368,10 +368,7 @@ namespace osu.Framework.Graphics.Transforms return min; } - if (lastAppliedTransformIndices.TryGetValue(targetMember, out int val)) - return val; - - return 0; + return lastAppliedTransformIndices.GetValueOrDefault(targetMember, 0); } /// diff --git a/osu.Framework/Input/Bindings/KeyBindingContainer.cs b/osu.Framework/Input/Bindings/KeyBindingContainer.cs index f9467ddf1..b64e06bc8 100644 --- a/osu.Framework/Input/Bindings/KeyBindingContainer.cs +++ b/osu.Framework/Input/Bindings/KeyBindingContainer.cs @@ -12,7 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Input.States; using osu.Framework.Lists; -using osu.Framework.Logging; +// using osu.Framework.Logging; using osuTK; namespace osu.Framework.Input.Bindings diff --git a/osu.Framework/Input/Events/KeyDownEvent.cs b/osu.Framework/Input/Events/KeyDownEvent.cs index a706b79c5..ead7f3309 100644 --- a/osu.Framework/Input/Events/KeyDownEvent.cs +++ b/osu.Framework/Input/Events/KeyDownEvent.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // 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 osuTK.Input; diff --git a/osu.Framework/Input/InputManager.cs b/osu.Framework/Input/InputManager.cs index 87efb4345..652239383 100644 --- a/osu.Framework/Input/InputManager.cs +++ b/osu.Framework/Input/InputManager.cs @@ -209,7 +209,7 @@ namespace osu.Framework.Input /// The button to find the manager for. /// The . public MouseButtonEventManager GetButtonEventManagerFor(MouseButton button) => - mouseButtonEventManagers.TryGetValue(button, out var manager) ? manager : null; + mouseButtonEventManagers.GetValueOrDefault(button); /// /// Create a for a specified key. diff --git a/osu.Framework/Platform/Linux/SDL3LinuxWindow.cs b/osu.Framework/Platform/Linux/SDL3LinuxWindow.cs index 2195f38fe..7afdf3ef1 100644 --- a/osu.Framework/Platform/Linux/SDL3LinuxWindow.cs +++ b/osu.Framework/Platform/Linux/SDL3LinuxWindow.cs @@ -11,7 +11,7 @@ namespace osu.Framework.Platform.Linux public SDL3LinuxWindow(GraphicsSurfaceType surfaceType, string appName, bool bypassCompositor) : 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(); } } } diff --git a/osu.Framework/Platform/SDL3/SDL3Clipboard.cs b/osu.Framework/Platform/SDL3/SDL3Clipboard.cs index 1c2b08fdf..8087e3195 100644 --- a/osu.Framework/Platform/SDL3/SDL3Clipboard.cs +++ b/osu.Framework/Platform/SDL3/SDL3Clipboard.cs @@ -42,7 +42,7 @@ namespace osu.Framework.Platform.SDL3 // assume that empty text means no text. 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? GetImage() { diff --git a/osu.Framework/Platform/SDL3/SDL3DesktopWindow.cs b/osu.Framework/Platform/SDL3/SDL3DesktopWindow.cs index ca86b8aff..e83e2a30f 100644 --- a/osu.Framework/Platform/SDL3/SDL3DesktopWindow.cs +++ b/osu.Framework/Platform/SDL3/SDL3DesktopWindow.cs @@ -17,9 +17,9 @@ namespace osu.Framework.Platform.SDL3 // 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. // 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_SetWindowFullscreen(SDLWindowHandle, false); - SDL_RestoreWindow(SDLWindowHandle); + SDL_SetWindowBordered(SDLWindowHandle, true).LogErrorIfFailed(); + SDL_SetWindowFullscreen(SDLWindowHandle, false).LogErrorIfFailed(); + SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed(); base.UpdateWindowStateAndSize(state, display, displayMode); } diff --git a/osu.Framework/Platform/SDL3/SDL3Exception.cs b/osu.Framework/Platform/SDL3/SDL3Exception.cs new file mode 100644 index 000000000..80a491b96 --- /dev/null +++ b/osu.Framework/Platform/SDL3/SDL3Exception.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . 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})"); +} diff --git a/osu.Framework/Platform/SDL3/SDL3Extensions.cs b/osu.Framework/Platform/SDL3/SDL3Extensions.cs index 158a71e82..028125a91 100644 --- a/osu.Framework/Platform/SDL3/SDL3Extensions.cs +++ b/osu.Framework/Platform/SDL3/SDL3Extensions.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Drawing; +using System.Runtime.CompilerServices; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics.Primitives; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.StateChanges; +using osu.Framework.Logging; using osuTK.Input; using SDL; using static SDL.SDL3; @@ -1045,7 +1048,7 @@ namespace osu.Framework.Platform.SDL3 { int bpp; 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); } @@ -1144,5 +1147,100 @@ namespace osu.Framework.Platform.SDL3 SDL_ClearError(); 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? LogErrorIfFailed(this SDLArray? 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* returnValue, [CallerArgumentExpression("returnValue")] string? expression = null) + where T : unmanaged + { + if (returnValue == null) + logError(expression); + + return returnValue; + } } } diff --git a/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs index 7951a24f6..56ca31116 100644 --- a/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs +++ b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs @@ -33,12 +33,12 @@ namespace osu.Framework.Platform.SDL3 switch (surfaceType) { case GraphicsSurfaceType.OpenGL: - SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_RED_SIZE, 8); - SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_GREEN_SIZE, 8); - SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_BLUE_SIZE, 8); - SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_ACCUM_ALPHA_SIZE, 0); - SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_DEPTH_SIZE, 16); - SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STENCIL_SIZE, 8); + SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_RED_SIZE, 8).ThrowIfFailed(); + SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_GREEN_SIZE, 8).ThrowIfFailed(); + SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_BLUE_SIZE, 8).ThrowIfFailed(); + SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_ACCUM_ALPHA_SIZE, 0).ThrowIfFailed(); + SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_DEPTH_SIZE, 16).ThrowIfFailed(); + SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STENCIL_SIZE, 8).ThrowIfFailed(); break; case GraphicsSurfaceType.Vulkan: @@ -60,7 +60,7 @@ namespace osu.Framework.Platform.SDL3 public Size GetDrawableSize() { int width, height; - SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height); + SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height).ThrowIfFailed(); return new Size(width, height); } @@ -70,19 +70,19 @@ namespace osu.Framework.Platform.SDL3 { 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: - SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 0); + SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3).ThrowIfFailed(); + SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 0).ThrowIfFailed(); } 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: - SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3).ThrowIfFailed(); + SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 2).ThrowIfFailed(); } context = SDL_GL_CreateContext(window.SDLWindowHandle); @@ -90,7 +90,7 @@ namespace osu.Framework.Platform.SDL3 if (context == null) 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(); } @@ -126,29 +126,13 @@ namespace osu.Framework.Platform.SDL3 string? str = Marshal.PtrToStringAnsi(new IntPtr(ptr)); Debug.Assert(str != null); - entryPointsInstance[i] = getProcAddress(str); + entryPointsInstance[i] = SDL_GL_GetProcAddress(str); } } 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 { get @@ -182,11 +166,8 @@ namespace osu.Framework.Platform.SDL3 } 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.MakeCurrent(IntPtr context) => SDL_GL_MakeCurrent(window.SDLWindowHandle, (SDL_GLContextState*)context); 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 @@ -220,7 +201,7 @@ namespace osu.Framework.Platform.SDL3 #region Android-specific implementation [SupportedOSPlatform("android")] - IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL_GetAndroidJNIEnv(); + IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL_GetAndroidJNIEnv().ThrowIfFailed(); [SupportedOSPlatform("android")] IntPtr IAndroidGraphicsSurface.SurfaceHandle => window.SurfaceHandle; diff --git a/osu.Framework/Platform/SDL3/SDL3MobileWindow.cs b/osu.Framework/Platform/SDL3/SDL3MobileWindow.cs index cae21ec3b..919fd2c37 100644 --- a/osu.Framework/Platform/SDL3/SDL3MobileWindow.cs +++ b/osu.Framework/Platform/SDL3/SDL3MobileWindow.cs @@ -15,7 +15,7 @@ namespace osu.Framework.Platform.SDL3 protected override unsafe void UpdateWindowStateAndSize(WindowState state, Display display, DisplayMode displayMode) { // 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. } diff --git a/osu.Framework/Platform/SDL3/SDL3Window.cs b/osu.Framework/Platform/SDL3/SDL3Window.cs index 79d627500..5c76198af 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window.cs @@ -75,7 +75,7 @@ namespace osu.Framework.Platform.SDL3 set { 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; var props = SDL_GetWindowProperties(SDLWindowHandle); + if (props == 0) + return IntPtr.Zero; switch (RuntimeInfo.OS) { @@ -134,6 +136,8 @@ namespace osu.Framework.Platform.SDL3 return IntPtr.Zero; var props = SDL_GetWindowProperties(SDLWindowHandle); + if (props == 0) + return IntPtr.Zero; if (IsWayland) return SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, IntPtr.Zero); @@ -161,7 +165,7 @@ namespace osu.Framework.Platform.SDL3 { ObjectHandle = new ObjectHandle(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)) { @@ -174,7 +178,6 @@ namespace osu.Framework.Platform.SDL3 SDL3 Revision: {SDL_GetRevision()} 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_SetEventFilter(&eventFilter, ObjectHandle.Handle); @@ -212,13 +215,13 @@ namespace osu.Framework.Platform.SDL3 flags |= WindowState.ToFlags(); flags |= graphicsSurface.Type.ToFlags(); - SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "0"u8); - SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"u8); - SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"u8); // 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_PEN_TOUCH_EVENTS, "0"u8); - SDL_SetHint(SDL_HINT_PEN_MOUSE_EVENTS, "0"u8); - SDL_SetHint(SDL_HINT_IME_IMPLEMENTED_UI, "composition"u8); + SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "0"u8).LogErrorIfFailed(); + SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"u8).LogErrorIfFailed(); + 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).LogErrorIfFailed(); // disable mouse events generating synthetic touch events on mobile platforms + SDL_SetHint(SDL_HINT_PEN_TOUCH_EVENTS, "0"u8).LogErrorIfFailed(); + SDL_SetHint(SDL_HINT_PEN_MOUSE_EVENTS, "0"u8).LogErrorIfFailed(); + SDL_SetHint(SDL_HINT_IME_IMPLEMENTED_UI, "composition"u8).LogErrorIfFailed(); 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. // 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. - SDL_StopTextInput(SDLWindowHandle); + SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed(); graphicsSurface.Initialise(); @@ -241,7 +244,7 @@ namespace osu.Framework.Platform.SDL3 /// public virtual void Run() { - SDL_AddEventWatch(&eventWatch, ObjectHandle.Handle); + SDL_AddEventWatch(&eventWatch, ObjectHandle.Handle).LogErrorIfFailed(); RunMainLoop(); } @@ -405,14 +408,14 @@ namespace osu.Framework.Platform.SDL3 var flags = SDL_GetWindowFlags(SDLWindowHandle); 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(() => { - SDL_HideWindow(SDLWindowHandle); + SDL_HideWindow(SDLWindowHandle).LogErrorIfFailed(); }); public void Show() => ScheduleCommand(() => @@ -430,7 +433,7 @@ namespace osu.Framework.Platform.SDL3 SDL_FlashWindow(SDLWindowHandle, flashUntilFocused ? SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED - : SDL_FlashOperation.SDL_FLASH_BRIEFLY); + : SDL_FlashOperation.SDL_FLASH_BRIEFLY).LogErrorIfFailed(); }); public void CancelFlash() => ScheduleCommand(() => @@ -438,12 +441,12 @@ namespace osu.Framework.Platform.SDL3 if (!RuntimeInfo.IsDesktop) 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()); /// /// Attempts to set the window's icon to the specified image. @@ -463,10 +466,12 @@ namespace osu.Framework.Platform.SDL3 fixed (Rgba32* ptr = pixelSpan) { 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); }); } @@ -495,7 +500,7 @@ namespace osu.Framework.Platform.SDL3 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++) HandleEvent(events[i]); } while (eventsRead == events_per_peep); diff --git a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs index ff947e408..823915fdf 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs @@ -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)}."); relativeMouseMode = value; - ScheduleCommand(() => SDL_SetWindowRelativeMouseMode(SDLWindowHandle, value)); + ScheduleCommand(() => SDL_SetWindowRelativeMouseMode(SDLWindowHandle, value).LogErrorIfFailed()); updateCursorConfinement(); } } @@ -69,7 +69,7 @@ namespace osu.Framework.Platform.SDL3 /// 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()); } /// @@ -101,9 +101,9 @@ namespace osu.Framework.Platform.SDL3 ScheduleCommand(() => { if (cursorVisible) - SDL_ShowCursor(); + SDL_ShowCursor().LogErrorIfFailed(); else - SDL_HideCursor(); + SDL_HideCursor().LogErrorIfFailed(); }); /// @@ -113,7 +113,7 @@ namespace osu.Framework.Platform.SDL3 { 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. // This is fine for our use case, as UserInputManager will clamp the mouse position. @@ -122,12 +122,12 @@ namespace osu.Framework.Platform.SDL3 ScheduleCommand(() => { var rect = ((RectangleI)(CursorConfineRect / Scale)).ToSDLRect(); - SDL_SetWindowMouseRect(SDLWindowHandle, &rect); + SDL_SetWindowMouseRect(SDLWindowHandle, &rect).LogErrorIfFailed(); }); } 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(() => { - currentTextInputProperties ??= SDL_CreateProperties(); + currentTextInputProperties ??= SDL_CreateProperties().ThrowIfFailed(); 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) - 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 - SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER); + SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER).LogErrorIfFailed(); 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 - 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()); /// /// Resets internal state of the platform-native IME. @@ -218,19 +218,19 @@ namespace osu.Framework.Platform.SDL3 /// public virtual void ResetIme() => ScheduleCommand(() => { - SDL_StopTextInput(SDLWindowHandle); + SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed(); if (currentTextInputProperties is SDL_PropertiesID props) - SDL_StartTextInputWithProperties(SDLWindowHandle, props); + SDL_StartTextInputWithProperties(SDLWindowHandle, props).LogErrorIfFailed(); else - SDL_StartTextInput(SDLWindowHandle); + SDL_StartTextInput(SDLWindowHandle).LogErrorIfFailed(); }); public void SetTextInputRect(RectangleF rect) => ScheduleCommand(() => { // TODO: SDL3 allows apps to set cursor position through the third parameter of SDL_SetTextInputArea. var sdlRect = ((RectangleI)(rect / Scale)).ToSDLRect(); - SDL_SetTextInputArea(SDLWindowHandle, &sdlRect, 0); + SDL_SetTextInputArea(SDLWindowHandle, &sdlRect, 0).LogErrorIfFailed(); }); #region SDL Event Handling @@ -359,11 +359,11 @@ namespace osu.Framework.Platform.SDL3 if (controllers.ContainsKey(instanceID)) return; - SDL_Joystick* joystick = SDL_OpenJoystick(instanceID); + SDL_Joystick* joystick = SDL3Extensions.LogErrorIfFailed(SDL_OpenJoystick(instanceID)); SDL_Gamepad* controller = null; if (SDL_IsGamepad(instanceID)) - controller = SDL_OpenGamepad(instanceID); + controller = SDL3Extensions.LogErrorIfFailed(SDL_OpenGamepad(instanceID)); controllers[instanceID] = new SDL3ControllerBindings(joystick, controller); } @@ -373,7 +373,7 @@ namespace osu.Framework.Platform.SDL3 /// private void populateJoysticks() { - using var joysticks = SDL_GetJoysticks(); + using var joysticks = SDL_GetJoysticks().LogErrorIfFailed(); if (joysticks == null) return; @@ -531,7 +531,7 @@ namespace osu.Framework.Platform.SDL3 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) { diff --git a/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs b/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs index d203a8719..7248173de 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs @@ -25,7 +25,7 @@ namespace osu.Framework.Platform.SDL3 config.BindWith(FrameworkSetting.MinimiseOnFocusLossInFullscreen, minimiseOnFocusLoss); 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); fetchDisplays(); @@ -70,7 +70,7 @@ namespace osu.Framework.Platform.SDL3 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}"); - ScheduleCommand(() => SDL_SetWindowMinimumSize(SDLWindowHandle, min.Width, min.Height)); + ScheduleCommand(() => SDL_SetWindowMinimumSize(SDLWindowHandle, min.Width, min.Height).LogErrorIfFailed()); }; sizeWindowed.MaxValueChanged += max => @@ -81,7 +81,7 @@ namespace osu.Framework.Platform.SDL3 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}"); - ScheduleCommand(() => SDL_SetWindowMaximumSize(SDLWindowHandle, max.Width, max.Height)); + ScheduleCommand(() => SDL_SetWindowMaximumSize(SDLWindowHandle, max.Width, max.Height).LogErrorIfFailed()); }; config.BindWith(FrameworkSetting.SizeFullscreen, sizeFullscreen); @@ -165,7 +165,7 @@ namespace osu.Framework.Platform.SDL3 set { 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; resizable = value; - ScheduleCommand(() => SDL_SetWindowResizable(SDLWindowHandle, value)); + ScheduleCommand(() => SDL_SetWindowResizable(SDLWindowHandle, value).LogErrorIfFailed()); } } @@ -245,9 +245,9 @@ namespace osu.Framework.Platform.SDL3 ScheduleCommand(() => { if (value) - SDL_ShowWindow(SDLWindowHandle); + SDL_ShowWindow(SDLWindowHandle).LogErrorIfFailed(); else - SDL_HideWindow(SDLWindowHandle); + SDL_HideWindow(SDLWindowHandle).LogErrorIfFailed(); }); } } @@ -392,7 +392,7 @@ namespace osu.Framework.Platform.SDL3 } display = new Display(displayIndex, - SDL_GetDisplayName(displayID), + SDL_GetDisplayName(displayID).LogErrorIfFailed(), new Rectangle(rect.x, rect.y, rect.w, rect.h), new Rectangle(usableBounds.x, usableBounds.y, usableBounds.w, usableBounds.h), displayModes); @@ -421,7 +421,7 @@ namespace osu.Framework.Platform.SDL3 get { SDL_Rect rect; - SDL_GetDisplayBounds(displayID, &rect); + SDL_GetDisplayBounds(displayID, &rect).LogErrorIfFailed(); 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) { int w, h; - SDL_GetWindowSize(SDLWindowHandle, &w, &h); + SDL_GetWindowSize(SDLWindowHandle, &w, &h).LogErrorIfFailed(); int drawableW = graphicsSurface.GetDrawableSize().Width; @@ -487,7 +487,7 @@ namespace osu.Framework.Platform.SDL3 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. int x, y; - SDL_GetWindowPosition(SDLWindowHandle, &x, &y); + SDL_GetWindowPosition(SDLWindowHandle, &x, &y).ThrowIfFailed(); var newPosition = new Point(x, y); if (!newPosition.Equals(Position)) @@ -605,7 +605,7 @@ namespace osu.Framework.Platform.SDL3 windowMaximised = maximized; } - var newDisplayID = SDL_GetDisplayForWindow(SDLWindowHandle); + var newDisplayID = SDL_GetDisplayForWindow(SDLWindowHandle).ThrowIfFailed(); if (displayID != newDisplayID) { @@ -676,9 +676,9 @@ namespace osu.Framework.Platform.SDL3 case WindowState.Normal: Size = sizeWindowed.Value; - SDL_RestoreWindow(SDLWindowHandle); - SDL_SetWindowSize(SDLWindowHandle, Size.Width, Size.Height); - SDL_SetWindowResizable(SDLWindowHandle, Resizable); + SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed(); + SDL_SetWindowSize(SDLWindowHandle, Size.Width, Size.Height).LogErrorIfFailed(); + SDL_SetWindowResizable(SDLWindowHandle, Resizable).LogErrorIfFailed(); readWindowPositionFromConfig(state, display); break; @@ -690,8 +690,8 @@ namespace osu.Framework.Platform.SDL3 ensureWindowOnDisplay(display); - SDL_SetWindowFullscreenMode(SDLWindowHandle, &closestMode); - SDL_SetWindowFullscreen(SDLWindowHandle, true); + SDL_SetWindowFullscreenMode(SDLWindowHandle, &closestMode).LogErrorIfFailed(); + SDL_SetWindowFullscreen(SDLWindowHandle, true).LogErrorIfFailed(); break; case WindowState.FullscreenBorderless: @@ -699,16 +699,16 @@ namespace osu.Framework.Platform.SDL3 break; case WindowState.Maximised: - SDL_RestoreWindow(SDLWindowHandle); + SDL_RestoreWindow(SDLWindowHandle).LogErrorIfFailed(); ensureWindowOnDisplay(display); - SDL_MaximizeWindow(SDLWindowHandle); + SDL_MaximizeWindow(SDLWindowHandle).LogErrorIfFailed(); break; case WindowState.Minimised: ensureWindowOnDisplay(display); - SDL_MinimizeWindow(SDLWindowHandle); + SDL_MinimizeWindow(SDLWindowHandle).LogErrorIfFailed(); break; } } @@ -721,7 +721,8 @@ namespace osu.Framework.Platform.SDL3 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"; if (mode != null) @@ -768,7 +769,7 @@ namespace osu.Framework.Platform.SDL3 { if (tryGetDisplayAtIndex(display.Index, out var requestedID)) { - if (requestedID == SDL_GetDisplayForWindow(SDLWindowHandle)) + if (requestedID == SDL_GetDisplayForWindow(SDLWindowHandle).LogErrorIfFailed()) return; } @@ -844,8 +845,8 @@ namespace osu.Framework.Platform.SDL3 ensureWindowOnDisplay(display); // this is a generally sane method of handling borderless, and works well on macOS and linux. - SDL_SetWindowFullscreenMode(SDLWindowHandle, null); - SDL_SetWindowFullscreen(SDLWindowHandle, true); + SDL_SetWindowFullscreenMode(SDLWindowHandle, null).LogErrorIfFailed(); + SDL_SetWindowFullscreen(SDLWindowHandle, true).LogErrorIfFailed(); return display.Bounds.Size; } diff --git a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs index ad651d80a..2c63b62db 100644 --- a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs @@ -88,7 +88,7 @@ namespace osu.Framework.Platform.Windows && RelativeMouseMode) { 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) { - SDL_SetWindowBordered(SDLWindowHandle, false); + SDL_SetWindowBordered(SDLWindowHandle, false).LogErrorIfFailed(); 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 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; return newSize; diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index 7b7ca5c41..c394b311a 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -9,6 +9,7 @@ using System.Linq; using ManagedBass; using ManagedBass.Mix; using ManagedBass.Wasapi; +using osu.Framework.Audio.Asio; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Development; @@ -124,13 +125,15 @@ namespace osu.Framework.Threading private WasapiProcedure? wasapiProcedure; private WasapiNotifyProcedure? wasapiNotifyProcedure; + private BassAsio.AsioProcedure asioProcedure = null!; + /// /// 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. /// private readonly Bindable globalMixerHandle = new Bindable(); - internal bool InitDevice(int deviceId, bool useExperimentalWasapi) + internal bool InitDevice(int deviceId, AudioThreadOutputMode outputMode, int? asioDeviceIndex = null) { 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. @@ -139,10 +142,40 @@ namespace osu.Framework.Threading if (!Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT return false; - if (useExperimentalWasapi) - attemptWasapiInitialisation(); - else - freeWasapi(); + switch (outputMode) + { + case AudioThreadOutputMode.Default: + 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); return true; @@ -160,6 +193,7 @@ namespace osu.Framework.Threading Bass.Free(); } + freeAsio(); freeWasapi(); 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) 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. 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. // If we don't do this, it gets GC'd away. @@ -238,11 +274,15 @@ namespace osu.Framework.Threading if (notify == WasapiNotificationType.DefaultOutput) { 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")}"); if (!initialised) @@ -267,6 +307,93 @@ namespace osu.Framework.Threading 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 } }