From 8bfba2bfc6d29b456e8ad9ce74192b70b3276113 Mon Sep 17 00:00:00 2001 From: Dylan Perks Date: Sat, 31 Aug 2024 23:15:10 +0100 Subject: [PATCH] WIP Windowing 3.0 for real this time --- sources/Core/Analyzers/Helpers.cs | 41 + .../Analyzers/HluSourceGenerator.Hosts.cs | 1030 ----------------- .../HluSourceGenerator.Registries.cs | 842 -------------- sources/Core/Analyzers/HluSourceGenerator.cs | 122 -- .../Analyzers/MockStaticAbstractAttribute.cs | 9 + .../Analyzers/Silk.NET.Core.Analyzers.csproj | 5 + .../StaticAbstractInterfaceMocking.cs | 583 ++++++++++ .../Core/Abstractions/AndroidPlatformInfo.cs | 9 +- .../Core/Core/Abstractions/BreakneckLock.cs | 195 ++++ .../Core/Abstractions/BreakneckRequest`1.cs | 64 + .../Core/Abstractions/BreakneckRequest`2.cs | 92 ++ .../Core/Abstractions/CocoaPlatformInfo.cs | 6 +- .../Core/Core/Abstractions/EGLPlatformInfo.cs | 9 +- .../Core/Core/Abstractions/HluExtensions.cs | 69 -- .../HluHostedComponentAttribute.cs | 60 - .../HluMissingComponentException.cs | 30 - .../HluRegisteredComponentAttribute`2.cs | 30 - .../Core/Abstractions/IHluComponentHost.cs | 44 - .../Abstractions/IHluComponentRegistry.cs | 92 -- .../Core/Abstractions/IHluConfiguration.cs | 35 - .../Core/Core/Abstractions/IPlatformInfo.cs | 15 - .../Core/Abstractions/UIKitPlatformInfo.cs | 8 +- .../Core/Abstractions/VivantePlatformInfo.cs | 9 +- .../Core/Abstractions/WaylandPlatformInfo.cs | 9 +- .../Core/Abstractions/Win32PlatformInfo.cs | 10 +- .../Core/Abstractions/WinRTPlatformInfo.cs | 6 +- .../Core/Core/Abstractions/X11PlatformInfo.cs | 6 +- sources/Core/Core/DSL/Default.cs | 13 +- sources/SDL/Extensions.cs | 16 + .../Windowing/Common/Components/ISurface.cs | 11 +- .../Common/Configuration/IConfigureHost.cs | 57 + .../Common/Configuration/IGLConfiguration.cs | 6 +- .../Configuration/ThreadConfiguration.cs | 59 + .../Windowing/Common/Hosting/HostEventKind.cs | 20 + .../Windowing/Common/Hosting/HostStatus.cs | 25 + .../Windowing/Common/Hosting/IHostActor.cs | 26 + .../Windowing/Common/Hosting/ISurfaceHost.cs | 234 ++++ .../Hosting/MultiThreadedSurfaceHost`1.cs | 410 +++++++ sources/Windowing/Common/Hosting/README.md | 3 + .../Windowing/Common/Hosting/SurfaceHandle.cs | 36 + .../Common/Hosting/SurfaceProperty.cs | 73 ++ .../Common/Hosting/SurfacePropertyName.cs | 29 + .../Common/Hosting/SurfacePropertyValue.cs | 36 + sources/Windowing/Common/ISurfaceActor.cs | 31 + .../Miscellaneous/SurfaceEventHandler.cs | 10 + .../Miscellaneous/SurfaceStateEventArgs.cs | 11 + .../Miscellaneous/SurfaceStateEventArgs`1.cs | 11 + .../Common/Miscellaneous/TimedEventArgs.cs | 10 + .../Common/Silk.NET.Windowing.Common.csproj | 4 + ...lk.NET.Windowing.Common.csproj.DotSettings | 3 +- sources/Windowing/Common/Surface.cs | 229 +++- sources/Windowing/Common/SurfaceBuilder`1.cs | 5 +- sources/Windowing/Common/SurfaceBuilder`2.cs | 5 +- sources/Windowing/Common/Surface`1.cs | 73 ++ .../Windowing/ReferenceImplementation.cs | 168 ++- .../Windowing/Windowing/SdlNativeWindow.cs | 528 --------- sources/Windowing/Windowing/SdlSurface.cs | 281 ----- .../Windowing/Silk.NET.Windowing.csproj | 1 + sources/Windowing/Windowing/SurfaceBuilder.cs | 5 +- tests/Core/Core/BreakneckRequestTests.cs | 121 ++ 60 files changed, 2693 insertions(+), 3287 deletions(-) create mode 100644 sources/Core/Analyzers/Helpers.cs delete mode 100644 sources/Core/Analyzers/HluSourceGenerator.Hosts.cs delete mode 100644 sources/Core/Analyzers/HluSourceGenerator.Registries.cs delete mode 100644 sources/Core/Analyzers/HluSourceGenerator.cs create mode 100644 sources/Core/Analyzers/MockStaticAbstractAttribute.cs create mode 100644 sources/Core/Analyzers/StaticAbstractInterfaceMocking.cs create mode 100644 sources/Core/Core/Abstractions/BreakneckLock.cs create mode 100644 sources/Core/Core/Abstractions/BreakneckRequest`1.cs create mode 100644 sources/Core/Core/Abstractions/BreakneckRequest`2.cs delete mode 100644 sources/Core/Core/Abstractions/HluExtensions.cs delete mode 100644 sources/Core/Core/Abstractions/HluHostedComponentAttribute.cs delete mode 100644 sources/Core/Core/Abstractions/HluMissingComponentException.cs delete mode 100644 sources/Core/Core/Abstractions/HluRegisteredComponentAttribute`2.cs delete mode 100644 sources/Core/Core/Abstractions/IHluComponentHost.cs delete mode 100644 sources/Core/Core/Abstractions/IHluComponentRegistry.cs delete mode 100644 sources/Core/Core/Abstractions/IHluConfiguration.cs delete mode 100644 sources/Core/Core/Abstractions/IPlatformInfo.cs create mode 100644 sources/Windowing/Common/Configuration/IConfigureHost.cs create mode 100644 sources/Windowing/Common/Configuration/ThreadConfiguration.cs create mode 100644 sources/Windowing/Common/Hosting/HostEventKind.cs create mode 100644 sources/Windowing/Common/Hosting/HostStatus.cs create mode 100644 sources/Windowing/Common/Hosting/IHostActor.cs create mode 100644 sources/Windowing/Common/Hosting/ISurfaceHost.cs create mode 100644 sources/Windowing/Common/Hosting/MultiThreadedSurfaceHost`1.cs create mode 100644 sources/Windowing/Common/Hosting/README.md create mode 100644 sources/Windowing/Common/Hosting/SurfaceHandle.cs create mode 100644 sources/Windowing/Common/Hosting/SurfaceProperty.cs create mode 100644 sources/Windowing/Common/Hosting/SurfacePropertyName.cs create mode 100644 sources/Windowing/Common/Hosting/SurfacePropertyValue.cs create mode 100644 sources/Windowing/Common/Miscellaneous/SurfaceEventHandler.cs create mode 100644 sources/Windowing/Common/Miscellaneous/SurfaceStateEventArgs.cs create mode 100644 sources/Windowing/Common/Miscellaneous/SurfaceStateEventArgs`1.cs create mode 100644 sources/Windowing/Common/Miscellaneous/TimedEventArgs.cs create mode 100644 sources/Windowing/Common/Surface`1.cs delete mode 100644 sources/Windowing/Windowing/SdlNativeWindow.cs delete mode 100644 sources/Windowing/Windowing/SdlSurface.cs create mode 100644 tests/Core/Core/BreakneckRequestTests.cs diff --git a/sources/Core/Analyzers/Helpers.cs b/sources/Core/Analyzers/Helpers.cs new file mode 100644 index 0000000000..528810a7e2 --- /dev/null +++ b/sources/Core/Analyzers/Helpers.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Microsoft.CodeAnalysis; + +namespace Silk.NET.Core.Analyzers; + +/// +/// Helpers used in the construction of Silk.NET's analyzers and generators. +/// +public static class Helpers +{ + /// + /// Registers sources embedded in the calling assembly with the given embedded resource names as post-initialization + /// output. + /// + /// The generator context. + /// The embedded resource names. + public static void RegisterPostInitializationEmbeddedSource( + this IncrementalGeneratorInitializationContext ctx, + params string[] embeddedResourceNames + ) + { + var caller = Assembly.GetCallingAssembly(); + ctx.RegisterPostInitializationOutput(x => + { + foreach (var res in embeddedResourceNames) + { + using var stream = caller.GetManifestResourceStream(res); + if (stream is null) + { + continue; + } + + using var sr = new StreamReader(stream); + x.AddSource(res, sr.ReadToEnd()); + } + }); + } +} diff --git a/sources/Core/Analyzers/HluSourceGenerator.Hosts.cs b/sources/Core/Analyzers/HluSourceGenerator.Hosts.cs deleted file mode 100644 index 5991d28c54..0000000000 --- a/sources/Core/Analyzers/HluSourceGenerator.Hosts.cs +++ /dev/null @@ -1,1030 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace Silk.NET.Core.Analyzers; - -partial class HluSourceGenerator -{ - private static CompilationUnitSyntax CreateHostPartial( - INamedTypeSymbol klass, - IEnumerable<( - bool IsMandatory, - INamedTypeSymbol? Class, - IFieldSymbol? Field, - string? FieldType, - SemanticModel SemanticModel - )> fieldInfo - ) - { - IEnumerable privateCtor = klass.InstanceConstructors.Any(x => - x.Parameters.Length == 0 - ) - ? [] - : - [ - ConstructorDeclaration(klass.Name) - .WithBody(Block()) - .WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword))) - ]; - fieldInfo = fieldInfo.ToArray(); - TypeDeclarationSyntax partial = ClassDeclaration(klass.Name) - .WithModifiers(TokenList(Token(SyntaxKind.PartialKeyword))) - .WithMembers( - List( - [ - _additionalComponentsField, - .. privateCtor, - GetHostCreateUninit(klass.Name), - GetHostCreate(klass.Name), - GetHostThrowIfMisconfigured(fieldInfo), - GetHostTryGet(fieldInfo), - GetHostTrySet(fieldInfo) - ] - ) - ) - .WithBaseList( - BaseList( - SingletonSeparatedList( - SimpleBaseType( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName(Token(SyntaxKind.GlobalKeyword)), - IdentifierName("Silk") - ), - IdentifierName("NET") - ), - IdentifierName("Core") - ), - IdentifierName("IHluComponentHost") - ) - ) - ) - ) - ); - return GetCompilationUnitForPartial(klass, partial); - } - - private static CompilationUnitSyntax GetCompilationUnitForPartial( - INamedTypeSymbol klass, - TypeDeclarationSyntax partial, - IEnumerable? inheritUsingsFrom = null - ) - { - var ns = klass.ContainingNamespace; - var parent = klass; - while ((parent = parent?.ContainingType) is not null) - { - partial = ( - parent.TypeKind switch - { - TypeKind.Class => (TypeDeclarationSyntax)ClassDeclaration(parent.Name), - TypeKind.Interface => InterfaceDeclaration(parent.Name), - TypeKind.Struct => StructDeclaration(parent.Name), - _ => throw new ArgumentOutOfRangeException() - } - ).WithModifiers(TokenList(Token(SyntaxKind.PartialKeyword))).WithMembers(SingletonList(partial)); - } - - return ( - ns?.IsGlobalNamespace ?? true - ? CompilationUnit() - .WithMembers( - SingletonList( - partial.WithLeadingTrivia( - Trivia( - NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true) - ) - ) - ) - ) - : CompilationUnit() - .WithUsings( - List( - (inheritUsingsFrom ?? klass.DeclaringSyntaxReferences) - .SelectMany(x => - x.SyntaxTree.GetRoot() - .DescendantNodes() - .OfType() - ) - .DistinctBy(x => x.Alias?.Name ?? x.Name) - ) - ) - .WithMembers( - SingletonList( - FileScopedNamespaceDeclaration( - IdentifierName( - ns.ToDisplayString( - SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle( - SymbolDisplayGlobalNamespaceStyle.Omitted - ) - ) - ) - ) - .WithMembers(SingletonList(partial)) - .WithLeadingTrivia( - Trivia( - NullableDirectiveTrivia( - Token(SyntaxKind.EnableKeyword), - true - ) - ) - ) - ) - ) - ).NormalizeWhitespace(); - } - - private static MethodDeclarationSyntax GetHostCreate(string klass) => - MethodDeclaration(IdentifierName(klass), Identifier("Create")) - .WithModifiers( - TokenList( - Token(_createDocs, SyntaxKind.PublicKeyword, TriviaList()), - Token(SyntaxKind.StaticKeyword) - ) - ) - .WithTypeParameterList( - TypeParameterList( - SeparatedList( - new SyntaxNodeOrToken[] - { - TypeParameter(Identifier("TConfiguration")), - Token(SyntaxKind.CommaToken), - TypeParameter(Identifier("TRegistry")) - } - ) - ) - ) - .WithParameterList( - ParameterList( - SingletonSeparatedList( - Parameter(Identifier("config")).WithType(IdentifierName("TConfiguration")) - ) - ) - ) - .WithConstraintClauses( - List( - new[] - { - TypeParameterConstraintClause(IdentifierName("TConfiguration")) - .WithConstraints( - SingletonSeparatedList( - TypeConstraint( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName( - Token(SyntaxKind.GlobalKeyword) - ), - IdentifierName("Silk") - ), - IdentifierName("NET") - ), - IdentifierName("Core") - ), - IdentifierName("IHluConfiguration") - ) - ) - ) - ), - TypeParameterConstraintClause(IdentifierName("TRegistry")) - .WithConstraints( - SingletonSeparatedList( - TypeConstraint( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName( - Token(SyntaxKind.GlobalKeyword) - ), - IdentifierName("Silk") - ), - IdentifierName("NET") - ), - IdentifierName("Core") - ), - IdentifierName("IHluComponentRegistry") - ) - ) - ) - ) - } - ) - ) - .WithBody( - Block( - (IEnumerable) - - [ - LocalDeclarationStatement( - VariableDeclaration( - IdentifierName( - Identifier( - TriviaList(), - SyntaxKind.VarKeyword, - "var", - "var", - TriviaList() - ) - ), - SingletonSeparatedList( - VariableDeclarator( - Identifier("ret"), - null, - EqualsValueClause( - ObjectCreationExpression( - IdentifierName(klass), - ArgumentList(), - null - ) - ) - ) - ) - ) - ), - ExpressionStatement( - InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName("TRegistry"), - GenericName( - Identifier("ConfigureDefaults"), - TypeArgumentList( - SeparatedList( - (IEnumerable) - - [ - IdentifierName(klass), - IdentifierName("TConfiguration"), - IdentifierName("TRegistry") - ] - ) - ) - ) - ), - ArgumentList( - SeparatedList( - [ - Argument(IdentifierName("ret")), - Argument(IdentifierName("config")) - ] - ) - ) - ) - ), - ExpressionStatement( - InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName("ret"), - IdentifierName("ThrowIfMisconfigured") - ) - ) - ), - ReturnStatement(IdentifierName("ret")) - ] - ) - ); - - private static MethodDeclarationSyntax GetHostThrowIfMisconfigured( - IEnumerable<( - bool IsMandatory, - INamedTypeSymbol? Class, - IFieldSymbol? Field, - string? FieldType, - SemanticModel SemanticModel - )> fieldInfo - ) => - MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), "ThrowIfMisconfigured") - .WithModifiers(TokenList(Token(_inheritDoc, SyntaxKind.PublicKeyword, TriviaList()))) - .WithBody( - Block( - fieldInfo - .Where(x => x is { IsMandatory: true, Field: not null }) - .Select(field => - ExpressionStatement( - InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName( - Token(SyntaxKind.GlobalKeyword) - ), - IdentifierName("Silk") - ), - IdentifierName("NET") - ), - IdentifierName("Core") - ), - IdentifierName("HluMissingComponentException") - ), - IdentifierName("ThrowIfNull") - ), - ArgumentList( - SingletonSeparatedList( - Argument(IdentifierName(field.Field!.Name)) - ) - ) - ) - ) - ) - ) - ); - - private static MethodDeclarationSyntax GetHostCreateUninit(string klass) => - MethodDeclaration(IdentifierName(klass), Identifier("CreateUninitialized")) - .WithModifiers( - TokenList( - Token( - TriviaList( - Trivia( - DocumentationCommentTrivia( - SyntaxKind.SingleLineDocumentationCommentTrivia, - List( - new XmlNodeSyntax[] - { - XmlText() - .WithTextTokens( - TokenList( - XmlTextLiteral( - TriviaList( - DocumentationCommentExterior("///") - ), - " ", - " ", - TriviaList() - ) - ) - ), - XmlExampleElement( - XmlText() - .WithTextTokens( - TokenList( - XmlTextNewLine( - TriviaList(), - "\n", - "\n", - TriviaList() - ), - XmlTextLiteral( - TriviaList( - DocumentationCommentExterior( - " ///" - ) - ), - " Creates an uninitialized ", - " Creates an uninitialized ", - TriviaList() - ) - ) - ), - XmlNullKeywordElement() - .WithAttributes( - SingletonList( - XmlCrefAttribute( - NameMemberCref( - IdentifierName(klass) - ) - ) - ) - ), - XmlText() - .WithTextTokens( - TokenList( - XmlTextLiteral( - TriviaList(), - ". This probably isn't what you want unless you're pre-configuring a host", - ". This probably isn't what you want unless you're pre-configuring a host", - TriviaList() - ), - XmlTextNewLine( - TriviaList(), - "\n", - "\n", - TriviaList() - ), - XmlTextLiteral( - TriviaList( - DocumentationCommentExterior( - " ///" - ) - ), - " with custom components.", - " with custom components.", - TriviaList() - ), - XmlTextNewLine( - TriviaList(), - "\n", - "\n", - TriviaList() - ), - XmlTextLiteral( - TriviaList( - DocumentationCommentExterior( - " ///" - ) - ), - " ", - " ", - TriviaList() - ) - ) - ) - ) - .WithStartTag( - XmlElementStartTag( - XmlName(Identifier("summary")) - ) - ) - .WithEndTag( - XmlElementEndTag(XmlName(Identifier("summary"))) - ), - XmlText() - .WithTextTokens( - TokenList( - XmlTextNewLine( - TriviaList(), - "\n", - "\n", - TriviaList() - ), - XmlTextLiteral( - TriviaList( - DocumentationCommentExterior( - " ///" - ) - ), - " ", - " ", - TriviaList() - ) - ) - ), - XmlExampleElement( - SingletonList( - XmlText() - .WithTextTokens( - TokenList( - XmlTextLiteral( - TriviaList(), - "The uninitialized component host.", - "The uninitialized component host.", - TriviaList() - ) - ) - ) - ) - ) - .WithStartTag( - XmlElementStartTag( - XmlName(Identifier("returns")) - ) - ) - .WithEndTag( - XmlElementEndTag(XmlName(Identifier("returns"))) - ), - XmlText() - .WithTextTokens( - TokenList( - XmlTextNewLine( - TriviaList(), - "\n", - "\n", - TriviaList() - ) - ) - ) - } - ) - ) - ) - ), - SyntaxKind.PublicKeyword, - TriviaList() - ), - Token(SyntaxKind.StaticKeyword) - ) - ) - .WithExpressionBody(ArrowExpressionClause(ImplicitObjectCreationExpression())) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); - - private static MethodDeclarationSyntax GetHostTryGet( - IEnumerable<( - bool IsMandatory, - INamedTypeSymbol? Class, - IFieldSymbol? Field, - string? FieldType, - SemanticModel SemanticModel - )> fieldInfo - ) => - MethodDeclaration(PredefinedType(Token(SyntaxKind.BoolKeyword)), Identifier("TryGet")) - .WithModifiers(TokenList(Token(_inheritDoc, SyntaxKind.PublicKeyword, TriviaList()))) - .WithTypeParameterList( - TypeParameterList(SingletonSeparatedList(TypeParameter(Identifier("TComponent")))) - ) - .WithParameterList( - ParameterList( - SingletonSeparatedList( - Parameter(Identifier("component")) - .WithAttributeLists( - SingletonList( - AttributeList( - SingletonSeparatedList( - Attribute( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName( - Token( - SyntaxKind.GlobalKeyword - ) - ), - IdentifierName("System") - ), - IdentifierName("Diagnostics") - ), - IdentifierName("CodeAnalysis") - ), - IdentifierName("NotNullWhen") - ) - ) - .WithArgumentList( - AttributeArgumentList( - SingletonSeparatedList( - AttributeArgument( - LiteralExpression( - SyntaxKind.TrueLiteralExpression - ) - ) - ) - ) - ) - ) - ) - ) - ) - .WithModifiers(TokenList(Token(SyntaxKind.OutKeyword))) - .WithType(NullableType(IdentifierName("TComponent"))) - ) - ) - ) - .WithBody( - Block( - (IEnumerable) - - [ - .. fieldInfo - .Where(x => x is { Field.Name: not null, FieldType: not null }) - .Select(x => - IfStatement( - BinaryExpression( - SyntaxKind.EqualsExpression, - TypeOfExpression(IdentifierName("TComponent")), - TypeOfExpression(ParseTypeName(x.FieldType!)) - ), - Block( - ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - IdentifierName("component"), - CastExpression( - NullableType(IdentifierName("TComponent")), - CastExpression( - NullableType( - PredefinedType( - Token(SyntaxKind.ObjectKeyword) - ) - ), - PostfixUnaryExpression( - SyntaxKind.SuppressNullableWarningExpression, - IdentifierName(x.Field!.Name) - ) - ) - ) - ) - ), - ReturnStatement( - BinaryExpression( - SyntaxKind.NotEqualsExpression, - IdentifierName(x.Field!.Name), - LiteralExpression( - SyntaxKind.DefaultLiteralExpression, - Token(SyntaxKind.DefaultKeyword) - ) - ) - ) - ) - ) - ), - ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - IdentifierName("component"), - LiteralExpression( - SyntaxKind.DefaultLiteralExpression, - Token(SyntaxKind.DefaultKeyword) - ) - ) - ), - ReturnStatement( - LiteralExpression( - SyntaxKind.FalseLiteralExpression, - Token(SyntaxKind.FalseKeyword) - ) - ) - ] - ) - ); - - private static MethodDeclarationSyntax GetHostTrySet( - IEnumerable<( - bool IsMandatory, - INamedTypeSymbol? Class, - IFieldSymbol? Field, - string? FieldType, - SemanticModel SemanticModel - )> fieldInfo - ) => - MethodDeclaration(PredefinedType(Token(SyntaxKind.BoolKeyword)), Identifier("TrySet")) - .WithModifiers(TokenList(Token(_inheritDoc, SyntaxKind.PublicKeyword, TriviaList()))) - .WithTypeParameterList( - TypeParameterList( - SeparatedList( - [ - TypeParameter(Identifier("TComponent")), - TypeParameter(Identifier("TImpl")) - ] - ) - ) - ) - .WithParameterList( - ParameterList( - SingletonSeparatedList( - Parameter(Identifier("component")).WithType(IdentifierName("TImpl")) - ) - ) - ) - .WithConstraintClauses( - SingletonList( - TypeParameterConstraintClause( - IdentifierName("TImpl"), - SingletonSeparatedList( - TypeConstraint(IdentifierName("TComponent")) - ) - ) - ) - ) - .WithBody( - Block( - (IEnumerable) - - [ - ExpressionStatement( - InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - QualifiedName( - AliasQualifiedName( - IdentifierName(Token(SyntaxKind.GlobalKeyword)), - IdentifierName("System") - ), - IdentifierName("ArgumentNullException") - ), - IdentifierName("ThrowIfNull") - ), - ArgumentList( - SingletonSeparatedList( - Argument(IdentifierName("component")) - ) - ) - ) - ), - .. fieldInfo - .Where(x => x is { FieldType: not null, Field.Name: not null }) - .Select(x => - IfStatement( - BinaryExpression( - SyntaxKind.EqualsExpression, - TypeOfExpression(IdentifierName("TComponent")), - TypeOfExpression(ParseTypeName(x.FieldType!)) - ), - Block( - ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - IdentifierName(x.Field!.Name), - CastExpression( - IdentifierName(x.FieldType!), - CastExpression( - PredefinedType( - Token(SyntaxKind.ObjectKeyword) - ), - PostfixUnaryExpression( - SyntaxKind.SuppressNullableWarningExpression, - IdentifierName("component") - ) - ) - ) - ) - ), - ReturnStatement( - LiteralExpression( - SyntaxKind.TrueLiteralExpression, - Token(SyntaxKind.TrueKeyword) - ) - ) - ) - ) - ), - ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - ElementAccessExpression( - ParenthesizedExpression( - AssignmentExpression( - SyntaxKind.CoalesceAssignmentExpression, - IdentifierName("_additionalComponents"), - CollectionExpression() - ) - ) - ) - .WithArgumentList( - BracketedArgumentList( - SingletonSeparatedList( - Argument( - TypeOfExpression( - IdentifierName("TComponent") - ) - ) - ) - ) - ), - IdentifierName("component") - ) - ), - ReturnStatement( - LiteralExpression( - SyntaxKind.TrueLiteralExpression, - Token(SyntaxKind.TrueKeyword) - ) - ) - ] - ) - ); - - private static readonly FieldDeclarationSyntax _additionalComponentsField = FieldDeclaration( - VariableDeclaration( - NullableType( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName(Token(SyntaxKind.GlobalKeyword)), - IdentifierName("System") - ), - IdentifierName("Collections") - ), - IdentifierName("Generic") - ), - GenericName(Identifier("Dictionary")) - .WithTypeArgumentList( - TypeArgumentList( - SeparatedList( - new SyntaxNodeOrToken[] - { - QualifiedName( - AliasQualifiedName( - IdentifierName( - Token(SyntaxKind.GlobalKeyword) - ), - IdentifierName("System") - ), - IdentifierName("Type") - ), - Token(SyntaxKind.CommaToken), - PredefinedType(Token(SyntaxKind.ObjectKeyword)) - } - ) - ) - ) - ) - ) - ) - .WithVariables( - SingletonSeparatedList(VariableDeclarator(Identifier("_additionalComponents"))) - ) - ) - .WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword))) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); - - private static readonly SyntaxTriviaList _inheritDoc = TriviaList( - Trivia( - DocumentationCommentTrivia( - SyntaxKind.SingleLineDocumentationCommentTrivia, - List( - new XmlNodeSyntax[] - { - XmlText() - .WithTextTokens( - TokenList( - XmlTextLiteral( - TriviaList(DocumentationCommentExterior("///")), - " ", - " ", - TriviaList() - ) - ) - ), - XmlNullKeywordElement().WithName(XmlName(Identifier("inheritdoc"))), - XmlText() - .WithTextTokens( - TokenList(XmlTextNewLine(TriviaList(), "\n", "\n", TriviaList())) - ) - } - ) - ) - ) - ); - - private static readonly SyntaxTriviaList _createDocs = TriviaList( - Trivia( - DocumentationCommentTrivia( - SyntaxKind.SingleLineDocumentationCommentTrivia, - List( - new XmlNodeSyntax[] - { - XmlText() - .WithTextTokens( - TokenList( - XmlTextLiteral( - TriviaList(DocumentationCommentExterior("///")), - " ", - " ", - TriviaList() - ) - ) - ), - XmlExampleElement( - XmlText() - .WithTextTokens( - TokenList( - XmlTextNewLine(TriviaList(), "\n", "\n", TriviaList()), - XmlTextLiteral( - TriviaList(DocumentationCommentExterior(" ///")), - " Creates a ", - " Creates a ", - TriviaList() - ) - ) - ), - XmlNullKeywordElement() - .WithAttributes( - SingletonList( - XmlCrefAttribute( - NameMemberCref(IdentifierName("Surface")) - ) - ) - ), - XmlText() - .WithTextTokens( - TokenList( - XmlTextLiteral( - TriviaList(), - " with the components from the given registry,", - " with the components from the given registry,", - TriviaList() - ), - XmlTextNewLine(TriviaList(), "\n", "\n", TriviaList()), - XmlTextLiteral( - TriviaList(DocumentationCommentExterior(" ///")), - " configured using the given configuration.", - " configured using the given configuration.", - TriviaList() - ), - XmlTextNewLine(TriviaList(), "\n", "\n", TriviaList()), - XmlTextLiteral( - TriviaList(DocumentationCommentExterior(" ///")), - " ", - " ", - TriviaList() - ) - ) - ) - ) - .WithStartTag(XmlElementStartTag(XmlName(Identifier("summary")))) - .WithEndTag(XmlElementEndTag(XmlName(Identifier("summary")))), - XmlText() - .WithTextTokens( - TokenList( - XmlTextNewLine(TriviaList(), "\n", "\n", TriviaList()), - XmlTextLiteral( - TriviaList(DocumentationCommentExterior(" ///")), - " ", - " ", - TriviaList() - ) - ) - ), - XmlExampleElement( - SingletonList( - XmlText() - .WithTextTokens( - TokenList( - XmlTextLiteral( - TriviaList(), - "The configuration for the components.", - "The configuration for the components.", - TriviaList() - ) - ) - ) - ) - ) - .WithStartTag( - XmlElementStartTag( - XmlName( - Identifier( - TriviaList(), - SyntaxKind.ParamKeyword, - "param", - "param", - TriviaList() - ) - ) - ) - .WithAttributes( - SingletonList( - XmlNameAttribute( - XmlName(Identifier("name")), - Token(SyntaxKind.DoubleQuoteToken), - IdentifierName("config"), - Token(SyntaxKind.DoubleQuoteToken) - ) - ) - ) - ) - .WithEndTag( - XmlElementEndTag( - XmlName( - Identifier( - TriviaList(), - SyntaxKind.ParamKeyword, - "param", - "param", - TriviaList() - ) - ) - ) - ), - XmlText() - .WithTextTokens( - TokenList( - XmlTextNewLine(TriviaList(), "\n", "\n", TriviaList()), - XmlTextLiteral( - TriviaList(DocumentationCommentExterior(" ///")), - " ", - " ", - TriviaList() - ) - ) - ), - XmlExampleElement( - SingletonList( - XmlText() - .WithTextTokens( - TokenList( - XmlTextLiteral( - TriviaList(), - "The instantiated object.", - "The instantiated object.", - TriviaList() - ) - ) - ) - ) - ) - .WithStartTag(XmlElementStartTag(XmlName(Identifier("returns")))) - .WithEndTag(XmlElementEndTag(XmlName(Identifier("returns")))), - XmlText() - .WithTextTokens( - TokenList(XmlTextNewLine(TriviaList(), "\n", "\n", TriviaList())) - ) - } - ) - ) - ) - ); -} diff --git a/sources/Core/Analyzers/HluSourceGenerator.Registries.cs b/sources/Core/Analyzers/HluSourceGenerator.Registries.cs deleted file mode 100644 index 7af8794b7c..0000000000 --- a/sources/Core/Analyzers/HluSourceGenerator.Registries.cs +++ /dev/null @@ -1,842 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace Silk.NET.Core.Analyzers; - -partial class HluSourceGenerator -{ - private static CompilationUnitSyntax CreateRegistryPartial( - INamedTypeSymbol klass, - SemanticModel semanticModel, - IReadOnlyList<( - INamedTypeSymbol ComponentType, - INamedTypeSymbol ImplementationType, - AttributeSyntax ApplicationSyntax, - bool IsDefault - )> components - ) => - GetCompilationUnitForPartial( - klass, - ClassDeclaration(klass.Name) - .WithModifiers(TokenList(Token(SyntaxKind.PartialKeyword))) - .WithBaseList( - BaseList( - SingletonSeparatedList( - SimpleBaseType( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName(Token(SyntaxKind.GlobalKeyword)), - IdentifierName("Silk") - ), - IdentifierName("NET") - ), - IdentifierName("Core") - ), - IdentifierName("IHluComponentRegistry") - ) - ) - ) - ) - ) - .WithMembers( - List( - [ - GetRegistryConfigureDefaults(components), - GetRegistryTryConfigureComponent(semanticModel, components) - ] - ) - ) - ); - - private static MethodDeclarationSyntax GetRegistryConfigureDefaults( - IEnumerable<( - INamedTypeSymbol ComponentType, - INamedTypeSymbol ImplementationType, - AttributeSyntax ApplicationSyntax, - bool IsDefault - )> components - ) => - MethodDeclaration( - PredefinedType(Token(SyntaxKind.VoidKeyword)), - Identifier("ConfigureDefaults") - ) - .WithModifiers( - TokenList( - Token(_inheritDoc, SyntaxKind.PublicKeyword, TriviaList()), - Token(SyntaxKind.StaticKeyword) - ) - ) - .WithTypeParameterList( - TypeParameterList( - SeparatedList( - [ - TypeParameter(Identifier("THost")), - TypeParameter(Identifier("TConfiguration")), - TypeParameter(Identifier("TRegistry")) - ] - ) - ) - ) - .WithParameterList( - ParameterList( - SeparatedList( - [ - Parameter(Identifier("dest")).WithType(IdentifierName("THost")), - Parameter(Identifier("config")) - .WithType(IdentifierName("TConfiguration")) - ] - ) - ) - ) - .WithConstraintClauses( - List( - new[] - { - TypeParameterConstraintClause(IdentifierName("THost")) - .WithConstraints( - SingletonSeparatedList( - TypeConstraint( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName( - Token(SyntaxKind.GlobalKeyword) - ), - IdentifierName("Silk") - ), - IdentifierName("NET") - ), - IdentifierName("Core") - ), - IdentifierName("IHluComponentHost") - ) - ) - ) - ), - TypeParameterConstraintClause(IdentifierName("TConfiguration")) - .WithConstraints( - SingletonSeparatedList( - TypeConstraint( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName( - Token(SyntaxKind.GlobalKeyword) - ), - IdentifierName("Silk") - ), - IdentifierName("NET") - ), - IdentifierName("Core") - ), - IdentifierName("IHluConfiguration") - ) - ) - ) - ), - TypeParameterConstraintClause(IdentifierName("TRegistry")) - .WithConstraints( - SingletonSeparatedList( - TypeConstraint( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName( - Token(SyntaxKind.GlobalKeyword) - ), - IdentifierName("Silk") - ), - IdentifierName("NET") - ), - IdentifierName("Core") - ), - IdentifierName("IHluComponentRegistry") - ) - ) - ) - ) - } - ) - ) - .WithBody( - Block( - components - .Where(x => x.IsDefault) - .Select(x => - ExpressionStatement( - InvocationExpression( - GenericName( - Identifier("TryConfigureComponent"), - TypeArgumentList( - SeparatedList( - [ - ((GenericNameSyntax)x.ApplicationSyntax.Name) - .TypeArgumentList - .Arguments[0], - IdentifierName("THost"), - IdentifierName("TConfiguration"), - IdentifierName("TRegistry") - ] - ) - ) - ), - ArgumentList( - SeparatedList( - [ - Argument(IdentifierName("dest")), - Argument(IdentifierName("config")) - ] - ) - ) - ) - ) - ) - ) - ); - - private static MethodDeclarationSyntax GetRegistryTryConfigureComponent( - SemanticModel semanticModel, - IReadOnlyList<( - INamedTypeSymbol ComponentType, - INamedTypeSymbol ImplementationType, - AttributeSyntax ApplicationSyntax, - bool IsDefault - )> components - ) => - MethodDeclaration( - PredefinedType(Token(SyntaxKind.BoolKeyword)), - Identifier("TryConfigureComponent") - ) - .WithModifiers( - TokenList( - Token(_inheritDoc, SyntaxKind.PublicKeyword, TriviaList()), - Token(SyntaxKind.StaticKeyword) - ) - ) - .WithTypeParameterList( - TypeParameterList( - SeparatedList( - [ - TypeParameter(Identifier("TComponent")), - TypeParameter(Identifier("THost")), - TypeParameter(Identifier("TConfiguration")), - TypeParameter(Identifier("TRegistry")) - ] - ) - ) - ) - .WithParameterList( - ParameterList( - SeparatedList( - [ - Parameter(Identifier("dest")).WithType(IdentifierName("THost")), - Parameter(Identifier("config")) - .WithType(IdentifierName("TConfiguration")) - ] - ) - ) - ) - .WithConstraintClauses( - List( - new[] - { - TypeParameterConstraintClause(IdentifierName("THost")) - .WithConstraints( - SingletonSeparatedList( - TypeConstraint( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName( - Token(SyntaxKind.GlobalKeyword) - ), - IdentifierName("Silk") - ), - IdentifierName("NET") - ), - IdentifierName("Core") - ), - IdentifierName("IHluComponentHost") - ) - ) - ) - ), - TypeParameterConstraintClause(IdentifierName("TConfiguration")) - .WithConstraints( - SingletonSeparatedList( - TypeConstraint( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName( - Token(SyntaxKind.GlobalKeyword) - ), - IdentifierName("Silk") - ), - IdentifierName("NET") - ), - IdentifierName("Core") - ), - IdentifierName("IHluConfiguration") - ) - ) - ) - ), - TypeParameterConstraintClause(IdentifierName("TRegistry")) - .WithConstraints( - SingletonSeparatedList( - TypeConstraint( - QualifiedName( - QualifiedName( - QualifiedName( - AliasQualifiedName( - IdentifierName( - Token(SyntaxKind.GlobalKeyword) - ), - IdentifierName("Silk") - ), - IdentifierName("NET") - ), - IdentifierName("Core") - ), - IdentifierName("IHluComponentRegistry") - ) - ) - ) - ) - } - ) - ) - .WithBody( - Block( - (IEnumerable) - - [ - IfStatement( - InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName("dest"), - GenericName( - Identifier("TryGet"), - TypeArgumentList( - SingletonSeparatedList( - IdentifierName("TComponent") - ) - ) - ) - ), - ArgumentList( - SingletonSeparatedList( - Argument( - null, - Token(SyntaxKind.OutKeyword), - IdentifierName("_") - ) - ) - ) - ), - ReturnStatement( - LiteralExpression( - SyntaxKind.FalseLiteralExpression, - Token(SyntaxKind.FalseKeyword) - ) - ) - ), - .. components.Select( - (x, compIdx) => - { - var encounteredFallbackOption = false; - return IfStatement( - BinaryExpression( - SyntaxKind.EqualsExpression, - TypeOfExpression(IdentifierName("TComponent")), - TypeOfExpression( - ((GenericNameSyntax)x.ApplicationSyntax.Name) - .TypeArgumentList - .Arguments[0] - ) - ), - Block( - x.ImplementationType.InstanceConstructors.Where(y => - y.DeclaredAccessibility == Accessibility.Public - ) - .Concat( - x.ImplementationType.GetMembers() - .OfType() - .Where(y => - y - is { - Name: "TryConfigure", - ReturnType.SpecialType: SpecialType.System_Boolean, - TypeParameters: [ - { - ConstraintTypes.Length: 1 - } - ], - DeclaredAccessibility: Accessibility.Public - } - && y.TypeParameters[0] - .ConstraintTypes[0] - .ToDisplayString(NamespaceQualified) - == "Silk.NET.Core.IHluComponentHost" - && y.Parameters[0] - .Type.Equals( - y.TypeParameters[0], - SymbolEqualityComparer.Default - ) - ) - ) - .Select(y => - ( - NumRequired: y.Parameters.Count(z => - z.Type.NullableAnnotation - == NullableAnnotation.NotAnnotated - && z.Type.SpecialType - != SpecialType.System_Nullable_T - ), - Ctor: y - ) - ) - .OrderByDescending(y => - (y.NumRequired * 1000) - + y.Ctor.Parameters.Length - - y.Ctor.TypeParameters.Length - ) - .TakeWhile(y => - !encounteredFallbackOption - && ( - y.NumRequired != 0 - || (encounteredFallbackOption = true) - ) - ) - .SelectMany( - (y, ctorIdx) => - GetRegistryConfigureMandatoryDependencies( - compIdx, - y.Ctor, - ctorIdx, - semanticModel, - [ - .. GetRegistryDependencyVariableDeclarations( - compIdx, - y.Ctor, - ctorIdx, - semanticModel - ), - .. GetRegistryTryConfigureOptionalDependencies( - compIdx, - y.Ctor, - ctorIdx, - semanticModel - ), - GetRegistryTrySetConfiguredComponent( - x.ApplicationSyntax, - compIdx, - y.Ctor, - ctorIdx - ) - ] - ) - ) - ) - ); - } - ), - ReturnStatement( - LiteralExpression( - SyntaxKind.FalseLiteralExpression, - Token(SyntaxKind.FalseKeyword) - ) - ) - ] - ) - ); - - private static IEnumerable GetRegistryDependencyVariableDeclarations( - int compIdx, - IMethodSymbol ctor, - int ctorIdx, - SemanticModel semanticModel - ) => - ctor - .Parameters.Skip(ctor.TypeParameters.Length) - .Select((z, pIdx) => (Component: z, PIdx: pIdx)) - .Where(z => - z.Component.Type.NullableAnnotation == NullableAnnotation.Annotated - || z.Component.Type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T - ) - .Select(z => - LocalDeclarationStatement( - VariableDeclaration( - NullableType( - ParseTypeName( - SymbolDisplay.ToMinimalDisplayString( - z.Component.Type, - semanticModel, - 0, - SymbolDisplayFormat.FullyQualifiedFormat - ) - ) - ), - SingletonSeparatedList( - VariableDeclarator( - Identifier($"dep{compIdx}Ctor{ctorIdx}Param{z.PIdx}"), - null, - EqualsValueClause( - PostfixUnaryExpression( - SyntaxKind.SuppressNullableWarningExpression, - LiteralExpression( - SyntaxKind.DefaultLiteralExpression, - Token(SyntaxKind.DefaultKeyword) - ) - ) - ) - ) - ) - ) - ) - ); - - private static readonly SymbolDisplayFormat NamespaceQualified = - SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle( - SymbolDisplayGlobalNamespaceStyle.Omitted - ); - - private static IEnumerable GetRegistryTryConfigureOptionalDependencies( - int compIdx, - IMethodSymbol ctor, - int ctorIdx, - SemanticModel semanticModel - ) => - ctor - .Parameters.Skip(ctor.TypeParameters.Length) - .Select((z, i) => (Component: z, PIdx: i)) - .Where(z => - z.Component.Type.NullableAnnotation == NullableAnnotation.Annotated - || z.Component.Type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T - ) - .Select(z => - IfStatement( - z - .Component.GetAttributes() - .Any(w => - w.AttributeClass?.ToDisplayString(NamespaceQualified) - == "Silk.NET.Core.HluHostedComponentAttribute" - ) - ? GetRegistryTryGetOrAddAndGetComponent( - z.Component.Type, - compIdx, - ctorIdx, - z.PIdx, - semanticModel - ) - : GetRegistryTryGetDependency( - z.Component.Type, - compIdx, - ctorIdx, - z.PIdx, - semanticModel - ), - ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - IdentifierName($"dep{compIdx}Ctor{ctorIdx}Param{z.PIdx}"), - IdentifierName($"dep{compIdx}Ctor{ctorIdx}Param{z.PIdx}Opt") - ) - ) - ) - ); - - private static IEnumerable GetRegistryConfigureMandatoryDependencies( - int compIdx, - IMethodSymbol ctor, - int ctorIdx, - SemanticModel semanticModel, - IEnumerable ifSatisfiedStatements - ) => - ctor - .Parameters.Skip(ctor.TypeParameters.Length) - .Select((z, i) => (Component: z, PIdx: i)) - .Where(z => - z.Component.Type.NullableAnnotation == NullableAnnotation.NotAnnotated - && z.Component.Type.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T - ) - .Aggregate( - (ExpressionSyntax?)null, - (current, x) => - { - var isComp = x - .Component.GetAttributes() - .Any(y => - y.AttributeClass?.ToDisplayString(NamespaceQualified) - == "Silk.NET.Core.HluHostedComponentAttribute" - ); - var declExpr = DeclarationExpression( - IdentifierName( - Identifier( - TriviaList(), - SyntaxKind.VarKeyword, - "var", - "var", - TriviaList() - ) - ), - SingleVariableDesignation( - Identifier($"dep{compIdx}Ctor{ctorIdx}Param{x.PIdx}") - ) - ); - var expr = isComp - ? GetRegistryHostTryConfigureComponentAndTryGet( - x.Component.Type, - compIdx, - ctorIdx, - x.PIdx, - semanticModel, - declExpr - ) - : ParenthesizedExpression( - GetRegistryTryGetDependency( - x.Component.Type, - compIdx, - ctorIdx, - x.PIdx, - semanticModel, - true, - declExpr - ) - ); - if (current is not null and not BinaryExpressionSyntax) - { - current = ParenthesizedExpression(current); - } - - return current is null - ? expr - : BinaryExpression(SyntaxKind.LogicalAndExpression, current, expr); - } - ) - is not { } mandatoryGetIfCondition - ? ifSatisfiedStatements - : [IfStatement(mandatoryGetIfCondition, Block(ifSatisfiedStatements))]; - - private static ExpressionSyntax GetRegistryTryGetOrAddAndGetComponent( - ITypeSymbol compType, - int compIdx, - int ctorIdx, - int pIdx, - SemanticModel semanticModel, - ExpressionSyntax? argument = null - ) => - BinaryExpression( - SyntaxKind.LogicalOrExpression, - GetRegistryTryGetDependency( - compType, - compIdx, - ctorIdx, - pIdx, - semanticModel, - false, - argument - ), - GetRegistryHostTryConfigureComponentAndTryGet( - compType, - compIdx, - ctorIdx, - pIdx, - semanticModel, - argument ?? IdentifierName($"dep{compIdx}Ctor{ctorIdx}Param{pIdx}Opt") - ) - ); - - private static ReturnStatementSyntax GetRegistryTrySetConfiguredComponent( - AttributeSyntax attr, - int compIdx, - IMethodSymbol ctor, - int ctorIdx - ) => - ReturnStatement( - InvocationExpression( - ctor.TypeParameters.Length > 0 - ? MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - ParseTypeName( - ctor.ContainingType.ToDisplayString( - SymbolDisplayFormat.FullyQualifiedFormat - ) - ), - GenericName( - Identifier(ctor.Name), - TypeArgumentList( - SingletonSeparatedList(IdentifierName("THost")) - ) - ) - ) - : MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName("dest"), - ((GenericNameSyntax)attr.Name).WithIdentifier(Identifier("TrySet")) - ), - ArgumentList( - SingletonSeparatedList( - Argument( - ObjectCreationExpression( - ((GenericNameSyntax)attr.Name).TypeArgumentList.Arguments[1], - ArgumentList( - SeparatedList( - ctor.Parameters.Select( - (_, pIdx) => - Argument( - IdentifierName( - (pIdx, ctor.TypeParameters.Length) is (0, 1) - ? "dest" - : $"dep{compIdx}Ctor{ctorIdx}Param{pIdx}" - ) - ) - ) - ) - ), - null - ) - ) - ) - ) - ) - ); - - private static InvocationExpressionSyntax GetRegistryTryGetDependency( - ITypeSymbol compType, - int compIdx, - int ctorIdx, - int pIdx, - SemanticModel semanticModel, - bool configuration = true, - ExpressionSyntax? argument = null - ) => - InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName(configuration ? "config" : "dest"), - GenericName( - Identifier(configuration ? "TryGetConfiguration" : "TryGet"), - TypeArgumentList( - SingletonSeparatedList( - NotNull( - ParseTypeName( - SymbolDisplay.ToMinimalDisplayString( - compType, - semanticModel, - 0, - SymbolDisplayFormat.FullyQualifiedFormat - ) - ) - ) - ) - ) - ) - ), - ArgumentList( - SingletonSeparatedList( - Argument( - null, - Token(SyntaxKind.OutKeyword), - argument - ?? DeclarationExpression( - IdentifierName( - Identifier( - TriviaList(), - SyntaxKind.VarKeyword, - "var", - "var", - TriviaList() - ) - ), - SingleVariableDesignation( - Identifier($"dep{compIdx}Ctor{ctorIdx}Param{pIdx}Opt") - ) - ) - ) - ) - ) - ); - - private static ParenthesizedExpressionSyntax GetRegistryHostTryConfigureComponentAndTryGet( - ITypeSymbol compType, - int compIdx, - int ctorIdx, - int pIdx, - SemanticModel semanticModel, - ExpressionSyntax? argument = null - ) => - ParenthesizedExpression( - BinaryExpression( - SyntaxKind.LogicalAndExpression, - InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName("TRegistry"), - GenericName( - Identifier("TryConfigureComponent"), - TypeArgumentList( - SeparatedList( - [ - NotNull( - ParseTypeName( - SymbolDisplay.ToMinimalDisplayString( - compType, - semanticModel, - 0, - SymbolDisplayFormat.FullyQualifiedFormat - ) - ) - ), - IdentifierName("THost"), - IdentifierName("TConfiguration"), - IdentifierName("TRegistry") - ] - ) - ) - ) - ), - ArgumentList( - SeparatedList( - [Argument(IdentifierName("dest")), Argument(IdentifierName("config"))] - ) - ) - ), - GetRegistryTryGetDependency( - compType, - compIdx, - ctorIdx, - pIdx, - semanticModel, - false, - argument - ) - ) - ); - - private static TypeSyntax NotNull(TypeSyntax syn) => - syn switch - { - NullableTypeSyntax nullableTypeSyntax => nullableTypeSyntax.ElementType, - PointerTypeSyntax pointerTypeSyntax - => pointerTypeSyntax.WithElementType(NotNull(pointerTypeSyntax.ElementType)), - RefTypeSyntax refTypeSyntax => refTypeSyntax.WithType(NotNull(refTypeSyntax.Type)), - ScopedTypeSyntax scopedTypeSyntax - => scopedTypeSyntax.WithType(NotNull(scopedTypeSyntax.Type)), - _ => syn - }; -} diff --git a/sources/Core/Analyzers/HluSourceGenerator.cs b/sources/Core/Analyzers/HluSourceGenerator.cs deleted file mode 100644 index 5a54a86651..0000000000 --- a/sources/Core/Analyzers/HluSourceGenerator.cs +++ /dev/null @@ -1,122 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace Silk.NET.Core.Analyzers; - -/// -/// A source generator to aid usage of HLU types. -/// -[Generator] -public partial class HluSourceGenerator : IIncrementalGenerator -{ - /// - public void Initialize(IncrementalGeneratorInitializationContext context) - { - context.RegisterSourceOutput( - context - .SyntaxProvider.ForAttributeWithMetadataName( - "Silk.NET.Core.HluHostedComponentAttribute", - (node, _) => - node - is VariableDeclaratorSyntax - { - Parent: VariableDeclarationSyntax - { - Parent: FieldDeclarationSyntax - { - Parent: ClassDeclarationSyntax - } - } - }, - (ctx, _) => - ( - IsMandatory: !ctx.Attributes[0] - .ConstructorArguments.Any(y => y.Value is false), - Class: (ctx.TargetSymbol as IFieldSymbol)?.ContainingType, - Field: ctx.TargetSymbol as IFieldSymbol, - FieldType: (ctx.TargetSymbol as IFieldSymbol)?.Type.ToDisplayString( - SymbolDisplayFormat.FullyQualifiedFormat.WithGenericsOptions( - SymbolDisplayGenericsOptions.IncludeTypeParameters - ) - ), - ctx.SemanticModel - ) - ) - .Collect() - .Select( - (x, _) => - x.Where(y => y.Class is not null) - .GroupBy(y => y.Class, SymbolEqualityComparer.Default) - ), - (outCtx, data) => - { - foreach (var klass in data) - { - if (klass.Key is not INamedTypeSymbol classSym) - { - continue; - } - - outCtx.AddSource( - $"{classSym.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}_HluComponentHost.g.cs", - CreateHostPartial(classSym, klass).ToFullString() - ); - } - } - ); - - context.RegisterSourceOutput( - context - .SyntaxProvider.ForAttributeWithMetadataName( - "Silk.NET.Core.HluRegisteredComponentAttribute`2", - (node, _) => node is ClassDeclarationSyntax, - (ctx, _) => - ( - Class: ctx.TargetSymbol as INamedTypeSymbol, - Components: ctx.Attributes.Select(x => - ( - ComponentType: x.AttributeClass?.TypeArguments[0] - as INamedTypeSymbol, - ImplementationType: x.AttributeClass?.TypeArguments[1] - as INamedTypeSymbol, - ApplicationSyntax: x.ApplicationSyntaxReference?.GetSyntax() - as AttributeSyntax, - IsDefault: !x.ConstructorArguments.Any(y => - y.Value is false - ) - ) - ) - .Where(x => - x - is { - ComponentType.TypeArguments.Length: 0, - ImplementationType: not null, - ApplicationSyntax: not null - } - ), - ctx.SemanticModel - ) - ) - .Where(y => y.Class is not null), - (outCtx, data) => - { - outCtx.AddSource( - $"{data.Class!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}_HluComponentRegistry.g.cs", - CreateRegistryPartial( - data.Class, - data.SemanticModel, - data.Components.ToArray()! - ) - .ToFullString() - ); - } - ); - } -} diff --git a/sources/Core/Analyzers/MockStaticAbstractAttribute.cs b/sources/Core/Analyzers/MockStaticAbstractAttribute.cs new file mode 100644 index 0000000000..418b4de445 --- /dev/null +++ b/sources/Core/Analyzers/MockStaticAbstractAttribute.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Core.Analyzers; + +/// +/// An attribute that triggers StaticAbstractInterfaceMocking to generate a mock. +/// +internal class MockStaticAbstractAttribute : Attribute; diff --git a/sources/Core/Analyzers/Silk.NET.Core.Analyzers.csproj b/sources/Core/Analyzers/Silk.NET.Core.Analyzers.csproj index 5f5f215d34..2a4a37379c 100644 --- a/sources/Core/Analyzers/Silk.NET.Core.Analyzers.csproj +++ b/sources/Core/Analyzers/Silk.NET.Core.Analyzers.csproj @@ -14,5 +14,10 @@ + + + + + diff --git a/sources/Core/Analyzers/StaticAbstractInterfaceMocking.cs b/sources/Core/Analyzers/StaticAbstractInterfaceMocking.cs new file mode 100644 index 0000000000..25ef3c3b4b --- /dev/null +++ b/sources/Core/Analyzers/StaticAbstractInterfaceMocking.cs @@ -0,0 +1,583 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Silk.NET.Core.Analyzers; + +/// +/// A source generator that allows creating mocks of static abstract interfaces (i.e. in the style of the Surface +/// Host APIs) +/// +[Generator] +public class StaticAbstractInterfaceMocking : CSharpSyntaxRewriter, IIncrementalGenerator +{ + private ThreadLocal?> _delegates = new(() => null); + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationEmbeddedSource( + "Silk.NET.Core.Analyzers.MockStaticAbstractAttribute.cs" + ); + context.RegisterSourceOutput( + context.SyntaxProvider.ForAttributeWithMetadataName( + "Silk.NET.Core.Analyzers.MockStaticAbstractAttribute", + (s, _) => s is InterfaceDeclarationSyntax, + (ctx, _) => + { + try + { + return (New: Visit(ctx.TargetNode), ctx.TargetNode); + } + catch (Exception ex) + { +#pragma warning disable RS1035 + File.WriteAllText( + "/Users/dylan/Documents/Silk.NET3/sources/Windowing/Common/tmp.log", + ex.ToString() + ); +#pragma warning restore RS1035 + throw; + } + } + ), + (s, a) => + { + if (a.New is null) + { + return; + } + + var nsStr = NsStr(a.TargetNode); + var @new = CompilationUnit() + .WithUsings( + List( + a.TargetNode.Ancestors() + .Last() + .DescendantNodesAndSelf() + .OfType() + ) + ) + .WithMembers( + string.IsNullOrWhiteSpace(nsStr) + ? SingletonList((MemberDeclarationSyntax)a.New) + : SingletonList( + FileScopedNamespaceDeclaration( + ParseName( + nsStr[ + ..( + nsStr.LastIndexOf('.') is not -1 and var i + ? i + : nsStr.Length + ) + ] + ) + ) + .WithMembers(SingletonList((MemberDeclarationSyntax)a.New)) + ) + ); + s.AddSource($"{nsStr}.cs", @new.NormalizeWhitespace().ToFullString()); + } + ); + } + + private static string NsStr(SyntaxNode node) => + string.Join( + '.', + node.AncestorsAndSelf() + .Select(x => + x switch + { + BaseNamespaceDeclarationSyntax bn => bn.Name.ToString(), + TypeDeclarationSyntax td => td.Identifier.ToString(), + _ => string.Empty + } + ) + .Where(x => x.Length > 0) + .Reverse() + ); + + /// + public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) + { + var before = _delegates.Value; + List delegates = []; + _delegates.Value = delegates; + var iface = (InterfaceDeclarationSyntax) + base.VisitInterfaceDeclaration( + node.WithMembers( + List( + node.Members.Where(x => + x is PropertyDeclarationSyntax or MethodDeclarationSyntax + ) + ) + ) + )!; + _delegates.Value = before; + return ClassDeclaration($"Mock{node.Identifier}") + .WithModifiers(TokenList(Token(SyntaxKind.InternalKeyword))) + .AddMembers( + [ + FieldDeclaration( + VariableDeclaration( + NullableType(IdentifierName("Sentinel")), + SingletonSeparatedList(VariableDeclarator("_current")) + ) + ) + .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))), + ClassDeclaration("Sentinel") + .WithBaseList( + BaseList( + SingletonSeparatedList( + SimpleBaseType(IdentifierName("IDisposable")) + ) + ) + ) + .WithParameterList( + ParameterList( + SeparatedList( + [ + Parameter(Identifier("previous")) + .WithType(NullableType(IdentifierName("Sentinel"))) + ] + ) + ) + ) + .WithMembers( + List( + [ + .. delegates.Select(x => + FieldDeclaration( + VariableDeclaration( + ParseTypeName( + "global::System.Collections.Concurrent.ConcurrentQueue?" + ), + SingletonSeparatedList( + VariableDeclarator( + $"_current{x.Identifier}" + ) + .WithInitializer( + EqualsValueClause( + LiteralExpression( + SyntaxKind.DefaultLiteralExpression + ) + ) + ) + ) + ) + ) + .WithModifiers( + TokenList(Token(SyntaxKind.InternalKeyword)) + ) + ), + MethodDeclaration( + PredefinedType(Token(SyntaxKind.VoidKeyword)), + "Dispose" + ) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithBody( + Block( + List( + [ + .. delegates.Select(x => + IfStatement( + BinaryExpression( + SyntaxKind.GreaterThanExpression, + ConditionalAccessExpression( + IdentifierName( + $"_current{x.Identifier}" + ), + MemberBindingExpression( + IdentifierName("Count") + ) + ), + LiteralExpression( + SyntaxKind.NumericLiteralExpression, + Literal(0) + ) + ), + ExpressionStatement( + ThrowExpression( + ImplicitObjectCreationExpression( + ArgumentList( + SingletonSeparatedList( + Argument( + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal( + $"{x.Identifier} has unmet expectations." + ) + ) + ) + ) + ), + null + ) + ) + ) + ) + ), + .. delegates.Select(x => + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName( + $"_current{x.Identifier}" + ), + CollectionExpression() + ) + ) + ), + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName( + $"Mock{node.Identifier}" + ), + IdentifierName("_current") + ), + IdentifierName("previous") + ) + ) + ] + ) + ) + ) + ] + ) + ), + MethodDeclaration(IdentifierName("IDisposable"), "Begin") + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword) + ) + ) + .WithExpressionBody( + ArrowExpressionClause( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("_current"), + ObjectCreationExpression(IdentifierName("Sentinel")) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument(IdentifierName("_current")) + ) + ) + ) + ) + ) + ) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + .. iface.Members, + .. delegates.Select(x => + MethodDeclaration( + PredefinedType(Token(SyntaxKind.VoidKeyword)), + Identifier($"Add{x.Identifier}") + ) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword) + ) + ) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter(Identifier("action")) + .WithType( + x.TypeParameterList is null + ? IdentifierName(x.Identifier) + : GenericName( + x.Identifier, + TypeArgumentList( + SeparatedList( + x.TypeParameterList.Parameters.Select( + y => + IdentifierName(y.Identifier) + ) + ) + ) + ) + ) + ) + ) + ) + .WithTypeParameterList(x.TypeParameterList) + .WithExpressionBody( + ArrowExpressionClause( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + ParenthesizedExpression( + AssignmentExpression( + SyntaxKind.CoalesceAssignmentExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + PostfixUnaryExpression( + SyntaxKind.SuppressNullableWarningExpression, + IdentifierName("_current") + ), + IdentifierName($"_current{x.Identifier}") + ), + ImplicitObjectCreationExpression() + ) + ), + IdentifierName("Enqueue") + ), + ArgumentList( + SingletonSeparatedList( + Argument(IdentifierName("action")) + ) + ) + ) + ) + ) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + ), + .. delegates + ] + ); + } + + private static BlockSyntax GetExpectationExecutionBody( + bool isVoidReturn, + string expectationIdentifier, + ArgumentListSyntax argList, + TypeArgumentListSyntax? typeParams + ) => + Block( + (IEnumerable) + + [ + IfStatement( + BinaryExpression( + SyntaxKind.LogicalAndExpression, + ParenthesizedExpression( + BinaryExpression( + SyntaxKind.CoalesceExpression, + InvocationExpression( + ConditionalAccessExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + PostfixUnaryExpression( + SyntaxKind.SuppressNullableWarningExpression, + IdentifierName("_current") + ), + IdentifierName($"_current{expectationIdentifier}") + ), + MemberBindingExpression(IdentifierName("TryDequeue")) + ), + ArgumentList( + SingletonSeparatedList( + Argument( + null, + Token(SyntaxKind.OutKeyword), + DeclarationExpression( + IdentifierName( + Identifier( + TriviaList(), + SyntaxKind.VarKeyword, + "var", + "var", + TriviaList() + ) + ), + SingleVariableDesignation( + Identifier("expectationObj") + ) + ) + ) + ) + ) + ), + LiteralExpression(SyntaxKind.FalseLiteralExpression) + ) + ), + IsPatternExpression( + IdentifierName("expectationObj"), + DeclarationPattern( + typeParams is null + ? IdentifierName(expectationIdentifier) + : GenericName( + Identifier(expectationIdentifier), + typeParams + ), + SingleVariableDesignation(Identifier("expectation")) + ) + ) + ), + isVoidReturn + ? ExpressionStatement( + InvocationExpression(IdentifierName("expectation"), argList) + ) + : ReturnStatement( + InvocationExpression(IdentifierName("expectation"), argList) + ), + ElseClause( + ExpressionStatement( + ThrowExpression( + ImplicitObjectCreationExpression( + ArgumentList( + SingletonSeparatedList( + Argument( + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal( + $"No matching expectations for {expectationIdentifier}." + ) + ) + ) + ) + ), + null + ) + ) + ) + ) + ), + ] + ); + + /// + public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) + { + CreateDelegatesForMember(node); + return base.VisitMethodDeclaration( + node.WithModifiers( + TokenList(node.Modifiers.Where(x => !x.IsKind(SyntaxKind.AbstractKeyword))) + ) + .WithBody( + GetExpectationExecutionBody( + node.ReturnType.ToString() == "void", + $"{node.Identifier}Expectation", + ArgumentList( + SeparatedList( + node.ParameterList.Parameters.Select(x => + Argument(IdentifierName(x.Identifier)) + .WithRefKindKeyword( + x.Modifiers.FirstOrDefault(y => + y.Kind() + is SyntaxKind.InKeyword + or SyntaxKind.OutKeyword + or SyntaxKind.RefKeyword + ) + ) + ) + ) + ), + node.TypeParameterList is null + ? null + : TypeArgumentList( + SeparatedList( + node.TypeParameterList.Parameters.Select(x => + IdentifierName(x.Identifier) + ) + ) + ) + ) + ) + .WithSemicolonToken(default) + ); + } + + /// + public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + CreateDelegatesForMember(node); + return base.VisitPropertyDeclaration( + node.WithModifiers( + TokenList(node.Modifiers.Where(x => !x.IsKind(SyntaxKind.AbstractKeyword))) + ) + .WithAccessorList( + ( + node.AccessorList + ?? throw new InvalidOperationException( + "Bad property declaration for mocking." + ) + ).WithAccessors( + List( + node.AccessorList.Accessors.Select(x => + x.WithBody( + GetExpectationExecutionBody( + !x.Keyword.IsKind(SyntaxKind.GetKeyword), + $"{x.Keyword.Kind().ToString()[..^"Keyword".Length]}{node.Identifier}Expectation", + x.Keyword.IsKind(SyntaxKind.GetKeyword) + ? ArgumentList() + : ArgumentList( + SingletonSeparatedList( + Argument(IdentifierName("value")) + ) + ), + null + ) + ) + .WithSemicolonToken(default) + ) + ) + ) + ) + ); + } + + private void CreateDelegatesForMember(MemberDeclarationSyntax decl) + { + if (!decl.Modifiers.Any(SyntaxKind.AbstractKeyword)) + { + return; + } + + var members = decl switch + { + MethodDeclarationSyntax meth + => + [ + DelegateDeclaration(meth.ReturnType, $"{meth.Identifier}Expectation") + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithParameterList(meth.ParameterList) + .WithTypeParameterList(meth.TypeParameterList) + ], + PropertyDeclarationSyntax prop + => prop.AccessorList?.Accessors.Select(x => + ( + x.Keyword.Kind() switch + { + SyntaxKind.GetKeyword + => DelegateDeclaration( + prop.Type, + $"Get{prop.Identifier}Expectation" + ), + SyntaxKind.SetKeyword + or SyntaxKind.AddKeyword + or SyntaxKind.RemoveKeyword + => DelegateDeclaration( + PredefinedType(Token(SyntaxKind.VoidKeyword)), + $"{x.Keyword.Kind().ToString()[..^"Keyword".Length]}{prop.Identifier}Expectation" + ) + .WithParameterList( + ParameterList( + SingletonSeparatedList(Parameter(Identifier("value"))) + ) + ), + _ => throw new ArgumentOutOfRangeException() + } + ).WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + ) ?? [], + _ => throw new ArgumentOutOfRangeException(nameof(decl)) + }; + + ( + _delegates.Value + ?? throw new InvalidOperationException( + $"{nameof(VisitInterfaceDeclaration)} has not been called" + ) + ).AddRange(members); + } +} diff --git a/sources/Core/Core/Abstractions/AndroidPlatformInfo.cs b/sources/Core/Core/Abstractions/AndroidPlatformInfo.cs index 545a0fc52e..621cde83e7 100644 --- a/sources/Core/Core/Abstractions/AndroidPlatformInfo.cs +++ b/sources/Core/Core/Abstractions/AndroidPlatformInfo.cs @@ -5,11 +5,4 @@ namespace Silk.NET.Core; /// /// ANativeWindow*. /// EGLSurface. -/// -/// Any handles relevant to the next layer down from this platform in cases where wrapper APIs are used. -/// -public readonly record struct AndroidPlatformInfo( - nint Window, - nint Surface, - IPlatformInfo? Next = null -) : IPlatformInfo; +public readonly record struct AndroidPlatformInfo(nint Window, nint Surface); diff --git a/sources/Core/Core/Abstractions/BreakneckLock.cs b/sources/Core/Core/Abstractions/BreakneckLock.cs new file mode 100644 index 0000000000..588b2d3576 --- /dev/null +++ b/sources/Core/Core/Abstractions/BreakneckLock.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Sourced from https://github.com/john-h-k/SpinLockSlim under the MIT license + +// MIT License +// +// Copyright (c) 2019 John Kelly +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +// ReSharper disable RedundantAssignment + +namespace Silk.NET.Core; + +/// +/// Provided a lightweight spin lock for synchronization in high performance +/// scenarios with a low hold time +/// +/// +/// This lock is very performant, but it is very dangerous (hence breakneck). +/// It's recommended to use the framework-provided locks where possible. +/// +public struct BreakneckLock +{ + private static int True => 1; + private static int False => 0; + + private const MethodImplOptions MaxOpt = (MethodImplOptions)768; + + private volatile int _acquired; // either 1 or 0 + + /// + /// Creates a new + /// + /// A new + [MethodImpl(MaxOpt)] + public static BreakneckLock Create() => new(); + + /// + /// Returns true if the lock is acquired, else false + /// +#pragma warning disable 420 // Unsafe.As<,> doesn't read the reference so the lack of volatility is not an issue, but we do need to treat the returned reference as volatile + public bool IsAcquired => Volatile.Read(ref Unsafe.As(ref _acquired)); +#pragma warning restore 420 + + /// + /// Enter the lock. If this method returns, + /// will be true. If an exception occurs, will indicate + /// whether the lock was taken and needs to be released using . + /// This method may never exit + /// + /// A reference to a bool that indicates whether the lock is taken. Must + /// be false when passed, else the internal state or return state may be corrupted. + /// If the method returns, this is guaranteed to be true + [MethodImpl(MaxOpt)] + public void Enter(ref bool taken) + { + // while acquired == 1, loop, then when it == 0, exit and set it to 1 + while (!TryAcquire()) + { + // NOP + } + + taken = true; + } + + /// + /// Enter the lock if it not acquired, else, do not. will be + /// true if the lock was taken, else false. If is + /// true, must be called to release it, else, it must not be called + /// + /// A reference to a bool that indicates whether the lock is taken. Must + /// be false when passed, else the internal state or return state may be corrupted + [MethodImpl(MaxOpt)] + public void TryEnter(ref bool taken) + { + taken = TryAcquire(); + } + + /// + /// Try to safely enter the lock a certain number of times (). + /// will be true if the lock was taken, else false. + /// If is true, must be called to release + /// it, else, it must not be called + /// + /// A reference to a bool that indicates whether the lock is taken. Must + /// be false when passed, else the internal state or return state may be corrupted + /// The number of attempts to acquire the lock before returning + /// without the lock + [MethodImpl(MaxOpt)] + public void TryEnter(ref bool taken, uint iterations) + { + // if it acquired == 0, change it to 1 and return true, else return false + while (!TryAcquire()) + { + if (unchecked(iterations--) == 0) // postfix decrement, so no issue if iterations == 0 at first + { + return; + } + } + + taken = true; + } + + /// + /// Try to safely enter the lock for a certain (). + /// will be true if the lock was taken, else false. + /// If is true, must be called to release + /// it, else, it must not be called + /// + /// A reference to a bool that indicates whether the lock is taken. Must + /// be false when passed, else the internal state or return state may be corrupted + /// The to attempt to acquire the lock for before + /// returning without the lock. A negative will cause undefined behaviour + [MethodImpl(MaxOpt)] + public void TryEnter(ref bool taken, TimeSpan timeout) + { + long start = Stopwatch.GetTimestamp(); + long end = unchecked((long)timeout.TotalMilliseconds * Stopwatch.Frequency + start); + + // if it acquired == 0, change it to 1 and return true, else return false + while (!TryAcquire()) + { + if (Stopwatch.GetTimestamp() >= end) + { + return; + } + } + + taken = true; + } + + /// + /// Exit the lock. This method is dangerous and must be called only once the caller is sure they have + /// ownership of the lock. + /// + [MethodImpl(MaxOpt)] + public void Exit() + { + // release the lock - int32 write will always be atomic + _acquired = False; + } + + /// + /// Exit the lock with an optional post-release memory barrier. This method is dangerous and must be called only + /// once the caller is sure they have ownership of the lock. + /// + /// Whether a memory barrier should be inserted after the release + [MethodImpl(MaxOpt)] + public void Exit(bool insertMemBarrier) + { + Exit(); + + if (insertMemBarrier) + Thread.MemoryBarrier(); + } + + /// + /// Exit the lock with a post-release memory barrier. This method is dangerous and must be called only once the + /// caller is sure they have ownership of the lock. + /// + [MethodImpl(MaxOpt)] + public void ExitWithBarrier() + { + Exit(); + Thread.MemoryBarrier(); + } + + [MethodImpl(MaxOpt)] + private bool TryAcquire() + { + // if it acquired == 0, change it to 1 and return true, else return false + return Interlocked.CompareExchange(ref _acquired, True, False) == False; + } +} diff --git a/sources/Core/Core/Abstractions/BreakneckRequest`1.cs b/sources/Core/Core/Abstractions/BreakneckRequest`1.cs new file mode 100644 index 0000000000..9f4c77d0b7 --- /dev/null +++ b/sources/Core/Core/Abstractions/BreakneckRequest`1.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Core; + +/// +/// Provides a lightweight mechanism for requesting a resource from a producer thread and spinning until the request is +/// fulfilled. +/// +/// +/// This struct is very performant, but it is very dangerous (hence breakneck). +/// It's recommended to use the framework-provided locks where possible. +/// +/// The resource to request. +public struct BreakneckRequest +{ + private int _currentSerial; + private int _requestedSerial; + private int _gate; + private TResource _result; + + /// + /// Used by the producing thread only to determine whether the resource is requested. + /// + public bool IsRequested => _requestedSerial != _currentSerial; + + /// + /// Requests the resource. + /// + /// The resource. + public TResource Request() + { + // 1. Get a ticket - we use Interlocked here because there are potentially many threads doing the same. + var ourTicket = Interlocked.Increment(ref _requestedSerial); + + // 2. Wait in line. + while (Interlocked.CompareExchange(ref _currentSerial, ourTicket, ourTicket) != ourTicket) + { } + + // 5. Get the result. + var ret = _result; + _result = default!; + Interlocked.Decrement(ref _gate); // 0 = "I have picked up my things" + return ret; + } + + /// + /// Provides the resource requested. + /// + /// The resource. + /// + /// This must only be called once it is validated that is true. Failure to follow + /// this will result in this function hanging forever. + /// + public void Provide(TResource resource) + { + // 3. Wait for the last requester in line to pick up their things. + while (Interlocked.CompareExchange(ref _gate, 1, 0) != 0) { } + + // 4. Serve the next ticket. + _result = resource; + Interlocked.Increment(ref _currentSerial); + } +} diff --git a/sources/Core/Core/Abstractions/BreakneckRequest`2.cs b/sources/Core/Core/Abstractions/BreakneckRequest`2.cs new file mode 100644 index 0000000000..fa33e7ebff --- /dev/null +++ b/sources/Core/Core/Abstractions/BreakneckRequest`2.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Silk.NET.Core; + +/// +/// Provides a lightweight mechanism for requesting a resource with request data from a producer thread and spinning +/// until the request is fulfilled. +/// +/// +/// This struct is very performant, but it is very dangerous (hence breakneck). +/// It's recommended to use the framework-provided locks where possible. +/// +/// The resource to request. +/// The parameters of the request. +public struct BreakneckRequest +{ + private int _currentSerial; + private int _requestedSerial; + private int _gate; + private TParameters _params; + private TResource _result; + + /// + /// Requests the resource. + /// + /// The resource. + public TResource Request(TParameters parameters) + { + // 1. Get a ticket - we use Interlocked here because there are potentially many threads doing the same. + var ourTicket = Interlocked.Increment(ref _requestedSerial); + + // 2. Wait in line. + while (Interlocked.CompareExchange(ref _currentSerial, ourTicket, ourTicket) != ourTicket) + { } + + // 6. Provide our parameters + _params = parameters; + Interlocked.Decrement(ref _gate); // 2 = "I have sent my parameters" + + // 9. Wait for the provider to give us the resource. + while (_gate != 1) { } + + // 10. Get the result. + var ret = _result; + _result = default!; + Interlocked.Decrement(ref _gate); // 0 = "I have picked up my things" + return ret; + } + + /// + /// Attempts to retrieve a request. + /// + /// The request parameters. + /// True if a request was found, false otherwise. + /// + /// must be called if this function returns true. + /// + public bool TryGetRequest([NotNullWhen(true)] out TParameters? parameters) + { + // 3. Determine whether we have a request to serve + if (_requestedSerial == _currentSerial) + { + parameters = default; + return false; + } + + // 4. Wait for the last requester in line to pick up their things. + while (Interlocked.CompareExchange(ref _gate, 3, 0) != 0) { } + + // 5. Serve the next ticket. + Interlocked.Increment(ref _currentSerial); + + // 7. Wait for the requester to give us their parameters. + while (_gate != 2) { } + parameters = _params!; + return true; + } + + /// + /// Provides the resource requested. + /// + /// The resource. + public void Provide(TResource resource) + { + // 8. Provide the result. + _result = resource; + Interlocked.Decrement(ref _gate); // 1 = "I now have the resource" + } +} diff --git a/sources/Core/Core/Abstractions/CocoaPlatformInfo.cs b/sources/Core/Core/Abstractions/CocoaPlatformInfo.cs index 3f0168881a..4ead42b322 100644 --- a/sources/Core/Core/Abstractions/CocoaPlatformInfo.cs +++ b/sources/Core/Core/Abstractions/CocoaPlatformInfo.cs @@ -4,8 +4,4 @@ namespace Silk.NET.Core; /// The Cocoa platform-specific handles. /// /// NSWindow. -/// -/// Any handles relevant to the next layer down from this platform in cases where wrapper APIs are used. -/// -public readonly record struct CocoaPlatformInfo(nint Window, IPlatformInfo? Next = null) - : IPlatformInfo; +public readonly record struct CocoaPlatformInfo(nint Window); diff --git a/sources/Core/Core/Abstractions/EGLPlatformInfo.cs b/sources/Core/Core/Abstractions/EGLPlatformInfo.cs index 9960ab0615..04308d3d9f 100644 --- a/sources/Core/Core/Abstractions/EGLPlatformInfo.cs +++ b/sources/Core/Core/Abstractions/EGLPlatformInfo.cs @@ -5,11 +5,4 @@ namespace Silk.NET.Core; /// /// EGLDisplay. /// EGLSurface. -/// -/// Any handles relevant to the next layer down from this platform in cases where wrapper APIs are used. -/// -public readonly record struct EGLPlatformInfo( - nint Display, - nint Surface, - IPlatformInfo? Next = null -) : IPlatformInfo; +public readonly record struct EGLPlatformInfo(nint Display, nint Surface); diff --git a/sources/Core/Core/Abstractions/HluExtensions.cs b/sources/Core/Core/Abstractions/HluExtensions.cs deleted file mode 100644 index a19d5b8c9c..0000000000 --- a/sources/Core/Core/Abstractions/HluExtensions.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Silk.NET.Core; - -/// -/// Contains extension methods (and utility APIs) for HLU types. -/// -public static class HluExtensions -{ - /// - /// Gets an implementation of the given component type if one has already been defined and, if not, queries the - /// registry to configure the component for the given host. - /// - /// The component host. - /// The container of configuration with which to initialise components. - /// The component type. - /// The type of the component host. - /// The configuration container type. - /// - /// The registry of component implementations, should the component type not already be configured on the host. - /// - /// The component. - /// - /// The component was missing in both the host and the registry (or otherwise wasn't added). - /// - public static TComponent GetOrAdd( - THost host, - TConfiguration config - ) - where THost : IHluComponentHost - where TConfiguration : IHluConfiguration - where TRegistry : IHluComponentRegistry - { - if (host.TryGet(out var existing)) - { - return existing; - } - - if ( - !TRegistry.TryConfigureComponent( - host, - config - ) || !host.TryGet(out var comp) - ) - { - throw new HluMissingComponentException(typeof(TComponent).FullName); - } - - return comp; - } - - /// - /// Gets a component or throws if it's not present. - /// - /// The component host. - /// The component type. - /// The component. - /// The component was not present on the host. - public static TComponent Get(this IHluComponentHost host) - { - if (!host.TryGet(out var comp)) - { - throw new HluMissingComponentException(typeof(TComponent).FullName); - } - - return comp; - } -} diff --git a/sources/Core/Core/Abstractions/HluHostedComponentAttribute.cs b/sources/Core/Core/Abstractions/HluHostedComponentAttribute.cs deleted file mode 100644 index ee686a3027..0000000000 --- a/sources/Core/Core/Abstractions/HluHostedComponentAttribute.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Silk.NET.Core; - -/// -/// An attribute used to denote a field as hosting a component in implementations of . -/// This attribute is recognised by the Silk.NET.Core.Analyzers source generators to produce an implementation of -/// on classes marked with this attribute for each hosted component. -///
-/// Please see for parameter usage. -///
-/// -/// For efficiency, some commonly-used components can be stored in fields instead of a map of s to -/// s (thereby requiring runtime checks upon usage). -/// -/// -/// Whether the component is optional. If it isn't, the generated Create method will throw if the component has -/// not been configured. This has no effect when used on a parameter, please use nullability annotations instead. -/// -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter)] -public sealed class HluHostedComponentAttribute(bool optional = false) : Attribute() -{ - /// - /// Whether the component is optional. If it isn't, the generated Create method will throw if the component - /// has not been configured. - /// - public bool IsOptional { get; } = optional; - - /// - /// Parent component types for which there may be multiple hosted components that additionally conform to the given - /// component type. For example, IGLComponent and IVkComponent could be grouped into - /// IGraphicsComponent. Groups should be used to optimise the storage of components that have similar use - /// cases but differing applicability. When the HLU source generator is used, component groups have the following - /// properties: - /// - /// - /// For : - /// - /// Any attempt to set TComponent that already has a component in any of the given groups will be refused. - /// To reuse the previous example, you can't set IVkComponent if IGLComponent has already been set. - /// In addition, calling this method where TComponent is the group type will be refused unless the operation - /// can succeed with TComponent being implicitly substituted with any of the grouped component types. That - /// is, setting IGraphicsComponent will attempt to recurse into setting IGLComponent or - /// IVkComponent if that is doable given the TImpl provided. - /// - /// - /// - /// For : - /// - /// If TComponent is the group type, the first configured component that is a member of that group will be - /// returned. This will have consistent behaviour due to the guarantees provided in - /// . TComponent being a type of a grouped component - /// will work in the same way as if that component wasn't grouped. - /// - /// - /// - /// - public Type[]? Groups { get; init; } -} diff --git a/sources/Core/Core/Abstractions/HluMissingComponentException.cs b/sources/Core/Core/Abstractions/HluMissingComponentException.cs deleted file mode 100644 index a80c3b8f64..0000000000 --- a/sources/Core/Core/Abstractions/HluMissingComponentException.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Silk.NET.Core; - -/// -/// Thrown when a HLU component is missing. -/// -/// The name of the missing component. -public class HluMissingComponentException(string? component) - : Exception( - $"The operation could not continue due to a missing component: {component}. This likely indicates an issue " - + "resolving a native library, but could indicate an incomplete backend." - ) -{ - /// - /// Throws a for if is - /// null. - /// - /// The object to check for null. - /// The component type. - /// If is null. - public static void ThrowIfNull(T? obj) - { - if (obj is null) - { - throw new HluMissingComponentException(typeof(T).FullName); - } - } -} diff --git a/sources/Core/Core/Abstractions/HluRegisteredComponentAttribute`2.cs b/sources/Core/Core/Abstractions/HluRegisteredComponentAttribute`2.cs deleted file mode 100644 index 011761c812..0000000000 --- a/sources/Core/Core/Abstractions/HluRegisteredComponentAttribute`2.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Silk.NET.Core; - -/// -/// Registers a component on a registry. This will cause the Silk.NET.Core.Analyzers source generators to -/// enumerate all constructors on and try to populate the parameters using -/// or, if the parameter is annotated with , -/// the or in use. This can be used to declare -/// dependencies on configuration or other components. The constructors are attempted in order of most parameters to -/// least. This logic will be provided within a implementation on the attributed -/// class. -/// -/// The component type. -/// The component implementation. -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor)] -public class HluRegisteredComponentAttribute(bool isDefault = true) : Attribute -{ - /// - /// Whether the registry will configure the as part of its - /// . - /// - public bool IsDefault { get; } = isDefault; - - /// - /// Whether to override the existing implementation (if any). - /// - public bool Override { get; init; } -} diff --git a/sources/Core/Core/Abstractions/IHluComponentHost.cs b/sources/Core/Core/Abstractions/IHluComponentHost.cs deleted file mode 100644 index c5466a0e2f..0000000000 --- a/sources/Core/Core/Abstractions/IHluComponentHost.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; - -namespace Silk.NET.Core; - -/// -/// A container of multiple components. -/// -public interface IHluComponentHost -{ - /// - /// Throws if the component host is missing required components. - /// - void ThrowIfMisconfigured(); - - /// - /// Gets an implementation of the given component. - /// - /// - /// The component implementation, or default if an implementation wasn't present. - /// - /// The component contract type - not the implementation type. - /// Whether the component was present in this container. - bool TryGet([NotNullWhen(true)] out TComponent? component); - - /// - /// Adds or overrides a component with the given implementation. - /// - /// The component implementation. - /// - /// The component contract type - not the implementation type. Corresponds with the type parameter for\ - /// . - /// - /// The implementation type. - /// - /// Configuring components that have already been configured on the host should be avoided unless it is explicitly - /// part of that component's contract. - /// - /// Whether the component was added. Currently, this is always true. - bool TrySet(TImpl comp) - where TImpl : TComponent; -} diff --git a/sources/Core/Core/Abstractions/IHluComponentRegistry.cs b/sources/Core/Core/Abstractions/IHluComponentRegistry.cs deleted file mode 100644 index 47ae66634b..0000000000 --- a/sources/Core/Core/Abstractions/IHluComponentRegistry.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Silk.NET.Core; - -/// -/// A source of component implementations. -/// -public interface IHluComponentRegistry -{ - /// - /// Configures the default components for this registry on a component host. In case this is a nested call, the - /// original registry used is provided should component dependencies need to be resolved while respecting user - /// precedence. - /// - /// The host on which to register components. - /// The source of configurations with which to initialise components. - /// The host type. - /// The type of the configuration source. - /// - /// The original registry. This may differ from the type implementing this interface if this registry is nested - /// within another registry. - /// - static abstract void ConfigureDefaults( - THost dest, - TConfiguration config - ) - where THost : IHluComponentHost - where TConfiguration : IHluConfiguration - where TRegistry : IHluComponentRegistry; - - /// - /// Configures an individual component. This is typically used within - /// to configure component dependencies while - /// respecting user precedence. - /// - /// The host on which to configure components. - /// The source of configurations with which to initialise components. - /// - /// The component contract type - this is not the implementation type. This is the type used in calls to - /// . - /// - /// The host type. - /// The type of the configuration source. - /// - /// The original registry. This may differ from the type implementing this interface if this registry is nested - /// within another registry. - /// - /// Whether the component was configured. - static abstract bool TryConfigureComponent( - THost dest, - TConfiguration config - ) - where THost : IHluComponentHost - where TConfiguration : IHluConfiguration - where TRegistry : IHluComponentRegistry; - - /// - /// Chains two implementations of together. This is used to facilitate the - /// builder pattern while preserving typing. - /// - /// The type of the first configuration to check. - /// The type of the second configuration to check. - public readonly record struct Chain : IHluComponentRegistry - where T1 : IHluComponentRegistry - where T2 : IHluComponentRegistry - { - /// - public static void ConfigureDefaults( - THost dest, - TConfiguration config - ) - where THost : IHluComponentHost - where TConfiguration : IHluConfiguration - where TRegistry : IHluComponentRegistry - { - T1.ConfigureDefaults(dest, config); - T2.ConfigureDefaults(dest, config); - } - - /// - public static bool TryConfigureComponent( - THost dest, - TConfiguration config - ) - where THost : IHluComponentHost - where TConfiguration : IHluConfiguration - where TRegistry : IHluComponentRegistry => - T1.TryConfigureComponent(dest, config) - || T2.TryConfigureComponent(dest, config); - } -} diff --git a/sources/Core/Core/Abstractions/IHluConfiguration.cs b/sources/Core/Core/Abstractions/IHluConfiguration.cs deleted file mode 100644 index e775e3ff4c..0000000000 --- a/sources/Core/Core/Abstractions/IHluConfiguration.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; - -namespace Silk.NET.Core; - -/// -/// Represents a type that holds initial configurations for components, to be captured upon component instantiation. -/// -public interface IHluConfiguration -{ - /// - /// Attempts to get a configuration of a specific type. - /// - /// The configuration if present. default if not. - /// The configuration type. - /// Whether the configuration was present. - bool TryGetConfiguration([NotNullWhen(true)] out T? config); - - /// - /// Chains two implementations of together. This is used to facilitate the builder - /// pattern while preserving typing. - /// - /// The type of the first configuration to check. - /// The type of the second configuration to check. - public readonly record struct Chain(T1 Left, T2 Right) : IHluConfiguration - where T1 : IHluConfiguration - where T2 : IHluConfiguration - { - /// - public bool TryGetConfiguration([NotNullWhen(true)] out T? config) => - Left.TryGetConfiguration(out config) || Right.TryGetConfiguration(out config); - } -} diff --git a/sources/Core/Core/Abstractions/IPlatformInfo.cs b/sources/Core/Core/Abstractions/IPlatformInfo.cs deleted file mode 100644 index 6ce501267f..0000000000 --- a/sources/Core/Core/Abstractions/IPlatformInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Silk.NET.Core; - -/// -/// Base component for platform info components. -/// -public interface IPlatformInfo -{ - /// - /// Gets any handles relevant to the next layer down from this platform in cases where wrapper APIs are used. - /// - IPlatformInfo? Next { get; } -} diff --git a/sources/Core/Core/Abstractions/UIKitPlatformInfo.cs b/sources/Core/Core/Abstractions/UIKitPlatformInfo.cs index cf4d541550..468673f1f7 100644 --- a/sources/Core/Core/Abstractions/UIKitPlatformInfo.cs +++ b/sources/Core/Core/Abstractions/UIKitPlatformInfo.cs @@ -7,13 +7,9 @@ namespace Silk.NET.Core; /// OpenGL framebuffer handle. /// OpenGL color buffer handle. /// OpenGL resolve framebuffer handle. -/// -/// Any handles relevant to the next layer down from this platform in cases where wrapper APIs are used. -/// public readonly record struct UIKitPlatformInfo( nint Window, uint Framebuffer, uint ColorBuffer, - uint ResolveFramebuffer, - IPlatformInfo? Next = null -) : IPlatformInfo; + uint ResolveFramebuffer +); diff --git a/sources/Core/Core/Abstractions/VivantePlatformInfo.cs b/sources/Core/Core/Abstractions/VivantePlatformInfo.cs index 40975a63f3..25a02093f5 100644 --- a/sources/Core/Core/Abstractions/VivantePlatformInfo.cs +++ b/sources/Core/Core/Abstractions/VivantePlatformInfo.cs @@ -5,11 +5,4 @@ namespace Silk.NET.Core; /// /// EGLNativeDisplayType. /// EGLNativeWindowType. -/// -/// Any handles relevant to the next layer down from this platform in cases where wrapper APIs are used. -/// -public readonly record struct VivantePlatformInfo( - nint Display, - nint Window, - IPlatformInfo? Next = null -) : IPlatformInfo; +public readonly record struct VivantePlatformInfo(nint Display, nint Window); diff --git a/sources/Core/Core/Abstractions/WaylandPlatformInfo.cs b/sources/Core/Core/Abstractions/WaylandPlatformInfo.cs index a68a7e224f..3dfa5a67d1 100644 --- a/sources/Core/Core/Abstractions/WaylandPlatformInfo.cs +++ b/sources/Core/Core/Abstractions/WaylandPlatformInfo.cs @@ -5,11 +5,4 @@ namespace Silk.NET.Core; /// /// wl_display. /// wl_surface. -/// -/// Any handles relevant to the next layer down from this platform in cases where wrapper APIs are used. -/// -public readonly record struct WaylandPlatformInfo( - nint Display, - nint Surface, - IPlatformInfo? Next = null -) : IPlatformInfo; +public readonly record struct WaylandPlatformInfo(nint Display, nint Surface); diff --git a/sources/Core/Core/Abstractions/Win32PlatformInfo.cs b/sources/Core/Core/Abstractions/Win32PlatformInfo.cs index cf692a1c1c..374c5bdc95 100644 --- a/sources/Core/Core/Abstractions/Win32PlatformInfo.cs +++ b/sources/Core/Core/Abstractions/Win32PlatformInfo.cs @@ -6,12 +6,4 @@ namespace Silk.NET.Core; /// HWND. /// HDC. /// HInstance. -/// -/// Any handles relevant to the next layer down from this platform in cases where wrapper APIs are used. -/// -public readonly record struct Win32PlatformInfo( - nint Hwnd, - nint HDC, - nint HInstance, - IPlatformInfo? Next = null -) : IPlatformInfo; +public readonly record struct Win32PlatformInfo(nint Hwnd, nint HDC, nint HInstance); diff --git a/sources/Core/Core/Abstractions/WinRTPlatformInfo.cs b/sources/Core/Core/Abstractions/WinRTPlatformInfo.cs index 32d7fb6533..58ccb7a95e 100644 --- a/sources/Core/Core/Abstractions/WinRTPlatformInfo.cs +++ b/sources/Core/Core/Abstractions/WinRTPlatformInfo.cs @@ -4,8 +4,4 @@ namespace Silk.NET.Core; /// The WinRT platform-specific handles. /// /// IInspectable. -/// -/// Any handles relevant to the next layer down from this platform in cases where wrapper APIs are used. -/// -public readonly record struct WinRTPlatformInfo(nint Inspectable, IPlatformInfo? Next = null) - : IPlatformInfo; +public readonly record struct WinRTPlatformInfo(nint Inspectable); diff --git a/sources/Core/Core/Abstractions/X11PlatformInfo.cs b/sources/Core/Core/Abstractions/X11PlatformInfo.cs index cb08e1cecc..2aba92d219 100644 --- a/sources/Core/Core/Abstractions/X11PlatformInfo.cs +++ b/sources/Core/Core/Abstractions/X11PlatformInfo.cs @@ -5,8 +5,4 @@ namespace Silk.NET.Core; /// /// X11Display. /// X11Window. -/// -/// Any handles relevant to the next layer down from this platform in cases where wrapper APIs are used. -/// -public readonly record struct X11PlatformInfo(nint Display, nint Window, IPlatformInfo? Next = null) - : IPlatformInfo; +public readonly record struct X11PlatformInfo(nint Display, nint Window); diff --git a/sources/Core/Core/DSL/Default.cs b/sources/Core/Core/DSL/Default.cs index 9418fe9b5a..2e3ace774d 100644 --- a/sources/Core/Core/DSL/Default.cs +++ b/sources/Core/Core/DSL/Default.cs @@ -17,14 +17,10 @@ namespace Silk.NET.Core; /// /// /// -/// -/// Use this in place of a type parameter in builder pattern types to represent the lack -/// of extra configurations. -/// /// /// /// -public struct Default : IBoolScheme, IHluConfiguration +public struct Default : IBoolScheme { /// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] @@ -35,11 +31,4 @@ public static T True() ret++; return ret; } - - /// - public bool TryGetConfiguration([NotNullWhen(true)] out T? config) - { - config = default; - return false; - } } diff --git a/sources/SDL/Extensions.cs b/sources/SDL/Extensions.cs index 5bea66005b..8f827afda3 100644 --- a/sources/SDL/Extensions.cs +++ b/sources/SDL/Extensions.cs @@ -26,4 +26,20 @@ public static void ThrowError(this ISdl sdl) sdl.ClearError(); } } + + public static void ThrowSdlError(this int ec) + { + if (ec != 0) + { + Sdl.ThrowError(); + } + } + + public static void ThrowSdlError(this int ec, ISdl sdl) + { + if (ec != 0) + { + sdl.ThrowError(); + } + } } diff --git a/sources/Windowing/Common/Components/ISurface.cs b/sources/Windowing/Common/Components/ISurface.cs index 22ead165e9..f1919b8849 100644 --- a/sources/Windowing/Common/Components/ISurface.cs +++ b/sources/Windowing/Common/Components/ISurface.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; +using Silk.NET.Windowing.Hosting; namespace Silk.NET.Windowing; @@ -10,21 +11,21 @@ namespace Silk.NET.Windowing; /// Represents a native surface that has a drawable area (using operating system APIs) and that produces operating /// system events. /// -public interface ISurface +public interface ISurface : IDisposable { /// /// Gets the size of the client area (drawing area) in pixels. /// /// /// This accounts for high density displays and may differ from the size in window coordinates. - /// Note that for this reason it is expected not to be accurate until is called. + /// Note that for this reason it is expected not to be accurate until is called. /// Vector2 ClientSize { get; } /// - /// Gets a unique handle representing this surface. + /// Gets a unique handle representing this surface. This may be null if is not called. /// - nint Handle { get; } + SurfaceHandle Handle { get; } /// /// Runs the window's event loop, dispatching events to the given actor. This may be blocking, but won't always be @@ -34,6 +35,6 @@ public interface ISurface /// /// The actor receiving events. /// The type of the actor. - void Run(T actor) + void Launch(T actor) where T : ISurfaceActor; } diff --git a/sources/Windowing/Common/Configuration/IConfigureHost.cs b/sources/Windowing/Common/Configuration/IConfigureHost.cs new file mode 100644 index 0000000000..362712cf8e --- /dev/null +++ b/sources/Windowing/Common/Configuration/IConfigureHost.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Silk.NET.Windowing.Hosting; + +namespace Silk.NET.Windowing; + +/// +/// An interface implemented on structures that contain configuration data that can be applied to a +/// surface or surface request. +/// +public interface IConfigureHost +{ + /// + /// Copies the configuration contained in this structure to the surface/request with the given handle on + /// . + /// + /// The handle. + /// The handle type. + /// The surface host. + void ConfigureHost(THandle handle) + where THandle : ISurfaceOrRequestHandle + where THost : ISurfaceHost; + + /// + /// Chains the two configurations together. + /// + /// The first configuration. + /// The second configuration. + /// The type of the first configuration. + /// The type of the second configuration. + public readonly record struct Chain(TFirst First, TSecond Second) + : IConfigureHost + where TFirst : IConfigureHost + where TSecond : IConfigureHost + { + /// + public void ConfigureHost(THandle handle) + where THandle : ISurfaceOrRequestHandle + where THost : ISurfaceHost + { + First.ConfigureHost(handle); + Second.ConfigureHost(handle); + } + } + + /// + /// An implementation of that does nothing. + /// + public readonly record struct Null : IConfigureHost + { + /// + public void ConfigureHost(THandle handle) + where THandle : ISurfaceOrRequestHandle + where THost : ISurfaceHost { } + } +} diff --git a/sources/Windowing/Common/Configuration/IGLConfiguration.cs b/sources/Windowing/Common/Configuration/IGLConfiguration.cs index c89bc3225b..6713313103 100644 --- a/sources/Windowing/Common/Configuration/IGLConfiguration.cs +++ b/sources/Windowing/Common/Configuration/IGLConfiguration.cs @@ -17,7 +17,7 @@ public interface IGLConfiguration /// /// Pass null or -1 to use the system default. /// - int? PreferredDepthBufferBits { get; set; } + int? PreferredDepthBufferBits { get; } /// /// Preferred stencil buffer bits of the window's framebuffer. @@ -25,7 +25,7 @@ public interface IGLConfiguration /// /// Pass null or -1 to use the system default. /// - int? PreferredStencilBufferBits { get; set; } + int? PreferredStencilBufferBits { get; } /// /// Preferred red, green, blue, and alpha bits of the window's framebuffer. @@ -33,7 +33,7 @@ public interface IGLConfiguration /// /// Pass null or -1 for any of the channels to use the system default. /// - Vector4D? PreferredBitDepth { get; set; } + Vector4D? PreferredBitDepth { get; } /// /// The API version to use. diff --git a/sources/Windowing/Common/Configuration/ThreadConfiguration.cs b/sources/Windowing/Common/Configuration/ThreadConfiguration.cs new file mode 100644 index 0000000000..4be3cd6da0 --- /dev/null +++ b/sources/Windowing/Common/Configuration/ThreadConfiguration.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Silk.NET.Windowing.Hosting; + +namespace Silk.NET.Windowing; + +/// +/// Configures the (or a with similar +/// threading rules implemented in terms of this configuration) with the given threading configuration. This must be +/// applied to the root surface. +/// +/// +/// Whether a separate event thread should be used for the root surface. If false, the root surface will be +/// responsible for pumping events to the other surfaces. This has the advantage of having only one core with a busy +/// loop instead of two, but has the disadvantage of potentially allowing the render logic of the root surface to cause +/// delay in the delivery of events to child surfaces. If true, the main thread's sole responsibility shall be +/// the delivery of surface events to surfaces running on other threads. This configuration has no effect on child +/// surfaces as the event loop is owned by the root surface. +/// +/// +/// If false, the event pump shall not advance past an event until all surfaces have read that event, sending the +/// surfaces that have read that event into a busy loop in the meantime. If true, a buffer is maintained to +/// allow each surface to retrieve events at their own pace. This is useful in cases where one surface is slowing down +/// all other surface due to the event pump being advanced at a sufficient rate. +/// +/// This configuration is not guaranteed to be honoured by a backend. +// TODO +// TODO If the default implementation (i.e. ) is being used without a +// TODO (i.e. the +// TODO +public readonly record struct ThreadConfiguration( + bool UseSeparateEventThread = false, + bool UseBufferedEventLoop = false +) : IConfigureHost +{ + /// + public void ConfigureHost(THandle handle) + where THandle : ISurfaceOrRequestHandle + where THost : ISurfaceHost + { + THost.SetSurfaceProperty( + handle, + new SurfaceProperty + { + PropertyName = SurfacePropertyName.UseSeparateEventThread, + Boolean = UseSeparateEventThread + } + ); + THost.SetSurfaceProperty( + handle, + new SurfaceProperty + { + PropertyName = SurfacePropertyName.UseBufferedEventLoop, + Boolean = UseBufferedEventLoop + } + ); + } +} diff --git a/sources/Windowing/Common/Hosting/HostEventKind.cs b/sources/Windowing/Common/Hosting/HostEventKind.cs new file mode 100644 index 0000000000..4fbe660771 --- /dev/null +++ b/sources/Windowing/Common/Hosting/HostEventKind.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Windowing.Hosting; + +/// +/// The types of events received on a surface host. +/// +public enum HostEventKind +{ + /// + /// No event was retrieved from the event pump. + /// + None, + + /// + /// A property changed on one of the hosted surfaces. + /// + SurfacePropertyChanged +} diff --git a/sources/Windowing/Common/Hosting/HostStatus.cs b/sources/Windowing/Common/Hosting/HostStatus.cs new file mode 100644 index 0000000000..630c827643 --- /dev/null +++ b/sources/Windowing/Common/Hosting/HostStatus.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Windowing.Hosting; + +/// +/// Status for various functions. +/// +public enum HostStatus +{ + /// + /// The operation failed and is unrecoverable. + /// + Failure, + + /// + /// The operation has concluded successfully. + /// + Success, + + /// + /// The operation is still executing. + /// + Continuing +} diff --git a/sources/Windowing/Common/Hosting/IHostActor.cs b/sources/Windowing/Common/Hosting/IHostActor.cs new file mode 100644 index 0000000000..7321f73930 --- /dev/null +++ b/sources/Windowing/Common/Hosting/IHostActor.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Windowing.Hosting; + +/// +/// An actor that responds to messages regarding the lifecycle of all surfaces hosted on the . +/// +public interface IHostActor +{ + /// + /// Called upon initialisation of each surface on the . + /// + /// The surface initialising. + void HandleInit(SurfaceHandle surface); + + /// + /// Called at every possible opportunity. + /// + void HandleTick(); + + /// + /// Called when the given surface is shutting down. + /// + void HandleQuit(SurfaceHandle surface); +} diff --git a/sources/Windowing/Common/Hosting/ISurfaceHost.cs b/sources/Windowing/Common/Hosting/ISurfaceHost.cs new file mode 100644 index 0000000000..064cc037a7 --- /dev/null +++ b/sources/Windowing/Common/Hosting/ISurfaceHost.cs @@ -0,0 +1,234 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Silk.NET.Core.Analyzers; + +namespace Silk.NET.Windowing.Hosting; + +/// +/// Represents a raw API for creating surfaces. +/// +/// +/// Many APIs must only be called on the "main thread". This refers to the thread that calls the entry +/// assembly's main function. +/// +[MockStaticAbstract] +public interface ISurfaceHost +{ + /// + /// Whether multiple surfaces can be spawned as children of the first surface created. + /// + /// + /// This is expected to be thread-safe. This should not be called before . + /// + static abstract bool IsMultiSurface { get; } + + /// + /// Creates a surface request. + /// + /// + /// A surface request. Must be destroyed either using . + /// + /// + /// This can be called on any thread. Any implementation that cannot honour this should defer any thread-sensitive + /// operations to surface creation time. + /// + static abstract RequestHandle CreateSurfaceRequest(); + + /// + /// Duplicates the surface request represented by and copies the data into + /// . + /// + /// The destination request. + /// + /// The source handle. If this is a window, then a point-in-time snapshot of the window's state shall be taken and + /// replicated on a best-effort basis in the request. + /// + /// The source handle type. + /// + /// This must be called on the main thread. Assumed to be a slow path operation. + /// + static abstract void CopySurfaceRequest(RequestHandle dest, TSrc src) + where TSrc : ISurfaceOrRequestHandle; + + /// + /// Destroys a surface request. + /// + /// The surface request. + /// + /// This can be called on any thread. Any implementation that cannot honour this should defer any thread-sensitive + /// operations to surface creation time. + /// + static abstract void DestroySurfaceRequest(RequestHandle surfaceRequest); + + /// + /// Attempts to incorporate existing window handles as part of the request for the surface. + /// + /// The surface request. + /// The platform info. + /// The platform info type. + /// Returns true except where the handles are incompatible in which case false is returned. + /// + /// This can be called on any thread. Any implementation that cannot honour this should defer any thread-sensitive + /// operations to surface creation time. + /// + static abstract bool TryInheritPlatformInfo( + RequestHandle surfaceRequest, + TPlatformInfo info + ) + where TPlatformInfo : notnull; + + /// + /// Launches the main surface. This is blocking in most cases, but is not guaranteed to block. Do not use + /// using var in code that launches surfaces - instead defer to the callbacks on your surface actor. If this + /// method does not block, you shouldn't either. + /// + /// The surface request. + /// An actor responding to surface lifecycle pulses. + /// The type of the host actor. + /// + /// This must be called on the main thread. Assumed that this is only called once. + /// + static abstract HostStatus LaunchMainSurface(RequestHandle surfaceRequest, TActor actor) + where TActor : IHostActor; + + /// + /// Launches a child surface. The root surface receives + /// + /// The parent surface. + /// The request for the child surface. + /// The child surface handle. This is also delivered to the host actor. + static abstract SurfaceHandle CreateChildSurface( + SurfaceHandle parent, + RequestHandle surfaceRequest + ); + + /// + /// Terminates the surface. This results in the of the main surface receiving a final + /// event for this handle. + /// + /// + static abstract void TerminateSurface(SurfaceHandle surface); + + /// + /// Attempts to get the platform info for the given surface. + /// + /// The surface. + /// The retrieved platform info. + /// The type of the platform info to retrieve. + /// Whether the platform info was found. + /// + /// This must be called on the main thread. + /// + static abstract bool TryGetPlatformInfo( + SurfaceHandle handle, + [NotNullWhen(true)] out TPlatformInfo? platformInfo + ); + + /// + /// Gets a property value on the given surface/request. + /// + /// The surface or surface request handle. + /// The property to retrieve. + /// The property value. + /// A or . + /// + /// If the property is not supported, this should return a default value for the property. The onus is on the caller + /// to check whether the property (or feature in which the property is required) is supported. This must be called + /// on the main thread unless is a . + /// + static abstract SurfaceProperty GetSurfaceProperty( + T handle, + SurfacePropertyName propertyName + ) + where T : ISurfaceOrRequestHandle; + + /// + /// Gets a property value on the given surface/request. + /// + /// The surface or surface request handle. + /// The property name and value. + /// A or . + /// + /// If the property is not supported, this should do nothing. The onus is on the caller to check whether the + /// property (or feature in which the property is required) is supported. This must be called on the main thread + /// unless is a . + /// + static abstract void SetSurfaceProperty(T handle, SurfaceProperty property) + where T : ISurfaceOrRequestHandle; + + /// + /// Creates an event pump to receive events regarding hosted surfaces. + /// + /// + /// A surface from which all events from all related surfaces will be received. + /// + /// The event pump. + /// + /// This must be called on the main thread. Not every implementation has the concept of an event pump resource, but + /// this should return a non-zero handle in any case. + /// + // RATIONALE: Dylan Says: I was having a real debate with myself as I've been extremely critical of having static + // state in the past. It makes sense for the surface host API to be a static, C-like API to ease mapping it to C + // APIs with which it's implemented, but in the most part there's still a concept of memory management/resource + // ownership. In SDL event pumps are global resources, but I didn't want this to leak out to potentially impact + // other implementations for which this isn't the case and risk having dangling static state. So I introduced the + // event pump concept. It is expected to be unused in our SDL implementation. + // + // NOTE: This originally was to be "one event pump per root surface" but I changed this to technically allow + // multiple pumps to exist even if the underlying implementation doesn't. The reason behind this is the routing of + // events from the root HLU surface to the child HLU surfaces would incur a virtual method penalty as we'd be + // storing the surface actor as an interface. + static abstract EventPumpHandle CreateEventPump(SurfaceHandle relatedSurface); + + /// + /// Destroys an event pump. + /// + /// The event pump. + /// This must be called on the main thread. + static abstract void DestroyEventPump(EventPumpHandle pump); + + /// + /// Pumps the event queue and determines whether there is an event to handle. must be + /// used after the event is inspected, otherwise subsequent calls will return false. + /// + /// The event pump. + /// + /// An implementation-defined integer identifying the event received. This should be maintained across calls to the + /// event pump to ensure the correct event is subsequently received from the event pump. Must only be modified upon + /// a successful query. + /// + /// True if an event has been taken from the event queue, false otherwise. + /// This must be called on the main thread. + static abstract HostEventKind QueryEvent(EventPumpHandle pump, ref int @event); + + /// + /// Gets the surface associated with the event retrieved from . + /// + /// The event pump to query. + /// The event as returned from . + /// The surface with which the event currently represented by the event pump is associated. + /// + /// This is expected to be thread-safe, but this function must only be used between a call to + /// and . + /// + static abstract SurfaceHandle GetEventSurface(EventPumpHandle pump, int @event); + + /// + /// Acknowledges the event contained in the event pump, allowing to query a new event. + /// + /// The pump containing the event. + /// The event being acknowledged as returned from . + /// This must be called on the main thread. + static abstract void AcknowledgeEvent(EventPumpHandle pump, int @event); + + /// + /// Gets the property changed as part of a event. + /// + /// The event pump from which the event was received. + /// The event being acknowledged as returned from . + /// The property changed. + static abstract SurfaceProperty GetEventPropertyChanged(EventPumpHandle pump, int @event); +} diff --git a/sources/Windowing/Common/Hosting/MultiThreadedSurfaceHost`1.cs b/sources/Windowing/Common/Hosting/MultiThreadedSurfaceHost`1.cs new file mode 100644 index 0000000000..40e8d75c2c --- /dev/null +++ b/sources/Windowing/Common/Hosting/MultiThreadedSurfaceHost`1.cs @@ -0,0 +1,410 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using Silk.NET.Core; +using Silk.NET.Maths; + +namespace Silk.NET.Windowing.Hosting; + +/// +/// An implementation of that can be used across multiple threads. This is used to implement +/// the logic required to allow multiple surfaces to run concurrently while still respecting the typical single-threaded +/// requirements of surface hosts. The thread the main surface is launched from is assumed to be the main thread, and +/// will run the event loop. Every surface will have its own thread that calls back to this event thread. +/// +/// The underlying surface host. +public class MultiThreadedSurfaceHost : ISurfaceHost + where TUnderlying : ISurfaceHost +{ + internal static SurfaceHandle RootSurface; + internal static BreakneckRequest Comms; + internal static EventPumpHandle EventPump; + internal static int EventPumpRefCount; + internal static int EventPumpRemainingAcks; + internal static int CurrentEvent; + internal static HostEventKind CurrentEventKind; + internal static int Querying; + internal static bool HasSeparateEventThread; + + internal enum ChildThreadRequestType + { + /// + /// Execute the given delegate. Used on slow paths. + /// + /// + /// Request: ObjectData = Func<object?>, Response: ObjectData = output from delegate + /// + Func, + + /// + /// Execute the given delegate. Used on slow paths. + /// + /// + /// Request: ObjectData = Action<object?>, Response: nothing + /// + Action, + + /// + /// Instructs the main thread to pump events. + /// + PumpEvents, + + /// + /// where T is . + /// + GetSurfaceProperty, + + /// + /// where T is . + /// + SetSurfaceProperty + } + + internal readonly record struct ChildThreadRequest( + ChildThreadRequestType OpCode, + object? ObjectData, + SurfaceProperty Data + ); + + internal readonly record struct EventThreadResponse( + ExceptionDispatchInfo? Error, + object? ObjectData, + SurfacePropertyValue Data + ) + { + public EventThreadResponse Assert() + { + Error?.Throw(); + return this; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool IsAssumedToBeOnAppropriateThread(T handle) + where T : ISurfaceOrRequestHandle => + !IsMultiSurface + || typeof(T) != typeof(SurfaceHandle) + || RootSurface == default + || ( + typeof(T) == typeof(SurfaceHandle) + && !HasSeparateEventThread + && RootSurface == (SurfaceHandle)(object)handle + ); + + private static T SlowPath(Func action, THandle subject) + where THandle : ISurfaceOrRequestHandle => + IsAssumedToBeOnAppropriateThread(subject) + ? action() + : (T) + Comms + .Request(new ChildThreadRequest(ChildThreadRequestType.Func, action, default)) + .Assert() + .ObjectData!; + + private static void SlowPath(Action action, THandle subject) + where THandle : ISurfaceOrRequestHandle + { + if (IsAssumedToBeOnAppropriateThread(subject)) + { + action(); + return; + } + + Comms + .Request(new ChildThreadRequest(ChildThreadRequestType.Action, action, default)) + .Assert(); + } + + /// + public static bool IsMultiSurface => TUnderlying.IsMultiSurface; + + /// + public static RequestHandle CreateSurfaceRequest() => TUnderlying.CreateSurfaceRequest(); + + /// + public static void CopySurfaceRequest(RequestHandle dest, TSrc src) + where TSrc : ISurfaceOrRequestHandle => + SlowPath(() => TUnderlying.CopySurfaceRequest(dest, src), src); + + /// + public static void DestroySurfaceRequest(RequestHandle surfaceRequest) => + TUnderlying.DestroySurfaceRequest(surfaceRequest); + + /// + public static bool TryInheritPlatformInfo( + RequestHandle surfaceRequest, + TPlatformInfo info + ) + where TPlatformInfo : notnull => TUnderlying.TryInheritPlatformInfo(surfaceRequest, info); + + /// + public static SurfaceHandle CreateChildSurface( + SurfaceHandle parent, + RequestHandle surfaceRequest + ) => SlowPath(() => TUnderlying.CreateChildSurface(parent, surfaceRequest), parent); + + /// + public static void TerminateSurface(SurfaceHandle surface) => + SlowPath(() => TUnderlying.TerminateSurface(surface), surface); + + /// + public static bool TryGetPlatformInfo( + SurfaceHandle handle, + [NotNullWhen(true)] out TPlatformInfo? platformInfo + ) + { + var (ret, retInfo) = SlowPath( + () => + ( + TUnderlying.TryGetPlatformInfo(handle, out TPlatformInfo? platformInfo), + platformInfo + ), + handle + ); + platformInfo = retInfo; + return ret; + } + + /// + public static SurfaceProperty GetSurfaceProperty(T handle, SurfacePropertyName propertyName) + where T : ISurfaceOrRequestHandle => + IsAssumedToBeOnAppropriateThread(handle) + ? TUnderlying.GetSurfaceProperty(handle, propertyName) + : new SurfaceProperty + { + PropertyName = propertyName, + Value = Comms + .Request( + new ChildThreadRequest( + ChildThreadRequestType.GetSurfaceProperty, + null, + new SurfaceProperty { PropertyName = propertyName } + ) + ) + .Assert() + .Data + }; + + /// + public static void SetSurfaceProperty(T handle, SurfaceProperty property) + where T : ISurfaceOrRequestHandle + { + if (IsAssumedToBeOnAppropriateThread(handle)) + { + TUnderlying.SetSurfaceProperty(handle, property); + return; + } + + Comms + .Request( + new ChildThreadRequest(ChildThreadRequestType.SetSurfaceProperty, null, property) + ) + .Assert(); + } + + /// + public static HostStatus LaunchMainSurface(RequestHandle surfaceRequest, TActor actor) + where TActor : IHostActor + { + if (RootSurface != default) + { + throw new InvalidOperationException( + "MultiThreadedSurfaceHost only supports one root surface per underlying host." + ); + } + + return TUnderlying.LaunchMainSurface(surfaceRequest, new RootHostActor(actor)); + } + + struct RootHostActor(TNext next) : IHostActor + where TNext : IHostActor + { + public void HandleInit(SurfaceHandle surface) + { + if (RootSurface == default) + { + RootSurface = surface; + } + + next.HandleInit(surface); + } + + public void HandleTick() + { + next.HandleTick(); + DoRequests(); + } + + public void HandleQuit(SurfaceHandle surface) + { + next.HandleQuit(surface); + if (surface == RootSurface) + { + DoRequests(); + RootSurface = default; + } + } + + private void DoRequests() + { + // Use a try block outside the per-request loop to avoid small exception regions which incur a small penalty + // in some cases. + bool errored; + do + { + try + { + errored = false; + while (Comms.TryGetRequest(out var req)) + { + switch (req.OpCode) + { + case ChildThreadRequestType.Func: + { + Comms.Provide( + new EventThreadResponse( + null, + Unsafe.As>(req.ObjectData)!.Invoke(), + default + ) + ); + break; + } + case ChildThreadRequestType.Action: + { + Unsafe.As(req.ObjectData)!.Invoke(); + Comms.Provide(default); + break; + } + case ChildThreadRequestType.PumpEvents: + { + Comms.Provide(default); // don't want them waiting for us + + // If this is the first time we're pumping, create the underlying pump and carry on. + // Otherwise, we need to acknowledge the previous event first. + if (EventPump == default) + { + EventPump = TUnderlying.CreateEventPump(RootSurface); + } + else if (CurrentEventKind != HostEventKind.None) + { + TUnderlying.AcknowledgeEvent(EventPump, CurrentEvent); + } + + // Query the event. + if ( + TUnderlying.QueryEvent(EventPump, ref CurrentEvent) + is not HostEventKind.None + and var kind + ) + { + CurrentEventKind = kind; + EventPumpRemainingAcks = EventPumpRefCount; + } + + Querying = 0; + break; + } + default: + { + throw new ArgumentOutOfRangeException(nameof(req.OpCode)); + } + } + } + } + catch (Exception e) + { + errored = true; + Comms.Provide( + new EventThreadResponse(ExceptionDispatchInfo.Capture(e), default, default) + ); + } + } while (errored); + } + } + + /// + public static EventPumpHandle CreateEventPump(SurfaceHandle relatedSurface) + { + if (CurrentEventKind != HostEventKind.None) + { + Interlocked.Increment(ref EventPumpRemainingAcks); + } + + // The actual pump is created in the PumpEvents action. + return new EventPumpHandle(Interlocked.Increment(ref EventPumpRefCount)); + } + + /// + public static void DestroyEventPump(EventPumpHandle pump) + { + Interlocked.Decrement(ref EventPumpRefCount); + if (CurrentEventKind != HostEventKind.None) + { + Interlocked.Decrement(ref EventPumpRemainingAcks); + } + } + + /// + public static HostEventKind QueryEvent(EventPumpHandle pump, ref int @event) + { + if (Querying > 0) + { + return HostEventKind.None; + } + + if (@event == CurrentEvent) + { + if (EventPumpRemainingAcks <= 0 && Interlocked.CompareExchange(ref Querying, 1, 0) == 0) + { + Comms.Request( + new ChildThreadRequest(ChildThreadRequestType.PumpEvents, default, default) + ); + // The main thread will pump events in the background and should hopefully be done by the time this is + // called again. + } + + return HostEventKind.None; + } + + @event = CurrentEvent; + return CurrentEventKind; + } + + /// + public static SurfaceHandle GetEventSurface(EventPumpHandle pump, int @event) => + TUnderlying.GetEventSurface(EventPump, @event); + + /// + public static void AcknowledgeEvent(EventPumpHandle pump, int @event) + { + if (@event != CurrentEvent) + { + return; + } + + if (Interlocked.Decrement(ref EventPumpRemainingAcks) == 0) + { + SlowPath( + () => + { + TUnderlying.DestroyEventPump(EventPump); + EventPump = default; + EventPumpRemainingAcks = 0; + CurrentEvent = 0; + CurrentEventKind = HostEventKind.None; + }, + default(SurfaceHandle) + ); + } + } + + /// + public static SurfaceProperty GetEventPropertyChanged(EventPumpHandle pump, int @event) => + TUnderlying.GetEventPropertyChanged(pump, @event); +} diff --git a/sources/Windowing/Common/Hosting/README.md b/sources/Windowing/Common/Hosting/README.md new file mode 100644 index 0000000000..ae7785fc50 --- /dev/null +++ b/sources/Windowing/Common/Hosting/README.md @@ -0,0 +1,3 @@ +# Surface Host API + +Silk.NET.Windowing.Hosting should contain all low-level, implementation APIs to avoid them cluttering users' code completion. diff --git a/sources/Windowing/Common/Hosting/SurfaceHandle.cs b/sources/Windowing/Common/Hosting/SurfaceHandle.cs new file mode 100644 index 0000000000..23c161c40e --- /dev/null +++ b/sources/Windowing/Common/Hosting/SurfaceHandle.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Silk.NET.Core; + +namespace Silk.NET.Windowing.Hosting; + +/// +/// Represents a strongly-typed handle to a surface hosted on a . +/// +/// The underlying handle. +public readonly record struct SurfaceHandle(nint Value) : ISurfaceOrRequestHandle; + +/// +/// Represents a strongly-typed handle to a request for a surface hosted on a . +/// +/// The underlying handle. +public readonly record struct RequestHandle(nint Value) : ISurfaceOrRequestHandle; + +/// +/// An interface assignable from either a or a . Any +/// implementation of this interface that isn't is treated as a . +/// +public interface ISurfaceOrRequestHandle +{ + /// + /// The underlying handle. + /// + nint Value { get; } +} + +/// +/// Represents a strongly-typed handle to an event pump for a surface hosted on a . +/// +/// The underlying value. +public readonly record struct EventPumpHandle(nint Value); diff --git a/sources/Windowing/Common/Hosting/SurfaceProperty.cs b/sources/Windowing/Common/Hosting/SurfaceProperty.cs new file mode 100644 index 0000000000..c5f42f5a54 --- /dev/null +++ b/sources/Windowing/Common/Hosting/SurfaceProperty.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Numerics; +using System.Runtime.InteropServices; +using Silk.NET.Maths; + +namespace Silk.NET.Windowing.Hosting; + +/// +/// Encapsulates a surface property as a union. +/// +public struct SurfaceProperty +{ + /// + /// The property name. + /// + public SurfacePropertyName PropertyName; + + /// + /// Determines whether represents a float property for which + /// should be used. In cases where the property represents a scalar float, only will be + /// used. This is expected to be known at the point of usage. + /// + public bool IsFloat => ((byte)PropertyName & 0b1110_0000) == 0; + + /// + /// Determines whether represents an integer property for which + /// should be used. In cases where the property represents a scalar float, only will be + /// used. This is expected to be known at the point of usage. + /// + public bool IsInteger => ((byte)PropertyName & 0b1110_0000) == 0b0010_0000; + + /// + /// Determines whether represents a boolean property. + /// + public bool IsBoolean => ((byte)PropertyName & 0b1110_0000) == 0b0100_0000; + + /// + /// The value for the property. + /// + public SurfacePropertyValue Value; + + /// + /// The property value for properties. In cases where the property represents + /// a scalar float, only will be used. This is expected to be known at the point of usage. + /// + public Vector2 Float + { + get => Value.Float; + set => Value.Float = value; + } + + /// + /// The property value for properties. In cases where the property + /// represents a scalar integer, only will be used. This is expected to be known at the + /// point of usage. + /// + public Vector2D Integer + { + get => Value.Integer; + set => Value.Integer = value; + } + + /// + /// The property value for properties. + /// + public bool Boolean + { + get => Value.Boolean; + set => Value.Boolean = value; + } +} diff --git a/sources/Windowing/Common/Hosting/SurfacePropertyName.cs b/sources/Windowing/Common/Hosting/SurfacePropertyName.cs new file mode 100644 index 0000000000..a930532cbd --- /dev/null +++ b/sources/Windowing/Common/Hosting/SurfacePropertyName.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Windowing.Hosting; + +/// +/// Enumerates properties available on surfaces. +/// +/// Do not rely on the integral values of this enum, they may change in the future. +public enum SurfacePropertyName : byte +{ + // XXXYYYYY + // X = Property Type (0b000 = Vector2, 0b001 = Vector2D, 0b010 = bool), Y = Property Name + /// + /// The client size. + /// + ClientSizeVector2 = 0b0000_0001, + + /// + /// Whether events should be pumped on a separate thread to the root surface. + /// + UseSeparateEventThread = 0b0100_0010, + + /// + /// Whether events should be buffered instead of serialized (i.e. don't wait for all surfaces to read an event + /// before polling for another event) + /// + UseBufferedEventLoop = 0b0100_0011 +} diff --git a/sources/Windowing/Common/Hosting/SurfacePropertyValue.cs b/sources/Windowing/Common/Hosting/SurfacePropertyValue.cs new file mode 100644 index 0000000000..f1be542bf7 --- /dev/null +++ b/sources/Windowing/Common/Hosting/SurfacePropertyValue.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Numerics; +using System.Runtime.InteropServices; +using Silk.NET.Maths; + +namespace Silk.NET.Windowing.Hosting; + +/// +/// A union of value types for properties. +/// +[StructLayout(LayoutKind.Explicit)] +public struct SurfacePropertyValue +{ + /// + /// The property value for properties. In cases where the property represents + /// a scalar float, only will be used. This is expected to be known at the point of usage. + /// + [FieldOffset(0)] + public Vector2 Float; + + /// + /// The property value for properties. In cases where the property + /// represents a scalar integer, only will be used. This is expected to be known at the + /// point of usage. + /// + [FieldOffset(0)] + public Vector2D Integer; + + /// + /// The property value for properties. + /// + [FieldOffset(0)] + public bool Boolean; +} diff --git a/sources/Windowing/Common/ISurfaceActor.cs b/sources/Windowing/Common/ISurfaceActor.cs index a0c75da225..e1236077e2 100644 --- a/sources/Windowing/Common/ISurfaceActor.cs +++ b/sources/Windowing/Common/ISurfaceActor.cs @@ -10,6 +10,37 @@ namespace Silk.NET.Windowing; /// public interface ISurfaceActor { + /// + /// Responds to the surface's initial creation. + /// + void HandleCreated(); + + /// + /// Responds to the surface being about to terminate irrevocably. + /// + void HandleTerminating(); + + /// + /// Responds surface requesting, possibly as a result of user action, closure. This may still be reversible at this + /// stage. + /// + void HandleClosing(); + + /// + /// Responds to the surface pausing on request of the operating system. + /// + void HandlePausing(); + + /// + /// Responds to the surface resuming on request of the operating system. + /// + void HandleResuming(); + + /// + /// Responds to the surface being issued a low memory warning. + /// + void HandleLowMemory(); + /// /// Responds to the client size changing. /// diff --git a/sources/Windowing/Common/Miscellaneous/SurfaceEventHandler.cs b/sources/Windowing/Common/Miscellaneous/SurfaceEventHandler.cs new file mode 100644 index 0000000000..22d1fa1a81 --- /dev/null +++ b/sources/Windowing/Common/Miscellaneous/SurfaceEventHandler.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Windowing; + +/// +/// A handler for an event raised by a . +/// +/// The event arguments. +public delegate void SurfaceEventHandler(Surface sender, T args); diff --git a/sources/Windowing/Common/Miscellaneous/SurfaceStateEventArgs.cs b/sources/Windowing/Common/Miscellaneous/SurfaceStateEventArgs.cs new file mode 100644 index 0000000000..6066dee433 --- /dev/null +++ b/sources/Windowing/Common/Miscellaneous/SurfaceStateEventArgs.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Windowing; + +/// +/// Event arguments for events raised by a change in surface state. If the state change is parameterised, +/// should be used instead. This struct is empty to allow for the non-breaking +/// addition of event arguments should the need arise in the future. +/// +public readonly record struct SurfaceStateEventArgs; diff --git a/sources/Windowing/Common/Miscellaneous/SurfaceStateEventArgs`1.cs b/sources/Windowing/Common/Miscellaneous/SurfaceStateEventArgs`1.cs new file mode 100644 index 0000000000..d239dd9fd8 --- /dev/null +++ b/sources/Windowing/Common/Miscellaneous/SurfaceStateEventArgs`1.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Windowing; + +/// +/// Event arguments for events raised in response to a surface state change. +/// +/// The new value of the state to which the event pertains. +/// The type of . +public readonly record struct SurfaceStateEventArgs(T Value); diff --git a/sources/Windowing/Common/Miscellaneous/TimedEventArgs.cs b/sources/Windowing/Common/Miscellaneous/TimedEventArgs.cs new file mode 100644 index 0000000000..201f51abfd --- /dev/null +++ b/sources/Windowing/Common/Miscellaneous/TimedEventArgs.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Windowing; + +/// +/// Event arguments for events raised on a regular schedule. +/// +/// The time since the last time this event was raised. +public readonly record struct TimedEventArgs(double DeltaSeconds); diff --git a/sources/Windowing/Common/Silk.NET.Windowing.Common.csproj b/sources/Windowing/Common/Silk.NET.Windowing.Common.csproj index 93fcd4f2d0..26eefa0132 100644 --- a/sources/Windowing/Common/Silk.NET.Windowing.Common.csproj +++ b/sources/Windowing/Common/Silk.NET.Windowing.Common.csproj @@ -16,4 +16,8 @@ + + + + diff --git a/sources/Windowing/Common/Silk.NET.Windowing.Common.csproj.DotSettings b/sources/Windowing/Common/Silk.NET.Windowing.Common.csproj.DotSettings index 8f8c492295..2dc3a51c39 100644 --- a/sources/Windowing/Common/Silk.NET.Windowing.Common.csproj.DotSettings +++ b/sources/Windowing/Common/Silk.NET.Windowing.Common.csproj.DotSettings @@ -1,3 +1,4 @@  True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/sources/Windowing/Common/Surface.cs b/sources/Windowing/Common/Surface.cs index b080e0ef6f..e7017a04b0 100644 --- a/sources/Windowing/Common/Surface.cs +++ b/sources/Windowing/Common/Surface.cs @@ -1,40 +1,233 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Numerics; -using Silk.NET.Core; +using Silk.NET.Windowing.Hosting; namespace Silk.NET.Windowing; /// -/// Represents an abstract surface from which operating system events can be received and to which user graphics can be -/// drawn using the capabilities provided by the platform. +/// An implementation of an using a , optionally providing an +/// event-based subscription mechanism for 's events. /// -public partial class Surface : ISurface +public abstract class Surface : ISurface, ISurfaceActor { - [HluHostedComponent] - private ISurface? _nativeSurface; + /// + /// Creates a surface with the given configuration and underlying implementation. + /// + /// The configuration for the surface. + /// The type of the configuration for the surface. + /// The surface implementation. + /// The surface. + /// + /// The underlying implementation may be wrapped in a if it + /// isn't already if is true to facilitate the use of + /// multi-threaded multi-window workflows. Note that a surface must only access its own properties from the thread + /// on which it was created. + /// + public static Surface Create(TExtra config) + where TExtra : IConfigureHost + where TImpl : ISurfaceHost + { + var req = TImpl.CreateSurfaceRequest(); + config.ConfigureHost(req); + if ( + !TImpl.IsMultiSurface + || typeof(IMultiThreadedSurfaceHost).IsAssignableFrom(typeof(TImpl)) + ) + { + return Surface.Create(req); + } + + return Surface>.Create(req); + } + + /// + /// Creates a surface with no thread safety, the given configuration, and underlying implementation. + /// + /// The configuration for the surface. + /// The type of the configuration for the surface. + /// The surface implementation. + /// The surface. + public static Surface CreateSingleThreaded(TExtra config) + where TExtra : IConfigureHost + where TImpl : ISurfaceHost => Surface.Create(config); + + /// + /// Marker interface so we can check in an inlining-friendly way whether TImpl is already a + /// without knowing TUnderlying. + /// + internal interface IMultiThreadedSurfaceHost; - [HluHostedComponent(true)] - private IPlatformInfo? _platformInfo; + /// + public abstract Vector2 ClientSize { get; } + + /// + public abstract SurfaceHandle Handle { get; protected set; } /// - /// Creates a surface with the given underlying and no other components. This is strongly - /// discouraged, you should use instead. + /// An event executed when the surface is first loaded. /// /// - /// This constructor is useful if you want to manually configure components. + /// If you are using a surface actor, this event will not be raised and instead forwarded to your surface actor. /// - /// The underlying surface. - public Surface(ISurface surface) => _nativeSurface = surface; + public event SurfaceEventHandler? Created; - /// - public Vector2 ClientSize => _nativeSurface!.ClientSize; + /// + /// An event executed when the surface is about to terminate irrevocably. + /// + /// + /// If you are using a surface actor, this event will not be raised and instead forwarded to your surface actor. + /// + public event SurfaceEventHandler? Terminating; + + /// + /// An event executed when the surface, possibly as a result of user action, is requesting closure. This may still + /// be reversible at this stage. + /// + /// + /// If you are using a surface actor, this event will not be raised and instead forwarded to your surface actor. + /// + public event SurfaceEventHandler? Closing; + + /// + /// An event executed when the surface is pausing on request of the operating system. + /// + /// + /// If you are using a surface actor, this event will not be raised and instead forwarded to your surface actor. + /// + public event SurfaceEventHandler? Pausing; + + /// + /// An event executed when the surface is resuming on request of the operating system. + /// + /// + /// If you are using a surface actor, this event will not be raised and instead forwarded to your surface actor. + /// + public event SurfaceEventHandler? Resuming; + + /// + /// An event executed when the surface is first loaded. + /// + /// + /// If you are using a surface actor, this event will not be raised and instead forwarded to your surface actor. + /// + public event SurfaceEventHandler? LowMemory; + + /// + /// An event executed when the changes. + /// + /// + /// If you are using a surface actor, this event will not be raised and instead forwarded to your surface actor. + /// + public event SurfaceEventHandler>? ClientSizeChanged; + + /// + /// An event executed times per second. + /// + /// + /// If you are using a surface actor, this event will not be raised and instead forwarded to your surface actor. + /// + public event SurfaceEventHandler? Update; + + /// + /// An event executed times per second. + /// + /// + /// If you are using a surface actor, this event will not be raised and instead forwarded to your surface actor. + /// + public event SurfaceEventHandler? Render; + + /// + /// The number of times per second that should be called. 0 or lower is interpreted as + /// "as fast as possible". + /// + public double UpdatesPerSecond + { + get => 1 / _updatePeriod; + set => _updatePeriod = 1 / value; + } + + private double _updatePeriod = 1 / 60.0; + + /// + /// The number of times per second that should be called. 0 or lower is interpreted as + /// "as fast as possible". + /// + public double FramesPerSecond + { + get => 1 / _renderPeriod; + set => _renderPeriod = 1 / value; + } + + private double _renderPeriod = 1 / 60.0; /// - public IntPtr Handle => _nativeSurface!.Handle; + /// + /// The events exposed on this class won't fire and will instead be forwarded to the given . + /// + public abstract void Launch(T actor) + where T : ISurfaceActor; + + /// + /// Runs the window's event loop, dispatching events to the event fields on this object. This may be blocking, + /// but won't always be blocking. You should dispose of your window and other resources in the window callbacks + /// themselves. For instance, on mobile platforms a busy thread is often created by the underlying implementation + /// implicitly, therefore the busy loop runs as a separate runnable. In this case, the window merely dispatches to + /// that thread. + /// + public abstract void Launch(); /// - public void Run(T actor) - where T : ISurfaceActor => _nativeSurface!.Run(actor); + public abstract void Dispose(); + + /// + /// Whether to force the main loop (i.e. the execution of and ) to be + /// uncapped. This is usually useful to set to true once the V-Sync swap interval has been set, and + /// effectively results in being ignored. + /// + protected bool ForceUncappedRender { get; set; } + + private Stopwatch _renderStopwatch = new(); + private Stopwatch _updateStopwatch = new(); + + void ISurfaceActor.HandleTick() + { + var delta = _updateStopwatch.Elapsed.TotalSeconds; + if (delta >= _updatePeriod) + { + _updateStopwatch.Restart(); + Update?.Invoke(this, new TimedEventArgs(delta)); + } + + delta = _renderStopwatch.Elapsed.TotalSeconds; + if (delta >= _renderPeriod || ForceUncappedRender) + { + _renderStopwatch.Restart(); + Render?.Invoke(this, new TimedEventArgs(delta)); + // TODO swap if MakeCurrent wasn't called on the OpenGL context and ShouldSwapAutomatically is enabled. + } + } + + void ISurfaceActor.HandleCreated() + { + _renderStopwatch.Restart(); + _updateStopwatch.Restart(); + Created?.Invoke(this, new SurfaceStateEventArgs()); + } + + void ISurfaceActor.HandleTerminating() => + Terminating?.Invoke(this, new SurfaceStateEventArgs()); + + void ISurfaceActor.HandleClosing() => Closing?.Invoke(this, new SurfaceStateEventArgs()); + + void ISurfaceActor.HandlePausing() => Pausing?.Invoke(this, new SurfaceStateEventArgs()); + + void ISurfaceActor.HandleResuming() => Resuming?.Invoke(this, new SurfaceStateEventArgs()); + + void ISurfaceActor.HandleLowMemory() => LowMemory?.Invoke(this, new SurfaceStateEventArgs()); + + void ISurfaceActor.HandleClientSizeChanged(Vector2 newSize) => + ClientSizeChanged?.Invoke(this, new SurfaceStateEventArgs(newSize)); } diff --git a/sources/Windowing/Common/SurfaceBuilder`1.cs b/sources/Windowing/Common/SurfaceBuilder`1.cs index b82187ca93..46a36db406 100644 --- a/sources/Windowing/Common/SurfaceBuilder`1.cs +++ b/sources/Windowing/Common/SurfaceBuilder`1.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Silk.NET.Core; +using Silk.NET.Windowing.Hosting; namespace Silk.NET.Windowing; @@ -10,12 +11,12 @@ namespace Silk.NET.Windowing; /// /// The implementation source of the surface components. public static class SurfaceBuilder - where TImpl : IHluComponentRegistry + where TImpl : ISurfaceHost { /// /// Creates a new with the default configuration, implemented by /// . /// /// The surface builder. - public static SurfaceBuilder Create() => new(default); + public static SurfaceBuilder Create() => new(default); } diff --git a/sources/Windowing/Common/SurfaceBuilder`2.cs b/sources/Windowing/Common/SurfaceBuilder`2.cs index f4b81209b9..fc4e70b5a7 100644 --- a/sources/Windowing/Common/SurfaceBuilder`2.cs +++ b/sources/Windowing/Common/SurfaceBuilder`2.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Silk.NET.Core; +using Silk.NET.Windowing.Hosting; namespace Silk.NET.Windowing; @@ -12,8 +13,8 @@ namespace Silk.NET.Windowing; /// The type of the extra configuration. /// The source of the implementation of the surface's components. public readonly record struct SurfaceBuilder(TExtra ExtraConfiguration) - where TExtra : IHluConfiguration - where TImpl : IHluComponentRegistry + where TExtra : IConfigureHost + where TImpl : ISurfaceHost { /// /// Creates a surface using the configuration encapsulated in this surface builder. diff --git a/sources/Windowing/Common/Surface`1.cs b/sources/Windowing/Common/Surface`1.cs new file mode 100644 index 0000000000..f589bef51b --- /dev/null +++ b/sources/Windowing/Common/Surface`1.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Numerics; +using Silk.NET.Core; +using Silk.NET.Windowing.Hosting; + +namespace Silk.NET.Windowing; + +/// +/// Represents an abstract surface from which operating system events can be received and to which user graphics can be +/// drawn using the capabilities provided by the platform. +/// +public sealed class Surface : Surface, IHostActor + where TImpl : ISurfaceHost +{ + internal Surface() { } + + /// + /// Creates a surface with the given configuration. + /// + /// The configuration for the surface.. + /// The type of the configuration for the surface. + /// The surface. + public static Surface Create(TExtra config) + where TExtra : IConfigureHost + { + var req = TImpl.CreateSurfaceRequest(); + config.ConfigureHost(req); + return Create(req); + } + + internal static Surface Create(RequestHandle req) => + // TODO init components based on the request properties configured. + new() { Request = req }; + + /// + public override Vector2 ClientSize => + Handle != default + ? TImpl.GetSurfaceProperty(Handle, SurfacePropertyName.ClientSizeVector2).Float + : TImpl.GetSurfaceProperty(Request, SurfacePropertyName.ClientSizeVector2).Float; + + /// + public override SurfaceHandle Handle { get; protected set; } + + private RequestHandle Request { get; set; } + + /// + public override void Launch(T actor) => TImpl.LaunchMainSurface(Request, this); + + /// + public override void Launch() => Launch(this); + + /// + public override void Dispose() + { + if (Request.Value is not 0) + { + TImpl.DestroySurfaceRequest(Request); + } + + if (Handle.Value is not 0) + { + TImpl.TerminateSurface(Handle); + } + } + + void IHostActor.HandleInit(SurfaceHandle surface) => throw new NotImplementedException(); + + void IHostActor.HandleTick() => throw new NotImplementedException(); + + void IHostActor.HandleQuit(SurfaceHandle surface) => throw new NotImplementedException(); +} diff --git a/sources/Windowing/Windowing/ReferenceImplementation.cs b/sources/Windowing/Windowing/ReferenceImplementation.cs index d79fbd3324..363c139fa6 100644 --- a/sources/Windowing/Windowing/ReferenceImplementation.cs +++ b/sources/Windowing/Windowing/ReferenceImplementation.cs @@ -1,13 +1,177 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using Silk.NET.Core; using Silk.NET.SDL; +using Silk.NET.Windowing.Hosting; namespace Silk.NET.Windowing; /// /// Represents the reference implementation for . This currently uses SDL. /// -[HluRegisteredComponent(Override = true)] -public partial class ReferenceImplementation; +public unsafe class ReferenceImplementation : ISurfaceHost +{ + /// + public static bool IsMultiSurface => + !( + OperatingSystem.IsAndroid() + || OperatingSystem.IsIOS() + || OperatingSystem.IsBrowser() + || OperatingSystem.IsTvOS() + || OperatingSystem.IsWatchOS() + ); + + /// + public static RequestHandle CreateSurfaceRequest() => + new(unchecked((nint)Sdl.CreateProperties())); + + /// + public static void CopySurfaceRequest(RequestHandle dest, TSrc src) + where TSrc : ISurfaceOrRequestHandle + { + // TODO needs handles to be creatable + throw new NotImplementedException(); + } + + /// + public static void DestroySurfaceRequest(RequestHandle surfaceRequest) => + Sdl.DestroyProperties(unchecked((uint)surfaceRequest.Value)); + + /// + public static bool TryInheritPlatformInfo( + RequestHandle surfaceRequest, + TPlatformInfo info + ) + where TPlatformInfo : notnull + { + var props = unchecked((uint)surfaceRequest.Value); + // #define SDL_PROP_WINDOW_CREATE_COCOA_WINDOW_POINTER "SDL.window.create.cocoa.window" + // #define SDL_PROP_WINDOW_CREATE_COCOA_VIEW_POINTER "SDL.window.create.cocoa.view" + // #define SDL_PROP_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN "SDL.window.create.wayland.surface_role_custom" + // #define SDL_PROP_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN "SDL.window.create.wayland.create_egl_window" + // #define SDL_PROP_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER "SDL.window.create.wayland.wl_surface" + // #define SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER "SDL.window.create.win32.hwnd" + // #define SDL_PROP_WINDOW_CREATE_WIN32_PIXEL_FORMAT_HWND_POINTER "SDL.window.create.win32.pixel_format_hwnd" + // #define SDL_PROP_WINDOW_CREATE_X11_WINDOW_NUMBER "SDL.window.create.x11.window" + if ( + typeof(TPlatformInfo) == typeof(CocoaPlatformInfo) + && (CocoaPlatformInfo)(object)info is var cocoa + ) + { + var ret = + Sdl.SetProperty( + props, + MemoryMarshal.Cast(Sdl.PropWindowCreateCocoaWindowPointer), // TODO string consts should use a special type that can collapse to either Ref or Ref + (void*)cocoa.Window // TODO nint to Ref? + ) == 0; + // TODO i know it's a try method but should we do something with this? + if (!ret) + { + Sdl.ClearError(); + } + + return ret; + } + + if ( + typeof(TPlatformInfo) == typeof(WaylandPlatformInfo) + && (WaylandPlatformInfo)(object)info is var wayland + ) + { + return Sdl.SetProperty( + props, + MemoryMarshal.Cast(Sdl.PropWindowCreateWaylandWlSurfacePointer), + (void*)wayland.Surface + ) == 0; + } + + if ( + typeof(TPlatformInfo) == typeof(Win32PlatformInfo) + && (Win32PlatformInfo)(object)info is var win32 + ) + { + return Sdl.SetProperty( + props, + MemoryMarshal.Cast(Sdl.PropWindowCreateWin32HwndPointer), + (void*)win32.Hwnd + ) == 0; + } + + if ( + typeof(TPlatformInfo) == typeof(X11PlatformInfo) + && (X11PlatformInfo)(object)info is var x11 + ) + { + return Sdl.SetNumberProperty( + props, + MemoryMarshal.Cast(Sdl.PropWindowCreateX11WindowNumber), + x11.Window + ) == 0; + } + + return false; + } + + /// + public static HostStatus LaunchMainSurface(RequestHandle surfaceRequest, TActor actor) + where TActor : IHostActor + { + var window = Sdl.CreateWindowWithProperties(unchecked((uint)surfaceRequest.Value)); + if (window == nullptr) + { + Sdl.ThrowError(); + } + + // TODO requires SDL_RunApp and SDL_EnterAppMainCallbacks + return HostStatus.Failure; + } + + /// + public static SurfaceHandle CreateChildSurface( + SurfaceHandle parent, + RequestHandle surfaceRequest + ) => throw new NotImplementedException(); + + /// + public static void TerminateSurface(SurfaceHandle surface) => + throw new NotImplementedException(); + + /// + public static bool TryGetPlatformInfo( + SurfaceHandle handle, + [NotNullWhen(true)] out TPlatformInfo? platformInfo + ) => throw new NotImplementedException(); + + /// + public static SurfaceProperty GetSurfaceProperty(T handle, SurfacePropertyName propertyName) + where T : ISurfaceOrRequestHandle => throw new NotImplementedException(); + + /// + public static void SetSurfaceProperty(T handle, SurfaceProperty property) + where T : ISurfaceOrRequestHandle => throw new NotImplementedException(); + + /// + public static EventPumpHandle CreateEventPump(SurfaceHandle relatedSurface) => new(1); + + /// + public static void DestroyEventPump(EventPumpHandle pump) { } + + /// + public static HostEventKind QueryEvent(EventPumpHandle pump, ref int @event) => + throw new NotImplementedException(); + + /// + public static SurfaceHandle GetEventSurface(EventPumpHandle pump, int @event) => + throw new NotImplementedException(); + + /// + public static void AcknowledgeEvent(EventPumpHandle pump, int @event) => + throw new NotImplementedException(); + + /// + public static SurfaceProperty GetEventPropertyChanged(EventPumpHandle pump, int @event) => + throw new NotImplementedException(); +} diff --git a/sources/Windowing/Windowing/SdlNativeWindow.cs b/sources/Windowing/Windowing/SdlNativeWindow.cs deleted file mode 100644 index eabcd62376..0000000000 --- a/sources/Windowing/Windowing/SdlNativeWindow.cs +++ /dev/null @@ -1,528 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -global using static Silk.NET.Core.DSL; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Silk.NET.Core; -using Silk.NET.SDL; - -namespace Silk.NET.Windowing; - -/// -/// A HLU component encapsulating SDL info. This should be registered to s as an -/// implementation of . -/// -public sealed class SdlNativeWindow : IPlatformInfo, IDisposable -{ - /// - /// Creates a component from the given base components. - /// - /// The base platform info, to create a SDL window from an existing window. - /// The SDL API interface (optional, defaults to ). - public unsafe SdlNativeWindow( - [HluHostedComponent] IPlatformInfo platform, - [HluHostedComponent] ISdl? sdl = null - ) - : this(sdl) - { - switch (platform) - { - // These are additional supported properties with X11: - // - `SDL_PROP_WINDOW_CREATE_X11_WINDOW_NUMBER`: the X11 Window associated - // with the window, if you want to wrap an existing window. - case X11PlatformInfo x11: - Api.SetNumberProperty( - Properties, - MemoryMarshal.Cast(Sdl.PropWindowCreateX11WindowNumber), - x11.Window - ); - Next = platform; - break; - // These are additional supported properties on macOS: - // - `SDL_PROP_WINDOW_CREATE_COCOA_WINDOW_POINTER`: the - // `(__unsafe_unretained)` NSWindow associated with the window, if you want - // to wrap an existing window. - // - `SDL_PROP_WINDOW_CREATE_COCOA_VIEW_POINTER`: the `(__unsafe_unretained)` - // NSView associated with the window, defaults to `[window contentView]` - case CocoaPlatformInfo cocoa: - Api.SetProperty( - Properties, - MemoryMarshal.Cast(Sdl.PropWindowCreateCocoaWindowPointer), - (void*)cocoa.Window // TODO should we add a nint -> Ref implicit cast? - ); - Next = platform; - break; - // These are additional supported properties on Wayland: - // - // - `SDL_PROP_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN` - true if - TODO do we want this? - // the application wants to use the Wayland surface for a custom role and - // does not want it attached to an XDG toplevel window. See - // [README/wayland](README/wayland) for more information on using custom - // surfaces. - // - `SDL_PROP_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN` - true if the - TODO do we want this? - // application wants an associated `wl_egl_window` object to be created, - // even if the window does not have the OpenGL property or flag set. - // - `SDL_PROP_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER` - the wl_surface - // associated with the window, if you want to wrap an existing window. See - // [README/wayland](README/wayland) for more information. - case WaylandPlatformInfo wayland: - Api.SetProperty( - Properties, - MemoryMarshal.Cast(Sdl.PropWindowCreateWaylandWlSurfacePointer), - (void*)wayland.Surface // TODO should we add a nint -> Ref implicit cast? - ); - Next = platform; - break; - // These are additional supported properties on Windows: - // - `SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER`: the HWND associated with the - // window, if you want to wrap an existing window. - // - `SDL_PROP_WINDOW_CREATE_WIN32_PIXEL_FORMAT_HWND_POINTER`: optional, - // another window to share pixel format with, useful for OpenGL windows - case Win32PlatformInfo win32: - Api.SetProperty( - Properties, - MemoryMarshal.Cast(Sdl.PropWindowCreateWin32HwndPointer), - (void*)win32.Hwnd - ); - Next = platform; - break; - } - } - - /// - /// Creates a component from the given base components. - /// - /// The SDL API interface (optional, defaults to ). - public SdlNativeWindow([HluHostedComponent] ISdl? sdl = null) - { - Api = sdl ?? Sdl.Instance; - Properties = Api.CreateProperties(); - Api.SetStringProperty( - Properties, - MemoryMarshal.Cast(Sdl.PropWindowCreateTitleString), - Assembly.GetEntryAssembly()?.GetName().Name ?? "Silk.NET Window" - ); - - // The bounds component will override this if the user has requested a window of a specific dimension. - // Otherwise, we enact the lowest common denominator across platforms - borderless fullscreen. - Api.SetBooleanProperty( - Properties, - MemoryMarshal.Cast(Sdl.PropWindowCreateBorderlessBoolean), - true - ); - Api.SetBooleanProperty( - Properties, - MemoryMarshal.Cast(Sdl.PropWindowCreateFullscreenBoolean), - true - ); - } - - /// - /// The SDL API interface in use. - /// - public ISdl Api { get; } - - /// - /// The SDL window handle. May be null. - /// - public WindowHandle Window { get; private set; } - - /// - /// The SDL window properties. Never zero. - /// - /// - /// If is not , then this tracks the properties of the - /// actual window. - /// - public uint Properties { get; private set; } - - /// - /// Gets a property for this window. - /// - /// The property name to use if the window has been created. - /// The property name to use if the window has not been created. - /// The default value to use. - /// - /// The type of the property. Must be one of: - /// - /// - /// : - /// For - /// - /// - /// : - /// For - /// - /// - /// : - /// For - /// - /// - /// - /// or where T is or : - /// - /// For - /// - /// - /// A pointer sized struct: - /// For - /// - /// - /// - /// The property, or the default value if it was not set. - /// - /// The window has not been created and no creation-time property name has been provided. - /// - /// An invalid type was used. - [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- required to optimise away throwing - public unsafe T? GetProperty( - ReadOnlySpan runtimePropertyName, - ReadOnlySpan creationTimePropertyName = default, - T? defaultValue = default! - ) - { -#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type - var propName = MemoryMarshal.Cast( - runtimePropertyName.IsEmpty - ? creationTimePropertyName - : Window == nullptr - ? creationTimePropertyName.IsEmpty - ? throw new ArgumentException( - "Window has not been created, and no creation-time property name has been provided.", - nameof(creationTimePropertyName) - ) - : creationTimePropertyName - : runtimePropertyName - ); - - // YES THIS IS TERRIBLE CODE, BUT IT MAKES THE REST OF OUR CODEBASE CLEAN. - // - Story of Silk.NET's life - if (typeof(T) == typeof(long)) - { - return (T) - (object)Api.GetNumberProperty(Properties, propName, (long)(object?)defaultValue!); - } - - if (typeof(T) == typeof(float)) - { - return (T) - (object)Api.GetFloatProperty(Properties, propName, (float)(object?)defaultValue!); - } - - if (typeof(T) == typeof(bool)) - { - return (T) - (object) - (bool) - Api.GetBooleanProperty(Properties, propName, (bool)(object?)defaultValue!); - } - - if ( - typeof(T) == typeof(Ptr) - || typeof(T) == typeof(Ptr) - || typeof(T) == typeof(string) - ) - { - var ret = Api.GetStringProperty(Properties, propName, nullptr); - return ret == nullptr - ? default - : typeof(T) == typeof(string) - ? (T)(object)ret.ReadToString() - : *(T*)&ret; - } - - if (sizeof(T) != sizeof(nint) || !typeof(T).IsValueType) - { - throw new InvalidOperationException( - "Can't retrieve using this type - must be long, float, bool, string" - + "(or a Ptr/Ptr), or a pointer-sized type." - ); - } - - void* ptr = Api.GetProperty(Properties, propName, *(void**)&defaultValue); - return *(T*)&ptr; -#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type - } - - /// - /// Sets a property for this window. - /// - /// The property name to use if the window has been created. - /// The property name to use if the window has not been created. - /// The value to set the property to. - /// - /// The type of the property. Must be one of: - /// - /// - /// : - /// For - /// - /// - /// : - /// For - /// - /// - /// : - /// For - /// - /// - /// - /// or where T is or : - /// - /// For - /// - /// - /// A pointer sized struct: - /// For - /// - /// - /// - /// - /// The window has not been created and no creation-time property name has been provided. - /// - /// An invalid type was used. - /// An error occurred while setting the property. - [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- required to optimise away throwing - public unsafe void SetProperty( - ReadOnlySpan runtimePropertyName, - T? value, - ReadOnlySpan creationTimePropertyName = default - ) - { -#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type - var propName = MemoryMarshal.Cast( - runtimePropertyName.IsEmpty - ? creationTimePropertyName - : Window == nullptr - ? creationTimePropertyName.IsEmpty - ? throw new ArgumentException( - "Window has not been created, and no creation-time property name has been provided.", - nameof(creationTimePropertyName) - ) - : creationTimePropertyName - : runtimePropertyName - ); - - // YES THIS IS TERRIBLE CODE, BUT IT MAKES THE REST OF OUR CODEBASE CLEAN. - // - Story of Silk.NET's life - if (typeof(T) == typeof(long)) - { - if (Api.SetNumberProperty(Properties, propName, (long)(object?)value!) != 0) - { - Api.ThrowError(); - } - } - - if (typeof(T) == typeof(float)) - { - if (Api.SetFloatProperty(Properties, propName, (float)(object?)value!) != 0) - { - Api.ThrowError(); - } - } - - if (typeof(T) == typeof(bool)) - { - if (Api.SetBooleanProperty(Properties, propName, (bool)(object?)value!) != 0) - { - Api.ThrowError(); - } - } - - if (typeof(T) == typeof(Ptr) || typeof(T) == typeof(Ptr)) - { - if (Api.SetStringProperty(Properties, propName, *(Ptr*)&value) != 0) - { - Api.ThrowError(); - } - } - - if (typeof(T) == typeof(string)) - { - if (Api.SetStringProperty(Properties, propName, (string?)(object?)value) != 0) - { - Api.ThrowError(); - } - } - - if (sizeof(T) != sizeof(nint) || !typeof(T).IsValueType) - { - throw new InvalidOperationException( - "Can't retrieve using this type - must be long, float, bool, string" - + "(or a Ptr/Ptr), or a pointer-sized type." - ); - } - - if (Api.SetProperty(Properties, propName, *(void**)&value) != 0) - { - Api.ThrowError(); - } -#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type - } - - /// - /// Creates the underlying window. If the window has already been created, this does nothing. - /// This should only be called once all initial creation time properties have been set. - /// - public void CreateWindow() - { - if (Window != nullptr) - { - return; - } - - if (Api.InitSubSystem((uint)InitFlags.Video) != 0) - { - Api.ThrowError(); - } - - Window = Api.CreateWindowWithProperties(Properties); - if (Window == nullptr) - { - Api.ThrowError(); - } - - Api.DestroyProperties(Properties); - Properties = Api.GetWindowProperties(Window); - // On Android: - // - `SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER`: the ANativeWindow associated - // with the window - // - `SDL_PROP_WINDOW_ANDROID_SURFACE_POINTER`: the EGLSurface associated with - // the window - if (GetProperty(Sdl.PropWindowAndroidWindowPointer) is not 0 and var aNativeWindow) - { - Next = new AndroidPlatformInfo( - aNativeWindow, - GetProperty(Sdl.PropWindowAndroidSurfacePointer) - ); - } - // On iOS: - // - `SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER`: the `(__unsafe_unretained)` - // UIWindow associated with the window - // - TODO `SDL_PROP_WINDOW_UIKIT_METAL_VIEW_TAG_NUMBER`: the NSInteger tag - // assocated with metal views on the window - // - `SDL_PROP_WINDOW_UIKIT_OPENGL_FRAMEBUFFER_NUMBER`: the OpenGL view's - // framebuffer object. It must be bound when rendering to the screen using - // OpenGL. - // - `SDL_PROP_WINDOW_UIKIT_OPENGL_RENDERBUFFER_NUMBER`: the OpenGL view's - // renderbuffer object. It must be bound when SDL_GL_SwapWindow is called. - // - `SDL_PROP_WINDOW_UIKIT_OPENGL_RESOLVE_FRAMEBUFFER_NUMBER`: the OpenGL - // view's resolve framebuffer, when MSAA is used. - else if (GetProperty(Sdl.PropWindowUikitWindowPointer) is not 0 and var uiWindow) - { - // TODO our bindings need to be updated - throw new NotImplementedException(); - } - // On KMS/DRM: - // - `SDL_PROP_WINDOW_KMSDRM_DEVICE_INDEX_NUMBER`: the device index associated - // with the window (e.g. the X in /dev/dri/cardX) - // - `SDL_PROP_WINDOW_KMSDRM_DRM_FD_NUMBER`: the DRM FD associated with the - // window - // - `SDL_PROP_WINDOW_KMSDRM_GBM_DEVICE_POINTER`: the GBM device associated - // with the window - // TODO we don't have KMS/DRM platform info structs yet - // On macOS: - // - // - `SDL_PROP_WINDOW_COCOA_WINDOW_POINTER`: the `(__unsafe_unretained)` - // NSWindow associated with the window - // - TODO `SDL_PROP_WINDOW_COCOA_METAL_VIEW_TAG_NUMBER`: the NSInteger tag - // assocated with metal views on the window - else if (GetProperty(Sdl.PropWindowCocoaWindowPointer) is not 0 and var nsWindow) - { - Next = new CocoaPlatformInfo(nsWindow); - } - // On Vivante: - // - `SDL_PROP_WINDOW_VIVANTE_DISPLAY_POINTER`: the EGLNativeDisplayType - // associated with the window - // - `SDL_PROP_WINDOW_VIVANTE_WINDOW_POINTER`: the EGLNativeWindowType - // associated with the window - // - TODO `SDL_PROP_WINDOW_VIVANTE_SURFACE_POINTER`: the EGLSurface associated with - // the window - else if (GetProperty(Sdl.PropWindowVivanteDisplayPointer) is not 0 and var vivDisplay) - { - Next = new VivantePlatformInfo( - vivDisplay, - GetProperty(Sdl.PropWindowVivanteWindowPointer) - ); - } - // On UWP: - // - `SDL_PROP_WINDOW_WINRT_WINDOW_POINTER`: the IInspectable CoreWindow - // associated with the window - else if (GetProperty(Sdl.PropWindowWinrtWindowPointer) is not 0 and var inspectable) - { - Next = new WinRTPlatformInfo(inspectable); - } - // On Windows: - // - `SDL_PROP_WINDOW_WIN32_HWND_POINTER`: the HWND associated with the window - // - `SDL_PROP_WINDOW_WIN32_HDC_POINTER`: the HDC associated with the window - // - `SDL_PROP_WINDOW_WIN32_INSTANCE_POINTER`: the HINSTANCE associated with - // the window - else if (GetProperty(Sdl.PropWindowWin32HwndPointer) is not 0 and var hwnd) - { - Next = new Win32PlatformInfo( - hwnd, - GetProperty(Sdl.PropWindowWin32HdcPointer), - GetProperty(Sdl.PropWindowWin32InstancePointer) - ); - } - // On Wayland: - // - // Note: The `xdg_*` window objects do not internally persist across window - // show/hide calls. They will be null if the window is hidden and must be - // queried each time it is shown. - // - // - `SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER`: the wl_display associated with - // the window - // - `SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER`: the wl_surface associated with - // the window - // - `SDL_PROP_WINDOW_WAYLAND_EGL_WINDOW_POINTER`: the wl_egl_window - // associated with the window - // - `SDL_PROP_WINDOW_WAYLAND_XDG_SURFACE_POINTER`: the xdg_surface associated - // with the window - // - `SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER`: the xdg_toplevel role - // associated with the window - // - 'SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_EXPORT_HANDLE_STRING': the export - // handle associated with the window - // - `SDL_PROP_WINDOW_WAYLAND_XDG_POPUP_POINTER`: the xdg_popup role - // associated with the window - // - `SDL_PROP_WINDOW_WAYLAND_XDG_POSITIONER_POINTER`: the xdg_positioner - // associated with the window, in popup mode - else if (GetProperty(Sdl.PropWindowWaylandSurfacePointer) is not 0 and var wlSurface) - { - Next = new WaylandPlatformInfo( - GetProperty(Sdl.PropWindowWaylandDisplayPointer), - wlSurface - ); - } - // On X11: - // - `SDL_PROP_WINDOW_X11_DISPLAY_POINTER`: the X11 Display associated with - // the window - // - `SDL_PROP_WINDOW_X11_SCREEN_NUMBER`: the screen number associated with - // the window - // - `SDL_PROP_WINDOW_X11_WINDOW_NUMBER`: the X11 Window associated with the - // window - else if (GetProperty(Sdl.PropWindowX11WindowNumber) is not 0 and var xWindow) - { - Next = new X11PlatformInfo( - GetProperty(Sdl.PropWindowX11DisplayPointer), - (nint)xWindow - ); - } - } - - /// - public IPlatformInfo? Next { get; private set; } - - private void ReleaseUnmanagedResources() => Api.DestroyWindow(Window); - - /// - public void Dispose() - { - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - - ~SdlNativeWindow() => ReleaseUnmanagedResources(); -} diff --git a/sources/Windowing/Windowing/SdlSurface.cs b/sources/Windowing/Windowing/SdlSurface.cs deleted file mode 100644 index 98151b5e57..0000000000 --- a/sources/Windowing/Windowing/SdlSurface.cs +++ /dev/null @@ -1,281 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Silk.NET.Core; -using Silk.NET.SDL; - -namespace Silk.NET.Windowing; - -internal class SdlSurface(SdlNativeWindow native) : ISurface, IDisposable -{ - // TODO child windows - public static bool TryConfigure(THost dest, [HluHostedComponent] IPlatformInfo platform) - where THost : IHluComponentHost => - Instance is not null - ? throw new InvalidOperationException( - "All subsequent surfaces must be created from the root surface i.e. must be a child of the " - + "first surface created. A surface has already been created without a parent." - ) - : platform is SdlNativeWindow native - && dest.TrySet(Instance = new SdlSurface(native)); - - public Vector2 ClientSize - { - get - { - if (native.Window == nullptr) - { - return default; - } - - int w = 0, - h = 0; - if (native.Api.GetWindowSizeInPixels(native.Window, w.AsRef(), h.AsRef()) != 0) - { - native.Api.ThrowError(); - } - - return new Vector2(w, h); - } - } - - public unsafe IntPtr Handle => (nint)native.Window.Handle; - - public void Run(T actor) - where T : ISurfaceActor - { - // TODO: ANDROID NOTE: There will be no blocking on Android! Instead, onCreate calls into the user's main - // TODO (assumed to be xplat code that assumes blocking) which when calling Run, will do nothing other than set - // the actor. The runnable returned from createMainRunnable will then call into SDL, so it's a slight - // separation. - Event @event = default; - while (true) - { - actor.HandleTick(); - - if (native.Api.PollEvent(@event.AsRef())) - { - switch ((EventType)@event.Type) - { - case EventType.First: - break; - case EventType.Quit: - { - break; - } - case EventType.Terminating: - break; - case EventType.LowMemory: - break; - case EventType.WillEnterBackground: - break; - case EventType.DidEnterBackground: - break; - case EventType.WillEnterForeground: - break; - case EventType.DidEnterForeground: - break; - case EventType.LocaleChanged: - break; - case EventType.SystemThemeChanged: - break; - case EventType.DisplayOrientation: - break; - case EventType.DisplayAdded: - break; - case EventType.DisplayRemoved: - break; - case EventType.DisplayMoved: - break; - case EventType.DisplayContentScaleChanged: - break; - case EventType.DisplayHdrStateChanged: - break; - case EventType.WindowShown: - break; - case EventType.WindowHidden: - break; - case EventType.WindowExposed: - break; - case EventType.WindowMoved: - break; - case EventType.WindowResized: - break; - case EventType.WindowPixelSizeChanged: - break; - case EventType.WindowMinimized: - break; - case EventType.WindowMaximized: - break; - case EventType.WindowRestored: - break; - case EventType.WindowMouseEnter: - break; - case EventType.WindowMouseLeave: - break; - case EventType.WindowFocusGained: - break; - case EventType.WindowFocusLost: - break; - case EventType.WindowCloseRequested: - break; - case EventType.WindowTakeFocus: - break; - case EventType.WindowHitTest: - break; - case EventType.WindowIccprofChanged: - break; - case EventType.WindowDisplayChanged: - break; - case EventType.WindowDisplayScaleChanged: - break; - case EventType.WindowOccluded: - break; - case EventType.WindowEnterFullscreen: - break; - case EventType.WindowLeaveFullscreen: - break; - case EventType.WindowDestroyed: - break; - case EventType.WindowPenEnter: - break; - case EventType.WindowPenLeave: - break; - case EventType.KeyDown: - break; - case EventType.KeyUp: - break; - case EventType.TextEditing: - break; - case EventType.TextInput: - break; - case EventType.KeymapChanged: - break; - case EventType.KeyboardAdded: - break; - case EventType.KeyboardRemoved: - break; - case EventType.MouseMotion: - break; - case EventType.MouseButtonDown: - break; - case EventType.MouseButtonUp: - break; - case EventType.MouseWheel: - break; - case EventType.MouseAdded: - break; - case EventType.MouseRemoved: - break; - case EventType.JoystickAxisMotion: - break; - case EventType.JoystickBallMotion: - break; - case EventType.JoystickHatMotion: - break; - case EventType.JoystickButtonDown: - break; - case EventType.JoystickButtonUp: - break; - case EventType.JoystickAdded: - break; - case EventType.JoystickRemoved: - break; - case EventType.JoystickBatteryUpdated: - break; - case EventType.JoystickUpdateComplete: - break; - case EventType.GamepadAxisMotion: - break; - case EventType.GamepadButtonDown: - break; - case EventType.GamepadButtonUp: - break; - case EventType.GamepadAdded: - break; - case EventType.GamepadRemoved: - break; - case EventType.GamepadRemapped: - break; - case EventType.GamepadTouchpadDown: - break; - case EventType.GamepadTouchpadMotion: - break; - case EventType.GamepadTouchpadUp: - break; - case EventType.GamepadSensorUpdate: - break; - case EventType.GamepadUpdateComplete: - break; - case EventType.GamepadSteamHandleUpdated: - break; - case EventType.FingerDown: - break; - case EventType.FingerUp: - break; - case EventType.FingerMotion: - break; - case EventType.ClipboardUpdate: - break; - case EventType.DropFile: - break; - case EventType.DropText: - break; - case EventType.DropBegin: - break; - case EventType.DropComplete: - break; - case EventType.DropPosition: - break; - case EventType.AudioDeviceAdded: - break; - case EventType.AudioDeviceRemoved: - break; - case EventType.AudioDeviceFormatChanged: - break; - case EventType.SensorUpdate: - break; - case EventType.PenDown: - break; - case EventType.PenUp: - break; - case EventType.PenMotion: - break; - case EventType.PenButtonDown: - break; - case EventType.PenButtonUp: - break; - case EventType.CameraDeviceAdded: - break; - case EventType.CameraDeviceRemoved: - break; - case EventType.CameraDeviceApproved: - break; - case EventType.CameraDeviceDenied: - break; - case EventType.RenderTargetsReset: - break; - case EventType.RenderDeviceReset: - break; - case EventType.PollSentinel: - break; - case EventType.User: - break; - case EventType.Last: - break; - case EventType.EnumPadding: - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } - } - - private static SdlSurface? Instance { get; set; } - - public void Dispose() => Instance = null; -} diff --git a/sources/Windowing/Windowing/Silk.NET.Windowing.csproj b/sources/Windowing/Windowing/Silk.NET.Windowing.csproj index 0d18f31d19..3dae47cc55 100644 --- a/sources/Windowing/Windowing/Silk.NET.Windowing.csproj +++ b/sources/Windowing/Windowing/Silk.NET.Windowing.csproj @@ -15,5 +15,6 @@ + diff --git a/sources/Windowing/Windowing/SurfaceBuilder.cs b/sources/Windowing/Windowing/SurfaceBuilder.cs index 796b55990e..6da05a068f 100644 --- a/sources/Windowing/Windowing/SurfaceBuilder.cs +++ b/sources/Windowing/Windowing/SurfaceBuilder.cs @@ -12,9 +12,10 @@ namespace Silk.NET.Windowing; public static class SurfaceBuilder { /// - /// Creates a using configuration and + /// Creates a using default configuration and /// the . /// /// The surface builder. - public static SurfaceBuilder Create() => new(default); + public static SurfaceBuilder Create() => + new(default); } diff --git a/tests/Core/Core/BreakneckRequestTests.cs b/tests/Core/Core/BreakneckRequestTests.cs new file mode 100644 index 0000000000..540053fa22 --- /dev/null +++ b/tests/Core/Core/BreakneckRequestTests.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Silk.NET.Core.UnitTests; + +public class BreakneckRequestTests +{ + [Test] + public void NoParams() + { + var req = new BreakneckRequest(); + + // Simulate an overflow. + Unsafe.As, int>(ref req) = int.MaxValue - 8; + Unsafe.Add(ref Unsafe.As, int>(ref req), 1) = int.MaxValue - 8; + + var barrier = new Barrier(5); + var rspCnt = 0; + for (var i = 0; i < 4; i++) + { + // var i1 = i; + new Thread(() => + { + barrier.SignalAndWait(); + for (int j = 0; j < 4; j++) + { + // Console.WriteLine($"Requesting for {i1}"); + if (req.Request() == 69) + { + // Console.WriteLine($"Got response for {i1}."); + Interlocked.Increment(ref rspCnt); + } + } + }) + { + IsBackground = true + }.Start(); + } + + barrier.SignalAndWait(); + var misses = 0; + bool missed; + for (var i = 0; i < 16; i += missed ? 0 : 1) + { + if (!req.IsRequested && misses <= int.MaxValue - 1) + { + misses++; + missed = true; + continue; + } + Assert.That(req.IsRequested, Is.True); + misses = 0; + missed = false; + // Console.WriteLine("providing"); + req.Provide(69); + } + + Thread.Sleep(50); + Assert.That(rspCnt, Is.EqualTo(16)); + Assert.That(req.IsRequested, Is.False); + } + + [Test] + public void Params() + { + var req = new BreakneckRequest(); + + // Simulate an overflow. + Unsafe.As, int>(ref req) = int.MaxValue - 8; + Unsafe.Add(ref Unsafe.As, int>(ref req), 1) = int.MaxValue - 8; + + var barrier = new Barrier(5); + var rspCnt = 0; + for (var i = 0; i < 4; i++) + { + var i1 = i; + new Thread(() => + { + barrier.SignalAndWait(); + for (var j = 0; j < 4; j++) + { + // Console.WriteLine($"Requesting for {i1}"); + if (req.Request(i1) == i1) + { + // Console.WriteLine($"Got response for {i1}."); + Interlocked.Increment(ref rspCnt); + } + } + }) + { + IsBackground = true + }.Start(); + } + + barrier.SignalAndWait(); + var misses = 0; + bool missed; + for (var i = 0; i < 16; i += missed ? 0 : 1) + { + var isRequested = req.TryGetRequest(out var echo); + if (!isRequested && misses <= int.MaxValue - 1) + { + misses++; + missed = true; + continue; + } + Assert.That(isRequested, Is.True); + misses = 0; + missed = false; + // Console.WriteLine("providing"); + req.Provide(echo); + } + + Thread.Sleep(50); + Assert.That(rspCnt, Is.EqualTo(16)); + Assert.That(req.TryGetRequest(out _), Is.False); + } +}