mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-13 11:20:31 +00:00
Merge branch 'master' into input-cache-sgen
This commit is contained in:
@@ -30,7 +30,7 @@ This framework is intended to take steps beyond what you would normally expect f
|
||||
- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download).
|
||||
- When running on linux, please have a system-wide ffmpeg installation available to support video decoding.
|
||||
- When running on Windows 7 or 8.1, *[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net60&pivots=os-windows#dependencies)** may be required to correctly run .NET 6 applications if your operating system is not up-to-date with the latest service packs.
|
||||
- When working with the codebase, we recommend using an IDE with intellisense and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [Jetbrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
|
||||
- When working with the codebase, we recommend using an IDE with intellisense and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [Jetbrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) plugin installed.
|
||||
|
||||
### Building
|
||||
|
||||
|
||||
38
UseLocalVeldrid.ps1
Normal file
38
UseLocalVeldrid.ps1
Normal file
@@ -0,0 +1,38 @@
|
||||
# Run this script to use a local copy of veldrid rather than fetching it from nuget.
|
||||
# It expects the veldrid directory to be at the same level as the osu-framework directory
|
||||
#
|
||||
# https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects
|
||||
|
||||
$FRAMEWORK_CSPROJ="osu.Framework/osu.Framework.csproj"
|
||||
$SLN="osu-framework.sln"
|
||||
|
||||
dotnet remove $FRAMEWORK_CSPROJ reference ppy.Veldrid;
|
||||
|
||||
dotnet sln $SLN add ../veldrid/src/Veldrid/Veldrid.csproj `
|
||||
../veldrid/src/Veldrid.MetalBindings/Veldrid.MetalBindings.csproj `
|
||||
../veldrid/src/Veldrid.OpenGLBindings/Veldrid.OpenGLBindings.csproj;
|
||||
|
||||
dotnet add $FRAMEWORK_CSPROJ reference ../veldrid/src/Veldrid/Veldrid.csproj;
|
||||
|
||||
$TMP=New-TemporaryFile
|
||||
|
||||
$SLNF=Get-Content "osu-framework.Desktop.slnf" | ConvertFrom-Json
|
||||
$SLNF.solution.projects += ("../veldrid/src/Veldrid/Veldrid.csproj")
|
||||
$SLNF.solution.projects += ("../veldrid/src/Veldrid.OpenGLBindings/Veldrid.OpenGLBindings.csproj")
|
||||
$SLNF.solution.projects += ("../veldrid/src/Veldrid.MetalBindings/Veldrid.MetalBindings.csproj")
|
||||
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
|
||||
Move-Item -Path $TMP -Destination "osu-framework.Desktop.slnf" -Force
|
||||
|
||||
$SLNF=Get-Content "osu-framework.Android.slnf" | ConvertFrom-Json
|
||||
$SLNF.solution.projects += ("../veldrid/src/Veldrid/Veldrid.csproj")
|
||||
$SLNF.solution.projects += ("../veldrid/src/Veldrid.OpenGLBindings/Veldrid.OpenGLBindings.csproj")
|
||||
$SLNF.solution.projects += ("../veldrid/src/Veldrid.MetalBindings/Veldrid.MetalBindings.csproj")
|
||||
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
|
||||
Move-Item -Path $TMP -Destination "osu-framework.Android.slnf" -Force
|
||||
|
||||
$SLNF=Get-Content "osu-framework.iOS.slnf" | ConvertFrom-Json
|
||||
$SLNF.solution.projects += ("../veldrid/src/Veldrid/Veldrid.csproj")
|
||||
$SLNF.solution.projects += ("../veldrid/src/Veldrid.OpenGLBindings/Veldrid.OpenGLBindings.csproj")
|
||||
$SLNF.solution.projects += ("../veldrid/src/Veldrid.MetalBindings/Veldrid.MetalBindings.csproj")
|
||||
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
|
||||
Move-Item -Path $TMP -Destination "osu-framework.iOS.slnf" -Force
|
||||
28
UseLocalVeldrid.sh
Executable file
28
UseLocalVeldrid.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Run this script to use a local copy of veldrid rather than fetching it from nuget.
|
||||
# It expects the veldrid directory to be at the same level as the osu-framework directory
|
||||
#
|
||||
# https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects
|
||||
|
||||
FRAMEWORK_CSPROJ="osu.Framework/osu.Framework.csproj"
|
||||
SLN="osu-framework.sln"
|
||||
|
||||
dotnet remove $FRAMEWORK_CSPROJ reference ppy.Veldrid
|
||||
|
||||
dotnet sln $SLN add ../veldrid/src/Veldrid/Veldrid.csproj \
|
||||
../veldrid/src/Veldrid.MetalBindings/Veldrid.MetalBindings.csproj \
|
||||
../veldrid/src/Veldrid.OpenGLBindings/Veldrid.OpenGLBindings.csproj
|
||||
|
||||
dotnet add $FRAMEWORK_CSPROJ reference ../veldrid/src/Veldrid/Veldrid.csproj
|
||||
|
||||
tmp=$(mktemp)
|
||||
|
||||
jq '.solution.projects += ["../veldrid/src/Veldrid/Veldrid.csproj", "../veldrid/src/Veldrid.MetalBindings/Veldrid.MetalBindings.csproj", "../veldrid/src/Veldrid.OpenGLBindings/Veldrid.OpenGLBindings.csproj"]' osu-framework.Desktop.slnf > $tmp
|
||||
mv -f $tmp osu-framework.Desktop.slnf
|
||||
|
||||
jq '.solution.projects += ["../veldrid/src/Veldrid/Veldrid.csproj", "../veldrid/src/Veldrid.MetalBindings/Veldrid.MetalBindings.csproj", "../veldrid/src/Veldrid.OpenGLBindings/Veldrid.OpenGLBindings.csproj"]' osu-framework.Android.slnf > $tmp
|
||||
mv -f $tmp osu-framework.Android.slnf
|
||||
|
||||
jq '.solution.projects += ["../veldrid/src/Veldrid/Veldrid.csproj", "../veldrid/src/Veldrid.MetalBindings/Veldrid.MetalBindings.csproj", "../veldrid/src/Veldrid.OpenGLBindings/Veldrid.OpenGLBindings.csproj"]' osu-framework.iOS.slnf > $tmp
|
||||
mv -f $tmp osu-framework.iOS.slnf
|
||||
7
global.json
Normal file
7
global.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "6.0.100",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CollectionNeverQueried_002ELocal/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CommentTypo/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CompareOfFloatsByEqualityOperator/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertClosureToMethodGroup/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertClosureToMethodGroup/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertConditionalTernaryExpressionToSwitchExpression/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertIfDoToWhile/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertIfStatementToConditionalTernaryExpression/@EntryIndexedValue">WARNING</s:String>
|
||||
@@ -349,6 +349,7 @@
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=JIT/@EntryIndexedValue">JIT</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LTRB/@EntryIndexedValue">LTRB</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MD/@EntryIndexedValue">MD5</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NRT/@EntryIndexedValue">NRT</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NS/@EntryIndexedValue">NS</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OS/@EntryIndexedValue">OS</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PM/@EntryIndexedValue">PM</s:String>
|
||||
|
||||
@@ -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 System.Collections.Generic;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osuTK.Graphics;
|
||||
@@ -10,24 +11,29 @@ namespace osu.Framework.Benchmarks
|
||||
[MemoryDiagnoser]
|
||||
public class BenchmarkColourInfo
|
||||
{
|
||||
private ColourInfo colourInfo;
|
||||
[ParamsSource(nameof(ColourParams))]
|
||||
public ColourInfo Colour { get; set; }
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
public IEnumerable<ColourInfo> ColourParams
|
||||
{
|
||||
colourInfo = ColourInfo.SingleColour(Color4.Transparent);
|
||||
get
|
||||
{
|
||||
yield return ColourInfo.SingleColour(Color4.Transparent);
|
||||
yield return ColourInfo.SingleColour(Color4.Cyan);
|
||||
yield return ColourInfo.SingleColour(Color4.DarkGray);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public SRGBColour ConvertToSRGBColour() => colourInfo;
|
||||
public SRGBColour ConvertToSRGBColour() => Colour;
|
||||
|
||||
[Benchmark]
|
||||
public Color4 ConvertToColor4() => ((SRGBColour)colourInfo).Linear;
|
||||
public Color4 ConvertToColor4() => ((SRGBColour)Colour).Linear;
|
||||
|
||||
[Benchmark]
|
||||
public Color4 ExtractAndConvertToColor4()
|
||||
{
|
||||
colourInfo.TryExtractSingleColour(out SRGBColour colour);
|
||||
Colour.TryExtractSingleColour(out SRGBColour colour);
|
||||
return colour.Linear;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
||||
<PackageReference Include="nunit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.5" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
|
||||
<!-- The following two are unused, but resolves warning MSB3277. -->
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageReference Include="System.CodeDom" Version="6.0.0" />
|
||||
<PackageReference Include="System.CodeDom" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -13,12 +13,12 @@ namespace osu.Framework.SourceGeneration.Analysers
|
||||
|
||||
public static readonly DiagnosticDescriptor MAKE_DI_CLASS_PARTIAL = new DiagnosticDescriptor(
|
||||
"OFSG001",
|
||||
"This class is a candidate for dependency injection and should be partial",
|
||||
"This class is a candidate for dependency injection and should be partial",
|
||||
"This type, or a nested type, is a candidate for dependency injection and should be partial",
|
||||
"This type, or a nested type, is a candidate for dependency injection and should be partial",
|
||||
"Performance",
|
||||
DiagnosticSeverity.Warning,
|
||||
true,
|
||||
"Classes that are candidates for dependency injection should be made partial to benefit from compile-time optimisations.");
|
||||
"Types that are candidates for dependency injection should be made partial to benefit from compile-time optimisations.");
|
||||
|
||||
#pragma warning restore RS2008
|
||||
}
|
||||
|
||||
@@ -24,19 +24,38 @@ namespace osu.Framework.SourceGeneration.Analysers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyses class definitions for implementations of IDrawable, ISourceGeneratedDependencyActivator, and Transformable.
|
||||
/// Analyses class definitions for implementations of IDependencyInjectionCandidateInterface.
|
||||
/// </summary>
|
||||
private void analyseClass(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var classSyntax = (ClassDeclarationSyntax)context.Node;
|
||||
|
||||
if (classSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
|
||||
if (classSyntax.Ancestors().OfType<ClassDeclarationSyntax>().Any())
|
||||
return;
|
||||
|
||||
INamedTypeSymbol? type = context.SemanticModel.GetDeclaredSymbol(classSyntax);
|
||||
analyseRecursively(context, classSyntax);
|
||||
|
||||
if (type?.AllInterfaces.Any(SyntaxHelpers.IsIDependencyInjectionCandidateInterface) == true)
|
||||
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.MAKE_DI_CLASS_PARTIAL, context.Node.GetLocation(), context.Node));
|
||||
static bool analyseRecursively(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax node)
|
||||
{
|
||||
bool requiresPartial = false;
|
||||
|
||||
// Child nodes always have to be analysed to provide diagnostics.
|
||||
foreach (var nested in node.DescendantNodes().OfType<ClassDeclarationSyntax>())
|
||||
requiresPartial |= analyseRecursively(context, nested);
|
||||
|
||||
// - If at least one child requires partial, then this node also needs to be partial regardless of its own type (optimisation).
|
||||
// - If no child requires partial, we need to check if this node is a DI candidate (e.g. If the node has no nested types).
|
||||
if (!requiresPartial)
|
||||
requiresPartial = context.SemanticModel.GetDeclaredSymbol(node)?.AllInterfaces.Any(SyntaxHelpers.IsIDependencyInjectionCandidateInterface) == true;
|
||||
|
||||
// Whether the node is already partial.
|
||||
bool isPartial = node.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
|
||||
|
||||
if (requiresPartial && !isPartial)
|
||||
context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.MAKE_DI_CLASS_PARTIAL, node.GetLocation(), node));
|
||||
|
||||
return requiresPartial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,18 +101,18 @@ namespace osu.Framework.SourceGeneration
|
||||
=> IsCachedAttribute(attribute?.AttributeClass);
|
||||
|
||||
public static bool IsBackgroundDependencyLoaderAttribute(ITypeSymbol? type)
|
||||
=> type?.Name == "BackgroundDependencyLoaderAttribute";
|
||||
=> type != null && GetFullyQualifiedTypeName(type) == "osu.Framework.Allocation.BackgroundDependencyLoaderAttribute";
|
||||
|
||||
public static bool IsResolvedAttribute(ITypeSymbol? type)
|
||||
=> type?.Name == "ResolvedAttribute";
|
||||
=> type != null && GetFullyQualifiedTypeName(type) == "osu.Framework.Allocation.ResolvedAttribute";
|
||||
|
||||
public static bool IsCachedAttribute(ITypeSymbol? type)
|
||||
=> type?.Name == "CachedAttribute";
|
||||
=> type != null && GetFullyQualifiedTypeName(type) == "osu.Framework.Allocation.CachedAttribute";
|
||||
|
||||
public static bool IsIDependencyInjectionCandidateInterface(ITypeSymbol? type)
|
||||
=> type?.Name == "IDependencyInjectionCandidate";
|
||||
=> type != null && GetFullyQualifiedTypeName(type) == "osu.Framework.Allocation.IDependencyInjectionCandidate";
|
||||
|
||||
public static string GetFullyQualifiedTypeName(INamedTypeSymbol type)
|
||||
public static string GetFullyQualifiedTypeName(ITypeSymbol type)
|
||||
=> type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,7 +7,7 @@ Templates to use when starting off with osu!framework. Create a fully-testable,
|
||||
```bash
|
||||
# install (or update) template package.
|
||||
# this only needs to be done once
|
||||
dotnet new -i ppy.osu.Framework.Templates
|
||||
dotnet new install ppy.osu.Framework.Templates
|
||||
|
||||
## IMPORTANT: Do not use spaces or hyphens in your project name for the following commands.
|
||||
## This does not play nice with the templating system.
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -44,7 +44,6 @@ namespace osu.Framework.Tests.Bindables
|
||||
|
||||
[TestCase(1.1f)]
|
||||
[TestCase("Not a value")]
|
||||
[TestCase("")]
|
||||
public void TestUnparsaebles(object value)
|
||||
{
|
||||
var bindable = new Bindable<TestEnum>();
|
||||
@@ -54,6 +53,18 @@ namespace osu.Framework.Tests.Bindables
|
||||
Assert.Throws<ArgumentException>(() => nullable.Parse(value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyString()
|
||||
{
|
||||
var bindable = new Bindable<TestEnum>();
|
||||
var nullable = new Bindable<TestEnum?>();
|
||||
|
||||
Assert.Throws<ArgumentException>(() => bindable.Parse(string.Empty));
|
||||
nullable.Parse(string.Empty);
|
||||
|
||||
Assert.That(nullable.Value, Is.Null);
|
||||
}
|
||||
|
||||
public enum TestEnum
|
||||
{
|
||||
Value1 = 0,
|
||||
|
||||
@@ -1067,6 +1067,36 @@ namespace osu.Framework.Tests.Bindables
|
||||
|
||||
#endregion
|
||||
|
||||
#region .ReplaceRange(index, count, newItems)
|
||||
|
||||
[Test]
|
||||
public void TestReplaceRangeNotifiesBoundLists()
|
||||
{
|
||||
string[] items = { "A", "B" };
|
||||
|
||||
bindableStringList.Add("0");
|
||||
bindableStringList.Add("1");
|
||||
|
||||
var list = new BindableList<string>();
|
||||
list.BindTo(bindableStringList);
|
||||
|
||||
NotifyCollectionChangedEventArgs triggeredArgs = null;
|
||||
list.CollectionChanged += (_, args) => triggeredArgs = args;
|
||||
|
||||
bindableStringList.ReplaceRange(0, 1, items);
|
||||
|
||||
Assert.That(list, Is.EquivalentTo(bindableStringList));
|
||||
Assert.That(list, Is.EquivalentTo(new[] { "A", "B", "1" }));
|
||||
|
||||
Assert.That(triggeredArgs.Action, Is.EqualTo(NotifyCollectionChangedAction.Replace));
|
||||
Assert.That(triggeredArgs.NewItems, Is.EquivalentTo(items));
|
||||
Assert.That(triggeredArgs.NewStartingIndex, Is.EqualTo(0));
|
||||
Assert.That(triggeredArgs.OldItems, Has.One.Items.EqualTo("0"));
|
||||
Assert.That(triggeredArgs.OldStartingIndex, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region .Clear()
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -107,6 +107,146 @@ namespace osu.Framework.Tests.Bindables
|
||||
Assert.That(value, Is.EqualTo(output));
|
||||
}
|
||||
|
||||
// Bindable<int>.Parse(null)
|
||||
[Test]
|
||||
public void TestParseNullIntoValueType()
|
||||
{
|
||||
Bindable<int> bindable = new Bindable<int>();
|
||||
Assert.That(() => bindable.Parse(null), Throws.ArgumentNullException);
|
||||
}
|
||||
|
||||
// Bindable<int>.Parse(string.Empty)
|
||||
[Test]
|
||||
public void TestParseEmptyStringIntoValueType()
|
||||
{
|
||||
Bindable<int> bindable = new Bindable<int>();
|
||||
Assert.Throws<FormatException>(() => bindable.Parse(string.Empty));
|
||||
}
|
||||
|
||||
// Bindable<int?>.Parse(null)
|
||||
[Test]
|
||||
public void TestParseNullIntoNullableValueType()
|
||||
{
|
||||
Bindable<int?> bindable = new Bindable<int?>();
|
||||
bindable.Parse(null);
|
||||
Assert.That(bindable.Value, Is.Null);
|
||||
}
|
||||
|
||||
// Bindable<int?>.Parse(string.Empty)
|
||||
[Test]
|
||||
public void TestParseEmptyStringIntoNullableValueType()
|
||||
{
|
||||
Bindable<int?> bindable = new Bindable<int?>();
|
||||
bindable.Parse(string.Empty);
|
||||
Assert.That(bindable.Value, Is.Null);
|
||||
}
|
||||
|
||||
// Bindable<Class>.Parse(null)
|
||||
[Test]
|
||||
public void TestParseNullIntoReferenceType()
|
||||
{
|
||||
Bindable<TestClass> bindable = new Bindable<TestClass>();
|
||||
bindable.Parse(null);
|
||||
Assert.That(bindable.Value, Is.Null);
|
||||
}
|
||||
|
||||
// Bindable<Class>.Parse(string.Empty)
|
||||
[Test]
|
||||
public void TestParseEmptyStringIntoReferenceType()
|
||||
{
|
||||
Bindable<TestClass> bindable = new Bindable<TestClass>();
|
||||
bindable.Parse(string.Empty);
|
||||
Assert.That(bindable.Value, Is.Null);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
// Bindable<Class>.Parse(null) -- NRT
|
||||
[Test]
|
||||
public void TestParseNullIntoReferenceTypeWithNRT()
|
||||
{
|
||||
Bindable<TestClass> bindable = new Bindable<TestClass>();
|
||||
bindable.Parse(null);
|
||||
Assert.That(bindable.Value, Is.Null);
|
||||
}
|
||||
|
||||
// Bindable<Class>.Parse(string.Empty) -- NRT
|
||||
[Test]
|
||||
public void TestParseEmptyStringIntoReferenceTypeWithNRT()
|
||||
{
|
||||
Bindable<TestClass> bindable = new Bindable<TestClass>();
|
||||
bindable.Parse(string.Empty);
|
||||
Assert.That(bindable.Value, Is.Null);
|
||||
}
|
||||
|
||||
// Bindable<Class?>.Parse(null) -- NRT
|
||||
[Test]
|
||||
public void TestParseNullIntoNullableReferenceTypeWithNRT()
|
||||
{
|
||||
Bindable<TestClass?> bindable = new Bindable<TestClass?>();
|
||||
bindable.Parse(null);
|
||||
Assert.That(bindable.Value, Is.Null);
|
||||
}
|
||||
|
||||
// Bindable<Class?>.Parse(string.Empty) -- NRT
|
||||
[Test]
|
||||
public void TestParseEmptyStringIntoNullableReferenceTypeWithNRT()
|
||||
{
|
||||
Bindable<TestClass?> bindable = new Bindable<TestClass?>();
|
||||
bindable.Parse(string.Empty);
|
||||
Assert.That(bindable.Value, Is.Null);
|
||||
}
|
||||
#nullable disable
|
||||
|
||||
[Test]
|
||||
public void TestParseNullIntoStringType()
|
||||
{
|
||||
Bindable<string> bindable = new Bindable<string>();
|
||||
bindable.Parse(null);
|
||||
Assert.That(bindable.Value, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestParseEmptyStringIntoStringType()
|
||||
{
|
||||
Bindable<string> bindable = new Bindable<string>();
|
||||
bindable.Parse(string.Empty);
|
||||
Assert.That(bindable.Value, Is.Empty);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
[Test]
|
||||
public void TestParseNullIntoStringTypeWithNRT()
|
||||
{
|
||||
Bindable<string> bindable = new Bindable<string>();
|
||||
bindable.Parse(null);
|
||||
Assert.That(bindable.Value, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestParseEmptyStringIntoStringTypeWithNRT()
|
||||
{
|
||||
Bindable<string> bindable = new Bindable<string>();
|
||||
bindable.Parse(string.Empty);
|
||||
Assert.That(bindable.Value, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestParseNullIntoNullableStringTypeWithNRT()
|
||||
{
|
||||
Bindable<string?> bindable = new Bindable<string?>();
|
||||
bindable.Parse(null);
|
||||
Assert.That(bindable.Value, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestParseEmptyStringIntoNullableStringTypeWithNRT()
|
||||
{
|
||||
Bindable<string?> bindable = new Bindable<string?>();
|
||||
bindable.Parse(string.Empty);
|
||||
Assert.That(bindable.Value, Is.Empty);
|
||||
}
|
||||
#nullable disable
|
||||
|
||||
private static IEnumerable<object[]> getParsingConversionTests()
|
||||
{
|
||||
var testTypes = new[]
|
||||
@@ -156,5 +296,10 @@ namespace osu.Framework.Tests.Bindables
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once ClassNeverInstantiated.Local
|
||||
private class TestClass
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,74 @@ namespace osu.Framework.Tests.Clocks
|
||||
|
||||
#endregion
|
||||
|
||||
#region Source changes
|
||||
|
||||
[Test]
|
||||
public void SourceChangeTransfersValueAdjustable()
|
||||
{
|
||||
// For decoupled clocks, value transfer is preferred in the direction of the track if possible.
|
||||
// In other words, we want to keep our current time even if the source changes, as long as the source supports it.
|
||||
//
|
||||
// This tests the case where it is supported.
|
||||
|
||||
const double first_source_time = 256000;
|
||||
const double second_source_time = 128000;
|
||||
|
||||
source.Seek(first_source_time);
|
||||
source.Start();
|
||||
|
||||
var secondSource = new TestClock
|
||||
{
|
||||
// importantly, test a value lower than the original source.
|
||||
// this is to both test value transfer *and* the case where time is going backwards, as
|
||||
// some clocks have special provisions for this.
|
||||
CurrentTime = second_source_time
|
||||
};
|
||||
|
||||
decoupleable.ProcessFrame();
|
||||
Assert.That(decoupleable.CurrentTime, Is.EqualTo(first_source_time));
|
||||
|
||||
decoupleable.ChangeSource(secondSource);
|
||||
decoupleable.ProcessFrame();
|
||||
|
||||
Assert.That(secondSource.CurrentTime, Is.EqualTo(first_source_time));
|
||||
Assert.That(decoupleable.CurrentTime, Is.EqualTo(first_source_time));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SourceChangeTransfersValueNonAdjustable()
|
||||
{
|
||||
// For decoupled clocks, value transfer is preferred in the direction of the track if possible.
|
||||
// In other words, we want to keep our current time even if the source changes, as long as the source supports it.
|
||||
//
|
||||
// This tests the case where it is NOT supported.
|
||||
|
||||
const double first_source_time = 256000;
|
||||
const double second_source_time = 128000;
|
||||
|
||||
source.Seek(first_source_time);
|
||||
source.Start();
|
||||
|
||||
var secondSource = new TestNonAdjustableClock
|
||||
{
|
||||
// importantly, test a value lower than the original source.
|
||||
// this is to both test value transfer *and* the case where time is going backwards, as
|
||||
// some clocks have special provisions for this.
|
||||
CurrentTime = second_source_time
|
||||
};
|
||||
|
||||
decoupleable.ProcessFrame();
|
||||
Assert.That(decoupleable.CurrentTime, Is.EqualTo(first_source_time));
|
||||
|
||||
decoupleable.ChangeSource(secondSource);
|
||||
decoupleable.ProcessFrame();
|
||||
|
||||
Assert.That(secondSource.CurrentTime, Is.EqualTo(second_source_time));
|
||||
Assert.That(decoupleable.CurrentTime, Is.EqualTo(second_source_time));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Offset start
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -69,6 +69,62 @@ namespace osu.Framework.Tests.Clocks
|
||||
Assert.Greater(interpolatedCount, 10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SourceChangeTransfersValueAdjustable()
|
||||
{
|
||||
// For interpolating clocks, value transfer is always in the direction of the interpolating clock.
|
||||
|
||||
const double first_source_time = 256000;
|
||||
const double second_source_time = 128000;
|
||||
|
||||
source.Seek(first_source_time);
|
||||
|
||||
var secondSource = new TestClock
|
||||
{
|
||||
// importantly, test a value lower than the original source.
|
||||
// this is to both test value transfer *and* the case where time is going backwards, as
|
||||
// some clocks have special provisions for this.
|
||||
CurrentTime = second_source_time
|
||||
};
|
||||
|
||||
interpolating.ProcessFrame();
|
||||
Assert.That(interpolating.CurrentTime, Is.EqualTo(first_source_time));
|
||||
|
||||
interpolating.ChangeSource(secondSource);
|
||||
interpolating.ProcessFrame();
|
||||
|
||||
Assert.That(secondSource.CurrentTime, Is.EqualTo(second_source_time));
|
||||
Assert.That(interpolating.CurrentTime, Is.EqualTo(second_source_time));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SourceChangeTransfersValueNonAdjustable()
|
||||
{
|
||||
// For interpolating clocks, value transfer is always in the direction of the interpolating clock.
|
||||
|
||||
const double first_source_time = 256000;
|
||||
const double second_source_time = 128000;
|
||||
|
||||
source.Seek(first_source_time);
|
||||
|
||||
var secondSource = new TestNonAdjustableClock
|
||||
{
|
||||
// importantly, test a value lower than the original source.
|
||||
// this is to both test value transfer *and* the case where time is going backwards, as
|
||||
// some clocks have special provisions for this.
|
||||
CurrentTime = second_source_time
|
||||
};
|
||||
|
||||
interpolating.ProcessFrame();
|
||||
Assert.That(interpolating.CurrentTime, Is.EqualTo(first_source_time));
|
||||
|
||||
interpolating.ChangeSource(secondSource);
|
||||
interpolating.ProcessFrame();
|
||||
|
||||
Assert.That(secondSource.CurrentTime, Is.EqualTo(second_source_time));
|
||||
Assert.That(interpolating.CurrentTime, Is.EqualTo(second_source_time));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NeverInterpolatesBackwardsOnInterpolationFail()
|
||||
{
|
||||
|
||||
15
osu.Framework.Tests/Clocks/TestNonAdjustableClock.cs
Normal file
15
osu.Framework.Tests/Clocks/TestNonAdjustableClock.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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.Timing;
|
||||
|
||||
namespace osu.Framework.Tests.Clocks
|
||||
{
|
||||
internal class TestNonAdjustableClock : IClock
|
||||
{
|
||||
public double CurrentTime { get; set; }
|
||||
public double Rate { get; set; } = 1;
|
||||
|
||||
public bool IsRunning => true;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace osu.Framework.Tests.Containers
|
||||
return result;
|
||||
});
|
||||
|
||||
AddStep("clear all children", () => Clear());
|
||||
AddStep("clear all children", Clear);
|
||||
|
||||
AddStep("load async", () => LoadComponentsAsync(composite, AddRange));
|
||||
|
||||
|
||||
@@ -463,6 +463,5 @@ namespace osu.Framework.Tests.Dependencies.Reflection
|
||||
{
|
||||
}
|
||||
}
|
||||
#nullable disable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +342,5 @@ namespace osu.Framework.Tests.Dependencies.Reflection
|
||||
[Resolved]
|
||||
public Bindable<int> Obj { get; private set; } = null!;
|
||||
}
|
||||
#nullable disable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,6 +467,5 @@ namespace osu.Framework.Tests.Dependencies.SourceGeneration
|
||||
{
|
||||
}
|
||||
}
|
||||
#nullable disable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +333,5 @@ namespace osu.Framework.Tests.Dependencies.SourceGeneration
|
||||
[Resolved]
|
||||
public Bindable<int> Obj { get; private set; } = null!;
|
||||
}
|
||||
#nullable disable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace osu.Framework.Tests.Exceptions
|
||||
runGameWithLogic(g =>
|
||||
{
|
||||
g.Scheduler.Add(() => Task.Run(() => throw new InvalidOperationException()));
|
||||
g.Scheduler.AddDelayed(() => collect(), 1, true);
|
||||
g.Scheduler.AddDelayed(collect, 1, true);
|
||||
|
||||
if (loggedException != null)
|
||||
throw loggedException;
|
||||
|
||||
@@ -407,6 +407,97 @@ namespace osu.Framework.Tests.Layout
|
||||
AddAssert("child not invalidated", () => !invalidated);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the state of childrenSizeDependencies by the time a <see cref="CompositeDrawable"/> is loaded, for various values of <see cref="Axes"/>.
|
||||
/// </summary>
|
||||
[TestCase(Axes.None)]
|
||||
[TestCase(Axes.X)]
|
||||
[TestCase(Axes.Y)]
|
||||
[TestCase(Axes.Both)]
|
||||
public void TestChildrenSizeDependenciesValidationOnLoad(Axes autoSizeAxes)
|
||||
{
|
||||
bool isValid = false;
|
||||
|
||||
AddStep("create test", () =>
|
||||
{
|
||||
Container child;
|
||||
Child = child = new Container { AutoSizeAxes = autoSizeAxes };
|
||||
isValid = child.ChildrenSizeDependenciesIsValid;
|
||||
});
|
||||
|
||||
if (autoSizeAxes != Axes.None)
|
||||
AddAssert("invalidated", () => !isValid);
|
||||
else
|
||||
AddAssert("valid", () => isValid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that setting <see cref="CompositeDrawable.AutoSizeAxes"/> causes an invalidation of childrenSizeDependencies when not <see cref="Axes.None"/>,
|
||||
/// and causes a validation of childrenSizeDependencies when <see cref="Axes.None"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestSettingAutoSizeAxesInvalidatesAndValidates()
|
||||
{
|
||||
Container child = null;
|
||||
bool isValid = false;
|
||||
|
||||
AddStep("create test", () =>
|
||||
{
|
||||
Child = child = new Container();
|
||||
isValid = child.ChildrenSizeDependenciesIsValid;
|
||||
});
|
||||
|
||||
AddAssert("initially valid", () => isValid);
|
||||
|
||||
AddStep("set autosize", () =>
|
||||
{
|
||||
child.AutoSizeAxes = Axes.Both;
|
||||
isValid = child.ChildrenSizeDependenciesIsValid;
|
||||
});
|
||||
|
||||
AddAssert("invalidated", () => !isValid);
|
||||
|
||||
AddStep("remove autosize", () =>
|
||||
{
|
||||
child.Invalidate(); // It will have automatically validated after the previous step.
|
||||
child.AutoSizeAxes = Axes.None;
|
||||
isValid = child.ChildrenSizeDependenciesIsValid;
|
||||
});
|
||||
|
||||
AddAssert("valid", () => isValid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a non-autosizing parent does not have its childrenSizeDependencies invalidated when a child invalidates.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestNonAutoSizingParentDoesNotInvalidateSizeDependenciesFromChild()
|
||||
{
|
||||
Container parent = null;
|
||||
Drawable child = null;
|
||||
bool isValid = false;
|
||||
|
||||
AddStep("create test", () =>
|
||||
{
|
||||
Child = parent = new Container
|
||||
{
|
||||
Child = child = new Box()
|
||||
};
|
||||
|
||||
isValid = parent.ChildrenSizeDependenciesIsValid;
|
||||
});
|
||||
|
||||
AddAssert("initially valid", () => isValid);
|
||||
|
||||
AddStep("invalidate child", () =>
|
||||
{
|
||||
child.Height = 100;
|
||||
isValid = parent.ChildrenSizeDependenciesIsValid;
|
||||
});
|
||||
|
||||
AddAssert("still valid", () => isValid);
|
||||
}
|
||||
|
||||
private partial class TestBox1 : Box
|
||||
{
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
|
||||
@@ -60,13 +60,13 @@ namespace osu.Framework.Tests.Shaders
|
||||
|
||||
private class TestGLRenderer : GLRenderer
|
||||
{
|
||||
protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer)
|
||||
=> new TestGLShader(this, name, parts.Cast<GLShaderPart>().ToArray());
|
||||
protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer, ShaderCompilationStore compilationStore)
|
||||
=> new TestGLShader(this, name, parts.Cast<GLShaderPart>().ToArray(), globalUniformBuffer, compilationStore);
|
||||
|
||||
private class TestGLShader : GLShader
|
||||
{
|
||||
internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts)
|
||||
: base(renderer, name, parts, null)
|
||||
internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer, ShaderCompilationStore compilationStore)
|
||||
: base(renderer, name, parts, globalUniformBuffer, compilationStore)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -40,13 +40,13 @@ namespace osu.Framework.Tests.Shaders
|
||||
|
||||
private class TestGLRenderer : GLRenderer
|
||||
{
|
||||
protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer)
|
||||
=> new TestGLShader(this, name, parts.Cast<GLShaderPart>().ToArray());
|
||||
protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer, ShaderCompilationStore compilationStore)
|
||||
=> new TestGLShader(this, name, parts.Cast<GLShaderPart>().ToArray(), globalUniformBuffer, compilationStore);
|
||||
|
||||
private class TestGLShader : GLShader
|
||||
{
|
||||
internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts)
|
||||
: base(renderer, name, parts, null)
|
||||
internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer, ShaderCompilationStore compilationStore)
|
||||
: base(renderer, name, parts, globalUniformBuffer, compilationStore)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ namespace osu.Framework.Tests.Visual.Containers
|
||||
public void TestPositionalUpdates()
|
||||
{
|
||||
AddStep("Move cursor to centre", () => InputManager.MoveMouseTo(container.ScreenSpaceDrawQuad.Centre));
|
||||
AddAssert("Cursor is centered", () => cursorCenteredInContainer());
|
||||
AddAssert("Cursor at mouse position", () => cursorAtMouseScreenSpace());
|
||||
AddAssert("Cursor is centered", cursorCenteredInContainer);
|
||||
AddAssert("Cursor at mouse position", cursorAtMouseScreenSpace);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -37,11 +37,11 @@ namespace osu.Framework.Tests.Visual.Containers
|
||||
{
|
||||
AddStep("Hide cursor container", () => cursorContainer.Alpha = 0f);
|
||||
AddStep("Move cursor to centre", () => InputManager.MoveMouseTo(Content.ScreenSpaceDrawQuad.Centre));
|
||||
AddAssert("Cursor is centered", () => cursorCenteredInContainer());
|
||||
AddAssert("Cursor at mouse position", () => cursorAtMouseScreenSpace());
|
||||
AddAssert("Cursor is centered", cursorCenteredInContainer);
|
||||
AddAssert("Cursor at mouse position", cursorAtMouseScreenSpace);
|
||||
AddStep("Show cursor container", () => cursorContainer.Alpha = 1f);
|
||||
AddAssert("Cursor is centered", () => cursorCenteredInContainer());
|
||||
AddAssert("Cursor at mouse position", () => cursorAtMouseScreenSpace());
|
||||
AddAssert("Cursor is centered", cursorCenteredInContainer);
|
||||
AddAssert("Cursor at mouse position", cursorAtMouseScreenSpace);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -50,9 +50,9 @@ namespace osu.Framework.Tests.Visual.Containers
|
||||
AddStep("Move cursor to centre", () => InputManager.MoveMouseTo(container.ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("Move container", () => container.Y += 50);
|
||||
AddAssert("Cursor no longer centered", () => !cursorCenteredInContainer());
|
||||
AddAssert("Cursor at mouse position", () => cursorAtMouseScreenSpace());
|
||||
AddAssert("Cursor at mouse position", cursorAtMouseScreenSpace);
|
||||
AddStep("Resize container", () => container.Size *= new Vector2(1.4f, 1));
|
||||
AddAssert("Cursor at mouse position", () => cursorAtMouseScreenSpace());
|
||||
AddAssert("Cursor at mouse position", cursorAtMouseScreenSpace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,8 +63,8 @@ namespace osu.Framework.Tests.Visual.Containers
|
||||
{
|
||||
AddStep("Move cursor to centre", () => InputManager.MoveMouseTo(Content.ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("Recreate container with mouse already in place", createContent);
|
||||
AddAssert("Cursor is centered", () => cursorCenteredInContainer());
|
||||
AddAssert("Cursor at mouse position", () => cursorAtMouseScreenSpace());
|
||||
AddAssert("Cursor is centered", cursorCenteredInContainer);
|
||||
AddAssert("Cursor at mouse position", cursorAtMouseScreenSpace);
|
||||
}
|
||||
|
||||
private bool cursorCenteredInContainer() =>
|
||||
|
||||
@@ -25,10 +25,7 @@ namespace osu.Framework.Tests.Visual.Containers
|
||||
private TestBox blendedBox;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Clear();
|
||||
});
|
||||
public void Setup() => Schedule(Clear);
|
||||
|
||||
[TearDownSteps]
|
||||
public void TearDownSteps()
|
||||
|
||||
@@ -289,13 +289,11 @@ namespace osu.Framework.Tests.Visual.Drawables
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
stateText.Text = State.ToString();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
stateText.Text = State.ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -206,12 +206,12 @@ namespace osu.Framework.Tests.Visual.Drawables
|
||||
AddStep("add box", () => Child = box = new AsyncPerformingBox(true));
|
||||
AddAssert("not spun", () => box.Rotation == 0);
|
||||
|
||||
AddStep("toggle execution mode", () => toggleExecutionMode());
|
||||
AddStep("toggle execution mode", toggleExecutionMode);
|
||||
|
||||
AddStep("trigger", () => box.ReleaseAsyncLoadCompleteLock());
|
||||
AddUntilStep("has spun", () => box.Rotation == 180);
|
||||
|
||||
AddStep("revert execution mode", () => toggleExecutionMode());
|
||||
AddStep("revert execution mode", toggleExecutionMode);
|
||||
|
||||
void toggleExecutionMode()
|
||||
{
|
||||
|
||||
@@ -27,7 +27,9 @@ namespace osu.Framework.Tests.Visual.Drawables
|
||||
private Track track;
|
||||
private Waveform waveform;
|
||||
private Container<Drawable> waveformContainer;
|
||||
private readonly Bindable<float> zoom = new BindableFloat(1) { MinValue = 0.1f, MaxValue = 20 };
|
||||
private readonly BindableFloat zoom = new BindableFloat(1) { MinValue = 0.1f, MaxValue = 2000 };
|
||||
|
||||
private ScrollContainer<Drawable> scroll;
|
||||
|
||||
private ITrackStore store;
|
||||
|
||||
@@ -75,7 +77,7 @@ namespace osu.Framework.Tests.Visual.Drawables
|
||||
},
|
||||
},
|
||||
},
|
||||
new BasicScrollContainer(Direction.Horizontal)
|
||||
scroll = new BasicScrollContainer(Direction.Horizontal)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = waveformContainer = new FillFlowContainer
|
||||
@@ -108,6 +110,26 @@ namespace osu.Framework.Tests.Visual.Drawables
|
||||
AddStep("Load stereo track", () => loadTrack(true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When zooming in very close – or even zooming in a normal amount on a very long track – the number of points in the waveform
|
||||
/// can become very high (in the millions).
|
||||
///
|
||||
/// In this case, we need to be careful no iteration is performed over the point data. This tests the case of being scrolled to the
|
||||
/// far end of the waveform, which is the worse-case-scenario and requires special consideration.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHighZoomEndOfTrackPerformance()
|
||||
{
|
||||
TestWaveform graph = null;
|
||||
|
||||
AddStep("create waveform", () => waveformContainer.Child = graph = new TestWaveform(track, 1) { Waveform = waveform });
|
||||
AddUntilStep("wait for load", () => graph.Regenerated);
|
||||
|
||||
AddStep("set zoom to highest", () => zoom.Value = zoom.MaxValue);
|
||||
|
||||
AddStep("seek to end", () => scroll.ScrollToEnd());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMonoTrack()
|
||||
{
|
||||
|
||||
54
osu.Framework.Tests/Visual/Graphics/TestSceneColours.cs
Normal file
54
osu.Framework.Tests/Visual/Graphics/TestSceneColours.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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.Linq;
|
||||
using System.Reflection;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Framework.Tests.Visual.Graphics
|
||||
{
|
||||
public partial class TestSceneColours : FrameworkTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
FillFlowContainer flow;
|
||||
|
||||
Child = new TooltipContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = flow = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Full,
|
||||
Padding = new MarginPadding(10),
|
||||
Spacing = new Vector2(5)
|
||||
}
|
||||
};
|
||||
|
||||
var colours = typeof(Colour4).GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.GetProperty)
|
||||
.Where(property => property.PropertyType == typeof(Colour4))
|
||||
.Select(property => (property.Name, (Colour4)property.GetMethod!.Invoke(null, null)!));
|
||||
|
||||
flow.ChildrenEnumerable = colours.Select(colour => new ColourBox(colour.Name, colour.Item2));
|
||||
}
|
||||
|
||||
private partial class ColourBox : Box, IHasTooltip
|
||||
{
|
||||
public ColourBox(string name, Colour4 colour)
|
||||
{
|
||||
Colour = colour;
|
||||
Name = name;
|
||||
Size = new Vector2(50);
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText => Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,10 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@@ -369,34 +369,43 @@ namespace osu.Framework.Tests.Visual.UserInterface
|
||||
AddStep("close dropdown", () => InputManager.Key(Key.Escape));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReplaceItemsInItemSource()
|
||||
{
|
||||
AddStep("clear bindable list", () => bindableList.Clear());
|
||||
toggleDropdownViaClick(bindableDropdown, "dropdown3");
|
||||
AddAssert("no elements in bindable dropdown", () => !bindableDropdown.Items.Any());
|
||||
|
||||
AddStep("add items to bindable", () => bindableList.AddRange(new[] { "one", "two", "three" }.Select(s => new TestModel(s))));
|
||||
AddStep("select three", () => bindableDropdown.Current.Value = "three");
|
||||
|
||||
AddStep("remove and then add items to bindable", () =>
|
||||
{
|
||||
bindableList.Clear();
|
||||
bindableList.AddRange(new[] { "four", "three" }.Select(s => new TestModel(s)));
|
||||
});
|
||||
|
||||
AddAssert("current value still three", () => bindableDropdown.Current.Value.Identifier, () => Is.EqualTo("three"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAccessBdlInGenerateItemText()
|
||||
{
|
||||
AddStep("add dropdown that uses BDL", () => Add(new BdlDropdown
|
||||
BdlDropdown dropdown = null!;
|
||||
|
||||
AddStep("add dropdown that uses BDL", () => Add(dropdown = new BdlDropdown
|
||||
{
|
||||
Width = 150,
|
||||
Position = new Vector2(250, 350),
|
||||
Items = new TestModel("test").Yield()
|
||||
}));
|
||||
|
||||
AddAssert("text is expected", () => dropdown.Menu.DrawableMenuItems.First().ChildrenOfType<SpriteText>().First().Text.ToString(), () => Is.EqualTo("loaded: test"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks that <see cref="Dropdown{T}.GenerateItemText"/> is not called before load when initialising with <see cref="Dropdown{T}.Current"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestBdlWithCurrent()
|
||||
{
|
||||
BdlDropdown dropdown = null!;
|
||||
|
||||
Bindable<TestModel> bindable = null!;
|
||||
|
||||
AddStep("add items to bindable", () => bindableList.AddRange(new[] { "one", "two", "three" }.Select(s => new TestModel(s))));
|
||||
AddStep("create current", () => bindable = new Bindable<TestModel>(bindableList[1]));
|
||||
|
||||
AddStep("add dropdown that uses BDL", () => Add(dropdown = new BdlDropdown
|
||||
{
|
||||
Width = 150,
|
||||
Position = new Vector2(250, 350),
|
||||
ItemSource = bindableList,
|
||||
Current = bindable,
|
||||
}));
|
||||
|
||||
AddAssert("text is expected", () => dropdown.Menu.DrawableMenuItems.First(d => d.IsSelected).ChildrenOfType<SpriteText>().First().Text.ToString(), () => Is.EqualTo("loaded: two"));
|
||||
}
|
||||
|
||||
private void toggleDropdownViaClick(TestDropdown dropdown, string dropdownName = null) => AddStep($"click {dropdownName ?? "dropdown"}", () =>
|
||||
@@ -440,14 +449,23 @@ namespace osu.Framework.Tests.Visual.UserInterface
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dropdown that will access <see cref="ResolvedAttribute"/> properties in <see cref="GenerateItemText"/>.
|
||||
/// Dropdown that will access state set by BDL load in <see cref="GenerateItemText"/>.
|
||||
/// </summary>
|
||||
private partial class BdlDropdown : TestDropdown
|
||||
{
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
private string text;
|
||||
|
||||
protected override LocalisableString GenerateItemText(TestModel item) => $"{host.Name}: {base.GenerateItemText(item)}";
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
text = "loaded";
|
||||
}
|
||||
|
||||
protected override LocalisableString GenerateItemText(TestModel item)
|
||||
{
|
||||
Assert.That(text, Is.Not.Null);
|
||||
return $"{text}: {base.GenerateItemText(item)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,16 +203,12 @@ namespace osu.Framework.Tests.Visual.UserInterface
|
||||
{
|
||||
this.FadeIn(1000, Easing.OutQuint);
|
||||
this.ScaleTo(1, 1000, Easing.OutElastic);
|
||||
|
||||
base.PopIn();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(1000, Easing.OutQuint);
|
||||
this.ScaleTo(0.4f, 1000, Easing.OutQuint);
|
||||
|
||||
base.PopOut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Foundation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Graphics.Video;
|
||||
using osu.Framework.Input.Bindings;
|
||||
@@ -66,7 +65,7 @@ namespace osu.Framework.iOS
|
||||
|
||||
UIApplication.SharedApplication.InvokeOnMainThread(() =>
|
||||
{
|
||||
NSUrl nsurl = NSUrl.FromString(url);
|
||||
NSUrl nsurl = NSUrl.FromString(url).AsNonNull();
|
||||
if (UIApplication.SharedApplication.CanOpenUrl(nsurl))
|
||||
UIApplication.SharedApplication.OpenUrl(nsurl, new NSDictionary(), null);
|
||||
});
|
||||
|
||||
@@ -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 System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using ObjCRuntime;
|
||||
@@ -48,6 +49,20 @@ namespace osu.Framework.iOS
|
||||
updateSafeArea();
|
||||
}
|
||||
|
||||
protected override void RunMainLoop()
|
||||
{
|
||||
// Delegate running the main loop to CADisplayLink.
|
||||
//
|
||||
// Note that this is most effective in single thread mode.
|
||||
// .. In multi-threaded mode it will time the *input* thread to the callbacks. This is kinda silly,
|
||||
// but users shouldn't be using multi-threaded mode in the first place. Disabling it completely on
|
||||
// iOS may be a good forward direction if this ever comes up, as a user may see a potentially higher
|
||||
// frame rate with multi-threaded mode turned on, but it is going to give them worse input latency
|
||||
// and higher power usage.
|
||||
SDL.SDL_iPhoneSetEventPump(SDL.SDL_bool.SDL_FALSE);
|
||||
SDL.SDL_iPhoneSetAnimationCallback(SDLWindowHandle, 1, _ => RunFrame(), IntPtr.Zero);
|
||||
}
|
||||
|
||||
private void updateSafeArea()
|
||||
{
|
||||
Debug.Assert(window != null);
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Runtime.ExceptionServices;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Framework.Allocation
|
||||
{
|
||||
@@ -89,13 +90,7 @@ namespace osu.Framework.Allocation
|
||||
}
|
||||
}
|
||||
|
||||
private static Func<IReadOnlyDependencyContainer, object> getDependency(Type type, Type requestingType, bool permitNulls) => dc =>
|
||||
{
|
||||
object val = dc.Get(type);
|
||||
if (val == null && !permitNulls)
|
||||
throw new DependencyNotRegisteredException(requestingType, type);
|
||||
|
||||
return val;
|
||||
};
|
||||
private static Func<IReadOnlyDependencyContainer, object> getDependency(Type type, Type requestingType, bool permitNulls)
|
||||
=> dc => SourceGeneratorUtils.GetDependency(dc, type, requestingType, null, null, permitNulls, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using JetBrains.Annotations;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Framework.Allocation
|
||||
{
|
||||
@@ -127,23 +128,20 @@ namespace osu.Framework.Allocation
|
||||
|
||||
var additionActivators = new List<Action<object, DependencyContainer, CacheInfo>>();
|
||||
|
||||
// Types within the framework should be able to cache value types if they desire (e.g. cancellation tokens)
|
||||
bool allowValueTypes = type.Assembly == typeof(Drawable).Assembly;
|
||||
|
||||
foreach (var iface in type.GetInterfaces())
|
||||
{
|
||||
foreach (var attribute in iface.GetCustomAttributes<CachedAttribute>())
|
||||
additionActivators.Add((target, dc, info) => dc.CacheAs(attribute.Type ?? iface, new CacheInfo(info.Name ?? attribute.Name, info.Parent), target, allowValueTypes));
|
||||
additionActivators.Add((target, dc, info) => SourceGeneratorUtils.CacheDependency(dc, type, target, info, attribute.Type ?? iface, attribute.Name, null));
|
||||
}
|
||||
|
||||
foreach (var attribute in type.GetCustomAttributes<CachedAttribute>())
|
||||
additionActivators.Add((target, dc, info) => dc.CacheAs(attribute.Type ?? type, new CacheInfo(info.Name ?? attribute.Name, info.Parent), target, allowValueTypes));
|
||||
additionActivators.Add((target, dc, info) => SourceGeneratorUtils.CacheDependency(dc, type, target, info, attribute.Type ?? type, attribute.Name, null));
|
||||
|
||||
foreach (var property in type.GetProperties(ACTIVATOR_FLAGS).Where(f => f.GetCustomAttributes<CachedAttribute>().Any()))
|
||||
additionActivators.AddRange(createMemberActivator(property, type, allowValueTypes));
|
||||
additionActivators.AddRange(createMemberActivator(property, type));
|
||||
|
||||
foreach (var field in type.GetFields(ACTIVATOR_FLAGS).Where(f => f.GetCustomAttributes<CachedAttribute>().Any()))
|
||||
additionActivators.AddRange(createMemberActivator(field, type, allowValueTypes));
|
||||
additionActivators.AddRange(createMemberActivator(field, type));
|
||||
|
||||
if (additionActivators.Count == 0)
|
||||
return (_, existing, _) => existing;
|
||||
@@ -158,7 +156,7 @@ namespace osu.Framework.Allocation
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<Action<object, DependencyContainer, CacheInfo>> createMemberActivator(MemberInfo member, Type type, bool allowValueTypes)
|
||||
private static IEnumerable<Action<object, DependencyContainer, CacheInfo>> createMemberActivator(MemberInfo member, Type type)
|
||||
{
|
||||
switch (member)
|
||||
{
|
||||
@@ -208,23 +206,7 @@ namespace osu.Framework.Allocation
|
||||
if (member is FieldInfo f)
|
||||
value = f.GetValue(target);
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
if (allowValueTypes)
|
||||
return;
|
||||
|
||||
throw new NullDependencyException($"Attempted to cache a null value: {type.ReadableName()}.{member.Name}.");
|
||||
}
|
||||
|
||||
var cacheInfo = new CacheInfo(info.Name ?? attribute.Name);
|
||||
|
||||
if (info.Parent != null)
|
||||
{
|
||||
// When a parent type exists, infer the property name if one is not provided
|
||||
cacheInfo = new CacheInfo(cacheInfo.Name ?? member.Name, info.Parent);
|
||||
}
|
||||
|
||||
dc.CacheAs(attribute.Type ?? value.GetType(), cacheInfo, value, allowValueTypes);
|
||||
SourceGeneratorUtils.CacheDependency(dc, type, value, info, attribute.Type, attribute.Name, member.Name);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Framework.Allocation
|
||||
{
|
||||
@@ -114,17 +114,8 @@ namespace osu.Framework.Allocation
|
||||
};
|
||||
}
|
||||
|
||||
private static Func<IReadOnlyDependencyContainer, object> getDependency(Type type, Type requestingType, bool permitNulls, CacheInfo info) => dc =>
|
||||
{
|
||||
object val = dc.Get(type, info);
|
||||
if (val == null && !permitNulls)
|
||||
throw new DependencyNotRegisteredException(requestingType, type);
|
||||
|
||||
if (val is IBindable bindableVal)
|
||||
return bindableVal.GetBoundCopy();
|
||||
|
||||
return val;
|
||||
};
|
||||
private static Func<IReadOnlyDependencyContainer, object> getDependency(Type type, Type requestingType, bool permitNulls, CacheInfo info)
|
||||
=> dc => SourceGeneratorUtils.GetDependency(dc, type, requestingType, info.Name, info.Parent, permitNulls, true);
|
||||
}
|
||||
|
||||
public class PropertyNotWritableException : Exception
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace osu.Framework.Audio.Track
|
||||
|
||||
relativeFrequencyHandler = new BassRelativeFrequencyHandler
|
||||
{
|
||||
FrequencyChangedToZero = () => stopInternal(),
|
||||
FrequencyChangedToZero = stopInternal,
|
||||
FrequencyChangedFromZero = () =>
|
||||
{
|
||||
// Do not resume the track if a play wasn't requested at all or has been paused via Stop().
|
||||
|
||||
@@ -247,14 +247,25 @@ namespace osu.Framework.Bindables
|
||||
/// <param name="input">The input which is to be parsed.</param>
|
||||
public virtual void Parse(object input)
|
||||
{
|
||||
Type underlyingType = typeof(T).GetUnderlyingNullableType() ?? typeof(T);
|
||||
|
||||
switch (input)
|
||||
{
|
||||
// Of note, this covers the case when the input is a string and `T` is `string`.
|
||||
// Both `string.Empty` and `null` are valid values for this type.
|
||||
case T t:
|
||||
Value = t;
|
||||
break;
|
||||
|
||||
case null:
|
||||
// Nullable value types and reference types (annotated or not) are allowed to be initialised with `null`.
|
||||
if (typeof(T).IsNullable() || typeof(T).IsClass)
|
||||
{
|
||||
Value = default;
|
||||
break;
|
||||
}
|
||||
|
||||
// Non-nullable value types can't convert from null.
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
case IBindable:
|
||||
if (!(input is IBindable<T> bindable))
|
||||
throw new ArgumentException($"Expected bindable of type {nameof(IBindable)}<{typeof(T)}>, got {input.GetType()}", nameof(input));
|
||||
@@ -263,6 +274,21 @@ namespace osu.Framework.Bindables
|
||||
break;
|
||||
|
||||
default:
|
||||
if (input is string strInput && string.IsNullOrEmpty(strInput))
|
||||
{
|
||||
// Nullable value types and reference types are initialised to `null` on empty strings.
|
||||
if (typeof(T).IsNullable() || typeof(T).IsClass)
|
||||
{
|
||||
Value = default;
|
||||
break;
|
||||
}
|
||||
|
||||
// Most likely all conversion methods will not accept empty strings, but we let this fall through so that the exception is thrown by .NET itself.
|
||||
// For example, DateTime.Parse() throws a more contextually relevant exception than int.Parse().
|
||||
}
|
||||
|
||||
Type underlyingType = typeof(T).GetUnderlyingNullableType() ?? typeof(T);
|
||||
|
||||
if (underlyingType.IsEnum)
|
||||
Value = (T)Enum.Parse(underlyingType, input.ToString().AsNonNull());
|
||||
else
|
||||
|
||||
@@ -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;
|
||||
|
||||
namespace osu.Framework.Bindables
|
||||
{
|
||||
public class BindableBool : Bindable<bool>
|
||||
@@ -10,8 +12,10 @@ namespace osu.Framework.Bindables
|
||||
{
|
||||
}
|
||||
|
||||
public override void Parse(object input)
|
||||
public override void Parse(object? input)
|
||||
{
|
||||
if (input == null) throw new ArgumentNullException(nameof(input));
|
||||
|
||||
if (input is "1")
|
||||
Value = true;
|
||||
else if (input is "0")
|
||||
|
||||
@@ -14,10 +14,12 @@ namespace osu.Framework.Bindables
|
||||
}
|
||||
|
||||
// 8-bit precision should probably be enough for serialization.
|
||||
public override string ToString(string format, IFormatProvider formatProvider) => Value.ToHex();
|
||||
public override string ToString(string? format, IFormatProvider? formatProvider) => Value.ToHex();
|
||||
|
||||
public override void Parse(object input)
|
||||
public override void Parse(object? input)
|
||||
{
|
||||
if (input == null) throw new ArgumentNullException(nameof(input));
|
||||
|
||||
switch (input)
|
||||
{
|
||||
case string str:
|
||||
|
||||
@@ -323,6 +323,41 @@ namespace osu.Framework.Bindables
|
||||
return removed.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces <paramref name="count"/> items starting from <paramref name="index"/> with <paramref name="newItems"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The index to start removing from.</param>
|
||||
/// <param name="count">The count of items to be removed.</param>
|
||||
/// <param name="newItems">The items to replace the removed items with.</param>
|
||||
public void ReplaceRange(int index, int count, IEnumerable<T> newItems)
|
||||
=> replaceRange(index, count, newItems as IList ?? newItems.ToArray(), null);
|
||||
|
||||
private void replaceRange(int index, int count, IList newItems, BindableList<T> caller)
|
||||
{
|
||||
ensureMutationAllowed();
|
||||
|
||||
var removedItems = collection.GetRange(index, count);
|
||||
|
||||
collection.RemoveRange(index, count);
|
||||
collection.InsertRange(index, newItems.Cast<T>());
|
||||
|
||||
if (removedItems.Count == 0 && newItems.Count == 0)
|
||||
return;
|
||||
|
||||
if (bindings != null)
|
||||
{
|
||||
foreach (var b in bindings)
|
||||
{
|
||||
// Prevent re-adding the item back to the callee.
|
||||
// That would result in a <see cref="StackOverflowException"/>.
|
||||
if (b != caller)
|
||||
b.replaceRange(index, count, newItems, this);
|
||||
}
|
||||
}
|
||||
|
||||
notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItems, removedItems, index));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the contents of this <see cref="BindableList{T}"/> to the given array, starting at the given index.
|
||||
/// </summary>
|
||||
|
||||
@@ -17,8 +17,10 @@ namespace osu.Framework.Bindables
|
||||
{
|
||||
}
|
||||
|
||||
public override void Parse(object input)
|
||||
public override void Parse(object? input)
|
||||
{
|
||||
if (input == null) throw new ArgumentNullException(nameof(input));
|
||||
|
||||
switch (input)
|
||||
{
|
||||
case string str:
|
||||
|
||||
@@ -19,10 +19,12 @@ namespace osu.Framework.Bindables
|
||||
{
|
||||
}
|
||||
|
||||
public override string ToString(string format, IFormatProvider formatProvider) => ((FormattableString)$"{Value.Width}x{Value.Height}").ToString(formatProvider);
|
||||
public override string ToString(string? format, IFormatProvider? formatProvider) => ((FormattableString)$"{Value.Width}x{Value.Height}").ToString(formatProvider);
|
||||
|
||||
public override void Parse(object input)
|
||||
public override void Parse(object? input)
|
||||
{
|
||||
if (input == null) throw new ArgumentNullException(nameof(input));
|
||||
|
||||
switch (input)
|
||||
{
|
||||
case string str:
|
||||
|
||||
@@ -11,9 +11,41 @@ namespace osu.Framework.Extensions.Color4Extensions
|
||||
{
|
||||
public const double GAMMA = 2.4;
|
||||
|
||||
public static double ToLinear(double color) => color <= 0.04045 ? color / 12.92 : Math.Pow((color + 0.055) / 1.055, GAMMA);
|
||||
// ToLinear is quite a hot path in the game.
|
||||
// MathF.Pow performs way faster than Math.Pow, however on Windows it lacks a fast path for x == 1.
|
||||
// Given passing color == 1 (White or Transparent) is very common, a fast path for 1 is added.
|
||||
|
||||
public static double ToSRGB(double color) => color < 0.0031308 ? 12.92 * color : 1.055 * Math.Pow(color, 1.0 / GAMMA) - 0.055;
|
||||
public static double ToLinear(double color)
|
||||
{
|
||||
if (color == 1)
|
||||
return 1;
|
||||
|
||||
return color <= 0.04045 ? color / 12.92 : Math.Pow((color + 0.055) / 1.055, GAMMA);
|
||||
}
|
||||
|
||||
public static float ToLinear(float color)
|
||||
{
|
||||
if (color == 1)
|
||||
return 1;
|
||||
|
||||
return color <= 0.04045f ? color / 12.92f : MathF.Pow((color + 0.055f) / 1.055f, (float)GAMMA);
|
||||
}
|
||||
|
||||
public static double ToSRGB(double color)
|
||||
{
|
||||
if (color == 1)
|
||||
return 1;
|
||||
|
||||
return color < 0.0031308 ? 12.92 * color : 1.055 * Math.Pow(color, 1.0 / GAMMA) - 0.055;
|
||||
}
|
||||
|
||||
public static float ToSRGB(float color)
|
||||
{
|
||||
if (color == 1)
|
||||
return 1;
|
||||
|
||||
return color < 0.0031308f ? 12.92f * color : 1.055f * MathF.Pow(color, 1.0f / (float)GAMMA) - 0.055f;
|
||||
}
|
||||
|
||||
public static Color4 Opacity(this Color4 color, float a) => new Color4(color.R, color.G, color.B, a);
|
||||
|
||||
@@ -21,16 +53,16 @@ namespace osu.Framework.Extensions.Color4Extensions
|
||||
|
||||
public static Color4 ToLinear(this Color4 colour) =>
|
||||
new Color4(
|
||||
(float)ToLinear(colour.R),
|
||||
(float)ToLinear(colour.G),
|
||||
(float)ToLinear(colour.B),
|
||||
ToLinear(colour.R),
|
||||
ToLinear(colour.G),
|
||||
ToLinear(colour.B),
|
||||
colour.A);
|
||||
|
||||
public static Color4 ToSRGB(this Color4 colour) =>
|
||||
new Color4(
|
||||
(float)ToSRGB(colour.R),
|
||||
(float)ToSRGB(colour.G),
|
||||
(float)ToSRGB(colour.B),
|
||||
ToSRGB(colour.R),
|
||||
ToSRGB(colour.G),
|
||||
ToSRGB(colour.B),
|
||||
colour.A);
|
||||
|
||||
public static Color4 MultiplySRGB(Color4 first, Color4 second)
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace osu.Framework
|
||||
public static bool ForceTestGC { get; }
|
||||
public static GraphicsSurfaceType? PreferredGraphicsSurface { get; }
|
||||
public static string? PreferredGraphicsRenderer { get; }
|
||||
public static int? StagingBufferType { get; }
|
||||
|
||||
static FrameworkEnvironment()
|
||||
{
|
||||
@@ -21,6 +22,9 @@ namespace osu.Framework
|
||||
ForceTestGC = Environment.GetEnvironmentVariable("OSU_TESTS_FORCED_GC") == "1";
|
||||
PreferredGraphicsSurface = Enum.TryParse<GraphicsSurfaceType>(Environment.GetEnvironmentVariable("OSU_GRAPHICS_SURFACE"), true, out var surface) ? surface : null;
|
||||
PreferredGraphicsRenderer = Environment.GetEnvironmentVariable("OSU_GRAPHICS_RENDERER")?.ToLowerInvariant();
|
||||
|
||||
if (int.TryParse(Environment.GetEnvironmentVariable("OSU_GRAPHICS_STAGING_BUFFER_TYPE"), out int stagingBufferImplementation))
|
||||
StagingBufferType = stagingBufferImplementation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -29,8 +27,8 @@ namespace osu.Framework.Graphics.Audio
|
||||
/// </summary>
|
||||
public partial class WaveformGraph : Drawable
|
||||
{
|
||||
private IShader shader;
|
||||
private Texture texture;
|
||||
private IShader shader = null!;
|
||||
private Texture texture = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ShaderManager shaders, IRenderer renderer)
|
||||
@@ -61,12 +59,12 @@ namespace osu.Framework.Graphics.Audio
|
||||
}
|
||||
}
|
||||
|
||||
private Waveform waveform;
|
||||
private Waveform? waveform;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Framework.Audio.Track.Waveform"/> to display.
|
||||
/// </summary>
|
||||
public Waveform Waveform
|
||||
public Waveform? Waveform
|
||||
{
|
||||
get => waveform;
|
||||
set
|
||||
@@ -174,10 +172,10 @@ namespace osu.Framework.Graphics.Audio
|
||||
return result;
|
||||
}
|
||||
|
||||
private CancellationTokenSource cancelSource = new CancellationTokenSource();
|
||||
private CancellationTokenSource? cancelSource = new CancellationTokenSource();
|
||||
|
||||
private long resampledVersion;
|
||||
private Waveform.Point[] resampledPoints;
|
||||
private Waveform.Point[]? resampledPoints;
|
||||
private int? resampledPointCount;
|
||||
private double resampledMaxHighIntensity;
|
||||
private double resampledMaxMidIntensity;
|
||||
@@ -186,7 +184,7 @@ namespace osu.Framework.Graphics.Audio
|
||||
private void queueRegeneration() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
int requiredPointCount = (int)Math.Max(0, Math.Ceiling(DrawWidth * Scale.X) * Resolution);
|
||||
if (requiredPointCount == resampledPointCount && !cancelSource.IsCancellationRequested)
|
||||
if (requiredPointCount == resampledPointCount && cancelSource?.IsCancellationRequested != false)
|
||||
return;
|
||||
|
||||
cancelGeneration();
|
||||
@@ -259,10 +257,10 @@ namespace osu.Framework.Graphics.Audio
|
||||
|
||||
private class WaveformDrawNode : DrawNode
|
||||
{
|
||||
private IShader shader;
|
||||
private Texture texture;
|
||||
private IShader shader = null!;
|
||||
private Texture? texture;
|
||||
|
||||
private List<Waveform.Point> points;
|
||||
private List<Waveform.Point>? points;
|
||||
|
||||
private Vector2 drawSize;
|
||||
|
||||
@@ -319,7 +317,7 @@ namespace osu.Framework.Graphics.Audio
|
||||
}
|
||||
}
|
||||
|
||||
private IVertexBatch<TexturedVertex2D> vertexBatch;
|
||||
private IVertexBatch<TexturedVertex2D>? vertexBatch;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
@@ -342,17 +340,16 @@ namespace osu.Framework.Graphics.Audio
|
||||
|
||||
float separation = drawSize.X / (points.Count - 1);
|
||||
|
||||
for (int i = 0; i < points.Count - 1; i++)
|
||||
// Equates to making sure that rightX >= localMaskingRectangle.Left at startIndex and leftX <= localMaskingRectangle.Right at endIndex.
|
||||
// Without this pre-check, very long waveform displays can get slow just from running the loop below (point counts in excess of 1mil).
|
||||
int startIndex = (int)Math.Clamp(localMaskingRectangle.Left / separation, 0, points.Count - 1);
|
||||
int endIndex = (int)Math.Clamp(localMaskingRectangle.Right / separation + 1, 0, points.Count - 1);
|
||||
|
||||
for (int i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
float leftX = i * separation;
|
||||
float rightX = (i + 1) * separation;
|
||||
|
||||
if (rightX < localMaskingRectangle.Left)
|
||||
continue;
|
||||
|
||||
if (leftX > localMaskingRectangle.Right)
|
||||
break; // X is always increasing
|
||||
|
||||
Color4 frequencyColour = baseColour;
|
||||
|
||||
// colouring is applied in the order of interest to a viewer.
|
||||
|
||||
@@ -230,13 +230,13 @@ namespace osu.Framework.Graphics
|
||||
/// Returns a new <see cref="Colour4"/> with an SRGB->Linear conversion applied
|
||||
/// to each of its chromatic components. Alpha is unchanged.
|
||||
/// </summary>
|
||||
public Colour4 ToLinear() => new Colour4((float)toLinear(R), (float)toLinear(G), (float)toLinear(B), A);
|
||||
public Colour4 ToLinear() => new Colour4(toLinear(R), toLinear(G), toLinear(B), A);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new <see cref="Colour4"/> with a Linear->SRGB conversion applied
|
||||
/// to each of its chromatic components. Alpha is unchanged.
|
||||
/// </summary>
|
||||
public Colour4 ToSRGB() => new Colour4((float)toSRGB(R), (float)toSRGB(G), (float)toSRGB(B), A);
|
||||
public Colour4 ToSRGB() => new Colour4(toSRGB(R), toSRGB(G), toSRGB(B), A);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="Colour4"/> as a 32-bit unsigned integer in the format RGBA.
|
||||
@@ -552,9 +552,21 @@ namespace osu.Framework.Graphics
|
||||
|
||||
private const double gamma = 2.4;
|
||||
|
||||
private static double toLinear(double color) => color <= 0.04045 ? color / 12.92 : Math.Pow((color + 0.055) / 1.055, gamma);
|
||||
private static float toLinear(float color)
|
||||
{
|
||||
if (color == 1)
|
||||
return 1;
|
||||
|
||||
private static double toSRGB(double color) => color < 0.0031308 ? 12.92 * color : 1.055 * Math.Pow(color, 1.0 / gamma) - 0.055;
|
||||
return color <= 0.04045f ? color / 12.92f : MathF.Pow((color + 0.055f) / 1.055f, (float)gamma);
|
||||
}
|
||||
|
||||
private static float toSRGB(float color)
|
||||
{
|
||||
if (color == 1)
|
||||
return 1;
|
||||
|
||||
return color < 0.0031308f ? 12.92f * color : 1.055f * MathF.Pow(color, 1.0f / (float)gamma) - 0.055f;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ namespace osu.Framework.Graphics.Containers
|
||||
private void load(ShaderManager shaders)
|
||||
{
|
||||
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
|
||||
blurShader = shaders.Load(VertexShaderDescriptor.BLUR, FragmentShaderDescriptor.BLUR);
|
||||
blurShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR);
|
||||
}
|
||||
|
||||
protected override DrawNode CreateDrawNode() => new BufferedContainerDrawNode(this, sharedData);
|
||||
|
||||
@@ -12,10 +12,8 @@ using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders.Types;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK.Graphics.ES30;
|
||||
|
||||
namespace osu.Framework.Graphics.Containers
|
||||
{
|
||||
@@ -27,8 +25,6 @@ namespace osu.Framework.Graphics.Containers
|
||||
|
||||
protected new CompositeDrawableDrawNode Child => (CompositeDrawableDrawNode)base.Child;
|
||||
|
||||
private readonly Action<TexturedVertex2D> addVertexAction;
|
||||
|
||||
private bool drawOriginal;
|
||||
private ColourInfo effectColour;
|
||||
private BlendingParameters effectBlending;
|
||||
@@ -39,19 +35,12 @@ namespace osu.Framework.Graphics.Containers
|
||||
private float blurRotation;
|
||||
|
||||
private long updateVersion;
|
||||
|
||||
private IShader blurShader;
|
||||
|
||||
public BufferedContainerDrawNode(BufferedContainer<T> source, BufferedContainerDrawNodeSharedData sharedData)
|
||||
: base(source, new CompositeDrawableDrawNode(source), sharedData)
|
||||
{
|
||||
addVertexAction = v =>
|
||||
{
|
||||
blurQuadBatch!.Add(new BlurVertex
|
||||
{
|
||||
Position = v.Position,
|
||||
TexturePosition = v.TexturePosition
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public override void ApplyState()
|
||||
@@ -106,12 +95,10 @@ namespace osu.Framework.Graphics.Containers
|
||||
}
|
||||
|
||||
private IUniformBuffer<BlurParameters> blurParametersBuffer;
|
||||
private IVertexBatch<BlurVertex> blurQuadBatch;
|
||||
|
||||
private void drawBlurredFrameBuffer(IRenderer renderer, int kernelRadius, float sigma, float blurRotation)
|
||||
{
|
||||
blurParametersBuffer ??= renderer.CreateUniformBuffer<BlurParameters>();
|
||||
blurQuadBatch ??= renderer.CreateQuadBatch<BlurVertex>(1, 1);
|
||||
|
||||
IFrameBuffer current = SharedData.CurrentEffectBuffer;
|
||||
IFrameBuffer target = SharedData.GetNextEffectBuffer();
|
||||
@@ -132,9 +119,7 @@ namespace osu.Framework.Graphics.Containers
|
||||
|
||||
blurShader.BindUniformBlock("m_BlurParameters", blurParametersBuffer);
|
||||
blurShader.Bind();
|
||||
|
||||
renderer.DrawFrameBuffer(current, new RectangleF(0, 0, current.Texture.Width, current.Texture.Height), ColourInfo.SingleColour(Color4.White), addVertexAction);
|
||||
|
||||
renderer.DrawFrameBuffer(current, new RectangleF(0, 0, current.Texture.Width, current.Texture.Height), ColourInfo.SingleColour(Color4.White));
|
||||
blurShader.Unbind();
|
||||
}
|
||||
}
|
||||
@@ -151,7 +136,6 @@ namespace osu.Framework.Graphics.Containers
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
blurParametersBuffer?.Dispose();
|
||||
blurQuadBatch?.Dispose();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
@@ -163,20 +147,6 @@ namespace osu.Framework.Graphics.Containers
|
||||
public UniformVector2 Direction;
|
||||
private readonly UniformPadding8 pad1;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct BlurVertex : IEquatable<BlurVertex>, IVertex
|
||||
{
|
||||
[VertexMember(2, VertexAttribPointerType.Float)]
|
||||
public Vector2 Position;
|
||||
|
||||
[VertexMember(2, VertexAttribPointerType.Float)]
|
||||
public Vector2 TexturePosition;
|
||||
|
||||
public readonly bool Equals(BlurVertex other) =>
|
||||
Position.Equals(other.Position)
|
||||
&& TexturePosition.Equals(other.TexturePosition);
|
||||
}
|
||||
}
|
||||
|
||||
private class BufferedContainerDrawNodeSharedData : BufferedDrawNodeSharedData
|
||||
|
||||
@@ -29,7 +29,6 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Framework.Graphics.Containers
|
||||
@@ -41,7 +40,6 @@ namespace osu.Framework.Graphics.Containers
|
||||
/// Additionally, <see cref="CompositeDrawable"/>s support various effects, such as masking, edge effect,
|
||||
/// padding, and automatic sizing depending on their children.
|
||||
/// </summary>
|
||||
[ExcludeFromDynamicCompile]
|
||||
public abstract partial class CompositeDrawable : Drawable
|
||||
{
|
||||
#region Construction and disposal
|
||||
@@ -56,6 +54,9 @@ namespace osu.Framework.Graphics.Containers
|
||||
internalChildren = new SortedList<Drawable>(childComparer);
|
||||
aliveInternalChildren = new SortedList<Drawable>(childComparer);
|
||||
|
||||
// Validate initially. Done before the layout is added below to prevent a callback to this composite.
|
||||
childrenSizeDependencies.Validate();
|
||||
|
||||
AddLayout(childrenSizeDependencies);
|
||||
}
|
||||
|
||||
@@ -1089,9 +1090,14 @@ namespace osu.Framework.Graphics.Containers
|
||||
// The invalidation still needs to occur as normal, since a derived CompositeDrawable may want to respond to children size invalidations.
|
||||
Invalidate(invalidation, InvalidationSource.Child);
|
||||
|
||||
// If all the changed axes were bypassed and an invalidation occurred, the children size dependencies can immediately be
|
||||
// re-validated without a recomputation, as a recomputation would not change the auto-sized size.
|
||||
if (wasValid && (axes & source.BypassAutoSizeAxes) == axes)
|
||||
// Skip axes that are bypassed.
|
||||
axes &= ~source.BypassAutoSizeAxes;
|
||||
|
||||
// Include only axes that this composite is autosizing for.
|
||||
axes &= AutoSizeAxes;
|
||||
|
||||
// If no remaining axes remain, then children size dependencies can immediately be re-validated as the auto-sized size would not change.
|
||||
if (wasValid && axes == Axes.None)
|
||||
childrenSizeDependencies.Validate();
|
||||
}
|
||||
|
||||
@@ -1779,7 +1785,12 @@ namespace osu.Framework.Graphics.Containers
|
||||
throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time.");
|
||||
|
||||
autoSizeAxes = value;
|
||||
childrenSizeDependencies.Invalidate();
|
||||
|
||||
if (value == Axes.None)
|
||||
childrenSizeDependencies.Validate();
|
||||
else
|
||||
childrenSizeDependencies.Invalidate();
|
||||
|
||||
OnSizingChanged();
|
||||
}
|
||||
}
|
||||
@@ -1801,6 +1812,11 @@ namespace osu.Framework.Graphics.Containers
|
||||
/// </summary>
|
||||
internal event Action OnAutoSize;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the <see cref="childrenSizeDependencies"/> layout is valid.
|
||||
/// </summary>
|
||||
internal bool ChildrenSizeDependenciesIsValid => childrenSizeDependencies.IsValid;
|
||||
|
||||
private readonly LayoutValue childrenSizeDependencies = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.Presence, InvalidationSource.Child);
|
||||
|
||||
public override float Width
|
||||
|
||||
@@ -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 osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Framework.Graphics.Containers
|
||||
{
|
||||
/// <summary>
|
||||
@@ -12,15 +14,21 @@ namespace osu.Framework.Graphics.Containers
|
||||
|
||||
public override bool AcceptsFocus => State.Value == Visibility.Visible;
|
||||
|
||||
protected override void PopIn()
|
||||
protected override void UpdateState(ValueChangedEvent<Visibility> state)
|
||||
{
|
||||
Schedule(() => GetContainingInputManager().TriggerFocusContention(this));
|
||||
}
|
||||
base.UpdateState(state);
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
if (HasFocus)
|
||||
GetContainingInputManager().ChangeFocus(null);
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case Visibility.Hidden:
|
||||
if (HasFocus)
|
||||
GetContainingInputManager().ChangeFocus(null);
|
||||
break;
|
||||
|
||||
case Visibility.Visible:
|
||||
Schedule(() => GetContainingInputManager().TriggerFocusContention(this));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +92,30 @@ namespace osu.Framework.Graphics.Containers
|
||||
base.Update();
|
||||
|
||||
if (!filterValid.IsValid)
|
||||
{
|
||||
canBeShownBindables.Clear();
|
||||
performFilter();
|
||||
filterValid.Validate();
|
||||
FilterCompleted?.Invoke();
|
||||
}
|
||||
Filter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately filter <see cref="IFilterable"/> children based on the current <see cref="SearchTerm"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Filtering is done automatically after a change to <see cref="SearchTerm"/>, on new drawables being added, and on certain changes to
|
||||
/// searchable children (like <see cref="IConditionalFilterable.CanBeShown"/> changing).
|
||||
///
|
||||
/// However, if <see cref="SearchContainer{T}"/> or any of its parents are hidden this will not be run.
|
||||
/// If an implementation relies on filtering to become present / visible, this method can be used to force a filter.
|
||||
///
|
||||
/// Note that this will only run if the current filter is not in an already valid state.
|
||||
/// </remarks>
|
||||
protected void Filter()
|
||||
{
|
||||
if (filterValid.IsValid)
|
||||
return;
|
||||
|
||||
canBeShownBindables.Clear();
|
||||
performFilter();
|
||||
filterValid.Validate();
|
||||
FilterCompleted?.Invoke();
|
||||
}
|
||||
|
||||
private void performFilter()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@@ -43,14 +44,18 @@ namespace osu.Framework.Graphics.Cursor
|
||||
base.LoadComplete();
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
inputManager.TouchLongPressBegan += (position, duration) =>
|
||||
{
|
||||
longPressFeedback.Position = Parent.ToLocalSpace(position);
|
||||
longPressFeedback.BeginAnimation(duration);
|
||||
};
|
||||
inputManager.TouchLongPressBegan += onLongPressBegan;
|
||||
inputManager.TouchLongPressCancelled += longPressFeedback.CancelAnimation;
|
||||
}
|
||||
|
||||
private void onLongPressBegan(Vector2 position, double duration)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
|
||||
longPressFeedback.Position = Parent.ToLocalSpace(position);
|
||||
longPressFeedback.BeginAnimation(duration);
|
||||
}
|
||||
|
||||
protected virtual Drawable CreateCursor() => new Cursor();
|
||||
|
||||
/// <summary>
|
||||
@@ -79,6 +84,17 @@ namespace osu.Framework.Graphics.Cursor
|
||||
Alpha = 0;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (inputManager.IsNotNull())
|
||||
{
|
||||
inputManager.TouchLongPressBegan -= onLongPressBegan;
|
||||
inputManager.TouchLongPressCancelled -= longPressFeedback.CancelAnimation;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class Cursor : CircularContainer
|
||||
{
|
||||
public Cursor()
|
||||
|
||||
@@ -32,7 +32,6 @@ using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.States;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK.Input;
|
||||
using Container = osu.Framework.Graphics.Containers.Container;
|
||||
@@ -51,7 +50,6 @@ namespace osu.Framework.Graphics
|
||||
/// Drawables are always rectangular in shape in their local coordinate system,
|
||||
/// which makes them quad-shaped in arbitrary (linearly transformed) coordinate systems.
|
||||
/// </summary>
|
||||
[ExcludeFromDynamicCompile]
|
||||
public abstract partial class Drawable : Transformable, IDisposable, IDrawable
|
||||
{
|
||||
#region Construction and disposal
|
||||
|
||||
@@ -280,7 +280,20 @@ namespace osu.Framework.Graphics.Lines
|
||||
// The path should not receive the true colour to avoid colour doubling when the frame-buffer is rendered to the back-buffer.
|
||||
public override DrawColourInfo DrawColourInfo => new DrawColourInfo(Color4.White, base.DrawColourInfo.Blending);
|
||||
|
||||
public Color4 BackgroundColour => new Color4(0, 0, 0, 0);
|
||||
private Color4 backgroundColour = new Color4(0, 0, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// The background colour to be used for the frame buffer this path is rendered to.
|
||||
/// </summary>
|
||||
public virtual Color4 BackgroundColour
|
||||
{
|
||||
get => backgroundColour;
|
||||
set
|
||||
{
|
||||
backgroundColour = value;
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly BufferedDrawNodeSharedData sharedData = new BufferedDrawNodeSharedData(new[] { RenderBufferFormat.D16 }, clipToRootNode: true);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osuTK.Graphics;
|
||||
@@ -39,6 +40,18 @@ namespace osu.Framework.Graphics.Lines
|
||||
}
|
||||
}
|
||||
|
||||
private Color4? customBackgroundColour;
|
||||
|
||||
/// <summary>
|
||||
/// The background colour to be used for the frame buffer this path is rendered to.
|
||||
/// For <see cref="SmoothPath"/>, this automatically defaults to the colour at 0 (the outermost colour of the path) to avoid aliasing issues.
|
||||
/// </summary>
|
||||
public override Color4 BackgroundColour
|
||||
{
|
||||
get => customBackgroundColour ?? base.BackgroundColour;
|
||||
set => customBackgroundColour = base.BackgroundColour = value;
|
||||
}
|
||||
|
||||
private readonly Cached textureCache = new Cached();
|
||||
|
||||
protected void InvalidateTexture()
|
||||
@@ -71,6 +84,9 @@ namespace osu.Framework.Graphics.Lines
|
||||
texture.SetData(new TextureUpload(raw));
|
||||
Texture = texture;
|
||||
|
||||
if (customBackgroundColour == null)
|
||||
base.BackgroundColour = ColourAt(0).Opacity(0);
|
||||
|
||||
textureCache.Validate();
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,6 @@ namespace osu.Framework.Graphics.OpenGL.Buffers
|
||||
if (value.Equals(data))
|
||||
return;
|
||||
|
||||
renderer.FlushCurrentBatch(FlushBatchSource.SetUniform);
|
||||
|
||||
setData(ref value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,8 +376,8 @@ namespace osu.Framework.Graphics.OpenGL
|
||||
return new GLShaderPart(this, name, rawData, glType, store);
|
||||
}
|
||||
|
||||
protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer)
|
||||
=> new GLShader(this, name, parts.Cast<GLShaderPart>().ToArray(), globalUniformBuffer);
|
||||
protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer, ShaderCompilationStore compilationStore)
|
||||
=> new GLShader(this, name, parts.Cast<GLShaderPart>().ToArray(), globalUniformBuffer, compilationStore);
|
||||
|
||||
public override IFrameBuffer CreateFrameBuffer(RenderBufferFormat[]? renderBufferFormats = null, TextureFilteringMode filteringMode = TextureFilteringMode.Linear)
|
||||
{
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.OpenGL.Buffers;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Threading;
|
||||
@@ -33,11 +32,6 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
private readonly Dictionary<string, GLUniformBlock> uniformBlocks = new Dictionary<string, GLUniformBlock>();
|
||||
private readonly List<Uniform<int>> textureUniforms = new List<Uniform<int>>();
|
||||
|
||||
/// <summary>
|
||||
/// Holds all <see cref="uniformBlocks"/> values for faster access than iterating on <see cref="Dictionary{TKey,TValue}.Values"/>.
|
||||
/// </summary>
|
||||
private readonly List<GLUniformBlock> uniformBlocksValues = new List<GLUniformBlock>();
|
||||
|
||||
public bool IsLoaded { get; private set; }
|
||||
|
||||
public bool IsBound { get; private set; }
|
||||
@@ -46,14 +40,14 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
|
||||
private readonly GLShaderPart vertexPart;
|
||||
private readonly GLShaderPart fragmentPart;
|
||||
private readonly VertexFragmentCompilationResult crossCompileResult;
|
||||
private readonly VertexFragmentShaderCompilation compilation;
|
||||
|
||||
internal GLShader(GLRenderer renderer, string name, GLShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer)
|
||||
internal GLShader(GLRenderer renderer, string name, GLShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer, ShaderCompilationStore compilationStore)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
this.name = name;
|
||||
this.globalUniformBuffer = globalUniformBuffer;
|
||||
this.parts = parts.Where(p => p != null).ToArray();
|
||||
this.parts = parts;
|
||||
|
||||
vertexPart = parts.Single(p => p.Type == ShaderType.VertexShader);
|
||||
fragmentPart = parts.Single(p => p.Type == ShaderType.FragmentShader);
|
||||
@@ -63,9 +57,9 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
try
|
||||
{
|
||||
// Shaders are in "Vulkan GLSL" format. They need to be cross-compiled to GLSL.
|
||||
crossCompileResult = SpirvCompilation.CompileVertexFragment(
|
||||
Encoding.UTF8.GetBytes(vertexPart.GetRawText()),
|
||||
Encoding.UTF8.GetBytes(fragmentPart.GetRawText()),
|
||||
compilation = compilationStore.CompileVertexFragment(
|
||||
vertexPart.GetRawText(),
|
||||
fragmentPart.GetRawText(),
|
||||
renderer.IsEmbedded ? CrossCompileTarget.ESSL : CrossCompileTarget.GLSL);
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -122,9 +116,6 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
for (int i = 0; i < textureUniforms.Count; i++)
|
||||
textureUniforms[i].Update();
|
||||
|
||||
foreach (var block in uniformBlocksValues)
|
||||
block?.Bind();
|
||||
|
||||
IsBound = true;
|
||||
}
|
||||
|
||||
@@ -151,18 +142,22 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
|
||||
public virtual void BindUniformBlock(string blockName, IUniformBuffer buffer)
|
||||
{
|
||||
if (buffer is not IGLUniformBuffer glBuffer)
|
||||
throw new ArgumentException($"Buffer must be an {nameof(IGLUniformBuffer)}.");
|
||||
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException(ToString(), "Can not retrieve uniforms from a disposed shader.");
|
||||
|
||||
EnsureShaderCompiled();
|
||||
|
||||
uniformBlocks[blockName].Assign(buffer);
|
||||
renderer.FlushCurrentBatch(FlushBatchSource.BindBuffer);
|
||||
GL.BindBufferBase(BufferRangeTarget.UniformBuffer, uniformBlocks[blockName].Binding, glBuffer.Id);
|
||||
}
|
||||
|
||||
private protected virtual bool CompileInternal()
|
||||
{
|
||||
vertexPart.Compile(crossCompileResult.VertexShader);
|
||||
fragmentPart.Compile(crossCompileResult.FragmentShader);
|
||||
vertexPart.Compile(compilation.VertexText);
|
||||
fragmentPart.Compile(compilation.FragmentText);
|
||||
|
||||
foreach (GLShaderPart p in parts)
|
||||
GL.AttachShader(this, p);
|
||||
@@ -179,7 +174,7 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
int blockBindingIndex = 0;
|
||||
int textureIndex = 0;
|
||||
|
||||
foreach (ResourceLayoutDescription layout in crossCompileResult.Reflection.ResourceLayouts)
|
||||
foreach (ResourceLayoutDescription layout in compilation.Reflection.ResourceLayouts)
|
||||
{
|
||||
if (layout.Elements.Length == 0)
|
||||
continue;
|
||||
@@ -194,9 +189,8 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
}
|
||||
else if (layout.Elements[0].Kind == ResourceKind.UniformBuffer)
|
||||
{
|
||||
var block = new GLUniformBlock(renderer, this, GL.GetUniformBlockIndex(this, layout.Elements[0].Name), blockBindingIndex++);
|
||||
var block = new GLUniformBlock(this, GL.GetUniformBlockIndex(this, layout.Elements[0].Name), blockBindingIndex++);
|
||||
uniformBlocks[layout.Elements[0].Name] = block;
|
||||
uniformBlocksValues.Add(block);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,15 +224,16 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
IsDisposed = true;
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
shaderCompileDelegate?.Cancel();
|
||||
IsDisposed = true;
|
||||
|
||||
if (programID != -1)
|
||||
DeleteProgram(this);
|
||||
}
|
||||
if (shaderCompileDelegate.IsNotNull())
|
||||
shaderCompileDelegate.Cancel();
|
||||
|
||||
if (programID != -1)
|
||||
DeleteProgram(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@@ -31,7 +29,7 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
|
||||
private int partID = -1;
|
||||
|
||||
public GLShaderPart(IRenderer renderer, string name, byte[] data, ShaderType type, IShaderStore store)
|
||||
public GLShaderPart(IRenderer renderer, string name, byte[]? data, ShaderType type, IShaderStore store)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
this.store = store;
|
||||
@@ -62,10 +60,10 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
shaderCodes[i] = uniform_pattern.Replace(shaderCodes[i], match => $"{match.Groups[1].Value}set = {int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture) + 1}{match.Groups[3].Value}");
|
||||
}
|
||||
|
||||
private string loadFile(byte[] bytes, bool mainFile)
|
||||
private string loadFile(byte[]? bytes, bool mainFile)
|
||||
{
|
||||
if (bytes == null)
|
||||
return null;
|
||||
return string.Empty;
|
||||
|
||||
using (MemoryStream ms = new MemoryStream(bytes))
|
||||
using (StreamReader sr = new StreamReader(ms))
|
||||
@@ -74,7 +72,7 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
|
||||
while (sr.Peek() != -1)
|
||||
{
|
||||
string line = sr.ReadLine();
|
||||
string? line = sr.ReadLine();
|
||||
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
@@ -123,10 +121,10 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
|
||||
if (!string.IsNullOrEmpty(backbufferCode))
|
||||
{
|
||||
string realMainName = "real_main_" + Guid.NewGuid().ToString("N");
|
||||
const string real_main_name = "__internal_real_main";
|
||||
|
||||
backbufferCode = backbufferCode.Replace("{{ real_main }}", realMainName);
|
||||
code = Regex.Replace(code, @"void main\((.*)\)", $"void {realMainName}()") + backbufferCode + '\n';
|
||||
backbufferCode = backbufferCode.Replace("{{ real_main }}", real_main_name);
|
||||
code = Regex.Replace(code, @"void main\((.*)\)", $"void {real_main_name}()") + backbufferCode + '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// 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 osu.Framework.Graphics.OpenGL.Buffers;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osuTK.Graphics.ES30;
|
||||
|
||||
namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
@@ -13,54 +10,20 @@ namespace osu.Framework.Graphics.OpenGL.Shaders
|
||||
/// </summary>
|
||||
internal class GLUniformBlock
|
||||
{
|
||||
private readonly GLRenderer renderer;
|
||||
private readonly int binding;
|
||||
private int assignedBuffer = -1;
|
||||
public readonly int Binding;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new uniform block.
|
||||
/// </summary>
|
||||
/// <param name="renderer">The renderer.</param>
|
||||
/// <param name="shader">The shader.</param>
|
||||
/// <param name="index">The index (location) of this block in the GL shader.</param>
|
||||
/// <param name="binding">A unique index for this block to bound to in the GL program.</param>
|
||||
public GLUniformBlock(GLRenderer renderer, GLShader shader, int index, int binding)
|
||||
public GLUniformBlock(GLShader shader, int index, int binding)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
this.binding = binding;
|
||||
Binding = binding;
|
||||
|
||||
// This creates a mapping in the shader program that binds the block at location `index` to location `binding` in the GL pipeline.
|
||||
GL.UniformBlockBinding(shader, index, binding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns an <see cref="IUniformBuffer{TData}"/> to this uniform block.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer to assign.</param>
|
||||
/// <exception cref="ArgumentException">If the provided buffer is not a <see cref="GLUniformBuffer{TData}"/>.</exception>
|
||||
public void Assign(IUniformBuffer buffer)
|
||||
{
|
||||
if (buffer is not IGLUniformBuffer glBuffer)
|
||||
throw new ArgumentException($"Provided argument must be a {typeof(GLUniformBuffer<>)}");
|
||||
|
||||
if (assignedBuffer == glBuffer.Id)
|
||||
return;
|
||||
|
||||
assignedBuffer = glBuffer.Id;
|
||||
|
||||
// If the shader was bound prior to this buffer being assigned, then the buffer needs to be bound immediately.
|
||||
Bind();
|
||||
}
|
||||
|
||||
public void Bind()
|
||||
{
|
||||
if (assignedBuffer == -1)
|
||||
return;
|
||||
|
||||
renderer.FlushCurrentBatch(FlushBatchSource.BindBuffer);
|
||||
|
||||
// Bind the assigned buffer to the correct slot in the GL pipeline.
|
||||
GL.BindBufferBase(BufferRangeTarget.UniformBuffer, binding, assignedBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,17 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Graphics.OpenGL.Buffers;
|
||||
using osu.Framework.Extensions.ImageExtensions;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Platform;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Graphics.ES30;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace osu.Framework.Graphics.OpenGL.Textures
|
||||
@@ -61,10 +64,12 @@ namespace osu.Framework.Graphics.OpenGL.Textures
|
||||
|
||||
private int internalWidth;
|
||||
private int internalHeight;
|
||||
private bool manualMipmaps;
|
||||
|
||||
private readonly List<RectangleI> uploadedRegions = new List<RectangleI>();
|
||||
|
||||
private readonly All filteringMode;
|
||||
private readonly Color4 initialisationColour;
|
||||
private readonly bool manualMipmaps;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="GLTexture"/>.
|
||||
@@ -196,11 +201,11 @@ namespace osu.Framework.Graphics.OpenGL.Textures
|
||||
if (!Available)
|
||||
return false;
|
||||
|
||||
uploadedRegions.Clear();
|
||||
|
||||
// We should never run raw OGL calls on another thread than the main thread due to race conditions.
|
||||
ThreadSafety.EnsureDrawThread();
|
||||
|
||||
List<RectangleI> uploadedRegions = new List<RectangleI>();
|
||||
|
||||
while (tryGetNextUpload(out ITextureUpload upload))
|
||||
{
|
||||
using (upload)
|
||||
@@ -429,45 +434,57 @@ namespace osu.Framework.Graphics.OpenGL.Textures
|
||||
}
|
||||
else
|
||||
{
|
||||
initializeLevel(upload.Level, Width, Height, upload.Format);
|
||||
initializeLevel(upload.Level, Width, Height);
|
||||
|
||||
GL.TexSubImage2D(TextureTarget2d.Texture2D, upload.Level, upload.Bounds.X, upload.Bounds.Y, upload.Bounds.Width, upload.Bounds.Height, upload.Format,
|
||||
PixelType.UnsignedByte, dataPointer);
|
||||
}
|
||||
|
||||
if (!manualMipmaps)
|
||||
{
|
||||
int width = internalWidth;
|
||||
int height = internalHeight;
|
||||
|
||||
for (int level = 1; level < IRenderer.MAX_MIPMAP_LEVELS + 1 && (width > 1 || height > 1); ++level)
|
||||
{
|
||||
width = Math.Max(width >> 1, 1);
|
||||
height = Math.Max(height >> 1, 1);
|
||||
|
||||
initializeLevel(level, width, height, upload.Format);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Just update content of the current texture
|
||||
else if (dataPointer != IntPtr.Zero)
|
||||
{
|
||||
Renderer.BindTexture(this);
|
||||
GL.TexSubImage2D(TextureTarget2d.Texture2D, upload.Level, upload.Bounds.X, upload.Bounds.Y, upload.Bounds.Width, upload.Bounds.Height, upload.Format, PixelType.UnsignedByte,
|
||||
dataPointer);
|
||||
|
||||
if (!manualMipmaps && upload.Level > 0)
|
||||
{
|
||||
//allocate mipmap levels
|
||||
int level = 1;
|
||||
int d = 2;
|
||||
|
||||
while (Width / d > 0)
|
||||
{
|
||||
initializeLevel(level, Width / d, Height / d);
|
||||
level++;
|
||||
d *= 2;
|
||||
}
|
||||
|
||||
manualMipmaps = true;
|
||||
}
|
||||
|
||||
int div = (int)Math.Pow(2, upload.Level);
|
||||
|
||||
GL.TexSubImage2D(TextureTarget2d.Texture2D, upload.Level, upload.Bounds.X / div, upload.Bounds.Y / div, upload.Bounds.Width / div, upload.Bounds.Height / div,
|
||||
upload.Format, PixelType.UnsignedByte, dataPointer);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeLevel(int level, int width, int height, PixelFormat format)
|
||||
private void initializeLevel(int level, int width, int height)
|
||||
{
|
||||
updateMemoryUsage(level, (long)width * height * 4);
|
||||
GL.TexImage2D(TextureTarget2d.Texture2D, level, TextureComponentCount.Rgba8, width, height, 0, format, PixelType.UnsignedByte, IntPtr.Zero);
|
||||
using (var image = createBackingImage(width, height))
|
||||
using (var pixels = image.CreateReadOnlyPixelSpan())
|
||||
{
|
||||
updateMemoryUsage(level, (long)width * height * 4);
|
||||
GL.TexImage2D(TextureTarget2d.Texture2D, level, TextureComponentCount.Rgba8, width, height, 0, PixelFormat.Rgba, PixelType.UnsignedByte,
|
||||
ref MemoryMarshal.GetReference(pixels.Span));
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize texture to solid color
|
||||
using var frameBuffer = new GLFrameBuffer(Renderer, this, level);
|
||||
Renderer.BindFrameBuffer(frameBuffer);
|
||||
Renderer.Clear(new ClearInfo(initialisationColour));
|
||||
Renderer.UnbindFrameBuffer(frameBuffer);
|
||||
private Image<Rgba32> createBackingImage(int width, int height)
|
||||
{
|
||||
// it is faster to initialise without a background specification if transparent black is all that's required.
|
||||
return initialisationColour == default
|
||||
? new Image<Rgba32>(width, height)
|
||||
: new Image<Rgba32>(width, height, new Rgba32(new Vector4(initialisationColour.R, initialisationColour.G, initialisationColour.B, initialisationColour.A)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Framework.Graphics.Performance
|
||||
@@ -36,11 +35,11 @@ namespace osu.Framework.Graphics.Performance
|
||||
Colour = Color4.Black,
|
||||
Alpha = 0.75f
|
||||
},
|
||||
counter = new CounterText
|
||||
counter = new SpriteText
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Spacing = new Vector2(-1, 0),
|
||||
Font = FrameworkFont.Regular,
|
||||
Text = @"...",
|
||||
}
|
||||
});
|
||||
@@ -104,16 +103,6 @@ namespace osu.Framework.Graphics.Performance
|
||||
+ (clock.Throttling ? $"{(clock.MaximumUpdateHz > 0 && clock.MaximumUpdateHz < 10000 ? clock.MaximumUpdateHz.ToString("0") : "∞"),4}hz" : string.Empty);
|
||||
}
|
||||
|
||||
private partial class CounterText : SpriteText
|
||||
{
|
||||
public CounterText()
|
||||
{
|
||||
Font = FrameworkFont.Regular.With(fixedWidth: true);
|
||||
}
|
||||
|
||||
protected override char[] FixedWidthExcludeCharacters { get; } = { ',', '.', ' ' };
|
||||
}
|
||||
|
||||
public void NewFrame(FrameStatistics frame)
|
||||
{
|
||||
if (!Counting) return;
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Threading;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
using osuTK.Graphics;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace osu.Framework.Graphics.Performance
|
||||
@@ -18,8 +20,17 @@ namespace osu.Framework.Graphics.Performance
|
||||
[Resolved]
|
||||
private GameHost host { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private FrameworkConfigManager config { get; set; } = null!;
|
||||
|
||||
private FrameStatisticsMode state;
|
||||
|
||||
private TextFlowContainer? infoText;
|
||||
|
||||
private Bindable<FrameSync> configFrameSync = null!;
|
||||
private Bindable<ExecutionMode> configExecutionMode = null!;
|
||||
private Bindable<WindowMode> configWindowMode = null!;
|
||||
|
||||
public event Action<FrameStatisticsMode>? StateChanged;
|
||||
|
||||
private bool initialised;
|
||||
@@ -46,7 +57,18 @@ namespace osu.Framework.Graphics.Performance
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
configFrameSync = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync);
|
||||
configFrameSync.BindValueChanged(_ => updateInfoText());
|
||||
|
||||
configExecutionMode = config.GetBindable<ExecutionMode>(FrameworkSetting.ExecutionMode);
|
||||
configExecutionMode.BindValueChanged(_ => updateInfoText());
|
||||
|
||||
configWindowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
|
||||
configWindowMode.BindValueChanged(_ => updateInfoText());
|
||||
|
||||
updateState();
|
||||
updateInfoText();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
@@ -65,13 +87,16 @@ namespace osu.Framework.Graphics.Performance
|
||||
|
||||
var uploadPool = createUploadPool();
|
||||
|
||||
Add(new SpriteText
|
||||
Add(infoText = new TextFlowContainer(cp => cp.Font = FrameworkFont.Condensed)
|
||||
{
|
||||
Text = $"Renderer: {host.RendererInfo}",
|
||||
Alpha = 0.75f,
|
||||
Origin = Anchor.TopRight,
|
||||
TextAnchor = Anchor.TopRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
});
|
||||
|
||||
updateInfoText();
|
||||
|
||||
foreach (GameThread t in host.Threads)
|
||||
Add(new FrameStatisticsDisplay(t, uploadPool) { State = state });
|
||||
}
|
||||
@@ -86,6 +111,37 @@ namespace osu.Framework.Graphics.Performance
|
||||
StateChanged?.Invoke(State);
|
||||
}
|
||||
|
||||
private void updateInfoText()
|
||||
{
|
||||
if (infoText == null)
|
||||
return;
|
||||
|
||||
infoText.Clear();
|
||||
|
||||
addHeader("Renderer:");
|
||||
addValue(host.RendererInfo);
|
||||
|
||||
infoText.NewLine();
|
||||
|
||||
addHeader("Limiter:");
|
||||
addValue(configFrameSync.ToString());
|
||||
addHeader("Execution:");
|
||||
addValue(configExecutionMode.ToString());
|
||||
addHeader("Mode:");
|
||||
addValue(configWindowMode.ToString());
|
||||
|
||||
void addHeader(string text) => infoText.AddText($"{text} ", cp =>
|
||||
{
|
||||
cp.Padding = new MarginPadding { Left = 5 };
|
||||
cp.Colour = Color4.Gray;
|
||||
});
|
||||
|
||||
void addValue(string text) => infoText.AddText(text, cp =>
|
||||
{
|
||||
cp.Font = cp.Font.With(weight: "Bold");
|
||||
});
|
||||
}
|
||||
|
||||
private ArrayPool<Rgba32> createUploadPool()
|
||||
{
|
||||
// bucket size should be enough to allow some overhead when running multi-threaded with draw at 60hz.
|
||||
|
||||
@@ -50,13 +50,15 @@ namespace osu.Framework.Graphics.Rendering.Dummy
|
||||
public DummyRenderer()
|
||||
{
|
||||
maskingInfo = default;
|
||||
WhitePixel = new Texture(new DummyNativeTexture(this), WrapMode.None, WrapMode.None);
|
||||
WhitePixel = new TextureWhitePixel(new Texture(new DummyNativeTexture(this), WrapMode.None, WrapMode.None));
|
||||
}
|
||||
|
||||
bool IRenderer.VerticalSync { get; set; } = true;
|
||||
|
||||
bool IRenderer.AllowTearing { get; set; }
|
||||
|
||||
Storage? IRenderer.CacheStorage { set { } }
|
||||
|
||||
void IRenderer.Initialise(IGraphicsSurface graphicsSurface)
|
||||
{
|
||||
IsInitialised = true;
|
||||
|
||||
@@ -51,6 +51,11 @@ namespace osu.Framework.Graphics.Rendering
|
||||
|
||||
protected internal bool AllowTearing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Storage"/> that can be used to cache objects.
|
||||
/// </summary>
|
||||
protected internal Storage? CacheStorage { set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed texture size.
|
||||
/// </summary>
|
||||
|
||||
@@ -44,6 +44,11 @@ namespace osu.Framework.Graphics.Rendering
|
||||
protected internal abstract bool VerticalSync { get; set; }
|
||||
protected internal abstract bool AllowTearing { get; set; }
|
||||
|
||||
protected internal Storage? CacheStorage
|
||||
{
|
||||
set => shaderCompilationStore.CacheStorage = value;
|
||||
}
|
||||
|
||||
public int MaxTextureSize { get; protected set; } = 4096; // default value is to allow roughly normal flow in cases we don't have graphics context, like headless CI.
|
||||
|
||||
public int MaxTexturesUploadedPerFrame { get; set; } = 32;
|
||||
@@ -94,6 +99,8 @@ namespace osu.Framework.Graphics.Rendering
|
||||
/// </summary>
|
||||
protected IShader? Shader { get; private set; }
|
||||
|
||||
private readonly ShaderCompilationStore shaderCompilationStore = new ShaderCompilationStore();
|
||||
|
||||
private readonly GlobalStatistic<int> statExpensiveOperationsQueued;
|
||||
private readonly GlobalStatistic<int> statTextureUploadsQueued;
|
||||
private readonly GlobalStatistic<int> statTextureUploadsDequeued;
|
||||
@@ -1036,7 +1043,7 @@ namespace osu.Framework.Graphics.Rendering
|
||||
protected abstract IShaderPart CreateShaderPart(IShaderStore store, string name, byte[]? rawData, ShaderPartType partType);
|
||||
|
||||
/// <inheritdoc cref="IRenderer.CreateShader"/>
|
||||
protected abstract IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer);
|
||||
protected abstract IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer, ShaderCompilationStore compilationStore);
|
||||
|
||||
private IShader? mipmapShader;
|
||||
|
||||
@@ -1056,7 +1063,7 @@ namespace osu.Framework.Graphics.Rendering
|
||||
{
|
||||
CreateShaderPart(store, "mipmap.vs", store.GetRawData("sh_mipmap.vs"), ShaderPartType.Vertex),
|
||||
CreateShaderPart(store, "mipmap.fs", store.GetRawData("sh_mipmap.fs"), ShaderPartType.Fragment),
|
||||
}, globalUniformBuffer.AsNonNull());
|
||||
}, globalUniformBuffer.AsNonNull(), shaderCompilationStore);
|
||||
|
||||
return mipmapShader;
|
||||
}
|
||||
@@ -1128,6 +1135,11 @@ namespace osu.Framework.Graphics.Rendering
|
||||
set => AllowTearing = value;
|
||||
}
|
||||
|
||||
Storage? IRenderer.CacheStorage
|
||||
{
|
||||
set => CacheStorage = value;
|
||||
}
|
||||
|
||||
IVertexBatch<TexturedVertex2D> IRenderer.DefaultQuadBatch => DefaultQuadBatch;
|
||||
void IRenderer.BeginFrame(Vector2 windowSize) => BeginFrame(windowSize);
|
||||
void IRenderer.FinishFrame() => FinishFrame();
|
||||
@@ -1143,7 +1155,7 @@ namespace osu.Framework.Graphics.Rendering
|
||||
void IRenderer.PopQuadBatch() => PopQuadBatch();
|
||||
Image<Rgba32> IRenderer.TakeScreenshot() => TakeScreenshot();
|
||||
IShaderPart IRenderer.CreateShaderPart(IShaderStore store, string name, byte[]? rawData, ShaderPartType partType) => CreateShaderPart(store, name, rawData, partType);
|
||||
IShader IRenderer.CreateShader(string name, IShaderPart[] parts) => CreateShader(name, parts, globalUniformBuffer!);
|
||||
IShader IRenderer.CreateShader(string name, IShaderPart[] parts) => CreateShader(name, parts, globalUniformBuffer!, shaderCompilationStore);
|
||||
|
||||
IVertexBatch<TVertex> IRenderer.CreateLinearBatch<TVertex>(int size, int maxBuffers, PrimitiveTopology topology)
|
||||
{
|
||||
|
||||
30
osu.Framework/Graphics/Shaders/ComputeProgramCompilation.cs
Normal file
30
osu.Framework/Graphics/Shaders/ComputeProgramCompilation.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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 Veldrid.SPIRV;
|
||||
|
||||
namespace osu.Framework.Graphics.Shaders
|
||||
{
|
||||
public class ComputeProgramCompilation
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this compilation was retrieved from cache.
|
||||
/// </summary>
|
||||
public bool WasCached { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The SpirV bytes for the program.
|
||||
/// </summary>
|
||||
public byte[] ProgramBytes { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The cross-compiled program text.
|
||||
/// </summary>
|
||||
public string ProgramText { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// A reflection of the shader program, describing the layout of resources.
|
||||
/// </summary>
|
||||
public SpirvReflection Reflection { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
139
osu.Framework/Graphics/Shaders/ShaderCompilationStore.cs
Normal file
139
osu.Framework/Graphics/Shaders/ShaderCompilationStore.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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.CodeAnalysis;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using Veldrid;
|
||||
using Veldrid.SPIRV;
|
||||
|
||||
namespace osu.Framework.Graphics.Shaders
|
||||
{
|
||||
public class ShaderCompilationStore
|
||||
{
|
||||
public Storage? CacheStorage { private get; set; }
|
||||
|
||||
public VertexFragmentShaderCompilation CompileVertexFragment(string vertexText, string fragmentText, CrossCompileTarget target)
|
||||
{
|
||||
// vertexHash#fragmentHash#target
|
||||
string filename = $"{vertexText.ComputeMD5Hash()}#{fragmentText.ComputeMD5Hash()}#{(int)target}";
|
||||
|
||||
if (tryGetCached(filename, out VertexFragmentShaderCompilation? existing))
|
||||
{
|
||||
existing.WasCached = true;
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Debug preserves names for reflection.
|
||||
byte[] vertexBytes = SpirvCompilation.CompileGlslToSpirv(vertexText, null, ShaderStages.Vertex, new GlslCompileOptions(true)).SpirvBytes;
|
||||
byte[] fragmentBytes = SpirvCompilation.CompileGlslToSpirv(fragmentText, null, ShaderStages.Fragment, new GlslCompileOptions(true)).SpirvBytes;
|
||||
|
||||
VertexFragmentCompilationResult crossResult = SpirvCompilation.CompileVertexFragment(vertexBytes, fragmentBytes, target, new CrossCompileOptions());
|
||||
VertexFragmentShaderCompilation compilation = new VertexFragmentShaderCompilation
|
||||
{
|
||||
VertexBytes = vertexBytes,
|
||||
FragmentBytes = fragmentBytes,
|
||||
VertexText = crossResult.VertexShader,
|
||||
FragmentText = crossResult.FragmentShader,
|
||||
Reflection = crossResult.Reflection
|
||||
};
|
||||
|
||||
saveToCache(filename, compilation);
|
||||
|
||||
return compilation;
|
||||
}
|
||||
|
||||
public ComputeProgramCompilation CompileCompute(string programText, CrossCompileTarget target)
|
||||
{
|
||||
// programHash#target
|
||||
string filename = $"{programText.ComputeMD5Hash()}#{(int)target}";
|
||||
|
||||
if (tryGetCached(filename, out ComputeProgramCompilation? existing))
|
||||
{
|
||||
existing.WasCached = true;
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Debug preserves names for reflection.
|
||||
byte[] programBytes = SpirvCompilation.CompileGlslToSpirv(programText, null, ShaderStages.Compute, new GlslCompileOptions(true)).SpirvBytes;
|
||||
|
||||
ComputeCompilationResult crossResult = SpirvCompilation.CompileCompute(programBytes, target, new CrossCompileOptions());
|
||||
ComputeProgramCompilation compilation = new ComputeProgramCompilation
|
||||
{
|
||||
ProgramBytes = programBytes,
|
||||
ProgramText = crossResult.ComputeShader,
|
||||
Reflection = crossResult.Reflection
|
||||
};
|
||||
|
||||
saveToCache(filename, compilation);
|
||||
|
||||
return compilation;
|
||||
}
|
||||
|
||||
private bool tryGetCached<T>(string filename, [NotNullWhen(true)] out T? compilation)
|
||||
where T : class
|
||||
{
|
||||
compilation = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (CacheStorage == null)
|
||||
return false;
|
||||
|
||||
if (!CacheStorage.Exists(filename))
|
||||
return false;
|
||||
|
||||
using var stream = CacheStorage.GetStream(filename);
|
||||
using var br = new BinaryReader(stream);
|
||||
|
||||
string checksum = br.ReadString();
|
||||
string data = br.ReadString();
|
||||
|
||||
if (data.ComputeMD5Hash() != checksum)
|
||||
{
|
||||
// Data corrupted..
|
||||
Logger.Log("Cached shader data is corrupted - recompiling.");
|
||||
return false;
|
||||
}
|
||||
|
||||
compilation = JsonConvert.DeserializeObject<T>(data)!;
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Failed to read cached shader compilation - recompiling.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void saveToCache(string filename, object compilation)
|
||||
{
|
||||
if (CacheStorage == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// ensure any stale cached versions are deleted.
|
||||
CacheStorage.Delete(filename);
|
||||
|
||||
using var stream = CacheStorage.CreateFileSafely(filename);
|
||||
using var bw = new BinaryWriter(stream);
|
||||
|
||||
string data = JsonConvert.SerializeObject(compilation);
|
||||
string checksum = data.ComputeMD5Hash();
|
||||
|
||||
bw.Write(checksum);
|
||||
bw.Write(data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Failed to save shader to cache.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,6 @@ namespace osu.Framework.Graphics.Shaders
|
||||
public const string TEXTURE_2 = "Texture2D";
|
||||
public const string TEXTURE_3 = "Texture3D";
|
||||
public const string POSITION = "Position";
|
||||
public const string BLUR = "Blur";
|
||||
}
|
||||
|
||||
public static class FragmentShaderDescriptor
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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 Veldrid.SPIRV;
|
||||
|
||||
namespace osu.Framework.Graphics.Shaders
|
||||
{
|
||||
public class VertexFragmentShaderCompilation
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this compilation was retrieved from cache.
|
||||
/// </summary>
|
||||
public bool WasCached { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The SpirV bytes for the vertex shader.
|
||||
/// </summary>
|
||||
public byte[] VertexBytes { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The SpirV bytes for the fragment shader.
|
||||
/// </summary>
|
||||
public byte[] FragmentBytes { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The cross-compiled vertex shader text.
|
||||
/// </summary>
|
||||
public string VertexText { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The cross-compiled fragment shader text.
|
||||
/// </summary>
|
||||
public string FragmentText { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// A reflection of the shader program, describing the layout of resources.
|
||||
/// </summary>
|
||||
public SpirvReflection Reflection { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace osu.Framework.Graphics.Sprites
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -289,6 +289,11 @@ namespace osu.Framework.Graphics.Sprites
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When <see cref="Truncate"/> is enabled, this indicates whether <see cref="Text"/> has been visually truncated.
|
||||
/// </summary>
|
||||
protected bool IsTruncated { get; private set; }
|
||||
|
||||
private bool requiresAutoSizedWidth => explicitWidth == null && (RelativeSizeAxes & Axes.X) == 0;
|
||||
|
||||
private bool requiresAutoSizedHeight => explicitHeight == null && (RelativeSizeAxes & Axes.Y) == 0;
|
||||
@@ -459,6 +464,8 @@ namespace osu.Framework.Graphics.Sprites
|
||||
if (charactersCache.IsValid)
|
||||
return;
|
||||
|
||||
IsTruncated = false;
|
||||
|
||||
charactersBacking.Clear();
|
||||
|
||||
// Todo: Re-enable this assert after autosize is split into two passes.
|
||||
@@ -476,6 +483,9 @@ namespace osu.Framework.Graphics.Sprites
|
||||
textBuilder.Reset();
|
||||
textBuilder.AddText(displayedText);
|
||||
textBounds = textBuilder.Bounds;
|
||||
|
||||
if (textBuilder is TruncatingTextBuilder truncatingTextBuilder)
|
||||
IsTruncated = truncatingTextBuilder.IsTruncated;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Framework.Graphics.Textures
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace osu.Framework.Graphics.Transforms
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace osu.Framework.Graphics.Transforms
|
||||
{
|
||||
public interface ITransformSequence
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Graphics.Containers;
|
||||
|
||||
namespace osu.Framework.Graphics.UserInterface
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
@@ -52,7 +53,7 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
if (boundItemSource != null)
|
||||
throw new InvalidOperationException($"Cannot manually set {nameof(Items)} when an {nameof(ItemSource)} is bound.");
|
||||
|
||||
Scheduler.AddOnce(setItems, value);
|
||||
setItems(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
return;
|
||||
|
||||
foreach (var entry in items)
|
||||
addDropdownItem(GenerateItemText(entry), entry);
|
||||
addDropdownItem(entry);
|
||||
|
||||
if (Current.Value == null || !itemMap.Keys.Contains(Current.Value, EqualityComparer<T>.Default))
|
||||
Current.Value = itemMap.Keys.FirstOrDefault();
|
||||
@@ -95,27 +96,20 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
/// Add a menu item directly while automatically generating a label.
|
||||
/// </summary>
|
||||
/// <param name="value">Value selected by the menu item.</param>
|
||||
public void AddDropdownItem(T value) => AddDropdownItem(GenerateItemText(value), value);
|
||||
|
||||
/// <summary>
|
||||
/// Add a menu item directly.
|
||||
/// </summary>
|
||||
/// <param name="text">Text to display on the menu item.</param>
|
||||
/// <param name="value">Value selected by the menu item.</param>
|
||||
protected void AddDropdownItem(LocalisableString text, T value)
|
||||
public void AddDropdownItem(T value)
|
||||
{
|
||||
if (boundItemSource != null)
|
||||
throw new InvalidOperationException($"Cannot manually add dropdown items when an {nameof(ItemSource)} is bound.");
|
||||
|
||||
addDropdownItem(text, value);
|
||||
addDropdownItem(value);
|
||||
}
|
||||
|
||||
private void addDropdownItem(LocalisableString text, T value)
|
||||
private void addDropdownItem(T value)
|
||||
{
|
||||
if (itemMap.ContainsKey(value))
|
||||
throw new ArgumentException($"The item {value} already exists in this {nameof(Dropdown<T>)}.");
|
||||
|
||||
var newItem = new DropdownMenuItem<T>(text, value, () =>
|
||||
var item = new DropdownMenuItem<T>(value, () =>
|
||||
{
|
||||
if (!Current.Disabled)
|
||||
Current.Value = value;
|
||||
@@ -123,8 +117,12 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
Menu.State = MenuState.Closed;
|
||||
});
|
||||
|
||||
Menu.Add(newItem);
|
||||
itemMap[value] = newItem;
|
||||
// inheritors expect that `virtual GenerateItemText` is only called when this dropdown is fully loaded.
|
||||
if (IsLoaded)
|
||||
item.Text.Value = GenerateItemText(value);
|
||||
|
||||
Menu.Add(item);
|
||||
itemMap[value] = item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -153,6 +151,12 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to generate the text to be shown for this <paramref name="item"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Can be overriden if custom behaviour is needed. Will only be called after this <see cref="Dropdown{T}"/> has fully loaded.
|
||||
/// </remarks>
|
||||
protected virtual LocalisableString GenerateItemText(T item)
|
||||
{
|
||||
switch (item)
|
||||
@@ -225,7 +229,7 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
Menu.State = MenuState.Closed;
|
||||
};
|
||||
|
||||
ItemSource.CollectionChanged += (_, _) => Scheduler.AddOnce(setItems, ItemSource);
|
||||
ItemSource.CollectionChanged += (_, _) => setItems(itemSource);
|
||||
}
|
||||
|
||||
private void preselectionConfirmed(int selectedIndex)
|
||||
@@ -264,6 +268,17 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
|
||||
foreach (var item in MenuItems)
|
||||
{
|
||||
Debug.Assert(string.IsNullOrEmpty(item.Text.Value.ToString()));
|
||||
item.Text.Value = GenerateItemText(item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@@ -277,7 +292,7 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
// null is not a valid value for Dictionary, so neither here
|
||||
if (args.NewValue == null && SelectedItem != null)
|
||||
{
|
||||
selectedItem = new DropdownMenuItem<T>(default, default);
|
||||
selectedItem = new DropdownMenuItem<T>(default(LocalisableString), default);
|
||||
}
|
||||
else if (SelectedItem == null || !EqualityComparer<T>.Default.Equals(SelectedItem.Value, args.NewValue))
|
||||
{
|
||||
|
||||
@@ -18,8 +18,8 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public DropdownMenuItem(LocalisableString text, T value, Action action)
|
||||
: base(text, action)
|
||||
public DropdownMenuItem(T value, Action action)
|
||||
: base(action)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,15 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
Text.Value = text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MenuItem"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The <see cref="Action"/> to perform when clicked.</param>
|
||||
protected MenuItem(Action action)
|
||||
{
|
||||
Action.Value = action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MenuItem"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
/// It typically is activated by another control and includes an arrow pointing to the location from which it emerged.
|
||||
/// (loosely paraphrasing: https://developer.apple.com/design/human-interface-guidelines/ios/views/popovers/)
|
||||
/// </summary>
|
||||
public abstract partial class Popover : FocusedOverlayContainer
|
||||
public abstract partial class Popover : OverlayContainer
|
||||
{
|
||||
protected override bool BlockPositionalInput => true;
|
||||
|
||||
@@ -27,6 +27,10 @@ namespace osu.Framework.Graphics.UserInterface
|
||||
|
||||
public override bool HandleNonPositionalInput => State.Value == Visibility.Visible;
|
||||
|
||||
public override bool RequestsFocus => State.Value == Visibility.Visible;
|
||||
|
||||
public override bool AcceptsFocus => State.Value == Visibility.Visible;
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Veldrid.Buffers;
|
||||
using Veldrid;
|
||||
using BufferUsage = Veldrid.BufferUsage;
|
||||
|
||||
namespace osu.Framework.Graphics.Veldrid.Batches
|
||||
{
|
||||
@@ -20,6 +19,6 @@ namespace osu.Framework.Graphics.Veldrid.Batches
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
protected override VeldridVertexBuffer<T> CreateVertexBuffer(VeldridRenderer renderer) => new VeldridLinearBuffer<T>(renderer, Size, type, BufferUsage.Dynamic);
|
||||
protected override VeldridVertexBuffer<T> CreateVertexBuffer(VeldridRenderer renderer) => new VeldridLinearBuffer<T>(renderer, Size, type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Veldrid.Buffers;
|
||||
using BufferUsage = Veldrid.BufferUsage;
|
||||
|
||||
namespace osu.Framework.Graphics.Veldrid.Batches
|
||||
{
|
||||
@@ -18,6 +17,6 @@ namespace osu.Framework.Graphics.Veldrid.Batches
|
||||
throw new OverflowException($"Attempted to initialise a {nameof(VeldridQuadBuffer<T>)} with more than {nameof(VeldridQuadBuffer<T>)}.{nameof(VeldridQuadBuffer<T>.MAX_QUADS)} quads ({VeldridQuadBuffer<T>.MAX_QUADS}).");
|
||||
}
|
||||
|
||||
protected override VeldridVertexBuffer<T> CreateVertexBuffer(VeldridRenderer renderer) => new VeldridQuadBuffer<T>(renderer, Size, BufferUsage.Dynamic);
|
||||
protected override VeldridVertexBuffer<T> CreateVertexBuffer(VeldridRenderer renderer) => new VeldridQuadBuffer<T>(renderer, Size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using Veldrid;
|
||||
|
||||
namespace osu.Framework.Graphics.Veldrid.Buffers.Staging
|
||||
{
|
||||
/// <summary>
|
||||
/// A staging buffer that stores data in managed memory and uses an intermediate driver buffer for copies.
|
||||
/// </summary>
|
||||
internal class DeferredStagingBuffer<T> : IStagingBuffer<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly VeldridRenderer renderer;
|
||||
private readonly IMemoryOwner<T> memoryOwner;
|
||||
|
||||
private readonly Memory<T> cpuBuffer;
|
||||
private readonly DeviceBuffer driverBuffer;
|
||||
|
||||
public DeferredStagingBuffer(VeldridRenderer renderer, uint count)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
Count = count;
|
||||
SizeInBytes = (uint)(Unsafe.SizeOf<T>() * count);
|
||||
|
||||
memoryOwner = SixLabors.ImageSharp.Configuration.Default.MemoryAllocator.Allocate<T>((int)Count, AllocationOptions.Clean);
|
||||
cpuBuffer = memoryOwner.Memory;
|
||||
|
||||
driverBuffer = renderer.Factory.CreateBuffer(new BufferDescription(SizeInBytes, BufferUsage.Staging));
|
||||
}
|
||||
|
||||
public uint SizeInBytes { get; }
|
||||
|
||||
public uint Count { get; }
|
||||
|
||||
public BufferUsage CopyTargetUsageFlags => 0;
|
||||
|
||||
public Span<T> Data => cpuBuffer.Span;
|
||||
|
||||
public void CopyTo(DeviceBuffer buffer, uint srcOffset, uint dstOffset, uint count)
|
||||
{
|
||||
renderer.Device.UpdateBuffer(
|
||||
driverBuffer,
|
||||
(uint)(srcOffset * Unsafe.SizeOf<T>()),
|
||||
ref Data[(int)srcOffset],
|
||||
(uint)(count * Unsafe.SizeOf<T>()));
|
||||
|
||||
renderer.BufferUpdateCommands.CopyBuffer(
|
||||
driverBuffer,
|
||||
(uint)(srcOffset * Unsafe.SizeOf<T>()),
|
||||
buffer,
|
||||
(uint)(dstOffset * Unsafe.SizeOf<T>()),
|
||||
(uint)(count * Unsafe.SizeOf<T>()));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
memoryOwner.Dispose();
|
||||
driverBuffer.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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 Veldrid;
|
||||
|
||||
namespace osu.Framework.Graphics.Veldrid.Buffers.Staging
|
||||
{
|
||||
internal interface IStagingBuffer<T> : IDisposable
|
||||
where T : unmanaged
|
||||
{
|
||||
/// <summary>
|
||||
/// The total size of this buffer in bytes.
|
||||
/// </summary>
|
||||
uint SizeInBytes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of elements in this buffer.
|
||||
/// </summary>
|
||||
uint Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Any extra flags required for the target of a <see cref="CopyTo"/> operation.
|
||||
/// </summary>
|
||||
BufferUsage CopyTargetUsageFlags { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The data contained in this buffer.
|
||||
/// </summary>
|
||||
Span<T> Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Copies data from this buffer into a <see cref="DeviceBuffer"/>.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The target buffer.</param>
|
||||
/// <param name="srcOffset">The offset into this buffer at which the copy should start.</param>
|
||||
/// <param name="dstOffset">The offset into <paramref name="buffer"/> at which the copy should start.</param>
|
||||
/// <param name="size">The number of elements to be copied.</param>
|
||||
void CopyTo(DeviceBuffer buffer, uint srcOffset, uint dstOffset, uint size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using Veldrid;
|
||||
|
||||
namespace osu.Framework.Graphics.Veldrid.Buffers.Staging
|
||||
{
|
||||
/// <summary>
|
||||
/// A staging buffer that uses a buffer in managed memory as its storage medium.
|
||||
/// </summary>
|
||||
internal class ManagedStagingBuffer<T> : IStagingBuffer<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly VeldridRenderer renderer;
|
||||
private readonly Memory<T> vertexMemory;
|
||||
private readonly IMemoryOwner<T> memoryOwner;
|
||||
|
||||
public ManagedStagingBuffer(VeldridRenderer renderer, uint count)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
Count = count;
|
||||
SizeInBytes = (uint)(Unsafe.SizeOf<T>() * count);
|
||||
|
||||
memoryOwner = SixLabors.ImageSharp.Configuration.Default.MemoryAllocator.Allocate<T>((int)Count, AllocationOptions.Clean);
|
||||
vertexMemory = memoryOwner.Memory;
|
||||
}
|
||||
|
||||
public uint SizeInBytes { get; }
|
||||
|
||||
public uint Count { get; }
|
||||
|
||||
public BufferUsage CopyTargetUsageFlags => BufferUsage.Dynamic;
|
||||
|
||||
public Span<T> Data => vertexMemory.Span;
|
||||
|
||||
public void CopyTo(DeviceBuffer buffer, uint srcOffset, uint dstOffset, uint count)
|
||||
{
|
||||
renderer.Device.UpdateBuffer(
|
||||
buffer,
|
||||
(uint)(dstOffset * Unsafe.SizeOf<T>()),
|
||||
ref Data[(int)srcOffset],
|
||||
(uint)(count * Unsafe.SizeOf<T>()));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
memoryOwner.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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.Runtime.CompilerServices;
|
||||
using Veldrid;
|
||||
|
||||
namespace osu.Framework.Graphics.Veldrid.Buffers.Staging
|
||||
{
|
||||
/// <summary>
|
||||
/// A staging buffer that uses a persistently-mapped device buffer as its storage medium.
|
||||
/// </summary>
|
||||
internal class PersistentStagingBuffer<T> : IStagingBuffer<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly VeldridRenderer renderer;
|
||||
private readonly DeviceBuffer stagingBuffer;
|
||||
private MappedResource? stagingBufferMap;
|
||||
|
||||
public PersistentStagingBuffer(VeldridRenderer renderer, uint count)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
|
||||
Count = count;
|
||||
SizeInBytes = (uint)(Unsafe.SizeOf<T>() * count);
|
||||
|
||||
stagingBuffer = renderer.Factory.CreateBuffer(new BufferDescription(SizeInBytes, BufferUsage.Staging));
|
||||
|
||||
Data.Clear();
|
||||
}
|
||||
|
||||
public uint SizeInBytes { get; }
|
||||
|
||||
public uint Count { get; }
|
||||
|
||||
public BufferUsage CopyTargetUsageFlags => 0;
|
||||
|
||||
public Span<T> Data
|
||||
{
|
||||
get
|
||||
{
|
||||
if (stagingBufferMap is not MappedResource map)
|
||||
stagingBufferMap = map = renderer.Device.Map(stagingBuffer, MapMode.ReadWrite);
|
||||
|
||||
unsafe
|
||||
{
|
||||
return new Span<T>(map.Data.ToPointer(), (int)Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyTo(DeviceBuffer buffer, uint srcOffset, uint dstOffset, uint size)
|
||||
{
|
||||
unmap();
|
||||
|
||||
renderer.BufferUpdateCommands.CopyBuffer(
|
||||
stagingBuffer,
|
||||
(uint)(srcOffset * Unsafe.SizeOf<T>()),
|
||||
buffer,
|
||||
(uint)(dstOffset * Unsafe.SizeOf<T>()),
|
||||
(uint)(size * Unsafe.SizeOf<T>()));
|
||||
}
|
||||
|
||||
private void unmap()
|
||||
{
|
||||
if (stagingBufferMap == null)
|
||||
return;
|
||||
|
||||
renderer.Device.Unmap(stagingBuffer);
|
||||
stagingBufferMap = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
unmap();
|
||||
stagingBuffer.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using Veldrid;
|
||||
using BufferUsage = Veldrid.BufferUsage;
|
||||
|
||||
namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
{
|
||||
@@ -17,8 +16,8 @@ namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
private readonly VeldridRenderer renderer;
|
||||
private readonly int amountVertices;
|
||||
|
||||
internal VeldridLinearBuffer(VeldridRenderer renderer, int amountVertices, PrimitiveTopology type, BufferUsage usage)
|
||||
: base(renderer, amountVertices, usage)
|
||||
internal VeldridLinearBuffer(VeldridRenderer renderer, int amountVertices, PrimitiveTopology type)
|
||||
: base(renderer, amountVertices)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
this.amountVertices = amountVertices;
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Diagnostics;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using Veldrid;
|
||||
using BufferUsage = Veldrid.BufferUsage;
|
||||
using PrimitiveTopology = Veldrid.PrimitiveTopology;
|
||||
|
||||
namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
@@ -24,8 +23,8 @@ namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
/// </summary>
|
||||
public const int MAX_QUADS = ushort.MaxValue / indices_per_quad;
|
||||
|
||||
internal VeldridQuadBuffer(VeldridRenderer renderer, int amountQuads, BufferUsage usage)
|
||||
: base(renderer, amountQuads * IRenderer.VERTICES_PER_QUAD, usage)
|
||||
internal VeldridQuadBuffer(VeldridRenderer renderer, int amountQuads)
|
||||
: base(renderer, amountQuads * IRenderer.VERTICES_PER_QUAD)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
amountIndices = amountQuads * indices_per_quad;
|
||||
|
||||
@@ -30,7 +30,8 @@ namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
|
||||
private readonly List<VeldridUniformBufferStorage<TData>> storages = new List<VeldridUniformBufferStorage<TData>>();
|
||||
private int currentStorageIndex;
|
||||
private TData? pendingData;
|
||||
private bool hasPendingData;
|
||||
private TData data;
|
||||
|
||||
private readonly VeldridRenderer renderer;
|
||||
|
||||
@@ -42,48 +43,50 @@ namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
|
||||
public TData Data
|
||||
{
|
||||
get => pendingData ?? currentStorage.Data;
|
||||
get => data;
|
||||
set
|
||||
{
|
||||
if (value.Equals(Data))
|
||||
return;
|
||||
data = value;
|
||||
hasPendingData = true;
|
||||
|
||||
// Flush the current draw call since the contents of the buffer will change.
|
||||
renderer.FlushCurrentBatch(FlushBatchSource.SetUniform);
|
||||
|
||||
pendingData = value;
|
||||
renderer.RegisterUniformBufferForReset(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ResourceSet GetResourceSet(ResourceLayout layout)
|
||||
{
|
||||
flushPendingData();
|
||||
flushData();
|
||||
return currentStorage.GetResourceSet(layout);
|
||||
}
|
||||
|
||||
private void flushPendingData()
|
||||
/// <summary>
|
||||
/// Writes the data of this UBO to the underlying storage.
|
||||
/// </summary>
|
||||
private void flushData()
|
||||
{
|
||||
if (pendingData is not TData pending)
|
||||
if (!hasPendingData)
|
||||
return;
|
||||
|
||||
pendingData = null;
|
||||
hasPendingData = false;
|
||||
|
||||
// Register this UBO to be reset in the next frame, but only once per frame.
|
||||
if (currentStorageIndex == 0)
|
||||
renderer.RegisterUniformBufferForReset(this);
|
||||
// If the contents of the UBO changed this frame...
|
||||
if (Data.Equals(currentStorage.Data))
|
||||
return;
|
||||
|
||||
// Advance the storage index to hold the new data.
|
||||
// Advance to a new target to hold the new data.
|
||||
// Note: It is illegal for a previously-drawn UBO to be updated with new data since UBOs are uploaded ahead of time in the frame.
|
||||
if (++currentStorageIndex == storages.Count)
|
||||
storages.Add(new VeldridUniformBufferStorage<TData>(renderer));
|
||||
else
|
||||
{
|
||||
// If the new storage previously existed, then it may already contain the data.
|
||||
if (pending.Equals(currentStorage.Data))
|
||||
// If we advanced to an existing target (from a previous frame), and since the target is always advanced before data is set,
|
||||
// the new target may already contain the same data from the previous frame.
|
||||
if (Data.Equals(currentStorage.Data))
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the data.
|
||||
currentStorage.Data = pending;
|
||||
currentStorage.Data = Data;
|
||||
|
||||
FrameStatistics.Increment(StatisticsCounterType.UniformUpl);
|
||||
}
|
||||
@@ -91,7 +94,8 @@ namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
public void ResetCounters()
|
||||
{
|
||||
currentStorageIndex = 0;
|
||||
pendingData = null;
|
||||
data = default;
|
||||
hasPendingData = false;
|
||||
}
|
||||
|
||||
~VeldridUniformBuffer()
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Veldrid.Buffers.Staging;
|
||||
using osu.Framework.Graphics.Veldrid.Vertices;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Statistics;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using Veldrid;
|
||||
using BufferUsage = Veldrid.BufferUsage;
|
||||
using PrimitiveTopology = Veldrid.PrimitiveTopology;
|
||||
@@ -23,18 +22,14 @@ namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
protected static readonly int STRIDE = VeldridVertexUtils<DepthWrappingVertex<T>>.STRIDE;
|
||||
|
||||
private readonly VeldridRenderer renderer;
|
||||
private readonly BufferUsage usage;
|
||||
|
||||
private Memory<DepthWrappingVertex<T>> vertexMemory;
|
||||
private IMemoryOwner<DepthWrappingVertex<T>>? memoryOwner;
|
||||
private NativeMemoryTracker.NativeMemoryLease? memoryLease;
|
||||
private IStagingBuffer<DepthWrappingVertex<T>>? stagingBuffer;
|
||||
private DeviceBuffer? gpuBuffer;
|
||||
|
||||
private DeviceBuffer? buffer;
|
||||
|
||||
protected VeldridVertexBuffer(VeldridRenderer renderer, int amountVertices, BufferUsage usage)
|
||||
protected VeldridVertexBuffer(VeldridRenderer renderer, int amountVertices)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
this.usage = usage;
|
||||
|
||||
Size = amountVertices;
|
||||
}
|
||||
@@ -47,7 +42,7 @@ namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
/// <returns>Whether the vertex changed.</returns>
|
||||
public bool SetVertex(int vertexIndex, T vertex)
|
||||
{
|
||||
ref var currentVertex = ref getMemory().Span[vertexIndex];
|
||||
ref var currentVertex = ref getMemory()[vertexIndex];
|
||||
|
||||
bool isNewVertex = !currentVertex.Vertex.Equals(vertex) || currentVertex.BackbufferDrawDepth != renderer.BackbufferDrawDepth;
|
||||
|
||||
@@ -69,8 +64,11 @@ namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
{
|
||||
ThreadSafety.EnsureDrawThread();
|
||||
|
||||
buffer = renderer.Factory.CreateBuffer(new BufferDescription((uint)(Size * STRIDE), BufferUsage.VertexBuffer | usage));
|
||||
memoryLease = NativeMemoryTracker.AddMemory(this, buffer.SizeInBytes);
|
||||
getMemory();
|
||||
Debug.Assert(stagingBuffer != null);
|
||||
|
||||
gpuBuffer = renderer.Factory.CreateBuffer(new BufferDescription((uint)(Size * STRIDE), BufferUsage.VertexBuffer | stagingBuffer.CopyTargetUsageFlags));
|
||||
memoryLease = NativeMemoryTracker.AddMemory(this, gpuBuffer.SizeInBytes);
|
||||
|
||||
// Ensure the device buffer is initialised to 0.
|
||||
Update();
|
||||
@@ -104,11 +102,11 @@ namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException(ToString(), "Can not bind disposed vertex buffers.");
|
||||
|
||||
if (buffer == null)
|
||||
if (gpuBuffer == null)
|
||||
Initialise();
|
||||
|
||||
Debug.Assert(buffer != null);
|
||||
renderer.BindVertexBuffer(buffer, VeldridVertexUtils<DepthWrappingVertex<T>>.Layout);
|
||||
Debug.Assert(gpuBuffer != null);
|
||||
renderer.BindVertexBuffer(gpuBuffer, VeldridVertexUtils<DepthWrappingVertex<T>>.Layout);
|
||||
}
|
||||
|
||||
public virtual void Unbind()
|
||||
@@ -143,30 +141,31 @@ namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
|
||||
public void UpdateRange(int startIndex, int endIndex)
|
||||
{
|
||||
if (buffer == null)
|
||||
if (gpuBuffer == null)
|
||||
Initialise();
|
||||
|
||||
Debug.Assert(stagingBuffer != null);
|
||||
Debug.Assert(gpuBuffer != null);
|
||||
|
||||
int countVertices = endIndex - startIndex;
|
||||
renderer.Device.UpdateBuffer(buffer, (uint)(startIndex * STRIDE), ref getMemory().Span[startIndex], (uint)(countVertices * STRIDE));
|
||||
stagingBuffer.CopyTo(gpuBuffer, (uint)startIndex, (uint)startIndex, (uint)countVertices);
|
||||
|
||||
FrameStatistics.Add(StatisticsCounterType.VerticesUpl, countVertices);
|
||||
}
|
||||
|
||||
private ref Memory<DepthWrappingVertex<T>> getMemory()
|
||||
private Span<DepthWrappingVertex<T>> getMemory()
|
||||
{
|
||||
ThreadSafety.EnsureDrawThread();
|
||||
|
||||
if (!InUse)
|
||||
{
|
||||
memoryOwner = SixLabors.ImageSharp.Configuration.Default.MemoryAllocator.Allocate<DepthWrappingVertex<T>>(Size, AllocationOptions.Clean);
|
||||
vertexMemory = memoryOwner.Memory;
|
||||
|
||||
stagingBuffer = renderer.CreateStagingBuffer<DepthWrappingVertex<T>>((uint)Size);
|
||||
renderer.RegisterVertexBufferUse(this);
|
||||
}
|
||||
|
||||
LastUseResetId = renderer.ResetId;
|
||||
|
||||
return ref vertexMemory;
|
||||
return stagingBuffer!.Data;
|
||||
}
|
||||
|
||||
public ulong LastUseResetId { get; private set; }
|
||||
@@ -178,12 +177,11 @@ namespace osu.Framework.Graphics.Veldrid.Buffers
|
||||
memoryLease?.Dispose();
|
||||
memoryLease = null;
|
||||
|
||||
buffer?.Dispose();
|
||||
buffer = null;
|
||||
stagingBuffer?.Dispose();
|
||||
stagingBuffer = null;
|
||||
|
||||
memoryOwner?.Dispose();
|
||||
memoryOwner = null;
|
||||
vertexMemory = Memory<DepthWrappingVertex<T>>.Empty;
|
||||
gpuBuffer?.Dispose();
|
||||
gpuBuffer = null;
|
||||
|
||||
LastUseResetId = 0;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
private readonly string name;
|
||||
private readonly VeldridShaderPart[] parts;
|
||||
private readonly IUniformBuffer<GlobalUniformData> globalUniformBuffer;
|
||||
private readonly ShaderCompilationStore compilationStore;
|
||||
private readonly VeldridRenderer renderer;
|
||||
|
||||
public Shader[]? Shaders;
|
||||
@@ -42,11 +43,12 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
private readonly Dictionary<string, VeldridUniformLayout> uniformLayouts = new Dictionary<string, VeldridUniformLayout>();
|
||||
private readonly List<VeldridUniformLayout> textureLayouts = new List<VeldridUniformLayout>();
|
||||
|
||||
public VeldridShader(VeldridRenderer renderer, string name, VeldridShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer)
|
||||
public VeldridShader(VeldridRenderer renderer, string name, VeldridShaderPart[] parts, IUniformBuffer<GlobalUniformData> globalUniformBuffer, ShaderCompilationStore compilationStore)
|
||||
{
|
||||
this.name = name;
|
||||
this.parts = parts;
|
||||
this.globalUniformBuffer = globalUniformBuffer;
|
||||
this.compilationStore = compilationStore;
|
||||
this.renderer = renderer;
|
||||
|
||||
// This part of the compilation is quite CPU expensive.
|
||||
@@ -92,7 +94,7 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
public void BindUniformBlock(string blockName, IUniformBuffer buffer)
|
||||
{
|
||||
if (buffer is not IVeldridUniformBuffer veldridBuffer)
|
||||
throw new InvalidOperationException();
|
||||
throw new ArgumentException($"Buffer must be an {nameof(IVeldridUniformBuffer)}.");
|
||||
|
||||
if (isDisposed)
|
||||
throw new ObjectDisposedException(ToString(), "Can not retrieve uniforms from a disposed shader.");
|
||||
@@ -108,15 +110,19 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
|
||||
private void compile()
|
||||
{
|
||||
Logger.Log($"🖍️ Compiling shader {name}...");
|
||||
|
||||
Debug.Assert(parts.Length == 2);
|
||||
|
||||
VeldridShaderPart vertex = parts.Single(p => p.Type == ShaderPartType.Vertex);
|
||||
VeldridShaderPart fragment = parts.Single(p => p.Type == ShaderPartType.Fragment);
|
||||
|
||||
// some attributes from the vertex output may not be used by the fragment shader, but that could break some renderers (e.g. D3D11).
|
||||
// therefore include any unused vertex output to a fragment shader as fragment input & output.
|
||||
fragment = fragment.WithPassthroughInput(vertex.Outputs);
|
||||
|
||||
try
|
||||
{
|
||||
bool cached = true;
|
||||
|
||||
vertexShaderDescription = new ShaderDescription(
|
||||
ShaderStages.Vertex,
|
||||
Array.Empty<byte>(),
|
||||
@@ -128,19 +134,21 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
renderer.Factory.BackendType == GraphicsBackend.Metal ? "main0" : "main");
|
||||
|
||||
// GLSL cross compile is always performed for reflection, even though the cross-compiled shaders aren't used under other backends.
|
||||
VertexFragmentCompilationResult crossCompileResult = SpirvCompilation.CompileVertexFragment(
|
||||
Encoding.UTF8.GetBytes(vertex.GetRawText()),
|
||||
Encoding.UTF8.GetBytes(fragment.GetRawText()),
|
||||
VertexFragmentShaderCompilation compilation = compilationStore.CompileVertexFragment(
|
||||
vertex.GetRawText(),
|
||||
fragment.GetRawText(),
|
||||
RuntimeInfo.IsMobile ? CrossCompileTarget.ESSL : CrossCompileTarget.GLSL);
|
||||
|
||||
cached &= compilation.WasCached;
|
||||
|
||||
if (renderer.SurfaceType == GraphicsSurfaceType.Vulkan)
|
||||
{
|
||||
vertexShaderDescription.ShaderBytes = SpirvCompilation.CompileGlslToSpirv(vertex.GetRawText(), null, ShaderStages.Vertex, GlslCompileOptions.Default).SpirvBytes;
|
||||
fragmentShaderDescription.ShaderBytes = SpirvCompilation.CompileGlslToSpirv(fragment.GetRawText(), null, ShaderStages.Fragment, GlslCompileOptions.Default).SpirvBytes;
|
||||
vertexShaderDescription.ShaderBytes = compilation.VertexBytes;
|
||||
fragmentShaderDescription.ShaderBytes = compilation.FragmentBytes;
|
||||
}
|
||||
else
|
||||
{
|
||||
VertexFragmentCompilationResult platformCrossCompileResult = crossCompileResult;
|
||||
VertexFragmentShaderCompilation platformCompilation = compilation;
|
||||
|
||||
// If we don't have an OpenGL surface, we need to cross-compile once more for the correct platform.
|
||||
if (renderer.SurfaceType != GraphicsSurfaceType.OpenGL)
|
||||
@@ -152,19 +160,21 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
_ => throw new InvalidOperationException($"Unsupported surface type: {renderer.SurfaceType}.")
|
||||
};
|
||||
|
||||
platformCrossCompileResult = SpirvCompilation.CompileVertexFragment(
|
||||
Encoding.UTF8.GetBytes(vertex.GetRawText()),
|
||||
Encoding.UTF8.GetBytes(fragment.GetRawText()),
|
||||
platformCompilation = compilationStore.CompileVertexFragment(
|
||||
vertex.GetRawText(),
|
||||
fragment.GetRawText(),
|
||||
target);
|
||||
|
||||
cached &= platformCompilation.WasCached;
|
||||
}
|
||||
|
||||
vertexShaderDescription.ShaderBytes = Encoding.UTF8.GetBytes(platformCrossCompileResult.VertexShader);
|
||||
fragmentShaderDescription.ShaderBytes = Encoding.UTF8.GetBytes(platformCrossCompileResult.FragmentShader);
|
||||
vertexShaderDescription.ShaderBytes = Encoding.UTF8.GetBytes(platformCompilation.VertexText);
|
||||
fragmentShaderDescription.ShaderBytes = Encoding.UTF8.GetBytes(platformCompilation.FragmentText);
|
||||
}
|
||||
|
||||
for (int set = 0; set < crossCompileResult.Reflection.ResourceLayouts.Length; set++)
|
||||
for (int set = 0; set < compilation.Reflection.ResourceLayouts.Length; set++)
|
||||
{
|
||||
ResourceLayoutDescription layout = crossCompileResult.Reflection.ResourceLayouts[set];
|
||||
ResourceLayoutDescription layout = compilation.Reflection.ResourceLayouts[set];
|
||||
|
||||
if (layout.Elements.Length == 0)
|
||||
continue;
|
||||
@@ -201,7 +211,9 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Log($"🖍️ Shader {name} compiled!");
|
||||
Logger.Log(cached
|
||||
? $"🖍️ Shader {name} loaded from cache!"
|
||||
: $"🖍️ Shader {name} compiled!");
|
||||
}
|
||||
catch (SpirvCompilationException e)
|
||||
{
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
@@ -13,15 +16,21 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
{
|
||||
internal class VeldridShaderPart : IShaderPart
|
||||
{
|
||||
public static readonly Regex SHADER_INPUT_PATTERN = new Regex(@"^\s*layout\s*\(\s*location\s*=\s*(-?\d+)\s*\)\s*(in\s+(?:(?:lowp|mediump|highp)\s+)?\w+\s+(\w+)\s*;)", RegexOptions.Multiline);
|
||||
private static readonly Regex shader_input_pattern = new Regex(@"^\s*layout\s*\(\s*location\s*=\s*(-?\d+)\s*\)\s*in\s+((?:(?:lowp|mediump|highp)\s+)?\w+)\s+(\w+)\s*;", RegexOptions.Multiline);
|
||||
private static readonly Regex shader_output_pattern = new Regex(@"^\s*layout\s*\(\s*location\s*=\s*(-?\d+)\s*\)\s*out\s+((?:(?:lowp|mediump|highp)\s+)?\w+)\s+(\w+)\s*;", RegexOptions.Multiline);
|
||||
private static readonly Regex uniform_pattern = new Regex(@"^(\s*layout\s*\(.*)set\s*=\s*(-?\d)(.*\)\s*uniform)", RegexOptions.Multiline);
|
||||
private static readonly Regex include_pattern = new Regex(@"^\s*#\s*include\s+[""<](.*)["">]");
|
||||
|
||||
public readonly ShaderPartType Type;
|
||||
|
||||
private readonly List<string> shaderCodes = new List<string>();
|
||||
private string header = string.Empty;
|
||||
|
||||
private readonly string code;
|
||||
private readonly IShaderStore store;
|
||||
|
||||
public readonly List<VeldridShaderAttribute> Inputs = new List<VeldridShaderAttribute>();
|
||||
public readonly List<VeldridShaderAttribute> Outputs = new List<VeldridShaderAttribute>();
|
||||
|
||||
public VeldridShaderPart(byte[]? data, ShaderPartType type, IShaderStore store)
|
||||
{
|
||||
this.store = store;
|
||||
@@ -29,29 +38,30 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
Type = type;
|
||||
|
||||
// Load the shader files.
|
||||
shaderCodes.Add(loadFile(data, true));
|
||||
code = loadFile(data, true);
|
||||
|
||||
int lastInputIndex = 0;
|
||||
|
||||
// Parse all shader inputs to find the last input index.
|
||||
for (int i = 0; i < shaderCodes.Count; i++)
|
||||
{
|
||||
foreach (Match m in SHADER_INPUT_PATTERN.Matches(shaderCodes[i]))
|
||||
lastInputIndex = Math.Max(lastInputIndex, int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture));
|
||||
}
|
||||
foreach (Match m in shader_input_pattern.Matches(code))
|
||||
lastInputIndex = Math.Max(lastInputIndex, int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture));
|
||||
|
||||
// Update the location of the m_BackbufferDrawDepth input to be placed after all other inputs.
|
||||
for (int i = 0; i < shaderCodes.Count; i++)
|
||||
shaderCodes[i] = shaderCodes[i].Replace("layout(location = -1)", $"layout(location = {lastInputIndex + 1})");
|
||||
code = code.Replace("layout(location = -1)", $"layout(location = {lastInputIndex + 1})");
|
||||
|
||||
// Increment the binding set of all uniform blocks.
|
||||
// After this transformation, the g_GlobalUniforms block is placed in set 0 and all other user blocks begin from 1.
|
||||
// The difference in implementation here (compared to above) is intentional, as uniform blocks must be consistent between the shader stages, so they can't be easily appended.
|
||||
for (int i = 0; i < shaderCodes.Count; i++)
|
||||
{
|
||||
shaderCodes[i] = uniform_pattern.Replace(shaderCodes[i],
|
||||
match => $"{match.Groups[1].Value}set = {int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture) + 1}{match.Groups[3].Value}");
|
||||
}
|
||||
code = uniform_pattern.Replace(code, match => $"{match.Groups[1].Value}set = {int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture) + 1}{match.Groups[3].Value}");
|
||||
}
|
||||
|
||||
private VeldridShaderPart(string code, string header, ShaderPartType type, IShaderStore store)
|
||||
{
|
||||
this.code = code;
|
||||
this.header = header;
|
||||
this.store = store;
|
||||
|
||||
Type = type;
|
||||
}
|
||||
|
||||
private string loadFile(byte[]? bytes, bool mainFile)
|
||||
@@ -62,7 +72,7 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
using (MemoryStream ms = new MemoryStream(bytes))
|
||||
using (StreamReader sr = new StreamReader(ms))
|
||||
{
|
||||
string code = string.Empty;
|
||||
string result = string.Empty;
|
||||
|
||||
while (sr.Peek() != -1)
|
||||
{
|
||||
@@ -70,19 +80,19 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
code += line + '\n';
|
||||
result += line + '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("#version", StringComparison.Ordinal)) // the version directive has to appear before anything else in the shader
|
||||
{
|
||||
shaderCodes.Insert(0, line + '\n');
|
||||
header = line + '\n' + header;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("#extension", StringComparison.Ordinal))
|
||||
{
|
||||
shaderCodes.Add(line + '\n');
|
||||
header += line + '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -92,42 +102,87 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
{
|
||||
string includeName = includeMatch.Groups[1].Value.Trim();
|
||||
|
||||
//#if DEBUG
|
||||
// byte[] rawData = null;
|
||||
// if (File.Exists(includeName))
|
||||
// rawData = File.ReadAllBytes(includeName);
|
||||
//#endif
|
||||
code += loadFile(store.GetRawData(includeName), false) + '\n';
|
||||
result += loadFile(store.GetRawData(includeName), false) + '\n';
|
||||
}
|
||||
else
|
||||
code += line + '\n';
|
||||
result += line + '\n';
|
||||
}
|
||||
|
||||
if (mainFile)
|
||||
{
|
||||
string internalIncludes = loadFile(store.GetRawData("Internal/sh_Compatibility.h"), false) + "\n";
|
||||
internalIncludes += loadFile(store.GetRawData("Internal/sh_GlobalUniforms.h"), false) + "\n";
|
||||
code = internalIncludes + code;
|
||||
result = internalIncludes + result;
|
||||
|
||||
if (Type == ShaderPartType.Vertex)
|
||||
Inputs.AddRange(shader_input_pattern.Matches(result).Select(m => new VeldridShaderAttribute(int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), m.Groups[2].Value)).ToList());
|
||||
Outputs.AddRange(shader_output_pattern.Matches(result).Select(m => new VeldridShaderAttribute(int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), m.Groups[2].Value)).ToList());
|
||||
|
||||
string outputCode = loadFile(store.GetRawData($"Internal/sh_{Type}_Output.h"), false);
|
||||
|
||||
if (!string.IsNullOrEmpty(outputCode))
|
||||
{
|
||||
string backbufferCode = loadFile(store.GetRawData("Internal/sh_Vertex_Output.h"), false);
|
||||
const string real_main_name = "__internal_real_main";
|
||||
|
||||
if (!string.IsNullOrEmpty(backbufferCode))
|
||||
{
|
||||
string realMainName = "real_main_" + Guid.NewGuid().ToString("N");
|
||||
|
||||
backbufferCode = backbufferCode.Replace("{{ real_main }}", realMainName);
|
||||
code = Regex.Replace(code, @"void main\((.*)\)", $"void {realMainName}()") + backbufferCode + '\n';
|
||||
}
|
||||
outputCode = outputCode.Replace("{{ real_main }}", real_main_name);
|
||||
result = Regex.Replace(result, @"void main\((.*)\)", $"void {real_main_name}()") + outputCode + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
return code;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetRawText() => string.Join('\n', shaderCodes);
|
||||
/// <summary>
|
||||
/// Creates a <see cref="VeldridShaderPart"/> based off this shader with a list of attributes passed through as input & output.
|
||||
/// Attributes from the list that are already defined in this shader will be ignored.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// In D3D11, unused fragment inputs cull their corresponding vertex output, which affects the vertex input/output structure leading to seemingly undefined behaviour.
|
||||
/// To prevent this from happening, make sure all unused vertex output are used and sent in the fragment output so that D3D11 doesn't omit them.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This creates a new <see cref="VeldridShaderPart"/> rather than altering this existing instance since this is cached at a <see cref="IShaderStore"/> level and should remain immutable.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="attributes">The list of attributes to include in the shader as input & output.</param>
|
||||
public VeldridShaderPart WithPassthroughInput(IReadOnlyList<VeldridShaderAttribute> attributes)
|
||||
{
|
||||
string result = code;
|
||||
|
||||
int outputLayoutIndex = Outputs.Max(m => m.Location) + 1;
|
||||
|
||||
var attributesLayout = new StringBuilder();
|
||||
var attributesAssignment = new StringBuilder();
|
||||
var outputAttributes = new List<VeldridShaderAttribute>();
|
||||
|
||||
foreach (VeldridShaderAttribute attribute in attributes)
|
||||
{
|
||||
if (Inputs.Any(i => attribute.Location == i.Location))
|
||||
continue;
|
||||
|
||||
string name = $"unused_input_{Guid.NewGuid():N}";
|
||||
|
||||
attributesLayout.AppendLine($"layout (location = {attribute.Location}) in {attribute.Type} {name};");
|
||||
attributesLayout.AppendLine($"layout (location = {outputLayoutIndex}) out {attribute.Type} __unused_o_{name};");
|
||||
attributesAssignment.Append($"__unused_o_{name} = {name};\n ");
|
||||
|
||||
outputAttributes.Add(attribute with { Location = outputLayoutIndex++ });
|
||||
}
|
||||
|
||||
// we're only using this for fragment shader so let's just assert that.
|
||||
Debug.Assert(Type == ShaderPartType.Fragment);
|
||||
|
||||
result = result.Replace("{{ fragment_output_layout }}", attributesLayout.ToString().Trim());
|
||||
result = result.Replace("{{ fragment_output_assignment }}", attributesAssignment.ToString().Trim());
|
||||
|
||||
var part = new VeldridShaderPart(result, header, Type, store);
|
||||
part.Inputs.AddRange(Inputs.Concat(attributes).DistinctBy(a => a.Location));
|
||||
part.Outputs.AddRange(Outputs.Concat(outputAttributes));
|
||||
return part;
|
||||
}
|
||||
|
||||
public string GetRawText() => header + '\n' + code;
|
||||
|
||||
#region IDisposable Support
|
||||
|
||||
@@ -137,4 +192,6 @@ namespace osu.Framework.Graphics.Veldrid.Shaders
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record struct VeldridShaderAttribute(int Location, string Type);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Extensions.ImageExtensions;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Graphics.Veldrid.Buffers;
|
||||
using osu.Framework.Platform;
|
||||
using osuTK.Graphics;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Veldrid;
|
||||
using PixelFormat = Veldrid.PixelFormat;
|
||||
@@ -49,6 +51,8 @@ namespace osu.Framework.Graphics.Veldrid.Textures
|
||||
|
||||
private readonly bool manualMipmaps;
|
||||
|
||||
private readonly List<RectangleI> uploadedRegions = new List<RectangleI>();
|
||||
|
||||
private readonly SamplerFilter filteringMode;
|
||||
private readonly Color4 initialisationColour;
|
||||
|
||||
@@ -210,7 +214,7 @@ namespace osu.Framework.Graphics.Veldrid.Textures
|
||||
// We should never run raw Veldrid calls on another thread than the draw thread due to race conditions.
|
||||
ThreadSafety.EnsureDrawThread();
|
||||
|
||||
List<RectangleI> uploadedRegions = new List<RectangleI>();
|
||||
uploadedRegions.Clear();
|
||||
|
||||
while (tryGetNextUpload(out ITextureUpload? upload))
|
||||
{
|
||||
@@ -427,6 +431,9 @@ namespace osu.Framework.Graphics.Veldrid.Textures
|
||||
/// <summary>
|
||||
/// The maximum number of mip levels provided by an <see cref="ITextureUpload"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This excludes automatic generation of mipmaps via the graphics backend.
|
||||
/// </remarks>
|
||||
private int maximumUploadedLod;
|
||||
|
||||
private Sampler createSampler()
|
||||
@@ -452,7 +459,6 @@ namespace osu.Framework.Graphics.Veldrid.Textures
|
||||
{
|
||||
Texture? texture = resources?.Texture;
|
||||
Sampler? sampler = resources?.Sampler;
|
||||
bool newTexture = false;
|
||||
|
||||
if (texture == null || texture.Width != Width || texture.Height != Height)
|
||||
{
|
||||
@@ -460,49 +466,56 @@ namespace osu.Framework.Graphics.Veldrid.Textures
|
||||
|
||||
var textureDescription = TextureDescription.Texture2D((uint)Width, (uint)Height, (uint)CalculateMipmapLevels(Width, Height), 1, PixelFormat.R8_G8_B8_A8_UNorm, Usages);
|
||||
texture = Renderer.Factory.CreateTexture(ref textureDescription);
|
||||
newTexture = true;
|
||||
|
||||
// todo: we may want to look into not having to allocate chunks of zero byte region for initialising textures
|
||||
// similar to how OpenGL allows calling glTexImage2D with null data pointer.
|
||||
initialiseLevel(texture, 0, Width, Height);
|
||||
|
||||
maximumUploadedLod = 0;
|
||||
}
|
||||
|
||||
int lastMaximumUploadedLod = maximumUploadedLod;
|
||||
|
||||
if (!upload.Data.IsEmpty && upload.Level > maximumUploadedLod)
|
||||
maximumUploadedLod = upload.Level;
|
||||
|
||||
if (sampler == null || maximumUploadedLod > lastMaximumUploadedLod)
|
||||
sampler = createSampler();
|
||||
|
||||
resources = new VeldridTextureResources(texture, sampler);
|
||||
|
||||
if (newTexture)
|
||||
{
|
||||
for (int i = 0; i < texture.MipLevels; i++)
|
||||
initialiseLevel(i, Width >> i, Height >> i);
|
||||
}
|
||||
|
||||
if (!upload.Data.IsEmpty)
|
||||
{
|
||||
// ensure all mip levels up to the target level are initialised.
|
||||
if (upload.Level > maximumUploadedLod)
|
||||
{
|
||||
for (int i = maximumUploadedLod + 1; i <= upload.Level; i++)
|
||||
initialiseLevel(texture, i, Width >> i, Height >> i);
|
||||
|
||||
maximumUploadedLod = upload.Level;
|
||||
}
|
||||
|
||||
Renderer.UpdateTexture(texture, upload.Bounds.X >> upload.Level, upload.Bounds.Y >> upload.Level, upload.Bounds.Width >> upload.Level, upload.Bounds.Height >> upload.Level,
|
||||
upload.Level, upload.Data);
|
||||
}
|
||||
|
||||
if (sampler == null || maximumUploadedLod > lastMaximumUploadedLod)
|
||||
{
|
||||
sampler?.Dispose();
|
||||
sampler = createSampler();
|
||||
}
|
||||
|
||||
resources = new VeldridTextureResources(texture, sampler);
|
||||
}
|
||||
|
||||
private unsafe void initialiseLevel(int level, int width, int height)
|
||||
private unsafe void initialiseLevel(Texture texture, int level, int width, int height)
|
||||
{
|
||||
updateMemoryUsage(level, (long)width * height * sizeof(Rgba32));
|
||||
using (var image = createBackingImage(width, height))
|
||||
using (var pixels = image.CreateReadOnlyPixelSpan())
|
||||
{
|
||||
updateMemoryUsage(level, (long)width * height * sizeof(Rgba32));
|
||||
Renderer.UpdateTexture(texture, 0, 0, width, height, level, pixels.Span);
|
||||
}
|
||||
}
|
||||
|
||||
using var commands = Renderer.Factory.CreateCommandList();
|
||||
using var frameBuffer = new VeldridFrameBuffer(Renderer, this, level);
|
||||
|
||||
commands.Begin();
|
||||
|
||||
// Initialize texture to solid color
|
||||
commands.SetFramebuffer(frameBuffer.Framebuffer);
|
||||
commands.ClearColorTarget(0, new RgbaFloat(initialisationColour.R, initialisationColour.G, initialisationColour.B, initialisationColour.A));
|
||||
|
||||
commands.End();
|
||||
Renderer.Device.SubmitCommands(commands);
|
||||
private Image<Rgba32> createBackingImage(int width, int height)
|
||||
{
|
||||
// it is faster to initialise without a background specification if transparent black is all that's required.
|
||||
return initialisationColour == default
|
||||
? new Image<Rgba32>(width, height)
|
||||
: new Image<Rgba32>(width, height, new Rgba32(new Vector4(initialisationColour.R, initialisationColour.G, initialisationColour.B, initialisationColour.A)));
|
||||
}
|
||||
|
||||
// todo: should this be limited to MAX_MIPMAP_LEVELS or was that constant supposed to be for automatic mipmap generation only?
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user