mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-15 03:20:30 +00:00
Merge branch 'master' into fxcop
This commit is contained in:
20
.idea/.idea.osu-framework.Desktop/.idea/runConfigurations/Benchmarks.xml
generated
Normal file
20
.idea/.idea.osu-framework.Desktop/.idea/runConfigurations/Benchmarks.xml
generated
Normal 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
16
.vscode/launch.json
vendored
@@ -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}"
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
96
osu.Framework.Benchmarks/BenchmarkFontLoading.cs
Normal file
96
osu.Framework.Benchmarks/BenchmarkFontLoading.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
osu.Framework.Benchmarks/BenchmarkTest.cs
Normal file
23
osu.Framework.Benchmarks/BenchmarkTest.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
17
osu.Framework.Benchmarks/Program.cs
Normal file
17
osu.Framework.Benchmarks/Program.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
osu.Framework.Benchmarks/osu.Framework.Benchmarks.csproj
Normal file
21
osu.Framework.Benchmarks/osu.Framework.Benchmarks.csproj
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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. -->
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
47
osu.Framework/IO/Stores/TimedExpiryGlyphStore.cs
Normal file
47
osu.Framework/IO/Stores/TimedExpiryGlyphStore.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
46
osu.Framework/Platform/Linux/Native/Library.cs
Normal file
46
osu.Framework/Platform/Linux/Native/Library.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
123
osu.Framework/Platform/MacOS/Native/NSStringEncoding.cs
Normal file
123
osu.Framework/Platform/MacOS/Native/NSStringEncoding.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user