···16161717 SetContentView(new AndroidGameView(this, CreateGame()));
1818 }
1919+2020+ protected override void OnPause() {
2121+ base.OnPause();
2222+ // Because Android is not playing nice with Background - we just kill it
2323+ System.Diagnostics.Process.GetCurrentProcess().Kill();
2424+ }
1925 }
2026}
···34343535 // test with test clock not elapsing
3636 double lastValue = interpolating.CurrentTime;
3737+3738 for (int i = 0; i < 100; i++)
3839 {
3940 interpolating.ProcessFrame();
···48494950 // test with test clock elapsing
5051 lastValue = interpolating.CurrentTime;
5252+5153 for (int i = 0; i < 100; i++)
5254 {
5355 source.CurrentTime += 50;
+1
osu.Framework.Tests/IO/TestDesktopStorage.cs
···1414 public void TestRelativePaths()
1515 {
1616 var guid = new Guid().ToString();
1717+1718 using (var storage = new TemporaryNativeStorage(guid))
1819 {
1920 var basePath = storage.GetFullPath(string.Empty);
+4
osu.Framework.Tests/IO/TestWebRequest.cs
···9696 List<long> startTimes = new List<long>();
97979898 List<Task> running = new List<Task>();
9999+99100 for (int i = 0; i < request_count; i++)
100101 {
101102 var request = new DelayedWebRequest
···354355 Assert.DoesNotThrow(request.Perform);
355356356357 var events = request.GetType().GetEvents(BindingFlags.Instance | BindingFlags.Public);
358358+357359 foreach (var e in events)
358360 {
359361 var field = request.GetType().GetField(e.Name, BindingFlags.Instance | BindingFlags.Public);
···368370 public void TestUnbindOnDispose([Values(true, false)] bool async)
369371 {
370372 WebRequest request;
373373+371374 using (request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
372375 {
373376 Method = HttpMethod.Get,
···383386 }
384387385388 var events = request.GetType().GetEvents(BindingFlags.Instance | BindingFlags.Public);
389389+386390 foreach (var e in events)
387391 {
388392 var field = request.GetType().GetField(e.Name, BindingFlags.Instance | BindingFlags.Public);
+1
osu.Framework.Tests/Lists/TestArrayExtensions.cs
···123123 public void TestNonSquareJaggedWithNullRowsToRectangular()
124124 {
125125 var jagged = new int[10][];
126126+126127 for (int i = 1; i < 10; i += 2)
127128 {
128129 if (i % 2 == 1)
+3
osu.Framework.Tests/Lists/TestWeakList.cs
···4848 var list = new WeakList<object> { obj, obj2, obj3 };
49495050 int count = 0;
5151+5152 foreach (var item in list)
5253 {
5354 if (count == 1)
···6869 var list = new WeakList<object> { obj, obj2, obj3 };
69707071 int count = 0;
7272+7173 foreach (var item in list)
7274 {
7375 if (count == 0)
···9092 GC.WaitForPendingFinalizers();
91939294 int index = 0;
9595+9396 foreach (var obj in list)
9497 {
9598 if (alive[index] != obj)
···55using osu.Framework.Graphics;
66using osu.Framework.Graphics.Containers;
77using osu.Framework.Graphics.Shapes;
88-using osu.Framework.Testing;
98using osuTK;
1091110namespace osu.Framework.Tests.Visual.Drawables
1211{
1313- public class TestSceneIsMaskedAway : TestScene
1212+ public class TestSceneIsMaskedAway : FrameworkTestScene
1413 {
1514 /// <summary>
1615 /// Tests that a box which is within the bounds of a parent is never masked away, regardless of whether the parent is masking or not.
···77using osu.Framework.Graphics.Containers;
88using osu.Framework.Graphics.Shapes;
99using osu.Framework.Graphics.Sprites;
1010-using osu.Framework.Testing;
1110using osuTK;
1211using osuTK.Graphics;
13121413namespace osu.Framework.Tests.Visual.Drawables
1514{
1615 [Description("Tests whether drawable updates occur before drawing.")]
1717- public class TestSceneUpdateBeforeDraw : TestScene
1616+ public class TestSceneUpdateBeforeDraw : FrameworkTestScene
1817 {
1918 /// <summary>
2019 /// Tests whether a <see cref="Drawable"/> is updated before being drawn when it is added to a parent
···66using osu.Framework.Graphics.Containers;
77using osu.Framework.Graphics.Shapes;
88using osu.Framework.MathUtils;
99-using osu.Framework.Testing;
109using osuTK;
11101211namespace osu.Framework.Tests.Visual.Layout
1312{
1413 [System.ComponentModel.Description("Rewinding of transforms that are important to layout.")]
1515- public class TestSceneLayoutTransformRewinding : TestScene
1414+ public class TestSceneLayoutTransformRewinding : FrameworkTestScene
1615 {
1716 private readonly ManualUpdateSubTreeContainer manualContainer;
1817
···7788namespace osu.Framework.Tests.Visual.Testing
99{
1010- public class TestSceneTest : TestScene
1010+ public class TestSceneTest : FrameworkTestScene
1111 {
1212 private int setupRun;
1313 private int setupStepsRun;
···108108 case PropertyInfo pi:
109109 action(((IBindable)pi.GetValue(targetShadowModel), (IBindable)pi.GetValue(target)));
110110 break;
111111+111112 case FieldInfo fi:
112113 action(((IBindable)fi.GetValue(targetShadowModel), (IBindable)fi.GetValue(target)));
113114 break;
+1
osu.Framework/Allocation/ObjectHandle.cs
···6767 try
6868 {
6969 var value = handle.Target;
7070+7071 if (value is T)
7172 {
7273 target = (T)value;
+2
osu.Framework/Allocation/ResolvedAttribute.cs
···7373 var activators = new List<Action<object, IReadOnlyDependencyContainer>>();
74747575 var properties = type.GetProperties(activator_flags).Where(f => f.GetCustomAttribute<ResolvedAttribute>() != null);
7676+7677 foreach (var property in properties)
7778 {
7879 if (!property.CanWrite)
···8586 var attribute = property.GetCustomAttribute<ResolvedAttribute>();
86878788 var cacheInfo = new CacheInfo(attribute.Name);
8989+8890 if (attribute.Parent != null)
8991 {
9092 // When a parent type exists, infer the property name if one is not provided
···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-using System;
55-using System.Collections.Generic;
66-using System.Linq;
74using osu.Framework.Bindables;
8596namespace osu.Framework.Audio
107{
1111- public class AdjustableAudioComponent : AudioComponent
88+ /// <summary>
99+ /// An audio component which allows for basic bindable adjustments to be applied.
1010+ /// </summary>
1111+ public class AdjustableAudioComponent : AudioComponent, IAggregateAudioAdjustment, IAdjustableAudioComponent
1212 {
1313- private readonly HashSet<BindableDouble> volumeAdjustments = new HashSet<BindableDouble>();
1414- private readonly HashSet<BindableDouble> balanceAdjustments = new HashSet<BindableDouble>();
1515- private readonly HashSet<BindableDouble> frequencyAdjustments = new HashSet<BindableDouble>();
1313+ private readonly AudioAdjustments adjustments = new AudioAdjustments();
16141715 /// <summary>
1818- /// Global volume of this component.
1616+ /// The volume of this component.
1917 /// </summary>
2020- public readonly BindableDouble Volume = new BindableDouble(1)
2121- {
2222- MinValue = 0,
2323- MaxValue = 1
2424- };
2525-2626- protected readonly BindableDouble VolumeCalculated = new BindableDouble(1)
2727- {
2828- MinValue = 0,
2929- MaxValue = 1
3030- };
1818+ public BindableDouble Volume => adjustments.Volume;
31193220 /// <summary>
3333- /// Playback balance of this sample (-1 .. 1 where 0 is centered)
2121+ /// The playback balance of this sample (-1 .. 1 where 0 is centered)
3422 /// </summary>
3535- public readonly BindableDouble Balance = new BindableDouble
3636- {
3737- MinValue = -1,
3838- MaxValue = 1
3939- };
4040-4141- protected readonly BindableDouble BalanceCalculated = new BindableDouble
4242- {
4343- MinValue = -1,
4444- MaxValue = 1
4545- };
2323+ public BindableDouble Balance => adjustments.Balance;
46244725 /// <summary>
4826 /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency.
4927 /// </summary>
5050- public readonly BindableDouble Frequency = new BindableDouble(1);
5151-5252- protected readonly BindableDouble FrequencyCalculated = new BindableDouble(1);
2828+ public BindableDouble Frequency => adjustments.Frequency;
53295430 protected AdjustableAudioComponent()
5531 {
5656- Volume.ValueChanged += e => InvalidateState(e.NewValue);
5757- Balance.ValueChanged += e => InvalidateState(e.NewValue);
5858- Frequency.ValueChanged += e => InvalidateState(e.NewValue);
3232+ AggregateVolume.ValueChanged += InvalidateState;
3333+ AggregateBalance.ValueChanged += InvalidateState;
3434+ AggregateFrequency.ValueChanged += InvalidateState;
5935 }
60366161- internal void InvalidateState(double newValue = 0) => EnqueueAction(OnStateChanged);
3737+ public void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable) =>
3838+ adjustments.AddAdjustment(type, adjustBindable);
62396363- internal virtual void OnStateChanged()
6464- {
6565- VolumeCalculated.Value = volumeAdjustments.Aggregate(Volume.Value, (current, adj) => current * adj.Value);
6666- BalanceCalculated.Value = balanceAdjustments.Aggregate(Balance.Value, (current, adj) => current + adj.Value);
6767- FrequencyCalculated.Value = frequencyAdjustments.Aggregate(Frequency.Value, (current, adj) => current * adj.Value);
6868- }
4040+ public void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable) =>
4141+ adjustments.RemoveAdjustment(type, adjustBindable);
69427070- public void AddAdjustmentDependency(AdjustableAudioComponent component)
7171- {
7272- AddAdjustment(AdjustableProperty.Balance, component.BalanceCalculated);
7373- AddAdjustment(AdjustableProperty.Frequency, component.FrequencyCalculated);
7474- AddAdjustment(AdjustableProperty.Volume, component.VolumeCalculated);
7575- }
4343+ internal void InvalidateState(ValueChangedEvent<double> valueChangedEvent = null) => EnqueueAction(OnStateChanged);
76447777- public void RemoveAdjustmentDependency(AdjustableAudioComponent component)
4545+ internal virtual void OnStateChanged()
7846 {
7979- RemoveAdjustment(AdjustableProperty.Balance, component.BalanceCalculated);
8080- RemoveAdjustment(AdjustableProperty.Frequency, component.FrequencyCalculated);
8181- RemoveAdjustment(AdjustableProperty.Volume, component.VolumeCalculated);
8247 }
83488484- public void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable) => EnqueueAction(() =>
8585- {
8686- switch (type)
8787- {
8888- case AdjustableProperty.Balance:
8989- if (balanceAdjustments.Contains(adjustBindable))
9090- throw new ArgumentException("An adjustable binding may only be registered once.");
4949+ /// <summary>
5050+ /// Bind all adjustments to another component's aggregated results.
5151+ /// </summary>
5252+ /// <param name="component">The other component (generally a direct parent).</param>
5353+ internal void BindAdjustments(IAggregateAudioAdjustment component) => adjustments.BindAdjustments(component);
91549292- balanceAdjustments.Add(adjustBindable);
9393- break;
9494- case AdjustableProperty.Frequency:
9595- if (frequencyAdjustments.Contains(adjustBindable))
9696- throw new ArgumentException("An adjustable binding may only be registered once.");
5555+ /// <summary>
5656+ /// Unbind all adjustments from another component's aggregated results.
5757+ /// </summary>
5858+ /// <param name="component">The other component (generally a direct parent).</param>
5959+ internal void UnbindAdjustments(IAggregateAudioAdjustment component) => adjustments.UnbindAdjustments(component);
97609898- frequencyAdjustments.Add(adjustBindable);
9999- break;
100100- case AdjustableProperty.Volume:
101101- if (volumeAdjustments.Contains(adjustBindable))
102102- throw new ArgumentException("An adjustable binding may only be registered once.");
6161+ public IBindable<double> AggregateVolume => adjustments.AggregateVolume;
10362104104- volumeAdjustments.Add(adjustBindable);
105105- break;
106106- }
6363+ public IBindable<double> AggregateBalance => adjustments.AggregateBalance;
10764108108- InvalidateState();
109109- });
110110-111111- public void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable) => EnqueueAction(() =>
112112- {
113113- switch (type)
114114- {
115115- case AdjustableProperty.Balance:
116116- balanceAdjustments.Remove(adjustBindable);
117117- break;
118118- case AdjustableProperty.Frequency:
119119- frequencyAdjustments.Remove(adjustBindable);
120120- break;
121121- case AdjustableProperty.Volume:
122122- volumeAdjustments.Remove(adjustBindable);
123123- break;
124124- }
125125-126126- InvalidateState();
127127- });
6565+ public IBindable<double> AggregateFrequency => adjustments.AggregateFrequency;
1286612967 protected override void Dispose(bool disposing)
13068 {
131131- volumeAdjustments.Clear();
132132- balanceAdjustments.Clear();
133133- frequencyAdjustments.Clear();
134134-13569 base.Dispose(disposing);
7070+7171+ AggregateVolume.UnbindAll();
7272+ AggregateBalance.UnbindAll();
7373+ AggregateFrequency.UnbindAll();
13674 }
13775 }
13876
+115
osu.Framework/Audio/AudioAdjustments.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.
33+44+using osu.Framework.Bindables;
55+66+namespace osu.Framework.Audio
77+{
88+ /// <summary>
99+ /// Provides adjustable and bindable attributes for an audio component.
1010+ /// Aggregates results as a <see cref="IAggregateAudioAdjustment"/>.
1111+ /// </summary>
1212+ public class AudioAdjustments : IAggregateAudioAdjustment, IAdjustableAudioComponent
1313+ {
1414+ /// <summary>
1515+ /// The volume of this component.
1616+ /// </summary>
1717+ public BindableDouble Volume { get; } = new BindableDouble(1)
1818+ {
1919+ MinValue = 0,
2020+ MaxValue = 1
2121+ };
2222+2323+ /// <summary>
2424+ /// The playback balance of this sample (-1 .. 1 where 0 is centered)
2525+ /// </summary>
2626+ public BindableDouble Balance { get; } = new BindableDouble
2727+ {
2828+ MinValue = -1,
2929+ MaxValue = 1
3030+ };
3131+3232+ /// <summary>
3333+ /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency.
3434+ /// </summary>
3535+ public BindableDouble Frequency { get; } = new BindableDouble(1);
3636+3737+ public IBindable<double> AggregateVolume => volumeAggregate.Result;
3838+ public IBindable<double> AggregateBalance => balanceAggregate.Result;
3939+ public IBindable<double> AggregateFrequency => frequencyAggregate.Result;
4040+4141+ private readonly AggregateBindable<double> volumeAggregate;
4242+ private readonly AggregateBindable<double> balanceAggregate;
4343+ private readonly AggregateBindable<double> frequencyAggregate;
4444+4545+ public AudioAdjustments()
4646+ {
4747+ volumeAggregate = new AggregateBindable<double>((a, b) => a * b, Volume.GetUnboundCopy());
4848+ volumeAggregate.AddSource(Volume);
4949+5050+ balanceAggregate = new AggregateBindable<double>((a, b) => a + b, Balance.GetUnboundCopy());
5151+ balanceAggregate.AddSource(Balance);
5252+5353+ frequencyAggregate = new AggregateBindable<double>((a, b) => a * b, Frequency.GetUnboundCopy());
5454+ frequencyAggregate.AddSource(Frequency);
5555+ }
5656+5757+ public void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable)
5858+ {
5959+ switch (type)
6060+ {
6161+ case AdjustableProperty.Balance:
6262+ balanceAggregate.AddSource(adjustBindable);
6363+ break;
6464+6565+ case AdjustableProperty.Frequency:
6666+ frequencyAggregate.AddSource(adjustBindable);
6767+ break;
6868+6969+ case AdjustableProperty.Volume:
7070+ volumeAggregate.AddSource(adjustBindable);
7171+ break;
7272+ }
7373+ }
7474+7575+ public void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable)
7676+ {
7777+ switch (type)
7878+ {
7979+ case AdjustableProperty.Balance:
8080+ balanceAggregate.RemoveSource(adjustBindable);
8181+ break;
8282+8383+ case AdjustableProperty.Frequency:
8484+ frequencyAggregate.RemoveSource(adjustBindable);
8585+ break;
8686+8787+ case AdjustableProperty.Volume:
8888+ volumeAggregate.RemoveSource(adjustBindable);
8989+ break;
9090+ }
9191+ }
9292+9393+ /// <summary>
9494+ /// Bind all adjustments from an <see cref="IAggregateAudioAdjustment"/>.
9595+ /// </summary>
9696+ /// <param name="component">The adjustment source.</param>
9797+ internal void BindAdjustments(IAggregateAudioAdjustment component)
9898+ {
9999+ volumeAggregate.AddSource(component.AggregateVolume);
100100+ balanceAggregate.AddSource(component.AggregateBalance);
101101+ frequencyAggregate.AddSource(component.AggregateFrequency);
102102+ }
103103+104104+ /// <summary>
105105+ /// Unbind all adjustments from an <see cref="IAggregateAudioAdjustment"/>.
106106+ /// </summary>
107107+ /// <param name="component">The adjustment source.</param>
108108+ internal void UnbindAdjustments(IAggregateAudioAdjustment component)
109109+ {
110110+ volumeAggregate.RemoveSource(component.AggregateVolume);
111111+ balanceAggregate.RemoveSource(component.AggregateBalance);
112112+ frequencyAggregate.RemoveSource(component.AggregateFrequency);
113113+ }
114114+ }
115115+}
+3-25
osu.Framework/Audio/AudioCollectionManager.cs
···99 /// <summary>
1010 /// A collection of audio components which need central property control.
1111 /// </summary>
1212- public class AudioCollectionManager<T> : AdjustableAudioComponent
1212+ public class AudioCollectionManager<T> : AdjustableAudioComponent, IBassAudio
1313 where T : AdjustableAudioComponent
1414 {
1515- protected List<T> Items = new List<T>();
1515+ internal List<T> Items = new List<T>();
16161717 public void AddItem(T item)
1818 {
1919- RegisterItem(item);
2020- AddItemToList(item);
2121- }
2222-2323- public void AddItemToList(T item)
2424- {
2519 EnqueueAction(delegate
2620 {
2721 if (Items.Contains(item)) return;
28222323+ item.BindAdjustments(this);
2924 Items.Add(item);
3025 });
3131- }
3232-3333- public void RegisterItem(T item)
3434- {
3535- EnqueueAction(() => item.AddAdjustmentDependency(this));
3636- }
3737-3838- public void UnregisterItem(T item)
3939- {
4040- EnqueueAction(() => item.RemoveAdjustmentDependency(this));
4141- }
4242-4343- internal override void OnStateChanged()
4444- {
4545- base.OnStateChanged();
4646- foreach (var item in Items)
4747- item.OnStateChanged();
4826 }
49275028 public virtual void UpdateDevice(int deviceIndex)
+26-12
osu.Framework/Audio/AudioComponent.cs
···991010namespace osu.Framework.Audio
1111{
1212- public class AudioComponent : IDisposable, IUpdateable
1212+ /// <summary>
1313+ /// A base class for audio components which offers audio thread deferring, disposal and basic update logic.
1414+ /// </summary>
1515+ public abstract class AudioComponent : IDisposable, IUpdateable
1316 {
1417 /// <summary>
1518 /// Audio operations will be run on a separate dedicated thread, so we need to schedule any audio API calls using this queue.
1619 /// </summary>
1720 protected ConcurrentQueue<Task> PendingActions = new ConcurrentQueue<Task>();
18212222+ private bool acceptingActions = true;
2323+2424+ /// <summary>
2525+ /// Enqueues an action to be performed on the audio thread.
2626+ /// </summary>
2727+ /// <param name="action">The action to perform.</param>
2828+ /// <returns>A task which can be used for continuation logic. May return a <see cref="Task.CompletedTask"/> if called while already on the audio thread.</returns>
1929 protected Task EnqueueAction(Action action)
2030 {
2131 if (ThreadSafety.IsAudioThread)
···3141 var task = new Task(action);
3242 PendingActions.Enqueue(task);
3343 return task;
3434- }
3535-3636- private bool acceptingActions = true;
3737-3838- ~AudioComponent()
3939- {
4040- Dispose(false);
4144 }
42454346 /// <summary>
4444- /// Run each loop of the audio thread after queued actions to allow components to update anything they need to.
4747+ /// Run each loop of the audio thread's execution after queued actions are completed to allow components to perform any additional operations.
4548 /// </summary>
4649 protected virtual void UpdateState()
4750 {
4851 }
49525353+ /// <summary>
5454+ /// Run each loop of the audio thread's execution, after <see cref="UpdateState"/> as a way to update any child components.
5555+ /// </summary>
5056 protected virtual void UpdateChildren()
5157 {
5258 }
···7379 }
74807581 /// <summary>
7676- /// This component has completed playback and is now in a stopped state.
8282+ /// Whether this component has completed playback and is in a stopped state.
7783 /// </summary>
7884 public virtual bool HasCompleted => !IsAlive;
79858086 /// <summary>
8181- /// This component has completed all processing and is ready to be removed from its parent.
8787+ /// When false, this component has completed all processing and is ready to be removed from its parent.
8288 /// </summary>
8389 public virtual bool IsAlive => !IsDisposed;
84909191+ /// <summary>
9292+ /// Whether this component has finished loading its resources.
9393+ /// </summary>
8594 public virtual bool IsLoaded => true;
86958796 #region IDisposable Support
88978989- protected volatile bool IsDisposed; // To detect redundant calls
9898+ ~AudioComponent()
9999+ {
100100+ Dispose(false);
101101+ }
102102+103103+ protected volatile bool IsDisposed;
9010491105 protected virtual void Dispose(bool disposing)
92106 {
+38-33
osu.Framework/Audio/AudioManager.cs
···2121 /// <summary>
2222 /// The manager component responsible for audio tracks (e.g. songs).
2323 /// </summary>
2424- public TrackManager Track => GetTrackManager();
2424+ public ITrackStore Tracks => globalTrackStore.Value;
25252626 /// <summary>
2727 /// The manager component responsible for audio samples (e.g. sound effects).
2828 /// </summary>
2929- public SampleManager Sample => GetSampleManager();
2929+ public ISampleStore Samples => globalSampleStore.Value;
30303131 /// <summary>
3232 /// The thread audio operations (mainly Bass calls) are ran on.
···8686 /// </summary>
8787 public Scheduler EventScheduler;
88888989- private readonly Lazy<TrackManager> globalTrackManager;
9090- private readonly Lazy<SampleManager> globalSampleManager;
8989+ private readonly Lazy<TrackStore> globalTrackStore;
9090+ private readonly Lazy<SampleStore> globalSampleStore;
91919292 /// <summary>
9393- /// Constructs an AudioManager given a track resource store, and a sample resource store.
9393+ /// Constructs an AudioStore given a track resource store, and a sample resource store.
9494 /// </summary>
9595 /// <param name="audioThread">The host's audio thread.</param>
9696 /// <param name="trackStore">The resource store containing all audio tracks to be used in the future.</param>
···108108 sampleStore.AddExtension(@"wav");
109109 sampleStore.AddExtension(@"mp3");
110110111111- globalTrackManager = new Lazy<TrackManager>(() => GetTrackManager(trackStore));
112112- globalSampleManager = new Lazy<SampleManager>(() => GetSampleManager(sampleStore));
111111+ globalTrackStore = new Lazy<TrackStore>(() =>
112112+ {
113113+ var store = new TrackStore(trackStore);
114114+ AddItem(store);
115115+ store.AddAdjustment(AdjustableProperty.Volume, VolumeTrack);
116116+ return store;
117117+ });
118118+119119+ globalSampleStore = new Lazy<SampleStore>(() =>
120120+ {
121121+ var store = new SampleStore(sampleStore);
122122+ AddItem(store);
123123+ store.AddAdjustment(AdjustableProperty.Volume, VolumeSample);
124124+ return store;
125125+ });
113126114127 scheduler.Add(() =>
115128 {
···155168 private IEnumerable<string> getDeviceNames(List<DeviceInfo> devices) => devices.Skip(1).Select(d => d.Name);
156169157170 /// <summary>
158158- /// Obtains the <see cref="TrackManager"/> corresponding to a given resource store.
159159- /// Returns the global <see cref="TrackManager"/> if no resource store is passed.
171171+ /// Obtains the <see cref="TrackStore"/> corresponding to a given resource store.
172172+ /// Returns the global <see cref="TrackStore"/> if no resource store is passed.
160173 /// </summary>
161161- /// <param name="store">The <see cref="IResourceStore{T}"/> of which to retrieve the <see cref="TrackManager"/>.</param>
162162- public TrackManager GetTrackManager(IResourceStore<byte[]> store = null)
174174+ /// <param name="store">The <see cref="IResourceStore{T}"/> of which to retrieve the <see cref="TrackStore"/>.</param>
175175+ public ITrackStore GetTrackStore(IResourceStore<byte[]> store = null)
163176 {
164164- if (store == null) return globalTrackManager.Value;
177177+ if (store == null) return globalTrackStore.Value;
165178166166- TrackManager tm = new TrackManager(store);
167167- AddItem(tm);
168168- tm.AddAdjustment(AdjustableProperty.Volume, VolumeTrack);
169169- VolumeTrack.ValueChanged += e => tm.InvalidateState(e.NewValue);
170170-179179+ TrackStore tm = new TrackStore(store);
180180+ globalTrackStore.Value.AddItem(tm);
171181 return tm;
172182 }
173183174184 /// <summary>
175175- /// Obtains the <see cref="SampleManager"/> corresponding to a given resource store.
176176- /// Returns the global <see cref="SampleManager"/> if no resource store is passed.
185185+ /// Obtains the <see cref="SampleStore"/> corresponding to a given resource store.
186186+ /// Returns the global <see cref="SampleStore"/> if no resource store is passed.
177187 /// </summary>
178178- /// <param name="store">The <see cref="IResourceStore{T}"/> of which to retrieve the <see cref="SampleManager"/>.</param>
179179- public SampleManager GetSampleManager(IResourceStore<byte[]> store = null)
188188+ /// <param name="store">The <see cref="IResourceStore{T}"/> of which to retrieve the <see cref="SampleStore"/>.</param>
189189+ public ISampleStore GetSampleStore(IResourceStore<byte[]> store = null)
180190 {
181181- if (store == null) return globalSampleManager.Value;
191191+ if (store == null) return globalSampleStore.Value;
182192183183- SampleManager sm = new SampleManager(store);
184184- AddItem(sm);
185185- sm.AddAdjustment(AdjustableProperty.Volume, VolumeSample);
186186- VolumeSample.ValueChanged += e => sm.InvalidateState(e.NewValue);
187187-193193+ SampleStore sm = new SampleStore(store);
194194+ globalSampleStore.Value.AddItem(sm);
188195 return sm;
189196 }
190197···209216 newDevice = audioDevices.Find(df => df.IsDefault).Name;
210217211218 bool oldDeviceValid = Bass.CurrentDevice >= 0;
219219+212220 if (oldDeviceValid)
213221 {
214222 DeviceInfo oldDeviceInfo = Bass.GetDeviceInfo(Bass.CurrentDevice);
···289297 return true;
290298 }
291299292292- public override void UpdateDevice(int deviceIndex)
293293- {
294294- Sample.UpdateDevice(deviceIndex);
295295- Track.UpdateDevice(deviceIndex);
296296- }
297297-298300 private void updateAvailableAudioDevices()
299301 {
300302 var currentDeviceList = getAllDevices().Where(d => d.IsEnabled).ToList();
···326328 {
327329 // use default device
328330 var device = Bass.GetDeviceInfo(Bass.CurrentDevice);
331331+329332 if (!device.IsDefault && !setAudioDevice())
330333 {
331334 if (!device.IsEnabled || !setAudioDevice(device.Name))
···345348 {
346349 // use whatever is the preferred device
347350 var device = Bass.GetDeviceInfo(Bass.CurrentDevice);
351351+348352 if (device.Name == AudioDevice.Value)
349353 {
350354 if (!device.IsEnabled && !setAudioDevice())
···362366 else
363367 {
364368 var preferredDevice = getAllDevices().SingleOrDefault(d => d.Name == AudioDevice.Value);
369369+365370 if (preferredDevice.Name == AudioDevice.Value && preferredDevice.IsEnabled)
366371 setAudioDevice(preferredDevice.Name);
367372 else if (!device.IsEnabled && !setAudioDevice())
+39
osu.Framework/Audio/IAdjustableAudioComponent.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.
33+44+using osu.Framework.Bindables;
55+66+namespace osu.Framework.Audio
77+{
88+ public interface IAdjustableAudioComponent
99+ {
1010+ /// <summary>
1111+ /// The volume of this component.
1212+ /// </summary>
1313+ BindableDouble Volume { get; }
1414+1515+ /// <summary>
1616+ /// The playback balance of this sample (-1 .. 1 where 0 is centered)
1717+ /// </summary>
1818+ BindableDouble Balance { get; }
1919+2020+ /// <summary>
2121+ /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency.
2222+ /// </summary>
2323+ BindableDouble Frequency { get; }
2424+2525+ /// <summary>
2626+ /// Add a bindable adjustment source.
2727+ /// </summary>
2828+ /// <param name="type">The target type for this adjustment.</param>
2929+ /// <param name="adjustBindable">The bindable adjustment.</param>
3030+ void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable);
3131+3232+ /// <summary>
3333+ /// Remove a bindable adjustment source.
3434+ /// </summary>
3535+ /// <param name="type">The target type for this adjustment.</param>
3636+ /// <param name="adjustBindable">The bindable adjustment.</param>
3737+ void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable);
3838+ }
3939+}
+12
osu.Framework/Audio/IAdjustableResourceStore.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.
33+44+using osu.Framework.IO.Stores;
55+66+namespace osu.Framework.Audio
77+{
88+ public interface IAdjustableResourceStore<T> : IResourceStore<T>, IAdjustableAudioComponent
99+ where T : AudioComponent
1010+ {
1111+ }
1212+}
+28
osu.Framework/Audio/IAggregateAudioAdjustment.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.
33+44+using osu.Framework.Bindables;
55+66+namespace osu.Framework.Audio
77+{
88+ /// <summary>
99+ /// Provides aggregated adjustments for an audio component.
1010+ /// </summary>
1111+ public interface IAggregateAudioAdjustment
1212+ {
1313+ /// <summary>
1414+ /// The aggregate volume of this component (0..1).
1515+ /// </summary>
1616+ IBindable<double> AggregateVolume { get; }
1717+1818+ /// <summary>
1919+ /// The aggregate playback balance of this sample (-1 .. 1 where 0 is centered)
2020+ /// </summary>
2121+ IBindable<double> AggregateBalance { get; }
2222+2323+ /// <summary>
2424+ /// The aggregate rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency.
2525+ /// </summary>
2626+ IBindable<double> AggregateFrequency { get; }
2727+ }
2828+}
+32
osu.Framework/Audio/Sample/ISampleChannel.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.
33+44+namespace osu.Framework.Audio.Sample
55+{
66+ /// <summary>
77+ /// A channel playing back an audio sample.
88+ /// </summary>
99+ public interface ISampleChannel
1010+ {
1111+ /// <summary>
1212+ /// Start playback.
1313+ /// </summary>
1414+ /// <param name="restart">Whether to restart the sample from the beginning.</param>
1515+ void Play(bool restart = true);
1616+1717+ /// <summary>
1818+ /// Stop playback.
1919+ /// </summary>
2020+ void Stop();
2121+2222+ /// <summary>
2323+ /// Whether the sample is playing.
2424+ /// </summary>
2525+ bool Playing { get; }
2626+2727+ /// <summary>
2828+ /// Whether the sample has finished playback.
2929+ /// </summary>
3030+ bool Played { get; }
3131+ }
3232+}
+2-2
osu.Framework/Audio/Sample/SampleBass.cs
···991010namespace osu.Framework.Audio.Sample
1111{
1212- internal class SampleBass : Sample, IBassAudio
1212+ internal sealed class SampleBass : Sample, IBassAudio
1313 {
1414 private volatile int sampleId;
15151616 public override bool IsLoaded => sampleId != 0;
17171818- public SampleBass(byte[] data, ConcurrentQueue<Task> customPendingActions = null, int concurrency = DEFAULT_CONCURRENCY)
1818+ internal SampleBass(byte[] data, ConcurrentQueue<Task> customPendingActions = null, int concurrency = DEFAULT_CONCURRENCY)
1919 : base(concurrency)
2020 {
2121 if (customPendingActions != null)
+1-1
osu.Framework/Audio/Sample/SampleChannel.cs
···6677namespace osu.Framework.Audio.Sample
88{
99- public abstract class SampleChannel : AdjustableAudioComponent
99+ public abstract class SampleChannel : AdjustableAudioComponent, ISampleChannel
1010 {
1111 protected bool WasStarted;
1212
···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+using System;
45using System.Collections.Concurrent;
66+using System.Collections.Generic;
57using System.IO;
68using osu.Framework.IO.Stores;
79using osu.Framework.Statistics;
810using System.Linq;
911using System.Threading.Tasks;
1212+using osu.Framework.Audio.Track;
10131114namespace osu.Framework.Audio.Sample
1215{
1313- public class SampleManager : AudioCollectionManager<SampleChannel>, IResourceStore<SampleChannel>
1616+ internal class SampleStore : AudioCollectionManager<AdjustableAudioComponent>, ISampleStore
1417 {
1518 private readonly IResourceStore<byte[]> store;
1619···2124 /// </summary>
2225 public int PlaybackConcurrency { get; set; } = Sample.DEFAULT_CONCURRENCY;
23262424- public SampleManager(IResourceStore<byte[]> store)
2727+ internal SampleStore(IResourceStore<byte[]> store)
2528 {
2629 this.store = store;
2730 }
28312932 public SampleChannel Get(string name)
3033 {
3434+ if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(SampleStore)}");
3535+3136 if (string.IsNullOrEmpty(name)) return null;
32373338 lock (sampleCache)
3439 {
3540 SampleChannel channel = null;
4141+3642 if (!sampleCache.TryGetValue(name, out Sample sample))
3743 {
3844 byte[] data = store.Get(name);
···41474248 if (sample != null)
4349 {
4444- channel = new SampleChannelBass(sample, AddItemToList);
4545- RegisterItem(channel);
5050+ channel = new SampleChannelBass(sample, AddItem);
4651 }
47524853 return channel;
···6671 }
67726873 public Stream GetStream(string name) => store.GetStream(name);
7474+7575+ public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
6976 }
7077}
+11
osu.Framework/Audio/Track/ISampleStore.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.
33+44+using osu.Framework.Audio.Sample;
55+66+namespace osu.Framework.Audio.Track
77+{
88+ public interface ISampleStore : IAdjustableResourceStore<SampleChannel>
99+ {
1010+ }
1111+}
+62
osu.Framework/Audio/Track/ITrack.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.
33+44+namespace osu.Framework.Audio.Track
55+{
66+ /// <summary>
77+ /// An audio track.
88+ /// </summary>
99+ public interface ITrack
1010+ {
1111+ /// <summary>
1212+ /// States if this track should repeat.
1313+ /// </summary>
1414+ bool Looping { get; set; }
1515+1616+ /// <summary>
1717+ /// Point in time in milliseconds to restart the track to on loop or <see cref="Restart"/>.
1818+ /// </summary>
1919+ double RestartPoint { get; set; }
2020+2121+ /// <summary>
2222+ /// Current position in milliseconds.
2323+ /// </summary>
2424+ double CurrentTime { get; }
2525+2626+ /// <summary>
2727+ /// Length of the track in milliseconds.
2828+ /// </summary>
2929+ double Length { get; set; }
3030+3131+ bool IsRunning { get; }
3232+3333+ /// <summary>
3434+ /// Reset this track to a logical default state.
3535+ /// </summary>
3636+ void Reset();
3737+3838+ /// <summary>
3939+ /// Restarts this track from the <see cref="Track.RestartPoint"/> while retaining adjustments.
4040+ /// </summary>
4141+ void Restart();
4242+4343+ void ResetSpeedAdjustments();
4444+4545+ /// <summary>
4646+ /// Seek to a new position.
4747+ /// </summary>
4848+ /// <param name="seek">New position in milliseconds</param>
4949+ /// <returns>Whether the seek was successful.</returns>
5050+ bool Seek(double seek);
5151+5252+ /// <summary>
5353+ /// Start playback.
5454+ /// </summary>
5555+ void Start();
5656+5757+ /// <summary>
5858+ /// Stop playback.
5959+ /// </summary>
6060+ void Stop();
6161+ }
6262+}
+17
osu.Framework/Audio/Track/ITrackStore.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.
33+44+using System;
55+66+namespace osu.Framework.Audio.Track
77+{
88+ public interface ITrackStore : IAdjustableResourceStore<Track>
99+ {
1010+ /// <summary>
1111+ /// Retrieve a <see cref="TrackVirtual"/> with no audio device backing.
1212+ /// </summary>
1313+ /// <param name="length">The length of the virtual track.</param>
1414+ /// <returns>A new virtual track.</returns>
1515+ Track GetVirtual(double length = Double.PositiveInfinity);
1616+ }
1717+}
+2-2
osu.Framework/Audio/Track/Track.cs
···8899namespace osu.Framework.Audio.Track
1010{
1111- public abstract class Track : AdjustableAudioComponent, IAdjustableClock, IHasTempoAdjust
1111+ public abstract class Track : AdjustableAudioComponent, IAdjustableClock, IHasTempoAdjust, ITrack
1212 {
1313 public event Action Completed;
1414 public event Action Failed;
···38383939 protected Track()
4040 {
4141- Tempo.ValueChanged += e => InvalidateState(e.NewValue);
4141+ Tempo.ValueChanged += InvalidateState;
4242 }
43434444 /// <summary>
+16-6
osu.Framework/Audio/Track/TrackBass.cs
···14141515namespace osu.Framework.Audio.Track
1616{
1717- public class TrackBass : Track, IBassAudio, IHasPitchAdjust
1717+ public sealed class TrackBass : Track, IBassAudio, IHasPitchAdjust
1818 {
1919+ public const int BYTES_PER_SAMPLE = 4;
2020+1921 private AsyncBufferStream dataStream;
20222123 /// <summary>
···3941 private bool isPlayed;
40424143 private long byteLength;
4444+4545+ /// <summary>
4646+ /// The last position that a seek will succeed for.
4747+ /// </summary>
4848+ private double lastSeekablePosition;
42494350 private FileCallbacks fileCallbacks;
4451 private SyncCallback stopCallback;
···96103 if (success)
97104 {
98105 Length = seconds * 1000;
106106+107107+ // Bass does not allow seeking to the end of the track, so the last available position is 1 sample before.
108108+ lastSeekablePosition = Bass.ChannelBytes2Seconds(activeStream, byteLength - BYTES_PER_SAMPLE) * 1000;
99109100110 Bass.ChannelGetAttribute(activeStream, ChannelAttribute.Frequency, out float frequency);
101111 initialFrequency = frequency;
···227237 {
228238 // At this point the track may not yet be loaded which is indicated by a 0 length.
229239 // In that case we still want to return true, hence the conservative length.
230230- double conservativeLength = Length == 0 ? double.MaxValue : Length;
240240+ double conservativeLength = Length == 0 ? double.MaxValue : lastSeekablePosition;
231241 double conservativeClamped = MathHelper.Clamp(seek, 0, conservativeLength);
232242233243 await EnqueueAction(() =>
···255265 {
256266 base.OnStateChanged();
257267258258- setDirection(FrequencyCalculated.Value < 0);
268268+ setDirection(AggregateFrequency.Value < 0);
259269260260- Bass.ChannelSetAttribute(activeStream, ChannelAttribute.Volume, VolumeCalculated.Value);
261261- Bass.ChannelSetAttribute(activeStream, ChannelAttribute.Pan, BalanceCalculated.Value);
270270+ Bass.ChannelSetAttribute(activeStream, ChannelAttribute.Volume, AggregateVolume.Value);
271271+ Bass.ChannelSetAttribute(activeStream, ChannelAttribute.Pan, AggregateBalance.Value);
262272 Bass.ChannelSetAttribute(activeStream, ChannelAttribute.Frequency, bassFreq);
263273 Bass.ChannelSetAttribute(tempoAdjustStream, ChannelAttribute.Tempo, (Math.Abs(Tempo.Value) - 1) * 100);
264274 }
265275266276 private volatile float initialFrequency;
267277268268- private int bassFreq => (int)MathHelper.Clamp(Math.Abs(initialFrequency * FrequencyCalculated.Value), 100, 100000);
278278+ private int bassFreq => (int)MathHelper.Clamp(Math.Abs(initialFrequency * AggregateFrequency.Value), 100, 100000);
269279270280 private volatile int bitrate;
271281
-31
osu.Framework/Audio/Track/TrackManager.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.
33-44-using osu.Framework.IO.Stores;
55-66-namespace osu.Framework.Audio.Track
77-{
88- public class TrackManager : AudioCollectionManager<Track>
99- {
1010- private readonly IResourceStore<byte[]> store;
1111-1212- public TrackManager(IResourceStore<byte[]> store)
1313- {
1414- this.store = store;
1515- }
1616-1717- public Track Get(string name)
1818- {
1919- if (string.IsNullOrEmpty(name)) return null;
2020-2121- var dataStream = store.GetStream(name);
2222-2323- if (dataStream == null)
2424- return null;
2525-2626- Track track = new TrackBass(dataStream);
2727- AddItem(track);
2828- return track;
2929- }
3030- }
3131-}
+52
osu.Framework/Audio/Track/TrackStore.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.
33+44+using System;
55+using System.Collections.Generic;
66+using System.IO;
77+using System.Threading.Tasks;
88+using osu.Framework.IO.Stores;
99+1010+namespace osu.Framework.Audio.Track
1111+{
1212+ internal class TrackStore : AudioCollectionManager<AdjustableAudioComponent>, ITrackStore
1313+ {
1414+ private readonly IResourceStore<byte[]> store;
1515+1616+ internal TrackStore(IResourceStore<byte[]> store)
1717+ {
1818+ this.store = store;
1919+ }
2020+2121+ public Track GetVirtual(double length = double.PositiveInfinity)
2222+ {
2323+ if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(TrackStore)}");
2424+2525+ var track = new TrackVirtual(length);
2626+ AddItem(track);
2727+ return track;
2828+ }
2929+3030+ public Track Get(string name)
3131+ {
3232+ if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(TrackStore)}");
3333+3434+ if (string.IsNullOrEmpty(name)) return null;
3535+3636+ var dataStream = store.GetStream(name);
3737+3838+ if (dataStream == null)
3939+ return null;
4040+4141+ Track track = new TrackBass(dataStream);
4242+ AddItem(track);
4343+ return track;
4444+ }
4545+4646+ public Task<Track> GetAsync(string name) => Task.Run(() => Get(name));
4747+4848+ public Stream GetStream(string name) => store.GetStream(name);
4949+5050+ public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
5151+ }
5252+}
+4-4
osu.Framework/Audio/Track/TrackVirtual.cs
···6677namespace osu.Framework.Audio.Track
88{
99- public class TrackVirtual : Track
99+ public sealed class TrackVirtual : Track
1010 {
1111 private readonly StopwatchClock clock = new StopwatchClock();
12121313 private double seekOffset;
14141515- public TrackVirtual()
1515+ public TrackVirtual(double length)
1616 {
1717- Length = double.PositiveInfinity;
1717+ Length = length;
1818 }
19192020 public override bool Seek(double seek)
···8989 base.OnStateChanged();
90909191 lock (clock)
9292- clock.Rate = Tempo.Value;
9292+ clock.Rate = Tempo.Value * AggregateFrequency.Value;
9393 }
9494 }
9595}
+42-41
osu.Framework/Audio/Track/Waveform.cs
···1919 public class Waveform : IDisposable
2020 {
2121 /// <summary>
2222- /// <see cref="WaveformPoint"/>s are initially generated to a 1ms resolution to cover most use cases.
2222+ /// <see cref="Point"/>s are initially generated to a 1ms resolution to cover most use cases.
2323 /// </summary>
2424 private const float resolution = 0.001f;
2525···2727 /// The data stream is iteratively decoded to provide this many points per iteration so as to not exceed BASS's internal buffer size.
2828 /// </summary>
2929 private const int points_per_iteration = 100000;
3030-3131- private const int bytes_per_sample = 4;
32303331 /// <summary>
3432 /// FFT1024 gives ~40hz accuracy.
···6159 private const double high_max = 12000;
62606361 private int channels;
6464- private List<WaveformPoint> points = new List<WaveformPoint>();
6262+ private List<Point> points = new List<Point>();
65636664 private readonly CancellationTokenSource cancelSource = new CancellationTokenSource();
6765 private readonly Task readTask;
···9391 // Each "point" is generated from a number of samples, each sample contains a number of channels
9492 int samplesPerPoint = (int)(info.Frequency * resolution * info.Channels);
95939696- int bytesPerPoint = samplesPerPoint * bytes_per_sample;
9494+ int bytesPerPoint = samplesPerPoint * TrackBass.BYTES_PER_SAMPLE;
97959896 points.Capacity = (int)(length / bytesPerPoint);
999710098 // Each iteration pulls in several samples
10199 int bytesPerIteration = bytesPerPoint * points_per_iteration;
102102- var sampleBuffer = new float[bytesPerIteration / bytes_per_sample];
100100+ var sampleBuffer = new float[bytesPerIteration / TrackBass.BYTES_PER_SAMPLE];
103101104102 // Read sample data
105103 while (length > 0)
106104 {
107105 length = Bass.ChannelGetData(decodeStream, sampleBuffer, bytesPerIteration);
108108- int samplesRead = (int)(length / bytes_per_sample);
106106+ int samplesRead = (int)(length / TrackBass.BYTES_PER_SAMPLE);
109107110108 // Each point is composed of multiple samples
111109 for (int i = 0; i < samplesRead; i += samplesPerPoint)
112110 {
113111 // Channels are interleaved in the sample data (data[0] -> channel0, data[1] -> channel1, data[2] -> channel0, etc)
114112 // samplesPerPoint assumes this interleaving behaviour
115115- var point = new WaveformPoint(info.Channels);
113113+ var point = new Point(info.Channels);
114114+116115 for (int j = i; j < i + samplesPerPoint; j += info.Channels)
117116 {
118117 // Find the maximum amplitude for each channel in the point
···135134 float[] bins = new float[fft_bins];
136135 int currentPoint = 0;
137136 long currentByte = 0;
137137+138138 while (length > 0)
139139 {
140140 length = Bass.ChannelGetData(decodeStream, bins, (int)fft_samples);
···190190191191 return await Task.Run(() =>
192192 {
193193- var generatedPoints = new List<WaveformPoint>();
193193+ var generatedPoints = new List<Point>();
194194 float pointsPerGeneratedPoint = (float)points.Count / pointCount;
195195196196 // Determines at which width (relative to the resolution) our smoothing filter is truncated.
···215215 int startIndex = (int)i - kernelWidth;
216216 int endIndex = (int)i + kernelWidth;
217217218218- var point = new WaveformPoint(channels);
218218+ var point = new Point(channels);
219219 float totalWeight = 0;
220220+220221 for (int j = startIndex; j < endIndex; j++)
221222 {
222223 if (j < 0 || j >= points.Count) continue;
···252253 /// <summary>
253254 /// Gets all the points represented by this <see cref="Waveform"/>.
254255 /// </summary>
255255- public List<WaveformPoint> GetPoints() => GetPointsAsync().Result;
256256+ public List<Point> GetPoints() => GetPointsAsync().Result;
256257257258 /// <summary>
258259 /// Gets all the points represented by this <see cref="Waveform"/>.
259260 /// </summary>
260260- public async Task<List<WaveformPoint>> GetPointsAsync()
261261+ public async Task<List<Point>> GetPointsAsync()
261262 {
262263 if (readTask == null)
263264 return points;
···267268 }
268269269270 /// <summary>
270270- /// Gets the number of channels represented by each <see cref="WaveformPoint"/>.
271271+ /// Gets the number of channels represented by each <see cref="Point"/>.
271272 /// </summary>
272273 public int GetChannels() => GetChannelsAsync().Result;
273274274275 /// <summary>
275275- /// Gets the number of channels represented by each <see cref="WaveformPoint"/>.
276276+ /// Gets the number of channels represented by each <see cref="Point"/>.
276277 /// </summary>
277278 public async Task<int> GetChannelsAsync()
278279 {
···314315 }
315316316317 #endregion
317317- }
318318319319- /// <summary>
320320- /// Represents a singular point of data in a <see cref="Waveform"/>.
321321- /// </summary>
322322- public class WaveformPoint
323323- {
324319 /// <summary>
325325- /// An array of amplitudes, one for each channel.
320320+ /// Represents a singular point of data in a <see cref="Waveform"/>.
326321 /// </summary>
327327- public readonly float[] Amplitude;
322322+ public class Point
323323+ {
324324+ /// <summary>
325325+ /// An array of amplitudes, one for each channel.
326326+ /// </summary>
327327+ public readonly float[] Amplitude;
328328329329- /// <summary>
330330- /// Unnormalised total intensity of the low-range (bass) frequencies.
331331- /// </summary>
332332- public double LowIntensity;
329329+ /// <summary>
330330+ /// Unnormalised total intensity of the low-range (bass) frequencies.
331331+ /// </summary>
332332+ public double LowIntensity;
333333334334- /// <summary>
335335- /// Unnormalised total intensity of the mid-range frequencies.
336336- /// </summary>
337337- public double MidIntensity;
334334+ /// <summary>
335335+ /// Unnormalised total intensity of the mid-range frequencies.
336336+ /// </summary>
337337+ public double MidIntensity;
338338339339- /// <summary>
340340- /// Unnormalised total intensity of the high-range (treble) frequencies.
341341- /// </summary>
342342- public double HighIntensity;
339339+ /// <summary>
340340+ /// Unnormalised total intensity of the high-range (treble) frequencies.
341341+ /// </summary>
342342+ public double HighIntensity;
343343344344- /// <summary>
345345- /// Cconstructs a <see cref="WaveformPoint"/>.
346346- /// </summary>
347347- /// <param name="channels">The number of channels that contain data.</param>
348348- public WaveformPoint(int channels)
349349- {
350350- Amplitude = new float[channels];
344344+ /// <summary>
345345+ /// Cconstructs a <see cref="Point"/>.
346346+ /// </summary>
347347+ /// <param name="channels">The number of channels that contain data.</param>
348348+ public Point(int channels)
349349+ {
350350+ Amplitude = new float[channels];
351351+ }
351352 }
352353 }
353354}
+96
osu.Framework/Bindables/AggregateBindable.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.
33+44+using System;
55+using System.Collections.Generic;
66+using System.Linq;
77+88+namespace osu.Framework.Bindables
99+{
1010+ /// <summary>
1111+ /// Combines multiple bindables into one aggregate bindable result.
1212+ /// </summary>
1313+ /// <typeparam name="T">The type of values.</typeparam>
1414+ public class AggregateBindable<T>
1515+ {
1616+ private readonly Func<T, T, T> aggregateFunction;
1717+1818+ /// <summary>
1919+ /// The final result after aggregating all added sources.
2020+ /// </summary>
2121+ public IBindable<T> Result => result;
2222+2323+ private readonly Bindable<T> result;
2424+2525+ private readonly T initialValue;
2626+2727+ /// <summary>
2828+ /// Create a new aggregate bindable.
2929+ /// </summary>
3030+ /// <param name="aggregateFunction">The function to be used for aggregation, taking two input <see cref="T"/> values and returning one output.</param>
3131+ /// <param name="resultBindable">An optional newly constructed bindable to use for <see cref="Result"/>. The initial value of this bindable is used as the initial value for the aggregate.</param>
3232+ public AggregateBindable(Func<T, T, T> aggregateFunction, Bindable<T> resultBindable = null)
3333+ {
3434+ this.aggregateFunction = aggregateFunction;
3535+ result = resultBindable ?? new Bindable<T>();
3636+ initialValue = result.Value;
3737+ }
3838+3939+ private readonly Dictionary<WeakReference, IBindable<T>> sourceMapping = new Dictionary<WeakReference, IBindable<T>>();
4040+4141+ /// <summary>
4242+ /// Add a new source to be included in aggregation.
4343+ /// </summary>
4444+ /// <param name="bindable">The bindable to add.</param>
4545+ public void AddSource(IBindable<T> bindable)
4646+ {
4747+ lock (sourceMapping)
4848+ {
4949+ if (findExistingWeak(bindable) != null)
5050+ return;
5151+5252+ var boundCopy = bindable.GetBoundCopy();
5353+ sourceMapping.Add(new WeakReference(bindable), boundCopy);
5454+ boundCopy.BindValueChanged(recalculateAggregate, true);
5555+ }
5656+ }
5757+5858+ /// <summary>
5959+ /// Remove a source from being included in aggregation.
6060+ /// </summary>
6161+ /// <param name="bindable">The bindable to remove.</param>
6262+ public void RemoveSource(IBindable<T> bindable)
6363+ {
6464+ lock (sourceMapping)
6565+ {
6666+ var weak = findExistingWeak(bindable);
6767+6868+ if (weak != null)
6969+ {
7070+ sourceMapping[weak].UnbindAll();
7171+ sourceMapping.Remove(weak);
7272+ }
7373+7474+ recalculateAggregate();
7575+ }
7676+ }
7777+7878+ private WeakReference findExistingWeak(IBindable<T> bindable) => sourceMapping.Keys.FirstOrDefault(k => k.Target == bindable);
7979+8080+ private void recalculateAggregate(ValueChangedEvent<T> obj = null)
8181+ {
8282+ T calculated = initialValue;
8383+8484+ lock (sourceMapping)
8585+ {
8686+ foreach (var dead in sourceMapping.Keys.Where(k => !k.IsAlive).ToArray())
8787+ sourceMapping.Remove(dead);
8888+8989+ foreach (var s in sourceMapping.Values)
9090+ calculated = aggregateFunction(calculated, s.Value);
9191+ }
9292+9393+ result.Value = calculated;
9494+ }
9595+ }
9696+}
+8-3
osu.Framework/Bindables/Bindable.cs
···209209 case T t:
210210 Value = t;
211211 break;
212212+212213 case string s:
213213- Value = typeof(T).IsEnum
214214- ? (T)Enum.Parse(typeof(T), s)
215215- : (T)Convert.ChangeType(s, typeof(T), CultureInfo.InvariantCulture);
214214+ var underlyingType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
215215+216216+ if (underlyingType.IsEnum)
217217+ Value = (T)Enum.Parse(underlyingType, s);
218218+ else
219219+ Value = (T)Convert.ChangeType(s, underlyingType, CultureInfo.InvariantCulture);
216220 break;
221221+217222 default:
218223 throw new ArgumentException($@"Could not parse provided {input.GetType()} ({input}) to {typeof(T)}.");
219224 }
+2
osu.Framework/Bindables/BindableList.cs
···350350 case null:
351351 Clear();
352352 break;
353353+353354 case IEnumerable<T> enumerable:
354355 Clear();
355356 AddRange(enumerable);
356357 break;
358358+357359 default:
358360 throw new ArgumentException($@"Could not parse provided {input.GetType()} ({input}) to {typeof(T)}.");
359361 }
···228228 case TypeCode.UInt64:
229229 case TypeCode.Int64:
230230 return true;
231231+231232 default:
232233 return false;
233234 }
···245246246247 byteBindable.Value = Convert.ToByte(val);
247248 break;
249249+248250 case TypeCode.SByte:
249251 var sbyteBindable = this as BindableNumber<sbyte>;
250252 if (sbyteBindable == null) throw new ArgumentNullException(nameof(sbyteBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
251253252254 sbyteBindable.Value = Convert.ToSByte(val);
253255 break;
256256+254257 case TypeCode.UInt16:
255258 var ushortBindable = this as BindableNumber<ushort>;
256259 if (ushortBindable == null) throw new ArgumentNullException(nameof(ushortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
257260258261 ushortBindable.Value = Convert.ToUInt16(val);
259262 break;
263263+260264 case TypeCode.Int16:
261265 var shortBindable = this as BindableNumber<short>;
262266 if (shortBindable == null) throw new ArgumentNullException(nameof(shortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
263267264268 shortBindable.Value = Convert.ToInt16(val);
265269 break;
270270+266271 case TypeCode.UInt32:
267272 var uintBindable = this as BindableNumber<uint>;
268273 if (uintBindable == null) throw new ArgumentNullException(nameof(uintBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
269274270275 uintBindable.Value = Convert.ToUInt32(val);
271276 break;
277277+272278 case TypeCode.Int32:
273279 var intBindable = this as BindableNumber<int>;
274280 if (intBindable == null) throw new ArgumentNullException(nameof(intBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
275281276282 intBindable.Value = Convert.ToInt32(val);
277283 break;
284284+278285 case TypeCode.UInt64:
279286 var ulongBindable = this as BindableNumber<ulong>;
280287 if (ulongBindable == null) throw new ArgumentNullException(nameof(ulongBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
281288282289 ulongBindable.Value = Convert.ToUInt64(val);
283290 break;
291291+284292 case TypeCode.Int64:
285293 var longBindable = this as BindableNumber<long>;
286294 if (longBindable == null) throw new ArgumentNullException(nameof(longBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
287295288296 longBindable.Value = Convert.ToInt64(val);
289297 break;
298298+290299 case TypeCode.Single:
291300 var floatBindable = this as BindableNumber<float>;
292301 if (floatBindable == null) throw new ArgumentNullException(nameof(floatBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
293302294303 floatBindable.Value = Convert.ToSingle(val);
295304 break;
305305+296306 case TypeCode.Double:
297307 var doubleBindable = this as BindableNumber<double>;
298308 if (doubleBindable == null) throw new ArgumentNullException(nameof(doubleBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
···313323314324 byteBindable.Value += Convert.ToByte(val);
315325 break;
326326+316327 case TypeCode.SByte:
317328 var sbyteBindable = this as BindableNumber<sbyte>;
318329 if (sbyteBindable == null) throw new ArgumentNullException(nameof(sbyteBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
319330320331 sbyteBindable.Value += Convert.ToSByte(val);
321332 break;
333333+322334 case TypeCode.UInt16:
323335 var ushortBindable = this as BindableNumber<ushort>;
324336 if (ushortBindable == null) throw new ArgumentNullException(nameof(ushortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
325337326338 ushortBindable.Value += Convert.ToUInt16(val);
327339 break;
340340+328341 case TypeCode.Int16:
329342 var shortBindable = this as BindableNumber<short>;
330343 if (shortBindable == null) throw new ArgumentNullException(nameof(shortBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
331344332345 shortBindable.Value += Convert.ToInt16(val);
333346 break;
347347+334348 case TypeCode.UInt32:
335349 var uintBindable = this as BindableNumber<uint>;
336350 if (uintBindable == null) throw new ArgumentNullException(nameof(uintBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
337351338352 uintBindable.Value += Convert.ToUInt32(val);
339353 break;
354354+340355 case TypeCode.Int32:
341356 var intBindable = this as BindableNumber<int>;
342357 if (intBindable == null) throw new ArgumentNullException(nameof(intBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
343358344359 intBindable.Value += Convert.ToInt32(val);
345360 break;
361361+346362 case TypeCode.UInt64:
347363 var ulongBindable = this as BindableNumber<ulong>;
348364 if (ulongBindable == null) throw new ArgumentNullException(nameof(ulongBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
349365350366 ulongBindable.Value += Convert.ToUInt64(val);
351367 break;
368368+352369 case TypeCode.Int64:
353370 var longBindable = this as BindableNumber<long>;
354371 if (longBindable == null) throw new ArgumentNullException(nameof(longBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
355372356373 longBindable.Value += Convert.ToInt64(val);
357374 break;
375375+358376 case TypeCode.Single:
359377 var floatBindable = this as BindableNumber<float>;
360378 if (floatBindable == null) throw new ArgumentNullException(nameof(floatBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
361379362380 floatBindable.Value += Convert.ToSingle(val);
363381 break;
382382+364383 case TypeCode.Double:
365384 var doubleBindable = this as BindableNumber<double>;
366385 if (doubleBindable == null) throw new ArgumentNullException(nameof(doubleBindable), $"Generic type {typeof(T)} does not match actual bindable type {GetType()}.");
+1
osu.Framework/Bindables/BindableSize.cs
···60606161 Value = new Size(int.Parse(split[0]), int.Parse(split[1]));
6262 break;
6363+6364 default:
6465 base.Parse(input);
6566 break;
···103103 return null;
104104105105 var jagged = new T[rectangular.GetLength(0)][];
106106+106107 for (int r = 0; r < rectangular.GetLength(0); r++)
107108 {
108109 jagged[r] = new T[rectangular.GetLength(1)];
···130131 var cols = rows == 0 ? 0 : jagged.Max(c => c?.Length ?? 0);
131132132133 var rectangular = new T[rows, cols];
134134+133135 for (int r = 0; r < rows; r++)
134136 for (int c = 0; c < cols; c++)
135137 {
+7-5
osu.Framework/Game.cs
···11-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
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.
3344using System;
···2020using osu.Framework.IO.Stores;
2121using osu.Framework.Localisation;
2222using osu.Framework.Platform;
2323-using GameWindow = osu.Framework.Platform.GameWindow;
24232524namespace osu.Framework
2625{
2726 public abstract class Game : Container, IKeyBindingHandler<FrameworkAction>
2827 {
2929- public GameWindow Window => Host?.Window;
2828+ public IWindow Window => Host?.Window;
30293130 public ResourceStore<byte[]> Resources { get; private set; }
3231···124123 Textures.AddStore(Host.CreateTextureLoaderStore(new OnlineStore()));
125124 dependencies.Cache(Textures);
126125127127- var tracks = new ResourceStore<byte[]>(Resources);
126126+ var tracks = new ResourceStore<byte[]>();
128127 tracks.AddStore(new NamespacedResourceStore<byte[]>(Resources, @"Tracks"));
129128 tracks.AddStore(new OnlineStore());
130129131131- var samples = new ResourceStore<byte[]>(Resources);
130130+ var samples = new ResourceStore<byte[]>();
132131 samples.AddStore(new NamespacedResourceStore<byte[]>(Resources, @"Samples"));
133132 samples.AddStore(new OnlineStore());
134133135134 Audio = new AudioManager(Host.AudioThread, tracks, samples) { EventScheduler = Scheduler };
136135 dependencies.Cache(Audio);
136136+137137+ dependencies.CacheAs(Audio.Tracks);
138138+ dependencies.CacheAs(Audio.Samples);
137139138140 // attach our bindables to the audio subsystem.
139141 config.BindWith(FrameworkSetting.AudioDevice, Audio.AudioDevice);
+2
osu.Framework/Graphics/Animations/Animation.cs
···129129 if (IsPlaying && frameData.Count > 0)
130130 {
131131 currentFrameTime += Time.Elapsed;
132132+132133 while (currentFrameTime > frameData[currentFrameIndex].Duration)
133134 {
134135 currentFrameTime -= frameData[currentFrameIndex].Duration;
135136 ++currentFrameIndex;
137137+136138 if (currentFrameIndex >= frameData.Count)
137139 {
138140 if (Repeat)
···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 System;
55+using JetBrains.Annotations;
66+using osu.Framework.Allocation;
77+using osu.Framework.Audio;
88+using osu.Framework.Bindables;
99+using osu.Framework.Graphics.Containers;
1010+using osu.Framework.Graphics.Transforms;
1111+1212+namespace osu.Framework.Graphics.Audio
1313+{
1414+ /// <summary>
1515+ /// A wrapper which allows audio components (or adjustments) to exist in the draw hierarchy.
1616+ /// </summary>
1717+ [Cached(typeof(IAggregateAudioAdjustment))]
1818+ public abstract class DrawableAudioWrapper : CompositeDrawable, IAggregateAudioAdjustment
1919+ {
2020+ /// <summary>
2121+ /// The volume of this component.
2222+ /// </summary>
2323+ public BindableDouble Volume => adjustments.Volume;
2424+2525+ /// <summary>
2626+ /// The playback balance of this sample (-1 .. 1 where 0 is centered)
2727+ /// </summary>
2828+ public BindableDouble Balance => adjustments.Balance;
2929+3030+ /// <summary>
3131+ /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency.
3232+ /// </summary>
3333+ public BindableDouble Frequency => adjustments.Frequency;
3434+3535+ private readonly AdjustableAudioComponent component;
3636+3737+ private readonly bool disposeUnderlyingComponentOnDispose;
3838+3939+ private readonly AudioAdjustments adjustments = new AudioAdjustments();
4040+4141+ /// <summary>
4242+ /// Creates a <see cref="DrawableAudioWrapper"/> that will contain a drawable child.
4343+ /// Generally used to add adjustments to a hierarchy without adding an audio component.
4444+ /// </summary>
4545+ /// <param name="content">The <see cref="Drawable"/> to be wrapped.</param>
4646+ protected DrawableAudioWrapper(Drawable content)
4747+ {
4848+ AddInternal(content);
4949+ }
5050+5151+ /// <summary>
5252+ /// Creates a <see cref="DrawableAudioWrapper"/> that will wrap an audio component (and contain no drawable content).
5353+ /// </summary>
5454+ /// <param name="component">The audio component to wrap.</param>
5555+ /// <param name="disposeUnderlyingComponentOnDispose">Whether the component should be automatically disposed on drawable disposal/expiry.</param>
5656+ protected DrawableAudioWrapper([NotNull] AdjustableAudioComponent component, bool disposeUnderlyingComponentOnDispose = true)
5757+ {
5858+ this.component = component ?? throw new ArgumentNullException(nameof(component));
5959+ this.disposeUnderlyingComponentOnDispose = disposeUnderlyingComponentOnDispose;
6060+6161+ component.BindAdjustments(adjustments);
6262+ }
6363+6464+ [BackgroundDependencyLoader(true)]
6565+ private void load(IAggregateAudioAdjustment parentAdjustment)
6666+ {
6767+ if (parentAdjustment != null)
6868+ adjustments.BindAdjustments(parentAdjustment);
6969+ }
7070+7171+ protected override void Dispose(bool isDisposing)
7272+ {
7373+ base.Dispose(isDisposing);
7474+ component?.UnbindAdjustments(adjustments);
7575+7676+ if (disposeUnderlyingComponentOnDispose)
7777+ component?.Dispose();
7878+ }
7979+8080+ public IBindable<double> AggregateVolume => adjustments.AggregateVolume;
8181+8282+ public IBindable<double> AggregateBalance => adjustments.AggregateBalance;
8383+8484+ public IBindable<double> AggregateFrequency => adjustments.AggregateFrequency;
8585+8686+ /// <summary>
8787+ /// Smoothly adjusts <see cref="Volume"/> over time.
8888+ /// </summary>
8989+ /// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
9090+ public TransformSequence<DrawableAudioWrapper> VolumeTo(double newVolume, double duration = 0, Easing easing = Easing.None) =>
9191+ this.TransformBindableTo(Volume, newVolume, duration, easing);
9292+9393+ /// <summary>
9494+ /// Smoothly adjusts <see cref="Balance"/> over time.
9595+ /// </summary>
9696+ /// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
9797+ public TransformSequence<DrawableAudioWrapper> BalanceTo(double newBalance, double duration = 0, Easing easing = Easing.None) =>
9898+ this.TransformBindableTo(Balance, newBalance, duration, easing);
9999+100100+ /// <summary>
101101+ /// Smoothly adjusts <see cref="Frequency"/> over time.
102102+ /// </summary>
103103+ /// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
104104+ public TransformSequence<DrawableAudioWrapper> FrequencyTo(double newFrequency, double duration = 0, Easing easing = Easing.None) =>
105105+ this.TransformBindableTo(Frequency, newFrequency, duration, easing);
106106+ }
107107+}
+34
osu.Framework/Graphics/Audio/DrawableSample.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.
33+44+using osu.Framework.Audio.Sample;
55+66+namespace osu.Framework.Graphics.Audio
77+{
88+ /// <summary>
99+ /// A <see cref="SampleChannel"/> wrapper to allow insertion in the draw hierarchy to allow transforms, lifetime management etc.
1010+ /// </summary>
1111+ public class DrawableSample : DrawableAudioWrapper, ISampleChannel
1212+ {
1313+ private readonly SampleChannel channel;
1414+1515+ /// <summary>
1616+ /// Construct a new drawable sample instance.
1717+ /// </summary>
1818+ /// <param name="channel">The audio sample to wrap.</param>
1919+ /// <param name="disposeChannelOnDisposal">Whether the sample should be automatically disposed on drawable disposal/expiry.</param>
2020+ public DrawableSample(SampleChannel channel, bool disposeChannelOnDisposal = true)
2121+ : base(channel, disposeChannelOnDisposal)
2222+ {
2323+ this.channel = channel;
2424+ }
2525+2626+ public void Play(bool restart = true) => channel.Play(restart);
2727+2828+ public void Stop() => channel.Stop();
2929+3030+ public bool Playing => channel.Playing;
3131+3232+ public bool Played => channel.Played;
3333+ }
3434+}
+60
osu.Framework/Graphics/Audio/DrawableTrack.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.
33+44+using osu.Framework.Audio.Track;
55+66+namespace osu.Framework.Graphics.Audio
77+{
88+ /// <summary>
99+ /// A <see cref="Track"/> wrapper to allow insertion in the draw hierarchy to allow transforms, lifetime management etc.
1010+ /// </summary>
1111+ public class DrawableTrack : DrawableAudioWrapper, ITrack
1212+ {
1313+ private readonly Track track;
1414+1515+ /// <summary>
1616+ /// Construct a new drawable track instance.
1717+ /// </summary>
1818+ /// <param name="track">The audio track to wrap.</param>
1919+ /// <param name="disposeTrackOnDisposal">Whether the track should be automatically disposed on drawable disposal/expiry.</param>
2020+ public DrawableTrack(Track track, bool disposeTrackOnDisposal = true)
2121+ : base(track, disposeTrackOnDisposal)
2222+ {
2323+ this.track = track;
2424+ }
2525+2626+ public bool Looping
2727+ {
2828+ get => track.Looping;
2929+ set => track.Looping = value;
3030+ }
3131+3232+ public double RestartPoint
3333+ {
3434+ get => track.RestartPoint;
3535+ set => track.RestartPoint = value;
3636+ }
3737+3838+ public double CurrentTime => track.CurrentTime;
3939+4040+ public double Length
4141+ {
4242+ get => track.Length;
4343+ set => track.Length = value;
4444+ }
4545+4646+ public bool IsRunning => track.IsRunning;
4747+4848+ public void Reset() => track.Reset();
4949+5050+ public void Restart() => track.Restart();
5151+5252+ public void ResetSpeedAdjustments() => track.ResetSpeedAdjustments();
5353+5454+ public bool Seek(double seek) => track.Seek(seek);
5555+5656+ public void Start() => track.Start();
5757+5858+ public void Stop() => track.Stop();
5959+ }
6060+}
+4-3
osu.Framework/Graphics/Audio/WaveformGraph.cs
···4343 private float resolution = 1;
44444545 /// <summary>
4646- /// Gets or sets the amount of <see cref="WaveformPoint"/>'s displayed relative to <see cref="WaveformGraph.DrawWidth"/>.
4646+ /// Gets or sets the amount of <see cref="Framework.Audio.Track.Waveform.Point"/>'s displayed relative to <see cref="WaveformGraph.DrawWidth"/>.
4747 /// </summary>
4848 public float Resolution
4949 {
···195195 private IShader shader;
196196 private Texture texture;
197197198198- private IReadOnlyList<WaveformPoint> points;
198198+ private IReadOnlyList<Waveform.Point> points;
199199200200 private Vector2 drawSize;
201201 private int channels;
···293293 );
294294 }
295295 break;
296296+296297 case 1:
297298 {
298299 quadToDraw = new Quad(
···306307 }
307308308309 quadToDraw *= DrawInfo.Matrix;
309309- texture.DrawQuad(quadToDraw, colour, null, vertexBatch.AddAction, Vector2.Divide(localInflationAmount, quadToDraw.Size));
310310+ DrawQuad(texture, quadToDraw, colour, null, vertexBatch.AddAction, Vector2.Divide(localInflationAmount, quadToDraw.Size));
310311 }
311312312313 shader.Unbind();
···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 System;
55+using osu.Framework.Allocation;
66+using osu.Framework.Graphics.OpenGL;
77+using osu.Framework.Graphics.OpenGL.Buffers;
88+using osu.Framework.Graphics.OpenGL.Vertices;
99+using osu.Framework.Graphics.Primitives;
1010+using osuTK;
1111+using osuTK.Graphics;
1212+using osuTK.Graphics.ES30;
1313+1414+namespace osu.Framework.Graphics
1515+{
1616+ public class BufferedDrawNode : TexturedShaderDrawNode
1717+ {
1818+ protected new IBufferedDrawable Source => (IBufferedDrawable)base.Source;
1919+2020+ /// <summary>
2121+ /// The child <see cref="DrawNode"/> which is used to populate the <see cref="FrameBuffer"/>s with.
2222+ /// </summary>
2323+ protected readonly DrawNode Child;
2424+2525+ /// <summary>
2626+ /// Data shared amongst all <see cref="BufferedDrawNode"/>s, providing storage for <see cref="FrameBuffer"/>s.
2727+ /// </summary>
2828+ protected readonly BufferedDrawNodeSharedData SharedData;
2929+3030+ /// <summary>
3131+ /// Contains the colour and blending information of this <see cref="DrawNode"/>.
3232+ /// </summary>
3333+ protected new DrawColourInfo DrawColourInfo { get; private set; }
3434+3535+ protected RectangleF DrawRectangle { get; private set; }
3636+3737+ private Color4 backgroundColour;
3838+ private RectangleF screenSpaceDrawRectangle;
3939+ private Vector2 frameBufferSize;
4040+4141+ private readonly All filteringMode;
4242+ private readonly RenderbufferInternalFormat[] formats;
4343+4444+ public BufferedDrawNode(IBufferedDrawable source, DrawNode child, BufferedDrawNodeSharedData sharedData, RenderbufferInternalFormat[] formats = null, bool pixelSnapping = false)
4545+ : base(source)
4646+ {
4747+ this.formats = formats;
4848+4949+ Child = child;
5050+ SharedData = sharedData;
5151+5252+ filteringMode = pixelSnapping ? All.Nearest : All.Linear;
5353+ }
5454+5555+ public override void ApplyState()
5656+ {
5757+ base.ApplyState();
5858+5959+ backgroundColour = Source.BackgroundColour;
6060+ screenSpaceDrawRectangle = Source.ScreenSpaceDrawQuad.AABBFloat;
6161+ DrawColourInfo = Source.FrameBufferDrawColour ?? new DrawColourInfo(Color4.White, base.DrawColourInfo.Blending);
6262+6363+ frameBufferSize = new Vector2((float)Math.Ceiling(screenSpaceDrawRectangle.Width), (float)Math.Ceiling(screenSpaceDrawRectangle.Height));
6464+ DrawRectangle = filteringMode == All.Nearest
6565+ ? new RectangleF(screenSpaceDrawRectangle.X, screenSpaceDrawRectangle.Y, frameBufferSize.X, frameBufferSize.Y)
6666+ : screenSpaceDrawRectangle;
6767+6868+ Child.ApplyState();
6969+ }
7070+7171+ /// <summary>
7272+ /// Whether this <see cref="BufferedDrawNode"/> should be redrawn.
7373+ /// </summary>
7474+ protected bool RequiresRedraw => GetDrawVersion() > SharedData.DrawVersion;
7575+7676+ /// <summary>
7777+ /// Retrieves the version of the state of this <see cref="DrawNode"/>.
7878+ /// The <see cref="BufferedDrawNode"/> will only re-render if this version is greater than that of the rendered <see cref="FrameBuffer"/>s.
7979+ /// </summary>
8080+ /// <remarks>
8181+ /// By default, the <see cref="BufferedDrawNode"/> is re-rendered with every <see cref="DrawNode"/> invalidation.
8282+ /// </remarks>
8383+ /// <returns>A version representing this <see cref="DrawNode"/>'s state.</returns>
8484+ protected virtual long GetDrawVersion() => InvalidationID;
8585+8686+ public sealed override void Draw(Action<TexturedVertex2D> vertexAction)
8787+ {
8888+ if (RequiresRedraw)
8989+ {
9090+ SharedData.ResetCurrentEffectBuffer();
9191+9292+ using (establishFrameBufferViewport())
9393+ {
9494+ // Fill the frame buffer with drawn children
9595+ using (BindFrameBuffer(SharedData.MainBuffer))
9696+ {
9797+ // We need to draw children as if they were zero-based to the top-left of the texture.
9898+ // We can do this by adding a translation component to our (orthogonal) projection matrix.
9999+ GLWrapper.PushOrtho(screenSpaceDrawRectangle);
100100+ GLWrapper.Clear(new ClearInfo(backgroundColour));
101101+102102+ Child.Draw(vertexAction);
103103+104104+ GLWrapper.PopOrtho();
105105+ }
106106+107107+ PopulateContents();
108108+ }
109109+110110+ SharedData.DrawVersion = GetDrawVersion();
111111+ }
112112+113113+ Shader.Bind();
114114+115115+ DrawContents();
116116+117117+ Shader.Unbind();
118118+ }
119119+120120+ /// <summary>
121121+ /// Populates the contents of the effect buffers of <see cref="SharedData"/>.
122122+ /// This is invoked after <see cref="Child"/> has been rendered to the main buffer.
123123+ /// </summary>
124124+ protected virtual void PopulateContents()
125125+ {
126126+ }
127127+128128+ /// <summary>
129129+ /// Draws the applicable effect buffers of <see cref="SharedData"/> to the back buffer.
130130+ /// </summary>
131131+ protected virtual void DrawContents()
132132+ {
133133+ GLWrapper.SetBlend(DrawColourInfo.Blending);
134134+ DrawFrameBuffer(SharedData.MainBuffer, DrawRectangle, DrawColourInfo.Colour);
135135+ }
136136+137137+ /// <summary>
138138+ /// Binds and initialises a <see cref="FrameBuffer"/> if required.
139139+ /// </summary>
140140+ /// <param name="frameBuffer">The <see cref="FrameBuffer"/> to bind.</param>
141141+ /// <returns>A token that must be disposed upon finishing use of <paramref name="frameBuffer"/>.</returns>
142142+ protected ValueInvokeOnDisposal BindFrameBuffer(FrameBuffer frameBuffer)
143143+ {
144144+ if (!frameBuffer.IsInitialized)
145145+ frameBuffer.Initialize(true, filteringMode);
146146+147147+ if (formats != null)
148148+ {
149149+ // These additional render buffers are only required if e.g. depth
150150+ // or stencil information needs to also be stored somewhere.
151151+ foreach (var f in formats)
152152+ frameBuffer.Attach(f);
153153+ }
154154+155155+ // This setter will also take care of allocating a texture of appropriate size within the frame buffer.
156156+ frameBuffer.Size = frameBufferSize;
157157+158158+ frameBuffer.Bind();
159159+160160+ return new ValueInvokeOnDisposal(frameBuffer.Unbind);
161161+ }
162162+163163+ private ValueInvokeOnDisposal establishFrameBufferViewport()
164164+ {
165165+ // Disable masking for generating the frame buffer since masking will be re-applied
166166+ // when actually drawing later on anyways. This allows more information to be captured
167167+ // in the frame buffer and helps with cached buffers being re-used.
168168+ RectangleI screenSpaceMaskingRect = new RectangleI((int)Math.Floor(screenSpaceDrawRectangle.X), (int)Math.Floor(screenSpaceDrawRectangle.Y), (int)frameBufferSize.X + 1, (int)frameBufferSize.Y + 1);
169169+170170+ GLWrapper.PushMaskingInfo(new MaskingInfo
171171+ {
172172+ ScreenSpaceAABB = screenSpaceMaskingRect,
173173+ MaskingRect = screenSpaceDrawRectangle,
174174+ ToMaskingSpace = Matrix3.Identity,
175175+ BlendRange = 1,
176176+ AlphaExponent = 1,
177177+ }, true);
178178+179179+ // Match viewport to FrameBuffer such that we don't draw unnecessary pixels.
180180+ GLWrapper.PushViewport(new RectangleI(0, 0, (int)frameBufferSize.X, (int)frameBufferSize.Y));
181181+182182+ return new ValueInvokeOnDisposal(returnViewport);
183183+ }
184184+185185+ private void returnViewport()
186186+ {
187187+ GLWrapper.PopViewport();
188188+ GLWrapper.PopMaskingInfo();
189189+ }
190190+ }
191191+}
···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 System;
55+using osu.Framework.Graphics.OpenGL.Buffers;
66+77+namespace osu.Framework.Graphics
88+{
99+ /// <summary>
1010+ /// Contains data which is shared between all <see cref="BufferedDrawNode"/>s of a <see cref="Drawable"/>.
1111+ /// </summary>
1212+ /// <remarks>
1313+ /// This should be constructed _once_ per <see cref="Drawable"/>, and given to the constructor of <see cref="BufferedDrawNode"/>.
1414+ /// </remarks>
1515+ public class BufferedDrawNodeSharedData : IDisposable
1616+ {
1717+ /// <summary>
1818+ /// The version of drawn contents currently present in <see cref="MainBuffer"/> and <see cref="effectBuffers"/>.
1919+ /// This should only be modified by <see cref="BufferedDrawNode"/>.
2020+ /// </summary>
2121+ internal long DrawVersion = -1;
2222+2323+ /// <summary>
2424+ /// The <see cref="FrameBuffer"/> which contains the original version of the rendered <see cref="Drawable"/>.
2525+ /// </summary>
2626+ public FrameBuffer MainBuffer { get; }
2727+2828+ /// <summary>
2929+ /// A set of <see cref="FrameBuffer"/>s which are used in a ping-pong manner to render effects to.
3030+ /// </summary>
3131+ private readonly FrameBuffer[] effectBuffers;
3232+3333+ /// <summary>
3434+ /// Creates a new <see cref="BufferedDrawNodeSharedData"/> with no effect buffers.
3535+ /// </summary>
3636+ public BufferedDrawNodeSharedData()
3737+ : this(0)
3838+ {
3939+ }
4040+4141+ /// <summary>
4242+ /// Creates a new <see cref="BufferedDrawNodeSharedData"/> with a specific amount of effect buffers.
4343+ /// </summary>
4444+ /// <param name="effectBufferCount">The number of effect buffers.</param>
4545+ /// <exception cref="ArgumentOutOfRangeException">If <paramref name="effectBufferCount"/> is less than 0.</exception>
4646+ public BufferedDrawNodeSharedData(int effectBufferCount)
4747+ {
4848+ if (effectBufferCount < 0)
4949+ throw new ArgumentOutOfRangeException(nameof(effectBufferCount), "Must be positive.");
5050+5151+ MainBuffer = new FrameBuffer();
5252+ effectBuffers = new FrameBuffer[effectBufferCount];
5353+5454+ for (int i = 0; i < effectBufferCount; i++)
5555+ effectBuffers[i] = new FrameBuffer();
5656+ }
5757+5858+ private int currentEffectBuffer = -1;
5959+6060+ /// <summary>
6161+ /// The <see cref="FrameBuffer"/> which contains the most up-to-date drawn effect.
6262+ /// </summary>
6363+ public FrameBuffer CurrentEffectBuffer => currentEffectBuffer == -1 ? MainBuffer : effectBuffers[currentEffectBuffer];
6464+6565+ /// <summary>
6666+ /// Retrieves the next <see cref="FrameBuffer"/> which effects can be rendered to.
6767+ /// </summary>
6868+ /// <exception cref="InvalidOperationException">If there are no available effect buffers.</exception>
6969+ public FrameBuffer GetNextEffectBuffer()
7070+ {
7171+ if (effectBuffers.Length == 0)
7272+ throw new InvalidOperationException($"The {nameof(BufferedDrawNode)} requested an effect buffer, but none were available.");
7373+7474+ if (++currentEffectBuffer >= effectBuffers.Length)
7575+ currentEffectBuffer = 0;
7676+ return effectBuffers[currentEffectBuffer];
7777+ }
7878+7979+ /// <summary>
8080+ /// Resets <see cref="CurrentEffectBuffer"/>.
8181+ /// This should only be called by <see cref="BufferedDrawNode"/>.
8282+ /// </summary>
8383+ internal void ResetCurrentEffectBuffer() => currentEffectBuffer = -1;
8484+8585+ ~BufferedDrawNodeSharedData()
8686+ {
8787+ Dispose(false);
8888+ }
8989+9090+ public void Dispose()
9191+ {
9292+ Dispose(true);
9393+ GC.SuppressFinalize(this);
9494+ }
9595+9696+ protected virtual void Dispose(bool isDisposing)
9797+ {
9898+ MainBuffer.Dispose();
9999+100100+ for (int i = 0; i < effectBuffers.Length; i++)
101101+ effectBuffers[i].Dispose();
102102+ }
103103+ }
104104+}
···1212using System;
1313using System.Collections.Generic;
1414using osu.Framework.Caching;
1515+using osu.Framework.Graphics.Sprites;
15161617namespace osu.Framework.Graphics.Containers
1718{
···3536 /// appearance of the container at the cost of performance. Such effects include
3637 /// uniform fading of children, blur, and other post-processing effects.
3738 /// </summary>
3838- public partial class BufferedContainer<T> : Container<T>, IBufferedContainer
3939+ public partial class BufferedContainer<T> : Container<T>, IBufferedContainer, IBufferedDrawable
3940 where T : Drawable
4041 {
4142 private bool drawOriginal;
···108109 get => pixelSnapping;
109110 set
110111 {
111111- if (sharedData != null)
112112- {
113113- for (int i = 0; i < sharedData.FrameBuffers.Length; i++)
114114- {
115115- if (sharedData.FrameBuffers[i].IsInitialized)
116116- throw new InvalidOperationException("May only set PixelSnapping before FrameBuffers are initialized (i.e. before the first draw).");
117117- }
118118- }
112112+ if (sharedData?.MainBuffer.IsInitialized == true)
113113+ throw new InvalidOperationException("May only set PixelSnapping before FrameBuffers are initialized (i.e. before the first draw).");
119114120115 pixelSnapping = value;
121116 }
···222217223218 protected override bool CanBeFlattened => false;
224219220220+ public IShader TextureShader { get; private set; }
221221+222222+ public IShader RoundedTextureShader { get; private set; }
223223+225224 private IShader blurShader;
226225227226 /// <summary>
···237236 [BackgroundDependencyLoader]
238237 private void load(ShaderManager shaders)
239238 {
239239+ TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
240240+ RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
240241 blurShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR);
241242 }
242243243244 private readonly BufferedContainerDrawNodeSharedData sharedData = new BufferedContainerDrawNodeSharedData();
244245245245- protected override DrawNode CreateDrawNode() => new BufferedContainerDrawNode(this, sharedData);
246246+ protected override DrawNode CreateDrawNode() => new BufferedContainerDrawNode(this, sharedData, attachedFormats.ToArray(), PixelSnapping);
246247247248 private readonly List<RenderbufferInternalFormat> attachedFormats = new List<RenderbufferInternalFormat>();
248249···313314 childrenUpdateVersion = updateVersion;
314315 }
315316316316- private DrawColourInfo baseDrawColourInfo => base.DrawColourInfo;
317317-318318- public override DrawColourInfo DrawColourInfo
317317+ /// <summary>
318318+ /// The blending which <see cref="BufferedContainerDrawNode"/> uses for the effect.
319319+ /// </summary>
320320+ public BlendingParameters DrawEffectBlending
319321 {
320322 get
321323 {
322322- DrawColourInfo result = base.DrawColourInfo;
324324+ BlendingParameters blending = EffectBlending;
325325+ if (blending.Mode == BlendingMode.Inherit)
326326+ blending.Mode = Blending.Mode;
323327324324- // When drawing our children to the frame buffer we do not
325325- // want their colour to be polluted by their parent (us!)
326326- // since our own color will be applied on top when we render
327327- // from the frame buffer to the back buffer later on.
328328- result.Colour = ColourInfo.SingleColour(Color4.White);
329329- return result;
328328+ if (blending.RGBEquation == BlendingEquation.Inherit)
329329+ blending.RGBEquation = Blending.RGBEquation;
330330+331331+ if (blending.AlphaEquation == BlendingEquation.Inherit)
332332+ blending.AlphaEquation = Blending.AlphaEquation;
333333+334334+ return blending;
330335 }
331336 }
337337+338338+ /// <summary>
339339+ /// Creates a view which can be added to a container to display the content of this <see cref="BufferedContainer{T}"/>.
340340+ /// </summary>
341341+ /// <returns>The view.</returns>
342342+ public BufferedContainerView<T> CreateView() => new BufferedContainerView<T>(this, sharedData);
343343+344344+ public DrawColourInfo? FrameBufferDrawColour => base.DrawColourInfo;
345345+346346+ // Children should not receive the true colour to avoid colour doubling when the frame-buffers are rendered to the back-buffer.
347347+ public override DrawColourInfo DrawColourInfo => new DrawColourInfo(Color4.White, base.DrawColourInfo.Blending);
332348333349 protected override void Dispose(bool isDisposing)
334350 {
···76767777 private CancellationTokenSource disposalCancellationSource;
78787979- private static readonly ThreadedTaskScheduler threaded_scheduler = new ThreadedTaskScheduler(4);
7979+ private static readonly ThreadedTaskScheduler threaded_scheduler = new ThreadedTaskScheduler(4, nameof(LoadComponentsAsync));
80808181 /// <summary>
8282 /// Loads a future child or grand-child of this <see cref="CompositeDrawable"/> asynchronously. <see cref="Dependencies"/>
···418418 return false;
419419420420 internalChildren.RemoveAt(index);
421421+421422 if (drawable.IsAlive)
422423 {
423424 aliveInternalChildren.Remove(drawable);
···563564 {
564565 case LoadState.NotLoaded:
565566 break;
567567+566568 case LoadState.Loading:
567569 if (Thread.CurrentThread != LoadThread)
568570 throw new InvalidThreadForChildMutationException(LoadState, "not on the load thread");
569571570572 break;
573573+571574 case LoadState.Ready:
572575 // Allow mutating from the load thread since parenting containers may still be in the loading state
573576 if (Thread.CurrentThread != LoadThread && !ThreadSafety.IsUpdateThread)
574577 throw new InvalidThreadForChildMutationException(LoadState, "not on the load or update threads");
575578576579 break;
580580+577581 case LoadState.Loaded:
578582 if (!ThreadSafety.IsUpdateThread)
579583 throw new InvalidThreadForChildMutationException(LoadState, "not on the update thread");
···985989 private static void addFromComposite(ulong frame, int treeIndex, bool forceNewDrawNode, ref int j, CompositeDrawable parentComposite, List<DrawNode> target)
986990 {
987991 SortedList<Drawable> children = parentComposite.aliveInternalChildren;
992992+988993 for (int i = 0; i < children.Count; ++i)
989994 {
990995 Drawable drawable = children[i];
···9951000 continue;
99610019971002 CompositeDrawable composite = drawable as CompositeDrawable;
10031003+9981004 if (composite?.CanBeFlattened == true)
9991005 {
10001006 if (!composite.IsMaskedAway)
···15501556 set
15511557 {
15521558 if ((AutoSizeAxes & Axes.X) != 0)
15531553- throw new InvalidOperationException($"The width of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} should only be manually set if it is relative to its parent.");
15591559+ throw new InvalidOperationException($"The width of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} can not be set manually.");
1554156015551561 base.Width = value;
15561562 }
···15681574 set
15691575 {
15701576 if ((AutoSizeAxes & Axes.Y) != 0)
15711571- throw new InvalidOperationException($"The height of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} should only be manually set if it is relative to its parent.");
15771577+ throw new InvalidOperationException($"The height of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} can not be set manually.");
1572157815731579 base.Height = value;
15741580 }
···15881594 set
15891595 {
15901596 if ((AutoSizeAxes & Axes.Both) != 0)
15911591- throw new InvalidOperationException($"The Size of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} should only be manually set if it is relative to its parent.");
15971597+ throw new InvalidOperationException($"The Size of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} can not be set manually.");
1592159815931599 base.Size = value;
15941600 }
···9797 protected override IEnumerable<Vector2> ComputeLayoutPositions()
9898 {
9999 var max = MaximumSize;
100100+100101 if (max == Vector2.Zero)
101102 {
102103 var s = ChildSize;
···128129129130 // First pass, computing initial flow positions
130131 Vector2 size = Vector2.Zero;
132132+131133 for (int i = 0; i < children.Length; ++i)
132134 {
133135 Drawable c = children[i];
···138140 {
139141 case FillDirection.Full:
140142 return Axes.Both;
143143+141144 case FillDirection.Horizontal:
142145 return Axes.X;
146146+143147 case FillDirection.Vertical:
144148 return Axes.Y;
149149+145150 default:
146151 throw new ArgumentException($"{direction.ToString()} is not defined");
147152 }
···192197 rowIndices[i] = rowOffsetsToMiddle.Count - 1;
193198194199 Vector2 stride = Vector2.Zero;
200200+195201 if (i < children.Length - 1)
196202 {
197203 // Compute stride. Note, that the stride depends on the origins of the drawables
···230236 + $"Consider using multiple instances of {nameof(FillFlowContainer)} if this is intentional.");
231237232238 break;
239239+233240 case FillDirection.Horizontal:
234241 if (c.RelativeAnchorPosition.X != ourRelativeAnchor.X)
235242 throw new InvalidOperationException(
···237244 + $"Consider using multiple instances of {nameof(FillFlowContainer)} if this is intentional.");
238245239246 break;
247247+240248 default:
241249 if (c.RelativeAnchorPosition != ourRelativeAnchor)
242250 throw new InvalidOperationException(
···164164 var positions = ComputeLayoutPositions().ToArray();
165165166166 int i = 0;
167167+167168 foreach (var d in FlowingChildren)
168169 {
169170 if (i > positions.Length)
···3344using System;
55using System.Collections.Generic;
66+using JetBrains.Annotations;
67using osu.Framework.Graphics.Transforms;
78using osu.Framework.Lists;
89···1718 /// <summary>
1819 /// The currently displayed <see cref="Drawable"/>. Null if no drawable is displayed.
1920 /// </summary>
2020- protected Drawable DisplayedDrawable { get; private set; }
2121+ protected Drawable DisplayedDrawable => displayedWrapper?.Content;
21222223 /// <summary>
2324 /// The <see cref="IEqualityComparer{T}"/> used to compare models to ensure that <see cref="Drawable"/>s are not updated unnecessarily.
···4950 }
50515152 /// <summary>
5353+ /// The wrapper which has the current displayed content.
5454+ /// </summary>
5555+ private DelayedLoadWrapper displayedWrapper;
5656+5757+ /// <summary>
5858+ /// The wrapper which is currently loading, or has finished loading (i.e <see cref="displayedWrapper"/>).
5959+ /// </summary>
6060+ private DelayedLoadWrapper currentWrapper;
6161+6262+ /// <summary>
5263 /// Constructs a new <see cref="ModelBackedDrawable{T}"/> with the default <typeparamref name="T"/> equality comparer.
5364 /// </summary>
5465 protected ModelBackedDrawable()
···8091 updateDrawable();
8192 }
82938383- private DelayedLoadWrapper currentWrapper;
8484- private bool placeholderDisplayed;
8585-8694 private void updateDrawable()
8795 {
8888- if (model == null)
8989- loadPlaceholder();
9696+ if (TransformImmediately)
9797+ {
9898+ // If loading to a new model and we've requested to transform immediately, load a null model to allow such transforms to occur
9999+ loadDrawable(null);
100100+ }
101101+102102+ loadDrawable(() => CreateDrawable(model));
103103+ }
104104+105105+ private void loadDrawable(Func<Drawable> createDrawableFunc)
106106+ {
107107+ // Remove the previous wrapper if the inner drawable hasn't finished loading.
108108+ if (currentWrapper?.DelayedLoadCompleted == false)
109109+ {
110110+ RemoveInternal(currentWrapper);
111111+ DisposeChildAsync(currentWrapper);
112112+ }
113113+114114+ currentWrapper = createWrapper(createDrawableFunc, LoadDelay);
115115+116116+ if (currentWrapper == null)
117117+ {
118118+ OnLoadStarted();
119119+ finishLoad(currentWrapper);
120120+ OnLoadFinished();
121121+ }
90122 else
91123 {
9292- if (FadeOutImmediately) loadPlaceholder();
9393- loadDrawable(CreateDrawable(model), false);
124124+ AddInternal(currentWrapper);
125125+ currentWrapper.DelayedLoadStarted += _ => OnLoadStarted();
126126+ currentWrapper.DelayedLoadComplete += _ =>
127127+ {
128128+ finishLoad(currentWrapper);
129129+ OnLoadFinished();
130130+ };
94131 }
95132 }
961339797- private void loadPlaceholder()
134134+ /// <summary>
135135+ /// Invoked when a <see cref="DelayedLoadWrapper"/> has finished loading its contents.
136136+ /// May be invoked multiple times for each <see cref="DelayedLoadWrapper"/>.
137137+ /// </summary>
138138+ /// <param name="wrapper">The <see cref="DelayedLoadWrapper"/>.</param>
139139+ private void finishLoad(DelayedLoadWrapper wrapper)
98140 {
9999- if (placeholderDisplayed)
141141+ var lastWrapper = displayedWrapper;
142142+143143+ // If the wrapper hasn't changed then this invocation must be a result of a reload (e.g. DelayedLoadUnloadWrapper)
144144+ // In this case, we do not want to transform/expire the wrapper
145145+ if (lastWrapper == wrapper)
100146 return;
101147102102- var placeholder = CreateDrawable(null);
148148+ // Make the new wrapper initially hidden
149149+ ApplyHideTransforms(wrapper);
150150+ wrapper?.FinishTransforms();
151151+152152+ var showTransforms = ApplyShowTransforms(wrapper);
153153+154154+ // If we have a non-null new wrapper, we need to wait for the show transformation to complete before hiding the old wrapper,
155155+ // otherwise, we can hide the old wrapper instantaneously and leave a blank display
156156+ var hideTransforms = wrapper == null
157157+ ? ApplyHideTransforms(lastWrapper)
158158+ : ((Drawable)lastWrapper)?.Delay(TransformDuration)?.Append(ApplyHideTransforms);
103159104104- loadDrawable(placeholder, true);
160160+ // Expire the last wrapper after the front-most transform has completed (the last wrapper is assumed to be invisible by that point)
161161+ (showTransforms ?? hideTransforms)?.OnComplete(_ => lastWrapper?.Expire());
105162106106- // in the case a placeholder has not been specified, this should not be set as to allow for a potential runtime change
107107- // of placeholder logic on a future load operation.
108108- placeholderDisplayed = placeholder != null;
163163+ displayedWrapper = wrapper;
109164 }
110165111111- private void loadDrawable(Drawable newDrawable, bool isPlaceholder)
166166+ /// <summary>
167167+ /// Creates a <see cref="DelayedLoadWrapper"/> which supports reloading.
168168+ /// </summary>
169169+ /// <param name="createContentFunc">A function that creates the wrapped <see cref="Drawable"/>.</param>
170170+ /// <param name="timeBeforeLoad">The time before loading should begin.</param>
171171+ /// <returns>A <see cref="DelayedLoadWrapper"/> or null if <see cref="createContentFunc"/> returns null.</returns>
172172+ private DelayedLoadWrapper createWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
112173 {
113113- // Remove the previous wrapper if the inner drawable hasn't finished loading.
114114- // We check IsLoaded on the content rather than DelayedLoadCompleted so that we can ensure that finishLoad() has not been called and DisplayedDrawable hasn't been updated
115115- if (currentWrapper?.Content.IsLoaded == false)
116116- {
117117- // Using .Expire() will be one frame too late, since children's lifetime has already been updated this frame
118118- RemoveInternal(currentWrapper);
119119- DisposeChildAsync(currentWrapper);
120120- }
174174+ var content = createContentFunc?.Invoke();
121175122122- currentWrapper = null;
176176+ if (content == null)
177177+ return null;
123178124124- if (newDrawable != null)
179179+ return CreateDelayedLoadWrapper(() =>
125180 {
126126- AddInternal(isPlaceholder ? newDrawable : currentWrapper = CreateDelayedLoadWrapper(newDrawable, LoadDelay));
127127-128128- if (isPlaceholder)
181181+ try
129182 {
130130- // Although the drawable is technically not loaded, it does have a clock and we need DisplayedDrawable to be updated instantly
131131- finishLoad();
183183+ // optimisation to use already constructed object (used above for null check).
184184+ return content ?? createContentFunc();
185185+ }
186186+ finally
187187+ {
188188+ // consume initial object if not already.
189189+ content = null;
132190 }
133133- else
134134- newDrawable.OnLoadComplete += _ => finishLoad();
135135- }
136136- else
137137- finishLoad();
191191+ }, timeBeforeLoad);
192192+ }
138193139139- void finishLoad()
140140- {
141141- var currentDrawable = DisplayedDrawable;
194194+ /// <summary>
195195+ /// Invoked when the <see cref="Drawable"/> representation of a model begins loading.
196196+ /// </summary>
197197+ protected virtual void OnLoadStarted()
198198+ {
199199+ }
142200143143- var transform = ReplaceDrawable(currentDrawable, newDrawable) ?? (currentDrawable ?? newDrawable)?.DelayUntilTransformsFinished();
144144- transform?.OnComplete(_ => currentDrawable?.Expire());
145145-146146- DisplayedDrawable = newDrawable;
147147- placeholderDisplayed = isPlaceholder;
148148- }
201201+ /// <summary>
202202+ /// Invoked when the <see cref="Drawable"/> representation of a model has finished loading.
203203+ /// </summary>
204204+ protected virtual void OnLoadFinished()
205205+ {
149206 }
150207151208 /// <summary>
152152- /// Determines whether the current <see cref="Drawable"/> should fade out straight away when switching to a new model,
153153- /// or whether it should wait until the new <see cref="Drawable"/> has finished loading.
209209+ /// Determines whether <see cref="ApplyHideTransforms"/> should be invoked immediately on the currently-displayed drawable when switching to a new model.
154210 /// </summary>
155155- protected virtual bool FadeOutImmediately => false;
211211+ protected virtual bool TransformImmediately => false;
156212157213 /// <summary>
158158- /// The time in milliseconds that <see cref="Drawable"/>s will fade in and out.
214214+ /// The default time in milliseconds for transforms applied through <see cref="ApplyHideTransforms"/> and <see cref="ApplyShowTransforms"/>.
159215 /// </summary>
160160- protected virtual double FadeDuration => 1000;
216216+ protected virtual double TransformDuration => 1000;
161217162218 /// <summary>
163219 /// The delay in milliseconds before <see cref="Drawable"/>s will begin loading.
···167223 /// <summary>
168224 /// Allows subclasses to customise the <see cref="DelayedLoadWrapper"/>.
169225 /// </summary>
170170- protected virtual DelayedLoadWrapper CreateDelayedLoadWrapper(Drawable content, double timeBeforeLoad) =>
171171- new DelayedLoadWrapper(content, timeBeforeLoad);
226226+ [NotNull]
227227+ protected virtual DelayedLoadWrapper CreateDelayedLoadWrapper([NotNull] Func<Drawable> createContentFunc, double timeBeforeLoad) =>
228228+ new DelayedLoadWrapper(createContentFunc(), timeBeforeLoad);
172229173230 /// <summary>
174174- /// Override to instantiate a custom <see cref="Drawable"/> based on the passed model.
175175- /// May be null to indicate that the model has no visual representation,
176176- /// in which case the placeholder will be used if it exists.
231231+ /// Creates a custom <see cref="Drawable"/> to display a model.
177232 /// </summary>
178233 /// <param name="model">The model that the <see cref="Drawable"/> should represent.</param>
179179- protected abstract Drawable CreateDrawable(T model);
234234+ /// <returns>A <see cref="Drawable"/> that represents <paramref name="model"/>, or null if no <see cref="Drawable"/> should be displayed.</returns>
235235+ [CanBeNull]
236236+ protected abstract Drawable CreateDrawable([CanBeNull] T model);
180237181238 /// <summary>
182182- /// Returns a <see cref="TransformSequence{Drawable}"/> that replaces the given <see cref="Drawable"/>s.
183183- /// Default functionality is to fade in the target from zero, or if it is null, to fade out the source.
239239+ /// Hides a drawable.
240240+ /// </summary>
241241+ /// <param name="drawable">The drawable that is to be hidden.</param>
242242+ /// <returns>The transform sequence.</returns>
243243+ protected virtual TransformSequence<Drawable> ApplyHideTransforms([CanBeNull] Drawable drawable)
244244+ => drawable?.FadeOut(TransformDuration, Easing.OutQuint);
245245+246246+ /// <summary>
247247+ /// Shows a drawable.
184248 /// </summary>
185185- /// <returns>The drawable.</returns>
186186- /// <param name="source">The <see cref="Drawable"/> to be replaced.</param>
187187- /// <param name="target">The <see cref="Drawable"/> we are replacing with.</param>
188188- protected virtual TransformSequence<Drawable> ReplaceDrawable(Drawable source, Drawable target) =>
189189- target?.FadeInFromZero(FadeDuration, Easing.OutQuint) ?? source?.FadeOut(FadeDuration, Easing.OutQuint);
249249+ /// <param name="drawable">The drawable that is to be shown.</param>
250250+ /// <returns>The transform sequence.</returns>
251251+ protected virtual TransformSequence<Drawable> ApplyShowTransforms([CanBeNull] Drawable drawable)
252252+ => drawable?.FadeIn(TransformDuration, Easing.OutQuint);
190253 }
191254}
···5353 // We keep track of all drawables we found while traversing the parent chain upwards.
5454 newChildDrawables.Clear();
5555 newChildDrawables.Add(candidate);
5656+5657 // When we encounter a drawable we already encountered before, then there is no need
5758 // to keep going upward, since we already recorded it previously. At that point we know
5859 // the drawables we found are in fact children of ours.
+3
osu.Framework/Graphics/Cursor/TooltipContainer.cs
···140140 base.Update();
141141142142 IHasTooltip target = findTooltipTarget();
143143+143144 if (target != null && target != currentlyDisplayed)
144145 {
145146 currentlyDisplayed = target;
···178179 return hasValidTooltip(draggedTarget) ? draggedTarget : null;
179180180181 IHasTooltip targetCandidate = FindTargets().Find(t => t.TooltipText != null);
182182+181183 // check this first - if we find no target candidate we still want to clear the recorded positions and update the lastCandidate.
182184 if (targetCandidate != lastCandidate)
183185 {
···191193 double appearDelay = (targetCandidate as IHasAppearDelay)?.AppearDelay ?? AppearDelay;
192194 // Always keep 10 positions at equally-sized time intervals that add up to AppearDelay.
193195 double positionRecordInterval = appearDelay / 10;
196196+194197 if (Time.Current - lastRecordedPositionTime >= positionRecordInterval)
195198 {
196199 lastRecordedPositionTime = Time.Current;
+196-5
osu.Framework/Graphics/DrawNode.cs
···3344using osu.Framework.Graphics.OpenGL;
55using System;
66+using System.Runtime.CompilerServices;
77+using osu.Framework.Graphics.Batches;
88+using osu.Framework.Graphics.Colour;
99+using osu.Framework.Graphics.OpenGL.Buffers;
1010+using osu.Framework.Graphics.OpenGL.Textures;
611using osu.Framework.Graphics.OpenGL.Vertices;
1212+using osu.Framework.Graphics.Primitives;
1313+using osu.Framework.Graphics.Textures;
1414+using osu.Framework.MathUtils.Clipping;
715using osu.Framework.Threading;
1616+using osuTK;
817918namespace osu.Framework.Graphics
1019{
···2231 /// <summary>
2332 /// Contains the colour and blending information of this <see cref="DrawNode"/>.
2433 /// </summary>
2525- protected internal DrawColourInfo DrawColourInfo { get; internal set; }
3434+ protected DrawColourInfo DrawColourInfo { get; private set; }
26352736 /// <summary>
2837 /// Identifies the state of this draw node with an invalidation state of its corresponding
···3948 private readonly AtomicCounter referenceCount = new AtomicCounter();
40494150 /// <summary>
5151+ /// The depth at which drawing should take place.
5252+ /// This is written to from the front-to-back pass and used in both passes.
5353+ /// </summary>
5454+ private float drawDepth;
5555+5656+ /// <summary>
4257 /// Creates a new <see cref="DrawNode"/>.
4358 /// </summary>
4459 /// <param name="source">The <see cref="Drawable"/> to draw with this <see cref="DrawNode"/>.</param>
···6176 }
62776378 /// <summary>
6464- /// Draws this draw node to the screen.
7979+ /// Draws this <see cref="DrawNode"/> to the screen.
6580 /// </summary>
6666- /// <param name="vertexAction">The action to be performed on each vertex of
6767- /// the draw node in order to draw it if required. This is primarily used by
6868- /// textured sprites.</param>
8181+ /// <remarks>
8282+ /// This is the back-to-front (BTF) pass. The back-buffer depth test function used is GL_LESS.<br />
8383+ /// The depth test will fail for samples that overlap the opaque interior of this <see cref="DrawNode"/> and any <see cref="DrawNode"/>s above this one.<br />
8484+ /// </remarks>
8585+ /// <param name="vertexAction">The action to be performed on each vertex of the draw node in order to draw it if required. This is primarily used by textured sprites.</param>
6986 public virtual void Draw(Action<TexturedVertex2D> vertexAction)
7087 {
7188 GLWrapper.SetBlend(DrawColourInfo.Blending);
8989+ GLWrapper.SetDrawDepth(drawDepth);
9090+ }
9191+9292+ /// <summary>
9393+ /// Draws the opaque interior of this <see cref="DrawNode"/> and all <see cref="DrawNode"/>s further down the scene graph, invoking <see cref="DrawOpaqueInterior"/> if <see cref="CanDrawOpaqueInterior"/>
9494+ /// indicates that an opaque interior can be drawn for each relevant <see cref="DrawNode"/>.
9595+ /// </summary>
9696+ /// <remarks>
9797+ /// This is the front-to-back pass. The back-buffer depth test function used is GL_LESS.<br />
9898+ /// If an opaque interior is not drawn: the current value of <paramref name="depthValue"/> is stored.<br />
9999+ /// If an opaque interior is drawn: <paramref name="depthValue"/> is incremented, stored, and the opaque interior vertices are drawn at the post-incremented depth value.
100100+ /// Incrementing <paramref name="depthValue"/> at this point allows for early-z testing to also occur within the front-to-back pass.<br />
101101+ /// </remarks>
102102+ /// <param name="depthValue">The previous depth value.</param>
103103+ /// <param name="vertexAction">The action to be performed on each vertex of the draw node in order to draw it if required. This is primarily used by textured sprites.</param>
104104+ internal virtual void DrawOpaqueInteriorSubTree(DepthValue depthValue, Action<TexturedVertex2D> vertexAction)
105105+ {
106106+ if (!depthValue.CanIncrement || !CanDrawOpaqueInterior)
107107+ {
108108+ // The back-to-front pass requires the depth value
109109+ drawDepth = depthValue;
110110+ return;
111111+ }
112112+113113+ // It is crucial to draw with an incremented depth value, consider the case of a box:
114114+ // In the front-to-back pass, the inner conservative area is drawn at depth X
115115+ // In the back-to-front pass, the full area is drawn at depth X, and the depth test function is set to GL_LESS, so the inner conservative area is not redrawn
116116+ // Furthermore, a back-to-front-drawn object above the box will be visible since it will be drawn with a depth of (X - increment), satisfying the depth test
117117+ drawDepth = depthValue.Increment();
118118+119119+ DrawOpaqueInterior(vertexAction);
120120+ }
121121+122122+ /// <summary>
123123+ /// Draws the opaque interior of this <see cref="DrawNode"/> to the screen.
124124+ /// The opaque interior must be a fully-opaque, non-blended area of this <see cref="DrawNode"/>, clipped to the current masking area via <code>DrawClipped()</code>.
125125+ /// See <see cref="Shapes.Box.BoxDrawNode"/> for an example implementation.
126126+ /// </summary>
127127+ /// <param name="vertexAction">The action to be performed on each vertex of the draw node in order to draw it if required. This is primarily used by textured sprites.</param>
128128+ protected virtual void DrawOpaqueInterior(Action<TexturedVertex2D> vertexAction)
129129+ {
130130+ GLWrapper.SetBlend(DrawColourInfo.Blending);
131131+ GLWrapper.SetDrawDepth(drawDepth);
132132+ }
133133+134134+ /// <summary>
135135+ /// Whether this <see cref="DrawNode"/> can draw a opaque interior. <see cref="DrawOpaqueInterior"/> will only be invoked if this value is <code>true</code>.
136136+ /// Should not return <code>true</code> if <see cref="DrawOpaqueInterior"/> will result in a no-op.
137137+ /// </summary>
138138+ protected internal virtual bool CanDrawOpaqueInterior => false;
139139+140140+ /// <summary>
141141+ /// Draws a triangle to the screen.
142142+ /// </summary>
143143+ /// <param name="texture">The texture to fill the triangle with.</param>
144144+ /// <param name="vertexTriangle">The triangle to draw.</param>
145145+ /// <param name="textureRect">The texture rectangle.</param>
146146+ /// <param name="drawColour">The vertex colour.</param>
147147+ /// <param name="vertexAction">An action that adds vertices to a <see cref="VertexBatch{T}"/>.</param>
148148+ /// <param name="inflationPercentage">The percentage amount that <see cref="textureRect"/> should be inflated.</param>
149149+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
150150+ protected void DrawTriangle(Texture texture, Triangle vertexTriangle, ColourInfo drawColour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null,
151151+ Vector2? inflationPercentage = null)
152152+ => texture.DrawTriangle(vertexTriangle, drawColour, textureRect, vertexAction, inflationPercentage);
153153+154154+ /// <summary>
155155+ /// Draws a triangle to the screen.
156156+ /// </summary>
157157+ /// <param name="texture">The texture to fill the triangle with.</param>
158158+ /// <param name="vertexTriangle">The triangle to draw.</param>
159159+ /// <param name="drawColour">The vertex colour.</param>
160160+ /// <param name="textureRect">The texture rectangle.</param>
161161+ /// <param name="vertexAction">An action that adds vertices to a <see cref="VertexBatch{T}"/>.</param>
162162+ /// <param name="inflationPercentage">The percentage amount that <see cref="textureRect"/> should be inflated.</param>
163163+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
164164+ protected void DrawTriangle(TextureGL texture, Triangle vertexTriangle, ColourInfo drawColour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null,
165165+ Vector2? inflationPercentage = null)
166166+ => texture.DrawTriangle(vertexTriangle, drawColour, textureRect, vertexAction, inflationPercentage);
167167+168168+ /// <summary>
169169+ /// Draws a quad to the screen.
170170+ /// </summary>
171171+ /// <param name="texture">The texture to fill the triangle with.</param>
172172+ /// <param name="vertexQuad">The quad to draw.</param>
173173+ /// <param name="textureRect">The texture rectangle.</param>
174174+ /// <param name="drawColour">The vertex colour.</param>
175175+ /// <param name="vertexAction">An action that adds vertices to a <see cref="VertexBatch{T}"/>.</param>
176176+ /// <param name="inflationPercentage">The percentage amount that <see cref="textureRect"/> should be inflated.</param>
177177+ /// <param name="blendRangeOverride">The range over which the edges of the <see cref="textureRect"/> should be blended.</param>
178178+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
179179+ protected void DrawQuad(Texture texture, Quad vertexQuad, ColourInfo drawColour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null,
180180+ Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null)
181181+ => texture.DrawQuad(vertexQuad, drawColour, textureRect, vertexAction, inflationPercentage: inflationPercentage, blendRangeOverride: blendRangeOverride);
182182+183183+ /// <summary>
184184+ /// Draws a quad to the screen.
185185+ /// </summary>
186186+ /// <param name="texture">The texture to fill the triangle with.</param>
187187+ /// <param name="vertexQuad">The quad to draw.</param>
188188+ /// <param name="drawColour">The vertex colour.</param>
189189+ /// <param name="textureRect">The texture rectangle.</param>
190190+ /// <param name="vertexAction">An action that adds vertices to a <see cref="VertexBatch{T}"/>.</param>
191191+ /// <param name="inflationPercentage">The percentage amount that <see cref="textureRect"/> should be inflated.</param>
192192+ /// <param name="blendRangeOverride">The range over which the edges of the <see cref="textureRect"/> should be blended.</param>
193193+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
194194+ protected void DrawQuad(TextureGL texture, Quad vertexQuad, ColourInfo drawColour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null,
195195+ Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null)
196196+ => texture.DrawQuad(vertexQuad, drawColour, textureRect, vertexAction, inflationPercentage: inflationPercentage, blendRangeOverride: blendRangeOverride);
197197+198198+ /// <summary>
199199+ /// Clips a <see cref="IConvexPolygon"/> to the current masking area and draws the resulting triangles to the screen using the specified texture.
200200+ /// </summary>
201201+ /// <param name="polygon">The polygon to draw.</param>
202202+ /// <param name="texture">The texture to fill the triangle with.</param>
203203+ /// <param name="textureRect">The texture rectangle.</param>
204204+ /// <param name="drawColour">The vertex colour.</param>
205205+ /// <param name="vertexAction">An action that adds vertices to a <see cref="VertexBatch{T}"/>.</param>
206206+ /// <param name="inflationPercentage">The percentage amount that <see cref="textureRect"/> should be inflated.</param>
207207+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
208208+ protected void DrawClipped<T>(ref T polygon, Texture texture, ColourInfo drawColour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null,
209209+ Vector2? inflationPercentage = null)
210210+ where T : IConvexPolygon
211211+ {
212212+ var maskingQuad = GLWrapper.CurrentMaskingInfo.ConservativeScreenSpaceQuad;
213213+214214+ var clipper = new ConvexPolygonClipper<Quad, T>(ref maskingQuad, ref polygon);
215215+ Span<Vector2> buffer = stackalloc Vector2[clipper.GetClipBufferSize()];
216216+ Span<Vector2> clippedRegion = clipper.Clip(buffer);
217217+218218+ for (int i = 2; i < clippedRegion.Length; i++)
219219+ DrawTriangle(texture, new Triangle(clippedRegion[0], clippedRegion[i - 1], clippedRegion[i]), drawColour, textureRect, vertexAction, inflationPercentage);
220220+ }
221221+222222+ /// <summary>
223223+ /// Clips a <see cref="IConvexPolygon"/> to the current masking area and draws the resulting triangles to the screen using the specified texture.
224224+ /// </summary>
225225+ /// <param name="polygon">The polygon to draw.</param>
226226+ /// <param name="texture">The texture to fill the triangle with.</param>
227227+ /// <param name="textureRect">The texture rectangle.</param>
228228+ /// <param name="drawColour">The vertex colour.</param>
229229+ /// <param name="vertexAction">An action that adds vertices to a <see cref="VertexBatch{T}"/>.</param>
230230+ /// <param name="inflationPercentage">The percentage amount that <see cref="textureRect"/> should be inflated.</param>
231231+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
232232+ protected void DrawClipped<T>(ref T polygon, TextureGL texture, ColourInfo drawColour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null,
233233+ Vector2? inflationPercentage = null)
234234+ where T : IConvexPolygon
235235+ {
236236+ var maskingQuad = GLWrapper.CurrentMaskingInfo.ConservativeScreenSpaceQuad;
237237+238238+ var clipper = new ConvexPolygonClipper<Quad, T>(ref maskingQuad, ref polygon);
239239+ Span<Vector2> buffer = stackalloc Vector2[clipper.GetClipBufferSize()];
240240+ Span<Vector2> clippedRegion = clipper.Clip(buffer);
241241+242242+ for (int i = 2; i < clippedRegion.Length; i++)
243243+ DrawTriangle(texture, new Triangle(clippedRegion[0], clippedRegion[i - 1], clippedRegion[i]), drawColour, textureRect, vertexAction, inflationPercentage);
244244+ }
245245+246246+ /// <summary>
247247+ /// Draws a <see cref="FrameBuffer"/> to the screen.
248248+ /// </summary>
249249+ /// <param name="frameBuffer">The <see cref="FrameBuffer"/> to draw.</param>
250250+ /// <param name="vertexQuad">The destination vertices.</param>
251251+ /// <param name="drawColour">The colour to draw the <paramref name="frameBuffer"/> with.</param>
252252+ /// <param name="vertexAction">An action that adds vertices to a <see cref="VertexBatch{T}"/>.</param>
253253+ /// <param name="inflationPercentage">The percentage amount that the frame buffer area should be inflated.</param>
254254+ /// <param name="blendRangeOverride">The range over which the edges of the frame buffer should be blended.</param>
255255+ protected void DrawFrameBuffer(FrameBuffer frameBuffer, Quad vertexQuad, ColourInfo drawColour, Action<TexturedVertex2D> vertexAction = null,
256256+ Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null)
257257+ {
258258+ // The strange Y coordinate and Height are a result of OpenGL coordinate systems having Y grow upwards and not downwards.
259259+ RectangleF textureRect = new RectangleF(0, frameBuffer.Texture.Height, frameBuffer.Texture.Width, -frameBuffer.Texture.Height);
260260+261261+ if (frameBuffer.Texture.Bind())
262262+ DrawQuad(frameBuffer.Texture, vertexQuad, drawColour, textureRect, vertexAction, inflationPercentage, blendRangeOverride);
72263 }
7326474265 /// <summary>
+24
osu.Framework/Graphics/Drawable.cs
···592592 get
593593 {
594594 Vector2 offset = Vector2.Zero;
595595+595596 if (Parent != null && RelativePositionAxes != Axes.None)
596597 {
597598 offset = Parent.RelativeChildOffset;
···844845 private void updateBypassAutoSizeAxes()
845846 {
846847 var value = RelativePositionAxes | RelativeSizeAxes | bypassAutoSizeAdditionalAxes;
848848+847849 if (bypassAutoSizeAxes != value)
848850 {
849851 var changedAxes = bypassAutoSizeAxes ^ value;
···14061408 protected InputManager GetContainingInputManager()
14071409 {
14081410 Drawable search = Parent;
14111411+14091412 while (search != null)
14101413 {
14111414 if (search is InputManager test) return test;
···17171720 internal virtual DrawNode GenerateDrawNodeSubtree(ulong frame, int treeIndex, bool forceNewDrawNode)
17181721 {
17191722 DrawNode node = drawNodes[treeIndex];
17231723+17201724 if (node == null || forceNewDrawNode)
17211725 {
17221726 drawNodes[treeIndex] = node = CreateDrawNode();
···18361840 {
18371841 case MouseMoveEvent mouseMove:
18381842 return OnMouseMove(mouseMove);
18431843+18391844 case HoverEvent hover:
18401845 return OnHover(hover);
18461846+18411847 case HoverLostEvent hoverLost:
18421848 OnHoverLost(hoverLost);
18431849 return false;
18501850+18441851 case MouseDownEvent mouseDown:
18451852 return OnMouseDown(mouseDown);
18531853+18461854 case MouseUpEvent mouseUp:
18471855 return OnMouseUp(mouseUp);
18561856+18481857 case ClickEvent click:
18491858 return OnClick(click);
18591859+18501860 case DoubleClickEvent doubleClick:
18511861 return OnDoubleClick(doubleClick);
18621862+18521863 case DragStartEvent dragStart:
18531864 return OnDragStart(dragStart);
18651865+18541866 case DragEvent drag:
18551867 return OnDrag(drag);
18681868+18561869 case DragEndEvent dragEnd:
18571870 return OnDragEnd(dragEnd);
18711871+18581872 case ScrollEvent scroll:
18591873 return OnScroll(scroll);
18741874+18601875 case FocusEvent focus:
18611876 OnFocus(focus);
18621877 return false;
18781878+18631879 case FocusLostEvent focusLost:
18641880 OnFocusLost(focusLost);
18651881 return false;
18821882+18661883 case KeyDownEvent keyDown:
18671884 return OnKeyDown(keyDown);
18851885+18681886 case KeyUpEvent keyUp:
18691887 return OnKeyUp(keyUp);
18881888+18701889 case JoystickPressEvent joystickPress:
18711890 return OnJoystickPress(joystickPress);
18911891+18721892 case JoystickReleaseEvent joystickRelease:
18731893 return OnJoystickRelease(joystickRelease);
18941894+18741895 default:
18751896 return false;
18761897 }
···20072028 private static bool get(Drawable drawable, ConcurrentDictionary<Type, bool> cache, bool positional)
20082029 {
20092030 var type = drawable.GetType();
20312031+20102032 if (!cache.TryGetValue(type, out var value))
20112033 {
20122034 value = compute(type, positional);
···20192041 private static bool compute(Type type, bool positional)
20202042 {
20212043 var inputMethods = positional ? positional_input_methods : non_positional_input_methods;
20442044+20222045 foreach (var inputMethod in inputMethods)
20232046 {
20242047 // check for any input method overrides which are at a higher level than drawable.
···20312054 }
2032205520332056 var inputInterfaces = positional ? positional_input_interfaces : non_positional_input_interfaces;
20572057+20342058 foreach (var inputInterface in inputInterfaces)
20352059 {
20362060 // check if this type implements any interface which requires a drawable to handle input.
···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 osu.Framework.Graphics.OpenGL.Buffers;
55+using osuTK.Graphics;
66+77+namespace osu.Framework.Graphics
88+{
99+ /// <summary>
1010+ /// Interface for <see cref="Drawable"/>s which can be drawn by a <see cref="BufferedDrawNode"/>.
1111+ /// </summary>
1212+ public interface IBufferedDrawable : ITexturedShaderDrawable
1313+ {
1414+ /// <summary>
1515+ /// The background colour of the <see cref="FrameBuffer"/>s.
1616+ /// Visually changes the colour which rendered alpha is blended against.
1717+ /// </summary>
1818+ /// <remarks>
1919+ /// This should generally be transparent-black or transparent-white, but can also be used to
2020+ /// colourise the background colour of the <see cref="FrameBuffer"/> with non-transparent colours.
2121+ /// </remarks>
2222+ Color4 BackgroundColour { get; }
2323+2424+ /// <summary>
2525+ /// The colour with which the <see cref="FrameBuffer"/>s are rendered to the screen.
2626+ /// A null value implies the <see cref="FrameBuffer"/>s should be drawn as they are.
2727+ /// </summary>
2828+ DrawColourInfo? FrameBufferDrawColour { get; }
2929+ }
3030+}
+18-4
osu.Framework/Graphics/Lines/Path.cs
···88using osu.Framework.Allocation;
99using System.Collections.Generic;
1010using osu.Framework.Caching;
1111+using osuTK.Graphics;
1212+using osuTK.Graphics.ES30;
11131214namespace osu.Framework.Graphics.Lines
1315{
1414- public partial class Path : Drawable, ITexturedShaderDrawable
1616+ public partial class Path : Drawable, IBufferedDrawable
1517 {
1618 public IShader RoundedTextureShader { get; private set; }
1719 public IShader TextureShader { get; private set; }
2020+ private IShader pathShader;
18211922 [BackgroundDependencyLoader]
2023 private void load(ShaderManager shaders)
2124 {
2222- RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE_ROUNDED);
2323- TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE);
2525+ RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
2626+ TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
2727+ pathShader = shaders.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE);
2428 }
25292630 private readonly List<Vector2> vertices = new List<Vector2>();
···174178 }
175179 }
176180177177- protected override DrawNode CreateDrawNode() => new PathDrawNode(this);
181181+ public DrawColourInfo? FrameBufferDrawColour => base.DrawColourInfo;
182182+183183+ // The path should not receive the true colour to avoid colour doubling when the frame-buffer is rendered to the back-buffer.
184184+ // Removal of blending allows for correct blending between the wedges of the path.
185185+ public override DrawColourInfo DrawColourInfo => new DrawColourInfo(Color4.White, new BlendingInfo(BlendingMode.None));
186186+187187+ public Color4 BackgroundColour => new Color4(0, 0, 0, 0);
188188+189189+ private readonly BufferedDrawNodeSharedData sharedData = new BufferedDrawNodeSharedData();
190190+191191+ protected override DrawNode CreateDrawNode() => new BufferedDrawNode(this, new PathDrawNode(this), sharedData, new[] { RenderbufferInternalFormat.DepthComponent16 });
178192 }
179193}
+6-3
osu.Framework/Graphics/Lines/Path_DrawNode.cs
···1212using osu.Framework.Graphics.OpenGL.Vertices;
1313using osuTK.Graphics;
1414using osu.Framework.Graphics.Colour;
1515+using osu.Framework.Graphics.Shaders;
15161617namespace osu.Framework.Graphics.Lines
1718{
1819 public partial class Path
1920 {
2020- private class PathDrawNode : TexturedShaderDrawNode
2121+ private class PathDrawNode : DrawNode
2122 {
2223 public const int MAX_RES = 24;
2324···2829 private Texture texture;
2930 private Vector2 drawSize;
3031 private float radius;
3232+ private IShader pathShader;
31333234 // We multiply the size param by 3 such that the amount of vertices is a multiple of the amount of vertices
3335 // per primitive (triangles in this case). Otherwise overflowing the batch will result in wrong
···5052 texture = Source.Texture;
5153 drawSize = Source.DrawSize;
5254 radius = Source.PathRadius;
5555+ pathShader = Source.pathShader;
5356 }
54575558 private Vector2 pointOnCircle(float angle) => new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle));
···207210208211 GLWrapper.PushDepthInfo(DepthInfo.Default);
209212210210- Shader.Bind();
213213+ pathShader.Bind();
211214212215 texture.TextureGL.WrapMode = TextureWrapMode.ClampToEdge;
213216 texture.TextureGL.Bind();
214217215218 updateVertexBuffer();
216219217217- Shader.Unbind();
220220+ pathShader.Unbind();
218221219222 GLWrapper.PopDepthInfo();
220223 }
···3344// ReSharper disable StaticMemberInGenericType
5566+using System;
67using System.Collections.Generic;
77-using System.Linq;
88using System.Reflection;
99using System.Runtime.InteropServices;
1010-using osuTK;
1110using osuTK.Graphics.ES30;
12111312namespace osu.Framework.Graphics.OpenGL.Vertices
···2120 /// <summary>
2221 /// The stride of the vertex of type <see cref="T"/>.
2322 /// </summary>
2424- public static readonly int STRIDE = BlittableValueType.StrideOf(default(T));
2323+ public static readonly int STRIDE = Marshal.SizeOf(default(T));
25242625 private static readonly List<VertexMemberAttribute> attributes = new List<VertexMemberAttribute>();
2726 private static int amountEnabledAttributes;
28272928 static VertexUtils()
3029 {
3131- // Use reflection to retrieve the members attached with a VertexMemberAttribute
3232- foreach (FieldInfo field in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Where(t => t.IsDefined(typeof(VertexMemberAttribute), true)))
3030+ addAttributesRecursive(typeof(T), 0);
3131+ }
3232+3333+ private static void addAttributesRecursive(Type type, int currentOffset)
3434+ {
3535+ foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
3336 {
3434- var attrib = (VertexMemberAttribute)field.GetCustomAttribute(typeof(VertexMemberAttribute));
3737+ int fieldOffset = currentOffset + Marshal.OffsetOf(type, field.Name).ToInt32();
35383636- // Because this is an un-seen vertex, the attribute locations are unknown, but they're needed for marshalling
3737- attrib.Offset = Marshal.OffsetOf(typeof(T), field.Name);
3939+ if (typeof(IVertex).IsAssignableFrom(field.FieldType))
4040+ {
4141+ // Vertices may contain others, but the attributes of contained vertices belong to the parent when marshalled, so they are recursively added for their parent
4242+ // Their field offsets must be adjusted to reflect the position of the child attribute in the parent vertex
4343+ addAttributesRecursive(field.FieldType, fieldOffset);
4444+ }
4545+ else if (field.IsDefined(typeof(VertexMemberAttribute), true))
4646+ {
4747+ var attrib = (VertexMemberAttribute)field.GetCustomAttribute(typeof(VertexMemberAttribute));
38483939- attributes.Add(attrib);
4949+ // Because this is an un-seen vertex, the attribute locations are unknown, but they're needed for marshalling
5050+ attrib.Offset = new IntPtr(fieldOffset);
5151+5252+ attributes.Add(attrib);
5353+ }
4054 }
4155 }
4256
···3344using System;
55using osu.Framework.Graphics.Containers;
66-using osu.Framework.Graphics.OpenGL;
77-using osu.Framework.Graphics.Textures;
88-using osuTK.Graphics.ES30;
96using osu.Framework.Threading;
107using System.Collections.Generic;
118···1310{
1411 internal class PerformanceOverlay : FillFlowContainer<FrameStatisticsDisplay>, IStateful<FrameStatisticsMode>
1512 {
1313+ private readonly IEnumerable<GameThread> threads;
1614 private FrameStatisticsMode state;
17151816 public event Action<FrameStatisticsMode> StateChanged;
19171818+ private bool initialised;
1919+2020 public FrameStatisticsMode State
2121 {
2222 get => state;
···3131 case FrameStatisticsMode.None:
3232 this.FadeOut(100);
3333 break;
3434+3435 case FrameStatisticsMode.Minimal:
3536 case FrameStatisticsMode.Full:
3737+ if (!initialised)
3838+ {
3939+ initialised = true;
4040+ foreach (GameThread t in threads)
4141+ Add(new FrameStatisticsDisplay(t) { State = state });
4242+ }
4343+3644 this.FadeIn(100);
3745 break;
3846 }
···46544755 public PerformanceOverlay(IEnumerable<GameThread> threads)
4856 {
5757+ this.threads = threads;
4958 Direction = FillDirection.Vertical;
5050- TextureAtlas atlas = new TextureAtlas(GLWrapper.MaxTextureSize, GLWrapper.MaxTextureSize, true, All.Nearest);
5151-5252- foreach (GameThread t in threads)
5353- Add(new FrameStatisticsDisplay(t, atlas) { State = state });
5459 }
5560 }
5661
+11-7
osu.Framework/Graphics/Primitives/IPolygon.cs
···99 public interface IPolygon
1010 {
1111 /// <summary>
1212- /// The vertices that define the axes spanned by this polygon.
1212+ /// The vertices that define the axes spanned by this polygon in screen-space counter-clockwise orientation.
1313 /// </summary>
1414 /// <remarks>
1515- /// Must be returned in a clockwise orientation. For best performance, vertices that form colinear edges should not be included.
1515+ /// Counter-clockwise orientation in screen-space coordinates is equivalent to a clockwise orientation in standard coordinates.
1616+ /// <para>
1717+ /// E.g. For the set of vertices { (0, 0), (1, 0), (0, 1), (1, 1) }, a counter-clockwise orientation is { (0, 0), (0, 1), (1, 1), (1, 0) }.
1818+ /// </para>
1619 /// </remarks>
1717- /// <returns>
1818- /// The vertices that define the axes spanned by this polygon.
1919- /// </returns>
2020+ /// <returns>The vertices that define the axes spanned by this polygon.</returns>
2021 ReadOnlySpan<Vector2> GetAxisVertices();
21222223 /// <summary>
2323- /// Retrieves the vertices of this polygon.
2424+ /// Retrieves the vertices of this polygon in screen-space counter-clockwise orientation.
2425 /// </summary>
2526 /// <remarks>
2626- /// Must be returned in a clockwise orientation.
2727+ /// Counter-clockwise orientation in screen-space coordinates is equivalent to a clockwise orientation in standard coordinates.
2828+ /// <para>
2929+ /// E.g. For the set of vertices { (0, 0), (1, 0), (0, 1), (1, 1) }, a counter-clockwise orientation is { (0, 0), (0, 1), (1, 1), (1, 0) }.
3030+ /// </para>
2731 /// </remarks>
2832 /// <returns>The vertices of this polygon.</returns>
2933 ReadOnlySpan<Vector2> GetVertices();
+46-13
osu.Framework/Graphics/Primitives/Line.cs
···22// See the LICENCE file in the repository root for full licence text.
3344using System;
55+using System.Runtime.CompilerServices;
56using osu.Framework.MathUtils;
67using osuTK;
78···1011 /// <summary>
1112 /// Represents a single line segment.
1213 /// </summary>
1313- public struct Line
1414+ public readonly struct Line
1415 {
1516 /// <summary>
1617 /// Begin point of the line.
1718 /// </summary>
1818- public Vector2 StartPoint;
1919+ public readonly Vector2 StartPoint;
19202021 /// <summary>
2122 /// End point of the line.
2223 /// </summary>
2323- public Vector2 EndPoint;
2424+ public readonly Vector2 EndPoint;
24252526 /// <summary>
2627 /// The length of the line.
···6263 /// </summary>
6364 /// <param name="t">A parameter representing the position along the line to compute. 0 yields the start point and 1 yields the end point.</param>
6465 /// <returns>The position along the line.</returns>
6565- public Vector2 At(float t) => StartPoint + Direction * t;
6666+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
6767+ public Vector2 At(float t) => new Vector2(StartPoint.X + (EndPoint.X - StartPoint.X) * t, StartPoint.Y + (EndPoint.Y - StartPoint.Y) * t);
66686769 /// <summary>
6870 /// Intersects this line with another.
···7173 /// <returns>Whether the two lines intersect and, if so, the distance along this line at which the intersection occurs.
7274 /// An intersection may occur even if the two lines don't touch, at which point the parameter will be outside the [0, 1] range.
7375 /// To compute the point of intersection, <see cref="At"/>.</returns>
7474- public (bool success, float distance) IntersectWith(Line other)
7676+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
7777+ public (bool success, float distance) IntersectWith(in Line other)
7578 {
7676- Vector2 diff1 = Direction;
7777- Vector2 diff2 = other.Direction;
7979+ bool success = TryIntersectWith(other, out var distance);
8080+ return (success, distance);
8181+ }
8282+8383+ /// <summary>
8484+ /// Intersects this line with another.
8585+ /// </summary>
8686+ /// <param name="other">The line to intersect with.</param>
8787+ /// <param name="distance">The distance along this line at which the intersection occurs. To compute the point of intersection, <see cref="At"/>.</param>
8888+ /// <returns>Whether the two lines intersect. An intersection may occur even if the two lines don't touch, at which point the parameter will be outside the [0, 1] range.</returns>
8989+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
9090+ public bool TryIntersectWith(in Line other, out float distance)
9191+ {
9292+ var startPoint = other.StartPoint;
9393+ var endPoint = other.EndPoint;
9494+9595+ return TryIntersectWith(ref startPoint, ref endPoint, out distance);
9696+ }
78977979- float denom = diff1.X * diff2.Y - diff1.Y * diff2.X;
9898+ /// <summary>
9999+ /// Intersects this line with another.
100100+ /// </summary>
101101+ /// <param name="otherStart">The start point of the other line to intsersect with.</param>
102102+ /// <param name="otherEnd">The end point of the other line to intersect with.</param>
103103+ /// <param name="distance">The distance along this line at which the intersection occurs. To compute the point of intersection, <see cref="At"/>.</param>
104104+ /// <returns>Whether the two lines intersect. An intersection may occur even if the two lines don't touch, at which point the parameter will be outside the [0, 1] range.</returns>
105105+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
106106+ public bool TryIntersectWith(ref Vector2 otherStart, ref Vector2 otherEnd, out float distance)
107107+ {
108108+ float otherYDist = otherEnd.Y - otherStart.Y;
109109+ float otherXDist = otherEnd.X - otherStart.X;
801108181- if (Precision.AlmostEquals(0, denom))
8282- return (false, 0); // Co-linear
111111+ float denom = (EndPoint.X - StartPoint.X) * otherYDist - (EndPoint.Y - StartPoint.Y) * otherXDist;
831128484- Vector2 d = other.StartPoint - StartPoint;
8585- float t = (d.X * diff2.Y - d.Y * diff2.X) / denom;
113113+ if (Precision.AlmostEquals(denom, 0))
114114+ {
115115+ distance = 0;
116116+ return false;
117117+ }
861188787- return (true, t);
119119+ distance = ((otherStart.X - StartPoint.X) * otherYDist - (otherStart.Y - StartPoint.Y) * otherXDist) / denom;
120120+ return true;
88121 }
8912290123 /// <summary>
+4-3
osu.Framework/Graphics/Primitives/Quad.cs
···1212 [StructLayout(LayoutKind.Sequential)]
1313 public struct Quad : IConvexPolygon, IEquatable<Quad>
1414 {
1515- // Note: Do not change the order of vertices.
1515+ // Note: Do not change the order of vertices. They are ordered in screen-space counter-clockwise fashion.
1616+ // See: IPolygon.GetVertices()
1617 public Vector2 TopLeft;
1717- public Vector2 TopRight;
1818+ public Vector2 BottomLeft;
1819 public Vector2 BottomRight;
1919- public Vector2 BottomLeft;
2020+ public Vector2 TopRight;
20212122 public Quad(Vector2 topLeft, Vector2 topRight, Vector2 bottomLeft, Vector2 bottomRight)
2223 {
···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 System;
55+using osuTK;
66+77+namespace osu.Framework.Graphics.Primitives
88+{
99+ public class SimpleConvexPolygon : IConvexPolygon
1010+ {
1111+ private readonly Vector2[] vertices;
1212+1313+ public SimpleConvexPolygon(Vector2[] vertices)
1414+ {
1515+ this.vertices = vertices;
1616+ }
1717+1818+ public ReadOnlySpan<Vector2> GetAxisVertices() => vertices;
1919+2020+ public ReadOnlySpan<Vector2> GetVertices() => vertices;
2121+2222+ public int MaxClipVertices => vertices.Length * 2;
2323+ }
2424+}
···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 System;
55+using osu.Framework.Allocation;
66+using osu.Framework.Graphics.Colour;
77+using osu.Framework.Graphics.Containers;
88+using osu.Framework.Graphics.OpenGL;
99+using osu.Framework.Graphics.OpenGL.Vertices;
1010+using osu.Framework.Graphics.Primitives;
1111+using osu.Framework.Graphics.Shaders;
1212+1313+namespace osu.Framework.Graphics.Sprites
1414+{
1515+ /// <summary>
1616+ /// A view that displays the contents of a <see cref="BufferedContainer{T}"/>.
1717+ /// </summary>
1818+ public class BufferedContainerView<T> : Drawable, ITexturedShaderDrawable
1919+ where T : Drawable
2020+ {
2121+ public IShader TextureShader { get; private set; }
2222+ public IShader RoundedTextureShader { get; private set; }
2323+2424+ private BufferedContainer<T> container;
2525+ private BufferedDrawNodeSharedData sharedData;
2626+2727+ internal BufferedContainerView(BufferedContainer<T> container, BufferedDrawNodeSharedData sharedData)
2828+ {
2929+ this.container = container;
3030+ this.sharedData = sharedData;
3131+3232+ container.OnDispose += removeContainer;
3333+ }
3434+3535+ [BackgroundDependencyLoader]
3636+ private void load(ShaderManager shaders)
3737+ {
3838+ TextureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
3939+ RoundedTextureShader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
4040+ }
4141+4242+ protected override DrawNode CreateDrawNode() => new BufferSpriteDrawNode(this);
4343+4444+ private bool synchronisedDrawQuad;
4545+4646+ /// <summary>
4747+ /// Whether this <see cref="BufferedContainerView{T}"/> should be drawn using the original <see cref="BufferedContainer{T}"/>'s draw quad.
4848+ /// </summary>
4949+ /// <remarks>
5050+ /// This can be useful to display the <see cref="BufferedContainer{T}"/> as an overlay on top of itself.
5151+ /// </remarks>
5252+ public bool SynchronisedDrawQuad
5353+ {
5454+ get => synchronisedDrawQuad;
5555+ set
5656+ {
5757+ if (value == synchronisedDrawQuad)
5858+ return;
5959+6060+ synchronisedDrawQuad = value;
6161+6262+ Invalidate(Invalidation.DrawNode);
6363+ }
6464+ }
6565+6666+ private bool displayOriginalEffects;
6767+6868+ /// <summary>
6969+ /// Whether the effects drawn by the <see cref="BufferedContainer{T}"/> should also be drawn for this view.
7070+ /// </summary>
7171+ public bool DisplayOriginalEffects
7272+ {
7373+ get => displayOriginalEffects;
7474+ set
7575+ {
7676+ if (displayOriginalEffects == value)
7777+ return;
7878+7979+ displayOriginalEffects = value;
8080+8181+ Invalidate(Invalidation.DrawNode);
8282+ }
8383+ }
8484+8585+ private void removeContainer()
8686+ {
8787+ if (container == null)
8888+ return;
8989+9090+ container.OnDispose -= removeContainer;
9191+9292+ container = null;
9393+ sharedData = null;
9494+9595+ Invalidate(Invalidation.DrawNode);
9696+ }
9797+9898+ protected override void Dispose(bool isDisposing)
9999+ {
100100+ base.Dispose(isDisposing);
101101+102102+ removeContainer();
103103+ }
104104+105105+ private class BufferSpriteDrawNode : TexturedShaderDrawNode
106106+ {
107107+ protected new BufferedContainerView<T> Source => (BufferedContainerView<T>)base.Source;
108108+109109+ private Quad screenSpaceDrawQuad;
110110+ private BufferedDrawNodeSharedData shared;
111111+ private bool displayOriginalEffects;
112112+113113+ private bool sourceDrawsOriginal;
114114+ private ColourInfo sourceEffectColour;
115115+ private BlendingParameters sourceEffectBlending;
116116+ private EffectPlacement sourceEffectPlacement;
117117+118118+ public BufferSpriteDrawNode(BufferedContainerView<T> source)
119119+ : base(source)
120120+ {
121121+ }
122122+123123+ public override void ApplyState()
124124+ {
125125+ base.ApplyState();
126126+127127+ screenSpaceDrawQuad = Source.synchronisedDrawQuad ? Source.container.ScreenSpaceDrawQuad : Source.ScreenSpaceDrawQuad;
128128+ shared = Source.sharedData;
129129+130130+ displayOriginalEffects = Source.displayOriginalEffects;
131131+ sourceDrawsOriginal = Source.container.DrawOriginal;
132132+ sourceEffectColour = Source.container.EffectColour;
133133+ sourceEffectBlending = Source.container.DrawEffectBlending;
134134+ sourceEffectPlacement = Source.container.EffectPlacement;
135135+ }
136136+137137+ public override void Draw(Action<TexturedVertex2D> vertexAction)
138138+ {
139139+ base.Draw(vertexAction);
140140+141141+ if (shared?.MainBuffer?.Texture?.Available != true || shared.DrawVersion == -1)
142142+ return;
143143+144144+ Shader.Bind();
145145+146146+ if (sourceEffectPlacement == EffectPlacement.InFront)
147147+ drawMainBuffer(vertexAction);
148148+149149+ drawEffectBuffer(vertexAction);
150150+151151+ if (sourceEffectPlacement == EffectPlacement.Behind)
152152+ drawMainBuffer(vertexAction);
153153+154154+ Shader.Unbind();
155155+ }
156156+157157+ private void drawMainBuffer(Action<TexturedVertex2D> vertexAction)
158158+ {
159159+ // If the original was drawn, draw it.
160160+ // Otherwise, if an effect will also not be drawn then we still need to display something - the original.
161161+ // Keep in mind that the effect MAY be the original itself, but is drawn through drawEffectBuffer().
162162+ if (!sourceDrawsOriginal && shouldDrawEffectBuffer)
163163+ return;
164164+165165+ GLWrapper.SetBlend(DrawColourInfo.Blending);
166166+ DrawFrameBuffer(shared.MainBuffer, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction);
167167+ }
168168+169169+ private void drawEffectBuffer(Action<TexturedVertex2D> vertexAction)
170170+ {
171171+ if (!shouldDrawEffectBuffer)
172172+ return;
173173+174174+ GLWrapper.SetBlend(new BlendingInfo(sourceEffectBlending));
175175+ ColourInfo finalEffectColour = DrawColourInfo.Colour;
176176+ finalEffectColour.ApplyChild(sourceEffectColour);
177177+178178+ DrawFrameBuffer(shared.CurrentEffectBuffer, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction);
179179+ }
180180+181181+ /// <summary>
182182+ /// Whether the source's current effect buffer should be drawn.
183183+ /// This is true if we explicitly want to draw it or if no effects were drawn by the source. In the case that no effects were drawn by the source,
184184+ /// the current effect buffer will be the main buffer, and what will be drawn is the main buffer with the effect blending applied.
185185+ /// </summary>
186186+ private bool shouldDrawEffectBuffer => displayOriginalEffects || shared.CurrentEffectBuffer == shared.MainBuffer;
187187+ }
188188+ }
189189+}
+1-1
osu.Framework/Graphics/Sprites/Sprite.cs
···6767 /// of this sprite will be set to the size of the texture.
6868 /// <see cref="Drawable.FillAspectRatio"/> is automatically set to the aspect ratio of the given texture or 1 if the texture is null.
6969 /// </summary>
7070- public Texture Texture
7070+ public virtual Texture Texture
7171 {
7272 get => texture;
7373 set
···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 osu.Framework.IO.Stores;
55-66-namespace osu.Framework.Graphics.Textures
77-{
88- public class PrefixTextureStore : TextureStore
99- {
1010- private readonly string prefix;
1111-1212- public PrefixTextureStore(string prefix, IResourceStore<TextureUpload> stores)
1313- : base(stores)
1414- {
1515- this.prefix = prefix;
1616- }
1717-1818- public override Texture Get(string name) => base.Get($@"{prefix}-{name}");
1919- }
2020-}
+24-4
osu.Framework/Graphics/Textures/Texture.cs
···3344using System;
55using System.IO;
66+using osu.Framework.Graphics.Batches;
67using osu.Framework.Graphics.OpenGL.Textures;
78using osu.Framework.Graphics.Primitives;
89using osuTK;
···112113113114 public RectangleF GetTextureRect(RectangleF? textureRect = null) => TextureGL.GetTextureRect(TextureBounds(textureRect));
114115115115- public void DrawTriangle(Triangle vertexTriangle, ColourInfo colour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null, Vector2? inflationPercentage = null)
116116+ /// <summary>
117117+ /// Draws a triangle to the screen.
118118+ /// </summary>
119119+ /// <param name="vertexTriangle">The triangle to draw.</param>
120120+ /// <param name="drawColour">The vertex colour.</param>
121121+ /// <param name="textureRect">The texture rectangle.</param>
122122+ /// <param name="vertexAction">An action that adds vertices to a <see cref="VertexBatch{T}"/>.</param>
123123+ /// <param name="inflationPercentage">The percentage amount that <see cref="textureRect"/> should be inflated.</param>
124124+ internal void DrawTriangle(Triangle vertexTriangle, ColourInfo drawColour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null,
125125+ Vector2? inflationPercentage = null)
116126 {
117127 if (TextureGL == null || !TextureGL.Bind()) return;
118128119119- TextureGL.DrawTriangle(vertexTriangle, TextureBounds(textureRect), colour, vertexAction, inflationPercentage);
129129+ TextureGL.DrawTriangle(vertexTriangle, drawColour, TextureBounds(textureRect), vertexAction, inflationPercentage);
120130 }
121131122122- public void DrawQuad(Quad vertexQuad, ColourInfo colour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null, Vector2? inflationPercentage = null, Vector2? blendRangeOverride = null)
132132+ /// <summary>
133133+ /// Draws a quad to the screen.
134134+ /// </summary>
135135+ /// <param name="vertexQuad">The quad to draw.</param>
136136+ /// <param name="drawColour">The vertex colour.</param>
137137+ /// <param name="textureRect">The texture rectangle.</param>
138138+ /// <param name="vertexAction">An action that adds vertices to a <see cref="VertexBatch{T}"/>.</param>
139139+ /// <param name="inflationPercentage">The percentage amount that <see cref="textureRect"/> should be inflated.</param>
140140+ /// <param name="blendRangeOverride">The range over which the edges of the <see cref="textureRect"/> should be blended.</param>
141141+ internal void DrawQuad(Quad vertexQuad, ColourInfo drawColour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null, Vector2? inflationPercentage = null,
142142+ Vector2? blendRangeOverride = null)
123143 {
124144 if (TextureGL == null || !TextureGL.Bind()) return;
125145126126- TextureGL.DrawQuad(vertexQuad, TextureBounds(textureRect), colour, vertexAction, inflationPercentage, blendRangeOverride);
146146+ TextureGL.DrawQuad(vertexQuad, drawColour, TextureBounds(textureRect), vertexAction, inflationPercentage, blendRangeOverride);
127147 }
128148129149 public override string ToString() => $@"{AssetName} ({Width}, {Height})";
+1
osu.Framework/Graphics/Textures/TextureAtlas.cs
···8585 Vector2I res = new Vector2I(0, currentY);
86868787 int maxY = currentY;
8888+8889 foreach (RectangleI bounds in subTextureBounds)
8990 {
9091 // +1 is required to prevent aliasing issues with sub-pixel positions while drawing. Bordering edged of other textures can show without it.
···335335 throw new InvalidOperationException($"Can not perform {nameof(Loop)} on an endless {nameof(TransformSequence<T>)}.");
336336337337 var iterDuration = endTime - startTime + pause;
338338+338339 foreach (var t in transforms)
339340 {
340341 Action tmpOnAbort = t.OnAbort;
···347348 // inserted in the correct order such that none of them trigger abortions on
348349 // each other due to instant re-sorting upon adding.
349350 double currentTransformTime = t.TargetTransformable.Time.Current;
351351+350352 while (t.EndTime <= currentTransformTime)
351353 {
352354 t.StartTime += iterDuration;
···101101 // dir is used so negative angles result in negative angularOffset.
102102 float angularOffset = dir * Math.Min(i * step, dir * angle);
103103 float normalisedOffset = angularOffset / MathHelper.TwoPi;
104104+104105 if (dir < 0)
105106 {
106107 normalisedOffset += 1.0f;
···293293 pos = Parent.ToSpaceOfOtherDrawable(pos, TextFlow);
294294295295 int i = 0;
296296+296297 foreach (Drawable d in TextFlow.Children)
297298 {
298299 if (d.DrawPosition.X + d.DrawSize.X / 2 > pos.X)
···449450450451 if (oldStart != selectionStart || oldEnd != selectionEnd)
451452 {
452452- audio.Sample.Get(@"Keyboard/key-movement")?.Play();
453453+ audio.Samples.Get(@"Keyboard/key-movement")?.Play();
453454 cursorAndLayout.Invalidate();
454455 }
455456 }
···468469 if (count == 0) return false;
469470470471 if (sound)
471471- audio.Sample.Get(@"Keyboard/key-delete")?.Play();
472472+ audio.Samples.Get(@"Keyboard/key-delete")?.Play();
472473473474 foreach (var d in TextFlow.Children.Skip(start).Take(count).ToArray()) //ToArray since we are removing items from the children in this block.
474475 {
···536537 foreach (char c in addText)
537538 {
538539 var ch = addCharacter(c);
540540+539541 if (ch == null)
540542 {
541543 notifyInputError();
···677679 if (!string.IsNullOrEmpty(pendingText) && !ReadOnly)
678680 {
679681 if (pendingText.Any(char.IsUpper))
680680- audio.Sample.Get(@"Keyboard/key-caps")?.Play();
682682+ audio.Samples.Get(@"Keyboard/key-caps")?.Play();
681683 else
682682- audio.Sample.Get($@"Keyboard/key-press-{RNG.Next(1, 5)}")?.Play();
684684+ audio.Samples.Get($@"Keyboard/key-press-{RNG.Next(1, 5)}")?.Play();
683685684686 insertString(pendingText);
685687 }
···704706 case Key.Escape:
705707 KillFocus();
706708 return true;
709709+707710 case Key.KeypadEnter:
708711 case Key.Enter:
709712 Commit();
···736739 Background.ClearTransforms();
737740 Background.FlashColour(BackgroundCommit, 400);
738741739739- audio.Sample.Get(@"Keyboard/key-confirm")?.Play();
742742+ audio.Samples.Get(@"Keyboard/key-confirm")?.Play();
740743 OnCommit?.Invoke(this, true);
741744 }
742745···949952 {
950953 //in the case of backspacing (or a NOP), we can exit early here.
951954 if (didDelete)
952952- audio.Sample.Get(@"Keyboard/key-delete")?.Play();
955955+ audio.Samples.Get(@"Keyboard/key-delete")?.Play();
953956 return;
954957 }
955958···957960 for (int i = matchCount; i < s.Length; i++)
958961 {
959962 Drawable dr = addCharacter(s[i]);
963963+960964 if (dr != null)
961965 {
962966 dr.Colour = Color4.Aqua;
···965969 }
966970 }
967971968968- audio.Sample.Get($@"Keyboard/key-press-{RNG.Next(1, 5)}")?.Play();
972972+ audio.Samples.Get($@"Keyboard/key-press-{RNG.Next(1, 5)}")?.Play();
969973 }
970974971975 #endregion
+37
osu.Framework/Graphics/Vector2Extensions.cs
···22// See the LICENCE file in the repository root for full licence text.
3344using System;
55+using System.Runtime.CompilerServices;
66+using osu.Framework.Graphics.Primitives;
57using osuTK;
6879namespace osu.Framework.Graphics
···7375 {
7476 result = (vec2.X - vec1.X) * (vec2.X - vec1.X) + (vec2.Y - vec1.Y) * (vec2.Y - vec1.Y);
7577 }
7878+7979+ /// <summary>
8080+ /// Retrieves the orientation of a set of vertices.
8181+ /// </summary>
8282+ /// <param name="vertices">The vertices.</param>
8383+ /// <returns>Twice the area enclosed by the vertices.
8484+ /// The vertices are clockwise-oriented if the value is positive.
8585+ /// The vertices are counter-clockwise-oriented if the value is negative.</returns>
8686+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
8787+ public static float GetOrientation(in ReadOnlySpan<Vector2> vertices)
8888+ {
8989+ if (vertices.Length == 0)
9090+ return 0;
9191+9292+ float rotation = 0;
9393+ for (int i = 0; i < vertices.Length - 1; ++i)
9494+ rotation += (vertices[i + 1].X - vertices[i].X) * (vertices[i + 1].Y + vertices[i].Y);
9595+9696+ rotation += (vertices[0].X - vertices[vertices.Length - 1].X) * (vertices[0].Y + vertices[vertices.Length - 1].Y);
9797+9898+ return rotation;
9999+ }
100100+101101+ /// <summary>
102102+ /// Determines whether a point is within the right half-plane of a line.
103103+ /// </summary>
104104+ /// <param name="line">The line.</param>
105105+ /// <param name="point">The point.</param>
106106+ /// <returns>Whether <paramref name="point"/> is in the right half-plane of <paramref name="line"/>.
107107+ /// If the point is colinear to the line, it is said to be in the right half-plane of the line.
108108+ /// </returns>
109109+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
110110+ public static bool InRightHalfPlaneOf(this Vector2 point, in Line line)
111111+ => (line.EndPoint.X - line.StartPoint.X) * (point.Y - line.StartPoint.Y)
112112+ - (line.EndPoint.Y - line.StartPoint.Y) * (point.X - line.StartPoint.X) <= 0;
76113 }
77114}
+10
osu.Framework/Graphics/Video/VideoDecoder.cs
···214214 case StdIo.SEEK_CUR:
215215 videoStream.Seek(offset, SeekOrigin.Current);
216216 break;
217217+217218 case StdIo.SEEK_END:
218219 videoStream.Seek(offset, SeekOrigin.End);
219220 break;
221221+220222 case StdIo.SEEK_SET:
221223 videoStream.Seek(offset, SeekOrigin.Begin);
222224 break;
225225+223226 case ffmpeg.AVSEEK_SIZE:
224227 return videoStream.Length;
228228+225229 default:
226230 return -1;
227231 }
···248252 throw new Exception("Could not find stream info.");
249253250254 var nStreams = formatContext->nb_streams;
255255+251256 for (var i = 0; i < nStreams; ++i)
252257 {
253258 stream = formatContext->streams[i];
254259255260 codecParams = *stream->codecpar;
261261+256262 if (codecParams.codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
257263 {
258264 timeBaseInSeconds = stream->time_base.GetValue();
···306312 if (readFrameResult >= 0)
307313 {
308314 state = DecoderState.Running;
315315+309316 if (packet->stream_index == stream->index)
310317 {
311318 if (ffmpeg.avcodec_send_packet(stream->codec, packet) < 0)
312319 throw new Exception("Error sending packet.");
313320314321 var result = ffmpeg.avcodec_receive_frame(stream->codec, frame);
322322+315323 if (result == 0)
316324 {
317325 var frameTime = (frame->best_effort_timestamp - stream->start_time) * timeBaseInSeconds * 1000;
326326+318327 if (!skipOutputUntilTime.HasValue || skipOutputUntilTime.Value < frameTime)
319328 {
320329 skipOutputUntilTime = null;
321330322331 SwsContext* swsCtx = null;
332332+323333 try
324334 {
325335 swsCtx = ffmpeg.sws_getContext(codecParams.width, codecParams.height, (AVPixelFormat)frame->format, codecParams.width, codecParams.height, AVPixelFormat.AV_PIX_FMT_RGBA, 0, null, null, null);
···2424 {
2525 case RuntimeInfo.Platform.MacOsx:
2626 return new MacOSGameHost(gameName, bindIPC, toolkitOptions, portableInstallation);
2727+2728 case RuntimeInfo.Platform.Linux:
2829 return new LinuxGameHost(gameName, bindIPC, toolkitOptions, portableInstallation);
3030+2931 case RuntimeInfo.Platform.Windows:
3032 return new WindowsGameHost(gameName, bindIPC, toolkitOptions, portableInstallation);
3333+3134 default:
3235 throw new InvalidOperationException($"Could not find a suitable host for the selected operating system ({Enum.GetName(typeof(RuntimeInfo.Platform), RuntimeInfo.OS)}).");
3336 }
+4
osu.Framework/IO/AsyncBufferStream.cs
···7676 return;
77777878 int last = -1;
7979+7980 while (!isLoaded && !isClosed)
8081 {
8182 cancellationToken.Token.ThrowIfCancellationRequested();
82838384 int curr = nextBlockToLoad;
8585+8486 if (curr < 0)
8587 {
8688 Thread.Sleep(1);
···211213 case SeekOrigin.Begin:
212214 Position = offset;
213215 break;
216216+214217 case SeekOrigin.Current:
215218 Position += offset;
216219 break;
220220+217221 case SeekOrigin.End:
218222 Position = data.Length + offset;
219223 break;
···5252 {
5353 int num = 0;
5454 int num2 = 0;
5555+5556 for (int i = 0; i < count; i++)
5657 {
5758 char ch = (char)bytes[offset + i];
5959+5860 if (paramEncode && ch == ' ')
5961 {
6062 num++;
···72747375 byte[] buffer = new byte[count + num2 * 2];
7476 int num4 = 0;
7777+7578 for (int j = 0; j < count; j++)
7679 {
7780 byte num6 = bytes[offset + j];
7881 char ch2 = (char)num6;
8282+7983 if (IsSafe(ch2))
8084 {
8185 buffer[num4++] = num6;
+2
osu.Framework/IO/Network/WebRequest.cs
···230230 private async Task internalPerform()
231231 {
232232 var url = Url;
233233+233234 if (!AllowInsecureRequests && !url.StartsWith(@"https://"))
234235 {
235236 logger.Add($"Insecure request was automatically converted to https ({Url})");
···459460 {
460461 // in the case we fail a request, spitting out the response in the log is quite helpful.
461462 ResponseStream.Seek(0, SeekOrigin.Begin);
463463+462464 using (StreamReader r = new StreamReader(ResponseStream, new UTF8Encoding(false, true), true, 1024, true))
463465 {
464466 try
-113
osu.Framework/IO/Stores/CachedResourceStore.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.
33-44-using System.Collections.Generic;
55-using System.Threading.Tasks;
66-77-namespace osu.Framework.IO.Stores
88-{
99- public class CachedResourceStore<T> : ResourceStore<T>
1010- {
1111- private readonly Dictionary<string, T> cache = new Dictionary<string, T>();
1212-1313- /// <summary>
1414- /// Initializes a resource store with no stores.
1515- /// </summary>
1616- public CachedResourceStore()
1717- {
1818- }
1919-2020- /// <summary>
2121- /// Initializes a resource store with a single store.
2222- /// </summary>
2323- /// <param name="stores">A collection of stores to add.</param>
2424- public CachedResourceStore(IResourceStore<T>[] stores)
2525- : base(stores)
2626- {
2727- }
2828-2929- /// <summary>
3030- /// Initializes a resource store with a collection of stores.
3131- /// </summary>
3232- /// <param name="store">The store.</param>
3333- public CachedResourceStore(IResourceStore<T> store)
3434- : base(store)
3535- {
3636- }
3737-3838- /// <summary>
3939- /// Notifies a bound delegate that the resource has changed.
4040- /// </summary>
4141- /// <param name="name">The resource that has changed.</param>
4242- protected override void NotifyChanged(string name)
4343- {
4444- cache.Remove(name);
4545- base.NotifyChanged(name);
4646- }
4747-4848- /// <summary>
4949- /// Adds a resource store to this store.
5050- /// </summary>
5151- /// <param name="store">The store to add.</param>
5252- public override void AddStore(IResourceStore<T> store)
5353- {
5454- base.AddStore(store);
5555-5656- if (store is ChangeableResourceStore<T> crm)
5757- crm.OnChanged += NotifyChanged;
5858- }
5959-6060- /// <summary>
6161- /// Removes a store from this store.
6262- /// </summary>
6363- /// <param name="store">The store to remove.</param>
6464- public override void RemoveStore(IResourceStore<T> store)
6565- {
6666- base.RemoveStore(store);
6767-6868- if (store is ChangeableResourceStore<T> crm)
6969- crm.OnChanged -= NotifyChanged;
7070- }
7171-7272- /// <summary>
7373- /// Retrieves an object from the store.
7474- /// </summary>
7575- /// <param name="name">The name of the object.</param>
7676- /// <returns>The object.</returns>
7777- public override async Task<T> GetAsync(string name)
7878- {
7979- if (cache.TryGetValue(name, out T result))
8080- return result;
8181-8282- result = await base.GetAsync(name);
8383-8484- if (result != null)
8585- cache[name] = result;
8686-8787- return result;
8888- }
8989-9090- /// <summary>
9191- /// Releases a resource from the cache.
9292- /// </summary>
9393- /// <param name="name">The resource to release.</param>
9494- public void Release(string name)
9595- {
9696- cache.Remove(name);
9797- }
9898-9999- /// <summary>
100100- /// Releases all resources stored in the cache.
101101- /// </summary>
102102- public void ResetCache()
103103- {
104104- cache.Clear();
105105- }
106106-107107- protected override void Dispose(bool disposing)
108108- {
109109- base.Dispose(disposing);
110110- ResetCache();
111111- }
112112- }
113113-}
···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 System;
55-66-namespace osu.Framework.IO.Stores
77-{
88- public class ChangeableResourceStore<T> : ResourceStore<T>
99- {
1010- public event Action<string> OnChanged;
1111-1212- protected void TriggerOnChanged(string name)
1313- {
1414- OnChanged?.Invoke(name);
1515- }
1616- }
1717-}
+5-4
osu.Framework/IO/Stores/DllResourceStore.cs
···5454 /// <summary>
5555 /// Retrieve a list of available resources provided by this store.
5656 /// </summary>
5757- public IEnumerable<string> AvailableResources =>
5757+ public IEnumerable<string> GetAvailableResources() =>
5858 assembly.GetManifestResourceNames().Select(n =>
5959 {
6060- var chars = n.ToCharArray();
6060+ n = n.Substring(n.StartsWith(prefix) ? prefix.Length + 1 : 0);
61616262- int startIndex = n.StartsWith(prefix) ? prefix.Length + 1 : 0;
6362 int lastDot = n.LastIndexOf('.');
64636565- for (int i = startIndex; i < lastDot; i++)
6464+ var chars = n.ToCharArray();
6565+6666+ for (int i = 0; i < lastDot; i++)
6667 if (chars[i] == '.')
6768 chars[i] = '/';
6869
···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 System.IO;
55-using System.Threading.Tasks;
66-77-namespace osu.Framework.IO.Stores
88-{
99- public class FileSystemResourceStore : ChangeableResourceStore<byte[]>
1010- {
1111- private readonly FileSystemWatcher watcher;
1212- private readonly string directory;
1313-1414- private bool isDisposed;
1515-1616- public FileSystemResourceStore(string directory)
1717- {
1818- this.directory = directory;
1919-2020- watcher = new FileSystemWatcher(directory)
2121- {
2222- EnableRaisingEvents = true
2323- };
2424- watcher.Renamed += watcherChanged;
2525- watcher.Changed += watcherChanged;
2626- watcher.Created += watcherChanged;
2727- }
2828-2929- #region Disposal
3030-3131- ~FileSystemResourceStore()
3232- {
3333- Dispose(false);
3434- }
3535-3636- protected override void Dispose(bool disposing)
3737- {
3838- if (isDisposed)
3939- return;
4040-4141- isDisposed = true;
4242-4343- watcher.Dispose();
4444- }
4545-4646- #endregion
4747-4848- private void watcherChanged(object sender, FileSystemEventArgs e)
4949- {
5050- TriggerOnChanged(e.FullPath.Replace(directory, string.Empty));
5151- }
5252-5353- public override async Task<byte[]> GetAsync(string name)
5454- {
5555- byte[] result;
5656-5757- using (FileStream stream = System.IO.File.OpenRead(Path.Combine(directory, name)))
5858- {
5959- result = new byte[stream.Length];
6060- await stream.ReadAsync(result, 0, (int)stream.Length);
6161- }
6262-6363- return result;
6464- }
6565- }
6666-}
+2
osu.Framework/IO/Stores/FontStore.cs
···4343 case FontStore fs:
4444 nestedFontStores.Add(fs);
4545 return;
4646+4647 case GlyphStore gs:
4748 glyphStores.Add(gs);
4849 queueLoad(gs);
···8586 case FontStore fs:
8687 nestedFontStores.Remove(fs);
8788 return;
8989+8890 case GlyphStore gs:
8991 glyphStores.Remove(gs);
9092 break;
+3
osu.Framework/IO/Stores/GlyphStore.cs
···22// See the LICENCE file in the repository root for full licence text.
3344using System;
55+using System.Collections.Generic;
56using System.IO;
67using System.Linq;
78using System.Threading.Tasks;
···139140 }
140141141142 public Stream GetStream(string name) => throw new NotSupportedException();
143143+144144+ public IEnumerable<string> GetAvailableResources() => Font.Characters.Keys.Select(k => $"{FontName}/{(char)k}");
142145143146 private int loadedPageCount;
144147 private int loadedGlyphCount;
+7
osu.Framework/IO/Stores/IResourceStore.cs
···22// See the LICENCE file in the repository root for full licence text.
3344using System;
55+using System.Collections.Generic;
56using System.IO;
67using System.Threading.Tasks;
78···2425 Task<T> GetAsync(string name);
25262627 Stream GetStream(string name);
2828+2929+ /// <summary>
3030+ /// Gets a collection of string representations of the resources available in this store.
3131+ /// </summary>
3232+ /// <returns>String representations of the resources available.</returns>
3333+ IEnumerable<string> GetAvailableResources();
2734 }
2835}
···22// See the LICENCE file in the repository root for full licence text.
3344using System.Collections.Generic;
55+using System.Linq;
5667namespace osu.Framework.IO.Stores
78{
···2122 }
22232324 protected override IEnumerable<string> GetFilenames(string name) => base.GetFilenames($@"{Namespace}/{name}");
2525+2626+ public override IEnumerable<string> GetAvailableResources() => base.GetAvailableResources()
2727+ .Where(x => x.StartsWith($"{Namespace}/"))
2828+ .Select(x => x.Remove(0, $"{Namespace}/".Length));
2429 }
2530}
+5-1
osu.Framework/IO/Stores/OnlineStore.cs
···11-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
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.
3344using System;
55+using System.Collections.Generic;
56using System.IO;
77+using System.Linq;
68using System.Threading.Tasks;
79using WebRequest = osu.Framework.IO.Network.WebRequest;
810···49515052 return new MemoryStream(ret);
5153 }
5454+5555+ public IEnumerable<string> GetAvailableResources() => Enumerable.Empty<string>();
52565357 #region IDisposable Support
5458
···2020 if (!handler.Initialize(Host)) return;
21212222 int index = inputHandlers.BinarySearch(handler, new InputHandlerComparer());
2323+2324 if (index < 0)
2425 {
2526 index = ~index;
···141141 {
142142 case MouseButton.Left:
143143 return new MouseLeftButtonEventManager(button);
144144+144145 default:
145146 return new MouseMinorButtonEventManager(button);
146147 }
···260261 if (!(keyboardRepeatKey is Key key)) return;
261262262263 keyboardRepeatTime -= Time.Elapsed;
264264+263265 while (keyboardRepeatTime < 0)
264266 {
265267 handleKeyDown(state, key, true);
···358360 }
359361360362 d.IsHovered = true;
363363+361364 if (d.TriggerEvent(new HoverEvent(state)))
362365 {
363366 hoverHandledDrawable = d;
···430433 case MousePositionChangeEvent mousePositionChange:
431434 HandleMousePositionChange(mousePositionChange);
432435 return;
436436+433437 case MouseScrollChangeEvent mouseScrollChange:
434438 HandleMouseScrollChange(mouseScrollChange);
435439 return;
440440+436441 case ButtonStateChangeEvent<MouseButton> mouseButtonStateChange:
437442 HandleMouseButtonStateChange(mouseButtonStateChange);
438443 return;
444444+439445 case ButtonStateChangeEvent<Key> keyboardKeyStateChange:
440446 HandleKeyboardKeyStateChange(keyboardKeyStateChange);
441447 return;
448448+442449 case ButtonStateChangeEvent<JoystickButton> joystickButtonStateChange:
443450 HandleJoystickButtonStateChange(joystickButtonStateChange);
444451 return;
···518525 {
519526 //ensure we are visible
520527 CompositeDrawable d = FocusedDrawable.Parent;
528528+521529 while (d != null)
522530 {
523531 if (!d.IsPresent || !d.IsAlive)
+1
osu.Framework/Input/MouseButtonEventManager.cs
···171171172172 // only drawables up to the one that handled mouse down should handle mouse up
173173 MouseDownInputQueue = positionalInputQueue;
174174+174175 if (handledBy != null)
175176 {
176177 var count = MouseDownInputQueue.IndexOf(handledBy) + 1;
+1
osu.Framework/Input/StateChanges/ButtonInput.cs
···6565 public void Apply(InputState state, IInputStateChangeHandler handler)
6666 {
6767 var buttonStates = GetButtonStates(state);
6868+6869 foreach (var entry in Entries)
6970 {
7071 if (buttonStates.SetPressed(entry.Button, entry.IsPressed))
···2626 public void Apply(InputState state, IInputStateChangeHandler handler)
2727 {
2828 var mouse = state.Mouse;
2929+2930 if (Delta != Vector2.Zero)
3031 {
3132 var lastScroll = mouse.Scroll;
+1
osu.Framework/Logging/Logger.cs
···204204 lock (static_sync_lock)
205205 {
206206 var nameLower = name.ToLower();
207207+207208 if (!static_loggers.TryGetValue(nameLower, out Logger l))
208209 {
209210 static_loggers[nameLower] = l = Enum.TryParse(name, true, out LoggingTarget target) ? new Logger(target) : new Logger(name);
···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 System;
55+using System.Runtime.CompilerServices;
66+using osu.Framework.Graphics;
77+using osu.Framework.Graphics.Primitives;
88+using osuTK;
99+1010+namespace osu.Framework.MathUtils.Clipping
1111+{
1212+ public readonly ref struct ConvexPolygonClipper<TClip, TSubject>
1313+ where TClip : IConvexPolygon
1414+ where TSubject : IConvexPolygon
1515+ {
1616+ private readonly TClip clipPolygon;
1717+ private readonly TSubject subjectPolygon;
1818+1919+ public ConvexPolygonClipper(ref TClip clipPolygon, ref TSubject subjectPolygon)
2020+ {
2121+ this.clipPolygon = clipPolygon;
2222+ this.subjectPolygon = subjectPolygon;
2323+ }
2424+2525+ /// <summary>
2626+ /// Determines the minimum buffer size required to clip the two polygons.
2727+ /// </summary>
2828+ /// <returns>The minimum buffer size required for <see cref="clipPolygon"/> to clip <see cref="subjectPolygon"/>.</returns>
2929+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
3030+ public int GetClipBufferSize()
3131+ {
3232+ // There can only be at most two intersections for each of the subject's vertices
3333+ return subjectPolygon.GetVertices().Length * 2;
3434+ }
3535+3636+ /// <summary>
3737+ /// Clips <see cref="subjectPolygon"/> by <see cref="clipPolygon"/>.
3838+ /// </summary>
3939+ /// <returns>A clockwise-ordered set of vertices representing the result of clipping <see cref="subjectPolygon"/> by <see cref="clipPolygon"/>.</returns>
4040+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
4141+ public Span<Vector2> Clip() => Clip(new Vector2[GetClipBufferSize()]);
4242+4343+ /// <summary>
4444+ /// Clips <see cref="subjectPolygon"/> by <see cref="clipPolygon"/> using an intermediate buffer.
4545+ /// </summary>
4646+ /// <param name="buffer">The buffer to contain the clipped vertices. Must have a length of <see cref="GetClipBufferSize"/>.</param>
4747+ /// <returns>A clockwise-ordered set of vertices representing the result of clipping <see cref="subjectPolygon"/> by <see cref="clipPolygon"/>.</returns>
4848+ public Span<Vector2> Clip(in Span<Vector2> buffer)
4949+ {
5050+ if (buffer.Length < GetClipBufferSize())
5151+ {
5252+ throw new ArgumentException($"Clip buffer must have a length of {GetClipBufferSize()}, but was {buffer.Length}."
5353+ + "Use GetClipBufferSize() to calculate the size of the buffer.", nameof(buffer));
5454+ }
5555+5656+ ReadOnlySpan<Vector2> origSubjectVertices = subjectPolygon.GetVertices();
5757+ if (origSubjectVertices.Length == 0)
5858+ return Span<Vector2>.Empty;
5959+6060+ ReadOnlySpan<Vector2> origClipVertices = clipPolygon.GetVertices();
6161+ if (origClipVertices.Length == 0)
6262+ return Span<Vector2>.Empty;
6363+6464+ // Add the subject vertices to the buffer and immediately normalise them
6565+ Span<Vector2> subjectVertices = getNormalised(origSubjectVertices, buffer.Slice(0, origSubjectVertices.Length), true);
6666+6767+ // Since the clip vertices aren't modified, we can use them as they are if they are normalised
6868+ // However if they are not normalised, then we must add them to the buffer and normalise them there
6969+ bool clipNormalised = Vector2Extensions.GetOrientation(origClipVertices) >= 0;
7070+ Span<Vector2> clipBuffer = clipNormalised ? null : stackalloc Vector2[origClipVertices.Length];
7171+ ReadOnlySpan<Vector2> clipVertices = clipNormalised
7272+ ? origClipVertices
7373+ : getNormalised(origClipVertices, clipBuffer, false);
7474+7575+ // Number of vertices in the buffer that need to be tested against
7676+ // This becomes the number of vertices in the resulting polygon after each clipping iteration
7777+ int inputCount = subjectVertices.Length;
7878+7979+ // Process the clip edge connecting the last vertex to the first vertex
8080+ inputCount = processClipEdge(new Line(clipVertices[clipVertices.Length - 1], clipVertices[0]), buffer, inputCount);
8181+8282+ // Process all other edges
8383+ for (int c = 1; c < clipVertices.Length; c++)
8484+ {
8585+ if (inputCount == 0)
8686+ break;
8787+8888+ inputCount = processClipEdge(new Line(clipVertices[c - 1], clipVertices[c]), buffer, inputCount);
8989+ }
9090+9191+ return buffer.Slice(0, inputCount);
9292+ }
9393+9494+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
9595+ private int processClipEdge(in Line clipEdge, in Span<Vector2> buffer, in int inputCount)
9696+ {
9797+ // Temporary storage for the vertices from the buffer as the buffer gets altered
9898+ Span<Vector2> inputVertices = stackalloc Vector2[buffer.Length];
9999+100100+ // Store the original vertices (buffer will get altered)
101101+ buffer.CopyTo(inputVertices);
102102+103103+ int outputCount = 0;
104104+105105+ // Process the edge connecting the last vertex with the first vertex
106106+ outputVertices(ref inputVertices[inputCount - 1], ref inputVertices[0], clipEdge, buffer, ref outputCount);
107107+108108+ // Process all other vertices
109109+ for (int i = 1; i < inputCount; i++)
110110+ outputVertices(ref inputVertices[i - 1], ref inputVertices[i], clipEdge, buffer, ref outputCount);
111111+112112+ return outputCount;
113113+ }
114114+115115+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
116116+ private void outputVertices(ref Vector2 startVertex, ref Vector2 endVertex, in Line clipEdge, in Span<Vector2> buffer, ref int bufferIndex)
117117+ {
118118+ if (endVertex.InRightHalfPlaneOf(clipEdge))
119119+ {
120120+ if (!startVertex.InRightHalfPlaneOf(clipEdge))
121121+ {
122122+ clipEdge.TryIntersectWith(ref startVertex, ref endVertex, out var t);
123123+ buffer[bufferIndex++] = clipEdge.At(t);
124124+ }
125125+126126+ buffer[bufferIndex++] = endVertex;
127127+ }
128128+ else if (startVertex.InRightHalfPlaneOf(clipEdge))
129129+ {
130130+ clipEdge.TryIntersectWith(ref startVertex, ref endVertex, out var t);
131131+ buffer[bufferIndex++] = clipEdge.At(t);
132132+ }
133133+ }
134134+135135+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
136136+ private Span<Vector2> getNormalised(in ReadOnlySpan<Vector2> original, in Span<Vector2> bufferSlice, bool verify)
137137+ {
138138+ original.CopyTo(bufferSlice);
139139+140140+ if (!verify || Vector2Extensions.GetOrientation(original) < 0)
141141+ bufferSlice.Reverse();
142142+143143+ return bufferSlice;
144144+ }
145145+ }
146146+}
+23
osu.Framework/MathUtils/Interpolation.cs
···7373 {
7474 int n = points.Length;
7575 double[] w = new double[n];
7676+7677 for (int i = 0; i < n; i++)
7778 {
7879 w[i] = 1;
···257258 case Easing.In:
258259 case Easing.InQuad:
259260 return time * time;
261261+260262 case Easing.Out:
261263 case Easing.OutQuad:
262264 return time * (2 - time);
265265+263266 case Easing.InOutQuad:
264267 if (time < .5) return time * time * 2;
265268···267270268271 case Easing.InCubic:
269272 return time * time * time;
273273+270274 case Easing.OutCubic:
271275 return --time * time * time + 1;
276276+272277 case Easing.InOutCubic:
273278 if (time < .5) return time * time * time * 4;
274279···276281277282 case Easing.InQuart:
278283 return time * time * time * time;
284284+279285 case Easing.OutQuart:
280286 return 1 - --time * time * time * time;
287287+281288 case Easing.InOutQuart:
282289 if (time < .5) return time * time * time * time * 8;
283290···285292286293 case Easing.InQuint:
287294 return time * time * time * time * time;
295295+288296 case Easing.OutQuint:
289297 return --time * time * time * time * time + 1;
298298+290299 case Easing.InOutQuint:
291300 if (time < .5) return time * time * time * time * time * 16;
292301···294303295304 case Easing.InSine:
296305 return 1 - Math.Cos(time * Math.PI * .5);
306306+297307 case Easing.OutSine:
298308 return Math.Sin(time * Math.PI * .5);
309309+299310 case Easing.InOutSine:
300311 return .5 - .5 * Math.Cos(Math.PI * time);
301312302313 case Easing.InExpo:
303314 return Math.Pow(2, 10 * (time - 1));
315315+304316 case Easing.OutExpo:
305317 return -Math.Pow(2, -10 * time) + 1;
318318+306319 case Easing.InOutExpo:
307320 if (time < .5) return .5 * Math.Pow(2, 20 * time - 10);
308321···310323311324 case Easing.InCirc:
312325 return 1 - Math.Sqrt(1 - time * time);
326326+313327 case Easing.OutCirc:
314328 return Math.Sqrt(1 - --time * time);
329329+315330 case Easing.InOutCirc:
316331 if ((time *= 2) < 1) return .5 - .5 * Math.Sqrt(1 - time * time);
317332···319334320335 case Easing.InElastic:
321336 return -Math.Pow(2, -10 + 10 * time) * Math.Sin((1 - elastic_const2 - time) * elastic_const);
337337+322338 case Easing.OutElastic:
323339 return Math.Pow(2, -10 * time) * Math.Sin((time - elastic_const2) * elastic_const) + 1;
340340+324341 case Easing.OutElasticHalf:
325342 return Math.Pow(2, -10 * time) * Math.Sin((.5 * time - elastic_const2) * elastic_const) + 1;
343343+326344 case Easing.OutElasticQuarter:
327345 return Math.Pow(2, -10 * time) * Math.Sin((.25 * time - elastic_const2) * elastic_const) + 1;
346346+328347 case Easing.InOutElastic:
329348 if ((time *= 2) < 1)
330349 return -.5 * Math.Pow(2, -10 + 10 * time) * Math.Sin((1 - elastic_const2 * 1.5 - time) * elastic_const / 1.5);
···333352334353 case Easing.InBack:
335354 return time * time * ((back_const + 1) * time - back_const);
355355+336356 case Easing.OutBack:
337357 return --time * time * ((back_const + 1) * time + back_const) + 1;
358358+338359 case Easing.InOutBack:
339360 if ((time *= 2) < 1) return .5 * time * time * ((back_const2 + 1) * time - back_const2);
340361···350371 return 1 - (7.5625 * (time -= 2.25 * bounce_const) * time + .9375);
351372352373 return 1 - (7.5625 * (time -= 2.625 * bounce_const) * time + .984375);
374374+353375 case Easing.OutBounce:
354376 if (time < bounce_const)
355377 return 7.5625 * time * time;
···359381 return 7.5625 * (time -= 2.25 * bounce_const) * time + .9375;
360382361383 return 7.5625 * (time -= 2.625 * bounce_const) * time + .984375;
384384+362385 case Easing.InOutBounce:
363386 if (time < .5) return .5 - .5 * ApplyEasing(Easing.OutBounce, 1 - time * 2);
364387
+4
osu.Framework/MathUtils/PathApproximator.cs
···5252 while (toFlatten.Count > 0)
5353 {
5454 Vector2[] parent = toFlatten.Pop();
5555+5556 if (bezierIsFlatEnough(parent))
5657 {
5758 // If the control points we currently operate on are sufficiently "flat", we use
···155156 // AC B lies.
156157 Vector2 orthoAtoC = c - a;
157158 orthoAtoC = new Vector2(orthoAtoC.Y, -orthoAtoC.X);
159159+158160 if (Vector2.Dot(orthoAtoC, b - a) < 0)
159161 {
160162 dir = -dir;
···211213212214 float minX = controlPoints[0].X;
213215 float maxX = controlPoints[0].X;
216216+214217 for (int i = 1; i < controlPoints.Length; i++)
215218 {
216219 minX = Math.Min(minX, controlPoints[i].X);
···293296 l[count + i] = r[i + 1];
294297295298 output.Add(controlPoints[0]);
299299+296300 for (int i = 1; i < count - 1; ++i)
297301 {
298302 int index = 2 * i;
+3
osu.Framework/Physics/RigidBodyContainer.cs
···127127 float usableLength = Math.Max(length - 2 * cornerRadius, 0);
128128129129 Vector2 normal = (b - a).PerpendicularRight.Normalized();
130130+130131 for (int j = 0; j < amount_side_steps; ++j)
131132 {
132133 Vertices.Add(a + dir * (cornerRadius + j * usableLength / (amount_side_steps - 1)));
···135136 }
136137137138 const int amount_corner_steps = 10;
139139+138140 if (cornerRadius > 0)
139141 {
140142 // Rounded corners
···207209 return false;
208210209211 bool didCollide = false;
212212+210213 for (int i = 0; i < Vertices.Count; ++i)
211214 {
212215 if (other.BodyContains(Vector2Extensions.Transform(Vertices[i], SimulationToScreenSpace)))
+4
osu.Framework/Platform/DesktopGameWindow.cs
···176176 case Input.ConfineMouseMode.Fullscreen:
177177 confine = WindowMode.Value != Configuration.WindowMode.Windowed;
178178 break;
179179+179180 case Input.ConfineMouseMode.Always:
180181 confine = true;
181182 break;
···202203 try
203204 {
204205 inWindowModeTransition = true;
206206+205207 switch (newMode)
206208 {
207209 case Configuration.WindowMode.Fullscreen:
···210212211213 WindowState = WindowState.Fullscreen;
212214 break;
215215+213216 case Configuration.WindowMode.Borderless:
214217 if (lastFullscreenDisplay != null)
215218 RestoreResolution(lastFullscreenDisplay);
···222225 ClientSize = new Size(currentDisplay.Bounds.Width + 1, currentDisplay.Bounds.Height + 1);
223226 Location = currentDisplay.Bounds.Location;
224227 break;
228228+225229 case Configuration.WindowMode.Windowed:
226230 if (lastFullscreenDisplay != null)
227231 RestoreResolution(lastFullscreenDisplay);
···1515using System.Drawing;
1616using JetBrains.Annotations;
1717using osu.Framework.Bindables;
1818+using osu.Framework.Graphics;
1819using Icon = osuTK.Icon;
19202021namespace osu.Framework.Platform
2122{
2222- public abstract class GameWindow : IGameWindow
2323+ public abstract class GameWindow : IWindow
2324 {
2425 /// <summary>
2526 /// The <see cref="IGraphicsContext"/> associated with this <see cref="GameWindow"/>.
···86878788 FocusedChanged += (o, e) => isActive.Value = Focused;
88898989- SupportedWindowModes.AddRange(DefaultSupportedWindowModes);
9090+ supportedWindowModes.AddRange(DefaultSupportedWindowModes);
90919192 bool firstUpdate = true;
9293 UpdateFrame += (o, e) =>
···228229 /// devices. This usually corresponds to areas of the screen hidden under notches and rounded corners.
229230 /// The safe area insets are provided by the operating system and dynamically change as the user rotates the device.
230231 /// </summary>
231231- public readonly BindableMarginPadding SafeAreaPadding = new BindableMarginPadding();
232232+ public virtual IBindable<MarginPadding> SafeAreaPadding { get; } = new BindableMarginPadding();
232233233233- public readonly BindableList<WindowMode> SupportedWindowModes = new BindableList<WindowMode>();
234234+ private readonly BindableList<WindowMode> supportedWindowModes = new BindableList<WindowMode>();
235235+236236+ public IBindableList<WindowMode> SupportedWindowModes => supportedWindowModes;
234237235238 public virtual WindowMode DefaultWindowMode => SupportedWindowModes.First();
236239···249252 case Configuration.WindowMode.Windowed:
250253 currentValue = Configuration.WindowMode.Borderless;
251254 break;
255255+252256 case Configuration.WindowMode.Borderless:
253257 currentValue = Configuration.WindowMode.Fullscreen;
254258 break;
259259+255260 case Configuration.WindowMode.Fullscreen:
256261 currentValue = Configuration.WindowMode.Windowed;
257262 break;
+91
osu.Framework/Platform/IWindow.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.
33+44+using System;
55+using System.Collections.Generic;
66+using JetBrains.Annotations;
77+using osu.Framework.Bindables;
88+using osu.Framework.Configuration;
99+using osu.Framework.Graphics;
1010+using osuTK;
1111+using osuTK.Platform;
1212+1313+namespace osu.Framework.Platform
1414+{
1515+ /// <summary>
1616+ /// Interface representation of the game window, intended to hide any implementation-specific code.
1717+ /// Currently inherits from osuTK; this will be removed as part of the <see cref="GameWindow"/> refactor.
1818+ /// </summary>
1919+ public interface IWindow : IGameWindow
2020+ {
2121+ /// <summary>
2222+ /// Cycles through the available <see cref="WindowMode"/>s as determined by <see cref="SupportedWindowModes"/>.
2323+ /// </summary>
2424+ void CycleMode();
2525+2626+ /// <summary>
2727+ /// Configures the <see cref="IWindow"/> based on the provided <see cref="FrameworkConfigManager"/>.
2828+ /// </summary>
2929+ /// <param name="config">The configuration manager to use.</param>
3030+ void SetupWindow(FrameworkConfigManager config);
3131+3232+ /// <summary>
3333+ /// Return value decides whether we should intercept and cancel this exit (if possible).
3434+ /// </summary>
3535+ [CanBeNull]
3636+ event Func<bool> ExitRequested;
3737+3838+ /// <summary>
3939+ /// Invoked when the <see cref="IWindow"/> has closed.
4040+ /// </summary>
4141+ [CanBeNull]
4242+ event Action Exited;
4343+4444+ /// <summary>
4545+ /// Whether the OS cursor is currently contained within the game window.
4646+ /// </summary>
4747+ bool CursorInWindow { get; }
4848+4949+ /// <summary>
5050+ /// Controls the state of the OS cursor.
5151+ /// </summary>
5252+ CursorState CursorState { get; set; }
5353+5454+ /// <summary>
5555+ /// Controls the vertical sync mode of the screen.
5656+ /// </summary>
5757+ VSyncMode VSync { get; set; }
5858+5959+ /// <summary>
6060+ /// Returns the default <see cref="WindowMode"/> for the implementation.
6161+ /// </summary>
6262+ WindowMode DefaultWindowMode { get; }
6363+6464+ /// <summary>
6565+ /// Gets the <see cref="DisplayDevice"/> that this window is currently on.
6666+ /// </summary>
6767+ DisplayDevice CurrentDisplay { get; }
6868+6969+ /// <summary>
7070+ /// Whether this <see cref="IWindow"/> is active (in the foreground).
7171+ /// </summary>
7272+ IBindable<bool> IsActive { get; }
7373+7474+ /// <summary>
7575+ /// Provides a <see cref="IBindable{MarginPadding}"/> that can be used to keep track of the "safe area" insets on mobile
7676+ /// devices. This usually corresponds to areas of the screen hidden under notches and rounded corners.
7777+ /// The safe area insets are provided by the operating system and dynamically change as the user rotates the device.
7878+ /// </summary>
7979+ IBindable<MarginPadding> SafeAreaPadding { get; }
8080+8181+ /// <summary>
8282+ /// The <see cref="WindowMode"/>s supported by this <see cref="IWindow"/> implementation.
8383+ /// </summary>
8484+ IBindableList<WindowMode> SupportedWindowModes { get; }
8585+8686+ /// <summary>
8787+ /// Available resolutions for full-screen display.
8888+ /// </summary>
8989+ IEnumerable<DisplayResolution> AvailableResolutions { get; }
9090+ }
9191+}
+1
osu.Framework/Platform/Linux/Native/Library.cs
···2222 public static void Load(string library, LoadFlags flags)
2323 {
2424 var paths = (string)AppContext.GetData("NATIVE_DLL_SEARCH_DIRECTORIES");
2525+2526 foreach (var path in paths.Split(':'))
2627 {
2728 if (dlopen(Path.Combine(path, library), flags) != IntPtr.Zero)
+1
osu.Framework/Platform/MacOS/MacOSGameWindow.cs
···130130 {
131131 // update the window mode if we have an update queued
132132 WindowMode? mode = pendingWindowMode;
133133+133134 if (mode.HasValue)
134135 {
135136 pendingWindowMode = null;
···8383 {
8484 int requested = Math.Min(width, height);
8585 int closest = -1;
8686+8687 for (int i = 0; i < iconDir.Count; i++)
8788 {
8889 var entry = iconDir.Entries[i];
···11+// Automatically included for every vertex shader.
22+33+attribute float m_BackbufferDrawDepth;
44+55+// Whether the backbuffer is currently being drawn to
66+uniform bool g_BackbufferDraw;
77+88+void main()
99+{
1010+ {{ real_main }}(); // Invoke real main func
1111+1212+ if (g_BackbufferDraw)
1313+ gl_Position.z = m_BackbufferDrawDepth;
1414+}
···4646 throw new InvalidCastException($"The test runner must be a {nameof(Game)}.");
47474848 runTask = Task.Factory.StartNew(() => host.Run(game), TaskCreationOptions.LongRunning);
4949+4950 while (!game.IsLoaded)
5051 {
5152 checkForErrors();
+3-2
osu.Framework/Threading/ThreadedTaskScheduler.cs
···2424 /// Initializes a new instance of the StaTaskScheduler class with the specified concurrency level.
2525 /// </summary>
2626 /// <param name="numberOfThreads">The number of threads that should be created and used by this scheduler.</param>
2727- public ThreadedTaskScheduler(int numberOfThreads)
2727+ /// <param name="name">The thread name to give threads in this pool.</param>
2828+ public ThreadedTaskScheduler(int numberOfThreads, string name)
2829 {
2930 if (numberOfThreads < 1)
3031 throw new ArgumentOutOfRangeException(nameof(numberOfThreads));
···3536 {
3637 var thread = new Thread(processTasks)
3738 {
3838- Name = "LoadComponentThreadPool",
3939+ Name = $"ThreadedTaskScheduler ({name})",
3940 IsBackground = true
4041 };
4142