···11using osu.Framework.Graphics;
22using osu.Framework.Screens;
33-using osu.Framework.Testing;
4354namespace TemplateGame.Game.Tests.Visual
65{
77- public class TestSceneMainScreen : TestScene
66+ public class TestSceneMainScreen : TemplateGameTestScene
87 {
98 // Add visual tests to ensure correct behaviour of your game: https://github.com/ppy/osu-framework/wiki/Development-and-Testing
109 // You can make changes to classes associated with the tests and they will recompile and update immediately.
···11using osu.Framework.Graphics;
22-using osu.Framework.Testing;
3243namespace TemplateGame.Game.Tests.Visual
54{
66- public class TestSceneSpinningBox : TestScene
55+ public class TestSceneSpinningBox : TemplateGameTestScene
76 {
87 // Add visual tests to ensure correct behaviour of your game: https://github.com/ppy/osu-framework/wiki/Development-and-Testing
98 // You can make changes to classes associated with the tests and they will recompile and update immediately.
···11using osu.Framework.Allocation;
22using osu.Framework.Platform;
33-using osu.Framework.Testing;
4354namespace TemplateGame.Game.Tests.Visual
65{
77- public class TestSceneTemplateGameGame : TestScene
66+ public class TestSceneTemplateGameGame : TemplateGameTestScene
87 {
98 // Add visual tests to ensure correct behaviour of your game: https://github.com/ppy/osu-framework/wiki/Development-and-Testing
109 // You can make changes to classes associated with the tests and they will recompile and update immediately.
···11using FlappyDon.Game.Elements;
22using osu.Framework.Allocation;
33-using osu.Framework.Testing;
4354namespace FlappyDon.Game.Tests.Visual
65{
···87 /// A test scene for testing the alignment
98 /// and placement of the sprites that make up the backdrop
109 /// </summary>
1111- public class TestSceneBackdrop : TestScene
1010+ public class TestSceneBackdrop : FlappyDonTestScene
1211 {
1312 [BackgroundDependencyLoader]
1413 private void load()
···11using osu.Framework.Allocation;
22using osu.Framework.Platform;
33-using osu.Framework.Testing;
4354namespace FlappyDon.Game.Tests.Visual
65{
···87 /// A test scene wrapping the entire game,
98 /// including audio.
109 /// </summary>
1111- public class TestSceneFlappyDonGame : TestScene
1010+ public class TestSceneFlappyDonGame : FlappyDonTestScene
1211 {
1312 private FlappyDonGame game;
1413
···11using FlappyDon.Game.Elements;
22using osu.Framework.Allocation;
33using osu.Framework.Graphics;
44-using osu.Framework.Testing;
5465namespace FlappyDon.Game.Tests.Visual
76{
87 /// <summary>
98 /// A scene to test the layout and positioning and rotation of two pipe sprites.
109 /// </summary>
1111- public class TestSceneObstacles : TestScene
1010+ public class TestSceneObstacles : FlappyDonTestScene
1211 {
1312 [BackgroundDependencyLoader]
1413 private void load()
···11using FlappyDon.Game.Elements;
22using osu.Framework.Allocation;
33using osu.Framework.Graphics;
44-using osu.Framework.Testing;
5465namespace FlappyDon.Game.Tests.Visual
76{
87 /// <summary>
98 /// A scene to test the layout and positioning and rotation of two pipe sprites.
109 /// </summary>
1111- public class TestScenePipeObstacle : TestScene
1010+ public class TestScenePipeObstacle : FlappyDonTestScene
1211 {
1312 [BackgroundDependencyLoader]
1413 private void load()
···22// See the LICENCE file in the repository root for full licence text.
3344using System.Reflection;
55-using System.Threading;
65using System.Threading.Tasks;
76using NUnit.Framework;
87using osu.Framework.Audio;
···40394140 manager?.Dispose();
42414343- Thread.Sleep(500);
4444-4545- Assert.IsTrue(thread.Exited);
4242+ AudioThreadTest.WaitForOrAssert(() => thread.Exited, "Audio thread did not exit in time");
4643 }
47444845 [Test]
+3-1
osu.Framework.Tests/Audio/SampleBassTest.cs
···3939 [TearDown]
4040 public void Teardown()
4141 {
4242- Bass.Free();
4242+ // See AudioThread.freeDevice().
4343+ if (RuntimeInfo.OS != RuntimeInfo.Platform.Linux)
4444+ Bass.Free();
4345 }
44464547 [Test]
+19-1
osu.Framework.Tests/Audio/TrackBassTest.cs
···4545 [TearDown]
4646 public void Teardown()
4747 {
4848- Bass.Free();
4848+ // See AudioThread.freeDevice().
4949+ if (RuntimeInfo.OS != RuntimeInfo.Platform.Linux)
5050+ Bass.Free();
4951 }
50525153 [Test]
···380382 // assert track channel still paused regardless of frequency because it's stopped via Stop() above.
381383 Assert.IsFalse(track.IsRunning);
382384 Assert.AreEqual(0, track.CurrentTime);
385385+ }
386386+387387+ [Test]
388388+ public void TestBitrate()
389389+ {
390390+ Assert.Greater(track.Bitrate, 0);
391391+ }
392392+393393+ [Test]
394394+ public void TestCurrentTimeUpdatedAfterInlineSeek()
395395+ {
396396+ track.StartAsync();
397397+ updateTrack();
398398+399399+ runOnAudioThread(() => track.Seek(20000));
400400+ Assert.That(track.CurrentTime, Is.EqualTo(20000).Within(100));
383401 }
384402385403 private void takeEffectsAndUpdateAfter(int after)
···11+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22+// See the LICENCE file in the repository root for full licence text.
33+44+using NUnit.Framework;
55+using osu.Framework.Input.Bindings;
66+77+namespace osu.Framework.Tests.Input
88+{
99+ [TestFixture]
1010+ public class KeyCombinationModifierTest
1111+ {
1212+ private static readonly object[][] key_combination_display_test_cases =
1313+ {
1414+ // test single combination matches.
1515+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Any, true },
1616+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, true },
1717+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Any, true },
1818+ new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, false },
1919+ new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, true },
2020+2121+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Exact, true },
2222+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, true },
2323+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Exact, true },
2424+ new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, false },
2525+ new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, true },
2626+2727+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Modifiers, true },
2828+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true },
2929+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Modifiers, true },
3030+ new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, false },
3131+ new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true },
3232+3333+ // test multiple combination matches.
3434+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Any, true },
3535+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, true },
3636+ new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, false },
3737+ new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, true },
3838+3939+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Exact, true },
4040+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, true },
4141+ new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, false },
4242+ new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, true },
4343+4444+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Modifiers, true },
4545+ new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true },
4646+ new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, false },
4747+ new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true },
4848+ };
4949+5050+ [TestCaseSource(nameof(key_combination_display_test_cases))]
5151+ public void TestLeftRightModifierHandling(KeyCombination candidate, KeyCombination pressed, KeyCombinationMatchingMode matchingMode, bool shouldContain)
5252+ => Assert.AreEqual(shouldContain, KeyCombination.ContainsAll(candidate.Keys, pressed.Keys, matchingMode));
5353+ }
5454+}
+8-1
osu.Framework.Tests/Input/KeyCombinationTest.cs
···1414 new object[] { new KeyCombination(InputKey.Alt, InputKey.F4), "Alt-F4" },
1515 new object[] { new KeyCombination(InputKey.D, InputKey.Control), "Ctrl-D" },
1616 new object[] { new KeyCombination(InputKey.Shift, InputKey.F, InputKey.Control), "Ctrl-Shift-F" },
1717- new object[] { new KeyCombination(InputKey.Alt, InputKey.Control, InputKey.Super, InputKey.Shift), "Ctrl-Alt-Shift-Win" }
1717+ new object[] { new KeyCombination(InputKey.Alt, InputKey.Control, InputKey.Super, InputKey.Shift), "Ctrl-Alt-Shift-Win" },
1818+ new object[] { new KeyCombination(InputKey.LAlt, InputKey.F4), "LAlt-F4" },
1919+ new object[] { new KeyCombination(InputKey.D, InputKey.LControl), "LCtrl-D" },
2020+ new object[] { new KeyCombination(InputKey.LShift, InputKey.F, InputKey.LControl), "LCtrl-LShift-F" },
2121+ new object[] { new KeyCombination(InputKey.LAlt, InputKey.LControl, InputKey.LSuper, InputKey.LShift), "LCtrl-LAlt-LShift-LWin" },
2222+ new object[] { new KeyCombination(InputKey.Alt, InputKey.LAlt, InputKey.RControl, InputKey.A), "RCtrl-LAlt-A" },
2323+ new object[] { new KeyCombination(InputKey.Shift, InputKey.LControl, InputKey.X), "LCtrl-Shift-X" },
2424+ new object[] { new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.Alt, InputKey.Super, InputKey.LAlt, InputKey.RShift, InputKey.LSuper), "Ctrl-LAlt-RShift-LWin" },
1825 };
19262027 [TestCaseSource(nameof(key_combination_display_test_cases))]
···11// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22// See the LICENCE file in the repository root for full licence text.
3344+#if NET5_0
45using System.Linq;
56using osu.Framework.Allocation;
67using osu.Framework.Graphics;
···160161 }
161162 }
162163}
164164+#endif
···105105 private readonly Lazy<TrackStore> globalTrackStore;
106106 private readonly Lazy<SampleStore> globalSampleStore;
107107108108- private bool didInitialise;
109109-110108 /// <summary>
111109 /// Constructs an AudioStore given a track resource store, and a sample resource store.
112110 /// </summary>
···157155 {
158156 }
159157 }
160160- })
161161- {
162162- IsBackground = true
163163- }.Start();
158158+ }) { IsBackground = true }.Start();
164159 });
165160 }
166161167162 protected override void Dispose(bool disposing)
168163 {
169164 cancelSource.Cancel();
165165+170166 thread.UnregisterManager(this);
171167172168 OnNewDevice = null;
173169 OnLostDevice = null;
174174-175175- FreeBass();
176170177171 base.Dispose(disposing);
178172 }
···186180 {
187181 scheduler.Add(() =>
188182 {
183183+ if (cancelSource.IsCancellationRequested)
184184+ return;
185185+189186 if (!IsCurrentDeviceValid())
190187 setAudioDevice();
191188 });
···258255259256 // initialize new device
260257 bool initSuccess = InitBass(deviceIndex);
261261-262262- if (Bass.LastError == Errors.Already)
263263- {
264264- // We check if the initialization error is that we already initialized the device
265265- // If it is, it means we can just tell Bass to use the already initialized device without much
266266- // other fuzz.
267267- Bass.CurrentDevice = deviceIndex;
268268- FreeBass();
269269- initSuccess = InitBass(deviceIndex);
270270- }
271271-272272- if (BassUtils.CheckFaulted(false))
258258+ if (Bass.LastError != Errors.Already && BassUtils.CheckFaulted(false))
273259 return false;
274260275261 if (!initSuccess)
···326312 // ensure there are no brief delays on audio operations (causing stream STALLs etc.) after periods of silence.
327313 Bass.Configure(ManagedBass.Configuration.DevNonStop, true);
328314329329- didInitialise = true;
315315+ bool didInit = Bass.Init(device);
330316331331- return Bass.Init(device);
332332- }
317317+ // If the device was already initialised, the device can be used without much fuss.
318318+ if (Bass.LastError == Errors.Already)
319319+ {
320320+ Bass.CurrentDevice = device;
321321+ didInit = true;
322322+ }
333323334334- protected void FreeBass()
335335- {
336336- if (!didInitialise) return;
324324+ if (didInit)
325325+ thread.RegisterInitialisedDevice(device);
337326338338- Bass.Free();
339339- didInitialise = false;
327327+ return didInit;
340328 }
341329342330 private void syncAudioDevices()
+18-4
osu.Framework/Audio/Track/TrackBass.cs
···22// See the LICENCE file in the repository root for full licence text.
3344using System;
55+using System.Diagnostics;
56using System.IO;
67using System.Threading;
78using ManagedBass;
···110111 // Bass does not allow seeking to the end of the track, so the last available position is 1 sample before.
111112 lastSeekablePosition = Bass.ChannelBytes2Seconds(activeStream, byteLength - BYTES_PER_SAMPLE) * 1000;
112113113113- bitrate = (int)Bass.ChannelGetAttribute(activeStream, ChannelAttribute.Bitrate);
114114-115114 stopCallback = new SyncCallback((a, b, c, d) => RaiseFailed());
116115 endCallback = new SyncCallback((a, b, c, d) =>
117116 {
···157156158157 BassFlags flags = Preview ? 0 : BassFlags.Decode | BassFlags.Prescan;
159158 int stream = Bass.CreateStream(StreamSystem.NoBuffer, flags, fileCallbacks.Callbacks, fileCallbacks.Handle);
159159+160160+ bitrate = (int)Math.Round(Bass.ChannelGetAttribute(stream, ChannelAttribute.Bitrate));
160161161162 if (!Preview)
162163 {
···210211 base.UpdateState();
211212212213 var running = isRunningState(Bass.ChannelIsActive(activeStream));
213213- var bytePosition = Bass.ChannelGetPosition(activeStream);
214214215215 // because device validity check isn't done frequently, when switching to "No sound" device,
216216 // there will be a brief time where this track will be stopped, before we resume it manually (see comments in UpdateDevice(int).)
217217 // this makes us appear to be playing, even if we may not be.
218218 isRunning = /*running ||*/ (isPlayed && !hasCompleted);
219219220220- Interlocked.Exchange(ref currentTime, Bass.ChannelBytes2Seconds(activeStream, bytePosition) * 1000);
220220+ updateCurrentTime();
221221222222 bassAmplitudeProcessor?.Update();
223223 }
···351351352352 if (pos != mixer.GetChannelPosition(activeStream))
353353 mixer.SetChannelPosition(activeStream, pos);
354354+355355+ // current time updates are safe to perform from enqueued actions,
356356+ // but not always safe to perform from BASS callbacks, since those can sometimes use a separate thread.
357357+ // if it's not safe to update immediately here, the next UpdateState() call is guaranteed to update the time safely anyway.
358358+ if (CanPerformInline)
359359+ updateCurrentTime();
360360+ }
361361+362362+ private void updateCurrentTime()
363363+ {
364364+ Debug.Assert(CanPerformInline);
365365+366366+ var bytePosition = mixer.GetChannelPosition(activeStream);
367367+ Interlocked.Exchange(ref currentTime, Bass.ChannelBytes2Seconds(activeStream, bytePosition) * 1000);
354368 }
355369356370 private double currentTime;
+6-6
osu.Framework/Bindables/Bindable.cs
···1919 /// A generic implementation of a <see cref="IBindable"/>
2020 /// </summary>
2121 /// <typeparam name="T">The type of our stored <see cref="Value"/>.</typeparam>
2222- public class Bindable<T> : IBindable<T>, ISerializableBindable
2222+ public class Bindable<T> : IBindable<T>, IBindable, IParseable, ISerializableBindable
2323 {
2424 /// <summary>
2525 /// An event which is raised when <see cref="Value"/> has changed (or manually via <see cref="TriggerValueChange"/>).
···252252 Value = bindable.Value;
253253 break;
254254255255- case string s when underlyingType.IsEnum:
256256- Value = (T)Enum.Parse(underlyingType, s);
257257- break;
258258-259255 default:
260260- Value = (T)Convert.ChangeType(input, underlyingType, CultureInfo.InvariantCulture);
256256+ if (underlyingType.IsEnum)
257257+ Value = (T)Enum.Parse(underlyingType, input.ToString());
258258+ else
259259+ Value = (T)Convert.ChangeType(input, underlyingType, CultureInfo.InvariantCulture);
260260+261261 break;
262262 }
263263 }
+1-1
osu.Framework/Bindables/BindableList.cs
···11111212namespace osu.Framework.Bindables
1313{
1414- public class BindableList<T> : IBindableList<T>, IList<T>, IList
1414+ public class BindableList<T> : IBindableList<T>, IBindable, IParseable, IList<T>, IList
1515 {
1616 /// <summary>
1717 /// An event which is raised when this <see cref="BindableList{T}"/> changes.
+5-5
osu.Framework/Bindables/IBindable.cs
···88 /// <summary>
99 /// An interface which can be bound to other <see cref="IBindable"/>s in order to watch for (and react to) <see cref="ICanBeDisabled.Disabled">Disabled</see> changes.
1010 /// </summary>
1111- public interface IBindable : IParseable, ICanBeDisabled, IHasDefaultValue, IUnbindable, IHasDescription
1111+ public interface IBindable : ICanBeDisabled, IHasDefaultValue, IUnbindable, IHasDescription
1212 {
1313 /// <summary>
1414 /// Binds ourselves to another bindable such that we receive any value limitations of the bindable we bind width.
···2020 /// An alias of <see cref="BindTo"/> provided for use in object initializer scenarios.
2121 /// Passes the provided value as the foreign (more permanent) bindable.
2222 /// </summary>
2323- public sealed IBindable BindTarget
2323+ sealed IBindable BindTarget
2424 {
2525 set => BindTo(value);
2626 }
···3838 /// An interface which can be bound to other <see cref="IBindable{T}"/>s in order to watch for (and react to) <see cref="ICanBeDisabled.Disabled">Disabled</see> and <see cref="IBindable{T}.Value">Value</see> changes.
3939 /// </summary>
4040 /// <typeparam name="T">The type of value encapsulated by this <see cref="IBindable{T}"/>.</typeparam>
4141- public interface IBindable<T> : IBindable
4141+ public interface IBindable<T> : ICanBeDisabled, IHasDefaultValue, IUnbindable, IHasDescription
4242 {
4343 /// <summary>
4444 /// An event which is raised when <see cref="Value"/> has changed.
···6565 /// An alias of <see cref="BindTo"/> provided for use in object initializer scenarios.
6666 /// Passes the provided value as the foreign (more permanent) bindable.
6767 /// </summary>
6868- new IBindable<T> BindTarget
6868+ IBindable<T> BindTarget
6969 {
7070 set => BindTo(value);
7171 }
···8383 /// a local reference.
8484 /// </summary>
8585 /// <returns>A weakly bound copy of the specified bindable.</returns>
8686- new IBindable<T> GetBoundCopy();
8686+ IBindable<T> GetBoundCopy();
8787 }
8888}
+3-3
osu.Framework/Bindables/IBindableList.cs
···1010 /// A readonly interface which can be bound to other <see cref="IBindableList{T}"/>s in order to watch for state and content changes.
1111 /// </summary>
1212 /// <typeparam name="T">The type of value encapsulated by this <see cref="IBindableList{T}"/>.</typeparam>
1313- public interface IBindableList<T> : IReadOnlyList<T>, IBindable, INotifyCollectionChanged
1313+ public interface IBindableList<T> : IReadOnlyList<T>, ICanBeDisabled, IHasDefaultValue, IUnbindable, IHasDescription, INotifyCollectionChanged
1414 {
1515 /// <summary>
1616 /// Binds self to another bindable such that we receive any values and value limitations of the bindable we bind width.
···3030 /// An alias of <see cref="BindTo"/> provided for use in object initializer scenarios.
3131 /// Passes the provided value as the foreign (more permanent) bindable.
3232 /// </summary>
3333- new sealed IBindableList<T> BindTarget
3333+ sealed IBindableList<T> BindTarget
3434 {
3535 set => BindTo(value);
3636 }
···4141 /// a local reference.
4242 /// </summary>
4343 /// <returns>A weakly bound copy of the specified bindable.</returns>
4444- new IBindableList<T> GetBoundCopy();
4444+ IBindableList<T> GetBoundCopy();
4545 }
4646}
+5-1
osu.Framework/Configuration/IniConfigManager.cs
···66using System.IO;
77using osu.Framework.Bindables;
88using osu.Framework.Extensions.ObjectExtensions;
99+using osu.Framework.Extensions.TypeExtensions;
910using osu.Framework.Logging;
1011using osu.Framework.Platform;
1112···5960 {
6061 try
6162 {
6262- b.Parse(val);
6363+ if (!(b is IParseable parseable))
6464+ throw new InvalidOperationException($"Bindable type {b.GetType().ReadableName()} is not {nameof(IParseable)}.");
6565+6666+ parseable.Parse(val);
6367 }
6468 catch (Exception e)
6569 {
+72-15
osu.Framework/Development/ThreadSafety.cs
···7788namespace osu.Framework.Development
99{
1010- internal static class ThreadSafety
1010+ /// <summary>
1111+ /// Utilities to ensure correct thread usage throughout a game.
1212+ /// </summary>
1313+ public static class ThreadSafety
1114 {
1515+ /// <summary>
1616+ /// Whether the current code is executing on the input thread.
1717+ /// </summary>
1818+ [field: ThreadStatic]
1919+ public static bool IsInputThread { get; internal set; }
2020+2121+ /// <summary>
2222+ /// Whether the current code is executing on the update thread.
2323+ /// </summary>
2424+ [field: ThreadStatic]
2525+ public static bool IsUpdateThread { get; internal set; }
2626+2727+ /// <summary>
2828+ /// Whether the current code is executing on the draw thread.
2929+ /// </summary>
3030+ [field: ThreadStatic]
3131+ public static bool IsDrawThread { get; internal set; }
3232+3333+ /// <summary>
3434+ /// Whether the current code is executing on the audio thread.
3535+ /// </summary>
3636+ [field: ThreadStatic]
3737+ public static bool IsAudioThread { get; internal set; }
3838+3939+ /// <summary>
4040+ /// Asserts that the current code is executing on the input thread.
4141+ /// </summary>
4242+ /// <remarks>
4343+ /// Only asserts in debug builds due to performance concerns.
4444+ /// </remarks>
4545+ [Conditional("DEBUG")]
4646+ internal static void EnsureInputThread() => Debug.Assert(IsInputThread);
4747+4848+ /// <summary>
4949+ /// Asserts that the current code is executing on the update thread.
5050+ /// </summary>
5151+ /// <remarks>
5252+ /// Only asserts in debug builds due to performance concerns.
5353+ /// </remarks>
1254 [Conditional("DEBUG")]
1355 internal static void EnsureUpdateThread() => Debug.Assert(IsUpdateThread);
14565757+ /// <summary>
5858+ /// Asserts that the current code is not executing on the update thread.
5959+ /// </summary>
6060+ /// <remarks>
6161+ /// Only asserts in debug builds due to performance concerns.
6262+ /// </remarks>
1563 [Conditional("DEBUG")]
1664 internal static void EnsureNotUpdateThread() => Debug.Assert(!IsUpdateThread);
17656666+ /// <summary>
6767+ /// Asserts that the current code is executing on the draw thread.
6868+ /// </summary>
6969+ /// <remarks>
7070+ /// Only asserts in debug builds due to performance concerns.
7171+ /// </remarks>
1872 [Conditional("DEBUG")]
1973 internal static void EnsureDrawThread() => Debug.Assert(IsDrawThread);
20747575+ /// <summary>
7676+ /// Asserts that the current code is executing on the audio thread.
7777+ /// </summary>
7878+ /// <remarks>
7979+ /// Only asserts in debug builds due to performance concerns.
8080+ /// </remarks>
8181+ [Conditional("DEBUG")]
8282+ internal static void EnsureAudioThread() => Debug.Assert(IsAudioThread);
8383+8484+ /// <summary>
8585+ /// The current execution mode.
8686+ /// </summary>
8787+ internal static ExecutionMode ExecutionMode;
8888+8989+ /// <summary>
9090+ /// Resets all statics for the current thread.
9191+ /// </summary>
2192 internal static void ResetAllForCurrentThread()
2293 {
2394 IsInputThread = false;
···2596 IsDrawThread = false;
2697 IsAudioThread = false;
2798 }
2828-2929- public static ExecutionMode ExecutionMode;
3030-3131- [ThreadStatic]
3232- public static bool IsInputThread;
3333-3434- [ThreadStatic]
3535- public static bool IsUpdateThread;
3636-3737- [ThreadStatic]
3838- public static bool IsDrawThread;
3939-4040- [ThreadStatic]
4141- public static bool IsAudioThread;
4299 }
43100}
···4040 /// Return every base type until (and excluding) <see cref="object"/>
4141 /// </summary>
4242 /// <param name="t"></param>
4343- /// <returns></returns>
4443 public static IEnumerable<Type> EnumerateBaseTypes(this Type t)
4544 {
4645 while (t != null && t != typeof(object))
-1
osu.Framework/Graphics/Colour4.cs
···8989 /// The final alpha is clamped to the 0-1 range.
9090 /// </summary>
9191 /// <param name="scalar">The value that the existing alpha will be multiplied by.</param>
9292- /// <returns></returns>
9392 public Colour4 MultiplyAlpha(float scalar)
9493 {
9594 if (scalar < 0)
···66using System.Linq;
77using osu.Framework.Allocation;
88using osu.Framework.Caching;
99+using osu.Framework.Extensions.EnumExtensions;
910using osu.Framework.Layout;
1011using osuTK;
1112···267268 {
268269 // Go through each row and get the width of the cell at the indexed column
269270 for (int r = 0; r < cellRows; r++)
270270- size = Math.Max(size, getCellWidth(Content[r]?[i]));
271271+ {
272272+ var cell = Content[r]?[i];
273273+ if (cell == null || cell.RelativeSizeAxes.HasFlagFast(axis))
274274+ continue;
275275+276276+ size = Math.Max(size, getCellWidth(cell));
277277+ }
271278 }
272279 else
273280 {
274281 // Go through each column and get the height of the cell at the indexed row
275282 for (int c = 0; c < cellColumns; c++)
276276- size = Math.Max(size, getCellHeight(Content[i]?[c]));
283283+ {
284284+ var cell = Content[i]?[c];
285285+ if (cell == null || cell.RelativeSizeAxes.HasFlagFast(axis))
286286+ continue;
287287+288288+ size = Math.Max(size, getCellHeight(cell));
289289+ }
277290 }
278291279292 sizes[i] = size;
···1818 protected virtual bool BlockPositionalInput => true;
19192020 /// <summary>
2121+ /// Scroll events are sometimes required to be handled differently to general positional input.
2222+ /// This covers whether scroll events that occur within this overlay's bounds are blocked or not.
2323+ /// Defaults to the same value as <see cref="BlockPositionalInput"/>
2424+ /// </summary>
2525+ protected virtual bool BlockScrollInput => BlockPositionalInput;
2626+2727+ /// <summary>
2128 /// Whether we should block any non-positional input from interacting with things behind us.
2229 /// </summary>
2330 protected virtual bool BlockNonPositionalInput => false;
···4148 switch (e)
4249 {
4350 case ScrollEvent _:
4444- if (BlockPositionalInput && base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition))
5151+ if (BlockScrollInput && base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition))
4552 return true;
46534754 break;
···135135 /// </summary>
136136 /// <param name="position">The value to clamp.</param>
137137 /// <param name="extension">An extension value beyond the normal extent.</param>
138138- /// <returns></returns>
139138 protected float Clamp(float position, float extension = 0) => Math.Max(Math.Min(position, ScrollableExtent + extension), -extension);
140139141140 protected override Container<T> Content => ScrollContent;
-13
osu.Framework/Graphics/Cursor/CursorContainer.cs
···77using osu.Framework.Graphics.Shapes;
88using osu.Framework.Input;
99using osu.Framework.Input.Events;
1010-using osu.Framework.Utils;
1110using osuTK;
1211using osuTK.Graphics;
1312···37363837 public override bool PropagatePositionalInputSubTree => IsPresent; // make sure we are still updating position during possible fade out.
39384040- private Vector2? lastPosition;
4141-4239 protected override bool OnMouseMove(MouseMoveEvent e)
4340 {
4444- // required due to IRequireHighFrequencyMousePosition firing with the last known position even when the source is not in a
4545- // valid state (ie. receiving updates from user or otherwise). in this case, we generally want the cursor to remain at its
4646- // last *relative* position.
4747- if (lastPosition.HasValue && Precision.AlmostEquals(e.ScreenSpaceMousePosition, lastPosition.Value))
4848- return false;
4949-5050- lastPosition = e.ScreenSpaceMousePosition;
5151-5252- ActiveCursor.RelativePositionAxes = Axes.None;
5341 ActiveCursor.Position = e.MousePosition;
5454- ActiveCursor.RelativePositionAxes = Axes.Both;
5542 return base.OnMouseMove(e);
5643 }
5744
···7878 /// Retrieves the colour from a position in the texture of the <see cref="Path"/>.
7979 /// </summary>
8080 /// <param name="position">The position within the texture. 0 indicates the outermost-point of the path, 1 indicates the centre of the path.</param>
8181- /// <returns></returns>
8281 protected virtual Color4 ColourAt(float position) => Color4.White;
8382 }
8483}
···156156 }
157157158158 /// <summary>
159159- /// Invoked when the lifetime of an entry has changed, so that an appropriate sorting order is maintained.
159159+ /// Invoked when the lifetime of an entry is going to changed.
160160 /// </summary>
161161- /// <param name="entry">The <see cref="LifetimeEntry"/> that changed.</param>
162162- /// <param name="lifetimeStart">The new start time.</param>
163163- /// <param name="lifetimeEnd">The new end time.</param>
164164- private void requestLifetimeUpdate(LifetimeEntry entry, double lifetimeStart, double lifetimeEnd)
161161+ private void requestLifetimeUpdate(LifetimeEntry entry)
165162 {
166163 // Entries in the past/future sets need to be re-sorted to prevent the comparer from becoming unstable.
167164 // To prevent, e.g. CompositeDrawable alive children changing during enumeration, the entry's state must not be updated immediately.
···178175 // Enqueue the entry to be processed in the next Update().
179176 newEntries.Add(entry);
180177 }
181181-182182- // Since the comparer has now been resolved, the lifetime update can proceed.
183183- entry.SetLifetime(lifetimeStart, lifetimeEnd);
184178 }
185179186180 /// <summary>
-1
osu.Framework/Graphics/UserInterface/Menu.cs
···538538 /// <summary>
539539 /// Creates a sub-menu for <see cref="MenuItem.Items"/> of <see cref="MenuItem"/>s added to this <see cref="Menu"/>.
540540 /// </summary>
541541- /// <returns></returns>
542541 protected abstract Menu CreateSubMenu();
543542544543 /// <summary>
+49-1
osu.Framework/Input/Bindings/InputKey.cs
···3535 Alt = 5,
36363737 /// <summary>
3838- /// The win key.
3838+ /// The windows/command key.
3939 /// </summary>
4040 [Order(4)]
4141 Super = 7,
···813813 /// The media next key.
814814 /// </summary>
815815 TrackNext,
816816+817817+ /// <summary>
818818+ /// The left shift key.
819819+ /// </summary>
820820+ [Order(3)]
821821+ LShift,
822822+823823+ /// <summary>
824824+ /// The right shift key.
825825+ /// </summary>
826826+ [Order(3)]
827827+ RShift,
828828+829829+ /// <summary>
830830+ /// The left control key.
831831+ /// </summary>
832832+ [Order(1)]
833833+ LControl,
834834+835835+ /// <summary>
836836+ /// The right control key.
837837+ /// </summary>
838838+ [Order(1)]
839839+ RControl,
840840+841841+ /// <summary>
842842+ /// The left alt key.
843843+ /// </summary>
844844+ [Order(2)]
845845+ LAlt,
846846+847847+ /// <summary>
848848+ /// The right alt key.
849849+ /// </summary>
850850+ [Order(2)]
851851+ RAlt,
852852+853853+ /// <summary>
854854+ /// The left windows/command key.
855855+ /// </summary>
856856+ [Order(4)]
857857+ LSuper,
858858+859859+ /// <summary>
860860+ /// The right windows/command key.
861861+ /// </summary>
862862+ [Order(4)]
863863+ RSuper,
816864817865 /// <summary>
818866 /// Indicates the first available joystick button.
···117117 if (keyDown.Repeat && !SendRepeats)
118118 return pressedBindings.Count > 0;
119119120120- return handleNewPressed(state, KeyCombination.FromKey(keyDown.Key), keyDown.Repeat);
120120+ foreach (var key in KeyCombination.FromKey(keyDown.Key))
121121+ {
122122+ if (handleNewPressed(state, key, keyDown.Repeat))
123123+ return true;
124124+ }
125125+126126+ return false;
121127122128 case KeyUpEvent keyUp:
123123- handleNewReleased(state, KeyCombination.FromKey(keyUp.Key));
129129+ // this is releasing the common shift when a remaining shift is still held.
130130+ // ie. press LShift, press RShift, release RShift will result in InputKey.Shift being incorrectly released.
131131+ foreach (var key in KeyCombination.FromKey(keyUp.Key))
132132+ handleNewReleased(state, key);
133133+124134 return false;
125135126136 case JoystickPressEvent joystickPress:
···177187 bool handled = false;
178188 var bindings = (repeat ? KeyBindings : KeyBindings?.Except(pressedBindings)) ?? Enumerable.Empty<IKeyBinding>();
179189 var newlyPressed = bindings.Where(m =>
180180- m.KeyCombination.Keys.Contains(newKey) // only handle bindings matching current key (not required for correct logic)
181181- && m.KeyCombination.IsPressed(pressedCombination, matchingMode));
190190+ m.KeyCombination.IsPressed(pressedCombination, matchingMode));
182191183192 if (KeyCombination.IsModifierKey(newKey))
184193 {
···289298 // we don't want to consider exact matching here as we are dealing with bindings, not actions.
290299 var newlyReleased = pressedBindings.Where(b => !b.KeyCombination.IsPressed(pressedCombination, KeyCombinationMatchingMode.Any)).ToList();
291300292292- Trace.Assert(newlyReleased.All(b => b.KeyCombination.Keys.Contains(releasedKey)));
301301+ Trace.Assert(newlyReleased.All(b => KeyCombination.ContainsKey(b.KeyCombination.Keys, releasedKey)));
293302294303 foreach (var binding in newlyReleased)
295304 {
+235-65
osu.Framework/Input/Bindings/KeyCombination.cs
···7878 if (Keys == pressedKeys.Keys) // Fast test for reference equality of underlying array
7979 return true;
80808181- switch (matchingMode)
8282- {
8383- case KeyCombinationMatchingMode.Any:
8484- return containsAll(pressedKeys.Keys, Keys, false);
8585-8686- case KeyCombinationMatchingMode.Exact:
8787- // Keys are always ordered
8888- return pressedKeys.Keys.SequenceEqual(Keys);
8989-9090- case KeyCombinationMatchingMode.Modifiers:
9191- return containsAll(pressedKeys.Keys, Keys, true);
9292-9393- default:
9494- return false;
9595- }
8181+ return ContainsAll(Keys, pressedKeys.Keys, matchingMode);
9682 }
97838484+ /// <summary>
8585+ /// Check whether the provided set of pressed keys matches the candidate binding.
8686+ /// </summary>
8787+ /// <param name="candidateKey">The candidate key binding to match against.</param>
8888+ /// <param name="pressedKey">The keys which have been pressed by a user.</param>
8989+ /// <param name="matchingMode">The matching mode to be used when checking.</param>
9090+ /// <returns>Whether this is a match.</returns>
9891 [MethodImpl(MethodImplOptions.AggressiveInlining)]
9999- private static bool containsAll(ImmutableArray<InputKey> pressedKey, ImmutableArray<InputKey> candidateKey, bool exactModifiers)
9292+ internal static bool ContainsAll(ImmutableArray<InputKey> candidateKey, ImmutableArray<InputKey> pressedKey, KeyCombinationMatchingMode matchingMode)
10093 {
10194 // can be local function once attribute on local functions are implemented
10295 // optimized to avoid allocation
10396 // Usually Keys.Count <= 3. Does not worth special logic for Contains().
10497 foreach (var key in candidateKey)
10598 {
106106- if (!pressedKey.Contains(key))
9999+ if (!ContainsKey(pressedKey, key))
107100 return false;
108101 }
109102110110- if (exactModifiers)
103103+ switch (matchingMode)
111104 {
112112- foreach (var key in pressedKey)
113113- {
114114- if (IsModifierKey(key) &&
115115- !candidateKey.Contains(key))
116116- return false;
117117- }
105105+ case KeyCombinationMatchingMode.Exact:
106106+ foreach (var key in pressedKey)
107107+ {
108108+ if (!ContainsKeyPermissive(candidateKey, key))
109109+ return false;
110110+ }
111111+112112+ break;
113113+114114+ case KeyCombinationMatchingMode.Modifiers:
115115+ foreach (var key in pressedKey)
116116+ {
117117+ if (IsModifierKey(key) && !ContainsKeyPermissive(candidateKey, key))
118118+ return false;
119119+ }
120120+121121+ break;
118122 }
119123120124 return true;
121125 }
122126127127+ /// <summary>
128128+ /// Check whether the provided key is part of the candidate binding.
129129+ /// This will match bidirectionally for modifier keys (LShift and Shift being present in both of the two parameters in either order will return true).
130130+ /// </summary>
131131+ /// <param name="candidate">The candidate key binding to match against.</param>
132132+ /// <param name="key">The key which has been pressed by a user.</param>
133133+ /// <returns>Whether this is a match.</returns>
134134+ internal static bool ContainsKeyPermissive(ImmutableArray<InputKey> candidate, InputKey key)
135135+ {
136136+ switch (key)
137137+ {
138138+ case InputKey.LControl:
139139+ case InputKey.RControl:
140140+ if (candidate.Contains(InputKey.Control))
141141+ return true;
142142+143143+ break;
144144+145145+ case InputKey.LShift:
146146+ case InputKey.RShift:
147147+ if (candidate.Contains(InputKey.Shift))
148148+ return true;
149149+150150+ break;
151151+152152+ case InputKey.RAlt:
153153+ case InputKey.LAlt:
154154+ if (candidate.Contains(InputKey.Alt))
155155+ return true;
156156+157157+ break;
158158+159159+ case InputKey.LSuper:
160160+ case InputKey.RSuper:
161161+ if (candidate.Contains(InputKey.Super))
162162+ return true;
163163+164164+ break;
165165+ }
166166+167167+ return ContainsKey(candidate, key);
168168+ }
169169+170170+ /// <summary>
171171+ /// Check whether a single key from a candidate binding is relevant to the currently pressed keys.
172172+ /// If the <paramref name="key"/> contains a left/right specific modifier, the <paramref name="candidate"/> must also for this to match.
173173+ /// </summary>
174174+ /// <param name="candidate">The candidate key binding to match against.</param>
175175+ /// <param name="key">The key which has been pressed by a user.</param>
176176+ /// <returns>Whether this is a match.</returns>
177177+ internal static bool ContainsKey(ImmutableArray<InputKey> candidate, InputKey key)
178178+ {
179179+ switch (key)
180180+ {
181181+ case InputKey.Control:
182182+ if (candidate.Contains(InputKey.LControl) || candidate.Contains(InputKey.RControl))
183183+ return true;
184184+185185+ break;
186186+187187+ case InputKey.Shift:
188188+ if (candidate.Contains(InputKey.LShift) || candidate.Contains(InputKey.RShift))
189189+ return true;
190190+191191+ break;
192192+193193+ case InputKey.Alt:
194194+ if (candidate.Contains(InputKey.LAlt) || candidate.Contains(InputKey.RAlt))
195195+ return true;
196196+197197+ break;
198198+199199+ case InputKey.Super:
200200+ if (candidate.Contains(InputKey.LSuper) || candidate.Contains(InputKey.RSuper))
201201+ return true;
202202+203203+ break;
204204+ }
205205+206206+ return candidate.Contains(key);
207207+ }
208208+123209 public bool Equals(KeyCombination other) => Keys.SequenceEqual(other.Keys);
124210125211 public override bool Equals(object obj) => obj is KeyCombination kc && Equals(kc);
···146232147233 public string ReadableString()
148234 {
149149- var sortedKeys = Keys.GetValuesInOrder();
150150- return string.Join('-', sortedKeys.Select(getReadableKey));
235235+ var sortedKeys = Keys.GetValuesInOrder().ToArray();
236236+237237+ return string.Join('-', sortedKeys.Select(key =>
238238+ {
239239+ switch (key)
240240+ {
241241+ case InputKey.Control:
242242+ if (sortedKeys.Contains(InputKey.LControl) || sortedKeys.Contains(InputKey.RControl))
243243+ return null;
244244+245245+ break;
246246+247247+ case InputKey.Shift:
248248+ if (sortedKeys.Contains(InputKey.LShift) || sortedKeys.Contains(InputKey.RShift))
249249+ return null;
250250+251251+ break;
252252+253253+ case InputKey.Alt:
254254+ if (sortedKeys.Contains(InputKey.LAlt) || sortedKeys.Contains(InputKey.RAlt))
255255+ return null;
256256+257257+ break;
258258+259259+ case InputKey.Super:
260260+ if (sortedKeys.Contains(InputKey.LSuper) || sortedKeys.Contains(InputKey.RSuper))
261261+ return null;
262262+263263+ break;
264264+ }
265265+266266+ return getReadableKey(key);
267267+ }).Where(s => !string.IsNullOrEmpty(s)));
151268 }
152269153270 [MethodImpl(MethodImplOptions.AggressiveInlining)]
154154- public static bool IsModifierKey(InputKey key) => key == InputKey.Control || key == InputKey.Shift || key == InputKey.Alt || key == InputKey.Super;
271271+ public static bool IsModifierKey(InputKey key)
272272+ {
273273+ switch (key)
274274+ {
275275+ case InputKey.LControl:
276276+ case InputKey.LShift:
277277+ case InputKey.LAlt:
278278+ case InputKey.LSuper:
279279+ case InputKey.RControl:
280280+ case InputKey.RShift:
281281+ case InputKey.RAlt:
282282+ case InputKey.RSuper:
283283+ case InputKey.Control:
284284+ case InputKey.Shift:
285285+ case InputKey.Alt:
286286+ case InputKey.Super:
287287+ return true;
288288+ }
289289+290290+ return false;
291291+ }
155292156156- private string getReadableKey(InputKey key)
293293+ private static string getReadableKey(InputKey key)
157294 {
158295 if (key >= InputKey.FirstTabletAuxiliaryButton)
159296 return $"Tablet Aux {key - InputKey.FirstTabletAuxiliaryButton + 1}";
···195332 case InputKey.Super:
196333 return "Win";
197334335335+ case InputKey.LShift:
336336+ return "LShift";
337337+338338+ case InputKey.LControl:
339339+ return "LCtrl";
340340+341341+ case InputKey.LAlt:
342342+ return "LAlt";
343343+344344+ case InputKey.LSuper:
345345+ return "LWin";
346346+347347+ case InputKey.RShift:
348348+ return "RShift";
349349+350350+ case InputKey.RControl:
351351+ return "RCtrl";
352352+353353+ case InputKey.RAlt:
354354+ return "RAlt";
355355+356356+ case InputKey.RSuper:
357357+ return "RWin";
358358+198359 case InputKey.Escape:
199360 return "Esc";
200361···217378 return "Caps";
218379219380 case InputKey.Number0:
220220- case InputKey.Keypad0:
221381 return "0";
382382+383383+ case InputKey.Keypad0:
384384+ return "Numpad0";
222385223386 case InputKey.Number1:
224224- case InputKey.Keypad1:
225387 return "1";
388388+389389+ case InputKey.Keypad1:
390390+ return "Numpad1";
226391227392 case InputKey.Number2:
228228- case InputKey.Keypad2:
229393 return "2";
394394+395395+ case InputKey.Keypad2:
396396+ return "Numpad2";
230397231398 case InputKey.Number3:
232232- case InputKey.Keypad3:
233399 return "3";
400400+401401+ case InputKey.Keypad3:
402402+ return "Numpad3";
234403235404 case InputKey.Number4:
236236- case InputKey.Keypad4:
237405 return "4";
406406+407407+ case InputKey.Keypad4:
408408+ return "Numpad4";
238409239410 case InputKey.Number5:
240240- case InputKey.Keypad5:
241411 return "5";
412412+413413+ case InputKey.Keypad5:
414414+ return "Numpad5";
242415243416 case InputKey.Number6:
244244- case InputKey.Keypad6:
245417 return "6";
418418+419419+ case InputKey.Keypad6:
420420+ return "Numpad6";
246421247422 case InputKey.Number7:
248248- case InputKey.Keypad7:
249423 return "7";
424424+425425+ case InputKey.Keypad7:
426426+ return "Numpad7";
250427251428 case InputKey.Number8:
252252- case InputKey.Keypad8:
253429 return "8";
430430+431431+ case InputKey.Keypad8:
432432+ return "Numpad8";
254433255434 case InputKey.Number9:
256256- case InputKey.Keypad9:
257435 return "9";
436436+437437+ case InputKey.Keypad9:
438438+ return "Numpad9";
258439259440 case InputKey.Tilde:
260441 return "~";
···364545 }
365546 }
366547367367- public static InputKey FromKey(Key key)
548548+ public static InputKey[] FromKey(Key key)
368549 {
369550 switch (key)
370551 {
371371- case Key.RShift:
372372- return InputKey.Shift;
552552+ case Key.LShift: return new[] { InputKey.Shift, InputKey.LShift };
553553+554554+ case Key.RShift: return new[] { InputKey.Shift, InputKey.RShift };
555555+556556+ case Key.LControl: return new[] { InputKey.Control, InputKey.LControl };
373557374374- case Key.RAlt:
375375- return InputKey.Alt;
558558+ case Key.RControl: return new[] { InputKey.Control, InputKey.RControl };
376559377377- case Key.RControl:
378378- return InputKey.Control;
560560+ case Key.LAlt: return new[] { InputKey.Alt, InputKey.LAlt };
379561380380- case Key.RWin:
381381- return InputKey.Super;
562562+ case Key.RAlt: return new[] { InputKey.Alt, InputKey.RAlt };
563563+564564+ case Key.LWin: return new[] { InputKey.Super, InputKey.LSuper };
565565+566566+ case Key.RWin: return new[] { InputKey.Super, InputKey.RSuper };
382567 }
383568384384- return (InputKey)key;
569569+ return new[] { (InputKey)key };
385570 }
386571387572 public static InputKey FromMouseButton(MouseButton button) => (InputKey)((int)InputKey.FirstMouseButton + button);
···449634 {
450635 foreach (var key in state.Keyboard.Keys)
451636 {
452452- InputKey iKey = FromKey(key);
453453-454454- switch (key)
637637+ foreach (var iKey in FromKey(key))
455638 {
456456- case Key.LShift:
457457- case Key.RShift:
458458- case Key.LAlt:
459459- case Key.RAlt:
460460- case Key.LControl:
461461- case Key.RControl:
462462- case Key.LWin:
463463- case Key.RWin:
464464- if (!keys.Contains(iKey))
465465- keys.Add(iKey);
466466- break;
467467-468468- default:
639639+ if (!keys.Contains(iKey))
469640 keys.Add(iKey);
470470- break;
471641 }
472642 }
473643 }
+5-1
osu.Framework/Input/Handlers/Midi/MidiHandler.cs
···66using System.Diagnostics;
77using System.IO;
88using System.Linq;
99+using System.Threading.Tasks;
910using Commons.Music.Midi;
1011using osu.Framework.Input.StateChanges;
1112using osu.Framework.Logging;
···110111111112 private void closeDevice(IMidiInput device)
112113 {
113113- device.CloseAsync().Wait();
114114 device.MessageReceived -= onMidiMessageReceived;
115115+116116+ // some devices may take some time to close, so this should be fire-and-forget.
117117+ // the internal implementations look to have their own (eventual) timeout logic.
118118+ Task.Factory.StartNew(() => device.CloseAsync(), TaskCreationOptions.LongRunning);
115119 }
116120117121 private void onMidiMessageReceived(object sender, MidiReceivedEventArgs e)
···5151 /// </summary>
5252 private bool absolutePositionReceived;
53535454+ /// <summary>
5555+ /// Whether the application should be handling the cursor.
5656+ /// </summary>
5757+ private bool cursorCaptured => isActive.Value && (window.CursorInWindow.Value || window.CursorState.HasFlagFast(CursorState.Confined));
5858+5459 public override bool Initialize(GameHost host)
5560 {
5661 if (!base.Initialize(host))
···106111 if (!Enabled.Value)
107112 return;
108113109109- if (!isSelfFeedback)
114114+ if (!isSelfFeedback && isActive.Value)
110115 // if another handler has updated the cursor position, handle updating the OS cursor so we can seamlessly revert
111116 // to mouse control at any point.
112117 window.UpdateMousePosition(position);
···151156 // relative mode requires at least one absolute input to arrive, to gain an additional position to work with.
152157 && absolutePositionReceived
153158 // relative mode only works when the window is active and the cursor is contained. aka the OS cursor isn't being displayed outside the window.
154154- && (isActive.Value && (window.CursorInWindow.Value || window.CursorState.HasFlagFast(CursorState.Confined)))
159159+ && cursorCaptured
155160 // relative mode shouldn't ever be enabled if the framework or a consumer has chosen not to hide the cursor.
156161 && window.CursorState.HasFlagFast(CursorState.Hidden);
157162
+3
osu.Framework/Input/InputManager.cs
···523523524524 foreach (var h in InputHandlers)
525525 {
526526+ if (!h.IsActive)
527527+ continue;
528528+526529 dequeuedInputs.Clear();
527530 h.CollectPendingInputs(dequeuedInputs);
528531
···5959 /// <param name="size">The <see cref="Size"/> to match.</param>
6060 /// <param name="bitsPerPixel">The bits per pixel to match. If null, the highest available bits per pixel will be used.</param>
6161 /// <param name="refreshRate">The refresh rate in hertz. If null, the highest available refresh rate will be used.</param>
6262- /// <returns></returns>
6362 public DisplayMode FindDisplayMode(Size size, int? bitsPerPixel = null, int? refreshRate = null) =>
6463 DisplayModes.Where(mode => mode.Size.Width <= size.Width && mode.Size.Height <= size.Height &&
6564 (bitsPerPixel == null || mode.BitsPerPixel == bitsPerPixel) &&
-2
osu.Framework/Platform/IWindow.cs
···151151 /// Convert a screen based coordinate to local window space.
152152 /// </summary>
153153 /// <param name="point"></param>
154154- /// <returns></returns>
155154 Point PointToClient(Point point);
156155157156 /// <summary>
158157 /// Convert a window based coordinate to global screen space.
159158 /// </summary>
160159 /// <param name="point"></param>
161161- /// <returns></returns>
162160 Point PointToScreen(Point point);
163161164162 /// <summary>
+6-4
osu.Framework/Platform/SDL2DesktopWindow.cs
···163163164164 public Bindable<WindowMode> WindowMode { get; } = new Bindable<WindowMode>();
165165166166- private readonly BindableBool isActive = new BindableBool(true);
166166+ private readonly BindableBool isActive = new BindableBool();
167167168168 public IBindable<bool> IsActive => isActive;
169169170170- private readonly BindableBool cursorInWindow = new BindableBool(true);
170170+ private readonly BindableBool cursorInWindow = new BindableBool();
171171172172 public IBindable<bool> CursorInWindow => cursorInWindow;
173173···881881 SDL.SDL_GetWindowPosition(SDLWindowHandle, out int x, out int y);
882882 var newPosition = new Point(x, y);
883883884884- if (WindowMode.Value == Configuration.WindowMode.Windowed && !newPosition.Equals(Position))
884884+ if (!newPosition.Equals(Position))
885885 {
886886 position = newPosition;
887887- storeWindowPositionToConfig();
888887 ScheduleEvent(() => Moved?.Invoke(newPosition));
888888+889889+ if (WindowMode.Value == Configuration.WindowMode.Windowed)
890890+ storeWindowPositionToConfig();
889891 }
890892891893 break;
+1-1
osu.Framework/Platform/Windows/WindowsWindow.cs
···45454646 // for now let's use the same 1px hack that we've always used to force borderless.
4747 SDL.SDL_SetWindowSize(SDLWindowHandle, newSize.Width, newSize.Height);
4848- SDL.SDL_SetWindowPosition(SDLWindowHandle, newPosition.X, newPosition.Y);
4848+ Position = newPosition;
49495050 return newSize;
5151 }
···3344using System;
55using System.Collections.Generic;
66+using System.IO;
67using System.Threading.Tasks;
88+using osu.Framework.Extensions.TypeExtensions;
79810namespace osu.Framework.Testing
911{
···3537 /// Resets this <see cref="ITypeReferenceBuilder"/>.
3638 /// </summary>
3739 void Reset();
4040+ }
4141+4242+ /// <summary>
4343+ /// Indicates that there was no link between a given test type and the changed file.
4444+ /// </summary>
4545+ internal class NoLinkBetweenTypesException : Exception
4646+ {
4747+ public NoLinkBetweenTypesException(Type testType, string changedFile)
4848+ : base($"The changed file \"{Path.GetFileName(changedFile)}\" is not used by the test \"{testType.ReadableName()}\".")
4949+ {
5050+ }
3851 }
3952}
···3344#if NET5_0
55using System;
66+using System.Collections.Concurrent;
67using System.Collections.Generic;
78using System.IO;
89using System.Linq;
···28292930 private readonly Logger logger;
30313131- private readonly Dictionary<TypeReference, IReadOnlyCollection<TypeReference>> referenceMap = new Dictionary<TypeReference, IReadOnlyCollection<TypeReference>>();
3232- private readonly Dictionary<Project, Compilation> compilationCache = new Dictionary<Project, Compilation>();
3333- private readonly Dictionary<string, SemanticModel> semanticModelCache = new Dictionary<string, SemanticModel>();
3434- private readonly Dictionary<TypeReference, bool> typeInheritsFromGameCache = new Dictionary<TypeReference, bool>();
3535- private readonly Dictionary<string, bool> syntaxExclusionMap = new Dictionary<string, bool>();
3636- private readonly HashSet<string> assembliesContainingReferencedInternalMembers = new HashSet<string>();
3232+ private readonly ConcurrentDictionary<TypeReference, IReadOnlyCollection<TypeReference>> referenceMap = new ConcurrentDictionary<TypeReference, IReadOnlyCollection<TypeReference>>();
3333+ private readonly ConcurrentDictionary<Project, Compilation> compilationCache = new ConcurrentDictionary<Project, Compilation>();
3434+ private readonly ConcurrentDictionary<string, SemanticModel> semanticModelCache = new ConcurrentDictionary<string, SemanticModel>();
3535+ private readonly ConcurrentDictionary<TypeReference, bool> typeInheritsFromGameCache = new ConcurrentDictionary<TypeReference, bool>();
3636+ private readonly ConcurrentDictionary<string, bool> syntaxExclusionMap = new ConcurrentDictionary<string, bool>();
3737+ private readonly ConcurrentDictionary<string, byte> assembliesContainingReferencedInternalMembers = new ConcurrentDictionary<string, byte>();
37383839 private Solution solution;
3940···56575758 await buildReferenceMapAsync(testType, changedFile).ConfigureAwait(false);
58595959- var directedGraph = getDirectedGraph();
6060+ var sources = getTypesFromFile(changedFile).ToArray();
6161+ if (sources.Length == 0)
6262+ throw new NoLinkBetweenTypesException(testType, changedFile);
60636161- return getReferencedFiles(getTypesFromFile(changedFile), directedGraph);
6464+ return getReferencedFiles(sources, getDirectedGraph());
6265 }
63666467 public async Task<IReadOnlyCollection<AssemblyReference>> GetReferencedAssemblies(Type testType, string changedFile) => await Task.Run(() =>
···67706871 var assemblies = new HashSet<AssemblyReference>();
69727373+ foreach (var asm in compilationCache.Values.SelectMany(c => c.ReferencedAssemblyNames))
7474+ addReference(Assembly.Load(asm.Name), false);
7075 foreach (var asm in AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic))
7176 addReference(asm, false);
7277 addReference(typeof(JetBrains.Annotations.NotNullAttribute).Assembly, true);
···8590 if (!force && loadedTypes.Any(t => t.Namespace == jetbrains_annotations_namespace))
8691 return;
87928888- bool containsReferencedInternalMember = assembliesContainingReferencedInternalMembers.Any(i => assembly.FullName?.Contains(i) == true);
9393+ bool containsReferencedInternalMember = assembliesContainingReferencedInternalMembers.Any(i => assembly.FullName?.Contains(i.Key) == true);
8994 assemblies.Add(new AssemblyReference(assembly, containsReferencedInternalMember));
9095 }
9196 }).ConfigureAwait(false);
···143148144149 foreach (var t in oldTypes)
145150 {
146146- referenceMap.Remove(t);
147147- typeInheritsFromGameCache.Remove(t);
151151+ referenceMap.TryRemove(t, out _);
152152+ typeInheritsFromGameCache.TryRemove(t, out _);
148153 }
149154150155 foreach (var t in oldTypes)
···166171 var semanticModel = await getSemanticModelAsync(syntaxTree).ConfigureAwait(false);
167172 var referencedTypes = await getReferencedTypesAsync(semanticModel).ConfigureAwait(false);
168173169169- referenceMap[TypeReference.FromSymbol(t.Symbol)] = referencedTypes;
174174+ referenceMap[TypeReference.FromSymbol(t.Symbol)] = referencedTypes.ToHashSet();
170175171176 foreach (var referenced in referencedTypes)
172177 await buildReferenceMapRecursiveAsync(referenced).ConfigureAwait(false);
···189194 /// <param name="rootReference">The root, where the map should start being build from.</param>
190195 private async Task buildReferenceMapRecursiveAsync(TypeReference rootReference)
191196 {
192192- var searchQueue = new Queue<TypeReference>();
193193- searchQueue.Enqueue(rootReference);
197197+ var searchQueue = new ConcurrentBag<TypeReference> { rootReference };
194198195199 while (searchQueue.Count > 0)
196200 {
197197- var toCheck = searchQueue.Dequeue();
198198- var referencedTypes = await getReferencedTypesAsync(toCheck).ConfigureAwait(false);
201201+ var toProcess = searchQueue.ToArray();
202202+ searchQueue.Clear();
199203200200- referenceMap[toCheck] = referencedTypes;
204204+ await Task.WhenAll(toProcess.Select(async toCheck =>
205205+ {
206206+ var referencedTypes = await getReferencedTypesAsync(toCheck).ConfigureAwait(false);
207207+ referenceMap[toCheck] = referencedTypes;
201208202202- foreach (var referenced in referencedTypes)
203203- {
204204- // We don't want to cycle over types that have already been explored.
205205- if (!referenceMap.ContainsKey(referenced))
209209+ foreach (var referenced in referencedTypes)
206210 {
207207- // Used for de-duping, so it must be added to the dictionary immediately.
208208- referenceMap[referenced] = null;
209209- searchQueue.Enqueue(referenced);
211211+ // We don't want to cycle over types that have already been explored.
212212+ if (referenceMap.TryAdd(referenced, null))
213213+ searchQueue.Add(referenced);
210214 }
211211- }
215215+ })).ConfigureAwait(false);
212216 }
213217 }
214218···238242 /// </summary>
239243 /// <param name="semanticModel">The target <see cref="SemanticModel"/>.</param>
240244 /// <returns>All <see cref="TypeReference"/>s referenced by <paramref name="semanticModel"/>.</returns>
241241- private async Task<HashSet<TypeReference>> getReferencedTypesAsync(SemanticModel semanticModel)
245245+ private async Task<ICollection<TypeReference>> getReferencedTypesAsync(SemanticModel semanticModel)
242246 {
243243- var result = new HashSet<TypeReference>();
247247+ var result = new ConcurrentDictionary<TypeReference, byte>();
244248245249 var root = await semanticModel.SyntaxTree.GetRootAsync().ConfigureAwait(false);
246246-247250 var descendantNodes = root.DescendantNodes(n =>
248251 {
249252 var kind = n.Kind();
···252255 // - Entire using lines.
253256 // - Namespace names (not entire namespaces).
254257 // - Entire static classes.
258258+ // - Variable declarators (names of variables).
259259+ // - The single IdentifierName child of an assignment expression (variable name), below.
260260+ // - The single IdentifierName child of an argument syntax (variable name), below.
261261+ // - The name of namespace declarations.
262262+ // - Name-colon syntaxes.
263263+ // - The expression of invocation expressions. Static classes are explicitly disallowed so the target type of an invocation must be available elsewhere in the syntax tree.
264264+ // - The single IdentifierName child of a foreach expression (source variable name), below.
265265+ // - The single 'var' IdentifierName child of a variable declaration, below.
266266+ // - Element access expressions.
255267256268 return kind != SyntaxKind.UsingDirective
257269 && kind != SyntaxKind.NamespaceKeyword
258258- && (kind != SyntaxKind.ClassDeclaration || ((ClassDeclarationSyntax)n).Modifiers.All(m => m.Kind() != SyntaxKind.StaticKeyword));
270270+ && (kind != SyntaxKind.ClassDeclaration || ((ClassDeclarationSyntax)n).Modifiers.All(m => m.Kind() != SyntaxKind.StaticKeyword))
271271+ && (kind != SyntaxKind.QualifiedName || !(n.Parent is NamespaceDeclarationSyntax))
272272+ && kind != SyntaxKind.NameColon
273273+ && (kind != SyntaxKind.QualifiedName || n.Parent?.Kind() != SyntaxKind.NamespaceDeclaration)
274274+ && kind != SyntaxKind.NameColon
275275+ && kind != SyntaxKind.ElementAccessExpression
276276+ && (n.Parent?.Kind() != SyntaxKind.InvocationExpression || n != ((InvocationExpressionSyntax)n.Parent).Expression);
259277 });
260278261261- // Find all the named type symbols in the syntax tree, and mark + recursively iterate through them.
262262- foreach (var node in descendantNodes)
279279+ // This hashset is used to prevent re-exploring syntaxes with the same name.
280280+ // Todo: This can be used across all files, but care needs to be taken for redefined types (via using X = y), using the same-named type from a different namespace, or via type hiding.
281281+ var seenTypes = new ConcurrentDictionary<string, byte>();
282282+283283+ await Task.WhenAll(descendantNodes.Select(node => Task.Run(() =>
263284 {
285285+ if (node.Kind() == SyntaxKind.IdentifierName && node.Parent != null)
286286+ {
287287+ // Ignore the variable name of assignment expressions.
288288+ if (node.Parent is AssignmentExpressionSyntax)
289289+ return;
290290+291291+ switch (node.Parent.Kind())
292292+ {
293293+ case SyntaxKind.VariableDeclarator: // Ignore the variable name of variable declarators.
294294+ case SyntaxKind.Argument: // Ignore the variable name of arguments.
295295+ case SyntaxKind.InvocationExpression: // Ignore a single identifier name expression of an invocation expression (e.g. IdentifierName()).
296296+ case SyntaxKind.ForEachStatement: // Ignore a single identifier of a foreach statement (the source).
297297+ case SyntaxKind.VariableDeclaration when node.ToString() == "var": // Ignore the single 'var' identifier of a variable declaration.
298298+ return;
299299+ }
300300+ }
301301+264302 switch (node.Kind())
265303 {
266304 case SyntaxKind.GenericName:
267305 case SyntaxKind.IdentifierName:
268306 {
269269- if (semanticModel.GetSymbolInfo(node).Symbol is INamedTypeSymbol t)
270270- addTypeSymbol(t);
307307+ string syntaxName = node.ToString();
308308+309309+ if (seenTypes.ContainsKey(syntaxName))
310310+ return;
311311+312312+ if (!tryNode(node, out var symbol))
313313+ return;
314314+315315+ // The node has been processed so we want to avoid re-processing the same node again if possible, as this is a costly operation.
316316+ // Note that the syntax name may differ from the finalised symbol name (e.g. member access).
317317+ // We can only prevent future reprocessing if the symbol name and syntax name exactly match because we can't determine that the type won't be accessed later, such as:
318318+ //
319319+ // A.X = 5; // Syntax name = A, Symbol name = B
320320+ // B.X = 5; // Syntax name = B, Symbol name = A
321321+ // public A B;
322322+ // public B A;
323323+ //
324324+ if (symbol.Name == syntaxName)
325325+ seenTypes.TryAdd(symbol.Name, 0);
326326+271327 break;
272328 }
329329+ }
330330+ }))).ConfigureAwait(false);
331331+332332+ return result.Keys;
273333274274- case SyntaxKind.AsExpression:
275275- case SyntaxKind.IsExpression:
276276- case SyntaxKind.SizeOfExpression:
277277- case SyntaxKind.TypeOfExpression:
278278- case SyntaxKind.CastExpression:
279279- case SyntaxKind.ObjectCreationExpression:
280280- {
281281- if (semanticModel.GetTypeInfo(node).Type is INamedTypeSymbol t)
282282- addTypeSymbol(t);
283283- break;
284284- }
334334+ bool tryNode(SyntaxNode node, out INamedTypeSymbol symbol)
335335+ {
336336+ if (semanticModel.GetSymbolInfo(node).Symbol is INamedTypeSymbol sType)
337337+ {
338338+ addTypeSymbol(sType);
339339+ symbol = sType;
340340+ return true;
341341+ }
342342+343343+ if (semanticModel.GetTypeInfo(node).Type is INamedTypeSymbol tType)
344344+ {
345345+ addTypeSymbol(tType);
346346+ symbol = tType;
347347+ return true;
285348 }
286286- }
287349288288- return result;
350350+ // Todo: Reduce the number of cases that fall through here.
351351+ symbol = null;
352352+ return false;
353353+ }
289354290355 void addTypeSymbol(INamedTypeSymbol typeSymbol)
291356 {
···316381 }
317382318383 if (typeSymbol.DeclaredAccessibility == Accessibility.Internal)
319319- assembliesContainingReferencedInternalMembers.Add(typeSymbol.ContainingAssembly.Name);
384384+ assembliesContainingReferencedInternalMembers.TryAdd(typeSymbol.ContainingAssembly.Name, 0);
320385321321- result.Add(reference);
386386+ result.TryAdd(reference, 0);
322387 }
323388 }
324389···463528 // Follow through the process for all parents.
464529 foreach (var p in node.Parents)
465530 {
531531+ int nextLevel = level + 1;
532532+466533 // Right-bound outlier test - exclude parents greater than 3x IQR. Always expand left-bound parents as they are unlikely to cause compilation errors.
467534 if (p.ExpansionFactor > rightBound)
535535+ {
536536+ logger.Add($"{(nextLevel > 0 ? $".{new string(' ', nextLevel * 2 - 1)}| " : string.Empty)} {node.ExpansionFactor} (rb: {rightBound}): {node} (!! EXCLUDED !!)");
468537 continue;
538538+ }
469539470470- getReferencedFilesRecursive(p, result, seenTypes, level + 1, expansions);
540540+ getReferencedFilesRecursive(p, result, seenTypes, nextLevel, expansions);
471541 }
472542 }
473543···490560491561 // When used via a nuget package, the local type name seems to always be more qualified than the symbol's type name.
492562 // E.g. Type name: osu.Framework.Game, symbol name: Framework.Game.
493493- if (typeof(Game).FullName?.Contains(reference.Symbol.ToString()) == true)
563563+ if (typeof(Game).FullName?.Contains(reference.ToString()) == true)
494564 return typeInheritsFromGameCache[reference] = true;
495565496566 if (reference.Symbol.BaseType == null)
···584654 private readonly struct TypeReference : IEquatable<TypeReference>
585655 {
586656 public readonly INamedTypeSymbol Symbol;
657657+ public readonly string ContainingNamespace;
658658+ public readonly string SymbolName;
587659588660 public TypeReference(INamedTypeSymbol symbol)
589661 {
590662 Symbol = symbol;
663663+ ContainingNamespace = symbol.ContainingNamespace.ToString();
664664+ SymbolName = symbol.ToString();
591665 }
592666593667 public bool Equals(TypeReference other)
594594- => Symbol.ContainingNamespace.ToString() == other.Symbol.ContainingNamespace.ToString()
595595- && Symbol.ToString() == other.Symbol.ToString();
668668+ => ContainingNamespace == other.ContainingNamespace
669669+ && SymbolName == other.SymbolName;
596670597671 public override int GetHashCode()
598672 {
599673 var hash = new HashCode();
600600- hash.Add(Symbol.ToString(), StringComparer.Ordinal);
674674+ hash.Add(SymbolName, StringComparer.Ordinal);
601675 return hash.ToHashCode();
602676 }
603677604604- public override string ToString() => Symbol.ToString();
678678+ public override string ToString() => SymbolName;
605679606680 public static TypeReference FromSymbol(INamedTypeSymbol symbol) => new TypeReference(symbol);
607681 }
-1
osu.Framework/Testing/TestScene.cs
···394394 /// Remove the "TestScene" prefix from a name.
395395 /// </summary>
396396 /// <param name="name"></param>
397397- /// <returns></returns>
398397 public static string RemovePrefix(string name)
399398 {
400399 return name.Replace("TestCase", string.Empty) // TestScene used to be called TestCase. This handles consumer projects which haven't updated their naming for the near future.
+28-3
osu.Framework/Threading/AudioThread.cs
···44using osu.Framework.Statistics;
55using System;
66using System.Collections.Generic;
77+using System.Diagnostics;
78using ManagedBass;
89using osu.Framework.Audio;
910using osu.Framework.Development;
···4546 };
46474748 private readonly List<AudioManager> managers = new List<AudioManager>();
4949+ private readonly HashSet<int> initialisedDevices = new HashSet<int>();
48504951 private static readonly GlobalStatistic<double> cpu_usage = GlobalStatistics.Get<double>("Audio", "Bass CPU%");
5052···6264 }
6365 }
64666565- public void RegisterManager(AudioManager manager)
6767+ internal void RegisterManager(AudioManager manager)
6668 {
6769 lock (managers)
6870 {
···7375 }
7476 }
75777676- public void UnregisterManager(AudioManager manager)
7878+ internal void UnregisterManager(AudioManager manager)
7779 {
7880 lock (managers)
7981 managers.Remove(manager);
8082 }
81838484+ internal void RegisterInitialisedDevice(int deviceId)
8585+ {
8686+ Debug.Assert(ThreadSafety.IsAudioThread);
8787+ initialisedDevices.Add(deviceId);
8888+ }
8989+8290 protected override void PerformExit()
8391 {
8492 base.PerformExit();
···103111 // Safety net to ensure we have freed all devices before exiting.
104112 // This is mainly required for device-lost scenarios.
105113 // See https://github.com/ppy/osu-framework/pull/3378 for further discussion.
106106- while (Bass.Free()) { }
114114+ foreach (var d in initialisedDevices)
115115+ freeDevice(d);
116116+ }
117117+118118+ private void freeDevice(int deviceId)
119119+ {
120120+ int lastDevice = Bass.CurrentDevice;
121121+122122+ // Freeing the 0 device on linux can cause deadlocks. This doesn't always happen immediately.
123123+ // Todo: Reproduce in native code and report to BASS at some point.
124124+ if (deviceId != 0 || RuntimeInfo.OS != RuntimeInfo.Platform.Linux)
125125+ {
126126+ Bass.CurrentDevice = deviceId;
127127+ Bass.Free();
128128+ }
129129+130130+ if (lastDevice != deviceId)
131131+ Bass.CurrentDevice = lastDevice;
107132 }
108133 }
109134}
···11// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22// See the LICENCE file in the repository root for full licence text.
3344+#nullable enable
55+46using System;
5768namespace osu.Framework.Timing
···3335 /// <summary>
3436 /// We need to be able to pass on adjustments to the source if it supports them.
3537 /// </summary>
3636- private IAdjustableClock adjustableSource => Source as IAdjustableClock;
3838+ private IAdjustableClock? adjustableSource => Source as IAdjustableClock;
37393840 public override double CurrentTime => currentTime;
3941···5254 public override double Rate
5355 {
5456 get => Source?.Rate ?? 1;
5555- set => adjustableSource.Rate = value;
5757+ set
5858+ {
5959+ if (adjustableSource == null)
6060+ throw new NotSupportedException("Source is not adjustable.");
6161+6262+ adjustableSource.Rate = value;
6363+ }
5664 }
57655866 public void ResetSpeedAdjustments() => Rate = 1;
···111119 currentTime = elapsedFrameTime < 0 ? Math.Min(currentTime, proposedTime) : Math.Max(currentTime, proposedTime);
112120 }
113121114114- public override void ChangeSource(IClock source)
122122+ public override void ChangeSource(IClock? source)
115123 {
116124 if (source == null) return;
117125
+7-3
osu.Framework/Timing/FramedClock.cs
···11// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22// See the LICENCE file in the repository root for full licence text.
3344+#nullable enable
55+46using osu.Framework.Extensions.TypeExtensions;
57using System;
68···1921 /// </summary>
2022 /// <param name="source">A source clock which will be used as the backing time source. If null, a StopwatchClock will be created. When provided, the CurrentTime of <paramref name="source"/> will be transferred instantly.</param>
2123 /// <param name="processSource">Whether the source clock's <see cref="ProcessFrame"/> method should be called during this clock's process call.</param>
2222- public FramedClock(IClock source = null, bool processSource = true)
2424+ public FramedClock(IClock? source = null, bool processSource = true)
2325 {
2426 this.processSource = processSource;
2525- ChangeSource(source ?? new StopwatchClock(true));
2727+ Source = source ?? new StopwatchClock(true);
2828+2929+ ChangeSource(Source);
2630 }
27312832 public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
···39434044 public double ElapsedFrameTime => CurrentTime - LastFrameTime;
41454242- public bool IsRunning => Source?.IsRunning ?? false;
4646+ public bool IsRunning => Source.IsRunning;
43474448 private readonly bool processSource;
4549
+2
osu.Framework/Timing/FramedOffsetClock.cs
···11// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22// See the LICENCE file in the repository root for full licence text.
3344+#nullable enable
55+46namespace osu.Framework.Timing
57{
68 /// <summary>
+8-6
osu.Framework/Timing/InterpolatingFramedClock.cs
···11// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22// See the LICENCE file in the repository root for full licence text.
3344+#nullable enable
55+46using System;
5768namespace osu.Framework.Timing
···1315 {
1416 private readonly FramedClock clock = new FramedClock(new StopwatchClock(true));
15171616- public IClock Source { get; private set; }
1818+ public IClock? Source { get; private set; }
17191818- protected IFrameBasedClock FramedSourceClock;
2020+ protected IFrameBasedClock? FramedSourceClock;
1921 protected double LastInterpolatedTime;
2022 protected double CurrentInterpolatedTime;
2123···23252426 public double FramesPerSecond => 0;
25272626- public virtual void ChangeSource(IClock source)
2828+ public virtual void ChangeSource(IClock? source)
2729 {
2830 if (source != null)
2931 {
···3537 CurrentInterpolatedTime = 0;
3638 }
37393838- public InterpolatingFramedClock(IClock source = null)
4040+ public InterpolatingFramedClock(IClock? source = null)
3941 {
4042 ChangeSource(source);
4143 }
···53555456 public virtual double Rate
5557 {
5656- get => FramedSourceClock.Rate;
5858+ get => FramedSourceClock?.Rate ?? 1;
5759 set => throw new NotSupportedException();
5860 }
59616062 public virtual bool IsRunning => sourceIsRunning;
61636262- public virtual double Drift => CurrentTime - FramedSourceClock.CurrentTime;
6464+ public virtual double Drift => CurrentTime - (FramedSourceClock?.CurrentTime ?? 0);
63656466 public virtual double ElapsedFrameTime => CurrentInterpolatedTime - LastInterpolatedTime;
6567
+2
osu.Framework/Timing/ThrottledFrameClock.cs
···11// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22// See the LICENCE file in the repository root for full licence text.
3344+#nullable enable
55+46using System;
57using System.Diagnostics;
68using System.Threading;
-1
osu.Framework/Utils/Interpolation.cs
···2626 /// <param name="final">The end value.</param>
2727 /// <param name="base">The base of the exponential. The valid range is [0, 1], where smaller values mean that the final value is achieved more quickly, and values closer to 1 results in slow convergence to the final value.</param>
2828 /// <param name="exponent">The exponent of the exponential. An exponent of 0 results in the start values, whereas larger exponents make the result converge to the final value.</param>
2929- /// <returns></returns>
3029 public static double Damp(double start, double final, double @base, double exponent)
3130 {
3231 if (@base < 0 || @base > 1)
-1
osu.Framework/Utils/PathApproximator.cs
···314314 /// Computes various properties that can be used to approximate the circular arc.
315315 /// </summary>
316316 /// <param name="controlPoints">Three distinct points on the arc.</param>
317317- /// <returns></returns>
318317 private static CircularArcProperties circularArcProperties(ReadOnlySpan<Vector2> controlPoints)
319318 {
320319 Vector2 a = controlPoints[0];