mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-13 11:20:31 +00:00
同步更新,以及增加asio和独占wasapi的支持
This commit is contained in:
40
osu.Framework.NativeLibs/update-bass.ps1
Normal file
40
osu.Framework.NativeLibs/update-bass.ps1
Normal 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 }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -108,6 +108,7 @@ namespace osu.Framework.Tests.Graphics
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestReadSaturated()
|
||||
{
|
||||
var tripleBuffer = new TripleBuffer<TestObject>();
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace osu.Framework.iOS
|
||||
/// <summary>
|
||||
/// Base <see cref="UIApplicationDelegate"/> implementation for osu!framework applications.
|
||||
/// </summary>
|
||||
public abstract class GameApplicationDelegate : UIApplicationDelegate
|
||||
public abstract class GameApplicationDelegate : UIResponder, IUIApplicationDelegate
|
||||
{
|
||||
internal event Action<string>? 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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="Game"/> class to launch.
|
||||
/// </summary>
|
||||
|
||||
@@ -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) })]
|
||||
|
||||
115
osu.Framework/Audio/Asio/BassAsio.cs
Normal file
115
osu.Framework/Audio/Asio/BassAsio.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <see cref="AudioDevice"/> to an empty string.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public IEnumerable<string> AudioDeviceNames => audioDeviceNames;
|
||||
public IEnumerable<string> AudioDeviceNames => getAudioDeviceEntries();
|
||||
|
||||
/// <summary>
|
||||
/// 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<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;
|
||||
|
||||
@@ -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 <see cref="AudioMixer"/> fall back to the global <see cref="SampleMixer"/>.
|
||||
/// </remarks>
|
||||
/// <param name="identifier">An identifier displayed on the audio mixer visualiser.</param>
|
||||
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
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method calls <see cref="Bass.Init(int, int, DeviceInitFlags, IntPtr, IntPtr)"/>.
|
||||
/// It can be overridden for unit testing.
|
||||
/// </summary>
|
||||
/// <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.
|
||||
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<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>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
/// <param name="fragment">The fragment shader name.</param>
|
||||
/// <returns>A cached <see cref="IShader"/> instance, if existing.</returns>
|
||||
public virtual IShader? GetCachedShader(string vertex, string fragment)
|
||||
=> shaderCache.TryGetValue((vertex, fragment), out IShader? shader) ? shader : null;
|
||||
=> shaderCache.GetValueOrDefault((vertex, fragment));
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <returns>A cached <see cref="IShaderPart"/> instance, if existing.</returns>
|
||||
public virtual IShaderPart? GetCachedShaderPart(string name)
|
||||
=> partCache.TryGetValue(name, out IShaderPart? part) ? part : null;
|
||||
=> partCache.GetValueOrDefault(name);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the raw data for a shader file.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 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 osu.Framework.Extensions.TypeExtensions;
|
||||
// using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Input.States;
|
||||
using osuTK.Input;
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ namespace osu.Framework.Input
|
||||
/// <param name="button">The button to find the manager for.</param>
|
||||
/// <returns>The <see cref="MouseButtonEventManager"/>.</returns>
|
||||
public MouseButtonEventManager GetButtonEventManagerFor(MouseButton button) =>
|
||||
mouseButtonEventManagers.TryGetValue(button, out var manager) ? manager : null;
|
||||
mouseButtonEventManagers.GetValueOrDefault(button);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="KeyEventManager"/> for a specified key.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TPixel>? GetImage<TPixel>()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
10
osu.Framework/Platform/SDL3/SDL3Exception.cs
Normal file
10
osu.Framework/Platform/SDL3/SDL3Exception.cs
Normal 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})");
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
// 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 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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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<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))
|
||||
{
|
||||
@@ -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
|
||||
/// </summary>
|
||||
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());
|
||||
|
||||
/// <summary>
|
||||
/// 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);
|
||||
|
||||
@@ -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
|
||||
/// </remarks>
|
||||
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>
|
||||
@@ -101,9 +101,9 @@ namespace osu.Framework.Platform.SDL3
|
||||
ScheduleCommand(() =>
|
||||
{
|
||||
if (cursorVisible)
|
||||
SDL_ShowCursor();
|
||||
SDL_ShowCursor().LogErrorIfFailed();
|
||||
else
|
||||
SDL_HideCursor();
|
||||
SDL_HideCursor().LogErrorIfFailed();
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
@@ -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());
|
||||
|
||||
/// <summary>
|
||||
/// Resets internal state of the platform-native IME.
|
||||
@@ -218,19 +218,19 @@ namespace osu.Framework.Platform.SDL3
|
||||
/// </summary>
|
||||
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
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user