Merge branch 'master' into fxcop

This commit is contained in:
Huo Yaoyuan
2019-12-09 17:34:49 +08:00
40 changed files with 804 additions and 135 deletions

View File

@@ -0,0 +1,20 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Benchmarks" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Framework.Benchmarks/bin/Debug/netcoreapp3.0/osu.Framework.Benchmarks.dll" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Framework.Benchmarks/bin/Debug/netcoreapp3.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/osu.Framework.Benchmarks/osu.Framework.Benchmarks.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value=".NETCoreApp,Version=v3.0" />
<method v="2">
<option name="Build" enabled="true" />
</method>
</configuration>
</component>

16
.vscode/launch.json vendored
View File

@@ -48,6 +48,22 @@
}
},
"console": "internalConsole"
},
{
"name": "Benchmarks",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Framework.Benchmarks/bin/Release/netcoreapp3.0/osu.Framework.Benchmarks.dll",
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
"linux": {
"env": {
"LD_LIBRARY_PATH": "${workspaceRoot}/osu.Framework.Benchmarks/bin/Release/netcoreapp3.0:${env:LD_LIBRARY_PATH}"
}
},
}
]
}

View File

@@ -4,6 +4,7 @@
<ProjectReference Include="..\SampleGame\SampleGame.csproj" />
<ProjectReference Include="..\osu.Framework.NativeLibs\osu.Framework.NativeLibs.csproj" />
<ProjectReference Include="..\osu.Framework.Tests\osu.Framework.Tests.csproj" />
<ProjectReference Include="..\osu.Framework.Benchmarks\osu.Framework.Benchmarks.csproj" />
<ProjectReference Include="..\osu.Framework\osu.Framework.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,7 +6,8 @@
"SampleGame\\SampleGame.csproj",
"osu.Framework.NativeLibs\\osu.Framework.NativeLibs.csproj",
"osu.Framework.Tests\\osu.Framework.Tests.csproj",
"osu.Framework\\osu.Framework.csproj"
"osu.Framework\\osu.Framework.csproj",
"osu.Framework.Benchmarks\\osu.Framework.Benchmarks.csproj"
]
}
}

View File

