Merge branch 'master' into input-cache-sgen

This commit is contained in:
Dean Herbert
2023-06-19 14:42:29 +09:00
158 changed files with 2265 additions and 700 deletions

View File

@@ -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
View 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
View 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
View File

@@ -0,0 +1,7 @@
{
"sdk": {
"version": "6.0.100",
"rollForward": "latestFeature"
}
}

View File

@@ -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>

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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;
}
}

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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;
}
}
}
}

View File

@@ -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>

View File

@@ -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.

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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]

View File

@@ -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
{
}
}
}

View File

@@ -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>

View File

@@ -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()
{

View 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;
}
}

View File

@@ -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));

View File

@@ -463,6 +463,5 @@ namespace osu.Framework.Tests.Dependencies.Reflection
{
}
}
#nullable disable
}
}

View File

@@ -342,6 +342,5 @@ namespace osu.Framework.Tests.Dependencies.Reflection
[Resolved]
public Bindable<int> Obj { get; private set; } = null!;
}
#nullable disable
}
}

View File

@@ -467,6 +467,5 @@ namespace osu.Framework.Tests.Dependencies.SourceGeneration
{
}
}
#nullable disable
}
}

View File

@@ -333,6 +333,5 @@ namespace osu.Framework.Tests.Dependencies.SourceGeneration
[Resolved]
public Bindable<int> Obj { get; private set; } = null!;
}
#nullable disable
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)
{
}

View File

@@ -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)
{
}

View File

@@ -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() =>

View File

@@ -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()

View File

@@ -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();
}

View File

@@ -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()
{

View File

@@ -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()
{

View 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;
}
}
}

View File

@@ -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)}";
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -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);
});

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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);

View File

@@ -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);
}
}

View File

@@ -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);
};
}
}

View File

@@ -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

View File

@@ -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().

View File

@@ -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

View File

@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
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")

View File

@@ -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:

View File

@@ -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>

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)

View File

@@ -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;
}
}
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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;
}
}
}
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -42,8 +42,6 @@ namespace osu.Framework.Graphics.OpenGL.Buffers
if (value.Equals(data))
return;
renderer.FlushCurrentBatch(FlushBatchSource.SetUniform);
setData(ref value);
}
}

View File

@@ -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)
{

View File

@@ -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

View File

@@ -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';
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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)));
}
}
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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;

View File

@@ -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>

View File

@@ -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)
{

View 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!;
}
}

View 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.");
}
}
}
}

View File

@@ -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

View File

@@ -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!;
}
}

View File

@@ -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>

View File

@@ -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
{

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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))
{

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View 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;
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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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()

View File

@@ -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;
}

View File

@@ -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)
{

View File

@@ -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);
}

View File

@@ -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