···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.Reflection;
45using System.Threading;
56using System.Threading.Tasks;
67using NUnit.Framework;
···2223 public void SetUp()
2324 {
2425 thread = new AudioThread();
2525- store = new NamespacedResourceStore<byte[]>(new DllResourceStore(@"osu.Framework.dll"), @"Resources");
2626+ store = new NamespacedResourceStore<byte[]>(new DllResourceStore(new AssemblyName("osu.Framework")), @"Resources");
26272728 manager = new AudioManager(thread, store, store);
2829
···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 NUnit.Framework;
66+77+namespace osu.Framework.Tests.Audio
88+{
99+ /// <remarks>
1010+ /// This unit will ALWAYS SKIP if the system does not have a physical audio device!!!
1111+ /// A physical audio device is required to simulate the "loss" of it during playback.
1212+ /// </remarks>
1313+ [TestFixture]
1414+ public class DeviceLosingAudioTest : AudioThreadTest
1515+ {
1616+ public override void SetUp()
1717+ {
1818+ base.SetUp();
1919+2020+ // wait for any device to be initialized
2121+ Manager.WaitForDeviceChange(-1);
2222+2323+ // if the initialized device is "No sound", it indicates that no other physical devices are available, so this unit should be ignored
2424+ if (Manager.CurrentDevice == 0)
2525+ Assert.Ignore("Physical audio devices are required for this unit.");
2626+2727+ // we don't want music playing in unit tests :)
2828+ Manager.Volume.Value = 0;
2929+ }
3030+3131+ [Test]
3232+ public void TestPlaybackWithDeviceLoss() => testPlayback(Manager.SimulateDeviceRestore, Manager.SimulateDeviceLoss);
3333+3434+ [Test]
3535+ public void TestPlaybackWithDeviceRestore() => testPlayback(Manager.SimulateDeviceLoss, Manager.SimulateDeviceRestore);
3636+3737+ private void testPlayback(Action preparation, Action simulate)
3838+ {
3939+ preparation();
4040+4141+ var track = Manager.Tracks.Get("Tracks.sample-track.mp3");
4242+4343+ // start track
4444+ track.Restart();
4545+4646+ WaitForOrAssert(() => track.IsRunning, "Track did not start running");
4747+4848+ WaitForOrAssert(() => track.CurrentTime > 0, "Track did not start running");
4949+5050+ // simulate change (loss/restore)
5151+ simulate();
5252+5353+ CheckTrackIsProgressing(track);
5454+5555+ // stop track
5656+ track.Stop();
5757+5858+ WaitForOrAssert(() => !track.IsRunning, "Track did not stop", 1000);
5959+6060+ // seek track
6161+ track.Seek(0);
6262+6363+ Assert.IsFalse(track.IsRunning);
6464+ WaitForOrAssert(() => track.CurrentTime == 0, "Track did not seek correctly", 1000);
6565+ }
6666+ }
6767+}
+44
osu.Framework.Tests/Audio/DevicelessAudioTest.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 NUnit.Framework;
55+66+namespace osu.Framework.Tests.Audio
77+{
88+ [TestFixture]
99+ public class DevicelessAudioTest : AudioThreadTest
1010+ {
1111+ public override void SetUp()
1212+ {
1313+ base.SetUp();
1414+1515+ // lose all devices
1616+ Manager.SimulateDeviceLoss();
1717+ }
1818+1919+ [Test]
2020+ public void TestPlayTrackWithoutDevices()
2121+ {
2222+ var track = Manager.Tracks.Get("Tracks.sample-track.mp3");
2323+2424+ // start track
2525+ track.Restart();
2626+ Assert.IsTrue(track.IsRunning);
2727+2828+ CheckTrackIsProgressing(track);
2929+3030+ // stop track
3131+ track.Stop();
3232+3333+ WaitForOrAssert(() => !track.IsRunning, "Track did not stop", 1000);
3434+3535+ Assert.IsFalse(track.IsRunning);
3636+3737+ // seek track
3838+ track.Seek(0);
3939+4040+ Assert.IsFalse(track.IsRunning);
4141+ WaitForOrAssert(() => track.CurrentTime == 0, "Track did not seek correctly", 1000);
4242+ }
4343+ }
4444+}
+1-1
osu.Framework.Tests/Audio/TrackBassTest.cs
···2626 // Initialize bass with no audio to make sure the test remains consistent even if there is no audio device.
2727 Bass.Init(0);
28282929- resources = new DllResourceStore("osu.Framework.Tests.dll");
2929+ resources = new DllResourceStore(typeof(TrackBassTest).Assembly);
30303131 track = new TrackBass(resources.GetStream("Resources.Tracks.sample-track.mp3"));
3232 updateTrack();
+4-6
osu.Framework.Tests/IO/FontStoreTest.cs
···1717 [OneTimeSetUp]
1818 public void OneTimeSetUp()
1919 {
2020- storage = new TemporaryNativeStorage("fontstore-test");
2121- fontResourceStore = new NamespacedResourceStore<byte[]>(new DllResourceStore(typeof(Drawable).Assembly.Location), "Resources.Fonts.OpenSans");
2222-2323- storage.GetFullPath("./", true);
2020+ storage = new TemporaryNativeStorage("fontstore-test", createIfEmpty: true);
2121+ fontResourceStore = new NamespacedResourceStore<byte[]>(new DllResourceStore(typeof(Drawable).Assembly), "Resources.Fonts.OpenSans");
2422 }
25232624 [Test]
2725 public void TestNestedScaleAdjust()
2826 {
2929- var fontStore = new FontStore(new GlyphStore(fontResourceStore, "OpenSans") { CacheStorage = storage }, scaleAdjust: 100);
3030- var nestedFontStore = new FontStore(new GlyphStore(fontResourceStore, "OpenSans-Bold") { CacheStorage = storage }, 10);
2727+ var fontStore = new FontStore(new RawCachingGlyphStore(fontResourceStore, "OpenSans") { CacheStorage = storage }, scaleAdjust: 100);
2828+ var nestedFontStore = new FontStore(new RawCachingGlyphStore(fontResourceStore, "OpenSans-Bold") { CacheStorage = storage }, 10);
31293230 fontStore.AddStore(nestedFontStore);
3331
···3939 /// <summary>
4040 /// The names of all available audio devices.
4141 /// </summary>
4242+ /// <remarks>
4343+ /// This property does not contain the names of disabled audio devices.
4444+ /// </remarks>
4245 public IEnumerable<string> AudioDeviceNames => audioDeviceNames;
43464447 /// <summary>
···5659 /// <see cref="string.Empty"/> denotes the OS default.
5760 /// </summary>
5861 public readonly Bindable<string> AudioDevice = new Bindable<string>();
5959-6060- private string currentAudioDevice;
61626263 /// <summary>
6364 /// Volume of all samples played game-wide.
···119120 return store;
120121 });
121122122122- scheduler.Add(() =>
123123+ // check for device validity every 100ms
124124+ scheduler.AddDelayed(() =>
123125 {
124126 try
125127 {
126126- setAudioDevice();
128128+ if (!IsCurrentDeviceValid())
129129+ setAudioDevice();
127130 }
128131 catch
129132 {
130133 }
131131- });
134134+ }, 100, true);
132135133133- scheduler.AddDelayed(delegate
136136+ // enumerate new list of devices every second
137137+ scheduler.AddDelayed(() =>
134138 {
135135- updateAvailableAudioDevices();
136136- checkAudioDeviceChanged();
139139+ try
140140+ {
141141+ setAudioDevice(AudioDevice.Value);
142142+ }
143143+ catch
144144+ {
145145+ }
137146 }, 1000, true);
138147 }
139148···149158150159 private void onDeviceChanged(ValueChangedEvent<string> args)
151160 {
152152- scheduler.Add(() => setAudioDevice(string.IsNullOrEmpty(args.NewValue) ? null : args.NewValue));
161161+ scheduler.Add(() => setAudioDevice(args.NewValue));
153162 }
154163155164 /// <summary>
156156- /// Returns a list of the names of recognized audio devices.
157157- /// </summary>
158158- /// <remarks>
159159- /// The No Sound device that is in the list of Audio Devices that are stored internally is not returned.
160160- /// Regarding the .Skip(1) as implementation for removing "No Sound", see http://bass.radio42.com/help/html/e5a666b4-1bdd-d1cb-555e-ce041997d52f.htm.
161161- /// </remarks>
162162- /// <returns>A list of the names of recognized audio devices.</returns>
163163- private IEnumerable<string> getDeviceNames(IEnumerable<DeviceInfo> devices) => devices.Skip(1).Select(d => d.Name);
164164-165165- /// <summary>
166165 /// Obtains the <see cref="TrackStore"/> corresponding to a given resource store.
167166 /// Returns the global <see cref="TrackStore"/> if no resource store is passed.
168167 /// </summary>
···190189 return sm;
191190 }
192191193193- private IEnumerable<DeviceInfo> enumerateAllDevices()
194194- {
195195- int deviceCount = Bass.DeviceCount;
196196- for (int i = 0; i < deviceCount; i++)
197197- yield return Bass.GetDeviceInfo(i);
198198- }
192192+ private DeviceInfo currentAudioDevice;
199193200200- private bool setAudioDevice(string preferredDevice = null)
194194+ /// <summary>
195195+ /// Sets the output audio device by its name.
196196+ /// This will automatically fall back to the system default device on failure.
197197+ /// </summary>
198198+ /// <param name="deviceName">Name of the audio device, or null to use the configured device preference <see cref="AudioDevice"/>.</param>
199199+ private bool setAudioDevice(string deviceName = null)
201200 {
202201 updateAvailableAudioDevices();
203202204204- string oldDevice = currentAudioDevice;
205205- string newDevice = preferredDevice;
203203+ deviceName ??= AudioDevice.Value;
206204207207- if (string.IsNullOrEmpty(newDevice))
208208- newDevice = audioDevices.Find(df => df.IsDefault).Name;
205205+ // try using the specified device
206206+ if (setAudioDevice(audioDevices.FindIndex(d => d.Name == deviceName)))
207207+ return true;
209208210210- bool oldDeviceValid = Bass.CurrentDevice >= 0;
209209+ // try using the system default device
210210+ if (setAudioDevice(audioDevices.FindIndex(d => d.Name != deviceName && d.IsDefault)))
211211+ return true;
211212212212- if (oldDeviceValid)
213213- {
214214- DeviceInfo oldDeviceInfo = Bass.GetDeviceInfo(Bass.CurrentDevice);
215215- oldDeviceValid &= oldDeviceInfo.IsEnabled && oldDeviceInfo.IsInitialized;
216216- }
217217-218218- if (newDevice == oldDevice && oldDeviceValid)
213213+ // no audio devices can be used, so try using Bass-provided "No sound" device as last resort
214214+ if (setAudioDevice(Bass.NoSoundDevice))
219215 return true;
220216221221- if (string.IsNullOrEmpty(newDevice))
222222- {
223223- Logger.Log(@"BASS Initialization failed (no audio device present)");
224224- return false;
225225- }
217217+ //we're fucked. even "No sound" device won't initialise.
218218+ currentAudioDevice = default;
219219+ return false;
220220+ }
226221227227- int newDeviceIndex = audioDevices.FindIndex(df => df.Name == newDevice);
222222+ private bool setAudioDevice(int deviceIndex)
223223+ {
224224+ var device = audioDevices.ElementAtOrDefault(deviceIndex);
228225229229- DeviceInfo newDeviceInfo = new DeviceInfo();
230230-231231- try
232232- {
233233- if (newDeviceIndex >= 0)
234234- newDeviceInfo = Bass.GetDeviceInfo(newDeviceIndex);
235235- //we may have previously initialised this device.
236236- }
237237- catch
238238- {
239239- }
226226+ // device is invalid
227227+ if (!device.IsEnabled)
228228+ return false;
240229241241- if (oldDeviceValid && (newDeviceInfo.Driver == null || !newDeviceInfo.IsEnabled))
242242- {
243243- //handles the case we are trying to load a user setting which is currently unavailable,
244244- //and we have already fallen back to a sane default.
230230+ // same device
231231+ if (device.IsInitialized && device.Name == currentAudioDevice.Name)
245232 return true;
246246- }
247233248248- if (!Bass.Init(newDeviceIndex) && Bass.LastError != Errors.Already)
249249- {
250250- //the new device didn't go as planned. we need another option.
251251-252252- if (preferredDevice == null)
253253- {
254254- //we're fucked. the default device won't initialise.
255255- currentAudioDevice = null;
256256- return false;
257257- }
258258-259259- //let's try again using the default device.
260260- return setAudioDevice();
261261- }
234234+ // initialize new device
235235+ if (!InitBass(deviceIndex) && Bass.LastError != Errors.Already)
236236+ return false;
262237263238 if (Bass.LastError == Errors.Already)
264239 {
265240 // We check if the initialization error is that we already initialized the device
266241 // If it is, it means we can just tell Bass to use the already initialized device without much
267242 // other fuzz.
268268- Bass.CurrentDevice = newDeviceIndex;
243243+ Bass.CurrentDevice = deviceIndex;
269244 Bass.Free();
270270- Bass.Init(newDeviceIndex);
245245+ InitBass(deviceIndex);
271246 }
272247273248 Trace.Assert(Bass.LastError == Errors.OK);
···275250 Logger.Log($@"BASS Initialized
276251 BASS Version: {Bass.Version}
277252 BASS FX Version: {ManagedBass.Fx.BassFx.Version}
278278- Device: {newDeviceInfo.Name}
279279- Drive: {newDeviceInfo.Driver}");
253253+ Device: {device.Name}
254254+ Drive: {device.Driver}");
280255281256 //we have successfully initialised a new device.
282282- currentAudioDevice = newDevice;
257257+ currentAudioDevice = device;
283258284284- UpdateDevice(newDeviceIndex);
259259+ UpdateDevice(deviceIndex);
285260286261 Bass.PlaybackBufferLength = 100;
287262 Bass.UpdatePeriod = 5;
···289264 return true;
290265 }
291266267267+ /// <summary>
268268+ /// This method calls <see cref="Bass.Init(int, int, DeviceInitFlags, IntPtr, IntPtr)"/>.
269269+ /// It can be overridden for unit testing.
270270+ /// </summary>
271271+ protected virtual bool InitBass(int device) => Bass.Init(device);
272272+292273 private void updateAvailableAudioDevices()
293274 {
294294- var currentDeviceList = enumerateAllDevices().Where(d => d.IsEnabled).ToList();
295295- var currentDeviceNames = getDeviceNames(currentDeviceList).ToList();
275275+ audioDevices = EnumerateAllDevices().ToList();
296276297297- var newDevices = currentDeviceNames.Except(audioDeviceNames).ToList();
298298- var lostDevices = audioDeviceNames.Except(currentDeviceNames).ToList();
277277+ // Bass should always be providing "No sound" device
278278+ Trace.Assert(audioDevices.Count > 0, "Bass did not provide any audio devices.");
279279+280280+ var oldDeviceNames = audioDeviceNames;
281281+ var newDeviceNames = audioDeviceNames = audioDevices.Skip(1).Where(d => d.IsEnabled).Select(d => d.Name).ToList();
282282+283283+ var newDevices = newDeviceNames.Except(oldDeviceNames).ToList();
284284+ var lostDevices = oldDeviceNames.Except(newDeviceNames).ToList();
299285300286 if (newDevices.Count > 0 || lostDevices.Count > 0)
301287 {
···307293 OnLostDevice?.Invoke(d);
308294 });
309295 }
296296+ }
310297311311- audioDevices = currentDeviceList;
312312- audioDeviceNames = currentDeviceNames;
298298+ protected virtual IEnumerable<DeviceInfo> EnumerateAllDevices()
299299+ {
300300+ int deviceCount = Bass.DeviceCount;
301301+ for (int i = 0; i < deviceCount; i++)
302302+ yield return Bass.GetDeviceInfo(i);
313303 }
314304315315- private void checkAudioDeviceChanged()
305305+ protected virtual bool IsCurrentDeviceValid()
316306 {
317317- try
318318- {
319319- if (AudioDevice.Value == string.Empty)
320320- {
321321- // use default device
322322- var device = Bass.GetDeviceInfo(Bass.CurrentDevice);
307307+ var deviceIndex = Bass.CurrentDevice;
308308+ var device = deviceIndex == Bass.DefaultDevice ? default : Bass.GetDeviceInfo(deviceIndex);
323309324324- if (!device.IsDefault && !setAudioDevice())
325325- {
326326- if (!device.IsEnabled || !setAudioDevice(device.Name))
327327- {
328328- foreach (var d in enumerateAllDevices())
329329- {
330330- if (d.Name == device.Name || !d.IsEnabled)
331331- continue;
332332-333333- if (setAudioDevice(d.Name))
334334- break;
335335- }
336336- }
337337- }
338338- }
339339- else
340340- {
341341- // use whatever is the preferred device
342342- var device = Bass.GetDeviceInfo(Bass.CurrentDevice);
343343-344344- if (device.Name == AudioDevice.Value)
345345- {
346346- if (!device.IsEnabled && !setAudioDevice())
347347- {
348348- foreach (var d in enumerateAllDevices())
349349- {
350350- if (d.Name == device.Name || !d.IsEnabled)
351351- continue;
352352-353353- if (setAudioDevice(d.Name))
354354- break;
355355- }
356356- }
357357- }
358358- else
359359- {
360360- var preferredDevice = enumerateAllDevices().SingleOrDefault(d => d.Name == AudioDevice.Value);
361361-362362- if (preferredDevice.Name == AudioDevice.Value && preferredDevice.IsEnabled)
363363- setAudioDevice(preferredDevice.Name);
364364- else if (!device.IsEnabled && !setAudioDevice())
365365- {
366366- foreach (var d in enumerateAllDevices())
367367- {
368368- if (d.Name == device.Name || !d.IsEnabled)
369369- continue;
370370-371371- if (setAudioDevice(d.Name))
372372- break;
373373- }
374374- }
375375- }
376376- }
377377- }
378378- catch
379379- {
380380- }
310310+ return device.IsEnabled && device.IsInitialized;
381311 }
382312383313 public override string ToString() => $@"{GetType().ReadableName()} ({currentAudioDevice})";
+36-11
osu.Framework/Audio/Track/TrackBass.cs
···137137 InvalidateState();
138138 }
139139140140+ /// <summary>
141141+ /// Returns whether the playback state is considered to be running or not.
142142+ /// This will only return true for <see cref="PlaybackState.Playing"/> and <see cref="PlaybackState.Stalled"/>.
143143+ /// </summary>
144144+ private static bool isRunningState(PlaybackState state) => state == PlaybackState.Playing || state == PlaybackState.Stalled;
145145+140146 void IBassAudio.UpdateDevice(int deviceIndex)
141147 {
142148 Bass.ChannelSetDevice(activeStream, deviceIndex);
143149 Trace.Assert(Bass.LastError == Errors.OK);
150150+151151+ // Bass may leave us in an invalid state after the output device changes (this is true for "No sound" device)
152152+ // if the observed state was playing before change, we should force things into a good state.
153153+ if (isPlayed)
154154+ {
155155+ // While on windows, changing to "No sound" changes the playback state correctly,
156156+ // on macOS it is left in a playing-but-stalled state. Forcefully stopping first fixes this.
157157+ stopInternal();
158158+ startInternal();
159159+ }
144160 }
145161146162 protected override void UpdateState()
147163 {
148148- isRunning = Bass.ChannelIsActive(activeStream) == PlaybackState.Playing;
164164+ var running = isRunningState(Bass.ChannelIsActive(activeStream));
165165+ var bytePosition = Bass.ChannelGetPosition(activeStream);
166166+167167+ // because device validity check isn't done frequently, when switching to "No sound" device,
168168+ // there will be a brief time where this track will be stopped, before we resume it manually (see comments in UpdateDevice(int).)
169169+ // this makes us appear to be playing, even if we may not be.
170170+ isRunning = running || (isPlayed && bytePosition != byteLength);
149171150150- Interlocked.Exchange(ref currentTime, Bass.ChannelBytes2Seconds(activeStream, Bass.ChannelGetPosition(activeStream)) * 1000);
172172+ Interlocked.Exchange(ref currentTime, Bass.ChannelBytes2Seconds(activeStream, bytePosition) * 1000);
151173152174 var leftChannel = isPlayed ? Bass.ChannelGetLevelLeft(activeStream) / 32768f : -1;
153175 var rightChannel = isPlayed ? Bass.ChannelGetLevelRight(activeStream) / 32768f : -1;
···207229208230 public Task StopAsync() => EnqueueAction(() =>
209231 {
210210- if (Bass.ChannelIsActive(activeStream) == PlaybackState.Playing)
211211- Bass.ChannelPause(activeStream);
212212-232232+ stopInternal();
213233 isPlayed = false;
214234 });
235235+236236+ private bool stopInternal() => isRunningState(Bass.ChannelIsActive(activeStream)) && Bass.ChannelPause(activeStream);
215237216238 private int direction;
217239···230252231253 public Task StartAsync() => EnqueueAction(() =>
232254 {
255255+ if (startInternal())
256256+ isPlayed = true;
257257+ });
258258+259259+ private bool startInternal()
260260+ {
233261 // Bass will restart the track if it has reached its end. This behavior isn't desirable so block locally.
234262 if (Bass.ChannelGetPosition(activeStream) == byteLength)
235235- return;
263263+ return false;
236264237237- if (Bass.ChannelPlay(activeStream))
238238- isPlayed = true;
239239- else
240240- isRunning = false;
241241- });
265265+ return Bass.ChannelPlay(activeStream);
266266+ }
242267243268 public override bool Seek(double seek) => SeekAsync(seek).Result;
244269
···3333 );
34343535 /// <summary>
3636- /// Whether the framework is currently logging performance issues via <see cref="FrameworkSetting.PerformanceLogging"/>.
3636+ /// Whether the framework is currently logging performance issues via <see cref="DebugSetting.PerformanceLogging"/>.
3737 /// This should be used only when a configuration is not available via DI or otherwise (ie. in a static context).
3838 /// </summary>
3939 public static bool LogPerformanceIssues { get; internal set; }
···22// See the LICENCE file in the repository root for full licence text.
3344using osu.Framework.Graphics.OpenGL.Buffers;
55+using osuTK;
56using osuTK.Graphics;
6778namespace osu.Framework.Graphics
···2627 /// A null value implies the <see cref="FrameBuffer"/>s should be drawn as they are.
2728 /// </summary>
2829 DrawColourInfo? FrameBufferDrawColour { get; }
3030+3131+ /// <summary>
3232+ /// The scale of the <see cref="FrameBuffer"/>s drawn relative to the size of this <see cref="IBufferedDrawable"/>.
3333+ /// </summary>
3434+ /// <remarks>
3535+ /// The contents of the <see cref="FrameBuffer"/>s are populated at this scale, however the scale of <see cref="Drawable"/>s remains unaffected.
3636+ /// </remarks>
3737+ Vector2 FrameBufferScale { get; }
2938 }
3039}
+2
osu.Framework/Graphics/Lines/Path.cs
···271271272272 public DrawColourInfo? FrameBufferDrawColour => base.DrawColourInfo;
273273274274+ public Vector2 FrameBufferScale { get; } = Vector2.One;
275275+274276 // The path should not receive the true colour to avoid colour doubling when the frame-buffer is rendered to the back-buffer.
275277 public override DrawColourInfo DrawColourInfo => new DrawColourInfo(Color4.White, base.DrawColourInfo.Blending);
276278
···22// See the LICENCE file in the repository root for full licence text.
3344using System;
55-using System.Threading;
65using osu.Framework.Graphics.Batches;
76using osu.Framework.Graphics.Primitives;
87using osuTK.Graphics.ES30;
···2322 ~TextureGL()
2423 {
2524 Dispose(false);
2626- }
2727-2828- internal int ReferenceCount;
2929-3030- public void Reference() => Interlocked.Increment(ref ReferenceCount);
3131-3232- public void Dereference()
3333- {
3434- if (Interlocked.Decrement(ref ReferenceCount) == 0)
3535- Dispose();
3625 }
37263827 /// <summary>
+1-1
osu.Framework/Graphics/Primitives/Line.cs
···3131 /// <summary>
3232 /// The direction of the second point from the first.
3333 /// </summary>
3434- public float Theta => (float)Math.Atan2(EndPoint.Y - StartPoint.Y, EndPoint.X - StartPoint.X);
3434+ public float Theta => MathF.Atan2(EndPoint.Y - StartPoint.Y, EndPoint.X - StartPoint.X);
35353636 /// <summary>
3737 /// The direction of this <see cref="Line"/>.
+1-1
osu.Framework/Graphics/Primitives/RectangleF.cs
···343343 float distX = Math.Max(0.0f, Math.Max(localSpacePos.X - Right, Left - localSpacePos.X));
344344 float distY = Math.Max(0.0f, Math.Max(localSpacePos.Y - Bottom, Top - localSpacePos.Y));
345345346346- return (float)Math.Pow(distX, exponent) + (float)Math.Pow(distY, exponent);
346346+ return MathF.Pow(distX, exponent) + MathF.Pow(distY, exponent);
347347 }
348348349349 // This could be optimized further in the future, but made for a simple implementation right now.
+1-1
osu.Framework/Graphics/Sprites/SpriteIcon.cs
···103103 //squared result for quadratic fall-off seems to give the best result.
104104 var avgColour = (Color4)DrawColourInfo.Colour.AverageColour;
105105106106- spriteShadow.Alpha = (float)Math.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2);
106106+ spriteShadow.Alpha = MathF.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2);
107107108108 layout.Validate();
109109 }
···5151 Shader.Bind();
52525353 var avgColour = (Color4)DrawColourInfo.Colour.AverageColour;
5454- float shadowAlpha = (float)Math.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2);
5454+ float shadowAlpha = MathF.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2);
55555656 //adjust shadow alpha based on highest component intensity to avoid muddy display of darker text.
5757 //squared result for quadratic fall-off seems to give the best result.
···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.Textures;
55+using osuTK.Graphics.ES30;
66+77+namespace osu.Framework.Graphics.Textures
88+{
99+ /// <summary>
1010+ /// A texture which can cleans up any resources held by the underlying <see cref="TextureGL"/> on <see cref="Dispose"/>.
1111+ /// </summary>
1212+ public class DisposableTexture : Texture
1313+ {
1414+ public DisposableTexture(TextureGL textureGl)
1515+ : base(textureGl)
1616+ {
1717+ }
1818+1919+ public DisposableTexture(int width, int height, bool manualMipmaps = false, All filteringMode = All.Linear)
2020+ : base(width, height, manualMipmaps, filteringMode)
2121+ {
2222+ }
2323+2424+ protected override void Dispose(bool isDisposing)
2525+ {
2626+ base.Dispose(isDisposing);
2727+ TextureGL.Dispose();
2828+ }
2929+ }
3030+}
···6767 glTexture = new TextureGLSingle(upload.Width, upload.Height, manualMipmaps, filteringMode);
68686969 Texture tex = new Texture(glTexture) { ScaleAdjust = ScaleAdjust };
7070-7170 tex.SetData(upload);
72717372 return tex;
···8988 lock (textureCache)
9089 {
9190 // refresh the texture if no longer available (may have been previously disposed).
9292- if (!textureCache.TryGetValue(name, out var tex) || tex?.Available == false)
9191+ if (!textureCache.TryGetValue(name, out var tex))
9392 {
9493 try
9594 {
···102101 }
103102104103 return tex;
104104+ }
105105+ }
106106+107107+ /// <summary>
108108+ /// Disposes and removes a texture with the specified name from the texture cache.
109109+ /// </summary>
110110+ /// <param name="name">The name of the texture to purge from the cache.</param>
111111+ protected void Purge(string name)
112112+ {
113113+ lock (textureCache)
114114+ {
115115+ if (textureCache.TryGetValue(name, out var tex))
116116+ tex.Dispose();
117117+ textureCache.Remove(name);
105118 }
106119 }
107120 }
+27-1
osu.Framework/Graphics/Textures/TextureUpload.cs
···3344using System;
55using System.IO;
66+using System.Runtime.InteropServices;
67using osu.Framework.Graphics.OpenGL;
78using osu.Framework.Graphics.OpenGL.Buffers;
89using osu.Framework.Graphics.Primitives;
1010+using osu.Framework.Logging;
911using osuTK.Graphics.ES30;
1012using SixLabors.ImageSharp;
1113using SixLabors.ImageSharp.Advanced;
1214using SixLabors.ImageSharp.PixelFormats;
1515+using StbiSharp;
13161417namespace osu.Framework.Graphics.Textures
1518{
···59626063 /// <summary>
6164 /// Create an upload from an arbitrary image stream.
6565+ /// Note that this bypasses per-platform image loading optimisations.
6666+ /// Use <see cref="TextureLoaderStore"/> as provided from GameHost where possible.
6267 /// </summary>
6368 /// <param name="stream">The image content.</param>
6469 public TextureUpload(Stream stream)
6565- : this(Image.Load<Rgba32>(stream))
7070+ : this(LoadFromStream<Rgba32>(stream))
7171+ {
7272+ }
7373+7474+ internal static Image<TPixel> LoadFromStream<TPixel>(Stream stream) where TPixel : unmanaged, IPixel<TPixel>
6675 {
7676+ long initialPos = stream.Position;
7777+7878+ try
7979+ {
8080+ using (var m = new MemoryStream())
8181+ {
8282+ stream.CopyTo(m);
8383+ using (var stbiImage = Stbi.LoadFromMemory(m, 4))
8484+ return Image.LoadPixelData(MemoryMarshal.Cast<byte, TPixel>(stbiImage.Data), stbiImage.Width, stbiImage.Height);
8585+ }
8686+ }
8787+ catch (Exception e)
8888+ {
8989+ Logger.Error(e, "Texture could not be loaded via STB; falling back to ImageSharp.");
9090+ stream.Position = initialPos;
9191+ return Image.Load<TPixel>(stream);
9292+ }
6793 }
68946995 /// <summary>
···22// See the LICENCE file in the repository root for full licence text.
3344using System;
55-using osu.Framework.Graphics.OpenGL;
55+using System.Threading;
66using osu.Framework.Graphics.OpenGL.Textures;
77-using osuTK.Graphics.ES30;
8798namespace osu.Framework.Graphics.Textures
109{
1110 /// <summary>
1212- /// A texture which updates the reference count of the underlying <see cref="TextureGL"/> on ctor and disposal.
1111+ /// A texture which shares a common reference count with all other textures using the same <see cref="TextureGL"/>.
1312 /// </summary>
1414- public class TextureWithRefCount : Texture
1313+ internal class TextureWithRefCount : Texture
1514 {
1616- public TextureWithRefCount(TextureGL textureGl)
1515+ private readonly ReferenceCount count;
1616+1717+ public TextureWithRefCount(TextureGL textureGl, ReferenceCount count)
1718 : base(textureGl)
1819 {
1919- textureGl.Reference();
2020- }
2020+ this.count = count;
21212222- public TextureWithRefCount(int width, int height, bool manualMipmaps = false, All filteringMode = All.Linear)
2323- : this(new TextureGLSingle(width, height, manualMipmaps, filteringMode))
2424- {
2222+ count.Increment();
2523 }
2626-2727- internal int ReferenceCount => base.TextureGL.ReferenceCount;
28242925 public sealed override TextureGL TextureGL
3026 {
3127 get
3228 {
3333- var tex = base.TextureGL;
3434- if (tex.ReferenceCount <= 0)
2929+ if (!Available)
3530 throw new InvalidOperationException($"Attempting to access a {nameof(TextureWithRefCount)}'s underlying texture after all references are lost.");
36313737- return tex;
3232+ return base.TextureGL;
3833 }
3934 }
40354141- // The base property references TextureGL, but doing so may throw an exception (above)
3636+ // The base property invokes the overridden TextureGL property, which will throw an exception if not available
3737+ // So this property is redirected to reference the intended member
4238 public sealed override bool Available => base.TextureGL.Available;
4343-4444- #region Disposal
45394640 ~TextureWithRefCount()
4741 {
···4943 Dispose(false);
5044 }
51455252- private bool isDisposed;
4646+ public bool IsDisposed { get; private set; }
53475448 protected override void Dispose(bool isDisposing)
5549 {
5650 base.Dispose(isDisposing);
57515858- if (isDisposed)
5252+ if (IsDisposed)
5953 return;
60546161- isDisposed = true;
5555+ IsDisposed = true;
62566363- GLWrapper.ScheduleDisposal(() => base.TextureGL.Dereference());
5757+ count.Decrement();
6458 }
65596666- #endregion
6060+ public class ReferenceCount
6161+ {
6262+ private readonly object lockObject;
6363+ private readonly Action onAllReferencesLost;
6464+6565+ private int referenceCount;
6666+6767+ /// <summary>
6868+ /// Creates a new <see cref="ReferenceCount"/>.
6969+ /// </summary>
7070+ /// <param name="lockObject">The <see cref="object"/> which locks will be taken out on.</param>
7171+ /// <param name="onAllReferencesLost">A delegate to invoke after all references have been lost.</param>
7272+ public ReferenceCount(object lockObject, Action onAllReferencesLost)
7373+ {
7474+ this.lockObject = lockObject;
7575+ this.onAllReferencesLost = onAllReferencesLost;
7676+ }
7777+7878+ /// <summary>
7979+ /// Increments the reference count.
8080+ /// </summary>
8181+ public void Increment()
8282+ {
8383+ lock (lockObject)
8484+ Interlocked.Increment(ref referenceCount);
8585+ }
8686+8787+ /// <summary>
8888+ /// Decrements the reference count, invoking <see cref="onAllReferencesLost"/> if there are no remaining references.
8989+ /// The delegate is invoked while a lock on the provided <see cref="lockObject"/> is held.
9090+ /// </summary>
9191+ public void Decrement()
9292+ {
9393+ lock (lockObject)
9494+ {
9595+ if (Interlocked.Decrement(ref referenceCount) == 0)
9696+ onAllReferencesLost?.Invoke();
9797+ }
9898+ }
9999+ }
67100 }
68101}
···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.Linq;
88+using osu.Framework.Extensions;
99+using osu.Framework.Graphics.Textures;
1010+using osu.Framework.Platform;
1111+using SharpFNT;
1212+using SixLabors.ImageSharp;
1313+using SixLabors.ImageSharp.Advanced;
1414+using SixLabors.ImageSharp.PixelFormats;
1515+using SixLabors.Primitives;
1616+1717+namespace osu.Framework.IO.Stores
1818+{
1919+ /// <summary>
2020+ /// A glyph store which caches font sprite sheets as raw pixels to disk on first use.
2121+ /// </summary>
2222+ /// <remarks>
2323+ /// This results in memory efficient lookups with good performance on solid state backed devices.
2424+ /// </remarks>
2525+ public class RawCachingGlyphStore : GlyphStore
2626+ {
2727+ public Storage CacheStorage;
2828+2929+ public RawCachingGlyphStore(ResourceStore<byte[]> store, string assetName = null, IResourceStore<TextureUpload> textureLoader = null)
3030+ : base(store, assetName, textureLoader)
3131+ {
3232+ }
3333+3434+ private readonly Dictionary<int, PageInfo> pageLookup = new Dictionary<int, PageInfo>();
3535+3636+ protected override TextureUpload LoadCharacter(Character character)
3737+ {
3838+ if (!pageLookup.TryGetValue(character.Page, out var pageInfo))
3939+ pageInfo = createCachedPageInfo(character.Page);
4040+4141+ return createTextureUpload(character, pageInfo);
4242+ }
4343+4444+ private PageInfo createCachedPageInfo(int page)
4545+ {
4646+ string filename = GetFilenameForPage(page);
4747+4848+ using (var stream = Store.GetStream(filename))
4949+ {
5050+ string streamMd5 = stream.ComputeMD5Hash();
5151+ string filenameMd5 = filename.ComputeMD5Hash();
5252+5353+ string accessFilename = $"{filenameMd5}#{streamMd5}";
5454+5555+ var existing = CacheStorage.GetFiles(string.Empty, $"{accessFilename}*").FirstOrDefault();
5656+5757+ if (existing != null)
5858+ {
5959+ var split = existing.Split('#');
6060+ return pageLookup[page] = new PageInfo
6161+ {
6262+ Size = new Size(int.Parse(split[2]), int.Parse(split[3])),
6363+ Filename = existing
6464+ };
6565+ }
6666+6767+ using (var convert = GetPageImage(page))
6868+ {
6969+ // todo: use i# memoryallocator once netstandard supports stream operations
7070+ byte[] output = new byte[convert.Width * convert.Height];
7171+7272+ var pxl = convert.Data;
7373+7474+ for (int i = 0; i < convert.Width * convert.Height; i++)
7575+ output[i] = pxl[i].A;
7676+7777+ // ensure any stale cached versions are deleted.
7878+ foreach (var f in CacheStorage.GetFiles(string.Empty, $"{filenameMd5}*"))
7979+ CacheStorage.Delete(f);
8080+8181+ accessFilename += $"#{convert.Width}#{convert.Height}";
8282+8383+ using (var outStream = CacheStorage.GetStream(accessFilename, FileAccess.Write, FileMode.Create))
8484+ outStream.Write(output, 0, output.Length);
8585+8686+ return pageLookup[page] = new PageInfo
8787+ {
8888+ Size = new Size(convert.Width, convert.Height),
8989+ Filename = accessFilename
9090+ };
9191+ }
9292+ }
9393+ }
9494+9595+ private TextureUpload createTextureUpload(Character character, PageInfo page)
9696+ {
9797+ int pageWidth = page.Size.Width;
9898+9999+ if (readBuffer == null || readBuffer.Length < pageWidth)
100100+ readBuffer = new byte[pageWidth];
101101+102102+ var image = new Image<Rgba32>(SixLabors.ImageSharp.Configuration.Default, character.Width, character.Height, new Rgba32(255, 255, 255, 0));
103103+104104+ using (var source = CacheStorage.GetStream(page.Filename))
105105+ {
106106+ var dest = image.GetPixelSpan();
107107+ source.Seek(pageWidth * character.Y, SeekOrigin.Current);
108108+109109+ // the spritesheet may have unused pixels trimmed
110110+ int readableHeight = Math.Min(character.Height, page.Size.Height - character.Y);
111111+ int readableWidth = Math.Min(character.Width, pageWidth - character.X);
112112+113113+ for (int y = 0; y < readableHeight; y++)
114114+ {
115115+ source.Read(readBuffer, 0, pageWidth);
116116+117117+ int writeOffset = y * character.Width;
118118+119119+ for (int x = 0; x < readableWidth; x++)
120120+ dest[writeOffset + x] = new Rgba32(255, 255, 255, readBuffer[character.X + x]);
121121+ }
122122+ }
123123+124124+ return new TextureUpload(image);
125125+ }
126126+127127+ private byte[] readBuffer;
128128+129129+ private class PageInfo
130130+ {
131131+ public string Filename;
132132+ public Size Size;
133133+ }
134134+ }
135135+}
+1-1
osu.Framework/Input/InputResampler.cs
···4545 }
46464747 // HD if it has fractions
4848- if (position.X - (float)Math.Truncate(position.X) != 0)
4848+ if (position.X - MathF.Truncate(position.X) != 0)
4949 isRawInput = true;
5050 }
5151