@@ -37,6 +37,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
osu.Framework.iOS.props = osu.Framework.iOS.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Framework.Benchmarks", "osu.Framework.Benchmarks\osu.Framework.Benchmarks.csproj", "{F294C804-8AE2-4020-841A-AF0D97FBE80C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -171,6 +173,18 @@ Global
{320089C6-A141-4D3E-BD5F-C4A6CE9E567B}.Release|Any CPU.Deploy.0 = Release|Any CPU
{320089C6-A141-4D3E-BD5F-C4A6CE9E567B}.Release|iPhone.ActiveCfg = Release|Any CPU
{320089C6-A141-4D3E-BD5F-C4A6CE9E567B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Debug|iPhone.Build.0 = Debug|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Release|Any CPU.Build.0 = Release|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Release|iPhone.ActiveCfg = Release|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Release|iPhone.Build.0 = Release|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{F294C804-8AE2-4020-841A-AF0D97FBE80C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1,96 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Reflection;
using BenchmarkDotNet.Attributes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.IO.Stores;
using osu.Framework.Tests;
using SixLabors.Memory;
namespace osu.Framework.Benchmarks
{
public class BenchmarkFontLoading : BenchmarkTest
{
private NamespacedResourceStore<byte[]> baseResources;
private TemporaryNativeStorage sharedTemp;
public override void SetUp()
{
SixLabors.ImageSharp.Configuration.Default.MemoryAllocator = ArrayPoolMemoryAllocator.CreateDefault();
baseResources = new NamespacedResourceStore<byte[]>(new DllResourceStore(@"osu.Framework.dll"), @"Resources");
sharedTemp = new TemporaryNativeStorage("fontstore-test" + Guid.NewGuid(), createIfEmpty: true);
}
[Params(1, 10, 100, 1000, 10000)]
public int FetchCount;
private const string font_name = @"Fonts/FontAwesome5/FontAwesome-Solid";
[Benchmark]
public void BenchmarkRawCachingReuse()
{
using (var store = new RawCachingGlyphStore(baseResources, font_name) { CacheStorage = sharedTemp })
runFor(store);
}
[Benchmark(Baseline = true)]
public void BenchmarkRawCaching()
{
using (var temp = new TemporaryNativeStorage("fontstore-test" + Guid.NewGuid(), createIfEmpty: true))
using (var store = new RawCachingGlyphStore(baseResources, font_name) { CacheStorage = temp })
runFor(store);
}
[Benchmark]
public void BenchmarkNoCache()
{
if (FetchCount > 100) // gets too slow.
throw new NotImplementedException();
using (var store = new GlyphStore(baseResources, font_name))
runFor(store);
}
[Benchmark]
public void BenchmarkTimedExpiry()
{
SixLabors.ImageSharp.Configuration.Default.MemoryAllocator = ArrayPoolMemoryAllocator.CreateDefault();
using (var store = new TimedExpiryGlyphStore(baseResources, font_name))
runFor(store);
}
[Benchmark]
public void BenchmarkTimedExpiryMemoryPooling()
{
using (var store = new TimedExpiryGlyphStore(baseResources, font_name))
runFor(store);
}
private void runFor(GlyphStore store)
{
store.LoadFontAsync().Wait();
var props = typeof(FontAwesome.Solid).GetProperties(BindingFlags.Public | BindingFlags.Static);
int remainingCount = FetchCount;
while (true)
{
foreach (var p in props)
{
var icon = (IconUsage)p.GetValue(null);
using (var upload = store.Get(icon.Icon.ToString()))
Trace.Assert(upload.Data != null);
if (remainingCount-- == 0)
return;
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using NUnit.Framework;
namespace osu.Framework.Benchmarks
{
[TestFixture]
[MemoryDiagnoser]
public abstract class BenchmarkTest
{
[GlobalSetup]
[OneTimeSetUp]
public virtual void SetUp()
{
}
[Test]
public void RunBenchmark() => BenchmarkRunner.Run(GetType());
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using BenchmarkDotNet.Running;
namespace osu.Framework.Benchmarks
{
public class Program
{
public static void Main(string[] args)
{
BenchmarkSwitcher
.FromAssembly(typeof(Program).Assembly)
.Run(args);
}
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Project">
<TargetFramework>netcoreapp3.0</TargetFramework>
<OutputType>WinExe</OutputType>
<Optimize>true</Optimize>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
<PackageReference Include="nunit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Framework.Tests\osu.Framework.Tests.csproj" />
<ProjectReference Include="..\osu.Framework\osu.Framework.csproj" />
</ItemGroup>
</Project>

View File

@@ -298,6 +298,30 @@ namespace osu.Framework.Tests.Bindables
});
}
[Test]
public void TestAddRangeEnumeratesOnlyOnce()
{
BindableList<int> list1 = new BindableList<int>();
BindableList<int> list2 = new BindableList<int>();
list2.BindTo(list1);
int addeditem = 0;
list1.ItemsAdded += items => addeditem = items.Single();
int counter = 0;
IEnumerable<int> valueEnumerable()
{
yield return counter++;
}
list1.AddRange(valueEnumerable());
Assert.That(list1[0], Is.EqualTo(0));
Assert.That(list2[0], Is.EqualTo(0));
Assert.That(addeditem, Is.EqualTo(0));
}
#endregion
#region .Insert

View File

@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Timing;
@@ -9,15 +11,15 @@ namespace osu.Framework.Tests.Clocks
[TestFixture]
public class DecoupleableClockTest
{
private TestClock source;
private DecoupleableInterpolatingFramedClock decoupleable;
private TestClockWithRange source;
private TestDecoupleableClock decoupleable;
[SetUp]
public void SetUp()
{
source = new TestClockPositiveOnly();
source = new TestClockWithRange();
decoupleable = new DecoupleableInterpolatingFramedClock();
decoupleable = new TestDecoupleableClock();
decoupleable.ChangeSource(source);
}
@@ -174,43 +176,85 @@ namespace osu.Framework.Tests.Clocks
Assert.AreEqual(0, decoupleable.CurrentTime);
}
/// <summary>
/// Tests that the decoupled clocks starts the source as a result of being able to handle the current time.
/// </summary>
[Test]
public void TestFromNegativeDecoupledMode()
public void TestDecoupledStartsSourceIfAllowable()
{
decoupleable.IsCoupled = false;
decoupleable.Seek(-1000);
decoupleable.CustomAllowableErrorMilliseconds = 1000;
decoupleable.Seek(-50);
decoupleable.ProcessFrame();
Assert.AreEqual(0, source.CurrentTime);
Assert.AreEqual(-1000, decoupleable.CurrentTime);
decoupleable.Start();
double? last = null;
while (decoupleable.CurrentTime < 0)
{
decoupleable.ProcessFrame();
Assert.AreEqual(0, source.CurrentTime);
if (last.HasValue)
Assert.GreaterOrEqual(decoupleable.CurrentTime, last);
last = decoupleable.CurrentTime;
}
// Delay a bit to make sure the clock crosses the 0 boundary
Thread.Sleep(100);
decoupleable.ProcessFrame();
Assert.GreaterOrEqual(decoupleable.CurrentTime, last);
Assert.GreaterOrEqual(decoupleable.CurrentTime, source.CurrentTime);
Assert.That(source.IsRunning, Is.True);
}
/// <summary>
/// Tests that during forward playback the decoupled clock always moves in the forwards direction after starting the source clock.
/// For this test, the source clock is started when the decoupled time crosses the 0ms-boundary.
/// </summary>
[Test]
public void TestForwardPlaybackDecoupledTimeDoesNotRewindAfterSourceStarts()
{
decoupleable.IsCoupled = false;
decoupleable.CustomAllowableErrorMilliseconds = 1000;
decoupleable.Seek(-50);
decoupleable.ProcessFrame();
decoupleable.Start();
// Delay a bit to make sure the clock crosses the 0ms boundary
Thread.Sleep(100);
decoupleable.ProcessFrame();
// Make sure that time doesn't rewind. Note that the source clock does not move by itself,
double last = decoupleable.CurrentTime;
decoupleable.ProcessFrame();
Assert.That(decoupleable.CurrentTime, Is.GreaterThanOrEqualTo(last));
}
/// <summary>
/// Tests that during backwards playback the decoupled clock always moves in the backwards direction after starting the source clock.
/// For this test, the source clock is started when the decoupled time crosses the 1000ms-boundary.
/// </summary>
[Test]
public void TestBackwardPlaybackDecoupledTimeDoesNotRewindAfterSourceStarts()
{
source.MaxTime = 1000;
decoupleable.IsCoupled = false;
decoupleable.CustomAllowableErrorMilliseconds = 1000;
decoupleable.Rate = -1;
// Bring the source clock into a good state by seeking to a valid time
decoupleable.Seek(1000);
decoupleable.Start();
decoupleable.ProcessFrame();
decoupleable.Stop();
decoupleable.Seek(1050);
decoupleable.ProcessFrame();
decoupleable.Start();
// Delay a bit to make sure the clock crosses the 1000ms boundary
Thread.Sleep(100);
decoupleable.ProcessFrame();
// Make sure that time doesn't rewind
double last = decoupleable.CurrentTime;
decoupleable.ProcessFrame();
Assert.That(decoupleable.CurrentTime, Is.LessThanOrEqualTo(last));
}
/// <summary>
/// Tests that the decoupled clock seeks the source clock to its time when it starts.
/// </summary>
[Test]
public void TestDecoupledStartWithSouceOffset()
public void TestDecoupledStartWithSourceOffset()
{
decoupleable.IsCoupled = false;
@@ -277,6 +321,22 @@ namespace osu.Framework.Tests.Clocks
Assert.AreNotEqual(source.CurrentTime, decoupleable.CurrentTime, "Coupled time should not match source time.");
}
/// <summary>
/// Tests that seeking a decoupled clock negatively does not cause it to seek to the positive source time.
/// </summary>
[Test]
public void TestDecoupledNotSeekedPositivelyByFailedNegativeSeek()
{
decoupleable.IsCoupled = false;
decoupleable.Start();
decoupleable.Seek(-5000);
Assert.That(source.IsRunning, Is.False);
Assert.That(decoupleable.IsRunning, Is.True);
Assert.That(decoupleable.CurrentTime, Is.LessThan(0));
}
#endregion
/// <summary>
@@ -309,11 +369,22 @@ namespace osu.Framework.Tests.Clocks
Assert.AreEqual(source.CurrentTime, decoupleable.CurrentTime, decoupleable.AllowableErrorMilliseconds, "Decoupled should match source time.");
}
private class TestClockPositiveOnly : TestClock
private class TestDecoupleableClock : DecoupleableInterpolatingFramedClock
{
public double? CustomAllowableErrorMilliseconds { get; set; }
public override double AllowableErrorMilliseconds => CustomAllowableErrorMilliseconds ?? base.AllowableErrorMilliseconds;
}
private class TestClockWithRange : TestClock
{
public double MinTime { get; set; } = 0;
public double MaxTime { get; set; } = double.PositiveInfinity;
public override bool Seek(double position)
{
if (position < 0) return false;
if (Math.Clamp(position, MinTime, MaxTime) != position)
return false;
return base.Seek(position);
}

View File

@@ -24,16 +24,17 @@ namespace osu.Framework.Tests.IO
[Test]
public void TestNestedScaleAdjust()
{
var fontStore = new FontStore(new RawCachingGlyphStore(fontResourceStore, "OpenSans") { CacheStorage = storage }, scaleAdjust: 100);
var nestedFontStore = new FontStore(new RawCachingGlyphStore(fontResourceStore, "OpenSans-Bold") { CacheStorage = storage }, 10);
using (var fontStore = new FontStore(new RawCachingGlyphStore(fontResourceStore, "OpenSans") { CacheStorage = storage }, scaleAdjust: 100))
using (var nestedFontStore = new FontStore(new RawCachingGlyphStore(fontResourceStore, "OpenSans-Bold") { CacheStorage = storage }, 10))
{
fontStore.AddStore(nestedFontStore);
fontStore.AddStore(nestedFontStore);
var normalGlyph = (TexturedCharacterGlyph)fontStore.Get("OpenSans", 'a');
var boldGlyph = (TexturedCharacterGlyph)fontStore.Get("OpenSans-Bold", 'a');
var normalGlyph = (TexturedCharacterGlyph)fontStore.Get("OpenSans", 'a');
var boldGlyph = (TexturedCharacterGlyph)fontStore.Get("OpenSans-Bold", 'a');
Assert.That(normalGlyph.Scale, Is.EqualTo(1f / 100));
Assert.That(boldGlyph.Scale, Is.EqualTo(1f / 10));
Assert.That(normalGlyph.Scale, Is.EqualTo(1f / 100));
Assert.That(boldGlyph.Scale, Is.EqualTo(1f / 10));
}
}
[OneTimeTearDown]

View File

@@ -348,5 +348,37 @@ namespace osu.Framework.Tests.Threading
scheduler.Update();
Assert.AreEqual(1, invocations);
}
/// <summary>
/// Tests that delegates added from inside a scheduled callback don't get executed when the scheduled callback cancels a prior intermediate task.
///
/// Delegate 1 - Added at the start.
/// Delegate 2 - Added at the start, cancelled by Delegate 1.
/// Delegate 3 - Added during Delegate 1 callback, should not get executed.
/// </summary>
[Test]
public void TestDelegateAddedInCallbackNotExecutedAfterIntermediateCancelledDelegate()
{
int invocations = 0;
// Delegate 2
var cancelled = new ScheduledDelegate(() => { });
// Delegate 1
scheduler.Add(() =>
{
invocations++;
cancelled.Cancel();
// Delegate 3
scheduler.Add(() => invocations++);
});
scheduler.Add(cancelled);
scheduler.Update();
Assert.That(invocations, Is.EqualTo(1));
}
}
}

View File

@@ -19,7 +19,7 @@ namespace osu.Framework.Tests.Visual.Audio
this.samples = samples;
}
[Test, Ignore("Needs no audio device support")]
[Test]
public void TestLoopingToggle()
{
AddStep("create sample", createSample);
@@ -27,16 +27,16 @@ namespace osu.Framework.Tests.Visual.Audio
AddStep("enable looping", () => sampleChannel.Looping = true);
AddStep("play sample", () => sampleChannel.Play());
AddAssert("is playing", () => sampleChannel.Playing);
AddUntilStep("is playing", () => sampleChannel.Playing);
AddWaitStep("wait", 1);
AddWaitStep("wait", 10);
AddAssert("is still playing", () => sampleChannel.Playing);
AddStep("disable looping", () => sampleChannel.Looping = false);
AddUntilStep("ensure stops", () => !sampleChannel.Playing);
}
[Test, Ignore("Needs no audio device support")]
[Test]
public void TestStopWhileLooping()
{
AddStep("create sample", createSample);
@@ -44,11 +44,11 @@ namespace osu.Framework.Tests.Visual.Audio
AddStep("enable looping", () => sampleChannel.Looping = true);
AddStep("play sample", () => sampleChannel.Play());
AddWaitStep("wait", 1);
AddWaitStep("wait", 10);
AddAssert("is playing", () => sampleChannel.Playing);
AddStep("stop playing", () => sampleChannel.Stop());
AddAssert("not playing", () => !sampleChannel.Playing);
AddUntilStep("not playing", () => !sampleChannel.Playing);
}
private void createSample()

View File

@@ -214,6 +214,20 @@ namespace osu.Framework.Tests.Visual.UserInterface
AddUntilStep("ensure new current", () => screen2.IsCurrentScreen());
}
[Test]
public void TestScreenPushedAfterExiting()
{
TestScreen screen1 = null;
AddStep("push", () => stack.Push(screen1 = new TestScreen()));
AddAssert("wait for current", () => screen1.IsCurrentScreen());
AddStep("exit screen1", () => screen1.Exit());
AddUntilStep("ensure exited", () => !screen1.IsCurrentScreen());
AddStep("push again", () => Assert.Throws<InvalidOperationException>(() => stack.Push(screen1)));
}
[Test]
public void TestPushToNonLoadedScreenFails()
{
@@ -286,7 +300,7 @@ namespace osu.Framework.Tests.Visual.UserInterface
{
// we can't use the [SetUp] screen stack as we need to change the ctor parameters.
Clear();
Add(stack = new ScreenStack(baseScreen = new TestScreen())
Add(stack = new ScreenStack(baseScreen = new TestScreen(id: 0))
{
RelativeSizeAxes = Axes.Both
});
@@ -296,13 +310,13 @@ namespace osu.Framework.Tests.Visual.UserInterface
AddStep("Perform setup", () =>
{
order = new List<int>();
screen1 = new TestScreenSlow
screen1 = new TestScreenSlow(1)
{
Entered = () => order.Add(1),
Suspended = () => order.Add(2),
Resumed = () => order.Add(5),
};
screen2 = new TestScreenSlow
screen2 = new TestScreenSlow(2)
{
Entered = () => order.Add(3),
Exited = () => order.Add(4),
@@ -347,6 +361,25 @@ namespace osu.Framework.Tests.Visual.UserInterface
AddAssert("order is correct", () => order.SequenceEqual(order.OrderBy(i => i)));
}
[Test]
public void TestEventsNotFiredBeforeScreenLoad()
{
Screen screen1 = null;
bool wasLoaded = true;
pushAndEnsureCurrent(() => screen1 = new TestScreen
{
// ReSharper disable once AccessToModifiedClosure
Entered = () => wasLoaded &= screen1?.IsLoaded == true,
// ReSharper disable once AccessToModifiedClosure
Suspended = () => wasLoaded &= screen1?.IsLoaded == true,
});
pushAndEnsureCurrent(() => new TestScreen(), () => screen1);
AddAssert("was loaded before events", () => wasLoaded);
}
[Test]
public void TestAsyncDoublePush()
{

View File

@@ -68,8 +68,8 @@
<PackageReference Include="FFmpeg.AutoGen" Version="4.2.0" />
<PackageReference Include="SharpFNT" Version="1.1.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0007" />
<PackageReference Include="System.Drawing.Common" Version="4.6.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.3.1" />
<PackageReference Include="System.Drawing.Common" Version="4.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
<PackageReference Include="ppy.osuTK.NS20" Version="1.0.115" />
<PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" />
<PackageReference Include="ppy.Microsoft.Diagnostics.Runtime" Version="0.9.180305.1" />
@@ -78,8 +78,8 @@
<PackageReference Include="ManagedBass" Version="2.0.4" />
<PackageReference Include="ManagedBass.Fx" Version="2.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.6.0" />
<PackageReference Include="System.Reflection.Emit.ILGeneration" Version="4.6.0" />
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
<PackageReference Include="System.Reflection.Emit.ILGeneration" Version="4.7.0" />
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
<!-- Manually downgrade for 4.5.1-4.5.3. Should be removed when 4.6.0 released. -->

View File

@@ -497,9 +497,9 @@ namespace osu.Framework.Bindables
/// <param name="items">The collection whose items should be added to this collection.</param>
/// <exception cref="InvalidOperationException">Thrown if this collection is <see cref="Disabled"/></exception>
public void AddRange(IEnumerable<T> items)
=> addRange(items, null);
=> addRange(items as ICollection<T> ?? items.ToArray(), null);
private void addRange(IEnumerable<T> items, BindableList<T> caller)
private void addRange(ICollection<T> items, BindableList<T> caller)
{
ensureMutationAllowed();

View File

@@ -137,6 +137,27 @@ namespace osu.Framework
config.BindWith(FrameworkSetting.VolumeEffect, Audio.VolumeSample);
config.BindWith(FrameworkSetting.VolumeMusic, Audio.VolumeTrack);
logOverlayVisibility = config.GetBindable<bool>(FrameworkSetting.ShowLogOverlay);
logOverlayVisibility.BindValueChanged(visibility =>
{
if (visibility.NewValue)
{
if (logOverlay == null)
{
LoadComponentAsync(logOverlay = new LogOverlay
{
Depth = float.MinValue / 2,
}, AddInternal);
}
logOverlay.Show();
}
else
{
logOverlay?.Hide();
}
}, true);
Shaders = new ShaderManager(new NamespacedResourceStore<byte[]>(Resources, @"Shaders"));
dependencies.Cache(Shaders);
@@ -200,6 +221,8 @@ namespace osu.Framework
private GlobalStatisticsDisplay globalStatistics;
private Bindable<bool> logOverlayVisibility;
public bool OnPressed(FrameworkAction action)
{
switch (action)
@@ -251,15 +274,7 @@ namespace osu.Framework
return true;
case FrameworkAction.ToggleLogOverlay:
if (logOverlay == null)
{
LoadComponentAsync(logOverlay = new LogOverlay
{
Depth = float.MinValue / 2,
}, AddInternal);
}
logOverlay.ToggleVisibility();
logOverlayVisibility.Value = !logOverlayVisibility.Value;
return true;
case FrameworkAction.ToggleFullscreen:
@@ -284,10 +299,10 @@ namespace osu.Framework
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Audio?.Dispose();
Audio = null;
base.Dispose(isDisposing);
}
}
}

View File

@@ -15,7 +15,6 @@ namespace osu.Framework.Graphics.Containers
// this shouldn't have to be done here, but it's the only place it works correctly.
// see https://github.com/ppy/osu-framework/pull/1666
CornerRadius = Math.Min(DrawSize.X, DrawSize.Y) / 2f;
CornerExponent = 2;
return base.GenerateDrawNodeSubtree(frame, treeIndex, forceNewDrawNode);
}

View File

@@ -152,7 +152,7 @@ namespace osu.Framework.Graphics.Containers
d.OnLoadComplete += _ => loadingComponents.Remove(d);
}
var taskScheduler = components.Any(c => c.IsLongLoading) ? long_load_scheduler : threaded_scheduler;
var taskScheduler = components.Any(c => c.IsLongRunning) ? long_load_scheduler : threaded_scheduler;
return Task.Factory.StartNew(() => loadComponents(components, deps, true), linkedSource.Token, TaskCreationOptions.HideScheduler, taskScheduler).ContinueWith(t =>
{
@@ -1340,11 +1340,11 @@ namespace osu.Framework.Graphics.Containers
}
}
private float cornerExponent = 2.5f;
private float cornerExponent = 2f;
/// <summary>
/// Determines how gentle the curve of the corner straightens. A value of 2 results in
/// circular arcs, a value of 2.5 (default) results in something closer to apple's "continuous corner".
/// Determines how gentle the curve of the corner straightens. A value of 2 (default) results in
/// circular arcs, a value of 2.5 results in something closer to apple's "continuous corner".
/// Values between 2 and 10 result in varying degrees of "continuousness", where larger values are smoother.
/// Values between 1 and 2 result in a "flatter" appearance than round corners.
/// Values between 0 and 1 result in a concave, round corner as opposed to a convex round corner,

View File

@@ -66,7 +66,7 @@ namespace osu.Framework.Graphics
private static readonly GlobalStatistic<int> total_count = GlobalStatistics.Get<int>(nameof(Drawable), $"Total {nameof(Drawable)}s");
private static readonly GlobalStatistic<int> finalize_disposals = GlobalStatistics.Get<int>(nameof(Drawable), "Finalizer disposals");
internal bool IsLongLoading => GetType().GetCustomAttribute<LongRunningLoadAttribute>() != null;
internal bool IsLongRunning => GetType().GetCustomAttribute<LongRunningLoadAttribute>() != null;
/// <summary>
/// Disposes this drawable.
@@ -223,8 +223,8 @@ namespace osu.Framework.Graphics
{
lock (loadLock)
{
if (!isDirectAsyncContext && IsLongLoading)
throw new InvalidOperationException("Tried to load long-loading in non-async context");
if (!isDirectAsyncContext && IsLongRunning)
throw new InvalidOperationException("Tried to load a long-running drawable in a non-direct async context. See https://git.io/Je1YF for more details.");
if (IsDisposed)
throw new ObjectDisposedException(ToString(), "Attempting to load an already disposed drawable.");

View File

@@ -71,8 +71,13 @@ namespace osu.Framework.Graphics.Textures
{
}
private static bool stbiNotFound;
internal static Image<TPixel> LoadFromStream<TPixel>(Stream stream) where TPixel : unmanaged, IPixel<TPixel>
{
if (stbiNotFound)
return Image.Load<TPixel>(stream);
long initialPos = stream.Position;
try
@@ -86,6 +91,9 @@ namespace osu.Framework.Graphics.Textures
}
catch (Exception e)
{
if (e is DllNotFoundException)
stbiNotFound = true;
Logger.Error(e, "Texture could not be loaded via STB; falling back to ImageSharp.");
stream.Position = initialPos;
return Image.Load<TPixel>(stream);

View File

@@ -7,7 +7,6 @@ using osu.Framework.Logging;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Development;
using osu.Framework.Timing;
@@ -23,8 +22,6 @@ namespace osu.Framework.Graphics.Visualisation
protected override bool BlockPositionalInput => false;
private Bindable<bool> enabled;
private StopwatchClock clock;
private readonly Box box;
@@ -113,21 +110,17 @@ namespace osu.Framework.Graphics.Visualisation
private void setHoldState(bool controlPressed)
{
box.Alpha = controlPressed ? 1 : background_alpha;
clock.Rate = controlPressed ? 0 : 1;
if (clock != null) clock.Rate = controlPressed ? 0 : 1;
}
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager config)
{
enabled = config.GetBindable<bool>(FrameworkSetting.ShowLogOverlay);
enabled.ValueChanged += e => State.Value = e.NewValue ? Visibility.Visible : Visibility.Hidden;
enabled.TriggerChange();
}
protected override void PopIn()
{
Logger.NewEntry += addEntry;
enabled.Value = true;
this.FadeIn(100);
}
@@ -135,7 +128,6 @@ namespace osu.Framework.Graphics.Visualisation
{
Logger.NewEntry -= addEntry;
setHoldState(false);
enabled.Value = false;
this.FadeOut(100);
}

View File

@@ -19,10 +19,10 @@ namespace osu.Framework.IO.Stores
{
string filePath = Path.Combine(Path.GetDirectoryName(Assembly.GetCallingAssembly().Location), dllName);
// prefer the local file if it exists, else load from standard dependency resolver.
assembly = System.IO.File.Exists(filePath) ? Assembly.LoadFile(filePath) : Assembly.Load(Path.GetFileNameWithoutExtension(dllName));
// prefer the local file if it exists, else load from assembly cache.
assembly = System.IO.File.Exists(filePath) ? Assembly.LoadFrom(filePath) : Assembly.Load(Path.GetFileNameWithoutExtension(dllName));
prefix = assembly.GetName().Name;
prefix = Path.GetFileNameWithoutExtension(dllName);
}
public DllResourceStore(AssemblyName name)

View File

@@ -197,6 +197,8 @@ namespace osu.Framework.IO.Stores
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
nestedFontStores.ForEach(f => f.Dispose());
glyphStores.ForEach(g => g.Dispose());
}
}

View File

@@ -21,6 +21,7 @@ namespace osu.Framework.IO.Stores
/// </summary>
/// <remarks>
/// This results in memory efficient lookups with good performance on solid state backed devices.
/// Consider <see cref="TimedExpiryGlyphStore"/> if disk IO is limited and memory usage is not an issue.
/// </remarks>
public class RawCachingGlyphStore : GlyphStore
{
@@ -130,8 +131,11 @@ namespace osu.Framework.IO.Stores
{
base.Dispose(disposing);
foreach (var h in pageStreamHandles)
h.Value.Dispose();
if (pageStreamHandles != null)
{
foreach (var h in pageStreamHandles)
h.Value?.Dispose();
}
}
private byte[] readBuffer;

View File

@@ -142,15 +142,9 @@ namespace osu.Framework.IO.Stores
{
foreach (string f in filenames)
{
try
{
var result = store.GetStream(f);
if (result != null)
return result;
}
catch
{
}
var result = store.GetStream(f);
if (result != null)
return result;
}
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
namespace osu.Framework.IO.Stores
{
/// <summary>
/// A glyph store which caches font sprite sheets in memory temporary, to allow for more efficient retrieval.
/// </summary>
/// <remarks>
/// This store has a higher memory overhead than <see cref="RawCachingGlyphStore"/>, but better performance and zero disk footprint.
/// </remarks>
public class TimedExpiryGlyphStore : GlyphStore
{
private readonly TimedExpiryCache<int, TextureUpload> texturePages = new TimedExpiryCache<int, TextureUpload>();
public TimedExpiryGlyphStore(ResourceStore<byte[]> store, string assetName = null)
: base(store, assetName)
{
}
protected override TextureUpload GetPageImage(int page)
{
if (!texturePages.TryGetValue(page, out var image))
{
loadedPageCount++;
texturePages.Add(page, image = base.GetPageImage(page));
}
return image;
}
private int loadedPageCount;
public override string ToString() => $@"GlyphStore({AssetName}) LoadedPages:{loadedPageCount} LoadedGlyphs:{LoadedGlyphCount}";
protected override void Dispose(bool disposing)
{
texturePages.Dispose();
}
}
}

View File

@@ -299,7 +299,7 @@ namespace osu.Framework.Logging
IEnumerable<string> lines = logOutput
.Replace(@"\r\n", @"\n")
.Split('\n')
.Select(s => $@"{DateTime.UtcNow.ToString("yyyy-MM-dd hh:mm:ss", CultureInfo.InvariantCulture)}: {s.Trim()}");
.Select(s => $@"{DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)}: {s.Trim()}");
if (outputToListeners)
{

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Platform.Linux.Native;
using osu.Framework.Platform.Linux.Sdl;
using osuTK;
@@ -18,6 +19,9 @@ namespace osu.Framework.Platform.Linux
base.SetupForRun();
Window = new LinuxGameWindow();
// required for the time being to address libbass_fx.so load failures (see https://github.com/ppy/osu/issues/2852)
Library.Load("libbass.so", Library.LoadFlags.RTLD_LAZY | Library.LoadFlags.RTLD_GLOBAL);
}
protected override Storage GetStorage(string baseName) => new LinuxStorage(baseName, this);

View File

@@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace osu.Framework.Platform.Linux.Native
{
public static class Library
{
[DllImport("libdl.so.2", EntryPoint = "dlopen")]
private static extern IntPtr dlopen(string library, LoadFlags flags);
/// <summary>
/// Loads a library with flags to use with dlopen. Uses <see cref="LoadFlags"/> for the flags
///
/// Uses NATIVE_DLL_SEARCH_DIRECTORIES and then ld.so for library paths
/// </summary>
/// <param name="library">Full name of the library</param>
/// <param name="flags">See 'man dlopen' for more information.</param>
public static void Load(string library, LoadFlags flags)
{
var paths = (string)AppContext.GetData("NATIVE_DLL_SEARCH_DIRECTORIES");
foreach (var path in paths.Split(':'))
{
if (dlopen(Path.Combine(path, library), flags) != IntPtr.Zero)
break;
}
}
[Flags]
public enum LoadFlags
{
RTLD_LAZY = 0x00001,
RTLD_NOW = 0x00002,
RTLD_BINDING_MASK = 0x00003,
RTLD_NOLOAD = 0x00004,
RTLD_DEEPBIND = 0x00008,
RTLD_GLOBAL = 0x00100,
RTLD_LOCAL = 0x00000,
RTLD_NODELETE = 0x01000
}
}
}

View File

@@ -2,26 +2,34 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
namespace osu.Framework.Platform.MacOS.Native
{
internal static class Cocoa
{
internal const string LIB_DL = "libdl.dylib";
internal const string LIB_APPKIT = "/System/Library/Frameworks/AppKit.framework/AppKit";
internal const string LIB_OBJ_C = "/usr/lib/libobjc.dylib";
internal const string LIB_CORE_GRAPHICS = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics";
internal const int RTLD_NOW = 2;
[DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")]
public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector);
[DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")]
public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector, int arg);
[DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")]
public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector, ulong ulong1);
[DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")]
public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector, IntPtr ptr1);
[DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")]
public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector, IntPtr intPtr1, int int1);
[DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")]
public static extern IntPtr SendIntPtr(IntPtr receiver, IntPtr selector, IntPtr ptr1, IntPtr ptr2);
@@ -61,13 +69,7 @@ namespace osu.Framework.Platform.MacOS.Native
[DllImport(LIB_OBJ_C, EntryPoint = "objc_msgSend")]
public static extern void SendVoid(IntPtr receiver, IntPtr selector, IntPtr intPtr1, IntPtr intPtr2, IntPtr intPtr3, IntPtr intPtr4);
private static readonly Type type_cocoa = typeof(osuTK.NativeWindow).Assembly.GetTypes().Single(x => x.Name == "Cocoa");
private static readonly MethodInfo method_cocoa_from_ns_string = type_cocoa.GetMethod("FromNSString");
private static readonly MethodInfo method_cocoa_to_ns_string = type_cocoa.GetMethod("ToNSString");
private static readonly MethodInfo method_cocoa_get_string_constant = type_cocoa.GetMethod("GetStringConstant");
public static IntPtr AppKitLibrary = (IntPtr)type_cocoa.GetField("AppKitLibrary").GetValue(null);
public static IntPtr FoundationLibrary = (IntPtr)type_cocoa.GetField("FoundationLibrary").GetValue(null);
public static IntPtr AppKitLibrary;
[DllImport(LIB_CORE_GRAPHICS, EntryPoint = "CGCursorIsVisible")]
public static extern bool CGCursorIsVisible();
@@ -75,10 +77,37 @@ namespace osu.Framework.Platform.MacOS.Native
[DllImport(LIB_CORE_GRAPHICS, EntryPoint = "CGEventSourceFlagsState")]
public static extern ulong CGEventSourceFlagsState(int stateID);
public static string FromNSString(IntPtr handle) => (string)method_cocoa_from_ns_string.Invoke(null, new object[] { handle });
[DllImport(LIB_DL)]
private static extern IntPtr dlsym(IntPtr handle, string name);
public static IntPtr ToNSString(string str) => (IntPtr)method_cocoa_to_ns_string.Invoke(null, new object[] { str });
[DllImport(LIB_DL)]
private static extern IntPtr dlopen(string fileName, int flags);
public static IntPtr GetStringConstant(IntPtr handle, string symbol) => (IntPtr)method_cocoa_get_string_constant.Invoke(null, new object[] { handle, symbol });
static Cocoa()
{
AppKitLibrary = dlopen(LIB_APPKIT, RTLD_NOW);
}
private static readonly IntPtr sel_c_string_using_encoding = Selector.Get("cStringUsingEncoding:");
public static string FromNSString(IntPtr handle) => Marshal.PtrToStringUni(SendIntPtr(handle, sel_c_string_using_encoding, (uint)NSStringEncoding.Unicode));
public static unsafe IntPtr ToNSString(string str)
{
if (str == null)
return IntPtr.Zero;
fixed (char* ptrFirstChar = str)
{
var handle = SendIntPtr(Class.Get("NSString"), Selector.Get("alloc"));
return SendIntPtr(handle, Selector.Get("initWithCharacters:length:"), (IntPtr)ptrFirstChar, str.Length);
}
}
public static IntPtr GetStringConstant(IntPtr handle, string symbol)
{
IntPtr ptr = dlsym(handle, symbol);
return ptr == IntPtr.Zero ? IntPtr.Zero : Marshal.ReadIntPtr(ptr);
}
}
}

View File

@@ -0,0 +1,123 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Framework.Platform.MacOS.Native
{
public enum NSStringEncoding : uint
{
/// <summary>
/// 7-bit ASCII encoding within 8 bit chars.
/// </summary>
ASCII = 1,
/// <summary>
/// 8-bit ASCII encoding with NEXTSTEP extensions.
/// </summary>
NEXTSTEP = 2,
/// <summary>
/// 8-bit EUC encoding for Japanese text.
/// </summary>
JapaneseEUC = 3,
/// <summary>
/// 8-bit representation of Unicode characters.
/// </summary>
UTF8 = 4,
/// <summary>
/// 8-bit ISO Latin 1 encoding.
/// </summary>
ISOLatin1 = 5,
/// <summary>
/// 8-bit Adobe Symbol encoding vector.
/// </summary>
Symbol = 6,
/// <summary>
/// 7-bit verbose ASCII to represent all Unicode characters.
/// </summary>
NonLossyASCII = 7,
/// <summary>
/// 8-bit Shift-JIS encoding for Japanese text.
/// </summary>
ShiftJIS = 8,
/// <summary>
/// 8-bit ISO Latin 2 encoding.
/// </summary>
ISOLatin2 = 9,
/// <summary>
/// The canonical Unicode encoding.
/// </summary>
Unicode = 10,
/// <summary>
/// Microsoft Windows codepage 1251, encoding Cyrillic characters.
/// </summary>
WindowsCP1251 = 11,
/// <summary>
/// Microsoft Windows codepage 1252, encoding Latin 1 characters.
/// </summary>
WindowsCP1252 = 12,
/// <summary>
/// Microsoft Windows codepage 1253, encoding Greek characters.
/// </summary>
WindowsCP1253 = 13,
/// <summary>
/// Microsoft Windows codepage 1254, encoding Turkish characters.
/// </summary>
WindowsCP1254 = 14,
/// <summary>
/// Microsoft Windows codepage 1250, encoding Latin 2 characters.
/// </summary>
WindowsCP1250 = 15,
/// <summary>
/// ISO 2022 Japanese encoding.
/// </summary>
ISO2022JP = 21,
/// <summary>
/// Classing Macintosh Roman encoding.
/// </summary>
MacOSRoman = 30,
/// <summary>
/// 16-bit UTF encoding.
/// </summary>
UTF16 = Unicode,
/// <summary>
/// <see cref="UTF16"/> encoding with explicit endianness.
/// </summary>
UTF16BigEndian = 0x90000100,
/// <summary>
/// <see cref="UTF16"/> encoding with explicit endianness.
/// </summary>
UTF16LittleEndian = 0x94000100,
/// <summary>
/// 32-bit UTF encoding.
/// </summary>
UTF32 = 0x8c000100,
/// <summary>
/// <see cref="UTF32"/> encoding with explicit endianness.
/// </summary>
UTF32BigEndian = 0x98000100,
/// <summary>
/// <see cref="UTF32"/> encoding with explicit endianness.
/// </summary>
UTF32LittleEndian = 0x9c000100
}
}

View File

@@ -65,6 +65,9 @@ namespace osu.Framework.Screens
/// <summary>
/// Pushes a <see cref="IScreen"/> to this <see cref="ScreenStack"/>.
/// </summary>
/// <remarks>
/// An <see cref="IScreen"/> cannot be pushed multiple times.
/// </remarks>
/// <param name="screen">The <see cref="IScreen"/> to push.</param>
public void Push(IScreen screen)
{
@@ -101,6 +104,12 @@ namespace osu.Framework.Screens
var newScreenDrawable = newScreen.AsDrawable();
if (newScreenDrawable.IsLoaded)
throw new InvalidOperationException("A screen should not be loaded before being pushed.");
// this needs to be queued here before the load is begun so it preceed any potential OnSuspending event (also attached to OnLoadComplete).
newScreenDrawable.OnLoadComplete += _ => newScreen.OnEntering(source);
if (source == null)
{
// this is the first screen to be loaded.
@@ -132,7 +141,6 @@ namespace osu.Framework.Screens
suspend(parent, child);
AddInternal(child.AsDrawable());
child.OnEntering(parent);
}
/// <summary>

View File

@@ -161,7 +161,7 @@ namespace osu.Framework.Testing
#if RELEASE
"RELEASE",
#endif
}, languageVersion: LanguageVersion.CSharp7_3);
}, languageVersion: LanguageVersion.Latest);
var references = assemblies.Select(a => MetadataReference.CreateFromFile(a));
while (!checkFileReady(lastTouchedFile))

View File

@@ -68,16 +68,23 @@ namespace osu.Framework.Testing
public void DestroyGameHost()
{
host.Exit();
runTask.Wait();
host.Dispose();
try
{
// clean up after each run
host.Storage.DeleteDirectory(string.Empty);
runTask.Wait();
}
catch
finally
{
host.Dispose();
try
{
// clean up after each run
host.Storage.DeleteDirectory(string.Empty);
}
catch
{
}
}
}

View File

@@ -59,6 +59,14 @@ namespace osu.Framework.Threading
protected override void PerformExit()
{
base.PerformExit();
lock (managers)
{
foreach (var manager in managers)
manager.Dispose();
managers.Clear();
}
ManagedBass.Bass.Free();
}
}

View File

@@ -104,11 +104,11 @@ namespace osu.Framework.Threading
while (getNextTask(out ScheduledDelegate sd))
{
if (sd.Cancelled || sd.Completed)
continue;
//todo: error handling
sd.RunTask();
if (!sd.Cancelled && !sd.Completed)
{
//todo: error handling
sd.RunTask();
}
if (++countRun == countToRun)
break;

View File

@@ -39,6 +39,10 @@ namespace osu.Framework.Timing
private double currentTime;
public double ProposedCurrentTime => useInterpolatedSourceTime ? base.CurrentTime : decoupledClock.CurrentTime;
public double ProposedElapsedTime => useInterpolatedSourceTime ? base.ElapsedFrameTime : decoupledClock.ElapsedFrameTime;
public override bool IsRunning => decoupledClock.IsRunning; // we always want to use our local IsRunning state, as it is more correct.
private double elapsedFrameTime;
@@ -62,40 +66,49 @@ namespace osu.Framework.Timing
{
base.ProcessFrame();
bool sourceRunning = Source?.IsRunning ?? false;
decoupledStopwatch.Rate = adjustableSource?.Rate ?? 1;
bool sourceRunning = Source?.IsRunning ?? false;
// if interpolating based on the source, keep the decoupled clock in sync with the interpolated time.
if (IsCoupled && sourceRunning)
decoupledStopwatch.Seek(base.CurrentTime);
// process the decoupled clock to update the current proposed time.
decoupledClock.ProcessFrame();
// if the source clock is started as a result of becoming capable of handling the decoupled time, the proposed time may change to reflect the interpolated source time.
// however the interpolated source time that was calculated inside base.ProcessFrame() (above) did not consider the current (post-seek) time of the source.
// in all other cases the proposed time will match before and after clocks are started/stopped.
double proposedTime = ProposedCurrentTime;
double elapsedTime = ProposedElapsedTime;
if (IsRunning)
{
if (IsCoupled)
{
// when coupled, we want to stop when our source clock stops.
if (sourceRunning)
decoupledStopwatch.Seek(base.CurrentTime);
else
if (!sourceRunning)
Stop();
}
else
{
// when decoupled, if we're running but our source isn't, we should try a seek to see if it's capable to handle the current time.
// when decoupled and running, we should try to start the source clock it if it's capable of handling the current time.
if (!sourceRunning)
Start();
}
}
else if (IsCoupled && sourceRunning)
{
// when coupled and not running, we want to start when the source clock starts.
Start();
decoupledStopwatch.Seek(CurrentTime);
}
decoupledClock.ProcessFrame();
elapsedFrameTime = elapsedTime;
double proposedTime = useInterpolatedSourceTime ? base.CurrentTime : decoupledClock.CurrentTime;
elapsedFrameTime = useInterpolatedSourceTime ? base.ElapsedFrameTime : decoupledClock.ElapsedFrameTime;
currentTime = elapsedFrameTime < 0 ? proposedTime : Math.Max(currentTime, proposedTime);
// the source may be started during playback but remain behind the current time in the playback direction for a number of frames.
// in such cases, the current time should remain paused until the source time catches up.
currentTime = elapsedFrameTime < 0 ? Math.Min(currentTime, proposedTime) : Math.Max(currentTime, proposedTime);
}
public override void ChangeSource(IClock source)
@@ -120,7 +133,7 @@ namespace osu.Framework.Timing
{
if (adjustableSource?.IsRunning == false)
{
if (adjustableSource.Seek(CurrentTime))
if (adjustableSource.Seek(ProposedCurrentTime))
//only start the source clock if our time values match.
//this handles the case where we seeked to an unsupported value and the source clock is out of sync.
adjustableSource.Start();

View File

@@ -27,7 +27,7 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="SharpFNT" Version="1.1.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0007" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="1.1.57604" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="ManagedBass" Version="2.0.4" />
@@ -35,7 +35,7 @@
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
<PackageReference Include="ppy.osuTK.NS20" Version="1.0.115" />
<PackageReference Include="StbiSharp" Version="1.0.8" />
<PackageReference Include="StbiSharp" Version="1.0.10" />
</ItemGroup>
<ItemGroup Condition="$(TargetFrameworkIdentifier) == '.NETCoreApp'">
<!-- DO NOT use ProjectReference for native packaging project.